Files
recover-server/internal/restore/pipeline.go
Olaf Berberich 29a2886402 Initial commit: Disaster recovery CLI tool
A Go-based CLI tool for recovering servers from backups to new cloud VMs.

Features:
- Multi-cloud support: Exoscale, Cloudscale, Hetzner Cloud
- Backup sources: Local filesystem, Hetzner Storage Box
- 6-stage restore pipeline with /etc whitelist protection
- DNS migration with safety checks and auto-rollback
- Dry-run by default, requires --yes to execute
- Cloud-init for SSH key injection

Packages:
- cmd/recover-server: CLI commands (recover, migrate-dns, list, cleanup)
- internal/providers: Cloud provider implementations
- internal/backup: Backup source implementations
- internal/restore: 6-stage restore pipeline
- internal/dns: Exoscale DNS management
- internal/ui: Prompts, progress, dry-run display
- internal/config: Environment and host configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 00:31:27 +00:00

131 lines
2.7 KiB
Go

package restore
import (
"context"
"fmt"
"time"
"recover-server/internal/backup"
"recover-server/internal/providers"
)
// Stage represents a restore stage
type Stage int
const (
StageSync Stage = iota + 1
StageEtc
StageSelectiveEtc
StageSSHKeys
StageServices
StageHealth
)
func (s Stage) String() string {
names := map[Stage]string{
StageSync: "Sync /root and /opt",
StageEtc: "Stage /etc backup",
StageSelectiveEtc: "Selective /etc restore",
StageSSHKeys: "Merge SSH keys",
StageServices: "Start services",
StageHealth: "Health verification",
}
return names[s]
}
// StageResult contains the result of a stage execution
type StageResult struct {
Stage Stage
Success bool
Message string
Duration time.Duration
Error error
}
// Pipeline orchestrates the restore process
type Pipeline struct {
VM *providers.VM
BackupSource backup.BackupSource
HostName string
SSHKeyPath string // Path to ephemeral private key
SSHUser string // Usually "root"
DryRun bool
Verbose bool
results []StageResult
}
// NewPipeline creates a new restore pipeline
func NewPipeline(vm *providers.VM, source backup.BackupSource, host, sshKeyPath string) *Pipeline {
return &Pipeline{
VM: vm,
BackupSource: source,
HostName: host,
SSHKeyPath: sshKeyPath,
SSHUser: "root",
results: make([]StageResult, 0),
}
}
// Run executes all stages
func (p *Pipeline) Run(ctx context.Context) error {
stages := []struct {
stage Stage
fn func(context.Context) error
}{
{StageSync, p.runSync},
{StageEtc, p.runEtcStaging},
{StageSelectiveEtc, p.runSelectiveEtc},
{StageSSHKeys, p.runSSHKeyMerge},
{StageServices, p.runServices},
{StageHealth, p.runHealth},
}
for _, s := range stages {
start := time.Now()
if p.Verbose {
fmt.Printf("\n=== Stage %d: %s ===\n", s.stage, s.stage)
}
if p.DryRun {
p.results = append(p.results, StageResult{
Stage: s.stage,
Success: true,
Message: "[DRY RUN] Would execute: " + s.stage.String(),
Duration: 0,
})
continue
}
err := s.fn(ctx)
result := StageResult{
Stage: s.stage,
Success: err == nil,
Duration: time.Since(start),
Error: err,
}
if err != nil {
result.Message = err.Error()
p.results = append(p.results, result)
return fmt.Errorf("stage %d (%s) failed: %w", s.stage, s.stage, err)
}
result.Message = "Completed successfully"
p.results = append(p.results, result)
}
return nil
}
// Results returns all stage results
func (p *Pipeline) Results() []StageResult {
return p.results
}
// sshTarget returns the SSH target string
func (p *Pipeline) sshTarget() string {
return fmt.Sprintf("%s@%s", p.SSHUser, p.VM.PublicIP)
}