1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Testing and fixing AD (#74)

* updates to AD

* fix linux build
This commit is contained in:
Craig Peterson
2017-04-13 10:19:51 -06:00
committed by GitHub
parent c044cca2dd
commit bb1dcac520
6 changed files with 86 additions and 72 deletions

View File

@ -254,16 +254,19 @@ var tests = []*TestCase{
tc("Change back to CNAME", cname("foo", "google.com.")), tc("Change back to CNAME", cname("foo", "google.com.")),
//NS //NS
tc("Empty"),
tc("NS for subdomain", ns("xyz", "ns2.foo.com.")), tc("NS for subdomain", ns("xyz", "ns2.foo.com.")),
tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")), tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")),
//IDNAs //IDNAs
tc("Empty"),
tc("Internationalized name", a("ööö", "1.2.3.4")), tc("Internationalized name", a("ööö", "1.2.3.4")),
tc("Change IDN", a("ööö", "2.2.2.2")), tc("Change IDN", a("ööö", "2.2.2.2")),
tc("Internationalized CNAME Target", cname("a", "ööö.com.")), tc("Internationalized CNAME Target", cname("a", "ööö.com.")),
tc("IDN CNAME AND Target", cname("öoö", "ööö.ööö.")), tc("IDN CNAME AND Target", cname("öoö", "ööö.ööö.")),
//MX //MX
tc("Empty"),
tc("MX record", mx("@", 5, "foo.com.")), tc("MX record", mx("@", 5, "foo.com.")),
tc("Second MX record, same prio", mx("@", 5, "foo.com."), mx("@", 5, "foo2.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.")), tc("3 MX", mx("@", 5, "foo.com."), mx("@", 5, "foo2.com."), mx("@", 15, "foo3.com.")),

View File

@ -4,7 +4,7 @@
}, },
"DNSIMPLE": { "DNSIMPLE": {
//16/17: no ns records managable. Not even for subdomains. //16/17: no ns records managable. Not even for subdomains.
"knownFailures": "16,17", "knownFailures": "17,18",
"domain": "$DNSIMPLE_DOMAIN", "domain": "$DNSIMPLE_DOMAIN",
"token": "$DNSIMPLE_TOKEN", "token": "$DNSIMPLE_TOKEN",
"baseurl": "https://api.sandbox.dnsimple.com" "baseurl": "https://api.sandbox.dnsimple.com"
@ -25,5 +25,10 @@
"domain": "$R53_DOMAIN", "domain": "$R53_DOMAIN",
"KeyId": "$R53_KEY_ID", "KeyId": "$R53_KEY_ID",
"SecretKey": "$R53_KEY" "SecretKey": "$R53_KEY"
},
"ACTIVEDIRECTORY_PS":{
"knownFailures": "17,18,19,25,26,27,28,29,30",
"domain": "$AD_DOMAIN",
"ADServer": "$AD_SERVER"
} }
} }

View File

@ -224,6 +224,16 @@ func (dc *DomainConfig) HasRecordTypeName(rtype, name string) bool {
return false 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) { func InterfaceToIP(i interface{}) (net.IP, error) {
switch v := i.(type) { switch v := i.(type) {
case float64: case float64:

View File

@ -32,6 +32,14 @@ func (c *adProvider) GetNameservers(string) ([]*models.Nameserver, error) {
// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections. // GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
func (c *adProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 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: // Read foundRecords:
foundRecords, err := c.getExistingRecords(dc.Name) foundRecords, err := c.getExistingRecords(dc.Name)
if err != nil { 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. // 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))) 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. // 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)) return logHelper(fmt.Sprintf("OUTPUT: START\r\n%s\r\nOUTPUT: END\r\n", s))
} }
// powerShellLogErr logs that a PowerShell command had an error. // 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 err := logHelper(fmt.Sprintf("ERROR: %v\r\r", e)) //Log error to powershell.log
if err != nil { if err != nil {
return err //Bubble up error created in logHelper return err //Bubble up error created in logHelper
@ -101,14 +109,14 @@ func powerShellLogErr(e error) error {
func logHelper(s string) error { func logHelper(s string) error {
logfile, err := os.OpenFile(*flagPsLog, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0660) logfile, err := os.OpenFile(*flagPsLog, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0660)
if err != nil { 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) _, err = fmt.Fprintln(logfile, s)
if err != nil { 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 { 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 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. // Get the JSON either from adzonedump or by running a PowerShell script.
data, err := c.getRecords(domainname) data, err := c.getRecords(domainname)
if err != nil { 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 var recs []*RecordConfigJson
err = json.Unmarshal(data, &recs) err = json.Unmarshal(data, &recs)
if err != nil { 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)) result := make([]*models.RecordConfig, 0, len(recs))
for i := range recs { for i := range recs {
t, err := recs[i].unpackRecord(domainname) t := recs[i].unpackRecord(domainname)
if err == nil { if t != nil {
result = append(result, t) result = append(result, t)
} }
} }
@ -152,7 +160,7 @@ func (c *adProvider) getExistingRecords(domainname string) ([]*models.RecordConf
return result, nil return result, nil
} }
func (r *RecordConfigJson) unpackRecord(origin string) (*models.RecordConfig, error) { func (r *RecordConfigJson) unpackRecord(origin string) *models.RecordConfig {
rc := models.RecordConfig{} rc := models.RecordConfig{}
rc.Name = strings.ToLower(r.Name) rc.Name = strings.ToLower(r.Name)
@ -165,37 +173,36 @@ func (r *RecordConfigJson) unpackRecord(origin string) (*models.RecordConfig, er
rc.Target = r.Data rc.Target = r.Data
case "CNAME": case "CNAME":
rc.Target = strings.ToLower(r.Data) rc.Target = strings.ToLower(r.Data)
case "AAAA", "MX", "NAPTR", "NS", "SOA", "SRV": case "NS", "SOA":
return nil, fmt.Errorf("Unimplemented: %v", r.Type) return nil
default: 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
return &rc, nil
} }
// powerShellDump runs a PowerShell command to get a dump of all records in a DNS zone. // powerShellDump runs a PowerShell command to get a dump of all records in a DNS zone.
func (c *adProvider) generatePowerShellZoneDump(domainname string) string { 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 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) cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_ZONE", domainname, -1)
cmd_txt = strings.Replace(cmd_txt, "REPLACE_WITH_COMPUTER_NAME", c.adServer, -1) cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_COMPUTER_NAME", c.adServer, -1)
cmd_txt = strings.Replace(cmd_txt, "REPLACE_WITH_FILENAMEPREFIX", zoneDumpFilenamePrefix, -1) cmdTxt = strings.Replace(cmdTxt, "REPLACE_WITH_FILENAMEPREFIX", zoneDumpFilenamePrefix, -1)
return cmd_txt return cmdTxt
} }
// generatePowerShellCreate generates PowerShell commands to ADD a record. // generatePowerShellCreate generates PowerShell commands to ADD a record.
func (c *adProvider) generatePowerShellCreate(domainname string, rec *models.RecordConfig) string { func (c *adProvider) generatePowerShellCreate(domainname string, rec *models.RecordConfig) string {
content := rec.Target content := rec.Target
text := "\r\n" // Skip a line. text := "\r\n" // Skip a line.
text += fmt.Sprintf("Add-DnsServerResourceRecord%s", rec.Type) text += fmt.Sprintf("Add-DnsServerResourceRecord%s", rec.Type)
text += fmt.Sprintf(` -ComputerName "%s"`, c.adServer) text += fmt.Sprintf(` -ComputerName "%s"`, c.adServer)
text += fmt.Sprintf(` -ZoneName "%s"`, domainname) text += fmt.Sprintf(` -ZoneName "%s"`, domainname)
text += fmt.Sprintf(` -Name "%s"`, rec.Name) text += fmt.Sprintf(` -Name "%s"`, rec.Name)
text += fmt.Sprintf(` -TimeToLive $(New-TimeSpan -Seconds %d)`, rec.TTL)
switch rec.Type { switch rec.Type {
case "CNAME": case "CNAME":
text += fmt.Sprintf(` -HostNameAlias "%s"`, content) 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), Msg: fmt.Sprintf("CREATE record: %s %s ttl(%d) %s", rec.Name, rec.Type, rec.TTL, rec.Target),
F: func() error { F: func() error {
return powerShellDoCommand(c.generatePowerShellCreate(domainname, rec)) return powerShellDoCommand(c.generatePowerShellCreate(domainname, rec), true)
}}, }},
} }
return arr return arr
@ -287,7 +294,7 @@ func (c *adProvider) modifyRec(domainname string, m diff.Correlation) *models.Co
return &models.Correction{ return &models.Correction{
Msg: m.String(), Msg: m.String(),
F: func() error { 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{ return &models.Correction{
Msg: fmt.Sprintf("DELETE record: %s %s ttl(%d) %s", rec.Name, rec.Type, rec.TTL, rec.Target), Msg: fmt.Sprintf("DELETE record: %s %s ttl(%d) %s", rec.Name, rec.Type, rec.TTL, rec.Target),
F: func() error { 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)
}, },
} }
} }

View File

@ -9,7 +9,7 @@ func (c *adProvider) getRecords(domainname string) ([]byte, error) {
return c.readZoneDump(domainname) return c.readZoneDump(domainname)
} }
func powerShellDoCommand(command string) error { func powerShellDoCommand(command string, shouldLog bool) error {
if !*flagFakePowerShell { if !*flagFakePowerShell {
panic("Can not happen: PowerShell on non-windows") panic("Can not happen: PowerShell on non-windows")
} }

View File

@ -5,36 +5,44 @@ import (
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"sync"
) )
var checkPS sync.Once
var psAvailible = false
func (c *adProvider) getRecords(domainname string) ([]byte, error) { func (c *adProvider) getRecords(domainname string) ([]byte, error) {
if !*flagFakePowerShell { // If we are using PowerShell, make sure it is enabled
// If we are using PowerShell, make sure it is enabled // and then run the PS1 command to generate the adzonedump file.
// and then run the PS1 command to generate the adzonedump file.
if !isPowerShellReady() { if !*flagFakePowerShell {
fmt.Printf("\n\n\n") checkPS.Do(func() {
fmt.Printf("***********************************************\n") psAvailible = isPowerShellReady()
fmt.Printf("PowerShell DnsServer module not installed.\n") if !psAvailible {
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\n\n")
fmt.Printf("***********************************************\n") fmt.Printf("***********************************************\n")
fmt.Printf("\n\n\n") fmt.Printf("PowerShell DnsServer module not installed.\n")
return nil, fmt.Errorf("PowerShell module DnsServer not installed.") 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 { if err != nil {
return []byte{}, err return []byte{}, err
} }
} }
// Return the contents of zone.*.json file instead. // Return the contents of zone.*.json file instead.
return c.readZoneDump(domainname) return c.readZoneDump(domainname)
} }
func isPowerShellReady() bool { 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))) q, err := strconv.ParseBool(strings.TrimSpace(string(query)))
if err != nil { if err != nil {
return false return false
@ -42,52 +50,33 @@ func isPowerShellReady() bool {
return q return q
} }
func powerShellDoCommand(command string) error { func powerShellDoCommand(command string, shouldLog bool) error {
if *flagFakePowerShell { if *flagFakePowerShell {
// If fake, just record the command. // If fake, just record the command.
return powerShellRecord(command) return powerShellRecord(command)
} }
_, err := powerShellExec(command) _, err := powerShellExec(command, shouldLog)
return err return err
} }
func powerShellExec(command string) ([]byte, error) { func powerShellExec(command string, shouldLog bool) ([]byte, error) {
// log it. // log it.
err := powerShellLogCommand(command) err := logCommand(command)
if err != nil { if err != nil {
return []byte{}, err return nil, err
} }
// Run it. // Run it.
out, err := exec.Command("powershell", "-NoProfile", command).CombinedOutput() out, err := exec.Command("powershell", "-NoProfile", command).CombinedOutput()
if err != nil { if err != nil {
// If there was an error, log it. // If there was an error, log it.
powerShellLogErr(err) logErr(err)
} }
// Return the result. if shouldLog {
return out, err err = logOutput(string(out))
} if err != nil {
return []byte{}, 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
} }
// Return the result. // Return the result.