Server IP : 85.214.239.14 / Your IP : 3.15.12.133 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /usr/lib/python3/dist-packages/ansible_collections/amazon/aws/plugins/modules/ |
Upload File : |
#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2018, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' --- module: route53 version_added: 5.0.0 short_description: add or delete entries in Amazons Route 53 DNS service description: - Creates and deletes DNS records in Amazons Route 53 service. - This module was originally added to C(community.aws) in release 1.0.0. options: state: description: - Specifies the state of the resource record. required: true aliases: [ 'command' ] choices: [ 'present', 'absent', 'get', 'create', 'delete' ] type: str zone: description: - The DNS zone to modify. - This is a required parameter, if parameter I(hosted_zone_id) is not supplied. type: str hosted_zone_id: description: - The Hosted Zone ID of the DNS zone to modify. - This is a required parameter, if parameter I(zone) is not supplied. type: str record: description: - The full DNS record to create or delete. required: true type: str ttl: description: - The TTL, in second, to give the new record. - Mutually exclusive with I(alias). default: 3600 type: int type: description: - The type of DNS record to create. required: true choices: [ 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'CAA', 'NS', 'SOA' ] type: str alias: description: - Indicates if this is an alias record. - Mutually exclusive with I(ttl). - Defaults to C(false). type: bool alias_hosted_zone_id: description: - The hosted zone identifier. type: str alias_evaluate_target_health: description: - Whether or not to evaluate an alias target health. Useful for aliases to Elastic Load Balancers. type: bool default: false value: description: - The new value when creating a DNS record. YAML lists or multiple comma-spaced values are allowed for non-alias records. type: list elements: str overwrite: description: - Whether an existing record should be overwritten on create if values do not match. type: bool retry_interval: description: - In the case that Route 53 is still servicing a prior request, this module will wait and try again after this many seconds. If you have many domain names, the default of C(500) seconds may be too long. default: 500 type: int private_zone: description: - If set to C(true), the private zone matching the requested name within the domain will be used if there are both public and private zones. - The default is to use the public zone. type: bool default: false identifier: description: - Have to be specified for Weighted, latency-based and failover resource record sets only. An identifier that differentiates among multiple resource record sets that have the same combination of DNS name and type. type: str 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. - Mutually exclusive with I(region) and I(failover). type: int 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 - Mutually exclusive with I(weight) and I(failover). type: str geo_location: description: - Allows to control how Amazon Route 53 responds to DNS queries based on the geographic origin of the query. - Two geolocation resource record sets that specify same geographic location cannot be created. - Non-geolocation resource record sets that have the same values for the Name and Type elements as geolocation resource record sets cannot be created. suboptions: continent_code: description: - The two-letter code for the continent. - Specifying I(continent_code) with either I(country_code) or I(subdivision_code) returns an InvalidInput error. type: str country_code: description: - The two-letter code for a country. - Amazon Route 53 uses the two-letter country codes that are specified in ISO standard 3166-1 alpha-2 . type: str subdivision_code: description: - The two-letter code for a state of the United States. - To specify I(subdivision_code), I(country_code) must be set to C(US). type: str type: dict version_added: 3.3.0 version_added_collection: community.aws health_check: description: - Health check to associate with this record type: str failover: description: - Failover resource record sets only. Whether this is the primary or secondary resource record set. Allowed values are PRIMARY and SECONDARY - Mutually exclusive with I(weight) and I(region). type: str choices: ['SECONDARY', 'PRIMARY'] vpc_id: description: - "When used in conjunction with private_zone: true, this will only modify records in the private hosted zone attached to this VPC." - This allows you to have multiple private hosted zones, all with the same name, attached to different VPCs. type: str wait: description: - Wait until the changes have been replicated to all Amazon Route 53 DNS servers. type: bool default: false wait_timeout: description: - How long to wait for the changes to be replicated, in seconds. default: 300 type: int author: - Bruce Pennypacker (@bpennypacker) - Mike Buzzetti (@jimbydamonk) extends_documentation_fragment: - amazon.aws.aws - amazon.aws.boto3 ''' RETURN = r''' nameservers: description: Nameservers associated with the zone. returned: when state is 'get' type: list sample: - ns-1036.awsdns-00.org. - ns-516.awsdns-00.net. - ns-1504.awsdns-00.co.uk. - ns-1.awsdns-00.com. set: description: Info specific to the resource record. returned: when state is 'get' type: complex contains: alias: description: Whether this is an alias. returned: always type: bool sample: false failover: description: Whether this is the primary or secondary resource record set. returned: always type: str sample: PRIMARY geo_location: description: geograpic location based on which Route53 resonds to DNS queries. returned: when configured type: dict sample: { continent_code: "NA", country_code: "US", subdivision_code: "CA" } version_added: 3.3.0 version_added_collection: community.aws health_check: description: health_check associated with this record. returned: always type: str identifier: description: An identifier that differentiates among multiple resource record sets that have the same combination of DNS name and type. returned: always type: str record: description: Domain name for the record set. returned: always type: str sample: new.foo.com. region: description: Which region this should be associated with for latency-based routing. returned: always type: str sample: us-west-2 ttl: description: Resource record cache TTL. returned: always type: str sample: '3600' type: description: Resource record set type. returned: always type: str sample: A value: description: Record value. returned: always type: str sample: 52.43.18.27 values: description: Record Values. returned: always type: list sample: - 52.43.18.27 weight: description: Weight of the record. returned: always type: str sample: '3' zone: description: Zone this record set belongs to. returned: always type: str sample: foo.bar.com. ''' EXAMPLES = r''' - name: Add new.foo.com as an A record with 3 IPs and wait until the changes have been replicated amazon.aws.route53: state: present zone: foo.com record: new.foo.com type: A ttl: 7200 value: 1.1.1.1,2.2.2.2,3.3.3.3 wait: true - name: Update new.foo.com as an A record with a list of 3 IPs and wait until the changes have been replicated amazon.aws.route53: state: present zone: foo.com record: new.foo.com type: A ttl: 7200 value: - 1.1.1.1 - 2.2.2.2 - 3.3.3.3 wait: true - name: Retrieve the details for new.foo.com amazon.aws.route53: state: get zone: foo.com record: new.foo.com type: A register: rec - name: Delete new.foo.com A record using the results from the get command amazon.aws.route53: state: absent zone: foo.com record: "{{ rec.set.record }}" ttl: "{{ rec.set.ttl }}" type: "{{ rec.set.type }}" value: "{{ rec.set.value }}" # Add an AAAA record. Note that because there are colons in the value # that the IPv6 address must be quoted. Also shows using the old form command=create. - name: Add an AAAA record amazon.aws.route53: command: create zone: foo.com record: localhost.foo.com type: AAAA ttl: 7200 value: "::1" # For more information on SRV records see: # https://en.wikipedia.org/wiki/SRV_record - name: Add a SRV record with multiple fields for a service on port 22222 amazon.aws.route53: state: present zone: foo.com record: "_example-service._tcp.foo.com" type: SRV value: "0 0 22222 host1.foo.com,0 0 22222 host2.foo.com" # Note that TXT and SPF records must be surrounded # by quotes when sent to Route 53: - name: Add a TXT record. amazon.aws.route53: state: present zone: foo.com record: localhost.foo.com type: TXT ttl: 7200 value: '"bar"' - name: Add an alias record that points to an Amazon ELB amazon.aws.route53: state: present zone: foo.com record: elb.foo.com type: A value: "{{ elb_dns_name }}" alias: True alias_hosted_zone_id: "{{ elb_zone_id }}" - name: Retrieve the details for elb.foo.com amazon.aws.route53: state: get zone: foo.com record: elb.foo.com type: A register: rec - name: Delete an alias record using the results from the get command amazon.aws.route53: state: absent zone: foo.com record: "{{ rec.set.record }}" ttl: "{{ rec.set.ttl }}" type: "{{ rec.set.type }}" value: "{{ rec.set.value }}" alias: True alias_hosted_zone_id: "{{ rec.set.alias_hosted_zone_id }}" - name: Add an alias record that points to an Amazon ELB and evaluates it health amazon.aws.route53: state: present zone: foo.com record: elb.foo.com type: A value: "{{ elb_dns_name }}" alias: True alias_hosted_zone_id: "{{ elb_zone_id }}" alias_evaluate_target_health: True - name: Add an AAAA record with Hosted Zone ID amazon.aws.route53: state: present zone: foo.com hosted_zone_id: Z2AABBCCDDEEFF record: localhost.foo.com type: AAAA ttl: 7200 value: "::1" - name: Use a routing policy to distribute traffic amazon.aws.route53: state: present 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" - name: Add a CAA record (RFC 6844) amazon.aws.route53: state: present zone: example.com record: example.com type: CAA value: - 0 issue "ca.example.net" - 0 issuewild ";" - 0 iodef "mailto:security@example.com" - name: Create a record with geo_location - country_code amazon.aws.route53: state: present zone: '{{ zone_one }}' record: 'geo-test.{{ zone_one }}' identifier: "geohost@www" type: A value: 1.1.1.1 ttl: 30 geo_location: country_code: US - name: Create a record with geo_location - subdivision code amazon.aws.route53: state: present zone: '{{ zone_one }}' record: 'geo-test.{{ zone_one }}' identifier: "geohost@www" type: A value: 1.1.1.1 ttl: 30 geo_location: country_code: US subdivision_code: TX ''' from operator import itemgetter try: import botocore except ImportError: pass # Handled by AnsibleAWSModule from ansible.module_utils._text import to_native from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_message from ansible_collections.amazon.aws.plugins.module_utils.core import scrub_none_parameters from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry from ansible_collections.amazon.aws.plugins.module_utils.waiters import get_waiter MAX_AWS_RETRIES = 10 # How many retries to perform when an API call is failing WAIT_RETRY = 5 # how many seconds to wait between propagation status polls @AWSRetry.jittered_backoff(retries=MAX_AWS_RETRIES) def _list_record_sets(route53, **kwargs): paginator = route53.get_paginator('list_resource_record_sets') return paginator.paginate(**kwargs).build_full_result()['ResourceRecordSets'] @AWSRetry.jittered_backoff(retries=MAX_AWS_RETRIES) def _list_hosted_zones(route53, **kwargs): paginator = route53.get_paginator('list_hosted_zones') return paginator.paginate(**kwargs).build_full_result()['HostedZones'] def get_record(route53, zone_id, record_name, record_type, record_identifier): record_sets_results = _list_record_sets(route53, HostedZoneId=zone_id) for record_set in record_sets_results: record_set['Name'] = record_set['Name'].encode().decode('unicode_escape') # If the record name and type is not equal, move to the next record if (record_name.lower(), record_type) != (record_set['Name'].lower(), record_set['Type']): continue if record_identifier and record_identifier != record_set.get("SetIdentifier"): continue return record_set return None def get_zone_id_by_name(route53, module, zone_name, want_private, want_vpc_id): """Finds a zone by name or zone_id""" hosted_zones_results = _list_hosted_zones(route53) for zone in hosted_zones_results: # 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)) zone_id = zone['Id'].replace("/hostedzone/", "") if private_zone == want_private and zone['Name'] == zone_name: if want_vpc_id: # NOTE: These details aren't available in other boto3 methods, hence the necessary # extra API call hosted_zone = route53.get_hosted_zone(aws_retry=True, Id=zone_id) if want_vpc_id in [v['VPCId'] for v in hosted_zone['VPCs']]: return zone_id else: return zone_id return None def format_record(record_in, zone_in, zone_id): """ Formats a record in a way that's consistent with the pre-boto3 migration values as well as returning the 'normal' boto3 style values """ if not record_in: return None record = dict(record_in) record['zone'] = zone_in record['hosted_zone_id'] = zone_id record['type'] = record_in.get('Type', None) record['record'] = record_in.get('Name').encode().decode('unicode_escape') record['ttl'] = record_in.get('TTL', None) record['identifier'] = record_in.get('SetIdentifier', None) record['weight'] = record_in.get('Weight', None) record['region'] = record_in.get('Region', None) record['failover'] = record_in.get('Failover', None) record['health_check'] = record_in.get('HealthCheckId', None) if record['ttl']: record['ttl'] = str(record['ttl']) if record['weight']: record['weight'] = str(record['weight']) if record['region']: record['region'] = str(record['region']) if record_in.get('AliasTarget'): record['alias'] = True record['value'] = record_in['AliasTarget'].get('DNSName') record['values'] = [record_in['AliasTarget'].get('DNSName')] record['alias_hosted_zone_id'] = record_in['AliasTarget'].get('HostedZoneId') record['alias_evaluate_target_health'] = record_in['AliasTarget'].get('EvaluateTargetHealth') else: record['alias'] = False records = [r.get('Value') for r in record_in.get('ResourceRecords')] record['value'] = ','.join(sorted(records)) record['values'] = sorted(records) return record def get_hosted_zone_nameservers(route53, zone_id): hosted_zone_name = route53.get_hosted_zone(aws_retry=True, Id=zone_id)['HostedZone']['Name'] resource_records_sets = _list_record_sets(route53, HostedZoneId=zone_id) nameservers_records = list( filter(lambda record: record['Name'] == hosted_zone_name and record['Type'] == 'NS', resource_records_sets) )[0]['ResourceRecords'] return [ns_record['Value'] for ns_record in nameservers_records] def main(): argument_spec = dict( state=dict(type='str', required=True, choices=['absent', 'create', 'delete', 'get', 'present'], aliases=['command']), zone=dict(type='str'), hosted_zone_id=dict(type='str'), record=dict(type='str', required=True), ttl=dict(type='int', default=3600), type=dict(type='str', required=True, choices=['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SPF', 'SRV', 'TXT']), alias=dict(type='bool'), alias_hosted_zone_id=dict(type='str'), alias_evaluate_target_health=dict(type='bool', default=False), value=dict(type='list', elements='str'), overwrite=dict(type='bool'), retry_interval=dict(type='int', default=500), private_zone=dict(type='bool', default=False), identifier=dict(type='str'), weight=dict(type='int'), region=dict(type='str'), geo_location=dict(type='dict', options=dict( continent_code=dict(type="str"), country_code=dict(type="str"), subdivision_code=dict(type="str")), required=False), health_check=dict(type='str'), failover=dict(type='str', choices=['PRIMARY', 'SECONDARY']), vpc_id=dict(type='str'), wait=dict(type='bool', default=False), wait_timeout=dict(type='int', default=300), ) module = AnsibleAWSModule( argument_spec=argument_spec, supports_check_mode=True, required_one_of=[['zone', 'hosted_zone_id']], # If alias is True then you must specify alias_hosted_zone as well required_together=[['alias', 'alias_hosted_zone_id']], # state=present, absent, create, delete THEN value is required required_if=( ('state', 'present', ['value']), ('state', 'create', ['value']), ), # failover, region and weight are mutually exclusive mutually_exclusive=[ ('failover', 'region', 'weight'), ('alias', 'ttl'), ], # failover, region, weight and geo_location require identifier required_by=dict( failover=('identifier',), region=('identifier',), weight=('identifier',), geo_location=('identifier'), ), ) if module.params['state'] in ('present', 'create'): command_in = 'create' elif module.params['state'] in ('absent', 'delete'): command_in = 'delete' elif module.params['state'] == 'get': command_in = 'get' zone_in = (module.params.get('zone') or '').lower() hosted_zone_id_in = module.params.get('hosted_zone_id') ttl_in = module.params.get('ttl') record_in = module.params.get('record').lower() type_in = module.params.get('type') value_in = module.params.get('value') or [] alias_in = module.params.get('alias') alias_hosted_zone_id_in = module.params.get('alias_hosted_zone_id') alias_evaluate_target_health_in = module.params.get('alias_evaluate_target_health') retry_interval_in = module.params.get('retry_interval') if module.params['vpc_id'] is not None: private_zone_in = True else: 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') vpc_id_in = module.params.get('vpc_id') wait_in = module.params.get('wait') wait_timeout_in = module.params.get('wait_timeout') geo_location = module.params.get('geo_location') if zone_in[-1:] != '.': zone_in += "." if record_in[-1:] != '.': record_in += "." if command_in == 'create' or command_in == 'delete': if alias_in and len(value_in) != 1: module.fail_json(msg="parameter 'value' must contain a single dns name for alias records") if (weight_in is None and region_in is None and failover_in is None and geo_location is None) and identifier_in is not None: module.fail_json(msg="You have specified identifier which makes sense only if you specify one of: weight, region, geo_location or failover.") retry_decorator = AWSRetry.jittered_backoff( retries=MAX_AWS_RETRIES, delay=retry_interval_in, catch_extra_error_codes=['PriorRequestNotComplete'], max_delay=max(60, retry_interval_in), ) # connect to the route53 endpoint try: route53 = module.client('route53', retry_decorator=retry_decorator) except botocore.exceptions.HTTPClientError as e: module.fail_json_aws(e, msg='Failed to connect to AWS') # Find the named zone ID zone_id = hosted_zone_id_in or get_zone_id_by_name(route53, module, zone_in, private_zone_in, vpc_id_in) # Verify that the requested zone is already defined in Route53 if zone_id is None: errmsg = "Zone %s does not exist in Route53" % (zone_in or hosted_zone_id_in) module.fail_json(msg=errmsg) aws_record = get_record(route53, zone_id, record_in, type_in, identifier_in) resource_record_set = scrub_none_parameters({ 'Name': record_in, 'Type': type_in, 'Weight': weight_in, 'Region': region_in, 'Failover': failover_in, 'TTL': ttl_in, 'ResourceRecords': [dict(Value=value) for value in value_in], 'HealthCheckId': health_check_in, 'SetIdentifier': identifier_in, }) if geo_location: continent_code = geo_location.get('continent_code') country_code = geo_location.get('country_code') subdivision_code = geo_location.get('subdivision_code') if continent_code and (country_code or subdivision_code): module.fail_json(changed=False, msg='While using geo_location, continent_code is mutually exclusive with country_code and subdivision_code.') if not any([continent_code, country_code, subdivision_code]): module.fail_json(changed=False, msg='To use geo_location please specify either continent_code, country_code, or subdivision_code.') if geo_location.get('subdivision_code') and geo_location.get('country_code').lower() != 'us': module.fail_json(changed=False, msg='To use subdivision_code, you must specify country_code as US.') # Build geo_location suboptions specification resource_record_set['GeoLocation'] = {} if continent_code: resource_record_set['GeoLocation']['ContinentCode'] = continent_code if country_code: resource_record_set['GeoLocation']['CountryCode'] = country_code if subdivision_code: resource_record_set['GeoLocation']['SubdivisionCode'] = subdivision_code if command_in == 'delete' and aws_record is not None: resource_record_set['TTL'] = aws_record.get('TTL') if not resource_record_set['ResourceRecords']: resource_record_set['ResourceRecords'] = aws_record.get('ResourceRecords') if alias_in: resource_record_set['AliasTarget'] = dict( HostedZoneId=alias_hosted_zone_id_in, DNSName=value_in[0], EvaluateTargetHealth=alias_evaluate_target_health_in ) if 'ResourceRecords' in resource_record_set: del resource_record_set['ResourceRecords'] if 'TTL' in resource_record_set: del resource_record_set['TTL'] # On CAA records order doesn't matter if type_in == 'CAA': resource_record_set['ResourceRecords'] = sorted(resource_record_set['ResourceRecords'], key=itemgetter('Value')) if aws_record: aws_record['ResourceRecords'] = sorted(aws_record['ResourceRecords'], key=itemgetter('Value')) if command_in == 'create' and aws_record == resource_record_set: rr_sets = [camel_dict_to_snake_dict(resource_record_set)] module.exit_json(changed=False, resource_records_sets=rr_sets) if command_in == 'get': if type_in == 'NS': ns = aws_record.get('values', []) else: # Retrieve name servers associated to the zone. ns = get_hosted_zone_nameservers(route53, zone_id) formatted_aws = format_record(aws_record, zone_in, zone_id) if formatted_aws is None: # record does not exist module.exit_json(changed=False, set=[], nameservers=ns, resource_record_sets=[]) rr_sets = [camel_dict_to_snake_dict(aws_record)] module.exit_json(changed=False, set=formatted_aws, nameservers=ns, resource_record_sets=rr_sets) if command_in == 'delete' and not aws_record: module.exit_json(changed=False) if command_in == 'create' or command_in == 'delete': if command_in == 'create' and aws_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() if not module.check_mode: try: change_resource_record_sets = route53.change_resource_record_sets( aws_retry=True, HostedZoneId=zone_id, ChangeBatch=dict( Changes=[ dict( Action=command, ResourceRecordSet=resource_record_set ) ] ) ) if wait_in: waiter = get_waiter(route53, 'resource_record_sets_changed') waiter.wait( Id=change_resource_record_sets['ChangeInfo']['Id'], WaiterConfig=dict( Delay=WAIT_RETRY, MaxAttempts=wait_timeout_in // WAIT_RETRY, ) ) except is_boto3_error_message('but it already exists'): module.exit_json(changed=False) except botocore.exceptions.WaiterError as e: module.fail_json_aws(e, msg='Timeout waiting for resource records changes to be applied') except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except module.fail_json_aws(e, msg='Failed to update records') except Exception as e: module.fail_json(msg='Unhandled exception. (%s)' % to_native(e)) rr_sets = [camel_dict_to_snake_dict(resource_record_set)] formatted_aws = format_record(aws_record, zone_in, zone_id) formatted_record = format_record(resource_record_set, zone_in, zone_id) module.exit_json( changed=True, diff=dict( before=formatted_aws, after=formatted_record if command_in != 'delete' else {}, resource_record_sets=rr_sets, ), ) if __name__ == '__main__': main()