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_provider(self): source = YamlProvider('test', join(dirname(__file__), 'config')) zone = Zone('unit.tests.', []) dynamic_zone = Zone('dynamic.tests.', []) # With target we don't add anything source.populate(zone, target=source) self.assertEquals(0, len(zone.records)) # without it we see everything source.populate(zone) self.assertEquals(18, len(zone.records)) source.populate(dynamic_zone) self.assertEquals(5, len(dynamic_zone.records)) # Assumption here is that a clean round-trip means that everything # worked as expected, data that went in came back out and could be # pulled in yet again and still match up. That assumes that the input # data completely exercises things. This assumption can be tested by # relatively well by running # ./script/coverage tests/test_octodns_provider_yaml.py and # looking at the coverage file # ./htmlcov/octodns_provider_yaml_py.html with TemporaryDirectory() as td: # Add some subdirs to make sure that it can create them directory = join(td.dirname, 'sub', 'dir') yaml_file = join(directory, 'unit.tests.yaml') dynamic_yaml_file = join(directory, 'dynamic.tests.yaml') target = YamlProvider('test', directory) # We add everything plan = target.plan(zone) self.assertEquals( 15, len([c for c in plan.changes if isinstance(c, Create)])) self.assertFalse(isfile(yaml_file)) # Now actually do it self.assertEquals(15, target.apply(plan)) self.assertTrue(isfile(yaml_file)) # Dynamic plan plan = target.plan(dynamic_zone) self.assertEquals( 5, len([c for c in plan.changes if isinstance(c, Create)])) self.assertFalse(isfile(dynamic_yaml_file)) # Apply it self.assertEquals(5, target.apply(plan)) self.assertTrue(isfile(dynamic_yaml_file)) # There should be no changes after the round trip reloaded = Zone('unit.tests.', []) target.populate(reloaded) self.assertDictEqual({'included': ['test']}, [ x for x in reloaded.records if x.name == 'included' ][0]._octodns) self.assertFalse(zone.changes(reloaded, target=source)) # A 2nd sync should still create everything plan = target.plan(zone) self.assertEquals( 15, len([c for c in plan.changes if isinstance(c, Create)])) with open(yaml_file) as fh: data = safe_load(fh.read()) # '' has some of both roots = sorted(data.pop(''), key=lambda r: r['type']) self.assertTrue('values' in roots[0]) # A self.assertTrue('geo' in roots[0]) # geo made the trip self.assertTrue('value' in roots[1]) # CAA self.assertTrue('values' in roots[2]) # SSHFP # these are stored as plural 'values' self.assertTrue('values' in data.pop('_srv._tcp')) self.assertTrue('values' in data.pop('mx')) self.assertTrue('values' in data.pop('naptr')) self.assertTrue('values' in data.pop('sub')) self.assertTrue('values' in data.pop('txt')) # these are stored as singular 'value' self.assertTrue('value' in data.pop('aaaa')) self.assertTrue('value' in data.pop('cname')) self.assertTrue('value' in data.pop('included')) self.assertTrue('value' in data.pop('ptr')) self.assertTrue('value' in data.pop('spf')) self.assertTrue('value' in data.pop('www')) self.assertTrue('value' in data.pop('www.sub')) # make sure nothing is left self.assertEquals([], list(data.keys())) with open(dynamic_yaml_file) as fh: data = safe_load(fh.read()) # make sure new dynamic records made the trip dyna = data.pop('a') self.assertTrue('values' in dyna) # self.assertTrue('dynamic' in dyna) # TODO: # make sure new dynamic records made the trip dyna = data.pop('aaaa') self.assertTrue('values' in dyna) # self.assertTrue('dynamic' in dyna) dyna = data.pop('cname') self.assertTrue('value' in dyna) # self.assertTrue('dynamic' in dyna) dyna = data.pop('real-ish-a') self.assertTrue('values' in dyna) # self.assertTrue('dynamic' in dyna) dyna = data.pop('simple-weighted') self.assertTrue('value' in dyna) # self.assertTrue('dynamic' in dyna) # make sure nothing is left self.assertEquals([], list(data.keys()))
def test_ignores_subs(self): got = Zone('example.com.', ['sub']) self.source.populate(got) self.assertEquals(10, len(got.records))
def test_ignored_records(self): zone_normal = Zone('unit.tests.', []) zone_ignored = Zone('unit.tests.', []) zone_missing = Zone('unit.tests.', []) normal = Record.new(zone_normal, 'www', { 'ttl': 60, 'type': 'A', 'value': '9.9.9.9', }) zone_normal.add_record(normal) ignored = Record.new(zone_ignored, 'www', { 'octodns': { 'ignored': True }, 'ttl': 60, 'type': 'A', 'value': '9.9.9.9', }) zone_ignored.add_record(ignored) provider = SimpleProvider() self.assertFalse(zone_normal.changes(zone_ignored, provider)) self.assertTrue(zone_normal.changes(zone_missing, provider)) self.assertFalse(zone_ignored.changes(zone_normal, provider)) self.assertFalse(zone_ignored.changes(zone_missing, provider)) self.assertTrue(zone_missing.changes(zone_normal, provider)) self.assertFalse(zone_missing.changes(zone_ignored, provider))
def test_changes(self): before = Zone('unit.tests.', []) a = ARecord(before, 'a', {'ttl': 42, 'value': '1.1.1.1'}) before.add_record(a) b = AaaaRecord(before, 'b', {'ttl': 42, 'value': '1:1:1::1'}) before.add_record(b) after = Zone('unit.tests.', []) after.add_record(a) after.add_record(b) target = SimpleProvider() # before == after -> no changes self.assertFalse(before.changes(after, target)) # add a record, delete a record -> [Delete, Create] c = ARecord(before, 'c', {'ttl': 42, 'value': '1.1.1.1'}) after.add_record(c) after.records.remove(b) self.assertEquals(after.records, set([a, c])) changes = before.changes(after, target) self.assertEquals(2, len(changes)) for change in changes: if isinstance(change, Create): create = change elif isinstance(change, Delete): delete = change self.assertEquals(b, delete.existing) self.assertFalse(delete.new) self.assertEquals(c, create.new) self.assertFalse(create.existing) delete.__repr__() create.__repr__() after = Zone('unit.tests.', []) changed = ARecord(before, 'a', {'ttl': 42, 'value': '2.2.2.2'}) after.add_record(changed) after.add_record(b) changes = before.changes(after, target) self.assertEquals(1, len(changes)) update = changes[0] self.assertIsInstance(update, Update) # Using changes here to get a full equality self.assertFalse(a.changes(update.existing, target)) self.assertFalse(changed.changes(update.new, target)) update.__repr__()
class TestNs1Provider(TestCase): zone = Zone('unit.tests.', []) expected = set() expected.add( Record.new(zone, '', { 'ttl': 32, 'type': 'A', 'value': '1.2.3.4', 'meta': {}, })) expected.add( Record.new(zone, 'foo', { 'ttl': 33, 'type': 'A', 'values': ['1.2.3.4', '1.2.3.5'], 'meta': {}, })) expected.add( Record.new( zone, 'geo', { 'ttl': 34, 'type': 'A', 'values': ['101.102.103.104', '101.102.103.105'], 'geo': { 'NA-US-NY': ['201.202.203.204'] }, 'meta': {}, })) expected.add( Record.new(zone, 'cname', { 'ttl': 34, 'type': 'CNAME', 'value': 'foo.unit.tests.', })) expected.add( Record.new( zone, '', { 'ttl': 35, 'type': 'MX', 'values': [{ 'preference': 10, 'exchange': 'mx1.unit.tests.', }, { 'preference': 20, 'exchange': 'mx2.unit.tests.', }] })) expected.add( Record.new( zone, 'naptr', { 'ttl': 36, 'type': 'NAPTR', 'values': [{ 'flags': 'U', 'order': 100, 'preference': 100, 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', 'service': 'SIP+D2U', }, { 'flags': 'S', 'order': 10, 'preference': 100, 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', 'service': 'SIP+D2U', }] })) expected.add( Record.new( zone, '', { 'ttl': 37, 'type': 'NS', 'values': ['ns1.unit.tests.', 'ns2.unit.tests.'], })) expected.add( Record.new( zone, '_srv._tcp', { 'ttl': 38, 'type': 'SRV', 'values': [{ 'priority': 10, 'weight': 20, 'port': 30, 'target': 'foo-1.unit.tests.', }, { 'priority': 12, 'weight': 30, 'port': 30, 'target': 'foo-2.unit.tests.', }] })) expected.add( Record.new( zone, 'sub', { 'ttl': 39, 'type': 'NS', 'values': ['ns3.unit.tests.', 'ns4.unit.tests.'], })) expected.add( Record.new( zone, '', { 'ttl': 40, 'type': 'CAA', 'value': { 'flags': 0, 'tag': 'issue', 'value': 'ca.unit.tests', }, })) nsone_records = [{ 'type': 'A', 'ttl': 32, 'short_answers': ['1.2.3.4'], 'domain': 'unit.tests.', }, { 'type': 'A', 'ttl': 33, 'short_answers': ['1.2.3.4', '1.2.3.5'], 'domain': 'foo.unit.tests.', }, { 'type': 'A', 'ttl': 34, 'short_answers': ['101.102.103.104', '101.102.103.105'], 'domain': 'geo.unit.tests', }, { 'type': 'CNAME', 'ttl': 34, 'short_answers': ['foo.unit.tests'], 'domain': 'cname.unit.tests.', }, { 'type': 'MX', 'ttl': 35, 'short_answers': ['10 mx1.unit.tests.', '20 mx2.unit.tests'], 'domain': 'unit.tests.', }, { 'type': 'NAPTR', 'ttl': 36, 'short_answers': [ '10 100 S SIP+D2U !^.*$!sip:[email protected]! .', '100 100 U SIP+D2U !^.*$!sip:[email protected]! .' ], 'domain': 'naptr.unit.tests.', }, { 'type': 'NS', 'ttl': 37, 'short_answers': ['ns1.unit.tests.', 'ns2.unit.tests'], 'domain': 'unit.tests.', }, { 'type': 'SRV', 'ttl': 38, 'short_answers': ['12 30 30 foo-2.unit.tests.', '10 20 30 foo-1.unit.tests'], 'domain': '_srv._tcp.unit.tests.', }, { 'type': 'NS', 'ttl': 39, 'short_answers': ['ns3.unit.tests.', 'ns4.unit.tests'], 'domain': 'sub.unit.tests.', }, { 'type': 'CAA', 'ttl': 40, 'short_answers': ['0 issue ca.unit.tests'], 'domain': 'unit.tests.', }] @patch('nsone.NSONE.loadZone') def test_populate(self, load_mock): provider = Ns1Provider('test', 'api-key') # Bad auth load_mock.side_effect = AuthException('unauthorized') zone = Zone('unit.tests.', []) with self.assertRaises(AuthException) as ctx: provider.populate(zone) self.assertEquals(load_mock.side_effect, ctx.exception) # General error load_mock.reset_mock() load_mock.side_effect = ResourceException('boom') zone = Zone('unit.tests.', []) with self.assertRaises(ResourceException) as ctx: provider.populate(zone) self.assertEquals(load_mock.side_effect, ctx.exception) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) # Non-existent zone doesn't populate anything load_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') zone = Zone('unit.tests.', []) exists = provider.populate(zone) self.assertEquals(set(), zone.records) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) self.assertFalse(exists) # Existing zone w/o records load_mock.reset_mock() nsone_zone = DummyZone([]) load_mock.side_effect = [nsone_zone] zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(1, len(zone.records)) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) # Existing zone w/records load_mock.reset_mock() nsone_zone = DummyZone(self.nsone_records) load_mock.side_effect = [nsone_zone] zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(self.expected, zone.records) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) # Test skipping unsupported record type load_mock.reset_mock() nsone_zone = DummyZone(self.nsone_records + [{ 'type': 'UNSUPPORTED', 'ttl': 42, 'short_answers': ['unsupported'], 'domain': 'unsupported.unit.tests.', }]) load_mock.side_effect = [nsone_zone] zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(self.expected, zone.records) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) @patch('nsone.NSONE.createZone') @patch('nsone.NSONE.loadZone') def test_sync(self, load_mock, create_mock): provider = Ns1Provider('test', 'api-key') desired = Zone('unit.tests.', []) for r in self.expected: desired.add_record(r) plan = provider.plan(desired) # everything except the root NS expected_n = len(self.expected) - 1 self.assertEquals(expected_n, len(plan.changes)) self.assertTrue(plan.exists) # Fails, general error load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = ResourceException('boom') with self.assertRaises(ResourceException) as ctx: provider.apply(plan) self.assertEquals(load_mock.side_effect, ctx.exception) # Fails, bad auth load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') create_mock.side_effect = AuthException('unauthorized') with self.assertRaises(AuthException) as ctx: provider.apply(plan) self.assertEquals(create_mock.side_effect, ctx.exception) # non-existent zone, create load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') # ugh, need a mock zone with a mock prop since we're using getattr, we # can actually control side effects on `meth` with that. mock_zone = Mock() mock_zone.add_SRV = Mock() mock_zone.add_SRV.side_effect = [ RateLimitException('boo', period=0), None, ] create_mock.side_effect = [mock_zone] got_n = provider.apply(plan) self.assertEquals(expected_n, got_n) # Update & delete load_mock.reset_mock() create_mock.reset_mock() nsone_zone = DummyZone(self.nsone_records + [{ 'type': 'A', 'ttl': 42, 'short_answers': ['9.9.9.9'], 'domain': 'delete-me.unit.tests.', }]) nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2' nsone_zone.loadRecord = Mock() zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search load_mock.side_effect = [nsone_zone, nsone_zone] plan = provider.plan(desired) self.assertEquals(3, len(plan.changes)) self.assertIsInstance(plan.changes[0], Update) self.assertIsInstance(plan.changes[2], Delete) # ugh, we need a mock record that can be returned from loadRecord for # the update and delete targets, we can add our side effects to that to # trigger rate limit handling mock_record = Mock() mock_record.update.side_effect = [ RateLimitException('one', period=0), None, None, ] mock_record.delete.side_effect = [ RateLimitException('two', period=0), None, None, ] nsone_zone.loadRecord.side_effect = [ mock_record, mock_record, mock_record ] got_n = provider.apply(plan) self.assertEquals(3, got_n) nsone_zone.loadRecord.assert_has_calls([ call('unit.tests', u'A'), call('geo', u'A'), call('delete-me', u'A'), ]) mock_record.assert_has_calls([ call.update(answers=[{ 'answer': [u'1.2.3.4'], 'meta': {} }], filters=[], ttl=32), call.update(answers=[{ u'answer': [u'1.2.3.4'], u'meta': {} }], filters=[], ttl=32), call.update(answers=[ { u'answer': [u'101.102.103.104'], u'meta': {} }, { u'answer': [u'101.102.103.105'], u'meta': {} }, { u'answer': [u'201.202.203.204'], u'meta': { u'iso_region_code': [u'NA-US-NY'] }, }, ], filters=[ { u'filter': u'shuffle', u'config': {} }, { u'filter': u'geotarget_country', u'config': {} }, { u'filter': u'select_first_n', u'config': { u'N': 1 } }, ], ttl=34), call.delete(), call.delete() ]) def test_escaping(self): provider = Ns1Provider('test', 'api-key') record = {'ttl': 31, 'short_answers': ['foo; bar baz; blip']} self.assertEquals(['foo\\; bar baz\\; blip'], provider._data_for_SPF('SPF', record)['values']) record = { 'ttl': 31, 'short_answers': ['no', 'foo; bar baz; blip', 'yes'] } self.assertEquals(['no', 'foo\\; bar baz\\; blip', 'yes'], provider._data_for_TXT('TXT', record)['values']) zone = Zone('unit.tests.', []) record = Record.new(zone, 'spf', { 'ttl': 34, 'type': 'SPF', 'value': 'foo\\; bar baz\\; blip' }) self.assertEquals(['foo; bar baz; blip'], provider._params_for_SPF(record)['answers']) record = Record.new(zone, 'txt', { 'ttl': 35, 'type': 'TXT', 'value': 'foo\\; bar baz\\; blip' }) self.assertEquals(['foo; bar baz; blip'], provider._params_for_TXT(record)['answers']) def test_data_for_CNAME(self): provider = Ns1Provider('test', 'api-key') # answers from nsone a_record = { 'ttl': 31, 'type': 'CNAME', 'short_answers': ['foo.unit.tests.'] } a_expected = {'ttl': 31, 'type': 'CNAME', 'value': 'foo.unit.tests.'} self.assertEqual(a_expected, provider._data_for_CNAME(a_record['type'], a_record)) # no answers from nsone b_record = {'ttl': 32, 'type': 'CNAME', 'short_answers': []} b_expected = {'ttl': 32, 'type': 'CNAME', 'value': None} self.assertEqual(b_expected, provider._data_for_CNAME(b_record['type'], b_record))
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_zone_records(self): provider = _get_provider() zone_payload = { "resultInfo": { "totalCount": 1, "offset": 0, "returnedCount": 1 }, "zones": [{ "properties": { "name": "octodns1.test." } }] } records_payload = { "zoneName": "octodns1.test.", "rrSets": [ { "ownerName": "octodns1.test.", "rrtype": "NS (2)", "ttl": 86400, "rdata": ["ns1.octodns1.test."] }, { "ownerName": "octodns1.test.", "rrtype": "SOA (6)", "ttl": 86400, "rdata": ["pdns1.ultradns.com. phelps.netflix.com. 1 10 10 10 10"] }, ], "resultInfo": { "totalCount": 2, "offset": 0, "returnedCount": 2 } } zone_path = '/v2/zones' rec_path = '/v2/zones/octodns1.test./rrsets' with requests_mock() as mock: mock.get( f'{self.host}{zone_path}?limit=100&q=zone_type%3APRIMARY&' 'offset=0', status_code=200, json=zone_payload) mock.get(f'{self.host}{rec_path}?offset=0&limit=100', status_code=200, json=records_payload) zone = Zone('octodns1.test.', []) self.assertTrue(provider.zone_records(zone)) self.assertEquals(mock.call_count, 2) # Populate the same zone again and confirm cache is hit self.assertTrue(provider.zone_records(zone)) self.assertEquals(mock.call_count, 2)
class TestUltraProvider(TestCase): expected = Zone('unit.tests.', []) host = 'https://restapi.ultradns.com' empty_body = [{"errorCode": 70002, "errorMessage": "Data not found."}] expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) def test_login(self): path = '/v2/authorization/token' # Bad Auth with requests_mock() as mock: mock.post(f'{self.host}{path}', status_code=401, text='{"errorCode": 60001}') with self.assertRaises(Exception) as ctx: UltraProvider('test', 'account', 'user', 'wrongpass') self.assertEquals('Unauthorized', str(ctx.exception)) # Good Auth with requests_mock() as mock: headers = {'Content-Type': 'application/x-www-form-urlencoded'} mock.post(f'{self.host}{path}', status_code=200, request_headers=headers, text='{"token type": "Bearer", "refresh_token": "abc", ' '"access_token":"123", "expires_in": "3600"}') UltraProvider('test', 'account', 'user', 'rightpass') self.assertEquals(1, mock.call_count) expected_payload = "grant_type=password&username=user&"\ "password=rightpass" self.assertEquals(parse_qs(mock.last_request.text), parse_qs(expected_payload)) def test_get_zones(self): provider = _get_provider() path = "/v2/zones" # Test authorization issue with requests_mock() as mock: mock.get(f'{self.host}{path}', status_code=400, json={ "errorCode": 60004, "errorMessage": "Authorization Header required" }) with self.assertRaises(HTTPError) as ctx: zones = provider.zones self.assertEquals(400, ctx.exception.response.status_code) # Test no zones exist error with requests_mock() as mock: mock.get(f'{self.host}{path}', status_code=404, headers={'Authorization': 'Bearer 123'}, json=self.empty_body) zones = provider.zones self.assertEquals(1, mock.call_count) self.assertEquals(list(), zones) # Reset zone cache so they are queried again provider._zones = None with requests_mock() as mock: payload = { "resultInfo": { "totalCount": 1, "offset": 0, "returnedCount": 1 }, "zones": [{ "properties": { "name": "testzone123.com.", "accountName": "testaccount", "type": "PRIMARY", "dnssecStatus": "UNSIGNED", "status": "ACTIVE", "owner": "user", "resourceRecordCount": 5, "lastModifiedDateTime": "2020-06-19T00:47Z" } }] } mock.get(f'{self.host}{path}', status_code=200, headers={'Authorization': 'Bearer 123'}, json=payload) zones = provider.zones self.assertEquals(1, mock.call_count) self.assertEquals(1, len(zones)) self.assertEquals('testzone123.com.', zones[0]) # Test different paging behavior provider._zones = None with requests_mock() as mock: mock.get( f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY&' 'offset=0', status_code=200, json={ "resultInfo": { "totalCount": 15, "offset": 0, "returnedCount": 10 }, "zones": [] }) mock.get( f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY' '&offset=10', status_code=200, json={ "resultInfo": { "totalCount": 15, "offset": 10, "returnedCount": 5 }, "zones": [] }) zones = provider.zones self.assertEquals(2, mock.call_count) def test_request(self): provider = _get_provider() path = '/foo' payload = {'a': 1} with requests_mock() as mock: mock.get(f'{self.host}{path}', status_code=401, headers={'Authorization': 'Bearer 123'}, json={}) with self.assertRaises(Exception) as ctx: provider._get(path) self.assertEquals('Unauthorized', str(ctx.exception)) # Test all GET patterns with requests_mock() as mock: mock.get(f'{self.host}{path}', status_code=200, headers={'Authorization': 'Bearer 123'}, json=payload) provider._get(path, json=payload) mock.get(f'{self.host}{path}?a=1', status_code=200, headers={'Authorization': 'Bearer 123'}) provider._get(path, params=payload, json_response=False) # Test all POST patterns with requests_mock() as mock: mock.post(f'{self.host}{path}', status_code=200, headers={'Authorization': 'Bearer 123'}, json=payload) provider._post(path, json=payload) mock.post(f'{self.host}{path}', status_code=200, headers={'Authorization': 'Bearer 123'}, text="{'a':1}") provider._post(path, data=payload, json_response=False) # Test all PUT patterns with requests_mock() as mock: mock.put(f'{self.host}{path}', status_code=200, headers={'Authorization': 'Bearer 123'}, json=payload) provider._put(path, json=payload) # Test all DELETE patterns with requests_mock() as mock: mock.delete(f'{self.host}{path}', status_code=200, headers={'Authorization': 'Bearer 123'}) provider._delete(path, json_response=False) def test_zone_records(self): provider = _get_provider() zone_payload = { "resultInfo": { "totalCount": 1, "offset": 0, "returnedCount": 1 }, "zones": [{ "properties": { "name": "octodns1.test." } }] } records_payload = { "zoneName": "octodns1.test.", "rrSets": [ { "ownerName": "octodns1.test.", "rrtype": "NS (2)", "ttl": 86400, "rdata": ["ns1.octodns1.test."] }, { "ownerName": "octodns1.test.", "rrtype": "SOA (6)", "ttl": 86400, "rdata": ["pdns1.ultradns.com. phelps.netflix.com. 1 10 10 10 10"] }, ], "resultInfo": { "totalCount": 2, "offset": 0, "returnedCount": 2 } } zone_path = '/v2/zones' rec_path = '/v2/zones/octodns1.test./rrsets' with requests_mock() as mock: mock.get( f'{self.host}{zone_path}?limit=100&q=zone_type%3APRIMARY&' 'offset=0', status_code=200, json=zone_payload) mock.get(f'{self.host}{rec_path}?offset=0&limit=100', status_code=200, json=records_payload) zone = Zone('octodns1.test.', []) self.assertTrue(provider.zone_records(zone)) self.assertEquals(mock.call_count, 2) # Populate the same zone again and confirm cache is hit self.assertTrue(provider.zone_records(zone)) self.assertEquals(mock.call_count, 2) def test_populate(self): provider = _get_provider() # Non-existent zone doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=404, json=self.empty_body) zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(set(), zone.records) # re-populating the same non-existent zone uses cache and makes no # calls again = Zone('unit.tests.', []) provider.populate(again) self.assertEquals(set(), again.records) # Test zones with data provider._zones = None path = '/v2/zones' with requests_mock() as mock: with open('tests/fixtures/ultra-zones-page-1.json') as fh: mock.get( f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY&' 'offset=0', status_code=200, text=fh.read()) with open('tests/fixtures/ultra-zones-page-2.json') as fh: mock.get( f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY&' 'offset=10', status_code=200, text=fh.read()) with open('tests/fixtures/ultra-records-page-1.json') as fh: rec_path = '/v2/zones/octodns1.test./rrsets' mock.get(f'{self.host}{rec_path}?offset=0&limit=100', status_code=200, text=fh.read()) with open('tests/fixtures/ultra-records-page-2.json') as fh: rec_path = '/v2/zones/octodns1.test./rrsets' mock.get(f'{self.host}{rec_path}?offset=10&limit=100', status_code=200, text=fh.read()) zone = Zone('octodns1.test.', []) self.assertTrue(provider.populate(zone)) self.assertEquals('octodns1.test.', zone.name) self.assertEquals(12, len(zone.records)) self.assertEquals(4, mock.call_count) def test_apply(self): provider = _get_provider() provider._request = Mock() provider._request.side_effect = [ UltraNoZonesExistException('No Zones'), None, # zone create ] + [None] * 15 # individual record creates # non-existent zone, create everything plan = provider.plan(self.expected) self.assertEquals(15, len(plan.changes)) self.assertEquals(15, provider.apply(plan)) self.assertFalse(plan.exists) provider._request.assert_has_calls( [ # created the domain call('POST', '/v2/zones', json={ 'properties': { 'name': 'unit.tests.', 'accountName': 'testacct', 'type': 'PRIMARY' }, 'primaryCreateInfo': { 'createType': 'NEW' } }), # Validate multi-ip apex A record is correct call('POST', '/v2/zones/unit.tests./rrsets/A/unit.tests.', json={ 'ttl': 300, 'rdata': ['1.2.3.4', '1.2.3.5'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'unit.tests.' } }), # make sure semicolons are not escaped when sending data call('POST', '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.', json={ 'ttl': 600, 'rdata': [ 'Bah bah black sheep', 'have you any wool.', 'v=DKIM1;k=rsa;s=email;h=sha256;' 'p=A/kinda+of/long/string+with+numb3rs' ] }), ], True) # expected number of total calls self.assertEquals(17, provider._request.call_count) # Create sample rrset payload to attempt to alter page1 = json_load(open('tests/fixtures/ultra-records-page-1.json')) page2 = json_load(open('tests/fixtures/ultra-records-page-2.json')) mock_rrsets = list() mock_rrsets.extend(page1['rrSets']) mock_rrsets.extend(page2['rrSets']) # Seed a bunch of records into a zone and verify update / delete ops provider._request.reset_mock() provider._zones = ['octodns1.test.'] provider.zone_records = Mock(return_value=mock_rrsets) provider._request.side_effect = [None] * 13 wanted = Zone('octodns1.test.', []) wanted.add_record( Record.new( wanted, '', { 'ttl': 60, # Change TTL 'type': 'A', 'value': '5.6.7.8' # Change number of IPs (3 -> 1) })) wanted.add_record( Record.new( wanted, 'txt', { 'ttl': 3600, 'type': 'TXT', 'values': [ # Alter TXT value "foobar", "v=spf1 include:mail.server.net ?all" ] })) plan = provider.plan(wanted) self.assertEquals(11, len(plan.changes)) self.assertEquals(11, provider.apply(plan)) self.assertTrue(plan.exists) provider._request.assert_has_calls( [ # Validate multi-ip apex A record replaced with standard A call('PUT', '/v2/zones/octodns1.test./rrsets/A/octodns1.test.', json={ 'ttl': 60, 'rdata': ['5.6.7.8'] }), # Make sure TXT value is properly updated call('PUT', '/v2/zones/octodns1.test./rrsets/TXT/txt.octodns1.test.', json={ 'ttl': 3600, 'rdata': ["foobar", "v=spf1 include:mail.server.net ?all"] }), # Confirm a few of the DELETE operations properly occur call('DELETE', '/v2/zones/octodns1.test./rrsets/A/a.octodns1.test.', json_response=False), call( 'DELETE', '/v2/zones/octodns1.test./rrsets/AAAA/aaaa.octodns1.test.', json_response=False), call('DELETE', '/v2/zones/octodns1.test./rrsets/CAA/caa.octodns1.test.', json_response=False), call( 'DELETE', '/v2/zones/octodns1.test./rrsets/CNAME/cname.octodns1.test.', json_response=False), ], True) def test_gen_data(self): provider = _get_provider() zone = Zone('unit.tests.', []) for name, _type, expected_path, expected_payload, expected_record in ( # A ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', { 'ttl': 60, 'rdata': ['1.2.3.4'] }, Record.new(zone, 'a', { 'ttl': 60, 'type': 'A', 'values': ['1.2.3.4'] })), ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', { 'ttl': 60, 'rdata': ['1.2.3.4', '5.6.7.8'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'a.unit.tests.' } }, Record.new(zone, 'a', { 'ttl': 60, 'type': 'A', 'values': ['1.2.3.4', '5.6.7.8'] })), # AAAA ('aaaa', 'AAAA', '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', { 'ttl': 60, 'rdata': ['::1'] }, Record.new(zone, 'aaaa', { 'ttl': 60, 'type': 'AAAA', 'values': ['::1'] })), ('aaaa', 'AAAA', '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', { 'ttl': 60, 'rdata': ['::1', '::2'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'aaaa.unit.tests.' } }, Record.new(zone, 'aaaa', { 'ttl': 60, 'type': 'AAAA', 'values': ['::1', '::2'] })), # CAA ('caa', 'CAA', '/v2/zones/unit.tests./rrsets/CAA/caa.unit.tests.', { 'ttl': 60, 'rdata': ['0 issue foo.com'] }, Record.new( zone, 'caa', { 'ttl': 60, 'type': 'CAA', 'values': [{ 'flags': 0, 'tag': 'issue', 'value': 'foo.com' }] })), # CNAME ('cname', 'CNAME', '/v2/zones/unit.tests./rrsets/CNAME/cname.unit.tests.', { 'ttl': 60, 'rdata': ['netflix.com.'] }, Record.new(zone, 'cname', { 'ttl': 60, 'type': 'CNAME', 'value': 'netflix.com.' })), # MX ('mx', 'MX', '/v2/zones/unit.tests./rrsets/MX/mx.unit.tests.', { 'ttl': 60, 'rdata': ['1 mx1.unit.tests.', '1 mx2.unit.tests.'] }, Record.new( zone, 'mx', { 'ttl': 60, 'type': 'MX', 'values': [{ 'preference': 1, 'exchange': 'mx1.unit.tests.' }, { 'preference': 1, 'exchange': 'mx2.unit.tests.' }] })), # NS ('ns', 'NS', '/v2/zones/unit.tests./rrsets/NS/ns.unit.tests.', { 'ttl': 60, 'rdata': ['ns1.unit.tests.', 'ns2.unit.tests.'] }, Record.new( zone, 'ns', { 'ttl': 60, 'type': 'NS', 'values': ['ns1.unit.tests.', 'ns2.unit.tests.'] })), # PTR ('ptr', 'PTR', '/v2/zones/unit.tests./rrsets/PTR/ptr.unit.tests.', { 'ttl': 60, 'rdata': ['a.unit.tests.'] }, Record.new(zone, 'ptr', { 'ttl': 60, 'type': 'PTR', 'value': 'a.unit.tests.' })), # SPF ('spf', 'SPF', '/v2/zones/unit.tests./rrsets/SPF/spf.unit.tests.', { 'ttl': 60, 'rdata': ['v=spf1 -all'] }, Record.new(zone, 'spf', { 'ttl': 60, 'type': 'SPF', 'values': ['v=spf1 -all'] })), # SRV ('_srv._tcp', 'SRV', '/v2/zones/unit.tests./rrsets/SRV/_srv._tcp.unit.tests.', { 'ttl': 60, 'rdata': ['10 20 443 target.unit.tests.'] }, Record.new( zone, '_srv._tcp', { 'ttl': 60, 'type': 'SRV', 'values': [{ 'priority': 10, 'weight': 20, 'port': 443, 'target': 'target.unit.tests.' }] })), # TXT ('txt', 'TXT', '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.', { 'ttl': 60, 'rdata': ['abc', 'def'] }, Record.new(zone, 'txt', { 'ttl': 60, 'type': 'TXT', 'values': ['abc', 'def'] })), # ALIAS ('', 'ALIAS', '/v2/zones/unit.tests./rrsets/APEXALIAS/unit.tests.', { 'ttl': 60, 'rdata': ['target.unit.tests.'] }, Record.new(zone, '', { 'ttl': 60, 'type': 'ALIAS', 'value': 'target.unit.tests.' })), ): # Validate path and payload based on record meet expectations path, payload = provider._gen_data(expected_record) self.assertEqual(expected_path, path) self.assertEqual(expected_payload, payload) # Use generator for record and confirm the output matches rec = provider._record_for(zone, name, _type, expected_payload, False) path, payload = provider._gen_data(rec) self.assertEqual(expected_path, path) self.assertEqual(expected_payload, payload)
class TestOvhProvider(TestCase): api_record = [] valid_dkim = [] invalid_dkim = [] valid_dkim_key = "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxLaG16G4SaE" \ "cXVdiIxTg7gKSGbHKQLm30CHib1h9FzS9nkcyvQSyQj1rMFyqC//" \ "tft3ohx3nvJl+bGCWxdtLYDSmir9PW54e5CTdxEh8MWRkBO3StF6" \ "QG/tAh3aTGDmkqhIJGLb87iHvpmVKqURmEUzJPv5KPJfWLofADI+" \ "q9lQIDAQAB" zone = Zone('unit.tests.', []) expected = set() # A, subdomain='' api_record.append({ 'fieldType': 'A', 'ttl': 100, 'target': '1.2.3.4', 'subDomain': '', 'id': 1 }) expected.add( Record.new(zone, '', { 'ttl': 100, 'type': 'A', 'value': '1.2.3.4', })) # A, subdomain='sub api_record.append({ 'fieldType': 'A', 'ttl': 200, 'target': '1.2.3.4', 'subDomain': 'sub', 'id': 2 }) expected.add( Record.new(zone, 'sub', { 'ttl': 200, 'type': 'A', 'value': '1.2.3.4', })) # CNAME api_record.append({ 'fieldType': 'CNAME', 'ttl': 300, 'target': 'unit.tests.', 'subDomain': 'www2', 'id': 3 }) expected.add( Record.new(zone, 'www2', { 'ttl': 300, 'type': 'CNAME', 'value': 'unit.tests.', })) # MX api_record.append({ 'fieldType': 'MX', 'ttl': 400, 'target': '10 mx1.unit.tests.', 'subDomain': '', 'id': 4 }) expected.add( Record.new( zone, '', { 'ttl': 400, 'type': 'MX', 'values': [{ 'preference': 10, 'exchange': 'mx1.unit.tests.', }] })) # NAPTR api_record.append({ 'fieldType': 'NAPTR', 'ttl': 500, 'target': '10 100 "S" "SIP+D2U" "!^.*$!sip:[email protected]!" .', 'subDomain': 'naptr', 'id': 5 }) expected.add( Record.new( zone, 'naptr', { 'ttl': 500, 'type': 'NAPTR', 'values': [{ 'flags': 'S', 'order': 10, 'preference': 100, 'regexp': '!^.*$!sip:[email protected]!', 'replacement': '.', 'service': 'SIP+D2U', }] })) # NS api_record.append({ 'fieldType': 'NS', 'ttl': 600, 'target': 'ns1.unit.tests.', 'subDomain': '', 'id': 6 }) api_record.append({ 'fieldType': 'NS', 'ttl': 600, 'target': 'ns2.unit.tests.', 'subDomain': '', 'id': 7 }) expected.add( Record.new( zone, '', { 'ttl': 600, 'type': 'NS', 'values': ['ns1.unit.tests.', 'ns2.unit.tests.'], })) # NS with sub api_record.append({ 'fieldType': 'NS', 'ttl': 700, 'target': 'ns3.unit.tests.', 'subDomain': 'www3', 'id': 8 }) api_record.append({ 'fieldType': 'NS', 'ttl': 700, 'target': 'ns4.unit.tests.', 'subDomain': 'www3', 'id': 9 }) expected.add( Record.new( zone, 'www3', { 'ttl': 700, 'type': 'NS', 'values': ['ns3.unit.tests.', 'ns4.unit.tests.'], })) api_record.append({ 'fieldType': 'SRV', 'ttl': 800, 'target': '10 20 30 foo-1.unit.tests.', 'subDomain': '_srv._tcp', 'id': 10 }) api_record.append({ 'fieldType': 'SRV', 'ttl': 800, 'target': '40 50 60 foo-2.unit.tests.', 'subDomain': '_srv._tcp', 'id': 11 }) expected.add( Record.new( zone, '_srv._tcp', { 'ttl': 800, 'type': 'SRV', 'values': [{ 'priority': 10, 'weight': 20, 'port': 30, 'target': 'foo-1.unit.tests.', }, { 'priority': 40, 'weight': 50, 'port': 60, 'target': 'foo-2.unit.tests.', }] })) # PTR api_record.append({ 'fieldType': 'PTR', 'ttl': 900, 'target': 'unit.tests.', 'subDomain': '4', 'id': 12 }) expected.add( Record.new(zone, '4', { 'ttl': 900, 'type': 'PTR', 'value': 'unit.tests.' })) # SPF api_record.append({ 'fieldType': 'SPF', 'ttl': 1000, 'target': 'v=spf1 include:unit.texts.redirect ~all', 'subDomain': '', 'id': 13 }) expected.add( Record.new( zone, '', { 'ttl': 1000, 'type': 'SPF', 'value': 'v=spf1 include:unit.texts.redirect ~all' })) # SSHFP api_record.append({ 'fieldType': 'SSHFP', 'ttl': 1100, 'target': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73 ', 'subDomain': '', 'id': 14 }) expected.add( Record.new( zone, '', { 'ttl': 1100, 'type': 'SSHFP', 'value': { 'algorithm': 1, 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', 'fingerprint_type': 1 } })) # AAAA api_record.append({ 'fieldType': 'AAAA', 'ttl': 1200, 'target': '1:1ec:1::1', 'subDomain': '', 'id': 15 }) expected.add( Record.new(zone, '', { 'ttl': 200, 'type': 'AAAA', 'value': '1:1ec:1::1', })) # DKIM api_record.append({ 'fieldType': 'DKIM', 'ttl': 1300, 'target': valid_dkim_key, 'subDomain': 'dkim', 'id': 16 }) expected.add( Record.new(zone, 'dkim', { 'ttl': 1300, 'type': 'TXT', 'value': valid_dkim_key, })) # TXT api_record.append({ 'fieldType': 'TXT', 'ttl': 1400, 'target': 'TXT text', 'subDomain': 'txt', 'id': 17 }) expected.add( Record.new(zone, 'txt', { 'ttl': 1400, 'type': 'TXT', 'value': 'TXT text', })) # LOC # We do not have associated record for LOC, as it's not managed api_record.append({ 'fieldType': 'LOC', 'ttl': 1500, 'target': '1 1 1 N 1 1 1 E 1m 1m', 'subDomain': '', 'id': 18 }) valid_dkim = [ valid_dkim_key, 'v=DKIM1 \\; %s' % valid_dkim_key, 'h=sha256 \\; %s' % valid_dkim_key, 'h=sha1 \\; %s' % valid_dkim_key, 's=* \\; %s' % valid_dkim_key, 's=email \\; %s' % valid_dkim_key, 't=y \\; %s' % valid_dkim_key, 't=s \\; %s' % valid_dkim_key, 'k=rsa \\; %s' % valid_dkim_key, 'n=notes \\; %s' % valid_dkim_key, 'g=granularity \\; %s' % valid_dkim_key, ] invalid_dkim = [ 'p=%invalid%', # Invalid public key 'v=DKIM1', # Missing public key 'v=DKIM2 \\; %s' % valid_dkim_key, # Invalid version 'h=sha512 \\; %s' % valid_dkim_key, # Invalid hash algo 's=fake \\; %s' % valid_dkim_key, # Invalid selector 't=fake \\; %s' % valid_dkim_key, # Invalid flag 'u=invalid \\; %s' % valid_dkim_key, # Invalid key ] @patch('ovh.Client') def test_populate(self, client_mock): provider = OvhProvider('test', 'endpoint', 'application_key', 'application_secret', 'consumer_key') with patch.object(provider._client, 'get') as get_mock: zone = Zone('unit.tests.', []) get_mock.side_effect = ResourceNotFoundError('boom') with self.assertRaises(APIError) as ctx: provider.populate(zone) self.assertEquals(get_mock.side_effect, ctx.exception) get_mock.side_effect = InvalidCredential('boom') with self.assertRaises(APIError) as ctx: provider.populate(zone) self.assertEquals(get_mock.side_effect, ctx.exception) zone = Zone('unit.tests.', []) get_mock.side_effect = ResourceNotFoundError('This service does ' 'not exist') exists = provider.populate(zone) self.assertEquals(set(), zone.records) self.assertFalse(exists) zone = Zone('unit.tests.', []) get_returns = [[record['id'] for record in self.api_record]] get_returns += self.api_record get_mock.side_effect = get_returns exists = provider.populate(zone) self.assertEquals(self.expected, zone.records) self.assertTrue(exists) @patch('ovh.Client') def test_is_valid_dkim(self, client_mock): """Test _is_valid_dkim""" provider = OvhProvider('test', 'endpoint', 'application_key', 'application_secret', 'consumer_key') for dkim in self.valid_dkim: self.assertTrue(provider._is_valid_dkim(dkim)) for dkim in self.invalid_dkim: self.assertFalse(provider._is_valid_dkim(dkim)) @patch('ovh.Client') def test_apply(self, client_mock): provider = OvhProvider('test', 'endpoint', 'application_key', 'application_secret', 'consumer_key') desired = Zone('unit.tests.', []) for r in self.expected: desired.add_record(r) with patch.object(provider._client, 'post') as get_mock: plan = provider.plan(desired) get_mock.side_effect = APIError('boom') with self.assertRaises(APIError) as ctx: provider.apply(plan) self.assertEquals(get_mock.side_effect, ctx.exception) # Records get by API call with patch.object(provider._client, 'get') as get_mock: get_returns = [[1, 2, 3, 4], { 'fieldType': 'A', 'ttl': 600, 'target': '5.6.7.8', 'subDomain': '', 'id': 100 }, { 'fieldType': 'A', 'ttl': 600, 'target': '5.6.7.8', 'subDomain': 'fake', 'id': 101 }, { 'fieldType': 'TXT', 'ttl': 600, 'target': 'fake txt record', 'subDomain': 'txt', 'id': 102 }, { 'fieldType': 'DKIM', 'ttl': 600, 'target': 'v=DKIM1; %s' % self.valid_dkim_key, 'subDomain': 'dkim', 'id': 103 }] get_mock.side_effect = get_returns plan = provider.plan(desired) with patch.object(provider._client, 'post') as post_mock, \ patch.object(provider._client, 'delete') as delete_mock: get_mock.side_effect = [[100], [101], [102], [103]] provider.apply(plan) wanted_calls = [ call(u'/domain/zone/unit.tests/record', fieldType=u'TXT', subDomain='txt', target=u'TXT text', ttl=1400), call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM', subDomain='dkim', target=self.valid_dkim_key, ttl=1300), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain=u'', target=u'1.2.3.4', ttl=100), call(u'/domain/zone/unit.tests/record', fieldType=u'SRV', subDomain='_srv._tcp', target=u'10 20 30 foo-1.unit.tests.', ttl=800), call(u'/domain/zone/unit.tests/record', fieldType=u'SRV', subDomain='_srv._tcp', target=u'40 50 60 foo-2.unit.tests.', ttl=800), call(u'/domain/zone/unit.tests/record', fieldType=u'PTR', subDomain='4', target=u'unit.tests.', ttl=900), call(u'/domain/zone/unit.tests/record', fieldType=u'NS', subDomain='www3', target=u'ns3.unit.tests.', ttl=700), call(u'/domain/zone/unit.tests/record', fieldType=u'NS', subDomain='www3', target=u'ns4.unit.tests.', ttl=700), call( u'/domain/zone/unit.tests/record', fieldType=u'SSHFP', subDomain=u'', ttl=1100, target=u'1 1 bf6b6825d2977c511a475bbefb88a' u'ad54' u'a92ac73', ), call(u'/domain/zone/unit.tests/record', fieldType=u'AAAA', subDomain=u'', target=u'1:1ec:1::1', ttl=200), call(u'/domain/zone/unit.tests/record', fieldType=u'MX', subDomain=u'', target=u'10 mx1.unit.tests.', ttl=400), call(u'/domain/zone/unit.tests/record', fieldType=u'CNAME', subDomain='www2', target=u'unit.tests.', ttl=300), call( u'/domain/zone/unit.tests/record', fieldType=u'SPF', subDomain=u'', ttl=1000, target=u'v=spf1 include:unit.texts.' u'redirect ~all', ), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain='sub', target=u'1.2.3.4', ttl=200), call(u'/domain/zone/unit.tests/record', fieldType=u'NAPTR', subDomain='naptr', ttl=500, target=u'10 100 "S" "SIP+D2U" "!^.*$!sip:' u'info@bar' u'.example.com!" .'), call(u'/domain/zone/unit.tests/refresh') ] post_mock.assert_has_calls(wanted_calls) # Get for delete calls wanted_get_calls = [ call(u'/domain/zone/unit.tests/record', fieldType=u'TXT', subDomain='txt'), call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM', subDomain='dkim'), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain=u''), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain='fake') ] get_mock.assert_has_calls(wanted_get_calls) # 4 delete calls for update and delete delete_mock.assert_has_calls([ call(u'/domain/zone/unit.tests/record/100'), call(u'/domain/zone/unit.tests/record/101'), call(u'/domain/zone/unit.tests/record/102'), call(u'/domain/zone/unit.tests/record/103') ])
def test_apply(self, client_mock): provider = OvhProvider('test', 'endpoint', 'application_key', 'application_secret', 'consumer_key') desired = Zone('unit.tests.', []) for r in self.expected: desired.add_record(r) with patch.object(provider._client, 'post') as get_mock: plan = provider.plan(desired) get_mock.side_effect = APIError('boom') with self.assertRaises(APIError) as ctx: provider.apply(plan) self.assertEquals(get_mock.side_effect, ctx.exception) # Records get by API call with patch.object(provider._client, 'get') as get_mock: get_returns = [[1, 2, 3, 4], { 'fieldType': 'A', 'ttl': 600, 'target': '5.6.7.8', 'subDomain': '', 'id': 100 }, { 'fieldType': 'A', 'ttl': 600, 'target': '5.6.7.8', 'subDomain': 'fake', 'id': 101 }, { 'fieldType': 'TXT', 'ttl': 600, 'target': 'fake txt record', 'subDomain': 'txt', 'id': 102 }, { 'fieldType': 'DKIM', 'ttl': 600, 'target': 'v=DKIM1; %s' % self.valid_dkim_key, 'subDomain': 'dkim', 'id': 103 }] get_mock.side_effect = get_returns plan = provider.plan(desired) with patch.object(provider._client, 'post') as post_mock, \ patch.object(provider._client, 'delete') as delete_mock: get_mock.side_effect = [[100], [101], [102], [103]] provider.apply(plan) wanted_calls = [ call(u'/domain/zone/unit.tests/record', fieldType=u'TXT', subDomain='txt', target=u'TXT text', ttl=1400), call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM', subDomain='dkim', target=self.valid_dkim_key, ttl=1300), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain=u'', target=u'1.2.3.4', ttl=100), call(u'/domain/zone/unit.tests/record', fieldType=u'SRV', subDomain='_srv._tcp', target=u'10 20 30 foo-1.unit.tests.', ttl=800), call(u'/domain/zone/unit.tests/record', fieldType=u'SRV', subDomain='_srv._tcp', target=u'40 50 60 foo-2.unit.tests.', ttl=800), call(u'/domain/zone/unit.tests/record', fieldType=u'PTR', subDomain='4', target=u'unit.tests.', ttl=900), call(u'/domain/zone/unit.tests/record', fieldType=u'NS', subDomain='www3', target=u'ns3.unit.tests.', ttl=700), call(u'/domain/zone/unit.tests/record', fieldType=u'NS', subDomain='www3', target=u'ns4.unit.tests.', ttl=700), call( u'/domain/zone/unit.tests/record', fieldType=u'SSHFP', subDomain=u'', ttl=1100, target=u'1 1 bf6b6825d2977c511a475bbefb88a' u'ad54' u'a92ac73', ), call(u'/domain/zone/unit.tests/record', fieldType=u'AAAA', subDomain=u'', target=u'1:1ec:1::1', ttl=200), call(u'/domain/zone/unit.tests/record', fieldType=u'MX', subDomain=u'', target=u'10 mx1.unit.tests.', ttl=400), call(u'/domain/zone/unit.tests/record', fieldType=u'CNAME', subDomain='www2', target=u'unit.tests.', ttl=300), call( u'/domain/zone/unit.tests/record', fieldType=u'SPF', subDomain=u'', ttl=1000, target=u'v=spf1 include:unit.texts.' u'redirect ~all', ), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain='sub', target=u'1.2.3.4', ttl=200), call(u'/domain/zone/unit.tests/record', fieldType=u'NAPTR', subDomain='naptr', ttl=500, target=u'10 100 "S" "SIP+D2U" "!^.*$!sip:' u'info@bar' u'.example.com!" .'), call(u'/domain/zone/unit.tests/refresh') ] post_mock.assert_has_calls(wanted_calls) # Get for delete calls wanted_get_calls = [ call(u'/domain/zone/unit.tests/record', fieldType=u'TXT', subDomain='txt'), call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM', subDomain='dkim'), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain=u''), call(u'/domain/zone/unit.tests/record', fieldType=u'A', subDomain='fake') ] get_mock.assert_has_calls(wanted_get_calls) # 4 delete calls for update and delete delete_mock.assert_has_calls([ call(u'/domain/zone/unit.tests/record/100'), call(u'/domain/zone/unit.tests/record/101'), call(u'/domain/zone/unit.tests/record/102'), call(u'/domain/zone/unit.tests/record/103') ])
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', str(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', str(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(f'{base}/', text=fh.read()) with open('tests/fixtures/dnsmadeeasy-records.json') as fh: mock.get(f'{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]
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', str(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', str(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(f'{base}/', text=fh.read()) with open('tests/fixtures/dnsmadeeasy-records.json') as fh: mock.get(f'{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_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)
class TestDigitalOceanProvider(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 = DigitalOceanProvider('test', 'token') # Bad auth with requests_mock() as mock: mock.get(ANY, status_code=401, text='{"id":"unauthorized",' '"message":"Unable to authenticate you."}') 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='{"id":"not_found","message":"The resource you ' 'were accessing could not be 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.digitalocean.com/v2/domains/unit.tests/' \ 'records?page=' with open('tests/fixtures/digitalocean-page-1.json') as fh: mock.get('{}{}'.format(base, 1), text=fh.read()) with open('tests/fixtures/digitalocean-page-2.json') as fh: mock.get('{}{}'.format(base, 2), 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): provider = DigitalOceanProvider('test', 'token') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) domain_after_creation = { "domain_records": [{ "id": 11189874, "type": "NS", "name": "@", "data": "ns1.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189875, "type": "NS", "name": "@", "data": "ns2.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189876, "type": "NS", "name": "@", "data": "ns3.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189877, "type": "A", "name": "@", "data": "192.0.2.1", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }], "links": {}, "meta": { "total": 4 } } # non-existent domain, create everything resp.json.side_effect = [ DigitalOceanClientNotFound, # no zone in populate DigitalOceanClientNotFound, # no domain during apply domain_after_creation ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 9 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={ 'ip_address': '192.0.2.1', 'name': 'unit.tests' }), # get all records in newly created zone call('GET', '/domains/unit.tests/records', {'page': 1}), # delete the initial A record call('DELETE', '/domains/unit.tests/records/11189877'), # created at least some of the record with expected data call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.4', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.5', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': 'ca.unit.tests.', 'flags': 0, 'name': '@', 'tag': 'issue', 'ttl': 3600, 'type': 'CAA' }), call('POST', '/domains/unit.tests/records', data={ 'name': '_imap._tcp', 'weight': 0, 'data': '.', 'priority': 0, 'ttl': 600, 'type': 'SRV', 'port': 0 }), call('POST', '/domains/unit.tests/records', data={ 'name': '_pop3._tcp', 'weight': 0, 'data': '.', 'priority': 0, 'ttl': 600, 'type': 'SRV', 'port': 0 }), call('POST', '/domains/unit.tests/records', data={ 'name': '_srv._tcp', 'weight': 20, 'data': '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', 'data': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'data': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'data': '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 delete for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/domains/unit.tests/records', data={ 'data': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/domains/unit.tests/records/11189899'), call('DELETE', '/domains/unit.tests/records/11189897'), call('DELETE', '/domains/unit.tests/records/11189898') ], any_order=True)
def test_apply(self): provider = _get_provider() provider._request = Mock() provider._request.side_effect = [ UltraNoZonesExistException('No Zones'), None, # zone create ] + [None] * 15 # individual record creates # non-existent zone, create everything plan = provider.plan(self.expected) self.assertEquals(15, len(plan.changes)) self.assertEquals(15, provider.apply(plan)) self.assertFalse(plan.exists) provider._request.assert_has_calls( [ # created the domain call('POST', '/v2/zones', json={ 'properties': { 'name': 'unit.tests.', 'accountName': 'testacct', 'type': 'PRIMARY' }, 'primaryCreateInfo': { 'createType': 'NEW' } }), # Validate multi-ip apex A record is correct call('POST', '/v2/zones/unit.tests./rrsets/A/unit.tests.', json={ 'ttl': 300, 'rdata': ['1.2.3.4', '1.2.3.5'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'unit.tests.' } }), # make sure semicolons are not escaped when sending data call('POST', '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.', json={ 'ttl': 600, 'rdata': [ 'Bah bah black sheep', 'have you any wool.', 'v=DKIM1;k=rsa;s=email;h=sha256;' 'p=A/kinda+of/long/string+with+numb3rs' ] }), ], True) # expected number of total calls self.assertEquals(17, provider._request.call_count) # Create sample rrset payload to attempt to alter page1 = json_load(open('tests/fixtures/ultra-records-page-1.json')) page2 = json_load(open('tests/fixtures/ultra-records-page-2.json')) mock_rrsets = list() mock_rrsets.extend(page1['rrSets']) mock_rrsets.extend(page2['rrSets']) # Seed a bunch of records into a zone and verify update / delete ops provider._request.reset_mock() provider._zones = ['octodns1.test.'] provider.zone_records = Mock(return_value=mock_rrsets) provider._request.side_effect = [None] * 13 wanted = Zone('octodns1.test.', []) wanted.add_record( Record.new( wanted, '', { 'ttl': 60, # Change TTL 'type': 'A', 'value': '5.6.7.8' # Change number of IPs (3 -> 1) })) wanted.add_record( Record.new( wanted, 'txt', { 'ttl': 3600, 'type': 'TXT', 'values': [ # Alter TXT value "foobar", "v=spf1 include:mail.server.net ?all" ] })) plan = provider.plan(wanted) self.assertEquals(11, len(plan.changes)) self.assertEquals(11, provider.apply(plan)) self.assertTrue(plan.exists) provider._request.assert_has_calls( [ # Validate multi-ip apex A record replaced with standard A call('PUT', '/v2/zones/octodns1.test./rrsets/A/octodns1.test.', json={ 'ttl': 60, 'rdata': ['5.6.7.8'] }), # Make sure TXT value is properly updated call('PUT', '/v2/zones/octodns1.test./rrsets/TXT/txt.octodns1.test.', json={ 'ttl': 3600, 'rdata': ["foobar", "v=spf1 include:mail.server.net ?all"] }), # Confirm a few of the DELETE operations properly occur call('DELETE', '/v2/zones/octodns1.test./rrsets/A/a.octodns1.test.', json_response=False), call( 'DELETE', '/v2/zones/octodns1.test./rrsets/AAAA/aaaa.octodns1.test.', json_response=False), call('DELETE', '/v2/zones/octodns1.test./rrsets/CAA/caa.octodns1.test.', json_response=False), call( 'DELETE', '/v2/zones/octodns1.test./rrsets/CNAME/cname.octodns1.test.', json_response=False), ], True)
def test_populate(self, load_mock): provider = Ns1Provider('test', 'api-key') # Bad auth load_mock.side_effect = AuthException('unauthorized') zone = Zone('unit.tests.', []) with self.assertRaises(AuthException) as ctx: provider.populate(zone) self.assertEquals(load_mock.side_effect, ctx.exception) # General error load_mock.reset_mock() load_mock.side_effect = ResourceException('boom') zone = Zone('unit.tests.', []) with self.assertRaises(ResourceException) as ctx: provider.populate(zone) self.assertEquals(load_mock.side_effect, ctx.exception) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) # Non-existent zone doesn't populate anything load_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') zone = Zone('unit.tests.', []) exists = provider.populate(zone) self.assertEquals(set(), zone.records) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) self.assertFalse(exists) # Existing zone w/o records load_mock.reset_mock() nsone_zone = DummyZone([]) load_mock.side_effect = [nsone_zone] zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(1, len(zone.records)) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) # Existing zone w/records load_mock.reset_mock() nsone_zone = DummyZone(self.nsone_records) load_mock.side_effect = [nsone_zone] zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(self.expected, zone.records) self.assertEquals(('unit.tests', ), load_mock.call_args[0]) # Test skipping unsupported record type load_mock.reset_mock() nsone_zone = DummyZone(self.nsone_records + [{ 'type': 'UNSUPPORTED', 'ttl': 42, 'short_answers': ['unsupported'], 'domain': 'unsupported.unit.tests.', }]) load_mock.side_effect = [nsone_zone] zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals(self.expected, zone.records) self.assertEquals(('unit.tests', ), load_mock.call_args[0])
def test_gen_data(self): provider = _get_provider() zone = Zone('unit.tests.', []) for name, _type, expected_path, expected_payload, expected_record in ( # A ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', { 'ttl': 60, 'rdata': ['1.2.3.4'] }, Record.new(zone, 'a', { 'ttl': 60, 'type': 'A', 'values': ['1.2.3.4'] })), ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', { 'ttl': 60, 'rdata': ['1.2.3.4', '5.6.7.8'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'a.unit.tests.' } }, Record.new(zone, 'a', { 'ttl': 60, 'type': 'A', 'values': ['1.2.3.4', '5.6.7.8'] })), # AAAA ('aaaa', 'AAAA', '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', { 'ttl': 60, 'rdata': ['::1'] }, Record.new(zone, 'aaaa', { 'ttl': 60, 'type': 'AAAA', 'values': ['::1'] })), ('aaaa', 'AAAA', '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', { 'ttl': 60, 'rdata': ['::1', '::2'], 'profile': { '@context': 'http://schemas.ultradns.com/RDPool.jsonschema', 'order': 'FIXED', 'description': 'aaaa.unit.tests.' } }, Record.new(zone, 'aaaa', { 'ttl': 60, 'type': 'AAAA', 'values': ['::1', '::2'] })), # CAA ('caa', 'CAA', '/v2/zones/unit.tests./rrsets/CAA/caa.unit.tests.', { 'ttl': 60, 'rdata': ['0 issue foo.com'] }, Record.new( zone, 'caa', { 'ttl': 60, 'type': 'CAA', 'values': [{ 'flags': 0, 'tag': 'issue', 'value': 'foo.com' }] })), # CNAME ('cname', 'CNAME', '/v2/zones/unit.tests./rrsets/CNAME/cname.unit.tests.', { 'ttl': 60, 'rdata': ['netflix.com.'] }, Record.new(zone, 'cname', { 'ttl': 60, 'type': 'CNAME', 'value': 'netflix.com.' })), # MX ('mx', 'MX', '/v2/zones/unit.tests./rrsets/MX/mx.unit.tests.', { 'ttl': 60, 'rdata': ['1 mx1.unit.tests.', '1 mx2.unit.tests.'] }, Record.new( zone, 'mx', { 'ttl': 60, 'type': 'MX', 'values': [{ 'preference': 1, 'exchange': 'mx1.unit.tests.' }, { 'preference': 1, 'exchange': 'mx2.unit.tests.' }] })), # NS ('ns', 'NS', '/v2/zones/unit.tests./rrsets/NS/ns.unit.tests.', { 'ttl': 60, 'rdata': ['ns1.unit.tests.', 'ns2.unit.tests.'] }, Record.new( zone, 'ns', { 'ttl': 60, 'type': 'NS', 'values': ['ns1.unit.tests.', 'ns2.unit.tests.'] })), # PTR ('ptr', 'PTR', '/v2/zones/unit.tests./rrsets/PTR/ptr.unit.tests.', { 'ttl': 60, 'rdata': ['a.unit.tests.'] }, Record.new(zone, 'ptr', { 'ttl': 60, 'type': 'PTR', 'value': 'a.unit.tests.' })), # SPF ('spf', 'SPF', '/v2/zones/unit.tests./rrsets/SPF/spf.unit.tests.', { 'ttl': 60, 'rdata': ['v=spf1 -all'] }, Record.new(zone, 'spf', { 'ttl': 60, 'type': 'SPF', 'values': ['v=spf1 -all'] })), # SRV ('_srv._tcp', 'SRV', '/v2/zones/unit.tests./rrsets/SRV/_srv._tcp.unit.tests.', { 'ttl': 60, 'rdata': ['10 20 443 target.unit.tests.'] }, Record.new( zone, '_srv._tcp', { 'ttl': 60, 'type': 'SRV', 'values': [{ 'priority': 10, 'weight': 20, 'port': 443, 'target': 'target.unit.tests.' }] })), # TXT ('txt', 'TXT', '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.', { 'ttl': 60, 'rdata': ['abc', 'def'] }, Record.new(zone, 'txt', { 'ttl': 60, 'type': 'TXT', 'values': ['abc', 'def'] })), # ALIAS ('', 'ALIAS', '/v2/zones/unit.tests./rrsets/APEXALIAS/unit.tests.', { 'ttl': 60, 'rdata': ['target.unit.tests.'] }, Record.new(zone, '', { 'ttl': 60, 'type': 'ALIAS', 'value': 'target.unit.tests.' })), ): # Validate path and payload based on record meet expectations path, payload = provider._gen_data(expected_record) self.assertEqual(expected_path, path) self.assertEqual(expected_payload, payload) # Use generator for record and confirm the output matches rec = provider._record_for(zone, name, _type, expected_payload, False) path, payload = provider._gen_data(rec) self.assertEqual(expected_path, path) self.assertEqual(expected_payload, payload)
def test_sync(self, load_mock, create_mock): provider = Ns1Provider('test', 'api-key') desired = Zone('unit.tests.', []) for r in self.expected: desired.add_record(r) plan = provider.plan(desired) # everything except the root NS expected_n = len(self.expected) - 1 self.assertEquals(expected_n, len(plan.changes)) self.assertTrue(plan.exists) # Fails, general error load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = ResourceException('boom') with self.assertRaises(ResourceException) as ctx: provider.apply(plan) self.assertEquals(load_mock.side_effect, ctx.exception) # Fails, bad auth load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') create_mock.side_effect = AuthException('unauthorized') with self.assertRaises(AuthException) as ctx: provider.apply(plan) self.assertEquals(create_mock.side_effect, ctx.exception) # non-existent zone, create load_mock.reset_mock() create_mock.reset_mock() load_mock.side_effect = \ ResourceException('server error: zone not found') # ugh, need a mock zone with a mock prop since we're using getattr, we # can actually control side effects on `meth` with that. mock_zone = Mock() mock_zone.add_SRV = Mock() mock_zone.add_SRV.side_effect = [ RateLimitException('boo', period=0), None, ] create_mock.side_effect = [mock_zone] got_n = provider.apply(plan) self.assertEquals(expected_n, got_n) # Update & delete load_mock.reset_mock() create_mock.reset_mock() nsone_zone = DummyZone(self.nsone_records + [{ 'type': 'A', 'ttl': 42, 'short_answers': ['9.9.9.9'], 'domain': 'delete-me.unit.tests.', }]) nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2' nsone_zone.loadRecord = Mock() zone_search = Mock() zone_search.return_value = [ { "domain": "geo.unit.tests", "zone": "unit.tests", "type": "A", "answers": [ { 'answer': ['1.1.1.1'], 'meta': {} }, { 'answer': ['1.2.3.4'], 'meta': { 'ca_province': ['ON'] } }, { 'answer': ['2.3.4.5'], 'meta': { 'us_state': ['NY'] } }, { 'answer': ['3.4.5.6'], 'meta': { 'country': ['US'] } }, { 'answer': ['4.5.6.7'], 'meta': { 'iso_region_code': ['NA-US-WA'] } }, ], 'ttl': 34, }, ] nsone_zone.search = zone_search load_mock.side_effect = [nsone_zone, nsone_zone] plan = provider.plan(desired) self.assertEquals(3, len(plan.changes)) self.assertIsInstance(plan.changes[0], Update) self.assertIsInstance(plan.changes[2], Delete) # ugh, we need a mock record that can be returned from loadRecord for # the update and delete targets, we can add our side effects to that to # trigger rate limit handling mock_record = Mock() mock_record.update.side_effect = [ RateLimitException('one', period=0), None, None, ] mock_record.delete.side_effect = [ RateLimitException('two', period=0), None, None, ] nsone_zone.loadRecord.side_effect = [ mock_record, mock_record, mock_record ] got_n = provider.apply(plan) self.assertEquals(3, got_n) nsone_zone.loadRecord.assert_has_calls([ call('unit.tests', u'A'), call('geo', u'A'), call('delete-me', u'A'), ]) mock_record.assert_has_calls([ call.update(answers=[{ 'answer': [u'1.2.3.4'], 'meta': {} }], filters=[], ttl=32), call.update(answers=[{ u'answer': [u'1.2.3.4'], u'meta': {} }], filters=[], ttl=32), call.update(answers=[ { u'answer': [u'101.102.103.104'], u'meta': {} }, { u'answer': [u'101.102.103.105'], u'meta': {} }, { u'answer': [u'201.202.203.204'], u'meta': { u'iso_region_code': [u'NA-US-NY'] }, }, ], filters=[ { u'filter': u'shuffle', u'config': {} }, { u'filter': u'geotarget_country', u'config': {} }, { u'filter': u'select_first_n', u'config': { u'N': 1 } }, ], ttl=34), call.delete(), call.delete() ])
def make_expected(self): expected = Zone('unit.tests.', []) source = YamlProvider('test', join(dirname(__file__), 'config')) source.populate(expected) return expected
def test_lowering(self): zone = Zone('UniT.TEsTs.', []) self.assertEquals('unit.tests.', zone.name)
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_cname_coexisting(self): zone = Zone('unit.tests.', []) a = Record.new(zone, 'www', { 'ttl': 60, 'type': 'A', 'value': '9.9.9.9', }) cname = Record.new(zone, 'www', { 'ttl': 60, 'type': 'CNAME', 'value': 'foo.bar.com.', }) # add cname to a zone.add_record(a) with self.assertRaises(InvalidNodeException): zone.add_record(cname) # add a to cname zone = Zone('unit.tests.', []) zone.add_record(cname) with self.assertRaises(InvalidNodeException): zone.add_record(a)
from octodns.record import Create, Delete, Record from octodns.provider.azuredns import _AzureRecord, AzureProvider, \ _check_endswith_dot, _parse_azure_type from octodns.zone import Zone from octodns.provider.base import Plan from azure.mgmt.dns.models import ARecord, AaaaRecord, CaaRecord, \ CnameRecord, MxRecord, SrvRecord, NsRecord, PtrRecord, TxtRecord, \ RecordSet, SoaRecord, Zone as AzureZone from msrestazure.azure_exceptions import CloudError from unittest import TestCase from mock import Mock, patch 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_provider(self): source = SplitYamlProvider('test', join(dirname(__file__), 'config/split')) zone = Zone('unit.tests.', []) dynamic_zone = Zone('dynamic.tests.', []) # With target we don't add anything source.populate(zone, target=source) self.assertEquals(0, len(zone.records)) # without it we see everything source.populate(zone) self.assertEquals(18, len(zone.records)) source.populate(dynamic_zone) self.assertEquals(5, len(dynamic_zone.records)) with TemporaryDirectory() as td: # Add some subdirs to make sure that it can create them directory = join(td.dirname, 'sub', 'dir') zone_dir = join(directory, 'unit.tests.') dynamic_zone_dir = join(directory, 'dynamic.tests.') target = SplitYamlProvider('test', directory) # We add everything plan = target.plan(zone) self.assertEquals( 15, len([c for c in plan.changes if isinstance(c, Create)])) self.assertFalse(isdir(zone_dir)) # Now actually do it self.assertEquals(15, target.apply(plan)) # Dynamic plan plan = target.plan(dynamic_zone) self.assertEquals( 5, len([c for c in plan.changes if isinstance(c, Create)])) self.assertFalse(isdir(dynamic_zone_dir)) # Apply it self.assertEquals(5, target.apply(plan)) self.assertTrue(isdir(dynamic_zone_dir)) # There should be no changes after the round trip reloaded = Zone('unit.tests.', []) target.populate(reloaded) self.assertDictEqual({'included': ['test']}, [ x for x in reloaded.records if x.name == 'included' ][0]._octodns) self.assertFalse(zone.changes(reloaded, target=source)) # A 2nd sync should still create everything plan = target.plan(zone) self.assertEquals( 15, len([c for c in plan.changes if isinstance(c, Create)])) yaml_file = join(zone_dir, '$unit.tests.yaml') self.assertTrue(isfile(yaml_file)) with open(yaml_file) as fh: data = safe_load(fh.read()) roots = sorted(data.pop(''), key=lambda r: r['type']) self.assertTrue('values' in roots[0]) # A self.assertTrue('geo' in roots[0]) # geo made the trip self.assertTrue('value' in roots[1]) # CAA self.assertTrue('values' in roots[2]) # SSHFP # These records are stored as plural "values." Check each file to # ensure correctness. for record_name in ('_srv._tcp', 'mx', 'naptr', 'sub', 'txt'): yaml_file = join(zone_dir, '{}.yaml'.format(record_name)) self.assertTrue(isfile(yaml_file)) with open(yaml_file) as fh: data = safe_load(fh.read()) self.assertTrue('values' in data.pop(record_name)) # These are stored as singular "value." Again, check each file. for record_name in ('aaaa', 'cname', 'included', 'ptr', 'spf', 'www.sub', 'www'): yaml_file = join(zone_dir, '{}.yaml'.format(record_name)) self.assertTrue(isfile(yaml_file)) with open(yaml_file) as fh: data = safe_load(fh.read()) self.assertTrue('value' in data.pop(record_name)) # Again with the plural, this time checking dynamic.tests. for record_name in ('a', 'aaaa', 'real-ish-a'): yaml_file = join(dynamic_zone_dir, '{}.yaml'.format(record_name)) self.assertTrue(isfile(yaml_file)) with open(yaml_file) as fh: data = safe_load(fh.read()) dyna = data.pop(record_name) self.assertTrue('values' in dyna) self.assertTrue('dynamic' in dyna) # Singular again. for record_name in ('cname', 'simple-weighted'): yaml_file = join(dynamic_zone_dir, '{}.yaml'.format(record_name)) self.assertTrue(isfile(yaml_file)) with open(yaml_file) as fh: data = safe_load(fh.read()) dyna = data.pop(record_name) self.assertTrue('value' in dyna) self.assertTrue('dynamic' in dyna)
def main(): parser = ArgumentParser(description=__doc__.split('\n')[1]) parser.add_argument('--config-file', required=True, help='The Manager configuration file to use') parser.add_argument('--zone', required=True, help='Zone to dump') parser.add_argument('--source', required=True, default=[], action='append', help='Source(s) to pull data from') parser.add_argument('--num-workers', default=4, help='Number of background workers') parser.add_argument('--timeout', default=1, help='Number seconds to wait for an answer') parser.add_argument('server', nargs='+', help='Servers to query') args = parser.parse_args() manager = Manager(args.config_file) log = getLogger('report') try: sources = [manager.providers[source] for source in args.source] except KeyError as e: raise Exception('Unknown source: {}'.format(e.args[0])) zone = Zone(args.zone, manager.configured_sub_zones(args.zone)) for source in sources: source.populate(zone) print('name,type,ttl,{},consistent'.format(','.join(args.server))) resolvers = [] ip_addr_re = re.compile(r'^[\d\.]+$') for server in args.server: resolver = AsyncResolver(configure=False, num_workers=int(args.num_workers)) if not ip_addr_re.match(server): server = str(query(server, 'A')[0]) log.info('server=%s', server) resolver.nameservers = [server] resolver.lifetime = int(args.timeout) resolvers.append(resolver) queries = {} for record in sorted(zone.records): queries[record] = [r.query(record.fqdn, record._type) for r in resolvers] for record, futures in sorted(queries.items(), key=lambda d: d[0]): stdout.write(record.fqdn) stdout.write(',') stdout.write(record._type) stdout.write(',') stdout.write(str(record.ttl)) compare = {} for future in futures: stdout.write(',') try: answers = [str(r) for r in future.result()] except (NoAnswer, NoNameservers): answers = ['*no answer*'] except NXDOMAIN: answers = ['*does not exist*'] except Timeout: answers = ['*timeout*'] stdout.write(' '.join(answers)) # sorting to ignore order answers = '*:*'.join(sorted(answers)).lower() compare[answers] = True stdout.write(',True\n' if len(compare) == 1 else ',False\n')
# # # from __future__ import absolute_import, division, print_function, \ unicode_literals from unittest import TestCase from octodns.processor.filter import TypeAllowlistFilter, TypeRejectlistFilter from octodns.record import Record from octodns.zone import Zone zone = Zone('unit.tests.', []) for record in [ Record.new(zone, 'a', { 'ttl': 30, 'type': 'A', 'value': '1.2.3.4', }), Record.new(zone, 'aaaa', { 'ttl': 30, 'type': 'AAAA', 'value': '::1', }), Record.new(zone, 'txt', { 'ttl': 30, 'type': 'TXT', 'value': 'Hello World!', }), Record.new(zone, 'a2', {
def test_apply(self): provider = DigitalOceanProvider('test', 'token') resp = Mock() resp.json = Mock() provider._client._request = Mock(return_value=resp) domain_after_creation = { "domain_records": [{ "id": 11189874, "type": "NS", "name": "@", "data": "ns1.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189875, "type": "NS", "name": "@", "data": "ns2.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189876, "type": "NS", "name": "@", "data": "ns3.digitalocean.com", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }, { "id": 11189877, "type": "A", "name": "@", "data": "192.0.2.1", "priority": None, "port": None, "ttl": 3600, "weight": None, "flags": None, "tag": None }], "links": {}, "meta": { "total": 4 } } # non-existent domain, create everything resp.json.side_effect = [ DigitalOceanClientNotFound, # no zone in populate DigitalOceanClientNotFound, # no domain during apply domain_after_creation ] plan = provider.plan(self.expected) # No root NS, no ignored, no excluded, no unsupported n = len(self.expected.records) - 9 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={ 'ip_address': '192.0.2.1', 'name': 'unit.tests' }), # get all records in newly created zone call('GET', '/domains/unit.tests/records', {'page': 1}), # delete the initial A record call('DELETE', '/domains/unit.tests/records/11189877'), # created at least some of the record with expected data call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.4', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': '1.2.3.5', 'name': '@', 'ttl': 300, 'type': 'A' }), call('POST', '/domains/unit.tests/records', data={ 'data': 'ca.unit.tests.', 'flags': 0, 'name': '@', 'tag': 'issue', 'ttl': 3600, 'type': 'CAA' }), call('POST', '/domains/unit.tests/records', data={ 'name': '_imap._tcp', 'weight': 0, 'data': '.', 'priority': 0, 'ttl': 600, 'type': 'SRV', 'port': 0 }), call('POST', '/domains/unit.tests/records', data={ 'name': '_pop3._tcp', 'weight': 0, 'data': '.', 'priority': 0, 'ttl': 600, 'type': 'SRV', 'port': 0 }), call('POST', '/domains/unit.tests/records', data={ 'name': '_srv._tcp', 'weight': 20, 'data': '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', 'data': '1.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189898, 'name': 'www', 'data': '2.2.3.4', 'ttl': 300, 'type': 'A', }, { 'id': 11189899, 'name': 'ttl', 'data': '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 delete for the 2 parts of the other provider._client._request.assert_has_calls([ call('POST', '/domains/unit.tests/records', data={ 'data': '3.2.3.4', 'type': 'A', 'name': 'ttl', 'ttl': 300 }), call('DELETE', '/domains/unit.tests/records/11189899'), call('DELETE', '/domains/unit.tests/records/11189897'), call('DELETE', '/domains/unit.tests/records/11189898') ], any_order=True)
def test_populate_normal(self): got = Zone('example.com.', []) self.source.populate(got) self.assertEquals(11, len(got.records)) expected = Zone('example.com.', []) for name, data in ( ('', { 'type': 'A', 'ttl': 30, 'values': ['10.2.3.4', '10.2.3.5'], }), ('sub', { 'type': 'NS', 'ttl': 30, 'values': ['ns1.ns.com.', 'ns2.ns.com.'], }), ('www', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.6', }), ('cname', { 'type': 'CNAME', 'ttl': 3600, 'value': 'www.example.com.', }), ('some-host-abc123', { 'type': 'A', 'ttl': 1800, 'value': '10.2.3.7', }), ('has-dup-def123', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.8', }), ('www.sub', { 'type': 'A', 'ttl': 3600, 'value': '1.2.3.4', }), ('has-dup-def456', { 'type': 'A', 'ttl': 3600, 'value': '10.2.3.8', }), ('', { 'type': 'MX', 'ttl': 3600, 'values': [{ 'preference': 10, 'exchange': 'smtp-1-host.example.com.', }, { 'preference': 20, 'exchange': 'smtp-2-host.example.com.', }] }), ('smtp', { 'type': 'MX', 'ttl': 1800, 'values': [{ 'preference': 30, 'exchange': 'smtp-1-host.example.com.', }, { 'preference': 40, 'exchange': 'smtp-2-host.example.com.', }] }), ): record = Record.new(expected, name, data) expected.add_record(record) changes = expected.changes(got, SimpleProvider()) self.assertEquals([], changes)
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.records.remove(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') ])