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_provider_dyn.py
2019-10-07 09:31:15 -07:00

2653 lines
92 KiB
Python

#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from dyn.tm.errors import DynectGetError
from dyn.tm.services.dsf import DSFResponsePool
from json import loads
from mock import MagicMock, call, patch
from unittest import TestCase
from octodns.record import Create, Delete, Record, Update
from octodns.provider.base import Plan
from octodns.provider.dyn import DynProvider, _CachingDynZone, DSFMonitor, \
_dynamic_value_sort_key
from octodns.zone import Zone
from helpers import SimpleProvider
class _DummyPool(object):
def __init__(self, response_pool_id):
self.response_pool_id = response_pool_id
self.deleted = False
def delete(self):
self.deleted = True
class TestDynProvider(TestCase):
expected = Zone('unit.tests.', [])
for name, data in (
('', {
'type': 'A',
'ttl': 300,
'values': ['1.2.3.4']
}),
('cname', {
'type': 'CNAME',
'ttl': 301,
'value': 'unit.tests.'
}),
('', {
'type': 'MX',
'ttl': 302,
'values': [{
'preference': 10,
'exchange': 'smtp-1.unit.tests.'
}, {
'preference': 20,
'exchange': 'smtp-2.unit.tests.'
}]
}),
('naptr', {
'type': 'NAPTR',
'ttl': 303,
'values': [{
'order': 100,
'preference': 101,
'flags': 'U',
'service': 'SIP+D2U',
'regexp': '!^.*$!sip:info@foo.example.com!',
'replacement': '.',
}, {
'order': 200,
'preference': 201,
'flags': 'U',
'service': 'SIP+D2U',
'regexp': '!^.*$!sip:info@bar.example.com!',
'replacement': '.',
}]
}),
('sub', {
'type': 'NS',
'ttl': 3600,
'values': ['ns3.p10.dynect.net.', 'ns3.p10.dynect.net.'],
}),
('ptr', {
'type': 'PTR',
'ttl': 304,
'value': 'xx.unit.tests.'
}),
('spf', {
'type': 'SPF',
'ttl': 305,
'values': ['v=spf1 ip4:192.168.0.1/16-all', 'v=spf1 -all'],
}),
('', {
'type': 'SSHFP',
'ttl': 306,
'value': {
'algorithm': 1,
'fingerprint_type': 1,
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73',
}
}),
('_srv._tcp', {
'type': 'SRV',
'ttl': 307,
'values': [{
'priority': 11,
'weight': 12,
'port': 10,
'target': 'foo-1.unit.tests.'
}, {
'priority': 21,
'weight': 22,
'port': 20,
'target': 'foo-2.unit.tests.'
}]}),
('', {
'type': 'CAA',
'ttl': 308,
'values': [{
'flags': 0,
'tag': 'issue',
'value': 'ca.unit.tests'
}]})):
expected.add_record(Record.new(expected, name, data))
@classmethod
def setUpClass(self):
# Get the DynectSession creation out of the way so that tests can
# ignore it
with patch('dyn.core.SessionEngine.execute',
return_value={'status': 'success'}):
provider = DynProvider('test', 'cust', 'user', 'pass')
provider._check_dyn_sess()
def setUp(self):
# Flush our zone to ensure we start fresh
_CachingDynZone.flush_zone(self.expected.name[:-1])
@patch('dyn.core.SessionEngine.execute')
def test_populate_non_existent(self, execute_mock):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Test Zone create
execute_mock.side_effect = [
DynectGetError('foo'),
]
got = Zone('unit.tests.', [])
provider.populate(got)
execute_mock.assert_has_calls([
call('/Zone/unit.tests/', 'GET', {}),
])
self.assertEquals(set(), got.records)
@patch('dyn.core.SessionEngine.execute')
def test_populate(self, execute_mock):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Test Zone create
execute_mock.side_effect = [
# get Zone
{'data': {}},
# get_all_records
{'data': {
'a_records': [{
'fqdn': 'unit.tests',
'rdata': {'address': '1.2.3.4'},
'record_id': 1,
'record_type': 'A',
'ttl': 300,
'zone': 'unit.tests',
}],
'cname_records': [{
'fqdn': 'cname.unit.tests',
'rdata': {'cname': 'unit.tests.'},
'record_id': 2,
'record_type': 'CNAME',
'ttl': 301,
'zone': 'unit.tests',
}],
'ns_records': [{
'fqdn': 'unit.tests',
'rdata': {'nsdname': 'ns1.p10.dynect.net.'},
'record_id': 254597562,
'record_type': 'NS',
'service_class': '',
'ttl': 3600,
'zone': 'unit.tests'
}, {
'fqdn': 'unit.tests',
'rdata': {'nsdname': 'ns2.p10.dynect.net.'},
'record_id': 254597563,
'record_type': 'NS',
'service_class': '',
'ttl': 3600,
'zone': 'unit.tests'
}, {
'fqdn': 'unit.tests',
'rdata': {'nsdname': 'ns3.p10.dynect.net.'},
'record_id': 254597564,
'record_type': 'NS',
'service_class': '',
'ttl': 3600,
'zone': 'unit.tests'
}, {
'fqdn': 'unit.tests',
'rdata': {'nsdname': 'ns4.p10.dynect.net.'},
'record_id': 254597565,
'record_type': 'NS',
'service_class': '',
'ttl': 3600,
'zone': 'unit.tests'
}, {
'fqdn': 'sub.unit.tests',
'rdata': {'nsdname': 'ns3.p10.dynect.net.'},
'record_id': 254597564,
'record_type': 'NS',
'service_class': '',
'ttl': 3600,
'zone': 'unit.tests'
}, {
'fqdn': 'sub.unit.tests',
'rdata': {'nsdname': 'ns3.p10.dynect.net.'},
'record_id': 254597564,
'record_type': 'NS',
'service_class': '',
'ttl': 3600,
'zone': 'unit.tests'
}],
'mx_records': [{
'fqdn': 'unit.tests',
'rdata': {'exchange': 'smtp-1.unit.tests.',
'preference': 10},
'record_id': 3,
'record_type': 'MX',
'ttl': 302,
'zone': 'unit.tests',
}, {
'fqdn': 'unit.tests',
'rdata': {'exchange': 'smtp-2.unit.tests.',
'preference': 20},
'record_id': 4,
'record_type': 'MX',
'ttl': 302,
'zone': 'unit.tests',
}],
'naptr_records': [{
'fqdn': 'naptr.unit.tests',
'rdata': {'flags': 'U',
'order': 100,
'preference': 101,
'regexp': '!^.*$!sip:info@foo.example.com!',
'replacement': '.',
'services': 'SIP+D2U'},
'record_id': 5,
'record_type': 'MX',
'ttl': 303,
'zone': 'unit.tests',
}, {
'fqdn': 'naptr.unit.tests',
'rdata': {'flags': 'U',
'order': 200,
'preference': 201,
'regexp': '!^.*$!sip:info@bar.example.com!',
'replacement': '.',
'services': 'SIP+D2U'},
'record_id': 6,
'record_type': 'MX',
'ttl': 303,
'zone': 'unit.tests',
}],
'ptr_records': [{
'fqdn': 'ptr.unit.tests',
'rdata': {'ptrdname': 'xx.unit.tests.'},
'record_id': 7,
'record_type': 'PTR',
'ttl': 304,
'zone': 'unit.tests',
}],
'soa_records': [{
'fqdn': 'unit.tests',
'rdata': {'txtdata': 'ns1.p16.dynect.net. '
'hostmaster.unit.tests. 4 3600 600 604800 1800'},
'record_id': 99,
'record_type': 'SOA',
'ttl': 299,
'zone': 'unit.tests',
}],
'spf_records': [{
'fqdn': 'spf.unit.tests',
'rdata': {'txtdata': 'v=spf1 ip4:192.168.0.1/16-all'},
'record_id': 8,
'record_type': 'SPF',
'ttl': 305,
'zone': 'unit.tests',
}, {
'fqdn': 'spf.unit.tests',
'rdata': {'txtdata': 'v=spf1 -all'},
'record_id': 8,
'record_type': 'SPF',
'ttl': 305,
'zone': 'unit.tests',
}],
'sshfp_records': [{
'fqdn': 'unit.tests',
'rdata': {'algorithm': 1,
'fingerprint':
'bf6b6825d2977c511a475bbefb88aad54a92ac73',
'fptype': 1},
'record_id': 9,
'record_type': 'SSHFP',
'ttl': 306,
'zone': 'unit.tests',
}],
'srv_records': [{
'fqdn': '_srv._tcp.unit.tests',
'rdata': {'port': 10,
'priority': 11,
'target': 'foo-1.unit.tests.',
'weight': 12},
'record_id': 10,
'record_type': 'SRV',
'ttl': 307,
'zone': 'unit.tests',
}, {
'fqdn': '_srv._tcp.unit.tests',
'rdata': {'port': 20,
'priority': 21,
'target': 'foo-2.unit.tests.',
'weight': 22},
'record_id': 11,
'record_type': 'SRV',
'ttl': 307,
'zone': 'unit.tests',
}],
'caa_records': [{
'fqdn': 'unit.tests',
'rdata': {'flags': 0,
'tag': 'issue',
'value': 'ca.unit.tests'},
'record_id': 12,
'record_type': 'cAA',
'ttl': 308,
'zone': 'unit.tests',
}],
}}
]
got = Zone('unit.tests.', [])
provider.populate(got)
execute_mock.assert_has_calls([
call('/Zone/unit.tests/', 'GET', {}),
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
])
changes = self.expected.changes(got, SimpleProvider())
self.assertEquals([], changes)
@patch('dyn.core.SessionEngine.execute')
def test_sync(self, execute_mock):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Test Zone create
execute_mock.side_effect = [
# No such zone, during populate
DynectGetError('foo'),
# No such zone, during sync
DynectGetError('foo'),
# get empty Zone
{'data': {}},
# get zone we can modify & delete with
{'data': {
# A top-level to delete
'a_records': [{
'fqdn': 'unit.tests',
'rdata': {'address': '1.2.3.4'},
'record_id': 1,
'record_type': 'A',
'ttl': 30,
'zone': 'unit.tests',
}, {
'fqdn': 'a.unit.tests',
'rdata': {'address': '2.3.4.5'},
'record_id': 2,
'record_type': 'A',
'ttl': 30,
'zone': 'unit.tests',
}],
# A node to delete
'cname_records': [{
'fqdn': 'cname.unit.tests',
'rdata': {'cname': 'unit.tests.'},
'record_id': 3,
'record_type': 'CNAME',
'ttl': 30,
'zone': 'unit.tests',
}],
# A record to leave alone
'ptr_records': [{
'fqdn': 'ptr.unit.tests',
'rdata': {'ptrdname': 'xx.unit.tests.'},
'record_id': 4,
'record_type': 'PTR',
'ttl': 30,
'zone': 'unit.tests',
}],
# A record to modify
'srv_records': [{
'fqdn': '_srv._tcp.unit.tests',
'rdata': {'port': 10,
'priority': 11,
'target': 'foo-1.unit.tests.',
'weight': 12},
'record_id': 5,
'record_type': 'SRV',
'ttl': 30,
'zone': 'unit.tests',
}, {
'fqdn': '_srv._tcp.unit.tests',
'rdata': {'port': 20,
'priority': 21,
'target': 'foo-2.unit.tests.',
'weight': 22},
'record_id': 6,
'record_type': 'SRV',
'ttl': 30,
'zone': 'unit.tests',
}],
}}
]
# No existing records, create all
with patch('dyn.tm.zones.Zone.add_record') as add_mock:
with patch('dyn.tm.zones.Zone._update') as update_mock:
plan = provider.plan(self.expected)
update_mock.assert_not_called()
provider.apply(plan)
update_mock.assert_called()
self.assertFalse(plan.exists)
add_mock.assert_called()
# Once for each dyn record (8 Records, 2 of which have dual values)
self.assertEquals(15, len(add_mock.call_args_list))
execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}),
call('/Zone/unit.tests/', 'GET', {})])
self.assertEquals(10, len(plan.changes))
execute_mock.reset_mock()
# Delete one and modify another
new = Zone('unit.tests.', [])
for name, data in (
('a', {
'type': 'A',
'ttl': 30,
'value': '2.3.4.5'
}),
('ptr', {
'type': 'PTR',
'ttl': 30,
'value': 'xx.unit.tests.'
}),
('_srv._tcp', {
'type': 'SRV',
'ttl': 30,
'values': [{
'priority': 31,
'weight': 12,
'port': 10,
'target': 'foo-1.unit.tests.'
}, {
'priority': 21,
'weight': 22,
'port': 20,
'target': 'foo-2.unit.tests.'
}]})):
new.add_record(Record.new(new, name, data))
with patch('dyn.tm.zones.Zone.add_record') as add_mock:
with patch('dyn.tm.records.DNSRecord.delete') as delete_mock:
with patch('dyn.tm.zones.Zone._update') as update_mock:
plan = provider.plan(new)
provider.apply(plan)
update_mock.assert_called()
self.assertTrue(plan.exists)
# we expect 4 deletes, 2 from actual deletes and 2 from
# updates which delete and recreate
self.assertEquals(4, len(delete_mock.call_args_list))
# the 2 (re)creates
self.assertEquals(2, len(add_mock.call_args_list))
execute_mock.assert_has_calls([
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
])
self.assertEquals(3, len(plan.changes))
class TestDynProviderGeo(TestCase):
with open('./tests/fixtures/dyn-traffic-director-get.json') as fh:
traffic_director_response = loads(fh.read())
@property
def traffic_directors_response(self):
return {
'data': [{
'active': 'Y',
'label': 'unit.tests.:A',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}, {
'active': 'Y',
'label': 'some.other.:A',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}, {
'active': 'Y',
'label': 'other format',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}]
}
# Doing this as a property so that we get a fresh copy each time, dyn's
# client lib messes with the return value and prevents it from working on
# subsequent uses otherwise
@property
def records_response(self):
return {
'data': {
'a_records': [{
'fqdn': 'unit.tests',
'rdata': {'address': '1.2.3.4'},
'record_id': 1,
'record_type': 'A',
'ttl': 301,
'zone': 'unit.tests',
}],
}
}
monitor_id = '42a'
monitors_response = {
'data': [{
'active': 'Y',
'agent_scheme': 'geo',
'dsf_monitor_id': monitor_id,
'endpoints': [],
'label': 'unit.tests.:A',
'notifier': [],
'expected': '',
'header': 'User-Agent: Dyn Monitor',
'host': 'unit.tests',
'path': '/_dns',
'port': '443',
'timeout': '10',
'probe_interval': '60',
'protocol': 'HTTPS',
'response_count': '2',
'retries': '2',
'services': ['12311']
}, {
'active': 'Y',
'agent_scheme': 'geo',
'dsf_monitor_id': 'b52',
'endpoints': [],
'label': 'old-label.unit.tests.',
'notifier': [],
'expected': '',
'header': 'User-Agent: Dyn Monitor',
'host': 'old-label.unit.tests',
'path': '/_dns',
'port': '443',
'timeout': '10',
'probe_interval': '60',
'protocol': 'HTTPS',
'response_count': '2',
'retries': '2',
'services': ['12312']
}],
'job_id': 3376281406,
'msgs': [{
'ERR_CD': None,
'INFO': 'DSFMonitor_get: Here are your monitors',
'LVL': 'INFO',
'SOURCE': 'BLL'
}],
'status': 'success'
}
expected_geo = Zone('unit.tests.', [])
geo_record = Record.new(expected_geo, '', {
'geo': {
'AF': ['2.2.3.4', '2.2.3.5'],
'AS-JP': ['3.2.3.4', '3.2.3.5'],
'NA-US': ['4.2.3.4', '4.2.3.5'],
'NA-US-CA': ['5.2.3.4', '5.2.3.5']
},
'ttl': 300,
'type': 'A',
'values': ['1.2.3.4', '1.2.3.5'],
})
expected_geo.add_record(geo_record)
expected_regular = Zone('unit.tests.', [])
regular_record = Record.new(expected_regular, '', {
'ttl': 301,
'type': 'A',
'value': '1.2.3.4',
})
expected_regular.add_record(regular_record)
def setUp(self):
# Flush our zone to ensure we start fresh
_CachingDynZone.flush_zone('unit.tests')
@patch('dyn.core.SessionEngine.execute')
def test_traffic_directors(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass', True)
# short-circuit session checking
provider._dyn_sess = True
provider.log.warn = MagicMock()
# no tds
mock.side_effect = [{'data': []}]
self.assertEquals({}, provider.traffic_directors)
# a supported td and an ignored one
response = {
'data': [{
'active': 'Y',
'label': 'unit.tests.:A',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}, {
'active': 'Y',
'label': 'geo.unit.tests.:A',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}, {
'active': 'Y',
'label': 'something else',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}],
'job_id': 3376164583,
'status': 'success'
}
mock.side_effect = [response]
# first make sure that we get the empty version from cache
self.assertEquals({}, provider.traffic_directors)
# reach in and bust the cache
provider._traffic_directors = None
tds = provider.traffic_directors
self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']),
set(tds.keys()))
self.assertEquals(['A'], list(tds['unit.tests.'].keys()))
self.assertEquals(['A'], list(tds['geo.unit.tests.'].keys()))
provider.log.warn.assert_called_with("Unsupported TrafficDirector "
"'%s'", 'something else')
@patch('dyn.core.SessionEngine.execute')
def test_traffic_director_monitor(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass', True)
# short-circuit session checking
provider._dyn_sess = True
existing = Zone('unit.tests.', [])
# no monitors, will try and create
geo_monitor_id = '42x'
mock.side_effect = [self.monitors_response, {
'data': {
'active': 'Y',
'dsf_monitor_id': geo_monitor_id,
'endpoints': [],
'label': 'geo.unit.tests.:A',
'notifier': '',
'expected': '',
'header': 'User-Agent: Dyn Monitor',
'host': 'geo.unit.tests.',
'path': '/_dns',
'port': '443',
'timeout': '10',
'probe_interval': '60',
'protocol': 'HTTPS',
'response_count': '2',
'retries': '2'
},
'job_id': 3376259461,
'msgs': [{'ERR_CD': None,
'INFO': 'add: Here is the new monitor',
'LVL': 'INFO',
'SOURCE': 'BLL'}],
'status': 'success'
}]
# ask for a monitor that doesn't exist
record = Record.new(existing, 'geo', {
'ttl': 60,
'type': 'A',
'value': '1.2.3.4',
'octodns': {
'healthcheck': {
'host': 'foo.bar',
'path': '/_ready'
}
}
})
monitor = provider._traffic_director_monitor(record)
self.assertEquals(geo_monitor_id, monitor.dsf_monitor_id)
# should see a request for the list and a create
mock.assert_has_calls([
call('/DSFMonitor/', 'GET', {'detail': 'Y'}),
call('/DSFMonitor/', 'POST', {
'retries': 2,
'protocol': 'HTTPS',
'response_count': 2,
'label': 'geo.unit.tests.:A',
'probe_interval': 60,
'active': 'Y',
'options': {
'path': '/_ready',
'host': 'foo.bar',
'header': 'User-Agent: Dyn Monitor',
'port': 443,
'timeout': 10
}
})
])
# created monitor is now cached
self.assertTrue('geo.unit.tests.:A' in
provider._traffic_director_monitors)
# pre-existing one is there too
self.assertTrue('unit.tests.:A' in
provider._traffic_director_monitors)
# now ask for a monitor that does exist
record = Record.new(existing, '', {
'ttl': 60,
'type': 'A',
'value': '1.2.3.4'
})
mock.reset_mock()
monitor = provider._traffic_director_monitor(record)
self.assertEquals(self.monitor_id, monitor.dsf_monitor_id)
# should have resulted in no calls b/c exists & we've cached the list
mock.assert_not_called()
# and finally for a monitor that exists, but with a differing config
record = Record.new(existing, '', {
'octodns': {
'healthcheck': {
'host': 'bleep.bloop',
'path': '/_nope',
'protocol': 'HTTP',
'port': 8080,
}
},
'ttl': 60,
'type': 'A',
'value': '1.2.3.4'
})
mock.reset_mock()
mock.side_effect = [{
'data': {
'active': 'Y',
'dsf_monitor_id': self.monitor_id,
'endpoints': [],
'label': 'unit.tests.:A',
'notifier': '',
'expected': '',
'header': 'User-Agent: Dyn Monitor',
'host': 'bleep.bloop',
'path': '/_nope',
'port': '8080',
'timeout': '10',
'probe_interval': '60',
'protocol': 'HTTP',
'response_count': '2',
'retries': '2'
},
'job_id': 3376259461,
'msgs': [{'ERR_CD': None,
'INFO': 'add: Here is the new monitor',
'LVL': 'INFO',
'SOURCE': 'BLL'}],
'status': 'success'
}]
monitor = provider._traffic_director_monitor(record)
self.assertEquals(self.monitor_id, monitor.dsf_monitor_id)
# should have resulted an update
mock.assert_has_calls([
call('/DSFMonitor/42a/', 'PUT', {
'protocol': 'HTTP',
'options': {
'path': '/_nope',
'host': 'bleep.bloop',
'header': 'User-Agent: Dyn Monitor',
'port': 8080,
'timeout': 10
}
})
])
# cached monitor should have been updated
self.assertTrue('unit.tests.:A' in
provider._traffic_director_monitors)
monitor = provider._traffic_director_monitors['unit.tests.:A']
self.assertEquals('bleep.bloop', monitor.host)
self.assertEquals('/_nope', monitor.path)
self.assertEquals('HTTP', monitor.protocol)
self.assertEquals('8080', monitor.port)
# test upgrading an old label
record = Record.new(existing, 'old-label', {
'ttl': 60,
'type': 'A',
'value': '1.2.3.4'
})
mock.reset_mock()
mock.side_effect = [{
'data': {
'active': 'Y',
'dsf_monitor_id': self.monitor_id,
'endpoints': [],
'label': 'old-label.unit.tests.:A',
'notifier': '',
'expected': '',
'header': 'User-Agent: Dyn Monitor',
'host': 'old-label.unit.tests',
'path': '/_dns',
'port': '443',
'timeout': '10',
'probe_interval': '60',
'protocol': 'HTTPS',
'response_count': '2',
'retries': '2'
},
'job_id': 3376259461,
'msgs': [{'ERR_CD': None,
'INFO': 'add: Here is the new monitor',
'LVL': 'INFO',
'SOURCE': 'BLL'}],
'status': 'success'
}]
monitor = provider._traffic_director_monitor(record)
self.assertEquals(self.monitor_id, monitor.dsf_monitor_id)
# should have resulted an update
mock.assert_has_calls([
call('/DSFMonitor/b52/', 'PUT', {
'label': 'old-label.unit.tests.:A'
})
])
# cached monitor should have been updated
self.assertTrue('old-label.unit.tests.:A' in
provider._traffic_director_monitors)
@patch('dyn.core.SessionEngine.execute')
def test_extra_changes(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass', True)
# short-circuit session checking
provider._dyn_sess = True
mock.side_effect = [self.monitors_response]
# non-geo
desired = Zone('unit.tests.', [])
record = Record.new(desired, '', {
'ttl': 60,
'type': 'A',
'value': '1.2.3.4',
})
desired.add_record(record)
extra = provider._extra_changes(desired=desired,
changes=[Create(record)])
self.assertEquals(0, len(extra))
# in changes, noop
desired = Zone('unit.tests.', [])
record = Record.new(desired, '', {
'geo': {
'NA': ['1.2.3.4'],
},
'ttl': 60,
'type': 'A',
'value': '1.2.3.4',
})
desired.add_record(record)
extra = provider._extra_changes(desired=desired,
changes=[Create(record)])
self.assertEquals(0, len(extra))
# no diff, no extra
extra = provider._extra_changes(desired=desired, changes=[])
self.assertEquals(0, len(extra))
# monitors should have been fetched now
mock.assert_called_once()
# diff in healthcheck, gets extra
desired = Zone('unit.tests.', [])
record = Record.new(desired, '', {
'geo': {
'NA': ['1.2.3.4'],
},
'octodns': {
'healthcheck': {
'host': 'foo.bar',
'path': '/_ready'
}
},
'ttl': 60,
'type': 'A',
'value': '1.2.3.4',
})
desired.add_record(record)
extra = provider._extra_changes(desired=desired, changes=[])
self.assertEquals(1, len(extra))
extra = extra[0]
self.assertIsInstance(extra, Update)
self.assertEquals(record, extra.record)
# missing health check
desired = Zone('unit.tests.', [])
record = Record.new(desired, 'geo', {
'geo': {
'NA': ['1.2.3.4'],
},
'ttl': 60,
'type': 'A',
'value': '1.2.3.4',
})
desired.add_record(record)
extra = provider._extra_changes(desired=desired, changes=[])
self.assertEquals(1, len(extra))
extra = extra[0]
self.assertIsInstance(extra, Update)
self.assertEquals(record, extra.record)
@patch('dyn.core.SessionEngine.execute')
def test_populate_traffic_directors_empty(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# empty all around
mock.side_effect = [
# get traffic directors
{'data': []},
# get zone
{'data': {}},
# get records
{'data': {}},
]
got = Zone('unit.tests.', [])
provider.populate(got)
self.assertEquals(0, len(got.records))
mock.assert_has_calls([
call('/DSF/', 'GET', {'detail': 'Y'}),
call('/Zone/unit.tests/', 'GET', {}),
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
])
@patch('dyn.core.SessionEngine.execute')
def test_populate_traffic_directors_td(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
got = Zone('unit.tests.', [])
zone_name = got.name[:-1]
# only traffic director
mock.side_effect = [
# get traffic directors
self.traffic_directors_response,
# get the first td's nodes
{'data': [{'fqdn': zone_name, 'zone': zone_name}]},
# get traffic director, b/c ^ matches
self.traffic_director_response,
# get the next td's nodes, not a match
{'data': [{'fqdn': 'other', 'zone': 'other'}]},
# get zone
{'data': {}},
# get records
{'data': {}},
]
provider.populate(got)
self.assertEquals(1, len(got.records))
self.assertFalse(self.expected_geo.changes(got, provider))
mock.assert_has_calls([
call('/DSF/', 'GET', {'detail': 'Y'}),
call('/DSFNode/2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
{'pending_changes': 'Y'}),
call('/DSFNode/3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
call('/Zone/unit.tests/', 'GET', {}),
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
])
@patch('dyn.core.SessionEngine.execute')
def test_populate_traffic_directors_regular(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# only regular
mock.side_effect = [
# get traffic directors
{'data': []},
# get zone
{'data': {}},
# get records
self.records_response
]
got = Zone('unit.tests.', [])
provider.populate(got)
self.assertEquals(1, len(got.records))
self.assertFalse(self.expected_regular.changes(got, provider))
mock.assert_has_calls([
call('/DSF/', 'GET', {'detail': 'Y'}),
call('/Zone/unit.tests/', 'GET', {}),
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
])
@patch('dyn.core.SessionEngine.execute')
def test_populate_traffic_directors_both(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# both traffic director and regular, regular is ignored
mock.side_effect = [
# get traffic directors
self.traffic_directors_response,
# grab its nodes, matches
{'data': [{'fqdn': 'unit.tests', 'zone': 'unit.tests'}]},
# get traffic director b/c match
self.traffic_director_response,
# grab next td's nodes, not a match
{'data': [{'fqdn': 'other', 'zone': 'other'}]},
# get zone
{'data': {}},
# get records
self.records_response
]
got = Zone('unit.tests.', [])
provider.populate(got)
self.assertEquals(1, len(got.records))
self.assertFalse(self.expected_geo.changes(got, provider))
mock.assert_has_calls([
call('/DSF/', 'GET', {'detail': 'Y'}),
call('/DSFNode/2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
{'pending_changes': 'Y'}),
call('/DSFNode/3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
call('/Zone/unit.tests/', 'GET', {}),
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
])
@patch('dyn.core.SessionEngine.execute')
def test_populate_traffic_director_busted(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
busted_traffic_director_response = {
"status": "success",
"data": {
"notifiers": [],
"rulesets": [],
"ttl": "300",
"active": "Y",
"service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI",
"nodes": [{
"fqdn": "unit.tests",
"zone": "unit.tests"
}],
"pending_change": "",
"label": "unit.tests.:A"
},
"job_id": 3376642606,
"msgs": [{
"INFO": "detail: Here is your service",
"LVL": "INFO",
"ERR_CD": None,
"SOURCE": "BLL"
}]
}
# busted traffic director
mock.side_effect = [
# get traffic directors
self.traffic_directors_response,
{'data': [{'fqdn': 'unit.tests', 'zone': 'unit.tests'}]},
# get traffic director
busted_traffic_director_response,
{'data': [{'fqdn': 'other', 'zone': 'other'}]},
# get zone
{'data': {}},
# get records
{'data': {}},
]
got = Zone('unit.tests.', [])
provider.populate(got)
self.assertEquals(1, len(got.records))
# we expect a change here for the record, the values aren't important,
# so just compare set contents (which does name and type)
self.assertEquals(self.expected_geo.records, got.records)
mock.assert_has_calls([
call('/DSF/', 'GET', {'detail': 'Y'}),
call('/DSFNode/2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
{'pending_changes': 'Y'}),
call('/DSFNode/3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
call('/Zone/unit.tests/', 'GET', {}),
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
])
@patch('dyn.core.SessionEngine.execute')
def test_apply_traffic_director(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# stubbing these out to avoid a lot of messy mocking, they'll be tested
# individually, we'll check for expected calls
provider._mod_geo_Create = MagicMock()
provider._mod_geo_Update = MagicMock()
provider._mod_geo_Delete = MagicMock()
provider._mod_Create = MagicMock()
provider._mod_Update = MagicMock()
provider._mod_Delete = MagicMock()
# busted traffic director
mock.side_effect = [
# get zone
{'data': {}},
# accept publish
{'data': {}},
]
desired = Zone('unit.tests.', [])
geo = self.geo_record
regular = self.regular_record
changes = [
Create(geo),
Create(regular),
Update(geo, geo),
Update(regular, regular),
Delete(geo),
Delete(regular),
]
plan = Plan(None, desired, changes, True)
provider._apply(plan)
mock.assert_has_calls([
call('/Zone/unit.tests/', 'GET', {}),
call('/Zone/unit.tests/', 'PUT', {'publish': True})
])
# should have seen 1 call to each
provider._mod_geo_Create.assert_called_once()
provider._mod_geo_Update.assert_called_once()
provider._mod_geo_Delete.assert_called_once()
provider._mod_Create.assert_called_once()
provider._mod_Update.assert_called_once()
provider._mod_Delete.assert_called_once()
@patch('dyn.core.SessionEngine.execute')
def test_mod_geo_create(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# will be tested separately
provider._mod_geo_rulesets = MagicMock()
mock.side_effect = [
# create traffic director
self.traffic_director_response,
# get traffic directors
self.traffic_directors_response
]
provider._mod_geo_Create(None, Create(self.geo_record))
# td now lives in cache
self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
# should have seen 1 gen call
provider._mod_geo_rulesets.assert_called_once()
def test_mod_geo_update_geo_geo(self):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# update of an existing td
# pre-populate the cache with our mock td
provider._traffic_directors = {
'unit.tests.': {
'A': 42,
}
}
# mock _mod_geo_rulesets
provider._mod_geo_rulesets = MagicMock()
geo = self.geo_record
change = Update(geo, geo)
provider._mod_geo_Update(None, change)
# still in cache
self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
# should have seen 1 gen call
provider._mod_geo_rulesets.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_geo_update_geo_regular(self, _):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# convert a td to a regular record
provider._mod_Create = MagicMock()
provider._mod_geo_Delete = MagicMock()
change = Update(self.geo_record, self.regular_record)
provider._mod_geo_Update(42, change)
# should have seen a call to create the new regular record
provider._mod_Create.assert_called_once_with(42, change)
# should have seen a call to delete the old td record
provider._mod_geo_Delete.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_geo_update_regular_geo(self, _):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# convert a regular record to a td
provider._mod_geo_Create = MagicMock()
provider._mod_Delete = MagicMock()
change = Update(self.regular_record, self.geo_record)
provider._mod_geo_Update(42, change)
# should have seen a call to create the new geo record
provider._mod_geo_Create.assert_called_once_with(42, change)
# should have seen a call to delete the old regular record
provider._mod_Delete.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_geo_delete(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
td_mock = MagicMock()
provider._traffic_directors = {
'unit.tests.': {
'A': td_mock,
}
}
provider._mod_geo_Delete(None, Delete(self.geo_record))
# delete called
td_mock.delete.assert_called_once()
# removed from cache
self.assertFalse('A' in provider.traffic_directors['unit.tests.'])
@patch('dyn.tm.services.DSFResponsePool.create')
def test_find_or_create_geo_pool(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
td = 42
# no candidates cache miss, so create
values = ['1.2.3.4', '1.2.3.5']
pool = provider._find_or_create_geo_pool(td, [], 'default', 'A',
values)
self.assertIsInstance(pool, DSFResponsePool)
self.assertEquals(1, len(pool.rs_chains))
records = pool.rs_chains[0].record_sets[0].records
self.assertEquals(values, [r.address for r in records])
mock.assert_called_once_with(td)
# cache hit, use the one we just created
mock.reset_mock()
pools = [pool]
cached = provider._find_or_create_geo_pool(td, pools, 'default', 'A',
values)
self.assertEquals(pool, cached)
mock.assert_not_called()
# cache miss, non-matching label
mock.reset_mock()
miss = provider._find_or_create_geo_pool(td, pools, 'NA-US-CA', 'A',
values)
self.assertNotEquals(pool, miss)
self.assertEquals('NA-US-CA', miss.label)
mock.assert_called_once_with(td)
# cache miss, matching label, mis-matching values
mock.reset_mock()
values = ['2.2.3.4.', '2.2.3.5']
miss = provider._find_or_create_geo_pool(td, pools, 'default', 'A',
values)
self.assertNotEquals(pool, miss)
mock.assert_called_once_with(td)
@patch('dyn.tm.services.DSFRuleset.add_response_pool')
@patch('dyn.tm.services.DSFRuleset.create')
# just lets us ignore the pool.create calls
@patch('dyn.tm.services.DSFResponsePool.create')
def test_mod_geo_rulesets_create(self, _, ruleset_create_mock,
add_response_pool_mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
td_mock = MagicMock()
td_mock._rulesets = []
provider._traffic_director_monitor = MagicMock()
provider._find_or_create_geo_pool = MagicMock()
td_mock.all_response_pools = []
provider._find_or_create_geo_pool.side_effect = [
_DummyPool('default'),
_DummyPool(1),
_DummyPool(2),
_DummyPool(3),
_DummyPool(4),
]
change = Create(self.geo_record)
provider._mod_geo_rulesets(td_mock, change)
ruleset_create_mock.assert_has_calls((
call(td_mock, index=0),
call(td_mock, index=0),
call(td_mock, index=0),
call(td_mock, index=0),
call(td_mock, index=0),
))
add_response_pool_mock.assert_has_calls((
# default
call('default'),
# first geo and it's fallback
call(1),
call('default', index=999),
# 2nd geo and it's fallback
call(2),
call('default', index=999),
# 3nd geo and it's fallback
call(3),
call('default', index=999),
# 4th geo and it's 2 levels of fallback
call(4),
call(3, index=999),
call('default', index=999),
))
# have to patch the place it's imported into, not where it lives
@patch('octodns.provider.dyn.get_response_pool')
@patch('dyn.tm.services.DSFRuleset.add_response_pool')
@patch('dyn.tm.services.DSFRuleset.create')
# just lets us ignore the pool.create calls
@patch('dyn.tm.services.DSFResponsePool.create')
def test_mod_geo_rulesets_existing(self, _, ruleset_create_mock,
add_response_pool_mock,
get_response_pool_mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
ruleset_mock = MagicMock()
ruleset_mock.response_pools = [_DummyPool(3)]
td_mock = MagicMock()
td_mock._rulesets = [
ruleset_mock,
]
provider._traffic_director_monitor = MagicMock()
provider._find_or_create_geo_pool = MagicMock()
unused_pool = _DummyPool('unused')
td_mock.all_response_pools = \
ruleset_mock.response_pools + [unused_pool]
get_response_pool_mock.return_value = unused_pool
provider._find_or_create_geo_pool.side_effect = [
_DummyPool('default'),
_DummyPool(1),
_DummyPool(2),
ruleset_mock.response_pools[0],
_DummyPool(4),
]
change = Create(self.geo_record)
provider._mod_geo_rulesets(td_mock, change)
ruleset_create_mock.assert_has_calls((
call(td_mock, index=2),
call(td_mock, index=2),
call(td_mock, index=2),
call(td_mock, index=2),
call(td_mock, index=2),
))
add_response_pool_mock.assert_has_calls((
# default
call('default'),
# first geo and it's fallback
call(1),
call('default', index=999),
# 2nd geo and it's fallback
call(2),
call('default', index=999),
# 3nd geo, from existing, and it's fallback
call(3),
call('default', index=999),
# 4th geo and it's 2 levels of fallback
call(4),
call(3, index=999),
call('default', index=999),
))
# unused poll should have been deleted
self.assertTrue(unused_pool.deleted)
# old ruleset ruleset should be deleted, it's pool will have been
# reused
ruleset_mock.delete.assert_called_once()
class TestDynProviderAlias(TestCase):
expected = Zone('unit.tests.', [])
for name, data in (
('', {
'type': 'ALIAS',
'ttl': 300,
'value': 'www.unit.tests.'
}),
('www', {
'type': 'A',
'ttl': 300,
'values': ['1.2.3.4']
})):
expected.add_record(Record.new(expected, name, data))
def setUp(self):
# Flush our zone to ensure we start fresh
_CachingDynZone.flush_zone(self.expected.name[:-1])
@patch('dyn.core.SessionEngine.execute')
def test_populate(self, execute_mock):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Test Zone create
execute_mock.side_effect = [
# get Zone
{'data': {}},
# get_all_records
{'data': {
'a_records': [{
'fqdn': 'www.unit.tests',
'rdata': {'address': '1.2.3.4'},
'record_id': 1,
'record_type': 'A',
'ttl': 300,
'zone': 'unit.tests',
}],
'alias_records': [{
'fqdn': 'unit.tests',
'rdata': {'alias': 'www.unit.tests.'},
'record_id': 2,
'record_type': 'ALIAS',
'ttl': 300,
'zone': 'unit.tests',
}],
}}
]
got = Zone('unit.tests.', [])
provider.populate(got)
execute_mock.assert_has_calls([
call('/Zone/unit.tests/', 'GET', {}),
call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
])
changes = self.expected.changes(got, SimpleProvider())
self.assertEquals([], changes)
@patch('dyn.core.SessionEngine.execute')
def test_sync(self, execute_mock):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Test Zone create
execute_mock.side_effect = [
# No such zone, during populate
DynectGetError('foo'),
# No such zone, during sync
DynectGetError('foo'),
# get empty Zone
{'data': {}},
# get zone we can modify & delete with
{'data': {
# A top-level to delete
'a_records': [{
'fqdn': 'www.unit.tests',
'rdata': {'address': '1.2.3.4'},
'record_id': 1,
'record_type': 'A',
'ttl': 300,
'zone': 'unit.tests',
}],
# A node to delete
'alias_records': [{
'fqdn': 'unit.tests',
'rdata': {'alias': 'www.unit.tests.'},
'record_id': 2,
'record_type': 'ALIAS',
'ttl': 300,
'zone': 'unit.tests',
}],
}}
]
# No existing records, create all
with patch('dyn.tm.zones.Zone.add_record') as add_mock:
with patch('dyn.tm.zones.Zone._update') as update_mock:
plan = provider.plan(self.expected)
update_mock.assert_not_called()
provider.apply(plan)
update_mock.assert_called()
add_mock.assert_called()
# Once for each dyn record
self.assertEquals(2, len(add_mock.call_args_list))
execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}),
call('/Zone/unit.tests/', 'GET', {})])
self.assertEquals(2, len(plan.changes))
# Need a class that doesn't do all the "real" stuff, but gets our monkey
# patching
class DummyDSFMonitor(DSFMonitor):
def __init__(self, host=None, path=None, protocol=None, port=None,
options_host=None, options_path=None, options_protocol=None,
options_port=None):
# not calling super on purpose
self._host = host
self._path = path
self._protocol = protocol
self._port = port
if options_host:
self._options = {
'host': options_host,
'path': options_path,
'protocol': options_protocol,
'port': options_port,
}
else:
self._options = None
class TestDSFMonitorMonkeyPatching(TestCase):
def test_host(self):
monitor = DummyDSFMonitor(host='host.com', path='/path',
protocol='HTTP', port=8080)
self.assertEquals('host.com', monitor.host)
self.assertEquals('/path', monitor.path)
self.assertEquals('HTTP', monitor.protocol)
self.assertEquals(8080, monitor.port)
monitor = DummyDSFMonitor(options_host='host.com',
options_path='/path',
options_protocol='HTTP', options_port=8080)
self.assertEquals('host.com', monitor.host)
self.assertEquals('/path', monitor.path)
monitor.host = 'other.com'
self.assertEquals('other.com', monitor.host)
monitor.path = '/other-path'
self.assertEquals('/other-path', monitor.path)
monitor.protocol = 'HTTPS'
self.assertEquals('HTTPS', monitor.protocol)
monitor.port = 8081
self.assertEquals(8081, monitor.port)
monitor = DummyDSFMonitor()
monitor.host = 'other.com'
self.assertEquals('other.com', monitor.host)
monitor = DummyDSFMonitor()
monitor.path = '/other-path'
self.assertEquals('/other-path', monitor.path)
monitor.protocol = 'HTTP'
self.assertEquals('HTTP', monitor.protocol)
monitor.port = 8080
self.assertEquals(8080, monitor.port)
# Just to exercise the _options init
monitor = DummyDSFMonitor()
monitor.protocol = 'HTTP'
self.assertEquals('HTTP', monitor.protocol)
monitor = DummyDSFMonitor()
monitor.port = 8080
self.assertEquals(8080, monitor.port)
class DummyRecord(object):
def __init__(self, address, weight, ttl):
self.address = address
self.weight = weight
self.ttl = ttl
class DummyRecordSets(object):
def __init__(self, records):
self.records = records
class DummyRsChains(object):
def __init__(self, records):
self.record_sets = [DummyRecordSets(records)]
class DummyResponsePool(object):
def __init__(self, label, records=[]):
self.label = label
if records:
self.rs_chains = [DummyRsChains(records)]
else:
self.rs_chains = []
def refresh(self):
pass
class DummyRuleset(object):
def __init__(self, label, response_pools=[],
criteria_type='always', criteria={}):
self.label = label
self.response_pools = response_pools
self.criteria_type = criteria_type
self.criteria = criteria
class DummyTrafficDirector(object):
def __init__(self, zone_name, rulesets=[], response_pools=[], ttl=42):
self.label = 'dummy:abcdef1234567890'
self.rulesets = rulesets
self.all_response_pools = response_pools
self.ttl = ttl
self.nodes = [{'zone': zone_name[:-1]}]
class TestDynProviderDynamic(TestCase):
def test_value_for_address(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
class DummyRecord(object):
def __init__(self, address, weight):
self.address = address
self.weight = weight
record = DummyRecord('1.2.3.4', 32)
self.assertEquals({
'value': record.address,
'weight': record.weight,
}, provider._value_for_A('A', record))
record = DummyRecord('2601:644:500:e210:62f8:1dff:feb8:947a', 32)
self.assertEquals({
'value': record.address,
'weight': record.weight,
}, provider._value_for_AAAA('AAAA', record))
def test_value_for_CNAME(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
class DummyRecord(object):
def __init__(self, cname, weight):
self.cname = cname
self.weight = weight
record = DummyRecord('foo.unit.tests.', 32)
self.assertEquals({
'value': record.cname,
'weight': record.weight,
}, provider._value_for_CNAME('CNAME', record))
def test_populate_dynamic_pools(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Empty data, empty returns
default, pools = provider._populate_dynamic_pools('A', [], [])
self.assertEquals({}, default)
self.assertEquals({}, pools)
records_a = [DummyRecord('1.2.3.4', 32, 60)]
default_a = DummyResponsePool('default', records_a)
# Just a default A
response_pools = [default_a]
default, pools = provider._populate_dynamic_pools('A', [],
response_pools)
self.assertEquals({
'ttl': 60,
'type': 'A',
'values': ['1.2.3.4'],
}, default)
self.assertEquals({}, pools)
multi_a = [
DummyRecord('1.2.3.5', 42, 90),
DummyRecord('1.2.3.6', 43, 90),
DummyRecord('1.2.3.7', 44, 90),
]
example_a = DummyResponsePool('example', multi_a)
# Just a named pool
response_pools = [example_a]
default, pools = provider._populate_dynamic_pools('A', [],
response_pools)
self.assertEquals({}, default)
self.assertEquals({
'example': {
'values': [{
'value': '1.2.3.5',
'weight': 42,
}, {
'value': '1.2.3.6',
'weight': 43,
}, {
'value': '1.2.3.7',
'weight': 44,
}],
},
}, pools)
# Named pool that shows up twice
response_pools = [example_a, example_a]
default, pools = provider._populate_dynamic_pools('A', [],
response_pools)
self.assertEquals({}, default)
self.assertEquals({
'example': {
'values': [{
'value': '1.2.3.5',
'weight': 42,
}, {
'value': '1.2.3.6',
'weight': 43,
}, {
'value': '1.2.3.7',
'weight': 44,
}],
},
}, pools)
# Default & named
response_pools = [example_a, default_a, example_a]
default, pools = provider._populate_dynamic_pools('A', [],
response_pools)
self.assertEquals({
'ttl': 60,
'type': 'A',
'values': ['1.2.3.4'],
}, default)
self.assertEquals({
'example': {
'values': [{
'value': '1.2.3.5',
'weight': 42,
}, {
'value': '1.2.3.6',
'weight': 43,
}, {
'value': '1.2.3.7',
'weight': 44,
}],
},
}, pools)
# empty rs_chains doesn't cause an example, just ignores
empty_a = DummyResponsePool('empty')
response_pools = [empty_a]
default, pools = provider._populate_dynamic_pools('A', [],
response_pools)
self.assertEquals({}, default)
self.assertEquals({}, pools)
def test_populate_dynamic_rules(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Empty
rulesets = []
pools = {}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([], rules)
# default: is ignored
rulesets = [DummyRuleset('default:')]
pools = {}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([], rules)
# No ResponsePools in RuleSet, ignored
rulesets = [DummyRuleset('0:abcdefg')]
pools = {}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([], rules)
# ResponsePool, no fallback
rulesets = [DummyRuleset('0:abcdefg', [
DummyResponsePool('some-pool')
])]
pools = {}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([{
'pool': 'some-pool',
}], rules)
# ResponsePool, with dfault fallback (ignored)
rulesets = [DummyRuleset('0:abcdefg', [
DummyResponsePool('some-pool'),
DummyResponsePool('default'),
])]
pools = {}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([{
'pool': 'some-pool',
}], rules)
# ResponsePool, with fallback
rulesets = [DummyRuleset('0:abcdefg', [
DummyResponsePool('some-pool'),
DummyResponsePool('some-fallback'),
])]
pools = {
'some-pool': {},
}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([{
'pool': 'some-pool',
}], rules)
# fallback has been installed
self.assertEquals({
'some-pool': {
'fallback': 'some-fallback',
}
}, pools)
# Unsupported criteria_type (ignored)
rulesets = [DummyRuleset('0:abcdefg', [
DummyResponsePool('some-pool')
], 'unsupported')]
pools = {}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([], rules)
# Geo Continent/Region
response_pools = [DummyResponsePool('some-pool')]
criteria = {
'geoip': {
'country': ['US'],
'province': ['or'],
'region': [14],
},
}
ruleset = DummyRuleset('0:abcdefg', response_pools,
'geoip', criteria)
rulesets = [ruleset]
pools = {}
rules = provider._populate_dynamic_rules(rulesets, pools)
self.assertEquals([{
'geos': ['AF', 'NA-US', 'NA-US-OR'],
'pool': 'some-pool',
}], rules)
def test_populate_dynamic_traffic_director(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
fqdn = 'dynamic.unit.tests.'
multi_a = [
DummyRecord('1.2.3.5', 1, 90),
DummyRecord('1.2.3.6', 1, 90),
DummyRecord('1.2.3.7', 1, 90),
]
default_response_pool = DummyResponsePool('default', multi_a)
pool1_response_pool = DummyResponsePool('pool1', multi_a)
rulesets = [
DummyRuleset('default', [default_response_pool]),
DummyRuleset('0:abcdef', [pool1_response_pool], 'geoip', {
'geoip': {
'country': ['US'],
'province': ['or'],
'region': [14],
},
}),
]
zone = Zone('unit.tests.', [])
td = DummyTrafficDirector(zone.name, rulesets,
[default_response_pool, pool1_response_pool])
record = provider._populate_dynamic_traffic_director(zone, fqdn, 'A',
td, rulesets,
True)
self.assertTrue(record)
self.assertEquals('A', record._type)
self.assertEquals(90, record.ttl)
self.assertEquals([
'1.2.3.5',
'1.2.3.6',
'1.2.3.7',
], record.values)
self.assertTrue('pool1' in record.dynamic.pools)
self.assertEquals({
'fallback': None,
'values': [{
'value': '1.2.3.5',
'weight': 1,
}, {
'value': '1.2.3.6',
'weight': 1,
}, {
'value': '1.2.3.7',
'weight': 1,
}]
}, record.dynamic.pools['pool1'].data)
self.assertEquals(2, len(record.dynamic.rules))
self.assertEquals({
'pool': 'default',
}, record.dynamic.rules[0].data)
self.assertEquals({
'pool': 'pool1',
'geos': ['AF', 'NA-US', 'NA-US-OR'],
}, record.dynamic.rules[1].data)
# Hack into the provider and create a fake list of traffic directors
provider._traffic_directors = {
'dynamic.unit.tests.': {
'A': td,
}
}
zone = Zone('unit.tests.', [])
records = provider._populate_traffic_directors(zone, lenient=True)
self.assertEquals(1, len(records))
def test_dynamic_records_for_A(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Empty
records = provider._dynamic_records_for_A([], {})
self.assertEquals([], records)
# Basic
values = [{
'value': '1.2.3.4',
}, {
'value': '1.2.3.5',
'weight': 42,
}]
records = provider._dynamic_records_for_A(values, {})
self.assertEquals(2, len(records))
record = records[0]
self.assertEquals('1.2.3.4', record.address)
self.assertEquals(1, record.weight)
record = records[1]
self.assertEquals('1.2.3.5', record.address)
self.assertEquals(42, record.weight)
# With extras
records = provider._dynamic_records_for_A(values, {
'automation': 'manual',
'eligible': True,
})
self.assertEquals(2, len(records))
record = records[0]
self.assertEquals('1.2.3.4', record.address)
self.assertEquals(1, record.weight)
self.assertEquals('manual', record._automation)
self.assertTrue(record.eligible)
def test_dynamic_records_for_AAAA(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Empty
records = provider._dynamic_records_for_AAAA([], {})
self.assertEquals([], records)
# Basic
values = [{
'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
}, {
'value': '2601:644:500:e210:62f8:1dff:feb8:947b',
'weight': 42,
}]
records = provider._dynamic_records_for_AAAA(values, {})
self.assertEquals(2, len(records))
record = records[0]
self.assertEquals('2601:644:500:e210:62f8:1dff:feb8:947a',
record.address)
self.assertEquals(1, record.weight)
record = records[1]
self.assertEquals('2601:644:500:e210:62f8:1dff:feb8:947b',
record.address)
self.assertEquals(42, record.weight)
# With extras
records = provider._dynamic_records_for_AAAA(values, {
'automation': 'manual',
'eligible': True,
})
self.assertEquals(2, len(records))
record = records[0]
self.assertEquals('2601:644:500:e210:62f8:1dff:feb8:947a',
record.address)
self.assertEquals(1, record.weight)
self.assertEquals('manual', record._automation)
self.assertTrue(record.eligible)
def test_dynamic_records_for_CNAME(self):
provider = DynProvider('test', 'cust', 'user', 'pass')
# Empty
records = provider._dynamic_records_for_CNAME([], {})
self.assertEquals([], records)
# Basic
values = [{
'value': 'target-1.unit.tests.',
}, {
'value': 'target-2.unit.tests.',
'weight': 42,
}]
records = provider._dynamic_records_for_CNAME(values, {})
self.assertEquals(2, len(records))
record = records[0]
self.assertEquals('target-1.unit.tests.', record.cname)
self.assertEquals(1, record.weight)
record = records[1]
self.assertEquals('target-2.unit.tests.', record.cname)
self.assertEquals(42, record.weight)
# With extras
records = provider._dynamic_records_for_CNAME(values, {
'automation': 'manual',
'eligible': True,
})
self.assertEquals(2, len(records))
record = records[0]
self.assertEquals('target-1.unit.tests.', record.cname)
self.assertEquals(1, record.weight)
self.assertEquals('manual', record._automation)
self.assertTrue(record.eligible)
def test_dynamic_value_sort_key(self):
values = [{
'value': '1.2.3.1',
}, {
'value': '1.2.3.27',
}, {
'value': '1.2.3.127',
}, {
'value': '1.2.3.2',
}]
self.assertEquals([{
'value': '1.2.3.1',
}, {
'value': '1.2.3.127',
}, {
'value': '1.2.3.2',
}, {
'value': '1.2.3.27',
}], sorted(values, key=_dynamic_value_sort_key))
@patch('dyn.tm.services.DSFResponsePool.create')
def test_find_or_create_dynamic_pools(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass')
td = 42
label = 'foo'
values = [{
'value': '1.2.3.1',
}, {
'value': '1.2.3.127',
}, {
'value': '1.2.3.2',
}, {
'value': '1.2.3.27',
}]
# A Pool with no existing pools, will create
pools = []
pool = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
values)
self.assertIsInstance(pool, DSFResponsePool)
self.assertEquals(1, len(pool.rs_chains))
self.assertEquals(1, len(pool.rs_chains[0].record_sets))
records = pool.rs_chains[0].record_sets[0].records
self.assertEquals(4, len(records))
self.assertEquals([v['value'] for v in values],
[r.address for r in records])
self.assertEquals([1 for r in records], [r.weight for r in records])
mock.assert_called_once_with(td)
# Ask for the pool we created above and include it in the canidate list
mock.reset_mock()
pools = [pool]
cached = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
values)
self.assertEquals(pool, cached)
mock.assert_not_called()
# Invalid candidate pool, still finds the valid one that's there too
mock.reset_mock()
invalid = DSFResponsePool(label, rs_chains=[])
pools = [invalid, pool]
cached = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
values)
self.assertEquals(pool, cached)
mock.assert_not_called()
# Ask for a pool with a different label, should create a new one
mock.reset_mock()
pools = [pool]
other = provider._find_or_create_dynamic_pool(td, pools, 'other', 'A',
values)
self.assertEquals('other', other.label)
mock.assert_called_once_with(td)
# Ask for a pool that matches label-wise, but has different values
values = [{
'value': '1.2.3.44',
}]
mock.reset_mock()
pools = [pool]
new = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
values)
self.assertEquals(label, new.label)
self.assertEquals(1, len(new.rs_chains))
self.assertEquals(1, len(new.rs_chains[0].record_sets))
records = new.rs_chains[0].record_sets[0].records
self.assertEquals(1, len(records))
self.assertEquals([v['value'] for v in values],
[r.address for r in records])
self.assertEquals([1 for r in records], [r.weight for r in records])
mock.assert_called_once_with(td)
zone = Zone('unit.tests.', [])
dynamic_a_record = Record.new(zone, '', {
'dynamic': {
'pools': {
'one': {
'values': [{
'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': {
'fallback': 'two',
'values': [{
'weight': 10,
'value': '4.4.4.4',
}, {
'weight': 12,
'value': '5.5.5.5',
}],
},
},
'rules': [{
'geos': ['AF', 'EU', 'AS-JP'],
'pool': 'three',
}, {
'geos': ['NA-US-CA'],
'pool': 'two',
}, {
'pool': 'one',
}],
},
'type': 'A',
'ttl': 60,
'values': [
'1.1.1.1',
'2.2.2.2',
],
})
geo_a_record = Record.new(zone, '', {
'geo': {
'AF': ['2.2.3.4', '2.2.3.5'],
'AS-JP': ['3.2.3.4', '3.2.3.5'],
'NA-US': ['4.2.3.4', '4.2.3.5'],
'NA-US-CA': ['5.2.3.4', '5.2.3.5']
},
'ttl': 300,
'type': 'A',
'values': ['1.2.3.4', '1.2.3.5'],
})
regular_a_record = Record.new(zone, '', {
'ttl': 301,
'type': 'A',
'value': '1.2.3.4',
})
dynamic_cname_record = Record.new(zone, 'www', {
'dynamic': {
'pools': {
'one': {
'values': [{
'value': 'target-0.unit.tests.',
}],
},
'two': {
# Testing out of order value sorting here
'values': [{
'value': 'target-1.unit.tests.',
}, {
'value': 'target-2.unit.tests.',
}],
},
'three': {
'values': [{
'weight': 10,
'value': 'target-3.unit.tests.',
}, {
'weight': 12,
'value': 'target-4.unit.tests.',
}],
},
},
'rules': [{
'geos': ['AF', 'EU', 'AS-JP'],
'pool': 'three',
}, {
'geos': ['NA-US-CA'],
'pool': 'two',
}, {
'pool': 'one',
}],
},
'type': 'CNAME',
'ttl': 60,
'value': 'target.unit.tests.',
})
dynamic_fallback_loop = Record.new(zone, '', {
'dynamic': {
'pools': {
'one': {
'values': [{
'value': '3.3.3.3',
}],
},
'two': {
# Testing out of order value sorting here
'fallback': 'three',
'values': [{
'value': '5.5.5.5',
}, {
'value': '4.4.4.4',
}],
},
'three': {
'fallback': 'two',
'values': [{
'weight': 10,
'value': '4.4.4.4',
}, {
'weight': 12,
'value': '5.5.5.5',
}],
},
},
'rules': [{
'geos': ['AF', 'EU', 'AS-JP'],
'pool': 'three',
}, {
'geos': ['NA-US-CA'],
'pool': 'two',
}, {
'pool': 'one',
}],
},
'type': 'A',
'ttl': 60,
'values': [
'1.1.1.1',
'2.2.2.2',
],
}, lenient=True)
@patch('dyn.tm.services.DSFRuleset.add_response_pool')
@patch('dyn.tm.services.DSFRuleset.create')
# just lets us ignore the pool.create calls
@patch('dyn.tm.services.DSFResponsePool.create')
def test_mod_dynamic_rulesets_create_CNAME(self, _, ruleset_create_mock,
add_response_pool_mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
td_mock = MagicMock()
td_mock._rulesets = []
provider._traffic_director_monitor = MagicMock()
provider._find_or_create_dynamic_pool = MagicMock()
td_mock.all_response_pools = []
provider._find_or_create_dynamic_pool.side_effect = [
_DummyPool('default'),
_DummyPool('one'),
_DummyPool('two'),
_DummyPool('three'),
]
change = Create(self.dynamic_cname_record)
provider._mod_dynamic_rulesets(td_mock, change)
add_response_pool_mock.assert_has_calls((
# default
call('default'),
# first dynamic and it's fallback
call('one'),
call('default', index=999),
# 2nd dynamic and it's fallback
call('three'),
call('default', index=999),
# 3nd dynamic and it's fallback
call('two'),
call('default', index=999),
))
ruleset_create_mock.assert_has_calls((
call(td_mock, index=0),
call(td_mock, index=0),
call(td_mock, index=0),
call(td_mock, index=0),
))
# have to patch the place it's imported into, not where it lives
@patch('octodns.provider.dyn.get_response_pool')
@patch('dyn.tm.services.DSFRuleset.add_response_pool')
@patch('dyn.tm.services.DSFRuleset.create')
# just lets us ignore the pool.create calls
@patch('dyn.tm.services.DSFResponsePool.create')
def test_mod_dynamic_rulesets_existing(self, _, ruleset_create_mock,
add_response_pool_mock,
get_response_pool_mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
ruleset_mock = MagicMock()
ruleset_mock.response_pools = [_DummyPool('three')]
td_mock = MagicMock()
td_mock._rulesets = [
ruleset_mock,
]
provider._traffic_director_monitor = MagicMock()
provider._find_or_create_dynamic_pool = MagicMock()
# Matching ttl
td_mock.ttl = self.dynamic_a_record.ttl
unused_pool = _DummyPool('unused')
td_mock.all_response_pools = \
ruleset_mock.response_pools + [unused_pool]
get_response_pool_mock.return_value = unused_pool
provider._find_or_create_dynamic_pool.side_effect = [
_DummyPool('default'),
_DummyPool('one'),
_DummyPool('two'),
ruleset_mock.response_pools[0],
]
change = Create(self.dynamic_a_record)
provider._mod_dynamic_rulesets(td_mock, change)
add_response_pool_mock.assert_has_calls((
# default
call('default'),
# first dynamic and it's fallback
call('one'),
call('default', index=999),
# 2nd dynamic and it's fallback
call('three'),
call('default', index=999),
# 3nd dynamic, from existing, and it's fallback
call('two'),
call('three', index=999),
call('default', index=999),
))
ruleset_create_mock.assert_has_calls((
call(td_mock, index=2),
call(td_mock, index=2),
call(td_mock, index=2),
call(td_mock, index=2),
))
# unused poll should have been deleted
self.assertTrue(unused_pool.deleted)
# old ruleset ruleset should be deleted, it's pool will have been
# reused
ruleset_mock.delete.assert_called_once()
# have to patch the place it's imported into, not where it lives
@patch('octodns.provider.dyn.get_response_pool')
@patch('dyn.tm.services.DSFRuleset.add_response_pool')
@patch('dyn.tm.services.DSFRuleset.create')
# just lets us ignore the pool.create calls
@patch('dyn.tm.services.DSFResponsePool.create')
def test_mod_dynamic_rulesets_fallback_loop(self, _, ruleset_create_mock,
add_response_pool_mock,
get_response_pool_mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
ruleset_mock = MagicMock()
ruleset_mock.response_pools = [_DummyPool('three')]
td_mock = MagicMock()
td_mock._rulesets = [
ruleset_mock,
]
provider._traffic_director_monitor = MagicMock()
provider._find_or_create_dynamic_pool = MagicMock()
# Matching ttl
td_mock.ttl = self.dynamic_fallback_loop.ttl
unused_pool = _DummyPool('unused')
td_mock.all_response_pools = \
ruleset_mock.response_pools + [unused_pool]
get_response_pool_mock.return_value = unused_pool
provider._find_or_create_dynamic_pool.side_effect = [
_DummyPool('default'),
_DummyPool('one'),
_DummyPool('two'),
ruleset_mock.response_pools[0],
]
change = Create(self.dynamic_fallback_loop)
provider._mod_dynamic_rulesets(td_mock, change)
add_response_pool_mock.assert_has_calls((
# default
call('default'),
# first dynamic and it's fallback
call('one'),
call('default', index=999),
# 2nd dynamic and it's fallback (no loop)
call('three'),
call('two', index=999),
call('default', index=999),
# 3nd dynamic and it's fallback (no loop)
call('two'),
call('three', index=999),
call('default', index=999),
))
ruleset_create_mock.assert_has_calls((
call(td_mock, index=2),
call(td_mock, index=2),
call(td_mock, index=2),
call(td_mock, index=2),
))
# unused poll should have been deleted
self.assertTrue(unused_pool.deleted)
# old ruleset ruleset should be deleted, it's pool will have been
# reused
ruleset_mock.delete.assert_called_once()
with open('./tests/fixtures/dyn-traffic-director-get.json') as fh:
traffic_director_response = loads(fh.read())
@property
def traffic_directors_response(self):
return {
'data': [{
'active': 'Y',
'label': 'unit.tests.:A',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}, {
'active': 'Y',
'label': 'some.other.:A',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}, {
'active': 'Y',
'label': 'other format',
'nodes': [],
'notifiers': [],
'pending_change': '',
'rulesets': [],
'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
'ttl': '300'
}]
}
@patch('dyn.core.SessionEngine.execute')
def test_mod_dynamic_create(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# will be tested separately
provider._mod_dynamic_rulesets = MagicMock()
mock.side_effect = [
# create traffic director
self.traffic_director_response,
# get traffic directors
self.traffic_directors_response
]
provider._mod_dynamic_Create(None, Create(self.dynamic_a_record))
# td now lives in cache
self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
# should have seen 1 gen call
provider._mod_dynamic_rulesets.assert_called_once()
def test_mod_dynamic_update_dynamic_dynamic(self):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# update of an existing dynamic td
# pre-populate the cache with our mock td
provider._traffic_directors = {
'unit.tests.': {
'A': 42,
}
}
# mock _mod_dynamic_rulesets
provider._mod_dynamic_rulesets = MagicMock()
dyn = self.dynamic_a_record
change = Update(dyn, dyn)
provider._mod_dynamic_Update(None, change)
# still in cache
self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
# should have seen 1 gen call
provider._mod_dynamic_rulesets.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_dynamic_update_dynamic_geo(self, _):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# convert a dynamic td to a geo record
provider._mod_geo_Update = MagicMock()
change = Update(self.dynamic_a_record, self.geo_a_record)
provider._mod_dynamic_Update(42, change)
# should have seen a call to create the new geo record
provider._mod_geo_Update.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_dynamic_update_dynamic_regular(self, _):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# convert a dynamic td to a regular record
provider._mod_Create = MagicMock()
provider._mod_dynamic_Delete = MagicMock()
change = Update(self.dynamic_a_record, self.regular_a_record)
provider._mod_dynamic_Update(42, change)
# should have seen a call to create the new regular record
provider._mod_Create.assert_called_once_with(42, change)
# should have seen a call to delete the old td record
provider._mod_dynamic_Delete.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_dynamic_update_geo_dynamic(self, _):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# convert a geo record to a dynamic td
# pre-populate the cache with our mock td
provider._traffic_directors = {
'unit.tests.': {
'A': 42,
}
}
# mock _mod_dynamic_rulesets
provider._mod_dynamic_rulesets = MagicMock()
change = Update(self.geo_a_record, self.dynamic_a_record)
provider._mod_dynamic_Update(None, change)
# still in cache
self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
# should have seen 1 gen call
provider._mod_dynamic_rulesets.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_dynamic_update_regular_dynamic(self, _):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# convert a regular record to a dynamic td
provider._mod_dynamic_Create = MagicMock()
provider._mod_Delete = MagicMock()
change = Update(self.regular_a_record, self.dynamic_a_record)
provider._mod_dynamic_Update(42, change)
# should have seen a call to create the new geo record
provider._mod_dynamic_Create.assert_called_once_with(42, change)
# should have seen a call to delete the old regular record
provider._mod_Delete.assert_called_once_with(42, change)
@patch('dyn.core.SessionEngine.execute')
def test_mod_dynamic_delete(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
td_mock = MagicMock()
provider._traffic_directors = {
'unit.tests.': {
'A': td_mock,
}
}
provider._mod_dynamic_Delete(None, Delete(self.dynamic_a_record))
# delete called
td_mock.delete.assert_called_once()
# removed from cache
self.assertFalse('A' in provider.traffic_directors['unit.tests.'])
@patch('dyn.core.SessionEngine.execute')
def test_apply_traffic_directors_dynamic(self, mock):
provider = DynProvider('test', 'cust', 'user', 'pass',
traffic_directors_enabled=True)
# will be tested separately
provider._mod_dynamic_Create = MagicMock()
changes = [Create(self.dynamic_a_record)]
provider._apply_traffic_directors(self.zone, changes, None)
provider._mod_dynamic_Create.assert_called_once()