Commit b1b40111 authored by godog's avatar godog

Merge branch 'recursive_extend_test' into 'master'

Recursive extend test

See merge request !1
parents a509a9ff d8b24f6d
Pipeline #2005 passed with stage
in 24 seconds
[tox]
envlist = py27
[testenv]
deps=
nose
......
......@@ -55,7 +55,7 @@ def _parse_record(value):
rtype, rdata = ('A', value)
else:
try:
rtype, rdata = value.split(' ', 1)
rtype, rdata = value.split(None, 1)
except ValueError:
raise Error('invalid record value "%s"' % value)
return rtype.upper(), rdata
......@@ -110,33 +110,52 @@ def _mklist(v):
return v if isinstance(v, list) else [v]
def _unlist(v):
if len(v) == 1:
return v[0]
return v
def _uniq(l):
return list(set(l))
def _merge_zones(a, b, replace):
"""Merge two zones (dictionaries), adding b to a.
Adds all the attributes of b to a. When a already has a given
attribute, what happens depends on the chosen mode: if replace is
True, the new value replaces the old one, otherwise they are
merged into a list.
Returns a (which is modified in-place).
"""
for key in b:
if not replace:
v = _mklist(a.get(key))
v.extend(_mklist(b[key]))
a[key] = _unlist(_uniq(v))
return a
class ZoneParser(object):
def __init__(self, config=None):
self.config = config or {}
self.zones = {'@default': {}}
self.resolved = {}
def load(self, yamls):
for objs in yamls:
for y in objs:
self._merge(y)
def _mergedict(self, a, b):
# 'a' is modified.
for key, value in b.iteritems():
a[key] = self._mergevalues(a.get(key, []), value)
def _merge(self, data):
for zone_name, zone_data in data.iteritems():
self._mergedict(
_merge_zones(
self.zones.setdefault(zone_name, {}),
zone_data)
def _mergevalues(self, va, vb):
result = _mklist(va) + _mklist(vb)
if len(result) == 1:
return result[0]
return result
zone_data,
False)
def render(self):
for zone_name, zone_data in self.zones.iteritems():
......@@ -147,17 +166,32 @@ class ZoneParser(object):
yield (zone_name, resolved, output)
def _resolve_references(self, zone_name, zone_data):
out = {}
bases = ['@default'] if not zone_name.startswith('@') else []
bases.extend(_mklist(zone_data.get('EXTENDS', [])))
if zone_name in self.resolved:
return self.resolved[zone_name]
out = dict(zone_data)
# All non-template zones implicitly inherit from @default.
extends = ['@default'] if not zone_name.startswith('@') else []
extends.extend(_mklist(zone_data.get('EXTENDS', [])))
extends = _uniq(extends)
replaces = _mklist(zone_data.get('REPLACES', []))
# Merge extends/replaces templates into 'out'.
for bases, replace in ((extends, False), (replaces, True)):
for base in bases:
try:
out.update(self._resolve_references(
base, self.zones[base]))
except KeyError:
if base not in self.zones:
raise Error('undefined template "%s"' % base)
_merge_zones(out, self._resolve_references(
base, self.zones[base]), replace)
self._expand_vars(out)
out.pop('EXTENDS', None)
out.pop('REPLACES', None)
self.resolved[zone_name] = out
return out
def _expand_vars(self, zone_data):
# This will break horribly if you use more than one
# variable substitution per record (but why would you?)
var_re = re.compile(r'\$([A-Za-z0-9_]+)')
......@@ -168,6 +202,7 @@ class ZoneParser(object):
return self.config[m.group(1)]
except KeyError:
raise Error('undefined config variable "%s"' % m.group(1))
def _expand_value(value):
var_value = _get_var(value)
if not var_value:
......@@ -189,9 +224,7 @@ class ZoneParser(object):
else:
tmp.append(tmpv)
value = tmp
out[key] = value
return out
zone_data[key] = value
def walk(root_dir):
......
......@@ -6,6 +6,20 @@ from StringIO import StringIO
from zonetool.zone import *
class TestMergeZones(unittest.TestCase):
def test_merge_zones(self):
a = {'_': ['1.2.3.4']}
b = {'_': ['TXT spf ...'], 'EXTENDS': ['a']}
c = {'_': ['2.3.4.5'], 'REPLACES': ['a']}
zp = ZoneParser()
zp.zones.update({'a': a, 'b': b, 'c': c})
result = zp._resolve_references('@b', b)
self.assertEqual(set(['1.2.3.4', 'TXT spf ...']), set(result['_']))
result = zp._resolve_references('@c', c)
self.assertEqual(['2.3.4.5'], result['_'])
TEST_DATA = [
'''
autistici.org:
......@@ -59,6 +73,32 @@ autistici.org:
''',
]
TEST_DATA_4 = [
'''
autistici.org:
EXTENDS:
- "@default"
_:
- 1.1.1.3
www: CNAME "www.l.autistici.org"
''',
'''
"@default":
EXTENDS:
- "@base"
_:
- 1.1.1.2
- NS ns2.autistici.org.
''',
'''
"@base":
_:
- 1.1.1.1
- MX 10 $FRONTENDS
- NS ns1.autistici.org.
''',
]
TEST_CONFIG = {
'FRONTENDS': ['82.94.249.234', '82.221.99.153'],
}
......@@ -93,7 +133,7 @@ class ZoneParserTest(unittest.TestCase):
zone_data = result[0][2]
expected_data = '''; Comments should be ignored
$TTL 3600
@ IN SOA localhost. hostmaster.autistici.org. (
@ IN SOA ns1.autistici.org. hostmaster.autistici.org. (
1521885904 ; Serial
43200
3600
......@@ -101,6 +141,8 @@ $TTL 3600
3600 )
IN A 82.221.99.153
IN A 82.94.249.234
IN NS ns1.autistici.org.
IN NS ns2.autistici.org.
onion IN TXT "blahblah.onion."
www IN A 82.221.99.153
www IN A 82.94.249.234
......@@ -118,9 +160,9 @@ www IN A 82.94.249.234
self.assertEquals('autistici.org', result[0][0])
# Verify that all 'www' entries have been merged properly.
www = result[0][1]['www']
expected = ['82.94.249.234', '82.221.99.153', 'TXT "web"',
'2a02:f48:2000:201::19', '2002:b2ff:9023::1']
www = set(result[0][1]['www'])
expected = set(['82.94.249.234', '82.221.99.153', 'TXT "web"',
'2a02:f48:2000:201::19', '2002:b2ff:9023::1'])
self.assertEquals(expected, www)
def test_expand_list_keywords(self):
......@@ -133,6 +175,14 @@ www IN A 82.94.249.234
num_ns = len(filter(lambda x: x.startswith('NS '), result[0][1]['_']))
self.assertEquals(num_ns, 2)
def test_recursive_extend(self):
self.zp.load(_loadyaml(TEST_DATA_4))
result = list(self.zp.render())
self.assertTrue(result)
for frontend in TEST_CONFIG['FRONTENDS']:
self.assertIn('MX 10 %s' % frontend, result[0][1]['_'])
self.assertEquals('autistici.org', result[0][0])
class ZoneWriterTestBase(unittest.TestCase):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment