diff --git a/changelogs/fragments/20240930-ec2_vpc_vpn_refactoring.yml b/changelogs/fragments/20240930-ec2_vpc_vpn_refactoring.yml new file mode 100644 index 00000000000..6ed1c793a65 --- /dev/null +++ b/changelogs/fragments/20240930-ec2_vpc_vpn_refactoring.yml @@ -0,0 +1,3 @@ +minor_changes: + - ec2_vpc_vpn - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` (https://github.com/ansible-collections/community.aws/pull/2160). + - ec2_vpc_vpn_info - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` (https://github.com/ansible-collections/community.aws/pull/2160). diff --git a/plugins/modules/ec2_vpc_vpn.py b/plugins/modules/ec2_vpc_vpn.py index abc97f796b7..2555e4cc3a7 100644 --- a/plugins/modules/ec2_vpc_vpn.py +++ b/plugins/modules/ec2_vpc_vpn.py @@ -10,15 +10,15 @@ version_added: 1.0.0 short_description: Create, modify, and delete EC2 VPN connections description: - - This module creates, modifies, and deletes VPN connections. Idempotence is achieved by using the filters - option or specifying the VPN connection identifier. + - This module creates, modifies, and deletes VPN connections. + - Idempotence is achieved by using the O(filters) option or specifying the VPN connection identifier. author: - - "Sloane Hertel (@s-hertel)" + - Sloane Hertel (@s-hertel) options: state: description: - The desired state of the VPN connection. - choices: ['present', 'absent'] + choices: ["present", "absent"] default: present required: false type: str @@ -29,13 +29,13 @@ connection_type: description: - The type of VPN connection. - - At this time only C(ipsec.1) is supported. - default: ipsec.1 + - At this time only V(ipsec.1) is supported. + default: "ipsec.1" type: str vpn_gateway_id: description: - The ID of the virtual private gateway. - - Mutually exclusive with I(transit_gateway_id). + - Mutually exclusive with O(transit_gateway_id). type: str vpn_connection_id: description: @@ -44,20 +44,27 @@ static_only: description: - Indicates whether the VPN connection uses static routes only. Static routes must be used for devices that don't support BGP. - default: False + default: false type: bool required: false transit_gateway_id: description: - The ID of the transit gateway. - - Mutually exclusive with I(vpn_gateway_id). + - Mutually exclusive with O(vpn_gateway_id). type: str version_added: 6.2.0 + local_ipv4_network_cidr: + description: + - The IPv4 CIDR on the customer gateway (on-premises) side of the VPN connection. + required: false + type: str + default: "0.0.0.0/0" + version_added: 9.0.0 tunnel_options: description: - - An optional list object containing no more than two dict members, each of which may contain I(TunnelInsideCidr) - and/or I(PreSharedKey) keys with appropriate string values. AWS defaults will apply in absence of either of - the aforementioned keys. + - An optional list object containing no more than two dict members, each of which may contain O(tunnel_options.TunnelInsideCidr) + and/or O(tunnel_options.PreSharedKey) keys with appropriate string values. + AWS defaults will apply in absence of either of the aforementioned keys. required: false type: list elements: dict @@ -65,26 +72,34 @@ suboptions: TunnelInsideCidr: type: str - description: The range of inside IP addresses for the tunnel. + description: + - The range of inside IPv4 addresses for the tunnel. + TunnelInsideIpv6Cidr: + type: str + description: + - The range of inside IPv6 addresses for the tunnel. + version_added: 9.0.0 PreSharedKey: type: str - description: The pre-shared key (PSK) to establish initial authentication between the virtual private gateway and customer gateway. + description: + - The pre-shared key (PSK) to establish initial authentication between the virtual private gateway and customer gateway. filters: description: - - An alternative to using I(vpn_connection_id). If multiple matches are found, vpn_connection_id is required. + - An alternative to using O(vpn_connection_id). If multiple matches are found, O(vpn_connection_id) is required. If one of the following suboptions is a list of items to filter by, only one item needs to match to find the VPN - that correlates. e.g. if the filter I(cidr) is C(['194.168.2.0/24', '192.168.2.0/24']) and the VPN route only has the - destination cidr block of C(192.168.2.0/24) it will be found with this filter (assuming there are not multiple - VPNs that are matched). Another example, if the filter I(vpn) is equal to C(['vpn-ccf7e7ad', 'vpn-cb0ae2a2']) and one + that correlates. e.g. if the filter O(filters.cidr) is V(["194.168.2.0/24", "192.168.2.0/24"]) and the VPN route only has the + destination cidr block of V(192.168.2.0/24) it will be found with this filter (assuming there are not multiple + VPNs that are matched). Another example, if the filter O(filters.vpn) is equal to V(["vpn-ccf7e7ad", "vpn-cb0ae2a2"]) and one of of the VPNs has the state deleted (exists but is unmodifiable) and the other exists and is not deleted, - it will be found via this filter. See examples. + it will be found via this filter. suboptions: cgw-config: description: - The customer gateway configuration of the VPN as a string (in the format of the return value) or a list of those strings. static-routes-only: description: - - The type of routing; C(true) or C(false). + - The type of routing; V(true) or V(false). + type: bool cidr: description: - The destination cidr of the VPN's route as a string or a list of those strings. @@ -107,6 +122,7 @@ tags: description: - A dict of key value pairs. + type: dict cgw: description: - The customer gateway id as a string or a list of those strings. @@ -145,79 +161,77 @@ EXAMPLES = r""" # Note: These examples do not set authentication details, see the AWS Guide for details. -- name: create a VPN connection with vpn_gateway_id +- name: Create a VPN connection with vpn_gateway_id community.aws.ec2_vpc_vpn: - state: present - vpn_gateway_id: vgw-XXXXXXXX - customer_gateway_id: cgw-XXXXXXXX + state: "present" + vpn_gateway_id: "vgw-XXXXXXXX" + customer_gateway_id: "cgw-XXXXXXXX" - name: Attach a vpn connection to transit gateway community.aws.ec2_vpc_vpn: - state: present - transit_gateway_id: tgw-XXXXXXXX - customer_gateway_id: cgw-XXXXXXXX + state: "present" + transit_gateway_id: "tgw-XXXXXXXX" + customer_gateway_id: "cgw-XXXXXXXX" -- name: modify VPN connection tags +- name: Modify VPN connection tags community.aws.ec2_vpc_vpn: - state: present - vpn_connection_id: vpn-XXXXXXXX + state: "present" + vpn_connection_id: "vpn-XXXXXXXX" tags: - Name: ansible-tag-1 - Other: ansible-tag-2 + Name: "ansible-tag-1" + Other: "ansible-tag-2" -- name: delete a connection +- name: Delete a connection community.aws.ec2_vpc_vpn: - vpn_connection_id: vpn-XXXXXXXX - state: absent + vpn_connection_id: "vpn-XXXXXXXX" + state: "absent" -- name: modify VPN tags (identifying VPN by filters) +- name: Modify VPN tags (identifying VPN by filters) community.aws.ec2_vpc_vpn: - state: present + state: "present" filters: - cidr: 194.168.1.0/24 + cidr: "194.168.1.0/24" tag-keys: - - Ansible - - Other + - "Ansible" + - "Other" tags: - New: Tag + New: "Tag" purge_tags: true static_only: true -- name: set up VPN with tunnel options utilizing 'TunnelInsideCidr' only +- name: Set up VPN with tunnel options utilizing 'TunnelInsideCidr' only community.aws.ec2_vpc_vpn: - state: present + state: "present" filters: - vpn: vpn-XXXXXXXX + vpn: "vpn-XXXXXXXX" static_only: true tunnel_options: - - - TunnelInsideCidr: '169.254.100.1/30' - - - TunnelInsideCidr: '169.254.100.5/30' + - TunnelInsideCidr: "169.254.100.1/30" + - TunnelInsideCidr: "169.254.100.5/30" -- name: add routes and remove any preexisting ones +- name: Add routes and remove any preexisting ones community.aws.ec2_vpc_vpn: - state: present + state: "present" filters: - vpn: vpn-XXXXXXXX + vpn: "vpn-XXXXXXXX" routes: - - 195.168.2.0/24 - - 196.168.2.0/24 + - "195.168.2.0/24" + - "196.168.2.0/24" purge_routes: true -- name: remove all routes +- name: Remove all routes community.aws.ec2_vpc_vpn: - state: present - vpn_connection_id: vpn-XXXXXXXX + state: "present" + vpn_connection_id: "vpn-XXXXXXXX" routes: [] purge_routes: true -- name: delete a VPN identified by filters +- name: Delete a VPN identified by filters community.aws.ec2_vpc_vpn: - state: absent + state: "absent" filters: tags: - Ansible: Tag + Ansible: "Tag" """ RETURN = r""" @@ -225,203 +239,281 @@ description: If the VPN connection has changed. type: bool returned: always - sample: - changed: true + sample: true customer_gateway_configuration: description: The configuration of the VPN connection. - returned: I(state=present) + returned: O(state=present) type: str customer_gateway_id: description: The customer gateway connected via the connection. type: str - returned: I(state=present) - sample: - customer_gateway_id: cgw-1220c87b + returned: O(state=present) + sample: "cgw-1220c87b" +gateway_association_state: + description: The current state of the gateway association. + type: str + returned: O(state=present) + sample: "associated" vpn_gateway_id: description: The virtual private gateway connected via the connection. type: str - returned: I(state=present) - sample: - vpn_gateway_id: vgw-cb0ae2a2 + returned: O(state=present) + sample: "vgw-cb0ae2a2" transit_gateway_id: description: The transit gateway id to which the vpn connection can be attached. type: str - returned: I(state=present) - sample: - transit_gateway_id: tgw-cb0ae2a2 + returned: O(state=present) + sample: "tgw-cb0ae2a2" options: - description: The VPN connection options (currently only containing static_routes_only). - type: complex - returned: I(state=present) + description: The VPN connection options. + type: list + elements: dict + returned: O(state=present) contains: static_routes_only: description: If the VPN connection only allows static routes. - returned: I(state=present) + returned: O(state=present) + type: bool + sample: true + enable_acceleration: + description: Indicates whether acceleration is enabled for the VPN connection. + returned: O(state=present) + type: bool + sample: false + local_ipv4_network_cidr: + description: The IPv4 CIDR on the customer gateway (on-premises) side of the VPN connection. + returned: O(state=present) + type: str + sample: "0.0.0.0/0" + outside_ip_address_type: + description: The external IP address of the VPN tunnel. + returned: O(state=present) + type: str + sample: "PublicIpv4" + remote_ipv4_network_cidr: + description: The IPv4 CIDR on the Amazon Web Services side of the VPN connection. + returned: O(state=present) type: str - sample: - static_routes_only: true + sample: "0.0.0.0/0" + tunnel_inside_ip_version: + description: Indicates whether the VPN tunnels process IPv4 or IPv6 traffic. + returned: O(state=present) + type: str + sample: "ipv4" + tunnel_options: + description: Indicates the VPN tunnel options. + returned: O(state=present) + type: list + elements: dict + sample: [{ + "log_options": { + "cloud_watch_log_options": { + "log_enabled": false + } + }, + "outside_ip_address": "34.225.101.10", + "pre_shared_key": "8n7hnjNE8zhIt4VpMOIfcrw6XnUTHLW9", + "tunnel_inside_cidr": "169.254.31.8/30" + }] + contains: + log_options: + description: Options for logging VPN tunnel activity. + returned: O(state=present) + type: dict + contains: + cloud_watch_log_options: + description: Options for sending VPN tunnel logs to CloudWatch. + type: dict + returned: O(state=present) + outside_ip_address: + description: The external IP address of the VPN tunnel. + type: str + returned: O(state=present) + pre_shared_key: + description: + - The pre-shared key (PSK) to establish initial authentication between the + virtual private gateway and the customer gateway. + type: str + returned: O(state=present) + tunnel_inside_cidr: + description: The range of inside IPv4 addresses for the tunnel. + type: str + returned: O(state=present) routes: description: The routes of the VPN connection. type: list - returned: I(state=present) - sample: - routes: [{ - 'destination_cidr_block': '192.168.1.0/24', - 'state': 'available' + returned: O(state=present) + sample: [{ + "destination_cidr_block": "192.168.1.0/24", + "state": "available" }] + contains: + destination_cidr_block: + description: + - The CIDR block associated with the local subnet of the customer data center. + type: str + returned: O(state=present) + source: + description: Indicates how the routes were provided. + type: str + returned: O(state=present) + state: + description: The current state of the static route. + type: str + returned: O(state=present) state: description: The status of the VPN connection. type: str - returned: I(state=present) - sample: - state: available + returned: O(state=present) + sample: "available" tags: description: The tags associated with the connection. type: dict - returned: I(state=present) - sample: - tags: - name: ansible-test - other: tag + returned: O(state=present) + sample: { + "name": "ansible-test", + "other": "tag" + } type: description: The type of VPN connection (currently only ipsec.1 is available). type: str - returned: I(state=present) - sample: - type: "ipsec.1" + returned: O(state=present) + sample: "ipsec.1" vgw_telemetry: type: list - returned: I(state=present) + returned: O(state=present) description: The telemetry for the VPN tunnel. - sample: - vgw_telemetry: [{ - 'outside_ip_address': 'string', - 'status': 'up', - 'last_status_change': 'datetime(2015, 1, 1)', - 'status_message': 'string', - 'accepted_route_count': 123 - }] + sample: [{ + "accepted_route_count": 0, + "last_status_change": "2024-09-30T13:12:33+00:00", + "outside_ip_address": "34.225.101.10", + "status": "DOWN", + "status_message": "IPSEC IS DOWN" + }] + contains: + accepted_route_count: + type: int + returned: O(state=present) + description: The number of accepted routes. + last_status_change: + type: str + returned: O(state=present) + description: The date and time of the last change in status. + outside_ip_address: + type: str + returned: O(state=present) + description: + - The Internet-routable IP address of the virtual private gateway's outside interface. + status: + type: str + returned: O(state=present) + description: The status of the VPN tunnel. + status_message: + type: str + returned: O(state=present) + description: If an error occurs, a description of the error. + certificate_arn: + description: The Amazon Resource Name of the virtual private gateway tunnel endpoint certificate. + returned: when a private certificate is used for authentication + type: str + sample: "arn:aws:acm:us-east-1:123456789012:certificate/c544d8ce-20b8-4fff-98b0-example" vpn_connection_id: description: The identifier for the VPN connection. type: str - returned: I(state=present) - sample: - vpn_connection_id: vpn-781e0e19 + returned: O(state=present) + sample: "vpn-781e0e19" """ try: - from botocore.exceptions import BotoCoreError - from botocore.exceptions import ClientError from botocore.exceptions import WaiterError except ImportError: pass # Handled by AnsibleAWSModule +from typing import Any +from typing import Dict +from typing import List +from typing import NoReturn +from typing import Optional +from typing import Tuple +from typing import Union + from ansible.module_utils._text import to_text from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry -from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_vpn_connection +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_vpn_connection_route +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_vpn_connection +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_vpn_connection_route +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_vpn_connections +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule -class VPNConnectionException(Exception): - def __init__(self, msg, exception=None): - super(VPNConnectionException, self).__init__(msg) - self.msg = msg - self.exception = exception - - -# AWS uses VpnGatewayLimitExceeded for both 'Too many VGWs' and 'Too many concurrent changes' -# we need to look at the mesage to tell the difference. -class VPNRetry(AWSRetry): - @staticmethod - def status_code_from_exception(error): - return ( - error.response["Error"]["Code"], - error.response["Error"]["Message"], - ) - - @staticmethod - def found(response_code, catch_extra_error_codes=None): - retry_on = ["The maximum number of mutating objects has been reached."] - - if catch_extra_error_codes: - retry_on.extend(catch_extra_error_codes) - if not isinstance(response_code, tuple): - response_code = (response_code,) - - for code in response_code: - if super().found(response_code, catch_extra_error_codes): - return True - - return False - - -def find_connection(connection, module_params, vpn_connection_id=None): +def find_vpn_connection( + client, module: AnsibleAWSModule, vpn_connection_id: Optional[str] = None +) -> Union[None, Dict[str, Any]]: """Looks for a unique VPN connection. Uses find_connection_response() to return the connection found, None, or raise an error if there were multiple viable connections.""" - filters = module_params.get("filters") + filters = module.params.get("filters") + params: Dict[str, Any] = {} # vpn_connection_id may be provided via module option; takes precedence over any filter values - if not vpn_connection_id and module_params.get("vpn_connection_id"): - vpn_connection_id = module_params.get("vpn_connection_id") + if not vpn_connection_id and module.params.get("vpn_connection_id"): + vpn_connection_id = module.params["vpn_connection_id"] if not isinstance(vpn_connection_id, list) and vpn_connection_id: vpn_connection_id = [to_text(vpn_connection_id)] elif isinstance(vpn_connection_id, list): vpn_connection_id = [to_text(connection) for connection in vpn_connection_id] - formatted_filter = [] + formatted_filter: List = [] # if vpn_connection_id is provided it will take precedence over any filters since it is a unique identifier if not vpn_connection_id: - formatted_filter = create_filter(module_params, provided_filters=filters) + formatted_filter = create_filter(module, filters) + + if vpn_connection_id: + params["VpnConnectionIds"] = vpn_connection_id + params["Filters"] = formatted_filter # see if there is a unique matching connection try: - if vpn_connection_id: - existing_conn = connection.describe_vpn_connections( - aws_retry=True, VpnConnectionIds=vpn_connection_id, Filters=formatted_filter - ) - else: - existing_conn = connection.describe_vpn_connections(aws_retry=True, Filters=formatted_filter) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed while describing VPN connection.", exception=e) + existing_conn = describe_vpn_connections(client, **params) + except AnsibleEC2Error as e: + module.fail_json_aws(e, msg="Failed while describing VPN connection.") - return find_connection_response(connections=existing_conn) + return find_connection_response(module, connections=existing_conn) -def add_routes(connection, vpn_connection_id, routes_to_add): +def add_routes(client, module: AnsibleAWSModule, vpn_connection_id: str, routes_to_add: List[Dict[str, Any]]) -> bool: + changed: bool = False for route in routes_to_add: try: - connection.create_vpn_connection_route( - aws_retry=True, VpnConnectionId=vpn_connection_id, DestinationCidrBlock=route - ) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException( - msg=f"Failed while adding route {route} to the VPN connection {vpn_connection_id}.", - exception=e, - ) + changed |= create_vpn_connection_route(client, vpn_connection_id, route) + except AnsibleEC2Error as e: + module.fail_json_aws(e, msg=f"Failed while adding route {route} to the VPN connection {vpn_connection_id}.") + return changed -def remove_routes(connection, vpn_connection_id, routes_to_remove): +def remove_routes( + client, module: AnsibleAWSModule, vpn_connection_id: str, routes_to_remove: List[Dict[str, Any]] +) -> bool: + changed: bool = False for route in routes_to_remove: try: - connection.delete_vpn_connection_route( - aws_retry=True, VpnConnectionId=vpn_connection_id, DestinationCidrBlock=route - ) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException( - msg=f"Failed to remove route {route} from the VPN connection {vpn_connection_id}.", - exception=e, - ) + changed |= delete_vpn_connection_route(client, vpn_connection_id, route) + except AnsibleEC2Error as e: + module.fail_json_aws(e, msg=f"Failed to remove route {route} from the VPN connection {vpn_connection_id}.") + return changed -def create_filter(module_params, provided_filters): +def create_filter(module, provided_filters: Dict[str, Any]) -> List[Dict[str, Any]]: """Creates a filter using the user-specified parameters and unmodifiable options that may have been specified in the task""" + boto3ify_filter = { "cgw-config": "customer-gateway-configuration", "static-routes-only": "option.static-routes-only", @@ -444,7 +536,7 @@ def create_filter(module_params, provided_filters): } flat_filter_dict = {} - formatted_filter = [] + formatted_filter: List = [] for raw_param in dict(provided_filters): # fix filter names to be recognized by boto3 @@ -454,7 +546,7 @@ def create_filter(module_params, provided_filters): elif raw_param in list(boto3ify_filter.items()): param = raw_param else: - raise VPNConnectionException(msg=f"{raw_param} is not a valid filter.") + module.fail_json(msg=f"{raw_param} is not a valid filter.") # reformat filters with special formats if param == "tag": @@ -474,8 +566,8 @@ def create_filter(module_params, provided_filters): # if customer_gateway, vpn_gateway, or vpn_connection was specified in the task but not the filter, add it for param in param_to_filter: - if param_to_filter[param] not in flat_filter_dict and module_params.get(param): - flat_filter_dict[param_to_filter[param]] = [module_params.get(param)] + if param_to_filter[param] not in flat_filter_dict and module.params.get(param): + flat_filter_dict[param_to_filter[param]] = [module.params.get(param)] # change the flat dict into something boto3 will understand formatted_filter = [{"Name": key, "Values": value} for key, value in flat_filter_dict.items()] @@ -483,18 +575,18 @@ def create_filter(module_params, provided_filters): return formatted_filter -def find_connection_response(connections=None): +def find_connection_response(module, connections: Optional[List[Dict[str, Any]]] = None) -> Optional[Dict[str, Any]]: """Determine if there is a viable unique match in the connections described. Returns the unique VPN connection if one is found, returns None if the connection does not exist, raise an error if multiple matches are found.""" # Found no connections - if not connections or "VpnConnections" not in connections: + if not connections: return None # Too many results - elif connections and len(connections["VpnConnections"]) > 1: + elif connections and len(connections) > 1: viable = [] - for each in connections["VpnConnections"]: + for each in connections: # deleted connections are not modifiable if each["State"] not in ("deleted", "deleting"): viable.append(each) @@ -505,7 +597,7 @@ def find_connection_response(connections=None): # Found a result but it was deleted already; since there was only one viable result create a new one return None else: - raise VPNConnectionException( + module.fail_json( msg=( "More than one matching VPN connection was found. " "To modify or delete a VPN please specify vpn_connection_id or add filters." @@ -513,26 +605,29 @@ def find_connection_response(connections=None): ) # Found unique match - elif connections and len(connections["VpnConnections"]) == 1: + elif connections and len(connections) == 1: # deleted connections are not modifiable - if connections["VpnConnections"][0]["State"] not in ("deleted", "deleting"): - return connections["VpnConnections"][0] + if connections[0]["State"] not in ("deleted", "deleting"): + return connections[0] def create_connection( - connection, - customer_gateway_id, - static_only, - vpn_gateway_id, - transit_gateway_id, - connection_type, - max_attempts, - delay, - tunnel_options=None, -): + client, + module: AnsibleAWSModule, + customer_gateway_id: Optional[str], + static_only: Optional[bool], + vpn_gateway_id: str, + transit_gateway_id: str, + connection_type: Optional[str], + max_attempts: Optional[int], + delay: Optional[int], + local_ipv4_network_cidr: Optional[str], + tunnel_options: Optional[List[Dict[str, Any]]] = None, +) -> Dict[str, Any]: """Creates a VPN connection""" - options = {"StaticRoutesOnly": static_only} + options = {"StaticRoutesOnly": static_only, "LocalIpv4NetworkCidr": local_ipv4_network_cidr} + if tunnel_options and len(tunnel_options) <= 2: t_opt = [] for m in tunnel_options: @@ -545,87 +640,67 @@ def create_connection( options["TunnelOptions"] = t_opt if not (customer_gateway_id and (vpn_gateway_id or transit_gateway_id)): - raise VPNConnectionException( + module.fail_json( msg=( "No matching connection was found. To create a new connection you must provide " "customer_gateway_id and one of either transit_gateway_id or vpn_gateway_id." ) ) - vpn_connection_params = {"Type": connection_type, "CustomerGatewayId": customer_gateway_id, "Options": options} + vpn_connection_params: Dict[str, Any] = { + "Type": connection_type, + "CustomerGatewayId": customer_gateway_id, + "Options": options, + } + if vpn_gateway_id: vpn_connection_params["VpnGatewayId"] = vpn_gateway_id if transit_gateway_id: vpn_connection_params["TransitGatewayId"] = transit_gateway_id try: - vpn = connection.create_vpn_connection(**vpn_connection_params) - connection.get_waiter("vpn_connection_available").wait( - VpnConnectionIds=[vpn["VpnConnection"]["VpnConnectionId"]], + vpn = create_vpn_connection(client, **vpn_connection_params) + client.get_waiter("vpn_connection_available").wait( + VpnConnectionIds=[vpn["VpnConnectionId"]], WaiterConfig={"Delay": delay, "MaxAttempts": max_attempts}, ) except WaiterError as e: - raise VPNConnectionException( - msg=f"Failed to wait for VPN connection {vpn['VpnConnection']['VpnConnectionId']} to be available", - exception=e, + module.fail_json_aws( + e, msg=f"Failed to wait for VPN connection {vpn['VpnConnection']['VpnConnectionId']} to be available" ) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg="Failed to create VPN connection", exception=e) + except AnsibleEC2Error as e: + module.fail_json_aws(e, msg="Failed to create VPN connection") - return vpn["VpnConnection"] + return vpn -def delete_connection(connection, vpn_connection_id, delay, max_attempts): +def delete_connection(client, module: AnsibleAWSModule, vpn_connection_id: str) -> NoReturn: """Deletes a VPN connection""" + + delay = module.params.get("delay") + max_attempts = module.params.get("wait_timeout") // delay + try: - connection.delete_vpn_connection(aws_retry=True, VpnConnectionId=vpn_connection_id) - connection.get_waiter("vpn_connection_deleted").wait( + delete_vpn_connection(client, vpn_connection_id) + client.get_waiter("vpn_connection_deleted").wait( VpnConnectionIds=[vpn_connection_id], WaiterConfig={"Delay": delay, "MaxAttempts": max_attempts} ) except WaiterError as e: - raise VPNConnectionException( - msg=f"Failed to wait for VPN connection {vpn_connection_id} to be removed", exception=e - ) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg=f"Failed to delete the VPN connection: {vpn_connection_id}", exception=e) - - -def add_tags(connection, vpn_connection_id, add): - try: - connection.create_tags(aws_retry=True, Resources=[vpn_connection_id], Tags=add) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg=f"Failed to add the tags: {add}.", exception=e) + module.fail_json_aws(e, msg=f"Failed to wait for VPN connection {vpn_connection_id} to be removed") + except AnsibleEC2Error as e: + module.fail_json_aws(e, msg=f"Failed to delete the VPN connection: {vpn_connection_id}") -def remove_tags(connection, vpn_connection_id, remove): - # format tags since they are a list in the format ['tag1', 'tag2', 'tag3'] - key_dict_list = [{"Key": tag} for tag in remove] - try: - connection.delete_tags(aws_retry=True, Resources=[vpn_connection_id], Tags=key_dict_list) - except (BotoCoreError, ClientError) as e: - raise VPNConnectionException(msg=f"Failed to remove the tags: {remove}.", exception=e) - - -def check_for_update(connection, module_params, vpn_connection_id): - """Determines if there are any tags or routes that need to be updated. Ensures non-modifiable attributes aren't expected to change.""" - tags = module_params.get("tags") - routes = module_params.get("routes") - purge_tags = module_params.get("purge_tags") - purge_routes = module_params.get("purge_routes") +def check_for_routes_update(client, module: AnsibleAWSModule, vpn_connection_id: str) -> Dict[str, Any]: + """Determines if there are any routes that need to be updated. Ensures non-modifiable attributes aren't expected to change.""" + routes = module.params.get("routes") + purge_routes = module.params.get("purge_routes") - vpn_connection = find_connection(connection, module_params, vpn_connection_id=vpn_connection_id) + vpn_connection = find_vpn_connection(client, module, vpn_connection_id) current_attrs = camel_dict_to_snake_dict(vpn_connection) # Initialize changes dict - changes = {"tags_to_add": [], "tags_to_remove": [], "routes_to_add": [], "routes_to_remove": []} + changes: Dict[str, Any] = {"routes_to_add": [], "routes_to_remove": []} - # Get changes to tags - current_tags = boto3_tag_list_to_ansible_dict(current_attrs.get("tags", []), "key", "value") - if tags is None: - changes["tags_to_remove"] = [] - changes["tags_to_add"] = [] - else: - tags_to_add, changes["tags_to_remove"] = compare_aws_tags(current_tags, tags, purge_tags) - changes["tags_to_add"] = ansible_dict_to_boto3_tag_list(tags_to_add) # Get changes to routes if "Routes" in vpn_connection: current_routes = [route["DestinationCidrBlock"] for route in vpn_connection["Routes"]] @@ -638,18 +713,18 @@ def check_for_update(connection, module_params, vpn_connection_id): if attribute in ("tags", "routes", "state"): continue elif attribute == "options": - will_be = module_params.get("static_only", None) + will_be = module.params.get("static_only") is_now = bool(current_attrs[attribute]["static_routes_only"]) attribute = "static_only" elif attribute == "type": - will_be = module_params.get("connection_type", None) + will_be = module.params.get("connection_type") is_now = current_attrs[attribute] else: is_now = current_attrs[attribute] - will_be = module_params.get(attribute, None) + will_be = module.params.get(attribute) if will_be is not None and to_text(will_be) != to_text(is_now): - raise VPNConnectionException( + module.fail_json( msg=( f"You cannot modify {attribute}, the current value of which is {is_now}. Modifiable VPN connection" f" attributes are tags and routes. The value you tried to change it to is {will_be}." @@ -659,42 +734,37 @@ def check_for_update(connection, module_params, vpn_connection_id): return changes -def make_changes(connection, vpn_connection_id, changes): - """changes is a dict with the keys 'tags_to_add', 'tags_to_remove', 'routes_to_add', 'routes_to_remove', - the values of which are lists (generated by check_for_update()). +def make_changes(client, module: AnsibleAWSModule, vpn_connection_id: str, changes: Dict[str, Any]) -> bool: + """changes is a dict with the keys 'routes_to_add', 'routes_to_remove', + the values of which are lists (generated by check_for_routes_update()). """ - changed = False - - if changes["tags_to_add"]: - changed = True - add_tags(connection, vpn_connection_id, changes["tags_to_add"]) - - if changes["tags_to_remove"]: - changed = True - remove_tags(connection, vpn_connection_id, changes["tags_to_remove"]) + changed: bool = False + + if module.params.get("tags") is not None: + changed |= ensure_ec2_tags( + client, + module, + vpn_connection_id, + resource_type="vpn-connection", + tags=module.params.get("tags"), + purge_tags=module.params.get("purge_tags"), + ) if changes["routes_to_add"]: - changed = True - add_routes(connection, vpn_connection_id, changes["routes_to_add"]) + changed |= add_routes(client, module, vpn_connection_id, changes["routes_to_add"]) if changes["routes_to_remove"]: - changed = True - remove_routes(connection, vpn_connection_id, changes["routes_to_remove"]) + changed |= remove_routes(client, module, vpn_connection_id, changes["routes_to_remove"]) return changed -def get_check_mode_results(connection, module_params, vpn_connection_id=None, current_state=None): +def get_check_mode_results( + module_params: Dict[str, Any], vpn_connection_id: Optional[str] = None, current_state: Optional[str] = None +) -> Tuple[bool, Dict[str, Any]]: """Returns the changes that would be made to a VPN Connection""" - state = module_params.get("state") - if state == "absent": - if vpn_connection_id: - return True, {} - else: - return False, {} - - changed = False - results = { + changed: bool = False + results: Dict[str, Any] = { "customer_gateway_configuration": "", "customer_gateway_id": module_params.get("customer_gateway_id"), "vpn_gateway_id": module_params.get("vpn_gateway_id"), @@ -703,8 +773,8 @@ def get_check_mode_results(connection, module_params, vpn_connection_id=None, cu "routes": [module_params.get("routes")], } - # get combined current tags and tags to set present_tags = module_params.get("tags") + # get combined current tags and tags to set if present_tags is None: pass elif current_state and "Tags" in current_state: @@ -717,6 +787,7 @@ def get_check_mode_results(connection, module_params, vpn_connection_id=None, cu results["tags"] = current_tags elif module_params.get("tags"): changed = True + if present_tags: results["tags"] = present_tags @@ -745,75 +816,75 @@ def get_check_mode_results(connection, module_params, vpn_connection_id=None, cu return changed, results -def ensure_present(connection, module_params, check_mode=False): +def ensure_present( + client, module: AnsibleAWSModule, vpn_connection: Optional[Dict[str, Any]] +) -> Tuple[bool, Dict[str, Any]]: """Creates and adds tags to a VPN connection. If the connection already exists update tags.""" - vpn_connection = find_connection(connection, module_params) - changed = False - delay = module_params.get("delay") - max_attempts = module_params.get("wait_timeout") // delay + changed: bool = False + delay = module.params.get("delay") + max_attempts = module.params.get("wait_timeout") // delay # No match but vpn_connection_id was specified. - if not vpn_connection and module_params.get("vpn_connection_id"): - raise VPNConnectionException( - msg="There is no VPN connection available or pending with that id. Did you delete it?" - ) + if not vpn_connection and module.params.get("vpn_connection_id"): + module.fail_json(msg="There is no VPN connection available or pending with that id. Did you delete it?") # Unique match was found. Check if attributes provided differ. elif vpn_connection: vpn_connection_id = vpn_connection["VpnConnectionId"] - # check_for_update returns a dict with the keys tags_to_add, tags_to_remove, routes_to_add, routes_to_remove - changes = check_for_update(connection, module_params, vpn_connection_id) - if check_mode: - return get_check_mode_results(connection, module_params, vpn_connection_id, current_state=vpn_connection) - changed = make_changes(connection, vpn_connection_id, changes) + # check_for_update returns a dict with the keys routes_to_add, routes_to_remove + changes = check_for_routes_update(client, module, vpn_connection_id) + + if module.check_mode: + return get_check_mode_results(module.params, vpn_connection_id, current_state=vpn_connection) + + changed |= make_changes(client, module, vpn_connection_id, changes) # No match was found. Create and tag a connection and add routes. else: changed = True - if check_mode: - return get_check_mode_results(connection, module_params) + + if module.check_mode: + return get_check_mode_results(module.params) + vpn_connection = create_connection( - connection, - customer_gateway_id=module_params.get("customer_gateway_id"), - static_only=module_params.get("static_only"), - vpn_gateway_id=module_params.get("vpn_gateway_id"), - transit_gateway_id=module_params.get("transit_gateway_id"), - connection_type=module_params.get("connection_type"), - tunnel_options=module_params.get("tunnel_options"), + client, + module, + customer_gateway_id=module.params.get("customer_gateway_id"), + static_only=module.params.get("static_only"), + vpn_gateway_id=module.params.get("vpn_gateway_id"), + transit_gateway_id=module.params.get("transit_gateway_id"), + connection_type=module.params.get("connection_type"), + local_ipv4_network_cidr=module.params.get("local_ipv4_network_cidr"), + tunnel_options=module.params.get("tunnel_options"), max_attempts=max_attempts, delay=delay, ) - changes = check_for_update(connection, module_params, vpn_connection["VpnConnectionId"]) - make_changes(connection, vpn_connection["VpnConnectionId"], changes) + + changes = check_for_routes_update(client, module, vpn_connection["VpnConnectionId"]) + make_changes(client, module, vpn_connection["VpnConnectionId"], changes) # get latest version if a change has been made and make tags output nice before returning it if vpn_connection: - vpn_connection = find_connection(connection, module_params, vpn_connection["VpnConnectionId"]) + vpn_connection = find_vpn_connection(client, module, vpn_connection["VpnConnectionId"]) if "Tags" in vpn_connection: vpn_connection["Tags"] = boto3_tag_list_to_ansible_dict(vpn_connection["Tags"]) - return changed, vpn_connection + return (changed, vpn_connection) -def ensure_absent(connection, module_params, check_mode=False): +def ensure_absent(client, module: AnsibleAWSModule, vpn_connection: Dict[str, Any]) -> bool: """Deletes a VPN connection if it exists.""" - vpn_connection = find_connection(connection, module_params) - - if check_mode: - return get_check_mode_results( - connection, module_params, vpn_connection["VpnConnectionId"] if vpn_connection else None - ) - - delay = module_params.get("delay") - max_attempts = module_params.get("wait_timeout") // delay + changed: bool = False if vpn_connection: - delete_connection(connection, vpn_connection["VpnConnectionId"], delay=delay, max_attempts=max_attempts) changed = True - else: - changed = False - return changed, {} + if module.check_mode: + return changed + + delete_connection(client, module, vpn_connection["VpnConnectionId"]) + + return changed def main(): @@ -824,7 +895,18 @@ def main(): tags=dict(type="dict", aliases=["resource_tags"]), connection_type=dict(default="ipsec.1", type="str"), transit_gateway_id=dict(type="str"), - tunnel_options=dict(no_log=True, type="list", default=[], elements="dict"), + local_ipv4_network_cidr=dict(type="str", default="0.0.0.0/0"), + tunnel_options=dict( + no_log=True, + type="list", + default=[], + elements="dict", + options=dict( + TunnelInsideCidr=dict(type="str"), + TunnelInsideIpv6Cidr=dict(type="str"), + PreSharedKey=dict(type="str", no_log=True), + ), + ), static_only=dict(default=False, type="bool"), customer_gateway_id=dict(type="str"), vpn_connection_id=dict(type="str"), @@ -843,21 +925,17 @@ def main(): supports_check_mode=True, mutually_exclusive=mutually_exclusive, ) - connection = module.client("ec2", retry_decorator=VPNRetry.jittered_backoff(retries=10)) + client = module.client("ec2") + response: Dict[str, Any] = {} state = module.params.get("state") - parameters = dict(module.params) - try: - if state == "present": - changed, response = ensure_present(connection, parameters, module.check_mode) - elif state == "absent": - changed, response = ensure_absent(connection, parameters, module.check_mode) - except VPNConnectionException as e: - if e.exception: - module.fail_json_aws(e.exception, msg=e.msg) - else: - module.fail_json(msg=e.msg) + vpn_connection = find_vpn_connection(client, module) + + if state == "present": + changed, response = ensure_present(client, module, vpn_connection) + elif state == "absent": + changed = ensure_absent(client, module, vpn_connection) module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) diff --git a/plugins/modules/ec2_vpc_vpn_info.py b/plugins/modules/ec2_vpc_vpn_info.py index d304e456833..a5d3f65db7d 100644 --- a/plugins/modules/ec2_vpc_vpn_info.py +++ b/plugins/modules/ec2_vpc_vpn_info.py @@ -8,9 +8,9 @@ --- module: ec2_vpc_vpn_info version_added: 1.0.0 -short_description: Gather information about VPN Connections in AWS. +short_description: Gather information about EC2 VPN Connections in AWS description: - - Gather information about VPN Connections in AWS. + - Gather information about EC2 VPN Connections in AWS. author: - Madhura Naniwadekar (@Madhura-CSI) options: @@ -23,7 +23,7 @@ default: {} vpn_connection_ids: description: - - Get details of a specific VPN connections using vpn connection ID/IDs. This value should be provided as a list. + - Get details of specific EC2 VPN Connection(s) using vpn connection ID/IDs. This value should be provided as a list. required: false type: list elements: str @@ -36,33 +36,34 @@ EXAMPLES = r""" # # Note: These examples do not set authentication details, see the AWS Guide for details. -- name: Gather information about all vpn connections +- name: Gather information about all EC2 VPN Connections community.aws.ec2_vpc_vpn_info: -- name: Gather information about a filtered list of vpn connections, based on tags +- name: Gather information about a filtered list of EC2 VPN Connections, based on tags community.aws.ec2_vpc_vpn_info: filters: - "tag:Name": test-connection + "tag:Name": "test-connection" register: vpn_conn_info -- name: Gather information about vpn connections by specifying connection IDs. +- name: Gather information about EC2 VPN Connections by specifying connection IDs community.aws.ec2_vpc_vpn_info: filters: - vpn-gateway-id: vgw-cbe66beb + "vpn-gateway-id": "vgw-cbe66beb" register: vpn_conn_info """ RETURN = r""" vpn_connections: - description: List of one or more VPN Connections. + description: List of one or more EC2 VPN Connections. + type: list + elements: dict returned: always - type: complex contains: category: description: The category of the VPN connection. returned: always type: str - sample: VPN + sample: "VPN" customer_gatway_configuration: description: The configuration information for the VPN connection's customer gateway (in the native XML format). returned: always @@ -71,50 +72,112 @@ description: The ID of the customer gateway at your end of the VPN connection. returned: always type: str - sample: cgw-17a53c37 + sample: "cgw-17a53c37" + gateway_association_state: + description: The current state of the gateway association. + type: str + sample: "associated" options: description: The VPN connection options. - returned: always - type: dict - sample: { - "static_routes_only": false - } + type: list + elements: dict + contains: + static_routes_only: + description: If the VPN connection only allows static routes. + type: bool + sample: true + enable_acceleration: + description: Indicates whether acceleration is enabled for the VPN connection. + type: bool + sample: false + local_ipv4_network_cidr: + description: The IPv4 CIDR on the customer gateway (on-premises) side of the VPN connection. + type: str + sample: "0.0.0.0/0" + outside_ip_address_type: + description: The external IP address of the VPN tunnel. + type: str + sample: "PublicIpv4" + remote_ipv4_network_cidr: + description: The IPv4 CIDR on the Amazon Web Services side of the VPN connection. + type: str + sample: "0.0.0.0/0" + tunnel_inside_ip_version: + description: Indicates whether the VPN tunnels process IPv4 or IPv6 traffic. + type: str + sample: "ipv4" + tunnel_options: + description: Indicates the VPN tunnel options. + type: list + elements: dict + sample: [ + { + "log_options": { + "cloud_watch_log_options": { + "log_enabled": false + } + }, + "outside_ip_address": "34.225.101.10", + "pre_shared_key": "8n7hnjNE8zhIt4VpMOIfcrw6XnUTHLW9", + "tunnel_inside_cidr": "169.254.31.8/30" + }, + ] + contains: + log_options: + description: Options for logging VPN tunnel activity. + type: dict + contains: + cloud_watch_log_options: + description: Options for sending VPN tunnel logs to CloudWatch. + type: dict + outside_ip_address: + description: The external IP address of the VPN tunnel. + type: str + pre_shared_key: + description: + - The pre-shared key (PSK) to establish initial authentication between the + virtual private gateway and the customer gateway. + type: str + tunnel_inside_cidr: + description: The range of inside IPv4 addresses for the tunnel. + type: str routes: description: List of static routes associated with the VPN connection. returned: always - type: complex + type: list + elements: dict contains: destination_cidr_block: - description: The CIDR block associated with the local subnet of the customer data center. - returned: always + description: + - The CIDR block associated with the local subnet of the customer data center. + type: str + source: + description: Indicates how the routes were provided. type: str - sample: 10.0.0.0/16 state: description: The current state of the static route. - returned: always type: str - sample: available state: description: The current state of the VPN connection. returned: always type: str - sample: available + sample: "available" tags: description: Any tags assigned to the VPN connection. returned: always type: dict sample: { - "Name": "test-conn" + "Name": "test-conn" } type: description: The type of VPN connection. returned: always type: str - sample: ipsec.1 + sample: "ipsec.1" vgw_telemetry: description: Information about the VPN tunnel. returned: always - type: complex + type: dict contains: accepted_route_count: description: The number of accepted routes. @@ -130,17 +193,17 @@ description: The Internet-routable IP address of the virtual private gateway's outside interface. returned: always type: str - sample: 13.127.79.191 + sample: "13.127.79.191" status: description: The status of the VPN tunnel. returned: always type: str - sample: DOWN + sample: "DOWN" status_message: description: If an error occurs, a description of the error. returned: always type: str - sample: IPSEC IS DOWN + sample: "IPSEC IS DOWN" certificate_arn: description: The Amazon Resource Name of the virtual private gateway tunnel endpoint certificate. returned: when a private certificate is used for authentication @@ -150,50 +213,51 @@ description: The ID of the VPN connection. returned: always type: str - sample: vpn-f700d5c0 + sample: "vpn-f700d5c0" vpn_gateway_id: description: The ID of the virtual private gateway at the AWS side of the VPN connection. returned: always type: str - sample: vgw-cbe56bfb + sample: "vgw-cbe56bfb" """ import json - -try: - from botocore.exceptions import BotoCoreError - from botocore.exceptions import ClientError -except ImportError: - pass # caught by AnsibleAWSModule +from typing import Any +from typing import Dict +from typing import NoReturn from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_vpn_connections from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict from ansible_collections.amazon.aws.plugins.module_utils.transformation import ansible_dict_to_boto3_filter_list from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule -def date_handler(obj): +def date_handler(obj: Dict[str, Any]) -> Dict[str, Any]: return obj.isoformat() if hasattr(obj, "isoformat") else obj -def list_vpn_connections(connection, module): - params = dict() +def list_vpn_connections(client, module: AnsibleAWSModule) -> NoReturn: + params: Dict[str, Any] = {} params["Filters"] = ansible_dict_to_boto3_filter_list(module.params.get("filters")) params["VpnConnectionIds"] = module.params.get("vpn_connection_ids") try: - result = json.loads(json.dumps(connection.describe_vpn_connections(**params), default=date_handler)) + result = json.loads(json.dumps(describe_vpn_connections(client, **params), default=date_handler)) except ValueError as e: - module.fail_json_aws(e, msg="Cannot validate JSON data") - except (ClientError, BotoCoreError) as e: + module.fail_json(e, msg="Cannot validate JSON data") + except AnsibleEC2Error as e: module.fail_json_aws(e, msg="Could not describe customer gateways") - snaked_vpn_connections = [camel_dict_to_snake_dict(vpn_connection) for vpn_connection in result["VpnConnections"]] + + snaked_vpn_connections = [camel_dict_to_snake_dict(vpn_connection) for vpn_connection in result] if snaked_vpn_connections: for vpn_connection in snaked_vpn_connections: vpn_connection["tags"] = boto3_tag_list_to_ansible_dict(vpn_connection.get("tags", [])) + module.exit_json(changed=False, vpn_connections=snaked_vpn_connections) diff --git a/tests/integration/targets/ec2_vpc_vpn/tasks/main.yml b/tests/integration/targets/ec2_vpc_vpn/tasks/main.yml index 9514d7cf350..6a9f9125688 100644 --- a/tests/integration/targets/ec2_vpc_vpn/tasks/main.yml +++ b/tests/integration/targets/ec2_vpc_vpn/tasks/main.yml @@ -1,5 +1,5 @@ --- -- name: 'ec2_vpc_vpn_info integration tests' +- name: EC2 VPN Connection integration tests collections: - amazon.aws module_defaults: @@ -11,8 +11,8 @@ block: # ============================================================ - - name: create a VPC - ec2_vpc_net: + - name: Create a VPC + amazon.aws.ec2_vpc_net: name: "{{ resource_prefix }}-vpc" state: present cidr_block: "10.0.0.0/26" @@ -21,27 +21,27 @@ Description: "Created by ansible-test" register: vpc_result - - name: create vpn gateway and attach it to vpc - ec2_vpc_vgw: + - name: Create an EC2 VPC gateway and attach it to VPC + community.aws.ec2_vpc_vgw: state: present vpc_id: '{{ vpc_result.vpc.id }}' name: "{{ resource_prefix }}-vgw" register: vgw - - name: create customer gateway - ec2_customer_gateway: + - name: Create customer gateway + community.aws.ec2_customer_gateway: bgp_asn: 12345 ip_address: 1.2.3.4 name: testcgw register: cgw - - name: create transit gateway - ec2_transit_gateway: + - name: Create transit gateway + community.aws.ec2_transit_gateway: description: "Transit Gateway for vpn attachment" register: tgw - - name: create vpn connection, with customer gateway, vpn_gateway_id and transit_gateway - ec2_vpc_vpn: + - name: Create an EC2 VPN Connection, with customer gateway, vpn_gateway_id and transit_gateway + community.aws.ec2_vpc_vpn: customer_gateway_id: '{{ cgw.gateway.customer_gateway.customer_gateway_id }}' vpn_gateway_id: '{{ vgw.vgw.id }}' transit_gateway_id: '{{ tgw.transit_gateway.transit_gateway_id }}' @@ -49,38 +49,38 @@ register: result ignore_errors: true - - name: assert creation of vpn failed - assert: + - name: Assert creation of vpn failed + ansible.builtin.assert: that: - result is failed - result.msg == "parameters are mutually exclusive: vpn_gateway_id|transit_gateway_id" - - - name: create vpn connection, with customer gateway and transit_gateway - ec2_vpc_vpn: + - name: Create EC2 VPN Connection, with customer gateway and transit_gateway + community.aws.ec2_vpc_vpn: customer_gateway_id: '{{ cgw.gateway.customer_gateway.customer_gateway_id }}' transit_gateway_id: '{{ tgw.transit_gateway.transit_gateway_id }}' state: present + wait_timeout: 1000 register: tgw_vpn - name: Store ID of VPN - set_fact: + ansible.builtin.set_fact: vpn_id: '{{ tgw_vpn.vpn_connection_id }}' # ============================================================ - - name: test success with no parameters - ec2_vpc_vpn_info: + - name: Test success with no parameters + community.aws.ec2_vpc_vpn_info: register: result - - name: assert success with no parameters - assert: + - name: Assert success with no parameters + ansible.builtin.assert: that: - 'result.changed == false' - 'result.vpn_connections != []' # ============================================================ - - name: Delete vpn created with transit gateway - ec2_vpc_vpn: + - name: Delete EC2 VPN Connection created with transit gateway + community.aws.ec2_vpc_vpn: state: absent vpn_connection_id: '{{ vpn_id }}' register: result @@ -91,38 +91,38 @@ # ============================================================ - - name: create vpn connection, with customer gateway and vpn gateway - ec2_vpc_vpn: + - name: Create EC2 VPN Connection, with customer gateway and vpn gateway + community.aws.ec2_vpc_vpn: customer_gateway_id: '{{ cgw.gateway.customer_gateway.customer_gateway_id }}' vpn_gateway_id: '{{ vgw.vgw.id }}' state: present register: vpn - - name: Store ID of VPN - set_fact: + - name: Store ID of the EC2 VPN Connection + ansible.builtin.set_fact: vpn_id: '{{ vpn.vpn_connection_id }}' # ============================================================ - - name: test success with no parameters - ec2_vpc_vpn_info: + - name: Test success with no parameters + community.aws.ec2_vpc_vpn_info: register: result - - name: assert success with no parameters - assert: + - name: Assert success with no parameters + ansible.builtin.assert: that: - 'result.changed == false' - 'result.vpn_connections != []' - - name: test success with customer gateway id as a filter - ec2_vpc_vpn_info: + - name: Test success with customer gateway id as a filter + community.aws.ec2_vpc_vpn_info: filters: customer-gateway-id: '{{ cgw.gateway.customer_gateway.customer_gateway_id }}' vpn-connection-id: '{{ vpn.vpn_connection_id }}' register: result - - name: assert success with customer gateway id as filter - assert: + - name: Assert success with customer gateway id as filter + ansible.builtin.assert: that: - 'result.changed == false' - 'result.vpn_connections != []' @@ -133,53 +133,57 @@ # ============================================================ - - name: delete vpn connection (check) - ec2_vpc_vpn: + - name: Delete EC2 VPN Connection (check_mode) + community.aws.ec2_vpc_vpn: state: absent vpn_connection_id: '{{ vpn_id }}' register: result - check_mode: True + check_mode: true - - assert: + - name: Assert EC2 VPN Connection is deleted (check_mode) + ansible.builtin.assert: that: - result is changed - - name: delete vpn connection - ec2_vpc_vpn: + - name: Delete EC2 VPN Connection + community.aws.ec2_vpc_vpn: state: absent vpn_connection_id: '{{ vpn_id }}' register: result - - assert: + - name: Assert EC2 VPN Connection is deleted + ansible.builtin.assert: that: - result is changed - - name: delete vpn connection - idempotency (check) - ec2_vpc_vpn: + - name: Delete EC2 VPN Connection - idempotency (check) + community.aws.ec2_vpc_vpn: state: absent vpn_connection_id: '{{ vpn_id }}' register: result - check_mode: True + check_mode: true - - assert: + - name: Assert result has not changed (idempotency check_mode) + ansible.builtin.assert: that: - result is not changed - - name: delete vpn connection - idempotency - ec2_vpc_vpn: + - name: Delete EC2 VPN Connection - idempotency + community.aws.ec2_vpc_vpn: state: absent vpn_connection_id: '{{ vpn_id }}' register: result - - assert: + - name: Assert result has not changed (idempotency) + ansible.builtin.assert: that: - result is not changed # ============================================================ always: - - name: delete vpn connection - ec2_vpc_vpn: + - name: Delete EC2 VPN Connection + community.aws.ec2_vpc_vpn: state: absent vpn_connection_id: '{{ vpn.vpn_connection_id }}' register: result @@ -188,8 +192,8 @@ until: result is not failed ignore_errors: true - - name: delete customer gateway - ec2_customer_gateway: + - name: Delete customer gateway + community.aws.ec2_customer_gateway: state: absent ip_address: 1.2.3.4 name: testcgw @@ -200,8 +204,8 @@ until: result is not failed ignore_errors: true - - name: delete vpn gateway - ec2_vpc_vgw: + - name: Delete VPN gateway + community.aws.ec2_vpc_vgw: state: absent vpn_gateway_id: '{{ vgw.vgw.id }}' register: result @@ -210,8 +214,8 @@ until: result is not failed ignore_errors: true - - name: delete vpc - ec2_vpc_net: + - name: Delete VPC + amazon.aws.ec2_vpc_net: name: "{{ resource_prefix }}-vpc" state: absent cidr_block: "10.0.0.0/26" @@ -221,8 +225,8 @@ until: result is not failed ignore_errors: true - - name: delete transit gateway - ec2_transit_gateway: + - name: Delete transit gateway + community.aws.ec2_transit_gateway: transit_gateway_id: '{{ tgw.transit_gateway.transit_gateway_id }}' state: absent ignore_errors: true diff --git a/tests/integration/targets/ec2_vpc_vpn/tasks/tags.yml b/tests/integration/targets/ec2_vpc_vpn/tasks/tags.yml index fb97f01faab..21ea2cfd618 100644 --- a/tests/integration/targets/ec2_vpc_vpn/tasks/tags.yml +++ b/tests/integration/targets/ec2_vpc_vpn/tasks/tags.yml @@ -34,61 +34,62 @@ # ============================================================ - - name: (check) add tags - ec2_vpc_vpn: + - name: Add tags (check_mode) + community.aws.ec2_vpc_vpn: tags: '{{ first_tags }}' state: 'present' register: tag_vpn - check_mode: True + check_mode: true - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - - name: add tags - ec2_vpc_vpn: + - name: Add tags + community.aws.ec2_vpc_vpn: tags: '{{ first_tags }}' state: 'present' register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: {} + - name: Get EC2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: {} register: tag_vpn_info - - name: verify the tags were added - assert: + - name: Verify the tags were added + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - tag_vpn_info.vpn_connections[0].vpn_connection_id == vpn_id - tag_vpn_info.vpn_connections[0].tags == first_tags - - name: (check) add tags - IDEMPOTENCY - ec2_vpc_vpn: + - name: Add tags - IDEMPOTENCY (check_mode) + community.aws.ec2_vpc_vpn: tags: '{{ first_tags }}' state: 'present' register: tag_vpn - check_mode: True + check_mode: true - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id - - name: add tags - IDEMPOTENCY - ec2_vpc_vpn: + - name: Add tags - IDEMPOTENCY + community.aws.ec2_vpc_vpn: tags: '{{ first_tags }}' state: 'present' register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: {} + + - name: Get EC2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: {} register: tag_vpn_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id @@ -97,80 +98,66 @@ # ============================================================ -# - name: get VPC VPN facts by filter -# ec2_vpc_vpn_info: -# filters: -# 'tag:Name': '{{ vgw_name }}' -# vpn_connection_ids: '{{ omit }}' -# register: tag_vpn_info -# -# - name: assert the facts are the same as before -# assert: -# that: -# - tag_vpn_info.vpn_connections | length == 1 -# - tag_vpn.vpn_connection_id == vpn_id -# - tag_vpn_info.vpn_connections[0].vpn_connection_id == vpn_id - - # ============================================================ - - - name: (check) modify tags with purge - ec2_vpc_vpn: + - name: Modify tags with purge (check_mode) + community.aws.ec2_vpc_vpn: tags: '{{ second_tags }}' state: 'present' purge_tags: true register: tag_vpn - check_mode: True + check_mode: true - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - - name: modify tags with purge - ec2_vpc_vpn: + - name: Modify tags with purge + community.aws.ec2_vpc_vpn: tags: '{{ second_tags }}' state: 'present' purge_tags: true register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: + + - name: Get EC2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: register: tag_vpn_info - - name: verify the tags were added - assert: + - name: Verify the tags were added + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - tag_vpn_info.vpn_connections[0].vpn_connection_id == vpn_id - tag_vpn_info.vpn_connections[0].tags == second_tags - - name: (check) modify tags with purge - IDEMPOTENCY - ec2_vpc_vpn: + - name: Modify tags with purge - IDEMPOTENCY (check_mode) + community.aws.ec2_vpc_vpn: tags: '{{ second_tags }}' state: 'present' purge_tags: true register: tag_vpn check_mode: True - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id - - name: modify tags with purge - IDEMPOTENCY - ec2_vpc_vpn: + - name: Modify tags with purge - IDEMPOTENCY + community.aws.ec2_vpc_vpn: tags: '{{ second_tags }}' state: 'present' purge_tags: true register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: + + - name: Get EC2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: register: tag_vpn_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id @@ -179,64 +166,66 @@ # ============================================================ - - name: (check) modify tags without purge - ec2_vpc_vpn: + - name: Modify tags without purge (check_mode) + community.aws.ec2_vpc_vpn: tags: '{{ third_tags }}' state: 'present' purge_tags: False register: tag_vpn check_mode: True - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - - name: modify tags without purge - ec2_vpc_vpn: + - name: Modify tags without purge + community.aws.ec2_vpc_vpn: tags: '{{ third_tags }}' state: 'present' purge_tags: False register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: + + - name: Get EC2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: register: tag_vpn_info - name: verify the tags were added - assert: + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - tag_vpn_info.vpn_connections[0].vpn_connection_id == vpn_id - tag_vpn_info.vpn_connections[0].tags == final_tags - - name: (check) modify tags without purge - IDEMPOTENCY - ec2_vpc_vpn: + - name: Modify tags without purge - IDEMPOTENCY (check_mode) + community.aws.ec2_vpc_vpn: tags: '{{ third_tags }}' state: 'present' - purge_tags: False + purge_tags: false register: tag_vpn - check_mode: True + check_mode: true - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id - - name: modify tags without purge - IDEMPOTENCY - ec2_vpc_vpn: + - name: Modify tags without purge - IDEMPOTENCY + community.aws.ec2_vpc_vpn: tags: '{{ third_tags }}' state: 'present' - purge_tags: False + purge_tags: false register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: + + - name: Get EC2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: register: tag_vpn_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id @@ -245,28 +234,29 @@ # ============================================================ - - name: (check) No change to tags without setting tags - ec2_vpc_vpn: + - name: No change to tags without setting tag (check_mode) + community.aws.ec2_vpc_vpn: state: 'present' register: tag_vpn - check_mode: True + check_mode: true - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id - name: No change to tags without setting tags - ec2_vpc_vpn: + community.aws.ec2_vpc_vpn: state: 'present' register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: + + - name: Get CE2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: register: tag_vpn_info - - name: verify no tags were added - assert: + - name: Verify no tags were added + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id @@ -275,63 +265,65 @@ # ============================================================ - - name: (check) remove tags - ec2_vpc_vpn: + - name: Remove tags (check_mode) + community.aws.ec2_vpc_vpn: tags: {} state: 'present' - purge_tags: True + purge_tags: true register: tag_vpn - check_mode: True + check_mode: true - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - - name: remove tags - ec2_vpc_vpn: + - name: Remove tags + community.aws.ec2_vpc_vpn: tags: {} - state: 'present' - purge_tags: True + state: present + purge_tags: true register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: + + - name: Get EC2 VPC VPN facts + community.aws.ec2_vpc_vpn_info: register: tag_vpn_info - - name: verify the tags were removed - assert: + - name: Verify the tags were removed + ansible.builtin.assert: that: - tag_vpn is changed - tag_vpn.vpn_connection_id == vpn_id - tag_vpn_info.vpn_connections[0].vpn_connection_id == vpn_id - - name: (check) remove tags - IDEMPOTENCY - ec2_vpc_vpn: + - name: Remove tags - IDEMPOTENCY (check_mode) + community.aws.ec2_vpc_vpn: tags: {} state: 'present' - purge_tags: True + purge_tags: true register: tag_vpn - check_mode: True + check_mode: true - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id - - name: remove tags - IDEMPOTENCY - ec2_vpc_vpn: + - name: Remove tags - IDEMPOTENCY + community.aws.ec2_vpc_vpn: tags: {} state: 'present' - purge_tags: True + purge_tags: true register: tag_vpn - - name: get VPC VPN facts - ec2_vpc_vpn_info: + + - name: Get VPC VPN facts + community.aws.ec2_vpc_vpn_info: register: tag_vpn_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - tag_vpn is not changed - tag_vpn.vpn_connection_id == vpn_id diff --git a/tests/unit/plugins/modules/test_ec2_vpc_vpn.py b/tests/unit/plugins/modules/test_ec2_vpc_vpn.py index 2b5db4226dd..8a7d2dee494 100644 --- a/tests/unit/plugins/modules/test_ec2_vpc_vpn.py +++ b/tests/unit/plugins/modules/test_ec2_vpc_vpn.py @@ -1,435 +1,263 @@ # (c) 2017 Red Hat Inc. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -__metaclass__ = type - -import os +from unittest.mock import MagicMock +from unittest.mock import Mock import pytest -import ansible_collections.amazon.aws.plugins.module_utils.retries as aws_retries -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_conn -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_aws_connection_info - -# Magic... Incorrectly identified by pylint as unused -# isort: off -# pylint: disable=unused-import -from ansible_collections.amazon.aws.tests.unit.utils.amazon_placebo_fixtures import maybe_sleep -from ansible_collections.amazon.aws.tests.unit.utils.amazon_placebo_fixtures import placeboify - -# pylint: enable=unused-import -# isort: on - from ansible_collections.community.aws.plugins.modules import ec2_vpc_vpn -class FailException(Exception): - pass - - -class FakeModule(object): - def __init__(self, **kwargs): - self.params = kwargs - - def fail_json_aws(self, *args, **kwargs): - self.exit_args = args - self.exit_kwargs = kwargs - raise FailException("FAIL") - - def fail_json(self, *args, **kwargs): - self.exit_args = args - self.exit_kwargs = kwargs - raise FailException("FAIL") - - def exit_json(self, *args, **kwargs): - self.exit_args = args - self.exit_kwargs = kwargs - - -def get_vgw(connection): - # see if two vgw exist and return them if so - vgw = connection.describe_vpn_gateways(Filters=[{"Name": "tag:Ansible_VPN", "Values": ["Test"]}]) - if len(vgw["VpnGateways"]) >= 2: - return [vgw["VpnGateways"][0]["VpnGatewayId"], vgw["VpnGateways"][1]["VpnGatewayId"]] - # otherwise create two and return them - vgw_1 = connection.create_vpn_gateway(Type="ipsec.1") - vgw_2 = connection.create_vpn_gateway(Type="ipsec.1") - for resource in (vgw_1, vgw_2): - connection.create_tags( - Resources=[resource["VpnGateway"]["VpnGatewayId"]], Tags=[{"Key": "Ansible_VPN", "Value": "Test"}] - ) - return [vgw_1["VpnGateway"]["VpnGatewayId"], vgw_2["VpnGateway"]["VpnGatewayId"]] - - -def get_cgw(connection): - # see if two cgw exist and return them if so - cgw = connection.describe_customer_gateways( - DryRun=False, - Filters=[{"Name": "state", "Values": ["available"]}, {"Name": "tag:Name", "Values": ["Ansible-CGW"]}], - ) - if len(cgw["CustomerGateways"]) >= 2: - return [cgw["CustomerGateways"][0]["CustomerGatewayId"], cgw["CustomerGateways"][1]["CustomerGatewayId"]] - # otherwise create and return them - cgw_1 = connection.create_customer_gateway(DryRun=False, Type="ipsec.1", PublicIp="9.8.7.6", BgpAsn=65000) - cgw_2 = connection.create_customer_gateway(DryRun=False, Type="ipsec.1", PublicIp="5.4.3.2", BgpAsn=65000) - for resource in (cgw_1, cgw_2): - connection.create_tags( - Resources=[resource["CustomerGateway"]["CustomerGatewayId"]], Tags=[{"Key": "Ansible-CGW", "Value": "Test"}] - ) - return [cgw_1["CustomerGateway"]["CustomerGatewayId"], cgw_2["CustomerGateway"]["CustomerGatewayId"]] - - -def get_dependencies(): - if os.getenv("PLACEBO_RECORD"): - module = FakeModule(**{}) - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - connection = boto3_conn( - module, conn_type="client", resource="ec2", region=region, endpoint=ec2_url, **aws_connect_kwargs - ) - vgw = get_vgw(connection) - cgw = get_cgw(connection) +@pytest.fixture +def ansible_module(): + module = MagicMock() + module.check_mode = False + module.params = {"delay": 5, "wait_timeout": 30} + module.fail_json.side_effect = SystemExit(1) + module.fail_json_aws.side_effect = SystemExit(1) + + return module + + +@pytest.mark.parametrize( + "vpn_connections, expected_result, expected_exception", + [ + # Case 1: Single VPN connection available + ( + [{"VpnConnectionId": "vpn-123", "State": "available"}], + {"VpnConnectionId": "vpn-123", "State": "available"}, + None, + ), + # Case 2: Multiple valid VPN connections available (expecting an exception) + ( + [ + {"VpnConnectionId": "vpn-123", "State": "available"}, + {"VpnConnectionId": "vpn-456", "State": "available"}, + ], + None, + "More than one matching VPN connection was found. To modify or delete a VPN please specify vpn_connection_id or add filters.", + ), + # Case 3: No VPN connections available + ([], None, None), + # Case 4: Multiple connections with one deleted (expecting the viable connection) + ( + [ + {"VpnConnectionId": "vpn-123", "State": "deleted"}, + {"VpnConnectionId": "vpn-456", "State": "available"}, + ], + {"VpnConnectionId": "vpn-456", "State": "available"}, + None, + ), + ], +) +def test_find_connection_response(ansible_module, vpn_connections, expected_result, expected_exception): + if expected_exception: + with pytest.raises(SystemExit) as e: # Assuming fail_json raises SystemExit + ec2_vpc_vpn.find_connection_response(ansible_module, vpn_connections) + assert e.value.code == 1 # Ensure exit code is as expected + # Check that the message is the same as expected + assert str(ansible_module.fail_json.call_args[1]["msg"]) == expected_exception else: - vgw = ["vgw-35d70c2b", "vgw-32d70c2c"] - cgw = ["cgw-6113c87f", "cgw-9e13c880"] - - return cgw, vgw - - -def setup_mod_conn(placeboify, params): - conn = placeboify.client("ec2") - retry_decorator = aws_retries.AWSRetry.jittered_backoff() - wrapped_conn = aws_retries.RetryingBotoClientWrapper(conn, retry_decorator) - m = FakeModule(**params) - return m, wrapped_conn - - -def make_params(cgw, vgw, tags=None, filters=None, routes=None): - tags = {} if tags is None else tags - filters = {} if filters is None else filters - routes = [] if routes is None else routes - - return { - "customer_gateway_id": cgw, - "static_only": True, - "vpn_gateway_id": vgw, - "connection_type": "ipsec.1", - "purge_tags": True, - "tags": tags, - "filters": filters, + result = ec2_vpc_vpn.find_connection_response(ansible_module, vpn_connections) + assert result == expected_result + + +@pytest.mark.parametrize( + "vpn_connection_id, filters, describe_response, expected_result, expected_exception", + [ + # Case 1: Single VPN connection found + ( + "vpn-123", + None, + {"VpnConnections": [{"VpnConnectionId": "vpn-123", "State": "available"}]}, + {"VpnConnectionId": "vpn-123", "State": "available"}, + None, + ), + # Case 2: Multiple VPN connections found (expecting an exception) + ( + "vpn-123", + None, + { + "VpnConnections": [ + {"VpnConnectionId": "vpn-123", "State": "available"}, + {"VpnConnectionId": "vpn-456", "State": "available"}, + ] + }, + None, + "More than one matching VPN connection was found. To modify or delete a VPN please specify vpn_connection_id or add filters.", + ), + # Case 3: No VPN connections found + ("vpn-123", None, {"VpnConnections": []}, None, None), + ], +) +def test_find_vpn_connection( + ansible_module, vpn_connection_id, filters, describe_response, expected_result, expected_exception +): + client = Mock() + ansible_module.params = {"vpn_connection_id": vpn_connection_id, "filters": filters} + + # Mock the describe_vpn_connections function + client.describe_vpn_connections.return_value = describe_response if describe_response else {} + + if expected_exception: + if "More than one matching VPN connection" in expected_exception: + with pytest.raises(SystemExit) as e: + ec2_vpc_vpn.find_vpn_connection(client, ansible_module) + # Check that the exception message matches the expected exception + assert str(ansible_module.fail_json.call_args[1]["msg"]) == expected_exception + else: + result = ec2_vpc_vpn.find_vpn_connection(client, ansible_module) + assert result == expected_result + + +@pytest.mark.parametrize( + "provided_filters, expected_result, expected_exception", + [ + ({"cgw": "cgw-123"}, [{"Name": "customer-gateway-id", "Values": ["cgw-123"]}], None), + ({"invalid_filter": "value"}, None, "invalid_filter is not a valid filter."), + ( + {"tags": {"key1": "value1", "key2": "value2"}}, + [{"Name": "tag:key1", "Values": ["value1"]}, {"Name": "tag:key2", "Values": ["value2"]}], + None, + ), + ({"static-routes-only": True}, [{"Name": "option.static-routes-only", "Values": ["true"]}], None), + ], +) +def test_create_filter(ansible_module, provided_filters, expected_result, expected_exception): + if expected_exception: + with pytest.raises(SystemExit) as e: + ec2_vpc_vpn.create_filter(ansible_module, provided_filters) + # Check that the exception message matches the expected exception + assert str(ansible_module.fail_json.call_args[1]["msg"]) == expected_exception + else: + result = ec2_vpc_vpn.create_filter(ansible_module, provided_filters) + assert result == expected_result + + +@pytest.mark.parametrize( + "params, expected_result, expected_exception", + [ + # Case 1: Successful creation of a VPN connection + ( + {"customer_gateway_id": "cgw-123", "vpn_gateway_id": "vgw-123", "static_only": True}, + {"VpnConnectionId": "vpn-123"}, + None, + ), + # Case 3: Missing required parameters (simulating failure) + ( + {"customer_gateway_id": None, "vpn_gateway_id": "vgw-123", "static_only": True}, + None, + "No matching connection was found. To create a new connection you must provide customer_gateway_id" + + " and one of either transit_gateway_id or vpn_gateway_id.", + ), + # Case 4: Both customer gateway and VPN gateway are None + ( + {"customer_gateway_id": None, "vpn_gateway_id": None, "static_only": False}, + None, + "No matching connection was found. To create a new connection you must provide customer_gateway_id" + + " and one of either transit_gateway_id or vpn_gateway_id.", + ), + # Case 5: Optional parameters passed (e.g., static routes) + ( + {"customer_gateway_id": "cgw-123", "vpn_gateway_id": "vgw-123", "static_only": True}, + {"VpnConnectionId": "vpn-456"}, + None, + ), + ], +) +def test_create_connection(ansible_module, params, expected_result, expected_exception): + client = Mock() + ansible_module.params = params + + if expected_exception: + client.create_vpn_connection.side_effect = Exception("AWS Error") + with pytest.raises(SystemExit) as e: # Assuming fail_json raises SystemExit + ec2_vpc_vpn.create_connection( + client, + ansible_module, + params["customer_gateway_id"], + params["static_only"], + params["vpn_gateway_id"], + None, + None, + None, + None, + None, + ) + # Check that the exception message matches the expected exception + assert str(ansible_module.fail_json.call_args[1]["msg"]) == expected_exception + else: + client.create_vpn_connection.return_value = {"VpnConnection": expected_result} + result = ec2_vpc_vpn.create_connection( + client, + ansible_module, + params["customer_gateway_id"], + params["static_only"], + params["vpn_gateway_id"], + None, + None, + None, + None, + None, + ) + assert result == expected_result + + +@pytest.mark.parametrize( + "vpn_connection_id, routes, purge_routes, current_routes, expected_result", + [ + # Case 1: No changes in routes + ( + "vpn-123", + ["10.0.0.0/16"], + False, + [{"DestinationCidrBlock": "10.0.0.0/16"}], + {"routes_to_add": [], "routes_to_remove": []}, + ), + # Case 3: Old routes empty, new routes not empty + ("vpn-123", ["10.0.1.0/16"], False, [], {"routes_to_add": ["10.0.1.0/16"], "routes_to_remove": []}), + # Case 4: New routes empty, old routes not empty + ( + "vpn-123", + [], + False, + [{"DestinationCidrBlock": "10.0.0.0/16"}], + {"routes_to_add": [], "routes_to_remove": []}, + ), + # Case 5: Purge routes - removing non-existent routes + ( + "vpn-123", + ["10.0.1.0/16"], + True, + [{"DestinationCidrBlock": "10.0.0.0/16"}], + {"routes_to_add": ["10.0.1.0/16"], "routes_to_remove": ["10.0.0.0/16"]}, + ), + # Case 6: Both old and new routes are empty + ("vpn-123", [], False, [], {"routes_to_add": [], "routes_to_remove": []}), + # Case 7: Purge routes with existing routes + ( + "vpn-123", + [], + True, + [{"DestinationCidrBlock": "10.0.0.0/16"}], + {"routes_to_add": [], "routes_to_remove": ["10.0.0.0/16"]}, + ), + ], +) +def test_check_for_routes_update( + ansible_module, vpn_connection_id, routes, purge_routes, current_routes, expected_result +): + ansible_module.params = { "routes": routes, - "delay": 15, - "wait_timeout": 600, + "purge_routes": purge_routes, } + # Mock the find_vpn_connection function + client = MagicMock() + ec2_vpc_vpn.find_vpn_connection = Mock(return_value={"Routes": current_routes}) -def make_conn(placeboify, module, connection): - customer_gateway_id = module.params["customer_gateway_id"] - static_only = module.params["static_only"] - vpn_gateway_id = module.params["vpn_gateway_id"] - connection_type = module.params["connection_type"] - changed = True - vpn = ec2_vpc_vpn.create_connection(connection, customer_gateway_id, static_only, vpn_gateway_id, connection_type) - return changed, vpn - - -def tear_down_conn(placeboify, connection, vpn_connection_id): - ec2_vpc_vpn.delete_connection(connection, vpn_connection_id, delay=15, max_attempts=40) - - -def setup_req(placeboify, number_of_results=1): - """returns dependencies for VPN connections""" - assert number_of_results in (1, 2) - results = [] - cgw, vgw = get_dependencies() - for each in range(0, number_of_results): - params = make_params(cgw[each], vgw[each]) - m, conn = setup_mod_conn(placeboify, params) - vpn = ec2_vpc_vpn.ensure_present(conn, params)[1] - - results.append({"module": m, "connection": conn, "vpn": vpn, "params": params}) - if number_of_results == 1: - return results[0] - else: - return results[0], results[1] - - -def test_find_connection_vpc_conn_id(placeboify, maybe_sleep): - # setup dependencies for 2 vpn connections - dependencies = setup_req(placeboify, 2) - dep1, dep2 = dependencies[0], dependencies[1] - params1, vpn1, _m1, conn1 = dep1["params"], dep1["vpn"], dep1["module"], dep1["connection"] - _params2, vpn2, _m2, conn2 = dep2["params"], dep2["vpn"], dep2["module"], dep2["connection"] - - # find the connection with a vpn_connection_id and assert it is the expected one - assert ( - vpn1["VpnConnectionId"] - == ec2_vpc_vpn.find_connection(conn1, params1, vpn1["VpnConnectionId"])["VpnConnectionId"] - ) - - tear_down_conn(placeboify, conn1, vpn1["VpnConnectionId"]) - tear_down_conn(placeboify, conn2, vpn2["VpnConnectionId"]) - - -def test_find_connection_filters(placeboify, maybe_sleep): - # setup dependencies for 2 vpn connections - dependencies = setup_req(placeboify, 2) - dep1, dep2 = dependencies[0], dependencies[1] - params1, vpn1, _m1, conn1 = dep1["params"], dep1["vpn"], dep1["module"], dep1["connection"] - params2, vpn2, _m2, conn2 = dep2["params"], dep2["vpn"], dep2["module"], dep2["connection"] - - # update to different tags - params1.update(tags={"Wrong": "Tag"}) - params2.update(tags={"Correct": "Tag"}) - ec2_vpc_vpn.ensure_present(conn1, params1) - ec2_vpc_vpn.ensure_present(conn2, params2) - - # create some new parameters for a filter - params = {"filters": {"tags": {"Correct": "Tag"}}} - - # find the connection that has the parameters above - found = ec2_vpc_vpn.find_connection(conn1, params) - - # assert the correct connection was found - assert found["VpnConnectionId"] == vpn2["VpnConnectionId"] - - # delete the connections - tear_down_conn(placeboify, conn1, vpn1["VpnConnectionId"]) - tear_down_conn(placeboify, conn2, vpn2["VpnConnectionId"]) - - -def test_find_connection_insufficient_filters(placeboify, maybe_sleep): - # get list of customer gateways and virtual private gateways - cgw, vgw = get_dependencies() - - # create two connections with the same tags - params = make_params(cgw[0], vgw[0], tags={"Correct": "Tag"}) - params2 = make_params(cgw[1], vgw[1], tags={"Correct": "Tag"}) - m, conn = setup_mod_conn(placeboify, params) - m2, conn2 = setup_mod_conn(placeboify, params2) - vpn1 = ec2_vpc_vpn.ensure_present(conn, m.params)[1] - vpn2 = ec2_vpc_vpn.ensure_present(conn2, m2.params)[1] - - # reset the parameters so only filtering by tags will occur - m.params = {"filters": {"tags": {"Correct": "Tag"}}} - - expected_message = "More than one matching VPN connection was found" - # assert that multiple matching connections have been found - with pytest.raises(ec2_vpc_vpn.VPNConnectionException, match=expected_message): - ec2_vpc_vpn.find_connection(conn, m.params) - - # delete the connections - tear_down_conn(placeboify, conn, vpn1["VpnConnectionId"]) - tear_down_conn(placeboify, conn, vpn2["VpnConnectionId"]) - - -def test_find_connection_nonexistent(placeboify, maybe_sleep): - # create parameters but don't create a connection with them - params = {"filters": {"tags": {"Correct": "Tag"}}} - m, conn = setup_mod_conn(placeboify, params) - - # try to find a connection with matching parameters and assert None are found - assert ec2_vpc_vpn.find_connection(conn, m.params) is None - - -def test_create_connection(placeboify, maybe_sleep): - # get list of customer gateways and virtual private gateways - cgw, vgw = get_dependencies() - - # create a connection - params = make_params(cgw[0], vgw[0]) - m, conn = setup_mod_conn(placeboify, params) - changed, vpn = ec2_vpc_vpn.ensure_present(conn, m.params) - - # assert that changed is true and that there is a connection id - assert changed is True - assert "VpnConnectionId" in vpn - - # delete connection - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) - - -def test_create_connection_that_exists(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - params, vpn, _m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - - # try to recreate the same connection - changed, vpn2 = ec2_vpc_vpn.ensure_present(conn, params) - - # nothing should have changed - assert changed is False - assert vpn["VpnConnectionId"] == vpn2["VpnConnectionId"] - - # delete connection - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) - - -def test_modify_deleted_connection(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - _params, vpn, m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - - # delete it - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) - - # try to update the deleted connection - m.params.update(vpn_connection_id=vpn["VpnConnectionId"]) - expected_message = "no VPN connection available or pending with that id" - with pytest.raises(ec2_vpc_vpn.VPNConnectionException, match=expected_message): - ec2_vpc_vpn.ensure_present(conn, m.params) - - -def test_delete_connection(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - _params, vpn, m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - - # delete it - changed, vpn = ec2_vpc_vpn.ensure_absent(conn, m.params) - - assert changed is True - assert vpn == {} - - -def test_delete_nonexistent_connection(placeboify, maybe_sleep): - # create parameters and ensure any connection matching (None) is deleted - params = {"filters": {"tags": {"ThisConnection": "DoesntExist"}}, "delay": 15, "wait_timeout": 600} - m, conn = setup_mod_conn(placeboify, params) - changed, vpn = ec2_vpc_vpn.ensure_absent(conn, m.params) - - assert changed is False - assert vpn == {} - - -def test_check_for_update_tags(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - _params, vpn, m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - - # add and remove a number of tags - m.params["tags"] = {"One": "one", "Two": "two"} - ec2_vpc_vpn.ensure_present(conn, m.params) - m.params["tags"] = {"Two": "two", "Three": "three", "Four": "four"} - changes = ec2_vpc_vpn.check_for_update(conn, m.params, vpn["VpnConnectionId"]) - - flat_dict_changes = boto3_tag_list_to_ansible_dict(changes["tags_to_add"]) - correct_changes = boto3_tag_list_to_ansible_dict( - [{"Key": "Three", "Value": "three"}, {"Key": "Four", "Value": "four"}] - ) - assert flat_dict_changes == correct_changes - assert changes["tags_to_remove"] == ["One"] - - # delete connection - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) - - -def test_check_for_update_nonmodifiable_attr(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - params, vpn, m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - current_vgw = params["vpn_gateway_id"] - - # update a parameter that isn't modifiable - m.params.update(vpn_gateway_id="invalidchange") - - expected_message = f"You cannot modify vpn_gateway_id, the current value of which is {current_vgw}. Modifiable VPN connection attributes are" - with pytest.raises(ec2_vpc_vpn.VPNConnectionException, match=expected_message): - ec2_vpc_vpn.check_for_update(conn, m.params, vpn["VpnConnectionId"]) - - # delete connection - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) - - -def test_add_tags(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - params, vpn, _m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - - # add a tag to the connection - ec2_vpc_vpn.add_tags(conn, vpn["VpnConnectionId"], add=[{"Key": "Ansible-Test", "Value": "VPN"}]) - - # assert tag is there - current_vpn = ec2_vpc_vpn.find_connection(conn, params) - assert current_vpn["Tags"] == [{"Key": "Ansible-Test", "Value": "VPN"}] - - # delete connection - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) - - -def test_remove_tags(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - params, vpn, _m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - - # remove a tag from the connection - ec2_vpc_vpn.remove_tags(conn, vpn["VpnConnectionId"], remove=["Ansible-Test"]) - - # assert the tag is gone - current_vpn = ec2_vpc_vpn.find_connection(conn, params) - assert "Tags" not in current_vpn - - # delete connection - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) - - -def test_add_routes(placeboify, maybe_sleep): - # setup dependencies for 1 vpn connection - dependencies = setup_req(placeboify, 1) - params, vpn, _m, conn = ( - dependencies["params"], - dependencies["vpn"], - dependencies["module"], - dependencies["connection"], - ) - - # create connection with a route - ec2_vpc_vpn.add_routes(conn, vpn["VpnConnectionId"], ["195.168.2.0/24", "196.168.2.0/24"]) - - # assert both routes are there - current_vpn = ec2_vpc_vpn.find_connection(conn, params) - assert set(each["DestinationCidrBlock"] for each in current_vpn["Routes"]) == set( - ["195.168.2.0/24", "196.168.2.0/24"] - ) - - # delete connection - tear_down_conn(placeboify, conn, vpn["VpnConnectionId"]) + # Call the function and check results + result = ec2_vpc_vpn.check_for_routes_update(client, ansible_module, vpn_connection_id) + assert result == expected_result