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
2017-10-18 10:38:09 -07:00

1286 lines
46 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
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()
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()
# 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_reponse(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',
'dsf_monitor_id': monitor_id,
'endpoints': [],
'label': 'unit.tests.',
'notifier': '',
'options': {
'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'
}],
'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 ingored 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'], tds['unit.tests.'].keys())
self.assertEquals(['A'], tds['geo.unit.tests.'].keys())
provider.log.warn.assert_called_with("Failed to load TraficDirector "
"'%s': %s", 'something else',
'need more than 1 value to '
'unpack')
@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
# 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.',
'notifier': '',
'options': {
'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
monitor = provider._traffic_director_monitor('geo.unit.tests.')
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.',
'probe_interval': 60,
'active': 'Y',
'options': {
'path': '/_dns',
'host': 'geo.unit.tests',
'header': 'User-Agent: Dyn Monitor',
'port': 443,
'timeout': 10
}
})
])
# created monitor is now cached
self.assertTrue('geo.unit.tests.' in
provider._traffic_director_monitors)
# pre-existing one is there too
self.assertTrue('unit.tests.' in
provider._traffic_director_monitors)
# now ask for a monitor that does exist
mock.reset_mock()
monitor = provider._traffic_director_monitor('unit.tests.')
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()
@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)
# only traffic director
mock.side_effect = [
# get traffic directors
self.traffic_directors_reponse,
# get traffic director
self.traffic_director_response,
# get zone
{'data': {}},
# get records
{'data': {}},
]
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/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
{'pending_changes': '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_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_reponse,
# get traffic director
self.traffic_director_response,
# 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/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
{'pending_changes': 'Y'}),
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_reponse,
# get traffic director
busted_traffic_director_response,
# 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/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
{'pending_changes': 'Y'}),
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)
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 seperately
provider._mod_rulesets = MagicMock()
mock.side_effect = [
# create traffic director
self.traffic_director_response,
# get traffic directors
self.traffic_directors_reponse
]
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_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_rulesets
provider._mod_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_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_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_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_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_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, non-matching label
mock.reset_mock()
values = ['2.2.3.4.', '2.2.3.5']
miss = provider._find_or_create_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_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_pool = MagicMock()
td_mock.all_response_pools = []
provider._find_or_create_pool.side_effect = [
_DummyPool('default'),
_DummyPool(1),
_DummyPool(2),
_DummyPool(3),
_DummyPool(4),
]
change = Create(self.geo_record)
provider._mod_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_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_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_pool.side_effect = [
_DummyPool('default'),
_DummyPool(1),
_DummyPool(2),
ruleset_mock.response_pools[0],
_DummyPool(4),
]
change = Create(self.geo_record)
provider._mod_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, 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))