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

100
internal/ui/prompts.go Normal file
View File

@@ -0,0 +1,100 @@
package ui
import (
"bufio"
"fmt"
"os"
"strings"
)
// ConfirmAction asks for yes/no confirmation
func ConfirmAction(message string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s [y/N]: ", message)
response, err := reader.ReadString('\n')
if err != nil {
return false
}
response = strings.TrimSpace(strings.ToLower(response))
return response == "y" || response == "yes"
}
// ConfirmHostname requires typing the hostname to confirm
func ConfirmHostname(hostname string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("\n⚠ DESTRUCTIVE OPERATION ⚠️\n")
fmt.Printf("This will modify DNS for: %s\n", hostname)
fmt.Printf("Type the hostname exactly to confirm: ")
response, err := reader.ReadString('\n')
if err != nil {
return false
}
response = strings.TrimSpace(response)
return response == hostname
}
// ConfirmRecovery requires confirmation for recovery operation
func ConfirmRecovery(host, source, target string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("\n=== RECOVERY CONFIRMATION ===\n")
fmt.Printf("Host: %s\n", host)
fmt.Printf("Source: %s\n", source)
fmt.Printf("Target: %s\n", target)
fmt.Printf("\nThis will create a new VM and restore data.\n")
fmt.Printf("Type 'RECOVER' to proceed: ")
response, err := reader.ReadString('\n')
if err != nil {
return false
}
response = strings.TrimSpace(response)
return response == "RECOVER"
}
// SelectOption presents options and returns selection
func SelectOption(prompt string, options []string) (int, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Println(prompt)
for i, opt := range options {
fmt.Printf(" %d. %s\n", i+1, opt)
}
fmt.Print("Selection: ")
var selection int
_, err := fmt.Fscanf(reader, "%d\n", &selection)
if err != nil {
return -1, err
}
if selection < 1 || selection > len(options) {
return -1, fmt.Errorf("invalid selection")
}
return selection - 1, nil
}
// PrintError prints an error message in red
func PrintError(format string, args ...interface{}) {
fmt.Printf("\033[31mError: "+format+"\033[0m\n", args...)
}
// PrintSuccess prints a success message in green
func PrintSuccess(format string, args ...interface{}) {
fmt.Printf("\033[32m✓ "+format+"\033[0m\n", args...)
}
// PrintWarning prints a warning message in yellow
func PrintWarning(format string, args ...interface{}) {
fmt.Printf("\033[33m⚠ "+format+"\033[0m\n", args...)
}
// PrintInfo prints an info message in blue
func PrintInfo(format string, args ...interface{}) {
fmt.Printf("\033[34m "+format+"\033[0m\n", args...)
}