mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Added server version checking
This commit is contained in:
@@ -19,13 +19,15 @@ class PowerDnsBaseProvider(BaseProvider):
|
|||||||
'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
|
'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
|
||||||
TIMEOUT = 5
|
TIMEOUT = 5
|
||||||
|
|
||||||
def __init__(self, id, host, api_key, soa_edit_api, port=8081,
|
def __init__(self, id, host, api_key, port=8081,
|
||||||
scheme="http", timeout=TIMEOUT, *args, **kwargs):
|
scheme="http", timeout=TIMEOUT, *args, **kwargs):
|
||||||
super(PowerDnsBaseProvider, self).__init__(id, *args, **kwargs)
|
super(PowerDnsBaseProvider, self).__init__(id, *args, **kwargs)
|
||||||
|
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.soa_edit_api = soa_edit_api
|
self.version_detected = ''
|
||||||
|
self.soa_edit_api = "INCEPTION-INCREMENT"
|
||||||
|
self.check_status_not_found = False
|
||||||
self.scheme = scheme
|
self.scheme = scheme
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
@@ -37,7 +39,8 @@ class PowerDnsBaseProvider(BaseProvider):
|
|||||||
self.log.debug('_request: method=%s, path=%s', method, path)
|
self.log.debug('_request: method=%s, path=%s', method, path)
|
||||||
|
|
||||||
url = '{}://{}:{}/api/v1/servers/localhost/{}' \
|
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)
|
resp = self._sess.request(method, url, json=data, timeout=self.timeout)
|
||||||
self.log.debug('_request: status=%d', resp.status_code)
|
self.log.debug('_request: status=%d', resp.status_code)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
@@ -166,20 +169,75 @@ class PowerDnsBaseProvider(BaseProvider):
|
|||||||
'ttl': rrset['ttl']
|
'ttl': rrset['ttl']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def detect_version(self):
|
||||||
|
# Only detect version once
|
||||||
|
if self.version_detected != '':
|
||||||
|
self.log.debug('detect_version: version %s allready detected',
|
||||||
|
self.version_detected)
|
||||||
|
return self.version_detected
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.log.debug('detect_version: getting version from server')
|
||||||
|
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))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.version_detected = resp.json()["version"]
|
||||||
|
self.log.debug('detect_version: got version %s from server',
|
||||||
|
self.version_detected)
|
||||||
|
self.configure_for_version(self.version_detected)
|
||||||
|
|
||||||
|
def configure_for_version(self, version):
|
||||||
|
major, minor, patch = version.split('.', 2)
|
||||||
|
major, minor, patch = int(major), int(minor), int(patch)
|
||||||
|
self.log.debug('configure_for_version: configure for '
|
||||||
|
'major: %s, minor: %s, patch: %s',
|
||||||
|
major, minor, patch)
|
||||||
|
|
||||||
|
# Defaults for v4.0.0
|
||||||
|
self.soa_edit_api = "INCEPTION-INCREMENT"
|
||||||
|
self.check_status_not_found = False
|
||||||
|
|
||||||
|
if major == 4 and minor >= 2:
|
||||||
|
self.log.debug("configure_for_version: Version >= 4.2")
|
||||||
|
self.soa_edit_api = "INCEPTION-INCREMENT"
|
||||||
|
self.check_status_not_found = True
|
||||||
|
|
||||||
|
if major == 4 and minor >= 3:
|
||||||
|
self.log.debug("configure_for_version: Version >= 4.3")
|
||||||
|
self.soa_edit_api = "DEFAULT"
|
||||||
|
self.check_status_not_found = True
|
||||||
|
|
||||||
def populate(self, zone, target=False, lenient=False):
|
def populate(self, zone, target=False, lenient=False):
|
||||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||||
target, lenient)
|
target, lenient)
|
||||||
|
|
||||||
|
self.detect_version()
|
||||||
|
|
||||||
resp = None
|
resp = None
|
||||||
try:
|
try:
|
||||||
resp = self._get('zones/{}'.format(zone.name))
|
resp = self._get('zones/{}'.format(zone.name))
|
||||||
self.log.debug('populate: loaded')
|
self.log.debug('populate: loaded')
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
|
error = self._get_error(e)
|
||||||
if e.response.status_code == 401:
|
if e.response.status_code == 401:
|
||||||
# Nicer error message for auth problems
|
# Nicer error message for auth problems
|
||||||
raise Exception('PowerDNS unauthorized host={}'
|
raise Exception('PowerDNS unauthorized host={}'
|
||||||
.format(self.host))
|
.format(self.host))
|
||||||
elif e.response.status_code in (404, 422):
|
elif e.response.status_code == 404 \
|
||||||
|
and self.check_status_not_found:
|
||||||
|
# 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
|
||||||
|
elif e.response.status_code == 422 \
|
||||||
|
and error.startswith('Could not find domain ') \
|
||||||
|
and not self.check_status_not_found:
|
||||||
# 404 or 422 means powerdns doesn't know anything about the
|
# 404 or 422 means powerdns doesn't know anything about the
|
||||||
# requested domain. We'll just ignore it here and leave the
|
# requested domain. We'll just ignore it here and leave the
|
||||||
# zone untouched.
|
# zone untouched.
|
||||||
@@ -339,13 +397,22 @@ class PowerDnsBaseProvider(BaseProvider):
|
|||||||
self.log.debug('_apply: patched')
|
self.log.debug('_apply: patched')
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
error = self._get_error(e)
|
error = self._get_error(e)
|
||||||
if e.response.status_code != 404 and \
|
if not (
|
||||||
not (e.response.status_code == 422 and
|
(
|
||||||
error.startswith('Could not find domain ')):
|
e.response.status_code == 404 and
|
||||||
self.log.error('_apply: status=%d, text=%s',
|
self.check_status_not_found
|
||||||
e.response.status_code,
|
) or (
|
||||||
e.response.text)
|
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
|
raise
|
||||||
|
|
||||||
self.log.info('_apply: creating zone=%s', desired.name)
|
self.log.info('_apply: creating zone=%s', desired.name)
|
||||||
# 404 or 422 means powerdns doesn't know anything about the
|
# 404 or 422 means powerdns doesn't know anything about the
|
||||||
# requested domain. We'll try to create it with the correct
|
# requested domain. We'll try to create it with the correct
|
||||||
@@ -398,17 +465,14 @@ class PowerDnsProvider(PowerDnsBaseProvider):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, id, host, api_key, port=8081, nameserver_values=None,
|
def __init__(self, id, host, api_key, port=8081, nameserver_values=None,
|
||||||
nameserver_ttl=600, soa_edit_api='INCEPTION-INCREMENT',
|
nameserver_ttl=600,
|
||||||
*args, **kwargs):
|
*args, **kwargs):
|
||||||
self.log = logging.getLogger('PowerDnsProvider[{}]'.format(id))
|
self.log = logging.getLogger('PowerDnsProvider[{}]'.format(id))
|
||||||
self.log.debug('__init__: id=%s, host=%s, port=%d, '
|
self.log.debug('__init__: id=%s, host=%s, port=%d, '
|
||||||
'nameserver_values=%s, nameserver_ttl=%d'
|
'nameserver_values=%s, nameserver_ttl=%d',
|
||||||
'soa_edit_api=%s',
|
id, host, port, nameserver_values, nameserver_ttl)
|
||||||
id, host, port, nameserver_values, nameserver_ttl,
|
|
||||||
soa_edit_api)
|
|
||||||
super(PowerDnsProvider, self).__init__(id, host=host, api_key=api_key,
|
super(PowerDnsProvider, self).__init__(id, host=host, api_key=api_key,
|
||||||
port=port,
|
port=port,
|
||||||
soa_edit_api=soa_edit_api,
|
|
||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
|
|
||||||
self.nameserver_values = nameserver_values
|
self.nameserver_values = nameserver_values
|
||||||
|
|||||||
@@ -41,11 +41,94 @@ with open('./tests/fixtures/powerdns-full-data.json') as fh:
|
|||||||
|
|
||||||
class TestPowerDnsProvider(TestCase):
|
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.detect_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.detect_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"})
|
||||||
|
|
||||||
|
provider.detect_version()
|
||||||
|
self.assertEquals(provider.version_detected, '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"})
|
||||||
|
provider.detect_version()
|
||||||
|
self.assertEquals(provider.version_detected, '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.version_detected = ''
|
||||||
|
provider.detect_version()
|
||||||
|
self.assertNotEquals(provider.version_detected, '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.'])
|
||||||
|
|
||||||
|
provider.check_status_not_found = None
|
||||||
|
provider.soa_edit_api = 'something else'
|
||||||
|
|
||||||
|
# Test version 4.1.0
|
||||||
|
provider.configure_for_version("4.1.0")
|
||||||
|
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.configure_for_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.configure_for_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):
|
def test_provider(self):
|
||||||
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
|
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
|
||||||
nameserver_values=['8.8.8.8.',
|
nameserver_values=['8.8.8.8.',
|
||||||
'9.9.9.9.'])
|
'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"})
|
||||||
|
|
||||||
|
provider.detect_version()
|
||||||
|
self.assertEquals(provider.version_detected, '4.1.10')
|
||||||
|
|
||||||
# Bad auth
|
# Bad auth
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
mock.get(ANY, status_code=401, text='Unauthorized')
|
mock.get(ANY, status_code=401, text='Unauthorized')
|
||||||
@@ -68,18 +151,19 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
mock.get(ANY, status_code=422,
|
mock.get(ANY, status_code=422,
|
||||||
json={'error': "Could not find domain 'unit.tests.'"})
|
json={'error': "Could not find domain 'unit.tests.'"})
|
||||||
|
|
||||||
zone = Zone('unit.tests.', [])
|
zone = Zone('unit.tests.', [])
|
||||||
provider.populate(zone)
|
provider.populate(zone)
|
||||||
self.assertEquals(set(), zone.records)
|
self.assertEquals(set(), zone.records)
|
||||||
|
|
||||||
# Non-existent zone in PowerDNS >=4.3.0 doesn't populate anything
|
# Non-existent zone in PowerDNS >=4.2.0 doesn't populate anything
|
||||||
|
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
mock.get(ANY, status_code=404, text='Not Found')
|
mock.get(ANY, status_code=404, text='Not Found')
|
||||||
|
provider.configure_for_version("4.2.0")
|
||||||
zone = Zone('unit.tests.', [])
|
zone = Zone('unit.tests.', [])
|
||||||
provider.populate(zone)
|
provider.populate(zone)
|
||||||
self.assertEquals(set(), zone.records)
|
self.assertEquals(set(), zone.records)
|
||||||
|
provider.configure_for_version("4.1.0")
|
||||||
|
|
||||||
# The rest of this is messy/complicated b/c it's dealing with mocking
|
# The rest of this is messy/complicated b/c it's dealing with mocking
|
||||||
|
|
||||||
@@ -124,7 +208,7 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
not_found = {'error': "Could not find domain 'unit.tests.'"}
|
not_found = {'error': "Could not find domain 'unit.tests.'"}
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
# get 422's, unknown zone
|
# 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
|
# patch 422's, unknown zone
|
||||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
||||||
# post 201, is response to the create with data
|
# post 201, is response to the create with data
|
||||||
@@ -136,6 +220,7 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
self.assertFalse(plan.exists)
|
self.assertFalse(plan.exists)
|
||||||
|
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
|
provider.configure_for_version('4.2.0')
|
||||||
# get 404's, unknown zone
|
# get 404's, unknown zone
|
||||||
mock.get(ANY, status_code=404, text='')
|
mock.get(ANY, status_code=404, text='')
|
||||||
# patch 404's, unknown zone
|
# patch 404's, unknown zone
|
||||||
@@ -147,10 +232,11 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
self.assertEquals(expected_n, len(plan.changes))
|
self.assertEquals(expected_n, len(plan.changes))
|
||||||
self.assertEquals(expected_n, provider.apply(plan))
|
self.assertEquals(expected_n, provider.apply(plan))
|
||||||
self.assertFalse(plan.exists)
|
self.assertFalse(plan.exists)
|
||||||
|
provider.configure_for_version('4.1.0')
|
||||||
|
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
# get 422's, unknown zone
|
# 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,
|
# patch 422's,
|
||||||
data = {'error': "Key 'name' not present or not a String"}
|
data = {'error': "Key 'name' not present or not a String"}
|
||||||
mock.patch(ANY, status_code=422, text=dumps(data))
|
mock.patch(ANY, status_code=422, text=dumps(data))
|
||||||
@@ -164,7 +250,7 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
|
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
# get 422's, unknown zone
|
# 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
|
# patch 500's, things just blew up
|
||||||
mock.patch(ANY, status_code=500, text='')
|
mock.patch(ANY, status_code=500, text='')
|
||||||
|
|
||||||
@@ -174,7 +260,7 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
|
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
# get 422's, unknown zone
|
# 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
|
# patch 500's, things just blew up
|
||||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
||||||
# post 422's, something wrong with create
|
# post 422's, something wrong with create
|
||||||
@@ -195,6 +281,8 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
# A small change to a single record
|
# A small change to a single record
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
mock.get(ANY, status_code=200, text=FULL_TEXT)
|
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, [])
|
missing = Zone(expected.name, [])
|
||||||
# Find and delete the SPF record
|
# Find and delete the SPF record
|
||||||
@@ -266,6 +354,8 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
mock.get(ANY, status_code=200, json=data)
|
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, '', {
|
unrelated_record = Record.new(expected, '', {
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
@@ -299,6 +389,8 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
mock.get(ANY, status_code=200, json=data)
|
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)
|
plan = provider.plan(expected)
|
||||||
self.assertEquals(1, len(plan.changes))
|
self.assertEquals(1, len(plan.changes))
|
||||||
@@ -312,22 +404,3 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
|
|
||||||
plan = provider.plan(expected)
|
plan = provider.plan(expected)
|
||||||
self.assertEquals(1, len(plan.changes))
|
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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user