1#!/usr/local/bin/python3.8 2# Copyright (c) 2017 Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8 9DOCUMENTATION = ''' 10--- 11module: aws_direct_connect_connection 12version_added: 1.0.0 13short_description: Creates, deletes, modifies a DirectConnect connection 14description: 15 - Create, update, or delete a Direct Connect connection between a network and a specific AWS Direct Connect location. 16 Upon creation the connection may be added to a link aggregation group or established as a standalone connection. 17 The connection may later be associated or disassociated with a link aggregation group. 18author: "Sloane Hertel (@s-hertel)" 19extends_documentation_fragment: 20- amazon.aws.aws 21- amazon.aws.ec2 22 23requirements: 24 - boto3 25 - botocore 26options: 27 state: 28 description: 29 - The state of the Direct Connect connection. 30 choices: 31 - present 32 - absent 33 type: str 34 required: true 35 name: 36 description: 37 - The name of the Direct Connect connection. This is required to create a 38 new connection. 39 - One of I(connection_id) or I(name) must be specified. 40 type: str 41 connection_id: 42 description: 43 - The ID of the Direct Connect connection. 44 - Modifying attributes of a connection with I(forced_update) will result in a new Direct Connect connection ID. 45 - One of I(connection_id) or I(name) must be specified. 46 type: str 47 location: 48 description: 49 - Where the Direct Connect connection is located. 50 - Required when I(state=present). 51 type: str 52 bandwidth: 53 description: 54 - The bandwidth of the Direct Connect connection. 55 - Required when I(state=present). 56 choices: 57 - 1Gbps 58 - 10Gbps 59 type: str 60 link_aggregation_group: 61 description: 62 - The ID of the link aggregation group you want to associate with the connection. 63 - This is optional when a stand-alone connection is desired. 64 type: str 65 forced_update: 66 description: 67 - To modify I(bandwidth) or I(location) the connection needs to be deleted and recreated. 68 - By default this will not happen. This option must be explicitly set to C(true) to change I(bandwith) or I(location). 69 type: bool 70 default: false 71''' 72 73EXAMPLES = """ 74 75# create a Direct Connect connection 76- community.aws.aws_direct_connect_connection: 77 name: ansible-test-connection 78 state: present 79 location: EqDC2 80 link_aggregation_group: dxlag-xxxxxxxx 81 bandwidth: 1Gbps 82 register: dc 83 84# disassociate the LAG from the connection 85- community.aws.aws_direct_connect_connection: 86 state: present 87 connection_id: dc.connection.connection_id 88 location: EqDC2 89 bandwidth: 1Gbps 90 91# replace the connection with one with more bandwidth 92- community.aws.aws_direct_connect_connection: 93 state: present 94 name: ansible-test-connection 95 location: EqDC2 96 bandwidth: 10Gbps 97 forced_update: true 98 99# delete the connection 100- community.aws.aws_direct_connect_connection: 101 state: absent 102 name: ansible-test-connection 103""" 104 105RETURN = """ 106connection: 107 description: The attributes of the direct connect connection. 108 type: complex 109 returned: I(state=present) 110 contains: 111 aws_device: 112 description: The endpoint which the physical connection terminates on. 113 returned: when the requested state is no longer 'requested' 114 type: str 115 sample: EqDC2-12pmo7hemtz1z 116 bandwidth: 117 description: The bandwidth of the connection. 118 returned: always 119 type: str 120 sample: 1Gbps 121 connection_id: 122 description: The ID of the connection. 123 returned: always 124 type: str 125 sample: dxcon-ffy9ywed 126 connection_name: 127 description: The name of the connection. 128 returned: always 129 type: str 130 sample: ansible-test-connection 131 connection_state: 132 description: The state of the connection. 133 returned: always 134 type: str 135 sample: pending 136 loa_issue_time: 137 description: The issue time of the connection's Letter of Authorization - Connecting Facility Assignment. 138 returned: when the LOA-CFA has been issued (the connection state will no longer be 'requested') 139 type: str 140 sample: '2018-03-20T17:36:26-04:00' 141 location: 142 description: The location of the connection. 143 returned: always 144 type: str 145 sample: EqDC2 146 owner_account: 147 description: The account that owns the direct connect connection. 148 returned: always 149 type: str 150 sample: '123456789012' 151 region: 152 description: The region in which the connection exists. 153 returned: always 154 type: str 155 sample: us-east-1 156""" 157 158import traceback 159 160try: 161 from botocore.exceptions import BotoCoreError, ClientError 162except ImportError: 163 pass # handled by imported AnsibleAWSModule 164 165from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict 166 167from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule 168from ansible_collections.amazon.aws.plugins.module_utils.direct_connect import DirectConnectError 169from ansible_collections.amazon.aws.plugins.module_utils.direct_connect import associate_connection_and_lag 170from ansible_collections.amazon.aws.plugins.module_utils.direct_connect import delete_connection 171from ansible_collections.amazon.aws.plugins.module_utils.direct_connect import disassociate_connection_and_lag 172from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry 173 174retry_params = {"tries": 10, "delay": 5, "backoff": 1.2, "catch_extra_error_codes": ["DirectConnectClientException"]} 175 176 177def connection_status(client, connection_id): 178 return connection_exists(client, connection_id=connection_id, connection_name=None, verify=False) 179 180 181def connection_exists(client, connection_id=None, connection_name=None, verify=True): 182 params = {} 183 if connection_id: 184 params['connectionId'] = connection_id 185 try: 186 response = AWSRetry.backoff(**retry_params)(client.describe_connections)(**params) 187 except (BotoCoreError, ClientError) as e: 188 if connection_id: 189 msg = "Failed to describe DirectConnect ID {0}".format(connection_id) 190 else: 191 msg = "Failed to describe DirectConnect connections" 192 raise DirectConnectError(msg=msg, 193 last_traceback=traceback.format_exc(), 194 exception=e) 195 196 match = [] 197 connection = [] 198 199 # look for matching connections 200 201 if len(response.get('connections', [])) == 1 and connection_id: 202 if response['connections'][0]['connectionState'] != 'deleted': 203 match.append(response['connections'][0]['connectionId']) 204 connection.extend(response['connections']) 205 206 for conn in response.get('connections', []): 207 if connection_name == conn['connectionName'] and conn['connectionState'] != 'deleted': 208 match.append(conn['connectionId']) 209 connection.append(conn) 210 211 # verifying if the connections exists; if true, return connection identifier, otherwise return False 212 if verify and len(match) == 1: 213 return match[0] 214 elif verify: 215 return False 216 # not verifying if the connection exists; just return current connection info 217 elif len(connection) == 1: 218 return {'connection': connection[0]} 219 return {'connection': {}} 220 221 222def create_connection(client, location, bandwidth, name, lag_id): 223 if not name: 224 raise DirectConnectError(msg="Failed to create a Direct Connect connection: name required.") 225 params = { 226 'location': location, 227 'bandwidth': bandwidth, 228 'connectionName': name, 229 } 230 if lag_id: 231 params['lagId'] = lag_id 232 233 try: 234 connection = AWSRetry.backoff(**retry_params)(client.create_connection)(**params) 235 except (BotoCoreError, ClientError) as e: 236 raise DirectConnectError(msg="Failed to create DirectConnect connection {0}".format(name), 237 last_traceback=traceback.format_exc(), 238 exception=e) 239 return connection['connectionId'] 240 241 242def changed_properties(current_status, location, bandwidth): 243 current_bandwidth = current_status['bandwidth'] 244 current_location = current_status['location'] 245 246 return current_bandwidth != bandwidth or current_location != location 247 248 249@AWSRetry.backoff(**retry_params) 250def update_associations(client, latest_state, connection_id, lag_id): 251 changed = False 252 if 'lagId' in latest_state and lag_id != latest_state['lagId']: 253 disassociate_connection_and_lag(client, connection_id, lag_id=latest_state['lagId']) 254 changed = True 255 if (changed and lag_id) or (lag_id and 'lagId' not in latest_state): 256 associate_connection_and_lag(client, connection_id, lag_id) 257 changed = True 258 return changed 259 260 261def ensure_present(client, connection_id, connection_name, location, bandwidth, lag_id, forced_update): 262 # the connection is found; get the latest state and see if it needs to be updated 263 if connection_id: 264 latest_state = connection_status(client, connection_id=connection_id)['connection'] 265 if changed_properties(latest_state, location, bandwidth) and forced_update: 266 ensure_absent(client, connection_id) 267 return ensure_present(client=client, 268 connection_id=None, 269 connection_name=connection_name, 270 location=location, 271 bandwidth=bandwidth, 272 lag_id=lag_id, 273 forced_update=forced_update) 274 elif update_associations(client, latest_state, connection_id, lag_id): 275 return True, connection_id 276 277 # no connection found; create a new one 278 else: 279 return True, create_connection(client, location, bandwidth, connection_name, lag_id) 280 281 return False, connection_id 282 283 284@AWSRetry.backoff(**retry_params) 285def ensure_absent(client, connection_id): 286 changed = False 287 if connection_id: 288 delete_connection(client, connection_id) 289 changed = True 290 291 return changed 292 293 294def main(): 295 argument_spec = dict( 296 state=dict(required=True, choices=['present', 'absent']), 297 name=dict(), 298 location=dict(), 299 bandwidth=dict(choices=['1Gbps', '10Gbps']), 300 link_aggregation_group=dict(), 301 connection_id=dict(), 302 forced_update=dict(type='bool', default=False) 303 ) 304 305 module = AnsibleAWSModule( 306 argument_spec=argument_spec, 307 required_one_of=[('connection_id', 'name')], 308 required_if=[('state', 'present', ('location', 'bandwidth'))] 309 ) 310 311 connection = module.client('directconnect') 312 313 state = module.params.get('state') 314 try: 315 connection_id = connection_exists( 316 connection, 317 connection_id=module.params.get('connection_id'), 318 connection_name=module.params.get('name') 319 ) 320 if not connection_id and module.params.get('connection_id'): 321 module.fail_json(msg="The Direct Connect connection {0} does not exist.".format(module.params.get('connection_id'))) 322 323 if state == 'present': 324 changed, connection_id = ensure_present(connection, 325 connection_id=connection_id, 326 connection_name=module.params.get('name'), 327 location=module.params.get('location'), 328 bandwidth=module.params.get('bandwidth'), 329 lag_id=module.params.get('link_aggregation_group'), 330 forced_update=module.params.get('forced_update')) 331 response = connection_status(connection, connection_id) 332 elif state == 'absent': 333 changed = ensure_absent(connection, connection_id) 334 response = {} 335 except DirectConnectError as e: 336 if e.last_traceback: 337 module.fail_json(msg=e.msg, exception=e.last_traceback, **camel_dict_to_snake_dict(e.exception.response)) 338 else: 339 module.fail_json(msg=e.msg) 340 341 module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) 342 343 344if __name__ == '__main__': 345 main() 346