forked from ansible-collections/amazon.aws
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds module aws_direct_connect_confirm_connection (ansible-collection…
…s#53) * Adds module aws_direct_connect_confirm_connection DirectConnect connections that are created by a Hosted provider require approval by users. This module simply finds the DirectConnect connection and confirms it if it's in the 'ordering' state. * Adding unit tests * Correcting test cases * Correct linting issue * Switch to AWSRetry decorator to correct test cases
- Loading branch information
Showing
2 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
156 changes: 156 additions & 0 deletions
156
plugins/modules/aws_direct_connect_confirm_connection.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
#!/usr/bin/python | ||
# Copyright (c) 2017 Ansible Project | ||
# 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 | ||
|
||
ANSIBLE_METADATA = {'metadata_version': '1.1', | ||
'status': ['preview'], | ||
'supported_by': 'community'} | ||
|
||
DOCUMENTATION = ''' | ||
--- | ||
module: aws_direct_connect_confirm_connection | ||
short_description: Confirms the creation of a hosted DirectConnect connection. | ||
description: | ||
- Confirms the creation of a hosted DirectConnect, which requires approval before it can be used. | ||
- DirectConnect connections that require approval would be in the 'ordering'. | ||
- After confirmation, they will move to the 'pending' state and finally the 'available' state. | ||
author: "Matt Traynham (@mtraynham)" | ||
extends_documentation_fragment: | ||
- amazon.aws.aws | ||
- amazon.aws.ec2 | ||
requirements: | ||
- boto3 | ||
- botocore | ||
options: | ||
name: | ||
description: | ||
- The name of the Direct Connect connection. | ||
- One of I(connection_id) or I(name) must be specified. | ||
type: str | ||
connection_id: | ||
description: | ||
- The ID of the Direct Connect connection. | ||
- One of I(connection_id) or I(name) must be specified. | ||
type: str | ||
''' | ||
|
||
EXAMPLES = ''' | ||
# confirm a Direct Connect by name | ||
- name: confirm the connection id | ||
aws_direct_connect_confirm_connection: | ||
name: my_host_direct_connect | ||
# confirm a Direct Connect by connection_id | ||
- name: confirm the connection id | ||
aws_direct_connect_confirm_connection: | ||
connection_id: dxcon-xxxxxxxx | ||
''' | ||
|
||
RETURN = ''' | ||
connection_state: | ||
description: The state of the connection. | ||
returned: always | ||
type: str | ||
sample: pending | ||
''' | ||
|
||
import traceback | ||
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule | ||
from ansible_collections.amazon.aws.plugins.module_utils.direct_connect import DirectConnectError | ||
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import (camel_dict_to_snake_dict, AWSRetry) | ||
|
||
try: | ||
from botocore.exceptions import BotoCoreError, ClientError | ||
except Exception: | ||
pass | ||
# handled by imported AnsibleAWSModule | ||
|
||
retry_params = {"tries": 10, "delay": 5, "backoff": 1.2, "catch_extra_error_codes": ["DirectConnectClientException"]} | ||
|
||
|
||
@AWSRetry.backoff(**retry_params) | ||
def describe_connections(client, params): | ||
return client.describe_connections(**params) | ||
|
||
|
||
def find_connection_id(client, connection_id=None, connection_name=None): | ||
params = {} | ||
if connection_id: | ||
params['connectionId'] = connection_id | ||
try: | ||
response = describe_connections(client, params) | ||
except (BotoCoreError, ClientError) as e: | ||
if connection_id: | ||
msg = "Failed to describe DirectConnect ID {0}".format(connection_id) | ||
else: | ||
msg = "Failed to describe DirectConnect connections" | ||
raise DirectConnectError(msg=msg, | ||
last_traceback=traceback.format_exc(), | ||
exception=e) | ||
|
||
match = [] | ||
if len(response.get('connections', [])) == 1 and connection_id: | ||
if response['connections'][0]['connectionState'] != 'deleted': | ||
match.append(response['connections'][0]['connectionId']) | ||
|
||
for conn in response.get('connections', []): | ||
if connection_name == conn['connectionName'] and conn['connectionState'] != 'deleted': | ||
match.append(conn['connectionId']) | ||
|
||
if len(match) == 1: | ||
return match[0] | ||
else: | ||
raise DirectConnectError(msg="Could not find a valid DirectConnect connection") | ||
|
||
|
||
def get_connection_state(client, connection_id): | ||
try: | ||
response = describe_connections(client, dict(connectionId=connection_id)) | ||
return response['connections'][0]['connectionState'] | ||
except (BotoCoreError, ClientError, IndexError) as e: | ||
raise DirectConnectError(msg="Failed to describe DirectConnect connection {0} state".format(connection_id), | ||
last_traceback=traceback.format_exc(), | ||
exception=e) | ||
|
||
|
||
def main(): | ||
argument_spec = dict( | ||
connection_id=dict(), | ||
name=dict() | ||
) | ||
module = AnsibleAWSModule(argument_spec=argument_spec, | ||
mutually_exclusive=[['connection_id', 'name']], | ||
required_one_of=[['connection_id', 'name']]) | ||
client = module.client('directconnect') | ||
|
||
connection_id = module.params['connection_id'] | ||
connection_name = module.params['name'] | ||
|
||
changed = False | ||
connection_state = None | ||
try: | ||
connection_id = find_connection_id(client, | ||
connection_id, | ||
connection_name) | ||
connection_state = get_connection_state(client, connection_id) | ||
if connection_state == 'ordering': | ||
client.confirm_connection(connectionId=connection_id) | ||
changed = True | ||
connection_state = get_connection_state(client, connection_id) | ||
except DirectConnectError as e: | ||
if e.last_traceback: | ||
module.fail_json(msg=e.msg, exception=e.last_traceback, **camel_dict_to_snake_dict(e.exception.response)) | ||
else: | ||
module.fail_json(msg=e.msg) | ||
|
||
module.exit_json(changed=changed, connection_state=connection_state) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
156 changes: 156 additions & 0 deletions
156
tests/unit/modules/test_aws_direct_connect_confirm_connection.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
# Make coding more python3-ish | ||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
from ansible_collections.community.aws.tests.unit.compat.mock import MagicMock, patch, call | ||
from ansible_collections.community.aws.tests.unit.modules.utils import (AnsibleExitJson, | ||
AnsibleFailJson, | ||
ModuleTestCase, | ||
set_module_args) | ||
from ansible_collections.community.aws.plugins.modules import aws_direct_connect_confirm_connection | ||
try: | ||
from botocore.exceptions import ClientError | ||
except ImportError: | ||
pass | ||
|
||
|
||
@patch('ansible_collections.amazon.aws.plugins.module_utils.core.HAS_BOTO3', new=True) | ||
@patch.object(aws_direct_connect_confirm_connection.AnsibleAWSModule, "client") | ||
class TestAWSDirectConnectConfirmConnection(ModuleTestCase): | ||
def test_missing_required_parameters(self, *args): | ||
set_module_args({}) | ||
with self.assertRaises(AnsibleFailJson) as exec_info: | ||
aws_direct_connect_confirm_connection.main() | ||
|
||
result = exec_info.exception.args[0] | ||
assert result["failed"] is True | ||
assert "name" in result["msg"] | ||
assert "connection_id" in result["msg"] | ||
|
||
def test_get_by_connection_id(self, mock_client): | ||
mock_client.return_value.describe_connections.return_value = { | ||
"connections": [ | ||
{ | ||
"connectionState": "requested", | ||
"connectionId": "dxcon-fgq9rgot", | ||
"location": "EqSe2", | ||
"connectionName": "ansible-test-connection", | ||
"bandwidth": "1Gbps", | ||
"ownerAccount": "448830907657", | ||
"region": "us-west-2" | ||
} | ||
] | ||
} | ||
set_module_args({ | ||
"connection_id": "dxcon-fgq9rgot" | ||
}) | ||
with self.assertRaises(AnsibleExitJson) as exec_info: | ||
aws_direct_connect_confirm_connection.main() | ||
|
||
result = exec_info.exception.args[0] | ||
assert result["changed"] is False | ||
assert result["connection_state"] == "requested" | ||
mock_client.return_value.describe_connections.assert_has_calls([ | ||
call(connectionId="dxcon-fgq9rgot") | ||
]) | ||
mock_client.return_value.confirm_connection.assert_not_called() | ||
|
||
def test_get_by_name(self, mock_client): | ||
mock_client.return_value.describe_connections.return_value = { | ||
"connections": [ | ||
{ | ||
"connectionState": "requested", | ||
"connectionId": "dxcon-fgq9rgot", | ||
"location": "EqSe2", | ||
"connectionName": "ansible-test-connection", | ||
"bandwidth": "1Gbps", | ||
"ownerAccount": "448830907657", | ||
"region": "us-west-2" | ||
} | ||
] | ||
} | ||
set_module_args({ | ||
"name": "ansible-test-connection" | ||
}) | ||
with self.assertRaises(AnsibleExitJson) as exec_info: | ||
aws_direct_connect_confirm_connection.main() | ||
|
||
result = exec_info.exception.args[0] | ||
assert result["changed"] is False | ||
assert result["connection_state"] == "requested" | ||
mock_client.return_value.describe_connections.assert_has_calls([ | ||
call(), | ||
call(connectionId="dxcon-fgq9rgot") | ||
]) | ||
mock_client.return_value.confirm_connection.assert_not_called() | ||
|
||
def test_missing_connection_id(self, mock_client): | ||
mock_client.return_value.describe_connections.side_effect = ClientError( | ||
{'Error': {'Code': 'ResourceNotFoundException'}}, 'DescribeConnection') | ||
set_module_args({ | ||
"connection_id": "dxcon-aaaabbbb" | ||
}) | ||
with self.assertRaises(AnsibleFailJson) as exec_info: | ||
aws_direct_connect_confirm_connection.main() | ||
|
||
result = exec_info.exception.args[0] | ||
assert result["failed"] is True | ||
mock_client.return_value.describe_connections.assert_has_calls([ | ||
call(connectionId="dxcon-aaaabbbb") | ||
]) | ||
|
||
def test_missing_name(self, mock_client): | ||
mock_client.return_value.describe_connections.return_value = { | ||
"connections": [ | ||
{ | ||
"connectionState": "requested", | ||
"connectionId": "dxcon-fgq9rgot", | ||
"location": "EqSe2", | ||
"connectionName": "ansible-test-connection", | ||
"bandwidth": "1Gbps", | ||
"ownerAccount": "448830907657", | ||
"region": "us-west-2" | ||
} | ||
] | ||
} | ||
set_module_args({ | ||
"name": "foobar" | ||
}) | ||
with self.assertRaises(AnsibleFailJson) as exec_info: | ||
aws_direct_connect_confirm_connection.main() | ||
|
||
result = exec_info.exception.args[0] | ||
assert result["failed"] is True | ||
mock_client.return_value.describe_connections.assert_has_calls([ | ||
call() | ||
]) | ||
|
||
def test_confirm(self, mock_client): | ||
mock_client.return_value.describe_connections.return_value = { | ||
"connections": [ | ||
{ | ||
"connectionState": "ordering", | ||
"connectionId": "dxcon-fgq9rgot", | ||
"location": "EqSe2", | ||
"connectionName": "ansible-test-connection", | ||
"bandwidth": "1Gbps", | ||
"ownerAccount": "448830907657", | ||
"region": "us-west-2" | ||
} | ||
] | ||
} | ||
mock_client.return_value.confirm_connection.return_value = [{}] | ||
set_module_args({ | ||
"connection_id": "dxcon-fgq9rgot" | ||
}) | ||
with self.assertRaises(AnsibleExitJson) as exec_info: | ||
aws_direct_connect_confirm_connection.main() | ||
|
||
result = exec_info.exception.args[0] | ||
assert result["changed"] is True | ||
mock_client.return_value.describe_connections.assert_has_calls([ | ||
call(connectionId="dxcon-fgq9rgot"), | ||
call(connectionId="dxcon-fgq9rgot"), | ||
call(connectionId="dxcon-fgq9rgot") | ||
]) | ||
mock_client.return_value.confirm_connection.assert_called_once_with(connectionId="dxcon-fgq9rgot") |