mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
1564 lines
52 KiB
Python
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'},
|
|
]
|
|
),
|
|
)
|