Update bind docs and Getting Started (#53)

* intitial work for bind refactor

* relax requirement that creds.json exists.

* Updating bind docs.
Fixes #48

* typo

* can exclude provider from default set in creds.json

* Add bind to integration tests. Fix for IDNs.
This commit is contained in:
Craig Peterson 2017-03-22 10:38:08 -06:00 committed by GitHub
parent 33b4d2b748
commit 854c84e652
8 changed files with 98 additions and 84 deletions

View file

@ -6,4 +6,31 @@ jsId: BIND
# Bind Provider # Bind Provider
This provider simply maintains a directory with a collection of .zone files. We currently copy zone files to our production servers and restart bind via This provider simply maintains a directory with a collection of .zone files. We currently copy zone files to our production servers and restart bind via
a script external to DNSControl. a script external to DNSControl.
## Configuration
The BIND provider does not require anything in `creds.json`. It does accept some (optional) metadata via your dns config when you create the provider:
{% highlight javascript %}
var bind = NewDnsProvider('bind', 'BIND', {
'default_soa': {
'master': 'ns1.mydomain.com.',
'mbox': 'sysadmin.mydomain.com.',
'refresh': 3600,
'retry': 600,
'expire': 604800,
'minttl': 1440,
},
'default_ns': [
'ns1.mydomain.com.',
'ns2.mydomain.com.',
'ns3.mydomain.com.',
'ns4.mydomain.com.'
]
})
{% endhighlight %}
If you need to customize your SOA or NS records, you can do it with this setup.
You can also provide a `-bindtree=directoryName` flag to change where the provider will look for and create zone files. The default is the `zones` directory where dnscontrol is run.

View file

@ -13,30 +13,19 @@ You can either download the latest [github release](https://github.com/StackExch
The first file you will need is a javascript file to describe your domains. The first file you will need is a javascript file to describe your domains.
Individual providers will vary slightly. See [the provider docs]({{site.github.url}}/provider-list) for specifics. Individual providers will vary slightly. See [the provider docs]({{site.github.url}}/provider-list) for specifics.
For this example we will use a domain registered with name.com, using their basic dns hosting. For this example we will use a single "BIND" provider that will generate zone files on our local file system.
The default name is `dnsconfig.js`: The default name is `dnsconfig.js`:
{% highlight js %} {% highlight js %}
var registrar = NewRegistrar("name.com",NAMEDOTCOM); var registrar = NewRegistrar("none","NONE"); // no registrar
var namecom = NewDnsProvider("name.com",NAMEDOTCOM); var bind = NewDnsProvider("bind","BIND");
D("example.com", registrar, DnsProvider(namecom), D("example.com", registrar, DnsProvider(bind),
A("@", "1.2.3.4") A("@", "1.2.3.4")
); );
{%endhighlight%} {%endhighlight%}
The second file is a json document to hold your api credentials. By default we use `providers.json`: You may modify this files to match your particular providers and domains. See [the javascript docs]({{site.github.url}}/js) and [the provider docs]({{site.github.url}}/provider-list) for more details. If you are using other providers, you will likely need to make a `creds.json` file with api tokens and other account information.
{% highlight json %}
{
"name.com":{
"apikey": "yourApiKeyFromName.com-klasjdkljasdlk235235235235",
"apiuser": "yourUsername"
}
}
{%endhighlight%}
You may modify these files to match your particular providers and domains. See [the javascript docs]({{site.github.url}}/js) for more details.
## 3. Run `dnscontrol preview` ## 3. Run `dnscontrol preview`
@ -44,4 +33,4 @@ This will print out a list of "corrections" that need to be performed. It will n
## 4. Run `dnscontrol push` ## 4. Run `dnscontrol push`
This will actually perform the required changes with the various providers. This will actually generate `zones/example.com.zone` for you. The bind provider is more configurable, and you can read more information [here.]({{site.github.url}}/providers/bind)

View file

@ -9,5 +9,8 @@
"domain": "$DNSIMPLE_DOMAIN", "domain": "$DNSIMPLE_DOMAIN",
"token": "$DNSIMPLE_TOKEN", "token": "$DNSIMPLE_TOKEN",
"baseurl": "https://api.sandbox.dnsimple.com" "baseurl": "https://api.sandbox.dnsimple.com"
},
"BIND":{
"domain": "example.com"
} }
} }

View file

@ -0,0 +1,4 @@
$TTL 300
@ IN SOA DEFAULT_NOT_SET. DEFAULT_NOT_SET. 2017032059 3600 600 604800 1440
IN NS foo.com.
xn--o-0gab IN CNAME xn--ndaaa.xn--ndaaa.

47
main.go
View file

@ -23,15 +23,12 @@ import (
//go:generate go run build/generate/generate.go //go:generate go run build/generate/generate.go
// One of these config options must be set.
var jsFile = flag.String("js", "dnsconfig.js", "Javascript file containing dns config") var jsFile = flag.String("js", "dnsconfig.js", "Javascript file containing dns config")
var stdin = flag.Bool("stdin", false, "Read domain config JSON from stdin") var credsFile = flag.String("creds", "creds.json", "Provider credentials JSON file")
var jsonInput = flag.String("json", "", "Read domain config from specified JSON file.")
var jsonOutputPre = flag.String("debugrawjson", "", "Write JSON intermediate to this file pre-normalization.") var jsonOutputPre = flag.String("debugrawjson", "", "Write JSON intermediate to this file pre-normalization.")
var jsonOutputPost = flag.String("debugjson", "", "During preview, write JSON intermediate to this file instead of stdout.") var jsonOutputPost = flag.String("debugjson", "", "During preview, write JSON intermediate to this file instead of stdout.")
var configFile = flag.String("creds", "creds.json", "Provider credentials JSON file")
var devMode = flag.Bool("dev", false, "Use helpers.js from disk instead of embedded") var devMode = flag.Bool("dev", false, "Use helpers.js from disk instead of embedded")
var flagProviders = flag.String("providers", "", "Providers to enable (comma seperated list); default is all-but-bind. Specify 'all' for all (including bind)") var flagProviders = flag.String("providers", "", "Providers to enable (comma seperated list); default is all-but-bind. Specify 'all' for all (including bind)")
@ -49,11 +46,7 @@ func main() {
} }
var dnsConfig *models.DNSConfig var dnsConfig *models.DNSConfig
if *stdin { if *jsFile != "" {
log.Fatal("Read from stdin not implemented yet.")
} else if *jsonInput != "" {
log.Fatal("Direct JSON read not implemented")
} else if *jsFile != "" {
text, err := ioutil.ReadFile(*jsFile) text, err := ioutil.ReadFile(*jsFile)
if err != nil { if err != nil {
log.Fatalf("Error reading %v: %v\n", *jsFile, err) log.Fatalf("Error reading %v: %v\n", *jsFile, err)
@ -117,9 +110,17 @@ func main() {
return return
} }
providerConfigs, err := config.LoadProviderConfigs(*configFile) providerConfigs, err := config.LoadProviderConfigs(*credsFile)
if err != nil { if err != nil {
log.Fatalf("error loading provider configurations: %s", err) log.Fatalf(err.Error())
}
nonDefaultProviders := []string{}
for name, vals := range providerConfigs {
// add "_exclude_from_defaults":"true" to a domain to exclude it from being run unless
// -providers=all or -providers=name
if vals["_exclude_from_defaults"] == "true" {
nonDefaultProviders = append(nonDefaultProviders, name)
}
} }
registrars, err := providers.CreateRegistrars(dnsConfig, providerConfigs) registrars, err := providers.CreateRegistrars(dnsConfig, providerConfigs)
if err != nil { if err != nil {
@ -168,7 +169,7 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
shouldrun := shouldRunProvider(prov, dc) shouldrun := shouldRunProvider(prov, dc, nonDefaultProviders)
statusLbl := "" statusLbl := ""
if !shouldrun { if !shouldrun {
statusLbl = "(skipping)" statusLbl = "(skipping)"
@ -197,7 +198,7 @@ func main() {
fmt.Printf("%d correction%s\n", len(corrections), plural) fmt.Printf("%d correction%s\n", len(corrections), plural)
anyErrors = printOrRunCorrections(corrections, command) || anyErrors anyErrors = printOrRunCorrections(corrections, command) || anyErrors
} }
if run := shouldRunProvider(domain.Registrar, domain); !run { if run := shouldRunProvider(domain.Registrar, domain, nonDefaultProviders); !run {
continue continue
} }
fmt.Printf("----- Registrar: %s\n", domain.Registrar) fmt.Printf("----- Registrar: %s\n", domain.Registrar)
@ -272,23 +273,17 @@ func printOrRunCorrections(corrections []*models.Correction, command string) (an
return anyErrors return anyErrors
} }
func shouldRunProvider(p string, dc *models.DomainConfig) bool { func shouldRunProvider(p string, dc *models.DomainConfig, nonDefaultProviders []string) bool {
if *flagProviders == "all" { if *flagProviders == "all" {
return true return true
} }
if *flagProviders == "" { if *flagProviders == "" {
return (p != "bind") for _, pr := range nonDefaultProviders {
// NOTE(tlim): Hardcoding bind is a hacky way to make it off by default. if pr == p {
// As a result, bind only runs if you list it in -providers or use return false
// -providers=all. }
// If you always want bind to run, call it something else in dnsconfig.js }
// for example `NewDSP('bindyes', 'BIND',`. return true
// We don't want this hack, but we shouldn't need this in the future
// so it doesn't make sense to write a lot of code to make it work.
// In the future, the above `return p != "bind"` can become `return true`.
// Alternatively we might want to add a complex system that permits
// fancy whitelist/blacklisting of providers with defaults and so on.
// In that case, all of this hack will go away.
} }
for _, prov := range strings.Split(*flagProviders, ",") { for _, prov := range strings.Split(*flagProviders, ",") {
if prov == p { if prov == p {

View file

@ -11,10 +11,6 @@ bind -
if an update is actually needed. The old zonefile is also used if an update is actually needed. The old zonefile is also used
as the basis for generating the new SOA serial number. as the basis for generating the new SOA serial number.
If -bind_skeletin_src and -bind_skeletin_dst is defined, a
recursive file copy is performed from src to dst. This is
useful for copying named.ca and other static files.
*/ */
import ( import (
@ -50,20 +46,20 @@ func (s SoaInfo) String() string {
} }
type Bind struct { type Bind struct {
Default_NS []string `json:"default_ns"` DefaultNS []string `json:"default_ns"`
Default_Soa SoaInfo `json:"default_soa"` DefaultSoa SoaInfo `json:"default_soa"`
nameservers []*models.Nameserver `json:"-"` nameservers []*models.Nameserver
} }
var bindBaseDir = flag.String("bindtree", "zones", "BIND: Directory that stores BIND zonefiles.") var bindBaseDir = flag.String("bindtree", "zones", "BIND: Directory that stores BIND zonefiles.")
//var bindSkeletin = flag.String("bind_skeletin", "skeletin/master/var/named/chroot/var/named/master", "") //var bindSkeletin = flag.String("bind_skeletin", "skeletin/master/var/named/chroot/var/named/master", "")
func rrToRecord(rr dns.RR, origin string, replace_serial uint32) (models.RecordConfig, uint32) { func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordConfig, uint32) {
// Convert's dns.RR into our native data type (models.RecordConfig). // Convert's dns.RR into our native data type (models.RecordConfig).
// Records are translated directly with no changes. // Records are translated directly with no changes.
// If it is an SOA for the apex domain and // If it is an SOA for the apex domain and
// replace_serial != 0, change the serial to replace_serial. // replaceSerial != 0, change the serial to replaceSerial.
// WARNING(tlim): This assumes SOAs do not have serial=0. // WARNING(tlim): This assumes SOAs do not have serial=0.
// If one is found, we replace it with serial=1. // If one is found, we replace it with serial=1.
var old_serial, new_serial uint32 var old_serial, new_serial uint32
@ -92,8 +88,8 @@ func rrToRecord(rr dns.RR, origin string, replace_serial uint32) (models.RecordC
old_serial = 1 old_serial = 1
} }
new_serial = v.Serial new_serial = v.Serial
if rc.Name == "@" && replace_serial != 0 { if rc.Name == "@" && replaceSerial != 0 {
new_serial = replace_serial new_serial = replaceSerial
} }
rc.Target = fmt.Sprintf("%v %v %v %v %v %v %v", rc.Target = fmt.Sprintf("%v %v %v %v %v %v %v",
v.Ns, v.Mbox, new_serial, v.Refresh, v.Retry, v.Expire, v.Minttl) v.Ns, v.Mbox, new_serial, v.Refresh, v.Retry, v.Expire, v.Minttl)
@ -107,12 +103,12 @@ func rrToRecord(rr dns.RR, origin string, replace_serial uint32) (models.RecordC
func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig { func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
// Make a default SOA record in case one isn't found: // Make a default SOA record in case one isn't found:
soa_rec := models.RecordConfig{ soaRec := models.RecordConfig{
Type: "SOA", Type: "SOA",
Name: "@", Name: "@",
} }
soa_rec.NameFQDN = dnsutil.AddOrigin(soa_rec.Name, origin) soaRec.NameFQDN = dnsutil.AddOrigin(soaRec.Name, origin)
//TODO(cpeterson): are these sane defaults?
if len(info.Ns) == 0 { if len(info.Ns) == 0 {
info.Ns = "DEFAULT_NOT_SET" info.Ns = "DEFAULT_NOT_SET"
} }
@ -134,9 +130,9 @@ func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
if info.Minttl == 0 { if info.Minttl == 0 {
info.Minttl = 1440 info.Minttl = 1440
} }
soa_rec.Target = info.String() soaRec.Target = info.String()
return &soa_rec return &soaRec
} }
func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) { func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
@ -144,7 +140,7 @@ func (c *Bind) GetNameservers(string) ([]*models.Nameserver, error) {
} }
func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
// Phase 1: Copy everything to []*models.RecordConfig: // Phase 1: Copy everything to []*models.RecordConfig:
// expectedRecords < dc.Records[i] // expectedRecords < dc.Records[i]
// foundRecords < zonefile // foundRecords < zonefile
@ -159,34 +155,34 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
// diff.Inc...(foundDiffRecords, expectedDiffRecords ) // diff.Inc...(foundDiffRecords, expectedDiffRecords )
// Default SOA record. If we see one in the zone, this will be replaced. // Default SOA record. If we see one in the zone, this will be replaced.
soa_rec := makeDefaultSOA(c.Default_Soa, dc.Name) soaRec := makeDefaultSOA(c.DefaultSoa, dc.Name)
// Read foundRecords: // Read foundRecords:
foundRecords := make([]*models.RecordConfig, 0) foundRecords := make([]*models.RecordConfig, 0)
var old_serial, new_serial uint32 var oldSerial, newSerial uint32
zonefile := filepath.Join(*bindBaseDir, strings.ToLower(dc.Name)+".zone") zonefile := filepath.Join(*bindBaseDir, strings.ToLower(dc.Name)+".zone")
found_fh, err := os.Open(zonefile) foundFH, err := os.Open(zonefile)
zone_file_found := err == nil zoneFileFound := err == nil
if err != nil && !os.IsNotExist(os.ErrNotExist) { if err != nil && !os.IsNotExist(os.ErrNotExist) {
// Don't whine if the file doesn't exist. However all other // Don't whine if the file doesn't exist. However all other
// errors will be reported. // errors will be reported.
fmt.Printf("Could not read zonefile: %v\n", err) fmt.Printf("Could not read zonefile: %v\n", err)
} else { } else {
for x := range dns.ParseZone(found_fh, dc.Name, zonefile) { for x := range dns.ParseZone(foundFH, dc.Name, zonefile) {
if x.Error != nil { if x.Error != nil {
log.Println("Error in zonefile:", x.Error) log.Println("Error in zonefile:", x.Error)
} else { } else {
rec, serial := rrToRecord(x.RR, dc.Name, old_serial) rec, serial := rrToRecord(x.RR, dc.Name, oldSerial)
if serial != 0 && old_serial != 0 { if serial != 0 && oldSerial != 0 {
log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile) log.Fatalf("Multiple SOA records in zonefile: %v\n", zonefile)
} }
if serial != 0 { if serial != 0 {
// This was an SOA record. Update the serial. // This was an SOA record. Update the serial.
old_serial = serial oldSerial = serial
new_serial = generate_serial(old_serial) newSerial = generate_serial(oldSerial)
// Regenerate with new serial: // Regenerate with new serial:
*soa_rec, _ = rrToRecord(x.RR, dc.Name, new_serial) *soaRec, _ = rrToRecord(x.RR, dc.Name, newSerial)
rec = *soa_rec rec = *soaRec
} }
foundRecords = append(foundRecords, &rec) foundRecords = append(foundRecords, &rec)
} }
@ -195,7 +191,7 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
// Add SOA record to expected set: // Add SOA record to expected set:
if !dc.HasRecordTypeName("SOA", "@") { if !dc.HasRecordTypeName("SOA", "@") {
dc.Records = append(dc.Records, soa_rec) dc.Records = append(dc.Records, soaRec)
} }
differ := diff.New(dc) differ := diff.New(dc)
@ -206,24 +202,24 @@ func (c *Bind) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correcti
changes := false changes := false
for _, i := range create { for _, i := range create {
changes = true changes = true
if zone_file_found { if zoneFileFound {
fmt.Fprintln(buf, i) fmt.Fprintln(buf, i)
} }
} }
for _, i := range del { for _, i := range del {
changes = true changes = true
if zone_file_found { if zoneFileFound {
fmt.Fprintln(buf, i) fmt.Fprintln(buf, i)
} }
} }
for _, i := range mod { for _, i := range mod {
changes = true changes = true
if zone_file_found { if zoneFileFound {
fmt.Fprintln(buf, i) fmt.Fprintln(buf, i)
} }
} }
msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name) msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name)
if !zone_file_found { if !zoneFileFound {
msg = msg + fmt.Sprintf(" (%d records)\n", len(create)) msg = msg + fmt.Sprintf(" (%d records)\n", len(create))
} }
msg += buf.String() msg += buf.String()
@ -270,7 +266,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
return nil, err return nil, err
} }
} }
api.nameservers = models.StringsToNameservers(api.Default_NS) api.nameservers = models.StringsToNameservers(api.DefaultNS)
return api, nil return api, nil
} }

View file

@ -19,6 +19,10 @@ func LoadProviderConfigs(fname string) (map[string]map[string]string, error) {
var results = map[string]map[string]string{} var results = map[string]map[string]string{}
dat, err := utfutil.ReadFile(fname, utfutil.POSIX) dat, err := utfutil.ReadFile(fname, utfutil.POSIX)
if err != nil { if err != nil {
//no creds file is ok. Bind requires nothing for example. Individual providers will error if things not found.
if os.IsNotExist(err) {
return results, nil
}
return nil, fmt.Errorf("While reading provider credentials file %v: %v", fname, err) return nil, fmt.Errorf("While reading provider credentials file %v: %v", fname, err)
} }
s := string(dat) s := string(dat)

View file

@ -88,12 +88,8 @@ func CreateRegistrars(d *models.DNSConfig, providerConfigs map[string]map[string
func CreateDsps(d *models.DNSConfig, providerConfigs map[string]map[string]string) (map[string]DNSServiceProvider, error) { func CreateDsps(d *models.DNSConfig, providerConfigs map[string]map[string]string) (map[string]DNSServiceProvider, error) {
dsps := map[string]DNSServiceProvider{} dsps := map[string]DNSServiceProvider{}
for _, dsp := range d.DNSProviders { for _, dsp := range d.DNSProviders {
//log.Printf("dsp.Name=%#v\n", dsp.Name) vals := providerConfigs[dsp.Name]
rawMsg, ok := providerConfigs[dsp.Name] provider, err := CreateDNSProvider(dsp.Type, vals, dsp.Metadata)
if !ok {
return nil, fmt.Errorf("DNSServiceProvider %s not listed in -providers file", dsp.Name)
}
provider, err := CreateDNSProvider(dsp.Type, rawMsg, dsp.Metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }