ansible-later/env_27/lib/python2.7/site-packages/ansible/modules/cloud/amazon/route53.py
2019-04-11 13:00:36 +02:00

685 lines
24 KiB
Python

#!/usr/bin/python
# Copyright: 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['stableinterface'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: route53
version_added: "1.3"
short_description: add or delete entries in Amazons Route53 DNS service
description:
- Creates and deletes DNS records in Amazons Route53 service
options:
state:
description:
- Specifies the state of the resource record. As of Ansible 2.4, the I(command) option has been changed
to I(state) as default and the choices 'present' and 'absent' have been added, but I(command) still works as well.
required: true
aliases: [ 'command' ]
choices: [ 'present', 'absent', 'get', 'create', 'delete' ]
zone:
description:
- The DNS zone to modify
required: true
hosted_zone_id:
description:
- The Hosted Zone ID of the DNS zone to modify
version_added: "2.0"
record:
description:
- The full DNS record to create or delete
required: true
ttl:
description:
- The TTL to give the new record
default: 3600 (one hour)
type:
description:
- The type of DNS record to create
required: true
choices: [ 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'CAA', 'NS', 'SOA' ]
alias:
description:
- Indicates if this is an alias record.
version_added: "1.9"
type: bool
default: 'no'
alias_hosted_zone_id:
description:
- The hosted zone identifier.
version_added: "1.9"
alias_evaluate_target_health:
description:
- Whether or not to evaluate an alias target health. Useful for aliases to Elastic Load Balancers.
type: bool
default: no
version_added: "2.1"
value:
description:
- The new value when creating a DNS record. YAML lists or multiple comma-spaced values are allowed for non-alias records.
- When deleting a record all values for the record must be specified or Route53 will not delete it.
overwrite:
description:
- Whether an existing record should be overwritten on create if values do not match
retry_interval:
description:
- In the case that route53 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 500 seconds may be too long.
default: 500
private_zone:
description:
- If set to C(yes), 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: 'no'
version_added: "1.9"
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.
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.
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
version_added: "2.0"
health_check:
description:
- Health check to associate with this record
version_added: "2.0"
failover:
description:
- Failover resource record sets only. Whether this is the primary or
secondary resource record set. Allowed values are PRIMARY and SECONDARY
version_added: "2.0"
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.
version_added: "2.0"
wait:
description:
- Wait until the changes have been replicated to all Amazon Route 53 DNS servers.
type: bool
default: 'no'
version_added: "2.1"
wait_timeout:
description:
- How long to wait for the changes to be replicated, in seconds.
default: 300
version_added: "2.1"
author:
- "Bruce Pennypacker (@bpennypacker)"
- "Mike Buzzetti <mike.buzzetti@gmail.com>"
extends_documentation_fragment: aws
'''
RETURN = '''
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: ""
returned: always
type: NoneType
sample: null
health_check:
description: health_check associated with this record
returned: always
type: NoneType
sample: null
identifier:
description: ""
returned: always
type: NoneType
sample: null
record:
description: domain name for the record set
returned: always
type: string
sample: new.foo.com.
region:
description: ""
returned: always
type:
sample:
ttl:
description: resource record cache TTL
returned: always
type: string
sample: '3600'
type:
description: record set type
returned: always
type: string
sample: A
value:
description: value
returned: always
type: string
sample: 52.43.18.27
values:
description: values
returned: always
type: list
sample:
- 52.43.18.27
weight:
description: weight of the record
returned: always
type: string
sample: '3'
zone:
description: zone this record set belongs to
returned: always
type: string
sample: foo.bar.com.
'''
EXAMPLES = '''
# Add new.foo.com as an A record with 3 IPs and wait until the changes have been replicated
- 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: yes
# Update new.foo.com as an A record with a list of 3 IPs and wait until the changes have been replicated
- 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: yes
# Retrieve the details for new.foo.com
- route53:
state: get
zone: foo.com
record: new.foo.com
type: A
register: rec
# Delete new.foo.com A record using the results from the get command
- 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.
- route53:
command: create
zone: foo.com
record: localhost.foo.com
type: AAAA
ttl: 7200
value: "::1"
# Add a SRV record with multiple fields for a service on port 22222
# For more information on SRV records see:
# https://en.wikipedia.org/wiki/SRV_record
- 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"
# Add a TXT record. Note that TXT and SPF records must be surrounded
# by quotes when sent to Route 53:
- route53:
state: present
zone: foo.com
record: localhost.foo.com
type: TXT
ttl: 7200
value: '"bar"'
# Add an alias record that points to an Amazon ELB:
- 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 }}"
# Retrieve the details for elb.foo.com
- route53:
state: get
zone: foo.com
record: elb.foo.com
type: A
register: rec
# Delete an alias record using the results from the get command
- 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 }}"
# Add an alias record that points to an Amazon ELB and evaluates it health:
- 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
# Add an AAAA record with Hosted Zone ID.
- route53:
state: present
zone: foo.com
hosted_zone_id: Z2AABBCCDDEEFF
record: localhost.foo.com
type: AAAA
ttl: 7200
value: "::1"
# Use a routing policy to distribute traffic:
- 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"
# Add a CAA record (RFC 6844):
- 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"
'''
import time
import distutils.version
try:
import boto
import boto.ec2
from boto.route53 import Route53Connection
from boto.route53.record import Record, ResourceRecordSets
from boto.route53.status import Status
HAS_BOTO = True
except ImportError:
HAS_BOTO = False
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info
MINIMUM_BOTO_VERSION = '2.28.0'
WAIT_RETRY_SLEEP = 5 # how many seconds to wait between propagation status polls
class TimeoutError(Exception):
pass
def get_zone_by_name(conn, module, zone_name, want_private, zone_id, want_vpc_id):
"""Finds a zone by name or zone_id"""
for zone in invoke_with_throttling_retries(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 and zone_id is None) or zone.id.replace('/hostedzone/', '') == zone_id):
if want_vpc_id:
# NOTE: These details aren't available in other boto methods, hence the necessary
# extra API call
hosted_zone = invoke_with_throttling_retries(conn.get_hosted_zone, zone.id)
zone_details = hosted_zone['GetHostedZoneResponse']
# this is to deal with this boto bug: https://github.com/boto/boto/pull/2882
if isinstance(zone_details['VPCs'], dict):
if zone_details['VPCs']['VPC']['VPCId'] == want_vpc_id:
return zone
else: # Forward compatibility for when boto fixes that bug
if want_vpc_id in [v['VPCId'] for v in zone_details['VPCs']]:
return zone
else:
return zone
return None
def commit(changes, retry_interval, wait, wait_timeout):
"""Commit changes, but retry PriorRequestNotComplete errors."""
result = None
retry = 10
while True:
try:
retry -= 1
result = changes.commit()
break
except boto.route53.exception.DNSServerError as e:
code = e.body.split("<Code>")[1]
code = code.split("</Code>")[0]
if code != 'PriorRequestNotComplete' or retry < 0:
raise e
time.sleep(float(retry_interval))
if wait:
timeout_time = time.time() + wait_timeout
connection = changes.connection
change = result['ChangeResourceRecordSetsResponse']['ChangeInfo']
status = Status(connection, change)
while status.status != 'INSYNC' and time.time() < timeout_time:
time.sleep(WAIT_RETRY_SLEEP)
status.update()
if time.time() >= timeout_time:
raise TimeoutError()
return result
# Shamelessly copied over from https://git.io/vgmDG
IGNORE_CODE = 'Throttling'
MAX_RETRIES = 5
def invoke_with_throttling_retries(function_ref, *argv, **kwargs):
retries = 0
while True:
try:
retval = function_ref(*argv, **kwargs)
return retval
except boto.exception.BotoServerError as e:
if e.code != IGNORE_CODE or retries == MAX_RETRIES:
raise e
time.sleep(5 * (2**retries))
retries += 1
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(dict(
state=dict(aliases=['command'], choices=['present', 'absent', 'get', 'create', 'delete'], required=True),
zone=dict(required=True),
hosted_zone_id=dict(required=False, default=None),
record=dict(required=True),
ttl=dict(required=False, type='int', default=3600),
type=dict(choices=['A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'CAA', 'NS', 'SOA'], required=True),
alias=dict(required=False, type='bool'),
alias_hosted_zone_id=dict(required=False),
alias_evaluate_target_health=dict(required=False, type='bool', default=False),
value=dict(required=False, type='list'),
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, default=None),
weight=dict(required=False, type='int'),
region=dict(required=False),
health_check=dict(required=False),
failover=dict(required=False, choices=['PRIMARY', 'SECONDARY']),
vpc_id=dict(required=False),
wait=dict(required=False, type='bool', default=False),
wait_timeout=dict(required=False, type='int', default=300),
))
# state=present, absent, create, delete THEN value is required
required_if = [('state', 'present', ['value']), ('state', 'create', ['value'])]
required_if.extend([('state', 'absent', ['value']), ('state', 'delete', ['value'])])
# If alias is True then you must specify alias_hosted_zone as well
required_together = [['alias', 'alias_hosted_zone_id']]
# failover, region, and weight are mutually exclusive
mutually_exclusive = [('failover', 'region', 'weight')]
module = AnsibleModule(argument_spec=argument_spec, required_together=required_together, required_if=required_if,
mutually_exclusive=mutually_exclusive, supports_check_mode=True)
if not HAS_BOTO:
module.fail_json(msg='boto required for this module')
if distutils.version.StrictVersion(boto.__version__) < distutils.version.StrictVersion(MINIMUM_BOTO_VERSION):
module.fail_json(msg='Found boto in version %s, but >= %s is required' % (boto.__version__, MINIMUM_BOTO_VERSION))
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').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')
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
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 not None or region_in is not None or failover_in is not None) and identifier_in is None:
module.fail_json(msg="If you specify failover, region or weight you must also specify identifier")
if (weight_in is None and region_in is None and failover_in 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 or failover.")
# connect to the route53 endpoint
try:
conn = Route53Connection(**aws_connect_kwargs)
except boto.exception.BotoServerError as e:
module.fail_json(msg=e.error_message)
# Find the named zone ID
zone = get_zone_by_name(conn, module, zone_in, private_zone_in, hosted_zone_id_in, vpc_id_in)
# Verify that the requested zone is already defined in Route53
if zone is None:
errmsg = "Zone %s does not exist in Route53" % zone_in
module.fail_json(msg=errmsg)
record = {}
found_record = False
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_in:
if alias_in:
wanted_rset.set_alias(alias_hosted_zone_id_in, v, alias_evaluate_target_health_in)
else:
wanted_rset.add_value(v)
need_to_sort_records = (type_in == 'CAA')
# Sort records for wanted_rset if necessary (keep original list)
unsorted_records = wanted_rset.resource_records
if need_to_sort_records:
wanted_rset.resource_records = sorted(unsorted_records)
sets = invoke_with_throttling_retries(conn.get_all_rrsets, zone.id, name=record_in,
type=type_in, identifier=identifier_in)
sets_iter = iter(sets)
while True:
try:
rset = invoke_with_throttling_retries(next, sets_iter)
except StopIteration:
break
# 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', '@')
# Need to save this changes in rset, because of comparing rset.to_xml() == wanted_rset.to_xml() in next block
rset.name = decoded_name
if identifier_in is not None:
identifier_in = str(identifier_in)
if rset.type == type_in and decoded_name.lower() == record_in.lower() and rset.identifier == identifier_in:
if need_to_sort_records:
# Sort records
rset.resource_records = sorted(rset.resource_records)
found_record = True
record['zone'] = zone_in
record['type'] = rset.type
record['record'] = decoded_name
record['ttl'] = rset.ttl
if hosted_zone_id_in:
record['hosted_zone_id'] = hosted_zone_id_in
record['identifier'] = rset.identifier
record['weight'] = rset.weight
record['region'] = rset.region
record['failover'] = rset.failover
record['health_check'] = rset.health_check
if hosted_zone_id_in:
record['hosted_zone_id'] = hosted_zone_id_in
if rset.alias_dns_name:
record['alias'] = True
record['value'] = rset.alias_dns_name
record['values'] = [rset.alias_dns_name]
record['alias_hosted_zone_id'] = rset.alias_hosted_zone_id
record['alias_evaluate_target_health'] = rset.alias_evaluate_target_health
else:
record['alias'] = False
record['value'] = ','.join(sorted(rset.resource_records))
record['values'] = sorted(rset.resource_records)
if command_in == 'create' and rset.to_xml() == wanted_rset.to_xml():
module.exit_json(changed=False)
# We need to look only at the first rrset returned by the above call,
# so break here. The returned elements begin with the one matching our
# requested name, type, and identifier, if such an element exists,
# followed by all others that come after it in alphabetical order.
# Therefore, if the first set does not match, no subsequent set will
# match either.
break
if command_in == 'get':
if type_in == 'NS':
ns = record.get('values', [])
else:
# Retrieve name servers associated to the zone.
z = invoke_with_throttling_retries(conn.get_zone, zone_in)
ns = invoke_with_throttling_retries(z.get_nameservers)
module.exit_json(changed=False, set=record, nameservers=ns)
if command_in == 'delete' and not found_record:
module.exit_json(changed=False)
changes = ResourceRecordSets(conn, zone.id)
if command_in == 'create' or command_in == 'delete':
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()
# Restore original order of records
wanted_rset.resource_records = unsorted_records
changes.add_change_record(command, wanted_rset)
if not module.check_mode:
try:
invoke_with_throttling_retries(commit, changes, retry_interval_in, wait_in, wait_timeout_in)
except boto.route53.exception.DNSServerError as e:
txt = e.body.split("<Message>")[1]
txt = txt.split("</Message>")[0]
if "but it already exists" in txt:
module.exit_json(changed=False)
else:
module.fail_json(msg=txt)
except TimeoutError:
module.fail_json(msg='Timeout waiting for changes to replicate')
module.exit_json(changed=True)
if __name__ == '__main__':
main()