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

ANSIBLE_METADATA = {'metadata_version': '1.0',
                    'status': ['preview'],
                    'supported_by': 'community'}


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()
