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:
178
internal/dns/exoscale.go
Normal file
178
internal/dns/exoscale.go
Normal file
@@ -0,0 +1,178 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user