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>
136 lines
3.0 KiB
Go
136 lines
3.0 KiB
Go
package dns
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// HealthResult contains the result of health checks
|
|
type HealthResult struct {
|
|
SSHReady bool
|
|
HTTPSReady bool
|
|
SSHError error
|
|
HTTPSError error
|
|
}
|
|
|
|
// HealthChecker performs health checks on VMs
|
|
type HealthChecker struct {
|
|
SSHTimeout time.Duration
|
|
HTTPSTimeout time.Duration
|
|
}
|
|
|
|
// NewHealthChecker creates a new health checker
|
|
func NewHealthChecker() *HealthChecker {
|
|
return &HealthChecker{
|
|
SSHTimeout: 10 * time.Second,
|
|
HTTPSTimeout: 10 * time.Second,
|
|
}
|
|
}
|
|
|
|
// CheckSSH checks if SSH is accessible
|
|
func (h *HealthChecker) CheckSSH(ctx context.Context, ip string, port int) error {
|
|
if port == 0 {
|
|
port = 22
|
|
}
|
|
|
|
address := fmt.Sprintf("%s:%d", ip, port)
|
|
|
|
dialer := &net.Dialer{
|
|
Timeout: h.SSHTimeout,
|
|
}
|
|
|
|
conn, err := dialer.DialContext(ctx, "tcp", address)
|
|
if err != nil {
|
|
return fmt.Errorf("SSH not accessible: %w", err)
|
|
}
|
|
conn.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckHTTPS checks if HTTPS is accessible
|
|
func (h *HealthChecker) CheckHTTPS(ctx context.Context, ip string, port int) error {
|
|
if port == 0 {
|
|
port = 443
|
|
}
|
|
|
|
// Use IP directly with insecure TLS (we just want to verify the port is open)
|
|
client := &http.Client{
|
|
Timeout: h.HTTPSTimeout,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
url := fmt.Sprintf("https://%s:%d/", ip, port)
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
// Connection refused or timeout is a failure
|
|
// But TLS errors mean the port is open (which is what we want)
|
|
if _, ok := err.(*tls.CertificateVerificationError); ok {
|
|
return nil // Port is open, TLS is working
|
|
}
|
|
|
|
// For other TLS errors, the port is still open
|
|
if netErr, ok := err.(net.Error); ok && !netErr.Timeout() {
|
|
// Non-timeout network error might still mean port is open
|
|
// Check if we can at least connect
|
|
conn, connErr := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), h.HTTPSTimeout)
|
|
if connErr == nil {
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("HTTPS not accessible: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckAll performs all health checks
|
|
func (h *HealthChecker) CheckAll(ctx context.Context, ip string) *HealthResult {
|
|
result := &HealthResult{}
|
|
|
|
result.SSHError = h.CheckSSH(ctx, ip, 22)
|
|
result.SSHReady = result.SSHError == nil
|
|
|
|
result.HTTPSError = h.CheckHTTPS(ctx, ip, 443)
|
|
result.HTTPSReady = result.HTTPSError == nil
|
|
|
|
return result
|
|
}
|
|
|
|
// WaitForReady waits for all health checks to pass
|
|
func (h *HealthChecker) WaitForReady(ctx context.Context, ip string, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
result := h.CheckAll(ctx, ip)
|
|
if result.SSHReady {
|
|
return nil // SSH is the minimum requirement
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(5 * time.Second):
|
|
// Continue checking
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("timeout waiting for VM to be ready")
|
|
}
|