9fda16070f
This commit adds modules that can manipulate Google Cloud DNS. The modules can create and delete zones, as well as records within zones.
381 lines
12 KiB
Python
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()
|