From bb1dcac520d97083a90413f977027e4010976f7b Mon Sep 17 00:00:00 2001 From: Craig Peterson Date: Thu, 13 Apr 2017 10:19:51 -0600 Subject: [PATCH] Testing and fixing AD (#74) * updates to AD * fix linux build --- integrationTest/integration_test.go | 3 + integrationTest/providers.json | 7 ++- models/dns.go | 10 ++++ providers/activedir/domains.go | 59 ++++++++++--------- providers/activedir/getzones_other.go | 2 +- providers/activedir/getzones_windows.go | 77 +++++++++++-------------- 6 files changed, 86 insertions(+), 72 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index e6c34ad04..f90539f3c 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -254,16 +254,19 @@ var tests = []*TestCase{ tc("Change back to CNAME", cname("foo", "google.com.")), //NS + tc("Empty"), tc("NS for subdomain", ns("xyz", "ns2.foo.com.")), tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")), //IDNAs + tc("Empty"), tc("Internationalized name", a("ööö", "1.2.3.4")), tc("Change IDN", a("ööö", "2.2.2.2")), tc("Internationalized CNAME Target", cname("a", "ööö.com.")), tc("IDN CNAME AND Target", cname("öoö", "ööö.ööö.")), //MX + tc("Empty"), tc("MX record", mx("@", 5, "foo.com.")), tc("Second MX record, same prio", mx("@", 5, "foo.com."), mx("@", 5, "foo2.com.")), tc("3 MX", mx("@", 5, "foo.com."), mx("@", 5, "foo2.com."), mx("@", 15, "foo3.com.")), diff --git a/integrationTest/providers.json b/integrationTest/providers.json index 43bcf5194..448ea5938 100644 --- a/integrationTest/providers.json +++ b/integrationTest/providers.json @@ -4,7 +4,7 @@ }, "DNSIMPLE": { //16/17: no ns records managable. Not even for subdomains. - "knownFailures": "16,17", + "knownFailures": "17,18", "domain": "$DNSIMPLE_DOMAIN", "token": "$DNSIMPLE_TOKEN", "baseurl": "https://api.sandbox.dnsimple.com" @@ -25,5 +25,10 @@ "domain": "$R53_DOMAIN", "KeyId": "$R53_KEY_ID", "SecretKey": "$R53_KEY" + }, + "ACTIVEDIRECTORY_PS":{ + "knownFailures": "17,18,19,25,26,27,28,29,30", + "domain": "$AD_DOMAIN", + "ADServer": "$AD_SERVER" } } \ No newline at end of file diff --git a/models/dns.go b/models/dns.go index 79d1604c3..47c0bbb7b 100644 --- a/models/dns.go +++ b/models/dns.go @@ -224,6 +224,16 @@ func (dc *DomainConfig) HasRecordTypeName(rtype, name string) bool { return false } +func (dc *DomainConfig) Filter(f func(r *RecordConfig) bool) { + recs := []*RecordConfig{} + for _, r := range dc.Records { + if f(r) { + recs = append(recs, r) + } + } + dc.Records = recs +} + func InterfaceToIP(i interface{}) (net.IP, error) { switch v := i.(type) { case float64: diff --git a/providers/activedir/domains.go b/providers/activedir/domains.go index ff19c0645..29416bf35 100644 --- a/providers/activedir/domains.go +++ b/providers/activedir/domains.go @@ -32,6 +32,14 @@ func (c *adProvider) GetNameservers(string) ([]*models.Nameserver, error) { // 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" { + log.Printf("WARNING: Active Directory only manages A and CNAME records. Won't consider %s %s", r.Type, r.NameFQDN) + return false + } + return true + }) + // Read foundRecords: foundRecords, err := c.getExistingRecords(dc.Name) if err != nil { @@ -80,17 +88,17 @@ func (c *adProvider) readZoneDump(domainname string) ([]byte, error) { } // powerShellLogCommand logs to flagPsLog that a PowerShell command is going to be run. -func powerShellLogCommand(command string) error { +func logCommand(command string) error { return logHelper(fmt.Sprintf("# %s\r\n%s\r\n", time.Now().UTC(), strings.TrimSpace(command))) } // powerShellLogOutput logs to flagPsLog that a PowerShell command is going to be run. -func powerShellLogOutput(s string) error { +func logOutput(s string) error { return logHelper(fmt.Sprintf("OUTPUT: START\r\n%s\r\nOUTPUT: END\r\n", s)) } // powerShellLogErr logs that a PowerShell command had an error. -func powerShellLogErr(e error) error { +func logErr(e error) error { err := logHelper(fmt.Sprintf("ERROR: %v\r\r", e)) //Log error to powershell.log if err != nil { return err //Bubble up error created in logHelper @@ -101,14 +109,14 @@ func powerShellLogErr(e error) error { func logHelper(s string) error { logfile, err := os.OpenFile(*flagPsLog, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0660) if err != nil { - return fmt.Errorf("ERROR: Can not create/append to %#v: %v\n", *flagPsLog, err) + return fmt.Errorf("error: Can not create/append to %#v: %v", *flagPsLog, err) } _, err = fmt.Fprintln(logfile, s) if err != nil { - return fmt.Errorf("ERROR: Append to %#v failed: %v\n", *flagPsLog, err) + return fmt.Errorf("error: Append to %#v failed: %v", *flagPsLog, err) } if logfile.Close() != nil { - return fmt.Errorf("ERROR: Closing %#v failed: %v\n", *flagPsLog, err) + return fmt.Errorf("ERROR: Closing %#v failed: %v", *flagPsLog, err) } return nil } @@ -132,19 +140,19 @@ func (c *adProvider) getExistingRecords(domainname string) ([]*models.RecordConf // Get the JSON either from adzonedump or by running a PowerShell script. data, err := c.getRecords(domainname) if err != nil { - return nil, fmt.Errorf("getRecords failed on %#v: %v\n", domainname, err) + return nil, fmt.Errorf("getRecords failed on %#v: %v", domainname, err) } var recs []*RecordConfigJson err = json.Unmarshal(data, &recs) if err != nil { - return nil, fmt.Errorf("json.Unmarshal failed on %#v: %v\n", domainname, err) + return nil, fmt.Errorf("json.Unmarshal failed on %#v: %v", domainname, err) } result := make([]*models.RecordConfig, 0, len(recs)) for i := range recs { - t, err := recs[i].unpackRecord(domainname) - if err == nil { + t := recs[i].unpackRecord(domainname) + if t != nil { result = append(result, t) } } @@ -152,7 +160,7 @@ func (c *adProvider) getExistingRecords(domainname string) ([]*models.RecordConf return result, nil } -func (r *RecordConfigJson) unpackRecord(origin string) (*models.RecordConfig, error) { +func (r *RecordConfigJson) unpackRecord(origin string) *models.RecordConfig { rc := models.RecordConfig{} rc.Name = strings.ToLower(r.Name) @@ -165,37 +173,36 @@ func (r *RecordConfigJson) unpackRecord(origin string) (*models.RecordConfig, er rc.Target = r.Data case "CNAME": rc.Target = strings.ToLower(r.Data) - case "AAAA", "MX", "NAPTR", "NS", "SOA", "SRV": - return nil, fmt.Errorf("Unimplemented: %v", r.Type) + case "NS", "SOA": + return nil default: - log.Fatalf("Unhandled models.RecordConfigJson type: %v (%v)\n", rc.Type, r) + log.Printf("Warning: Record of type %s found in AD zone. Will be ignored.", rc.Type) + return nil } - - return &rc, nil + return &rc } // powerShellDump runs a PowerShell command to get a dump of all records in a DNS zone. func (c *adProvider) generatePowerShellZoneDump(domainname string) string { - cmd_txt := `@("REPLACE_WITH_ZONE") | %{ + 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 }` - cmd_txt = strings.Replace(cmd_txt, "REPLACE_WITH_ZONE", domainname, -1) - cmd_txt = strings.Replace(cmd_txt, "REPLACE_WITH_COMPUTER_NAME", c.adServer, -1) - cmd_txt = strings.Replace(cmd_txt, "REPLACE_WITH_FILENAMEPREFIX", zoneDumpFilenamePrefix, -1) + cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_ZONE", domainname, -1) + cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_COMPUTER_NAME", c.adServer, -1) + cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_FILENAMEPREFIX", zoneDumpFilenamePrefix, -1) - return cmd_txt + return cmdTxt } // generatePowerShellCreate generates PowerShell commands to ADD a record. func (c *adProvider) generatePowerShellCreate(domainname string, rec *models.RecordConfig) string { - content := rec.Target - text := "\r\n" // Skip a line. text += fmt.Sprintf("Add-DnsServerResourceRecord%s", rec.Type) text += fmt.Sprintf(` -ComputerName "%s"`, c.adServer) text += fmt.Sprintf(` -ZoneName "%s"`, domainname) text += fmt.Sprintf(` -Name "%s"`, rec.Name) + text += fmt.Sprintf(` -TimeToLive $(New-TimeSpan -Seconds %d)`, rec.TTL) switch rec.Type { case "CNAME": text += fmt.Sprintf(` -HostNameAlias "%s"`, content) @@ -276,7 +283,7 @@ func (c *adProvider) createRec(domainname string, rec *models.RecordConfig) []*m { Msg: fmt.Sprintf("CREATE record: %s %s ttl(%d) %s", rec.Name, rec.Type, rec.TTL, rec.Target), F: func() error { - return powerShellDoCommand(c.generatePowerShellCreate(domainname, rec)) + return powerShellDoCommand(c.generatePowerShellCreate(domainname, rec), true) }}, } return arr @@ -287,7 +294,7 @@ func (c *adProvider) modifyRec(domainname string, m diff.Correlation) *models.Co return &models.Correction{ Msg: m.String(), F: func() error { - return powerShellDoCommand(c.generatePowerShellModify(domainname, rec.Name, rec.Type, old.Target, rec.Target, old.TTL, rec.TTL)) + return powerShellDoCommand(c.generatePowerShellModify(domainname, rec.Name, rec.Type, old.Target, rec.Target, old.TTL, rec.TTL), true) }, } } @@ -296,7 +303,7 @@ func (c *adProvider) deleteRec(domainname string, rec *models.RecordConfig) *mod return &models.Correction{ Msg: fmt.Sprintf("DELETE record: %s %s ttl(%d) %s", rec.Name, rec.Type, rec.TTL, rec.Target), F: func() error { - return powerShellDoCommand(c.generatePowerShellDelete(domainname, rec.Name, rec.Type, rec.Target)) + return powerShellDoCommand(c.generatePowerShellDelete(domainname, rec.Name, rec.Type, rec.Target), true) }, } } diff --git a/providers/activedir/getzones_other.go b/providers/activedir/getzones_other.go index ac39fd19c..de7ccac09 100644 --- a/providers/activedir/getzones_other.go +++ b/providers/activedir/getzones_other.go @@ -9,7 +9,7 @@ func (c *adProvider) getRecords(domainname string) ([]byte, error) { return c.readZoneDump(domainname) } -func powerShellDoCommand(command string) error { +func powerShellDoCommand(command string, shouldLog bool) error { if !*flagFakePowerShell { panic("Can not happen: PowerShell on non-windows") } diff --git a/providers/activedir/getzones_windows.go b/providers/activedir/getzones_windows.go index 893c3a6be..4fe8d9d74 100644 --- a/providers/activedir/getzones_windows.go +++ b/providers/activedir/getzones_windows.go @@ -5,36 +5,44 @@ import ( "os/exec" "strconv" "strings" + "sync" ) +var checkPS sync.Once +var psAvailible = false + func (c *adProvider) getRecords(domainname string) ([]byte, error) { - if !*flagFakePowerShell { - // If we are using PowerShell, make sure it is enabled - // and then run the PS1 command to generate the adzonedump file. + // If we are using PowerShell, make sure it is enabled + // and then run the PS1 command to generate the adzonedump file. - if !isPowerShellReady() { - fmt.Printf("\n\n\n") - fmt.Printf("***********************************************\n") - fmt.Printf("PowerShell DnsServer module not installed.\n") - fmt.Printf("See http://social.technet.microsoft.com/wiki/contents/articles/2202.remote-server-administration-tools-rsat-for-windows-client-and-windows-server-dsforum2wiki.aspx\n") - fmt.Printf("***********************************************\n") - fmt.Printf("\n\n\n") - return nil, fmt.Errorf("PowerShell module DnsServer not installed.") + if !*flagFakePowerShell { + checkPS.Do(func() { + psAvailible = isPowerShellReady() + if !psAvailible { + fmt.Printf("\n\n\n") + fmt.Printf("***********************************************\n") + fmt.Printf("PowerShell DnsServer module not installed.\n") + fmt.Printf("See http://social.technet.microsoft.com/wiki/contents/articles/2202.remote-server-administration-tools-rsat-for-windows-client-and-windows-server-dsforum2wiki.aspx\n") + fmt.Printf("***********************************************\n") + fmt.Printf("\n\n\n") + } + }) + if !psAvailible { + return nil, fmt.Errorf("powershell module DnsServer not installed") } - _, err := powerShellExecCombined(c.generatePowerShellZoneDump(domainname)) + _, err := powerShellExec(c.generatePowerShellZoneDump(domainname), true) if err != nil { return []byte{}, err } } - // Return the contents of zone.*.json file instead. return c.readZoneDump(domainname) } func isPowerShellReady() bool { - query, _ := powerShellExec(`(Get-Module -ListAvailable DnsServer) -ne $null`) + query, _ := powerShellExec(`(Get-Module -ListAvailable DnsServer) -ne $null`, true) q, err := strconv.ParseBool(strings.TrimSpace(string(query))) if err != nil { return false @@ -42,52 +50,33 @@ func isPowerShellReady() bool { return q } -func powerShellDoCommand(command string) error { +func powerShellDoCommand(command string, shouldLog bool) error { if *flagFakePowerShell { // If fake, just record the command. return powerShellRecord(command) } - _, err := powerShellExec(command) + _, err := powerShellExec(command, shouldLog) return err } -func powerShellExec(command string) ([]byte, error) { +func powerShellExec(command string, shouldLog bool) ([]byte, error) { // log it. - err := powerShellLogCommand(command) + err := logCommand(command) if err != nil { - return []byte{}, err + return nil, err } // Run it. out, err := exec.Command("powershell", "-NoProfile", command).CombinedOutput() if err != nil { // If there was an error, log it. - powerShellLogErr(err) + logErr(err) } - // Return the result. - return out, err -} - -// powerShellExecCombined runs a PS1 command and logs the output. This is useful when the output should be none or very small. -func powerShellExecCombined(command string) ([]byte, error) { - // log it. - err := powerShellLogCommand(command) - if err != nil { - return []byte{}, err - } - - // Run it. - out, err := exec.Command("powershell", "-NoProfile", command).CombinedOutput() - if err != nil { - // If there was an error, log it. - powerShellLogErr(err) - return out, err - } - - // Log output. - err = powerShellLogOutput(string(out)) - if err != nil { - return []byte{}, err + if shouldLog { + err = logOutput(string(out)) + if err != nil { + return []byte{}, err + } } // Return the result.