Files
dbbackup/cmd/migrate.go
Alexander Renz 22915102d4
All checks were successful
CI/CD / Test (push) Successful in 1m17s
CI/CD / Lint (push) Successful in 1m24s
CI/CD / Build & Release (push) Has been skipped
CRITICAL FIX: Eliminate all hardcoded /tmp paths - respect WorkDir configuration
This is a critical bugfix release addressing multiple hardcoded temporary directory paths
that prevented proper use of the WorkDir configuration option.

PROBLEM:
Users configuring WorkDir (e.g., /u01/dba/tmp) for systems with small root filesystems
still experienced failures because critical operations hardcoded /tmp instead of respecting
the configured WorkDir. This made the WorkDir option essentially non-functional.

FIXED LOCATIONS:
1. internal/restore/engine.go:632 - CRITICAL: Used BackupDir instead of WorkDir for extraction
2. cmd/restore.go:354,834 - CLI restore/diagnose commands ignored WorkDir
3. cmd/migrate.go:208,347 - Migration commands hardcoded /tmp
4. internal/migrate/engine.go:120 - Migration engine ignored WorkDir
5. internal/config/config.go:224 - SwapFilePath hardcoded /tmp
6. internal/config/config.go:519 - Backup directory fallback hardcoded /tmp
7. internal/tui/restore_exec.go:161 - Debug logs hardcoded /tmp
8. internal/tui/settings.go:805 - Directory browser default hardcoded /tmp
9. internal/tui/restore_preview.go:474 - Display message hardcoded /tmp

NEW FEATURES:
- Added Config.GetEffectiveWorkDir() helper method
- WorkDir now respects WORK_DIR environment variable
- All temp operations now consistently use configured WorkDir with /tmp fallback

IMPACT:
- Restores on systems with small root disks now work properly with WorkDir configured
- Admins can control disk space usage for all temporary operations
- Debug logs, extraction dirs, swap files all respect WorkDir setting

Version: 3.42.1 (Critical Fix Release)
2026-01-07 20:41:53 +01:00

455 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
}
// Create source config first to get WorkDir
sourceCfg := config.New()
sourceCfg.Host = migrateSourceHost
sourceCfg.Port = migrateSourcePort
sourceCfg.User = migrateSourceUser
sourceCfg.Password = migrateSourcePassword
workdir := migrateWorkdir
if workdir == "" {
// Use WorkDir from config if available
workdir = filepath.Join(sourceCfg.GetEffectiveWorkDir(), "dbbackup-migrate")
}
// Create working directory
if err := os.MkdirAll(workdir, 0755); err != nil {
return fmt.Errorf("failed to create working directory: %w", err)
}
// Update source config with remaining settings
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 == "" {
tempCfg := config.New()
workdir = filepath.Join(tempCfg.GetEffectiveWorkDir(), "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
}