mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Route53Provider dynamic support
This commit is contained in:
+347
-18
@@ -14,6 +14,7 @@ import logging
|
||||
import re
|
||||
|
||||
from ..record import Record, Update
|
||||
from ..record.geo import GeoCodes
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
@@ -29,19 +30,84 @@ def _octal_replace(s):
|
||||
class _Route53Record(object):
|
||||
|
||||
@classmethod
|
||||
def new(self, provider, record, hosted_zone_id, creating):
|
||||
def _new_dynamic(cls, provider, record, hosted_zone_id, creating):
|
||||
ret = set()
|
||||
if getattr(record, 'geo', False):
|
||||
ret.add(_Route53GeoDefault(provider, record, creating))
|
||||
for ident, geo in record.geo.items():
|
||||
ret.add(_Route53GeoRecord(provider, record, ident, geo,
|
||||
creating))
|
||||
else:
|
||||
ret.add(_Route53Record(provider, record, creating))
|
||||
|
||||
# HostedZoneId wants just the last bit, but the place we're getting
|
||||
# this from looks like /hostedzone/Z424CArX3BB224
|
||||
hosted_zone_id = hosted_zone_id.split('/', 2)[-1]
|
||||
|
||||
# Create the default pool
|
||||
fqdn = record.fqdn
|
||||
ret.add(_Route53Record(provider, record, creating,
|
||||
'_octodns-default-pool.{}'.format(fqdn)))
|
||||
|
||||
# Pools
|
||||
for pool_name, pool in record.dynamic.pools.items():
|
||||
|
||||
# Create the primary
|
||||
ret.add(_Route53DynamicPool(provider, hosted_zone_id, record,
|
||||
pool_name, creating))
|
||||
|
||||
# Create the fallback
|
||||
fallback = pool.data.get('fallback', False)
|
||||
if fallback:
|
||||
# We have an explicitly configured fallback
|
||||
ret.add(_Route53DynamicPool(provider, hosted_zone_id, record,
|
||||
pool_name, creating,
|
||||
target_name=fallback))
|
||||
else:
|
||||
# We fallback on the default
|
||||
ret.add(_Route53DynamicPool(provider, hosted_zone_id, record,
|
||||
pool_name, creating,
|
||||
target_name='default'))
|
||||
|
||||
# Create the values
|
||||
for i, value in enumerate(pool.data['values']):
|
||||
weight = value['weight']
|
||||
value = value['value']
|
||||
ret.add(_Route53DynamicValue(provider, record, pool_name,
|
||||
value, weight, i, creating))
|
||||
|
||||
# Rules
|
||||
for i, rule in enumerate(record.dynamic.rules):
|
||||
pool_name = rule.data['pool']
|
||||
geos = rule.data.get('geos', [])
|
||||
if geos:
|
||||
for geo in geos:
|
||||
ret.add(_Route53DynamicRule(provider, hosted_zone_id,
|
||||
record, pool_name, i,
|
||||
creating, geo=geo))
|
||||
else:
|
||||
ret.add(_Route53DynamicRule(provider, hosted_zone_id, record,
|
||||
pool_name, i, creating))
|
||||
|
||||
return ret
|
||||
|
||||
def __init__(self, provider, record, creating):
|
||||
self.fqdn = record.fqdn
|
||||
@classmethod
|
||||
def _new_geo(cls, provider, record, creating):
|
||||
ret = set()
|
||||
|
||||
ret.add(_Route53GeoDefault(provider, record, creating))
|
||||
for ident, geo in record.geo.items():
|
||||
ret.add(_Route53GeoRecord(provider, record, ident, geo,
|
||||
creating))
|
||||
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def new(cls, provider, record, hosted_zone_id, creating):
|
||||
|
||||
if getattr(record, 'dynamic', False):
|
||||
ret = cls._new_dynamic(provider, record, hosted_zone_id, creating)
|
||||
return ret
|
||||
elif getattr(record, 'geo', False):
|
||||
return cls._new_geo(provider, record, creating)
|
||||
|
||||
return set((_Route53Record(provider, record, creating),))
|
||||
|
||||
def __init__(self, provider, record, creating, fqdn_override=None):
|
||||
self.fqdn = fqdn_override or record.fqdn
|
||||
self._type = record._type
|
||||
self.ttl = record.ttl
|
||||
|
||||
@@ -148,6 +214,172 @@ class _Route53Record(object):
|
||||
return [self._value_for_SRV(v, record) for v in record.values]
|
||||
|
||||
|
||||
class _Route53DynamicPool(_Route53Record):
|
||||
|
||||
def __init__(self, provider, hosted_zone_id, record, pool_name, creating,
|
||||
target_name=None):
|
||||
fqdn_override = '_octodns-{}-pool.{}'.format(pool_name, record.fqdn)
|
||||
super(_Route53DynamicPool, self) \
|
||||
.__init__(provider, record, creating, fqdn_override=fqdn_override)
|
||||
|
||||
self.hosted_zone_id = hosted_zone_id
|
||||
self.pool_name = pool_name
|
||||
|
||||
self.target_name = target_name
|
||||
if target_name:
|
||||
# We're pointing down the chain
|
||||
self.target_dns_name = '_octodns-{}-pool.{}'.format(target_name,
|
||||
record.fqdn)
|
||||
else:
|
||||
# We're a paimary, point at our values
|
||||
self.target_dns_name = '_octodns-{}-value.{}'.format(pool_name,
|
||||
record.fqdn)
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return 'Secondary' if self.target_name else 'Primary'
|
||||
|
||||
@property
|
||||
def identifer(self):
|
||||
if self.target_name:
|
||||
return '{}-{}-{}'.format(self.pool_name, self.mode,
|
||||
self.target_name)
|
||||
return '{}-{}'.format(self.pool_name, self.mode)
|
||||
|
||||
def mod(self, action):
|
||||
return {
|
||||
'Action': action,
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': self.target_dns_name,
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': self.hosted_zone_id,
|
||||
},
|
||||
'Failover': 'SECONDARY' if self.target_name else 'PRIMARY',
|
||||
'Name': self.fqdn,
|
||||
'SetIdentifier': self.identifer,
|
||||
'Type': self._type,
|
||||
}
|
||||
}
|
||||
|
||||
def __hash__(self):
|
||||
return '{}:{}:{}'.format(self.fqdn, self._type,
|
||||
self.identifer).__hash__()
|
||||
|
||||
def __repr__(self):
|
||||
return '_Route53DynamicPool<{} {} {} {}>' \
|
||||
.format(self.fqdn, self._type, self.mode, self.target_dns_name)
|
||||
|
||||
|
||||
class _Route53DynamicRule(_Route53Record):
|
||||
|
||||
def __init__(self, provider, hosted_zone_id, record, pool_name, index,
|
||||
creating, geo=None):
|
||||
super(_Route53DynamicRule, self).__init__(provider, record, creating)
|
||||
|
||||
self.hosted_zone_id = hosted_zone_id
|
||||
self.geo = geo
|
||||
self.pool_name = pool_name
|
||||
self.index = index
|
||||
|
||||
self.target_dns_name = '_octodns-{}-pool.{}'.format(pool_name,
|
||||
record.fqdn)
|
||||
|
||||
@property
|
||||
def identifer(self):
|
||||
return '{}-{}-{}'.format(self.index, self.pool_name, self.geo)
|
||||
|
||||
def mod(self, action):
|
||||
rrset = {
|
||||
'AliasTarget': {
|
||||
'DNSName': self.target_dns_name,
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': self.hosted_zone_id,
|
||||
},
|
||||
'GeoLocation': {
|
||||
'CountryCode': '*'
|
||||
},
|
||||
'Name': self.fqdn,
|
||||
'SetIdentifier': self.identifer,
|
||||
'Type': self._type,
|
||||
}
|
||||
|
||||
if self.geo:
|
||||
geo = GeoCodes.parse(self.geo)
|
||||
|
||||
if geo['province_code']:
|
||||
rrset['GeoLocation'] = {
|
||||
'CountryCode': geo['country_code'],
|
||||
'SubdivisionCode': geo['province_code'],
|
||||
}
|
||||
elif geo['country_code']:
|
||||
rrset['GeoLocation'] = {
|
||||
'CountryCode': geo['country_code']
|
||||
}
|
||||
else:
|
||||
rrset['GeoLocation'] = {
|
||||
'ContinentCode': geo['continent_code'],
|
||||
}
|
||||
|
||||
return {
|
||||
'Action': action,
|
||||
'ResourceRecordSet': rrset,
|
||||
}
|
||||
|
||||
def __hash__(self):
|
||||
return '{}:{}:{}'.format(self.fqdn, self._type,
|
||||
self.identifer).__hash__()
|
||||
|
||||
def __repr__(self):
|
||||
return '_Route53DynamicRule<{} {} {} {} {}>' \
|
||||
.format(self.fqdn, self._type, self.index, self.geo,
|
||||
self.target_dns_name)
|
||||
|
||||
|
||||
class _Route53DynamicValue(_Route53Record):
|
||||
|
||||
def __init__(self, provider, record, pool_name, value, weight, index,
|
||||
creating):
|
||||
fqdn_override = '_octodns-{}-value.{}'.format(pool_name, record.fqdn)
|
||||
super(_Route53DynamicValue, self).__init__(provider, record, creating,
|
||||
fqdn_override=fqdn_override)
|
||||
|
||||
self.pool_name = pool_name
|
||||
self.index = index
|
||||
value_convert = getattr(self, '_value_convert_{}'.format(record._type))
|
||||
self.value = value_convert(value, record)
|
||||
self.weight = weight
|
||||
|
||||
self.health_check_id = provider.get_health_check_id(record, self.value,
|
||||
creating)
|
||||
|
||||
@property
|
||||
def identifer(self):
|
||||
return '{}-{:03d}'.format(self.pool_name, self.index)
|
||||
|
||||
def mod(self, action):
|
||||
return {
|
||||
'Action': action,
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': self.health_check_id,
|
||||
'Name': self.fqdn,
|
||||
'ResourceRecords': [{'Value': self.value}],
|
||||
'SetIdentifier': self.identifer,
|
||||
'TTL': self.ttl,
|
||||
'Type': self._type,
|
||||
'Weight': self.weight,
|
||||
}
|
||||
}
|
||||
|
||||
def __hash__(self):
|
||||
return '{}:{}:{}'.format(self.fqdn, self._type,
|
||||
self.identifer).__hash__()
|
||||
|
||||
def __repr__(self):
|
||||
return '_Route53DynamicValue<{} {} {} {}>' \
|
||||
.format(self.fqdn, self._type, self.identifer, self.value)
|
||||
|
||||
|
||||
class _Route53GeoDefault(_Route53Record):
|
||||
|
||||
def mod(self, action):
|
||||
@@ -285,8 +517,7 @@ class Route53Provider(BaseProvider):
|
||||
In general the account used will need full permissions on Route53.
|
||||
'''
|
||||
SUPPORTS_GEO = True
|
||||
# TODO: dynamic
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS_DYNAMIC = True
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR',
|
||||
'SPF', 'SRV', 'TXT'))
|
||||
|
||||
@@ -518,6 +749,76 @@ class Route53Provider(BaseProvider):
|
||||
|
||||
return self._r53_rrsets[zone_id]
|
||||
|
||||
def _data_for_dynamic(self, name, _type, rrsets):
|
||||
pools = defaultdict(lambda: {'values': []})
|
||||
# Data to build our rules will be collected here and "converted" into
|
||||
# their final form below
|
||||
rules = defaultdict(lambda: {'pool': None, 'geos': []})
|
||||
# Base/empty data
|
||||
data = {
|
||||
'dynamic': {
|
||||
'pools': pools,
|
||||
'rules': [],
|
||||
}
|
||||
}
|
||||
|
||||
# For all the rrsets that comprise this dynamic record
|
||||
for rrset in rrsets:
|
||||
name = rrset['Name']
|
||||
if '-pool.' in name:
|
||||
# This is a pool rrset
|
||||
pool_name = name.split('.', 1)[0][9:-5]
|
||||
if pool_name == 'default':
|
||||
# default becomes the base for the record and its
|
||||
# value(s) will fill the non-dynamic values
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
data.update(data_for(rrset))
|
||||
elif rrset['Failover'] == 'SECONDARY':
|
||||
# This is a failover record, we'll ignore PRIMARY, but
|
||||
# SECONDARY will tell us what the pool's fallback is
|
||||
fallback_name = rrset['AliasTarget']['DNSName'] \
|
||||
.split('.', 1)[0][9:-5]
|
||||
# Don't care about default fallbacks, anything else
|
||||
# we'll record
|
||||
if fallback_name != 'default':
|
||||
pools[pool_name]['fallback'] = fallback_name
|
||||
elif 'GeoLocation' in rrset:
|
||||
# These are rules
|
||||
_id = rrset['SetIdentifier']
|
||||
# We record rule index as the first part of set-id, the 2nd
|
||||
# part just ensures uniqueness across geos and is ignored
|
||||
i = int(_id.split('-', 1)[0])
|
||||
# Parse the pool name out of _octodns-<pool-name>-pool.
|
||||
pool = rrset['AliasTarget']['DNSName'].split('.', 1)[0][9:-5]
|
||||
# Record the pool
|
||||
rules[i]['pool'] = pool
|
||||
# Record geo if we have one
|
||||
geo = self._parse_geo(rrset)
|
||||
if geo:
|
||||
rules[i]['geos'].append(geo)
|
||||
else:
|
||||
# These are the pool value(s)
|
||||
pool_name = rrset['SetIdentifier'][:-4]
|
||||
# TODO: handle different value types
|
||||
value = rrset['ResourceRecords'][0]['Value']
|
||||
pools[pool_name]['values'].append({
|
||||
'value': value,
|
||||
'weight': rrset['Weight'],
|
||||
})
|
||||
|
||||
# Convert our map of rules into an ordered list now that we have all
|
||||
# the data
|
||||
for _, rule in sorted(rules.items()):
|
||||
r = {
|
||||
'pool': rule['pool'],
|
||||
}
|
||||
geos = sorted(rule['geos'])
|
||||
if geos:
|
||||
r['geos'] = geos
|
||||
data['dynamic']['rules'].append(r)
|
||||
|
||||
return data
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
@@ -529,21 +830,46 @@ class Route53Provider(BaseProvider):
|
||||
if zone_id:
|
||||
exists = True
|
||||
records = defaultdict(lambda: defaultdict(list))
|
||||
dynamic = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
for rrset in self._load_records(zone_id):
|
||||
record_name = zone.hostname_from_fqdn(rrset['Name'])
|
||||
record_name = _octal_replace(record_name)
|
||||
record_type = rrset['Type']
|
||||
if record_type not in self.SUPPORTS:
|
||||
# Skip stuff we don't support
|
||||
continue
|
||||
if 'AliasTarget' in rrset:
|
||||
# Alias records are Route53 specific and are not
|
||||
# portable, so we need to skip them
|
||||
self.log.warning("%s is an Alias record. Skipping..."
|
||||
% rrset['Name'])
|
||||
if record_name.startswith('_octodns-'):
|
||||
# Part of a dynamic record
|
||||
try:
|
||||
record_name = record_name.split('.', 1)[1]
|
||||
except IndexError:
|
||||
record_name = ''
|
||||
dynamic[record_name][record_type].append(rrset)
|
||||
continue
|
||||
elif 'AliasTarget' in rrset:
|
||||
if rrset['AliasTarget']['DNSName'].startswith('_octodns-'):
|
||||
# Part of a dynamic record
|
||||
dynamic[record_name][record_type].append(rrset)
|
||||
else:
|
||||
# Alias records are Route53 specific and are not
|
||||
# portable, so we need to skip them
|
||||
self.log.warning("%s is an Alias record. Skipping..."
|
||||
% rrset['Name'])
|
||||
continue
|
||||
# A basic record (potentially including geo)
|
||||
data = getattr(self, '_data_for_{}'.format(record_type))(rrset)
|
||||
records[record_name][record_type].append(data)
|
||||
|
||||
# Convert the dynamic rrsets to Records
|
||||
for name, types in dynamic.items():
|
||||
for _type, rrsets in types.items():
|
||||
data = self._data_for_dynamic(name, _type, rrsets)
|
||||
record = Record.new(zone, name, data, source=self,
|
||||
lenient=lenient)
|
||||
zone.add_record(record, lenient=lenient)
|
||||
|
||||
# Convert the basic (potentially with geo) rrsets to records
|
||||
for name, types in records.items():
|
||||
for _type, data in types.items():
|
||||
if len(data) > 1:
|
||||
@@ -590,6 +916,7 @@ class Route53Provider(BaseProvider):
|
||||
# ignore anything else
|
||||
continue
|
||||
checks[health_check['Id']] = health_check
|
||||
|
||||
more = resp['IsTruncated']
|
||||
start['Marker'] = resp.get('NextMarker', None)
|
||||
|
||||
@@ -778,6 +1105,7 @@ class Route53Provider(BaseProvider):
|
||||
return self._gen_mods('DELETE', existing_records)
|
||||
|
||||
def _extra_changes(self, desired, changes, **kwargs):
|
||||
# TODO: dynamic records extra changes...
|
||||
self.log.debug('_extra_changes: desired=%s', desired.name)
|
||||
zone_id = self._get_zone_id(desired.name)
|
||||
if not zone_id:
|
||||
@@ -862,7 +1190,8 @@ class Route53Provider(BaseProvider):
|
||||
mods.sort(key=_mod_keyer)
|
||||
|
||||
mods_rs_count = sum(
|
||||
[len(m['ResourceRecordSet']['ResourceRecords']) for m in mods]
|
||||
[len(m['ResourceRecordSet'].get('ResourceRecords', ''))
|
||||
for m in mods]
|
||||
)
|
||||
|
||||
if mods_rs_count > self.max_changes:
|
||||
|
||||
@@ -42,6 +42,202 @@ class TestOctalReplace(TestCase):
|
||||
self.assertEquals(expected, _octal_replace(s))
|
||||
|
||||
|
||||
dynamic_rrsets = [{
|
||||
'Name': '_octodns-default-pool.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.1.2.1'},
|
||||
{'Value': '1.1.2.2'}],
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'HealthCheckId': '76',
|
||||
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.4.1.1'}],
|
||||
'SetIdentifier': 'ap-southeast-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 2
|
||||
}, {
|
||||
'HealthCheckId': '09',
|
||||
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.4.1.2'}],
|
||||
'SetIdentifier': 'ap-southeast-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 2
|
||||
}, {
|
||||
'HealthCheckId': 'ab',
|
||||
'Name': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.3.1.1'}],
|
||||
'SetIdentifier': 'eu-central-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1
|
||||
}, {
|
||||
'HealthCheckId': '1e',
|
||||
'Name': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.3.1.2'}],
|
||||
'SetIdentifier': 'eu-central-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1
|
||||
}, {
|
||||
'HealthCheckId': '2a',
|
||||
'Name': '_octodns-us-east-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.5.1.1'}],
|
||||
'SetIdentifier': 'us-east-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1
|
||||
}, {
|
||||
'HealthCheckId': '61',
|
||||
'Name': '_octodns-us-east-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.5.1.2'}],
|
||||
'SetIdentifier': 'us-east-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1,
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-default-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Secondary-default',
|
||||
'Type': 'A'
|
||||
}, {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'
|
||||
},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Primary',
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'eu-central-1-Secondary-default',
|
||||
'Type': 'A'
|
||||
}, {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'
|
||||
},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'eu-central-1-Primary',
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'ap-southeast-1-Secondary-default',
|
||||
'Type': 'A'
|
||||
}, {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'
|
||||
},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'ap-southeast-1-Primary',
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'GeoLocation': {'CountryCode': 'JP'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-ap-southeast-1-AS-JP',
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'GeoLocation': {'CountryCode': 'CN'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-ap-southeast-1-AS-CN',
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'GeoLocation': {'ContinentCode': 'NA-US-FL'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '2-eu-central-1-NA-US-FL',
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'GeoLocation': {'ContinentCode': 'EU'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '2-eu-central-1-EU',
|
||||
'Type': 'A',
|
||||
}, {
|
||||
'AliasTarget': {'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'Z2'},
|
||||
'GeoLocation': {'CountryCode': '*'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '3-us-east-1-None',
|
||||
'Type': 'A',
|
||||
}]
|
||||
|
||||
dynamic_record_data = {
|
||||
'dynamic': {
|
||||
'pools': {
|
||||
'ap-southeast-1': {
|
||||
'fallback': 'us-east-1',
|
||||
'values': [{
|
||||
'weight': 2, 'value': '1.4.1.1'
|
||||
}, {
|
||||
'weight': 2, 'value': '1.4.1.2'
|
||||
}]
|
||||
},
|
||||
'eu-central-1': {
|
||||
'fallback': 'us-east-1',
|
||||
'values': [{
|
||||
'weight': 1, 'value': '1.3.1.1'
|
||||
}, {
|
||||
'weight': 1, 'value': '1.3.1.2'
|
||||
}],
|
||||
},
|
||||
'us-east-1': {
|
||||
'values': [{
|
||||
'weight': 1, 'value': '1.5.1.1'
|
||||
}, {
|
||||
'weight': 1, 'value': '1.5.1.2'
|
||||
}],
|
||||
}
|
||||
},
|
||||
'rules': [{
|
||||
'geos': ['AS-CN', 'AS-JP'],
|
||||
'pool': 'ap-southeast-1',
|
||||
}, {
|
||||
'geos': ['EU', 'NA-US-FL'],
|
||||
'pool': 'eu-central-1',
|
||||
}, {
|
||||
'pool': 'us-east-1',
|
||||
}],
|
||||
},
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'values': [
|
||||
'1.1.2.1',
|
||||
'1.1.2.2',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class TestRoute53Provider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
for name, data in (
|
||||
@@ -1534,6 +1730,71 @@ class TestRoute53Provider(TestCase):
|
||||
._unique_id_handlers['retry-config-route53']
|
||||
['handler']._checker.__dict__['_max_attempts'])
|
||||
|
||||
def test_data_for_dynamic(self):
|
||||
provider = Route53Provider('test', 'abc', '123')
|
||||
|
||||
data = provider._data_for_dynamic('', 'A', dynamic_rrsets)
|
||||
self.assertEquals(dynamic_record_data, data)
|
||||
|
||||
@patch('octodns.provider.route53.Route53Provider._get_zone_id')
|
||||
@patch('octodns.provider.route53.Route53Provider._load_records')
|
||||
def test_dynamic_populate(self, load_records_mock, get_zone_id_mock):
|
||||
provider = Route53Provider('test', 'abc', '123')
|
||||
|
||||
get_zone_id_mock.side_effect = ['z44']
|
||||
load_records_mock.side_effect = [dynamic_rrsets]
|
||||
|
||||
got = Zone('unit.tests.', [])
|
||||
provider.populate(got)
|
||||
|
||||
self.assertEquals(1, len(got.records))
|
||||
record = list(got.records)[0]
|
||||
self.assertEquals('', record.name)
|
||||
self.assertEquals('A', record._type)
|
||||
self.assertEquals([
|
||||
'1.1.2.1',
|
||||
'1.1.2.2',
|
||||
], record.values)
|
||||
self.assertTrue(record.dynamic)
|
||||
|
||||
self.assertEquals({
|
||||
'ap-southeast-1': {
|
||||
'fallback': 'us-east-1',
|
||||
'values': [{
|
||||
'weight': 2, 'value': '1.4.1.1'
|
||||
}, {
|
||||
'weight': 2, 'value': '1.4.1.2'
|
||||
}]
|
||||
},
|
||||
'eu-central-1': {
|
||||
'fallback': 'us-east-1',
|
||||
'values': [{
|
||||
'weight': 1, 'value': '1.3.1.1'
|
||||
}, {
|
||||
'weight': 1, 'value': '1.3.1.2'
|
||||
}],
|
||||
},
|
||||
'us-east-1': {
|
||||
'fallback': None,
|
||||
'values': [{
|
||||
'weight': 1, 'value': '1.5.1.1'
|
||||
}, {
|
||||
'weight': 1, 'value': '1.5.1.2'
|
||||
}],
|
||||
}
|
||||
}, {k: v.data for k, v in record.dynamic.pools.items()})
|
||||
|
||||
self.assertEquals([
|
||||
{
|
||||
'geos': ['AS-CN', 'AS-JP'],
|
||||
'pool': 'ap-southeast-1',
|
||||
}, {
|
||||
'geos': ['EU', 'NA-US-FL'],
|
||||
'pool': 'eu-central-1',
|
||||
}, {
|
||||
'pool': 'us-east-1',
|
||||
}], [r.data for r in record.dynamic.rules])
|
||||
|
||||
|
||||
class TestRoute53Records(TestCase):
|
||||
existing = Zone('unit.tests.', [])
|
||||
@@ -1551,8 +1812,9 @@ class TestRoute53Records(TestCase):
|
||||
route53_record = _Route53Record(None, self.record_a, False)
|
||||
|
||||
for value in (None, '', 'foo', 'bar', '1.2.3.4'):
|
||||
self.assertEquals(value, route53_record
|
||||
._value_convert_value(value, self.record_a))
|
||||
converted = route53_record._value_convert_value(value,
|
||||
self.record_a)
|
||||
self.assertEquals(value, converted)
|
||||
|
||||
record_txt = Record.new(self.existing, 'txt', {
|
||||
'ttl': 98,
|
||||
@@ -1624,6 +1886,242 @@ class TestRoute53Records(TestCase):
|
||||
e.__repr__()
|
||||
f.__repr__()
|
||||
|
||||
def test_new_dynamic(self):
|
||||
provider = Route53Provider('test', 'abc', '123')
|
||||
|
||||
# Just so boto won't try and make any calls
|
||||
stubber = Stubber(provider._conn)
|
||||
stubber.activate()
|
||||
|
||||
# We'll assume we create all healthchecks here, this functionality is
|
||||
# thoroughly tested elsewhere
|
||||
provider._health_checks = {}
|
||||
# When asked for a healthcheck return dummy info
|
||||
provider.get_health_check_id = lambda r, v, c: 'hc42'
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
record = Record.new(zone, '', dynamic_record_data)
|
||||
|
||||
# Convert a record into _Route53Records
|
||||
route53_records = _Route53Record.new(provider, record, 'z45',
|
||||
creating=True)
|
||||
self.assertEquals(18, len(route53_records))
|
||||
|
||||
# Convert the route53_records into mods
|
||||
self.assertEquals([{
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.4.1.2'}],
|
||||
'SetIdentifier': 'ap-southeast-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 2
|
||||
}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.4.1.1'}],
|
||||
'SetIdentifier': 'ap-southeast-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 2
|
||||
}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'
|
||||
},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'JP'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '0-ap-southeast-1-AS-JP',
|
||||
'Type': 'A'
|
||||
}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'US',
|
||||
'SubdivisionCode': 'FL',
|
||||
},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-eu-central-1-NA-US-FL',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': '*'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '2-us-east-1-None',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'ap-southeast-1-Secondary-us-east-1',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'CN'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '0-ap-southeast-1-AS-CN',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Primary',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'ContinentCode': 'EU'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-eu-central-1-EU',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'eu-central-1-Primary',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'Name': '_octodns-default-pool.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.1.2.1'},
|
||||
{
|
||||
'Value': '1.1.2.2'}],
|
||||
'TTL': 60,
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.3.1.2'}],
|
||||
'SetIdentifier': 'eu-central-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.3.1.1'}],
|
||||
'SetIdentifier': 'eu-central-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-default-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Secondary-default',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-us-east-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.5.1.2'}],
|
||||
'SetIdentifier': 'us-east-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-us-east-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.5.1.1'}],
|
||||
'SetIdentifier': 'us-east-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'ap-southeast-1-Primary',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'eu-central-1-Secondary-us-east-1',
|
||||
'Type': 'A'}
|
||||
}], [r.mod('CREATE') for r in route53_records])
|
||||
|
||||
for route53_record in route53_records:
|
||||
# Smoke test stringification
|
||||
route53_record.__repr__()
|
||||
|
||||
|
||||
class TestModKeyer(TestCase):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user