#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (C) 2015 CallFire Inc. # 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 ################################################################################ DOCUMENTATION = ''' --- module: gcdns_zone short_description: Creates or removes zones in Google Cloud DNS description: - Creates or removes managed zones in Google Cloud DNS. author: "William Albert (@walbert947)" requirements: - "apache-libcloud >= 0.19.0" deprecated: removed_in: 2.0.0 # was Ansible 2.12 why: Updated modules released with increased functionality alternative: Use M(google.cloud.gcp_dns_managed_zone) instead. options: state: description: - Whether the given zone should or should not be present. 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. default: "" service_account_email: description: - The e-mail address for a service account with access to Google Cloud DNS. 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. credentials_file: description: - The path to the JSON file associated with the service account email. project_id: description: - The Google Cloud Platform project ID to use. notes: - See also M(community.general.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: str sample: This is an awesome zone state: description: Whether the zone is present or absent returned: success type: str sample: present zone: description: The zone's DNS name returned: success type: str 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 # The libcloud Google Cloud DNS provider. PROVIDER = Provider.GOOGLE HAS_LIBCLOUD = True except ImportError: HAS_LIBCLOUD = False PROVIDER = None from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.gcdns import gcdns_connect ################################################################################ # 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 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) if __name__ == '__main__': main()