diff --git a/integrationTest/providers.json b/integrationTest/providers.json index 6711d20fc..9b3bd3e00 100644 --- a/integrationTest/providers.json +++ b/integrationTest/providers.json @@ -2,7 +2,7 @@ "ACTIVEDIRECTORY_PS": { "ADServer": "$AD_SERVER", "domain": "$AD_DOMAIN", - "knownFailures": "20,21,22,29,30,31,32,33,34,35,38,39,40,41,48,49,51,52,53" + "knownFailures": "29,30,31,32,33,34,35,38,39,40,41,48,49,51,52,53" }, "BIND": { "domain": "example.com" diff --git a/providers/activedir/domains.go b/providers/activedir/domains.go index 4cf9375eb..64254fb94 100644 --- a/providers/activedir/domains.go +++ b/providers/activedir/domains.go @@ -29,12 +29,24 @@ func (c *adProvider) GetNameservers(string) ([]*models.Nameserver, error) { return nil, nil } +// list of types this provider supports. +// until it is up to speed with all the built-in types. +var supportedTypes = map[string]bool{ + "A": true, + "AAAA": true, + "CNAME": true, + "NS": true, +} + // GetDomainCorrections gets existing records, diffs them against existing, and returns corrections. func (c *adProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { dc.Filter(func(r *models.RecordConfig) bool { - if r.Type != "A" && r.Type != "CNAME" { - printer.Warnf("Active Directory only manages A and CNAME records. Won't consider %s %s\n", r.Type, r.GetLabelFQDN()) + if r.Type == "NS" && r.Name == "@" { + return false + } + if !supportedTypes[r.Type] { + printer.Warnf("Active Directory only manages certain record types. Won't consider %s %s\n", r.Type, r.GetLabelFQDN()) return false } return true @@ -142,24 +154,38 @@ func (c *adProvider) getExistingRecords(domainname string) ([]*models.RecordConf } var recs []*RecordConfigJson + jdata := string(data) + // when there is only a single record, AD powershell does not + // wrap it in an array as our types expect. This makes sure it is always an array. + if strings.HasPrefix(strings.TrimSpace(jdata), "{") { + jdata = "[" + jdata + "]" + data = []byte(jdata) + } err = json.Unmarshal(data, &recs) if err != nil { return nil, errors.Errorf("json.Unmarshal failed on %#v: %v", domainname, err) } result := make([]*models.RecordConfig, 0, len(recs)) - for i := range recs { - t := recs[i].unpackRecord(domainname) + unsupportedCounts := map[string]int{} + for _, rec := range recs { + t, supportedType := rec.unpackRecord(domainname) + if !supportedType { + unsupportedCounts[rec.Type]++ + } if t != nil { result = append(result, t) } } + for t, count := range unsupportedCounts { + printer.Warnf("%d records of type %s found in AD zone. These will be ignored.\n", count, t) + } return result, nil } -func (r *RecordConfigJson) unpackRecord(origin string) *models.RecordConfig { - rc := models.RecordConfig{ +func (r *RecordConfigJson) unpackRecord(origin string) (rc *models.RecordConfig, supported bool) { + rc = &models.RecordConfig{ Type: r.Type, TTL: r.TTL, } @@ -169,19 +195,24 @@ func (r *RecordConfigJson) unpackRecord(origin string) *models.RecordConfig { rc.SetTarget(r.Data) case "CNAME": rc.SetTarget(strings.ToLower(r.Data)) - case "NS", "SOA": - return nil + case "NS": + // skip root NS + if rc.Name == "@" { + return nil, true + } + rc.SetTarget(strings.ToLower(r.Data)) + case "SOA": + return nil, true default: - printer.Warnf("Record of type %s found in AD zone. Will be ignored.\n", rc.Type) - return nil + return nil, false } - return &rc + return rc, true } // powerShellDump runs a PowerShell command to get a dump of all records in a DNS zone. func (c *adProvider) generatePowerShellZoneDump(domainname string) string { cmdTxt := `@("REPLACE_WITH_ZONE") | %{ -Get-DnsServerResourceRecord -ComputerName REPLACE_WITH_COMPUTER_NAME -ZoneName $_ | select hostname,recordtype,@{n="timestamp";e={$_.timestamp.tostring()}},@{n="timetolive";e={$_.timetolive.totalseconds}},@{n="recorddata";e={($_.recorddata.ipv4address,$_.recorddata.ipv6address,$_.recorddata.HostNameAlias,"other_record" -ne $null)[0]-as [string]}} | ConvertTo-Json > REPLACE_WITH_FILENAMEPREFIX.REPLACE_WITH_ZONE.json +Get-DnsServerResourceRecord -ComputerName REPLACE_WITH_COMPUTER_NAME -ZoneName $_ | select hostname,recordtype,@{n="timestamp";e={$_.timestamp.tostring()}},@{n="timetolive";e={$_.timetolive.totalseconds}},@{n="recorddata";e={($_.recorddata.ipv4address,$_.recorddata.ipv6address,$_.recorddata.HostNameAlias,$_.recorddata.NameServer,"unsupported_record_type" -ne $null)[0]-as [string]}} | ConvertTo-Json > REPLACE_WITH_FILENAMEPREFIX.REPLACE_WITH_ZONE.json }` cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_ZONE", domainname, -1) cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_COMPUTER_NAME", c.adServer, -1) @@ -193,7 +224,11 @@ Get-DnsServerResourceRecord -ComputerName REPLACE_WITH_COMPUTER_NAME -ZoneName $ func (c *adProvider) generatePowerShellCreate(domainname string, rec *models.RecordConfig) string { content := rec.GetTargetField() text := "\r\n" // Skip a line. - text += fmt.Sprintf("Add-DnsServerResourceRecord%s", rec.Type) + funcSuffix := rec.Type + if rec.Type == "NS" { + funcSuffix = "" + } + text += fmt.Sprintf("Add-DnsServerResourceRecord%s", funcSuffix) text += fmt.Sprintf(` -ComputerName "%s"`, c.adServer) text += fmt.Sprintf(` -ZoneName "%s"`, domainname) text += fmt.Sprintf(` -Name "%s"`, rec.GetLabel()) @@ -204,7 +239,7 @@ func (c *adProvider) generatePowerShellCreate(domainname string, rec *models.Rec case "A": text += fmt.Sprintf(` -IPv4Address "%s"`, content) case "NS": - text = fmt.Sprintf("\r\n"+`echo "Skipping NS update (%v %v)"`+"\r\n", rec.GetLabel(), rec.GetTargetDebug()) + text += fmt.Sprintf(` -NS -NameServer "%s"`, content) default: panic(errors.Errorf("generatePowerShellCreate() does not yet handle recType=%s recName=%#v content=%#v)", rec.Type, rec.GetLabel(), content)) @@ -220,14 +255,15 @@ func (c *adProvider) generatePowerShellCreate(domainname string, rec *models.Rec func (c *adProvider) generatePowerShellModify(domainname, recName, recType, oldContent, newContent string, oldTTL, newTTL uint32) string { var queryField, queryContent string + queryContent = `"` + oldContent + `"` switch recType { // #rtype_variations case "A": queryField = "IPv4address" - queryContent = `"` + oldContent + `"` case "CNAME": queryField = "HostNameAlias" - queryContent = `"` + oldContent + `"` + case "NS": + queryField = "NameServer" default: panic(errors.Errorf("generatePowerShellModify() does not yet handle recType=%s recName=%#v content=(%#v, %#v)", recType, recName, oldContent, newContent)) // We panic so that we quickly find any switch statements