mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-12 02:17:43 +08:00
b968de3ef7
* gandi/livedns: Print actual changes to be pushed Currently, preview & push output prints all domain records values: $ dnscontrol preview ----- DNS Provider: gandi...1 correction #1: Setting dns records for example.com: mail.example.com. 1.2.3.4 "ga" "bu" "zo" With this change, it would only show what changes from current state: $ dnscontrol preview ----- DNS Provider: gandi...1 correction #1: Setting dns records for example.com: CREATE TXT ga.example.com "ga" ttl=10800 MODIFY TXT bu.example.com "bu" ttl=10800 DELETE TXT meu.example.com "meu" ttl=10800 * Add import
283 lines
8.2 KiB
Go
283 lines
8.2 KiB
Go
package gandi
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/StackExchange/dnscontrol/models"
|
|
"github.com/StackExchange/dnscontrol/pkg/printer"
|
|
"github.com/StackExchange/dnscontrol/providers"
|
|
"github.com/StackExchange/dnscontrol/providers/diff"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
gandiclient "github.com/prasmussen/gandi-api/client"
|
|
gandilivedomain "github.com/prasmussen/gandi-api/live_dns/domain"
|
|
gandiliverecord "github.com/prasmussen/gandi-api/live_dns/record"
|
|
gandilivezone "github.com/prasmussen/gandi-api/live_dns/zone"
|
|
)
|
|
|
|
var liveFeatures = providers.DocumentationNotes{
|
|
providers.CanUseCAA: providers.Can(),
|
|
providers.CanUsePTR: providers.Can(),
|
|
providers.CanUseSRV: providers.Can(),
|
|
providers.CantUseNOPURGE: providers.Cannot(),
|
|
providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"),
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
}
|
|
|
|
func init() {
|
|
providers.RegisterDomainServiceProviderType("GANDI-LIVEDNS", newLiveDsp, liveFeatures)
|
|
}
|
|
|
|
func newLiveDsp(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
APIKey := m["apikey"]
|
|
if APIKey == "" {
|
|
return nil, errors.Errorf("missing Gandi apikey")
|
|
}
|
|
|
|
return newLiveClient(APIKey), nil
|
|
}
|
|
|
|
type domainManager interface {
|
|
Info(string) (*gandilivedomain.Info, error)
|
|
Records(string) gandiliverecord.Manager
|
|
}
|
|
|
|
type zoneManager interface {
|
|
InfoByUUID(uuid.UUID) (*gandilivezone.Info, error)
|
|
Create(gandilivezone.Info) (*gandilivezone.CreateStatus, error)
|
|
Set(string, gandilivezone.Info) (*gandilivezone.Status, error)
|
|
Records(gandilivezone.Info) gandiliverecord.Manager
|
|
}
|
|
|
|
type liveClient struct {
|
|
client *gandiclient.Client
|
|
zoneManager zoneManager
|
|
domainManager domainManager
|
|
}
|
|
|
|
func newLiveClient(APIKey string) *liveClient {
|
|
cl := gandiclient.New(APIKey, gandiclient.LiveDNS)
|
|
return &liveClient{
|
|
client: cl,
|
|
zoneManager: gandilivezone.New(cl),
|
|
domainManager: gandilivedomain.New(cl),
|
|
}
|
|
}
|
|
|
|
// GetNameservers returns the list of gandi name servers for a given domain
|
|
func (c *liveClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
domains := []string{}
|
|
response, err := c.client.Get("/nameservers/"+domain, &domains)
|
|
if err != nil {
|
|
return nil, errors.Errorf("failed to get nameservers for domain %s", domain)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
ns := []*models.Nameserver{}
|
|
for _, domain := range domains {
|
|
ns = append(ns, &models.Nameserver{Name: domain})
|
|
}
|
|
return ns, nil
|
|
}
|
|
|
|
// GetDomainCorrections returns a list of corrections recommended for this domain.
|
|
func (c *liveClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
dc.Punycode()
|
|
records, err := c.domainManager.Records(dc.Name).List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
foundRecords := c.recordConfigFromInfo(records, dc.Name)
|
|
recordsToKeep, records, err := c.recordsToInfo(dc.Records)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dc.Records = recordsToKeep
|
|
|
|
// Normalize
|
|
models.PostProcessRecords(foundRecords)
|
|
|
|
differ := diff.New(dc)
|
|
|
|
_, create, del, mod := differ.IncrementalDiff(foundRecords)
|
|
|
|
buf := &bytes.Buffer{}
|
|
// Print a list of changes. Generate an actual change that is the zone
|
|
changes := false
|
|
for _, i := range create {
|
|
changes = true
|
|
fmt.Fprintln(buf, i)
|
|
}
|
|
for _, i := range del {
|
|
changes = true
|
|
fmt.Fprintln(buf, i)
|
|
}
|
|
for _, i := range mod {
|
|
changes = true
|
|
fmt.Fprintln(buf, i)
|
|
}
|
|
|
|
if changes {
|
|
message := fmt.Sprintf("Setting dns records for %s:", dc.Name)
|
|
message += "\n" + buf.String()
|
|
return []*models.Correction{
|
|
{
|
|
Msg: message,
|
|
F: func() error {
|
|
return c.createZone(dc.Name, records)
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
return []*models.Correction{}, nil
|
|
}
|
|
|
|
// createZone creates a new empty zone for the domain, populates it with the record infos and associates the domain to it
|
|
func (c *liveClient) createZone(domainname string, records []*gandiliverecord.Info) error {
|
|
domainInfo, err := c.domainManager.Info(domainname)
|
|
infos, err := c.zoneManager.InfoByUUID(*domainInfo.ZoneUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
infos.Name = fmt.Sprintf("zone created by dnscontrol for %s on %s", domainname, time.Now().Format(time.RFC3339))
|
|
printer.Debugf("DEBUG: createZone SharingID=%v\n", infos.SharingID)
|
|
|
|
// duplicate zone Infos
|
|
status, err := c.zoneManager.Create(*infos)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
zoneInfos, err := c.zoneManager.InfoByUUID(*status.UUID)
|
|
if err != nil {
|
|
// gandi might take some time to make the new zone available
|
|
for i := 0; i < 10; i++ {
|
|
printer.Printf("zone info not yet available. Delay and retry: %s\n", err.Error())
|
|
time.Sleep(100 * time.Millisecond)
|
|
zoneInfos, err = c.zoneManager.InfoByUUID(*status.UUID)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recordManager := c.zoneManager.Records(*zoneInfos)
|
|
for _, record := range records {
|
|
_, err := recordManager.Create(*record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = c.zoneManager.Set(domainname, *zoneInfos)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// recordConfigFromInfo takes a DNS record from Gandi liveDNS and returns our native RecordConfig format.
|
|
func (c *liveClient) recordConfigFromInfo(infos []*gandiliverecord.Info, origin string) []*models.RecordConfig {
|
|
rcs := []*models.RecordConfig{}
|
|
for _, info := range infos {
|
|
// TXT records might have multiple values. In that case,
|
|
// they are all for the TXT record at that label.
|
|
if info.Type == "TXT" {
|
|
rc := &models.RecordConfig{
|
|
Type: info.Type,
|
|
Original: info,
|
|
TTL: uint32(info.TTL),
|
|
}
|
|
rc.SetLabel(info.Name, origin)
|
|
var parsed []string
|
|
for _, txt := range info.Values {
|
|
parsed = append(parsed, models.StripQuotes(txt))
|
|
}
|
|
err := rc.SetTargetTXTs(parsed)
|
|
if err != nil {
|
|
panic(errors.Wrapf(err, "recordConfigFromInfo=TXT failed"))
|
|
}
|
|
rcs = append(rcs, rc)
|
|
} else {
|
|
// All other record types might have multiple values, but that means
|
|
// we should create one Recordconfig for each one.
|
|
for _, value := range info.Values {
|
|
rc := &models.RecordConfig{
|
|
Type: info.Type,
|
|
Original: info,
|
|
TTL: uint32(info.TTL),
|
|
}
|
|
rc.SetLabel(info.Name, origin)
|
|
switch rtype := info.Type; rtype {
|
|
default:
|
|
err := rc.PopulateFromString(rtype, value, origin)
|
|
if err != nil {
|
|
panic(errors.Wrapf(err, "recordConfigFromInfo failed"))
|
|
}
|
|
}
|
|
rcs = append(rcs, rc)
|
|
}
|
|
}
|
|
}
|
|
return rcs
|
|
}
|
|
|
|
// recordsToInfo generates gandi record sets and filters incompatible entries from native records format
|
|
func (c *liveClient) recordsToInfo(records models.Records) (models.Records, []*gandiliverecord.Info, error) {
|
|
recordSets := map[string]map[string]*gandiliverecord.Info{}
|
|
recordInfos := []*gandiliverecord.Info{}
|
|
recordToKeep := models.Records{}
|
|
|
|
for _, rec := range records {
|
|
if rec.TTL < 300 {
|
|
printer.Warnf("Gandi does not support ttls < 300. %s will not be set to %d.\n", rec.GetLabelFQDN(), rec.TTL)
|
|
rec.TTL = 300
|
|
}
|
|
if rec.TTL > 2592000 {
|
|
return nil, nil, errors.Errorf("ERROR: Gandi does not support TTLs > 30 days (TTL=%d)", rec.TTL)
|
|
}
|
|
if rec.Type == "NS" && rec.GetLabel() == "@" {
|
|
if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") {
|
|
printer.Warnf("Gandi does not support changing apex NS records. %s will not be added.\n", rec.GetTargetField())
|
|
}
|
|
continue
|
|
}
|
|
r, ok := recordSets[rec.GetLabel()][rec.Type]
|
|
if !ok {
|
|
_, ok := recordSets[rec.GetLabel()]
|
|
if !ok {
|
|
recordSets[rec.GetLabel()] = map[string]*gandiliverecord.Info{}
|
|
}
|
|
r = &gandiliverecord.Info{
|
|
Type: rec.Type,
|
|
Name: rec.GetLabel(),
|
|
TTL: int64(rec.TTL),
|
|
}
|
|
recordInfos = append(recordInfos, r)
|
|
recordSets[rec.GetLabel()][rec.Type] = r
|
|
} else {
|
|
if r.TTL != int64(rec.TTL) {
|
|
printer.Warnf(
|
|
"Gandi liveDNS API does not support different TTL for the couple fqdn/type. Will use TTL of %d for %s %s\n",
|
|
r.TTL,
|
|
r.Type,
|
|
r.Name,
|
|
)
|
|
}
|
|
}
|
|
recordToKeep = append(recordToKeep, rec)
|
|
if rec.Type == "TXT" {
|
|
for _, t := range rec.TxtStrings {
|
|
r.Values = append(r.Values, "\""+t+"\"") // FIXME(tlim): Should do proper quoting.
|
|
}
|
|
} else {
|
|
r.Values = append(r.Values, rec.GetTargetCombined())
|
|
}
|
|
}
|
|
return recordToKeep, recordInfos, nil
|
|
}
|