mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-12-25 09:12:31 +08:00
parent
c044cca2dd
commit
bb1dcac520
6 changed files with 86 additions and 72 deletions
|
@ -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.")),
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue