class TestDnsMadeEasyProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record(Record.new(expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) # Add some ALIAS records expected.add_record(Record.new(expected, '', { 'ttl': 1800, 'type': 'ALIAS', 'value': 'aname.unit.tests.' })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break def test_populate(self): provider = DnsMadeEasyProvider('test', 'api', 'secret') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"error": ["API key not found"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # Bad request with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"error": ["Rate limit exceeded"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('\n - Rate limit exceeded', text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='<html><head></head><body></body></html>') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dnsmadeeasy.com/V2.0/dns/managed' with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: mock.get('{}{}'.format(base, '/'), text=fh.read()) with open('tests/fixtures/dnsmadeeasy-records.json') as fh: mock.get('{}{}'.format(base, '/123123/records'), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(14, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(14, len(again.records)) # bust the cache del provider._zone_records[zone.name] def test_apply(self): # Create provider with sandbox enabled provider = DnsMadeEasyProvider('test', 'api', 'secret', True) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: domains = json.load(fh) # non-existent domain, create everything resp.json.side_effect = [ DnsMadeEasyClientNotFound, # no zone in populate DnsMadeEasyClientNotFound, # no domain during apply domains ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 10 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # created the domain call('POST', '/', data={'name': 'unit.tests'}), # get all domains to build the cache call('GET', '/'), # created at least some of the record with expected data call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.4', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.5', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'ANAME', 'name': '', 'value': 'aname.unit.tests.', 'ttl': 1800}), call('POST', '/123123/records', data={ 'name': '', 'value': 'ca.unit.tests', 'issuerCritical': 0, 'caaType': 'issue', 'ttl': 3600, 'type': 'CAA'}), call('POST', '/123123/records', data={ 'name': '_srv._tcp', 'weight': 20, 'value': 'foo-1.unit.tests.', 'priority': 10, 'ttl': 600, 'type': 'SRV', 'port': 30 }), ]) self.assertEquals(26, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[ { 'id': 11189897, 'name': 'www', 'value': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'value': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'value': '3.2.3.4', 'ttl': 600, 'type': 'A', } ]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record(Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/123123/records', data={ 'value': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/11189899'), call('DELETE', '/123123/records/11189897'), call('DELETE', '/123123/records/11189898') ], any_order=True)
def test_process_plan(self): ownership = OwnershipProcessor('ownership') provider = PlannableProvider('helper') # No plan, is a quick noop self.assertFalse(ownership.process_plan(None)) # Nothing exists create both records and ownership ownership_added = ownership.process_source_zone(zone.copy()) plan = provider.plan(ownership_added) self.assertTrue(plan) # Double the number of records self.assertEquals(len(records) * 2, len(plan.changes)) # Now process the plan, shouldn't make any changes, we're creating # everything got = ownership.process_plan(plan) self.assertTrue(got) self.assertEquals(len(records) * 2, len(got.changes)) # Something extra exists and doesn't have ownership TXT, leave it # alone, we don't own it. extra_a = Record.new(zone, 'extra-a', { 'ttl': 30, 'type': 'A', 'value': '4.4.4.4', }) plan.existing.add_record(extra_a) # If we'd done a "real" plan we'd have a delete for the extra thing. plan.changes.append(Delete(extra_a)) # Process the plan, shouldn't make any changes since the extra bit is # something we don't own got = ownership.process_plan(plan) self.assertTrue(got) self.assertEquals(len(records) * 2, len(got.changes)) # Something extra exists and does have an ownership record so we will # delete it... copy = Zone('unit.tests.', []) for record in records.values(): if record.name != 'the-a': copy.add_record(record) # New ownership, without the `the-a` ownership_added = ownership.process_source_zone(copy) self.assertEquals(len(records) * 2 - 2, len(ownership_added.records)) plan = provider.plan(ownership_added) # Fake the extra existing by adding the record, its ownership, and the # two delete changes. the_a = records['the-a'] plan.existing.add_record(the_a) name = f'{ownership.txt_name}.a.the-a' the_a_ownership = Record.new(zone, name, { 'ttl': 30, 'type': 'TXT', 'value': ownership.txt_value, }) plan.existing.add_record(the_a_ownership) plan.changes.append(Delete(the_a)) plan.changes.append(Delete(the_a_ownership)) # Finally process the plan, should be a noop and we should get the same # plan out, meaning the planned deletes were allowed to happen. got = ownership.process_plan(plan) self.assertTrue(got) self.assertEquals(plan, got) self.assertEquals(len(plan.changes), len(got.changes))
def test_populate(self): _expected = self.make_expected() # Unhappy Plan - Not authenticated # Live test against API, will fail in an unauthorized error with self.assertRaises(WebFault) as ctx: provider = TransipProvider('test', 'unittest', self.bogus_key) zone = Zone('unit.tests.', []) provider.populate(zone, True) self.assertEquals(str('WebFault'), str(ctx.exception.__class__.__name__)) self.assertEquals(str('200'), ctx.exception.fault.faultcode) # Unhappy Plan - Zone does not exists # Will trigger an exception if provider is used as a target for a # non-existing zone with self.assertRaises(Exception) as ctx: provider = TransipProvider('test', 'unittest', self.bogus_key) provider._client = MockDomainService('unittest', self.bogus_key) zone = Zone('notfound.unit.tests.', []) provider.populate(zone, True) self.assertEquals(str('TransipNewZoneException'), str(ctx.exception.__class__.__name__)) self.assertEquals( 'populate: (102) Transip used as target' + ' for non-existing zone: notfound.unit.tests.', text_type(ctx.exception)) # Happy Plan - Zone does not exists # Won't trigger an exception if provider is NOT used as a target for a # non-existing zone. provider = TransipProvider('test', 'unittest', self.bogus_key) provider._client = MockDomainService('unittest', self.bogus_key) zone = Zone('notfound.unit.tests.', []) provider.populate(zone, False) # Happy Plan - Populate with mockup records provider = TransipProvider('test', 'unittest', self.bogus_key) provider._client = MockDomainService('unittest', self.bogus_key) provider._client.mockup(_expected.records) zone = Zone('unit.tests.', []) provider.populate(zone, False) # Transip allows relative values for types like cname, mx. # Test is these are correctly appended with the domain provider._currentZone = zone self.assertEquals("www.unit.tests.", provider._parse_to_fqdn("www")) self.assertEquals("www.unit.tests.", provider._parse_to_fqdn("www.unit.tests.")) self.assertEquals("www.sub.sub.sub.unit.tests.", provider._parse_to_fqdn("www.sub.sub.sub")) self.assertEquals("unit.tests.", provider._parse_to_fqdn("@")) # Happy Plan - Even if the zone has no records the zone should exist provider = TransipProvider('test', 'unittest', self.bogus_key) provider._client = MockDomainService('unittest', self.bogus_key) zone = Zone('unit.tests.', []) exists = provider.populate(zone, True) self.assertTrue(exists, 'populate should return true') return
def test_populate(self): provider = DnsimpleProvider('test', 'token', 42) # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"message": "Authentication failed"}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"message": "Domain `foo.bar` not found"}') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \ 'records?page=' with open('tests/fixtures/dnsimple-page-1.json') as fh: mock.get('{}{}'.format(base, 1), text=fh.read()) with open('tests/fixtures/dnsimple-page-2.json') as fh: mock.get('{}{}'.format(base, 2), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(16, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(1, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(16, len(again.records)) # bust the cache del provider._zone_records[zone.name] # test handling of invalid content with requests_mock() as mock: with open('tests/fixtures/dnsimple-invalid-content.json') as fh: mock.get(ANY, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone, lenient=True) self.assertEquals(set([ Record.new(zone, '', { 'ttl': 3600, 'type': 'SSHFP', 'values': [] }, lenient=True), Record.new(zone, '_srv._tcp', { 'ttl': 600, 'type': 'SRV', 'values': [] }, lenient=True), Record.new(zone, 'naptr', { 'ttl': 600, 'type': 'NAPTR', 'values': [] }, lenient=True), ]), zone.records)
def test_existing_nameservers(self): ns_values = ['8.8.8.8.', '9.9.9.9.'] provider = PowerDnsProvider('test', 'non.existent', 'api-key', nameserver_values=ns_values) expected = Zone('unit.tests.', []) ns_record = Record.new(expected, '', { 'type': 'NS', 'ttl': 600, 'values': ns_values }) expected.add_record(ns_record) # no changes with requests_mock() as mock: data = { 'rrsets': [{ 'comments': [], 'name': 'unit.tests.', 'records': [{ 'content': '8.8.8.8.', 'disabled': False }, { 'content': '9.9.9.9.', 'disabled': False }], 'ttl': 600, 'type': 'NS' }, { 'comments': [], 'name': 'unit.tests.', 'records': [{ 'content': '1.2.3.4', 'disabled': False, }], 'ttl': 60, 'type': 'A' }] } mock.get(ANY, status_code=200, json=data) mock.get('http://non.existent:8081/api/v1/servers/localhost', status_code=200, json={'version': '4.1.0'}) unrelated_record = Record.new(expected, '', { 'type': 'A', 'ttl': 60, 'value': '1.2.3.4' }) expected.add_record(unrelated_record) plan = provider.plan(expected) self.assertFalse(plan) # remove it now that we don't need the unrelated change any longer expected._remove_record(unrelated_record) # ttl diff with requests_mock() as mock: data = { 'rrsets': [{ 'comments': [], 'name': 'unit.tests.', 'records': [ { 'content': '8.8.8.8.', 'disabled': False }, { 'content': '9.9.9.9.', 'disabled': False }, ], 'ttl': 3600, 'type': 'NS' }] } mock.get(ANY, status_code=200, json=data) mock.get('http://non.existent:8081/api/v1/servers/localhost', status_code=200, json={'version': '4.1.0'}) plan = provider.plan(expected) self.assertEquals(1, len(plan.changes)) # create with requests_mock() as mock: data = {'rrsets': []} mock.get(ANY, status_code=200, json=data) plan = provider.plan(expected) self.assertEquals(1, len(plan.changes))
def test__apply(self, *_): class DummyDesired: def __init__(self, name, changes): self.name = name self.changes = changes apply_z = Zone("unit.tests.", []) create_r = Record.new(apply_z, '', { 'ttl': 0, 'type': 'A', 'values': ['1.2.3.4', '10.10.10.10']}) delete_r = Record.new(apply_z, 'a', { 'ttl': 1, 'type': 'A', 'values': ['1.2.3.4', '1.1.1.1']}) update_existing_r = Record.new(apply_z, 'aa', { 'ttl': 9001, 'type': 'A', 'values': ['1.2.4.3']}) update_new_r = Record.new(apply_z, 'aa', { 'ttl': 666, 'type': 'A', 'values': ['1.4.3.2']}) gcloud_zone_mock = DummyGoogleCloudZone("unit.tests.", "unit-tests") status_mock = Mock() return_values_for_status = iter( ["pending"] * 11 + ['done', 'done']) type(status_mock).status = PropertyMock( side_effect=lambda: next(return_values_for_status)) gcloud_zone_mock.changes = Mock(return_value=status_mock) provider = self._get_provider() provider.gcloud_client = Mock() provider._gcloud_zones = {"unit.tests.": gcloud_zone_mock} desired = Mock() desired.name = "unit.tests." changes = [] changes.append(Create(create_r)) changes.append(Delete(delete_r)) changes.append(Update(existing=update_existing_r, new=update_new_r)) provider.apply(Plan( existing=[update_existing_r, delete_r], desired=desired, changes=changes, exists=True )) calls_mock = gcloud_zone_mock.changes.return_value mocked_calls = [] for mock_call in calls_mock.add_record_set.mock_calls: mocked_calls.append(mock_call[1][0]) self.assertEqual(mocked_calls, [ DummyResourceRecordSet( 'unit.tests.', 'A', 0, ['1.2.3.4', '10.10.10.10']), DummyResourceRecordSet( 'aa.unit.tests.', 'A', 666, ['1.4.3.2']) ]) mocked_calls2 = [] for mock_call in calls_mock.delete_record_set.mock_calls: mocked_calls2.append(mock_call[1][0]) self.assertEqual(mocked_calls2, [ DummyResourceRecordSet( 'a.unit.tests.', 'A', 1, ['1.2.3.4', '1.1.1.1']), DummyResourceRecordSet( 'aa.unit.tests.', 'A', 9001, ['1.2.4.3']) ]) type(status_mock).status = "pending" with self.assertRaises(RuntimeError): provider.apply(Plan( existing=[update_existing_r, delete_r], desired=desired, changes=changes, exists=True )) unsupported_change = Mock() unsupported_change.__len__ = Mock(return_value=1) type_mock = Mock() type_mock._type = "A" unsupported_change.record = type_mock mock_plan = Mock() type(mock_plan).desired = PropertyMock(return_value=DummyDesired( "dummy name", [])) type(mock_plan).changes = [unsupported_change] with self.assertRaises(RuntimeError): provider.apply(mock_plan)
def test_apply(self): provider = DnsimpleProvider('test', 'token', 42) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existent domain, create everything resp.json.side_effect = [ DnsimpleClientNotFound, # no zone in populate DnsimpleClientNotFound, # no domain during apply ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded n = len(self.expected.records) - 3 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) self.assertFalse(plan.exists) provider._client._request.assert_has_calls([ # created the domain call('POST', '/domains', data={'name': 'unit.tests'}), # created at least some of the record with expected data call('POST', '/zones/unit.tests/records', data={ 'content': '1.2.3.4', 'type': 'A', 'name': '', 'ttl': 300}), call('POST', '/zones/unit.tests/records', data={ 'content': '1.2.3.5', 'type': 'A', 'name': '', 'ttl': 300}), call('POST', '/zones/unit.tests/records', data={ 'content': '0 issue "ca.unit.tests"', 'type': 'CAA', 'name': '', 'ttl': 3600}), call('POST', '/zones/unit.tests/records', data={ 'content': '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49', 'type': 'SSHFP', 'name': '', 'ttl': 3600}), call('POST', '/zones/unit.tests/records', data={ 'content': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73', 'type': 'SSHFP', 'name': '', 'ttl': 3600}), call('POST', '/zones/unit.tests/records', data={ 'content': '20 30 foo-1.unit.tests.', 'priority': 10, 'type': 'SRV', 'name': '_srv._tcp', 'ttl': 600 }), ]) # expected number of total calls self.assertEquals(28, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[ { 'id': 11189897, 'name': 'www', 'content': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'content': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'content': '3.2.3.4', 'ttl': 600, 'type': 'A', } ]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record(Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertTrue(plan.exists) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/zones/unit.tests/records', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/zones/unit.tests/records/11189899'), call('DELETE', '/zones/unit.tests/records/11189897'), call('DELETE', '/zones/unit.tests/records/11189898') ], any_order=True)
def test_sync_create(self): provider, stubber = self._get_stubbed_provider() got = Zone('unit.tests.', []) list_hosted_zones_resp = { 'HostedZones': [], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) plan = provider.plan(self.expected) self.assertEquals(9, len(plan.changes)) self.assertFalse(plan.exists) for change in plan.changes: self.assertIsInstance(change, Create) stubber.assert_no_pending_responses() create_hosted_zone_resp = { 'HostedZone': { 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }, 'ChangeInfo': { 'Id': 'a12', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', 'Comment': 'hrm', }, 'DelegationSet': { 'Id': 'b23', 'CallerReference': 'blip', 'NameServers': [ 'n12.unit.tests.', ], }, 'Location': 'us-east-1', } stubber.add_response('create_hosted_zone', create_hosted_zone_resp, { 'Name': got.name, 'CallerReference': ANY, }) stubber.add_response( 'list_health_checks', { 'HealthChecks': self.health_checks, 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, { 'HostedZoneId': 'z42', 'ChangeBatch': ANY }) self.assertEquals(9, provider.apply(plan)) stubber.assert_no_pending_responses()
def test_apply(self): provider = CloudflareProvider('test', 'email', 'token') provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create ] + [None] * 16 # individual record creates # non-existant zone, create everything plan = provider.plan(self.expected) self.assertEquals(9, len(plan.changes)) self.assertEquals(9, provider.apply(plan)) provider._request.assert_has_calls( [ # created the domain call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), # created at least one of the record with expected data call('POST', '/zones/42/dns_records', data={ 'content': 'ns1.unit.tests.', 'type': 'NS', 'name': 'under.unit.tests', 'ttl': 3600 }), # make sure semicolons are not escaped when sending data call('POST', '/zones/42/dns_records', data={ 'content': 'v=DKIM1;k=rsa;s=email;h=sha256;' 'p=A/kinda+of/long/string+with+numb3rs', 'type': 'TXT', 'name': 'txt.unit.tests', 'ttl': 600 }), ], True) # expected number of total calls self.assertEquals(18, provider._request.call_count) provider._request.reset_mock() provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "www.unit.tests", "content": "1.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "A", "name": "www.unit.tests", "content": "2.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "nc.unit.tests", "content": "3.2.3.4", "proxiable": True, "proxied": False, "ttl": 120, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "ttl.unit.tests", "content": "4.2.3.4", "proxiable": True, "proxied": False, "ttl": 600, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, ]) # we don't care about the POST/create return values provider._request.return_value = {} provider._request.side_effect = None wanted = Zone('unit.tests.', []) wanted.add_record( Record.new( wanted, 'nc', { 'ttl': 60, # TTL is below their min 'type': 'A', 'value': '3.2.3.4' })) wanted.add_record( Record.new( wanted, 'ttl', { 'ttl': 300, # TTL change 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) # only see the delete & ttl update, below min-ttl is filtered out self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._request.assert_has_calls([ call('POST', '/zones/42/dns_records', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl.unit.tests', 'ttl': 300 }), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997655'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997653'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997654') ])
def test_populate(self): provider, stubber = self._get_stubbed_provider() got = Zone('unit.tests.', []) with self.assertRaises(ClientError): stubber.add_client_error('list_hosted_zones') provider.populate(got) with self.assertRaises(ClientError): list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) stubber.add_client_error('list_resource_record_sets', expected_params={'HostedZoneId': u'z42'}) provider.populate(got) stubber.assert_no_pending_responses() # list_hosted_zones has been cached from now on so we don't have to # worry about stubbing it list_resource_record_sets_resp_p1 = { 'ResourceRecordSets': [{ 'Name': 'simple.unit.tests.', 'Type': 'A', 'ResourceRecords': [{ 'Value': '1.2.3.4', }, { 'Value': '2.2.3.4', }], 'TTL': 60, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'CountryCode': '*', }, 'ResourceRecords': [{ 'Value': '2.2.3.4', }, { 'Value': '3.2.3.4', }], 'TTL': 61, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'ContinentCode': 'AF', }, 'ResourceRecords': [{ 'Value': '4.2.3.4', }], 'TTL': 61, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'CountryCode': 'US', }, 'ResourceRecords': [{ 'Value': '5.2.3.4', }, { 'Value': '6.2.3.4', }], 'TTL': 61, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'CountryCode': 'US', 'SubdivisionCode': 'CA', }, 'ResourceRecords': [{ 'Value': '7.2.3.4', }], 'TTL': 61, }], 'IsTruncated': True, 'NextRecordName': 'next_name', 'NextRecordType': 'next_type', 'MaxItems': '100', } stubber.add_response('list_resource_record_sets', list_resource_record_sets_resp_p1, {'HostedZoneId': 'z42'}) list_resource_record_sets_resp_p2 = { 'ResourceRecordSets': [{ 'Name': 'cname.unit.tests.', 'Type': 'CNAME', 'ResourceRecords': [{ 'Value': 'unit.tests.', }], 'TTL': 62, }, { 'Name': 'txt.unit.tests.', 'Type': 'TXT', 'ResourceRecords': [{ 'Value': '"Hello World!"', }, { 'Value': '"Goodbye World?"', }], 'TTL': 63, }, { 'Name': 'unit.tests.', 'Type': 'MX', 'ResourceRecords': [{ 'Value': '10 smtp-1.unit.tests.', }, { 'Value': '20 smtp-2.unit.tests.', }], 'TTL': 64, }, { 'Name': 'naptr.unit.tests.', 'Type': 'NAPTR', 'ResourceRecords': [{ 'Value': '10 20 "U" "SIP+D2U" ' '"!^.*$!sip:[email protected]!" .', }], 'TTL': 65, }, { 'Name': '_srv._tcp.unit.tests.', 'Type': 'SRV', 'ResourceRecords': [{ 'Value': '10 20 30 cname.unit.tests.', }], 'TTL': 66, }, { 'Name': 'unit.tests.', 'Type': 'NS', 'ResourceRecords': [{ 'Value': 'ns1.unit.tests.', }], 'TTL': 67, }, { 'Name': 'sub.unit.tests.', 'Type': 'NS', 'GeoLocation': { 'ContinentCode': 'AF', }, 'ResourceRecords': [{ 'Value': '5.2.3.4.', }, { 'Value': '6.2.3.4.', }], 'TTL': 68, }, { 'Name': 'soa.unit.tests.', 'Type': 'SOA', 'ResourceRecords': [{ 'Value': 'ns1.unit.tests.', }], 'TTL': 69, }, { 'Name': 'unit.tests.', 'Type': 'CAA', 'ResourceRecords': [{ 'Value': '0 issue "ca.unit.tests"', }], 'TTL': 69, }, { 'AliasTarget': { 'HostedZoneId': 'Z119WBBTVP5WFX', 'EvaluateTargetHealth': False, 'DNSName': 'unit.tests.' }, 'Type': 'A', 'Name': 'alias.unit.tests.' }], 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response( 'list_resource_record_sets', list_resource_record_sets_resp_p2, { 'HostedZoneId': 'z42', 'StartRecordName': 'next_name', 'StartRecordType': 'next_type' }) # Load everything provider.populate(got) # Make sure we got what we expected changes = self.expected.changes(got, GeoProvider()) self.assertEquals(0, len(changes)) stubber.assert_no_pending_responses() # Populate a zone that doesn't exist nonexistent = Zone('does.not.exist.', []) provider.populate(nonexistent) self.assertEquals(set(), nonexistent.records)
class TestRoute53Provider(TestCase): expected = Zone('unit.tests.', []) for name, data in ( ('simple', { 'ttl': 60, 'type': 'A', 'values': ['1.2.3.4', '2.2.3.4'] }), ('', { 'ttl': 61, 'type': 'A', 'values': ['2.2.3.4', '3.2.3.4'], 'geo': { 'AF': ['4.2.3.4'], 'NA-US': ['5.2.3.4', '6.2.3.4'], 'NA-US-CA': ['7.2.3.4'] } }), ('cname', { 'ttl': 62, 'type': 'CNAME', 'value': 'unit.tests.' }), ('txt', { 'ttl': 63, 'type': 'TXT', 'values': ['Hello World!', 'Goodbye World?'] }), ('', { 'ttl': 64, 'type': 'MX', 'values': [{ 'preference': 10, 'exchange': 'smtp-1.unit.tests.', }, { 'preference': 20, 'exchange': 'smtp-2.unit.tests.', }] }), ('naptr', { 'ttl': 65, 'type': 'NAPTR', 'value': { 'order': 10, 'preference': 20, 'flags': 'U', 'service': 'SIP+D2U', 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', } }), ('_srv._tcp', { 'ttl': 66, 'type': 'SRV', 'value': { 'priority': 10, 'weight': 20, 'port': 30, 'target': 'cname.unit.tests.' } }), ('', { 'ttl': 67, 'type': 'NS', 'values': ['8.2.3.4.', '9.2.3.4.'] }), ('sub', { 'ttl': 68, 'type': 'NS', 'values': ['5.2.3.4.', '6.2.3.4.'] }), ('', { 'ttl': 69, 'type': 'CAA', 'value': { 'flags': 0, 'tag': 'issue', 'value': 'ca.unit.tests' } }), ): record = Record.new(expected, name, data) expected.add_record(record) caller_ref = '{}:A:unit.tests.:1324' \ .format(Route53Provider.HEALTH_CHECK_VERSION) health_checks = [ { 'Id': '42', 'CallerReference': caller_ref, 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '4.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, }, { 'Id': 'ignored-also', 'CallerReference': 'something-else', 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '5.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 42, }, { 'Id': '43', 'CallerReference': caller_ref, 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '5.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, }, { 'Id': '44', 'CallerReference': caller_ref, 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '7.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, }, { 'Id': '45', # won't match anything based on type 'CallerReference': caller_ref.replace(':A:', ':AAAA:'), 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '7.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, } ] def _get_stubbed_provider(self): provider = Route53Provider('test', 'abc', '123') # Use the stubber stubber = Stubber(provider._conn) stubber.activate() return (provider, stubber) def _get_stubbed_fallback_auth_provider(self): provider = Route53Provider('test') # Use the stubber stubber = Stubber(provider._conn) stubber.activate() return (provider, stubber) def test_populate_with_fallback(self): provider, stubber = self._get_stubbed_fallback_auth_provider() got = Zone('unit.tests.', []) with self.assertRaises(ClientError): stubber.add_client_error('list_hosted_zones') provider.populate(got) def test_populate(self): provider, stubber = self._get_stubbed_provider() got = Zone('unit.tests.', []) with self.assertRaises(ClientError): stubber.add_client_error('list_hosted_zones') provider.populate(got) with self.assertRaises(ClientError): list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) stubber.add_client_error('list_resource_record_sets', expected_params={'HostedZoneId': u'z42'}) provider.populate(got) stubber.assert_no_pending_responses() # list_hosted_zones has been cached from now on so we don't have to # worry about stubbing it list_resource_record_sets_resp_p1 = { 'ResourceRecordSets': [{ 'Name': 'simple.unit.tests.', 'Type': 'A', 'ResourceRecords': [{ 'Value': '1.2.3.4', }, { 'Value': '2.2.3.4', }], 'TTL': 60, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'CountryCode': '*', }, 'ResourceRecords': [{ 'Value': '2.2.3.4', }, { 'Value': '3.2.3.4', }], 'TTL': 61, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'ContinentCode': 'AF', }, 'ResourceRecords': [{ 'Value': '4.2.3.4', }], 'TTL': 61, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'CountryCode': 'US', }, 'ResourceRecords': [{ 'Value': '5.2.3.4', }, { 'Value': '6.2.3.4', }], 'TTL': 61, }, { 'Name': 'unit.tests.', 'Type': 'A', 'GeoLocation': { 'CountryCode': 'US', 'SubdivisionCode': 'CA', }, 'ResourceRecords': [{ 'Value': '7.2.3.4', }], 'TTL': 61, }], 'IsTruncated': True, 'NextRecordName': 'next_name', 'NextRecordType': 'next_type', 'MaxItems': '100', } stubber.add_response('list_resource_record_sets', list_resource_record_sets_resp_p1, {'HostedZoneId': 'z42'}) list_resource_record_sets_resp_p2 = { 'ResourceRecordSets': [{ 'Name': 'cname.unit.tests.', 'Type': 'CNAME', 'ResourceRecords': [{ 'Value': 'unit.tests.', }], 'TTL': 62, }, { 'Name': 'txt.unit.tests.', 'Type': 'TXT', 'ResourceRecords': [{ 'Value': '"Hello World!"', }, { 'Value': '"Goodbye World?"', }], 'TTL': 63, }, { 'Name': 'unit.tests.', 'Type': 'MX', 'ResourceRecords': [{ 'Value': '10 smtp-1.unit.tests.', }, { 'Value': '20 smtp-2.unit.tests.', }], 'TTL': 64, }, { 'Name': 'naptr.unit.tests.', 'Type': 'NAPTR', 'ResourceRecords': [{ 'Value': '10 20 "U" "SIP+D2U" ' '"!^.*$!sip:[email protected]!" .', }], 'TTL': 65, }, { 'Name': '_srv._tcp.unit.tests.', 'Type': 'SRV', 'ResourceRecords': [{ 'Value': '10 20 30 cname.unit.tests.', }], 'TTL': 66, }, { 'Name': 'unit.tests.', 'Type': 'NS', 'ResourceRecords': [{ 'Value': 'ns1.unit.tests.', }], 'TTL': 67, }, { 'Name': 'sub.unit.tests.', 'Type': 'NS', 'GeoLocation': { 'ContinentCode': 'AF', }, 'ResourceRecords': [{ 'Value': '5.2.3.4.', }, { 'Value': '6.2.3.4.', }], 'TTL': 68, }, { 'Name': 'soa.unit.tests.', 'Type': 'SOA', 'ResourceRecords': [{ 'Value': 'ns1.unit.tests.', }], 'TTL': 69, }, { 'Name': 'unit.tests.', 'Type': 'CAA', 'ResourceRecords': [{ 'Value': '0 issue "ca.unit.tests"', }], 'TTL': 69, }, { 'AliasTarget': { 'HostedZoneId': 'Z119WBBTVP5WFX', 'EvaluateTargetHealth': False, 'DNSName': 'unit.tests.' }, 'Type': 'A', 'Name': 'alias.unit.tests.' }], 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response( 'list_resource_record_sets', list_resource_record_sets_resp_p2, { 'HostedZoneId': 'z42', 'StartRecordName': 'next_name', 'StartRecordType': 'next_type' }) # Load everything provider.populate(got) # Make sure we got what we expected changes = self.expected.changes(got, GeoProvider()) self.assertEquals(0, len(changes)) stubber.assert_no_pending_responses() # Populate a zone that doesn't exist nonexistent = Zone('does.not.exist.', []) provider.populate(nonexistent) self.assertEquals(set(), nonexistent.records) def test_sync(self): provider, stubber = self._get_stubbed_provider() list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) list_resource_record_sets_resp = { 'ResourceRecordSets': [], 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_resource_record_sets', list_resource_record_sets_resp, {'HostedZoneId': 'z42'}) plan = provider.plan(self.expected) self.assertEquals(9, len(plan.changes)) self.assertTrue(plan.exists) for change in plan.changes: self.assertIsInstance(change, Create) stubber.assert_no_pending_responses() stubber.add_response( 'list_health_checks', { 'HealthChecks': self.health_checks, 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, { 'HostedZoneId': 'z42', 'ChangeBatch': ANY }) self.assertEquals(9, provider.apply(plan)) stubber.assert_no_pending_responses() # Delete by monkey patching in a populate that includes an extra record def add_extra_populate(existing, target, lenient): for record in self.expected.records: existing.add_record(record) record = Record.new(existing, 'extra', { 'ttl': 99, 'type': 'A', 'values': ['9.9.9.9'] }) existing.add_record(record) provider.populate = add_extra_populate change_resource_record_sets_params = { 'ChangeBatch': { 'Changes': [{ 'Action': 'DELETE', 'ResourceRecordSet': { 'Name': 'extra.unit.tests.', 'ResourceRecords': [{ 'Value': u'9.9.9.9' }], 'TTL': 99, 'Type': 'A' } }], u'Comment': ANY }, 'HostedZoneId': u'z42' } stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, change_resource_record_sets_params) plan = provider.plan(self.expected) self.assertEquals(1, len(plan.changes)) self.assertIsInstance(plan.changes[0], Delete) self.assertEquals(1, provider.apply(plan)) stubber.assert_no_pending_responses() # Update by monkey patching in a populate that modifies the A record # with geos def mod_geo_populate(existing, target, lenient): for record in self.expected.records: if record._type != 'A' or not record.geo: existing.add_record(record) record = Record.new( existing, '', { 'ttl': 61, 'type': 'A', 'values': ['8.2.3.4', '3.2.3.4'], 'geo': { 'AF': ['4.2.3.4'], 'NA-US': ['5.2.3.4', '6.2.3.4'], 'NA-US-KY': ['7.2.3.4'] } }) existing.add_record(record) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) provider.populate = mod_geo_populate change_resource_record_sets_params = { 'ChangeBatch': { 'Changes': [{ 'Action': 'DELETE', 'ResourceRecordSet': { 'GeoLocation': { 'CountryCode': 'US', 'SubdivisionCode': 'KY' }, 'Name': 'unit.tests.', 'ResourceRecords': [{ 'Value': '7.2.3.4' }], 'SetIdentifier': 'NA-US-KY', 'TTL': 61, 'Type': 'A' } }, { 'Action': 'CREATE', 'ResourceRecordSet': { 'GeoLocation': { 'CountryCode': 'US', 'SubdivisionCode': 'CA' }, 'Name': 'unit.tests.', 'ResourceRecords': [{ 'Value': '7.2.3.4' }], 'SetIdentifier': 'NA-US-CA', 'TTL': 61, 'Type': 'A' } }, { 'Action': 'UPSERT', 'ResourceRecordSet': { 'GeoLocation': { 'ContinentCode': 'AF' }, 'Name': 'unit.tests.', 'ResourceRecords': [{ 'Value': '4.2.3.4' }], 'SetIdentifier': 'AF', 'TTL': 61, 'Type': 'A' } }, { 'Action': 'UPSERT', 'ResourceRecordSet': { 'GeoLocation': { 'CountryCode': '*' }, 'Name': 'unit.tests.', 'ResourceRecords': [{ 'Value': '2.2.3.4' }, { 'Value': '3.2.3.4' }], 'SetIdentifier': 'default', 'TTL': 61, 'Type': 'A' } }, { 'Action': 'UPSERT', 'ResourceRecordSet': { 'GeoLocation': { 'CountryCode': 'US' }, 'Name': 'unit.tests.', 'ResourceRecords': [{ 'Value': '5.2.3.4' }, { 'Value': '6.2.3.4' }], 'SetIdentifier': 'NA-US', 'TTL': 61, 'Type': 'A' } }], 'Comment': ANY }, 'HostedZoneId': 'z42' } stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, change_resource_record_sets_params) plan = provider.plan(self.expected) self.assertEquals(1, len(plan.changes)) self.assertIsInstance(plan.changes[0], Update) self.assertEquals(1, provider.apply(plan)) stubber.assert_no_pending_responses() # Update converting to non-geo by monkey patching in a populate that # modifies the A record with geos def mod_add_geo_populate(existing, target, lenient): for record in self.expected.records: if record._type != 'A' or record.geo: existing.add_record(record) record = Record.new( existing, 'simple', { 'ttl': 61, 'type': 'A', 'values': ['1.2.3.4', '2.2.3.4'], 'geo': { 'OC': ['3.2.3.4', '4.2.3.4'], } }) existing.add_record(record) provider.populate = mod_add_geo_populate change_resource_record_sets_params = { 'ChangeBatch': { 'Changes': [{ 'Action': 'DELETE', 'ResourceRecordSet': { 'GeoLocation': { 'CountryCode': '*' }, 'Name': 'simple.unit.tests.', 'ResourceRecords': [{ 'Value': '1.2.3.4' }, { 'Value': '2.2.3.4' }], 'SetIdentifier': 'default', 'TTL': 61, 'Type': 'A' } }, { 'Action': 'DELETE', 'ResourceRecordSet': { 'GeoLocation': { 'ContinentCode': 'OC' }, 'Name': 'simple.unit.tests.', 'ResourceRecords': [{ 'Value': '3.2.3.4' }, { 'Value': '4.2.3.4' }], 'SetIdentifier': 'OC', 'TTL': 61, 'Type': 'A' } }, { 'Action': 'CREATE', 'ResourceRecordSet': { 'Name': 'simple.unit.tests.', 'ResourceRecords': [{ 'Value': '1.2.3.4' }, { 'Value': '2.2.3.4' }], 'TTL': 60, 'Type': 'A' } }], 'Comment': ANY }, 'HostedZoneId': 'z42' } stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, change_resource_record_sets_params) plan = provider.plan(self.expected) self.assertEquals(1, len(plan.changes)) self.assertIsInstance(plan.changes[0], Update) self.assertEquals(1, provider.apply(plan)) stubber.assert_no_pending_responses() def test_sync_create(self): provider, stubber = self._get_stubbed_provider() got = Zone('unit.tests.', []) list_hosted_zones_resp = { 'HostedZones': [], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) plan = provider.plan(self.expected) self.assertEquals(9, len(plan.changes)) self.assertFalse(plan.exists) for change in plan.changes: self.assertIsInstance(change, Create) stubber.assert_no_pending_responses() create_hosted_zone_resp = { 'HostedZone': { 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }, 'ChangeInfo': { 'Id': 'a12', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', 'Comment': 'hrm', }, 'DelegationSet': { 'Id': 'b23', 'CallerReference': 'blip', 'NameServers': [ 'n12.unit.tests.', ], }, 'Location': 'us-east-1', } stubber.add_response('create_hosted_zone', create_hosted_zone_resp, { 'Name': got.name, 'CallerReference': ANY, }) stubber.add_response( 'list_health_checks', { 'HealthChecks': self.health_checks, 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, { 'HostedZoneId': 'z42', 'ChangeBatch': ANY }) self.assertEquals(9, provider.apply(plan)) stubber.assert_no_pending_responses() def test_health_checks_pagination(self): provider, stubber = self._get_stubbed_provider() health_checks_p1 = [{ 'Id': '42', 'CallerReference': self.caller_ref, 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '4.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, }, { 'Id': '43', 'CallerReference': 'abc123', 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '9.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, }] stubber.add_response( 'list_health_checks', { 'HealthChecks': health_checks_p1, 'IsTruncated': True, 'MaxItems': '2', 'Marker': '', 'NextMarker': 'moar', }) health_checks_p2 = [{ 'Id': '44', 'CallerReference': self.caller_ref, 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '8.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, }] stubber.add_response( 'list_health_checks', { 'HealthChecks': health_checks_p2, 'IsTruncated': False, 'MaxItems': '2', 'Marker': 'moar', }, {'Marker': 'moar'}) health_checks = provider.health_checks self.assertEquals( { '42': health_checks_p1[0], '44': health_checks_p2[0], }, health_checks) stubber.assert_no_pending_responses() # get without create record = Record.new( self.expected, '', { 'ttl': 61, 'type': 'A', 'values': ['2.2.3.4', '3.2.3.4'], 'geo': { 'AF': ['4.2.3.4'], } }) id = provider.get_health_check_id(record, 'AF', record.geo['AF'], True) self.assertEquals('42', id) def test_health_check_create(self): provider, stubber = self._get_stubbed_provider() # No match based on type caller_ref = \ '{}:AAAA:foo1234'.format(Route53Provider.HEALTH_CHECK_VERSION) health_checks = [ { 'Id': '42', # No match based on version 'CallerReference': '9999:A:foo1234', 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '4.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, }, { 'Id': '43', 'CallerReference': caller_ref, 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '4.2.3.4', 'ResourcePath': '/_dns', 'Type': 'HTTPS', 'Port': 443, }, 'HealthCheckVersion': 2, } ] stubber.add_response( 'list_health_checks', { 'HealthChecks': health_checks, 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) health_check_config = { 'EnableSNI': False, 'FailureThreshold': 6, 'FullyQualifiedDomainName': 'foo.bar.com', 'IPAddress': '4.2.3.4', 'MeasureLatency': True, 'Port': 8080, 'RequestInterval': 10, 'ResourcePath': '/_status', 'Type': 'HTTP' } stubber.add_response( 'create_health_check', { 'HealthCheck': { 'Id': '42', 'CallerReference': self.caller_ref, 'HealthCheckConfig': health_check_config, 'HealthCheckVersion': 1, }, 'Location': 'http://url', }, { 'CallerReference': ANY, 'HealthCheckConfig': health_check_config, }) record = Record.new( self.expected, '', { 'ttl': 61, 'type': 'A', 'values': ['2.2.3.4', '3.2.3.4'], 'geo': { 'AF': ['4.2.3.4'], }, 'octodns': { 'healthcheck': { 'host': 'foo.bar.com', 'path': '/_status', 'port': 8080, 'protocol': 'HTTP', }, } }) # if not allowed to create returns none id = provider.get_health_check_id(record, 'AF', record.geo['AF'], False) self.assertFalse(id) # when allowed to create we do id = provider.get_health_check_id(record, 'AF', record.geo['AF'], True) self.assertEquals('42', id) stubber.assert_no_pending_responses() def test_health_check_gc(self): provider, stubber = self._get_stubbed_provider() stubber.add_response( 'list_health_checks', { 'HealthChecks': self.health_checks, 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) record = Record.new( self.expected, '', { 'ttl': 61, 'type': 'A', 'values': ['2.2.3.4', '3.2.3.4'], 'geo': { 'AF': ['4.2.3.4'], 'NA-US': ['5.2.3.4', '6.2.3.4'], # removed one geo } }) # gc no longer in_use records (directly) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) provider._gc_health_checks(record, [ DummyR53Record('42'), DummyR53Record('43'), ]) stubber.assert_no_pending_responses() # gc through _mod_Create stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) change = Create(record) provider._mod_Create(change) stubber.assert_no_pending_responses() # gc through _mod_Update stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) # first record is ignored for our purposes, we have to pass something change = Update(record, record) provider._mod_Create(change) stubber.assert_no_pending_responses() # gc through _mod_Delete, expect 3 to go away, can't check order # b/c it's not deterministic stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) change = Delete(record) provider._mod_Delete(change) stubber.assert_no_pending_responses() # gc only AAAA, leave the A's alone stubber.add_response('delete_health_check', {}, { 'HealthCheckId': '45', }) record = Record.new( self.expected, '', { 'ttl': 61, 'type': 'AAAA', 'value': '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b' }) provider._gc_health_checks(record, []) stubber.assert_no_pending_responses() def test_no_extra_changes(self): provider, stubber = self._get_stubbed_provider() list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) # empty is empty desired = Zone('unit.tests.', []) extra = provider._extra_changes(desired=desired, changes=[]) self.assertEquals([], extra) stubber.assert_no_pending_responses() # single record w/o geo is empty desired = Zone('unit.tests.', []) record = Record.new(desired, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', }) desired.add_record(record) extra = provider._extra_changes(desired=desired, changes=[]) self.assertEquals([], extra) stubber.assert_no_pending_responses() # short-circuit for unknown zone other = Zone('other.tests.', []) extra = provider._extra_changes(desired=other, changes=[]) self.assertEquals([], extra) stubber.assert_no_pending_responses() def test_extra_change_no_health_check(self): provider, stubber = self._get_stubbed_provider() list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) # record with geo and no health check returns change desired = Zone('unit.tests.', []) record = Record.new( desired, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', 'geo': { 'NA': ['2.2.3.4'], } }) desired.add_record(record) extra = provider._extra_changes(desired, []) self.assertEquals(0, len(extra)) stubber.assert_no_pending_responses() def test_extra_change_has_wrong_health_check(self): provider, stubber = self._get_stubbed_provider() list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) # record with geo and no health check returns change desired = Zone('unit.tests.', []) record = Record.new( desired, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', 'geo': { 'NA': ['2.2.3.4'], } }) desired.add_record(record) extra = provider._extra_changes(desired, []) self.assertEquals(0, len(extra)) stubber.assert_no_pending_responses() for change in (Create(record), Update(record, record), Delete(record)): extra = provider._extra_changes(desired=desired, changes=[change]) self.assertEquals(0, len(extra)) stubber.assert_no_pending_responses() def test_extra_change_has_health_check(self): provider, stubber = self._get_stubbed_provider() list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) # record with geo and no health check returns change desired = Zone('unit.tests.', []) record = Record.new( desired, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', 'geo': { 'NA': ['2.2.3.4'], } }) desired.add_record(record) extra = provider._extra_changes(desired, []) self.assertEquals(0, len(extra)) stubber.assert_no_pending_responses() # change b/c of healthcheck path record._octodns['healthcheck'] = {'path': '/_ready'} extra = provider._extra_changes(desired=desired, changes=[]) self.assertEquals(0, len(extra)) stubber.assert_no_pending_responses() # change b/c of healthcheck host record._octodns['healthcheck'] = {'host': 'foo.bar.io'} extra = provider._extra_changes(desired=desired, changes=[]) self.assertEquals(0, len(extra)) stubber.assert_no_pending_responses() def _get_test_plan(self, max_changes): provider = Route53Provider('test', 'abc', '123', max_changes) # Use the stubber stubber = Stubber(provider._conn) stubber.activate() got = Zone('unit.tests.', []) list_hosted_zones_resp = { 'HostedZones': [], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) create_hosted_zone_resp = { 'HostedZone': { 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }, 'ChangeInfo': { 'Id': 'a12', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', 'Comment': 'hrm', }, 'DelegationSet': { 'Id': 'b23', 'CallerReference': 'blip', 'NameServers': [ 'n12.unit.tests.', ], }, 'Location': 'us-east-1', } stubber.add_response('create_hosted_zone', create_hosted_zone_resp, { 'Name': got.name, 'CallerReference': ANY, }) stubber.add_response( 'list_health_checks', { 'HealthChecks': self.health_checks, 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, { 'HostedZoneId': 'z42', 'ChangeBatch': ANY }) plan = provider.plan(self.expected) return provider, plan # _get_test_plan() returns a plan with 11 modifications, 17 RRs @patch('octodns.provider.route53.Route53Provider._really_apply') def test_apply_1(self, really_apply_mock): # 18 RRs with max of 19 should only get applied in one call provider, plan = self._get_test_plan(19) provider.apply(plan) really_apply_mock.assert_called_once() @patch('octodns.provider.route53.Route53Provider._really_apply') def test_apply_2(self, really_apply_mock): # 18 RRs with max of 17 should only get applied in two calls provider, plan = self._get_test_plan(18) provider.apply(plan) self.assertEquals(2, really_apply_mock.call_count) @patch('octodns.provider.route53.Route53Provider._really_apply') def test_apply_3(self, really_apply_mock): # with a max of seven modifications, four calls provider, plan = self._get_test_plan(7) provider.apply(plan) self.assertEquals(4, really_apply_mock.call_count) @patch('octodns.provider.route53.Route53Provider._really_apply') def test_apply_4(self, really_apply_mock): # with a max of 11 modifications, two calls provider, plan = self._get_test_plan(11) provider.apply(plan) self.assertEquals(2, really_apply_mock.call_count) @patch('octodns.provider.route53.Route53Provider._really_apply') def test_apply_bad(self, really_apply_mock): # with a max of 1 modifications, fail provider, plan = self._get_test_plan(1) with self.assertRaises(Exception) as ctx: provider.apply(plan) self.assertTrue('modifications' in ctx.exception.message) def test_semicolon_fixup(self): provider = Route53Provider('test', 'abc', '123') self.assertEquals( { 'type': 'TXT', 'ttl': 30, 'values': [ 'abcd\\; ef\\;g', 'hij\\; klm\\;n', ], }, provider._data_for_quoted({ 'ResourceRecords': [{ 'Value': '"abcd; ef;g"', }, { 'Value': '"hij\\; klm\\;n"', }], 'TTL': 30, 'Type': 'TXT', })) def test_client_max_attempts(self): provider = Route53Provider('test', 'abc', '123', client_max_attempts=42) # NOTE: this will break if boto ever changes the impl details... self.assertEquals( 43, provider._conn.meta.events. _unique_id_handlers['retry-config-route53'] ['handler']._checker.__dict__['_max_attempts'])
def test_route53_record(self): existing = Zone('unit.tests.', []) record_a = Record.new( existing, '', { 'geo': { 'NA-US': ['2.2.2.2', '3.3.3.3'], 'OC': ['4.4.4.4', '5.5.5.5'] }, 'ttl': 99, 'type': 'A', 'values': ['9.9.9.9'] }) a = _Route53Record(None, record_a, False) self.assertEquals(a, a) b = _Route53Record( None, Record.new(existing, '', { 'ttl': 32, 'type': 'A', 'values': ['8.8.8.8', '1.1.1.1'] }), False) self.assertEquals(b, b) c = _Route53Record( None, Record.new(existing, 'other', { 'ttl': 99, 'type': 'A', 'values': ['9.9.9.9'] }), False) self.assertEquals(c, c) d = _Route53Record( None, Record.new( existing, '', { 'ttl': 42, 'type': 'MX', 'value': { 'preference': 10, 'exchange': 'foo.bar.' } }), False) self.assertEquals(d, d) # Same fqdn & type is same record self.assertEquals(a, b) # Same name & different type is not the same self.assertNotEquals(a, d) # Different name & same type is not the same self.assertNotEquals(a, c) # Same everything, different class is not the same e = _Route53GeoDefault(None, record_a, False) self.assertNotEquals(a, e) class DummyProvider(object): def get_health_check_id(self, *args, **kwargs): return None provider = DummyProvider() f = _Route53GeoRecord(provider, record_a, 'NA-US', record_a.geo['NA-US'], False) self.assertEquals(f, f) g = _Route53GeoRecord(provider, record_a, 'OC', record_a.geo['OC'], False) self.assertEquals(g, g) # Geo and non-geo are not the same, using Geo as primary to get it's # __cmp__ self.assertNotEquals(f, a) # Same everything, different geo's is not the same self.assertNotEquals(f, g) # Make sure it doesn't blow up a.__repr__() e.__repr__() f.__repr__()
def _get_test_plan(self, max_changes): provider = Route53Provider('test', 'abc', '123', max_changes) # Use the stubber stubber = Stubber(provider._conn) stubber.activate() got = Zone('unit.tests.', []) list_hosted_zones_resp = { 'HostedZones': [], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) create_hosted_zone_resp = { 'HostedZone': { 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }, 'ChangeInfo': { 'Id': 'a12', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', 'Comment': 'hrm', }, 'DelegationSet': { 'Id': 'b23', 'CallerReference': 'blip', 'NameServers': [ 'n12.unit.tests.', ], }, 'Location': 'us-east-1', } stubber.add_response('create_hosted_zone', create_hosted_zone_resp, { 'Name': got.name, 'CallerReference': ANY, }) stubber.add_response( 'list_health_checks', { 'HealthChecks': self.health_checks, 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': ANY, }) stubber.add_response( 'change_resource_record_sets', { 'ChangeInfo': { 'Id': 'id', 'Status': 'PENDING', 'SubmittedAt': '2017-01-29T01:02:03Z', } }, { 'HostedZoneId': 'z42', 'ChangeBatch': ANY }) plan = provider.plan(self.expected) return provider, plan
def test_populate(self): provider = DnsMadeEasyProvider('test', 'api', 'secret') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"error": ["API key not found"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # Bad request with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"error": ["Rate limit exceeded"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('\n - Rate limit exceeded', text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='<html><head></head><body></body></html>') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dnsmadeeasy.com/V2.0/dns/managed' with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: mock.get('{}{}'.format(base, '/'), text=fh.read()) with open('tests/fixtures/dnsmadeeasy-records.json') as fh: mock.get('{}{}'.format(base, '/123123/records'), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(14, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(14, len(again.records)) # bust the cache del provider._zone_records[zone.name]
def test_populate(self): provider = ConstellixProvider('test', 'api', 'secret') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"errors": ["Unable to authenticate token"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # Bad request with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"errors": ["\\"unittests\\" is not ' 'a valid domain name"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('\n - "unittests" is not a valid domain name', text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='<html><head></head><body></body></html>') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dns.constellix.com/v1/domains' with open('tests/fixtures/constellix-domains.json') as fh: mock.get('{}{}'.format(base, ''), text=fh.read()) with open('tests/fixtures/constellix-records.json') as fh: mock.get('{}{}'.format(base, '/123123/records'), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(16, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(16, len(again.records)) # bust the cache del provider._zone_records[zone.name]
class TestCloudflareProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record( Record.new( expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break empty = {'result': [], 'result_info': {'count': 0, 'per_page': 0}} def test_populate(self): provider = CloudflareProvider('test', 'email', 'token') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=403, text='{"success":false,"errors":[{"code":9103,' '"message":"Unknown X-Auth-Key or X-Auth-Email"}],' '"messages":[],"result":null}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unknown X-Auth-Key or X-Auth-Email', ctx.exception.message) # Bad auth, unknown resp with requests_mock() as mock: mock.get(ANY, status_code=403, text='{}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Authentication error', ctx.exception.message) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existant zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=200, json=self.empty) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # re-populating the same non-existant zone uses cache and makes no # calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(set(), again.records) # bust zone cache provider._zones = None # existing zone with data with requests_mock() as mock: base = 'https://api.cloudflare.com/client/v4/zones' # zones with open('tests/fixtures/cloudflare-zones-page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-zones-page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) mock.get('{}?page=3'.format(base), status_code=200, json={ 'result': [], 'result_info': { 'count': 0, 'per_page': 0 } }) # records base = '{}/234234243423aaabb334342aaa343435/dns_records' \ .format(base) with open('tests/fixtures/cloudflare-dns_records-' 'page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-dns_records-' 'page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(9, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # re-populating the same zone/records comes out of cache, no calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(9, len(again.records)) def test_apply(self): provider = CloudflareProvider('test', 'email', 'token') provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create ] + [None] * 16 # individual record creates # non-existant zone, create everything plan = provider.plan(self.expected) self.assertEquals(9, len(plan.changes)) self.assertEquals(9, provider.apply(plan)) provider._request.assert_has_calls( [ # created the domain call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), # created at least one of the record with expected data call('POST', '/zones/42/dns_records', data={ 'content': 'ns1.unit.tests.', 'type': 'NS', 'name': 'under.unit.tests', 'ttl': 3600 }), # make sure semicolons are not escaped when sending data call('POST', '/zones/42/dns_records', data={ 'content': 'v=DKIM1;k=rsa;s=email;h=sha256;' 'p=A/kinda+of/long/string+with+numb3rs', 'type': 'TXT', 'name': 'txt.unit.tests', 'ttl': 600 }), ], True) # expected number of total calls self.assertEquals(18, provider._request.call_count) provider._request.reset_mock() provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "www.unit.tests", "content": "1.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:43.420689Z", "created_on": "2017-03-11T18:01:43.420689Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997654", "type": "A", "name": "www.unit.tests", "content": "2.2.3.4", "proxiable": True, "proxied": False, "ttl": 300, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "nc.unit.tests", "content": "3.2.3.4", "proxiable": True, "proxied": False, "ttl": 120, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, { "id": "fc12ab34cd5611334422ab3322997655", "type": "A", "name": "ttl.unit.tests", "content": "4.2.3.4", "proxiable": True, "proxied": False, "ttl": 600, "locked": False, "zone_id": "ff12ab34cd5611334422ab3322997650", "zone_name": "unit.tests", "modified_on": "2017-03-11T18:01:44.030044Z", "created_on": "2017-03-11T18:01:44.030044Z", "meta": { "auto_added": False } }, ]) # we don't care about the POST/create return values provider._request.return_value = {} provider._request.side_effect = None wanted = Zone('unit.tests.', []) wanted.add_record( Record.new( wanted, 'nc', { 'ttl': 60, # TTL is below their min 'type': 'A', 'value': '3.2.3.4' })) wanted.add_record( Record.new( wanted, 'ttl', { 'ttl': 300, # TTL change 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) # only see the delete & ttl update, below min-ttl is filtered out self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._request.assert_has_calls([ call('POST', '/zones/42/dns_records', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl.unit.tests', 'ttl': 300 }), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997655'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997653'), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997654') ])
# # from __future__ import absolute_import, division, print_function, \ unicode_literals from octodns.record import Create, Delete, Update, Record from octodns.provider.googlecloud import GoogleCloudProvider from octodns.zone import Zone from octodns.provider.base import Plan, BaseProvider from unittest import TestCase from mock import Mock, patch, PropertyMock zone = Zone(name='unit.tests.', sub_zones=[]) octo_records = [] octo_records.append(Record.new(zone, '', { 'ttl': 0, 'type': 'A', 'values': ['1.2.3.4', '10.10.10.10']})) octo_records.append(Record.new(zone, 'a', { 'ttl': 1, 'type': 'A', 'values': ['1.2.3.4', '1.1.1.1']})) octo_records.append(Record.new(zone, 'aa', { 'ttl': 9001, 'type': 'A', 'values': ['1.2.4.3']})) octo_records.append(Record.new(zone, 'aaa', { 'ttl': 2,
def test_populate(self): provider = CloudflareProvider('test', 'email', 'token') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=403, text='{"success":false,"errors":[{"code":9103,' '"message":"Unknown X-Auth-Key or X-Auth-Email"}],' '"messages":[],"result":null}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unknown X-Auth-Key or X-Auth-Email', ctx.exception.message) # Bad auth, unknown resp with requests_mock() as mock: mock.get(ANY, status_code=403, text='{}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Authentication error', ctx.exception.message) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existant zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=200, json=self.empty) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # re-populating the same non-existant zone uses cache and makes no # calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(set(), again.records) # bust zone cache provider._zones = None # existing zone with data with requests_mock() as mock: base = 'https://api.cloudflare.com/client/v4/zones' # zones with open('tests/fixtures/cloudflare-zones-page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-zones-page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) mock.get('{}?page=3'.format(base), status_code=200, json={ 'result': [], 'result_info': { 'count': 0, 'per_page': 0 } }) # records base = '{}/234234243423aaabb334342aaa343435/dns_records' \ .format(base) with open('tests/fixtures/cloudflare-dns_records-' 'page-1.json') as fh: mock.get('{}?page=1'.format(base), status_code=200, text=fh.read()) with open('tests/fixtures/cloudflare-dns_records-' 'page-2.json') as fh: mock.get('{}?page=2'.format(base), status_code=200, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(9, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # re-populating the same zone/records comes out of cache, no calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(9, len(again.records))
def test_base_provider(self): with self.assertRaises(NotImplementedError) as ctx: BaseProvider('base') self.assertEquals('Abstract base class, log property missing', text_type(ctx.exception)) class HasLog(BaseProvider): log = getLogger('HasLog') with self.assertRaises(NotImplementedError) as ctx: HasLog('haslog') self.assertEquals('Abstract base class, SUPPORTS_GEO property missing', text_type(ctx.exception)) class HasSupportsGeo(HasLog): SUPPORTS_GEO = False zone = Zone('unit.tests.', ['sub']) with self.assertRaises(NotImplementedError) as ctx: HasSupportsGeo('hassupportsgeo').populate(zone) self.assertEquals('Abstract base class, SUPPORTS property missing', text_type(ctx.exception)) class HasSupports(HasSupportsGeo): SUPPORTS = set(('A', )) with self.assertRaises(NotImplementedError) as ctx: HasSupports('hassupports').populate(zone) self.assertEquals('Abstract base class, populate method missing', text_type(ctx.exception)) # SUPPORTS_DYNAMIC has a default/fallback self.assertFalse(HasSupports('hassupports').SUPPORTS_DYNAMIC) # But can be overridden class HasSupportsDyanmic(HasSupports): SUPPORTS_DYNAMIC = True self.assertTrue( HasSupportsDyanmic('hassupportsdynamic').SUPPORTS_DYNAMIC) class HasPopulate(HasSupports): def populate(self, zone, target=False, lenient=False): zone.add_record(Record.new(zone, '', { 'ttl': 60, 'type': 'A', 'value': '2.3.4.5' }), lenient=lenient) zone.add_record(Record.new(zone, 'going', { 'ttl': 60, 'type': 'A', 'value': '3.4.5.6' }), lenient=lenient) zone.add_record(Record.new(zone, 'foo.sub', { 'ttl': 61, 'type': 'A', 'value': '4.5.6.7' }), lenient=lenient) zone.add_record( Record.new(zone, '', { 'ttl': 60, 'type': 'A', 'value': '1.2.3.4' })) self.assertTrue( HasSupports('hassupportsgeo').supports(list(zone.records)[0])) plan = HasPopulate('haspopulate').plan(zone) self.assertEquals(3, len(plan.changes)) with self.assertRaises(NotImplementedError) as ctx: HasPopulate('haspopulate').apply(plan) self.assertEquals('Abstract base class, _apply method missing', text_type(ctx.exception))
def main(): """check-zone based on octodns config file and dns zone Will query all 4 DNS servers configured for the zone in GCP. """ parser = ArgumentParser(description=__doc__.split('\n')[1]) parser.add_argument('--config-file', required=True, help='The OctoDNS configuration file to use') parser.add_argument('--zone', action='append', required=True, help='zone to check') args = parser.parse_args() manager = Manager(args.config_file) for zone_name in args.zone: print('Checking records for {}'.format(zone_name)) zone = Zone(zone_name, manager.configured_sub_zones(zone_name)) # Read our YAML configuration yaml_config = manager.providers['config'] # Build a GCP provider in our project to read the nameservers from it gcp = manager.providers['gcp'] project = gcp.gcloud_client.project # Retrieve the DNS Servers directly from the GCP configuration dns_servers = gcp.gcloud_zones[zone_name].name_servers # k8s.io resolvers for testing without access to gcp #dns_servers = ["NS-CLOUD-D1.GOOGLEDOMAINS.COM", "NS-CLOUD-D2.GOOGLEDOMAINS.COM", "NS-CLOUD-D3.GOOGLEDOMAINS.COM", "NS-CLOUD-D4.GOOGLEDOMAINS.COM"] print('Using GCP project {}'.format(project)) print('name,type,ttl,{},consistent'.format(','.join(dns_servers))) # Populate the zone with those records defined in our YAML config yaml_config.populate(zone) # This would populate the zone with records already defined in Google Cloud DNS # gcp.populate(zone) # Configure Resolvers (one per DNS server) resolvers = configure_resolvers(dns_servers) # Populate the queries to make based on zone record configuration queries = {} for record in sorted(zone.records): queries[record] = [ r.query(record.fqdn, record._type) for r in resolvers ] # No dns_error unless we find one dns_error = False dns_error = verify_dns(queries) if dns_error: sys.exit(1) sys.exit(0)
class TestDnsimpleProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record(Record.new(expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break def test_populate(self): provider = DnsimpleProvider('test', 'token', 42) # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"message": "Authentication failed"}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='{"message": "Domain `foo.bar` not found"}') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \ 'records?page=' with open('tests/fixtures/dnsimple-page-1.json') as fh: mock.get('{}{}'.format(base, 1), text=fh.read()) with open('tests/fixtures/dnsimple-page-2.json') as fh: mock.get('{}{}'.format(base, 2), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(16, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(1, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(16, len(again.records)) # bust the cache del provider._zone_records[zone.name] # test handling of invalid content with requests_mock() as mock: with open('tests/fixtures/dnsimple-invalid-content.json') as fh: mock.get(ANY, text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone, lenient=True) self.assertEquals(set([ Record.new(zone, '', { 'ttl': 3600, 'type': 'SSHFP', 'values': [] }, lenient=True), Record.new(zone, '_srv._tcp', { 'ttl': 600, 'type': 'SRV', 'values': [] }, lenient=True), Record.new(zone, 'naptr', { 'ttl': 600, 'type': 'NAPTR', 'values': [] }, lenient=True), ]), zone.records) def test_apply(self): provider = DnsimpleProvider('test', 'token', 42) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existent domain, create everything resp.json.side_effect = [ DnsimpleClientNotFound, # no zone in populate DnsimpleClientNotFound, # no domain during apply ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded n = len(self.expected.records) - 3 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) self.assertFalse(plan.exists) provider._client._request.assert_has_calls([ # created the domain call('POST', '/domains', data={'name': 'unit.tests'}), # created at least some of the record with expected data call('POST', '/zones/unit.tests/records', data={ 'content': '1.2.3.4', 'type': 'A', 'name': '', 'ttl': 300}), call('POST', '/zones/unit.tests/records', data={ 'content': '1.2.3.5', 'type': 'A', 'name': '', 'ttl': 300}), call('POST', '/zones/unit.tests/records', data={ 'content': '0 issue "ca.unit.tests"', 'type': 'CAA', 'name': '', 'ttl': 3600}), call('POST', '/zones/unit.tests/records', data={ 'content': '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49', 'type': 'SSHFP', 'name': '', 'ttl': 3600}), call('POST', '/zones/unit.tests/records', data={ 'content': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73', 'type': 'SSHFP', 'name': '', 'ttl': 3600}), call('POST', '/zones/unit.tests/records', data={ 'content': '20 30 foo-1.unit.tests.', 'priority': 10, 'type': 'SRV', 'name': '_srv._tcp', 'ttl': 600 }), ]) # expected number of total calls self.assertEquals(28, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[ { 'id': 11189897, 'name': 'www', 'content': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'content': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'content': '3.2.3.4', 'ttl': 600, 'type': 'A', } ]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record(Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertTrue(plan.exists) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/zones/unit.tests/records', data={ 'content': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/zones/unit.tests/records/11189899'), call('DELETE', '/zones/unit.tests/records/11189897'), call('DELETE', '/zones/unit.tests/records/11189898') ], any_order=True)
def test_missing_dot(self): with self.assertRaises(Exception) as ctx: Zone('not.allowed', []) self.assertTrue('missing ending dot' in ctx.exception.message)
def test_provider(self): provider = PowerDnsProvider('test', 'non.existent', 'api-key', nameserver_values=['8.8.8.8.', '9.9.9.9.']) # Test version detection with requests_mock() as mock: mock.get('http://non.existent:8081/api/v1/servers/localhost', status_code=200, json={'version': "4.1.10"}) self.assertEquals(provider.powerdns_version, [4, 1, 10]) # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='Unauthorized') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertTrue('unauthorized' in text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone in PowerDNS <4.3.0 doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=422, json={'error': "Could not find domain 'unit.tests.'"}) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # Non-existent zone in PowerDNS >=4.2.0 doesn't populate anything provider._powerdns_version = [4, 2, 0] with requests_mock() as mock: mock.get(ANY, status_code=404, text='Not Found') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) provider._powerdns_version = [4, 1, 0] # The rest of this is messy/complicated b/c it's dealing with mocking expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) expected_n = len(expected.records) - 3 self.assertEquals(16, expected_n) # No diffs == no changes with requests_mock() as mock: mock.get(ANY, status_code=200, text=FULL_TEXT) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(16, len(zone.records)) changes = expected.changes(zone, provider) self.assertEquals(0, len(changes)) # Used in a minute def assert_rrsets_callback(request, context): data = loads(request.body) self.assertEquals(expected_n, len(data['rrsets'])) return '' # No existing records -> creates for every record in expected with requests_mock() as mock: mock.get(ANY, status_code=200, text=EMPTY_TEXT) # post 201, is response to the create with data mock.patch(ANY, status_code=201, text=assert_rrsets_callback) plan = provider.plan(expected) self.assertEquals(expected_n, len(plan.changes)) self.assertEquals(expected_n, provider.apply(plan)) self.assertTrue(plan.exists) # Non-existent zone -> creates for every record in expected # OMG this is f*****g ugly, probably better to ditch requests_mocks and # just mock things for real as it doesn't seem to provide a way to get # at the request params or verify that things were called from what I # can tell not_found = {'error': "Could not find domain 'unit.tests.'"} with requests_mock() as mock: # get 422's, unknown zone mock.get(ANY, status_code=422, text=dumps(not_found)) # patch 422's, unknown zone mock.patch(ANY, status_code=422, text=dumps(not_found)) # post 201, is response to the create with data mock.post(ANY, status_code=201, text=assert_rrsets_callback) plan = provider.plan(expected) self.assertEquals(expected_n, len(plan.changes)) self.assertEquals(expected_n, provider.apply(plan)) self.assertFalse(plan.exists) provider._powerdns_version = [4, 2, 0] with requests_mock() as mock: # get 404's, unknown zone mock.get(ANY, status_code=404, text='') # patch 404's, unknown zone mock.patch(ANY, status_code=404, text=dumps(not_found)) # post 201, is response to the create with data mock.post(ANY, status_code=201, text=assert_rrsets_callback) plan = provider.plan(expected) self.assertEquals(expected_n, len(plan.changes)) self.assertEquals(expected_n, provider.apply(plan)) self.assertFalse(plan.exists) provider._powerdns_version = [4, 1, 0] with requests_mock() as mock: # get 422's, unknown zone mock.get(ANY, status_code=422, text=dumps(not_found)) # patch 422's, data = {'error': "Key 'name' not present or not a String"} mock.patch(ANY, status_code=422, text=dumps(data)) with self.assertRaises(HTTPError) as ctx: plan = provider.plan(expected) provider.apply(plan) response = ctx.exception.response self.assertEquals(422, response.status_code) self.assertTrue('error' in response.json()) with requests_mock() as mock: # get 422's, unknown zone mock.get(ANY, status_code=422, text=dumps(not_found)) # patch 500's, things just blew up mock.patch(ANY, status_code=500, text='') with self.assertRaises(HTTPError): plan = provider.plan(expected) provider.apply(plan) with requests_mock() as mock: # get 422's, unknown zone mock.get(ANY, status_code=422, text=dumps(not_found)) # patch 500's, things just blew up mock.patch(ANY, status_code=422, text=dumps(not_found)) # post 422's, something wrong with create mock.post(ANY, status_code=422, text='Hello Word!') with self.assertRaises(HTTPError): plan = provider.plan(expected) provider.apply(plan)
def test_lowering(self): zone = Zone('UniT.TEsTs.', []) self.assertEquals('unit.tests.', zone.name)
# # # from __future__ import absolute_import, division, print_function, \ unicode_literals from unittest import TestCase from octodns.processor.ownership import OwnershipProcessor from octodns.record import Delete, Record from octodns.zone import Zone from helpers import PlannableProvider zone = Zone('unit.tests.', []) records = {} for record in [ Record.new(zone, '', { 'ttl': 30, 'type': 'A', 'values': [ '1.2.3.4', '5.6.7.8', ], }), Record.new(zone, 'the-a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', }),
def test_apply(self): provider = ConstellixProvider('test', 'api', 'secret') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existent domain, create everything resp.json.side_effect = [ [], # no domains returned during populate [{ 'id': 123123, 'name': 'unit.tests' }], # domain created in apply ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 7 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # get all domains to build the cache call('GET', ''), # created the domain call('POST', '/', data={'names': ['unit.tests']}) ]) # These two checks are broken up so that ordering doesn't break things. # Python3 doesn't make the calls in a consistent order so different # things follow the GET / on different runs provider._client._request.assert_has_calls([ call('POST', '/123123/records/SRV', data={ 'roundRobin': [{ 'priority': 10, 'weight': 20, 'value': 'foo-1.unit.tests.', 'port': 30 }, { 'priority': 12, 'weight': 20, 'value': 'foo-2.unit.tests.', 'port': 30 }], 'name': '_srv._tcp', 'ttl': 600, }), ]) self.assertEquals(19, provider._client._request.call_count) provider._client._request.reset_mock() provider._client.records = Mock( return_value=[{ 'id': 11189897, 'type': 'A', 'name': 'www', 'ttl': 300, 'value': [ '1.2.3.4', '2.2.3.4', ] }, { 'id': 11189898, 'type': 'A', 'name': 'ttl', 'ttl': 600, 'value': ['3.2.3.4'] }, { 'id': 11189899, 'type': 'ALIAS', 'name': 'alias', 'ttl': 600, 'value': [{ 'value': 'aname.unit.tests.' }] }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertEquals(3, len(plan.changes)) self.assertEquals(3, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/123123/records/A', data={ 'roundRobin': [{ 'value': '3.2.3.4' }], 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/A/11189897'), call('DELETE', '/123123/records/A/11189898'), call('DELETE', '/123123/records/ANAME/11189899') ], any_order=True)
def make_expected(self): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) return expected
class TestConstellixProvider(TestCase): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) # Our test suite differs a bit, add our NS and remove the simple one expected.add_record( Record.new( expected, 'under', { 'ttl': 3600, 'type': 'NS', 'values': [ 'ns1.unit.tests.', 'ns2.unit.tests.', ] })) # Add some ALIAS records expected.add_record( Record.new(expected, '', { 'ttl': 1800, 'type': 'ALIAS', 'value': 'aname.unit.tests.' })) for record in list(expected.records): if record.name == 'sub' and record._type == 'NS': expected._remove_record(record) break def test_populate(self): provider = ConstellixProvider('test', 'api', 'secret') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"errors": ["Unable to authenticate token"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('Unauthorized', text_type(ctx.exception)) # Bad request with requests_mock() as mock: mock.get(ANY, status_code=400, text='{"errors": ["\\"unittests\\" is not ' 'a valid domain name"]}') with self.assertRaises(Exception) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals('\n - "unittests" is not a valid domain name', text_type(ctx.exception)) # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') with self.assertRaises(HTTPError) as ctx: zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, text='<html><head></head><body></body></html>') zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # No diffs == no changes with requests_mock() as mock: base = 'https://api.dns.constellix.com/v1/domains' with open('tests/fixtures/constellix-domains.json') as fh: mock.get('{}{}'.format(base, ''), text=fh.read()) with open('tests/fixtures/constellix-records.json') as fh: mock.get('{}{}'.format(base, '/123123/records'), text=fh.read()) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(16, len(zone.records)) changes = self.expected.changes(zone, provider) self.assertEquals(0, len(changes)) # 2nd populate makes no network calls/all from cache again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(16, len(again.records)) # bust the cache del provider._zone_records[zone.name] def test_apply(self): provider = ConstellixProvider('test', 'api', 'secret') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) # non-existent domain, create everything resp.json.side_effect = [ [], # no domains returned during populate [{ 'id': 123123, 'name': 'unit.tests' }], # domain created in apply ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 7 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # get all domains to build the cache call('GET', ''), # created the domain call('POST', '/', data={'names': ['unit.tests']}) ]) # These two checks are broken up so that ordering doesn't break things. # Python3 doesn't make the calls in a consistent order so different # things follow the GET / on different runs provider._client._request.assert_has_calls([ call('POST', '/123123/records/SRV', data={ 'roundRobin': [{ 'priority': 10, 'weight': 20, 'value': 'foo-1.unit.tests.', 'port': 30 }, { 'priority': 12, 'weight': 20, 'value': 'foo-2.unit.tests.', 'port': 30 }], 'name': '_srv._tcp', 'ttl': 600, }), ]) self.assertEquals(19, provider._client._request.call_count) provider._client._request.reset_mock() provider._client.records = Mock( return_value=[{ 'id': 11189897, 'type': 'A', 'name': 'www', 'ttl': 300, 'value': [ '1.2.3.4', '2.2.3.4', ] }, { 'id': 11189898, 'type': 'A', 'name': 'ttl', 'ttl': 600, 'value': ['3.2.3.4'] }, { 'id': 11189899, 'type': 'ALIAS', 'name': 'alias', 'ttl': 600, 'value': [{ 'value': 'aname.unit.tests.' }] }]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record( Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertEquals(3, len(plan.changes)) self.assertEquals(3, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/123123/records/A', data={ 'roundRobin': [{ 'value': '3.2.3.4' }], 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/A/11189897'), call('DELETE', '/123123/records/A/11189898'), call('DELETE', '/123123/records/ANAME/11189899') ], any_order=True)
def test_extra_change_has_wrong_health_check(self): provider, stubber = self._get_stubbed_provider() list_hosted_zones_resp = { 'HostedZones': [{ 'Name': 'unit.tests.', 'Id': 'z42', 'CallerReference': 'abc', }], 'Marker': 'm', 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) # record with geo and no health check returns change existing = Zone('unit.tests.', []) record = Record.new( existing, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', 'geo': { 'NA': ['2.2.3.4'], } }) existing.add_record(record) list_resource_record_sets_resp = { 'ResourceRecordSets': [{ 'Name': 'a.unit.tests.', 'Type': 'A', 'GeoLocation': { 'ContinentCode': 'NA', }, 'ResourceRecords': [{ 'Value': '2.2.3.4', }], 'TTL': 61, 'HealthCheckId': '42', }], 'IsTruncated': False, 'MaxItems': '100', } stubber.add_response('list_resource_record_sets', list_resource_record_sets_resp, {'HostedZoneId': 'z42'}) stubber.add_response( 'list_health_checks', { 'HealthChecks': [{ 'Id': '42', 'CallerReference': 'foo', 'HealthCheckConfig': { 'Type': 'HTTPS', 'FullyQualifiedDomainName': 'unit.tests', 'IPAddress': '2.2.3.4', }, 'HealthCheckVersion': 2, }], 'IsTruncated': False, 'MaxItems': '100', 'Marker': '', }) extra = provider._extra_changes(existing, []) self.assertEquals(1, len(extra)) stubber.assert_no_pending_responses() for change in (Create(record), Update(record, record), Delete(record)): extra = provider._extra_changes(existing, [change]) self.assertEquals(0, len(extra)) stubber.assert_no_pending_responses()
def test_apply(self): # Create provider with sandbox enabled provider = DnsMadeEasyProvider('test', 'api', 'secret', True) resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: domains = json.load(fh) # non-existent domain, create everything resp.json.side_effect = [ DnsMadeEasyClientNotFound, # no zone in populate DnsMadeEasyClientNotFound, # no domain during apply domains ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 10 self.assertEquals(n, len(plan.changes)) self.assertEquals(n, provider.apply(plan)) provider._client._request.assert_has_calls([ # created the domain call('POST', '/', data={'name': 'unit.tests'}), # get all domains to build the cache call('GET', '/'), # created at least some of the record with expected data call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.4', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'A', 'name': '', 'value': '1.2.3.5', 'ttl': 300}), call('POST', '/123123/records', data={ 'type': 'ANAME', 'name': '', 'value': 'aname.unit.tests.', 'ttl': 1800}), call('POST', '/123123/records', data={ 'name': '', 'value': 'ca.unit.tests', 'issuerCritical': 0, 'caaType': 'issue', 'ttl': 3600, 'type': 'CAA'}), call('POST', '/123123/records', data={ 'name': '_srv._tcp', 'weight': 20, 'value': 'foo-1.unit.tests.', 'priority': 10, 'ttl': 600, 'type': 'SRV', 'port': 30 }), ]) self.assertEquals(26, provider._client._request.call_count) provider._client._request.reset_mock() # delete 1 and update 1 provider._client.records = Mock(return_value=[ { 'id': 11189897, 'name': 'www', 'value': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'value': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'value': '3.2.3.4', 'ttl': 600, 'type': 'A', } ]) # Domain exists, we don't care about return resp.json.side_effect = ['{}'] wanted = Zone('unit.tests.', []) wanted.add_record(Record.new(wanted, 'ttl', { 'ttl': 300, 'type': 'A', 'value': '3.2.3.4' })) plan = provider.plan(wanted) self.assertEquals(2, len(plan.changes)) self.assertEquals(2, provider.apply(plan)) # recreate for update, and deletes for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/123123/records', data={ 'value': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/123123/records/11189899'), call('DELETE', '/123123/records/11189897'), call('DELETE', '/123123/records/11189898') ], any_order=True)