2025-10-07 10:59:52 +01:00

637 lines
22 KiB
Plaintext
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
}
}