github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/providers/gandi/livedns.go (about) 1 package gandi 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "strings" 8 "time" 9 10 "github.com/StackExchange/dnscontrol/models" 11 "github.com/StackExchange/dnscontrol/providers" 12 "github.com/StackExchange/dnscontrol/providers/diff" 13 "github.com/google/uuid" 14 "github.com/pkg/errors" 15 gandiclient "github.com/prasmussen/gandi-api/client" 16 gandilivedomain "github.com/prasmussen/gandi-api/live_dns/domain" 17 gandiliverecord "github.com/prasmussen/gandi-api/live_dns/record" 18 gandilivezone "github.com/prasmussen/gandi-api/live_dns/zone" 19 ) 20 21 // Enable/disable debug output: 22 const debug = false 23 24 var liveFeatures = providers.DocumentationNotes{ 25 providers.CanUseCAA: providers.Can(), 26 providers.CanUsePTR: providers.Can(), 27 providers.CanUseSRV: providers.Can(), 28 providers.CantUseNOPURGE: providers.Cannot(), 29 providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"), 30 providers.DocOfficiallySupported: providers.Cannot(), 31 } 32 33 func init() { 34 providers.RegisterDomainServiceProviderType("GANDI-LIVEDNS", newLiveDsp, liveFeatures) 35 } 36 37 func newLiveDsp(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 38 APIKey := m["apikey"] 39 if APIKey == "" { 40 return nil, errors.Errorf("missing Gandi apikey") 41 } 42 43 return newLiveClient(APIKey), nil 44 } 45 46 type domainManager interface { 47 Info(string) (*gandilivedomain.Info, error) 48 Records(string) gandiliverecord.Manager 49 } 50 51 type zoneManager interface { 52 InfoByUUID(uuid.UUID) (*gandilivezone.Info, error) 53 Create(gandilivezone.Info) (*gandilivezone.CreateStatus, error) 54 Set(string, gandilivezone.Info) (*gandilivezone.Status, error) 55 Records(gandilivezone.Info) gandiliverecord.Manager 56 } 57 58 type liveClient struct { 59 client *gandiclient.Client 60 zoneManager zoneManager 61 domainManager domainManager 62 } 63 64 func newLiveClient(APIKey string) *liveClient { 65 cl := gandiclient.New(APIKey, gandiclient.LiveDNS) 66 return &liveClient{ 67 client: cl, 68 zoneManager: gandilivezone.New(cl), 69 domainManager: gandilivedomain.New(cl), 70 } 71 } 72 73 // GetNameservers returns the list of gandi name servers for a given domain 74 func (c *liveClient) GetNameservers(domain string) ([]*models.Nameserver, error) { 75 domains := []string{} 76 response, err := c.client.Get("/nameservers/"+domain, &domains) 77 if err != nil { 78 return nil, errors.Errorf("failed to get nameservers for domain %s", domain) 79 } 80 defer response.Body.Close() 81 82 ns := []*models.Nameserver{} 83 for _, domain := range domains { 84 ns = append(ns, &models.Nameserver{Name: domain}) 85 } 86 return ns, nil 87 } 88 89 // GetDomainCorrections returns a list of corrections recommended for this domain. 90 func (c *liveClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 91 dc.Punycode() 92 records, err := c.domainManager.Records(dc.Name).List() 93 if err != nil { 94 return nil, err 95 } 96 foundRecords := c.recordConfigFromInfo(records, dc.Name) 97 recordsToKeep, records, err := c.recordsToInfo(dc.Records) 98 if err != nil { 99 return nil, err 100 } 101 dc.Records = recordsToKeep 102 103 // Normalize 104 models.PostProcessRecords(foundRecords) 105 106 differ := diff.New(dc) 107 108 _, create, del, mod := differ.IncrementalDiff(foundRecords) 109 if len(create)+len(del)+len(mod) > 0 { 110 message := fmt.Sprintf("Setting dns records for %s:", dc.Name) 111 for _, record := range dc.Records { 112 message += "\n" + record.GetTargetCombined() 113 } 114 return []*models.Correction{ 115 { 116 Msg: message, 117 F: func() error { 118 return c.createZone(dc.Name, records) 119 }, 120 }, 121 }, nil 122 } 123 return []*models.Correction{}, nil 124 } 125 126 // createZone creates a new empty zone for the domain, populates it with the record infos and associates the domain to it 127 func (c *liveClient) createZone(domainname string, records []*gandiliverecord.Info) error { 128 domainInfo, err := c.domainManager.Info(domainname) 129 infos, err := c.zoneManager.InfoByUUID(*domainInfo.ZoneUUID) 130 if err != nil { 131 return err 132 } 133 infos.Name = fmt.Sprintf("zone created by dnscontrol for %s on %s", domainname, time.Now().Format(time.RFC3339)) 134 if debug { 135 fmt.Printf("DEBUG: createZone SharingID=%v\n", infos.SharingID) 136 } 137 // duplicate zone Infos 138 status, err := c.zoneManager.Create(*infos) 139 if err != nil { 140 return err 141 } 142 zoneInfos, err := c.zoneManager.InfoByUUID(*status.UUID) 143 if err != nil { 144 // gandi might take some time to make the new zone available 145 for i := 0; i < 10; i++ { 146 log.Printf("INFO: zone info not yet available. Delay and retry: %s", err.Error()) 147 time.Sleep(100 * time.Millisecond) 148 zoneInfos, err = c.zoneManager.InfoByUUID(*status.UUID) 149 if err == nil { 150 break 151 } 152 } 153 } 154 if err != nil { 155 return err 156 } 157 recordManager := c.zoneManager.Records(*zoneInfos) 158 for _, record := range records { 159 _, err := recordManager.Create(*record) 160 if err != nil { 161 return err 162 } 163 } 164 _, err = c.zoneManager.Set(domainname, *zoneInfos) 165 if err != nil { 166 return err 167 } 168 169 return nil 170 } 171 172 // recordConfigFromInfo takes a DNS record from Gandi liveDNS and returns our native RecordConfig format. 173 func (c *liveClient) recordConfigFromInfo(infos []*gandiliverecord.Info, origin string) []*models.RecordConfig { 174 rcs := []*models.RecordConfig{} 175 for _, info := range infos { 176 // TXT records might have multiple values. In that case, 177 // they are all for the TXT record at that label. 178 if info.Type == "TXT" { 179 rc := &models.RecordConfig{ 180 Type: info.Type, 181 Original: info, 182 TTL: uint32(info.TTL), 183 } 184 rc.SetLabel(info.Name, origin) 185 var parsed []string 186 for _, txt := range info.Values { 187 parsed = append(parsed, models.StripQuotes(txt)) 188 } 189 err := rc.SetTargetTXTs(parsed) 190 if err != nil { 191 panic(errors.Wrapf(err, "recordConfigFromInfo=TXT failed")) 192 } 193 rcs = append(rcs, rc) 194 } else { 195 // All other record types might have multiple values, but that means 196 // we should create one Recordconfig for each one. 197 for _, value := range info.Values { 198 rc := &models.RecordConfig{ 199 Type: info.Type, 200 Original: info, 201 TTL: uint32(info.TTL), 202 } 203 rc.SetLabel(info.Name, origin) 204 switch rtype := info.Type; rtype { 205 default: 206 err := rc.PopulateFromString(rtype, value, origin) 207 if err != nil { 208 panic(errors.Wrapf(err, "recordConfigFromInfo failed")) 209 } 210 } 211 rcs = append(rcs, rc) 212 } 213 } 214 } 215 return rcs 216 } 217 218 // recordsToInfo generates gandi record sets and filters incompatible entries from native records format 219 func (c *liveClient) recordsToInfo(records models.Records) (models.Records, []*gandiliverecord.Info, error) { 220 recordSets := map[string]map[string]*gandiliverecord.Info{} 221 recordInfos := []*gandiliverecord.Info{} 222 recordToKeep := models.Records{} 223 224 for _, rec := range records { 225 if rec.TTL < 300 { 226 log.Printf("WARNING: Gandi does not support ttls < 300. %s will not be set to %d.", rec.GetLabelFQDN(), rec.TTL) 227 rec.TTL = 300 228 } 229 if rec.TTL > 2592000 { 230 return nil, nil, errors.Errorf("ERROR: Gandi does not support TTLs > 30 days (TTL=%d)", rec.TTL) 231 } 232 if rec.Type == "NS" && rec.GetLabel() == "@" { 233 if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") { 234 log.Printf("WARNING: Gandi does not support changing apex NS records. %s will not be added.", rec.GetTargetField()) 235 } 236 continue 237 } 238 r, ok := recordSets[rec.GetLabel()][rec.Type] 239 if !ok { 240 _, ok := recordSets[rec.GetLabel()] 241 if !ok { 242 recordSets[rec.GetLabel()] = map[string]*gandiliverecord.Info{} 243 } 244 r = &gandiliverecord.Info{ 245 Type: rec.Type, 246 Name: rec.GetLabel(), 247 TTL: int64(rec.TTL), 248 } 249 recordInfos = append(recordInfos, r) 250 recordSets[rec.GetLabel()][rec.Type] = r 251 } else { 252 if r.TTL != int64(rec.TTL) { 253 log.Printf( 254 "WARNING: Gandi liveDNS API does not support different TTL for the couple fqdn/type. Will use TTL of %d for %s %s", 255 r.TTL, 256 r.Type, 257 r.Name, 258 ) 259 } 260 } 261 recordToKeep = append(recordToKeep, rec) 262 if rec.Type == "TXT" { 263 for _, t := range rec.TxtStrings { 264 r.Values = append(r.Values, "\""+t+"\"") // FIXME(tlim): Should do proper quoting. 265 } 266 } else { 267 r.Values = append(r.Values, rec.GetTargetCombined()) 268 } 269 } 270 return recordToKeep, recordInfos, nil 271 }