mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge pull request #1 from maikelpoot/powerdns-4.3.x-support
Powerdns 4.3.x support
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## v0.9.11 - 2020-??-?? - ???????????????
|
||||
|
||||
* Added support for TCP health checking to dynamic records
|
||||
|
||||
## v0.9.10 - 2020-04-20 - Dynamic NS1 and lots of misc
|
||||
|
||||
* Added support for dynamic records to Ns1Provider, updated client and rate
|
||||
|
||||
@@ -11,6 +11,8 @@ It is similar to [Netflix/denominator](https://github.com/Netflix/denominator).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [DNS as code - Tools for managing DNS across multiple providers](#dns-as-code---tools-for-managing-dns-across-multiple-providers)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Getting started](#getting-started)
|
||||
- [Workspace](#workspace)
|
||||
- [Config](#config)
|
||||
@@ -178,7 +180,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
|--|--|--|--|--|
|
||||
| [AzureProvider](/octodns/provider/azuredns.py) | azure-mgmt-dns | A, AAAA, CAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
|
||||
| [Akamai](/octodns/provider/edgedns.py) | edgegrid-python | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT | No | |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | | A, AAAA, ALIAS, CAA, CNAME, MX, NS, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | | A, AAAA, ALIAS, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [ConstellixProvider](/octodns/provider/constellix.py) | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [DigitalOceanProvider](/octodns/provider/digitalocean.py) | | A, AAAA, CAA, CNAME, MX, NS, TXT, SRV | No | CAA tags restricted |
|
||||
| [DnsMadeEasyProvider](/octodns/provider/dnsmadeeasy.py) | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
|
||||
@@ -103,7 +103,7 @@ test:
|
||||
| host | FQDN for host header and SNI | - |
|
||||
| path | path to check | _dns |
|
||||
| port | port to check | 443 |
|
||||
| protocol | HTTP/HTTPS | HTTPS |
|
||||
| protocol | HTTP/HTTPS/TCP | HTTPS |
|
||||
|
||||
#### Route53 Healtch Check Options
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from logging import getLogger
|
||||
from requests import Session
|
||||
from time import sleep
|
||||
|
||||
from ..record import Record, Update
|
||||
from .base import BaseProvider
|
||||
@@ -18,7 +19,7 @@ class CloudflareError(Exception):
|
||||
def __init__(self, data):
|
||||
try:
|
||||
message = data['errors'][0]['message']
|
||||
except (IndexError, KeyError):
|
||||
except (IndexError, KeyError, TypeError):
|
||||
message = 'Cloudflare error'
|
||||
super(CloudflareError, self).__init__(message)
|
||||
|
||||
@@ -28,6 +29,11 @@ class CloudflareAuthenticationError(CloudflareError):
|
||||
CloudflareError.__init__(self, data)
|
||||
|
||||
|
||||
class CloudflareRateLimitError(CloudflareError):
|
||||
def __init__(self, data):
|
||||
CloudflareError.__init__(self, data)
|
||||
|
||||
|
||||
_PROXIABLE_RECORD_TYPES = {'A', 'AAAA', 'ALIAS', 'CNAME'}
|
||||
|
||||
|
||||
@@ -47,6 +53,11 @@ class CloudflareProvider(BaseProvider):
|
||||
#
|
||||
# See: https://support.cloudflare.com/hc/en-us/articles/115000830351
|
||||
cdn: false
|
||||
# Optional. Default: 4. Number of times to retry if a 429 response
|
||||
# is received.
|
||||
retry_count: 4
|
||||
# Optional. Default: 300. Number of seconds to wait before retrying.
|
||||
retry_period: 300
|
||||
|
||||
Note: The "proxied" flag of "A", "AAAA" and "CNAME" records can be managed
|
||||
via the YAML provider like so:
|
||||
@@ -60,13 +71,14 @@ class CloudflareProvider(BaseProvider):
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SRV',
|
||||
'SPF', 'TXT'))
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR',
|
||||
'SRV', 'SPF', 'TXT'))
|
||||
|
||||
MIN_TTL = 120
|
||||
TIMEOUT = 15
|
||||
|
||||
def __init__(self, id, email=None, token=None, cdn=False, *args, **kwargs):
|
||||
def __init__(self, id, email=None, token=None, cdn=False, retry_count=4,
|
||||
retry_period=300, *args, **kwargs):
|
||||
self.log = getLogger('CloudflareProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, email=%s, token=***, cdn=%s', id,
|
||||
email, cdn)
|
||||
@@ -85,11 +97,27 @@ class CloudflareProvider(BaseProvider):
|
||||
'Authorization': 'Bearer {}'.format(token),
|
||||
})
|
||||
self.cdn = cdn
|
||||
self.retry_count = retry_count
|
||||
self.retry_period = retry_period
|
||||
self._sess = sess
|
||||
|
||||
self._zones = None
|
||||
self._zone_records = {}
|
||||
|
||||
def _try_request(self, *args, **kwargs):
|
||||
tries = self.retry_count
|
||||
while True: # We'll raise to break after our tries expire
|
||||
try:
|
||||
return self._request(*args, **kwargs)
|
||||
except CloudflareRateLimitError:
|
||||
if tries <= 1:
|
||||
raise
|
||||
tries -= 1
|
||||
self.log.warn('rate limit encountered, pausing '
|
||||
'for %ds and trying again, %d remaining',
|
||||
self.retry_period, tries)
|
||||
sleep(self.retry_period)
|
||||
|
||||
def _request(self, method, path, params=None, data=None):
|
||||
self.log.debug('_request: method=%s, path=%s', method, path)
|
||||
|
||||
@@ -101,6 +129,8 @@ class CloudflareProvider(BaseProvider):
|
||||
raise CloudflareError(resp.json())
|
||||
if resp.status_code == 403:
|
||||
raise CloudflareAuthenticationError(resp.json())
|
||||
if resp.status_code == 429:
|
||||
raise CloudflareRateLimitError(resp.json())
|
||||
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
@@ -111,7 +141,8 @@ class CloudflareProvider(BaseProvider):
|
||||
page = 1
|
||||
zones = []
|
||||
while page:
|
||||
resp = self._request('GET', '/zones', params={'page': page})
|
||||
resp = self._try_request('GET', '/zones',
|
||||
params={'page': page})
|
||||
zones += resp['result']
|
||||
info = resp['result_info']
|
||||
if info['count'] > 0 and info['count'] == info['per_page']:
|
||||
@@ -173,6 +204,7 @@ class CloudflareProvider(BaseProvider):
|
||||
}
|
||||
|
||||
_data_for_ALIAS = _data_for_CNAME
|
||||
_data_for_PTR = _data_for_CNAME
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
@@ -219,7 +251,7 @@ class CloudflareProvider(BaseProvider):
|
||||
path = '/zones/{}/dns_records'.format(zone_id)
|
||||
page = 1
|
||||
while page:
|
||||
resp = self._request('GET', path, params={'page': page})
|
||||
resp = self._try_request('GET', path, params={'page': page})
|
||||
records += resp['result']
|
||||
info = resp['result_info']
|
||||
if info['count'] > 0 and info['count'] == info['per_page']:
|
||||
@@ -339,6 +371,8 @@ class CloudflareProvider(BaseProvider):
|
||||
def _contents_for_CNAME(self, record):
|
||||
yield {'content': record.value}
|
||||
|
||||
_contents_for_PTR = _contents_for_CNAME
|
||||
|
||||
def _contents_for_MX(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
@@ -430,7 +464,7 @@ class CloudflareProvider(BaseProvider):
|
||||
zone_id = self.zones[new.zone.name]
|
||||
path = '/zones/{}/dns_records'.format(zone_id)
|
||||
for content in self._gen_data(new):
|
||||
self._request('POST', path, data=content)
|
||||
self._try_request('POST', path, data=content)
|
||||
|
||||
def _apply_Update(self, change):
|
||||
zone = change.new.zone
|
||||
@@ -519,7 +553,7 @@ class CloudflareProvider(BaseProvider):
|
||||
path = '/zones/{}/dns_records'.format(zone_id)
|
||||
for _, data in sorted(creates.items()):
|
||||
self.log.debug('_apply_Update: creating %s', data)
|
||||
self._request('POST', path, data=data)
|
||||
self._try_request('POST', path, data=data)
|
||||
|
||||
# Updates
|
||||
for _, info in sorted(updates.items()):
|
||||
@@ -529,7 +563,7 @@ class CloudflareProvider(BaseProvider):
|
||||
path = '/zones/{}/dns_records/{}'.format(zone_id, record_id)
|
||||
self.log.debug('_apply_Update: updating %s, %s -> %s',
|
||||
record_id, data, old_data)
|
||||
self._request('PUT', path, data=data)
|
||||
self._try_request('PUT', path, data=data)
|
||||
|
||||
# Deletes
|
||||
for _, info in sorted(deletes.items()):
|
||||
@@ -538,7 +572,7 @@ class CloudflareProvider(BaseProvider):
|
||||
path = '/zones/{}/dns_records/{}'.format(zone_id, record_id)
|
||||
self.log.debug('_apply_Update: removing %s, %s', record_id,
|
||||
old_data)
|
||||
self._request('DELETE', path)
|
||||
self._try_request('DELETE', path)
|
||||
|
||||
def _apply_Delete(self, change):
|
||||
existing = change.existing
|
||||
@@ -551,7 +585,7 @@ class CloudflareProvider(BaseProvider):
|
||||
existing_type == record['type']:
|
||||
path = '/zones/{}/dns_records/{}'.format(record['zone_id'],
|
||||
record['id'])
|
||||
self._request('DELETE', path)
|
||||
self._try_request('DELETE', path)
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
@@ -566,7 +600,7 @@ class CloudflareProvider(BaseProvider):
|
||||
'name': name[:-1],
|
||||
'jump_start': False,
|
||||
}
|
||||
resp = self._request('POST', '/zones', data=data)
|
||||
resp = self._try_request('POST', '/zones', data=data)
|
||||
zone_id = resp['result']['id']
|
||||
self.zones[name] = zone_id
|
||||
self._zone_records[name] = {}
|
||||
|
||||
@@ -1141,7 +1141,7 @@ class DynProvider(BaseProvider):
|
||||
pools[rpid] = get_response_pool(rpid, td)
|
||||
# now that we have full objects for the complete set of existing pools,
|
||||
# a list will be more useful
|
||||
pools = pools.values()
|
||||
pools = list(pools.values())
|
||||
|
||||
# Rulesets
|
||||
|
||||
|
||||
+19
-12
@@ -253,7 +253,9 @@ class Ns1Provider(BaseProvider):
|
||||
|
||||
def _REGION_FILTER(self, with_disabled):
|
||||
return self._update_filter({
|
||||
'config': {},
|
||||
'config': {
|
||||
'remove_no_georegion': True
|
||||
},
|
||||
'filter': u'geofence_regional'
|
||||
}, with_disabled)
|
||||
|
||||
@@ -766,8 +768,7 @@ class Ns1Provider(BaseProvider):
|
||||
for iso_region, target in record.geo.items():
|
||||
key = 'iso_region_code'
|
||||
value = iso_region
|
||||
if not has_country and \
|
||||
len(value.split('-')) > 1: # pragma: nocover
|
||||
if not has_country and len(value.split('-')) > 1:
|
||||
has_country = True
|
||||
for answer in target.values:
|
||||
params['answers'].append(
|
||||
@@ -856,18 +857,13 @@ class Ns1Provider(BaseProvider):
|
||||
host = record.fqdn[:-1]
|
||||
_type = record._type
|
||||
|
||||
request = r'GET {path} HTTP/1.0\r\nHost: {host}\r\n' \
|
||||
r'User-agent: NS1\r\n\r\n'.format(path=record.healthcheck_path,
|
||||
host=record.healthcheck_host)
|
||||
|
||||
return {
|
||||
ret = {
|
||||
'active': True,
|
||||
'config': {
|
||||
'connect_timeout': 2000,
|
||||
'host': value,
|
||||
'port': record.healthcheck_port,
|
||||
'response_timeout': 10000,
|
||||
'send': request,
|
||||
'ssl': record.healthcheck_protocol == 'HTTPS',
|
||||
},
|
||||
'frequency': 60,
|
||||
@@ -881,12 +877,23 @@ class Ns1Provider(BaseProvider):
|
||||
'rapid_recheck': False,
|
||||
'region_scope': 'fixed',
|
||||
'regions': self.monitor_regions,
|
||||
'rules': [{
|
||||
}
|
||||
|
||||
if record.healthcheck_protocol != 'TCP':
|
||||
# IF it's HTTP we need to send the request string
|
||||
path = record.healthcheck_path
|
||||
host = record.healthcheck_host
|
||||
request = r'GET {path} HTTP/1.0\r\nHost: {host}\r\n' \
|
||||
r'User-agent: NS1\r\n\r\n'.format(path=path, host=host)
|
||||
ret['config']['send'] = request
|
||||
# We'll also expect a HTTP response
|
||||
ret['rules'] = [{
|
||||
'comparison': 'contains',
|
||||
'key': 'output',
|
||||
'value': '200 OK',
|
||||
}],
|
||||
}
|
||||
}]
|
||||
|
||||
return ret
|
||||
|
||||
def _monitor_is_match(self, expected, have):
|
||||
# Make sure what we have matches what's in expected exactly. Anything
|
||||
|
||||
@@ -19,13 +19,15 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
|
||||
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):
|
||||
super(PowerDnsBaseProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
self.host = host
|
||||
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.timeout = timeout
|
||||
|
||||
@@ -37,7 +39,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()
|
||||
@@ -166,20 +169,75 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
'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):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
|
||||
self.detect_version()
|
||||
|
||||
resp = None
|
||||
try:
|
||||
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 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
|
||||
# requested domain. We'll just ignore it here and leave the
|
||||
# zone untouched.
|
||||
@@ -339,13 +397,22 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
self.log.debug('_apply: patched')
|
||||
except HTTPError as e:
|
||||
error = self._get_error(e)
|
||||
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)
|
||||
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)
|
||||
# 404 or 422 means powerdns doesn't know anything about the
|
||||
# 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,
|
||||
nameserver_ttl=600, soa_edit_api='INCEPTION-INCREMENT',
|
||||
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'
|
||||
'soa_edit_api=%s',
|
||||
id, host, port, nameserver_values, nameserver_ttl,
|
||||
soa_edit_api)
|
||||
'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,
|
||||
soa_edit_api=soa_edit_api,
|
||||
*args, **kwargs)
|
||||
|
||||
self.nameserver_values = nameserver_values
|
||||
|
||||
@@ -1046,8 +1046,11 @@ class Route53Provider(BaseProvider):
|
||||
# No value so give this a None to match value's
|
||||
config_ip_address = None
|
||||
|
||||
return host == config['FullyQualifiedDomainName'] and \
|
||||
path == config['ResourcePath'] and protocol == config['Type'] \
|
||||
fully_qualified_domain_name = config.get('FullyQualifiedDomainName',
|
||||
None)
|
||||
resource_path = config.get('ResourcePath', None)
|
||||
return host == fully_qualified_domain_name and \
|
||||
path == resource_path and protocol == config['Type'] \
|
||||
and port == config['Port'] and \
|
||||
measure_latency == config['MeasureLatency'] and \
|
||||
value == config_ip_address
|
||||
@@ -1103,13 +1106,14 @@ class Route53Provider(BaseProvider):
|
||||
config = {
|
||||
'EnableSNI': healthcheck_protocol == 'HTTPS',
|
||||
'FailureThreshold': 6,
|
||||
'FullyQualifiedDomainName': healthcheck_host,
|
||||
'MeasureLatency': healthcheck_latency,
|
||||
'Port': healthcheck_port,
|
||||
'RequestInterval': 10,
|
||||
'ResourcePath': healthcheck_path,
|
||||
'Type': healthcheck_protocol,
|
||||
}
|
||||
if healthcheck_protocol != 'TCP':
|
||||
config['FullyQualifiedDomainName'] = healthcheck_host
|
||||
config['ResourcePath'] = healthcheck_path
|
||||
if value:
|
||||
config['IPAddress'] = value
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class Record(EqualityTupleMixin):
|
||||
reasons.append('missing ttl')
|
||||
try:
|
||||
if data['octodns']['healthcheck']['protocol'] \
|
||||
not in ('HTTP', 'HTTPS'):
|
||||
not in ('HTTP', 'HTTPS', 'TCP'):
|
||||
reasons.append('invalid healthcheck protocol')
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -181,15 +181,21 @@ class Record(EqualityTupleMixin):
|
||||
|
||||
@property
|
||||
def healthcheck_host(self):
|
||||
healthcheck = self._octodns.get('healthcheck', {})
|
||||
if healthcheck.get('protocol', None) == 'TCP':
|
||||
return None
|
||||
try:
|
||||
return self._octodns['healthcheck']['host']
|
||||
return healthcheck['host']
|
||||
except KeyError:
|
||||
return self.fqdn[:-1]
|
||||
|
||||
@property
|
||||
def healthcheck_path(self):
|
||||
healthcheck = self._octodns.get('healthcheck', {})
|
||||
if healthcheck.get('protocol', None) == 'TCP':
|
||||
return None
|
||||
try:
|
||||
return self._octodns['healthcheck']['path']
|
||||
return healthcheck['path']
|
||||
except KeyError:
|
||||
return '/_dns'
|
||||
|
||||
|
||||
@@ -63,9 +63,15 @@ class GeoCodes(object):
|
||||
|
||||
@classmethod
|
||||
def province_to_code(cls, province):
|
||||
# We get to cheat on this one since we only support provinces in NA-US
|
||||
if province not in geo_data['NA']['US']['provinces']:
|
||||
# We cheat on this one a little since we only support provinces in
|
||||
# NA-US, NA-CA
|
||||
if (province not in geo_data['NA']['US']['provinces'] and
|
||||
province not in geo_data['NA']['CA']['provinces']):
|
||||
cls.log.warn('country_to_code: unrecognized province "%s"',
|
||||
province)
|
||||
return
|
||||
return 'NA-US-{}'.format(province)
|
||||
if province in geo_data['NA']['US']['provinces']:
|
||||
country = 'US'
|
||||
if province in geo_data['NA']['CA']['provinces']:
|
||||
country = 'CA'
|
||||
return 'NA-{}-{}'.format(country, province)
|
||||
|
||||
@@ -55,7 +55,7 @@ geo_data = \
|
||||
'SO': {'name': 'Somalia'},
|
||||
'SS': {'name': 'South Sudan'},
|
||||
'ST': {'name': 'Sao Tome and Principe'},
|
||||
'SZ': {'name': 'Swaziland'},
|
||||
'SZ': {'name': 'Eswatini'},
|
||||
'TD': {'name': 'Chad'},
|
||||
'TG': {'name': 'Togo'},
|
||||
'TN': {'name': 'Tunisia'},
|
||||
@@ -157,7 +157,7 @@ geo_data = \
|
||||
'MC': {'name': 'Monaco'},
|
||||
'MD': {'name': 'Moldova, Republic of'},
|
||||
'ME': {'name': 'Montenegro'},
|
||||
'MK': {'name': 'Macedonia, Republic of'},
|
||||
'MK': {'name': 'North Macedonia'},
|
||||
'MT': {'name': 'Malta'},
|
||||
'NL': {'name': 'Netherlands'},
|
||||
'NO': {'name': 'Norway'},
|
||||
@@ -183,7 +183,20 @@ geo_data = \
|
||||
'BQ': {'name': 'Bonaire, Sint Eustatius and Saba'},
|
||||
'BS': {'name': 'Bahamas'},
|
||||
'BZ': {'name': 'Belize'},
|
||||
'CA': {'name': 'Canada'},
|
||||
'CA': {'name': 'Canada',
|
||||
'provinces': {'AB': {'name': 'Alberta'},
|
||||
'BC': {'name': 'British Columbia'},
|
||||
'MB': {'name': 'Manitoba'},
|
||||
'NB': {'name': 'New Brunswick'},
|
||||
'NL': {'name': 'Newfoundland and Labrador'},
|
||||
'NS': {'name': 'Nova Scotia'},
|
||||
'NT': {'name': 'Northwest Territories'},
|
||||
'NU': {'name': 'Nunavut'},
|
||||
'ON': {'name': 'Ontario'},
|
||||
'PE': {'name': 'Prince Edward Island'},
|
||||
'QC': {'name': 'Quebec'},
|
||||
'SK': {'name': 'Saskatchewan'},
|
||||
'YT': {'name': 'Yukon Territory'}}},
|
||||
'CR': {'name': 'Costa Rica'},
|
||||
'CU': {'name': 'Cuba'},
|
||||
'CW': {'name': 'Curaçao'},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
coverage
|
||||
mock
|
||||
nose
|
||||
pycodestyle==2.5.0
|
||||
pycodestyle==2.6.0
|
||||
pyflakes==2.2.0
|
||||
readme_renderer[md]==26.0
|
||||
requests_mock
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
PyYaml==5.3.1
|
||||
azure-common==1.1.25
|
||||
azure-mgmt-dns==3.0.0
|
||||
boto3==1.13.0
|
||||
botocore==1.16.0
|
||||
boto3==1.13.19
|
||||
botocore==1.16.19
|
||||
dnspython==1.16.0
|
||||
docutils==0.16
|
||||
dyn==1.8.1
|
||||
@@ -14,13 +14,13 @@ ipaddress==1.0.23
|
||||
jmespath==0.9.5
|
||||
msrestazure==0.6.3
|
||||
natsort==6.2.1
|
||||
ns1-python==0.15.0
|
||||
ns1-python==0.16.0
|
||||
ovh==0.5.0
|
||||
pycountry-convert==0.7.2
|
||||
pycountry==19.8.18
|
||||
python-dateutil==2.8.1
|
||||
requests==2.23.0
|
||||
s3transfer==0.3.3
|
||||
setuptools==44.1.0
|
||||
six==1.14.0
|
||||
setuptools==44.1.1
|
||||
six==1.15.0
|
||||
transip==2.1.2
|
||||
|
||||
@@ -26,6 +26,12 @@ export DYN_PASSWORD=
|
||||
export DYN_USERNAME=
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=
|
||||
|
||||
# Don't allow disabling coverage
|
||||
grep -r -I --line-number "# pragma: nocover" octodns && {
|
||||
echo "Code coverage should not be disabled"
|
||||
exit 1
|
||||
}
|
||||
|
||||
coverage run --branch --source=octodns --omit=octodns/cmds/* "$(command -v nosetests)" --with-xunit "$@"
|
||||
coverage html
|
||||
coverage xml
|
||||
|
||||
@@ -8,8 +8,8 @@ from pycountry_convert import country_alpha2_to_continent_code
|
||||
|
||||
subs = defaultdict(dict)
|
||||
for subdivision in subdivisions:
|
||||
# Route53 only supports US states, Dyn supports US states and CA provinces, but for now we'll just do US
|
||||
if subdivision.country_code not in ('US'):
|
||||
# Route53 only supports US states, Dyn (and others) support US states and CA provinces
|
||||
if subdivision.country_code not in ('US', 'CA'):
|
||||
continue
|
||||
subs[subdivision.country_code][subdivision.code[3:]] = {
|
||||
'name': subdivision.name
|
||||
|
||||
+1
-1
@@ -17,5 +17,5 @@ fi
|
||||
|
||||
SOURCES="*.py octodns/*.py octodns/*/*.py tests/*.py"
|
||||
|
||||
pycodestyle --ignore=E221,E241,E251,E722,W504 $SOURCES
|
||||
pycodestyle --ignore=E221,E241,E251,E722,E741,W504 $SOURCES
|
||||
pyflakes $SOURCES
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@
|
||||
"per_page": 10,
|
||||
"total_pages": 2,
|
||||
"count": 10,
|
||||
"total_count": 19
|
||||
"total_count": 20
|
||||
},
|
||||
"success": true,
|
||||
"errors": [],
|
||||
|
||||
+19
-2
@@ -157,6 +157,23 @@
|
||||
"auto_added": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997677",
|
||||
"type": "PTR",
|
||||
"name": "ptr.unit.tests",
|
||||
"content": "foo.bar.com",
|
||||
"proxiable": true,
|
||||
"proxied": false,
|
||||
"ttl": 300,
|
||||
"locked": false,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.940682Z",
|
||||
"created_on": "2017-03-11T18:01:43.940682Z",
|
||||
"meta": {
|
||||
"auto_added": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997656",
|
||||
"type": "SRV",
|
||||
@@ -212,8 +229,8 @@
|
||||
"page": 2,
|
||||
"per_page": 11,
|
||||
"total_pages": 2,
|
||||
"count": 9,
|
||||
"total_count": 21
|
||||
"count": 10,
|
||||
"total_count": 20
|
||||
},
|
||||
"success": true,
|
||||
"errors": [],
|
||||
|
||||
@@ -14,7 +14,8 @@ from unittest import TestCase
|
||||
|
||||
from octodns.record import Record, Update
|
||||
from octodns.provider.base import Plan
|
||||
from octodns.provider.cloudflare import CloudflareProvider
|
||||
from octodns.provider.cloudflare import CloudflareProvider, \
|
||||
CloudflareRateLimitError
|
||||
from octodns.provider.yaml import YamlProvider
|
||||
from octodns.zone import Zone
|
||||
|
||||
@@ -52,7 +53,7 @@ class TestCloudflareProvider(TestCase):
|
||||
empty = {'result': [], 'result_info': {'count': 0, 'per_page': 0}}
|
||||
|
||||
def test_populate(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
provider = CloudflareProvider('test', 'email', 'token', retry_period=0)
|
||||
|
||||
# Bad requests
|
||||
with requests_mock() as mock:
|
||||
@@ -103,6 +104,36 @@ class TestCloudflareProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Rate Limit error
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=429,
|
||||
text='{"success":false,"errors":[{"code":10100,'
|
||||
'"message":"More than 1200 requests per 300 seconds '
|
||||
'reached. Please wait and consider throttling your '
|
||||
'request speed"}],"messages":[],"result":null}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertEquals('CloudflareRateLimitError',
|
||||
type(ctx.exception).__name__)
|
||||
self.assertEquals('More than 1200 requests per 300 seconds '
|
||||
'reached. Please wait and consider throttling '
|
||||
'your request speed', text_type(ctx.exception))
|
||||
|
||||
# Rate Limit error, unknown resp
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=429, text='{}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertEquals('CloudflareRateLimitError',
|
||||
type(ctx.exception).__name__)
|
||||
self.assertEquals('Cloudflare error', text_type(ctx.exception))
|
||||
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=200, json=self.empty)
|
||||
@@ -149,7 +180,7 @@ class TestCloudflareProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(12, len(zone.records))
|
||||
self.assertEquals(13, len(zone.records))
|
||||
|
||||
changes = self.expected.changes(zone, provider)
|
||||
|
||||
@@ -158,10 +189,10 @@ class TestCloudflareProvider(TestCase):
|
||||
# re-populating the same zone/records comes out of cache, no calls
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(12, len(again.records))
|
||||
self.assertEquals(13, len(again.records))
|
||||
|
||||
def test_apply(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
provider = CloudflareProvider('test', 'email', 'token', retry_period=0)
|
||||
|
||||
provider._request = Mock()
|
||||
|
||||
@@ -172,12 +203,12 @@ class TestCloudflareProvider(TestCase):
|
||||
'id': 42,
|
||||
}
|
||||
}, # zone create
|
||||
] + [None] * 20 # individual record creates
|
||||
] + [None] * 22 # individual record creates
|
||||
|
||||
# non-existent zone, create everything
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(12, len(plan.changes))
|
||||
self.assertEquals(12, provider.apply(plan))
|
||||
self.assertEquals(13, len(plan.changes))
|
||||
self.assertEquals(13, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
@@ -203,7 +234,7 @@ class TestCloudflareProvider(TestCase):
|
||||
}),
|
||||
], True)
|
||||
# expected number of total calls
|
||||
self.assertEquals(22, provider._request.call_count)
|
||||
self.assertEquals(23, provider._request.call_count)
|
||||
|
||||
provider._request.reset_mock()
|
||||
|
||||
@@ -280,7 +311,11 @@ class TestCloudflareProvider(TestCase):
|
||||
|
||||
# we don't care about the POST/create return values
|
||||
provider._request.return_value = {}
|
||||
provider._request.side_effect = None
|
||||
|
||||
# Test out the create rate-limit handling, then 9 successes
|
||||
provider._request.side_effect = [
|
||||
CloudflareRateLimitError('{}'),
|
||||
] + ([None] * 3)
|
||||
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, 'nc', {
|
||||
@@ -316,7 +351,7 @@ class TestCloudflareProvider(TestCase):
|
||||
])
|
||||
|
||||
def test_update_add_swap(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
provider = CloudflareProvider('test', 'email', 'token', retry_period=0)
|
||||
|
||||
provider.zone_records = Mock(return_value=[
|
||||
{
|
||||
@@ -357,6 +392,7 @@ class TestCloudflareProvider(TestCase):
|
||||
|
||||
provider._request = Mock()
|
||||
provider._request.side_effect = [
|
||||
CloudflareRateLimitError('{}'),
|
||||
self.empty, # no zones
|
||||
{
|
||||
'result': {
|
||||
@@ -423,7 +459,7 @@ class TestCloudflareProvider(TestCase):
|
||||
def test_update_delete(self):
|
||||
# We need another run so that we can delete, we can't both add and
|
||||
# delete in one go b/c of swaps
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
provider = CloudflareProvider('test', 'email', 'token', retry_period=0)
|
||||
|
||||
provider.zone_records = Mock(return_value=[
|
||||
{
|
||||
@@ -464,6 +500,7 @@ class TestCloudflareProvider(TestCase):
|
||||
|
||||
provider._request = Mock()
|
||||
provider._request.side_effect = [
|
||||
CloudflareRateLimitError('{}'),
|
||||
self.empty, # no zones
|
||||
{
|
||||
'result': {
|
||||
@@ -510,6 +547,25 @@ class TestCloudflareProvider(TestCase):
|
||||
'fc12ab34cd5611334422ab3322997653')
|
||||
])
|
||||
|
||||
def test_ptr(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
# PTR record
|
||||
ptr_record = Record.new(zone, 'ptr', {
|
||||
'ttl': 300,
|
||||
'type': 'PTR',
|
||||
'value': 'foo.bar.com.'
|
||||
})
|
||||
|
||||
ptr_record_contents = provider._gen_data(ptr_record)
|
||||
self.assertEquals({
|
||||
'name': 'ptr.unit.tests',
|
||||
'ttl': 300,
|
||||
'type': 'PTR',
|
||||
'content': 'foo.bar.com.'
|
||||
}, list(ptr_record_contents)[0])
|
||||
|
||||
def test_srv(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
|
||||
@@ -1223,3 +1279,64 @@ class TestCloudflareProvider(TestCase):
|
||||
provider = CloudflareProvider('test', token='token 123')
|
||||
headers = provider._sess.headers
|
||||
self.assertEquals('Bearer token 123', headers['Authorization'])
|
||||
|
||||
def test_retry_behavior(self):
|
||||
provider = CloudflareProvider('test', token='token 123',
|
||||
email='email 234', retry_period=0)
|
||||
result = {
|
||||
"success": True,
|
||||
"errors": [],
|
||||
"messages": [],
|
||||
"result": [],
|
||||
"result_info": {
|
||||
"count": 1,
|
||||
"per_page": 50
|
||||
}
|
||||
}
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider._request = Mock()
|
||||
|
||||
# No retry required, just calls and is returned
|
||||
provider._zones = None
|
||||
provider._request.reset_mock()
|
||||
provider._request.side_effect = [result]
|
||||
self.assertEquals([], provider.zone_records(zone))
|
||||
provider._request.assert_has_calls([call('GET', '/zones',
|
||||
params={'page': 1})])
|
||||
|
||||
# One retry required
|
||||
provider._zones = None
|
||||
provider._request.reset_mock()
|
||||
provider._request.side_effect = [
|
||||
CloudflareRateLimitError('{}'),
|
||||
result
|
||||
]
|
||||
self.assertEquals([], provider.zone_records(zone))
|
||||
provider._request.assert_has_calls([call('GET', '/zones',
|
||||
params={'page': 1})])
|
||||
|
||||
# Two retries required
|
||||
provider._zones = None
|
||||
provider._request.reset_mock()
|
||||
provider._request.side_effect = [
|
||||
CloudflareRateLimitError('{}'),
|
||||
CloudflareRateLimitError('{}'),
|
||||
result
|
||||
]
|
||||
self.assertEquals([], provider.zone_records(zone))
|
||||
provider._request.assert_has_calls([call('GET', '/zones',
|
||||
params={'page': 1})])
|
||||
|
||||
# # Exhaust our retries
|
||||
provider._zones = None
|
||||
provider._request.reset_mock()
|
||||
provider._request.side_effect = [
|
||||
CloudflareRateLimitError({"errors": [{"message": "first"}]}),
|
||||
CloudflareRateLimitError({"errors": [{"message": "boo"}]}),
|
||||
CloudflareRateLimitError({"errors": [{"message": "boo"}]}),
|
||||
CloudflareRateLimitError({"errors": [{"message": "boo"}]}),
|
||||
CloudflareRateLimitError({"errors": [{"message": "last"}]}),
|
||||
]
|
||||
with self.assertRaises(CloudflareRateLimitError) as ctx:
|
||||
provider.zone_records(zone)
|
||||
self.assertEquals('last', text_type(ctx.exception))
|
||||
|
||||
@@ -717,6 +717,13 @@ class TestNs1ProviderDynamic(TestCase):
|
||||
monitor = provider._monitor_gen(self.record, value)
|
||||
self.assertTrue(monitor['config']['ssl'])
|
||||
|
||||
self.record._octodns['healthcheck']['protocol'] = 'TCP'
|
||||
monitor = provider._monitor_gen(self.record, value)
|
||||
# No http send done
|
||||
self.assertFalse('send' in monitor['config'])
|
||||
# No http response expected
|
||||
self.assertFalse('rules' in monitor)
|
||||
|
||||
def test_monitor_is_match(self):
|
||||
provider = Ns1Provider('test', 'api-key')
|
||||
|
||||
@@ -1059,6 +1066,16 @@ class TestNs1ProviderDynamic(TestCase):
|
||||
call(self.record, '3.4.5.6', 'mid-3'),
|
||||
])
|
||||
|
||||
record = Record.new(self.zone, 'geo', {
|
||||
'ttl': 34,
|
||||
'type': 'A',
|
||||
'values': ['101.102.103.104', '101.102.103.105'],
|
||||
'geo': {'EU': ['201.202.203.204']},
|
||||
'meta': {},
|
||||
})
|
||||
params, _ = provider._params_for_geo_A(record)
|
||||
self.assertEquals([], params['filters'])
|
||||
|
||||
def test_data_for_dynamic_A(self):
|
||||
provider = Ns1Provider('test', 'api-key')
|
||||
|
||||
|
||||
@@ -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.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):
|
||||
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"})
|
||||
|
||||
provider.detect_version()
|
||||
self.assertEquals(provider.version_detected, '4.1.10')
|
||||
|
||||
# Bad auth
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=401, text='Unauthorized')
|
||||
@@ -68,18 +151,19 @@ class TestPowerDnsProvider(TestCase):
|
||||
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.3.0 doesn't populate anything
|
||||
# Non-existent zone in PowerDNS >=4.2.0 doesn't populate anything
|
||||
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404, text='Not Found')
|
||||
|
||||
provider.configure_for_version("4.2.0")
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
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
|
||||
|
||||
@@ -124,7 +208,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
|
||||
@@ -136,6 +220,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
with requests_mock() as mock:
|
||||
provider.configure_for_version('4.2.0')
|
||||
# get 404's, unknown zone
|
||||
mock.get(ANY, status_code=404, text='')
|
||||
# patch 404's, unknown zone
|
||||
@@ -147,10 +232,11 @@ class TestPowerDnsProvider(TestCase):
|
||||
self.assertEquals(expected_n, len(plan.changes))
|
||||
self.assertEquals(expected_n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
provider.configure_for_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))
|
||||
@@ -164,7 +250,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='')
|
||||
|
||||
@@ -174,7 +260,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
|
||||
@@ -195,6 +281,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
|
||||
@@ -266,6 +354,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',
|
||||
@@ -299,6 +389,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))
|
||||
@@ -312,22 +404,3 @@ 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)
|
||||
|
||||
@@ -1213,6 +1213,35 @@ class TestRoute53Provider(TestCase):
|
||||
self.assertEquals('42', id)
|
||||
stubber.assert_no_pending_responses()
|
||||
|
||||
# TCP health check
|
||||
|
||||
health_check_config = {
|
||||
'EnableSNI': False,
|
||||
'FailureThreshold': 6,
|
||||
'MeasureLatency': True,
|
||||
'Port': 8080,
|
||||
'RequestInterval': 10,
|
||||
'Type': 'TCP'
|
||||
}
|
||||
stubber.add_response('create_health_check', {
|
||||
'HealthCheck': {
|
||||
'Id': '42',
|
||||
'CallerReference': self.caller_ref,
|
||||
'HealthCheckConfig': health_check_config,
|
||||
'HealthCheckVersion': 1,
|
||||
},
|
||||
'Location': 'http://url',
|
||||
}, {
|
||||
'CallerReference': ANY,
|
||||
'HealthCheckConfig': health_check_config,
|
||||
})
|
||||
stubber.add_response('change_tags_for_resource', {})
|
||||
|
||||
record._octodns['healthcheck']['protocol'] = 'TCP'
|
||||
id = provider.get_health_check_id(record, 'target-1.unit.tests.', True)
|
||||
self.assertEquals('42', id)
|
||||
stubber.assert_no_pending_responses()
|
||||
|
||||
def test_health_check_measure_latency(self):
|
||||
provider, stubber = self._get_stubbed_provider()
|
||||
record_true = Record.new(self.expected, 'a', {
|
||||
|
||||
@@ -886,6 +886,40 @@ class TestRecord(TestCase):
|
||||
self.assertEquals('HTTPS', new.healthcheck_protocol)
|
||||
self.assertEquals(443, new.healthcheck_port)
|
||||
|
||||
def test_healthcheck_tcp(self):
|
||||
new = Record.new(self.zone, 'a', {
|
||||
'ttl': 44,
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
'octodns': {
|
||||
'healthcheck': {
|
||||
'path': '/ignored',
|
||||
'host': 'completely.ignored',
|
||||
'protocol': 'TCP',
|
||||
'port': 8080,
|
||||
}
|
||||
}
|
||||
})
|
||||
self.assertIsNone(new.healthcheck_path)
|
||||
self.assertIsNone(new.healthcheck_host)
|
||||
self.assertEquals('TCP', new.healthcheck_protocol)
|
||||
self.assertEquals(8080, new.healthcheck_port)
|
||||
|
||||
new = Record.new(self.zone, 'a', {
|
||||
'ttl': 44,
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
'octodns': {
|
||||
'healthcheck': {
|
||||
'protocol': 'TCP',
|
||||
}
|
||||
}
|
||||
})
|
||||
self.assertIsNone(new.healthcheck_path)
|
||||
self.assertIsNone(new.healthcheck_host)
|
||||
self.assertEquals('TCP', new.healthcheck_protocol)
|
||||
self.assertEquals(443, new.healthcheck_port)
|
||||
|
||||
def test_inored(self):
|
||||
new = Record.new(self.zone, 'txt', {
|
||||
'ttl': 44,
|
||||
|
||||
@@ -77,4 +77,6 @@ class TestRecordGeoCodes(TestCase):
|
||||
def test_province_to_code(self):
|
||||
self.assertEquals('NA-US-OR', GeoCodes.province_to_code('OR'))
|
||||
self.assertEquals('NA-US-KY', GeoCodes.province_to_code('KY'))
|
||||
self.assertEquals('NA-CA-AB', GeoCodes.province_to_code('AB'))
|
||||
self.assertEquals('NA-CA-BC', GeoCodes.province_to_code('BC'))
|
||||
self.assertFalse(GeoCodes.province_to_code('XX'))
|
||||
|
||||
Reference in New Issue
Block a user