diff --git a/cloud/google/gcdns_record.py b/cloud/google/gcdns_record.py
new file mode 100644
index 00000000000..ebccfca5dcb
--- /dev/null
+++ b/cloud/google/gcdns_record.py
@@ -0,0 +1,790 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 CallFire Inc.
+#
+# This file is part of Ansible.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+################################################################################
+# Documentation
+################################################################################
+
+DOCUMENTATION = '''
+---
+module: gcdns_record
+short_description: Creates or removes resource records in Google Cloud DNS
+description:
+ - Creates or removes resource records in Google Cloud DNS.
+version_added: "2.2"
+author: "William Albert (@walbert947)"
+requirements:
+ - "python >= 2.6"
+ - "apache-libcloud >= 0.19.0"
+options:
+ state:
+ description:
+ - Whether the given resource record should or should not be present.
+ required: false
+ choices: ["present", "absent"]
+ default: "present"
+ record:
+ description:
+ - The fully-qualified domain name of the resource record.
+ required: true
+ aliases: ['name']
+ zone:
+ description:
+ - The DNS domain name of the zone (e.g., example.com).
+ - One of either I(zone) or I(zone_id) must be specified as an
+ option, or the module will fail.
+ - If both I(zone) and I(zone_id) are specifed, I(zone_id) will be
+ used.
+ required: false
+ zone_id:
+ description:
+ - The Google Cloud ID of the zone (e.g., example-com).
+ - One of either I(zone) or I(zone_id) must be specified as an
+ option, or the module will fail.
+ - These usually take the form of domain names with the dots replaced
+ with dashes. A zone ID will never have any dots in it.
+ - I(zone_id) can be faster than I(zone) in projects with a large
+ number of zones.
+ - If both I(zone) and I(zone_id) are specifed, I(zone_id) will be
+ used.
+ required: false
+ type:
+ description:
+ - The type of resource record to add.
+ required: true
+ choices: [ 'A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'SOA', 'NS', 'MX', 'SPF', 'PTR' ]
+ values:
+ description:
+ - The values to use for the resource record.
+ - I(values) must be specified if I(state) is C(present) or
+ I(overwrite) is C(True), or the module will fail.
+ - Valid values vary based on the record's I(type). In addition,
+ resource records that contain a DNS domain name in the value
+ field (e.g., CNAME, PTR, SRV, .etc) MUST include a trailing dot
+ in the value.
+ - Individual string values for TXT records must be enclosed in
+ double quotes.
+ - For resource records that have the same name but different
+ values (e.g., multiple A records), they must be defined as
+ multiple list entries in a single record.
+ required: false
+ aliases: ['value']
+ ttl:
+ description:
+ - The amount of time in seconds that a resource record will remain
+ cached by a caching resolver.
+ required: false
+ default: 300
+ overwrite:
+ description:
+ - Whether an attempt to overwrite an existing record should succeed
+ or fail. The behavior of this option depends on I(state).
+ - If I(state) is C(present) and I(overwrite) is C(True), this
+ module will replace an existing resource record of the same name
+ with the provided I(values). If I(state) is C(present) and
+ I(overwrite) is C(False), this module will fail if there is an
+ existing resource record with the same name and type, but
+ different resource data.
+ - If I(state) is C(absent) and I(overwrite) is C(True), this
+ module will remove the given resource record unconditionally.
+ If I(state) is C(absent) and I(overwrite) is C(False), this
+ module will fail if the provided values do not match exactly
+ with the existing resource record's values.
+ required: false
+ choices: [True, False]
+ default: False
+ service_account_email:
+ description:
+ - The e-mail address for a service account with access to Google
+ Cloud DNS.
+ required: false
+ default: null
+ pem_file:
+ description:
+ - The path to the PEM file associated with the service account
+ email.
+ - This option is deprecated and may be removed in a future release.
+ Use I(credentials_file) instead.
+ required: false
+ default: null
+ credentials_file:
+ description:
+ - The path to the JSON file associated with the service account
+ email.
+ required: false
+ default: null
+ project_id:
+ description:
+ - The Google Cloud Platform project ID to use.
+ required: false
+ default: null
+notes:
+ - See also M(gcdns_zone).
+ - This modules's underlying library does not support in-place updates for
+ DNS resource records. Instead, resource records are quickly deleted and
+ recreated.
+ - SOA records are technically supported, but their functionality is limited
+ to verifying that a zone's existing SOA record matches a pre-determined
+ value. The SOA record cannot be updated.
+ - Root NS records cannot be updated.
+ - NAPTR records are not supported.
+'''
+
+EXAMPLES = '''
+# Create an A record.
+- gcdns_record:
+ record: 'www1.example.com'
+ zone: 'example.com'
+ type: A
+ value: '1.2.3.4'
+
+# Update an existing record.
+- gcdns_record:
+ record: 'www1.example.com'
+ zone: 'example.com'
+ type: A
+ overwrite: true
+ value: '5.6.7.8'
+
+# Remove an A record.
+- gcdns_record:
+ record: 'www1.example.com'
+ zone_id: 'example-com'
+ state: absent
+ type: A
+ value: '5.6.7.8'
+
+# Create a CNAME record.
+- gcdns_record:
+ record: 'www.example.com'
+ zone_id: 'example-com'
+ type: CNAME
+ value: 'www.example.com.' # Note the trailing dot
+
+# Create an MX record with a custom TTL.
+- gcdns_record:
+ record: 'example.com'
+ zone: 'example.com'
+ type: MX
+ ttl: 3600
+ value: '10 mail.example.com.' # Note the trailing dot
+
+# Create multiple A records with the same name.
+- gcdns_record:
+ record: 'api.example.com'
+ zone_id: 'example-com'
+ type: A
+ values:
+ - '10.1.2.3'
+ - '10.4.5.6'
+ - '10.7.8.9'
+ - '192.168.5.10'
+
+# Change the value of an existing record with multiple values.
+- gcdns_record:
+ record: 'api.example.com'
+ zone: 'example.com'
+ type: A
+ overwrite: true
+ values: # WARNING: All values in a record will be replaced
+ - '10.1.2.3'
+ - '10.5.5.7' # The changed record
+ - '10.7.8.9'
+ - '192.168.5.10'
+
+# Safely remove a multi-line record.
+- gcdns_record:
+ record: 'api.example.com'
+ zone_id: 'example-com'
+ state: absent
+ type: A
+ values: # NOTE: All of the values must match exactly
+ - '10.1.2.3'
+ - '10.5.5.7'
+ - '10.7.8.9'
+ - '192.168.5.10'
+
+# Unconditionally remove a record.
+- gcdns_record:
+ record: 'api.example.com'
+ zone_id: 'example-com'
+ state: absent
+ overwrite: true # overwrite is true, so no values are needed
+ type: A
+
+# Create an AAAA record
+- gcdns_record:
+ record: 'www1.example.com'
+ zone: 'example.com'
+ type: AAAA
+ value: 'fd00:db8::1'
+
+# Create a PTR record
+- gcdns_record:
+ record: '10.5.168.192.in-addr.arpa'
+ zone: '5.168.192.in-addr.arpa'
+ type: PTR
+ value: 'api.example.com.' # Note the trailing dot.
+
+# Create an NS record
+- gcdns_record:
+ record: 'subdomain.example.com'
+ zone: 'example.com'
+ type: NS
+ ttl: 21600
+ values:
+ - 'ns-cloud-d1.googledomains.com.' # Note the trailing dots on values
+ - 'ns-cloud-d2.googledomains.com.'
+ - 'ns-cloud-d3.googledomains.com.'
+ - 'ns-cloud-d4.googledomains.com.'
+
+# Create a TXT record
+- gcdns_record:
+ record: 'example.com'
+ zone_id: 'example-com'
+ type: TXT
+ values:
+ - '"v=spf1 include:_spf.google.com -all"' # A single-string TXT value
+ - '"hello " "world"' # A multi-string TXT value
+'''
+
+RETURN = '''
+overwrite:
+ description: Whether to the module was allowed to overwrite the record
+ returned: success
+ type: boolean
+ sample: True
+record:
+ description: Fully-qualified domain name of the resource record
+ returned: success
+ type: string
+ sample: mail.example.com.
+state:
+ description: Whether the record is present or absent
+ returned: success
+ type: string
+ sample: present
+ttl:
+ description: The time-to-live of the resource record
+ returned: success
+ type: int
+ sample: 300
+type:
+ description: The type of the resource record
+ returned: success
+ type: string
+ sample: A
+values:
+ description: The resource record values
+ returned: success
+ type: list
+ sample: ['5.6.7.8', '9.10.11.12']
+zone:
+ description: The dns name of the zone
+ returned: success
+ type: string
+ sample: example.com.
+zone_id:
+ description: The Google Cloud DNS ID of the zone
+ returned: success
+ type: string
+ sample: example-com
+'''
+
+
+################################################################################
+# Imports
+################################################################################
+
+import socket
+from distutils.version import LooseVersion
+
+try:
+ from libcloud import __version__ as LIBCLOUD_VERSION
+ from libcloud.common.google import InvalidRequestError
+ from libcloud.common.types import LibcloudError
+ from libcloud.dns.types import Provider
+ from libcloud.dns.types import RecordDoesNotExistError
+ from libcloud.dns.types import ZoneDoesNotExistError
+ HAS_LIBCLOUD = True
+except ImportError:
+ HAS_LIBCLOUD = False
+
+
+################################################################################
+# Constants
+################################################################################
+
+# Apache libcloud 0.19.0 was the first to contain the non-beta Google Cloud DNS
+# v1 API. Earlier versions contained the beta v1 API, which has since been
+# deprecated and decommissioned.
+MINIMUM_LIBCLOUD_VERSION = '0.19.0'
+
+# The libcloud Google Cloud DNS provider.
+PROVIDER = Provider.GOOGLE
+
+# The records that libcloud's Google Cloud DNS provider supports.
+#
+# Libcloud has a RECORD_TYPE_MAP dictionary in the provider that also contains
+# this information and is the authoritative source on which records are
+# supported, but accessing the dictionary requires creating a Google Cloud DNS
+# driver object, which is done in a helper module.
+#
+# I'm hard-coding the supported record types here, because they (hopefully!)
+# shouldn't change much, and it allows me to use it as a "choices" parameter
+# in an AnsibleModule argument_spec.
+SUPPORTED_RECORD_TYPES = [ 'A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'SOA', 'NS', 'MX', 'SPF', 'PTR' ]
+
+
+################################################################################
+# Functions
+################################################################################
+
+def create_record(module, gcdns, zone, record):
+ """Creates or overwrites a resource record."""
+
+ overwrite = module.boolean(module.params['overwrite'])
+ record_name = module.params['record']
+ record_type = module.params['type']
+ ttl = module.params['ttl']
+ values = module.params['values']
+ data = dict(ttl=ttl, rrdatas=values)
+
+ # Google Cloud DNS wants the trailing dot on all DNS names.
+ if record_name[-1] != '.':
+ record_name = record_name + '.'
+
+ # If we found a record, we need to check if the values match.
+ if record is not None:
+ # If the record matches, we obviously don't have to change anything.
+ if _records_match(record.data['ttl'], record.data['rrdatas'], ttl, values):
+ return False
+
+ # The record doesn't match, so we need to check if we can overwrite it.
+ if not overwrite:
+ module.fail_json(
+ msg = 'cannot overwrite existing record, overwrite protection enabled',
+ changed = False
+ )
+
+ # The record either doesn't exist, or it exists and we can overwrite it.
+ if record is None and not module.check_mode:
+ # There's no existing record, so we'll just create it.
+ try:
+ gcdns.create_record(record_name, zone, record_type, data)
+ except InvalidRequestError as error:
+ if error.code == 'invalid':
+ # The resource record name and type are valid by themselves, but
+ # not when combined (e.g., an 'A' record with "www.example.com"
+ # as its value).
+ module.fail_json(
+ msg = 'value is invalid for the given type: ' +
+ "%s, got value: %s" % (record_type, values),
+ changed = False
+ )
+
+ elif error.code == 'cnameResourceRecordSetConflict':
+ # We're attempting to create a CNAME resource record when we
+ # already have another type of resource record with the name
+ # domain name.
+ module.fail_json(
+ msg = "non-CNAME resource record already exists: %s" % record_name,
+ changed = False
+ )
+
+ else:
+ # The error is something else that we don't know how to handle,
+ # so we'll just re-raise the exception.
+ raise
+
+ elif record is not None and not module.check_mode:
+ # The Google provider in libcloud doesn't support updating a record in
+ # place, so if the record already exists, we need to delete it and
+ # recreate it using the new information.
+ gcdns.delete_record(record)
+
+ try:
+ gcdns.create_record(record_name, zone, record_type, data)
+ except InvalidRequestError:
+ # Something blew up when creating the record. This will usually be a
+ # result of invalid value data in the new record. Unfortunately, we
+ # already changed the state of the record by deleting the old one,
+ # so we'll try to roll back before failing out.
+ try:
+ gcdns.create_record(record.name, record.zone, record.type, record.data)
+ module.fail_json(
+ msg = 'error updating record, the original record was restored',
+ changed = False
+ )
+ except LibcloudError:
+ # We deleted the old record, couldn't create the new record, and
+ # couldn't roll back. That really sucks. We'll dump the original
+ # record to the failure output so the user can resore it if
+ # necessary.
+ module.fail_json(
+ msg = 'error updating record, and could not restore original record, ' +
+ "original name: %s " % record.name +
+ "original zone: %s " % record.zone +
+ "original type: %s " % record.type +
+ "original data: %s" % record.data,
+ changed = True)
+
+ return True
+
+
+def remove_record(module, gcdns, record):
+ """Remove a resource record."""
+
+ overwrite = module.boolean(module.params['overwrite'])
+ ttl = module.params['ttl']
+ values = module.params['values']
+
+ # If there is no record, we're obviously done.
+ if record is None:
+ return False
+
+ # If there is an existing record, do our values match the values of the
+ # existing record?
+ if not overwrite:
+ if not _records_match(record.data['ttl'], record.data['rrdatas'], ttl, values):
+ module.fail_json(
+ msg = 'cannot delete due to non-matching ttl or values: ' +
+ "ttl: %d, values: %s " % (ttl, values) +
+ "original ttl: %d, original values: %s" % (record.data['ttl'], record.data['rrdatas']),
+ changed = False
+ )
+
+ # If we got to this point, we're okay to delete the record.
+ if not module.check_mode:
+ gcdns.delete_record(record)
+
+ return True
+
+
+def _get_record(gcdns, zone, record_type, record_name):
+ """Gets the record object for a given FQDN."""
+
+ # The record ID is a combination of its type and FQDN. For example, the
+ # ID of an A record for www.example.com would be 'A:www.example.com.'
+ record_id = "%s:%s" % (record_type, record_name)
+
+ try:
+ return gcdns.get_record(zone.id, record_id)
+ except RecordDoesNotExistError:
+ return None
+
+
+def _get_zone(gcdns, zone_name, zone_id):
+ """Gets the zone object for a given domain name."""
+
+ if zone_id is not None:
+ try:
+ return gcdns.get_zone(zone_id)
+ except ZoneDoesNotExistError:
+ return None
+
+ # To create a zone, we need to supply a domain name. However, to delete a
+ # zone, we need to supply a zone ID. Zone ID's are often based on domain
+ # names, but that's not guaranteed, so we'll iterate through the list of
+ # zones to see if we can find a matching domain name.
+ available_zones = gcdns.iterate_zones()
+ found_zone = None
+
+ for zone in available_zones:
+ if zone.domain == zone_name:
+ found_zone = zone
+ break
+
+ return found_zone
+
+
+def _records_match(old_ttl, old_values, new_ttl, new_values):
+ """Checks to see if original and new TTL and values match."""
+
+ matches = True
+
+ if old_ttl != new_ttl:
+ matches = False
+ if old_values != new_values:
+ matches = False
+
+ return matches
+
+
+def _sanity_check(module):
+ """Run sanity checks that don't depend on info from the zone/record."""
+
+ overwrite = module.params['overwrite']
+ record_name = module.params['record']
+ record_type = module.params['type']
+ state = module.params['state']
+ ttl = module.params['ttl']
+ values = module.params['values']
+
+ # Apache libcloud needs to be installed and at least the minimum version.
+ if not HAS_LIBCLOUD:
+ module.fail_json(
+ msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION,
+ changed = False
+ )
+ elif LooseVersion(LIBCLOUD_VERSION) < MINIMUM_LIBCLOUD_VERSION:
+ module.fail_json(
+ msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION,
+ changed = False
+ )
+
+ # A negative TTL is not permitted (how would they even work?!).
+ if ttl < 0:
+ module.fail_json(
+ msg = 'TTL cannot be less than zero, got: %d' % ttl,
+ changed = False
+ )
+
+ # Deleting SOA records is not permitted.
+ if record_type == 'SOA' and state == 'absent':
+ module.fail_json(msg='cannot delete SOA records', changed=False)
+
+ # Updating SOA records is not permitted.
+ if record_type == 'SOA' and state == 'present' and overwrite:
+ module.fail_json(msg='cannot update SOA records', changed=False)
+
+ # Some sanity checks depend on what value was supplied.
+ if values is not None and (state == 'present' or not overwrite):
+ # A records must contain valid IPv4 addresses.
+ if record_type == 'A':
+ for value in values:
+ try:
+ socket.inet_aton(value)
+ except socket.error:
+ module.fail_json(
+ msg = 'invalid A record value, got: %s' % value,
+ changed = False
+ )
+
+ # AAAA records must contain valid IPv6 addresses.
+ if record_type == 'AAAA':
+ for value in values:
+ try:
+ socket.inet_pton(socket.AF_INET6, value)
+ except socket.error:
+ module.fail_json(
+ msg = 'invalid AAAA record value, got: %s' % value,
+ changed = False
+ )
+
+ # CNAME and SOA records can't have multiple values.
+ if record_type in ['CNAME', 'SOA'] and len(values) > 1:
+ module.fail_json(
+ msg = 'CNAME or SOA records cannot have more than one value, ' +
+ "got: %s" % values,
+ changed = False
+ )
+
+ # Google Cloud DNS does not support wildcard NS records.
+ if record_type == 'NS' and record_name[0] == '*':
+ module.fail_json(
+ msg = "wildcard NS records not allowed, got: %s" % record_name,
+ changed = False
+ )
+
+ # Values for txt records must begin and end with a double quote.
+ if record_type == 'TXT':
+ for value in values:
+ if value[0] != '"' and value[-1] != '"':
+ module.fail_json(
+ msg = 'TXT values must be enclosed in double quotes, ' +
+ 'got: %s' % value,
+ changed = False
+ )
+
+
+def _additional_sanity_checks(module, zone):
+ """Run input sanity checks that depend on info from the zone/record."""
+
+ overwrite = module.params['overwrite']
+ record_name = module.params['record']
+ record_type = module.params['type']
+ state = module.params['state']
+
+ # CNAME records are not allowed to have the same name as the root domain.
+ if record_type == 'CNAME' and record_name == zone.domain:
+ module.fail_json(
+ msg = 'CNAME records cannot match the zone name',
+ changed = False
+ )
+
+ # The root domain must always have an NS record.
+ if record_type == 'NS' and record_name == zone.domain and state == 'absent':
+ module.fail_json(
+ msg = 'cannot delete root NS records',
+ changed = False
+ )
+
+ # Updating NS records with the name as the root domain is not allowed
+ # because libcloud does not support in-place updates and root domain NS
+ # records cannot be removed.
+ if record_type == 'NS' and record_name == zone.domain and overwrite:
+ module.fail_json(
+ msg = 'cannot update existing root NS records',
+ changed = False
+ )
+
+ # SOA records with names that don't match the root domain are not permitted
+ # (and wouldn't make sense anyway).
+ if record_type == 'SOA' and record_name != zone.domain:
+ module.fail_json(
+ msg = 'non-root SOA records are not permitted, got: %s' % record_name,
+ changed = False
+ )
+
+
+################################################################################
+# Main
+################################################################################
+
+def main():
+ """Main function"""
+
+ module = AnsibleModule(
+ argument_spec = dict(
+ state = dict(default='present', choices=['present', 'absent'], type='str'),
+ record = dict(required=True, aliases=['name'], type='str'),
+ zone = dict(type='str'),
+ zone_id = dict(type='str'),
+ type = dict(required=True, choices=SUPPORTED_RECORD_TYPES, type='str'),
+ values = dict(aliases=['value'], type='list'),
+ ttl = dict(default=300, type='int'),
+ overwrite = dict(default=False, type='bool'),
+ service_account_email = dict(type='str'),
+ pem_file = dict(type='path'),
+ credentials_file = dict(type='path'),
+ project_id = dict(type='str')
+ ),
+ required_if = [
+ ('state', 'present', ['values']),
+ ('overwrite', False, ['values'])
+ ],
+ required_one_of = [['zone', 'zone_id']],
+ supports_check_mode = True
+ )
+
+ _sanity_check(module)
+
+ record_name = module.params['record']
+ record_type = module.params['type']
+ state = module.params['state']
+ ttl = module.params['ttl']
+ zone_name = module.params['zone']
+ zone_id = module.params['zone_id']
+
+ json_output = dict(
+ state = state,
+ record = record_name,
+ zone = zone_name,
+ zone_id = zone_id,
+ type = record_type,
+ values = module.params['values'],
+ ttl = ttl,
+ overwrite = module.boolean(module.params['overwrite'])
+ )
+
+ # Google Cloud DNS wants the trailing dot on all DNS names.
+ if zone_name is not None and zone_name[-1] != '.':
+ zone_name = zone_name + '.'
+ if record_name[-1] != '.':
+ record_name = record_name + '.'
+
+ # Build a connection object that we can use to connect with Google Cloud
+ # DNS.
+ gcdns = gcdns_connect(module, provider=PROVIDER)
+
+ # We need to check that the zone we're creating a record for actually
+ # exists.
+ zone = _get_zone(gcdns, zone_name, zone_id)
+ if zone is None and zone_name is not None:
+ module.fail_json(
+ msg = 'zone name was not found: %s' % zone_name,
+ changed = False
+ )
+ elif zone is None and zone_id is not None:
+ module.fail_json(
+ msg = 'zone id was not found: %s' % zone_id,
+ changed = False
+ )
+
+ # Populate the returns with the actual zone information.
+ json_output['zone'] = zone.domain
+ json_output['zone_id'] = zone.id
+
+ # We also need to check if the record we want to create or remove actually
+ # exists.
+ try:
+ record = _get_record(gcdns, zone, record_type, record_name)
+ except InvalidRequestError:
+ # We gave Google Cloud DNS an invalid DNS record name.
+ module.fail_json(
+ msg = 'record name is invalid: %s' % record_name,
+ changed = False
+ )
+
+ _additional_sanity_checks(module, zone)
+
+ diff = dict()
+
+ # Build the 'before' diff
+ if record is None:
+ diff['before'] = ''
+ diff['before_header'] = ''
+ else:
+ diff['before'] = dict(
+ record = record.data['name'],
+ type = record.data['type'],
+ values = record.data['rrdatas'],
+ ttl = record.data['ttl']
+ )
+ diff['before_header'] = "%s:%s" % (record_type, record_name)
+
+ # Create, remove, or modify the record.
+ if state == 'present':
+ diff['after'] = dict(
+ record = record_name,
+ type = record_type,
+ values = module.params['values'],
+ ttl = ttl
+ )
+ diff['after_header'] = "%s:%s" % (record_type, record_name)
+
+ changed = create_record(module, gcdns, zone, record)
+
+ elif state == 'absent':
+ diff['after'] = ''
+ diff['after_header'] = ''
+
+ changed = remove_record(module, gcdns, record)
+
+ module.exit_json(changed=changed, diff=diff, **json_output)
+
+
+from ansible.module_utils.basic import *
+from ansible.module_utils.gcdns import *
+
+if __name__ == '__main__':
+ main()
diff --git a/cloud/google/gcdns_zone.py b/cloud/google/gcdns_zone.py
new file mode 100644
index 00000000000..4b7bd16985b
--- /dev/null
+++ b/cloud/google/gcdns_zone.py
@@ -0,0 +1,381 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 CallFire Inc.
+#
+# This file is part of Ansible.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+################################################################################
+# Documentation
+################################################################################
+
+DOCUMENTATION = '''
+---
+module: gcdns_zone
+short_description: Creates or removes zones in Google Cloud DNS
+description:
+ - Creates or removes managed zones in Google Cloud DNS.
+version_added: "2.2"
+author: "William Albert (@walbert947)"
+requirements:
+ - "python >= 2.6"
+ - "apache-libcloud >= 0.19.0"
+options:
+ state:
+ description:
+ - Whether the given zone should or should not be present.
+ required: false
+ choices: ["present", "absent"]
+ default: "present"
+ zone:
+ description:
+ - The DNS domain name of the zone.
+ - This is NOT the Google Cloud DNS zone ID (e.g., example-com). If
+ you attempt to specify a zone ID, this module will attempt to
+ create a TLD and will fail.
+ required: true
+ aliases: ['name']
+ description:
+ description:
+ - An arbitrary text string to use for the zone description.
+ required: false
+ default: ""
+ service_account_email:
+ description:
+ - The e-mail address for a service account with access to Google
+ Cloud DNS.
+ required: false
+ default: null
+ pem_file:
+ description:
+ - The path to the PEM file associated with the service account
+ email.
+ - This option is deprecated and may be removed in a future release.
+ Use I(credentials_file) instead.
+ required: false
+ default: null
+ credentials_file:
+ description:
+ - The path to the JSON file associated with the service account
+ email.
+ required: false
+ default: null
+ project_id:
+ description:
+ - The Google Cloud Platform project ID to use.
+ required: false
+ default: null
+notes:
+ - See also M(gcdns_record).
+ - Zones that are newly created must still be set up with a domain registrar
+ before they can be used.
+'''
+
+EXAMPLES = '''
+# Basic zone creation example.
+- name: Create a basic zone with the minimum number of parameters.
+ gcdns_zone: zone=example.com
+
+# Zone removal example.
+- name: Remove a zone.
+ gcdns_zone: zone=example.com state=absent
+
+# Zone creation with description
+- name: Creating a zone with a description
+ gcdns_zone: zone=example.com description="This is an awesome zone"
+'''
+
+RETURN = '''
+description:
+ description: The zone's description
+ returned: success
+ type: string
+ sample: This is an awesome zone
+state:
+ description: Whether the zone is present or absent
+ returned: success
+ type: string
+ sample: present
+zone:
+ description: The zone's DNS name
+ returned: success
+ type: string
+ sample: example.com.
+'''
+
+
+################################################################################
+# Imports
+################################################################################
+
+from distutils.version import LooseVersion
+
+try:
+ from libcloud import __version__ as LIBCLOUD_VERSION
+ from libcloud.common.google import InvalidRequestError
+ from libcloud.common.google import ResourceExistsError
+ from libcloud.common.google import ResourceNotFoundError
+ from libcloud.dns.types import Provider
+ HAS_LIBCLOUD = True
+except ImportError:
+ HAS_LIBCLOUD = False
+
+
+################################################################################
+# Constants
+################################################################################
+
+# Apache libcloud 0.19.0 was the first to contain the non-beta Google Cloud DNS
+# v1 API. Earlier versions contained the beta v1 API, which has since been
+# deprecated and decommissioned.
+MINIMUM_LIBCLOUD_VERSION = '0.19.0'
+
+# The libcloud Google Cloud DNS provider.
+PROVIDER = Provider.GOOGLE
+
+# The URL used to verify ownership of a zone in Google Cloud DNS.
+ZONE_VERIFICATION_URL= 'https://www.google.com/webmasters/verification/'
+
+################################################################################
+# Functions
+################################################################################
+
+def create_zone(module, gcdns, zone):
+ """Creates a new Google Cloud DNS zone."""
+
+ description = module.params['description']
+ extra = dict(description = description)
+ zone_name = module.params['zone']
+
+ # Google Cloud DNS wants the trailing dot on the domain name.
+ if zone_name[-1] != '.':
+ zone_name = zone_name + '.'
+
+ # If we got a zone back, then the domain exists.
+ if zone is not None:
+ return False
+
+ # The zone doesn't exist yet.
+ try:
+ if not module.check_mode:
+ gcdns.create_zone(domain=zone_name, extra=extra)
+ return True
+
+ except ResourceExistsError:
+ # The zone already exists. We checked for this already, so either
+ # Google is lying, or someone was a ninja and created the zone
+ # within milliseconds of us checking for its existence. In any case,
+ # the zone has already been created, so we have nothing more to do.
+ return False
+
+ except InvalidRequestError as error:
+ if error.code == 'invalid':
+ # The zone name or a parameter might be completely invalid. This is
+ # typically caused by an illegal DNS name (e.g. foo..com).
+ module.fail_json(
+ msg = "zone name is not a valid DNS name: %s" % zone_name,
+ changed = False
+ )
+
+ elif error.code == 'managedZoneDnsNameNotAvailable':
+ # Google Cloud DNS will refuse to create zones with certain domain
+ # names, such as TLDs, ccTLDs, or special domain names such as
+ # example.com.
+ module.fail_json(
+ msg = "zone name is reserved or already in use: %s" % zone_name,
+ changed = False
+ )
+
+ elif error.code == 'verifyManagedZoneDnsNameOwnership':
+ # This domain name needs to be verified before Google will create
+ # it. This occurs when a user attempts to create a zone which shares
+ # a domain name with a zone hosted elsewhere in Google Cloud DNS.
+ module.fail_json(
+ msg = "ownership of zone %s needs to be verified at %s" % (zone_name, ZONE_VERIFICATION_URL),
+ changed = False
+ )
+
+ else:
+ # The error is something else that we don't know how to handle,
+ # so we'll just re-raise the exception.
+ raise
+
+
+def remove_zone(module, gcdns, zone):
+ """Removes an existing Google Cloud DNS zone."""
+
+ # If there's no zone, then we're obviously done.
+ if zone is None:
+ return False
+
+ # An empty zone will have two resource records:
+ # 1. An NS record with a list of authoritative name servers
+ # 2. An SOA record
+ # If any additional resource records are present, Google Cloud DNS will
+ # refuse to remove the zone.
+ if len(zone.list_records()) > 2:
+ module.fail_json(
+ msg = "zone is not empty and cannot be removed: %s" % zone.domain,
+ changed = False
+ )
+
+ try:
+ if not module.check_mode:
+ gcdns.delete_zone(zone)
+ return True
+
+ except ResourceNotFoundError:
+ # When we performed our check, the zone existed. It may have been
+ # deleted by something else. It's gone, so whatever.
+ return False
+
+ except InvalidRequestError as error:
+ if error.code == 'containerNotEmpty':
+ # When we performed our check, the zone existed and was empty. In
+ # the milliseconds between the check and the removal command,
+ # records were added to the zone.
+ module.fail_json(
+ msg = "zone is not empty and cannot be removed: %s" % zone.domain,
+ changed = False
+ )
+
+ else:
+ # The error is something else that we don't know how to handle,
+ # so we'll just re-raise the exception.
+ raise
+
+
+def _get_zone(gcdns, zone_name):
+ """Gets the zone object for a given domain name."""
+
+ # To create a zone, we need to supply a zone name. However, to delete a
+ # zone, we need to supply a zone ID. Zone ID's are often based on zone
+ # names, but that's not guaranteed, so we'll iterate through the list of
+ # zones to see if we can find a matching name.
+ available_zones = gcdns.iterate_zones()
+ found_zone = None
+
+ for zone in available_zones:
+ if zone.domain == zone_name:
+ found_zone = zone
+ break
+
+ return found_zone
+
+def _sanity_check(module):
+ """Run module sanity checks."""
+
+ zone_name = module.params['zone']
+
+ # Apache libcloud needs to be installed and at least the minimum version.
+ if not HAS_LIBCLOUD:
+ module.fail_json(
+ msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION,
+ changed = False
+ )
+ elif LooseVersion(LIBCLOUD_VERSION) < MINIMUM_LIBCLOUD_VERSION:
+ module.fail_json(
+ msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION,
+ changed = False
+ )
+
+ # Google Cloud DNS does not support the creation of TLDs.
+ if '.' not in zone_name or len([label for label in zone_name.split('.') if label]) == 1:
+ module.fail_json(
+ msg = 'cannot create top-level domain: %s' % zone_name,
+ changed = False
+ )
+
+################################################################################
+# Main
+################################################################################
+
+def main():
+ """Main function"""
+
+ module = AnsibleModule(
+ argument_spec = dict(
+ state = dict(default='present', choices=['present', 'absent'], type='str'),
+ zone = dict(required=True, aliases=['name'], type='str'),
+ description = dict(default='', type='str'),
+ service_account_email = dict(type='str'),
+ pem_file = dict(type='path'),
+ credentials_file = dict(type='path'),
+ project_id = dict(type='str')
+ ),
+ supports_check_mode = True
+ )
+
+ _sanity_check(module)
+
+ zone_name = module.params['zone']
+ state = module.params['state']
+
+ # Google Cloud DNS wants the trailing dot on the domain name.
+ if zone_name[-1] != '.':
+ zone_name = zone_name + '.'
+
+ json_output = dict(
+ state = state,
+ zone = zone_name,
+ description = module.params['description']
+ )
+
+ # Build a connection object that was can use to connect with Google
+ # Cloud DNS.
+ gcdns = gcdns_connect(module, provider=PROVIDER)
+
+ # We need to check if the zone we're attempting to create already exists.
+ zone = _get_zone(gcdns, zone_name)
+
+ diff = dict()
+
+ # Build the 'before' diff
+ if zone is None:
+ diff['before'] = ''
+ diff['before_header'] = ''
+ else:
+ diff['before'] = dict(
+ zone = zone.domain,
+ description = zone.extra['description']
+ )
+ diff['before_header'] = zone_name
+
+ # Create or remove the zone.
+ if state == 'present':
+ diff['after'] = dict(
+ zone = zone_name,
+ description = module.params['description']
+ )
+ diff['after_header'] = zone_name
+
+ changed = create_zone(module, gcdns, zone)
+
+ elif state == 'absent':
+ diff['after'] = ''
+ diff['after_header'] = ''
+
+ changed = remove_zone(module, gcdns, zone)
+
+ module.exit_json(changed=changed, diff=diff, **json_output)
+
+
+from ansible.module_utils.basic import *
+from ansible.module_utils.gcdns import *
+
+if __name__ == '__main__':
+ main()