- Add 'migrate cluster' command for full cluster migration - Add 'migrate single' command for single database migration - Support PostgreSQL and MySQL database migration - Staged migration: backup from source → restore to target - Pre-flight checks validate connectivity before execution - Dry-run mode by default (--confirm to execute) - Support for --clean, --keep-backup, --exclude options - Parallel backup/restore with configurable jobs - Automatic cleanup of temporary backup files
451 lines
14 KiB
Go
451 lines
14 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"dbbackup/internal/config"
|
|
"dbbackup/internal/migrate"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
// Source connection flags
|
|
migrateSourceHost string
|
|
migrateSourcePort int
|
|
migrateSourceUser string
|
|
migrateSourcePassword string
|
|
migrateSourceSSLMode string
|
|
|
|
// Target connection flags
|
|
migrateTargetHost string
|
|
migrateTargetPort int
|
|
migrateTargetUser string
|
|
migrateTargetPassword string
|
|
migrateTargetDatabase string
|
|
migrateTargetSSLMode string
|
|
|
|
// Migration options
|
|
migrateWorkdir string
|
|
migrateClean bool
|
|
migrateConfirm bool
|
|
migrateDryRun bool
|
|
migrateKeepBackup bool
|
|
migrateJobs int
|
|
migrateVerbose bool
|
|
migrateExclude []string
|
|
)
|
|
|
|
// migrateCmd represents the migrate command
|
|
var migrateCmd = &cobra.Command{
|
|
Use: "migrate",
|
|
Short: "Migrate databases between servers",
|
|
Long: `Migrate databases from one server to another.
|
|
|
|
This command performs a staged migration:
|
|
1. Creates a backup from the source server
|
|
2. Stores backup in a working directory
|
|
3. Restores the backup to the target server
|
|
4. Cleans up temporary files (unless --keep-backup)
|
|
|
|
Supports PostgreSQL and MySQL cluster migration or single database migration.
|
|
|
|
Examples:
|
|
# Migrate entire PostgreSQL cluster
|
|
dbbackup migrate cluster \
|
|
--source-host old-server --source-port 5432 --source-user postgres \
|
|
--target-host new-server --target-port 5432 --target-user postgres \
|
|
--confirm
|
|
|
|
# Migrate single database
|
|
dbbackup migrate single mydb \
|
|
--source-host old-server --source-user postgres \
|
|
--target-host new-server --target-user postgres \
|
|
--confirm
|
|
|
|
# Dry-run to preview migration
|
|
dbbackup migrate cluster \
|
|
--source-host old-server \
|
|
--target-host new-server \
|
|
--dry-run
|
|
`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cmd.Help()
|
|
},
|
|
}
|
|
|
|
// migrateClusterCmd migrates an entire database cluster
|
|
var migrateClusterCmd = &cobra.Command{
|
|
Use: "cluster",
|
|
Short: "Migrate entire database cluster to target server",
|
|
Long: `Migrate all databases from source cluster to target server.
|
|
|
|
This command:
|
|
1. Connects to source server and lists all databases
|
|
2. Creates individual backups of each database
|
|
3. Restores each database to target server
|
|
4. Optionally cleans up backup files after successful migration
|
|
|
|
Requirements:
|
|
- Database client tools (pg_dump/pg_restore or mysqldump/mysql)
|
|
- Network access to both source and target servers
|
|
- Sufficient disk space in working directory for backups
|
|
|
|
Safety features:
|
|
- Dry-run mode by default (use --confirm to execute)
|
|
- Pre-flight checks on both servers
|
|
- Optional backup retention after migration
|
|
|
|
Examples:
|
|
# Preview migration
|
|
dbbackup migrate cluster \
|
|
--source-host old-server \
|
|
--target-host new-server
|
|
|
|
# Execute migration with cleanup of existing databases
|
|
dbbackup migrate cluster \
|
|
--source-host old-server --source-user postgres \
|
|
--target-host new-server --target-user postgres \
|
|
--clean --confirm
|
|
|
|
# Exclude specific databases
|
|
dbbackup migrate cluster \
|
|
--source-host old-server \
|
|
--target-host new-server \
|
|
--exclude template0,template1 \
|
|
--confirm
|
|
`,
|
|
RunE: runMigrateCluster,
|
|
}
|
|
|
|
// migrateSingleCmd migrates a single database
|
|
var migrateSingleCmd = &cobra.Command{
|
|
Use: "single [database-name]",
|
|
Short: "Migrate single database to target server",
|
|
Long: `Migrate a single database from source server to target server.
|
|
|
|
Examples:
|
|
# Migrate database to same name on target
|
|
dbbackup migrate single myapp_db \
|
|
--source-host old-server \
|
|
--target-host new-server \
|
|
--confirm
|
|
|
|
# Migrate to different database name
|
|
dbbackup migrate single myapp_db \
|
|
--source-host old-server \
|
|
--target-host new-server \
|
|
--target-database myapp_db_new \
|
|
--confirm
|
|
`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runMigrateSingle,
|
|
}
|
|
|
|
func init() {
|
|
// Add migrate command to root
|
|
rootCmd.AddCommand(migrateCmd)
|
|
|
|
// Add subcommands
|
|
migrateCmd.AddCommand(migrateClusterCmd)
|
|
migrateCmd.AddCommand(migrateSingleCmd)
|
|
|
|
// Source connection flags
|
|
migrateCmd.PersistentFlags().StringVar(&migrateSourceHost, "source-host", "localhost", "Source database host")
|
|
migrateCmd.PersistentFlags().IntVar(&migrateSourcePort, "source-port", 5432, "Source database port")
|
|
migrateCmd.PersistentFlags().StringVar(&migrateSourceUser, "source-user", "", "Source database user")
|
|
migrateCmd.PersistentFlags().StringVar(&migrateSourcePassword, "source-password", "", "Source database password")
|
|
migrateCmd.PersistentFlags().StringVar(&migrateSourceSSLMode, "source-ssl-mode", "prefer", "Source SSL mode (disable, prefer, require)")
|
|
|
|
// Target connection flags
|
|
migrateCmd.PersistentFlags().StringVar(&migrateTargetHost, "target-host", "", "Target database host (required)")
|
|
migrateCmd.PersistentFlags().IntVar(&migrateTargetPort, "target-port", 5432, "Target database port")
|
|
migrateCmd.PersistentFlags().StringVar(&migrateTargetUser, "target-user", "", "Target database user (default: same as source)")
|
|
migrateCmd.PersistentFlags().StringVar(&migrateTargetPassword, "target-password", "", "Target database password")
|
|
migrateCmd.PersistentFlags().StringVar(&migrateTargetSSLMode, "target-ssl-mode", "prefer", "Target SSL mode (disable, prefer, require)")
|
|
|
|
// Single database specific flags
|
|
migrateSingleCmd.Flags().StringVar(&migrateTargetDatabase, "target-database", "", "Target database name (default: same as source)")
|
|
|
|
// Cluster specific flags
|
|
migrateClusterCmd.Flags().StringSliceVar(&migrateExclude, "exclude", []string{}, "Databases to exclude from migration")
|
|
|
|
// Migration options
|
|
migrateCmd.PersistentFlags().StringVar(&migrateWorkdir, "workdir", "", "Working directory for backup files (default: system temp)")
|
|
migrateCmd.PersistentFlags().BoolVar(&migrateClean, "clean", false, "Drop existing databases on target before restore")
|
|
migrateCmd.PersistentFlags().BoolVar(&migrateConfirm, "confirm", false, "Confirm and execute migration (default: dry-run)")
|
|
migrateCmd.PersistentFlags().BoolVar(&migrateDryRun, "dry-run", false, "Preview migration without executing")
|
|
migrateCmd.PersistentFlags().BoolVar(&migrateKeepBackup, "keep-backup", false, "Keep backup files after successful migration")
|
|
migrateCmd.PersistentFlags().IntVar(&migrateJobs, "jobs", 4, "Parallel jobs for backup/restore")
|
|
migrateCmd.PersistentFlags().BoolVar(&migrateVerbose, "verbose", false, "Verbose output")
|
|
|
|
// Mark required flags
|
|
migrateCmd.MarkPersistentFlagRequired("target-host")
|
|
}
|
|
|
|
func runMigrateCluster(cmd *cobra.Command, args []string) error {
|
|
// Validate target host
|
|
if migrateTargetHost == "" {
|
|
return fmt.Errorf("--target-host is required")
|
|
}
|
|
|
|
// Set defaults
|
|
if migrateSourceUser == "" {
|
|
migrateSourceUser = os.Getenv("USER")
|
|
}
|
|
if migrateTargetUser == "" {
|
|
migrateTargetUser = migrateSourceUser
|
|
}
|
|
|
|
workdir := migrateWorkdir
|
|
if workdir == "" {
|
|
workdir = filepath.Join(os.TempDir(), "dbbackup-migrate")
|
|
}
|
|
|
|
// Create working directory
|
|
if err := os.MkdirAll(workdir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create working directory: %w", err)
|
|
}
|
|
|
|
// Create source config
|
|
sourceCfg := config.New()
|
|
sourceCfg.Host = migrateSourceHost
|
|
sourceCfg.Port = migrateSourcePort
|
|
sourceCfg.User = migrateSourceUser
|
|
sourceCfg.Password = migrateSourcePassword
|
|
sourceCfg.SSLMode = migrateSourceSSLMode
|
|
sourceCfg.Database = "postgres" // Default connection database
|
|
sourceCfg.DatabaseType = cfg.DatabaseType
|
|
sourceCfg.BackupDir = workdir
|
|
sourceCfg.DumpJobs = migrateJobs
|
|
|
|
// Create target config
|
|
targetCfg := config.New()
|
|
targetCfg.Host = migrateTargetHost
|
|
targetCfg.Port = migrateTargetPort
|
|
targetCfg.User = migrateTargetUser
|
|
targetCfg.Password = migrateTargetPassword
|
|
targetCfg.SSLMode = migrateTargetSSLMode
|
|
targetCfg.Database = "postgres"
|
|
targetCfg.DatabaseType = cfg.DatabaseType
|
|
targetCfg.BackupDir = workdir
|
|
|
|
// Create migration engine
|
|
engine, err := migrate.NewEngine(sourceCfg, targetCfg, log)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create migration engine: %w", err)
|
|
}
|
|
defer engine.Close()
|
|
|
|
// Configure engine
|
|
engine.SetWorkDir(workdir)
|
|
engine.SetKeepBackup(migrateKeepBackup)
|
|
engine.SetJobs(migrateJobs)
|
|
engine.SetDryRun(migrateDryRun || !migrateConfirm)
|
|
engine.SetVerbose(migrateVerbose)
|
|
engine.SetCleanTarget(migrateClean)
|
|
|
|
// Setup context with cancellation
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Handle interrupt signals
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigChan
|
|
log.Warn("Received interrupt signal, cancelling migration...")
|
|
cancel()
|
|
}()
|
|
|
|
// Connect to databases
|
|
if err := engine.Connect(ctx); err != nil {
|
|
return fmt.Errorf("failed to connect: %w", err)
|
|
}
|
|
|
|
// Print migration plan
|
|
fmt.Println()
|
|
fmt.Println("=== Cluster Migration Plan ===")
|
|
fmt.Println()
|
|
fmt.Printf("Source: %s@%s:%d\n", migrateSourceUser, migrateSourceHost, migrateSourcePort)
|
|
fmt.Printf("Target: %s@%s:%d\n", migrateTargetUser, migrateTargetHost, migrateTargetPort)
|
|
fmt.Printf("Database Type: %s\n", cfg.DatabaseType)
|
|
fmt.Printf("Working Directory: %s\n", workdir)
|
|
fmt.Printf("Clean Target: %v\n", migrateClean)
|
|
fmt.Printf("Keep Backup: %v\n", migrateKeepBackup)
|
|
fmt.Printf("Parallel Jobs: %d\n", migrateJobs)
|
|
if len(migrateExclude) > 0 {
|
|
fmt.Printf("Excluded: %v\n", migrateExclude)
|
|
}
|
|
fmt.Println()
|
|
|
|
isDryRun := migrateDryRun || !migrateConfirm
|
|
if isDryRun {
|
|
fmt.Println("Mode: DRY-RUN (use --confirm to execute)")
|
|
fmt.Println()
|
|
return engine.PreflightCheck(ctx)
|
|
}
|
|
|
|
fmt.Println("Mode: EXECUTE")
|
|
fmt.Println()
|
|
|
|
// Execute migration
|
|
startTime := time.Now()
|
|
result, err := engine.MigrateCluster(ctx, migrateExclude)
|
|
duration := time.Since(startTime)
|
|
|
|
if err != nil {
|
|
log.Error("Migration failed", "error", err, "duration", duration)
|
|
return fmt.Errorf("migration failed: %w", err)
|
|
}
|
|
|
|
// Print results
|
|
fmt.Println()
|
|
fmt.Println("=== Migration Complete ===")
|
|
fmt.Println()
|
|
fmt.Printf("Duration: %s\n", duration.Round(time.Second))
|
|
fmt.Printf("Databases Migrated: %d\n", result.DatabaseCount)
|
|
if result.BackupPath != "" && migrateKeepBackup {
|
|
fmt.Printf("Backup Location: %s\n", result.BackupPath)
|
|
}
|
|
fmt.Println()
|
|
|
|
return nil
|
|
}
|
|
|
|
func runMigrateSingle(cmd *cobra.Command, args []string) error {
|
|
dbName := args[0]
|
|
|
|
// Validate target host
|
|
if migrateTargetHost == "" {
|
|
return fmt.Errorf("--target-host is required")
|
|
}
|
|
|
|
// Set defaults
|
|
if migrateSourceUser == "" {
|
|
migrateSourceUser = os.Getenv("USER")
|
|
}
|
|
if migrateTargetUser == "" {
|
|
migrateTargetUser = migrateSourceUser
|
|
}
|
|
|
|
targetDB := migrateTargetDatabase
|
|
if targetDB == "" {
|
|
targetDB = dbName
|
|
}
|
|
|
|
workdir := migrateWorkdir
|
|
if workdir == "" {
|
|
workdir = filepath.Join(os.TempDir(), "dbbackup-migrate")
|
|
}
|
|
|
|
// Create working directory
|
|
if err := os.MkdirAll(workdir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create working directory: %w", err)
|
|
}
|
|
|
|
// Create source config
|
|
sourceCfg := config.New()
|
|
sourceCfg.Host = migrateSourceHost
|
|
sourceCfg.Port = migrateSourcePort
|
|
sourceCfg.User = migrateSourceUser
|
|
sourceCfg.Password = migrateSourcePassword
|
|
sourceCfg.SSLMode = migrateSourceSSLMode
|
|
sourceCfg.Database = dbName
|
|
sourceCfg.DatabaseType = cfg.DatabaseType
|
|
sourceCfg.BackupDir = workdir
|
|
sourceCfg.DumpJobs = migrateJobs
|
|
|
|
// Create target config
|
|
targetCfg := config.New()
|
|
targetCfg.Host = migrateTargetHost
|
|
targetCfg.Port = migrateTargetPort
|
|
targetCfg.User = migrateTargetUser
|
|
targetCfg.Password = migrateTargetPassword
|
|
targetCfg.SSLMode = migrateTargetSSLMode
|
|
targetCfg.Database = targetDB
|
|
targetCfg.DatabaseType = cfg.DatabaseType
|
|
targetCfg.BackupDir = workdir
|
|
|
|
// Create migration engine
|
|
engine, err := migrate.NewEngine(sourceCfg, targetCfg, log)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create migration engine: %w", err)
|
|
}
|
|
defer engine.Close()
|
|
|
|
// Configure engine
|
|
engine.SetWorkDir(workdir)
|
|
engine.SetKeepBackup(migrateKeepBackup)
|
|
engine.SetJobs(migrateJobs)
|
|
engine.SetDryRun(migrateDryRun || !migrateConfirm)
|
|
engine.SetVerbose(migrateVerbose)
|
|
engine.SetCleanTarget(migrateClean)
|
|
|
|
// Setup context with cancellation
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Handle interrupt signals
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigChan
|
|
log.Warn("Received interrupt signal, cancelling migration...")
|
|
cancel()
|
|
}()
|
|
|
|
// Connect to databases
|
|
if err := engine.Connect(ctx); err != nil {
|
|
return fmt.Errorf("failed to connect: %w", err)
|
|
}
|
|
|
|
// Print migration plan
|
|
fmt.Println()
|
|
fmt.Println("=== Single Database Migration Plan ===")
|
|
fmt.Println()
|
|
fmt.Printf("Source: %s@%s:%d/%s\n", migrateSourceUser, migrateSourceHost, migrateSourcePort, dbName)
|
|
fmt.Printf("Target: %s@%s:%d/%s\n", migrateTargetUser, migrateTargetHost, migrateTargetPort, targetDB)
|
|
fmt.Printf("Database Type: %s\n", cfg.DatabaseType)
|
|
fmt.Printf("Working Directory: %s\n", workdir)
|
|
fmt.Printf("Clean Target: %v\n", migrateClean)
|
|
fmt.Printf("Keep Backup: %v\n", migrateKeepBackup)
|
|
fmt.Println()
|
|
|
|
isDryRun := migrateDryRun || !migrateConfirm
|
|
if isDryRun {
|
|
fmt.Println("Mode: DRY-RUN (use --confirm to execute)")
|
|
fmt.Println()
|
|
return engine.PreflightCheck(ctx)
|
|
}
|
|
|
|
fmt.Println("Mode: EXECUTE")
|
|
fmt.Println()
|
|
|
|
// Execute migration
|
|
startTime := time.Now()
|
|
err = engine.MigrateSingle(ctx, dbName, targetDB)
|
|
duration := time.Since(startTime)
|
|
|
|
if err != nil {
|
|
log.Error("Migration failed", "error", err, "duration", duration)
|
|
return fmt.Errorf("migration failed: %w", err)
|
|
}
|
|
|
|
// Print results
|
|
fmt.Println()
|
|
fmt.Println("=== Migration Complete ===")
|
|
fmt.Println()
|
|
fmt.Printf("Duration: %s\n", duration.Round(time.Second))
|
|
fmt.Printf("Database: %s -> %s\n", dbName, targetDB)
|
|
fmt.Println()
|
|
|
|
return nil
|
|
}
|