637 lines
22 KiB
Plaintext
Executable File
637 lines
22 KiB
Plaintext
Executable File
#!/usr/bin/env nu
|
||
|
||
# Storage Migration Tool for Orchestrator
|
||
#
|
||
# This script provides a user-friendly interface for migrating data between
|
||
# different storage backends using the Rust migration library.
|
||
|
||
use std log
|
||
|
||
# Migration configuration
|
||
const ORCHESTRATOR_BIN = "./target/release/orchestrator"
|
||
const DEFAULT_BATCH_SIZE = 100
|
||
const DEFAULT_MAX_RETRIES = 3
|
||
|
||
# Available storage types
|
||
const STORAGE_TYPES = ["filesystem", "surrealdb-embedded", "surrealdb-server"]
|
||
|
||
# Storage migration parameters
|
||
def main [
|
||
--from (-f): string # Source storage type (filesystem, surrealdb-embedded, surrealdb-server)
|
||
--to (-t): string # Target storage type (filesystem, surrealdb-embedded, surrealdb-server)
|
||
--source-dir: string # Source data directory
|
||
--target-dir: string # Target data directory
|
||
--surrealdb-url: string # SurrealDB server URL (for server mode)
|
||
--username: string # SurrealDB username (for server mode)
|
||
--password: string # SurrealDB password (for server mode)
|
||
--namespace: string = "orchestrator" # SurrealDB namespace
|
||
--database: string = "tasks" # SurrealDB database
|
||
--dry-run (-n) # Perform dry run without actual migration
|
||
--no-backup # Skip backup creation
|
||
--no-verify # Skip data integrity verification
|
||
--batch-size: int = 100 # Batch size for migration operations
|
||
--max-retries: int = 3 # Maximum retry attempts
|
||
--continue-on-error # Continue migration on non-critical errors
|
||
--status-filter: string # Filter tasks by status (comma-separated)
|
||
--created-after: string # Filter tasks created after date (YYYY-MM-DD)
|
||
--created-before: string # Filter tasks created before date (YYYY-MM-DD)
|
||
--backup-path: string # Custom backup file path
|
||
--verbose (-v) # Enable verbose logging
|
||
--interactive (-i) # Run interactive migration wizard
|
||
] {
|
||
# Initialize logging
|
||
if $verbose {
|
||
log info "Starting storage migration tool"
|
||
}
|
||
|
||
# Run interactive wizard if requested
|
||
if $interactive {
|
||
run_interactive_wizard
|
||
return
|
||
}
|
||
|
||
# Validate required parameters
|
||
if $from == null or $to == null {
|
||
print_usage
|
||
return
|
||
}
|
||
|
||
# Validate storage types
|
||
validate_storage_types $from $to
|
||
|
||
# Build migration config
|
||
let config = build_migration_config {
|
||
from: $from
|
||
to: $to
|
||
source_dir: $source_dir
|
||
target_dir: $target_dir
|
||
surrealdb_url: $surrealdb_url
|
||
username: $username
|
||
password: $password
|
||
namespace: $namespace
|
||
database: $database
|
||
dry_run: $dry_run
|
||
no_backup: $no_backup
|
||
no_verify: $no_verify
|
||
batch_size: $batch_size
|
||
max_retries: $max_retries
|
||
continue_on_error: $continue_on_error
|
||
status_filter: $status_filter
|
||
created_after: $created_after
|
||
created_before: $created_before
|
||
backup_path: $backup_path
|
||
verbose: $verbose
|
||
}
|
||
|
||
# Execute migration
|
||
execute_migration $config $verbose
|
||
}
|
||
|
||
# Interactive migration wizard
|
||
def run_interactive_wizard [] {
|
||
print "\n🚀 Storage Migration Wizard"
|
||
print "==========================\n"
|
||
|
||
# Get source storage configuration
|
||
print "📁 Source Storage Configuration"
|
||
let source = get_storage_config "source"
|
||
|
||
print "\n📁 Target Storage Configuration"
|
||
let target = get_storage_config "target"
|
||
|
||
# Get migration options
|
||
print "\n⚙️ Migration Options"
|
||
let dry_run = (["yes", "no"] | input list "Perform dry run? ") == "yes"
|
||
let create_backup = (["yes", "no"] | input list "Create backup? ") == "yes"
|
||
let verify_integrity = (["yes", "no"] | input list "Verify data integrity? ") == "yes"
|
||
|
||
let batch_size = (input "Batch size for migration (default: 100): ") |
|
||
if ($in | is-empty) { 100 } else { $in | into int }
|
||
|
||
# Optional filters
|
||
print "\n🔍 Data Filters (optional)"
|
||
let status_filter = input "Filter by task status (comma-separated, e.g., Pending,Failed): "
|
||
let created_after = input "Filter tasks created after (YYYY-MM-DD): "
|
||
let created_before = input "Filter tasks created before (YYYY-MM-DD): "
|
||
|
||
# Confirmation
|
||
print "\n📋 Migration Summary"
|
||
print $"From: ($source.type) @ ($source.config | get data_dir? | default 'N/A')"
|
||
print $"To: ($target.type) @ ($target.config | get data_dir? | default 'N/A')"
|
||
print $"Dry run: ($dry_run)"
|
||
print $"Create backup: ($create_backup)"
|
||
print $"Verify integrity: ($verify_integrity)"
|
||
print $"Batch size: ($batch_size)"
|
||
|
||
if not ($status_filter | is-empty) {
|
||
print $"Status filter: ($status_filter)"
|
||
}
|
||
if not ($created_after | is-empty) {
|
||
print $"Created after: ($created_after)"
|
||
}
|
||
if not ($created_before | is-empty) {
|
||
print $"Created before: ($created_before)"
|
||
}
|
||
|
||
let confirm = (["yes", "no"] | input list "\n▶️ Proceed with migration? ") == "yes"
|
||
|
||
if not $confirm {
|
||
print "❌ Migration cancelled"
|
||
return
|
||
}
|
||
|
||
# Build full configuration
|
||
let config = {
|
||
from: $source.type
|
||
to: $target.type
|
||
source_config: $source.config
|
||
target_config: $target.config
|
||
dry_run: $dry_run
|
||
create_backup: $create_backup
|
||
verify_integrity: $verify_integrity
|
||
batch_size: $batch_size
|
||
status_filter: $status_filter
|
||
created_after: $created_after
|
||
created_before: $created_before
|
||
}
|
||
|
||
# Execute migration
|
||
execute_migration_interactive $config
|
||
}
|
||
|
||
# Get storage configuration interactively
|
||
def get_storage_config [role: string] -> record {
|
||
let storage_type = $STORAGE_TYPES | input list $"Select ($role) storage type: "
|
||
|
||
match $storage_type {
|
||
"filesystem" => {
|
||
let data_dir = input $"($role | str capitalize) data directory: "
|
||
{
|
||
type: $storage_type,
|
||
config: {
|
||
data_dir: $data_dir
|
||
}
|
||
}
|
||
}
|
||
"surrealdb-embedded" => {
|
||
let data_dir = input $"($role | str capitalize) data directory: "
|
||
{
|
||
type: $storage_type,
|
||
config: {
|
||
data_dir: $data_dir
|
||
}
|
||
}
|
||
}
|
||
"surrealdb-server" => {
|
||
let url = input $"($role | str capitalize) SurrealDB server URL: "
|
||
let username = input $"($role | str capitalize) username: "
|
||
let password = input $"($role | str capitalize) password: " --sensitive
|
||
let namespace = input $"($role | str capitalize) namespace (default: orchestrator): " |
|
||
if ($in | is-empty) { "orchestrator" } else { $in }
|
||
let database = input $"($role | str capitalize) database (default: tasks): " |
|
||
if ($in | is-empty) { "tasks" } else { $in }
|
||
{
|
||
type: $storage_type,
|
||
config: {
|
||
url: $url,
|
||
username: $username,
|
||
password: $password,
|
||
namespace: $namespace,
|
||
database: $database
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# Build migration configuration from parameters
|
||
def build_migration_config [params: record] -> record {
|
||
mut config = {
|
||
source_type: $params.from
|
||
target_type: $params.to
|
||
options: {
|
||
dry_run: ($params.dry_run? | default false)
|
||
create_backup: (not ($params.no_backup? | default false))
|
||
verify_integrity: (not ($params.no_verify? | default false))
|
||
batch_size: ($params.batch_size? | default $DEFAULT_BATCH_SIZE)
|
||
max_retries: ($params.max_retries? | default $DEFAULT_MAX_RETRIES)
|
||
continue_on_error: ($params.continue_on_error? | default false)
|
||
}
|
||
}
|
||
|
||
# Add source configuration
|
||
match $params.from {
|
||
"filesystem" => {
|
||
if ($params.source_dir | is-empty) {
|
||
error make { msg: "Source directory required for filesystem storage" }
|
||
}
|
||
$config = ($config | upsert source_config {
|
||
storage_type: "filesystem"
|
||
data_dir: $params.source_dir
|
||
})
|
||
}
|
||
"surrealdb-embedded" => {
|
||
if ($params.source_dir | is-empty) {
|
||
error make { msg: "Source directory required for SurrealDB embedded storage" }
|
||
}
|
||
$config = ($config | upsert source_config {
|
||
storage_type: "surrealdb-embedded"
|
||
data_dir: $params.source_dir
|
||
surrealdb_namespace: ($params.namespace | default "orchestrator")
|
||
surrealdb_database: ($params.database | default "tasks")
|
||
})
|
||
}
|
||
"surrealdb-server" => {
|
||
if ($params.surrealdb_url | is-empty) or ($params.username | is-empty) or ($params.password | is-empty) {
|
||
error make { msg: "URL, username, and password required for SurrealDB server storage" }
|
||
}
|
||
$config = ($config | upsert source_config {
|
||
storage_type: "surrealdb-server"
|
||
data_dir: ""
|
||
surrealdb_url: $params.surrealdb_url
|
||
surrealdb_username: $params.username
|
||
surrealdb_password: $params.password
|
||
surrealdb_namespace: ($params.namespace | default "orchestrator")
|
||
surrealdb_database: ($params.database | default "tasks")
|
||
})
|
||
}
|
||
}
|
||
|
||
# Add target configuration
|
||
match $params.to {
|
||
"filesystem" => {
|
||
if ($params.target_dir | is-empty) {
|
||
error make { msg: "Target directory required for filesystem storage" }
|
||
}
|
||
$config = ($config | upsert target_config {
|
||
storage_type: "filesystem"
|
||
data_dir: $params.target_dir
|
||
})
|
||
}
|
||
"surrealdb-embedded" => {
|
||
if ($params.target_dir | is-empty) {
|
||
error make { msg: "Target directory required for SurrealDB embedded storage" }
|
||
}
|
||
$config = ($config | upsert target_config {
|
||
storage_type: "surrealdb-embedded"
|
||
data_dir: $params.target_dir
|
||
surrealdb_namespace: ($params.namespace | default "orchestrator")
|
||
surrealdb_database: ($params.database | default "tasks")
|
||
})
|
||
}
|
||
"surrealdb-server" => {
|
||
if ($params.surrealdb_url | is-empty) or ($params.username | is-empty) or ($params.password | is-empty) {
|
||
error make { msg: "URL, username, and password required for SurrealDB server storage" }
|
||
}
|
||
$config = ($config | upsert target_config {
|
||
storage_type: "surrealdb-server"
|
||
data_dir: ""
|
||
surrealdb_url: $params.surrealdb_url
|
||
surrealdb_username: $params.username
|
||
surrealdb_password: $params.password
|
||
surrealdb_namespace: ($params.namespace | default "orchestrator")
|
||
surrealdb_database: ($params.database | default "tasks")
|
||
})
|
||
}
|
||
}
|
||
|
||
# Add optional filters
|
||
if not ($params.status_filter | is-empty) {
|
||
$config.options = ($config.options | upsert status_filter ($params.status_filter | split row ","))
|
||
}
|
||
|
||
if not ($params.created_after | is-empty) {
|
||
$config.options = ($config.options | upsert created_after $params.created_after)
|
||
}
|
||
|
||
if not ($params.created_before | is-empty) {
|
||
$config.options = ($config.options | upsert created_before $params.created_before)
|
||
}
|
||
|
||
if not ($params.backup_path | is-empty) {
|
||
$config.options = ($config.options | upsert backup_path $params.backup_path)
|
||
}
|
||
|
||
$config
|
||
}
|
||
|
||
# Execute migration using the Rust binary
|
||
def execute_migration [config: record, verbose: bool = false] {
|
||
print "\n🚀 Starting Storage Migration"
|
||
print "=============================="
|
||
|
||
if $verbose {
|
||
log info $"Migration configuration: ($config)"
|
||
}
|
||
|
||
# Write temporary config file
|
||
let config_file = $"/tmp/migration_config_(random uuid).json"
|
||
$config | to json | save $config_file
|
||
|
||
# Build orchestrator command
|
||
mut cmd_args = [
|
||
"migrate"
|
||
"--config-file" $config_file
|
||
]
|
||
|
||
if $verbose {
|
||
$cmd_args = ($cmd_args | append "--verbose")
|
||
}
|
||
|
||
# Execute migration
|
||
try {
|
||
let result = run-external $ORCHESTRATOR_BIN ...$cmd_args
|
||
|
||
if $verbose {
|
||
log info $"Migration completed: ($result)"
|
||
}
|
||
|
||
print "✅ Migration completed successfully!"
|
||
|
||
# Parse and display results if available
|
||
if ($result | str contains "Migration Report") {
|
||
print "\n📊 Migration Report"
|
||
print "==================="
|
||
print $result
|
||
}
|
||
} catch {
|
||
print "❌ Migration failed!"
|
||
|
||
# Try to get error details from the binary
|
||
let error_result = run-external $ORCHESTRATOR_BIN ...$cmd_args --dry-run
|
||
print $"Error details: ($error_result)"
|
||
} finally {
|
||
# Clean up temporary config file
|
||
rm -f $config_file
|
||
}
|
||
}
|
||
|
||
# Execute interactive migration
|
||
def execute_migration_interactive [config: record] {
|
||
print "\n🚀 Executing Migration..."
|
||
print "========================\n"
|
||
|
||
# Create a simplified config for the binary
|
||
let binary_config = {
|
||
source_type: $config.from
|
||
target_type: $config.to
|
||
source_config: $config.source_config
|
||
target_config: $config.target_config
|
||
options: {
|
||
dry_run: $config.dry_run
|
||
create_backup: $config.create_backup
|
||
verify_integrity: $config.verify_integrity
|
||
batch_size: $config.batch_size
|
||
}
|
||
}
|
||
|
||
# Add filters if present
|
||
if ($config.status_filter? | is-not-empty) {
|
||
$binary_config.options = ($binary_config.options | upsert status_filter ($config.status_filter | split row ","))
|
||
}
|
||
|
||
# Write config and execute
|
||
let config_file = $"/tmp/migration_config_(random uuid).json"
|
||
$binary_config | to json | save $config_file
|
||
|
||
try {
|
||
# Real-time progress monitoring
|
||
print "📊 Migration Progress:"
|
||
print "=====================\n"
|
||
|
||
let result = run-external $ORCHESTRATOR_BIN "migrate" "--config-file" $config_file "--progress"
|
||
|
||
print "\n✅ Migration completed successfully!"
|
||
print $result
|
||
|
||
} catch {
|
||
print "\n❌ Migration failed!"
|
||
print "Check the logs for more details."
|
||
} finally {
|
||
rm -f $config_file
|
||
}
|
||
}
|
||
|
||
# Validate storage types
|
||
def validate_storage_types [from: string, to: string] {
|
||
if $from not-in $STORAGE_TYPES {
|
||
error make {
|
||
msg: $"Invalid source storage type: ($from). Available types: ($STORAGE_TYPES | str join ', ')"
|
||
}
|
||
}
|
||
|
||
if $to not-in $STORAGE_TYPES {
|
||
error make {
|
||
msg: $"Invalid target storage type: ($to). Available types: ($STORAGE_TYPES | str join ', ')"
|
||
}
|
||
}
|
||
|
||
if $from == $to {
|
||
print "⚠️ Warning: Source and target storage types are the same"
|
||
}
|
||
}
|
||
|
||
# Print usage information
|
||
def print_usage [] {
|
||
print "\n📖 Storage Migration Tool Usage"
|
||
print "==============================="
|
||
print ""
|
||
print "Interactive mode:"
|
||
print " ./migrate-storage.nu --interactive"
|
||
print ""
|
||
print "Direct migration:"
|
||
print " ./migrate-storage.nu --from <source> --to <target> [OPTIONS]"
|
||
print ""
|
||
print "Examples:"
|
||
print " # Filesystem to SurrealDB embedded"
|
||
print " ./migrate-storage.nu --from filesystem --to surrealdb-embedded \\"
|
||
print " --source-dir ./data --target-dir ./surrealdb-data"
|
||
print ""
|
||
print " # SurrealDB embedded to server"
|
||
print " ./migrate-storage.nu --from surrealdb-embedded --to surrealdb-server \\"
|
||
print " --source-dir ./surrealdb-data --surrealdb-url ws://localhost:8000 \\"
|
||
print " --username admin --password secret"
|
||
print ""
|
||
print " # Dry run migration"
|
||
print " ./migrate-storage.nu --from filesystem --to surrealdb-embedded \\"
|
||
print " --source-dir ./data --target-dir ./new-data --dry-run"
|
||
print ""
|
||
print "Storage Types:"
|
||
print " - filesystem: File-based JSON storage"
|
||
print " - surrealdb-embedded: Embedded SurrealDB with RocksDB"
|
||
print " - surrealdb-server: Remote SurrealDB server"
|
||
print ""
|
||
print "Options:"
|
||
print " --dry-run Perform dry run without actual migration"
|
||
print " --no-backup Skip backup creation"
|
||
print " --no-verify Skip data integrity verification"
|
||
print " --batch-size <n> Batch size for migration operations (default: 100)"
|
||
print " --max-retries <n> Maximum retry attempts (default: 3)"
|
||
print " --continue-on-error Continue migration on non-critical errors"
|
||
print " --status-filter <s> Filter tasks by status (comma-separated)"
|
||
print " --created-after <d> Filter tasks created after date (YYYY-MM-DD)"
|
||
print " --created-before <d> Filter tasks created before date (YYYY-MM-DD)"
|
||
print " --backup-path <p> Custom backup file path"
|
||
print " --verbose Enable verbose logging"
|
||
print ""
|
||
}
|
||
|
||
# Show available commands
|
||
def "migrate help" [] {
|
||
print_usage
|
||
}
|
||
|
||
# List available storage types
|
||
def "migrate list-types" [] {
|
||
print "\n🔧 Available Storage Types"
|
||
print "=========================="
|
||
|
||
for type in $STORAGE_TYPES {
|
||
match $type {
|
||
"filesystem" => {
|
||
print $"📁 ($type)"
|
||
print " Description: File-based JSON storage"
|
||
print " Required: --source-dir / --target-dir"
|
||
print " Features: Simple, portable, human-readable"
|
||
print ""
|
||
}
|
||
"surrealdb-embedded" => {
|
||
print $"🗃️ ($type)"
|
||
print " Description: Embedded SurrealDB with RocksDB engine"
|
||
print " Required: --source-dir / --target-dir"
|
||
print " Features: High performance, ACID compliance, embedded"
|
||
print ""
|
||
}
|
||
"surrealdb-server" => {
|
||
print $"🌐 ($type)"
|
||
print " Description: Remote SurrealDB server via WebSocket"
|
||
print " Required: --surrealdb-url, --username, --password"
|
||
print " Features: Scalable, distributed, real-time"
|
||
print ""
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# Validate migration prerequisites
|
||
def "migrate validate" [
|
||
--from: string # Source storage type
|
||
--to: string # Target storage type
|
||
--source-dir: string
|
||
--target-dir: string
|
||
--surrealdb-url: string
|
||
--username: string
|
||
--password: string
|
||
] {
|
||
print "\n🔍 Validating Migration Prerequisites"
|
||
print "===================================="
|
||
|
||
mut errors = []
|
||
mut warnings = []
|
||
|
||
# Validate storage types
|
||
if $from not-in $STORAGE_TYPES {
|
||
$errors = ($errors | append $"Invalid source type: ($from)")
|
||
}
|
||
|
||
if $to not-in $STORAGE_TYPES {
|
||
$errors = ($errors | append $"Invalid target type: ($to)")
|
||
}
|
||
|
||
# Validate source configuration
|
||
match $from {
|
||
"filesystem" => {
|
||
if ($source_dir | is-empty) {
|
||
$errors = ($errors | append "Source directory required for filesystem")
|
||
} else if not ($source_dir | path exists) {
|
||
$warnings = ($warnings | append $"Source directory does not exist: ($source_dir)")
|
||
}
|
||
}
|
||
"surrealdb-embedded" => {
|
||
if ($source_dir | is-empty) {
|
||
$errors = ($errors | append "Source directory required for SurrealDB embedded")
|
||
}
|
||
}
|
||
"surrealdb-server" => {
|
||
if ($surrealdb_url | is-empty) {
|
||
$errors = ($errors | append "SurrealDB URL required for server mode")
|
||
}
|
||
if ($username | is-empty) {
|
||
$errors = ($errors | append "Username required for SurrealDB server")
|
||
}
|
||
if ($password | is-empty) {
|
||
$errors = ($errors | append "Password required for SurrealDB server")
|
||
}
|
||
}
|
||
}
|
||
|
||
# Validate target configuration
|
||
match $to {
|
||
"filesystem" => {
|
||
if ($target_dir | is-empty) {
|
||
$errors = ($errors | append "Target directory required for filesystem")
|
||
} else {
|
||
let parent_dir = ($target_dir | path dirname)
|
||
if not ($parent_dir | path exists) {
|
||
$warnings = ($warnings | append $"Target parent directory does not exist: ($parent_dir)")
|
||
}
|
||
}
|
||
}
|
||
"surrealdb-embedded" => {
|
||
if ($target_dir | is-empty) {
|
||
$errors = ($errors | append "Target directory required for SurrealDB embedded")
|
||
}
|
||
}
|
||
"surrealdb-server" => {
|
||
if ($surrealdb_url | is-empty) {
|
||
$errors = ($errors | append "SurrealDB URL required for server mode")
|
||
}
|
||
if ($username | is-empty) {
|
||
$errors = ($errors | append "Username required for SurrealDB server")
|
||
}
|
||
if ($password | is-empty) {
|
||
$errors = ($errors | append "Password required for SurrealDB server")
|
||
}
|
||
}
|
||
}
|
||
|
||
# Check if orchestrator binary exists
|
||
if not ($ORCHESTRATOR_BIN | path exists) {
|
||
$warnings = ($warnings | append $"Orchestrator binary not found at ($ORCHESTRATOR_BIN). Build with: cargo build --release")
|
||
}
|
||
|
||
# Report results
|
||
if ($errors | length) > 0 {
|
||
print "❌ Validation Errors:"
|
||
for error in $errors {
|
||
print $" • ($error)"
|
||
}
|
||
return
|
||
}
|
||
|
||
if ($warnings | length) > 0 {
|
||
print "⚠️ Warnings:"
|
||
for warning in $warnings {
|
||
print $" • ($warning)"
|
||
}
|
||
print ""
|
||
}
|
||
|
||
print "✅ Validation passed!"
|
||
|
||
if $from == $to {
|
||
print "ℹ️ Note: Source and target types are the same - ensure directories/connections differ"
|
||
}
|
||
}
|
||
|
||
# Check migration status (if available)
|
||
def "migrate status" [] {
|
||
print "🔍 Checking Migration Status..."
|
||
|
||
try {
|
||
let status = run-external $ORCHESTRATOR_BIN "migrate" "--status"
|
||
print $status
|
||
} catch {
|
||
print "No active migrations found or binary not available"
|
||
}
|
||
} |