From f457f539385def2bc1d8e910c94a0d187bafb77e Mon Sep 17 00:00:00 2001 From: Adam Mielke Date: Wed, 22 Apr 2020 15:36:41 -0700 Subject: [PATCH] Support PowerDNS 4.3.x --- octodns/provider/powerdns.py | 44 ++++++++++++++++--------- tests/test_octodns_provider_powerdns.py | 42 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/octodns/provider/powerdns.py b/octodns/provider/powerdns.py index 8d75163..847e292 100644 --- a/octodns/provider/powerdns.py +++ b/octodns/provider/powerdns.py @@ -19,12 +19,13 @@ class PowerDnsBaseProvider(BaseProvider): 'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT')) TIMEOUT = 5 - def __init__(self, id, host, api_key, port=8081, scheme="http", - timeout=TIMEOUT, *args, **kwargs): + def __init__(self, id, host, api_key, soa_edit_api, port=8081, + scheme="http", timeout=TIMEOUT, *args, **kwargs): super(PowerDnsBaseProvider, self).__init__(id, *args, **kwargs) self.host = host self.port = port + self.soa_edit_api = soa_edit_api self.scheme = scheme self.timeout = timeout @@ -178,10 +179,10 @@ class PowerDnsBaseProvider(BaseProvider): # Nicer error message for auth problems raise Exception('PowerDNS unauthorized host={}' .format(self.host)) - elif e.response.status_code == 422: - # 422 means powerdns doesn't know anything about the requested - # domain. We'll just ignore it here and leave the zone - # untouched. + elif e.response.status_code in (404, 422): + # 404 or 422 means powerdns doesn't know anything about the + # requested domain. We'll just ignore it here and leave the + # zone untouched. pass else: # just re-throw @@ -338,23 +339,25 @@ class PowerDnsBaseProvider(BaseProvider): self.log.debug('_apply: patched') except HTTPError as e: error = self._get_error(e) - if e.response.status_code != 422 or \ - not error.startswith('Could not find domain '): + if e.response.status_code != 404 and \ + not (e.response.status_code == 422 and + error.startswith('Could not find domain ')): self.log.error('_apply: status=%d, text=%s', e.response.status_code, e.response.text) raise self.log.info('_apply: creating zone=%s', desired.name) - # 422 means powerdns doesn't know anything about the requested - # domain. We'll try to create it with the correct records instead - # of update. Hopefully all the mods are creates :-) + # 404 or 422 means powerdns doesn't know anything about the + # requested domain. We'll try to create it with the correct + # records instead of update. Hopefully all the mods are + # creates :-) data = { 'name': desired.name, 'kind': 'Master', 'masters': [], 'nameservers': [], 'rrsets': mods, - 'soa_edit_api': 'INCEPTION-INCREMENT', + 'soa_edit_api': self.soa_edit_api, 'serial': 0, } try: @@ -388,16 +391,25 @@ class PowerDnsProvider(PowerDnsBaseProvider): - 1.2.3.5. # The nameserver record TTL when managed, (optional, default 600) nameserver_ttl: 600 + # The SOA-EDIT-API value to set for newly created zones (optional) + # Defaults to INCEPTION-INCREMENT. PowerDNS >=4.3.x users should use + # 'DEFAULT' instead, as INCEPTION-INCREMENT is no longer a valid value. + soa_edit_api: INCEPTION-INCREMENT ''' def __init__(self, id, host, api_key, port=8081, nameserver_values=None, - nameserver_ttl=600, *args, **kwargs): + nameserver_ttl=600, soa_edit_api='INCEPTION-INCREMENT', + *args, **kwargs): self.log = logging.getLogger('PowerDnsProvider[{}]'.format(id)) self.log.debug('__init__: id=%s, host=%s, port=%d, ' - 'nameserver_values=%s, nameserver_ttl=%d', - id, host, port, nameserver_values, nameserver_ttl) + 'nameserver_values=%s, nameserver_ttl=%d' + 'soa_edit_api=%s', + id, host, port, nameserver_values, nameserver_ttl, + soa_edit_api) super(PowerDnsProvider, self).__init__(id, host=host, api_key=api_key, - port=port, *args, **kwargs) + port=port, + soa_edit_api=soa_edit_api, + *args, **kwargs) self.nameserver_values = nameserver_values self.nameserver_ttl = nameserver_ttl diff --git a/tests/test_octodns_provider_powerdns.py b/tests/test_octodns_provider_powerdns.py index 6baee6c..7223617 100644 --- a/tests/test_octodns_provider_powerdns.py +++ b/tests/test_octodns_provider_powerdns.py @@ -64,7 +64,7 @@ class TestPowerDnsProvider(TestCase): provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) - # Non-existent zone doesn't populate anything + # Non-existent zone in PowerDNS <4.3.0 doesn't populate anything with requests_mock() as mock: mock.get(ANY, status_code=422, json={'error': "Could not find domain 'unit.tests.'"}) @@ -73,6 +73,14 @@ class TestPowerDnsProvider(TestCase): provider.populate(zone) self.assertEquals(set(), zone.records) + # Non-existent zone in PowerDNS >=4.3.0 doesn't populate anything + with requests_mock() as mock: + mock.get(ANY, status_code=404, text='Not Found') + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(set(), zone.records) + # The rest of this is messy/complicated b/c it's dealing with mocking expected = Zone('unit.tests.', []) @@ -127,6 +135,19 @@ class TestPowerDnsProvider(TestCase): self.assertEquals(expected_n, provider.apply(plan)) self.assertFalse(plan.exists) + with requests_mock() as mock: + # get 404's, unknown zone + mock.get(ANY, status_code=404, text='') + # patch 404's, unknown zone + mock.patch(ANY, status_code=404, text=dumps(not_found)) + # post 201, is response to the create with data + mock.post(ANY, status_code=201, text=assert_rrsets_callback) + + plan = provider.plan(expected) + self.assertEquals(expected_n, len(plan.changes)) + self.assertEquals(expected_n, provider.apply(plan)) + self.assertFalse(plan.exists) + with requests_mock() as mock: # get 422's, unknown zone mock.get(ANY, status_code=422, text='') @@ -291,3 +312,22 @@ class TestPowerDnsProvider(TestCase): plan = provider.plan(expected) self.assertEquals(1, len(plan.changes)) + + def test_soa_edit_api(self): + provider = PowerDnsProvider('test', 'non.existent', 'api-key', + soa_edit_api='DEFAULT') + + def assert_soa_edit_api_callback(request, context): + data = loads(request.body) + self.assertEquals('DEFAULT', data['soa_edit_api']) + return '' + + with requests_mock() as mock: + mock.get(ANY, status_code=404) + mock.patch(ANY, status_code=404) + mock.post(ANY, status_code=204, text=assert_soa_edit_api_callback) + zone = Zone('unit.tests.', []) + source = YamlProvider('test', join(dirname(__file__), 'config')) + source.populate(zone) + plan = provider.plan(zone) + provider.apply(plan)