mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge pull request #540 from acm1/powerdns-4.3.x-support
Support PowerDNS 4.3.x
This commit is contained in:
@@ -19,8 +19,8 @@ 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, port=8081,
|
||||
scheme="http", timeout=TIMEOUT, *args, **kwargs):
|
||||
super(PowerDnsBaseProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
self.host = host
|
||||
@@ -28,6 +28,8 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
self.scheme = scheme
|
||||
self.timeout = timeout
|
||||
|
||||
self._powerdns_version = None
|
||||
|
||||
sess = Session()
|
||||
sess.headers.update({'X-API-Key': api_key})
|
||||
self._sess = sess
|
||||
@@ -36,7 +38,8 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
self.log.debug('_request: method=%s, path=%s', method, path)
|
||||
|
||||
url = '{}://{}:{}/api/v1/servers/localhost/{}' \
|
||||
.format(self.scheme, self.host, self.port, path)
|
||||
.format(self.scheme, self.host, self.port, path).rstrip("/")
|
||||
# Strip trailing / from url.
|
||||
resp = self._sess.request(method, url, json=data, timeout=self.timeout)
|
||||
self.log.debug('_request: status=%d', resp.status_code)
|
||||
resp.raise_for_status()
|
||||
@@ -165,6 +168,42 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
'ttl': rrset['ttl']
|
||||
}
|
||||
|
||||
@property
|
||||
def powerdns_version(self):
|
||||
if self._powerdns_version is None:
|
||||
try:
|
||||
resp = self._get('')
|
||||
except HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
# Nicer error message for auth problems
|
||||
raise Exception('PowerDNS unauthorized host={}'
|
||||
.format(self.host))
|
||||
raise
|
||||
|
||||
version = resp.json()['version']
|
||||
self.log.debug('powerdns_version: got version %s from server',
|
||||
version)
|
||||
self._powerdns_version = [int(p) for p in version.split('.')]
|
||||
|
||||
return self._powerdns_version
|
||||
|
||||
@property
|
||||
def soa_edit_api(self):
|
||||
# >>> [4, 4, 3] >= [4, 3]
|
||||
# True
|
||||
# >>> [4, 3, 3] >= [4, 3]
|
||||
# True
|
||||
# >>> [4, 1, 3] >= [4, 3]
|
||||
# False
|
||||
if self.powerdns_version >= [4, 3]:
|
||||
return 'DEFAULT'
|
||||
return 'INCEPTION-INCREMENT'
|
||||
|
||||
@property
|
||||
def check_status_not_found(self):
|
||||
# >=4.2.x returns 404 when not found
|
||||
return self.powerdns_version >= [4, 2]
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
@@ -174,11 +213,20 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
resp = self._get('zones/{}'.format(zone.name))
|
||||
self.log.debug('populate: loaded')
|
||||
except HTTPError as e:
|
||||
error = self._get_error(e)
|
||||
if e.response.status_code == 401:
|
||||
# Nicer error message for auth problems
|
||||
raise Exception('PowerDNS unauthorized host={}'
|
||||
.format(self.host))
|
||||
elif e.response.status_code == 422:
|
||||
elif e.response.status_code == 404 \
|
||||
and self.check_status_not_found:
|
||||
# 404 means powerdns doesn't know anything about the requested
|
||||
# domain. We'll just ignore it here and leave the zone
|
||||
# untouched.
|
||||
pass
|
||||
elif e.response.status_code == 422 \
|
||||
and error.startswith('Could not find domain ') \
|
||||
and not self.check_status_not_found:
|
||||
# 422 means powerdns doesn't know anything about the requested
|
||||
# domain. We'll just ignore it here and leave the zone
|
||||
# untouched.
|
||||
@@ -338,23 +386,34 @@ 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 '):
|
||||
self.log.error('_apply: status=%d, text=%s',
|
||||
e.response.status_code,
|
||||
e.response.text)
|
||||
if not (
|
||||
(
|
||||
e.response.status_code == 404 and
|
||||
self.check_status_not_found
|
||||
) or (
|
||||
e.response.status_code == 422 and
|
||||
error.startswith('Could not find domain ') and
|
||||
not self.check_status_not_found
|
||||
)
|
||||
):
|
||||
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:
|
||||
@@ -391,13 +450,15 @@ class PowerDnsProvider(PowerDnsBaseProvider):
|
||||
'''
|
||||
|
||||
def __init__(self, id, host, api_key, port=8081, nameserver_values=None,
|
||||
nameserver_ttl=600, *args, **kwargs):
|
||||
nameserver_ttl=600,
|
||||
*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)
|
||||
super(PowerDnsProvider, self).__init__(id, host=host, api_key=api_key,
|
||||
port=port, *args, **kwargs)
|
||||
port=port,
|
||||
*args, **kwargs)
|
||||
|
||||
self.nameserver_values = nameserver_values
|
||||
self.nameserver_ttl = nameserver_ttl
|
||||
|
@@ -41,11 +41,94 @@ with open('./tests/fixtures/powerdns-full-data.json') as fh:
|
||||
|
||||
class TestPowerDnsProvider(TestCase):
|
||||
|
||||
def test_provider_version_detection(self):
|
||||
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
|
||||
nameserver_values=['8.8.8.8.',
|
||||
'9.9.9.9.'])
|
||||
# Bad auth
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=401, text='Unauthorized')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider.powerdns_version
|
||||
self.assertTrue('unauthorized' in text_type(ctx.exception))
|
||||
|
||||
# Api not found
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404, text='Not Found')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider.powerdns_version
|
||||
self.assertTrue('404' in text_type(ctx.exception))
|
||||
|
||||
# Test version detection
|
||||
with requests_mock() as mock:
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': "4.1.10"})
|
||||
self.assertEquals(provider.powerdns_version, [4, 1, 10])
|
||||
|
||||
# Test version detection for second time (should stay at 4.1.10)
|
||||
with requests_mock() as mock:
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': "4.2.0"})
|
||||
self.assertEquals(provider.powerdns_version, [4, 1, 10])
|
||||
|
||||
# Test version detection
|
||||
with requests_mock() as mock:
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': "4.2.0"})
|
||||
|
||||
# Reset version, so detection will try again
|
||||
provider._powerdns_version = None
|
||||
self.assertNotEquals(provider.powerdns_version, [4, 1, 10])
|
||||
|
||||
def test_provider_version_config(self):
|
||||
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
|
||||
nameserver_values=['8.8.8.8.',
|
||||
'9.9.9.9.'])
|
||||
|
||||
# Test version 4.1.0
|
||||
provider._powerdns_version = None
|
||||
with requests_mock() as mock:
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': "4.1.10"})
|
||||
self.assertEquals(provider.soa_edit_api, 'INCEPTION-INCREMENT')
|
||||
self.assertFalse(
|
||||
provider.check_status_not_found,
|
||||
'check_status_not_found should be false '
|
||||
'for version 4.1.x and below')
|
||||
|
||||
# Test version 4.2.0
|
||||
provider._powerdns_version = None
|
||||
with requests_mock() as mock:
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': "4.2.0"})
|
||||
self.assertEquals(provider.soa_edit_api, 'INCEPTION-INCREMENT')
|
||||
self.assertTrue(
|
||||
provider.check_status_not_found,
|
||||
'check_status_not_found should be true for version 4.2.x')
|
||||
|
||||
# Test version 4.3.0
|
||||
provider._powerdns_version = None
|
||||
with requests_mock() as mock:
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': "4.3.0"})
|
||||
self.assertEquals(provider.soa_edit_api, 'DEFAULT')
|
||||
self.assertTrue(
|
||||
provider.check_status_not_found,
|
||||
'check_status_not_found should be true for version 4.3.x')
|
||||
|
||||
def test_provider(self):
|
||||
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
|
||||
nameserver_values=['8.8.8.8.',
|
||||
'9.9.9.9.'])
|
||||
|
||||
# Test version detection
|
||||
with requests_mock() as mock:
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': "4.1.10"})
|
||||
self.assertEquals(provider.powerdns_version, [4, 1, 10])
|
||||
|
||||
# Bad auth
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=401, text='Unauthorized')
|
||||
@@ -64,15 +147,25 @@ 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.'"})
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
|
||||
# Non-existent zone in PowerDNS >=4.2.0 doesn't populate anything
|
||||
|
||||
provider._powerdns_version = [4, 2, 0]
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404, text='Not Found')
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
|
||||
provider._powerdns_version = [4, 1, 0]
|
||||
|
||||
# The rest of this is messy/complicated b/c it's dealing with mocking
|
||||
|
||||
expected = Zone('unit.tests.', [])
|
||||
@@ -116,7 +209,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
not_found = {'error': "Could not find domain 'unit.tests.'"}
|
||||
with requests_mock() as mock:
|
||||
# get 422's, unknown zone
|
||||
mock.get(ANY, status_code=422, text='')
|
||||
mock.get(ANY, status_code=422, text=dumps(not_found))
|
||||
# patch 422's, unknown zone
|
||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
||||
# post 201, is response to the create with data
|
||||
@@ -127,9 +220,24 @@ class TestPowerDnsProvider(TestCase):
|
||||
self.assertEquals(expected_n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._powerdns_version = [4, 2, 0]
|
||||
with requests_mock() as mock:
|
||||
# get 404's, unknown zone
|
||||
mock.get(ANY, status_code=404, text='')
|
||||
# patch 404's, unknown zone
|
||||
mock.patch(ANY, status_code=404, text=dumps(not_found))
|
||||
# post 201, is response to the create with data
|
||||
mock.post(ANY, status_code=201, text=assert_rrsets_callback)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
self.assertEquals(expected_n, len(plan.changes))
|
||||
self.assertEquals(expected_n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._powerdns_version = [4, 1, 0]
|
||||
with requests_mock() as mock:
|
||||
# get 422's, unknown zone
|
||||
mock.get(ANY, status_code=422, text='')
|
||||
mock.get(ANY, status_code=422, text=dumps(not_found))
|
||||
# patch 422's,
|
||||
data = {'error': "Key 'name' not present or not a String"}
|
||||
mock.patch(ANY, status_code=422, text=dumps(data))
|
||||
@@ -143,7 +251,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
|
||||
with requests_mock() as mock:
|
||||
# get 422's, unknown zone
|
||||
mock.get(ANY, status_code=422, text='')
|
||||
mock.get(ANY, status_code=422, text=dumps(not_found))
|
||||
# patch 500's, things just blew up
|
||||
mock.patch(ANY, status_code=500, text='')
|
||||
|
||||
@@ -153,7 +261,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
|
||||
with requests_mock() as mock:
|
||||
# get 422's, unknown zone
|
||||
mock.get(ANY, status_code=422, text='')
|
||||
mock.get(ANY, status_code=422, text=dumps(not_found))
|
||||
# patch 500's, things just blew up
|
||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
||||
# post 422's, something wrong with create
|
||||
@@ -174,6 +282,8 @@ class TestPowerDnsProvider(TestCase):
|
||||
# A small change to a single record
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=200, text=FULL_TEXT)
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': '4.1.0'})
|
||||
|
||||
missing = Zone(expected.name, [])
|
||||
# Find and delete the SPF record
|
||||
@@ -245,6 +355,8 @@ class TestPowerDnsProvider(TestCase):
|
||||
}]
|
||||
}
|
||||
mock.get(ANY, status_code=200, json=data)
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': '4.1.0'})
|
||||
|
||||
unrelated_record = Record.new(expected, '', {
|
||||
'type': 'A',
|
||||
@@ -278,6 +390,8 @@ class TestPowerDnsProvider(TestCase):
|
||||
}]
|
||||
}
|
||||
mock.get(ANY, status_code=200, json=data)
|
||||
mock.get('http://non.existent:8081/api/v1/servers/localhost',
|
||||
status_code=200, json={'version': '4.1.0'})
|
||||
|
||||
plan = provider.plan(expected)
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
|
Reference in New Issue
Block a user