#!/usr/bin/python
# Copyright 2017 Google 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


ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}
DOCUMENTATION = '''
module: gcp_backend_service
version_added: "2.4"
short_description: Create or Destroy a Backend Service.
description:
    - Create or Destroy a Backend Service.  See
      U(https://cloud.google.com/compute/docs/load-balancing/http/backend-service) for an overview.
      Full install/configuration instructions for the Google Cloud modules can
      be found in the comments of ansible/test/gce_tests.py.
requirements:
  - "python >= 2.6"
  - "apache-libcloud >= 1.3.0"
notes:
  - Update is not currently supported.
  - Only global backend services are currently supported. Regional backends not currently supported.
  - Internal load balancing not currently supported.
author:
  - "Tom Melendez (@supertom) <tom@supertom.com>"
options:
  backend_service_name:
    description:
       - Name of the Backend Service.
    required: true
  backends:
    description:
       - List of backends that make up the backend service. A backend is made up of
         an instance group and optionally several other parameters.  See
         U(https://cloud.google.com/compute/docs/reference/latest/backendServices)
         for details.
    required: true
  healthchecks:
    description:
       - List of healthchecks. Only one healthcheck is supported.
    required: true
  enable_cdn:
    description:
       - If true, enable Cloud CDN for this Backend Service.
  port_name:
    description:
      - Name of the port on the managed instance group (MIG) that backend
        services can forward data to. Required for external load balancing.
  protocol:
    description:
       - The protocol this Backend Service uses to communicate with backends.
         Possible values are HTTP, HTTPS, TCP, and SSL. The default is HTTP.
    required: false
  timeout:
    description:
       - How many seconds to wait for the backend before considering it a failed
         request. Default is 30 seconds. Valid range is 1-86400.
    required: false
  service_account_email:
    description:
      - Service account email
  credentials_file:
    description:
      - Path to the JSON file associated with the service account email.
  project_id:
    description:
      - GCE project ID.
  state:
    description:
      - Desired state of the resource
    default: "present"
    choices: ["absent", "present"]
'''

EXAMPLES = '''
- name: Create Minimum Backend Service
  gcp_backend_service:
    service_account_email: "{{ service_account_email }}"
    credentials_file: "{{ credentials_file }}"
    project_id: "{{ project_id }}"
    backend_service_name: "{{ bes }}"
    backends:
    - instance_group: managed_instance_group_1
    healthchecks:
    - healthcheck_name_for_backend_service
    port_name: myhttpport
    state: present

- name: Create BES with extended backend parameters
  gcp_backend_service:
    service_account_email: "{{ service_account_email }}"
    credentials_file: "{{ credentials_file }}"
    project_id: "{{ project_id }}"
    backend_service_name: "{{ bes }}"
    backends:
    - instance_group: managed_instance_group_1
      max_utilization: 0.6
      max_rate: 10
    - instance_group: managed_instance_group_2
      max_utilization: 0.5
      max_rate: 4
    healthchecks:
    - healthcheck_name_for_backend_service
    port_name: myhttpport
    state: present
    timeout: 60
'''

RETURN = '''
backend_service_created:
    description: Indicator Backend Service was created.
    returned: When a Backend Service is created.
    type: boolean
    sample: "True"
backend_service_deleted:
    description: Indicator Backend Service was deleted.
    returned: When a Backend Service is deleted.
    type: boolean
    sample: "True"
backend_service_name:
    description: Name of the Backend Service.
    returned: Always.
    type: string
    sample: "my-backend-service"
backends:
    description: List of backends (comprised of instance_group) that
                 make up a Backend Service.
    returned: When a Backend Service exists.
    type: list
    sample: "[ { 'instance_group': 'mig_one', 'zone': 'us-central1-b'} ]"
enable_cdn:
    description: If Cloud CDN is enabled. null if not set.
    returned: When a backend service exists.
    type: boolean
    sample: "True"
healthchecks:
    description: List of healthchecks applied to the Backend Service.
    returned: When a Backend Service exists.
    type: list
    sample: "[ 'my-healthcheck' ]"
protocol:
    description: Protocol used to communicate with the Backends.
    returned: When a Backend Service exists.
    type: string
    sample: "HTTP"
port_name:
    description: Name of Backend Port.
    returned: When a Backend Service exists.
    type: string
    sample: "myhttpport"
timeout:
    description: In seconds, how long before a request sent to a backend is
                 considered failed.
    returned: If specified.
    type: int
    sample: "myhttpport"
'''

try:
    from ast import literal_eval
    HAS_PYTHON26 = True
except ImportError:
    HAS_PYTHON26 = False

try:
    import libcloud
    from libcloud.compute.types import Provider
    from libcloud.compute.providers import get_driver
    from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
        ResourceExistsError, ResourceInUseError, ResourceNotFoundError
    from libcloud.compute.drivers.gce import GCEAddress
    _ = Provider.GCE
    HAS_LIBCLOUD = True
except ImportError:
    HAS_LIBCLOUD = False

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.gce import gce_connect
from ansible.module_utils.gcp import check_params


def _validate_params(params):
    """
    Validate backend_service params.

    This function calls _validate_backend_params to verify
    the backend-specific parameters.

    :param params: Ansible dictionary containing configuration.
    :type  params: ``dict``

    :return: True or raises ValueError
    :rtype: ``bool`` or `class:ValueError`
    """
    fields = [
        {'name': 'timeout', 'type': int, 'min': 1, 'max': 86400},
    ]
    try:
        check_params(params, fields)
        _validate_backend_params(params['backends'])
    except:
        raise

    return (True, '')


def _validate_backend_params(backends):
    """
    Validate configuration for backends.

    :param backends: Ansible dictionary containing backends configuration (only).
    :type  backends: ``dict``

    :return: True or raises ValueError
    :rtype: ``bool`` or `class:ValueError`
    """
    fields = [
        {'name': 'balancing_mode', 'type': str, 'values': ['UTILIZATION', 'RATE', 'CONNECTION']},
        {'name': 'max_utilization', 'type': float},
        {'name': 'max_connections', 'type': int},
        {'name': 'max_rate', 'type': int},
        {'name': 'max_rate_per_instance', 'type': float},
    ]

    if not backends:
        raise ValueError('backends should be a list.')

    for backend in backends:
        try:
            check_params(backend, fields)
        except:
            raise

        if 'max_rate' in backend and 'max_rate_per_instance' in backend:
            raise ValueError('Both maxRate or maxRatePerInstance cannot be set.')

    return (True, '')


def get_backend_service(gce, name):
    """
    Get a Backend Service from GCE.

    :param gce: An initialized GCE driver object.
    :type gce:  :class: `GCENodeDriver`

    :param name: Name of the Backend Service.
    :type name:  ``str``

    :return: A GCEBackendService object or None.
    :rtype: :class: `GCEBackendService` or None
    """
    try:
        # Does the Backend Service already exist?
        return gce.ex_get_backendservice(name=name)

    except ResourceNotFoundError:
        return None


def get_healthcheck(gce, name):
    return gce.ex_get_healthcheck(name)


def get_instancegroup(gce, name, zone=None):
    return gce.ex_get_instancegroup(name=name, zone=zone)


def create_backend_service(gce, params):
    """
    Create a new Backend Service.

    :param gce: An initialized GCE driver object.
    :type gce:  :class: `GCENodeDriver`

    :param params: Dictionary of parameters needed by the module.
    :type params:  ``dict``

    :return: Tuple with changed stats
    :rtype: tuple in the format of (bool, bool)
    """
    from copy import deepcopy

    changed = False
    return_data = False
    # only one healthcheck is currently supported
    hc_name = params['healthchecks'][0]
    hc = get_healthcheck(gce, hc_name)
    backends = []
    for backend in params['backends']:
        ig = get_instancegroup(gce, backend['instance_group'],
                               backend.get('zone', None))
        kwargs = deepcopy(backend)
        kwargs['instance_group'] = ig
        backends.append(gce.ex_create_backend(
            **kwargs))

    bes = gce.ex_create_backendservice(
        name=params['backend_service_name'], healthchecks=[hc], backends=backends,
        enable_cdn=params['enable_cdn'], port_name=params['port_name'],
        timeout_sec=params['timeout'], protocol=params['protocol'])

    if bes:
        changed = True
        return_data = True

    return (changed, return_data)


def delete_backend_service(bes):
    """
    Delete a Backend Service. The Instance Groups are NOT destroyed.
    """
    changed = False
    return_data = False
    if bes.destroy():
        changed = True
        return_data = True
    return (changed, return_data)


def main():
    module = AnsibleModule(argument_spec=dict(
        backends=dict(type='list', required=True),
        backend_service_name=dict(required=True),
        healthchecks=dict(type='list', required=True),
        service_account_email=dict(),
        service_account_permissions=dict(type='list'),
        enable_cdn=dict(type='bool'),
        port_name=dict(type='str'),
        protocol=dict(type='str', default='TCP',
                      choices=['HTTP', 'HTTPS', 'SSL', 'TCP']),
        timeout=dict(type='int'),
        state=dict(choices=['absent', 'present'], default='present'),
        pem_file=dict(),
        credentials_file=dict(),
        project_id=dict(), ), )

    if not HAS_PYTHON26:
        module.fail_json(
            msg="GCE module requires python's 'ast' module, python v2.6+")
    if not HAS_LIBCLOUD:
        module.fail_json(
            msg='libcloud with GCE Backend Service support (1.3+) required for this module.')

    gce = gce_connect(module)
    if not hasattr(gce, 'ex_create_instancegroupmanager'):
        module.fail_json(
            msg='libcloud with GCE Backend Service support (1.3+) required for this module.',
            changed=False)

    params = {}
    params['state'] = module.params.get('state')
    params['backend_service_name'] = module.params.get('backend_service_name')
    params['backends'] = module.params.get('backends')
    params['healthchecks'] = module.params.get('healthchecks')
    params['enable_cdn'] = module.params.get('enable_cdn', None)
    params['port_name'] = module.params.get('port_name', None)
    params['protocol'] = module.params.get('protocol', None)
    params['timeout'] = module.params.get('timeout', None)

    try:
        _validate_params(params)
    except Exception as e:
        module.fail_json(msg=e.message, changed=False)

    changed = False
    json_output = {'state': params['state']}
    bes = get_backend_service(gce, params['backend_service_name'])

    if not bes:
        if params['state'] == 'absent':
            # Doesn't exist and state==absent.
            changed = False
            module.fail_json(
                msg="Cannot delete unknown backend service: %s" %
                (params['backend_service_name']))
        else:
            # Create
            (changed, json_output['backend_service_created']) = create_backend_service(gce,
                                                                                       params)
    elif params['state'] == 'absent':
        # Delete
        (changed, json_output['backend_service_deleted']) = delete_backend_service(bes)
    else:
        # TODO(supertom): Add update support when it is available in libcloud.
        changed = False

    json_output['changed'] = changed
    json_output.update(params)
    module.exit_json(**json_output)


if __name__ == '__main__':
    main()
