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>
103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
package restore
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// startDocker ensures Docker is running and starts compose stacks
|
|
func (p *Pipeline) startDocker(ctx context.Context) error {
|
|
// Ensure Docker is enabled and running
|
|
if err := p.remoteCmd(ctx, "systemctl enable docker"); err != nil {
|
|
return fmt.Errorf("failed to enable docker: %w", err)
|
|
}
|
|
|
|
if err := p.remoteCmd(ctx, "systemctl start docker"); err != nil {
|
|
return fmt.Errorf("failed to start docker: %w", err)
|
|
}
|
|
|
|
// Wait for Docker to be ready
|
|
for i := 0; i < 30; i++ {
|
|
if err := p.remoteCmd(ctx, "docker info > /dev/null 2>&1"); err == nil {
|
|
break
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
|
|
// Find and start docker-compose stacks
|
|
findCmd := "find /opt -name 'docker-compose.yml' -o -name 'docker-compose.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' 2>/dev/null | head -20"
|
|
output, err := p.remoteCmdOutput(ctx, findCmd)
|
|
if err != nil || strings.TrimSpace(output) == "" {
|
|
if p.Verbose {
|
|
fmt.Println(" No docker-compose files found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
composeFiles := strings.Split(strings.TrimSpace(output), "\n")
|
|
for _, file := range composeFiles {
|
|
if file == "" {
|
|
continue
|
|
}
|
|
|
|
// Get directory containing compose file
|
|
dir := file[:strings.LastIndex(file, "/")]
|
|
|
|
if p.Verbose {
|
|
fmt.Printf(" Starting compose stack in %s\n", dir)
|
|
}
|
|
|
|
// Try docker compose (v2) first, fall back to docker-compose (v1)
|
|
startCmd := fmt.Sprintf("cd %s && (docker compose up -d 2>/dev/null || docker-compose up -d)", dir)
|
|
if err := p.remoteCmd(ctx, startCmd); err != nil {
|
|
if p.Verbose {
|
|
fmt.Printf(" Warning: failed to start stack in %s: %v\n", dir, err)
|
|
}
|
|
// Don't fail on individual stack failures
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runHealth performs health verification
|
|
func (p *Pipeline) runHealth(ctx context.Context) error {
|
|
checks := []struct {
|
|
name string
|
|
cmd string
|
|
require bool
|
|
}{
|
|
{"SSH accessible", "echo ok", true},
|
|
{"Docker running", "docker info > /dev/null 2>&1 && echo ok", true},
|
|
{"Network connectivity", "ping -c 1 8.8.8.8 > /dev/null 2>&1 && echo ok", false},
|
|
{"DNS resolution", "host google.com > /dev/null 2>&1 && echo ok", false},
|
|
}
|
|
|
|
var failures []string
|
|
|
|
for _, check := range checks {
|
|
output, err := p.remoteCmdOutput(ctx, check.cmd)
|
|
success := err == nil && strings.TrimSpace(output) == "ok"
|
|
|
|
status := "✓"
|
|
if !success {
|
|
status = "✗"
|
|
if check.require {
|
|
failures = append(failures, check.name)
|
|
}
|
|
}
|
|
|
|
if p.Verbose {
|
|
fmt.Printf(" %s %s\n", status, check.name)
|
|
}
|
|
}
|
|
|
|
if len(failures) > 0 {
|
|
return fmt.Errorf("required health checks failed: %v", failures)
|
|
}
|
|
|
|
return nil
|
|
}
|