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.")),
//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.")),

View file

@ -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"
}
}

View file

@ -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:

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.
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)
},
}
}

View file

@ -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")
}

View file

@ -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.