diff --git a/octodns/provider/route53.py b/octodns/provider/route53.py index 7291a6b..a3187b9 100644 --- a/octodns/provider/route53.py +++ b/octodns/provider/route53.py @@ -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. + 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 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 - 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']) + 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: diff --git a/tests/test_octodns_provider_route53.py b/tests/test_octodns_provider_route53.py index 1d48891..7c32d8b 100644 --- a/tests/test_octodns_provider_route53.py +++ b/tests/test_octodns_provider_route53.py @@ -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):