mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge pull request #303 from github/dynamic-records
Base dynamic record support
This commit is contained in:
@@ -1,3 +1,22 @@
|
||||
## v0.9.4 - ....-..-.. - Dynamic Records Beta
|
||||
|
||||
* Dynamic Records (more powerful/flexible replacement for geo)
|
||||
* Will support A, AAAA, and CNAME out the gate and include the ability to
|
||||
weight records. It should provide a foundation for further suppport
|
||||
if/when needed.
|
||||
* Major refactoring and improvements to validation of the Record hierarchy,
|
||||
things are much more consisntely implemented now and error messages should
|
||||
be more actionable/clear. Both the base values and dynamic values use the
|
||||
same validatio logic.
|
||||
* natsort version bump to address setup issues
|
||||
* DNSSimple TXT record handling fixes, ; it's always ;
|
||||
* Route53Provider support for sessiom tokens
|
||||
* Add ALIAS to the list of Cloudflare record types that support proxying
|
||||
* Fix for TTL bug in Dyn CCA records
|
||||
* Records updated so that 'octodns' record metadata is persisted through
|
||||
YamlProvider
|
||||
* Added --version support to ArguementParser (thus all commands)
|
||||
|
||||
## v0.9.3 - 2018-10-29 - Misc. stuff sort of release
|
||||
|
||||
* ZoneFile source added
|
||||
|
||||
@@ -37,6 +37,13 @@ class _AggregateTarget(object):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def SUPPORTS_DYNAMIC(self):
|
||||
for target in self.targets:
|
||||
if not target.SUPPORTS_DYNAMIC:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class MakeThreadFuture(object):
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ class AzureProvider(BaseProvider):
|
||||
possible to also hard-code into the config file: eg, resource_group.
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT'))
|
||||
|
||||
def __init__(self, id, client_id, key, directory_id, sub_id,
|
||||
|
||||
@@ -59,6 +59,7 @@ class CloudflareProvider(BaseProvider):
|
||||
value: 1.2.3.4
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SRV',
|
||||
'SPF', 'TXT'))
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ class DigitalOceanProvider(BaseProvider):
|
||||
token: foo
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV'))
|
||||
|
||||
def __init__(self, id, token, *args, **kwargs):
|
||||
|
||||
@@ -91,6 +91,7 @@ class DnsimpleProvider(BaseProvider):
|
||||
account: 42
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
||||
'PTR', 'SPF', 'SRV', 'SSHFP', 'TXT'))
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ class DnsMadeEasyProvider(BaseProvider):
|
||||
sandbox: true
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX',
|
||||
'NS', 'PTR', 'SPF', 'SRV', 'TXT'))
|
||||
|
||||
|
||||
@@ -259,6 +259,11 @@ class DynProvider(BaseProvider):
|
||||
def SUPPORTS_GEO(self):
|
||||
return self.traffic_directors_enabled
|
||||
|
||||
@property
|
||||
def SUPPORTS_DYNAMIC(self):
|
||||
# TODO: dynamic
|
||||
return False
|
||||
|
||||
def _check_dyn_sess(self):
|
||||
# We don't have to worry about locking for the check since the
|
||||
# underlying pieces are pre-thread. We can check to see if this thread
|
||||
|
||||
@@ -25,6 +25,7 @@ class EtcHostsProvider(BaseProvider):
|
||||
directory: ./hosts
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME'))
|
||||
|
||||
def __init__(self, id, directory, *args, **kwargs):
|
||||
|
||||
@@ -40,6 +40,7 @@ class GoogleCloudProvider(BaseProvider):
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR',
|
||||
'NS', 'PTR', 'SPF', 'SRV', 'TXT'))
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
|
||||
CHANGE_LOOP_WAIT = 5
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class Ns1Provider(BaseProvider):
|
||||
api_key: env/NS1_API_KEY
|
||||
'''
|
||||
SUPPORTS_GEO = True
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR',
|
||||
'NS', 'PTR', 'SPF', 'SRV', 'TXT'))
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class OvhProvider(BaseProvider):
|
||||
"""
|
||||
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
ZONE_NOT_FOUND_MESSAGE = 'This service does not exist'
|
||||
|
||||
# This variable is also used in populate method to filter which OVH record
|
||||
|
||||
@@ -14,6 +14,7 @@ from .base import BaseProvider
|
||||
|
||||
class PowerDnsBaseProvider(BaseProvider):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
||||
'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
|
||||
TIMEOUT = 5
|
||||
|
||||
@@ -38,6 +38,7 @@ def unescape_semicolon(s):
|
||||
|
||||
class RackspaceProvider(BaseProvider):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NS', 'PTR', 'SPF',
|
||||
'TXT'))
|
||||
TIMEOUT = 5
|
||||
|
||||
@@ -232,6 +232,8 @@ class Route53Provider(BaseProvider):
|
||||
In general the account used will need full permissions on Route53.
|
||||
'''
|
||||
SUPPORTS_GEO = True
|
||||
# TODO: dynamic
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR',
|
||||
'SPF', 'SRV', 'TXT'))
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class YamlProvider(BaseProvider):
|
||||
enforce_order: True
|
||||
'''
|
||||
SUPPORTS_GEO = True
|
||||
SUPPORTS_DYNAMIC = True
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
||||
'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT'))
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ from ipaddress import IPv4Address, IPv6Address
|
||||
from logging import getLogger
|
||||
import re
|
||||
|
||||
from .geo import GeoCodes
|
||||
|
||||
|
||||
class Change(object):
|
||||
|
||||
@@ -253,38 +255,10 @@ class _ValuesMixin(object):
|
||||
@classmethod
|
||||
def validate(cls, name, data):
|
||||
reasons = super(_ValuesMixin, cls).validate(name, data)
|
||||
values = []
|
||||
try:
|
||||
values = data['values']
|
||||
if not values:
|
||||
values = []
|
||||
reasons.append('missing value(s)')
|
||||
else:
|
||||
# loop through copy of values
|
||||
# remove invalid value from values
|
||||
for value in list(values):
|
||||
if value is None:
|
||||
reasons.append('missing value(s)')
|
||||
values.remove(value)
|
||||
elif len(value) == 0:
|
||||
reasons.append('empty value')
|
||||
values.remove(value)
|
||||
except KeyError:
|
||||
try:
|
||||
value = data['value']
|
||||
if value is None:
|
||||
reasons.append('missing value(s)')
|
||||
values = []
|
||||
elif len(value) == 0:
|
||||
reasons.append('empty value')
|
||||
values = []
|
||||
else:
|
||||
values = [value]
|
||||
except KeyError:
|
||||
reasons.append('missing value(s)')
|
||||
|
||||
for value in values:
|
||||
reasons.extend(cls._validate_value(value))
|
||||
values = data.get('values', data.get('value', []))
|
||||
|
||||
reasons.extend(cls._value_type.validate(values, cls._type))
|
||||
|
||||
return reasons
|
||||
|
||||
@@ -294,7 +268,7 @@ class _ValuesMixin(object):
|
||||
values = data['values']
|
||||
except KeyError:
|
||||
values = [data['value']]
|
||||
self.values = sorted(self._process_values(values))
|
||||
self.values = sorted(self._value_type.process(values))
|
||||
|
||||
def changes(self, other, target):
|
||||
if self.values != other.values:
|
||||
@@ -336,19 +310,13 @@ class _GeoMixin(_ValuesMixin):
|
||||
reasons = super(_GeoMixin, cls).validate(name, data)
|
||||
try:
|
||||
geo = dict(data['geo'])
|
||||
# TODO: validate legal codes
|
||||
for code, values in geo.items():
|
||||
reasons.extend(GeoValue._validate_geo(code))
|
||||
for value in values:
|
||||
reasons.extend(cls._validate_value(value))
|
||||
reasons.extend(cls._value_type.validate(values, cls._type))
|
||||
except KeyError:
|
||||
pass
|
||||
return reasons
|
||||
|
||||
# TODO: support 'value' as well
|
||||
# TODO: move away from "data" hash to strict params, it's kind of leaking
|
||||
# the yaml implementation into here and then forcing it back out into
|
||||
# non-yaml providers during input
|
||||
def __init__(self, zone, name, data, *args, **kwargs):
|
||||
super(_GeoMixin, self).__init__(zone, name, data, *args, **kwargs)
|
||||
try:
|
||||
@@ -382,59 +350,18 @@ class _GeoMixin(_ValuesMixin):
|
||||
return super(_GeoMixin, self).__repr__()
|
||||
|
||||
|
||||
class ARecord(_GeoMixin, Record):
|
||||
_type = 'A'
|
||||
|
||||
@classmethod
|
||||
def _validate_value(self, value):
|
||||
reasons = []
|
||||
try:
|
||||
IPv4Address(unicode(value))
|
||||
except Exception:
|
||||
reasons.append('invalid ip address "{}"'.format(value))
|
||||
return reasons
|
||||
|
||||
def _process_values(self, values):
|
||||
return values
|
||||
|
||||
|
||||
class AaaaRecord(_GeoMixin, Record):
|
||||
_type = 'AAAA'
|
||||
|
||||
@classmethod
|
||||
def _validate_value(self, value):
|
||||
reasons = []
|
||||
try:
|
||||
IPv6Address(unicode(value))
|
||||
except Exception:
|
||||
reasons.append('invalid ip address "{}"'.format(value))
|
||||
return reasons
|
||||
|
||||
def _process_values(self, values):
|
||||
return values
|
||||
|
||||
|
||||
class _ValueMixin(object):
|
||||
|
||||
@classmethod
|
||||
def validate(cls, name, data):
|
||||
reasons = super(_ValueMixin, cls).validate(name, data)
|
||||
value = None
|
||||
try:
|
||||
value = data['value']
|
||||
if value is None:
|
||||
reasons.append('missing value')
|
||||
elif value == '':
|
||||
reasons.append('empty value')
|
||||
except KeyError:
|
||||
reasons.append('missing value')
|
||||
if value:
|
||||
reasons.extend(cls._validate_value(value))
|
||||
reasons.extend(cls._value_type.validate(data.get('value', None),
|
||||
cls._type))
|
||||
return reasons
|
||||
|
||||
def __init__(self, zone, name, data, source=None):
|
||||
super(_ValueMixin, self).__init__(zone, name, data, source=source)
|
||||
self.value = self._process_value(data['value'])
|
||||
self.value = self._value_type.process(data['value'])
|
||||
|
||||
def changes(self, other, target):
|
||||
if self.value != other.value:
|
||||
@@ -453,40 +380,359 @@ class _ValueMixin(object):
|
||||
self.fqdn, self.value)
|
||||
|
||||
|
||||
class AliasRecord(_ValueMixin, Record):
|
||||
_type = 'ALIAS'
|
||||
class _DynamicPool(object):
|
||||
|
||||
def __init__(self, _id, data):
|
||||
self._id = _id
|
||||
self.data = data
|
||||
|
||||
def _data(self):
|
||||
return self.data
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.data == other.data
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return '{}'.format(self.data)
|
||||
|
||||
|
||||
class _DynamicRule(object):
|
||||
|
||||
def __init__(self, i, data):
|
||||
self.i = i
|
||||
self.data = data
|
||||
|
||||
def _data(self):
|
||||
return self.data
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.data == other.data
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return '{}'.format(self.data)
|
||||
|
||||
|
||||
class _Dynamic(object):
|
||||
|
||||
def __init__(self, pools, rules):
|
||||
self.pools = pools
|
||||
self.rules = rules
|
||||
|
||||
def _data(self):
|
||||
pools = {}
|
||||
for _id, pool in self.pools.items():
|
||||
pools[_id] = pool._data()
|
||||
rules = []
|
||||
for rule in self.rules:
|
||||
rules.append(rule._data())
|
||||
return {
|
||||
'pools': pools,
|
||||
'rules': rules,
|
||||
}
|
||||
|
||||
def __eq__(self, other):
|
||||
ret = self.pools == other.pools and self.rules == other.rules
|
||||
return ret
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return '{}, {}'.format(self.pools, self.rules)
|
||||
|
||||
|
||||
class _DynamicMixin(object):
|
||||
geo_re = re.compile(r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
|
||||
r'(-(?P<subdivision_code>\w\w))?)?$')
|
||||
|
||||
@classmethod
|
||||
def _validate_value(self, value):
|
||||
reasons = []
|
||||
if not value.endswith('.'):
|
||||
reasons.append('missing trailing .')
|
||||
def validate(cls, name, data):
|
||||
reasons = super(_DynamicMixin, cls).validate(name, data)
|
||||
|
||||
if 'dynamic' not in data:
|
||||
return reasons
|
||||
|
||||
try:
|
||||
pools = data['dynamic']['pools']
|
||||
except KeyError:
|
||||
pools = {}
|
||||
|
||||
if not isinstance(pools, dict):
|
||||
reasons.append('pools must be a dict')
|
||||
elif not pools:
|
||||
reasons.append('missing pools')
|
||||
else:
|
||||
for _id, pool in sorted(pools.items()):
|
||||
if not isinstance(pool, dict):
|
||||
reasons.append('pool "{}" must be a dict'.format(_id))
|
||||
continue
|
||||
try:
|
||||
values = pool['values']
|
||||
except KeyError:
|
||||
reasons.append('pool "{}" is missing values'.format(_id))
|
||||
continue
|
||||
|
||||
for i, value in enumerate(values):
|
||||
value_num = i + 1
|
||||
try:
|
||||
weight = value['weight']
|
||||
weight = int(weight)
|
||||
if weight < 1 or weight > 255:
|
||||
reasons.append('invalid weight "{}" in pool "{}" '
|
||||
'value {}'.format(weight, _id,
|
||||
value_num))
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
reasons.append('invalid weight "{}" in pool "{}" '
|
||||
'value {}'.format(weight, _id,
|
||||
value_num))
|
||||
|
||||
try:
|
||||
value = value['value']
|
||||
reasons.extend(cls._value_type.validate(value,
|
||||
cls._type))
|
||||
except KeyError:
|
||||
reasons.append('missing value in pool "{}" '
|
||||
'value {}'.format(_id, value_num))
|
||||
|
||||
fallback = pool.get('fallback', None)
|
||||
if fallback is not None and fallback not in pools:
|
||||
reasons.append('undefined fallback "{}" for pool "{}"'
|
||||
.format(fallback, _id))
|
||||
|
||||
# Check for loops
|
||||
fallback = pools[_id].get('fallback', None)
|
||||
seen = [_id, fallback]
|
||||
while fallback is not None:
|
||||
# See if there's a next fallback
|
||||
fallback = pools.get(fallback, {}).get('fallback', None)
|
||||
if fallback in seen:
|
||||
loop = ' -> '.join(seen)
|
||||
reasons.append('loop in pool fallbacks: {}'
|
||||
.format(loop))
|
||||
# exit the loop
|
||||
break
|
||||
seen.append(fallback)
|
||||
|
||||
try:
|
||||
rules = data['dynamic']['rules']
|
||||
except KeyError:
|
||||
rules = []
|
||||
|
||||
if not isinstance(rules, (list, tuple)):
|
||||
reasons.append('rules must be a list')
|
||||
elif not rules:
|
||||
reasons.append('missing rules')
|
||||
else:
|
||||
seen_default = False
|
||||
|
||||
for i, rule in enumerate(rules):
|
||||
rule_num = i + 1
|
||||
try:
|
||||
pool = rule['pool']
|
||||
except KeyError:
|
||||
reasons.append('rule {} missing pool'.format(rule_num))
|
||||
continue
|
||||
|
||||
if not isinstance(pool, basestring):
|
||||
reasons.append('rule {} invalid pool "{}"'
|
||||
.format(rule_num, pool))
|
||||
elif pool not in pools:
|
||||
reasons.append('rule {} undefined pool "{}"'
|
||||
.format(rule_num, pool))
|
||||
|
||||
try:
|
||||
geos = rule['geos']
|
||||
except KeyError:
|
||||
geos = []
|
||||
if seen_default:
|
||||
reasons.append('rule {} duplicate default'
|
||||
.format(rule_num))
|
||||
seen_default = True
|
||||
|
||||
if not isinstance(geos, (list, tuple)):
|
||||
reasons.append('rule {} geos must be a list'
|
||||
.format(rule_num))
|
||||
else:
|
||||
for geo in geos:
|
||||
reasons.extend(GeoCodes.validate(geo, 'rule {} '
|
||||
.format(rule_num)))
|
||||
|
||||
return reasons
|
||||
|
||||
def _process_value(self, value):
|
||||
def __init__(self, zone, name, data, *args, **kwargs):
|
||||
super(_DynamicMixin, self).__init__(zone, name, data, *args,
|
||||
**kwargs)
|
||||
|
||||
self.dynamic = {}
|
||||
|
||||
if 'dynamic' not in data:
|
||||
return
|
||||
|
||||
# pools
|
||||
try:
|
||||
pools = dict(data['dynamic']['pools'])
|
||||
except:
|
||||
pools = {}
|
||||
|
||||
for _id, pool in sorted(pools.items()):
|
||||
pools[_id] = _DynamicPool(_id, pool)
|
||||
|
||||
# rules
|
||||
try:
|
||||
rules = list(data['dynamic']['rules'])
|
||||
except:
|
||||
rules = []
|
||||
|
||||
parsed = []
|
||||
for i, rule in enumerate(rules):
|
||||
parsed.append(_DynamicRule(i, rule))
|
||||
|
||||
# dynamic
|
||||
self.dynamic = _Dynamic(pools, parsed)
|
||||
|
||||
def _data(self):
|
||||
ret = super(_DynamicMixin, self)._data()
|
||||
if self.dynamic:
|
||||
ret['dynamic'] = self.dynamic._data()
|
||||
return ret
|
||||
|
||||
def changes(self, other, target):
|
||||
if target.SUPPORTS_DYNAMIC:
|
||||
if self.dynamic != other.dynamic:
|
||||
return Update(self, other)
|
||||
return super(_DynamicMixin, self).changes(other, target)
|
||||
|
||||
def __repr__(self):
|
||||
# TODO: improve this whole thing, we need multi-line...
|
||||
if self.dynamic:
|
||||
# TODO: this hack can't going to cut it, as part of said
|
||||
# improvements the value types should deal with serializing their
|
||||
# value
|
||||
try:
|
||||
values = self.values
|
||||
except AttributeError:
|
||||
values = self.value
|
||||
|
||||
return '<{} {} {}, {}, {}, {}>'.format(self.__class__.__name__,
|
||||
self._type, self.ttl,
|
||||
self.fqdn, values,
|
||||
self.dynamic)
|
||||
return super(_DynamicMixin, self).__repr__()
|
||||
|
||||
|
||||
class _IpList(object):
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data, _type):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
if len(data) == 0:
|
||||
return ['missing value(s)']
|
||||
reasons = []
|
||||
for value in data:
|
||||
if value is '':
|
||||
reasons.append('empty value')
|
||||
elif value is None:
|
||||
reasons.append('missing value(s)')
|
||||
else:
|
||||
try:
|
||||
cls._address_type(unicode(value))
|
||||
except Exception:
|
||||
reasons.append('invalid {} address "{}"'
|
||||
.format(cls._address_name, value))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return values
|
||||
|
||||
|
||||
class Ipv4List(_IpList):
|
||||
_address_name = 'IPv4'
|
||||
_address_type = IPv4Address
|
||||
|
||||
|
||||
class Ipv6List(_IpList):
|
||||
_address_name = 'IPv6'
|
||||
_address_type = IPv6Address
|
||||
|
||||
|
||||
class _TargetValue(object):
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data, _type):
|
||||
reasons = []
|
||||
if data == '':
|
||||
reasons.append('empty value')
|
||||
elif not data:
|
||||
reasons.append('missing value')
|
||||
elif not data.endswith('.'):
|
||||
reasons.append('{} value "{}" missing trailing .'
|
||||
.format(_type, data))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class CnameValue(_TargetValue):
|
||||
pass
|
||||
|
||||
|
||||
class ARecord(_DynamicMixin, _GeoMixin, Record):
|
||||
_type = 'A'
|
||||
_value_type = Ipv4List
|
||||
|
||||
|
||||
class AaaaRecord(_DynamicMixin, _GeoMixin, Record):
|
||||
_type = 'AAAA'
|
||||
_value_type = Ipv6List
|
||||
|
||||
|
||||
class AliasValue(_TargetValue):
|
||||
pass
|
||||
|
||||
|
||||
class AliasRecord(_ValueMixin, Record):
|
||||
_type = 'ALIAS'
|
||||
_value_type = AliasValue
|
||||
|
||||
|
||||
class CaaValue(object):
|
||||
# https://tools.ietf.org/html/rfc6844#page-5
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
def validate(cls, data, _type):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
try:
|
||||
flags = int(value.get('flags', 0))
|
||||
if flags < 0 or flags > 255:
|
||||
reasons.append('invalid flags "{}"'.format(flags))
|
||||
except ValueError:
|
||||
reasons.append('invalid flags "{}"'.format(value['flags']))
|
||||
|
||||
if 'tag' not in value:
|
||||
reasons.append('missing tag')
|
||||
if 'value' not in value:
|
||||
reasons.append('missing value')
|
||||
for value in data:
|
||||
try:
|
||||
flags = int(value.get('flags', 0))
|
||||
if flags < 0 or flags > 255:
|
||||
reasons.append('invalid flags "{}"'.format(flags))
|
||||
except ValueError:
|
||||
reasons.append('invalid flags "{}"'.format(value['flags']))
|
||||
|
||||
if 'tag' not in value:
|
||||
reasons.append('missing tag')
|
||||
if 'value' not in value:
|
||||
reasons.append('missing value')
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return [CaaValue(v) for v in values]
|
||||
|
||||
def __init__(self, value):
|
||||
self.flags = int(value.get('flags', 0))
|
||||
self.tag = value['tag']
|
||||
@@ -513,17 +759,12 @@ class CaaValue(object):
|
||||
|
||||
class CaaRecord(_ValuesMixin, Record):
|
||||
_type = 'CAA'
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
return CaaValue._validate_value(value)
|
||||
|
||||
def _process_values(self, values):
|
||||
return [CaaValue(v) for v in values]
|
||||
_value_type = CaaValue
|
||||
|
||||
|
||||
class CnameRecord(_ValueMixin, Record):
|
||||
class CnameRecord(_DynamicMixin, _ValueMixin, Record):
|
||||
_type = 'CNAME'
|
||||
_value_type = CnameValue
|
||||
|
||||
@classmethod
|
||||
def validate(cls, name, data):
|
||||
@@ -533,41 +774,39 @@ class CnameRecord(_ValueMixin, Record):
|
||||
reasons.extend(super(CnameRecord, cls).validate(name, data))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
reasons = []
|
||||
if not value.endswith('.'):
|
||||
reasons.append('missing trailing .')
|
||||
return reasons
|
||||
|
||||
def _process_value(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class MxValue(object):
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
def validate(cls, data, _type):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
try:
|
||||
for value in data:
|
||||
try:
|
||||
int(value['preference'])
|
||||
try:
|
||||
int(value['preference'])
|
||||
except KeyError:
|
||||
int(value['priority'])
|
||||
except KeyError:
|
||||
int(value['priority'])
|
||||
except KeyError:
|
||||
reasons.append('missing preference')
|
||||
except ValueError:
|
||||
reasons.append('invalid preference "{}"'
|
||||
.format(value['preference']))
|
||||
exchange = None
|
||||
try:
|
||||
exchange = value.get('exchange', None) or value['value']
|
||||
if not exchange.endswith('.'):
|
||||
reasons.append('missing trailing .')
|
||||
except KeyError:
|
||||
reasons.append('missing exchange')
|
||||
reasons.append('missing preference')
|
||||
except ValueError:
|
||||
reasons.append('invalid preference "{}"'
|
||||
.format(value['preference']))
|
||||
exchange = None
|
||||
try:
|
||||
exchange = value.get('exchange', None) or value['value']
|
||||
if not exchange.endswith('.'):
|
||||
reasons.append('MX value "{}" missing trailing .'
|
||||
.format(exchange))
|
||||
except KeyError:
|
||||
reasons.append('missing exchange')
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return [MxValue(v) for v in values]
|
||||
|
||||
def __init__(self, value):
|
||||
# RFC1035 says preference, half the providers use priority
|
||||
try:
|
||||
@@ -600,47 +839,49 @@ class MxValue(object):
|
||||
|
||||
class MxRecord(_ValuesMixin, Record):
|
||||
_type = 'MX'
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
return MxValue._validate_value(value)
|
||||
|
||||
def _process_values(self, values):
|
||||
return [MxValue(v) for v in values]
|
||||
_value_type = MxValue
|
||||
|
||||
|
||||
class NaptrValue(object):
|
||||
VALID_FLAGS = ('S', 'A', 'U', 'P')
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, data):
|
||||
def validate(cls, data, _type):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
try:
|
||||
int(data['order'])
|
||||
except KeyError:
|
||||
reasons.append('missing order')
|
||||
except ValueError:
|
||||
reasons.append('invalid order "{}"'.format(data['order']))
|
||||
try:
|
||||
int(data['preference'])
|
||||
except KeyError:
|
||||
reasons.append('missing preference')
|
||||
except ValueError:
|
||||
reasons.append('invalid preference "{}"'
|
||||
.format(data['preference']))
|
||||
try:
|
||||
flags = data['flags']
|
||||
if flags not in cls.VALID_FLAGS:
|
||||
reasons.append('unrecognized flags "{}"'.format(flags))
|
||||
except KeyError:
|
||||
reasons.append('missing flags')
|
||||
for value in data:
|
||||
try:
|
||||
int(value['order'])
|
||||
except KeyError:
|
||||
reasons.append('missing order')
|
||||
except ValueError:
|
||||
reasons.append('invalid order "{}"'.format(value['order']))
|
||||
try:
|
||||
int(value['preference'])
|
||||
except KeyError:
|
||||
reasons.append('missing preference')
|
||||
except ValueError:
|
||||
reasons.append('invalid preference "{}"'
|
||||
.format(value['preference']))
|
||||
try:
|
||||
flags = value['flags']
|
||||
if flags not in cls.VALID_FLAGS:
|
||||
reasons.append('unrecognized flags "{}"'.format(flags))
|
||||
except KeyError:
|
||||
reasons.append('missing flags')
|
||||
|
||||
# TODO: validate these... they're non-trivial
|
||||
for k in ('service', 'regexp', 'replacement'):
|
||||
if k not in value:
|
||||
reasons.append('missing {}'.format(k))
|
||||
|
||||
# TODO: validate these... they're non-trivial
|
||||
for k in ('service', 'regexp', 'replacement'):
|
||||
if k not in data:
|
||||
reasons.append('missing {}'.format(k))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return [NaptrValue(v) for v in values]
|
||||
|
||||
def __init__(self, value):
|
||||
self.order = int(value['order'])
|
||||
self.preference = int(value['preference'])
|
||||
@@ -684,41 +925,41 @@ class NaptrValue(object):
|
||||
|
||||
class NaptrRecord(_ValuesMixin, Record):
|
||||
_type = 'NAPTR'
|
||||
_value_type = NaptrValue
|
||||
|
||||
|
||||
class _NsValue(object):
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
return NaptrValue._validate_value(value)
|
||||
def validate(cls, data, _type):
|
||||
if not data:
|
||||
return ['missing value(s)']
|
||||
elif not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
for value in data:
|
||||
if not value.endswith('.'):
|
||||
reasons.append('NS value "{}" missing trailing .'
|
||||
.format(value))
|
||||
return reasons
|
||||
|
||||
def _process_values(self, values):
|
||||
return [NaptrValue(v) for v in values]
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return values
|
||||
|
||||
|
||||
class NsRecord(_ValuesMixin, Record):
|
||||
_type = 'NS'
|
||||
_value_type = _NsValue
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
reasons = []
|
||||
if not value.endswith('.'):
|
||||
reasons.append('missing trailing .')
|
||||
return reasons
|
||||
|
||||
def _process_values(self, values):
|
||||
return values
|
||||
class PtrValue(_TargetValue):
|
||||
pass
|
||||
|
||||
|
||||
class PtrRecord(_ValueMixin, Record):
|
||||
_type = 'PTR'
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
reasons = []
|
||||
if not value.endswith('.'):
|
||||
reasons.append('missing trailing .')
|
||||
return reasons
|
||||
|
||||
def _process_value(self, value):
|
||||
return value
|
||||
_value_type = PtrValue
|
||||
|
||||
|
||||
class SshfpValue(object):
|
||||
@@ -726,30 +967,39 @@ class SshfpValue(object):
|
||||
VALID_FINGERPRINT_TYPES = (1, 2)
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
def validate(cls, data, _type):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
try:
|
||||
algorithm = int(value['algorithm'])
|
||||
if algorithm not in cls.VALID_ALGORITHMS:
|
||||
reasons.append('unrecognized algorithm "{}"'.format(algorithm))
|
||||
except KeyError:
|
||||
reasons.append('missing algorithm')
|
||||
except ValueError:
|
||||
reasons.append('invalid algorithm "{}"'.format(value['algorithm']))
|
||||
try:
|
||||
fingerprint_type = int(value['fingerprint_type'])
|
||||
if fingerprint_type not in cls.VALID_FINGERPRINT_TYPES:
|
||||
reasons.append('unrecognized fingerprint_type "{}"'
|
||||
.format(fingerprint_type))
|
||||
except KeyError:
|
||||
reasons.append('missing fingerprint_type')
|
||||
except ValueError:
|
||||
reasons.append('invalid fingerprint_type "{}"'
|
||||
.format(value['fingerprint_type']))
|
||||
if 'fingerprint' not in value:
|
||||
reasons.append('missing fingerprint')
|
||||
for value in data:
|
||||
try:
|
||||
algorithm = int(value['algorithm'])
|
||||
if algorithm not in cls.VALID_ALGORITHMS:
|
||||
reasons.append('unrecognized algorithm "{}"'
|
||||
.format(algorithm))
|
||||
except KeyError:
|
||||
reasons.append('missing algorithm')
|
||||
except ValueError:
|
||||
reasons.append('invalid algorithm "{}"'
|
||||
.format(value['algorithm']))
|
||||
try:
|
||||
fingerprint_type = int(value['fingerprint_type'])
|
||||
if fingerprint_type not in cls.VALID_FINGERPRINT_TYPES:
|
||||
reasons.append('unrecognized fingerprint_type "{}"'
|
||||
.format(fingerprint_type))
|
||||
except KeyError:
|
||||
reasons.append('missing fingerprint_type')
|
||||
except ValueError:
|
||||
reasons.append('invalid fingerprint_type "{}"'
|
||||
.format(value['fingerprint_type']))
|
||||
if 'fingerprint' not in value:
|
||||
reasons.append('missing fingerprint')
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return [SshfpValue(v) for v in values]
|
||||
|
||||
def __init__(self, value):
|
||||
self.algorithm = int(value['algorithm'])
|
||||
self.fingerprint_type = int(value['fingerprint_type'])
|
||||
@@ -777,34 +1027,12 @@ class SshfpValue(object):
|
||||
|
||||
class SshfpRecord(_ValuesMixin, Record):
|
||||
_type = 'SSHFP'
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
return SshfpValue._validate_value(value)
|
||||
|
||||
def _process_values(self, values):
|
||||
return [SshfpValue(v) for v in values]
|
||||
|
||||
|
||||
_unescaped_semicolon_re = re.compile(r'\w;')
|
||||
_value_type = SshfpValue
|
||||
|
||||
|
||||
class _ChunkedValuesMixin(_ValuesMixin):
|
||||
CHUNK_SIZE = 255
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
if _unescaped_semicolon_re.search(value):
|
||||
return ['unescaped ;']
|
||||
return []
|
||||
|
||||
def _process_values(self, values):
|
||||
ret = []
|
||||
for v in values:
|
||||
if v and v[0] == '"':
|
||||
v = v[1:-1]
|
||||
ret.append(v.replace('" "', ''))
|
||||
return ret
|
||||
_unescaped_semicolon_re = re.compile(r'\w;')
|
||||
|
||||
@property
|
||||
def chunked_values(self):
|
||||
@@ -818,41 +1046,76 @@ class _ChunkedValuesMixin(_ValuesMixin):
|
||||
return values
|
||||
|
||||
|
||||
class _ChunkedValue(object):
|
||||
_unescaped_semicolon_re = re.compile(r'\w;')
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data, _type):
|
||||
if not data:
|
||||
return ['missing value(s)']
|
||||
elif not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
for value in data:
|
||||
if cls._unescaped_semicolon_re.search(value):
|
||||
reasons.append('unescaped ; in "{}"'.format(value))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
ret = []
|
||||
for v in values:
|
||||
if v and v[0] == '"':
|
||||
v = v[1:-1]
|
||||
ret.append(v.replace('" "', ''))
|
||||
return ret
|
||||
|
||||
|
||||
class SpfRecord(_ChunkedValuesMixin, Record):
|
||||
_type = 'SPF'
|
||||
_value_type = _ChunkedValue
|
||||
|
||||
|
||||
class SrvValue(object):
|
||||
|
||||
@classmethod
|
||||
def _validate_value(self, value):
|
||||
def validate(cls, data, _type):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
# TODO: validate algorithm and fingerprint_type values
|
||||
try:
|
||||
int(value['priority'])
|
||||
except KeyError:
|
||||
reasons.append('missing priority')
|
||||
except ValueError:
|
||||
reasons.append('invalid priority "{}"'.format(value['priority']))
|
||||
try:
|
||||
int(value['weight'])
|
||||
except KeyError:
|
||||
reasons.append('missing weight')
|
||||
except ValueError:
|
||||
reasons.append('invalid weight "{}"'.format(value['weight']))
|
||||
try:
|
||||
int(value['port'])
|
||||
except KeyError:
|
||||
reasons.append('missing port')
|
||||
except ValueError:
|
||||
reasons.append('invalid port "{}"'.format(value['port']))
|
||||
try:
|
||||
if not value['target'].endswith('.'):
|
||||
reasons.append('missing trailing .')
|
||||
except KeyError:
|
||||
reasons.append('missing target')
|
||||
for value in data:
|
||||
# TODO: validate algorithm and fingerprint_type values
|
||||
try:
|
||||
int(value['priority'])
|
||||
except KeyError:
|
||||
reasons.append('missing priority')
|
||||
except ValueError:
|
||||
reasons.append('invalid priority "{}"'
|
||||
.format(value['priority']))
|
||||
try:
|
||||
int(value['weight'])
|
||||
except KeyError:
|
||||
reasons.append('missing weight')
|
||||
except ValueError:
|
||||
reasons.append('invalid weight "{}"'.format(value['weight']))
|
||||
try:
|
||||
int(value['port'])
|
||||
except KeyError:
|
||||
reasons.append('missing port')
|
||||
except ValueError:
|
||||
reasons.append('invalid port "{}"'.format(value['port']))
|
||||
try:
|
||||
if not value['target'].endswith('.'):
|
||||
reasons.append('SRV value "{}" missing trailing .'
|
||||
.format(value['target']))
|
||||
except KeyError:
|
||||
reasons.append('missing target')
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return [SrvValue(v) for v in values]
|
||||
|
||||
def __init__(self, value):
|
||||
self.priority = int(value['priority'])
|
||||
self.weight = int(value['weight'])
|
||||
@@ -884,6 +1147,7 @@ class SrvValue(object):
|
||||
|
||||
class SrvRecord(_ValuesMixin, Record):
|
||||
_type = 'SRV'
|
||||
_value_type = SrvValue
|
||||
_name_re = re.compile(r'^_[^\.]+\.[^\.]+')
|
||||
|
||||
@classmethod
|
||||
@@ -894,13 +1158,11 @@ class SrvRecord(_ValuesMixin, Record):
|
||||
reasons.extend(super(SrvRecord, cls).validate(name, data))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
return SrvValue._validate_value(value)
|
||||
|
||||
def _process_values(self, values):
|
||||
return [SrvValue(v) for v in values]
|
||||
class _TxtValue(_ChunkedValue):
|
||||
pass
|
||||
|
||||
|
||||
class TxtRecord(_ChunkedValuesMixin, Record):
|
||||
_type = 'TXT'
|
||||
_value_type = _TxtValue
|
||||
@@ -0,0 +1,35 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from .geo_data import geo_data
|
||||
|
||||
|
||||
class GeoCodes(object):
|
||||
__COUNTRIES = None
|
||||
|
||||
@classmethod
|
||||
def validate(cls, code, prefix):
|
||||
'''
|
||||
Validates an octoDNS geo code making sure that it is a valid and
|
||||
corresponding:
|
||||
* continent
|
||||
* continent & country
|
||||
* continent, country, & province
|
||||
'''
|
||||
reasons = []
|
||||
|
||||
pieces = code.split('-')
|
||||
n = len(pieces)
|
||||
if n > 3:
|
||||
reasons.append('{}invalid geo code "{}"'.format(prefix, code))
|
||||
elif n > 0 and pieces[0] not in geo_data:
|
||||
reasons.append('{}unknown continent code "{}"'
|
||||
.format(prefix, code))
|
||||
elif n > 1 and pieces[1] not in geo_data[pieces[0]]:
|
||||
reasons.append('{}unknown country code "{}"'.format(prefix, code))
|
||||
elif n > 2 and \
|
||||
pieces[2] not in geo_data[pieces[0]][pieces[1]]['provinces']:
|
||||
reasons.append('{}unknown province code "{}"'.format(prefix, code))
|
||||
|
||||
return reasons
|
||||
@@ -0,0 +1,316 @@
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is generated using
|
||||
# ./script/generate-geo-data > octodns/record/geo_data.py
|
||||
# do not modify it directly
|
||||
#
|
||||
|
||||
geo_data = \
|
||||
{'AF': {'AO': {'name': 'Angola'},
|
||||
'BF': {'name': 'Burkina Faso'},
|
||||
'BI': {'name': 'Burundi'},
|
||||
'BJ': {'name': 'Benin'},
|
||||
'BW': {'name': 'Botswana'},
|
||||
'CD': {'name': 'Congo, The Democratic Republic of the'},
|
||||
'CF': {'name': 'Central African Republic'},
|
||||
'CG': {'name': 'Congo'},
|
||||
'CI': {'name': "Côte d'Ivoire"},
|
||||
'CM': {'name': 'Cameroon'},
|
||||
'CV': {'name': 'Cabo Verde'},
|
||||
'DJ': {'name': 'Djibouti'},
|
||||
'DZ': {'name': 'Algeria'},
|
||||
'EG': {'name': 'Egypt'},
|
||||
'EH': {'name': 'Western Sahara'},
|
||||
'ER': {'name': 'Eritrea'},
|
||||
'ET': {'name': 'Ethiopia'},
|
||||
'GA': {'name': 'Gabon'},
|
||||
'GH': {'name': 'Ghana'},
|
||||
'GM': {'name': 'Gambia'},
|
||||
'GN': {'name': 'Guinea'},
|
||||
'GQ': {'name': 'Equatorial Guinea'},
|
||||
'GW': {'name': 'Guinea-Bissau'},
|
||||
'KE': {'name': 'Kenya'},
|
||||
'KM': {'name': 'Comoros'},
|
||||
'LR': {'name': 'Liberia'},
|
||||
'LS': {'name': 'Lesotho'},
|
||||
'LY': {'name': 'Libya'},
|
||||
'MA': {'name': 'Morocco'},
|
||||
'MG': {'name': 'Madagascar'},
|
||||
'ML': {'name': 'Mali'},
|
||||
'MR': {'name': 'Mauritania'},
|
||||
'MU': {'name': 'Mauritius'},
|
||||
'MW': {'name': 'Malawi'},
|
||||
'MZ': {'name': 'Mozambique'},
|
||||
'NA': {'name': 'Namibia'},
|
||||
'NE': {'name': 'Niger'},
|
||||
'NG': {'name': 'Nigeria'},
|
||||
'RE': {'name': 'Réunion'},
|
||||
'RW': {'name': 'Rwanda'},
|
||||
'SC': {'name': 'Seychelles'},
|
||||
'SD': {'name': 'Sudan'},
|
||||
'SH': {'name': 'Saint Helena, Ascension and Tristan da Cunha'},
|
||||
'SL': {'name': 'Sierra Leone'},
|
||||
'SN': {'name': 'Senegal'},
|
||||
'SO': {'name': 'Somalia'},
|
||||
'SS': {'name': 'South Sudan'},
|
||||
'ST': {'name': 'Sao Tome and Principe'},
|
||||
'SZ': {'name': 'Swaziland'},
|
||||
'TD': {'name': 'Chad'},
|
||||
'TG': {'name': 'Togo'},
|
||||
'TN': {'name': 'Tunisia'},
|
||||
'TZ': {'name': 'Tanzania, United Republic of'},
|
||||
'UG': {'name': 'Uganda'},
|
||||
'YT': {'name': 'Mayotte'},
|
||||
'ZA': {'name': 'South Africa'},
|
||||
'ZM': {'name': 'Zambia'},
|
||||
'ZW': {'name': 'Zimbabwe'}},
|
||||
'AN': {'AQ': {'name': 'Antarctica'},
|
||||
'BV': {'name': 'Bouvet Island'},
|
||||
'HM': {'name': 'Heard Island and McDonald Islands'},
|
||||
'TF': {'name': 'French Southern Territories'}},
|
||||
'AS': {'AE': {'name': 'United Arab Emirates'},
|
||||
'AF': {'name': 'Afghanistan'},
|
||||
'AM': {'name': 'Armenia'},
|
||||
'AZ': {'name': 'Azerbaijan'},
|
||||
'BD': {'name': 'Bangladesh'},
|
||||
'BH': {'name': 'Bahrain'},
|
||||
'BN': {'name': 'Brunei Darussalam'},
|
||||
'BT': {'name': 'Bhutan'},
|
||||
'CC': {'name': 'Cocos (Keeling) Islands'},
|
||||
'CN': {'name': 'China'},
|
||||
'CX': {'name': 'Christmas Island'},
|
||||
'CY': {'name': 'Cyprus'},
|
||||
'GE': {'name': 'Georgia'},
|
||||
'HK': {'name': 'Hong Kong'},
|
||||
'ID': {'name': 'Indonesia'},
|
||||
'IL': {'name': 'Israel'},
|
||||
'IN': {'name': 'India'},
|
||||
'IO': {'name': 'British Indian Ocean Territory'},
|
||||
'IQ': {'name': 'Iraq'},
|
||||
'IR': {'name': 'Iran, Islamic Republic of'},
|
||||
'JO': {'name': 'Jordan'},
|
||||
'JP': {'name': 'Japan'},
|
||||
'KG': {'name': 'Kyrgyzstan'},
|
||||
'KH': {'name': 'Cambodia'},
|
||||
'KP': {'name': "Korea, Democratic People's Republic of"},
|
||||
'KR': {'name': 'Korea, Republic of'},
|
||||
'KW': {'name': 'Kuwait'},
|
||||
'KZ': {'name': 'Kazakhstan'},
|
||||
'LA': {'name': "Lao People's Democratic Republic"},
|
||||
'LB': {'name': 'Lebanon'},
|
||||
'LK': {'name': 'Sri Lanka'},
|
||||
'MM': {'name': 'Myanmar'},
|
||||
'MN': {'name': 'Mongolia'},
|
||||
'MO': {'name': 'Macao'},
|
||||
'MV': {'name': 'Maldives'},
|
||||
'MY': {'name': 'Malaysia'},
|
||||
'NP': {'name': 'Nepal'},
|
||||
'OM': {'name': 'Oman'},
|
||||
'PH': {'name': 'Philippines'},
|
||||
'PK': {'name': 'Pakistan'},
|
||||
'PS': {'name': 'Palestine, State of'},
|
||||
'QA': {'name': 'Qatar'},
|
||||
'SA': {'name': 'Saudi Arabia'},
|
||||
'SG': {'name': 'Singapore'},
|
||||
'SY': {'name': 'Syrian Arab Republic'},
|
||||
'TH': {'name': 'Thailand'},
|
||||
'TJ': {'name': 'Tajikistan'},
|
||||
'TM': {'name': 'Turkmenistan'},
|
||||
'TR': {'name': 'Turkey'},
|
||||
'TW': {'name': 'Taiwan, Province of China'},
|
||||
'UZ': {'name': 'Uzbekistan'},
|
||||
'VN': {'name': 'Viet Nam'},
|
||||
'YE': {'name': 'Yemen'}},
|
||||
'EU': {'AD': {'name': 'Andorra'},
|
||||
'AL': {'name': 'Albania'},
|
||||
'AT': {'name': 'Austria'},
|
||||
'AX': {'name': 'Åland Islands'},
|
||||
'BA': {'name': 'Bosnia and Herzegovina'},
|
||||
'BE': {'name': 'Belgium'},
|
||||
'BG': {'name': 'Bulgaria'},
|
||||
'BY': {'name': 'Belarus'},
|
||||
'CH': {'name': 'Switzerland'},
|
||||
'CZ': {'name': 'Czechia'},
|
||||
'DE': {'name': 'Germany'},
|
||||
'DK': {'name': 'Denmark'},
|
||||
'EE': {'name': 'Estonia'},
|
||||
'ES': {'name': 'Spain'},
|
||||
'FI': {'name': 'Finland'},
|
||||
'FO': {'name': 'Faroe Islands'},
|
||||
'FR': {'name': 'France'},
|
||||
'GB': {'name': 'United Kingdom'},
|
||||
'GG': {'name': 'Guernsey'},
|
||||
'GI': {'name': 'Gibraltar'},
|
||||
'GR': {'name': 'Greece'},
|
||||
'HR': {'name': 'Croatia'},
|
||||
'HU': {'name': 'Hungary'},
|
||||
'IE': {'name': 'Ireland'},
|
||||
'IM': {'name': 'Isle of Man'},
|
||||
'IS': {'name': 'Iceland'},
|
||||
'IT': {'name': 'Italy'},
|
||||
'JE': {'name': 'Jersey'},
|
||||
'LI': {'name': 'Liechtenstein'},
|
||||
'LT': {'name': 'Lithuania'},
|
||||
'LU': {'name': 'Luxembourg'},
|
||||
'LV': {'name': 'Latvia'},
|
||||
'MC': {'name': 'Monaco'},
|
||||
'MD': {'name': 'Moldova, Republic of'},
|
||||
'ME': {'name': 'Montenegro'},
|
||||
'MK': {'name': 'Macedonia, Republic of'},
|
||||
'MT': {'name': 'Malta'},
|
||||
'NL': {'name': 'Netherlands'},
|
||||
'NO': {'name': 'Norway'},
|
||||
'PL': {'name': 'Poland'},
|
||||
'PT': {'name': 'Portugal'},
|
||||
'RO': {'name': 'Romania'},
|
||||
'RS': {'name': 'Serbia'},
|
||||
'RU': {'name': 'Russian Federation'},
|
||||
'SE': {'name': 'Sweden'},
|
||||
'SI': {'name': 'Slovenia'},
|
||||
'SJ': {'name': 'Svalbard and Jan Mayen'},
|
||||
'SK': {'name': 'Slovakia'},
|
||||
'SM': {'name': 'San Marino'},
|
||||
'UA': {'name': 'Ukraine'},
|
||||
'VA': {'name': 'Holy See (Vatican City State)'}},
|
||||
'ID': {'TL': {'name': 'Timor-Leste'}},
|
||||
'NA': {'AG': {'name': 'Antigua and Barbuda'},
|
||||
'AI': {'name': 'Anguilla'},
|
||||
'AW': {'name': 'Aruba'},
|
||||
'BB': {'name': 'Barbados'},
|
||||
'BL': {'name': 'Saint Barthélemy'},
|
||||
'BM': {'name': 'Bermuda'},
|
||||
'BQ': {'name': 'Bonaire, Sint Eustatius and Saba'},
|
||||
'BS': {'name': 'Bahamas'},
|
||||
'BZ': {'name': 'Belize'},
|
||||
'CA': {'name': 'Canada'},
|
||||
'CR': {'name': 'Costa Rica'},
|
||||
'CU': {'name': 'Cuba'},
|
||||
'CW': {'name': 'Curaçao'},
|
||||
'DM': {'name': 'Dominica'},
|
||||
'DO': {'name': 'Dominican Republic'},
|
||||
'GD': {'name': 'Grenada'},
|
||||
'GL': {'name': 'Greenland'},
|
||||
'GP': {'name': 'Guadeloupe'},
|
||||
'GT': {'name': 'Guatemala'},
|
||||
'HN': {'name': 'Honduras'},
|
||||
'HT': {'name': 'Haiti'},
|
||||
'JM': {'name': 'Jamaica'},
|
||||
'KN': {'name': 'Saint Kitts and Nevis'},
|
||||
'KY': {'name': 'Cayman Islands'},
|
||||
'LC': {'name': 'Saint Lucia'},
|
||||
'MF': {'name': 'Saint Martin (French part)'},
|
||||
'MQ': {'name': 'Martinique'},
|
||||
'MS': {'name': 'Montserrat'},
|
||||
'MX': {'name': 'Mexico'},
|
||||
'NI': {'name': 'Nicaragua'},
|
||||
'PA': {'name': 'Panama'},
|
||||
'PM': {'name': 'Saint Pierre and Miquelon'},
|
||||
'PR': {'name': 'Puerto Rico'},
|
||||
'SV': {'name': 'El Salvador'},
|
||||
'SX': {'name': 'Sint Maarten (Dutch part)'},
|
||||
'TC': {'name': 'Turks and Caicos Islands'},
|
||||
'TT': {'name': 'Trinidad and Tobago'},
|
||||
'US': {'name': 'United States',
|
||||
'provinces': {'AK': {'name': 'Alaska'},
|
||||
'AL': {'name': 'Alabama'},
|
||||
'AR': {'name': 'Arkansas'},
|
||||
'AS': {'name': 'American Samoa'},
|
||||
'AZ': {'name': 'Arizona'},
|
||||
'CA': {'name': 'California'},
|
||||
'CO': {'name': 'Colorado'},
|
||||
'CT': {'name': 'Connecticut'},
|
||||
'DC': {'name': 'District of Columbia'},
|
||||
'DE': {'name': 'Delaware'},
|
||||
'FL': {'name': 'Florida'},
|
||||
'GA': {'name': 'Georgia'},
|
||||
'GU': {'name': 'Guam'},
|
||||
'HI': {'name': 'Hawaii'},
|
||||
'IA': {'name': 'Iowa'},
|
||||
'ID': {'name': 'Idaho'},
|
||||
'IL': {'name': 'Illinois'},
|
||||
'IN': {'name': 'Indiana'},
|
||||
'KS': {'name': 'Kansas'},
|
||||
'KY': {'name': 'Kentucky'},
|
||||
'LA': {'name': 'Louisiana'},
|
||||
'MA': {'name': 'Massachusetts'},
|
||||
'MD': {'name': 'Maryland'},
|
||||
'ME': {'name': 'Maine'},
|
||||
'MI': {'name': 'Michigan'},
|
||||
'MN': {'name': 'Minnesota'},
|
||||
'MO': {'name': 'Missouri'},
|
||||
'MP': {'name': 'Northern Mariana Islands'},
|
||||
'MS': {'name': 'Mississippi'},
|
||||
'MT': {'name': 'Montana'},
|
||||
'NC': {'name': 'North Carolina'},
|
||||
'ND': {'name': 'North Dakota'},
|
||||
'NE': {'name': 'Nebraska'},
|
||||
'NH': {'name': 'New Hampshire'},
|
||||
'NJ': {'name': 'New Jersey'},
|
||||
'NM': {'name': 'New Mexico'},
|
||||
'NV': {'name': 'Nevada'},
|
||||
'NY': {'name': 'New York'},
|
||||
'OH': {'name': 'Ohio'},
|
||||
'OK': {'name': 'Oklahoma'},
|
||||
'OR': {'name': 'Oregon'},
|
||||
'PA': {'name': 'Pennsylvania'},
|
||||
'PR': {'name': 'Puerto Rico'},
|
||||
'RI': {'name': 'Rhode Island'},
|
||||
'SC': {'name': 'South Carolina'},
|
||||
'SD': {'name': 'South Dakota'},
|
||||
'TN': {'name': 'Tennessee'},
|
||||
'TX': {'name': 'Texas'},
|
||||
'UM': {'name': 'United States Minor Outlying '
|
||||
'Islands'},
|
||||
'UT': {'name': 'Utah'},
|
||||
'VA': {'name': 'Virginia'},
|
||||
'VI': {'name': 'Virgin Islands'},
|
||||
'VT': {'name': 'Vermont'},
|
||||
'WA': {'name': 'Washington'},
|
||||
'WI': {'name': 'Wisconsin'},
|
||||
'WV': {'name': 'West Virginia'},
|
||||
'WY': {'name': 'Wyoming'}}},
|
||||
'VC': {'name': 'Saint Vincent and the Grenadines'},
|
||||
'VG': {'name': 'Virgin Islands, British'},
|
||||
'VI': {'name': 'Virgin Islands, U.S.'}},
|
||||
'OC': {'AS': {'name': 'American Samoa'},
|
||||
'AU': {'name': 'Australia'},
|
||||
'CK': {'name': 'Cook Islands'},
|
||||
'FJ': {'name': 'Fiji'},
|
||||
'FM': {'name': 'Micronesia, Federated States of'},
|
||||
'GU': {'name': 'Guam'},
|
||||
'KI': {'name': 'Kiribati'},
|
||||
'MH': {'name': 'Marshall Islands'},
|
||||
'MP': {'name': 'Northern Mariana Islands'},
|
||||
'NC': {'name': 'New Caledonia'},
|
||||
'NF': {'name': 'Norfolk Island'},
|
||||
'NR': {'name': 'Nauru'},
|
||||
'NU': {'name': 'Niue'},
|
||||
'NZ': {'name': 'New Zealand'},
|
||||
'PF': {'name': 'French Polynesia'},
|
||||
'PG': {'name': 'Papua New Guinea'},
|
||||
'PN': {'name': 'Pitcairn'},
|
||||
'PW': {'name': 'Palau'},
|
||||
'SB': {'name': 'Solomon Islands'},
|
||||
'TK': {'name': 'Tokelau'},
|
||||
'TO': {'name': 'Tonga'},
|
||||
'TV': {'name': 'Tuvalu'},
|
||||
'UM': {'name': 'United States Minor Outlying Islands'},
|
||||
'VU': {'name': 'Vanuatu'},
|
||||
'WF': {'name': 'Wallis and Futuna'},
|
||||
'WS': {'name': 'Samoa'}},
|
||||
'SA': {'AR': {'name': 'Argentina'},
|
||||
'BO': {'name': 'Bolivia, Plurinational State of'},
|
||||
'BR': {'name': 'Brazil'},
|
||||
'CL': {'name': 'Chile'},
|
||||
'CO': {'name': 'Colombia'},
|
||||
'EC': {'name': 'Ecuador'},
|
||||
'FK': {'name': 'Falkland Islands (Malvinas)'},
|
||||
'GF': {'name': 'French Guiana'},
|
||||
'GS': {'name': 'South Georgia and the South Sandwich Islands'},
|
||||
'GY': {'name': 'Guyana'},
|
||||
'PE': {'name': 'Peru'},
|
||||
'PY': {'name': 'Paraguay'},
|
||||
'SR': {'name': 'Suriname'},
|
||||
'UY': {'name': 'Uruguay'},
|
||||
'VE': {'name': 'Venezuela, Bolivarian Republic of'}}}
|
||||
@@ -24,6 +24,7 @@ from .base import BaseSource
|
||||
class AxfrBaseSource(BaseSource):
|
||||
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SPF',
|
||||
'SRV', 'TXT'))
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ class BaseSource(object):
|
||||
if not hasattr(self, 'SUPPORTS_GEO'):
|
||||
raise NotImplementedError('Abstract base class, SUPPORTS_GEO '
|
||||
'property missing')
|
||||
if not hasattr(self, 'SUPPORTS_DYNAMIC'):
|
||||
raise NotImplementedError('Abstract base class, SUPPORTS_DYNAMIC '
|
||||
'property missing')
|
||||
if not hasattr(self, 'SUPPORTS'):
|
||||
raise NotImplementedError('Abstract base class, SUPPORTS '
|
||||
'property missing')
|
||||
|
||||
@@ -19,6 +19,7 @@ from .base import BaseSource
|
||||
|
||||
class TinyDnsBaseSource(BaseSource):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'CNAME', 'MX', 'NS'))
|
||||
|
||||
split_re = re.compile(r':+')
|
||||
|
||||
@@ -2,6 +2,8 @@ coverage
|
||||
mock
|
||||
nose
|
||||
pycodestyle==2.4.0
|
||||
pycountry>=18.12.8
|
||||
pycountry_convert>=0.7.2
|
||||
pyflakes==1.6.0
|
||||
requests_mock
|
||||
twine==1.11.0
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from collections import defaultdict
|
||||
from pprint import pformat
|
||||
from pycountry import countries, subdivisions
|
||||
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'):
|
||||
continue
|
||||
subs[subdivision.country_code][subdivision.code[3:]] = {
|
||||
'name': subdivision.name
|
||||
}
|
||||
subs = dict(subs)
|
||||
#pprint(subs)
|
||||
|
||||
# These are best guesses at things pycountry_convert doesn't map
|
||||
continent_backups = {
|
||||
'AQ': 'AN',
|
||||
'EH': 'AF',
|
||||
'PN': 'OC',
|
||||
'SX': 'NA',
|
||||
'TF': 'AN',
|
||||
'TL': 'ID',
|
||||
'UM': 'OC',
|
||||
'VA': 'EU',
|
||||
}
|
||||
|
||||
geos = defaultdict(dict)
|
||||
for country in countries:
|
||||
try:
|
||||
continent_code = country_alpha2_to_continent_code(country.alpha_2)
|
||||
except KeyError:
|
||||
try:
|
||||
continent_code = continent_backups[country.alpha_2]
|
||||
except KeyError:
|
||||
raise
|
||||
print('{} {} {}'.format(country.alpha_2, country.name, getattr(country, 'official_name', '')))
|
||||
|
||||
geos[continent_code][country.alpha_2] = {
|
||||
'name': country.name
|
||||
}
|
||||
|
||||
try:
|
||||
geos[continent_code][country.alpha_2]['provinces'] = subs[country.alpha_2]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
geos = dict(geos)
|
||||
data = pformat(geos).replace('\n', '\n ')
|
||||
|
||||
print('''#
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is generated using
|
||||
# ./script/generate-geo-data > octodns/record/geo_data.py
|
||||
# do not modify it directly
|
||||
#
|
||||
|
||||
geo_data = \\
|
||||
{}'''.format(data))
|
||||
@@ -0,0 +1,197 @@
|
||||
---
|
||||
a:
|
||||
dynamic:
|
||||
pools:
|
||||
ams:
|
||||
# TODO: make value possible
|
||||
values:
|
||||
- value: 1.1.1.1
|
||||
iad:
|
||||
values:
|
||||
# TODO: make value optional
|
||||
- value: 2.2.2.2
|
||||
- value: 3.3.3.3
|
||||
lax:
|
||||
values:
|
||||
- value: 4.4.4.4
|
||||
sea:
|
||||
values:
|
||||
- value: 6.6.6.6
|
||||
weight: 10
|
||||
- value: 5.5.5.5
|
||||
weight: 25
|
||||
rules:
|
||||
- geos:
|
||||
- EU-GB
|
||||
pool: iad
|
||||
- geos:
|
||||
- EU
|
||||
pool: ams
|
||||
- geos:
|
||||
- NA-US-CA
|
||||
- NA-US-OR
|
||||
- NA-US-WA
|
||||
pool: sea
|
||||
- pool: iad
|
||||
type: A
|
||||
values:
|
||||
- 2.2.2.2
|
||||
- 3.3.3.3
|
||||
aaaa:
|
||||
dynamic:
|
||||
pools:
|
||||
ams:
|
||||
values:
|
||||
- value: 2601:642:500:e210:62f8:1dff:feb8:9471
|
||||
iad:
|
||||
values:
|
||||
- value: 2601:642:500:e210:62f8:1dff:feb8:9472
|
||||
- value: 2601:642:500:e210:62f8:1dff:feb8:9473
|
||||
lax:
|
||||
values:
|
||||
- value: 2601:642:500:e210:62f8:1dff:feb8:9474
|
||||
sea:
|
||||
values:
|
||||
- value: 2601:642:500:e210:62f8:1dff:feb8:9475
|
||||
weight: 1
|
||||
- value: 2601:642:500:e210:62f8:1dff:feb8:9476
|
||||
weight: 2
|
||||
rules:
|
||||
- geos:
|
||||
- EU-GB
|
||||
pool: iad
|
||||
- geos:
|
||||
- EU
|
||||
pool: ams
|
||||
- geos:
|
||||
- NA-US-CA
|
||||
- NA-US-OR
|
||||
- NA-US-WA
|
||||
pool: sea
|
||||
- pool: iad
|
||||
type: AAAA
|
||||
values:
|
||||
- 2601:642:500:e210:62f8:1dff:feb8:947a
|
||||
- 2601:644:500:e210:62f8:1dff:feb8:947a
|
||||
cname:
|
||||
dynamic:
|
||||
pools:
|
||||
ams:
|
||||
values:
|
||||
- value: target-ams.unit.tests.
|
||||
iad:
|
||||
values:
|
||||
- value: target-iad.unit.tests.
|
||||
lax:
|
||||
values:
|
||||
- value: target-lax.unit.tests.
|
||||
sea:
|
||||
values:
|
||||
- value: target-sea-1.unit.tests.
|
||||
weight: 100
|
||||
- value: target-sea-2.unit.tests.
|
||||
weight: 175
|
||||
rules:
|
||||
- geos:
|
||||
- EU-GB
|
||||
pool: iad
|
||||
- geos:
|
||||
- EU
|
||||
pool: ams
|
||||
- geos:
|
||||
- NA-US-CA
|
||||
- NA-US-OR
|
||||
- NA-US-WA
|
||||
pool: sea
|
||||
- pool: iad
|
||||
type: CNAME
|
||||
value: target.unit.tests.
|
||||
real-ish-a:
|
||||
dynamic:
|
||||
pools:
|
||||
ap-southeast-1:
|
||||
values:
|
||||
# ap-southeast-1a
|
||||
- value: 1.4.1.1
|
||||
weight: 2
|
||||
- value: 1.4.1.2
|
||||
weight: 2
|
||||
# ap-southeast-1b
|
||||
- value: 1.4.2.1
|
||||
- value: 1.4.2.2
|
||||
# ap-southeast-1c
|
||||
- value: 1.4.3.1
|
||||
- value: 1.4.3.2
|
||||
eu-central-1:
|
||||
values:
|
||||
# eu-central-1a
|
||||
- value: 1.3.1.1
|
||||
- value: 1.3.1.2
|
||||
# eu-central-1b
|
||||
- value: 1.3.2.1
|
||||
- value: 1.3.2.2
|
||||
# eu-central-1c
|
||||
- value: 1.3.3.1
|
||||
- value: 1.3.3.2
|
||||
us-east-1:
|
||||
values:
|
||||
# us-east-1a
|
||||
- value: 1.1.1.1
|
||||
- value: 1.1.1.2
|
||||
# us-east-1b
|
||||
- value: 1.1.2.1
|
||||
- value: 1.1.2.2
|
||||
# us-east-1c
|
||||
- value: 1.1.3.1
|
||||
- value: 1.1.3.2
|
||||
us-west-2:
|
||||
values:
|
||||
# us-west-2a
|
||||
- value: 1.2.1.1
|
||||
- value: 1.2.1.2
|
||||
# us-west-2b
|
||||
- value: 1.2.2.1
|
||||
- value: 1.2.2.2
|
||||
# us-west-2c
|
||||
- value: 1.2.3.1
|
||||
- value: 1.2.3.2
|
||||
rules:
|
||||
- geos:
|
||||
# TODO: require sorted
|
||||
- NA-US-CA
|
||||
- NA-US-OR
|
||||
- NA-US-WA
|
||||
pool: us-west-2
|
||||
- geos:
|
||||
- AS-CN
|
||||
pool: ap-southeast-1
|
||||
- geos:
|
||||
- AF
|
||||
- EU
|
||||
pool: eu-central-1
|
||||
- pool: us-east-1
|
||||
type: A
|
||||
values:
|
||||
# Generally these should match the values of your "default" rule's pools as
|
||||
# if everything fails healthchecks they'll fallback to this
|
||||
- 1.1.1.1
|
||||
- 1.1.1.2
|
||||
- 1.1.2.1
|
||||
- 1.1.2.2
|
||||
- 1.1.3.1
|
||||
- 1.1.3.2
|
||||
simple-weighted:
|
||||
dynamic:
|
||||
pools:
|
||||
default:
|
||||
values:
|
||||
- value: one.unit.tests.
|
||||
weight: 3
|
||||
- value: two.unit.tests.
|
||||
weight: 2
|
||||
rules:
|
||||
- pool: default
|
||||
type: CNAME
|
||||
# CNAMEs don't support health checks (currently) so these will never be used
|
||||
# on providers with dynamic support
|
||||
value: default.unit.tests.
|
||||
@@ -17,6 +17,7 @@ class SimpleSource(object):
|
||||
|
||||
class SimpleProvider(object):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A',))
|
||||
id = 'test'
|
||||
|
||||
@@ -35,6 +36,25 @@ class SimpleProvider(object):
|
||||
|
||||
class GeoProvider(object):
|
||||
SUPPORTS_GEO = True
|
||||
SUPPORTS_DYNAMIC = False
|
||||
id = 'test'
|
||||
|
||||
def __init__(self, id='test'):
|
||||
pass
|
||||
|
||||
def populate(self, zone, source=False, lenient=False):
|
||||
pass
|
||||
|
||||
def supports(self, record):
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
class DynamicProvider(object):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = True
|
||||
id = 'test'
|
||||
|
||||
def __init__(self, id='test'):
|
||||
|
||||
@@ -14,8 +14,8 @@ from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager
|
||||
from octodns.yaml import safe_load
|
||||
from octodns.zone import Zone
|
||||
|
||||
from helpers import GeoProvider, NoSshFpProvider, SimpleProvider, \
|
||||
TemporaryDirectory
|
||||
from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \
|
||||
SimpleProvider, TemporaryDirectory
|
||||
|
||||
config_dir = join(dirname(__file__), 'config')
|
||||
|
||||
@@ -187,6 +187,7 @@ class TestManager(TestCase):
|
||||
def test_aggregate_target(self):
|
||||
simple = SimpleProvider()
|
||||
geo = GeoProvider()
|
||||
dynamic = DynamicProvider()
|
||||
nosshfp = NoSshFpProvider()
|
||||
|
||||
self.assertFalse(_AggregateTarget([simple, simple]).SUPPORTS_GEO)
|
||||
@@ -194,6 +195,11 @@ class TestManager(TestCase):
|
||||
self.assertFalse(_AggregateTarget([geo, simple]).SUPPORTS_GEO)
|
||||
self.assertTrue(_AggregateTarget([geo, geo]).SUPPORTS_GEO)
|
||||
|
||||
self.assertFalse(_AggregateTarget([simple, simple]).SUPPORTS_DYNAMIC)
|
||||
self.assertFalse(_AggregateTarget([simple, dynamic]).SUPPORTS_DYNAMIC)
|
||||
self.assertFalse(_AggregateTarget([dynamic, simple]).SUPPORTS_DYNAMIC)
|
||||
self.assertTrue(_AggregateTarget([dynamic, dynamic]).SUPPORTS_DYNAMIC)
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
record = Record.new(zone, 'sshfp', {
|
||||
'ttl': 60,
|
||||
|
||||
@@ -61,13 +61,21 @@ class TestBaseProvider(TestCase):
|
||||
class HasSupportsGeo(HasLog):
|
||||
SUPPORTS_GEO = False
|
||||
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupportsGeo('hassupportsgeo')
|
||||
self.assertEquals('Abstract base class, SUPPORTS_DYNAMIC '
|
||||
'property missing', ctx.exception.message)
|
||||
|
||||
class HasSupportsDyanmic(HasSupportsGeo):
|
||||
SUPPORTS_DYNAMIC = False
|
||||
|
||||
zone = Zone('unit.tests.', ['sub'])
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupportsGeo('hassupportsgeo').populate(zone)
|
||||
HasSupportsDyanmic('hassupportsdynamic').populate(zone)
|
||||
self.assertEquals('Abstract base class, SUPPORTS property missing',
|
||||
ctx.exception.message)
|
||||
|
||||
class HasSupports(HasSupportsGeo):
|
||||
class HasSupports(HasSupportsDyanmic):
|
||||
SUPPORTS = set(('A',))
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupports('hassupports').populate(zone)
|
||||
|
||||
@@ -23,6 +23,7 @@ class TestYamlProvider(TestCase):
|
||||
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)
|
||||
@@ -32,6 +33,9 @@ class TestYamlProvider(TestCase):
|
||||
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
|
||||
@@ -45,6 +49,7 @@ class TestYamlProvider(TestCase):
|
||||
# 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
|
||||
@@ -57,6 +62,15 @@ class TestYamlProvider(TestCase):
|
||||
self.assertEquals(15, target.apply(plan))
|
||||
self.assertTrue(isfile(yaml_file))
|
||||
|
||||
# Dynamic plan
|
||||
plan = target.plan(dynamic_zone)
|
||||
self.assertEquals(5, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
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)
|
||||
@@ -77,21 +91,58 @@ class TestYamlProvider(TestCase):
|
||||
data = safe_load(fh.read())
|
||||
|
||||
# '' has some of both
|
||||
roots = sorted(data[''], key=lambda r: r['type'])
|
||||
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['mx'])
|
||||
self.assertTrue('values' in data['naptr'])
|
||||
self.assertTrue('values' in data['_srv._tcp'])
|
||||
self.assertTrue('values' in data['txt'])
|
||||
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['aaaa'])
|
||||
self.assertTrue('value' in data['ptr'])
|
||||
self.assertTrue('value' in data['spf'])
|
||||
self.assertTrue('value' in data['www'])
|
||||
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([], 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([], data.keys())
|
||||
|
||||
def test_empty(self):
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
|
||||
+1320
-25
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record.geo import GeoCodes
|
||||
|
||||
|
||||
class TestRecordGeoCodes(TestCase):
|
||||
|
||||
def test_validate(self):
|
||||
prefix = 'xyz '
|
||||
|
||||
# All valid
|
||||
self.assertEquals([], GeoCodes.validate('NA', prefix))
|
||||
self.assertEquals([], GeoCodes.validate('NA-US', prefix))
|
||||
self.assertEquals([], GeoCodes.validate('NA-US-OR', prefix))
|
||||
|
||||
# Just plain bad
|
||||
self.assertEquals(['xyz invalid geo code "XX-YY-ZZ-AA"'],
|
||||
GeoCodes.validate('XX-YY-ZZ-AA', prefix))
|
||||
self.assertEquals(['xyz unknown continent code "X-Y-Z"'],
|
||||
GeoCodes.validate('X-Y-Z', prefix))
|
||||
self.assertEquals(['xyz unknown continent code "XXX-Y-Z"'],
|
||||
GeoCodes.validate('XXX-Y-Z', prefix))
|
||||
|
||||
# Bad continent
|
||||
self.assertEquals(['xyz unknown continent code "XX"'],
|
||||
GeoCodes.validate('XX', prefix))
|
||||
# Bad continent good country
|
||||
self.assertEquals(['xyz unknown continent code "XX-US"'],
|
||||
GeoCodes.validate('XX-US', prefix))
|
||||
# Bad continent good country and province
|
||||
self.assertEquals(['xyz unknown continent code "XX-US-OR"'],
|
||||
GeoCodes.validate('XX-US-OR', prefix))
|
||||
|
||||
# Bad country, good continent
|
||||
self.assertEquals(['xyz unknown country code "NA-XX"'],
|
||||
GeoCodes.validate('NA-XX', prefix))
|
||||
# Bad country, good continent and state
|
||||
self.assertEquals(['xyz unknown country code "NA-XX-OR"'],
|
||||
GeoCodes.validate('NA-XX-OR', prefix))
|
||||
# Good country, good continent, but bad match
|
||||
self.assertEquals(['xyz unknown country code "NA-GB"'],
|
||||
GeoCodes.validate('NA-GB', prefix))
|
||||
|
||||
# Bad province code, good continent and country
|
||||
self.assertEquals(['xyz unknown province code "NA-US-XX"'],
|
||||
GeoCodes.validate('NA-US-XX', prefix))
|
||||
@@ -111,6 +111,7 @@ class TestZone(TestCase):
|
||||
class NoAaaaProvider(object):
|
||||
id = 'no-aaaa'
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
|
||||
def supports(self, record):
|
||||
return record._type != 'AAAA'
|
||||
|
||||
Reference in New Issue
Block a user