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

108
internal/ui/dryrun.go Normal file
View File

@@ -0,0 +1,108 @@
package ui
import (
"fmt"
)
// DryRun tracks dry-run mode operations
type DryRun struct {
Enabled bool
Operations []DryRunOp
}
// DryRunOp represents a single operation that would be performed
type DryRunOp struct {
Component string // e.g., "VM", "DNS", "Restore"
Action string // e.g., "Create", "Update", "Delete"
Description string
}
// NewDryRun creates a dry-run tracker
func NewDryRun(enabled bool) *DryRun {
return &DryRun{
Enabled: enabled,
Operations: make([]DryRunOp, 0),
}
}
// AddOperation records an operation
func (d *DryRun) AddOperation(component, action, description string) {
d.Operations = append(d.Operations, DryRunOp{
Component: component,
Action: action,
Description: description,
})
}
// Print displays all recorded operations
func (d *DryRun) Print() {
if !d.Enabled {
return
}
fmt.Println("\n" + HeaderLine("DRY RUN - No changes will be made"))
fmt.Println()
if len(d.Operations) == 0 {
fmt.Println("No operations would be performed.")
return
}
for i, op := range d.Operations {
fmt.Printf("%d. [%s] %s: %s\n", i+1, op.Component, op.Action, op.Description)
}
fmt.Println()
fmt.Println("To execute these operations, run with --yes flag")
}
// HeaderLine creates a formatted header
func HeaderLine(title string) string {
return fmt.Sprintf("=== %s ===", title)
}
// TablePrint prints data as a simple table
func TablePrint(headers []string, rows [][]string) {
// Calculate column widths
widths := make([]int, len(headers))
for i, h := range headers {
widths[i] = len(h)
}
for _, row := range rows {
for i, cell := range row {
if i < len(widths) && len(cell) > widths[i] {
widths[i] = len(cell)
}
}
}
// Print header
for i, h := range headers {
fmt.Printf("%-*s ", widths[i], h)
}
fmt.Println()
// Print separator
for i := range headers {
fmt.Printf("%s ", repeat("-", widths[i]))
}
fmt.Println()
// Print rows
for _, row := range rows {
for i, cell := range row {
if i < len(widths) {
fmt.Printf("%-*s ", widths[i], cell)
}
}
fmt.Println()
}
}
func repeat(s string, n int) string {
result := ""
for i := 0; i < n; i++ {
result += s
}
return result
}