From 99e2557b428930c690733a1ffe891ef28cd38bea Mon Sep 17 00:00:00 2001 From: zimbatm Date: Tue, 5 May 2015 16:07:18 +0100 Subject: [PATCH] route53: add support for routing policies It is now possible to pass various routing policies if an identity is provided. This commit also introduces multiple optimisations: * Only fetch records for the given domain * Use UPSERT instead of DELETE+CREATE to update existing records --- lib/ansible/modules/cloud/amazon/route53.py | 133 +++++++++++++++----- 1 file changed, 101 insertions(+), 32 deletions(-) diff --git a/lib/ansible/modules/cloud/amazon/route53.py b/lib/ansible/modules/cloud/amazon/route53.py index 8869a68a86a..a869efc6894 100644 --- a/lib/ansible/modules/cloud/amazon/route53.py +++ b/lib/ansible/modules/cloud/amazon/route53.py @@ -93,6 +93,45 @@ options: required: false default: false version_added: "1.9" + identifier: + description: + - Weighted and latency-based resource record sets only. An identifier + that differentiates among multiple resource record sets that have the + same combination of DNS name and type. + required: false + default: null + version_added: "2.0" + weight: + description: + - Weighted resource record sets only. Among resource record sets that + have the same combination of DNS name and type, a value that + determines what portion of traffic for the current resource record set + is routed to the associated location. + required: false + default: null + version_added: "2.0" + region: + description: + - Latency-based resource record sets only Among resource record sets + that have the same combination of DNS name and type, a value that + determines which region this should be associated with for the + latency-based routing + required: false + default: null + version_added: "2.0" + health_check: + description: + - Health check to associate with this record + required: false + default: null + version_added: "2.0" + failover: + description: + - Failover resource record sets only. Whether this is the primary or + secondary resource record set. + required: false + default: null + version_added: "2.0" author: "Bruce Pennypacker (@bpennypacker)" extends_documentation_fragment: aws ''' @@ -156,6 +195,18 @@ EXAMPLES = ''' alias=True alias_hosted_zone_id="{{ elb_zone_id }}" +# Use a routing policy to distribute traffic: +- route53: + command: "create" + zone: "foo.com" + record: "www.foo.com" + type: "CNAME" + value: "host1.foo.com" + ttl: 30 + # Routing policy + identifier: "host1@www" + weight: 100 + health_check: "d994b780-3150-49fd-9205-356abdd42e75" ''' @@ -165,11 +216,21 @@ try: import boto from boto import route53 from boto.route53 import Route53Connection - from boto.route53.record import ResourceRecordSets + from boto.route53.record import Record, ResourceRecordSets HAS_BOTO = True except ImportError: HAS_BOTO = False +def get_zone_by_name(conn, module, zone_name, want_private): + """Finds a zone by name""" + for zone in conn.get_zones(): + # only save this zone id if the private status of the zone matches + # the private_zone_in boolean specified in the params + private_zone = module.boolean(zone.config.get('PrivateZone', False)) + if private_zone == want_private and zone.name == zone_name: + return zone + return None + def commit(changes, retry_interval): """Commit changes, but retry PriorRequestNotComplete errors.""" @@ -199,6 +260,11 @@ def main(): overwrite = dict(required=False, type='bool'), retry_interval = dict(required=False, default=500) private_zone = dict(required=False, type='bool', default=False), + identifier = dict(required=False), + weight = dict(required=False, type='int'), + region = dict(required=False), + health_check = dict(required=False), + failover = dict(required=False), ) ) module = AnsibleModule(argument_spec=argument_spec) @@ -215,6 +281,11 @@ def main(): alias_hosted_zone_id_in = module.params.get('alias_hosted_zone_id') retry_interval_in = module.params.get('retry_interval') private_zone_in = module.params.get('private_zone') + identifier_in = module.params.get('identifier') + weight_in = module.params.get('weight') + region_in = module.params.get('region') + health_check_in = module.params.get('health_check') + failover_in = module.params.get('failover') region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) @@ -247,32 +318,34 @@ def main(): except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) - # Get all the existing hosted zones and save their ID's - zones = {} - results = conn.get_all_hosted_zones() - for r53zone in results['ListHostedZonesResponse']['HostedZones']: - # only save this zone id if the private status of the zone matches - # the private_zone_in boolean specified in the params - if module.boolean(r53zone['Config'].get('PrivateZone', False)) == private_zone_in: - zone_id = r53zone['Id'].replace('/hostedzone/', '') - zones[r53zone['Name']] = zone_id + # Find the named zone ID + zone = get_zone_by_name(conn, module, zone_in, private_zone_in) # Verify that the requested zone is already defined in Route53 - if not zone_in in zones: + if zone is None: errmsg = "Zone %s does not exist in Route53" % zone_in module.fail_json(msg = errmsg) record = {} found_record = False - sets = conn.get_all_rrsets(zones[zone_in]) + wanted_rset = Record(name=record_in, type=type_in, ttl=ttl_in, + identifier=identifier_in, weight=weight_in, region=region_in, + health_check=health_check_in, failover=failover_in) + for v in value_list: + if alias_in: + wanted_rset.set_alias(alias_hosted_zone_id_in, v) + else: + wanted_rset.add_value(v) + + sets = conn.get_all_rrsets(zone.id, name=record_in, type=type_in, identifier=identifier_in) for rset in sets: # Due to a bug in either AWS or Boto, "special" characters are returned as octals, preventing round # tripping of things like * and @. decoded_name = rset.name.replace(r'\052', '*') decoded_name = decoded_name.replace(r'\100', '@') - if rset.type == type_in and decoded_name.lower() == record_in.lower(): + if rset.type == type_in and decoded_name.lower() == record_in.lower() and rset.identifier == identifier_in: found_record = True record['zone'] = zone_in record['type'] = rset.type @@ -280,6 +353,11 @@ def main(): record['ttl'] = rset.ttl record['value'] = ','.join(sorted(rset.resource_records)) record['values'] = sorted(rset.resource_records) + record['identifier'] = rset.identifier + record['weight'] = rset.weight + record['region'] = rset.region + record['failover'] = rset.failover + record['health_check'] = rset.health_check if rset.alias_dns_name: record['alias'] = True record['value'] = rset.alias_dns_name @@ -289,8 +367,9 @@ def main(): record['alias'] = False record['value'] = ','.join(sorted(rset.resource_records)) record['values'] = sorted(rset.resource_records) - if value_list == sorted(rset.resource_records) and int(record['ttl']) == ttl_in and command_in == 'create': + if command_in == 'create' and rset.to_xml() == wanted_rset.to_xml(): module.exit_json(changed=False) + break if command_in == 'get': module.exit_json(changed=False, set=record) @@ -298,26 +377,16 @@ def main(): if command_in == 'delete' and not found_record: module.exit_json(changed=False) - changes = ResourceRecordSets(conn, zones[zone_in]) - - if command_in == 'create' and found_record: - if not module.params['overwrite']: - module.fail_json(msg = "Record already exists with different value. Set 'overwrite' to replace it") - else: - change = changes.add_change("DELETE", record_in, type_in, record['ttl']) - for v in record['values']: - if record['alias']: - change.set_alias(record['alias_hosted_zone_id'], v) - else: - change.add_value(v) + changes = ResourceRecordSets(conn, zone.id) if command_in == 'create' or command_in == 'delete': - change = changes.add_change(command_in.upper(), record_in, type_in, ttl_in) - for v in value_list: - if module.params['alias']: - change.set_alias(alias_hosted_zone_id_in, v) - else: - change.add_value(v) + if command_in == 'create' and found_record: + if not module.params['overwrite']: + module.fail_json(msg = "Record already exists with different value. Set 'overwrite' to replace it") + command = 'UPSERT' + else: + command = command_in.upper() + changes.add_change_record(command, wanted_rset) try: result = commit(changes, retry_interval_in)