1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00
Files
github-octodns/tests/test_octodns_record_dynamic.py
2023-05-03 21:31:19 -07:00

1564 lines
52 KiB
Python

#
#
#
from unittest import TestCase
from helpers import DynamicProvider, SimpleProvider
from octodns.idna import idna_encode
from octodns.record import Record
from octodns.record.a import ARecord, Ipv4Value
from octodns.record.aaaa import AaaaRecord
from octodns.record.cname import CnameRecord
from octodns.record.dynamic import (
_Dynamic,
_DynamicMixin,
_DynamicPool,
_DynamicRule,
)
from octodns.record.exception import ValidationError
from octodns.zone import Zone
class TestRecordDynamic(TestCase):
zone = Zone('unit.tests.', [])
def test_dynamic_record_copy(self):
a_data = {
'dynamic': {
'pools': {'one': {'values': [{'value': '3.3.3.3'}]}},
'rules': [{'pool': 'one'}],
},
'octodns': {'healthcheck': {'protocol': 'TCP', 'port': 80}},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
}
record1 = Record.new(self.zone, 'a', a_data)
record2 = record1.copy()
self.assertEqual(record1._octodns, record2._octodns)
def test_healthcheck(self):
new = Record.new(
self.zone,
'a',
{
'ttl': 44,
'type': 'A',
'value': '1.2.3.4',
'octodns': {
'healthcheck': {
'path': '/_ready',
'host': 'bleep.bloop',
'protocol': 'HTTP',
'port': 8080,
}
},
},
)
self.assertEqual('/_ready', new.healthcheck_path)
self.assertEqual('bleep.bloop', new.healthcheck_host())
self.assertEqual('HTTP', new.healthcheck_protocol)
self.assertEqual(8080, new.healthcheck_port)
# empty host value in healthcheck
new = Record.new(
self.zone,
'a',
{
'ttl': 44,
'type': 'A',
'value': '1.2.3.4',
'octodns': {
'healthcheck': {
'path': '/_ready',
'host': None,
'protocol': 'HTTP',
'port': 8080,
}
},
},
)
self.assertEqual('1.2.3.4', new.healthcheck_host(value="1.2.3.4"))
new = Record.new(
self.zone, 'a', {'ttl': 44, 'type': 'A', 'value': '1.2.3.4'}
)
self.assertEqual('/_dns', new.healthcheck_path)
self.assertEqual('a.unit.tests', new.healthcheck_host())
self.assertEqual('HTTPS', new.healthcheck_protocol)
self.assertEqual(443, new.healthcheck_port)
def test_healthcheck_tcp(self):
new = Record.new(
self.zone,
'a',
{
'ttl': 44,
'type': 'A',
'value': '1.2.3.4',
'octodns': {
'healthcheck': {
'path': '/ignored',
'host': 'completely.ignored',
'protocol': 'TCP',
'port': 8080,
}
},
},
)
self.assertIsNone(new.healthcheck_path)
self.assertIsNone(new.healthcheck_host())
self.assertEqual('TCP', new.healthcheck_protocol)
self.assertEqual(8080, new.healthcheck_port)
new = Record.new(
self.zone,
'a',
{
'ttl': 44,
'type': 'A',
'value': '1.2.3.4',
'octodns': {'healthcheck': {'protocol': 'TCP'}},
},
)
self.assertIsNone(new.healthcheck_path)
self.assertIsNone(new.healthcheck_host())
self.assertEqual('TCP', new.healthcheck_protocol)
self.assertEqual(443, new.healthcheck_port)
def test_simple_a_weighted(self):
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'weight': 10, 'value': '3.3.3.3'}]},
'two': {
# Testing out of order value sorting here
'values': [{'value': '5.5.5.5'}, {'value': '4.4.4.4'}]
},
'three': {
'values': [
{'weight': 10, 'value': '4.4.4.4'},
{'weight': 12, 'value': '5.5.5.5'},
]
},
},
'rules': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'pool': 'one'},
],
},
'ttl': 60,
'values': ['1.1.1.1', '2.2.2.2'],
}
a = ARecord(self.zone, 'weighted', a_data)
self.assertEqual('A', a._type)
self.assertEqual(a_data['ttl'], a.ttl)
self.assertEqual(a_data['values'], a.values)
dynamic = a.dynamic
self.assertTrue(dynamic)
pools = dynamic.pools
self.assertTrue(pools)
self.assertEqual(
{'value': '3.3.3.3', 'weight': 1, 'status': 'obey'},
pools['one'].data['values'][0],
)
self.assertEqual(
[
{'value': '4.4.4.4', 'weight': 1, 'status': 'obey'},
{'value': '5.5.5.5', 'weight': 1, 'status': 'obey'},
],
pools['two'].data['values'],
)
self.assertEqual(
[
{'weight': 10, 'value': '4.4.4.4', 'status': 'obey'},
{'weight': 12, 'value': '5.5.5.5', 'status': 'obey'},
],
pools['three'].data['values'],
)
rules = dynamic.rules
self.assertTrue(rules)
self.assertEqual(a_data['dynamic']['rules'][0], rules[0].data)
# smoke test of _DynamicMixin.__repr__
a.__repr__()
delattr(a, 'values')
a.value = 'abc'
a.__repr__()
def test_simple_aaaa_weighted(self):
aaaa_data = {
'dynamic': {
'pools': {
'one': '2601:642:500:e210:62f8:1dff:feb8:9473',
'two': [
'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': ['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': {
# Testing out of order value sorting here
'values': [
{'value': '2601:642:500:e210:62f8:1dff:feb8:9475'},
{'value': '2601:642:500:e210:62f8:1dff:feb8:9474'},
]
},
'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,
'values': [
'2601:642:500:e210:62f8:1dff:feb8:9471',
'2601:642:500:e210:62f8:1dff:feb8:9472',
],
}
aaaa = AaaaRecord(self.zone, 'weighted', aaaa_data)
self.assertEqual('AAAA', aaaa._type)
self.assertEqual(aaaa_data['ttl'], aaaa.ttl)
self.assertEqual(aaaa_data['values'], aaaa.values)
dynamic = aaaa.dynamic
self.assertTrue(dynamic)
pools = dynamic.pools
self.assertTrue(pools)
self.assertEqual(
{
'value': '2601:642:500:e210:62f8:1dff:feb8:9473',
'weight': 1,
'status': 'obey',
},
pools['one'].data['values'][0],
)
self.assertEqual(
[
{
'value': '2601:642:500:e210:62f8:1dff:feb8:9474',
'weight': 1,
'status': 'obey',
},
{
'value': '2601:642:500:e210:62f8:1dff:feb8:9475',
'weight': 1,
'status': 'obey',
},
],
pools['two'].data['values'],
)
self.assertEqual(
[
{
'weight': 10,
'value': '2601:642:500:e210:62f8:1dff:feb8:9476',
'status': 'obey',
},
{
'weight': 12,
'value': '2601:642:500:e210:62f8:1dff:feb8:9477',
'status': 'obey',
},
],
pools['three'].data['values'],
)
rules = dynamic.rules
self.assertTrue(rules)
self.assertEqual(aaaa_data['dynamic']['rules'][0], rules[0].data)
def test_simple_cname_weighted(self):
cname_data = {
'dynamic': {
'pools': {
'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': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'pool': 'one'},
],
},
'ttl': 60,
'value': 'cname.target.',
}
cname = CnameRecord(self.zone, 'weighted', cname_data)
self.assertEqual('CNAME', cname._type)
self.assertEqual(cname_data['ttl'], cname.ttl)
self.assertEqual(cname_data['value'], cname.value)
dynamic = cname.dynamic
self.assertTrue(dynamic)
pools = dynamic.pools
self.assertTrue(pools)
self.assertEqual(
{'value': 'one.cname.target.', 'weight': 1, 'status': 'obey'},
pools['one'].data['values'][0],
)
self.assertEqual(
{'value': 'two.cname.target.', 'weight': 1, 'status': 'obey'},
pools['two'].data['values'][0],
)
self.assertEqual(
[
{
'value': 'three-1.cname.target.',
'weight': 12,
'status': 'obey',
},
{
'value': 'three-2.cname.target.',
'weight': 32,
'status': 'obey',
},
],
pools['three'].data['values'],
)
rules = dynamic.rules
self.assertTrue(rules)
self.assertEqual(cname_data['dynamic']['rules'][0], rules[0].data)
def test_dynamic_validation(self):
# Missing pools
a_data = {
'dynamic': {'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.assertEqual(
['missing pools', 'rule 1 undefined pool "one"'],
ctx.exception.reasons,
)
# Empty pools
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.assertEqual(
['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.assertEqual(
['pools must be a dict', 'rule 1 undefined pool "one"'],
ctx.exception.reasons,
)
# Invalid addresses
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': 'this-aint-right'}]},
'two': {
'fallback': 'one',
'values': [
{'value': '4.4.4.4'},
{'value': 'nor-is-this'},
],
},
'three': {
'fallback': 'two',
'values': [
{'weight': 1, 'value': '5.5.5.5'},
{'weight': 2, 'value': 'yet-another-bad-one'},
],
},
},
'rules': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
[
'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': {
'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': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
['pool "one" is missing values'], ctx.exception.reasons
)
# pool value not a dict
a_data = {
'dynamic': {
'pools': {
'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': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(['pool "one" must be a dict'], ctx.exception.reasons)
# empty pool value
a_data = {
'dynamic': {
'pools': {
'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': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
['pool "one" is missing values'], ctx.exception.reasons
)
# invalid int weight
a_data = {
'dynamic': {
'pools': {
'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': 101, 'value': '7.7.7.7'},
]
},
},
'rules': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
['invalid weight "101" in pool "three" value 2'],
ctx.exception.reasons,
)
# invalid non-int weight
a_data = {
'dynamic': {
'pools': {
'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': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
['invalid weight "foo" in pool "three" value 2'],
ctx.exception.reasons,
)
# single value with weight!=1
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'weight': 12, 'value': '6.6.6.6'}]}
},
'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.assertEqual(
['pool "one" has single value with weight!=1'],
ctx.exception.reasons,
)
# invalid fallback
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'fallback': 'invalid',
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}],
},
'three': {
'fallback': 'two',
'values': [
{'weight': 1, 'value': '6.6.6.6'},
{'weight': 5, 'value': '7.7.7.7'},
],
},
},
'rules': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
['undefined fallback "invalid" for pool "two"'],
ctx.exception.reasons,
)
# fallback loop
a_data = {
'dynamic': {
'pools': {
'one': {
'fallback': 'three',
'values': [{'value': '3.3.3.3'}],
},
'two': {
'fallback': 'one',
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}],
},
'three': {
'fallback': 'two',
'values': [
{'weight': 1, 'value': '6.6.6.6'},
{'weight': 5, 'value': '7.7.7.7'},
],
},
},
'rules': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
[
'loop in pool fallbacks: one -> three -> two',
'loop in pool fallbacks: three -> two -> one',
'loop in pool fallbacks: two -> one -> three',
],
ctx.exception.reasons,
)
# multiple pool problems
a_data = {
'dynamic': {
'pools': {
'one': '',
'two': {
'values': [{'value': '4.4.4.4'}, {'value': 'blip'}]
},
'three': {
'values': [
{'weight': 1},
{'weight': 5000, 'value': '7.7.7.7'},
]
},
},
'rules': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'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.assertEqual(
[
'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,
)
# missing rules, and unused pools
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
}
},
'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.assertEqual(
['missing rules', 'unused pools: "one", "two"'],
ctx.exception.reasons,
)
# empty rules
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.assertEqual(
['missing rules', 'unused pools: "one", "two"'],
ctx.exception.reasons,
)
# 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.assertEqual(
['rules must be a list', 'unused pools: "one", "two"'],
ctx.exception.reasons,
)
# rule without pool
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [{'geos': ['NA-US-CA']}, {'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.assertEqual(
['rule 1 missing pool', 'unused pools: "two"'],
ctx.exception.reasons,
)
# rule with non-string pools
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [{'geos': ['NA-US-CA'], 'pool': []}, {'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.assertEqual(
['rule 1 invalid pool "[]"', 'unused pools: "two"'],
ctx.exception.reasons,
)
# rule references non-existent pool
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [
{'geos': ['NA-US-CA'], 'pool': 'non-existent'},
{'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.assertEqual(
["rule 1 undefined pool \"non-existent\"", 'unused pools: "two"'],
ctx.exception.reasons,
)
# rule with invalid subnets
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [
{'subnets': '10.1.0.0/16', 'pool': 'two'},
{'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.assertEqual(
['rule 1 subnets must be a list'], ctx.exception.reasons
)
# rule with invalid subnet
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [
{'subnets': ['invalid'], 'pool': 'two'},
{'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.assertEqual(
['rule 1 invalid subnet "invalid"'], ctx.exception.reasons
)
# rule with invalid geos
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [{'geos': 'NA-US-CA', 'pool': 'two'}, {'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.assertEqual(['rule 1 geos must be a list'], ctx.exception.reasons)
# rule with invalid geo
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [
{'geos': ['invalid'], 'pool': 'two'},
{'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.assertEqual(
['rule 1 unknown continent code "invalid"'], ctx.exception.reasons
)
# multiple default rules
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [{'pool': 'two'}, {'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.assertEqual(['rule 2 duplicate default'], ctx.exception.reasons)
# repeated pool in rules
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [
{'geos': ['EU'], 'pool': 'two'},
{'geos': ['AF'], 'pool': 'one'},
{'geos': ['OC'], 'pool': 'one'},
{'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.assertEqual(
['rule 3 invalid, target pool "one" reused'], ctx.exception.reasons
)
# Repeated pool is OK if later one is a default
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [
{'geos': ['EU-GB'], 'pool': 'one'},
{'geos': ['EU'], 'pool': 'two'},
{'pool': 'one'},
],
},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
}
# This should be valid, no exception
Record.new(self.zone, 'bad', a_data)
# invalid status
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '2.2.2.2', 'status': 'none'}]}
},
'rules': [{'pool': 'one'}],
},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1'],
}
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, 'bad', a_data)
self.assertIn('invalid status', ctx.exception.reasons[0])
def test_dynamic_lenient(self):
# Missing pools
a_data = {
'dynamic': {
'rules': [{'geos': ['EU'], 'pool': 'two'}, {'pool': 'one'}]
},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
}
a = Record.new(self.zone, 'bad', a_data, lenient=True)
self.assertEqual(
{'pools': {}, 'rules': a_data['dynamic']['rules']},
a._data()['dynamic'],
)
# Missing rule
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [
{'value': '4.4.4.4'},
{'value': '5.5.5.5', 'weight': 2},
]
},
}
},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
}
a = Record.new(self.zone, 'bad', a_data, lenient=True)
self.assertEqual(
{
'pools': {
'one': {
'fallback': None,
'values': [
{'value': '3.3.3.3', 'weight': 1, 'status': 'obey'}
],
},
'two': {
'fallback': None,
'values': [
{'value': '4.4.4.4', 'weight': 1, 'status': 'obey'},
{'value': '5.5.5.5', 'weight': 2, 'status': 'obey'},
],
},
},
'rules': [],
},
a._data()['dynamic'],
)
# rule without pool
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [
{'value': '4.4.4.4'},
{'value': '5.5.5.5', 'weight': 2},
]
},
},
'rules': [{'geos': ['EU'], 'pool': 'two'}, {}],
},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
}
a = Record.new(self.zone, 'bad', a_data, lenient=True)
self.assertEqual(
{
'pools': {
'one': {
'fallback': None,
'values': [
{'value': '3.3.3.3', 'weight': 1, 'status': 'obey'}
],
},
'two': {
'fallback': None,
'values': [
{'value': '4.4.4.4', 'weight': 1, 'status': 'obey'},
{'value': '5.5.5.5', 'weight': 2, 'status': 'obey'},
],
},
},
'rules': a_data['dynamic']['rules'],
},
a._data()['dynamic'],
)
def test_dynamic_changes(self):
simple = SimpleProvider()
dynamic = DynamicProvider()
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [{'geos': ['EU'], 'pool': 'two'}, {'pool': 'one'}],
},
'ttl': 60,
'values': ['1.1.1.1', '2.2.2.2'],
}
a = ARecord(self.zone, 'weighted', a_data)
dup = ARecord(self.zone, 'weighted', a_data)
b_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [
{'value': '4.4.4.4', 'weight': 2},
{'value': '5.5.5.5'},
]
},
},
'rules': [{'geos': ['EU'], 'pool': 'two'}, {'pool': 'one'}],
},
'ttl': 60,
'values': ['1.1.1.1', '2.2.2.2'],
}
b = ARecord(self.zone, 'weighted', b_data)
c_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
},
'rules': [{'geos': ['NA'], 'pool': 'two'}, {'pool': 'one'}],
},
'ttl': 60,
'values': ['1.1.1.1', '2.2.2.2'],
}
c = ARecord(self.zone, 'weighted', c_data)
# a changes a (identical dup) is never true
self.assertFalse(a.changes(dup, simple))
self.assertFalse(a.changes(dup, dynamic))
# a changes b is not true for simple
self.assertFalse(a.changes(b, simple))
# but is true for dynamic
update = a.changes(b, dynamic)
self.assertEqual(a, update.existing)
self.assertEqual(b, update.new)
# transitive
self.assertFalse(b.changes(a, simple))
update = b.changes(a, dynamic)
self.assertEqual(a, update.existing)
self.assertEqual(b, update.new)
# same for a change c
self.assertFalse(a.changes(c, simple))
self.assertTrue(a.changes(c, dynamic))
self.assertFalse(c.changes(a, simple))
self.assertTrue(c.changes(a, dynamic))
# smoke test some of the equiality bits
self.assertEqual(a.dynamic.pools, a.dynamic.pools)
self.assertEqual(a.dynamic.pools['one'], a.dynamic.pools['one'])
self.assertNotEqual(a.dynamic.pools['one'], a.dynamic.pools['two'])
self.assertEqual(a.dynamic.rules, a.dynamic.rules)
self.assertEqual(a.dynamic.rules[0], a.dynamic.rules[0])
self.assertNotEqual(a.dynamic.rules[0], c.dynamic.rules[0])
def test_dynamic_and_geo_validation(self):
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '5.5.5.5'}, {'value': '4.4.4.4'}]
},
'three': {
'values': [
{'weight': 10, 'value': '4.4.4.4'},
{'weight': 12, 'value': '5.5.5.5'},
]
},
},
'rules': [
{'geos': ['AF', 'EU'], 'pool': 'three'},
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'pool': 'one'},
],
},
'geo': {'NA': ['1.2.3.5'], 'NA-US': ['1.2.3.5', '1.2.3.6']},
'type': 'A',
'ttl': 60,
'values': ['1.1.1.1', '2.2.2.2'],
}
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, 'bad', a_data)
self.assertEqual(
['"dynamic" record with "geo" content'], ctx.exception.reasons
)
def test_dynamic_eqs(self):
pool_one = _DynamicPool(
'one', {'values': [{'value': '1.2.3.4'}]}, Ipv4Value
)
pool_two = _DynamicPool(
'two', {'values': [{'value': '1.2.3.5'}]}, Ipv4Value
)
self.assertEqual(pool_one, pool_one)
self.assertNotEqual(pool_one, pool_two)
self.assertNotEqual(pool_one, 42)
pools = {'one': pool_one, 'two': pool_two}
rule_one = _DynamicRule(0, {'pool': 'one'})
rule_two = _DynamicRule(1, {'pool': 'two'})
self.assertEqual(rule_one, rule_one)
self.assertNotEqual(rule_one, rule_two)
self.assertNotEqual(rule_one, 42)
rules = [rule_one, rule_two]
dynamic = _Dynamic(pools, rules)
other = _Dynamic({}, [])
self.assertEqual(dynamic, dynamic)
self.assertNotEqual(dynamic, other)
self.assertNotEqual(dynamic, 42)
def test_dynamic_cname_idna(self):
a_utf8 = 'natación.mx.'
a_encoded = idna_encode(a_utf8)
b_utf8 = 'гэрбүл.mn.'
b_encoded = idna_encode(b_utf8)
cname_data = {
'dynamic': {
'pools': {
'one': {
# Testing out of order value sorting here
'values': [
{'value': 'b.unit.tests.'},
{'value': 'a.unit.tests.'},
]
},
'two': {
'values': [
# some utf8 values we expect to be idna encoded
{'weight': 10, 'value': a_utf8},
{'weight': 12, 'value': b_utf8},
]
},
},
'rules': [
{'geos': ['NA-US-CA'], 'pool': 'two'},
{'pool': 'one'},
],
},
'type': 'CNAME',
'ttl': 60,
'value': a_utf8,
}
cname = Record.new(self.zone, 'cname', cname_data)
self.assertEqual(a_encoded, cname.value)
self.assertEqual(
{
'fallback': None,
'values': [
{'weight': 1, 'value': 'a.unit.tests.', 'status': 'obey'},
{'weight': 1, 'value': 'b.unit.tests.', 'status': 'obey'},
],
},
cname.dynamic.pools['one'].data,
)
self.assertEqual(
{
'fallback': None,
'values': [
{'weight': 12, 'value': b_encoded, 'status': 'obey'},
{'weight': 10, 'value': a_encoded, 'status': 'obey'},
],
},
cname.dynamic.pools['two'].data,
)
def test_dynamic_mixin_validate_rules(self):
# this one is fine we get more generic with subsequent rules
pools = {'iad', 'sfo'}
rules = [
{'geos': ('AS', 'NA-CA', 'NA-US-OR'), 'pool': 'sfo'},
{'geos': ('EU', 'NA'), 'pool': 'iad'},
{'pool': 'iad'},
]
reasons, pools_seen = _DynamicMixin._validate_rules(pools, rules)
self.assertFalse(reasons)
self.assertEqual({'sfo', 'iad'}, pools_seen)
# this one targets NA in rule 0 and then NA-Ca in rule 1
pools = {'iad', 'sfo'}
rules = [
{'geos': ('AS', 'NA'), 'pool': 'sfo'},
{'geos': ('EU', 'NA-CA'), 'pool': 'iad'},
{'pool': 'iad'},
]
reasons, pools_seen = _DynamicMixin._validate_rules(pools, rules)
self.assertEqual(
[
'rule 2 targets geo NA-CA which is more specific than the previously seen NA in rule 1'
],
reasons,
)
# this one targets NA and NA-US in rule 0
pools = {'iad', 'sfo'}
rules = [
{'geos': ('AS', 'NA-US', 'NA'), 'pool': 'sfo'},
{'pool': 'iad'},
]
reasons, pools_seen = _DynamicMixin._validate_rules(pools, rules)
self.assertEqual(
[
'rule 1 targets geo NA-US which is more specific than the previously seen NA in rule 1'
],
reasons,
)
# this one targets the same geo in multiple rules
pools = {'iad', 'sfo'}
rules = [
{'geos': ('AS', 'NA'), 'pool': 'sfo'},
{'geos': ('EU', 'NA'), 'pool': 'iad'},
{'pool': 'iad'},
]
reasons, pools_seen = _DynamicMixin._validate_rules(pools, rules)
self.assertEqual(
['rule 2 targets geo NA which has previously been seen in rule 1'],
reasons,
)
# this one doesn't have a catch-all rule at the end
pools = {'iad', 'sfo'}
rules = [
{'geos': ('AS', 'NA-CA', 'NA-US-OR'), 'pool': 'sfo'},
{'geos': ('EU', 'NA'), 'pool': 'iad'},
]
reasons, pools_seen = _DynamicMixin._validate_rules(pools, rules)
self.assertEqual(
['final rule has "subnets" and/or "geos" and is not catchall'],
reasons,
)
def test_dynamic_subnet_rule_ordering(self):
# boiler plate
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
'three': {'values': [{'value': '2.2.2.2'}]},
}
},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
}
dynamic = a_data['dynamic']
def validate_rules(rules):
dynamic['rules'] = rules
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, 'bad', a_data)
return ctx.exception.reasons
# valid subnet-only → subnet+geo
dynamic['rules'] = [
{'subnets': ['10.1.0.0/16'], 'pool': 'one'},
{'subnets': ['11.1.0.0/16'], 'geos': ['NA'], 'pool': 'two'},
{'pool': 'three'},
]
record = Record.new(self.zone, 'good', a_data)
self.assertEqual(
'10.1.0.0/16', record.dynamic.rules[0].data['subnets'][0]
)
# geo-only → subnet-only
self.assertEqual(
[
'rule 2 with only subnet targeting should appear before all geo targeting rules'
],
validate_rules(
[
{'geos': ['NA'], 'pool': 'two'},
{'subnets': ['10.1.0.0/16'], 'pool': 'one'},
{'pool': 'three'},
]
),
)
# geo-only → subnet+geo
self.assertEqual(
[
'rule 2 with subnet(s) and geo(s) should appear before all geo-only rules'
],
validate_rules(
[
{'geos': ['NA'], 'pool': 'two'},
{'subnets': ['10.1.0.0/16'], 'geos': ['AS'], 'pool': 'one'},
{'pool': 'three'},
]
),
)
# subnet+geo → subnet-only
self.assertEqual(
[
'rule 2 with only subnet targeting should appear before all geo targeting rules'
],
validate_rules(
[
{'subnets': ['11.1.0.0/16'], 'geos': ['NA'], 'pool': 'two'},
{'subnets': ['10.1.0.0/16'], 'pool': 'one'},
{'pool': 'three'},
]
),
)
# geo-only → subnet+geo → subnet-only
self.assertEqual(
[
'rule 2 with subnet(s) and geo(s) should appear before all geo-only rules',
'rule 3 with only subnet targeting should appear before all geo targeting rules',
],
validate_rules(
[
{'geos': ['NA'], 'pool': 'two'},
{'subnets': ['10.1.0.0/16'], 'geos': ['AS'], 'pool': 'one'},
{'subnets': ['11.1.0.0/16'], 'pool': 'three'},
{'pool': 'one'},
]
),
)
def test_dynanic_subnet_ordering(self):
# boiler plate
a_data = {
'dynamic': {
'pools': {
'one': {'values': [{'value': '3.3.3.3'}]},
'two': {
'values': [{'value': '4.4.4.4'}, {'value': '5.5.5.5'}]
},
'three': {'values': [{'value': '2.2.2.2'}]},
}
},
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
}
dynamic = a_data['dynamic']
def validate_rules(rules):
dynamic['rules'] = rules
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, 'bad', a_data)
return ctx.exception.reasons
# duplicate subnet
self.assertEqual(
[
'rule 2 targets subnet 10.1.0.0/16 which has previously been seen in rule 1'
],
validate_rules(
[
{'subnets': ['10.1.0.0/16'], 'pool': 'two'},
{'subnets': ['10.1.0.0/16'], 'pool': 'one'},
{'pool': 'three'},
]
),
)
# more specific subnet than previous
self.assertEqual(
[
'rule 2 targets subnet 10.1.1.0/24 which is more specific than the previously seen 10.1.0.0/16 in rule 1'
],
validate_rules(
[
{'subnets': ['10.1.0.0/16'], 'pool': 'two'},
{'subnets': ['10.1.1.0/24'], 'pool': 'one'},
{'pool': 'three'},
]
),
)
# sub-subnet in the same rule
self.assertEqual(
[
'rule 1 targets subnet 10.1.1.0/24 which is more specific than the previously seen 10.1.0.0/16 in rule 1'
],
validate_rules(
[
{'subnets': ['10.1.0.0/16', '10.1.1.0/24'], 'pool': 'two'},
{'subnets': ['11.1.0.0/16'], 'pool': 'one'},
{'pool': 'three'},
]
),
)