#!/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()