Files
recover-server/internal/dns/exoscale.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

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