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