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>
179 lines
4.2 KiB
Go
179 lines
4.2 KiB
Go
package dns
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
|
|
v3 "github.com/exoscale/egoscale/v3"
|
|
"github.com/exoscale/egoscale/v3/credentials"
|
|
)
|
|
|
|
// ExoscaleDNS manages DNS records via Exoscale API
|
|
type ExoscaleDNS struct {
|
|
client *v3.Client
|
|
}
|
|
|
|
// DNSRecord represents a DNS record
|
|
type DNSRecord struct {
|
|
ID string
|
|
Type string // A, AAAA, CNAME, etc.
|
|
Name string // subdomain or @ for apex
|
|
Content string // IP or target
|
|
TTL int64
|
|
}
|
|
|
|
// NewExoscaleDNS creates a new Exoscale DNS client
|
|
func NewExoscaleDNS(apiKey, apiSecret string) (*ExoscaleDNS, error) {
|
|
creds := credentials.NewStaticCredentials(apiKey, apiSecret)
|
|
client, err := v3.NewClient(creds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Exoscale client: %w", err)
|
|
}
|
|
|
|
return &ExoscaleDNS{client: client}, nil
|
|
}
|
|
|
|
// GetRecord gets a specific DNS record
|
|
func (d *ExoscaleDNS) GetRecord(ctx context.Context, zone, recordType, name string) (*DNSRecord, error) {
|
|
domains, err := d.client.ListDNSDomains(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list domains: %w", err)
|
|
}
|
|
|
|
var domainID v3.UUID
|
|
for _, domain := range domains.DNSDomains {
|
|
if domain.UnicodeName == zone {
|
|
domainID = domain.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
if domainID == "" {
|
|
return nil, fmt.Errorf("zone %s not found", zone)
|
|
}
|
|
|
|
records, err := d.client.ListDNSDomainRecords(ctx, domainID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list records: %w", err)
|
|
}
|
|
|
|
for _, rec := range records.DNSDomainRecords {
|
|
if rec.Type == v3.DNSDomainRecordType(recordType) && rec.Name == name {
|
|
return &DNSRecord{
|
|
ID: string(rec.ID),
|
|
Type: string(rec.Type),
|
|
Name: rec.Name,
|
|
Content: rec.Content,
|
|
TTL: rec.Ttl,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("record %s.%s not found", name, zone)
|
|
}
|
|
|
|
// UpdateRecord updates a DNS record
|
|
func (d *ExoscaleDNS) UpdateRecord(ctx context.Context, zone string, record *DNSRecord) error {
|
|
domains, err := d.client.ListDNSDomains(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list domains: %w", err)
|
|
}
|
|
|
|
var domainID v3.UUID
|
|
for _, domain := range domains.DNSDomains {
|
|
if domain.UnicodeName == zone {
|
|
domainID = domain.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
if domainID == "" {
|
|
return fmt.Errorf("zone %s not found", zone)
|
|
}
|
|
|
|
op, err := d.client.UpdateDNSDomainRecord(ctx, domainID, v3.UUID(record.ID), v3.UpdateDNSDomainRecordRequest{
|
|
Content: record.Content,
|
|
Ttl: record.TTL,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update record: %w", err)
|
|
}
|
|
|
|
_, err = d.client.Wait(ctx, op, v3.OperationStateSuccess)
|
|
if err != nil {
|
|
return fmt.Errorf("update operation failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListRecords lists all records in a zone
|
|
func (d *ExoscaleDNS) ListRecords(ctx context.Context, zone string) ([]DNSRecord, error) {
|
|
domains, err := d.client.ListDNSDomains(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list domains: %w", err)
|
|
}
|
|
|
|
var domainID v3.UUID
|
|
for _, domain := range domains.DNSDomains {
|
|
if domain.UnicodeName == zone {
|
|
domainID = domain.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
if domainID == "" {
|
|
return nil, fmt.Errorf("zone %s not found", zone)
|
|
}
|
|
|
|
records, err := d.client.ListDNSDomainRecords(ctx, domainID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list records: %w", err)
|
|
}
|
|
|
|
var result []DNSRecord
|
|
for _, rec := range records.DNSDomainRecords {
|
|
result = append(result, DNSRecord{
|
|
ID: string(rec.ID),
|
|
Type: string(rec.Type),
|
|
Name: rec.Name,
|
|
Content: rec.Content,
|
|
TTL: rec.Ttl,
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ListZones lists all DNS zones managed in Exoscale
|
|
func (d *ExoscaleDNS) ListZones(ctx context.Context) ([]string, error) {
|
|
domains, err := d.client.ListDNSDomains(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list domains: %w", err)
|
|
}
|
|
|
|
var zones []string
|
|
for _, domain := range domains.DNSDomains {
|
|
zones = append(zones, domain.UnicodeName)
|
|
}
|
|
|
|
return zones, nil
|
|
}
|
|
|
|
// ResolveCurrentIP resolves the current IP for a hostname
|
|
func ResolveCurrentIP(hostname string) (string, error) {
|
|
ips, err := net.LookupIP(hostname)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, ip := range ips {
|
|
if ipv4 := ip.To4(); ipv4 != nil {
|
|
return ipv4.String(), nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("no IPv4 address found for %s", hostname)
|
|
}
|