Browse Source

add Zone File source, reads Bind compatible zone files

pull/276/head
Adam Smith 7 years ago
parent
commit
fd3de1e08b
5 changed files with 114 additions and 2 deletions
  1. +1
    -0
      README.md
  2. +72
    -0
      octodns/source/axfr.py
  3. +33
    -2
      tests/test_octodns_source_axfr.py
  4. +8
    -0
      tests/zones/invalid.zone.
  5. +0
    -0
      tests/zones/unit.tests.

+ 1
- 0
README.md View File

@ -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 | | | [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 | | | [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 | | [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 | | [TinyDnsFileSource](/octodns/source/tinydns.py) | | A, CNAME, MX, NS, PTR | No | read-only |
| [YamlProvider](/octodns/provider/yaml.py) | | All | Yes | config | | [YamlProvider](/octodns/provider/yaml.py) | | All | Yes | config |


+ 72
- 0
octodns/source/axfr.py View File

@ -13,6 +13,8 @@ import dns.rdatatype
from dns.exception import DNSException from dns.exception import DNSException
from collections import defaultdict from collections import defaultdict
from os import listdir
from os.path import join
import logging import logging
from ..record import Record from ..record import Record
@ -160,3 +162,73 @@ class AxfrSource(AxfrBaseSource):
}) })
return records 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]

+ 33
- 2
tests/test_octodns_source_axfr.py View File

@ -11,14 +11,15 @@ from dns.exception import DNSException
from mock import patch from mock import patch
from unittest import TestCase 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 from octodns.zone import Zone
class TestAxfrSource(TestCase): class TestAxfrSource(TestCase):
source = AxfrSource('test', 'localhost') 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) 'unit.tests', relativize=False)
@patch('dns.zone.from_xfr') @patch('dns.zone.from_xfr')
@ -38,3 +39,33 @@ class TestAxfrSource(TestCase):
self.source.populate(zone) self.source.populate(zone)
self.assertEquals('Unable to Perform Zone Transfer', self.assertEquals('Unable to Perform Zone Transfer',
ctx.exception.message) 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
- 0
tests/zones/invalid.zone. View 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)
)

tests/zones/unit.tests.db → tests/zones/unit.tests. View File


Loading…
Cancel
Save