ansible/cloud/google/gcdns_zone.py
William Albert 9fda16070f Add modules to support Google Cloud DNS (#2252)
This commit adds modules that can manipulate Google Cloud DNS. The modules can create and delete zones, as well as records within zones.
2016-07-28 12:56:35 -04:00

381 lines
12 KiB
Python

#!/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 <http://www.gnu.org/licenses/>.
################################################################################
# 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'] = '<absent>'
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'] = '<absent>'
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()