#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015 CenturyLink
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = '''
module: clc_server
short_description: Create, Delete, Start and Stop servers in CenturyLink Cloud.
description:
  - An Ansible module to Create, Delete, Start and Stop servers in CenturyLink Cloud.
options:
  additional_disks:
    description:
      - The list of additional disks for the server
    type: list
    elements: dict
    default: []
  add_public_ip:
    description:
      - Whether to add a public ip to the server
    type: bool
    default: 'no'
  alias:
    description:
      - The account alias to provision the servers under.
    type: str
  anti_affinity_policy_id:
    description:
      - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_name'.
    type: str
  anti_affinity_policy_name:
    description:
      - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_id'.
    type: str
  alert_policy_id:
    description:
      - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_name'.
    type: str
  alert_policy_name:
    description:
      - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_id'.
    type: str
  count:
    description:
      - The number of servers to build (mutually exclusive with exact_count)
    default: 1
    type: int
  count_group:
    description:
      - Required when exact_count is specified.  The Server Group use to determine how many servers to deploy.
    type: str
  cpu:
    description:
      - How many CPUs to provision on the server
    default: 1
    type: int
  cpu_autoscale_policy_id:
    description:
      - The autoscale policy to assign to the server.
    type: str
  custom_fields:
    description:
      - The list of custom fields to set on the server.
    type: list
    default: []
    elements: dict
  description:
    description:
      - The description to set for the server.
    type: str
  exact_count:
    description:
      - Run in idempotent mode.  Will insure that this exact number of servers are running in the provided group,
        creating and deleting them to reach that count.  Requires count_group to be set.
    type: int
  group:
    description:
      - The Server Group to create servers under.
    type: str
    default: 'Default Group'
  ip_address:
    description:
      - The IP Address for the server. One is assigned if not provided.
    type: str
  location:
    description:
      - The Datacenter to create servers in.
    type: str
  managed_os:
    description:
      - Whether to create the server as 'Managed' or not.
    type: bool
    default: 'no'
    required: False
  memory:
    description:
      - Memory in GB.
    type: int
    default: 1
  name:
    description:
      - A 1 to 6 character identifier to use for the server. This is required when state is 'present'
    type: str
  network_id:
    description:
      - The network UUID on which to create servers.
    type: str
  packages:
    description:
      - The list of blue print packages to run on the server after its created.
    type: list
    elements: dict
    default: []
  password:
    description:
      - Password for the administrator / root user
    type: str
  primary_dns:
    description:
      - Primary DNS used by the server.
    type: str
  public_ip_protocol:
    description:
      - The protocol to use for the public ip if add_public_ip is set to True.
    type: str
    default: 'TCP'
    choices: ['TCP', 'UDP', 'ICMP']
  public_ip_ports:
    description:
      - A list of ports to allow on the firewall to the servers public ip, if add_public_ip is set to True.
    type: list
    elements: dict
    default: []
  secondary_dns:
    description:
      - Secondary DNS used by the server.
    type: str
  server_ids:
    description:
      - Required for started, stopped, and absent states.
        A list of server Ids to insure are started, stopped, or absent.
    type: list
    default: []
    elements: str
  source_server_password:
    description:
      - The password for the source server if a clone is specified.
    type: str
  state:
    description:
      - The state to insure that the provided resources are in.
    type: str
    default: 'present'
    choices: ['present', 'absent', 'started', 'stopped']
  storage_type:
    description:
      - The type of storage to attach to the server.
    type: str
    default: 'standard'
    choices: ['standard', 'hyperscale']
  template:
    description:
      - The template to use for server creation.  Will search for a template if a partial string is provided.
        This is required when state is 'present'
    type: str
  ttl:
    description:
      - The time to live for the server in seconds.  The server will be deleted when this time expires.
    type: str
  type:
    description:
      - The type of server to create.
    type: str
    default: 'standard'
    choices: ['standard', 'hyperscale', 'bareMetal']
  configuration_id:
    description:
      -  Only required for bare metal servers.
         Specifies the identifier for the specific configuration type of bare metal server to deploy.
    type: str
  os_type:
    description:
      - Only required for bare metal servers.
        Specifies the OS to provision with the bare metal server.
    type: str
    choices: ['redHat6_64Bit', 'centOS6_64Bit', 'windows2012R2Standard_64Bit', 'ubuntu14_64Bit']
  wait:
    description:
      - Whether to wait for the provisioning tasks to finish before returning.
    type: bool
    default: 'yes'
requirements:
    - python = 2.7
    - requests >= 2.5.0
    - clc-sdk
author: "CLC Runner (@clc-runner)"
notes:
    - To use this module, it is required to set the below environment variables which enables access to the
      Centurylink Cloud
          - CLC_V2_API_USERNAME, the account login id for the centurylink cloud
          - CLC_V2_API_PASSWORD, the account password for the centurylink cloud
    - Alternatively, the module accepts the API token and account alias. The API token can be generated using the
      CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
          - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
          - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
    - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
'''

EXAMPLES = '''
# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples

- name: Provision a single Ubuntu Server
  community.general.clc_server:
    name: test
    template: ubuntu-14-64
    count: 1
    group: Default Group
    state: present

- name: Ensure 'Default Group' has exactly 5 servers
  community.general.clc_server:
    name: test
    template: ubuntu-14-64
    exact_count: 5
    count_group: Default Group
    group: Default Group

- name: Stop a Server
  community.general.clc_server:
    server_ids:
      - UC1ACCT-TEST01
    state: stopped

- name: Start a Server
  community.general.clc_server:
    server_ids:
      - UC1ACCT-TEST01
    state: started

- name: Delete a Server
  community.general.clc_server:
    server_ids:
      - UC1ACCT-TEST01
    state: absent
'''

RETURN = '''
server_ids:
    description: The list of server ids that are created
    returned: success
    type: list
    sample:
        [
            "UC1TEST-SVR01",
            "UC1TEST-SVR02"
        ]
partially_created_server_ids:
    description: The list of server ids that are partially created
    returned: success
    type: list
    sample:
        [
            "UC1TEST-SVR01",
            "UC1TEST-SVR02"
        ]
servers:
    description: The list of server objects returned from CLC
    returned: success
    type: list
    sample:
        [
           {
              "changeInfo":{
                 "createdBy":"service.wfad",
                 "createdDate":1438196820,
                 "modifiedBy":"service.wfad",
                 "modifiedDate":1438196820
              },
              "description":"test-server",
              "details":{
                 "alertPolicies":[

                 ],
                 "cpu":1,
                 "customFields":[

                 ],
                 "diskCount":3,
                 "disks":[
                    {
                       "id":"0:0",
                       "partitionPaths":[

                       ],
                       "sizeGB":1
                    },
                    {
                       "id":"0:1",
                       "partitionPaths":[

                       ],
                       "sizeGB":2
                    },
                    {
                       "id":"0:2",
                       "partitionPaths":[

                       ],
                       "sizeGB":14
                    }
                 ],
                 "hostName":"",
                 "inMaintenanceMode":false,
                 "ipAddresses":[
                    {
                       "internal":"10.1.1.1"
                    }
                 ],
                 "memoryGB":1,
                 "memoryMB":1024,
                 "partitions":[

                 ],
                 "powerState":"started",
                 "snapshots":[

                 ],
                 "storageGB":17
              },
              "groupId":"086ac1dfe0b6411989e8d1b77c4065f0",
              "id":"test-server",
              "ipaddress":"10.120.45.23",
              "isTemplate":false,
              "links":[
                 {
                    "href":"/v2/servers/wfad/test-server",
                    "id":"test-server",
                    "rel":"self",
                    "verbs":[
                       "GET",
                       "PATCH",
                       "DELETE"
                    ]
                 },
                 {
                    "href":"/v2/groups/wfad/086ac1dfe0b6411989e8d1b77c4065f0",
                    "id":"086ac1dfe0b6411989e8d1b77c4065f0",
                    "rel":"group"
                 },
                 {
                    "href":"/v2/accounts/wfad",
                    "id":"wfad",
                    "rel":"account"
                 },
                 {
                    "href":"/v2/billing/wfad/serverPricing/test-server",
                    "rel":"billing"
                 },
                 {
                    "href":"/v2/servers/wfad/test-server/publicIPAddresses",
                    "rel":"publicIPAddresses",
                    "verbs":[
                       "POST"
                    ]
                 },
                 {
                    "href":"/v2/servers/wfad/test-server/credentials",
                    "rel":"credentials"
                 },
                 {
                    "href":"/v2/servers/wfad/test-server/statistics",
                    "rel":"statistics"
                 },
                 {
                    "href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/upcomingScheduledActivities",
                    "rel":"upcomingScheduledActivities"
                 },
                 {
                    "href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/scheduledActivities",
                    "rel":"scheduledActivities",
                    "verbs":[
                       "GET",
                       "POST"
                    ]
                 },
                 {
                    "href":"/v2/servers/wfad/test-server/capabilities",
                    "rel":"capabilities"
                 },
                 {
                    "href":"/v2/servers/wfad/test-server/alertPolicies",
                    "rel":"alertPolicyMappings",
                    "verbs":[
                       "POST"
                    ]
                 },
                 {
                    "href":"/v2/servers/wfad/test-server/antiAffinityPolicy",
                    "rel":"antiAffinityPolicyMapping",
                    "verbs":[
                       "PUT",
                       "DELETE"
                    ]
                 },
                 {
                    "href":"/v2/servers/wfad/test-server/cpuAutoscalePolicy",
                    "rel":"cpuAutoscalePolicyMapping",
                    "verbs":[
                       "PUT",
                       "DELETE"
                    ]
                 }
              ],
              "locationId":"UC1",
              "name":"test-server",
              "os":"ubuntu14_64Bit",
              "osType":"Ubuntu 14 64-bit",
              "status":"active",
              "storageType":"standard",
              "type":"standard"
           }
        ]
'''

__version__ = '${version}'

import json
import os
import time
import traceback

from ansible_collections.community.general.plugins.module_utils.version import LooseVersion

REQUESTS_IMP_ERR = None
try:
    import requests
except ImportError:
    REQUESTS_IMP_ERR = traceback.format_exc()
    REQUESTS_FOUND = False
else:
    REQUESTS_FOUND = True

#
#  Requires the clc-python-sdk.
#  sudo pip install clc-sdk
#
CLC_IMP_ERR = None
try:
    import clc as clc_sdk
    from clc import CLCException
    from clc import APIFailedResponse
except ImportError:
    CLC_IMP_ERR = traceback.format_exc()
    CLC_FOUND = False
    clc_sdk = None
else:
    CLC_FOUND = True

from ansible.module_utils.basic import AnsibleModule, missing_required_lib


class ClcServer:
    clc = clc_sdk

    def __init__(self, module):
        """
        Construct module
        """
        self.clc = clc_sdk
        self.module = module
        self.group_dict = {}

        if not CLC_FOUND:
            self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
        if not REQUESTS_FOUND:
            self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
        if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
            self.module.fail_json(
                msg='requests library  version should be >= 2.5.0')

        self._set_user_agent(self.clc)

    def process_request(self):
        """
        Process the request - Main Code Path
        :return: Returns with either an exit_json or fail_json
        """
        changed = False
        new_server_ids = []
        server_dict_array = []

        self._set_clc_credentials_from_env()
        self.module.params = self._validate_module_params(
            self.clc,
            self.module)
        p = self.module.params
        state = p.get('state')

        #
        #  Handle each state
        #
        partial_servers_ids = []
        if state == 'absent':
            server_ids = p['server_ids']
            if not isinstance(server_ids, list):
                return self.module.fail_json(
                    msg='server_ids needs to be a list of instances to delete: %s' %
                    server_ids)

            (changed,
             server_dict_array,
             new_server_ids) = self._delete_servers(module=self.module,
                                                    clc=self.clc,
                                                    server_ids=server_ids)

        elif state in ('started', 'stopped'):
            server_ids = p.get('server_ids')
            if not isinstance(server_ids, list):
                return self.module.fail_json(
                    msg='server_ids needs to be a list of servers to run: %s' %
                    server_ids)

            (changed,
             server_dict_array,
             new_server_ids) = self._start_stop_servers(self.module,
                                                        self.clc,
                                                        server_ids)

        elif state == 'present':
            # Changed is always set to true when provisioning new instances
            if not p.get('template') and p.get('type') != 'bareMetal':
                return self.module.fail_json(
                    msg='template parameter is required for new instance')

            if p.get('exact_count') is None:
                (server_dict_array,
                 new_server_ids,
                 partial_servers_ids,
                 changed) = self._create_servers(self.module,
                                                 self.clc)
            else:
                (server_dict_array,
                 new_server_ids,
                 partial_servers_ids,
                 changed) = self._enforce_count(self.module,
                                                self.clc)

        self.module.exit_json(
            changed=changed,
            server_ids=new_server_ids,
            partially_created_server_ids=partial_servers_ids,
            servers=server_dict_array)

    @staticmethod
    def _define_module_argument_spec():
        """
        Define the argument spec for the ansible module
        :return: argument spec dictionary
        """
        argument_spec = dict(
            name=dict(),
            template=dict(),
            group=dict(default='Default Group'),
            network_id=dict(),
            location=dict(),
            cpu=dict(default=1, type='int'),
            memory=dict(default=1, type='int'),
            alias=dict(),
            password=dict(no_log=True),
            ip_address=dict(),
            storage_type=dict(
                default='standard',
                choices=[
                    'standard',
                    'hyperscale']),
            type=dict(default='standard', choices=['standard', 'hyperscale', 'bareMetal']),
            primary_dns=dict(),
            secondary_dns=dict(),
            additional_disks=dict(type='list', default=[], elements='dict'),
            custom_fields=dict(type='list', default=[], elements='dict'),
            ttl=dict(),
            managed_os=dict(type='bool', default=False),
            description=dict(),
            source_server_password=dict(no_log=True),
            cpu_autoscale_policy_id=dict(),
            anti_affinity_policy_id=dict(),
            anti_affinity_policy_name=dict(),
            alert_policy_id=dict(),
            alert_policy_name=dict(),
            packages=dict(type='list', default=[], elements='dict'),
            state=dict(
                default='present',
                choices=[
                    'present',
                    'absent',
                    'started',
                    'stopped']),
            count=dict(type='int', default=1),
            exact_count=dict(type='int', ),
            count_group=dict(),
            server_ids=dict(type='list', default=[], elements='str'),
            add_public_ip=dict(type='bool', default=False),
            public_ip_protocol=dict(
                default='TCP',
                choices=[
                    'TCP',
                    'UDP',
                    'ICMP']),
            public_ip_ports=dict(type='list', default=[], elements='dict'),
            configuration_id=dict(),
            os_type=dict(choices=[
                'redHat6_64Bit',
                'centOS6_64Bit',
                'windows2012R2Standard_64Bit',
                'ubuntu14_64Bit'
            ]),
            wait=dict(type='bool', default=True))

        mutually_exclusive = [
            ['exact_count', 'count'],
            ['exact_count', 'state'],
            ['anti_affinity_policy_id', 'anti_affinity_policy_name'],
            ['alert_policy_id', 'alert_policy_name'],
        ]
        return {"argument_spec": argument_spec,
                "mutually_exclusive": mutually_exclusive}

    def _set_clc_credentials_from_env(self):
        """
        Set the CLC Credentials on the sdk by reading environment variables
        :return: none
        """
        env = os.environ
        v2_api_token = env.get('CLC_V2_API_TOKEN', False)
        v2_api_username = env.get('CLC_V2_API_USERNAME', False)
        v2_api_passwd = env.get('CLC_V2_API_PASSWD', False)
        clc_alias = env.get('CLC_ACCT_ALIAS', False)
        api_url = env.get('CLC_V2_API_URL', False)
        if api_url:
            self.clc.defaults.ENDPOINT_URL_V2 = api_url

        if v2_api_token and clc_alias:
            self.clc._LOGIN_TOKEN_V2 = v2_api_token
            self.clc._V2_ENABLED = True
            self.clc.ALIAS = clc_alias
        elif v2_api_username and v2_api_passwd:
            self.clc.v2.SetCredentials(
                api_username=v2_api_username,
                api_passwd=v2_api_passwd)
        else:
            return self.module.fail_json(
                msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD "
                    "environment variables")

    @staticmethod
    def _validate_module_params(clc, module):
        """
        Validate the module params, and lookup default values.
        :param clc: clc-sdk instance to use
        :param module: module to validate
        :return: dictionary of validated params
        """
        params = module.params
        datacenter = ClcServer._find_datacenter(clc, module)

        ClcServer._validate_types(module)
        ClcServer._validate_name(module)

        params['alias'] = ClcServer._find_alias(clc, module)
        params['cpu'] = ClcServer._find_cpu(clc, module)
        params['memory'] = ClcServer._find_memory(clc, module)
        params['description'] = ClcServer._find_description(module)
        params['ttl'] = ClcServer._find_ttl(clc, module)
        params['template'] = ClcServer._find_template_id(module, datacenter)
        params['group'] = ClcServer._find_group(module, datacenter).id
        params['network_id'] = ClcServer._find_network_id(module, datacenter)
        params['anti_affinity_policy_id'] = ClcServer._find_aa_policy_id(
            clc,
            module)
        params['alert_policy_id'] = ClcServer._find_alert_policy_id(
            clc,
            module)

        return params

    @staticmethod
    def _find_datacenter(clc, module):
        """
        Find the datacenter by calling the CLC API.
        :param clc: clc-sdk instance to use
        :param module: module to validate
        :return: clc-sdk.Datacenter instance
        """
        location = module.params.get('location')
        try:
            if not location:
                account = clc.v2.Account()
                location = account.data.get('primaryDataCenter')
            data_center = clc.v2.Datacenter(location)
            return data_center
        except CLCException:
            module.fail_json(msg="Unable to find location: {0}".format(location))

    @staticmethod
    def _find_alias(clc, module):
        """
        Find or Validate the Account Alias by calling the CLC API
        :param clc: clc-sdk instance to use
        :param module: module to validate
        :return: clc-sdk.Account instance
        """
        alias = module.params.get('alias')
        if not alias:
            try:
                alias = clc.v2.Account.GetAlias()
            except CLCException as ex:
                module.fail_json(msg='Unable to find account alias. {0}'.format(
                    ex.message
                ))
        return alias

    @staticmethod
    def _find_cpu(clc, module):
        """
        Find or validate the CPU value by calling the CLC API
        :param clc: clc-sdk instance to use
        :param module: module to validate
        :return: Int value for CPU
        """
        cpu = module.params.get('cpu')
        group_id = module.params.get('group_id')
        alias = module.params.get('alias')
        state = module.params.get('state')

        if not cpu and state == 'present':
            group = clc.v2.Group(id=group_id,
                                 alias=alias)
            if group.Defaults("cpu"):
                cpu = group.Defaults("cpu")
            else:
                module.fail_json(
                    msg=str("Can\'t determine a default cpu value. Please provide a value for cpu."))
        return cpu

    @staticmethod
    def _find_memory(clc, module):
        """
        Find or validate the Memory value by calling the CLC API
        :param clc: clc-sdk instance to use
        :param module: module to validate
        :return: Int value for Memory
        """
        memory = module.params.get('memory')
        group_id = module.params.get('group_id')
        alias = module.params.get('alias')
        state = module.params.get('state')

        if not memory and state == 'present':
            group = clc.v2.Group(id=group_id,
                                 alias=alias)
            if group.Defaults("memory"):
                memory = group.Defaults("memory")
            else:
                module.fail_json(msg=str(
                    "Can\'t determine a default memory value. Please provide a value for memory."))
        return memory

    @staticmethod
    def _find_description(module):
        """
        Set the description module param to name if description is blank
        :param module: the module to validate
        :return: string description
        """
        description = module.params.get('description')
        if not description:
            description = module.params.get('name')
        return description

    @staticmethod
    def _validate_types(module):
        """
        Validate that type and storage_type are set appropriately, and fail if not
        :param module: the module to validate
        :return: none
        """
        state = module.params.get('state')
        server_type = module.params.get(
            'type').lower() if module.params.get('type') else None
        storage_type = module.params.get(
            'storage_type').lower() if module.params.get('storage_type') else None

        if state == "present":
            if server_type == "standard" and storage_type not in (
                    "standard", "premium"):
                module.fail_json(
                    msg=str("Standard VMs must have storage_type = 'standard' or 'premium'"))

            if server_type == "hyperscale" and storage_type != "hyperscale":
                module.fail_json(
                    msg=str("Hyperscale VMs must have storage_type = 'hyperscale'"))

    @staticmethod
    def _validate_name(module):
        """
        Validate that name is the correct length if provided, fail if it's not
        :param module: the module to validate
        :return: none
        """
        server_name = module.params.get('name')
        state = module.params.get('state')

        if state == 'present' and (
                len(server_name) < 1 or len(server_name) > 6):
            module.fail_json(msg=str(
                "When state = 'present', name must be a string with a minimum length of 1 and a maximum length of 6"))

    @staticmethod
    def _find_ttl(clc, module):
        """
        Validate that TTL is > 3600 if set, and fail if not
        :param clc: clc-sdk instance to use
        :param module: module to validate
        :return: validated ttl
        """
        ttl = module.params.get('ttl')

        if ttl:
            if ttl <= 3600:
                return module.fail_json(msg=str("Ttl cannot be <= 3600"))
            else:
                ttl = clc.v2.time_utils.SecondsToZuluTS(int(time.time()) + ttl)
        return ttl

    @staticmethod
    def _find_template_id(module, datacenter):
        """
        Find the template id by calling the CLC API.
        :param module: the module to validate
        :param datacenter: the datacenter to search for the template
        :return: a valid clc template id
        """
        lookup_template = module.params.get('template')
        state = module.params.get('state')
        type = module.params.get('type')
        result = None

        if state == 'present' and type != 'bareMetal':
            try:
                result = datacenter.Templates().Search(lookup_template)[0].id
            except CLCException:
                module.fail_json(
                    msg=str(
                        "Unable to find a template: " +
                        lookup_template +
                        " in location: " +
                        datacenter.id))
        return result

    @staticmethod
    def _find_network_id(module, datacenter):
        """
        Validate the provided network id or return a default.
        :param module: the module to validate
        :param datacenter: the datacenter to search for a network id
        :return: a valid network id
        """
        network_id = module.params.get('network_id')

        if not network_id:
            try:
                network_id = datacenter.Networks().networks[0].id
                # -- added for clc-sdk 2.23 compatibility
                # datacenter_networks = clc_sdk.v2.Networks(
                #   networks_lst=datacenter._DeploymentCapabilities()['deployableNetworks'])
                # network_id = datacenter_networks.networks[0].id
                # -- end
            except CLCException:
                module.fail_json(
                    msg=str(
                        "Unable to find a network in location: " +
                        datacenter.id))

        return network_id

    @staticmethod
    def _find_aa_policy_id(clc, module):
        """
        Validate if the anti affinity policy exist for the given name and throw error if not
        :param clc: the clc-sdk instance
        :param module: the module to validate
        :return: aa_policy_id: the anti affinity policy id of the given name.
        """
        aa_policy_id = module.params.get('anti_affinity_policy_id')
        aa_policy_name = module.params.get('anti_affinity_policy_name')
        if not aa_policy_id and aa_policy_name:
            alias = module.params.get('alias')
            aa_policy_id = ClcServer._get_anti_affinity_policy_id(
                clc,
                module,
                alias,
                aa_policy_name)
            if not aa_policy_id:
                module.fail_json(
                    msg='No anti affinity policy was found with policy name : %s' % aa_policy_name)
        return aa_policy_id

    @staticmethod
    def _find_alert_policy_id(clc, module):
        """
        Validate if the alert policy exist for the given name and throw error if not
        :param clc: the clc-sdk instance
        :param module: the module to validate
        :return: alert_policy_id: the alert policy id of the given name.
        """
        alert_policy_id = module.params.get('alert_policy_id')
        alert_policy_name = module.params.get('alert_policy_name')
        if not alert_policy_id and alert_policy_name:
            alias = module.params.get('alias')
            alert_policy_id = ClcServer._get_alert_policy_id_by_name(
                clc=clc,
                module=module,
                alias=alias,
                alert_policy_name=alert_policy_name
            )
            if not alert_policy_id:
                module.fail_json(
                    msg='No alert policy exist with name : %s' % alert_policy_name)
        return alert_policy_id

    def _create_servers(self, module, clc, override_count=None):
        """
        Create New Servers in CLC cloud
        :param module: the AnsibleModule object
        :param clc: the clc-sdk instance to use
        :return: a list of dictionaries with server information about the servers that were created
        """
        p = module.params
        request_list = []
        servers = []
        server_dict_array = []
        created_server_ids = []
        partial_created_servers_ids = []

        add_public_ip = p.get('add_public_ip')
        public_ip_protocol = p.get('public_ip_protocol')
        public_ip_ports = p.get('public_ip_ports')

        params = {
            'name': p.get('name'),
            'template': p.get('template'),
            'group_id': p.get('group'),
            'network_id': p.get('network_id'),
            'cpu': p.get('cpu'),
            'memory': p.get('memory'),
            'alias': p.get('alias'),
            'password': p.get('password'),
            'ip_address': p.get('ip_address'),
            'storage_type': p.get('storage_type'),
            'type': p.get('type'),
            'primary_dns': p.get('primary_dns'),
            'secondary_dns': p.get('secondary_dns'),
            'additional_disks': p.get('additional_disks'),
            'custom_fields': p.get('custom_fields'),
            'ttl': p.get('ttl'),
            'managed_os': p.get('managed_os'),
            'description': p.get('description'),
            'source_server_password': p.get('source_server_password'),
            'cpu_autoscale_policy_id': p.get('cpu_autoscale_policy_id'),
            'anti_affinity_policy_id': p.get('anti_affinity_policy_id'),
            'packages': p.get('packages'),
            'configuration_id': p.get('configuration_id'),
            'os_type': p.get('os_type')
        }

        count = override_count if override_count else p.get('count')

        changed = False if count == 0 else True

        if not changed:
            return server_dict_array, created_server_ids, partial_created_servers_ids, changed
        for i in range(0, count):
            if not module.check_mode:
                req = self._create_clc_server(clc=clc,
                                              module=module,
                                              server_params=params)
                server = req.requests[0].Server()
                request_list.append(req)
                servers.append(server)

        self._wait_for_requests(module, request_list)
        self._refresh_servers(module, servers)

        ip_failed_servers = self._add_public_ip_to_servers(
            module=module,
            should_add_public_ip=add_public_ip,
            servers=servers,
            public_ip_protocol=public_ip_protocol,
            public_ip_ports=public_ip_ports)
        ap_failed_servers = self._add_alert_policy_to_servers(clc=clc,
                                                              module=module,
                                                              servers=servers)

        for server in servers:
            if server in ip_failed_servers or server in ap_failed_servers:
                partial_created_servers_ids.append(server.id)
            else:
                # reload server details
                server = clc.v2.Server(server.id)
                server.data['ipaddress'] = server.details[
                    'ipAddresses'][0]['internal']

                if add_public_ip and len(server.PublicIPs().public_ips) > 0:
                    server.data['publicip'] = str(
                        server.PublicIPs().public_ips[0])
                created_server_ids.append(server.id)
            server_dict_array.append(server.data)

        return server_dict_array, created_server_ids, partial_created_servers_ids, changed

    def _enforce_count(self, module, clc):
        """
        Enforce that there is the right number of servers in the provided group.
        Starts or stops servers as necessary.
        :param module: the AnsibleModule object
        :param clc: the clc-sdk instance to use
        :return: a list of dictionaries with server information about the servers that were created or deleted
        """
        p = module.params
        changed = False
        count_group = p.get('count_group')
        datacenter = ClcServer._find_datacenter(clc, module)
        exact_count = p.get('exact_count')
        server_dict_array = []
        partial_servers_ids = []
        changed_server_ids = []

        # fail here if the exact count was specified without filtering
        # on a group, as this may lead to a undesired removal of instances
        if exact_count and count_group is None:
            return module.fail_json(
                msg="you must use the 'count_group' option with exact_count")

        servers, running_servers = ClcServer._find_running_servers_by_group(
            module, datacenter, count_group)

        if len(running_servers) == exact_count:
            changed = False

        elif len(running_servers) < exact_count:
            to_create = exact_count - len(running_servers)
            server_dict_array, changed_server_ids, partial_servers_ids, changed \
                = self._create_servers(module, clc, override_count=to_create)

            for server in server_dict_array:
                running_servers.append(server)

        elif len(running_servers) > exact_count:
            to_remove = len(running_servers) - exact_count
            all_server_ids = sorted([x.id for x in running_servers])
            remove_ids = all_server_ids[0:to_remove]

            (changed, server_dict_array, changed_server_ids) \
                = ClcServer._delete_servers(module, clc, remove_ids)

        return server_dict_array, changed_server_ids, partial_servers_ids, changed

    @staticmethod
    def _wait_for_requests(module, request_list):
        """
        Block until server provisioning requests are completed.
        :param module: the AnsibleModule object
        :param request_list: a list of clc-sdk.Request instances
        :return: none
        """
        wait = module.params.get('wait')
        if wait:
            # Requests.WaitUntilComplete() returns the count of failed requests
            failed_requests_count = sum(
                [request.WaitUntilComplete() for request in request_list])

            if failed_requests_count > 0:
                module.fail_json(
                    msg='Unable to process server request')

    @staticmethod
    def _refresh_servers(module, servers):
        """
        Loop through a list of servers and refresh them.
        :param module: the AnsibleModule object
        :param servers: list of clc-sdk.Server instances to refresh
        :return: none
        """
        for server in servers:
            try:
                server.Refresh()
            except CLCException as ex:
                module.fail_json(msg='Unable to refresh the server {0}. {1}'.format(
                    server.id, ex.message
                ))

    @staticmethod
    def _add_public_ip_to_servers(
            module,
            should_add_public_ip,
            servers,
            public_ip_protocol,
            public_ip_ports):
        """
        Create a public IP for servers
        :param module: the AnsibleModule object
        :param should_add_public_ip: boolean - whether or not to provision a public ip for servers.  Skipped if False
        :param servers: List of servers to add public ips to
        :param public_ip_protocol: a protocol to allow for the public ips
        :param public_ip_ports: list of ports to allow for the public ips
        :return: none
        """
        failed_servers = []
        if not should_add_public_ip:
            return failed_servers

        ports_lst = []
        request_list = []
        server = None

        for port in public_ip_ports:
            ports_lst.append(
                {'protocol': public_ip_protocol, 'port': port})
        try:
            if not module.check_mode:
                for server in servers:
                    request = server.PublicIPs().Add(ports_lst)
                    request_list.append(request)
        except APIFailedResponse:
            failed_servers.append(server)
        ClcServer._wait_for_requests(module, request_list)
        return failed_servers

    @staticmethod
    def _add_alert_policy_to_servers(clc, module, servers):
        """
        Associate the alert policy to servers
        :param clc: the clc-sdk instance to use
        :param module: the AnsibleModule object
        :param servers: List of servers to add alert policy to
        :return: failed_servers: the list of servers which failed while associating alert policy
        """
        failed_servers = []
        p = module.params
        alert_policy_id = p.get('alert_policy_id')
        alias = p.get('alias')

        if alert_policy_id and not module.check_mode:
            for server in servers:
                try:
                    ClcServer._add_alert_policy_to_server(
                        clc=clc,
                        alias=alias,
                        server_id=server.id,
                        alert_policy_id=alert_policy_id)
                except CLCException:
                    failed_servers.append(server)
        return failed_servers

    @staticmethod
    def _add_alert_policy_to_server(
            clc, alias, server_id, alert_policy_id):
        """
        Associate an alert policy to a clc server
        :param clc: the clc-sdk instance to use
        :param alias: the clc account alias
        :param server_id: The clc server id
        :param alert_policy_id: the alert policy id to be associated to the server
        :return: none
        """
        try:
            clc.v2.API.Call(
                method='POST',
                url='servers/%s/%s/alertPolicies' % (alias, server_id),
                payload=json.dumps(
                    {
                        'id': alert_policy_id
                    }))
        except APIFailedResponse as e:
            raise CLCException(
                'Failed to associate alert policy to the server : {0} with Error {1}'.format(
                    server_id, str(e.response_text)))

    @staticmethod
    def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name):
        """
        Returns the alert policy id for the given alert policy name
        :param clc: the clc-sdk instance to use
        :param module: the AnsibleModule object
        :param alias: the clc account alias
        :param alert_policy_name: the name of the alert policy
        :return: alert_policy_id: the alert policy id
        """
        alert_policy_id = None
        policies = clc.v2.API.Call('GET', '/v2/alertPolicies/%s' % alias)
        if not policies:
            return alert_policy_id
        for policy in policies.get('items'):
            if policy.get('name') == alert_policy_name:
                if not alert_policy_id:
                    alert_policy_id = policy.get('id')
                else:
                    return module.fail_json(
                        msg='multiple alert policies were found with policy name : %s' % alert_policy_name)
        return alert_policy_id

    @staticmethod
    def _delete_servers(module, clc, server_ids):
        """
        Delete the servers on the provided list
        :param module: the AnsibleModule object
        :param clc: the clc-sdk instance to use
        :param server_ids: list of servers to delete
        :return: a list of dictionaries with server information about the servers that were deleted
        """
        terminated_server_ids = []
        server_dict_array = []
        request_list = []

        if not isinstance(server_ids, list) or len(server_ids) < 1:
            return module.fail_json(
                msg='server_ids should be a list of servers, aborting')

        servers = clc.v2.Servers(server_ids).Servers()
        for server in servers:
            if not module.check_mode:
                request_list.append(server.Delete())
        ClcServer._wait_for_requests(module, request_list)

        for server in servers:
            terminated_server_ids.append(server.id)

        return True, server_dict_array, terminated_server_ids

    @staticmethod
    def _start_stop_servers(module, clc, server_ids):
        """
        Start or Stop the servers on the provided list
        :param module: the AnsibleModule object
        :param clc: the clc-sdk instance to use
        :param server_ids: list of servers to start or stop
        :return: a list of dictionaries with server information about the servers that were started or stopped
        """
        p = module.params
        state = p.get('state')
        changed = False
        changed_servers = []
        server_dict_array = []
        result_server_ids = []
        request_list = []

        if not isinstance(server_ids, list) or len(server_ids) < 1:
            return module.fail_json(
                msg='server_ids should be a list of servers, aborting')

        servers = clc.v2.Servers(server_ids).Servers()
        for server in servers:
            if server.powerState != state:
                changed_servers.append(server)
                if not module.check_mode:
                    request_list.append(
                        ClcServer._change_server_power_state(
                            module,
                            server,
                            state))
                changed = True

        ClcServer._wait_for_requests(module, request_list)
        ClcServer._refresh_servers(module, changed_servers)

        for server in set(changed_servers + servers):
            try:
                server.data['ipaddress'] = server.details[
                    'ipAddresses'][0]['internal']
                server.data['publicip'] = str(
                    server.PublicIPs().public_ips[0])
            except (KeyError, IndexError):
                pass

            server_dict_array.append(server.data)
            result_server_ids.append(server.id)

        return changed, server_dict_array, result_server_ids

    @staticmethod
    def _change_server_power_state(module, server, state):
        """
        Change the server powerState
        :param module: the module to check for intended state
        :param server: the server to start or stop
        :param state: the intended powerState for the server
        :return: the request object from clc-sdk call
        """
        result = None
        try:
            if state == 'started':
                result = server.PowerOn()
            else:
                # Try to shut down the server and fall back to power off when unable to shut down.
                result = server.ShutDown()
                if result and hasattr(result, 'requests') and result.requests[0]:
                    return result
                else:
                    result = server.PowerOff()
        except CLCException:
            module.fail_json(
                msg='Unable to change power state for server {0}'.format(
                    server.id))
        return result

    @staticmethod
    def _find_running_servers_by_group(module, datacenter, count_group):
        """
        Find a list of running servers in the provided group
        :param module: the AnsibleModule object
        :param datacenter: the clc-sdk.Datacenter instance to use to lookup the group
        :param count_group: the group to count the servers
        :return: list of servers, and list of running servers
        """
        group = ClcServer._find_group(
            module=module,
            datacenter=datacenter,
            lookup_group=count_group)

        servers = group.Servers().Servers()
        running_servers = []

        for server in servers:
            if server.status == 'active' and server.powerState == 'started':
                running_servers.append(server)

        return servers, running_servers

    @staticmethod
    def _find_group(module, datacenter, lookup_group=None):
        """
        Find a server group in a datacenter by calling the CLC API
        :param module: the AnsibleModule instance
        :param datacenter: clc-sdk.Datacenter instance to search for the group
        :param lookup_group: string name of the group to search for
        :return: clc-sdk.Group instance
        """
        if not lookup_group:
            lookup_group = module.params.get('group')
        try:
            return datacenter.Groups().Get(lookup_group)
        except CLCException:
            pass

        # The search above only acts on the main
        result = ClcServer._find_group_recursive(
            module,
            datacenter.Groups(),
            lookup_group)

        if result is None:
            module.fail_json(
                msg=str(
                    "Unable to find group: " +
                    lookup_group +
                    " in location: " +
                    datacenter.id))

        return result

    @staticmethod
    def _find_group_recursive(module, group_list, lookup_group):
        """
        Find a server group by recursively walking the tree
        :param module: the AnsibleModule instance to use
        :param group_list: a list of groups to search
        :param lookup_group: the group to look for
        :return: list of groups
        """
        result = None
        for group in group_list.groups:
            subgroups = group.Subgroups()
            try:
                return subgroups.Get(lookup_group)
            except CLCException:
                result = ClcServer._find_group_recursive(
                    module,
                    subgroups,
                    lookup_group)

            if result is not None:
                break

        return result

    @staticmethod
    def _create_clc_server(
            clc,
            module,
            server_params):
        """
        Call the CLC Rest API to Create a Server
        :param clc: the clc-python-sdk instance to use
        :param module: the AnsibleModule instance to use
        :param server_params: a dictionary of params to use to create the servers
        :return: clc-sdk.Request object linked to the queued server request
        """

        try:
            res = clc.v2.API.Call(
                method='POST',
                url='servers/%s' %
                (server_params.get('alias')),
                payload=json.dumps(
                    {
                        'name': server_params.get('name'),
                        'description': server_params.get('description'),
                        'groupId': server_params.get('group_id'),
                        'sourceServerId': server_params.get('template'),
                        'isManagedOS': server_params.get('managed_os'),
                        'primaryDNS': server_params.get('primary_dns'),
                        'secondaryDNS': server_params.get('secondary_dns'),
                        'networkId': server_params.get('network_id'),
                        'ipAddress': server_params.get('ip_address'),
                        'password': server_params.get('password'),
                        'sourceServerPassword': server_params.get('source_server_password'),
                        'cpu': server_params.get('cpu'),
                        'cpuAutoscalePolicyId': server_params.get('cpu_autoscale_policy_id'),
                        'memoryGB': server_params.get('memory'),
                        'type': server_params.get('type'),
                        'storageType': server_params.get('storage_type'),
                        'antiAffinityPolicyId': server_params.get('anti_affinity_policy_id'),
                        'customFields': server_params.get('custom_fields'),
                        'additionalDisks': server_params.get('additional_disks'),
                        'ttl': server_params.get('ttl'),
                        'packages': server_params.get('packages'),
                        'configurationId': server_params.get('configuration_id'),
                        'osType': server_params.get('os_type')}))

            result = clc.v2.Requests(res)
        except APIFailedResponse as ex:
            return module.fail_json(msg='Unable to create the server: {0}. {1}'.format(
                server_params.get('name'),
                ex.response_text
            ))

        #
        # Patch the Request object so that it returns a valid server

        # Find the server's UUID from the API response
        server_uuid = [obj['id']
                       for obj in res['links'] if obj['rel'] == 'self'][0]

        # Change the request server method to a _find_server_by_uuid closure so
        # that it will work
        result.requests[0].Server = lambda: ClcServer._find_server_by_uuid_w_retry(
            clc,
            module,
            server_uuid,
            server_params.get('alias'))

        return result

    @staticmethod
    def _get_anti_affinity_policy_id(clc, module, alias, aa_policy_name):
        """
        retrieves the anti affinity policy id of the server based on the name of the policy
        :param clc: the clc-sdk instance to use
        :param module: the AnsibleModule object
        :param alias: the CLC account alias
        :param aa_policy_name: the anti affinity policy name
        :return: aa_policy_id: The anti affinity policy id
        """
        aa_policy_id = None
        try:
            aa_policies = clc.v2.API.Call(method='GET',
                                          url='antiAffinityPolicies/%s' % alias)
        except APIFailedResponse as ex:
            return module.fail_json(msg='Unable to fetch anti affinity policies for account: {0}. {1}'.format(
                alias, ex.response_text))
        for aa_policy in aa_policies.get('items'):
            if aa_policy.get('name') == aa_policy_name:
                if not aa_policy_id:
                    aa_policy_id = aa_policy.get('id')
                else:
                    return module.fail_json(
                        msg='multiple anti affinity policies were found with policy name : %s' % aa_policy_name)
        return aa_policy_id

    #
    #  This is the function that gets patched to the Request.server object using a lamda closure
    #

    @staticmethod
    def _find_server_by_uuid_w_retry(
            clc, module, svr_uuid, alias=None, retries=5, back_out=2):
        """
        Find the clc server by the UUID returned from the provisioning request.  Retry the request if a 404 is returned.
        :param clc: the clc-sdk instance to use
        :param module: the AnsibleModule object
        :param svr_uuid: UUID of the server
        :param retries: the number of retry attempts to make prior to fail. default is 5
        :param alias: the Account Alias to search
        :return: a clc-sdk.Server instance
        """
        if not alias:
            alias = clc.v2.Account.GetAlias()

        # Wait and retry if the api returns a 404
        while True:
            retries -= 1
            try:
                server_obj = clc.v2.API.Call(
                    method='GET', url='servers/%s/%s?uuid=true' %
                    (alias, svr_uuid))
                server_id = server_obj['id']
                server = clc.v2.Server(
                    id=server_id,
                    alias=alias,
                    server_obj=server_obj)
                return server

            except APIFailedResponse as e:
                if e.response_status_code != 404:
                    return module.fail_json(
                        msg='A failure response was received from CLC API when '
                        'attempting to get details for a server:  UUID=%s, Code=%i, Message=%s' %
                        (svr_uuid, e.response_status_code, e.message))
                if retries == 0:
                    return module.fail_json(
                        msg='Unable to reach the CLC API after 5 attempts')
                time.sleep(back_out)
                back_out *= 2

    @staticmethod
    def _set_user_agent(clc):
        if hasattr(clc, 'SetRequestsSession'):
            agent_string = "ClcAnsibleModule/" + __version__
            ses = requests.Session()
            ses.headers.update({"Api-Client": agent_string})
            ses.headers['User-Agent'] += " " + agent_string
            clc.SetRequestsSession(ses)


def main():
    """
    The main function.  Instantiates the module and calls process_request.
    :return: none
    """
    argument_dict = ClcServer._define_module_argument_spec()
    module = AnsibleModule(supports_check_mode=True, **argument_dict)
    clc_server = ClcServer(module)
    clc_server.process_request()


if __name__ == '__main__':
    main()
