def test_safe_no_existing(self): # existing records fewer than MIN_EXISTING_RECORDS is safe zone = Zone('unit.tests.', []) record = Record.new(zone, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', }) updates = [Update(record, record), Update(record, record)] Plan(zone, zone, updates, True).raise_if_unsafe()
def test_apply_traffic_director(self, mock): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # stubbing these out to avoid a lot of messy mocking, they'll be tested # individually, we'll check for expected calls provider._mod_geo_Create = MagicMock() provider._mod_geo_Update = MagicMock() provider._mod_geo_Delete = MagicMock() provider._mod_Create = MagicMock() provider._mod_Update = MagicMock() provider._mod_Delete = MagicMock() # busted traffic director mock.side_effect = [ # get zone { 'data': {} }, # accept publish { 'data': {} }, ] desired = Zone('unit.tests.', []) geo = self.geo_record regular = self.regular_record changes = [ Create(geo), Create(regular), Update(geo, geo), Update(regular, regular), Delete(geo), Delete(regular), ] plan = Plan(None, desired, changes) provider._apply(plan) mock.assert_has_calls([ call('/Zone/unit.tests/', 'GET', {}), call('/Zone/unit.tests/', 'PUT', {'publish': True}) ]) # should have seen 1 call to each provider._mod_geo_Create.assert_called_once() provider._mod_geo_Update.assert_called_once() provider._mod_geo_Delete.assert_called_once() provider._mod_Create.assert_called_once() provider._mod_Update.assert_called_once() provider._mod_Delete.assert_called_once()
def test_safe_updates_min_existing(self): # MAX_SAFE_UPDATE_PCENT+1 fails when more # than MIN_EXISTING_RECORDS exist zone = Zone('unit.tests.', []) record = Record.new(zone, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', }) for i in range(int(Plan.MIN_EXISTING_RECORDS)): zone.add_record( Record.new(zone, unicode(i), { 'ttl': 60, 'type': 'A', 'value': '2.3.4.5' })) changes = [ Update(record, record) for i in range( int(Plan.MIN_EXISTING_RECORDS * Plan.MAX_SAFE_UPDATE_PCENT) + 1) ] with self.assertRaises(UnsafePlan) as ctx: Plan(zone, zone, changes, True).raise_if_unsafe() self.assertTrue('Too many updates' in ctx.exception.message)
def test_safe_updates_min_existing_override(self): safe_pcent = .4 # 40% + 1 fails when more # than MIN_EXISTING_RECORDS exist zone = Zone('unit.tests.', []) record = Record.new(zone, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', }) for i in range(int(Plan.MIN_EXISTING_RECORDS)): zone.add_record( Record.new(zone, str(i), { 'ttl': 60, 'type': 'A', 'value': '2.3.4.5' })) changes = [ Update(record, record) for i in range(int(Plan.MIN_EXISTING_RECORDS * safe_pcent) + 1) ] with self.assertRaises(UnsafePlan) as ctx: Plan(zone, zone, changes, update_pcent_threshold=safe_pcent).raise_if_unsafe() self.assertTrue('Too many updates' in ctx.exception.message)
def test_mod_geo_update_geo_geo(self): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # update of an existing td # pre-populate the cache with our mock td provider._traffic_directors = { 'unit.tests.': { 'A': 42, } } # mock _mod_rulesets provider._mod_rulesets = MagicMock() geo = self.geo_record change = Update(geo, geo) provider._mod_geo_Update(None, change) # still in cache self.assertTrue('A' in provider.traffic_directors['unit.tests.']) # should have seen 1 gen call provider._mod_rulesets.assert_called_once_with(42, change)
def test_too_many_updates(self): existing = self.existing.copy() changes = [] # No records, no changes, we're good plan = HelperPlan(existing, None, changes, True) plan.raise_if_unsafe() # Four records, no changes, we're good existing.add_record(self.record_1) existing.add_record(self.record_2) existing.add_record(self.record_3) existing.add_record(self.record_4) plan = HelperPlan(existing, None, changes, True) plan.raise_if_unsafe() # Creates don't count against us changes.append(Create(self.record_1)) changes.append(Create(self.record_2)) changes.append(Create(self.record_3)) changes.append(Create(self.record_4)) plan = HelperPlan(existing, None, changes, True) plan.raise_if_unsafe() # One update, still good (25%, default threshold is 33%) changes.append(Update(self.record_1, self.record_1)) plan = HelperPlan(existing, None, changes, True) plan.raise_if_unsafe() # Two and we're over the threshold changes.append(Update(self.record_2, self.record_2)) plan = HelperPlan(existing, None, changes, True) with self.assertRaises(TooMuchChange) as ctx: plan.raise_if_unsafe() self.assertTrue('Too many updates', str(ctx.exception)) # If we require more records before applying we're still OK though plan = HelperPlan(existing, None, changes, True, min_existing=10) plan.raise_if_unsafe()
def test_root_ns_change(self): existing = self.existing.copy() changes = [] # No records, no changes, we're good plan = HelperPlan(existing, None, changes, True) plan.raise_if_unsafe() existing.add_record(self.record_1) existing.add_record(self.record_2) existing.add_record(self.record_3) existing.add_record(self.record_4) # Non NS changes and we're still good changes.append(Update(self.record_1, self.record_1)) plan = HelperPlan(existing, None, changes, True) plan.raise_if_unsafe() # Add a change to a non-root NS record, we're OK ns_record = Record.new( existing, 'sub', data={ 'type': 'NS', 'ttl': 43, 'values': ('ns1.unit.tests.', 'ns1.unit.tests.'), }, ) changes.append(Delete(ns_record)) plan = HelperPlan(existing, None, changes, True) plan.raise_if_unsafe() # Remove that Delete so that we don't go over the delete threshold changes.pop(-1) # Delete the root NS record and we get an unsafe root_ns_record = Record.new( existing, '', data={ 'type': 'NS', 'ttl': 43, 'values': ('ns3.unit.tests.', 'ns4.unit.tests.'), }, ) changes.append(Delete(root_ns_record)) plan = HelperPlan(existing, None, changes, True) with self.assertRaises(RootNsChange) as ctx: plan.raise_if_unsafe() self.assertTrue('Root Ns record change', str(ctx.exception))
def test_change(self): existing = Record.new(self.zone, 'txt', { 'ttl': 44, 'type': 'TXT', 'value': 'some text', }) new = Record.new(self.zone, 'txt', { 'ttl': 44, 'type': 'TXT', 'value': 'some change', }) create = Create(new) self.assertEquals(new.values, create.record.values) update = Update(existing, new) self.assertEquals(new.values, update.record.values) delete = Delete(existing) self.assertEquals(existing.values, delete.record.values)
def test_mod_geo_update_regular_geo(self, _): provider = DynProvider('test', 'cust', 'user', 'pass', traffic_directors_enabled=True) # convert a regular record to a td provider._mod_geo_Create = MagicMock() provider._mod_Delete = MagicMock() change = Update(self.regular_record, self.geo_record) provider._mod_geo_Update(42, change) # should have seen a call to create the new geo record provider._mod_geo_Create.assert_called_once_with(42, change) # should have seen a call to delete the old regular record provider._mod_Delete.assert_called_once_with(42, change)
def test_proxiedrecordandnewttl_includechange_returnsfalse(self): provider = CloudflareProvider('test', 'email', 'token') zone = Zone('unit.tests.', []) existing = set_record_proxied_flag( Record.new(zone, 'a', { 'ttl': 1, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }), True) new = Record.new(zone, 'a', { 'ttl': 300, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }) change = Update(existing, new) include_change = provider._include_change(change) self.assertFalse(include_change)
def test_safe_updates_min_existing_pcent(self): # MAX_SAFE_UPDATE_PCENT is safe when more # than MIN_EXISTING_RECORDS exist zone = Zone('unit.tests.', []) record = Record.new(zone, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', }) for i in range(int(Plan.MIN_EXISTING_RECORDS)): zone.add_record(Record.new(zone, str(i), { 'ttl': 60, 'type': 'A', 'value': '2.3.4.5' })) changes = [Update(record, record) for i in range(int(Plan.MIN_EXISTING_RECORDS * Plan.MAX_SAFE_UPDATE_PCENT))] Plan(zone, zone, changes).raise_if_unsafe()
def test_include_change_returns_false(self, fake_http): fake_http.get(f'{self.API_URL}/', json=self.domain) fake_http.head(f'{self.API_URL}/', headers={'X-Total-Count': str(len(self.domain))}) provider = SelectelProvider(123, 'test_token') zone = Zone('unit.tests.', []) exist_record = Record.new(zone, '', { 'ttl': 60, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }) new = Record.new(zone, '', { 'ttl': 10, 'type': 'A', 'values': ['1.1.1.1', '2.2.2.2'] }) change = Update(exist_record, new) include_change = provider._include_change(change) self.assertFalse(include_change)
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_update_delete(self): # We need another run so that we can delete, we can't both add and # delete in one go b/c of swaps provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "NS", "name": "unit.tests", "content": "ns1.foo.bar", "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": "NS", "name": "unit.tests", "content": "ns2.foo.bar", "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 } }, ]) provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create None, None, ] # Add something and delete something zone = Zone('unit.tests.', []) existing = Record.new( zone, '', { 'ttl': 300, 'type': 'NS', # This matches the zone data above, one to delete, one to leave 'values': ['ns1.foo.bar.', 'ns2.foo.bar.'], }) new = Record.new( zone, '', { 'ttl': 300, 'type': 'NS', # This leaves one and deletes one 'value': 'ns2.foo.bar.', }) change = Update(existing, new) plan = Plan(zone, zone, [change]) provider._apply(plan) provider._request.assert_has_calls([ call('GET', '/zones', params={'page': 1}), call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), call( 'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' 'dns_records/fc12ab34cd5611334422ab3322997653') ])
def test_update_add_swap(self): provider = CloudflareProvider('test', 'email', 'token') provider.zone_records = Mock(return_value=[ { "id": "fc12ab34cd5611334422ab3322997653", "type": "A", "name": "a.unit.tests", "content": "1.1.1.1", "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": "a.unit.tests", "content": "2.2.2.2", "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 } }, ]) provider._request = Mock() provider._request.side_effect = [ self.empty, # no zones { 'result': { 'id': 42, } }, # zone create None, None, ] # Add something and delete something zone = Zone('unit.tests.', []) existing = Record.new( zone, 'a', { 'ttl': 300, 'type': 'A', # This matches the zone data above, one to swap, one to leave 'values': ['1.1.1.1', '2.2.2.2'], }) new = Record.new( zone, 'a', { 'ttl': 300, 'type': 'A', # This leaves one, swaps ones, and adds one 'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'], }) change = Update(existing, new) plan = Plan(zone, zone, [change]) provider._apply(plan) provider._request.assert_has_calls([ call('GET', '/zones', params={'page': 1}), call('POST', '/zones', data={ 'jump_start': False, 'name': 'unit.tests' }), call('PUT', '/zones/ff12ab34cd5611334422ab3322997650/dns_records/' 'fc12ab34cd5611334422ab3322997653', data={ 'content': '4.4.4.4', 'type': 'A', 'name': 'a.unit.tests', 'ttl': 300 }), call('POST', '/zones/42/dns_records', data={ 'content': '3.3.3.3', 'type': 'A', 'name': 'a.unit.tests', 'ttl': 300 }) ])
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_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 } }) class DummyRecord(object): def __init__(self, health_check_id): self.health_check_id = health_check_id # gc no longer in_use records (directly) stubber.add_response('delete_health_check', {}, { 'HealthCheckId': '44', }) provider._gc_health_checks(record, [ DummyRecord('42'), DummyRecord('43'), ]) stubber.assert_no_pending_responses() # gc through _mod_Create stubber.add_response('delete_health_check', {}, { 'HealthCheckId': '44', }) change = Create(record) provider._mod_Create(change) stubber.assert_no_pending_responses() # gc through _mod_Update stubber.add_response('delete_health_check', {}, { 'HealthCheckId': '44', }) # 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__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=return_values_for_status.next) 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)) 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)) 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)
'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'], }, simple) create = Create( Record.new(zone, 'b', { 'ttl': 60, 'type': 'CNAME', 'value': 'foo.unit.tests.' }, simple)) create2 = Create( Record.new(zone, 'c', { 'ttl': 60, 'type': 'CNAME', 'value': 'foo.unit.tests.' })) update = Update(existing, new) delete = Delete(new) changes = [create, create2, delete, update] plans = [ (simple, Plan(zone, zone, changes, True)), (simple, Plan(zone, zone, changes, False)), ] class TestPlanLogger(TestCase): def test_invalid_level(self): with self.assertRaises(Exception) as ctx: PlanLogger('invalid', 'not-a-level') self.assertEqual('Unsupported level: not-a-level', str(ctx.exception)) def test_create(self):