From e16648ab1f036d0864f28b35bb7e022a439fa978 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 5 Dec 2018 16:28:41 -0800 Subject: [PATCH] Progress towards fully fleshed out pools & rules --- octodns/record.py | 70 ++-- tests/config/dynamic.tests.yaml | 192 +++++++--- tests/test_octodns_provider_yaml.py | 10 +- tests/test_octodns_record.py | 567 ++++++++++++++++++++++------ 4 files changed, 652 insertions(+), 187 deletions(-) diff --git a/octodns/record.py b/octodns/record.py index 5f81e35..6517d74 100644 --- a/octodns/record.py +++ b/octodns/record.py @@ -469,11 +469,43 @@ class _DynamicMixin(object): except KeyError: pools = {} - if not pools: + if not isinstance(pools, dict): + reasons.append('pools must be a dict') + elif not pools: reasons.append('missing pools') else: - for pool in sorted(pools.values()): - reasons.extend(cls._value_type.validate(pool, cls)) + for _id, pool in sorted(pools.items()): + if not isinstance(pool, dict): + reasons.append('pool "{}" must be a dict'.format(_id)) + continue + try: + values = pool['values'] + except KeyError: + reasons.append('pool "{}" is missing values'.format(_id)) + continue + + for value_num, value in enumerate(values): + value_num += 1 + try: + weight = value['weight'] + weight = int(weight) + if weight < 1 or weight > 255: + reasons.append('invalid weight "{}" in pool "{}" ' + 'value {}'.format(weight, _id, + value_num)) + except KeyError: + pass + except ValueError: + reasons.append('invalid weight "{}" in pool "{}" ' + 'value {}'.format(weight, _id, + value_num)) + + try: + value = value['value'] + reasons.extend(cls._value_type.validate(value, cls)) + except KeyError: + reasons.append('missing value in pool "{}" ' + 'value {}'.format(_id, value_num)) try: rules = data['dynamic']['rules'] @@ -488,26 +520,19 @@ class _DynamicMixin(object): for rule_num, rule in enumerate(rules): rule_num += 1 try: - rule_pools = rule['pools'] + pool = rule['pool'] 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)) + reasons.append('rule {} missing pool'.format(rule_num)) + continue + + if not isinstance(pool, basestring): + reasons.append('rule {} invalid pool "{}"' + .format(rule_num, pool)) + elif pool not in pools: + reasons.append('rule {} undefined pool "{}"' + .format(rule_num, pool)) + + # TODO: validate GEOs if present return reasons @@ -575,6 +600,7 @@ class _DynamicMixin(object): class Ipv4List(object): @classmethod + # TODO: remove record_cls it's redudant (cls) def validate(cls, data, record_cls): if not isinstance(data, (list, tuple)): data = (data,) diff --git a/tests/config/dynamic.tests.yaml b/tests/config/dynamic.tests.yaml index b927300..ac83ad5 100644 --- a/tests/config/dynamic.tests.yaml +++ b/tests/config/dynamic.tests.yaml @@ -2,29 +2,35 @@ a: dynamic: pools: - ams: 1.1.1.1 + ams: + # TODO: make value possible + values: + - value: 1.1.1.1 iad: - - 2.2.2.2 - - 3.3.3.3 - lax: 4.4.4.4 - sea: 5.5.5.5 + values: + # TODO: make value optional + - value: 2.2.2.2 + - value: 3.3.3.3 + lax: + values: + - value: 4.4.4.4 + sea: + values: + - value: 6.6.6.6 + weight: 10 + - value: 5.5.5.5 + weight: 25 rules: - geo: EU-UK - pools: - 10: iad + pool: iad - geo: EU - pools: - 10: ams - 10: iad + pool: ams - geos: - NA-US-CA - NA-US-OR - NA-US-WA - pools: - 25: iad - 75: sea - - pools: - 10: iad + pool: sea + - pool: iad type: A values: - 2.2.2.2 @@ -32,29 +38,33 @@ a: aaaa: dynamic: pools: - ams: 2601:642:500:e210:62f8:1dff:feb8:9471 + ams: + values: + - value: 2601:642:500:e210:62f8:1dff:feb8:9471 iad: - - 2601:642:500:e210:62f8:1dff:feb8:9472 - - 2601:642:500:e210:62f8:1dff:feb8:9473 - lax: 2601:642:500:e210:62f8:1dff:feb8:9474 - sea: 2601:642:500:e210:62f8:1dff:feb8:9475 + values: + - value: 2601:642:500:e210:62f8:1dff:feb8:9472 + - value: 2601:642:500:e210:62f8:1dff:feb8:9473 + lax: + values: + - value: 2601:642:500:e210:62f8:1dff:feb8:9474 + sea: + values: + - value: 2601:642:500:e210:62f8:1dff:feb8:9475 + weight: 1 + - value: 2601:642:500:e210:62f8:1dff:feb8:9476 + weight: 2 rules: - geo: EU-UK - pools: - 10: iad + pool: iad - geo: EU - pools: - 10: ams - 10: iad + pool: ams - geos: - NA-US-CA - NA-US-OR - NA-US-WA - pools: - 25: iad - 75: sea - - pools: - 10: iad + pool: sea + - pool: iad type: AAAA values: - 2601:642:500:e210:62f8:1dff:feb8:947a @@ -62,38 +72,120 @@ aaaa: cname: dynamic: pools: - ams: target-ams.unit.tests. - iad: target-iad.unit.tests. - lax: target-lax.unit.tests. - sea: target-sea.unit.tests. + ams: + values: + - value: target-ams.unit.tests. + iad: + values: + - value: target-iad.unit.tests. + lax: + values: + - value: target-lax.unit.tests. + sea: + values: + - value: target-sea-1.unit.tests. + weight: 100 + - value: target-sea-2.unit.tests. + weight: 175 rules: - geo: EU-UK - pools: - 10: iad + pool: iad - geo: EU - pools: - 10: ams - 10: iad + pool: ams - geos: - NA-US-CA - NA-US-OR - NA-US-WA - pools: - 12: sea - 250: iad - - pools: - 1: sea - 4: iad + pool: sea + - pool: iad type: CNAME value: target.unit.tests. +real-ish-a: + dynamic: + pools: + ap-southeast-1: + values: + # ap-southeast-1a + - value: 1.4.1.1 + weight: 2 + - value: 1.4.1.2 + weight: 2 + # ap-southeast-1b + - value: 1.4.2.1 + - value: 1.4.2.2 + # ap-southeast-1c + - value: 1.4.3.1 + - value: 1.4.3.2 + eu-central-1: + values: + # eu-central-1a + - value: 1.3.1.1 + - value: 1.3.1.2 + # eu-central-1b + - value: 1.3.2.1 + - value: 1.3.2.2 + # eu-central-1c + - value: 1.3.3.1 + - value: 1.3.3.2 + us-east-1: + values: + # us-east-1a + - value: 1.1.1.1 + - value: 1.1.1.2 + # us-east-1b + - value: 1.1.2.1 + - value: 1.1.2.2 + # us-east-1c + - value: 1.1.3.1 + - value: 1.1.3.2 + us-west-2: + values: + # us-west-2a + - value: 1.2.1.1 + - value: 1.2.1.2 + # us-west-2b + - value: 1.2.2.1 + - value: 1.2.2.2 + # us-west-2c + - value: 1.2.3.1 + - value: 1.2.3.2 + rules: + - geos: + # TODO: require sorted + - NA-US-CA + - NA-US-OR + - NA-US-WA + pool: us-west-2 + - geos: + - AS-CN + pool: ap-southeast-1 + - geos: + - AF + - EU + pool: eu-central-1 + - pool: us-east-1 + type: A + values: + # Generally these should match the values of your "default" rule's pools as + # if everything fails healthchecks they'll fallback to this + - 1.1.1.1 + - 1.1.1.2 + - 1.1.2.1 + - 1.1.2.2 + - 1.1.3.1 + - 1.1.3.2 simple-weighted: dynamic: pools: - one: one.unit.tests. - two: two.unit.tests. + default: + values: + - value: one.unit.tests. + weight: 3 + - value: two.unit.tests. + weight: 2 rules: - - pools: - 100: one - 200: two + - pool: default type: CNAME + # CNAMEs don't support health checks (currently) so these will never be used + # on providers with dynamic support value: default.unit.tests. diff --git a/tests/test_octodns_provider_yaml.py b/tests/test_octodns_provider_yaml.py index d9be9d1..74261de 100644 --- a/tests/test_octodns_provider_yaml.py +++ b/tests/test_octodns_provider_yaml.py @@ -34,7 +34,7 @@ class TestYamlProvider(TestCase): self.assertEquals(18, len(zone.records)) source.populate(dynamic_zone) - self.assertEquals(4, len(dynamic_zone.records)) + self.assertEquals(5, len(dynamic_zone.records)) # Assumption here is that a clean round-trip means that everything # worked as expected, data that went in came back out and could be @@ -64,11 +64,11 @@ class TestYamlProvider(TestCase): # Dynamic plan plan = target.plan(dynamic_zone) - self.assertEquals(4, len(filter(lambda c: isinstance(c, Create), + self.assertEquals(5, len(filter(lambda c: isinstance(c, Create), plan.changes))) self.assertFalse(isfile(dynamic_yaml_file)) # Apply it - self.assertEquals(4, target.apply(plan)) + self.assertEquals(5, target.apply(plan)) self.assertTrue(isfile(dynamic_yaml_file)) # There should be no changes after the round trip @@ -133,6 +133,10 @@ class TestYamlProvider(TestCase): self.assertTrue('value' in dyna) # self.assertTrue('dynamic' in dyna) + dyna = data.pop('real-ish-a') + self.assertTrue('values' in dyna) + # self.assertTrue('dynamic' in dyna) + dyna = data.pop('simple-weighted') self.assertTrue('value' in dyna) # self.assertTrue('dynamic' in dyna) diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 4591530..0a4e4df 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -1900,17 +1900,36 @@ class TestDynamicRecords(TestCase): a_data = { 'dynamic': { 'pools': { - 'one': '3.3.3.3', - 'two': [ - '4.4.4.4', - '5.5.5.5', - ], + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }], + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }], + }, + 'three': { + 'values': [{ + 'weight': 10, + 'value': '4.4.4.4', + }, { + 'weight': 12, + 'value': '5.5.5.5', + }], + }, }, 'rules': [{ - 'pools': { - 100: 'one', - 200: 'two', - } + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -1931,6 +1950,8 @@ class TestDynamicRecords(TestCase): self.assertTrue(pools) self.assertEquals(a_data['dynamic']['pools']['one'], pools['one'].data) self.assertEquals(a_data['dynamic']['pools']['two'], pools['two'].data) + self.assertEquals(a_data['dynamic']['pools']['three'], + pools['three'].data) rules = dynamic.rules self.assertTrue(rules) @@ -1945,12 +1966,58 @@ class TestDynamicRecords(TestCase): '2601:642:500:e210:62f8:1dff:feb8:9474', '2601:642:500:e210:62f8:1dff:feb8:9475', ], + 'three': { + 1: '2601:642:500:e210:62f8:1dff:feb8:9476', + 2: '2601:642:500:e210:62f8:1dff:feb8:9477', + }, }, 'rules': [{ - 'pools': { - 100: 'one', - 200: 'two', - } + 'pools': [ + 'three', + 'two', + 'one', + ], + }], + }, + 'ttl': 60, + 'values': [ + '2601:642:500:e210:62f8:1dff:feb8:9471', + '2601:642:500:e210:62f8:1dff:feb8:9472', + ], + } + aaaa_data = { + 'dynamic': { + 'pools': { + 'one': { + 'values': [{ + 'value': '2601:642:500:e210:62f8:1dff:feb8:9473', + }], + }, + 'two': { + 'values': [{ + 'value': '2601:642:500:e210:62f8:1dff:feb8:9474', + }, { + 'value': '2601:642:500:e210:62f8:1dff:feb8:9475', + }], + }, + 'three': { + 'values': [{ + 'weight': 10, + 'value': '2601:642:500:e210:62f8:1dff:feb8:9476', + }, { + 'weight': 12, + 'value': '2601:642:500:e210:62f8:1dff:feb8:9477', + }], + }, + }, + 'rules': [{ + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -1973,6 +2040,8 @@ class TestDynamicRecords(TestCase): pools['one'].data) self.assertEquals(aaaa_data['dynamic']['pools']['two'], pools['two'].data) + self.assertEquals(aaaa_data['dynamic']['pools']['three'], + pools['three'].data) rules = dynamic.rules self.assertTrue(rules) @@ -1982,14 +2051,34 @@ class TestDynamicRecords(TestCase): cname_data = { 'dynamic': { 'pools': { - 'one': 'one.cname.target.', - 'two': 'two.cname.target.', + 'one': { + 'values': [{ + 'value': 'one.cname.target.', + }], + }, + 'two': { + 'values': [{ + 'value': 'two.cname.target.', + }], + }, + 'three': { + 'values': [{ + 'weight': 12, + 'value': 'three-1.cname.target.', + }, { + 'weight': 32, + 'value': 'three-2.cname.target.', + }] + }, }, 'rules': [{ - 'pools': { - 100: 'one', - 200: 'two', - } + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2009,6 +2098,8 @@ class TestDynamicRecords(TestCase): pools['one'].data) self.assertEquals(cname_data['dynamic']['pools']['two'], pools['two'].data) + self.assertEquals(cname_data['dynamic']['pools']['three'], + pools['three'].data) rules = dynamic.rules self.assertTrue(rules) @@ -2019,9 +2110,7 @@ class TestDynamicRecords(TestCase): a_data = { 'dynamic': { 'rules': [{ - 'pools': { - 1: 'one', - } + 'pool': 'one', }], }, 'ttl': 60, @@ -2033,7 +2122,7 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['missing pools', 'undefined pool "one"'], + self.assertEquals(['missing pools', 'rule 1 undefined pool "one"'], ctx.exception.reasons) # Empty pools @@ -2042,9 +2131,7 @@ class TestDynamicRecords(TestCase): 'pools': { }, 'rules': [{ - 'pools': { - 1: 'one', - } + 'pool': 'one', }], }, 'ttl': 60, @@ -2056,24 +2143,64 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['missing pools', 'undefined pool "one"'], + self.assertEquals(['missing pools', 'rule 1 undefined pool "one"'], + ctx.exception.reasons) + + # pools not a dict + a_data = { + 'dynamic': { + 'pools': [], + 'rules': [{ + 'pool': '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(['pools must be a dict', + 'rule 1 undefined pool "one"'], ctx.exception.reasons) # Invalid addresses a_data = { 'dynamic': { 'pools': { - 'one': 'this-aint-right', - 'two': [ - '4.4.4.4', - 'nor-is-this', - ], + 'one': { + 'values': [{ + 'value': 'this-aint-right', + }], + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': 'nor-is-this', + }] + }, + 'three': { + 'values': [{ + 'weight': 1, + 'value': '5.5.5.5', + }, { + 'weight': 2, + 'value': 'yet-another-bad-one', + }], + }, }, 'rules': [{ - 'pools': { - 100: 'one', - 200: 'two', - } + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2085,25 +2212,42 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['invalid IPv4 address "nor-is-this"', - 'invalid IPv4 address "this-aint-right"'], - ctx.exception.reasons) + self.assertEquals([ + 'invalid IPv4 address "this-aint-right"', + 'invalid IPv4 address "yet-another-bad-one"', + 'invalid IPv4 address "nor-is-this"', + ], ctx.exception.reasons) # missing value(s) a_data = { 'dynamic': { 'pools': { - 'one': [], - 'two': [ - '3.3.3.3', - '4.4.4.4', - ], + 'one': {}, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, + 'three': { + 'values': [{ + 'weight': 1, + 'value': '6.6.6.6', + }, { + 'weight': 2, + 'value': '7.7.7.7', + }], + }, }, 'rules': [{ - 'pools': { - 100: 'one', - 200: 'two', - } + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2115,23 +2259,39 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['missing value(s)'], ctx.exception.reasons) + self.assertEquals(['pool "one" is missing values'], + ctx.exception.reasons) - # Empty value + # pool valu not a dict a_data = { 'dynamic': { 'pools': { 'one': '', - 'two': [ - '3.3.3.3', - 'blip', - ], + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, + 'three': { + 'values': [{ + 'weight': 1, + 'value': '6.6.6.6', + }, { + 'weight': 2, + 'value': '7.7.7.7', + }], + }, }, 'rules': [{ - 'pools': { - 100: 'one', - 200: 'two', - } + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2143,24 +2303,39 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['invalid IPv4 address "blip"', 'empty value'], + self.assertEquals(['pool "one" must be a dict'], ctx.exception.reasons) - # multiple problems + # empty pool value a_data = { 'dynamic': { 'pools': { - 'one': '', - 'two': [ - '3.3.3.3', - 'blip', - ], + 'one': {}, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, + 'three': { + 'values': [{ + 'weight': 1, + 'value': '6.6.6.6', + }, { + 'weight': 2, + 'value': '7.7.7.7', + }], + }, }, 'rules': [{ - 'pools': { - 100: 'one', - 200: 'two', - } + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2172,15 +2347,44 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['invalid IPv4 address "blip"', 'empty value'], + self.assertEquals(['pool "one" is missing values'], ctx.exception.reasons) - # missing rules + # invalid int weight a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }] + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, + 'three': { + 'values': [{ + 'weight': 1, + 'value': '6.6.6.6', + }, { + 'weight': 256, + 'value': '7.7.7.7', + }], + }, }, + 'rules': [{ + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', + }], }, 'ttl': 60, 'type': 'A', @@ -2191,15 +2395,44 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['missing rules'], ctx.exception.reasons) + self.assertEquals(['invalid weight "256" in pool "three" value 2'], + ctx.exception.reasons) - # empty rules + # invalid non-int weight a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }] + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, + 'three': { + 'values': [{ + 'weight': 1, + 'value': '6.6.6.6', + }, { + 'weight': 'foo', + 'value': '7.7.7.7', + }], + }, }, - 'rules': [], + 'rules': [{ + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', + }], }, 'ttl': 60, 'type': 'A', @@ -2210,15 +2443,39 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['missing rules'], ctx.exception.reasons) + self.assertEquals(['invalid weight "foo" in pool "three" value 2'], + ctx.exception.reasons) - # rules not a list/tuple + # multiple pool problems a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': '', + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': 'blip', + }] + }, + 'three': { + 'values': [{ + 'weight': 1, + }, { + 'weight': 5000, + 'value': '7.7.7.7', + }], + }, }, - 'rules': {}, + 'rules': [{ + 'geos': ['AF', 'EU'], + 'pool': 'three', + }, { + 'geos': ['NA-US-CA'], + 'pool': 'two', + }, { + 'pool': 'one', + }], }, 'ttl': 60, 'type': 'A', @@ -2229,16 +2486,30 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['rules must be a list'], ctx.exception.reasons) + self.assertEquals([ + 'pool "one" must be a dict', + 'missing value in pool "three" value 1', + 'invalid weight "5000" in pool "three" value 2', + 'invalid IPv4 address "blip"', + ], ctx.exception.reasons) - # rule without pools + # missing rules a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }] + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, }, - 'rules': [{ - }], }, 'ttl': 60, 'type': 'A', @@ -2249,17 +2520,26 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['rule 1 missing pools'], ctx.exception.reasons) + self.assertEquals(['missing rules'], ctx.exception.reasons) - # rule with non-dict pools + # empty rules a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }] + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, }, - 'rules': [{ - 'pools': ['one'], - }], + 'rules': [], }, 'ttl': 60, 'type': 'A', @@ -2270,19 +2550,59 @@ class TestDynamicRecords(TestCase): } 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) + self.assertEquals(['missing rules'], ctx.exception.reasons) - # rule references non-existant pool + # rules not a list/tuple + a_data = { + 'dynamic': { + 'pools': { + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }] + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, + }, + '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 pool a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }], + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, }, 'rules': [{ - 'pools': { - 10: 'non-existant' - } + 'geos': ['NA-US-CA'], + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2294,19 +2614,30 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(["undefined pool \"non-existant\""], - ctx.exception.reasons) + self.assertEquals(['rule 1 missing pool'], ctx.exception.reasons) - # invalid int weight + # rule with non-string pools a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }] + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, }, 'rules': [{ - 'pools': { - 256: 'one' - } + 'geos': ['NA-US-CA'], + 'pool': [], + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2318,19 +2649,31 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['invalid pool weight "256"'], + self.assertEquals(['rule 1 invalid pool "[]"'], ctx.exception.reasons) - # invalid non-int weight + # rule references non-existant pool a_data = { 'dynamic': { 'pools': { - 'one': '1.2.3.4', + 'one': { + 'values': [{ + 'value': '3.3.3.3', + }] + }, + 'two': { + 'values': [{ + 'value': '4.4.4.4', + }, { + 'value': '5.5.5.5', + }] + }, }, 'rules': [{ - 'pools': { - 'foo': 'one' - } + 'geos': ['NA-US-CA'], + 'pool': 'non-existant', + }, { + 'pool': 'one', }], }, 'ttl': 60, @@ -2342,7 +2685,7 @@ class TestDynamicRecords(TestCase): } with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, 'bad', a_data) - self.assertEquals(['invalid pool weight "foo"'], + self.assertEquals(["rule 1 undefined pool \"non-existant\""], ctx.exception.reasons) def test_dynamic_lenient(self):