mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
add Zone File source, reads Bind compatible zone files
This commit is contained in:
@@ -163,6 +163,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| [Rackspace](/octodns/provider/rackspace.py) | | A, AAAA, ALIAS, CNAME, MX, NS, PTR, SPF, TXT | No | |
|
||||
| [Route53](/octodns/provider/route53.py) | boto3 | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | Yes | |
|
||||
| [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
||||
| [ZoneFileSource](/octodns/source/axfr.py) | | A, AAAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
||||
| [TinyDnsFileSource](/octodns/source/tinydns.py) | | A, CNAME, MX, NS, PTR | No | read-only |
|
||||
| [YamlProvider](/octodns/provider/yaml.py) | | All | Yes | config |
|
||||
|
||||
|
@@ -13,6 +13,8 @@ import dns.rdatatype
|
||||
from dns.exception import DNSException
|
||||
|
||||
from collections import defaultdict
|
||||
from os import listdir
|
||||
from os.path import join
|
||||
import logging
|
||||
|
||||
from ..record import Record
|
||||
@@ -160,3 +162,73 @@ class AxfrSource(AxfrBaseSource):
|
||||
})
|
||||
|
||||
return records
|
||||
|
||||
|
||||
class ZoneFileSourceException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ZoneFileSourceNotFound(ZoneFileSourceException):
|
||||
|
||||
def __init__(self):
|
||||
super(ZoneFileSourceNotFound, self).__init__(
|
||||
'Zone file not found')
|
||||
|
||||
|
||||
class ZoneFileSourceLoadFailure(ZoneFileSourceException):
|
||||
|
||||
def __init__(self, error):
|
||||
super(ZoneFileSourceLoadFailure, self).__init__(
|
||||
error.message)
|
||||
|
||||
|
||||
class ZoneFileSource(AxfrBaseSource):
|
||||
'''
|
||||
Bind compatible zone file source
|
||||
|
||||
zonefile:
|
||||
class: octodns.source.axfr.ZoneFileSource
|
||||
# The directory holding the zone files
|
||||
# Filenames should match zone name (eg. example.com.)
|
||||
directory: ./zonefiles
|
||||
'''
|
||||
def __init__(self, id, directory):
|
||||
self.log = logging.getLogger('ZoneFileSource[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, directory=%s', id, directory)
|
||||
super(ZoneFileSource, self).__init__(id)
|
||||
self.directory = directory
|
||||
|
||||
self._zone_records = {}
|
||||
|
||||
def _load_zone_file(self, zone_name):
|
||||
zonefiles = listdir(self.directory)
|
||||
if zone_name in zonefiles:
|
||||
try:
|
||||
z = dns.zone.from_file(join(self.directory, zone_name),
|
||||
zone_name, relativize=False)
|
||||
except DNSException as error:
|
||||
raise ZoneFileSourceLoadFailure(error)
|
||||
else:
|
||||
raise ZoneFileSourceNotFound()
|
||||
|
||||
return z
|
||||
|
||||
def zone_records(self, zone):
|
||||
if zone.name not in self._zone_records:
|
||||
try:
|
||||
z = self._load_zone_file(zone.name)
|
||||
records = []
|
||||
for (name, ttl, rdata) in z.iterate_rdatas():
|
||||
rdtype = dns.rdatatype.to_text(rdata.rdtype)
|
||||
records.append({
|
||||
"name": name.to_text(),
|
||||
"ttl": ttl,
|
||||
"type": rdtype,
|
||||
"value": rdata.to_text()
|
||||
})
|
||||
|
||||
self._zone_records[zone.name] = records
|
||||
except ZoneFileSourceNotFound:
|
||||
return []
|
||||
|
||||
return self._zone_records[zone.name]
|
||||
|
@@ -11,14 +11,15 @@ from dns.exception import DNSException
|
||||
from mock import patch
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.source.axfr import AxfrSource, AxfrSourceZoneTransferFailed
|
||||
from octodns.source.axfr import AxfrSource, AxfrSourceZoneTransferFailed, \
|
||||
ZoneFileSource, ZoneFileSourceLoadFailure
|
||||
from octodns.zone import Zone
|
||||
|
||||
|
||||
class TestAxfrSource(TestCase):
|
||||
source = AxfrSource('test', 'localhost')
|
||||
|
||||
forward_zonefile = dns.zone.from_file('./tests/zones/unit.tests.db',
|
||||
forward_zonefile = dns.zone.from_file('./tests/zones/unit.tests.',
|
||||
'unit.tests', relativize=False)
|
||||
|
||||
@patch('dns.zone.from_xfr')
|
||||
@@ -38,3 +39,33 @@ class TestAxfrSource(TestCase):
|
||||
self.source.populate(zone)
|
||||
self.assertEquals('Unable to Perform Zone Transfer',
|
||||
ctx.exception.message)
|
||||
|
||||
|
||||
class TestZoneFileSource(TestCase):
|
||||
source = ZoneFileSource('test', './tests/zones')
|
||||
|
||||
def test_populate(self):
|
||||
# Valid zone file in directory
|
||||
valid = Zone('unit.tests.', [])
|
||||
self.source.populate(valid)
|
||||
self.assertEquals(11, len(valid.records))
|
||||
|
||||
# 2nd populate does not read file again
|
||||
again = Zone('unit.tests.', [])
|
||||
self.source.populate(again)
|
||||
self.assertEquals(11, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del self.source._zone_records[valid.name]
|
||||
|
||||
# No zone file in directory
|
||||
missing = Zone('missing.zone.', [])
|
||||
self.source.populate(missing)
|
||||
self.assertEquals(0, len(missing.records))
|
||||
|
||||
# Zone file is not valid
|
||||
with self.assertRaises(ZoneFileSourceLoadFailure) as ctx:
|
||||
zone = Zone('invalid.zone.', [])
|
||||
self.source.populate(zone)
|
||||
self.assertEquals('The DNS zone has no NS RRset at its origin.',
|
||||
ctx.exception.message)
|
||||
|
8
tests/zones/invalid.zone.
Normal file
8
tests/zones/invalid.zone.
Normal file
@@ -0,0 +1,8 @@
|
||||
$ORIGIN invalid.zone.
|
||||
@ IN SOA ns1.invalid.zone. root.invalid.zone. (
|
||||
2018071501 ; Serial
|
||||
3600 ; Refresh (1 hour)
|
||||
600 ; Retry (10 minutes)
|
||||
604800 ; Expire (1 week)
|
||||
3600 ; NXDOMAIN ttl (1 hour)
|
||||
)
|
Reference in New Issue
Block a user