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>
This commit is contained in:
Olaf Berberich
2025-12-08 00:31:27 +00:00
commit 29a2886402
26 changed files with 3826 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package restore
import (
"context"
"fmt"
"strings"
)
// runServices starts restored services
func (p *Pipeline) runServices(ctx context.Context) error {
// Start WireGuard interfaces
if err := p.startWireGuard(ctx); err != nil {
// WireGuard is optional, log but don't fail
if p.Verbose {
fmt.Printf(" WireGuard: %v\n", err)
}
}
// Start Docker
if err := p.startDocker(ctx); err != nil {
return fmt.Errorf("failed to start Docker: %w", err)
}
return nil
}
// startWireGuard enables and starts WireGuard interfaces
func (p *Pipeline) startWireGuard(ctx context.Context) error {
// Check if WireGuard configs exist
checkCmd := "ls /etc/wireguard/*.conf 2>/dev/null | head -5"
output, err := p.remoteCmdOutput(ctx, checkCmd)
if err != nil || strings.TrimSpace(output) == "" {
return fmt.Errorf("no WireGuard configs found")
}
// Get interface names
configs := strings.Split(strings.TrimSpace(output), "\n")
for _, conf := range configs {
if conf == "" {
continue
}
// Extract interface name from path (e.g., /etc/wireguard/wg0.conf -> wg0)
parts := strings.Split(conf, "/")
filename := parts[len(parts)-1]
iface := strings.TrimSuffix(filename, ".conf")
if p.Verbose {
fmt.Printf(" Starting WireGuard interface: %s\n", iface)
}
// Enable and start
enableCmd := fmt.Sprintf("systemctl enable wg-quick@%s", iface)
startCmd := fmt.Sprintf("systemctl start wg-quick@%s", iface)
p.remoteCmd(ctx, enableCmd)
if err := p.remoteCmd(ctx, startCmd); err != nil {
return fmt.Errorf("failed to start %s: %w", iface, err)
}
}
return nil
}