Files
recover-server/internal/dns/health.go
Olaf Berberich 29a2886402 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>
2025-12-08 00:31:27 +00:00

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")
}