github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/softlayer/softlayerProvider.go (about) 1 package softlayer 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "github.com/softlayer/softlayer-go/datatypes" 10 "github.com/softlayer/softlayer-go/filter" 11 "github.com/softlayer/softlayer-go/services" 12 "github.com/softlayer/softlayer-go/session" 13 14 "github.com/StackExchange/dnscontrol/v2/models" 15 "github.com/StackExchange/dnscontrol/v2/providers" 16 "github.com/StackExchange/dnscontrol/v2/providers/diff" 17 ) 18 19 // SoftLayer is the protocol handle for this provider. 20 type SoftLayer struct { 21 Session *session.Session 22 } 23 24 var features = providers.DocumentationNotes{ 25 providers.CanUseSRV: providers.Can(), 26 providers.CanGetZones: providers.Unimplemented(), 27 } 28 29 func init() { 30 providers.RegisterDomainServiceProviderType("SOFTLAYER", newReg, features) 31 } 32 33 func newReg(conf map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) { 34 s := session.New(conf["username"], conf["api_key"], conf["endpoint_url"], conf["timeout"]) 35 36 if len(s.UserName) == 0 || len(s.APIKey) == 0 { 37 return nil, fmt.Errorf("SoftLayer UserName and APIKey must be provided") 38 } 39 40 // s.Debug = true 41 42 api := &SoftLayer{ 43 Session: s, 44 } 45 46 return api, nil 47 } 48 49 // GetNameservers returns the nameservers for a domain. 50 func (s *SoftLayer) GetNameservers(domain string) ([]*models.Nameserver, error) { 51 // Always use the same nameservers for softlayer 52 nservers := []string{"ns1.softlayer.com", "ns2.softlayer.com"} 53 return models.StringsToNameservers(nservers), nil 54 } 55 56 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 57 func (client *SoftLayer) GetZoneRecords(domain string) (models.Records, error) { 58 return nil, fmt.Errorf("not implemented") 59 // This enables the get-zones subcommand. 60 // Implement this by extracting the code from GetDomainCorrections into 61 // a single function. For most providers this should be relatively easy. 62 } 63 64 // GetDomainCorrections returns corrections to update a domain. 65 func (s *SoftLayer) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 66 corrections := []*models.Correction{} 67 68 domain, err := s.getDomain(&dc.Name) 69 70 if err != nil { 71 return nil, err 72 } 73 74 actual, err := s.getExistingRecords(domain) 75 76 if err != nil { 77 return nil, err 78 } 79 80 _, create, delete, modify := diff.New(dc).IncrementalDiff(actual) 81 82 for _, del := range delete { 83 existing := del.Existing.Original.(datatypes.Dns_Domain_ResourceRecord) 84 corrections = append(corrections, &models.Correction{ 85 Msg: del.String(), 86 F: s.deleteRecordFunc(*existing.Id), 87 }) 88 } 89 90 for _, cre := range create { 91 corrections = append(corrections, &models.Correction{ 92 Msg: cre.String(), 93 F: s.createRecordFunc(cre.Desired, domain), 94 }) 95 } 96 97 for _, mod := range modify { 98 existing := mod.Existing.Original.(datatypes.Dns_Domain_ResourceRecord) 99 corrections = append(corrections, &models.Correction{ 100 Msg: mod.String(), 101 F: s.updateRecordFunc(&existing, mod.Desired), 102 }) 103 } 104 105 return corrections, nil 106 } 107 108 func (s *SoftLayer) getDomain(name *string) (*datatypes.Dns_Domain, error) { 109 domains, err := services.GetAccountService(s.Session). 110 Filter(filter.Path("domains.name").Eq(name).Build()). 111 Mask("resourceRecords"). 112 GetDomains() 113 114 if err != nil { 115 return nil, err 116 } 117 118 if len(domains) == 0 { 119 return nil, fmt.Errorf("Didn't find a domain matching %s", *name) 120 } else if len(domains) > 1 { 121 return nil, fmt.Errorf("Found %d domains matching %s", len(domains), *name) 122 } 123 124 return &domains[0], nil 125 } 126 127 func (s *SoftLayer) getExistingRecords(domain *datatypes.Dns_Domain) ([]*models.RecordConfig, error) { 128 actual := []*models.RecordConfig{} 129 130 for _, record := range domain.ResourceRecords { 131 recType := strings.ToUpper(*record.Type) 132 133 if recType == "SOA" { 134 continue 135 } 136 137 recConfig := &models.RecordConfig{ 138 Type: recType, 139 TTL: uint32(*record.Ttl), 140 Original: record, 141 } 142 recConfig.SetTarget(*record.Data) 143 144 switch recType { 145 case "SRV": 146 var service, protocol string = "", "_tcp" 147 148 if record.Weight != nil { 149 recConfig.SrvWeight = uint16(*record.Weight) 150 } 151 if record.Port != nil { 152 recConfig.SrvPort = uint16(*record.Port) 153 } 154 if record.Priority != nil { 155 recConfig.SrvPriority = uint16(*record.Priority) 156 } 157 if record.Protocol != nil { 158 protocol = *record.Protocol 159 } 160 if record.Service != nil { 161 service = *record.Service 162 } 163 recConfig.SetLabel(fmt.Sprintf("%s.%s", service, strings.ToLower(protocol)), *domain.Name) 164 case "TXT": 165 recConfig.TxtStrings = append(recConfig.TxtStrings, *record.Data) 166 fallthrough 167 case "MX": 168 if record.MxPriority != nil { 169 recConfig.MxPreference = uint16(*record.MxPriority) 170 } 171 fallthrough 172 default: 173 recConfig.SetLabel(*record.Host, *domain.Name) 174 } 175 176 actual = append(actual, recConfig) 177 } 178 179 // Normalize 180 models.PostProcessRecords(actual) 181 182 return actual, nil 183 } 184 185 func (s *SoftLayer) createRecordFunc(desired *models.RecordConfig, domain *datatypes.Dns_Domain) func() error { 186 var ttl, preference, domainID int = int(desired.TTL), int(desired.MxPreference), *domain.Id 187 var weight, priority, port int = int(desired.SrvWeight), int(desired.SrvPriority), int(desired.SrvPort) 188 var host, data, newType string = desired.GetLabel(), desired.GetTargetField(), desired.Type 189 var err error 190 191 srvRegexp := regexp.MustCompile(`^_(?P<Service>\w+)\.\_(?P<Protocol>\w+)$`) 192 193 return func() error { 194 newRecord := datatypes.Dns_Domain_ResourceRecord{ 195 DomainId: &domainID, 196 Ttl: &ttl, 197 Type: &newType, 198 Data: &data, 199 Host: &host, 200 } 201 202 switch newType { 203 case "MX": 204 service := services.GetDnsDomainResourceRecordMxTypeService(s.Session) 205 206 newRecord.MxPriority = &preference 207 208 newMx := datatypes.Dns_Domain_ResourceRecord_MxType{ 209 Dns_Domain_ResourceRecord: newRecord, 210 } 211 212 _, err = service.CreateObject(&newMx) 213 214 case "SRV": 215 service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session) 216 result := srvRegexp.FindStringSubmatch(host) 217 218 if len(result) != 3 { 219 return fmt.Errorf("SRV Record must match format \"_service._protocol\" not %s", host) 220 } 221 222 var serviceName, protocol string = result[1], strings.ToLower(result[2]) 223 224 newSrv := datatypes.Dns_Domain_ResourceRecord_SrvType{ 225 Dns_Domain_ResourceRecord: newRecord, 226 Service: &serviceName, 227 Port: &port, 228 Priority: &priority, 229 Protocol: &protocol, 230 Weight: &weight, 231 } 232 233 _, err = service.CreateObject(&newSrv) 234 235 default: 236 service := services.GetDnsDomainResourceRecordService(s.Session) 237 _, err = service.CreateObject(&newRecord) 238 } 239 240 return err 241 } 242 } 243 244 func (s *SoftLayer) deleteRecordFunc(resID int) func() error { 245 // seems to be no problem deleting MX and SRV records via common interface 246 return func() error { 247 _, err := services.GetDnsDomainResourceRecordService(s.Session). 248 Id(resID). 249 DeleteObject() 250 251 return err 252 } 253 } 254 255 func (s *SoftLayer) updateRecordFunc(existing *datatypes.Dns_Domain_ResourceRecord, desired *models.RecordConfig) func() error { 256 var ttl, preference int = int(desired.TTL), int(desired.MxPreference) 257 var priority, weight, port int = int(desired.SrvPriority), int(desired.SrvWeight), int(desired.SrvPort) 258 259 return func() error { 260 var changes = false 261 var err error 262 263 switch desired.Type { 264 case "MX": 265 service := services.GetDnsDomainResourceRecordMxTypeService(s.Session) 266 updated := datatypes.Dns_Domain_ResourceRecord_MxType{} 267 268 label := desired.GetLabel() 269 if label != *existing.Host { 270 updated.Host = &label 271 changes = true 272 } 273 274 target := desired.GetTargetField() 275 if target != *existing.Data { 276 updated.Data = &target 277 changes = true 278 } 279 280 if ttl != *existing.Ttl { 281 updated.Ttl = &ttl 282 changes = true 283 } 284 285 if preference != *existing.MxPriority { 286 updated.MxPriority = &preference 287 changes = true 288 } 289 290 if !changes { 291 return fmt.Errorf("didn't find changes when I expect some") 292 } 293 294 _, err = service.Id(*existing.Id).EditObject(&updated) 295 296 case "SRV": 297 service := services.GetDnsDomainResourceRecordSrvTypeService(s.Session) 298 updated := datatypes.Dns_Domain_ResourceRecord_SrvType{} 299 300 label := desired.GetLabel() 301 if label != *existing.Host { 302 updated.Host = &label 303 changes = true 304 } 305 306 target := desired.GetTargetField() 307 if target != *existing.Data { 308 updated.Data = &target 309 changes = true 310 } 311 312 if ttl != *existing.Ttl { 313 updated.Ttl = &ttl 314 changes = true 315 } 316 317 if priority != *existing.Priority { 318 updated.Priority = &priority 319 changes = true 320 } 321 322 if weight != *existing.Weight { 323 updated.Weight = &weight 324 changes = true 325 } 326 327 if port != *existing.Port { 328 updated.Port = &port 329 changes = true 330 } 331 332 // TODO: handle service & protocol - or does that just result in a 333 // delete and recreate? 334 335 if !changes { 336 return fmt.Errorf("didn't find changes when I expect some") 337 } 338 339 _, err = service.Id(*existing.Id).EditObject(&updated) 340 341 default: 342 service := services.GetDnsDomainResourceRecordService(s.Session) 343 updated := datatypes.Dns_Domain_ResourceRecord{} 344 345 label := desired.GetLabel() 346 if label != *existing.Host { 347 updated.Host = &label 348 changes = true 349 } 350 351 target := desired.GetTargetField() 352 if target != *existing.Data { 353 updated.Data = &target 354 changes = true 355 } 356 357 if ttl != *existing.Ttl { 358 updated.Ttl = &ttl 359 changes = true 360 } 361 362 if !changes { 363 return fmt.Errorf("didn't find changes when I expect some") 364 } 365 366 _, err = service.Id(*existing.Id).EditObject(&updated) 367 } 368 369 return err 370 } 371 }