diff --git a/cloud/centurylink/__init__.py b/cloud/centurylink/__init__.py new file mode 100644 index 00000000000..71f0abcff9d --- /dev/null +++ b/cloud/centurylink/__init__.py @@ -0,0 +1 @@ +__version__ = "${version}" diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py new file mode 100644 index 00000000000..644f3817c4f --- /dev/null +++ b/cloud/centurylink/clc_aa_policy.py @@ -0,0 +1,294 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ + +DOCUMENTATION = ''' +module: clc_aa_policy +short_descirption: Create or Delete Anti Affinity Policies at CenturyLink Cloud. +description: + - An Ansible module to Create or Delete Anti Affinity Policies at CenturyLink Cloud. +options: + name: + description: + - The name of the Anti Affinity Policy. + required: True + location: + description: + - Datacenter in which the policy lives/should live. + required: True + state: + description: + - Whether to create or delete the policy. + required: False + default: present + choices: ['present','absent'] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +--- +- name: Create AA Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create an Anti Affinity Policy + clc_aa_policy: + name: 'Hammer Time' + location: 'UK3' + state: present + register: policy + + - name: debug + debug: var=policy + +--- +- name: Delete AA Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Anti Affinity Policy + clc_aa_policy: + name: 'Hammer Time' + location: 'UK3' + state: absent + register: policy + + - name: debug + debug: var=policy +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + clc_found = False + clc_sdk = None +else: + clc_found = True + + +class ClcAntiAffinityPolicy(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + self.policy_dict = {} + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + location=dict(required=True), + alias=dict(default=None), + wait=dict(default=True), + state=dict(default='present', choices=['present', 'absent']), + ) + return argument_spec + + # Module Behavior Goodness + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_clc_credentials_from_env() + self.policy_dict = self._get_policies_for_datacenter(p) + + if p['state'] == "absent": + changed, policy = self._ensure_policy_is_absent(p) + else: + changed, policy = self._ensure_policy_is_present(p) + + if hasattr(policy, 'data'): + policy = policy.data + elif hasattr(policy, '__dict__'): + policy = policy.__dict__ + + self.module.exit_json(changed=changed, policy=policy) + + 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") + + def _get_policies_for_datacenter(self, p): + """ + Get the Policies for a datacenter by calling the CLC API. + :param p: datacenter to get policies from + :return: policies in the datacenter + """ + response = {} + + policies = self.clc.v2.AntiAffinity.GetAll(location=p['location']) + + for policy in policies: + response[policy.name] = policy + return response + + def _create_policy(self, p): + """ + Create an Anti Affinnity Policy using the CLC API. + :param p: datacenter to create policy in + :return: response dictionary from the CLC API. + """ + return self.clc.v2.AntiAffinity.Create( + name=p['name'], + location=p['location']) + + def _delete_policy(self, p): + """ + Delete an Anti Affinity Policy using the CLC API. + :param p: datacenter to delete a policy from + :return: none + """ + policy = self.policy_dict[p['name']] + policy.Delete() + + def _policy_exists(self, policy_name): + """ + Check to see if an Anti Affinity Policy exists + :param policy_name: name of the policy + :return: boolean of if the policy exists + """ + if policy_name in self.policy_dict: + return self.policy_dict.get(policy_name) + + return False + + def _ensure_policy_is_absent(self, p): + """ + Makes sure that a policy is absent + :param p: dictionary of policy name + :return: tuple of if a deletion occurred and the name of the policy that was deleted + """ + changed = False + if self._policy_exists(policy_name=p['name']): + changed = True + if not self.module.check_mode: + self._delete_policy(p) + return changed, None + + def _ensure_policy_is_present(self, p): + """ + Ensures that a policy is present + :param p: dictonary of a policy name + :return: tuple of if an addition occurred and the name of the policy that was added + """ + changed = False + policy = self._policy_exists(policy_name=p['name']) + if not policy: + changed = True + policy = None + if not self.module.check_mode: + policy = self._create_policy(p) + return changed, policy + + @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 + """ + module = AnsibleModule( + argument_spec=ClcAntiAffinityPolicy._define_module_argument_spec(), + supports_check_mode=True) + clc_aa_policy = ClcAntiAffinityPolicy(module) + clc_aa_policy.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py new file mode 100644 index 00000000000..75467967a85 --- /dev/null +++ b/cloud/centurylink/clc_alert_policy.py @@ -0,0 +1,473 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ + +DOCUMENTATION = ''' +module: clc_alert_policy +short_descirption: Create or Delete Alert Policies at CenturyLink Cloud. +description: + - An Ansible module to Create or Delete Alert Policies at CenturyLink Cloud. +options: + alias: + description: + - The alias of your CLC Account + required: True + name: + description: + - The name of the alert policy. This is mutually exclusive with id + default: None + aliases: [] + id: + description: + - The alert policy id. This is mutually exclusive with name + default: None + aliases: [] + alert_recipients: + description: + - A list of recipient email ids to notify the alert. + required: True + aliases: [] + metric: + description: + - The metric on which to measure the condition that will trigger the alert. + required: True + default: None + choices: ['cpu','memory','disk'] + aliases: [] + duration: + description: + - The length of time in minutes that the condition must exceed the threshold. + required: True + default: None + aliases: [] + threshold: + description: + - The threshold that will trigger the alert when the metric equals or exceeds it. + This number represents a percentage and must be a value between 5.0 - 95.0 that is a multiple of 5.0 + required: True + default: None + aliases: [] + state: + description: + - Whether to create or delete the policy. + required: False + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +--- +- name: Create Alert Policy Example + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create an Alert Policy for disk above 80% for 5 minutes + clc_alert_policy: + alias: wfad + name: 'alert for disk > 80%' + alert_recipients: + - test1@centurylink.com + - test2@centurylink.com + metric: 'disk' + duration: '00:05:00' + threshold: 80 + state: present + register: policy + + - name: debug + debug: var=policy + +--- +- name: Delete Alert Policy Example + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Alert Policy + clc_alert_policy: + alias: wfad + name: 'alert for disk > 80%' + state: absent + register: policy + + - name: debug + debug: var=policy +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + clc_found = False + clc_sdk = None +else: + clc_found = True + + +class ClcAlertPolicy(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + self.policy_dict = {} + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(default=None), + id=dict(default=None), + alias=dict(required=True, default=None), + alert_recipients=dict(type='list', required=False, default=None), + metric=dict(required=False, choices=['cpu', 'memory', 'disk'], default=None), + duration=dict(required=False, type='str', default=None), + threshold=dict(required=False, type='int', default=None), + state=dict(default='present', choices=['present', 'absent']) + ) + mutually_exclusive = [ + ['name', 'id'] + ] + return {'argument_spec': argument_spec, + 'mutually_exclusive': mutually_exclusive} + + # Module Behavior Goodness + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_clc_credentials_from_env() + self.policy_dict = self._get_alert_policies(p['alias']) + + if p['state'] == 'present': + changed, policy = self._ensure_alert_policy_is_present() + else: + changed, policy = self._ensure_alert_policy_is_absent() + + self.module.exit_json(changed=changed, policy=policy) + + 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") + + def _ensure_alert_policy_is_present(self): + """ + Ensures that the alert policy is present + :return: (changed, policy) + canged: A flag representing if anything is modified + policy: the created/updated alert policy + """ + changed = False + p = self.module.params + policy_name = p.get('name') + alias = p.get('alias') + if not policy_name: + self.module.fail_json(msg='Policy name is a required') + policy = self._alert_policy_exists(alias, policy_name) + if not policy: + changed = True + policy = None + if not self.module.check_mode: + policy = self._create_alert_policy() + else: + changed_u, policy = self._ensure_alert_policy_is_updated(policy) + if changed_u: + changed = True + return changed, policy + + def _ensure_alert_policy_is_absent(self): + """ + Ensures that the alert policy is absent + :return: (changed, None) + canged: A flag representing if anything is modified + """ + changed = False + p = self.module.params + alert_policy_id = p.get('id') + alert_policy_name = p.get('name') + alias = p.get('alias') + if not alert_policy_id and not alert_policy_name: + self.module.fail_json( + msg='Either alert policy id or policy name is required') + if not alert_policy_id and alert_policy_name: + alert_policy_id = self._get_alert_policy_id( + self.module, + alert_policy_name) + if alert_policy_id and alert_policy_id in self.policy_dict: + changed = True + if not self.module.check_mode: + self._delete_alert_policy(alias, alert_policy_id) + return changed, None + + def _ensure_alert_policy_is_updated(self, alert_policy): + """ + Ensures the aliert policy is updated if anything is changed in the alert policy configuration + :param alert_policy: the targetalert policy + :return: (changed, policy) + canged: A flag representing if anything is modified + policy: the updated the alert policy + """ + changed = False + p = self.module.params + alert_policy_id = alert_policy.get('id') + email_list = p.get('alert_recipients') + metric = p.get('metric') + duration = p.get('duration') + threshold = p.get('threshold') + policy = alert_policy + if (metric and metric != str(alert_policy.get('triggers')[0].get('metric'))) or \ + (duration and duration != str(alert_policy.get('triggers')[0].get('duration'))) or \ + (threshold and float(threshold) != float(alert_policy.get('triggers')[0].get('threshold'))): + changed = True + elif email_list: + t_email_list = list( + alert_policy.get('actions')[0].get('settings').get('recipients')) + if set(email_list) != set(t_email_list): + changed = True + if changed and not self.module.check_mode: + policy = self._update_alert_policy(alert_policy_id) + return changed, policy + + def _get_alert_policies(self, alias): + """ + Get the alert policies for account alias by calling the CLC API. + :param alias: the account alias + :return: the alert policies for the account alias + """ + response = {} + + policies = self.clc.v2.API.Call('GET', + '/v2/alertPolicies/%s' + % (alias)) + + for policy in policies.get('items'): + response[policy.get('id')] = policy + return response + + def _create_alert_policy(self): + """ + Create an alert Policy using the CLC API. + :return: response dictionary from the CLC API. + """ + p = self.module.params + alias = p['alias'] + email_list = p['alert_recipients'] + metric = p['metric'] + duration = p['duration'] + threshold = p['threshold'] + name = p['name'] + arguments = json.dumps( + { + 'name': name, + 'actions': [{ + 'action': 'email', + 'settings': { + 'recipients': email_list + } + }], + 'triggers': [{ + 'metric': metric, + 'duration': duration, + 'threshold': threshold + }] + } + ) + try: + result = self.clc.v2.API.Call( + 'POST', + '/v2/alertPolicies/%s' % + (alias), + arguments) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to create alert policy. %s' % str( + e.response_text)) + return result + + def _update_alert_policy(self, alert_policy_id): + """ + Update alert policy using the CLC API. + :param alert_policy_id: The clc alert policy id + :return: response dictionary from the CLC API. + """ + p = self.module.params + alias = p['alias'] + email_list = p['alert_recipients'] + metric = p['metric'] + duration = p['duration'] + threshold = p['threshold'] + name = p['name'] + arguments = json.dumps( + { + 'name': name, + 'actions': [{ + 'action': 'email', + 'settings': { + 'recipients': email_list + } + }], + 'triggers': [{ + 'metric': metric, + 'duration': duration, + 'threshold': threshold + }] + } + ) + try: + result = self.clc.v2.API.Call( + 'PUT', '/v2/alertPolicies/%s/%s' % + (alias, alert_policy_id), arguments) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to update alert policy. %s' % str( + e.response_text)) + return result + + def _delete_alert_policy(self, alias, policy_id): + """ + Delete an alert policy using the CLC API. + :param alias : the account alias + :param policy_id: the alert policy id + :return: response dictionary from the CLC API. + """ + try: + result = self.clc.v2.API.Call( + 'DELETE', '/v2/alertPolicies/%s/%s' % + (alias, policy_id), None) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to delete alert policy. %s' % str( + e.response_text)) + return result + + def _alert_policy_exists(self, alias, policy_name): + """ + Check to see if an alert policy exists + :param policy_name: name of the alert policy + :return: boolean of if the policy exists + """ + result = False + for id in self.policy_dict: + if self.policy_dict.get(id).get('name') == policy_name: + result = self.policy_dict.get(id) + return result + + def _get_alert_policy_id(self, module, alert_policy_name): + """ + retrieves the alert policy id of the account based on the name of the policy + :param module: the AnsibleModule object + :param alert_policy_name: the alert policy name + :return: alert_policy_id: The alert policy id + """ + alert_policy_id = None + for id in self.policy_dict: + if self.policy_dict.get(id).get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = id + else: + return module.fail_json( + msg='mutiple alert policies were found with policy name : %s' % + (alert_policy_name)) + return alert_policy_id + + @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 = ClcAlertPolicy._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_alert_policy = ClcAlertPolicy(module) + clc_alert_policy.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py new file mode 100644 index 00000000000..80cc18a24ca --- /dev/null +++ b/cloud/centurylink/clc_blueprint_package.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_blueprint_package +short_desciption: deploys a blue print package on a set of servers in CenturyLink Cloud. +description: + - An Ansible module to deploy blue print package on a set of servers in CenturyLink Cloud. +options: + server_ids: + description: + - A list of server Ids to deploy the blue print package. + default: [] + required: True + aliases: [] + package_id: + description: + - The package id of the blue print. + default: None + required: True + aliases: [] + package_params: + description: + - The dictionary of arguments required to deploy the blue print. + default: {} + required: False + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Deploy package + clc_blueprint_package: + server_ids: + - UC1WFSDANS01 + - UC1WFSDANS02 + package_id: 77abb844-579d-478d-3955-c69ab4a7ba1a + package_params: {} +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcBlueprintPackage(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + 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 + """ + p = self.module.params + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_clc_credentials_from_env() + + server_ids = p['server_ids'] + package_id = p['package_id'] + package_params = p['package_params'] + state = p['state'] + if state == 'present': + changed, changed_server_ids, requests = self.ensure_package_installed( + server_ids, package_id, package_params) + if not self.module.check_mode: + self._wait_for_requests_to_complete(requests) + self.module.exit_json(changed=changed, server_ids=changed_server_ids) + + @staticmethod + def define_argument_spec(): + """ + This function defnines the dictionary object required for + package module + :return: the package dictionary object + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + package_id=dict(required=True), + package_params=dict(type='dict', default={}), + wait=dict(default=True), + state=dict(default='present', choices=['present']) + ) + return argument_spec + + def ensure_package_installed(self, server_ids, package_id, package_params): + """ + Ensure the package is installed in the given list of servers + :param server_ids: the server list where the package needs to be installed + :param package_id: the package id + :param package_params: the package arguments + :return: (changed, server_ids) + changed: A flag indicating if a change was made + server_ids: The list of servers modfied + """ + changed = False + requests = [] + servers = self._get_servers_from_clc( + server_ids, + 'Failed to get servers from CLC') + try: + for server in servers: + request = self.clc_install_package( + server, + package_id, + package_params) + requests.append(request) + changed = True + except CLCException as ex: + self.module.fail_json( + msg='Failed while installing package : %s with Error : %s' % + (package_id, ex)) + return changed, server_ids, requests + + def clc_install_package(self, server, package_id, package_params): + """ + Read all servers from CLC and executes each package from package_list + :param server_list: The target list of servers where the packages needs to be installed + :param package_list: The list of packages to be installed + :return: (changed, server_ids) + changed: A flag indicating if a change was made + server_ids: The list of servers modfied + """ + result = None + if not self.module.check_mode: + result = server.ExecutePackage( + package_id=package_id, + parameters=package_params) + return result + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process package install request') + + def _get_servers_from_clc(self, server_list, message): + """ + Internal function to fetch list of CLC server objects from a list of server ids + :param the list server ids + :return the list of CLC server objects + """ + try: + return self.clc.v2.Servers(server_list).servers + except CLCException as ex: + self.module.fail_json(msg=message + ': %s' % ex) + + 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 _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(): + """ + Main function + :return: None + """ + module = AnsibleModule( + argument_spec=ClcBlueprintPackage.define_argument_spec(), + supports_check_mode=True + ) + clc_blueprint_package = ClcBlueprintPackage(module) + clc_blueprint_package.process_request() + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py new file mode 100644 index 00000000000..260c82bc885 --- /dev/null +++ b/cloud/centurylink/clc_firewall_policy.py @@ -0,0 +1,542 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); + +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_firewall_policy +short_desciption: Create/delete/update firewall policies +description: + - Create or delete or updated firewall polices on Centurylink Centurylink Cloud +options: + location: + description: + - Target datacenter for the firewall policy + default: None + required: True + aliases: [] + state: + description: + - Whether to create or delete the firewall policy + default: present + required: True + choices: ['present', 'absent'] + aliases: [] + source: + description: + - Source addresses for traffic on the originating firewall + default: None + required: For Creation + aliases: [] + destination: + description: + - Destination addresses for traffic on the terminating firewall + default: None + required: For Creation + aliases: [] + ports: + description: + - types of ports associated with the policy. TCP & UDP can take in single ports or port ranges. + default: None + required: False + choices: ['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456'] + aliases: [] + firewall_policy_id: + description: + - Id of the firewall policy + default: None + required: False + aliases: [] + source_account_alias: + description: + - CLC alias for the source account + default: None + required: True + aliases: [] + destination_account_alias: + description: + - CLC alias for the destination account + default: None + required: False + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False ] + aliases: [] + enabled: + description: + - If the firewall policy is enabled or disabled + default: true + required: False + choices: [ true, false ] + aliases: [] + +''' + +EXAMPLES = ''' +--- +- name: Create Firewall Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create / Verify an Firewall Policy at CenturyLink Cloud + clc_firewall: + source_account_alias: WFAD + location: VA1 + state: present + source: 10.128.216.0/24 + destination: 10.128.216.0/24 + ports: Any + destination_account_alias: WFAD + +--- +- name: Delete Firewall Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Firewall Policy at CenturyLink Cloud + clc_firewall: + source_account_alias: WFAD + location: VA1 + state: present + firewall_policy_id: c62105233d7a4231bd2e91b9c791eaae +''' + +__version__ = '${version}' + +import urlparse +from time import sleep +import requests + +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcFirewallPolicy(): + + clc = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.firewall_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + location=dict(required=True, defualt=None), + source_account_alias=dict(required=True, default=None), + destination_account_alias=dict(default=None), + firewall_policy_id=dict(default=None), + ports=dict(default=None, type='list'), + source=dict(defualt=None, type='list'), + destination=dict(defualt=None, type='list'), + wait=dict(default=True), + state=dict(default='present', choices=['present', 'absent']), + enabled=dict(defualt=None) + ) + return argument_spec + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + location = self.module.params.get('location') + source_account_alias = self.module.params.get('source_account_alias') + destination_account_alias = self.module.params.get( + 'destination_account_alias') + firewall_policy_id = self.module.params.get('firewall_policy_id') + ports = self.module.params.get('ports') + source = self.module.params.get('source') + destination = self.module.params.get('destination') + wait = self.module.params.get('wait') + state = self.module.params.get('state') + enabled = self.module.params.get('enabled') + + self.firewall_dict = { + 'location': location, + 'source_account_alias': source_account_alias, + 'destination_account_alias': destination_account_alias, + 'firewall_policy_id': firewall_policy_id, + 'ports': ports, + 'source': source, + 'destination': destination, + 'wait': wait, + 'state': state, + 'enabled': enabled} + + self._set_clc_credentials_from_env() + requests = [] + + if state == 'absent': + changed, firewall_policy_id, response = self._ensure_firewall_policy_is_absent( + source_account_alias, location, self.firewall_dict) + + elif state == 'present': + changed, firewall_policy_id, response = self._ensure_firewall_policy_is_present( + source_account_alias, location, self.firewall_dict) + else: + return self.module.fail_json(msg="Unknown State: " + state) + + return self.module.exit_json( + changed=changed, + firewall_policy_id=firewall_policy_id) + + @staticmethod + def _get_policy_id_from_response(response): + """ + Method to parse out the policy id from creation response + :param response: response from firewall creation control + :return: policy_id: firewall policy id from creation call + """ + url = response.get('links')[0]['href'] + path = urlparse.urlparse(url).path + path_list = os.path.split(path) + policy_id = path_list[-1] + return policy_id + + 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") + + def _ensure_firewall_policy_is_present( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: (changed, firewall_policy, response) + changed: flag for if a change occurred + firewall_policy: policy that was changed + response: response from CLC API call + """ + changed = False + response = {} + firewall_policy_id = firewall_dict.get('firewall_policy_id') + + if firewall_policy_id is None: + if not self.module.check_mode: + response = self._create_firewall_policy( + source_account_alias, + location, + firewall_dict) + firewall_policy_id = self._get_policy_id_from_response( + response) + self._wait_for_requests_to_complete( + firewall_dict.get('wait'), + source_account_alias, + location, + firewall_policy_id) + changed = True + else: + get_before_response, success = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if not success: + return self.module.fail_json( + msg='Unable to find the firewall policy id : %s' % + firewall_policy_id) + changed = self._compare_get_request_with_dict( + get_before_response, + firewall_dict) + if not self.module.check_mode and changed: + response = self._update_firewall_policy( + source_account_alias, + location, + firewall_policy_id, + firewall_dict) + self._wait_for_requests_to_complete( + firewall_dict.get('wait'), + source_account_alias, + location, + firewall_policy_id) + return changed, firewall_policy_id, response + + def _ensure_firewall_policy_is_absent( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is removed if present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: firewall policy to delete + :return: (changed, firewall_policy_id, response) + changed: flag for if a change occurred + firewall_policy_id: policy that was changed + response: response from CLC API call + """ + changed = False + response = [] + firewall_policy_id = firewall_dict.get('firewall_policy_id') + result, success = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if success: + if not self.module.check_mode: + response = self._delete_firewall_policy( + source_account_alias, + location, + firewall_policy_id) + changed = True + return changed, firewall_policy_id, response + + def _create_firewall_policy( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: response from CLC API call + """ + payload = { + 'destinationAccount': firewall_dict.get('destination_account_alias'), + 'source': firewall_dict.get('source'), + 'destination': firewall_dict.get('destination'), + 'ports': firewall_dict.get('ports')} + try: + response = self.clc.v2.API.Call( + 'POST', '/v2-experimental/firewallPolicies/%s/%s' % + (source_account_alias, location), payload) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to successfully create firewall policy. %s" % + str(e.response_text)) + return response + + def _delete_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id): + """ + Deletes a given firewall policy for an account alias in a datacenter + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: firewall policy to delete + :return: response: response from CLC API call + """ + try: + response = self.clc.v2.API.Call( + 'DELETE', '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, location, firewall_policy_id)) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to successfully delete firewall policy. %s" % + str(e.response_text)) + return response + + def _update_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id, + firewall_dict): + """ + Updates a firewall policy for a given datacenter and account alias + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: firewall policy to delete + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: response: response from CLC API call + """ + try: + response = self.clc.v2.API.Call( + 'PUT', + '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, + location, + firewall_policy_id), + firewall_dict) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to successfully update firewall policy. %s" % + str(e.response_text)) + return response + + @staticmethod + def _compare_get_request_with_dict(response, firewall_dict): + """ + Helper method to compare the json response for getting the firewall policy with the request parameters + :param response: response from the get method + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: changed: Boolean that returns true if there are differences between the response parameters and the playbook parameters + """ + + changed = False + + response_dest_account_alias = response.get('destinationAccount') + response_enabled = response.get('enabled') + response_source = response.get('source') + response_dest = response.get('destination') + response_ports = response.get('ports') + + request_dest_account_alias = firewall_dict.get( + 'destination_account_alias') + request_enabled = firewall_dict.get('enabled') + if request_enabled is None: + request_enabled = True + request_source = firewall_dict.get('source') + request_dest = firewall_dict.get('destination') + request_ports = firewall_dict.get('ports') + + if ( + response_dest_account_alias and str(response_dest_account_alias) != str(request_dest_account_alias)) or ( + response_enabled != request_enabled) or ( + response_source and response_source != request_source) or ( + response_dest and response_dest != request_dest) or ( + response_ports and response_ports != request_ports): + changed = True + return changed + + def _get_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id): + """ + Get back details for a particular firewall policy + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: id of the firewall policy to get + :return: response from CLC API call + """ + response = [] + success = False + try: + response = self.clc.v2.API.Call( + 'GET', '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, location, firewall_policy_id)) + success = True + except: + pass + return response, success + + def _wait_for_requests_to_complete( + self, + wait, + source_account_alias, + location, + firewall_policy_id): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if wait: + response, success = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if response.get('status') == 'pending': + sleep(2) + self._wait_for_requests_to_complete( + wait, + source_account_alias, + location, + firewall_policy_id) + return None + + @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 + """ + module = AnsibleModule( + argument_spec=ClcFirewallPolicy._define_module_argument_spec(), + supports_check_mode=True) + + clc_firewall = ClcFirewallPolicy(module) + clc_firewall.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py new file mode 100644 index 00000000000..a4fd976d429 --- /dev/null +++ b/cloud/centurylink/clc_group.py @@ -0,0 +1,370 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_group +short_desciption: Create/delete Server Groups at Centurylink Cloud +description: + - Create or delete Server Groups at Centurylink Centurylink Cloud +options: + name: + description: + - The name of the Server Group + description: + description: + - A description of the Server Group + parent: + description: + - The parent group of the server group + location: + description: + - Datacenter to create the group in + state: + description: + - Whether to create or delete the group + default: present + choices: ['present', 'absent'] + +''' + +EXAMPLES = ''' + +# Create a Server Group + +--- +- name: Create Server Group + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create / Verify a Server Group at CenturyLink Cloud + clc_group: + name: 'My Cool Server Group' + parent: 'Default Group' + state: present + register: clc + + - name: debug + debug: var=clc + +# Delete a Server Group + +--- +- name: Delete Server Group + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete / Verify Absent a Server Group at CenturyLink Cloud + clc_group: + name: 'My Cool Server Group' + parent: 'Default Group' + state: absent + register: clc + + - name: debug + debug: var=clc + +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcGroup(object): + + clc = None + root_group = None + + 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='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + location = self.module.params.get('location') + group_name = self.module.params.get('name') + parent_name = self.module.params.get('parent') + group_description = self.module.params.get('description') + state = self.module.params.get('state') + + self._set_clc_credentials_from_env() + self.group_dict = self._get_group_tree_for_datacenter( + datacenter=location) + + if state == "absent": + changed, group, response = self._ensure_group_is_absent( + group_name=group_name, parent_name=parent_name) + + else: + changed, group, response = self._ensure_group_is_present( + group_name=group_name, parent_name=parent_name, group_description=group_description) + + + self.module.exit_json(changed=changed, group=group_name) + + # + # Functions to define the Ansible module and its arguments + # + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + parent=dict(default=None), + location=dict(default=None), + alias=dict(default=None), + custom_fields=dict(type='list', default=[]), + server_ids=dict(type='list', default=[]), + state=dict(default='present', choices=['present', 'absent'])) + + return argument_spec + + # + # Module Behavior Functions + # + + 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") + + def _ensure_group_is_absent(self, group_name, parent_name): + """ + Ensure that group_name is absent by deleting it if necessary + :param group_name: string - the name of the clc server group to delete + :param parent_name: string - the name of the parent group for group_name + :return: changed, group + """ + changed = False + group = [] + results = [] + + if self._group_exists(group_name=group_name, parent_name=parent_name): + if not self.module.check_mode: + group.append(group_name) + for g in group: + result = self._delete_group(group_name) + results.append(result) + changed = True + return changed, group, results + + def _delete_group(self, group_name): + """ + Delete the provided server group + :param group_name: string - the server group to delete + :return: none + """ + group, parent = self.group_dict.get(group_name) + response = group.Delete() + return response + + def _ensure_group_is_present( + self, + group_name, + parent_name, + group_description): + """ + Checks to see if a server group exists, creates it if it doesn't. + :param group_name: the name of the group to validate/create + :param parent_name: the name of the parent group for group_name + :param group_description: a short description of the server group (used when creating) + :return: (changed, group) - + changed: Boolean- whether a change was made, + group: A clc group object for the group + """ + assert self.root_group, "Implementation Error: Root Group not set" + parent = parent_name if parent_name is not None else self.root_group.name + description = group_description + changed = False + results = [] + groups = [] + group = group_name + + parent_exists = self._group_exists(group_name=parent, parent_name=None) + child_exists = self._group_exists(group_name=group_name, parent_name=parent) + + if parent_exists and child_exists: + group, parent = self.group_dict[group_name] + changed = False + elif parent_exists and not child_exists: + if not self.module.check_mode: + groups.append(group_name) + for g in groups: + group = self._create_group( + group=group, + parent=parent, + description=description) + results.append(group) + changed = True + else: + self.module.fail_json( + msg="parent group: " + + parent + + " does not exist") + + return changed, group, results + + def _create_group(self, group, parent, description): + """ + Create the provided server group + :param group: clc_sdk.Group - the group to create + :param parent: clc_sdk.Parent - the parent group for {group} + :param description: string - a text description of the group + :return: clc_sdk.Group - the created group + """ + + (parent, grandparent) = self.group_dict[parent] + return parent.Create(name=group, description=description) + + # + # Utility Functions + # + + def _group_exists(self, group_name, parent_name): + """ + Check to see if a group exists + :param group_name: string - the group to check + :param parent_name: string - the parent of group_name + :return: boolean - whether the group exists + """ + result = False + if group_name in self.group_dict: + (group, parent) = self.group_dict[group_name] + if parent_name is None or parent_name == parent.name: + result = True + return result + + def _get_group_tree_for_datacenter(self, datacenter=None, alias=None): + """ + Walk the tree of groups for a datacenter + :param datacenter: string - the datacenter to walk (ex: 'UC1') + :param alias: string - the account alias to search. Defaults to the current user's account + :return: a dictionary of groups and parents + """ + self.root_group = self.clc.v2.Datacenter( + location=datacenter).RootGroup() + return self._walk_groups_recursive( + parent_group=None, + child_group=self.root_group) + + def _walk_groups_recursive(self, parent_group, child_group): + """ + Walk a parent-child tree of groups, starting with the provided child group + :param parent_group: clc_sdk.Group - the parent group to start the walk + :param child_group: clc_sdk.Group - the child group to start the walk + :return: a dictionary of groups and parents + """ + result = {str(child_group): (child_group, parent_group)} + groups = child_group.Subgroups().groups + if len(groups) > 0: + for group in groups: + if group.type != 'default': + continue + + result.update(self._walk_groups_recursive(child_group, group)) + return result + + @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 + """ + module = AnsibleModule(argument_spec=ClcGroup._define_module_argument_spec(), supports_check_mode=True) + + clc_group = ClcGroup(module) + clc_group.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py new file mode 100644 index 00000000000..058954c687b --- /dev/null +++ b/cloud/centurylink/clc_loadbalancer.py @@ -0,0 +1,759 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: +short_desciption: Create, Delete shared loadbalancers in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete shared loadbalancers in CenturyLink Cloud. +options: +options: + name: + description: + - The name of the loadbalancer + required: True + description: + description: + - A description for your loadbalancer + alias: + description: + - The alias of your CLC Account + required: True + location: + description: + - The location of the datacenter your load balancer resides in + required: True + method: + description: + -The balancing method for this pool + default: roundRobin + choices: ['sticky', 'roundRobin'] + persistence: + description: + - The persistence method for this load balancer + default: standard + choices: ['standard', 'sticky'] + port: + description: + - Port to configure on the public-facing side of the load balancer pool + choices: [80, 443] + nodes: + description: + - A list of nodes that you want added to your load balancer pool + status: + description: + - The status of your loadbalancer + default: enabled + choices: ['enabled', 'disabled'] + state: + description: + - Whether to create or delete the load balancer pool + default: present + choices: ['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent'] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples +- name: Create Loadbalancer + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: present + +- name: Add node to an existing loadbalancer pool + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + state: nodes_present + +- name: Remove node from an existing loadbalancer pool + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + state: nodes_absent + +- name: Delete LoadbalancerPool + hosts: localhost + connection: local + tasks: + - name: Actually Delete things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: port_absent + +- name: Delete Loadbalancer + hosts: localhost + connection: local + tasks: + - name: Actually Delete things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: absent + +''' + +__version__ = '${version}' + +import requests +from time import sleep + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + +class ClcLoadBalancer(): + + clc = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.lb_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + + loadbalancer_name=self.module.params.get('name') + loadbalancer_alias=self.module.params.get('alias') + loadbalancer_location=self.module.params.get('location') + loadbalancer_description=self.module.params.get('description') + loadbalancer_port=self.module.params.get('port') + loadbalancer_method=self.module.params.get('method') + loadbalancer_persistence=self.module.params.get('persistence') + loadbalancer_nodes=self.module.params.get('nodes') + loadbalancer_status=self.module.params.get('status') + state=self.module.params.get('state') + + if loadbalancer_description == None: + loadbalancer_description = loadbalancer_name + + self._set_clc_credentials_from_env() + + self.lb_dict = self._get_loadbalancer_list(alias=loadbalancer_alias, location=loadbalancer_location) + + if state == 'present': + changed, result_lb, lb_id = self.ensure_loadbalancer_present(name=loadbalancer_name, + alias=loadbalancer_alias, + location=loadbalancer_location, + description=loadbalancer_description, + status=loadbalancer_status) + if loadbalancer_port: + changed, result_pool, pool_id = self.ensure_loadbalancerpool_present(lb_id=lb_id, + alias=loadbalancer_alias, + location=loadbalancer_location, + method=loadbalancer_method, + persistence=loadbalancer_persistence, + port=loadbalancer_port) + + if loadbalancer_nodes: + changed, result_nodes = self.ensure_lbpool_nodes_set(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes + ) + elif state == 'absent': + changed, result_lb = self.ensure_loadbalancer_absent(name=loadbalancer_name, + alias=loadbalancer_alias, + location=loadbalancer_location) + + elif state == 'port_absent': + changed, result_lb = self.ensure_loadbalancerpool_absent(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port) + + elif state == 'nodes_present': + changed, result_lb = self.ensure_lbpool_nodes_present(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes) + + elif state == 'nodes_absent': + changed, result_lb = self.ensure_lbpool_nodes_absent(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes) + + self.module.exit_json(changed=changed, loadbalancer=result_lb) + # + # Functions to define the Ansible module and its arguments + # + def ensure_loadbalancer_present(self,name,alias,location,description,status): + """ + Check for loadbalancer presence (available) + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :param description: Description of loadbalancer + :param status: Enabled / Disabled + :return: True / False + """ + changed = False + result = None + lb_id = self._loadbalancer_exists(name=name) + if lb_id: + result = name + changed = False + else: + if not self.module.check_mode: + result = self.create_loadbalancer(name=name, + alias=alias, + location=location, + description=description, + status=status) + lb_id = result.get('id') + changed = True + + return changed, result, lb_id + + def ensure_loadbalancerpool_present(self, lb_id, alias, location, method, persistence, port): + """ + Checks to see if a load balancer pool exists and creates one if it does not. + :param name: The loadbalancer name + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param method: the load balancing method + :param persistence: the load balancing persistence type + :param port: the port that the load balancer will listen on + :return: (changed, group, pool_id) - + changed: Boolean whether a change was made + result: The result from the CLC API call + pool_id: The string id of the pool + """ + changed = False + result = None + if not lb_id: + return False, None, None + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if not pool_id: + changed = True + if not self.module.check_mode: + result = self.create_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, method=method, persistence=persistence, port=port) + pool_id = result.get('id') + + else: + changed = False + result = port + + return changed, result, pool_id + + def ensure_loadbalancer_absent(self,name,alias,location): + """ + Check for loadbalancer presence (not available) + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :return: (changed, result) + changed: Boolean whether a change was made + result: The result from the CLC API Call + """ + changed = False + result = None + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + if not self.module.check_mode: + result = self.delete_loadbalancer(alias=alias, + location=location, + name=name) + changed = True + else: + result = name + changed = False + return changed, result + + def ensure_loadbalancerpool_absent(self, alias, location, name, port): + """ + Checks to see if a load balancer pool exists and deletes it if it does + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param loadbalancer: the name of the load balancer + :param port: the port that the load balancer will listen on + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = None + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + changed = True + if not self.module.check_mode: + result = self.delete_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, pool_id=pool_id) + else: + changed = False + result = "Pool doesn't exist" + else: + result = "LB Doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_set(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and set the nodes if any in the list doesn't exist + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: The list of nodes to be updated to the pool + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + result = {} + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + nodes_exist = self._loadbalancerpool_nodes_exists(alias=alias, + location=location, + port=port, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_check=nodes) + if not nodes_exist: + changed = True + result = self.set_loadbalancernodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_present(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: the list of nodes to be added + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + changed, result = self.add_lbpool_nodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_add=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_absent(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: the list of nodes to be removed + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + changed, result = self.remove_lbpool_nodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_remove=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def create_loadbalancer(self,name,alias,location,description,status): + """ + Create a loadbalancer w/ params + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :param description: Description for loadbalancer to be created + :param status: Enabled / Disabled + :return: Success / Failure + """ + result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s' % (alias, location), json.dumps({"name":name,"description":description,"status":status})) + sleep(1) + return result + + def create_loadbalancerpool(self, alias, location, lb_id, method, persistence, port): + """ + Creates a pool on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param method: the load balancing method + :param persistence: the load balancing persistence type + :param port: the port that the load balancer will listen on + :return: result: The result from the create API call + """ + result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id), json.dumps({"port":port, "method":method, "persistence":persistence})) + return result + + def delete_loadbalancer(self,alias,location,name): + """ + Delete CLC loadbalancer + :param alias: Alias for account + :param location: Datacenter + :param name: Name of the loadbalancer to delete + :return: 204 if successful else failure + """ + lb_id = self._get_loadbalancer_id(name=name) + result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s' % (alias, location, lb_id)) + return result + + def delete_loadbalancerpool(self, alias, location, lb_id, pool_id): + """ + Delete a pool on the provided load balancer + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :return: result: The result from the delete API call + """ + result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s' % (alias, location, lb_id, pool_id)) + return result + + def _get_loadbalancer_id(self, name): + """ + Retrieve unique ID of loadbalancer + :param name: Name of loadbalancer + :return: Unique ID of loadbalancer + """ + for lb in self.lb_dict: + if lb.get('name') == name: + id = lb.get('id') + return id + + def _get_loadbalancer_list(self, alias, location): + """ + Retrieve a list of loadbalancers + :param alias: Alias for account + :param location: Datacenter + :return: JSON data for all loadbalancers at datacenter + """ + return self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s' % (alias, location)) + + def _loadbalancer_exists(self, name): + """ + Verify a loadbalancer exists + :param name: Name of loadbalancer + :return: False or the ID of the existing loadbalancer + """ + result = False + + for lb in self.lb_dict: + if lb.get('name') == name: + result = lb.get('id') + return result + + def _loadbalancerpool_exists(self, alias, location, port, lb_id): + """ + Checks to see if a pool exists on the specified port on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param port: the port to check and see if it exists + :param lb_id: the id string of the provided load balancer + :return: result: The id string of the pool or False + """ + result = False + pool_list = self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id)) + for pool in pool_list: + if int(pool.get('port')) == int(port): + result = pool.get('id') + + return result + + def _loadbalancerpool_nodes_exists(self, alias, location, port, lb_id, pool_id, nodes_to_check): + """ + Checks to see if a set of nodes exists on the specified port on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param port: the port to check and see if it exists + :param lb_id: the id string of the provided load balancer + :param pool_id: the id string of the load balancer pool + :param nodes_to_check: the list of nodes to check for + :return: result: The id string of the pool or False + """ + result = False + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_check: + if not node.get('status'): + node['status'] = 'enabled' + if node in nodes: + result = True + else: + result = False + return result + + def set_loadbalancernodes(self, alias, location, lb_id, pool_id, nodes): + """ + Updates nodes to the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes: a list of dictionaries containing the nodes to set + :return: result: The result from the API call + """ + result = None + if not lb_id: + return result + if not self.module.check_mode: + result = self.clc.v2.API.Call('PUT', + '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' + % (alias, location, lb_id, pool_id), json.dumps(nodes)) + return result + + def add_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_add): + """ + Add nodes to the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes: a list of dictionaries containing the nodes to add + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = {} + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_add: + if not node.get('status'): + node['status'] = 'enabled' + if not node in nodes: + changed = True + nodes.append(node) + if changed == True and not self.module.check_mode: + result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) + return changed, result + + def remove_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_remove): + """ + Removes nodes from the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes: a list of dictionaries containing the nodes to remove + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = {} + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_remove: + if not node.get('status'): + node['status'] = 'enabled' + if node in nodes: + changed = True + nodes.remove(node) + if changed == True and not self.module.check_mode: + result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) + return changed, result + + def _get_lbpool_nodes(self, alias, location, lb_id, pool_id): + """ + Return the list of nodes available to the provided load balancer pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :return: result: The list of nodes + """ + result = self.clc.v2.API.Call('GET', + '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' + % (alias, location, lb_id, pool_id)) + return result + + @staticmethod + def define_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + location=dict(required=True, default=None), + alias=dict(required=True, default=None), + port=dict(choices=[80, 443]), + method=dict(choices=['leastConnection', 'roundRobin']), + persistence=dict(choices=['standard', 'sticky']), + nodes=dict(type='list', default=[]), + status=dict(default='enabled', choices=['enabled', 'disabled']), + state=dict(default='present', choices=['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent']), + wait=dict(type='bool', default=True) + ) + + return argument_spec + + # + # Module Behavior Functions + # + + 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 _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 + """ + module = AnsibleModule(argument_spec=ClcLoadBalancer.define_argument_spec(), + supports_check_mode=True) + clc_loadbalancer = ClcLoadBalancer(module) + clc_loadbalancer.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py new file mode 100644 index 00000000000..1a1e4d5b858 --- /dev/null +++ b/cloud/centurylink/clc_modify_server.py @@ -0,0 +1,710 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_modify_server +short_desciption: modify servers in CenturyLink Cloud. +description: + - An Ansible module to modify servers in CenturyLink Cloud. +options: + server_ids: + description: + - A list of server Ids to modify. + default: [] + required: True + aliases: [] + cpu: + description: + - How many CPUs to update on the server + default: None + required: False + aliases: [] + memory: + description: + - Memory in GB. + default: None + required: False + aliases: [] + anti_affinity_policy_id: + description: + - The anti affinity policy id to be set for a heperscale server. + This is mutually exclusive with 'anti_affinity_policy_name' + default: None + required: False + aliases: [] + anti_affinity_policy_name: + description: + - The anti affinity policy name to be set for a heperscale server. + This is mutually exclusive with 'anti_affinity_policy_id' + default: None + required: False + aliases: [] + alert_policy_id: + description: + - The alert policy id to be associated. + This is mutually exclusive with 'alert_policy_name' + default: None + required: False + aliases: [] + alert_policy_name: + description: + - The alert policy name to be associated. + This is mutually exclusive with 'alert_policy_id' + default: None + required: False + aliases: [] + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent'] + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: set the cpu count to 4 on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + cpu: 4 + state: present + +- name: set the memory to 8GB on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + memory: 8 + state: present + +- name: set the anti affinity policy on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + anti_affinity_policy_name: 'aa_policy' + state: present + +- name: set the alert policy on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + alert_policy_name: 'alert_policy' + state: present + +- name: set the memory to 16GB and cpu to 8 core on a lust if servers + clc_server: + server_ids: ['UC1ACCTTEST01','UC1ACCTTEST02'] + cpu: 8 + memory: 16 + state: present +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcModifyServer(): + 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='clc-python-sdk required for this module') + + 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 + """ + self._set_clc_credentials_from_env() + + p = self.module.params + + 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 modify: %s' % + server_ids) + + (changed, server_dict_array, new_server_ids) = ClcModifyServer._modify_servers( + module=self.module, clc=self.clc, server_ids=server_ids) + + self.module.exit_json( + changed=changed, + server_ids=new_server_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( + server_ids=dict(type='list', required=True), + state=dict(default='present', choices=['present', 'absent']), + cpu=dict(), + memory=dict(), + anti_affinity_policy_id=dict(), + anti_affinity_policy_name=dict(), + alert_policy_id=dict(), + alert_policy_name=dict(), + wait=dict(type='bool', default=True) + ) + mutually_exclusive = [ + ['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 _wait_for_requests(clc, requests, servers, wait): + """ + Block until server provisioning requests are completed. + :param clc: the clc-sdk instance to use + :param requests: a list of clc-sdk.Request instances + :param servers: a list of servers to refresh + :param wait: a boolean on whether to block or not. This function is skipped if True + :return: none + """ + if wait: + # Requests.WaitUntilComplete() returns the count of failed requests + failed_requests_count = sum( + [request.WaitUntilComplete() for request in requests]) + + if failed_requests_count > 0: + raise clc + else: + ClcModifyServer._refresh_servers(servers) + + @staticmethod + def _refresh_servers(servers): + """ + Loop through a list of servers and refresh them + :param servers: list of clc-sdk.Server instances to refresh + :return: none + """ + for server in servers: + server.Refresh() + + @staticmethod + def _modify_servers(module, clc, server_ids): + """ + modify the servers configuration on the provided list + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :param server_ids: list of servers to modify + :return: a list of dictionaries with server information about the servers that were modified + """ + p = module.params + wait = p.get('wait') + state = p.get('state') + server_params = { + 'cpu': p.get('cpu'), + 'memory': p.get('memory'), + 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), + 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), + 'alert_policy_id': p.get('alert_policy_id'), + 'alert_policy_name': p.get('alert_policy_name'), + } + changed = False + server_changed = False + aa_changed = False + ap_changed = False + server_dict_array = [] + result_server_ids = [] + requests = [] + + 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() + if state == 'present': + for server in servers: + server_changed, server_result, changed_servers = ClcModifyServer._ensure_server_config( + clc, module, None, server, server_params) + if server_result: + requests.append(server_result) + aa_changed, changed_servers = ClcModifyServer._ensure_aa_policy( + clc, module, None, server, server_params) + ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_present( + clc, module, None, server, server_params) + elif state == 'absent': + for server in servers: + ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_absent( + clc, module, None, server, server_params) + if server_changed or aa_changed or ap_changed: + changed = True + + if wait: + for r in requests: + r.WaitUntilComplete() + for server in changed_servers: + server.Refresh() + + for server in changed_servers: + server_dict_array.append(server.data) + result_server_ids.append(server.id) + + return changed, server_dict_array, result_server_ids + + @staticmethod + def _ensure_server_config( + clc, module, alias, server, server_params): + """ + ensures the server is updated with the provided cpu and memory + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + cpu = server_params.get('cpu') + memory = server_params.get('memory') + changed = False + result = None + changed_servers = [] + + if not cpu: + cpu = server.cpu + if not memory: + memory = server.memory + if memory != server.memory or cpu != server.cpu: + changed_servers.append(server) + result = ClcModifyServer._modify_clc_server( + clc, + module, + None, + server.id, + cpu, + memory) + changed = True + return changed, result, changed_servers + + @staticmethod + def _modify_clc_server(clc, module, acct_alias, server_id, cpu, memory): + """ + Modify the memory or CPU on a clc server. This function is not yet implemented. + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the clc account alias to look up the server + :param server_id: id of the server to modify + :param cpu: the new cpu value + :param memory: the new memory value + :return: the result of CLC API call + """ + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + if not server_id: + return module.fail_json( + msg='server_id must be provided to modify the server') + + result = None + + if not module.check_mode: + + # Update the server configuation + job_obj = clc.v2.API.Call('PATCH', + 'servers/%s/%s' % (acct_alias, + server_id), + json.dumps([{"op": "set", + "member": "memory", + "value": memory}, + {"op": "set", + "member": "cpu", + "value": cpu}])) + result = clc.v2.Requests(job_obj) + return result + + @staticmethod + def _ensure_aa_policy( + clc, module, acct_alias, server, server_params): + """ + ensures the server is updated with the provided anti affinity policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + changed_servers = [] + + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + + aa_policy_id = server_params.get('anti_affinity_policy_id') + aa_policy_name = server_params.get('anti_affinity_policy_name') + if not aa_policy_id and aa_policy_name: + aa_policy_id = ClcModifyServer._get_aa_policy_id_by_name( + clc, + module, + acct_alias, + aa_policy_name) + current_aa_policy_id = ClcModifyServer._get_aa_policy_id_of_server( + clc, + module, + acct_alias, + server.id) + + if aa_policy_id and aa_policy_id != current_aa_policy_id: + if server not in changed_servers: + changed_servers.append(server) + ClcModifyServer._modify_aa_policy( + clc, + module, + acct_alias, + server.id, + aa_policy_id) + changed = True + return changed, changed_servers + + @staticmethod + def _modify_aa_policy(clc, module, acct_alias, server_id, aa_policy_id): + """ + modifies the anti affinity policy of the CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param aa_policy_id: the anti affinity policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + result = clc.v2.API.Call('PUT', + 'servers/%s/%s/antiAffinityPolicy' % ( + acct_alias, + server_id), + json.dumps({"id": aa_policy_id})) + return result + + @staticmethod + def _get_aa_policy_id_by_name(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 + aa_policies = clc.v2.API.Call(method='GET', + url='antiAffinityPolicies/%s' % (alias)) + 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='mutiple anti affinity policies were found with policy name : %s' % + (aa_policy_name)) + if not aa_policy_id: + return module.fail_json( + msg='No anti affinity policy was found with policy name : %s' % + (aa_policy_name)) + return aa_policy_id + + @staticmethod + def _get_aa_policy_id_of_server(clc, module, alias, server_id): + """ + retrieves the anti affinity policy id of the server based on the CLC server id + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param server_id: the CLC server id + :return: aa_policy_id: The anti affinity policy id + """ + aa_policy_id = None + try: + result = clc.v2.API.Call( + method='GET', url='servers/%s/%s/antiAffinityPolicy' % + (alias, server_id)) + aa_policy_id = result.get('id') + except APIFailedResponse as e: + if e.response_status_code != 404: + raise e + return aa_policy_id + + @staticmethod + def _ensure_alert_policy_present( + clc, module, acct_alias, server, server_params): + """ + ensures the server is updated with the provided alert policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + changed_servers = [] + + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + + alert_policy_id = server_params.get('alert_policy_id') + alert_policy_name = server_params.get('alert_policy_name') + if not alert_policy_id and alert_policy_name: + alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( + clc, + module, + acct_alias, + alert_policy_name) + if alert_policy_id and not ClcModifyServer._alert_policy_exists(server, alert_policy_id): + if server not in changed_servers: + changed_servers.append(server) + ClcModifyServer._add_alert_policy_to_server( + clc, + module, + acct_alias, + server.id, + alert_policy_id) + changed = True + return changed, changed_servers + + @staticmethod + def _ensure_alert_policy_absent( + clc, module, acct_alias, server, server_params): + """ + ensures the alert policy is removed from the server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = None + changed_servers = [] + + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + + alert_policy_id = server_params.get('alert_policy_id') + alert_policy_name = server_params.get('alert_policy_name') + if not alert_policy_id and alert_policy_name: + alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( + clc, + module, + acct_alias, + alert_policy_name) + + if alert_policy_id and ClcModifyServer._alert_policy_exists(server, alert_policy_id): + if server not in changed_servers: + changed_servers.append(server) + ClcModifyServer._remove_alert_policy_to_server( + clc, + module, + acct_alias, + server.id, + alert_policy_id) + changed = True + return changed, changed_servers + + @staticmethod + def _add_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): + """ + add the alert policy to CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param alert_policy_id: the alert policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('POST', + 'servers/%s/%s/alertPolicies' % ( + acct_alias, + server_id), + json.dumps({"id": alert_policy_id})) + except clc.APIFailedResponse as e: + return module.fail_json( + msg='Unable to set alert policy to the server : %s. %s' % (server_id, str(e.response_text))) + return result + + @staticmethod + def _remove_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): + """ + remove the alert policy to the CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param alert_policy_id: the alert policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('DELETE', + 'servers/%s/%s/alertPolicies/%s' + % (acct_alias, server_id, alert_policy_id)) + except clc.APIFailedResponse as e: + return module.fail_json( + msg='Unable to remove alert policy to the server : %s. %s' % (server_id, str(e.response_text))) + return result + + @staticmethod + def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): + """ + retrieves the alert 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 alert_policy_name: the alert policy name + :return: alert_policy_id: The alert policy id + """ + alert_policy_id = None + alert_policies = clc.v2.API.Call(method='GET', + url='alertPolicies/%s' % (alias)) + for alert_policy in alert_policies.get('items'): + if alert_policy.get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = alert_policy.get('id') + else: + return module.fail_json( + msg='mutiple alert policies were found with policy name : %s' % + (alert_policy_name)) + return alert_policy_id + + @staticmethod + def _alert_policy_exists(server, alert_policy_id): + """ + Checks if the alert policy exists for the server + :param server: the clc server object + :param alert_policy_id: the alert policy + :return: True: if the given alert policy id associated to the server, False otherwise + """ + result = False + alert_policies = server.alertPolicies + if alert_policies: + for alert_policy in alert_policies: + if alert_policy.get('id') == alert_policy_id: + result = True + return result + + @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 = ClcModifyServer._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_modify_server = ClcModifyServer(module) + clc_modify_server.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py new file mode 100644 index 00000000000..2e525a51455 --- /dev/null +++ b/cloud/centurylink/clc_publicip.py @@ -0,0 +1,316 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_publicip +short_description: Add and Delete public ips on servers in CenturyLink Cloud. +description: + - An Ansible module to add or delete public ip addresses on an existing server or servers in CenturyLink Cloud. +options: + protocol: + descirption: + - The protocol that the public IP will listen for. + default: TCP + required: False + ports: + description: + - A list of ports to expose. + required: True + server_ids: + description: + - A list of servers to create public ips on. + required: True + state: + description: + - Determine wheteher to create or delete public IPs. If present module will not create a second public ip if one + already exists. + default: present + choices: ['present', 'absent'] + required: False + wait: + description: + - Whether to wait for the tasks to finish before returning. + choices: [ True, False ] + default: True + required: False +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Add Public IP to Server + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create Public IP For Servers + clc_publicip: + protocol: 'TCP' + ports: + - 80 + server_ids: + - UC1ACCTSRVR01 + - UC1ACCTSRVR02 + state: present + register: clc + + - name: debug + debug: var=clc + +- name: Delete Public IP from Server + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create Public IP For Servers + clc_publicip: + server_ids: + - UC1ACCTSRVR01 + - UC1ACCTSRVR02 + state: absent + register: clc + + - name: debug + debug: var=clc +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcPublicIp(object): + clc = clc_sdk + module = None + group_dict = {} + + def __init__(self, module): + """ + Construct module + """ + self.module = module + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :param params: dictionary of module parameters + :return: Returns with either an exit_json or fail_json + """ + self._set_clc_credentials_from_env() + params = self.module.params + server_ids = params['server_ids'] + ports = params['ports'] + protocol = params['protocol'] + state = params['state'] + requests = [] + chagned_server_ids = [] + changed = False + + if state == 'present': + changed, chagned_server_ids, requests = self.ensure_public_ip_present( + server_ids=server_ids, protocol=protocol, ports=ports) + elif state == 'absent': + changed, chagned_server_ids, requests = self.ensure_public_ip_absent( + server_ids=server_ids) + else: + return self.module.fail_json(msg="Unknown State: " + state) + self._wait_for_requests_to_complete(requests) + return self.module.exit_json(changed=changed, + server_ids=chagned_server_ids) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + protocol=dict(default='TCP'), + ports=dict(type='list'), + wait=dict(type='bool', default=True), + state=dict(default='present', choices=['present', 'absent']), + ) + return argument_spec + + def ensure_public_ip_present(self, server_ids, protocol, ports): + """ + Ensures the given server ids having the public ip available + :param server_ids: the list of server ids + :param protocol: the ip protocol + :param ports: the list of ports to expose + :return: (changed, changed_server_ids, results) + changed: A flag indicating if there is any change + changed_server_ids : the list of server ids that are changed + results: The result list from clc public ip call + """ + changed = False + results = [] + changed_server_ids = [] + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.PublicIPs().public_ips) == 0] + ports_to_expose = [{'protocol': protocol, 'port': port} + for port in ports] + for server in servers_to_change: + if not self.module.check_mode: + result = server.PublicIPs().Add(ports_to_expose) + results.append(result) + changed_server_ids.append(server.id) + changed = True + return changed, changed_server_ids, results + + def ensure_public_ip_absent(self, server_ids): + """ + Ensures the given server ids having the public ip removed if there is any + :param server_ids: the list of server ids + :return: (changed, changed_server_ids, results) + changed: A flag indicating if there is any change + changed_server_ids : the list of server ids that are changed + results: The result list from clc public ip call + """ + changed = False + results = [] + changed_server_ids = [] + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.PublicIPs().public_ips) > 0] + ips_to_delete = [] + for server in servers_to_change: + for ip_address in server.PublicIPs().public_ips: + ips_to_delete.append(ip_address) + for server in servers_to_change: + if not self.module.check_mode: + for ip in ips_to_delete: + result = ip.Delete() + results.append(result) + changed_server_ids.append(server.id) + changed = True + return changed, changed_server_ids, results + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process public ip request') + + 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") + + def _get_servers_from_clc(self, server_ids, message): + """ + Gets list of servers form CLC api + """ + try: + return self.clc.v2.Servers(server_ids).servers + except CLCException as exception: + self.module.fail_json(msg=message + ': %s' % exception) + + @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 + """ + module = AnsibleModule( + argument_spec=ClcPublicIp._define_module_argument_spec(), + supports_check_mode=True + ) + clc_public_ip = ClcPublicIp(module) + clc_public_ip.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py new file mode 100644 index 00000000000..e102cd21f47 --- /dev/null +++ b/cloud/centurylink/clc_server.py @@ -0,0 +1,1323 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_server +short_desciption: 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: + - Specify additional disks for the server + required: False + default: None + aliases: [] + add_public_ip: + description: + - Whether to add a public ip to the server + required: False + default: False + choices: [False, True] + aliases: [] + alias: + description: + - The account alias to provision the servers under. + default: + - The default alias for the API credentials + required: False + default: None + aliases: [] + anti_affinity_policy_id: + description: + - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_name'. + required: False + default: None + aliases: [] + anti_affinity_policy_name: + description: + - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_id'. + required: False + default: None + aliases: [] + alert_policy_id: + description: + - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_name'. + required: False + default: None + aliases: [] + alert_policy_name: + description: + - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_id'. + required: False + default: None + aliases: [] + + count: + description: + - The number of servers to build (mutually exclusive with exact_count) + default: None + aliases: [] + count_group: + description: + - Required when exact_count is specified. The Server Group use to determine how many severs to deploy. + default: 1 + required: False + aliases: [] + cpu: + description: + - How many CPUs to provision on the server + default: None + required: False + aliases: [] + cpu_autoscale_policy_id: + description: + - The autoscale policy to assign to the server. + default: None + required: False + aliases: [] + custom_fields: + description: + - A dictionary of custom fields to set on the server. + default: [] + required: False + aliases: [] + description: + description: + - The description to set for the server. + default: None + required: False + aliases: [] + 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. + default: None + required: False + aliases: [] + group: + description: + - The Server Group to create servers under. + default: 'Default Group' + required: False + aliases: [] + ip_address: + description: + - The IP Address for the server. One is assigned if not provided. + default: None + required: False + aliases: [] + location: + description: + - The Datacenter to create servers in. + default: None + required: False + aliases: [] + managed_os: + description: + - Whether to create the server as 'Managed' or not. + default: False + required: False + choices: [True, False] + aliases: [] + memory: + description: + - Memory in GB. + default: 1 + required: False + aliases: [] + name: + description: + - A 1 to 6 character identifier to use for the server. + default: None + required: False + aliases: [] + network_id: + description: + - The network UUID on which to create servers. + default: None + required: False + aliases: [] + packages: + description: + - Blueprints to run on the server after its created. + default: [] + required: False + aliases: [] + password: + description: + - Password for the administrator user + default: None + required: False + aliases: [] + primary_dns: + description: + - Primary DNS used by the server. + default: None + required: False + aliases: [] + public_ip_protocol: + description: + - The protocol to use for the public ip if add_public_ip is set to True. + default: 'TCP' + required: False + aliases: [] + public_ip_ports: + description: + - A list of ports to allow on the firewall to thes servers public ip, if add_public_ip is set to True. + default: [] + required: False + aliases: [] + secondary_dns: + description: + - Secondary DNS used by the server. + default: None + required: False + aliases: [] + server_ids: + description: + - Required for started, stopped, and absent states. A list of server Ids to insure are started, stopped, or absent. + default: [] + required: False + aliases: [] + source_server_password: + description: + - The password for the source server if a clone is specified. + default: None + required: False + aliases: [] + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent', 'started', 'stopped'] + aliases: [] + storage_type: + description: + - The type of storage to attach to the server. + default: 'standard' + required: False + choices: ['standard', 'hyperscale'] + aliases: [] + template: + description: + - The template to use for server creation. Will search for a template if a partial string is provided. + default: None + required: false + aliases: [] + ttl: + description: + - The time to live for the server in seconds. The server will be deleted when this time expires. + default: None + required: False + aliases: [] + type: + description: + - The type of server to create. + default: 'standard' + required: False + choices: ['standard', 'hyperscale'] + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +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 + clc_server: + name: test + template: ubuntu-14-64 + count: 1 + group: 'Default Group' + state: present + +- name: Ensure 'Default Group' has exactly 5 servers + clc_server: + name: test + template: ubuntu-14-64 + exact_count: 5 + count_group: 'Default Group' + group: 'Default Group' + +- name: Stop a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: stopped + +- name: Start a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: started + +- name: Delete a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: absent +''' + +__version__ = '${version}' + +import requests +from time import sleep + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +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='clc-python-sdk required for this module') + + 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 + """ + self._set_clc_credentials_from_env() + + self.module.params = ClcServer._validate_module_params(self.clc, + self.module) + p = self.module.params + state = p.get('state') + + # + # Handle each state + # + + if state == 'absent': + server_ids = p['server_ids'] + if not isinstance(server_ids, list): + 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) = ClcServer._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): + 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) = ClcServer._startstop_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'): + 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, + changed) = ClcServer._create_servers(self.module, + self.clc) + else: + (server_dict_array, + new_server_ids, + changed) = ClcServer._enforce_count(self.module, + self.clc) + + self.module.exit_json( + changed=changed, + server_ids=new_server_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(default=None), + cpu=dict(default=1), + memory=dict(default='1'), + alias=dict(default=None), + password=dict(default=None), + ip_address=dict(default=None), + storage_type=dict(default='standard'), + type=dict( + default='standard', + choices=[ + 'standard', + 'hyperscale']), + primary_dns=dict(default=None), + secondary_dns=dict(default=None), + additional_disks=dict(type='list', default=[]), + custom_fields=dict(type='list', default=[]), + ttl=dict(default=None), + managed_os=dict(type='bool', default=False), + description=dict(default=None), + source_server_password=dict(default=None), + cpu_autoscale_policy_id=dict(default=None), + anti_affinity_policy_id=dict(default=None), + anti_affinity_policy_name=dict(default=None), + alert_policy_id=dict(default=None), + alert_policy_name=dict(default=None), + packages=dict(type='list', default=[]), + state=dict( + default='present', + choices=[ + 'present', + 'absent', + 'started', + 'stopped']), + count=dict(type='int', default='1'), + exact_count=dict(type='int', default=None), + count_group=dict(), + server_ids=dict(type='list'), + add_public_ip=dict(type='bool', default=False), + public_ip_protocol=dict(default='TCP'), + public_ip_ports=dict(type='list'), + 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) + + 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: + datacenter = clc.v2.Datacenter(location) + return datacenter + except CLCException: + module.fail_json(msg=str("Unable to find location: " + 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: + alias = clc.v2.Account.GetAlias() + 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("Cannot 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( + "Cannot 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') + 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 type == "standard" and storage_type not in ( + "standard", "premium"): + module.fail_json( + msg=str("Standard VMs must have storage_type = 'standard' or 'premium'")) + + if type == "hyperscale" and storage_type != "hyperscale": + module.fail_json( + msg=str("Hyperscale VMs must have storage_type = 'hyperscale'")) + + @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: + 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') + result = None + + if state == 'present': + 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 + except CLCException: + module.fail_json( + msg=str( + "Unable to find a network in location: " + + datacenter.id)) + + return network_id + + @staticmethod + def _create_servers(module, clc, override_count=None): + """ + Create New Servers + :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 + requests = [] + servers = [] + server_dict_array = [] + created_server_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') + wait = p.get('wait') + + 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'), + 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), + 'packages': p.get('packages') + } + + count = override_count if override_count else p.get('count') + + changed = False if count == 0 else True + + if changed: + for i in range(0, count): + if not module.check_mode: + req = ClcServer._create_clc_server(clc=clc, + module=module, + server_params=params) + server = req.requests[0].Server() + requests.append(req) + servers.append(server) + + ClcServer._wait_for_requests(clc, requests, servers, wait) + + ClcServer._add_public_ip_to_servers( + should_add_public_ip=add_public_ip, + servers=servers, + public_ip_protocol=public_ip_protocol, + public_ip_ports=public_ip_ports, + wait=wait) + ClcServer._add_alert_policy_to_servers(clc=clc, + module=module, + servers=servers) + + for server in servers: + # 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]) + + server_dict_array.append(server.data) + created_server_ids.append(server.id) + + return server_dict_array, created_server_ids, changed + + @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 + """ + name = module.params.get('name') + state = module.params.get('state') + + if state == 'present' and (len(name) < 1 or len(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")) + +# +# Functions to execute the module's behaviors +# (called from main()) +# + + @staticmethod + def _enforce_count(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_server_ids = None + changed = False + count_group = p.get('count_group') + datacenter = ClcServer._find_datacenter(clc, module) + exact_count = p.get('exact_count') + server_dict_array = [] + + # 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: + 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: + changed = True + to_create = exact_count - len(running_servers) + server_dict_array, changed_server_ids, changed \ + = ClcServer._create_servers(module, clc, override_count=to_create) + + for server in server_dict_array: + running_servers.append(server) + + elif len(running_servers) > exact_count: + changed = True + 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, changed + + @staticmethod + def _wait_for_requests(clc, requests, servers, wait): + """ + Block until server provisioning requests are completed. + :param clc: the clc-sdk instance to use + :param requests: a list of clc-sdk.Request instances + :param servers: a list of servers to refresh + :param wait: a boolean on whether to block or not. This function is skipped if True + :return: none + """ + if wait: + # Requests.WaitUntilComplete() returns the count of failed requests + failed_requests_count = sum( + [request.WaitUntilComplete() for request in requests]) + + if failed_requests_count > 0: + raise clc + else: + ClcServer._refresh_servers(servers) + + @staticmethod + def _refresh_servers(servers): + """ + Loop through a list of servers and refresh them + :param servers: list of clc-sdk.Server instances to refresh + :return: none + """ + for server in servers: + server.Refresh() + + @staticmethod + def _add_public_ip_to_servers( + should_add_public_ip, + servers, + public_ip_protocol, + public_ip_ports, + wait): + """ + Create a public IP for servers + :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 + :param wait: boolean - whether to block until the provisioning requests complete + :return: none + """ + + if should_add_public_ip: + ports_lst = [] + requests = [] + + for port in public_ip_ports: + ports_lst.append( + {'protocol': public_ip_protocol, 'port': port}) + + for server in servers: + requests.append(server.PublicIPs().Add(ports_lst)) + + if wait: + for r in requests: + r.WaitUntilComplete() + + @staticmethod + def _add_alert_policy_to_servers(clc, module, servers): + """ + Associate an 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: none + """ + p = module.params + alert_policy_id = p.get('alert_policy_id') + alert_policy_name = p.get('alert_policy_name') + alias = p.get('alias') + if not alert_policy_id and alert_policy_name: + 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)) + for server in servers: + ClcServer._add_alert_policy_to_server( + clc=clc, + module=module, + alias=alias, + server_id=server.id, + alert_policy_id=alert_policy_id) + + @staticmethod + def _add_alert_policy_to_server(clc, module, alias, server_id, alert_policy_id): + """ + Associate an alert policy to a clc server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the clc account alias + :param serverid: 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 clc.APIFailedResponse as e: + return module.fail_json( + msg='Failed to associate alert policy to the server : %s with Error %s' + % (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: 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='mutiple 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 + """ + # Whether to wait for termination to complete before returning + p = module.params + wait = p.get('wait') + terminated_server_ids = [] + server_dict_array = [] + requests = [] + + changed = False + if not isinstance(server_ids, list) or len(server_ids) < 1: + module.fail_json( + msg='server_ids should be a list of servers, aborting') + + servers = clc.v2.Servers(server_ids).Servers() + changed = True + + for server in servers: + if not module.check_mode: + requests.append(server.Delete()) + + if wait: + for r in requests: + r.WaitUntilComplete() + + for server in servers: + terminated_server_ids.append(server.id) + + return changed, server_dict_array, terminated_server_ids + + @staticmethod + def _startstop_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 + wait = p.get('wait') + state = p.get('state') + changed = False + changed_servers = [] + server_dict_array = [] + result_server_ids = [] + requests = [] + + if not isinstance(server_ids, list) or len(server_ids) < 1: + 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: + requests.append( + ClcServer._change_server_power_state( + module, + server, + state)) + changed = True + + if wait: + for r in requests: + r.WaitUntilComplete() + for server in changed_servers: + server.Refresh() + + for server in changed_servers: + 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: + result = server.PowerOff() + except: + module.fail_json( + msg='Unable to change state for server {0}'.format( + server.id)) + return result + 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 + """ + result = None + if not lookup_group: + lookup_group = module.params.get('group') + try: + return datacenter.Groups().Get(lookup_group) + except: + 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: + 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 server_params: a dictionary of params to use to create the servers + :return: clc-sdk.Request object linked to the queued server request + """ + + aa_policy_id = server_params.get('anti_affinity_policy_id') + aa_policy_name = server_params.get('anti_affinity_policy_name') + if not aa_policy_id and aa_policy_name: + aa_policy_id = ClcServer._get_anti_affinity_policy_id( + clc, + module, + server_params.get('alias'), + aa_policy_name) + + 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': aa_policy_id, + 'customFields': server_params.get('custom_fields'), + 'additionalDisks': server_params.get('additional_disks'), + 'ttl': server_params.get('ttl'), + 'packages': server_params.get('packages')})) + + result = clc.v2.Requests(res) + + # + # 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 + aa_policies = clc.v2.API.Call(method='GET', + url='antiAffinityPolicies/%s' % (alias)) + 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='mutiple anti affinity policies were found with policy name : %s' % + (aa_policy_name)) + if not aa_policy_id: + return module.fail_json( + msg='No anti affinity policy was 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, backout=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 svr_uuid: UUID of the server + :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: + 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)) + return + if retries == 0: + module.fail_json( + msg='Unable to reach the CLC API after 5 attempts') + return + + sleep(backout) + backout = backout * 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() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py new file mode 100644 index 00000000000..9ca1474f248 --- /dev/null +++ b/cloud/centurylink/clc_server_snapshot.py @@ -0,0 +1,341 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_server +short_desciption: Create, Delete and Restore server snapshots in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete and Restore server snapshots in CenturyLink Cloud. +options: + server_ids: + description: + - A list of server Ids to snapshot. + default: [] + required: True + aliases: [] + expiration_days: + description: + - The number of days to keep the server snapshot before it expires. + default: 7 + required: False + aliases: [] + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent', 'restore'] + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Create server snapshot + clc_server_snapshot: + server_ids: + - UC1WFSDTEST01 + - UC1WFSDTEST02 + expiration_days: 10 + wait: True + state: present + +- name: Restore server snapshot + clc_server_snapshot: + server_ids: + - UC1WFSDTEST01 + - UC1WFSDTEST02 + wait: True + state: restore + +- name: Delete server snapshot + clc_server_snapshot: + server_ids: + - UC1WFSDTEST01 + - UC1WFSDTEST02 + wait: True + state: absent +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + clc_found = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcSnapshot(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + 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 + """ + p = self.module.params + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + server_ids = p['server_ids'] + expiration_days = p['expiration_days'] + state = p['state'] + + if not server_ids: + return self.module.fail_json(msg='List of Server ids are required') + + self._set_clc_credentials_from_env() + if state == 'present': + changed, requests, changed_servers = self.ensure_server_snapshot_present(server_ids=server_ids, + expiration_days=expiration_days) + elif state == 'absent': + changed, requests, changed_servers = self.ensure_server_snapshot_absent( + server_ids=server_ids) + elif state == 'restore': + changed, requests, changed_servers = self.ensure_server_snapshot_restore( + server_ids=server_ids) + else: + return self.module.fail_json(msg="Unknown State: " + state) + + self._wait_for_requests_to_complete(requests) + return self.module.exit_json( + changed=changed, + server_ids=changed_servers) + + def ensure_server_snapshot_present(self, server_ids, expiration_days): + """ + Ensures the given set of server_ids have the snapshots created + :param server_ids: The list of server_ids to create the snapshot + :param expiration_days: The number of days to keep the snapshot + :return: (changed, result, changed_servers) + changed: A flag indicating whether any change was made + result: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + result = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) == 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + res = server.CreateSnapshot( + delete_existing=True, + expiration_days=expiration_days) + result.append(res) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, result, changed_servers + + def ensure_server_snapshot_absent(self, server_ids): + """ + Ensures the given set of server_ids have the snapshots removed + :param server_ids: The list of server_ids to delete the snapshot + :return: (changed, result, changed_servers) + changed: A flag indicating whether any change was made + result: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + result = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) > 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + res = server.DeleteSnapshot() + result.append(res) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, result, changed_servers + + def ensure_server_snapshot_restore(self, server_ids): + """ + Ensures the given set of server_ids have the snapshots restored + :param server_ids: The list of server_ids to delete the snapshot + :return: (changed, result, changed_servers) + changed: A flag indicating whether any change was made + result: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + result = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) > 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + res = server.RestoreSnapshot() + result.append(res) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, result, changed_servers + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process server snapshot request') + + @staticmethod + def define_argument_spec(): + """ + This function defnines the dictionary object required for + package module + :return: the package dictionary object + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + expiration_days=dict(default=7), + wait=dict(default=True), + state=dict( + default='present', + choices=[ + 'present', + 'absent', + 'restore']), + ) + return argument_spec + + def _get_servers_from_clc(self, server_list, message): + """ + Internal function to fetch list of CLC server objects from a list of server ids + :param the list server ids + :return the list of CLC server objects + """ + try: + return self.clc.v2.Servers(server_list).servers + except CLCException as ex: + return self.module.fail_json(msg=message + ': %s' % ex) + + 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 _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(): + """ + Main function + :return: None + """ + module = AnsibleModule( + argument_spec=ClcSnapshot.define_argument_spec(), + supports_check_mode=True + ) + clc_snapshot = ClcSnapshot(module) + clc_snapshot.process_request() + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main()