From 70c35aac26606f4737df83a96b69b41cae8809e3 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 3 Dec 2018 14:24:56 -0800 Subject: [PATCH] WIP implementation of dyanmic pools & rules validation --- octodns/record.py | 47 ++++- tests/config/dynamic.tests.yaml | 15 +- tests/test_octodns_record.py | 305 +++++++++++++++++++++++++++++++- 3 files changed, 356 insertions(+), 11 deletions(-) diff --git a/octodns/record.py b/octodns/record.py index 3fa4b05..665ceba 100644 --- a/octodns/record.py +++ b/octodns/record.py @@ -395,12 +395,55 @@ class _DynamicMixin(object): @classmethod def validate(cls, name, data): reasons = super(_DynamicMixin, cls).validate(name, data) + + if 'dynamic' not in data: + return reasons + try: pools = data['dynamic']['pools'] except KeyError: pools = {} - for pool in sorted(pools.values()): - reasons.extend(cls._value_type.validate(pool)) + + if not pools: + reasons.append('missing pools') + else: + for pool in sorted(pools.values()): + reasons.extend(cls._value_type.validate(pool)) + + try: + rules = data['dynamic']['rules'] + except KeyError: + rules = [] + + if not isinstance(rules, (list, tuple)): + reasons.append('rules must be a list') + elif not rules: + reasons.append('missing rules') + else: + for rule_num, rule in enumerate(rules): + rule_num += 1 + try: + rule_pools = rule['pools'] + except KeyError: + rule_pools = {} + if not rule_pools: + reasons.append('rule {} missing pools'.format(rule_num)) + elif not isinstance(rule_pools, dict): + reasons.append('rule {} pools must be a dict' + .format(rule_num)) + else: + for weight, pool in rule_pools.items(): + try: + weight = int(weight) + if weight < 1 or weight > 255: + reasons.append('invalid pool weight "{}"' + .format(weight)) + except ValueError: + reasons.append('invalid pool weight "{}"' + .format(weight)) + if pool not in pools: + reasons.append('undefined pool "{}"'.format(pool)) + return reasons def __init__(self, zone, name, data, *args, **kwargs): diff --git a/tests/config/dynamic.tests.yaml b/tests/config/dynamic.tests.yaml index 07d3a54..18db9c9 100644 --- a/tests/config/dynamic.tests.yaml +++ b/tests/config/dynamic.tests.yaml @@ -11,11 +11,11 @@ a: rules: - geo: EU-UK pools: - - iad + 10: iad - geo: EU pools: - - ams - - iad + 10: ams + 10: iad - geos: - NA-US-CA - NA-US-OR @@ -23,7 +23,8 @@ a: pools: 25: iad 75: sea - - pool: iad + - pools: + 10: iad type: A values: - 2.2.2.2 @@ -42,11 +43,11 @@ cname: rules: - geo: EU-UK pools: - - iad + 10: iad - geo: EU pools: - - ams - - iad + 10: ams + 10: iad - geos: - NA-US-CA - NA-US-OR diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index c52a9aa..7a71d4c 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -1926,6 +1926,51 @@ class TestDynamicRecords(TestCase): self.assertEquals(a_data['dynamic'], a.dynamic) def test_a_validation(self): + # Missing pools + a_data = { + 'dynamic': { + 'rules': [{ + 'pools': { + 1: 'one', + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['missing pools', 'undefined pool "one"'], + ctx.exception.reasons) + + # Empty pools + a_data = { + 'dynamic': { + 'pools': { + }, + 'rules': [{ + 'pools': { + 1: 'one', + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['missing pools', 'undefined pool "one"'], + ctx.exception.reasons) + + # Invalid addresses a_data = { 'dynamic': { 'pools': { @@ -1937,8 +1982,8 @@ class TestDynamicRecords(TestCase): }, 'rules': [{ 'pools': { - 100: '5.5.5.5', - 200: '6.6.6.6', + 100: 'one', + 200: 'two', } }], }, @@ -1954,3 +1999,259 @@ class TestDynamicRecords(TestCase): self.assertEquals(['invalid IPv4 address "nor-is-this"', 'invalid IPv4 address "this-aint-right"'], ctx.exception.reasons) + + # missing value(s) + a_data = { + 'dynamic': { + 'pools': { + 'one': [], + 'two': [ + '3.3.3.3', + '4.4.4.4', + ], + }, + 'rules': [{ + 'pools': { + 100: 'one', + 200: 'two', + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['missing value(s)'], ctx.exception.reasons) + + # Empty value + a_data = { + 'dynamic': { + 'pools': { + 'one': '', + 'two': [ + '3.3.3.3', + 'blip', + ], + }, + 'rules': [{ + 'pools': { + 100: 'one', + 200: 'two', + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['invalid IPv4 address "blip"', 'empty value'], + ctx.exception.reasons) + + # multiple problems + a_data = { + 'dynamic': { + 'pools': { + 'one': '', + 'two': [ + '3.3.3.3', + 'blip', + ], + }, + 'rules': [{ + 'pools': { + 100: 'one', + 200: 'two', + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['invalid IPv4 address "blip"', 'empty value'], + ctx.exception.reasons) + + # missing rules + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['missing rules'], ctx.exception.reasons) + + # empty rules + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + 'rules': [], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['missing rules'], ctx.exception.reasons) + + # rules not a list/tuple + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + 'rules': {}, + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['rules must be a list'], ctx.exception.reasons) + + # rule without pools + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + 'rules': [{ + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['rule 1 missing pools'], ctx.exception.reasons) + + # rule with non-dict pools + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + 'rules': [{ + 'pools': ['one'], + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(["rule 1 pools must be a dict"], + ctx.exception.reasons) + + # rule references non-existant pool + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + 'rules': [{ + 'pools': { + 10: 'non-existant' + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(["undefined pool \"non-existant\""], + ctx.exception.reasons) + + # invalid int weight + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + 'rules': [{ + 'pools': { + 256: 'one' + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['invalid pool weight "256"'], + ctx.exception.reasons) + + # invalid non-int weight + a_data = { + 'dynamic': { + 'pools': { + 'one': '1.2.3.4', + }, + 'rules': [{ + 'pools': { + 'foo': 'one' + } + }], + }, + 'ttl': 60, + 'type': 'A', + 'values': [ + '1.1.1.1', + '2.2.2.2', + ], + } + with self.assertRaises(ValidationError) as ctx: + Record.new(self.zone, 'bad', a_data) + self.assertEquals(['invalid pool weight "foo"'], + ctx.exception.reasons)