2020-03-09 10:11:07 +01:00
|
|
|
#!/usr/bin/python
|
2021-08-08 10:40:22 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Copyright (c) 2015 CenturyLink
|
|
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
|
|
|
|
DOCUMENTATION = '''
|
|
|
|
module: clc_group
|
|
|
|
short_description: 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
|
2020-11-23 21:47:52 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
required: True
|
|
|
|
description:
|
|
|
|
description:
|
|
|
|
- A description of the Server Group
|
2020-11-23 21:47:52 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
required: False
|
|
|
|
parent:
|
|
|
|
description:
|
|
|
|
- The parent group of the server group. If parent is not provided, it creates the group at top level.
|
2020-11-23 21:47:52 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
required: False
|
|
|
|
location:
|
|
|
|
description:
|
|
|
|
- Datacenter to create the group in. If location is not provided, the group gets created in the default datacenter
|
|
|
|
associated with the account
|
2020-11-23 21:47:52 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
required: False
|
|
|
|
state:
|
|
|
|
description:
|
|
|
|
- Whether to create or delete the group
|
2020-11-23 21:47:52 +01:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
default: present
|
|
|
|
choices: ['present', 'absent']
|
|
|
|
wait:
|
|
|
|
description:
|
|
|
|
- Whether to wait for the tasks to finish before returning.
|
|
|
|
type: bool
|
|
|
|
default: True
|
|
|
|
required: False
|
|
|
|
requirements:
|
|
|
|
- python = 2.7
|
|
|
|
- requests >= 2.5.0
|
|
|
|
- clc-sdk
|
|
|
|
author: "CLC Runner (@clc-runner)"
|
|
|
|
notes:
|
|
|
|
- To use this module, it is required to set the below environment variables which enables access to the
|
|
|
|
Centurylink Cloud
|
|
|
|
- CLC_V2_API_USERNAME, the account login id for the centurylink cloud
|
|
|
|
- CLC_V2_API_PASSWORD, the account password for the centurylink cloud
|
|
|
|
- Alternatively, the module accepts the API token and account alias. The API token can be generated using the
|
|
|
|
CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login
|
|
|
|
- CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login
|
|
|
|
- CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud
|
|
|
|
- Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment.
|
|
|
|
'''
|
|
|
|
|
|
|
|
EXAMPLES = '''
|
|
|
|
|
|
|
|
# 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
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.clc_group:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: My Cool Server Group
|
|
|
|
parent: Default Group
|
|
|
|
state: present
|
|
|
|
register: clc
|
|
|
|
|
2020-05-16 15:07:51 +02:00
|
|
|
- name: Debug
|
2020-07-14 17:28:08 +02:00
|
|
|
ansible.builtin.debug:
|
2020-03-09 10:11:07 +01:00
|
|
|
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
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.clc_group:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: My Cool Server Group
|
|
|
|
parent: Default Group
|
|
|
|
state: absent
|
|
|
|
register: clc
|
|
|
|
|
2020-05-16 15:07:51 +02:00
|
|
|
- name: Debug
|
2020-07-14 17:28:08 +02:00
|
|
|
ansible.builtin.debug:
|
2020-03-09 10:11:07 +01:00
|
|
|
var: clc
|
|
|
|
'''
|
|
|
|
|
|
|
|
RETURN = '''
|
|
|
|
group:
|
|
|
|
description: The group information
|
|
|
|
returned: success
|
|
|
|
type: dict
|
|
|
|
sample:
|
|
|
|
{
|
|
|
|
"changeInfo":{
|
|
|
|
"createdBy":"service.wfad",
|
|
|
|
"createdDate":"2015-07-29T18:52:47Z",
|
|
|
|
"modifiedBy":"service.wfad",
|
|
|
|
"modifiedDate":"2015-07-29T18:52:47Z"
|
|
|
|
},
|
|
|
|
"customFields":[
|
|
|
|
|
|
|
|
],
|
|
|
|
"description":"test group",
|
|
|
|
"groups":[
|
|
|
|
|
|
|
|
],
|
|
|
|
"id":"bb5f12a3c6044ae4ad0a03e73ae12cd1",
|
|
|
|
"links":[
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad",
|
|
|
|
"rel":"createGroup",
|
|
|
|
"verbs":[
|
|
|
|
"POST"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/servers/wfad",
|
|
|
|
"rel":"createServer",
|
|
|
|
"verbs":[
|
|
|
|
"POST"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1",
|
|
|
|
"rel":"self",
|
|
|
|
"verbs":[
|
|
|
|
"GET",
|
|
|
|
"PATCH",
|
|
|
|
"DELETE"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/086ac1dfe0b6411989e8d1b77c4065f0",
|
|
|
|
"id":"086ac1dfe0b6411989e8d1b77c4065f0",
|
|
|
|
"rel":"parentGroup"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/defaults",
|
|
|
|
"rel":"defaults",
|
|
|
|
"verbs":[
|
|
|
|
"GET",
|
|
|
|
"POST"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/billing",
|
|
|
|
"rel":"billing"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/archive",
|
|
|
|
"rel":"archiveGroupAction"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/statistics",
|
|
|
|
"rel":"statistics"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/upcomingScheduledActivities",
|
|
|
|
"rel":"upcomingScheduledActivities"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/horizontalAutoscalePolicy",
|
|
|
|
"rel":"horizontalAutoscalePolicyMapping",
|
|
|
|
"verbs":[
|
|
|
|
"GET",
|
|
|
|
"PUT",
|
|
|
|
"DELETE"
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/scheduledActivities",
|
|
|
|
"rel":"scheduledActivities",
|
|
|
|
"verbs":[
|
|
|
|
"GET",
|
|
|
|
"POST"
|
|
|
|
]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"locationId":"UC1",
|
|
|
|
"name":"test group",
|
|
|
|
"status":"active",
|
|
|
|
"type":"default"
|
|
|
|
}
|
|
|
|
'''
|
|
|
|
|
|
|
|
__version__ = '${version}'
|
|
|
|
|
|
|
|
import os
|
|
|
|
import traceback
|
2021-12-24 19:15:47 +01:00
|
|
|
|
|
|
|
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
REQUESTS_IMP_ERR = None
|
|
|
|
try:
|
|
|
|
import requests
|
|
|
|
except ImportError:
|
|
|
|
REQUESTS_IMP_ERR = traceback.format_exc()
|
|
|
|
REQUESTS_FOUND = False
|
|
|
|
else:
|
|
|
|
REQUESTS_FOUND = True
|
|
|
|
|
|
|
|
#
|
|
|
|
# Requires the clc-python-sdk.
|
|
|
|
# sudo pip install clc-sdk
|
|
|
|
#
|
|
|
|
CLC_IMP_ERR = None
|
|
|
|
try:
|
|
|
|
import clc as clc_sdk
|
|
|
|
from clc import CLCException
|
|
|
|
except ImportError:
|
|
|
|
CLC_IMP_ERR = traceback.format_exc()
|
|
|
|
CLC_FOUND = False
|
|
|
|
clc_sdk = None
|
|
|
|
else:
|
|
|
|
CLC_FOUND = True
|
|
|
|
|
|
|
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
|
|
|
|
|
|
|
|
|
|
class 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=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
|
|
|
|
if not REQUESTS_FOUND:
|
|
|
|
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
|
|
|
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
|
|
|
|
self.module.fail_json(
|
|
|
|
msg='requests library version should be >= 2.5.0')
|
|
|
|
|
|
|
|
self._set_user_agent(self.clc)
|
|
|
|
|
|
|
|
def process_request(self):
|
|
|
|
"""
|
|
|
|
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, requests = self._ensure_group_is_absent(
|
|
|
|
group_name=group_name, parent_name=parent_name)
|
|
|
|
if requests:
|
|
|
|
self._wait_for_requests_to_complete(requests)
|
|
|
|
else:
|
|
|
|
changed, group = self._ensure_group_is_present(
|
|
|
|
group_name=group_name, parent_name=parent_name, group_description=group_description)
|
|
|
|
try:
|
|
|
|
group = group.data
|
|
|
|
except AttributeError:
|
|
|
|
group = group_name
|
|
|
|
self.module.exit_json(changed=changed, group=group)
|
|
|
|
|
|
|
|
@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),
|
|
|
|
state=dict(default='present', choices=['present', 'absent']),
|
|
|
|
wait=dict(type='bool', default=True))
|
|
|
|
|
|
|
|
return argument_spec
|
|
|
|
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
response = None
|
|
|
|
group, parent = self.group_dict.get(group_name)
|
|
|
|
try:
|
|
|
|
response = group.Delete()
|
|
|
|
except CLCException as ex:
|
|
|
|
self.module.fail_json(msg='Failed to delete group :{0}. {1}'.format(
|
|
|
|
group_name, ex.response_text
|
|
|
|
))
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
if not self.root_group:
|
|
|
|
raise AssertionError("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
|
|
|
|
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:
|
|
|
|
group = self._create_group(
|
|
|
|
group=group,
|
|
|
|
parent=parent,
|
|
|
|
description=description)
|
|
|
|
changed = True
|
|
|
|
else:
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="parent group: " +
|
|
|
|
parent +
|
|
|
|
" does not exist")
|
|
|
|
|
|
|
|
return changed, group
|
|
|
|
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
response = None
|
|
|
|
(parent, grandparent) = self.group_dict[parent]
|
|
|
|
try:
|
|
|
|
response = parent.Create(name=group, description=description)
|
|
|
|
except CLCException as ex:
|
|
|
|
self.module.fail_json(msg='Failed to create group :{0}. {1}'.format(
|
|
|
|
group, ex.response_text))
|
|
|
|
return response
|
|
|
|
|
|
|
|
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):
|
|
|
|
"""
|
|
|
|
Walk the tree of groups for a datacenter
|
|
|
|
:param datacenter: string - the datacenter to walk (ex: 'UC1')
|
|
|
|
: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
|
|
|
|
|
|
|
|
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 group request')
|
|
|
|
|
|
|
|
@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()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|