Browse Source

TindyDNS rework passing tests

pull/1020/head
Ross McFarland 2 years ago
parent
commit
ca3a4541a0
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
3 changed files with 114 additions and 102 deletions
  1. +77
    -85
      octodns/source/tinydns.py
  2. +35
    -15
      tests/test_octodns_source_tinydns.py
  3. +2
    -2
      tests/zones/tinydns/example.com

+ 77
- 85
octodns/source/tinydns.py View File

@ -3,16 +3,14 @@
# #
import logging import logging
import re
import textwrap import textwrap
from collections import defaultdict from collections import defaultdict
from ipaddress import ip_address from ipaddress import ip_address
from os import listdir from os import listdir
from os.path import join from os.path import join
from pprint import pprint
from ..record import Record from ..record import Record
from ..zone import DuplicateRecordException, SubzoneRecordException
from ..zone import SubzoneRecordException
from .base import BaseSource from .base import BaseSource
@ -28,9 +26,15 @@ class TinyDnsBaseSource(BaseSource):
def _records_for_at(self, zone, name, lines, arpa=False): def _records_for_at(self, zone, name, lines, arpa=False):
# @fqdn:ip:x:dist:ttl:timestamp:lo # @fqdn:ip:x:dist:ttl:timestamp:lo
# MX (and optional A) # MX (and optional A)
if arpa: if arpa:
# no arpa
return [] return []
if not zone.owns('MX', name):
# if name doesn't live under our zone there's nothing for us to do
return
# see if we can find a ttl on any of the lines, first one wins # see if we can find a ttl on any of the lines, first one wins
ttl = self.default_ttl ttl = self.default_ttl
for line in lines: for line in lines:
@ -46,7 +50,7 @@ class TinyDnsBaseSource(BaseSource):
# if there's a . in the mx we hit a special case and use it as-is # if there's a . in the mx we hit a special case and use it as-is
if '.' not in mx: if '.' not in mx:
# otherwise we treat it as the MX hostnam and construct the rest # otherwise we treat it as the MX hostnam and construct the rest
mx = f'{mx}.ns.{zone.name}'
mx = f'{mx}.mx.{zone.name}'
elif mx[-1] != '.': elif mx[-1] != '.':
mx = f'{mx}.' mx = f'{mx}.'
@ -59,8 +63,7 @@ class TinyDnsBaseSource(BaseSource):
# if we have an IP then we need to create an A for the MX # if we have an IP then we need to create an A for the MX
ip = line[1] ip = line[1]
if ip: if ip:
mx_name = zone.hostname_from_fqdn(mx)
yield 'A', mx_name, ttl, [ip]
yield 'A', mx, ttl, [ip]
values.append({'preference': dist, 'exchange': mx}) values.append({'preference': dist, 'exchange': mx})
@ -69,9 +72,15 @@ class TinyDnsBaseSource(BaseSource):
def _records_for_C(self, zone, name, lines, arpa=False): def _records_for_C(self, zone, name, lines, arpa=False):
# Cfqdn:p:ttl:timestamp:lo # Cfqdn:p:ttl:timestamp:lo
# CNAME # CNAME
if arpa: if arpa:
# no arpa
return [] return []
if not zone.owns('CNAME', name):
# if name doesn't live under our zone there's nothing for us to do
return
value = lines[0][1] value = lines[0][1]
if value[-1] != '.': if value[-1] != '.':
value = f'{value}.' value = f'{value}.'
@ -88,19 +97,35 @@ class TinyDnsBaseSource(BaseSource):
yield 'CNAME', name, ttl, [value] yield 'CNAME', name, ttl, [value]
def _records_for_caret(self, zone, name, lines, arpa=False): def _records_for_caret(self, zone, name, lines, arpa=False):
# .fqdn:ip:x:ttl:timestamp:lo
# NS (and optional A)
# ^fqdn:p:ttl:timestamp:lo
# PTR, line may be a A/AAAA or straight PTR
if not arpa: if not arpa:
print('bailing')
# we only operate on arpa
return [] return []
print('here')
values = []
names = defaultdict(list)
for line in lines: for line in lines:
value = line[1]
if line[0].endswith('in-addr.arpa') or line[0].endswith(
'ip6.arpa.'
):
# it's a straight PTR record, already in in-addr.arpa format,
# 2nd item is the name it points to
name = line[0]
value = line[1]
else:
# it's not a PTR we need to build up the PTR data from what
# we're given
value = line[0]
addr = line[1]
if '.' not in addr:
addr = u':'.join(textwrap.wrap(line[1], 4))
addr = ip_address(addr)
name = addr.reverse_pointer
if value[-1] != '.': if value[-1] != '.':
value = f'{value}.' value = f'{value}.'
values.append(value)
names[name].append(value)
# see if we can find a ttl on any of the lines, first one wins # see if we can find a ttl on any of the lines, first one wins
ttl = self.default_ttl ttl = self.default_ttl
@ -111,23 +136,30 @@ class TinyDnsBaseSource(BaseSource):
except IndexError: except IndexError:
pass pass
pprint({'caret': values})
yield 'PTR', name, ttl, values
for name, values in names.items():
if zone.owns('PTR', name):
yield 'PTR', name, ttl, values
def _records_for_equal(self, zone, name, lines, arpa=False): def _records_for_equal(self, zone, name, lines, arpa=False):
# =fqdn:ip:ttl:timestamp:lo # =fqdn:ip:ttl:timestamp:lo
# A (arpa False) & PTR (arpa True) # A (arpa False) & PTR (arpa True)
print(f'here for {name}: {lines}')
yield from self._records_for_plus(zone, name, lines, arpa)
yield from self._records_for_caret(zone, name, lines, arpa)
if arpa:
yield from self._records_for_caret(zone, name, lines, arpa)
else:
yield from self._records_for_plus(zone, name, lines, arpa)
def _records_for_dot(self, zone, name, lines, arpa=False): def _records_for_dot(self, zone, name, lines, arpa=False):
# .fqdn:ip:x:ttl:timestamp:lo # .fqdn:ip:x:ttl:timestamp:lo
# NS (and optional A) # NS (and optional A)
if arpa: if arpa:
# no arpa
return [] return []
if not zone.owns('NS', name):
# if name doesn't live under our zone there's nothing for us to do
return
# see if we can find a ttl on any of the lines, first one wins # see if we can find a ttl on any of the lines, first one wins
ttl = self.default_ttl ttl = self.default_ttl
for line in lines: for line in lines:
@ -150,8 +182,7 @@ class TinyDnsBaseSource(BaseSource):
# if we have an IP then we need to create an A for the MX # if we have an IP then we need to create an A for the MX
ip = line[1] ip = line[1]
if ip: if ip:
ns_name = zone.hostname_from_fqdn(ns)
yield 'A', ns_name, ttl, [ip]
yield 'A', ns, ttl, [ip]
values.append(ns) values.append(ns)
@ -162,9 +193,15 @@ class TinyDnsBaseSource(BaseSource):
def _records_for_plus(self, zone, name, lines, arpa=False): def _records_for_plus(self, zone, name, lines, arpa=False):
# +fqdn:ip:ttl:timestamp:lo # +fqdn:ip:ttl:timestamp:lo
# A # A
if arpa: if arpa:
# no arpa
return [] return []
if not zone.owns('A', name):
# if name doesn't live under our zone there's nothing for us to do
return
# collect our ip(s) # collect our ip(s)
ips = [l[1] for l in lines if l[1] != '0.0.0.0'] ips = [l[1] for l in lines if l[1] != '0.0.0.0']
@ -186,9 +223,15 @@ class TinyDnsBaseSource(BaseSource):
def _records_for_quote(self, zone, name, lines, arpa=False): def _records_for_quote(self, zone, name, lines, arpa=False):
# 'fqdn:s:ttl:timestamp:lo # 'fqdn:s:ttl:timestamp:lo
# TXT # TXT
if arpa: if arpa:
# no arpa
return [] return []
if not zone.owns('TXT', name):
# if name doesn't live under our zone there's nothing for us to do
return
# collect our ip(s) # collect our ip(s)
values = [ values = [
l[1].encode('latin1').decode('unicode-escape').replace(";", "\\;") l[1].encode('latin1').decode('unicode-escape').replace(";", "\\;")
@ -209,9 +252,15 @@ class TinyDnsBaseSource(BaseSource):
def _records_for_three(self, zone, name, lines, arpa=False): def _records_for_three(self, zone, name, lines, arpa=False):
# 3fqdn:ip:ttl:timestamp:lo # 3fqdn:ip:ttl:timestamp:lo
# AAAA # AAAA
if arpa: if arpa:
# no arpa
return [] return []
if not zone.owns('AAAA', name):
# if name doesn't live under our zone there's nothing for us to do
return
# collect our ip(s) # collect our ip(s)
ips = [] ips = []
for line in lines: for line in lines:
@ -234,8 +283,10 @@ class TinyDnsBaseSource(BaseSource):
def _records_for_six(self, zone, name, lines, arpa=False): def _records_for_six(self, zone, name, lines, arpa=False):
# 6fqdn:ip:ttl:timestamp:lo # 6fqdn:ip:ttl:timestamp:lo
# AAAA (arpa False) & PTR (arpa True) # AAAA (arpa False) & PTR (arpa True)
yield from self._records_for_three(zone, name, lines, arpa)
yield from self._records_for_caret(zone, name, lines, arpa)
if arpa:
yield from self._records_for_caret(zone, name, lines, arpa)
else:
yield from self._records_for_three(zone, name, lines, arpa)
SYMBOL_MAP = { SYMBOL_MAP = {
'=': _records_for_equal, # A '=': _records_for_equal, # A
@ -256,8 +307,6 @@ class TinyDnsBaseSource(BaseSource):
} }
def _process_lines(self, zone, lines): def _process_lines(self, zone, lines):
name_re = re.compile(fr'((?P<name>.+)\.)?{zone.name[:-1]}\.?$')
data = defaultdict(lambda: defaultdict(list)) data = defaultdict(lambda: defaultdict(list))
for line in lines: for line in lines:
symbol = line[0] symbol = line[0]
@ -266,15 +315,7 @@ class TinyDnsBaseSource(BaseSource):
line = line[1:].split('#', 1)[0] line = line[1:].split('#', 1)[0]
# Split on :'s including :: and strip leading/trailing ws # Split on :'s including :: and strip leading/trailing ws
line = [p.strip() for p in line.split(':')] line = [p.strip() for p in line.split(':')]
# make sure the name portion matches the zone we're currently
# working on
name = line[0]
if not name_re.match(name):
self.log.info('skipping name %s, not a match, %s: ', name, line)
continue
# remove the zone name
name = zone.hostname_from_fqdn(name)
data[symbol][name].append(line)
data[symbol][line[0]].append(line)
return data return data
@ -294,6 +335,8 @@ class TinyDnsBaseSource(BaseSource):
for _type, name, ttl, values in records_for( for _type, name, ttl, values in records_for(
self, zone, name, lines, arpa=arpa self, zone, name, lines, arpa=arpa
): ):
# remove the zone name
name = zone.hostname_from_fqdn(name)
types[_type][name].extend(values) types[_type][name].extend(values)
# last one wins # last one wins
ttls[_type][name] = ttl ttls[_type][name] = ttl
@ -318,7 +361,6 @@ class TinyDnsBaseSource(BaseSource):
# first group lines by their symbol and name # first group lines by their symbol and name
symbols = self._process_lines(zone, self._lines()) symbols = self._process_lines(zone, self._lines())
pprint({'symbols': symbols})
# then work through those to group values by their _type and name # then work through those to group values by their _type and name
zone_name = zone.name zone_name = zone.name
@ -326,7 +368,6 @@ class TinyDnsBaseSource(BaseSource):
'ip6.arpa.' 'ip6.arpa.'
) )
types, ttls = self._process_symbols(zone, symbols, arpa) types, ttls = self._process_symbols(zone, symbols, arpa)
pprint({'types': types, 'ttls': ttls})
# now we finally have all the values for each (soon to be) record # now we finally have all the values for each (soon to be) record
# collected together, turn them into their coresponding record and add # collected together, turn them into their coresponding record and add
@ -338,9 +379,7 @@ class TinyDnsBaseSource(BaseSource):
data['values'] = values data['values'] = values
else: else:
data['value'] = values[0] data['value'] = values[0]
pprint({'name': name, 'data': data})
record = Record.new(zone, name, data, lenient=lenient) record = Record.new(zone, name, data, lenient=lenient)
pprint({'lenient': lenient})
try: try:
zone.add_record(record, lenient=lenient) zone.add_record(record, lenient=lenient)
except SubzoneRecordException: except SubzoneRecordException:
@ -352,53 +391,6 @@ class TinyDnsBaseSource(BaseSource):
'populate: found %s records', len(zone.records) - before 'populate: found %s records', len(zone.records) - before
) )
def _populate_arpa_arpa(self, zone, lenient):
name_re = re.compile(fr'(?P<name>.+)\.{zone.name[:-1]}\.?$')
for line in self._lines():
_type = line[0]
# We're only interested in = (A+PTR), and ^ (PTR) records
if _type not in ('=', '^', '&'):
continue
# Skip type, remove trailing comments, and omit newline
line = line[1:].split('#', 1)[0]
# Split on :'s including :: and strip leading/trailing ws
line = [p.strip() for p in self.split_re.split(line)]
if line[0].endswith('in-addr.arpa'):
# since it's already in in-addr.arpa format
match = name_re.match(line[0])
value = line[1]
else:
addr = ip_address(line[1])
match = name_re.match(addr.reverse_pointer)
value = line[0]
if match:
try:
ttl = line[2]
except IndexError:
ttl = self.default_ttl
if value[-1] != '.':
value = f'{value}.'
name = match.group('name')
record = Record.new(
zone,
name,
{'ttl': ttl, 'type': 'PTR', 'value': value},
source=self,
lenient=lenient,
)
try:
zone.add_record(record, lenient=lenient)
except DuplicateRecordException:
self.log.warning(
f'Duplicate PTR record for {addr}, skipping'
)
class TinyDnsFileSource(TinyDnsBaseSource): class TinyDnsFileSource(TinyDnsBaseSource):
''' '''


+ 35
- 15
tests/test_octodns_source_tinydns.py View File

@ -17,7 +17,7 @@ class TestTinyDnsFileSource(TestCase):
def test_populate_normal(self): def test_populate_normal(self):
got = Zone('example.com.', []) got = Zone('example.com.', [])
self.source.populate(got) self.source.populate(got)
self.assertEqual(17, len(got.records))
self.assertEqual(24, len(got.records))
expected = Zone('example.com.', []) expected = Zone('example.com.', [])
for name, data in ( for name, data in (
@ -26,8 +26,13 @@ class TestTinyDnsFileSource(TestCase):
'', '',
{ {
'type': 'NS', 'type': 'NS',
'ttl': 3600,
'values': ['ns1.ns.com.', 'ns2.ns.com.'],
'ttl': 31,
'values': [
'a.ns.example.com.',
'b.ns.example.com.',
'ns1.ns.com.',
'ns2.ns.com.',
],
}, },
), ),
( (
@ -75,11 +80,11 @@ class TestTinyDnsFileSource(TestCase):
'values': [ 'values': [
{ {
'preference': 30, 'preference': 30,
'exchange': 'smtp-1-host.example.com.',
'exchange': 'smtp-3-host.mx.example.com.',
}, },
{ {
'preference': 40, 'preference': 40,
'exchange': 'smtp-2-host.example.com.',
'exchange': 'smtp-4-host.mx.example.com.',
}, },
], ],
}, },
@ -111,6 +116,26 @@ class TestTinyDnsFileSource(TestCase):
'value': 'v=DKIM1\\; k=rsa\\; p=blah', 'value': 'v=DKIM1\\; k=rsa\\; p=blah',
}, },
), ),
('b.ns', {'type': 'A', 'ttl': 31, 'value': '43.44.45.46'}),
('a.ns', {'type': 'A', 'ttl': 3600, 'value': '42.43.44.45'}),
(
'smtp-3-host.mx',
{'type': 'A', 'ttl': 1800, 'value': '21.22.23.24'},
),
(
'smtp-4-host.mx',
{'type': 'A', 'ttl': 1800, 'value': '22.23.24.25'},
),
('ns5.ns', {'type': 'A', 'ttl': 30, 'value': '14.15.16.17'}),
('ns6.ns', {'type': 'A', 'ttl': 30, 'value': '15.16.17.18'}),
(
'other',
{
'type': 'NS',
'ttl': 30,
'values': ['ns5.ns.example.com.', 'ns6.ns.example.com.'],
},
),
): ):
record = Record.new(expected, name, data) record = Record.new(expected, name, data)
expected.add_record(record) expected.add_record(record)
@ -162,7 +187,10 @@ class TestTinyDnsFileSource(TestCase):
{ {
'type': 'PTR', 'type': 'PTR',
'ttl': 3600, 'ttl': 3600,
'value': 'has-dup-def123.example.com.',
'values': [
'has-dup-def123.example.com.',
'has-dup-def456.example.com.',
],
}, },
), ),
( (
@ -178,18 +206,10 @@ class TestTinyDnsFileSource(TestCase):
expected.add_record(record) expected.add_record(record)
changes = expected.changes(got, SimpleProvider()) changes = expected.changes(got, SimpleProvider())
from pprint import pprint
pprint(
{
'changes': changes,
'expected': expected.records,
'got': got.records,
}
)
self.assertEqual([], changes) self.assertEqual([], changes)
def test_ignores_subs(self): def test_ignores_subs(self):
got = Zone('example.com.', ['sub']) got = Zone('example.com.', ['sub'])
self.source.populate(got) self.source.populate(got)
# we don't see one www.sub.example.com. record b/c it's in a sub
self.assertEqual(23, len(got.records)) self.assertEqual(23, len(got.records))

+ 2
- 2
tests/zones/tinydns/example.com View File

@ -28,8 +28,8 @@ Ccname.other.foo:www.other.foo
@example.com::smtp-1-host.example.com:10 @example.com::smtp-1-host.example.com:10
@example.com.::smtp-2-host.example.com:20 @example.com.::smtp-2-host.example.com:20
# MX with ttl and ip # MX with ttl and ip
@smtp.example.com:21.22.23.24:smtp-1-host:30:1800
@smtp.example.com.:22.23.24.25:smtp-2-host:40:1800
@smtp.example.com:21.22.23.24:smtp-3-host:30:1800
@smtp.example.com.:22.23.24.25:smtp-4-host:40:1800
# NS for sub # NS for sub
.sub.example.com::ns3.ns.com:30 .sub.example.com::ns3.ns.com:30


Loading…
Cancel
Save