1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# 4# Copyright (c) 2015 CenturyLink 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11DOCUMENTATION = ''' 12module: clc_publicip 13short_description: Add and Delete public ips on servers in CenturyLink Cloud. 14description: 15 - An Ansible module to add or delete public ip addresses on an existing server or servers in CenturyLink Cloud. 16options: 17 protocol: 18 description: 19 - The protocol that the public IP will listen for. 20 type: str 21 default: TCP 22 choices: ['TCP', 'UDP', 'ICMP'] 23 ports: 24 description: 25 - A list of ports to expose. This is required when state is 'present' 26 type: list 27 elements: int 28 server_ids: 29 description: 30 - A list of servers to create public ips on. 31 type: list 32 required: True 33 elements: str 34 state: 35 description: 36 - Determine whether to create or delete public IPs. If present module will not create a second public ip if one 37 already exists. 38 type: str 39 default: present 40 choices: ['present', 'absent'] 41 wait: 42 description: 43 - Whether to wait for the tasks to finish before returning. 44 type: bool 45 default: 'yes' 46requirements: 47 - python = 2.7 48 - requests >= 2.5.0 49 - clc-sdk 50author: "CLC Runner (@clc-runner)" 51notes: 52 - To use this module, it is required to set the below environment variables which enables access to the 53 Centurylink Cloud 54 - CLC_V2_API_USERNAME, the account login id for the centurylink cloud 55 - CLC_V2_API_PASSWORD, the account password for the centurylink cloud 56 - Alternatively, the module accepts the API token and account alias. The API token can be generated using the 57 CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login 58 - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login 59 - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud 60 - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. 61''' 62 63EXAMPLES = ''' 64# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples 65 66- name: Add Public IP to Server 67 hosts: localhost 68 gather_facts: False 69 connection: local 70 tasks: 71 - name: Create Public IP For Servers 72 community.general.clc_publicip: 73 protocol: TCP 74 ports: 75 - 80 76 server_ids: 77 - UC1TEST-SVR01 78 - UC1TEST-SVR02 79 state: present 80 register: clc 81 82 - name: Debug 83 ansible.builtin.debug: 84 var: clc 85 86- name: Delete Public IP from Server 87 hosts: localhost 88 gather_facts: False 89 connection: local 90 tasks: 91 - name: Create Public IP For Servers 92 community.general.clc_publicip: 93 server_ids: 94 - UC1TEST-SVR01 95 - UC1TEST-SVR02 96 state: absent 97 register: clc 98 99 - name: Debug 100 ansible.builtin.debug: 101 var: clc 102''' 103 104RETURN = ''' 105server_ids: 106 description: The list of server ids that are changed 107 returned: success 108 type: list 109 sample: 110 [ 111 "UC1TEST-SVR01", 112 "UC1TEST-SVR02" 113 ] 114''' 115 116__version__ = '${version}' 117 118import os 119import traceback 120from distutils.version import LooseVersion 121 122REQUESTS_IMP_ERR = None 123try: 124 import requests 125except ImportError: 126 REQUESTS_IMP_ERR = traceback.format_exc() 127 REQUESTS_FOUND = False 128else: 129 REQUESTS_FOUND = True 130 131# 132# Requires the clc-python-sdk. 133# sudo pip install clc-sdk 134# 135CLC_IMP_ERR = None 136try: 137 import clc as clc_sdk 138 from clc import CLCException 139except ImportError: 140 CLC_IMP_ERR = traceback.format_exc() 141 CLC_FOUND = False 142 clc_sdk = None 143else: 144 CLC_FOUND = True 145 146from ansible.module_utils.basic import AnsibleModule, missing_required_lib 147 148 149class ClcPublicIp(object): 150 clc = clc_sdk 151 module = None 152 153 def __init__(self, module): 154 """ 155 Construct module 156 """ 157 self.module = module 158 if not CLC_FOUND: 159 self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR) 160 if not REQUESTS_FOUND: 161 self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR) 162 if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'): 163 self.module.fail_json( 164 msg='requests library version should be >= 2.5.0') 165 166 self._set_user_agent(self.clc) 167 168 def process_request(self): 169 """ 170 Process the request - Main Code Path 171 :return: Returns with either an exit_json or fail_json 172 """ 173 self._set_clc_credentials_from_env() 174 params = self.module.params 175 server_ids = params['server_ids'] 176 ports = params['ports'] 177 protocol = params['protocol'] 178 state = params['state'] 179 180 if state == 'present': 181 changed, changed_server_ids, requests = self.ensure_public_ip_present( 182 server_ids=server_ids, protocol=protocol, ports=ports) 183 elif state == 'absent': 184 changed, changed_server_ids, requests = self.ensure_public_ip_absent( 185 server_ids=server_ids) 186 else: 187 return self.module.fail_json(msg="Unknown State: " + state) 188 self._wait_for_requests_to_complete(requests) 189 return self.module.exit_json(changed=changed, 190 server_ids=changed_server_ids) 191 192 @staticmethod 193 def _define_module_argument_spec(): 194 """ 195 Define the argument spec for the ansible module 196 :return: argument spec dictionary 197 """ 198 argument_spec = dict( 199 server_ids=dict(type='list', required=True, elements='str'), 200 protocol=dict(default='TCP', choices=['TCP', 'UDP', 'ICMP']), 201 ports=dict(type='list', elements='int'), 202 wait=dict(type='bool', default=True), 203 state=dict(default='present', choices=['present', 'absent']), 204 ) 205 return argument_spec 206 207 def ensure_public_ip_present(self, server_ids, protocol, ports): 208 """ 209 Ensures the given server ids having the public ip available 210 :param server_ids: the list of server ids 211 :param protocol: the ip protocol 212 :param ports: the list of ports to expose 213 :return: (changed, changed_server_ids, results) 214 changed: A flag indicating if there is any change 215 changed_server_ids : the list of server ids that are changed 216 results: The result list from clc public ip call 217 """ 218 changed = False 219 results = [] 220 changed_server_ids = [] 221 servers = self._get_servers_from_clc( 222 server_ids, 223 'Failed to obtain server list from the CLC API') 224 servers_to_change = [ 225 server for server in servers if len( 226 server.PublicIPs().public_ips) == 0] 227 ports_to_expose = [{'protocol': protocol, 'port': port} 228 for port in ports] 229 for server in servers_to_change: 230 if not self.module.check_mode: 231 result = self._add_publicip_to_server(server, ports_to_expose) 232 results.append(result) 233 changed_server_ids.append(server.id) 234 changed = True 235 return changed, changed_server_ids, results 236 237 def _add_publicip_to_server(self, server, ports_to_expose): 238 result = None 239 try: 240 result = server.PublicIPs().Add(ports_to_expose) 241 except CLCException as ex: 242 self.module.fail_json(msg='Failed to add public ip to the server : {0}. {1}'.format( 243 server.id, ex.response_text 244 )) 245 return result 246 247 def ensure_public_ip_absent(self, server_ids): 248 """ 249 Ensures the given server ids having the public ip removed if there is any 250 :param server_ids: the list of server ids 251 :return: (changed, changed_server_ids, results) 252 changed: A flag indicating if there is any change 253 changed_server_ids : the list of server ids that are changed 254 results: The result list from clc public ip call 255 """ 256 changed = False 257 results = [] 258 changed_server_ids = [] 259 servers = self._get_servers_from_clc( 260 server_ids, 261 'Failed to obtain server list from the CLC API') 262 servers_to_change = [ 263 server for server in servers if len( 264 server.PublicIPs().public_ips) > 0] 265 for server in servers_to_change: 266 if not self.module.check_mode: 267 result = self._remove_publicip_from_server(server) 268 results.append(result) 269 changed_server_ids.append(server.id) 270 changed = True 271 return changed, changed_server_ids, results 272 273 def _remove_publicip_from_server(self, server): 274 result = None 275 try: 276 for ip_address in server.PublicIPs().public_ips: 277 result = ip_address.Delete() 278 except CLCException as ex: 279 self.module.fail_json(msg='Failed to remove public ip from the server : {0}. {1}'.format( 280 server.id, ex.response_text 281 )) 282 return result 283 284 def _wait_for_requests_to_complete(self, requests_lst): 285 """ 286 Waits until the CLC requests are complete if the wait argument is True 287 :param requests_lst: The list of CLC request objects 288 :return: none 289 """ 290 if not self.module.params['wait']: 291 return 292 for request in requests_lst: 293 request.WaitUntilComplete() 294 for request_details in request.requests: 295 if request_details.Status() != 'succeeded': 296 self.module.fail_json( 297 msg='Unable to process public ip request') 298 299 def _set_clc_credentials_from_env(self): 300 """ 301 Set the CLC Credentials on the sdk by reading environment variables 302 :return: none 303 """ 304 env = os.environ 305 v2_api_token = env.get('CLC_V2_API_TOKEN', False) 306 v2_api_username = env.get('CLC_V2_API_USERNAME', False) 307 v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) 308 clc_alias = env.get('CLC_ACCT_ALIAS', False) 309 api_url = env.get('CLC_V2_API_URL', False) 310 311 if api_url: 312 self.clc.defaults.ENDPOINT_URL_V2 = api_url 313 314 if v2_api_token and clc_alias: 315 self.clc._LOGIN_TOKEN_V2 = v2_api_token 316 self.clc._V2_ENABLED = True 317 self.clc.ALIAS = clc_alias 318 elif v2_api_username and v2_api_passwd: 319 self.clc.v2.SetCredentials( 320 api_username=v2_api_username, 321 api_passwd=v2_api_passwd) 322 else: 323 return self.module.fail_json( 324 msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " 325 "environment variables") 326 327 def _get_servers_from_clc(self, server_ids, message): 328 """ 329 Gets list of servers form CLC api 330 """ 331 try: 332 return self.clc.v2.Servers(server_ids).servers 333 except CLCException as exception: 334 self.module.fail_json(msg=message + ': %s' % exception) 335 336 @staticmethod 337 def _set_user_agent(clc): 338 if hasattr(clc, 'SetRequestsSession'): 339 agent_string = "ClcAnsibleModule/" + __version__ 340 ses = requests.Session() 341 ses.headers.update({"Api-Client": agent_string}) 342 ses.headers['User-Agent'] += " " + agent_string 343 clc.SetRequestsSession(ses) 344 345 346def main(): 347 """ 348 The main function. Instantiates the module and calls process_request. 349 :return: none 350 """ 351 module = AnsibleModule( 352 argument_spec=ClcPublicIp._define_module_argument_spec(), 353 supports_check_mode=True 354 ) 355 clc_public_ip = ClcPublicIp(module) 356 clc_public_ip.process_request() 357 358 359if __name__ == '__main__': 360 main() 361