From 7b516590372e0a32498864a3445033d79474cbab Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:24:32 +0200 Subject: [PATCH 1/9] Prepare modules ec2_vpc_egress_igw for promotion (#2152) SUMMARY Refactor module ec2_vpc_egress_igw to use shared code from amazon.aws.plugins.module_utils.ec2 ec2_vpc_egress_igw - add support for tagging ISSUE TYPE Feature Pull Request New Module Pull Request COMPONENT NAME ec2_vpc_egress_igw Reviewed-by: Alina Buzachis Reviewed-by: Bikouo Aubin Reviewed-by: GomathiselviS --- ...23-refactor-ec2_vpc_egress_igw-modules.yml | 4 + plugins/modules/ec2_vpc_egress_igw.py | 191 +++++++++++------- .../targets/ec2_vpc_egress_igw/meta/main.yml | 1 - .../targets/ec2_vpc_egress_igw/tasks/main.yml | 159 +++++++++++---- 4 files changed, 238 insertions(+), 117 deletions(-) create mode 100644 changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml delete mode 100644 tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml diff --git a/changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml b/changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml new file mode 100644 index 00000000000..c67e90cc8d4 --- /dev/null +++ b/changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml @@ -0,0 +1,4 @@ +--- +minor_changes: + - ec2_vpc_egress_igw - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` util (https://github.com/ansible-collections/community.aws/pull/2152). + - ec2_vpc_egress_igw - Add the possibility to update/add tags on Egress only internet gateway (https://github.com/ansible-collections/community.aws/pull/2152). \ No newline at end of file diff --git a/plugins/modules/ec2_vpc_egress_igw.py b/plugins/modules/ec2_vpc_egress_igw.py index 1bd65f501c8..8a1a520b7aa 100644 --- a/plugins/modules/ec2_vpc_egress_igw.py +++ b/plugins/modules/ec2_vpc_egress_igw.py @@ -25,10 +25,13 @@ default: present choices: [ 'present', 'absent' ] type: str +notes: + - Support for O(tags) and O(purge_tags) was added in release 9.0.0. extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules - amazon.aws.boto3 + - amazon.aws.tags.modules """ EXAMPLES = r""" @@ -36,10 +39,15 @@ # Ensure that the VPC has an Internet Gateway. # The Internet Gateway ID is can be accessed via {{eigw.gateway_id}} for use in setting up NATs etc. -- community.aws.ec2_vpc_egress_igw: +- name: Create Egress internet only gateway + community.aws.ec2_vpc_egress_igw: vpc_id: vpc-abcdefgh state: present - register: eigw + +- name: Delete Egress internet only gateway + community.aws.ec2_vpc_egress_igw: + vpc_id: vpc-abcdefgh + state: absent """ RETURN = r""" @@ -53,22 +61,30 @@ returned: always type: str sample: vpc-012345678 +tags: + description: Any tags assigned to the internet gateway. + returned: always + type: dict """ -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule +from typing import Any +from typing import Dict +from typing import Optional +from typing import Union from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_egress_only_internet_gateway +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_egress_only_internet_gateway +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_egress_only_internet_gateways +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.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule -def delete_eigw(module, connection, eigw_id): +def delete_eigw(module: AnsibleAWSModule, connection, eigw_id: str) -> Dict[str, Union[str, bool]]: """ Delete EIGW. @@ -76,27 +92,23 @@ def delete_eigw(module, connection, eigw_id): connection : boto3 client connection object eigw_id : ID of the EIGW to delete """ - changed = False - try: - response = connection.delete_egress_only_internet_gateway( - aws_retry=True, DryRun=module.check_mode, EgressOnlyInternetGatewayId=eigw_id + vpc_id = module.params.get("vpc_id") + + if module.check_mode: + return dict( + changed=True, msg=f"Would have deleted Egress internet only Gateway id '{eigw_id}' if not in check mode." ) - except is_boto3_error_code("DryRunOperation"): - changed = True - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Could not delete Egress-Only Internet Gateway {eigw_id} from VPC {module.vpc_id}") - if not module.check_mode: - changed = response.get("ReturnCode", False) + try: + changed = delete_egress_only_internet_gateway(connection, egress_only_internet_gateway_id=eigw_id) + except AnsibleEC2Error as e: + module.fail_json_aws(e) - return changed + return dict(changed=changed) -def create_eigw(module, connection, vpc_id): +def create_eigw(module: AnsibleAWSModule, connection, vpc_id: str) -> Dict[str, Union[str, bool]]: """ Create EIGW. @@ -104,43 +116,35 @@ def create_eigw(module, connection, vpc_id): connection : boto3 client connection object vpc_id : ID of the VPC we are operating on """ + + if module.check_mode: + return dict(changed=True, msg="Would have created Egress internet only Gateway if not in check mode.") + gateway_id = None changed = False try: - response = connection.create_egress_only_internet_gateway( - aws_retry=True, DryRun=module.check_mode, VpcId=vpc_id - ) - except is_boto3_error_code("DryRunOperation"): - # When boto3 method is run with DryRun=True it returns an error on success - # We need to catch the error and return something valid + response = create_egress_only_internet_gateway(connection, vpc_id=vpc_id, tags=module.params.get("tags")) changed = True - except is_boto3_error_code("InvalidVpcID.NotFound") as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"invalid vpc ID '{vpc_id}' provided") - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Could not create Egress-Only Internet Gateway for vpc ID {vpc_id}") - - if not module.check_mode: - gateway = response.get("EgressOnlyInternetGateway", {}) - state = gateway.get("Attachments", [{}])[0].get("State") - gateway_id = gateway.get("EgressOnlyInternetGatewayId") - - if gateway_id and state in ("attached", "attaching"): - changed = True - else: - # EIGW gave back a bad attachment state or an invalid response so we error out - module.fail_json( - msg=f"Unable to create and attach Egress Only Internet Gateway to VPCId: {vpc_id}. Bad or no state in response", - **camel_dict_to_snake_dict(response), - ) + except AnsibleEC2Error as e: + module.fail_json_aws(e) + + gateway = response.get("EgressOnlyInternetGateway", {}) + state = gateway.get("Attachments", [{}])[0].get("State") + gateway_id = gateway.get("EgressOnlyInternetGatewayId") + tags = boto3_tag_list_to_ansible_dict(gateway.get("Tags", [])) + + if not gateway_id or state not in ("attached", "attaching"): + # EIGW gave back a bad attachment state or an invalid response so we error out + module.fail_json( + msg=f"Unable to create and attach Egress Only Internet Gateway to VPCId: {vpc_id}. Bad or no state in response", + **camel_dict_to_snake_dict(response), + ) - return changed, gateway_id + return dict(changed=changed, gateway_id=gateway_id, tags=tags) -def describe_eigws(module, connection, vpc_id): +def find_egress_only_igw(module: AnsibleAWSModule, connection, vpc_id: str) -> Optional[Dict[str, Any]]: """ Describe EIGWs. @@ -148,43 +152,80 @@ def describe_eigws(module, connection, vpc_id): connection : boto3 client connection object vpc_id : ID of the VPC we are operating on """ - gateway_id = None + result = None try: - response = connection.describe_egress_only_internet_gateways(aws_retry=True) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Could not get list of existing Egress-Only Internet Gateways") + for eigw in describe_egress_only_internet_gateways(connection): + for attachment in eigw.get("Attachments", []): + if attachment.get("VpcId") == vpc_id and attachment.get("State") in ("attached", "attaching"): + return { + "gateway_id": eigw.get("EgressOnlyInternetGatewayId"), + "tags": boto3_tag_list_to_ansible_dict(eigw.get("Tags", [])), + } + except AnsibleEC2Error as e: + module.fail_json_aws(e) - for eigw in response.get("EgressOnlyInternetGateways", []): - for attachment in eigw.get("Attachments", []): - if attachment.get("VpcId") == vpc_id and attachment.get("State") in ("attached", "attaching"): - gateway_id = eigw.get("EgressOnlyInternetGatewayId") + return result - return gateway_id + +def ensure_present(connection, module: AnsibleAWSModule, existing: Optional[Dict[str, Any]]) -> None: + vpc_id = module.params.get("vpc_id") + result = dict(vpc_id=vpc_id, changed=False) + + if not existing: + result.update(create_eigw(module, connection, vpc_id)) + else: + egress_only_igw_id = existing.get("gateway_id") + changed = False + result = existing + tags = module.params.get("tags") + purge_tags = module.params.get("purge_tags") + if tags is not None: + changed = ensure_ec2_tags( + connection, + module, + egress_only_igw_id, + resource_type="egress-only-internet-gateway", + tags=tags, + purge_tags=purge_tags, + ) + result.update(dict(changed=changed, vpc_id=vpc_id)) + + module.exit_json(**result) + + +def ensure_absent(connection, module: AnsibleAWSModule, existing: Optional[Dict[str, Any]]) -> None: + vpc_id = module.params.get("vpc_id") + if not existing: + module.exit_json(changed=False, msg=f"No Egress only internet gateway attached to the VPC id '{vpc_id}'") + + egress_only_igw_id = existing.get("gateway_id") + result = dict(gateway_id=egress_only_igw_id, vpc_id=vpc_id, changed=False) + result.update(delete_eigw(module, connection, egress_only_igw_id)) + module.exit_json(**result) def main(): - argument_spec = dict(vpc_id=dict(required=True), state=dict(default="present", choices=["present", "absent"])) + argument_spec = dict( + vpc_id=dict(required=True), + state=dict(default="present", choices=["present", "absent"]), + tags=dict(type="dict", aliases=["resource_tags"]), + purge_tags=dict(type="bool", default=True), + ) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - retry_decorator = AWSRetry.jittered_backoff(retries=10) - connection = module.client("ec2", retry_decorator=retry_decorator) + connection = module.client("ec2") vpc_id = module.params.get("vpc_id") state = module.params.get("state") - eigw_id = describe_eigws(module, connection, vpc_id) - - result = dict(gateway_id=eigw_id, vpc_id=vpc_id) - changed = False - - if state == "present" and not eigw_id: - changed, result["gateway_id"] = create_eigw(module, connection, vpc_id) - elif state == "absent" and eigw_id: - changed = delete_eigw(module, connection, eigw_id) + existing_egress_only_igw = find_egress_only_igw(module, connection, vpc_id) - module.exit_json(changed=changed, **result) + if state == "present": + ensure_present(connection, module, existing_egress_only_igw) + else: + ensure_absent(connection, module, existing_egress_only_igw) if __name__ == "__main__": diff --git a/tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml b/tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml deleted file mode 100644 index 32cf5dda7ed..00000000000 --- a/tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml +++ /dev/null @@ -1 +0,0 @@ -dependencies: [] diff --git a/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml b/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml index 75fff0e4e22..56bb89decaf 100644 --- a/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml +++ b/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml @@ -1,7 +1,5 @@ --- -- name: 'ec2_vpc_egress_igw integration tests' - collections: - - amazon.aws +- name: Run ec2_vpc_egress_igw integration tests module_defaults: group/aws: access_key: '{{ aws_access_key }}' @@ -11,35 +9,36 @@ block: # ============================================================ - - name: test failure with no parameters - ec2_vpc_egress_igw: + - name: Test failure with no parameters + community.aws.ec2_vpc_egress_igw: register: result ignore_errors: true - - name: assert failure with no parameters - assert: + - name: Assert failure with no parameters + ansible.builtin.assert: that: - - 'result.failed' + - result is failed - 'result.msg == "missing required arguments: vpc_id"' # ============================================================ - - name: test failure with non-existent VPC ID - ec2_vpc_egress_igw: + - name: Test failure with non-existent VPC ID + community.aws.ec2_vpc_egress_igw: state: present vpc_id: vpc-02394e50abc1807e8 register: result ignore_errors: true - - name: assert failure with non-existent VPC ID - assert: + - name: Assert failure with non-existent VPC ID + ansible.builtin.assert: that: - - 'result.failed' - - 'result.error.code == "InvalidVpcID.NotFound"' - - '"invalid vpc ID" in result.msg' + - result is failed + - e_msg in result.exception + vars: + e_msg: "The vpc ID 'vpc-02394e50abc1807e8' does not exist" # ============================================================ - - 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.232.232.128/26" @@ -49,55 +48,133 @@ register: vpc_result # ============================================================ - - name: create egress-only internet gateway (expected changed=true) - ec2_vpc_egress_igw: + - name: Create egress-only internet gateway using check_mode=true + community.aws.ec2_vpc_egress_igw: + state: present + vpc_id: "{{ vpc_result.vpc.id }}" + register: vpc_eigw_create_check_mode + check_mode: true + + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_create_check_mode is changed + + # # ============================================================ + - name: Create egress-only internet gateway (expected changed=true) + community.aws.ec2_vpc_egress_igw: state: present vpc_id: "{{ vpc_result.vpc.id }}" register: vpc_eigw_create - - name: assert creation happened (expected changed=true) - assert: + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: that: - - 'vpc_eigw_create' - - 'vpc_eigw_create.gateway_id.startswith("eigw-")' - - 'vpc_eigw_create.vpc_id == vpc_result.vpc.id' + - vpc_eigw_create is changed - # ============================================================ - - name: attempt to recreate egress-only internet gateway on VPC (expected changed=false) - ec2_vpc_egress_igw: + # # ============================================================ + - name: Create egress-only internet gateway once again (idempotency) + community.aws.ec2_vpc_egress_igw: state: present vpc_id: "{{ vpc_result.vpc.id }}" - register: vpc_eigw_recreate + register: vpc_eigw_create_idempotency - - name: assert recreation did nothing (expected changed=false) + - name: Assert module returned changed and the Egress IGW was not created assert: that: - - 'vpc_eigw_recreate.changed == False' - - 'vpc_eigw_recreate.gateway_id == vpc_eigw_create.gateway_id' - - 'vpc_eigw_recreate.vpc_id == vpc_eigw_create.vpc_id' + - vpc_eigw_create_idempotency is not changed + - vpc_eigw_create_idempotency.gateway_id == vpc_eigw_create.gateway_id - # ============================================================ - - name: test state=absent (expected changed=true) + # # ============================================================ + - name: Delete egress-only internet gateway (check_mode) ec2_vpc_egress_igw: state: absent vpc_id: "{{ vpc_result.vpc.id }}" + register: vpc_eigw_delete_check_mode + check_mode: true + + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_delete_check_mode is changed + - vpc_eigw_create_idempotency.gateway_id == vpc_eigw_delete_check_mode.gateway_id + + # # ============================================================ + - name: Delete egress-only internet gateway once again (idempotency) + community.aws.ec2_vpc_egress_igw: + state: absent + vpc_id: "{{ vpc_result.vpc.id }}" register: vpc_eigw_delete - - name: assert state=absent (expected changed=true) - assert: + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_delete is changed + - vpc_eigw_create_idempotency.gateway_id == vpc_eigw_delete.gateway_id + + # # ============================================================ + - name: Delete egress-only internet gateway + ec2_vpc_egress_igw: + state: absent + vpc_id: "{{ vpc_result.vpc.id }}" + register: vpc_eigw_delete_idempotency + + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_delete_idempotency is not changed + + ## ============================================================ + ## Tagging + - name: Create Egress only internet gateway with tags + community.aws.ec2_vpc_egress_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + ResourcePrefix: "{{ resource_prefix }}" + VpcId: "{{ vpc_result.vpc.id }}" + register: create_with_tags + + - name: Assert that the Egress IGW was created with tags + ansible.builtin.assert: + that: + - create_with_tags is changed + + - name: Trying to update tags (no change) + community.aws.ec2_vpc_egress_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + ResourcePrefix: "{{ resource_prefix }}" + VpcId: "{{ vpc_result.vpc.id }}" + register: update_tags + + - name: Assert that the Egress IGW was not updated + ansible.builtin.assert: + that: + - update_tags is not changed + + - name: Add tag to existing tags + community.aws.ec2_vpc_egress_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Phase: integration + purge_tags: false + register: add_tag + + - name: Assert that the Egress IGW was created with tags + ansible.builtin.assert: that: - - 'vpc_eigw_delete.changed' + - add_tag is changed always: # ============================================================ - - name: tidy up EIGW - ec2_vpc_egress_igw: + - name: Tidy up EIGW + community.aws.ec2_vpc_egress_igw: state: absent vpc_id: "{{ vpc_result.vpc.id }}" ignore_errors: true - - name: tidy up VPC - ec2_vpc_net: + - name: Tidy up VPC + amazon.aws.ec2_vpc_net: name: "{{ resource_prefix }}-vpc" state: absent cidr_block: "10.232.232.128/26" From 7f11acb15425b200f6fe17600e534d6d113b7c09 Mon Sep 17 00:00:00 2001 From: Mandar Kulkarni Date: Mon, 7 Oct 2024 16:14:53 -0700 Subject: [PATCH 2/9] DNM Migrate elb_classic_lb_info* modules and tests (#2163) Depends-On: ansible-collections/amazon.aws#2315 Remove elb_classic_lb_info* modules and tests These modules have been migrated to amazon.aws Update runtime.yml with redirects to that collection Update ignore files Reviewed-by: Bikouo Aubin Reviewed-by: Alina Buzachis --- ...39-elb_classic_lb_info-refactor-module.yml | 2 - .../fragments/migrate_elb_classic_lb_info.yml | 5 + meta/runtime.yml | 3 +- plugins/modules/elb_classic_lb_info.py | 516 ------------------ .../targets/elb_classic_lb_info/aliases | 1 - .../elb_classic_lb_info/defaults/main.yml | 3 - .../targets/elb_classic_lb_info/meta/main.yml | 1 - .../elb_classic_lb_info/tasks/main.yml | 311 ----------- .../targets/elb_classic_lb_info/vars/main.yml | 2 - 9 files changed, 7 insertions(+), 837 deletions(-) delete mode 100644 changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml create mode 100644 changelogs/fragments/migrate_elb_classic_lb_info.yml delete mode 100644 plugins/modules/elb_classic_lb_info.py delete mode 100644 tests/integration/targets/elb_classic_lb_info/aliases delete mode 100644 tests/integration/targets/elb_classic_lb_info/defaults/main.yml delete mode 100644 tests/integration/targets/elb_classic_lb_info/meta/main.yml delete mode 100644 tests/integration/targets/elb_classic_lb_info/tasks/main.yml delete mode 100644 tests/integration/targets/elb_classic_lb_info/vars/main.yml diff --git a/changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml b/changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml deleted file mode 100644 index e2ee0cb7a91..00000000000 --- a/changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: -- elb_classic_lb_info - Refactor elb_classic_lb_info module (https://github.com/ansible-collections/community.aws/pull/2139). diff --git a/changelogs/fragments/migrate_elb_classic_lb_info.yml b/changelogs/fragments/migrate_elb_classic_lb_info.yml new file mode 100644 index 00000000000..f48c34ee3c4 --- /dev/null +++ b/changelogs/fragments/migrate_elb_classic_lb_info.yml @@ -0,0 +1,5 @@ +--- +breaking_changes: + - elb_classic_lb_info - The module has been migrated from the ``community.aws`` + collection. Playbooks using the Fully Qualified Collection Name for this module + should be updated to use ``amazon.aws.elb_classic_lb_info``. diff --git a/meta/runtime.yml b/meta/runtime.yml index ea50b016258..fffae44294d 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -146,7 +146,6 @@ action_groups: - elasticache_subnet_group - elasticbeanstalk_app - elb_classic_lb - - elb_classic_lb_info - elb_instance - elb_network_lb - elb_target @@ -520,6 +519,8 @@ plugin_routing: redirect: amazon.aws.s3_bucket_info sts_assume_role: redirect: amazon.aws.sts_assume_role + elb_classic_lb_info: + redirect: amazon.aws.elb_classic_lb_info module_utils: route53: redirect: amazon.aws.route53 \ No newline at end of file diff --git a/plugins/modules/elb_classic_lb_info.py b/plugins/modules/elb_classic_lb_info.py deleted file mode 100644 index 2115ca70f1e..00000000000 --- a/plugins/modules/elb_classic_lb_info.py +++ /dev/null @@ -1,516 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Contributors to the Ansible project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -DOCUMENTATION = r""" ---- -module: elb_classic_lb_info -version_added: 1.0.0 -short_description: Gather information about EC2 Classic Elastic Load Balancers in AWS -description: - - Gather information about EC2 Classic Elastic Load Balancers in AWS. -author: - - "Michael Schultz (@mjschultz)" - - "Fernando Jose Pando (@nand0p)" -options: - names: - description: - - List of ELB names to gather information about. Pass this option to gather information about a set of ELBs, otherwise, all ELBs are returned. - type: list - elements: str - default: [] -extends_documentation_fragment: - - amazon.aws.common.modules - - amazon.aws.region.modules - - amazon.aws.boto3 -""" - -EXAMPLES = r""" -# Note: These examples do not set authentication details, see the AWS Guide for details. -# Output format tries to match amazon.aws.elb_classic_lb module input parameters - -- name: Gather information about all ELBs - community.aws.elb_classic_lb_info: - register: elb_info - -- ansible.builtin.debug: - msg: "{{ item.dns_name }}" - loop: "{{ elb_info.elbs }}" - -- name: Gather information about a particular ELB - community.aws.elb_classic_lb_info: - names: frontend-prod-elb - register: elb_info - -- ansible.builtin.debug: - msg: "{{ elb_info.elbs.0.dns_name }}" - -- name: Gather information about a set of ELBs - community.aws.elb_classic_lb_info: - names: - - frontend-prod-elb - - backend-prod-elb - register: elb_info - -- ansible.builtin.debug: - msg: "{{ item.dns_name }}" - loop: "{{ elb_info.elbs }}" -""" - -RETURN = r""" -elbs: - description: A list of load balancers. - returned: always - type: list - elements: dict - contains: - attributes: - description: Information about the load balancer attributes. - returned: always - type: dict - contains: - access_log: - description: Information on whether access logs are enabled or not. - type: dict - sample: { - "enabled": false - } - additional_attributes: - description: Information about additional load balancer attributes. - type: list - elements: dict - sample: [ - { - "key": "elb.http.desyncmitigationmode", - "value": "defensive" - } - ] - connection_draining: - description: - - Information on connection draining configuration of elastic load balancer. - type: dict - sample: { - "enabled": true, - "timeout": 300 - } - contains: - enabled: - description: Whether connection draining is enabled. - type: bool - returned: always - timeout: - description: The maximum time, in seconds, to keep the existing connections open before deregistering the instances. - type: int - returned: always - connection_settings: - description: Information on connection settings. - type: dict - sample: { - "idle_timeout": 60 - } - cross_zone_load_balancing: - description: Information on whether cross zone load balancing is enabled or not. - type: dict - sample: { - "enabled": true - } - availability_zones: - description: The Availability Zones for the load balancer. - type: list - elements: str - returned: always - sample: [ - "us-west-2a" - ] - backend_server_descriptions: - description: Information about your EC2 instances. - type: list - elements: dict - returned: always - sample: [ - { - instance_port: 8085, - policy_names: [ - 'MyPolicy1', - ] - }, - ] - canonical_hosted_zone_name: - description: The DNS name of the load balancer. - type: str - returned: always - sample: "test-123456789.us-west-2.elb.amazonaws.com" - canonical_hosted_zone_name_id: - description: The ID of the Amazon Route 53 hosted zone for the load balancer. - type: str - returned: always - sample: "Z1Z1ZZ5HABSF5" - created_time: - description: The date and time the load balancer was created. - type: str - returned: always - sample: "2024-09-04T17:52:22.270000+00:00" - dns_name: - description: The DNS name of the load balancer. - type: str - returned: "always" - sample: "test-123456789.us-west-2.elb.amazonaws.com" - health_check: - description: Information about the health checks conducted on the load balancer. - type: dict - returned: always - sample: { - "healthy_threshold": 10, - "interval": 5, - "target": "HTTP:80/index.html", - "timeout": 2, - "unhealthy_threshold": 2 - } - contains: - healthy_threshold: - description: The number of consecutive health checks successes required before moving the instance to the Healthy state. - type: int - returned: always - interval: - description: The approximate interval, in seconds, between health checks of an individual instance. - type: int - returned: always - target: - description: The instance being checked. The protocol is either TCP, HTTP, HTTPS, or SSL. The range of valid ports is one (1) through 65535. - type: str - returned: always - timeout: - description: The amount of time, in seconds, during which no response means a failed health check. - type: int - returned: always - unhealthy_threshold: - description: The number of consecutive health checks successes required before moving the instance to the Unhealthy state. - type: int - returned: always - instances: - description: The IDs of the instances for the load balancer. - type: list - elements: dict - returned: always - sample: [ - { - "instance_id": "i-11d1f111ea111111b" - } - ] - instances_inservice: - description: Information about instances for load balancer in state "InService". - type: list - returned: always - sample: [ - "i-11d1f111ea111111b" - ] - instances_inservice_count: - description: Total number of instances for load balancer with state "InService". - type: int - returned: always - sample: 1 - instances_outofservice: - description: Information about instances for load balancer in state "OutOfService". - type: list - returned: always - sample: [ - "i-11d1f111ea111111b" - ] - instances_outofservice_count: - description: Total number of instances for load balancer with state "OutOfService". - type: int - returned: always - sample: 0 - instances_unknownservice: - description: Information about instances for load balancer in state "Unknown". - type: list - returned: always - sample: [ - "i-11d1f111ea111111b" - ] - instances_unknownservice_count: - description: Total number of instances for load balancer with state "Unknown". - type: int - returned: always - sample: 1 - listener_descriptions: - description: Information about the listeners for the load balancer. - type: list - elements: dict - returned: always - sample: [ - { - "listener": { - "instance_port": 80, - "instance_protocol": "HTTP", - "load_balancer_port": 80, - "protocol": "HTTP" - }, - "policy_names": [] - } - ] - load_balancer_name: - description: The name of the elastic load balancer. - type: str - returned: always - sample: "MyLoadBalancer" - policies: - description: Information about the policies defined for the load balancer. - type: dict - returned: always - sample: { - "app_cookie_stickiness_policies": [], - "lb_cookie_stickiness_policies": [], - "other_policies": [] - } - contains: - app_cookie_stickiness_policies: - description: The stickiness policies created using CreateAppCookieStickinessPolicy. - type: list - returned: always - lb_cookie_stickiness_policies: - description: The stickiness policies created using CreateLBCookieStickinessPolicy. - type: list - returned: always - other_policies: - description: The policies other than the stickiness policies. - type: list - returned: always - scheme: - description: The type of load balancer. - type: str - returned: always - sample: "internet-facing" - security_groups: - description: The security groups for the load balancer. - type: list - returned: always - sample: [ - "sg-111111af1111cb111" - ] - source_security_group: - description: - - The security group for the load balancer, - which are used as part of inbound rules for registered instances. - type: dict - returned: always - sample: { - "group_name": "default", - "owner_alias": "721111111111" - } - contains: - group_name: - description: The name of the security group. - type: str - returned: always - owner_alias: - description: The owner of the security group. - type: str - returned: always - subnets: - description: The IDs of the subnets for the load balancer. - type: list - returned: always - sample: [ - "subnet-111111af1111cb111" - ] - tags: - description: The tags associated with a load balancer. - type: dict - returned: always - sample: { - "Env": "Dev", - "Owner": "Dev001" - } - vpc_id: - description: The ID of the VPC for the load balancer. - type: str - returned: always - sample: "vpc-0cc28c9e20d111111" -""" - -from typing import Any -from typing import Dict -from typing import List -from typing import Tuple -from typing import Union - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict - -from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry -from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict - - -def list_elbs(connection: Any, load_balancer_names: List[str]) -> List[Dict]: - """ - List Elastic Load Balancers (ELBs) and their detailed information. - - Parameters: - connection (boto3.client): The Boto3 ELB client object. - load_balancer_names (List[str]): List of ELB names to gather information about. - - Returns: - A list of dictionaries where each dictionary contains informtion about one ELB. - """ - results = [] - - if not load_balancer_names: - for lb in get_all_lb(connection): - results.append(describe_elb(connection, lb)) - - for load_balancer_name in load_balancer_names: - lb = get_lb(connection, load_balancer_name) - if not lb: - continue - results.append(describe_elb(connection, lb)) - return results - - -def describe_elb(connection: Any, lb: Dict[str, Any]) -> Dict[str, Any]: - """ - Describes an Elastic Load Balancer (ELB). - - Parameters: - connection (boto3.client): The Boto3 ELB client object. - lb (Dict): Dictionary containing ELB . - - Returns: - A dictionary with detailed information of the ELB. - """ - description = camel_dict_to_snake_dict(lb) - name = lb["LoadBalancerName"] - instances = lb.get("Instances", []) - description["tags"] = get_tags(connection, name) - description["instances_inservice"], description["instances_inservice_count"] = lb_instance_health( - connection, name, instances, "InService" - ) - description["instances_outofservice"], description["instances_outofservice_count"] = lb_instance_health( - connection, name, instances, "OutOfService" - ) - description["instances_unknownservice"], description["instances_unknownservice_count"] = lb_instance_health( - connection, name, instances, "Unknown" - ) - description["attributes"] = get_lb_attributes(connection, name) - return description - - -@AWSRetry.jittered_backoff() -def get_all_lb(connection: Any) -> List: - """ - Get paginated result for information of all Elastic Load Balancers. - - Parameters: - connection (boto3.client): The Boto3 ELB client object. - - Returns: - A list of dictionaries containing descriptions of all ELBs. - """ - paginator = connection.get_paginator("describe_load_balancers") - return paginator.paginate().build_full_result()["LoadBalancerDescriptions"] - - -def get_lb(connection: Any, load_balancer_name: str) -> Union[Dict[str, Any], List]: - """ - Describes a specific Elastic Load Balancer (ELB) by name. - - Parameters: - connection (boto3.client): The Boto3 ELB client object. - load_balancer_name (str): Name of the ELB to gather information about. - - Returns: - A dictionary with detailed information of the specified ELB. - """ - try: - return connection.describe_load_balancers(aws_retry=True, LoadBalancerNames=[load_balancer_name])[ - "LoadBalancerDescriptions" - ][0] - except is_boto3_error_code("LoadBalancerNotFound"): - return [] - - -def get_lb_attributes(connection: Any, load_balancer_name: str) -> Dict[str, Any]: - """ - Retrieves attributes of specific Elastic Load Balancer (ELB) by name. - - Parameters: - connection (boto3.client): The Boto3 ELB client object. - load_balancer_name (str): Name of the ELB to gather information about. - - Returns: - A dictionary with detailed information of the attributes of specified ELB. - """ - attributes = connection.describe_load_balancer_attributes(aws_retry=True, LoadBalancerName=load_balancer_name).get( - "LoadBalancerAttributes", {} - ) - return camel_dict_to_snake_dict(attributes) - - -def get_tags(connection: Any, load_balancer_name: str) -> Dict[str, Any]: - """ - Retrieves tags of specific Elastic Load Balancer (ELB) by name. - - Parameters: - connection (boto3.client): The Boto3 ELB client object. - load_balancer_name (str): Name of the ELB to gather information about. - - Returns: - A dictionary of tags associated with the specified ELB. - """ - tags = connection.describe_tags(aws_retry=True, LoadBalancerNames=[load_balancer_name])["TagDescriptions"] - if not tags: - return {} - return boto3_tag_list_to_ansible_dict(tags[0]["Tags"]) - - -def lb_instance_health( - connection: Any, load_balancer_name: str, instances: List[Dict[str, Any]], state: str -) -> Tuple[List[str], int]: - """ - Describes the health status of instances associated with a specific Elastic Load Balancer (ELB). - - Parameters: - connection (Any): The Boto3 client object for ELB. - load_balancer_name (str): The name of the ELB. - instances (List[Dict]): List of dictionaries containing instances associated with the ELB. - state (str): The health state to filter by (e.g., "InService", "OutOfService", "Unknown"). - - Returns: - Tuple[List, int]: A tuple containing a list of instance IDs matching state and the count of matching instances. - """ - instance_states = connection.describe_instance_health(LoadBalancerName=load_balancer_name, Instances=instances).get( - "InstanceStates", [] - ) - instate = [instance["InstanceId"] for instance in instance_states if instance["State"] == state] - return instate, len(instate) - - -def main(): - argument_spec = dict( - names=dict(default=[], type="list", elements="str"), - ) - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - connection = module.client("elb", retry_decorator=AWSRetry.jittered_backoff(retries=5, delay=5)) - - try: - elbs = list_elbs(connection, module.params.get("names")) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get load balancer information.") - - module.exit_json(elbs=elbs) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/targets/elb_classic_lb_info/aliases b/tests/integration/targets/elb_classic_lb_info/aliases deleted file mode 100644 index 4ef4b2067d0..00000000000 --- a/tests/integration/targets/elb_classic_lb_info/aliases +++ /dev/null @@ -1 +0,0 @@ -cloud/aws diff --git a/tests/integration/targets/elb_classic_lb_info/defaults/main.yml b/tests/integration/targets/elb_classic_lb_info/defaults/main.yml deleted file mode 100644 index bd059e26ea7..00000000000 --- a/tests/integration/targets/elb_classic_lb_info/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -# defaults file for test_ec2_eip -elb_name: 'ansible-test-{{ tiny_prefix }}-ecli' diff --git a/tests/integration/targets/elb_classic_lb_info/meta/main.yml b/tests/integration/targets/elb_classic_lb_info/meta/main.yml deleted file mode 100644 index 32cf5dda7ed..00000000000 --- a/tests/integration/targets/elb_classic_lb_info/meta/main.yml +++ /dev/null @@ -1 +0,0 @@ -dependencies: [] diff --git a/tests/integration/targets/elb_classic_lb_info/tasks/main.yml b/tests/integration/targets/elb_classic_lb_info/tasks/main.yml deleted file mode 100644 index b09e8807269..00000000000 --- a/tests/integration/targets/elb_classic_lb_info/tasks/main.yml +++ /dev/null @@ -1,311 +0,0 @@ ---- -# __Test Info__ -# Create a self signed cert and upload it to AWS -# http://www.akadia.com/services/ssh_test_certificate.html -# http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/ssl-server-cert.html - -# __Test Outline__ -# -# __elb_classic_lb__ -# create test elb with listeners and certificate -# change AZ's -# change listeners -# remove listeners -# remove elb - -# __elb_classic_lb_info_ -# get nonexistent load balancer - -- module_defaults: - group/aws: - region: "{{ aws_region }}" - access_key: "{{ aws_access_key }}" - secret_key: "{{ aws_secret_key }}" - session_token: "{{ security_token | default(omit) }}" - block: - - # ============================================================ - # create test elb with listeners, certificate, and health check - - - name: Create ELB - elb_classic_lb: - name: "{{ elb_name }}" - state: present - zones: - - "{{ aws_region }}a" - - "{{ aws_region }}b" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - - protocol: http - load_balancer_port: 8080 - instance_port: 8080 - health_check: - ping_protocol: http - ping_port: 80 - ping_path: "/index.html" - response_timeout: 5 - interval: 30 - unhealthy_threshold: 2 - healthy_threshold: 10 - register: create - - - assert: - that: - - create is changed - # We rely on these for the info test, make sure they're what we expect - - aws_region ~ 'a' in create.elb.zones - - aws_region ~ 'b' in create.elb.zones - - create.elb.health_check.healthy_threshold == 10 - - create.elb.health_check.interval == 30 - - create.elb.health_check.target == "HTTP:80/index.html" - - create.elb.health_check.timeout == 5 - - create.elb.health_check.unhealthy_threshold == 2 - - '[80, 80, "HTTP", "HTTP"] in create.elb.listeners' - - '[8080, 8080, "HTTP", "HTTP"] in create.elb.listeners' - - - name: Get ELB info - elb_classic_lb_info: - names: "{{ elb_name }}" - register: info - - - assert: - that: - - info.elbs|length == 1 - - elb.availability_zones|length == 2 - - aws_region ~ 'a' in elb.availability_zones - - aws_region ~ 'b' in elb.availability_zones - - elb.health_check.healthy_threshold == 10 - - elb.health_check.interval == 30 - - elb.health_check.target == "HTTP:80/index.html" - - elb.health_check.timeout == 5 - - elb.health_check.unhealthy_threshold == 2 - - '{"instance_port": 80, "instance_protocol": "HTTP", "load_balancer_port": 80, "protocol": "HTTP"} == listeners[0]' - - '{"instance_port": 8080, "instance_protocol": "HTTP", "load_balancer_port": 8080, "protocol": "HTTP"} == listeners[1]' - vars: - elb: "{{ info.elbs[0] }}" - listeners: "{{ elb.listener_descriptions|map(attribute='listener')|sort(attribute='load_balancer_port') }}" - - # ============================================================ - - # check ports, would be cool, but we are at the mercy of AWS - # to start things in a timely manner - - #- name: check to make sure 80 is listening - # wait_for: host={{ info.elb.dns_name }} port=80 timeout=600 - # register: result - - #- name: assert can connect to port# - # assert: 'result.state == "started"' - - #- name: check to make sure 443 is listening - # wait_for: host={{ info.elb.dns_name }} port=443 timeout=600 - # register: result - - #- name: assert can connect to port# - # assert: 'result.state == "started"' - - # ============================================================ - - # Change AZ's - - - name: Change AZ's - elb_classic_lb: - name: "{{ elb_name }}" - state: present - zones: - - "{{ aws_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_zones: yes - health_check: - ping_protocol: http - ping_port: 80 - ping_path: "/index.html" - response_timeout: 5 - interval: 30 - unhealthy_threshold: 2 - healthy_threshold: 10 - register: update_az - - - assert: - that: - - update_az is changed - - update_az.elb.zones[0] == aws_region ~ 'c' - - - name: Get ELB info after changing AZ's - elb_classic_lb_info: - names: "{{ elb_name }}" - register: info - - - assert: - that: - - elb.availability_zones|length == 1 - - aws_region ~ 'c' in elb.availability_zones[0] - vars: - elb: "{{ info.elbs[0] }}" - - # ============================================================ - - # Update AZ's - - - name: Update AZ's - elb_classic_lb: - name: "{{ elb_name }}" - state: present - zones: - - "{{ aws_region }}a" - - "{{ aws_region }}b" - - "{{ aws_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_zones: yes - register: update_az - - - assert: - that: - - update_az is changed - - aws_region ~ 'a' in update_az.elb.zones - - aws_region ~ 'b' in update_az.elb.zones - - aws_region ~ 'c' in update_az.elb.zones - - - name: Get ELB info after updating AZ's - elb_classic_lb_info: - names: "{{ elb_name }}" - register: info - - - assert: - that: - - elb.availability_zones|length == 3 - - aws_region ~ 'a' in elb.availability_zones - - aws_region ~ 'b' in elb.availability_zones - - aws_region ~ 'c' in elb.availability_zones - vars: - elb: "{{ info.elbs[0] }}" - - # ============================================================ - - # Purge Listeners - - - name: Purge Listeners - elb_classic_lb: - name: "{{ elb_name }}" - state: present - zones: - - "{{ aws_region }}a" - - "{{ aws_region }}b" - - "{{ aws_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 81 - purge_listeners: yes - register: purge_listeners - - - assert: - that: - - purge_listeners is changed - - '[80, 81, "HTTP", "HTTP"] in purge_listeners.elb.listeners' - - purge_listeners.elb.listeners|length == 1 - - - name: Get ELB info after purging listeners - elb_classic_lb_info: - names: "{{ elb_name }}" - register: info - - - assert: - that: - - elb.listener_descriptions|length == 1 - - '{"instance_port": 81, "instance_protocol": "HTTP", "load_balancer_port": 80, "protocol": "HTTP"} == elb.listener_descriptions[0].listener' - vars: - elb: "{{ info.elbs[0] }}" - - - # ============================================================ - - # add Listeners - - - name: Add Listeners - elb_classic_lb: - name: "{{ elb_name }}" - state: present - zones: - - "{{ aws_region }}a" - - "{{ aws_region }}b" - - "{{ aws_region }}c" - listeners: - - protocol: http - load_balancer_port: 8081 - instance_port: 8081 - purge_listeners: no - register: update_listeners - - - assert: - that: - - update_listeners is changed - - '[80, 81, "HTTP", "HTTP"] in update_listeners.elb.listeners' - - '[8081, 8081, "HTTP", "HTTP"] in update_listeners.elb.listeners' - - update_listeners.elb.listeners|length == 2 - - - name: Get ELB info after adding listeners - elb_classic_lb_info: - names: "{{ elb_name }}" - register: info - - - assert: - that: - - elb.listener_descriptions|length == 2 - - '{"instance_port": 81, "instance_protocol": "HTTP", "load_balancer_port": 80, "protocol": "HTTP"} == listeners[0]' - - '{"instance_port": 8081, "instance_protocol": "HTTP", "load_balancer_port": 8081, "protocol": "HTTP"} == listeners[1]' - vars: - elb: "{{ info.elbs[0] }}" - listeners: "{{ elb.listener_descriptions|map(attribute='listener')|sort(attribute='load_balancer_port') }}" - - # ============================================================ - - # Test getting nonexistent load balancer - - name: get nonexistent load balancer - elb_classic_lb_info: - names: "invalid-elb" - register: info - - - assert: - that: - - info.elbs|length==0 - - # Test getting a valid and nonexistent load balancer - - name: get nonexistent load balancer - elb_classic_lb_info: - names: ["{{ elb_name }}", "invalid-elb"] - register: info - - - assert: - that: - - info.elbs|length==1 - - info.elbs[0].load_balancer_name == elb_name - - # ============================================================ - - - name: get all load balancers - elb_classic_lb_info: - names: "{{ omit }}" - register: info - - - assert: - that: - - info.elbs|length>0 - - always: - - # ============================================================ - - name: remove the test load balancer completely - elb_classic_lb: - name: "{{ elb_name }}" - state: absent - register: result - ignore_errors: true diff --git a/tests/integration/targets/elb_classic_lb_info/vars/main.yml b/tests/integration/targets/elb_classic_lb_info/vars/main.yml deleted file mode 100644 index 79194af1ef5..00000000000 --- a/tests/integration/targets/elb_classic_lb_info/vars/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# vars file for test_ec2_elb_lb From 4cf5a6f2f7c1367a20447ac29af0e63f39071afa Mon Sep 17 00:00:00 2001 From: Mandar Kulkarni Date: Wed, 9 Oct 2024 17:02:16 -0700 Subject: [PATCH 3/9] Revert "DNM Migrate elb_classic_lb_info* modules and tests (#2163)" (#2170) This reverts commit 7f11acb. Reverts #2163 #2163 changes are being reverted as it has been agreed to hold off on merging the migration PRs until a plan is set to migrate all planned modules at once. We need to disable the squash strategy in github first to retain commit history. SUMMARY Reviewed-by: GomathiselviS --- ...39-elb_classic_lb_info-refactor-module.yml | 2 + .../fragments/migrate_elb_classic_lb_info.yml | 5 - meta/runtime.yml | 3 +- plugins/modules/elb_classic_lb_info.py | 516 ++++++++++++++++++ .../targets/elb_classic_lb_info/aliases | 1 + .../elb_classic_lb_info/defaults/main.yml | 3 + .../targets/elb_classic_lb_info/meta/main.yml | 1 + .../elb_classic_lb_info/tasks/main.yml | 311 +++++++++++ .../targets/elb_classic_lb_info/vars/main.yml | 2 + 9 files changed, 837 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml delete mode 100644 changelogs/fragments/migrate_elb_classic_lb_info.yml create mode 100644 plugins/modules/elb_classic_lb_info.py create mode 100644 tests/integration/targets/elb_classic_lb_info/aliases create mode 100644 tests/integration/targets/elb_classic_lb_info/defaults/main.yml create mode 100644 tests/integration/targets/elb_classic_lb_info/meta/main.yml create mode 100644 tests/integration/targets/elb_classic_lb_info/tasks/main.yml create mode 100644 tests/integration/targets/elb_classic_lb_info/vars/main.yml diff --git a/changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml b/changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml new file mode 100644 index 00000000000..e2ee0cb7a91 --- /dev/null +++ b/changelogs/fragments/2139-elb_classic_lb_info-refactor-module.yml @@ -0,0 +1,2 @@ +minor_changes: +- elb_classic_lb_info - Refactor elb_classic_lb_info module (https://github.com/ansible-collections/community.aws/pull/2139). diff --git a/changelogs/fragments/migrate_elb_classic_lb_info.yml b/changelogs/fragments/migrate_elb_classic_lb_info.yml deleted file mode 100644 index f48c34ee3c4..00000000000 --- a/changelogs/fragments/migrate_elb_classic_lb_info.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -breaking_changes: - - elb_classic_lb_info - The module has been migrated from the ``community.aws`` - collection. Playbooks using the Fully Qualified Collection Name for this module - should be updated to use ``amazon.aws.elb_classic_lb_info``. diff --git a/meta/runtime.yml b/meta/runtime.yml index fffae44294d..ea50b016258 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -146,6 +146,7 @@ action_groups: - elasticache_subnet_group - elasticbeanstalk_app - elb_classic_lb + - elb_classic_lb_info - elb_instance - elb_network_lb - elb_target @@ -519,8 +520,6 @@ plugin_routing: redirect: amazon.aws.s3_bucket_info sts_assume_role: redirect: amazon.aws.sts_assume_role - elb_classic_lb_info: - redirect: amazon.aws.elb_classic_lb_info module_utils: route53: redirect: amazon.aws.route53 \ No newline at end of file diff --git a/plugins/modules/elb_classic_lb_info.py b/plugins/modules/elb_classic_lb_info.py new file mode 100644 index 00000000000..2115ca70f1e --- /dev/null +++ b/plugins/modules/elb_classic_lb_info.py @@ -0,0 +1,516 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: elb_classic_lb_info +version_added: 1.0.0 +short_description: Gather information about EC2 Classic Elastic Load Balancers in AWS +description: + - Gather information about EC2 Classic Elastic Load Balancers in AWS. +author: + - "Michael Schultz (@mjschultz)" + - "Fernando Jose Pando (@nand0p)" +options: + names: + description: + - List of ELB names to gather information about. Pass this option to gather information about a set of ELBs, otherwise, all ELBs are returned. + type: list + elements: str + default: [] +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. +# Output format tries to match amazon.aws.elb_classic_lb module input parameters + +- name: Gather information about all ELBs + community.aws.elb_classic_lb_info: + register: elb_info + +- ansible.builtin.debug: + msg: "{{ item.dns_name }}" + loop: "{{ elb_info.elbs }}" + +- name: Gather information about a particular ELB + community.aws.elb_classic_lb_info: + names: frontend-prod-elb + register: elb_info + +- ansible.builtin.debug: + msg: "{{ elb_info.elbs.0.dns_name }}" + +- name: Gather information about a set of ELBs + community.aws.elb_classic_lb_info: + names: + - frontend-prod-elb + - backend-prod-elb + register: elb_info + +- ansible.builtin.debug: + msg: "{{ item.dns_name }}" + loop: "{{ elb_info.elbs }}" +""" + +RETURN = r""" +elbs: + description: A list of load balancers. + returned: always + type: list + elements: dict + contains: + attributes: + description: Information about the load balancer attributes. + returned: always + type: dict + contains: + access_log: + description: Information on whether access logs are enabled or not. + type: dict + sample: { + "enabled": false + } + additional_attributes: + description: Information about additional load balancer attributes. + type: list + elements: dict + sample: [ + { + "key": "elb.http.desyncmitigationmode", + "value": "defensive" + } + ] + connection_draining: + description: + - Information on connection draining configuration of elastic load balancer. + type: dict + sample: { + "enabled": true, + "timeout": 300 + } + contains: + enabled: + description: Whether connection draining is enabled. + type: bool + returned: always + timeout: + description: The maximum time, in seconds, to keep the existing connections open before deregistering the instances. + type: int + returned: always + connection_settings: + description: Information on connection settings. + type: dict + sample: { + "idle_timeout": 60 + } + cross_zone_load_balancing: + description: Information on whether cross zone load balancing is enabled or not. + type: dict + sample: { + "enabled": true + } + availability_zones: + description: The Availability Zones for the load balancer. + type: list + elements: str + returned: always + sample: [ + "us-west-2a" + ] + backend_server_descriptions: + description: Information about your EC2 instances. + type: list + elements: dict + returned: always + sample: [ + { + instance_port: 8085, + policy_names: [ + 'MyPolicy1', + ] + }, + ] + canonical_hosted_zone_name: + description: The DNS name of the load balancer. + type: str + returned: always + sample: "test-123456789.us-west-2.elb.amazonaws.com" + canonical_hosted_zone_name_id: + description: The ID of the Amazon Route 53 hosted zone for the load balancer. + type: str + returned: always + sample: "Z1Z1ZZ5HABSF5" + created_time: + description: The date and time the load balancer was created. + type: str + returned: always + sample: "2024-09-04T17:52:22.270000+00:00" + dns_name: + description: The DNS name of the load balancer. + type: str + returned: "always" + sample: "test-123456789.us-west-2.elb.amazonaws.com" + health_check: + description: Information about the health checks conducted on the load balancer. + type: dict + returned: always + sample: { + "healthy_threshold": 10, + "interval": 5, + "target": "HTTP:80/index.html", + "timeout": 2, + "unhealthy_threshold": 2 + } + contains: + healthy_threshold: + description: The number of consecutive health checks successes required before moving the instance to the Healthy state. + type: int + returned: always + interval: + description: The approximate interval, in seconds, between health checks of an individual instance. + type: int + returned: always + target: + description: The instance being checked. The protocol is either TCP, HTTP, HTTPS, or SSL. The range of valid ports is one (1) through 65535. + type: str + returned: always + timeout: + description: The amount of time, in seconds, during which no response means a failed health check. + type: int + returned: always + unhealthy_threshold: + description: The number of consecutive health checks successes required before moving the instance to the Unhealthy state. + type: int + returned: always + instances: + description: The IDs of the instances for the load balancer. + type: list + elements: dict + returned: always + sample: [ + { + "instance_id": "i-11d1f111ea111111b" + } + ] + instances_inservice: + description: Information about instances for load balancer in state "InService". + type: list + returned: always + sample: [ + "i-11d1f111ea111111b" + ] + instances_inservice_count: + description: Total number of instances for load balancer with state "InService". + type: int + returned: always + sample: 1 + instances_outofservice: + description: Information about instances for load balancer in state "OutOfService". + type: list + returned: always + sample: [ + "i-11d1f111ea111111b" + ] + instances_outofservice_count: + description: Total number of instances for load balancer with state "OutOfService". + type: int + returned: always + sample: 0 + instances_unknownservice: + description: Information about instances for load balancer in state "Unknown". + type: list + returned: always + sample: [ + "i-11d1f111ea111111b" + ] + instances_unknownservice_count: + description: Total number of instances for load balancer with state "Unknown". + type: int + returned: always + sample: 1 + listener_descriptions: + description: Information about the listeners for the load balancer. + type: list + elements: dict + returned: always + sample: [ + { + "listener": { + "instance_port": 80, + "instance_protocol": "HTTP", + "load_balancer_port": 80, + "protocol": "HTTP" + }, + "policy_names": [] + } + ] + load_balancer_name: + description: The name of the elastic load balancer. + type: str + returned: always + sample: "MyLoadBalancer" + policies: + description: Information about the policies defined for the load balancer. + type: dict + returned: always + sample: { + "app_cookie_stickiness_policies": [], + "lb_cookie_stickiness_policies": [], + "other_policies": [] + } + contains: + app_cookie_stickiness_policies: + description: The stickiness policies created using CreateAppCookieStickinessPolicy. + type: list + returned: always + lb_cookie_stickiness_policies: + description: The stickiness policies created using CreateLBCookieStickinessPolicy. + type: list + returned: always + other_policies: + description: The policies other than the stickiness policies. + type: list + returned: always + scheme: + description: The type of load balancer. + type: str + returned: always + sample: "internet-facing" + security_groups: + description: The security groups for the load balancer. + type: list + returned: always + sample: [ + "sg-111111af1111cb111" + ] + source_security_group: + description: + - The security group for the load balancer, + which are used as part of inbound rules for registered instances. + type: dict + returned: always + sample: { + "group_name": "default", + "owner_alias": "721111111111" + } + contains: + group_name: + description: The name of the security group. + type: str + returned: always + owner_alias: + description: The owner of the security group. + type: str + returned: always + subnets: + description: The IDs of the subnets for the load balancer. + type: list + returned: always + sample: [ + "subnet-111111af1111cb111" + ] + tags: + description: The tags associated with a load balancer. + type: dict + returned: always + sample: { + "Env": "Dev", + "Owner": "Dev001" + } + vpc_id: + description: The ID of the VPC for the load balancer. + type: str + returned: always + sample: "vpc-0cc28c9e20d111111" +""" + +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple +from typing import Union + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict + + +def list_elbs(connection: Any, load_balancer_names: List[str]) -> List[Dict]: + """ + List Elastic Load Balancers (ELBs) and their detailed information. + + Parameters: + connection (boto3.client): The Boto3 ELB client object. + load_balancer_names (List[str]): List of ELB names to gather information about. + + Returns: + A list of dictionaries where each dictionary contains informtion about one ELB. + """ + results = [] + + if not load_balancer_names: + for lb in get_all_lb(connection): + results.append(describe_elb(connection, lb)) + + for load_balancer_name in load_balancer_names: + lb = get_lb(connection, load_balancer_name) + if not lb: + continue + results.append(describe_elb(connection, lb)) + return results + + +def describe_elb(connection: Any, lb: Dict[str, Any]) -> Dict[str, Any]: + """ + Describes an Elastic Load Balancer (ELB). + + Parameters: + connection (boto3.client): The Boto3 ELB client object. + lb (Dict): Dictionary containing ELB . + + Returns: + A dictionary with detailed information of the ELB. + """ + description = camel_dict_to_snake_dict(lb) + name = lb["LoadBalancerName"] + instances = lb.get("Instances", []) + description["tags"] = get_tags(connection, name) + description["instances_inservice"], description["instances_inservice_count"] = lb_instance_health( + connection, name, instances, "InService" + ) + description["instances_outofservice"], description["instances_outofservice_count"] = lb_instance_health( + connection, name, instances, "OutOfService" + ) + description["instances_unknownservice"], description["instances_unknownservice_count"] = lb_instance_health( + connection, name, instances, "Unknown" + ) + description["attributes"] = get_lb_attributes(connection, name) + return description + + +@AWSRetry.jittered_backoff() +def get_all_lb(connection: Any) -> List: + """ + Get paginated result for information of all Elastic Load Balancers. + + Parameters: + connection (boto3.client): The Boto3 ELB client object. + + Returns: + A list of dictionaries containing descriptions of all ELBs. + """ + paginator = connection.get_paginator("describe_load_balancers") + return paginator.paginate().build_full_result()["LoadBalancerDescriptions"] + + +def get_lb(connection: Any, load_balancer_name: str) -> Union[Dict[str, Any], List]: + """ + Describes a specific Elastic Load Balancer (ELB) by name. + + Parameters: + connection (boto3.client): The Boto3 ELB client object. + load_balancer_name (str): Name of the ELB to gather information about. + + Returns: + A dictionary with detailed information of the specified ELB. + """ + try: + return connection.describe_load_balancers(aws_retry=True, LoadBalancerNames=[load_balancer_name])[ + "LoadBalancerDescriptions" + ][0] + except is_boto3_error_code("LoadBalancerNotFound"): + return [] + + +def get_lb_attributes(connection: Any, load_balancer_name: str) -> Dict[str, Any]: + """ + Retrieves attributes of specific Elastic Load Balancer (ELB) by name. + + Parameters: + connection (boto3.client): The Boto3 ELB client object. + load_balancer_name (str): Name of the ELB to gather information about. + + Returns: + A dictionary with detailed information of the attributes of specified ELB. + """ + attributes = connection.describe_load_balancer_attributes(aws_retry=True, LoadBalancerName=load_balancer_name).get( + "LoadBalancerAttributes", {} + ) + return camel_dict_to_snake_dict(attributes) + + +def get_tags(connection: Any, load_balancer_name: str) -> Dict[str, Any]: + """ + Retrieves tags of specific Elastic Load Balancer (ELB) by name. + + Parameters: + connection (boto3.client): The Boto3 ELB client object. + load_balancer_name (str): Name of the ELB to gather information about. + + Returns: + A dictionary of tags associated with the specified ELB. + """ + tags = connection.describe_tags(aws_retry=True, LoadBalancerNames=[load_balancer_name])["TagDescriptions"] + if not tags: + return {} + return boto3_tag_list_to_ansible_dict(tags[0]["Tags"]) + + +def lb_instance_health( + connection: Any, load_balancer_name: str, instances: List[Dict[str, Any]], state: str +) -> Tuple[List[str], int]: + """ + Describes the health status of instances associated with a specific Elastic Load Balancer (ELB). + + Parameters: + connection (Any): The Boto3 client object for ELB. + load_balancer_name (str): The name of the ELB. + instances (List[Dict]): List of dictionaries containing instances associated with the ELB. + state (str): The health state to filter by (e.g., "InService", "OutOfService", "Unknown"). + + Returns: + Tuple[List, int]: A tuple containing a list of instance IDs matching state and the count of matching instances. + """ + instance_states = connection.describe_instance_health(LoadBalancerName=load_balancer_name, Instances=instances).get( + "InstanceStates", [] + ) + instate = [instance["InstanceId"] for instance in instance_states if instance["State"] == state] + return instate, len(instate) + + +def main(): + argument_spec = dict( + names=dict(default=[], type="list", elements="str"), + ) + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + connection = module.client("elb", retry_decorator=AWSRetry.jittered_backoff(retries=5, delay=5)) + + try: + elbs = list_elbs(connection, module.params.get("names")) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to get load balancer information.") + + module.exit_json(elbs=elbs) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/elb_classic_lb_info/aliases b/tests/integration/targets/elb_classic_lb_info/aliases new file mode 100644 index 00000000000..4ef4b2067d0 --- /dev/null +++ b/tests/integration/targets/elb_classic_lb_info/aliases @@ -0,0 +1 @@ +cloud/aws diff --git a/tests/integration/targets/elb_classic_lb_info/defaults/main.yml b/tests/integration/targets/elb_classic_lb_info/defaults/main.yml new file mode 100644 index 00000000000..bd059e26ea7 --- /dev/null +++ b/tests/integration/targets/elb_classic_lb_info/defaults/main.yml @@ -0,0 +1,3 @@ +--- +# defaults file for test_ec2_eip +elb_name: 'ansible-test-{{ tiny_prefix }}-ecli' diff --git a/tests/integration/targets/elb_classic_lb_info/meta/main.yml b/tests/integration/targets/elb_classic_lb_info/meta/main.yml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/tests/integration/targets/elb_classic_lb_info/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/elb_classic_lb_info/tasks/main.yml b/tests/integration/targets/elb_classic_lb_info/tasks/main.yml new file mode 100644 index 00000000000..b09e8807269 --- /dev/null +++ b/tests/integration/targets/elb_classic_lb_info/tasks/main.yml @@ -0,0 +1,311 @@ +--- +# __Test Info__ +# Create a self signed cert and upload it to AWS +# http://www.akadia.com/services/ssh_test_certificate.html +# http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/ssl-server-cert.html + +# __Test Outline__ +# +# __elb_classic_lb__ +# create test elb with listeners and certificate +# change AZ's +# change listeners +# remove listeners +# remove elb + +# __elb_classic_lb_info_ +# get nonexistent load balancer + +- module_defaults: + group/aws: + region: "{{ aws_region }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + session_token: "{{ security_token | default(omit) }}" + block: + + # ============================================================ + # create test elb with listeners, certificate, and health check + + - name: Create ELB + elb_classic_lb: + name: "{{ elb_name }}" + state: present + zones: + - "{{ aws_region }}a" + - "{{ aws_region }}b" + listeners: + - protocol: http + load_balancer_port: 80 + instance_port: 80 + - protocol: http + load_balancer_port: 8080 + instance_port: 8080 + health_check: + ping_protocol: http + ping_port: 80 + ping_path: "/index.html" + response_timeout: 5 + interval: 30 + unhealthy_threshold: 2 + healthy_threshold: 10 + register: create + + - assert: + that: + - create is changed + # We rely on these for the info test, make sure they're what we expect + - aws_region ~ 'a' in create.elb.zones + - aws_region ~ 'b' in create.elb.zones + - create.elb.health_check.healthy_threshold == 10 + - create.elb.health_check.interval == 30 + - create.elb.health_check.target == "HTTP:80/index.html" + - create.elb.health_check.timeout == 5 + - create.elb.health_check.unhealthy_threshold == 2 + - '[80, 80, "HTTP", "HTTP"] in create.elb.listeners' + - '[8080, 8080, "HTTP", "HTTP"] in create.elb.listeners' + + - name: Get ELB info + elb_classic_lb_info: + names: "{{ elb_name }}" + register: info + + - assert: + that: + - info.elbs|length == 1 + - elb.availability_zones|length == 2 + - aws_region ~ 'a' in elb.availability_zones + - aws_region ~ 'b' in elb.availability_zones + - elb.health_check.healthy_threshold == 10 + - elb.health_check.interval == 30 + - elb.health_check.target == "HTTP:80/index.html" + - elb.health_check.timeout == 5 + - elb.health_check.unhealthy_threshold == 2 + - '{"instance_port": 80, "instance_protocol": "HTTP", "load_balancer_port": 80, "protocol": "HTTP"} == listeners[0]' + - '{"instance_port": 8080, "instance_protocol": "HTTP", "load_balancer_port": 8080, "protocol": "HTTP"} == listeners[1]' + vars: + elb: "{{ info.elbs[0] }}" + listeners: "{{ elb.listener_descriptions|map(attribute='listener')|sort(attribute='load_balancer_port') }}" + + # ============================================================ + + # check ports, would be cool, but we are at the mercy of AWS + # to start things in a timely manner + + #- name: check to make sure 80 is listening + # wait_for: host={{ info.elb.dns_name }} port=80 timeout=600 + # register: result + + #- name: assert can connect to port# + # assert: 'result.state == "started"' + + #- name: check to make sure 443 is listening + # wait_for: host={{ info.elb.dns_name }} port=443 timeout=600 + # register: result + + #- name: assert can connect to port# + # assert: 'result.state == "started"' + + # ============================================================ + + # Change AZ's + + - name: Change AZ's + elb_classic_lb: + name: "{{ elb_name }}" + state: present + zones: + - "{{ aws_region }}c" + listeners: + - protocol: http + load_balancer_port: 80 + instance_port: 80 + purge_zones: yes + health_check: + ping_protocol: http + ping_port: 80 + ping_path: "/index.html" + response_timeout: 5 + interval: 30 + unhealthy_threshold: 2 + healthy_threshold: 10 + register: update_az + + - assert: + that: + - update_az is changed + - update_az.elb.zones[0] == aws_region ~ 'c' + + - name: Get ELB info after changing AZ's + elb_classic_lb_info: + names: "{{ elb_name }}" + register: info + + - assert: + that: + - elb.availability_zones|length == 1 + - aws_region ~ 'c' in elb.availability_zones[0] + vars: + elb: "{{ info.elbs[0] }}" + + # ============================================================ + + # Update AZ's + + - name: Update AZ's + elb_classic_lb: + name: "{{ elb_name }}" + state: present + zones: + - "{{ aws_region }}a" + - "{{ aws_region }}b" + - "{{ aws_region }}c" + listeners: + - protocol: http + load_balancer_port: 80 + instance_port: 80 + purge_zones: yes + register: update_az + + - assert: + that: + - update_az is changed + - aws_region ~ 'a' in update_az.elb.zones + - aws_region ~ 'b' in update_az.elb.zones + - aws_region ~ 'c' in update_az.elb.zones + + - name: Get ELB info after updating AZ's + elb_classic_lb_info: + names: "{{ elb_name }}" + register: info + + - assert: + that: + - elb.availability_zones|length == 3 + - aws_region ~ 'a' in elb.availability_zones + - aws_region ~ 'b' in elb.availability_zones + - aws_region ~ 'c' in elb.availability_zones + vars: + elb: "{{ info.elbs[0] }}" + + # ============================================================ + + # Purge Listeners + + - name: Purge Listeners + elb_classic_lb: + name: "{{ elb_name }}" + state: present + zones: + - "{{ aws_region }}a" + - "{{ aws_region }}b" + - "{{ aws_region }}c" + listeners: + - protocol: http + load_balancer_port: 80 + instance_port: 81 + purge_listeners: yes + register: purge_listeners + + - assert: + that: + - purge_listeners is changed + - '[80, 81, "HTTP", "HTTP"] in purge_listeners.elb.listeners' + - purge_listeners.elb.listeners|length == 1 + + - name: Get ELB info after purging listeners + elb_classic_lb_info: + names: "{{ elb_name }}" + register: info + + - assert: + that: + - elb.listener_descriptions|length == 1 + - '{"instance_port": 81, "instance_protocol": "HTTP", "load_balancer_port": 80, "protocol": "HTTP"} == elb.listener_descriptions[0].listener' + vars: + elb: "{{ info.elbs[0] }}" + + + # ============================================================ + + # add Listeners + + - name: Add Listeners + elb_classic_lb: + name: "{{ elb_name }}" + state: present + zones: + - "{{ aws_region }}a" + - "{{ aws_region }}b" + - "{{ aws_region }}c" + listeners: + - protocol: http + load_balancer_port: 8081 + instance_port: 8081 + purge_listeners: no + register: update_listeners + + - assert: + that: + - update_listeners is changed + - '[80, 81, "HTTP", "HTTP"] in update_listeners.elb.listeners' + - '[8081, 8081, "HTTP", "HTTP"] in update_listeners.elb.listeners' + - update_listeners.elb.listeners|length == 2 + + - name: Get ELB info after adding listeners + elb_classic_lb_info: + names: "{{ elb_name }}" + register: info + + - assert: + that: + - elb.listener_descriptions|length == 2 + - '{"instance_port": 81, "instance_protocol": "HTTP", "load_balancer_port": 80, "protocol": "HTTP"} == listeners[0]' + - '{"instance_port": 8081, "instance_protocol": "HTTP", "load_balancer_port": 8081, "protocol": "HTTP"} == listeners[1]' + vars: + elb: "{{ info.elbs[0] }}" + listeners: "{{ elb.listener_descriptions|map(attribute='listener')|sort(attribute='load_balancer_port') }}" + + # ============================================================ + + # Test getting nonexistent load balancer + - name: get nonexistent load balancer + elb_classic_lb_info: + names: "invalid-elb" + register: info + + - assert: + that: + - info.elbs|length==0 + + # Test getting a valid and nonexistent load balancer + - name: get nonexistent load balancer + elb_classic_lb_info: + names: ["{{ elb_name }}", "invalid-elb"] + register: info + + - assert: + that: + - info.elbs|length==1 + - info.elbs[0].load_balancer_name == elb_name + + # ============================================================ + + - name: get all load balancers + elb_classic_lb_info: + names: "{{ omit }}" + register: info + + - assert: + that: + - info.elbs|length>0 + + always: + + # ============================================================ + - name: remove the test load balancer completely + elb_classic_lb: + name: "{{ elb_name }}" + state: absent + register: result + ignore_errors: true diff --git a/tests/integration/targets/elb_classic_lb_info/vars/main.yml b/tests/integration/targets/elb_classic_lb_info/vars/main.yml new file mode 100644 index 00000000000..79194af1ef5 --- /dev/null +++ b/tests/integration/targets/elb_classic_lb_info/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for test_ec2_elb_lb From fcd780eee6864804561e3e45d09c3185c161852b Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Thu, 10 Oct 2024 20:32:27 +0200 Subject: [PATCH 4/9] Refactor ec2_vpc_peer* modules (#2153) SUMMARY Depends-On: ansible-collections/amazon.aws#2303 Refactor ec2_vpc_peer* modules Once the module is migrated to amazon.aws I will deprecate result returned by the info module and rename the ec2_vpc_peer module (see #2154). ISSUE TYPE Bugfix Pull Request Docs Pull Request Feature Pull Request New Module Pull Request COMPONENT NAME ec2_vpc_peer ec2_vpc_peering_info ADDITIONAL INFORMATION Reviewed-by: Bikouo Aubin Reviewed-by: GomathiselviS Reviewed-by: Alina Buzachis --- .../20240924-ec2_vpc_peer-refactor.yml | 4 + plugins/modules/ec2_vpc_peer.py | 479 +++++++++--------- plugins/modules/ec2_vpc_peering_info.py | 222 ++++++-- .../targets/ec2_vpc_peer/tasks/main.yml | 313 ++++++++---- 4 files changed, 648 insertions(+), 370 deletions(-) create mode 100644 changelogs/fragments/20240924-ec2_vpc_peer-refactor.yml diff --git a/changelogs/fragments/20240924-ec2_vpc_peer-refactor.yml b/changelogs/fragments/20240924-ec2_vpc_peer-refactor.yml new file mode 100644 index 00000000000..97158a3bac0 --- /dev/null +++ b/changelogs/fragments/20240924-ec2_vpc_peer-refactor.yml @@ -0,0 +1,4 @@ +--- +minor_changes: + - ec2_vpc_peer - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` (https://github.com/ansible-collections/community.aws/pull/2153). + - ec2_vpc_peering_info - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` (https://github.com/ansible-collections/community.aws/pull/2153). diff --git a/plugins/modules/ec2_vpc_peer.py b/plugins/modules/ec2_vpc_peer.py index 2a731bf23e4..94f4b125219 100644 --- a/plugins/modules/ec2_vpc_peer.py +++ b/plugins/modules/ec2_vpc_peer.py @@ -51,9 +51,10 @@ default: false type: bool notes: - - Support for I(purge_tags) was added in release 2.0.0. + - Support for O(purge_tags) was added in release 2.0.0. author: - Mike Mochan (@mmochan) + - Alina Buzachis (@alinabuzachis) extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules @@ -63,152 +64,152 @@ EXAMPLES = r""" # Complete example to create and accept a local peering connection. -- name: Create local account VPC peering Connection +- name: Create local account EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - state: present + region: "ap-southeast-2" + vpc_id: "vpc-12345678" + peer_vpc_id: "vpc-87654321" + state: "present" tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for VPC 21 to VPC 22" + CostCode: "CC1234" + Project: "phoenix" register: vpc_peer -- name: Accept local VPC peering request +- name: Accept local EC2 VPC Peering request community.aws.ec2_vpc_peer: - region: ap-southeast-2 + region: "ap-southeast-2" peering_id: "{{ vpc_peer.peering_id }}" - state: accept + state: "accept" register: action_peer # Complete example to delete a local peering connection. -- name: Create local account VPC peering Connection +- name: Create local account EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - state: present + region: "ap-southeast-2" + vpc_id: "vpc-12345678" + peer_vpc_id: "vpc-87654321" + state: "present" tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for VPC 21 to VPC 22" + CostCode: "CC1234" + Project: "phoenix" register: vpc_peer -- name: delete a local VPC peering Connection +- name: Delete a local EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 + region: "ap-southeast-2" peering_id: "{{ vpc_peer.peering_id }}" - state: absent + state: "absent" register: vpc_peer # Complete example to create and accept a cross account peering connection. -- name: Create cross account VPC peering Connection +- name: Create cross account EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-12345678 - peer_owner_id: 123456789012 - state: present + region: "ap-southeast-2" + vpc_id: "vpc-12345678" + peer_vpc_id: "vpc-12345678" + peer_owner_id: "123456789012" + state: "present" tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for VPC 21 to VPC 22" + CostCode: "CC1234" + Project: "phoenix" register: vpc_peer -- name: Accept peering connection from remote account +- name: Accept EC2 VPC Peering Connection from remote account community.aws.ec2_vpc_peer: - region: ap-southeast-2 + region: "ap-southeast-2" peering_id: "{{ vpc_peer.peering_id }}" - profile: bot03_profile_for_cross_account - state: accept + profile: "bot03_profile_for_cross_account" + state: "accept" register: vpc_peer # Complete example to create and accept an intra-region peering connection. -- name: Create intra-region VPC peering Connection +- name: Create intra-region EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: us-east-1 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - peer_region: us-west-2 - state: present + region: "us-east-1" + vpc_id: "vpc-12345678" + peer_vpc_id: "vpc-87654321" + peer_region: "us-west-2" + state: "present" tags: - Name: Peering connection for us-east-1 VPC to us-west-2 VPC - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for us-east-1 VPC to us-west-2 VPC" + CostCode: "CC1234" + Project: "phoenix" register: vpc_peer -- name: Accept peering connection from peer region +- name: Accept EC2 VPC Peering Connection from peer region community.aws.ec2_vpc_peer: - region: us-west-2 + region: "us-west-2" peering_id: "{{ vpc_peer.peering_id }}" - state: accept + state: "accept" register: vpc_peer # Complete example to create and reject a local peering connection. -- name: Create local account VPC peering Connection +- name: Create local account EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - state: present + region: "ap-southeast-2" + vpc_id: "vpc-12345678" + peer_vpc_id: "vpc-87654321" + state: "present" tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for VPC 21 to VPC 22" + CostCode: "CC1234" + Project: "phoenix" register: vpc_peer -- name: Reject a local VPC peering Connection +- name: Reject a local EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 + region: "ap-southeast-2" peering_id: "{{ vpc_peer.peering_id }}" - state: reject + state: "reject" # Complete example to create and accept a cross account peering connection. -- name: Create cross account VPC peering Connection +- name: Create cross account EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-12345678 - peer_owner_id: 123456789012 - state: present + region: "ap-southeast-2" + vpc_id: "vpc-12345678" + peer_vpc_id: "vpc-12345678" + peer_owner_id: "123456789012" + state: "present" tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for VPC 21 to VPC 22" + CostCode: "CC1234" + Project: "phoenix" register: vpc_peer -- name: Accept a cross account VPC peering connection request +- name: Accept a cross account EC2 VPC Peering Connection request community.aws.ec2_vpc_peer: - region: ap-southeast-2 + region: "ap-southeast-2" peering_id: "{{ vpc_peer.peering_id }}" - profile: bot03_profile_for_cross_account - state: accept + profile: "bot03_profile_for_cross_account" + state: "accept" tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for VPC 21 to VPC 22" + CostCode: "CC1234" + Project: "phoenix" # Complete example to create and reject a cross account peering connection. -- name: Create cross account VPC peering Connection +- name: Create cross account EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 - vpc_id: vpc-12345678 - peer_vpc_id: vpc-12345678 - peer_owner_id: 123456789012 - state: present + region: "ap-southeast-2" + vpc_id: "vpc-12345678" + peer_vpc_id: "vpc-12345678" + peer_owner_id: "123456789012" + state: "present" tags: - Name: Peering connection for VPC 21 to VPC 22 - CostCode: CC1234 - Project: phoenix + Name: "Peering connection for VPC 21 to VPC 22" + CostCode: "CC1234" + Project: "phoenix" register: vpc_peer -- name: Reject a cross account VPC peering Connection +- name: Reject a cross account EC2 VPC Peering Connection community.aws.ec2_vpc_peer: - region: ap-southeast-2 + region: "ap-southeast-2" peering_id: "{{ vpc_peer.peering_id }}" - profile: bot03_profile_for_cross_account - state: reject + profile: "bot03_profile_for_cross_account" + state: "reject" """ RETURN = r""" @@ -216,37 +217,38 @@ description: The id of the VPC peering connection created/deleted. returned: always type: str - sample: pcx-034223d7c0aec3cde + sample: "pcx-034223d7c0aec3cde" vpc_peering_connection: - description: The details of the VPC peering connection as returned by Boto3 (snake cased). + description: The details of the VPC peering connection. returned: success - type: complex + type: dict contains: accepter_vpc_info: description: Information about the VPC which accepted the connection. returned: success - type: complex + type: dict contains: cidr_block: description: The primary CIDR for the VPC. returned: when connection is in the accepted state. type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" cidr_block_set: description: A list of all CIDRs for the VPC. returned: when connection is in the accepted state. - type: complex + type: list + elements: dict contains: cidr_block: description: A CIDR block used by the VPC. returned: success type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" owner_id: description: The AWS account that owns the VPC. returned: success type: str - example: 123456789012 + sample: "123456789012" peering_options: description: Additional peering configuration. returned: when connection is in the accepted state. @@ -268,37 +270,38 @@ description: The AWS region that the VPC is in. returned: success type: str - example: us-east-1 + sample: "us-east-1" vpc_id: description: The ID of the VPC returned: success type: str - example: vpc-0123456789abcdef0 + sample: "vpc-0123456789abcdef0" requester_vpc_info: description: Information about the VPC which requested the connection. returned: success - type: complex + type: dict contains: cidr_block: description: The primary CIDR for the VPC. returned: when connection is not in the deleted state. type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" cidr_block_set: description: A list of all CIDRs for the VPC. returned: when connection is not in the deleted state. - type: complex + type: list + elements: dict contains: cidr_block: description: A CIDR block used by the VPC returned: success type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" owner_id: description: The AWS account that owns the VPC. returned: success type: str - example: 123456789012 + sample: "123456789012" peering_options: description: Additional peering configuration. returned: when connection is not in the deleted state. @@ -320,12 +323,12 @@ description: The AWS region that the VPC is in. returned: success type: str - example: us-east-1 + sample: "us-east-1" vpc_id: description: The ID of the VPC returned: success type: str - example: vpc-0123456789abcdef0 + sample: "vpc-0123456789abcdef0" status: description: Details of the current status of the connection. returned: success @@ -335,21 +338,25 @@ description: A short code describing the status of the connection. returned: success type: str - example: active + sample: "active" message: description: Additional information about the status of the connection. returned: success type: str - example: Pending Acceptance by 123456789012 + sample: "Pending Acceptance by 123456789012" tags: description: Tags applied to the connection. returned: success type: dict + expiration_time: + description: The time that an unaccepted VPC peering connection will expire. + type: str + sample: "2024-10-01T12:11:12+00:00" vpc_peering_connection_id: description: The ID of the VPC peering connection. returned: success type: str - example: "pcx-0123456789abcdef0" + sample: "pcx-0123456789abcdef0" """ try: @@ -357,215 +364,214 @@ except ImportError: pass # Handled by AnsibleAWSModule +from typing import Any +from typing import Dict +from typing import NoReturn +from typing import Tuple + from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import add_ec2_tags +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import accept_vpc_peering_connection +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_vpc_peering_connection +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_vpc_peering_connection +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_vpc_peering_connections from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import reject_vpc_peering_connection 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 boto3_tag_specifications 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 wait_for_state(client, module, state, pcx_id): +def wait_for_state(client, module: AnsibleAWSModule, state: str, peering_id: str) -> NoReturn: waiter = client.get_waiter("vpc_peering_connection_exists") - peer_filter = { - "vpc-peering-connection-id": pcx_id, + filters = { + "vpc-peering-connection-id": peering_id, "status-code": state, } try: - waiter.wait(Filters=ansible_dict_to_boto3_filter_list(peer_filter)) + waiter.wait(Filters=ansible_dict_to_boto3_filter_list(filters)) except botocore.exceptions.WaiterError as e: module.fail_json_aws(e, "Failed to wait for state change") except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Enable to describe Peerig Connection while waiting for state to change") + module.fail_json_aws(e, "Unable to describe Peering Connection while waiting for state to change") -def describe_peering_connections(params, client): - peer_filter = { +def describe_peering_connections(client, module: AnsibleAWSModule, params) -> Dict[str, Any]: + peering_connections: Dict = {} + + filters = { "requester-vpc-info.vpc-id": params["VpcId"], "accepter-vpc-info.vpc-id": params["PeerVpcId"], } - result = client.describe_vpc_peering_connections( - aws_retry=True, - Filters=ansible_dict_to_boto3_filter_list(peer_filter), - ) - if result["VpcPeeringConnections"] == []: + + peering_connections = describe_vpc_peering_connections(client, Filters=ansible_dict_to_boto3_filter_list(filters)) + if peering_connections == []: # Try again with the VPC/Peer relationship reversed - peer_filter = { + filters = { "requester-vpc-info.vpc-id": params["PeerVpcId"], "accepter-vpc-info.vpc-id": params["VpcId"], } - result = client.describe_vpc_peering_connections( - aws_retry=True, - Filters=ansible_dict_to_boto3_filter_list(peer_filter), + peering_connections = describe_vpc_peering_connections( + client, Filters=ansible_dict_to_boto3_filter_list(filters) ) - return result + return peering_connections + + +def is_active(peering_connection: Dict[str, Any]) -> bool: + return peering_connection["Status"]["Code"] == "active" + +def is_rejected(peering_connection: Dict[str, Any]) -> bool: + return peering_connection["Status"]["Code"] == "rejected" -def is_active(peering_conn): - return peering_conn["Status"]["Code"] == "active" +def is_pending(peering_connection: Dict[str, Any]) -> bool: + return peering_connection["Status"]["Code"] == "pending-acceptance" -def is_pending(peering_conn): - return peering_conn["Status"]["Code"] == "pending-acceptance" +def is_deleted(peering_connection: Dict[str, Any]) -> bool: + return peering_connection["Status"]["Code"] == "deleted" + + +def create_peering_connection(client, module: AnsibleAWSModule) -> Tuple[bool, Dict[str, Any]]: + changed: bool = False + params: Dict = {} -def create_peer_connection(client, module): - changed = False - params = dict() params["VpcId"] = module.params.get("vpc_id") params["PeerVpcId"] = module.params.get("peer_vpc_id") + if module.params.get("peer_region"): - params["PeerRegion"] = module.params.get("peer_region") + params["PeerRegion"] = module.params["peer_region"] + if module.params.get("peer_owner_id"): - params["PeerOwnerId"] = str(module.params.get("peer_owner_id")) - peering_conns = describe_peering_connections(params, client) - for peering_conn in peering_conns["VpcPeeringConnections"]: - pcx_id = peering_conn["VpcPeeringConnectionId"] - if ensure_ec2_tags( + params["PeerOwnerId"] = module.params["peer_owner_id"] + + peering_connections = describe_peering_connections(client, module, params) + for peering_connection in peering_connections: + changed |= ensure_ec2_tags( client, module, - pcx_id, + peering_connection["VpcPeeringConnectionId"], purge_tags=module.params.get("purge_tags"), tags=module.params.get("tags"), - ): - changed = True - if is_active(peering_conn): - return (changed, peering_conn) - if is_pending(peering_conn): - return (changed, peering_conn) - try: - peering_conn = client.create_vpc_peering_connection(aws_retry=True, **params) - pcx_id = peering_conn["VpcPeeringConnection"]["VpcPeeringConnectionId"] - if module.params.get("tags"): - # Once the minimum botocore version is bumped to > 1.17.24 - # (hopefully community.aws 3.0.0) we can add the tags to the - # creation parameters - add_ec2_tags( - client, - module, - pcx_id, - module.params.get("tags"), - retry_codes=["InvalidVpcPeeringConnectionID.NotFound"], - ) - if module.params.get("wait"): - wait_for_state(client, module, "pending-acceptance", pcx_id) - changed = True - return (changed, peering_conn["VpcPeeringConnection"]) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) + ) + if is_active(peering_connection): + return (changed, peering_connection) -def remove_peer_connection(client, module): - pcx_id = module.params.get("peering_id") - if pcx_id: - peering_conn = get_peering_connection_by_id(pcx_id, client, module) + if is_pending(peering_connection): + return (changed, peering_connection) + + if module.params.get("tags"): + params["TagSpecifications"] = boto3_tag_specifications(module.params["tags"], types="vpc-peering-connection") + + if module.check_mode: + return (True, {"VpcPeeringConnectionId": ""}) + + peering_connection = create_vpc_peering_connection(client, **params) + if module.params.get("wait"): + wait_for_state(client, module, "pending-acceptance", peering_connection["VpcPeeringConnectionId"]) + changed = True + return (changed, peering_connection) + + +def delete_peering_connection(client, module: AnsibleAWSModule) -> NoReturn: + peering_id = module.params.get("peering_id") + if peering_id: + peering_connection = get_peering_connection_by_id(client, module, peering_id) else: - params = dict() + params: Dict = {} params["VpcId"] = module.params.get("vpc_id") params["PeerVpcId"] = module.params.get("peer_vpc_id") params["PeerRegion"] = module.params.get("peer_region") + if module.params.get("peer_owner_id"): - params["PeerOwnerId"] = str(module.params.get("peer_owner_id")) - peering_conn = describe_peering_connections(params, client)["VpcPeeringConnections"][0] + params["PeerOwnerId"] = module.params["peer_owner_id"] + + peering_connection = describe_peering_connections(client, module, params)[0] - if not peering_conn: + if not peering_connection: module.exit_json(changed=False) else: - pcx_id = pcx_id or peering_conn["VpcPeeringConnectionId"] + peering_id = peering_id or peering_connection["VpcPeeringConnectionId"] + + if is_deleted(peering_connection): + module.exit_json(msg="Connection in deleted state.", changed=False, peering_id=peering_id) - if peering_conn["Status"]["Code"] == "deleted": - module.exit_json(msg="Connection in deleted state.", changed=False, peering_id=pcx_id) - if peering_conn["Status"]["Code"] == "rejected": + if is_rejected(peering_connection): module.exit_json( msg="Connection has been rejected. State cannot be changed and will be removed automatically by AWS", changed=False, - peering_id=pcx_id, + peering_id=peering_id, ) - try: - params = dict() - params["VpcPeeringConnectionId"] = pcx_id - client.delete_vpc_peering_connection(aws_retry=True, **params) + if not module.check_mode: + delete_vpc_peering_connection(client, peering_id) if module.params.get("wait"): - wait_for_state(client, module, "deleted", pcx_id) - module.exit_json(changed=True, peering_id=pcx_id) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) + wait_for_state(client, module, "deleted", peering_id) + + module.exit_json(changed=True, peering_id=peering_id) -def get_peering_connection_by_id(peering_id, client, module): - params = dict() - params["VpcPeeringConnectionIds"] = [peering_id] +def get_peering_connection_by_id(client, module: AnsibleAWSModule, peering_id: str) -> Dict[str, Any]: + filters: Dict = {} + filters["VpcPeeringConnectionIds"] = [peering_id] + try: - vpc_peering_connection = client.describe_vpc_peering_connections(aws_retry=True, **params) - return vpc_peering_connection["VpcPeeringConnections"][0] + result = describe_vpc_peering_connections(client, VpcPeeringConnectionIds=[peering_id]) + return result[0] except is_boto3_error_code("InvalidVpcPeeringConnectionId.Malformed") as e: module.fail_json_aws(e, msg="Malformed connection ID") - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Error while describing peering connection by peering_id") -def accept_reject(state, client, module): - changed = False - params = dict() +def accept_reject_peering_connection(client, module: AnsibleAWSModule, state: str) -> Tuple[bool, Dict[str, Any]]: + changed: bool = False + peering_id = module.params.get("peering_id") - params["VpcPeeringConnectionId"] = peering_id - vpc_peering_connection = get_peering_connection_by_id(peering_id, client, module) - peering_status = vpc_peering_connection["Status"]["Code"] + vpc_peering_connection = get_peering_connection_by_id(client, module, peering_id) - if peering_status not in ["active", "rejected"]: - try: + if not (is_active(vpc_peering_connection) or is_rejected(vpc_peering_connection)): + if not module.check_mode: if state == "accept": - client.accept_vpc_peering_connection(aws_retry=True, **params) + changed |= accept_vpc_peering_connection(client, peering_id) target_state = "active" else: - client.reject_vpc_peering_connection(aws_retry=True, **params) + changed |= reject_vpc_peering_connection(client, peering_id) target_state = "rejected" - if module.params.get("tags"): - add_ec2_tags( - client, - module, - peering_id, - module.params.get("tags"), - retry_codes=["InvalidVpcPeeringConnectionID.NotFound"], - ) - changed = True + if module.params.get("wait"): wait_for_state(client, module, target_state, peering_id) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - if ensure_ec2_tags( + + changed = True + + changed |= ensure_ec2_tags( client, module, peering_id, purge_tags=module.params.get("purge_tags"), tags=module.params.get("tags"), - ): - changed = True + ) + + # Reload peering conection info to return latest state/params + vpc_peering_connection = get_peering_connection_by_id(client, module, peering_id) - # Relaod peering conection infos to return latest state/params - vpc_peering_connection = get_peering_connection_by_id(peering_id, client, module) return (changed, vpc_peering_connection) def main(): argument_spec = dict( - vpc_id=dict(), - peer_vpc_id=dict(), - peer_region=dict(), - peering_id=dict(), - peer_owner_id=dict(), + vpc_id=dict(type="str"), + peer_vpc_id=dict(type="str"), + peer_region=dict(type="str"), + peering_id=dict(type="str"), + peer_owner_id=dict(type="str"), tags=dict(required=False, type="dict", aliases=["resource_tags"]), purge_tags=dict(default=True, type="bool"), - state=dict(default="present", choices=["present", "absent", "accept", "reject"]), + state=dict(default="present", type="str", choices=["present", "absent", "accept", "reject"]), wait=dict(default=False, type="bool"), ) required_if = [ @@ -574,29 +580,26 @@ def main(): ("state", "reject", ["peering_id"]), ] - module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if) + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) state = module.params.get("state") peering_id = module.params.get("peering_id") vpc_id = module.params.get("vpc_id") peer_vpc_id = module.params.get("peer_vpc_id") - try: - client = module.client("ec2", retry_decorator=AWSRetry.jittered_backoff()) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to connect to AWS") + client = module.client("ec2") if state == "present": - (changed, results) = create_peer_connection(client, module) + (changed, results) = create_peering_connection(client, module) elif state == "absent": if not peering_id and (not vpc_id or not peer_vpc_id): module.fail_json( msg="state is absent but one of the following is missing: peering_id or [vpc_id, peer_vpc_id]" ) - remove_peer_connection(client, module) + delete_peering_connection(client, module) else: - (changed, results) = accept_reject(state, client, module) + (changed, results) = accept_reject_peering_connection(client, module, state) formatted_results = camel_dict_to_snake_dict(results) # Turn the resource tags from boto3 into an ansible friendly tag dictionary diff --git a/plugins/modules/ec2_vpc_peering_info.py b/plugins/modules/ec2_vpc_peering_info.py index badc9f8fd80..066211ccbe5 100644 --- a/plugins/modules/ec2_vpc_peering_info.py +++ b/plugins/modules/ec2_vpc_peering_info.py @@ -6,7 +6,7 @@ DOCUMENTATION = r""" module: ec2_vpc_peering_info -short_description: Retrieves AWS VPC Peering details using AWS methods. +short_description: Retrieves AWS VPC Peering details using AWS methods version_added: 1.0.0 description: - Gets various details related to AWS VPC Peers @@ -25,6 +25,7 @@ default: {} author: - Karen Cheng (@Etherdaemon) + - Alina Buzachis (@alinabuzachis) extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules @@ -32,8 +33,7 @@ """ EXAMPLES = r""" -# Simple example of listing all VPC Peers -- name: List all vpc peers +- name: List all EC2 VPC Peering Connections community.aws.ec2_vpc_peering_info: region: ap-southeast-2 register: all_vpc_peers @@ -42,19 +42,19 @@ ansible.builtin.debug: msg: "{{ all_vpc_peers.result }}" -- name: Get details on specific VPC peer +- name: Get details on specific EC2 VPC Peering Connection community.aws.ec2_vpc_peering_info: peer_connection_ids: - - pcx-12345678 - - pcx-87654321 - region: ap-southeast-2 + - "pcx-12345678" + - "pcx-87654321" + region: "ap-southeast-2" register: all_vpc_peers -- name: Get all vpc peers with specific filters +- name: Get all EC2 VPC Peering Connections with specific filters community.aws.ec2_vpc_peering_info: - region: ap-southeast-2 + region: "ap-southeast-2" filters: - status-code: ['pending-acceptance'] + status-code: ["pending-acceptance"] register: pending_vpc_peers """ @@ -63,32 +63,34 @@ description: Details of the matching VPC peering connections. returned: success type: list + elements: dict contains: accepter_vpc_info: description: Information about the VPC which accepted the connection. returned: success - type: complex + type: dict contains: cidr_block: description: The primary CIDR for the VPC. returned: when connection is in the accepted state. type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" cidr_block_set: description: A list of all CIDRs for the VPC. returned: when connection is in the accepted state. - type: complex + type: list + elements: dict contains: cidr_block: description: A CIDR block used by the VPC. returned: success type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" owner_id: description: The AWS account that owns the VPC. returned: success type: str - example: 123456789012 + sample: "123456789012" peering_options: description: Additional peering configuration. returned: when connection is in the accepted state. @@ -110,37 +112,38 @@ description: The AWS region that the VPC is in. returned: success type: str - example: us-east-1 + sample: "us-east-1" vpc_id: description: The ID of the VPC returned: success type: str - example: vpc-0123456789abcdef0 + sample: "vpc-0123456789abcdef0" requester_vpc_info: description: Information about the VPC which requested the connection. returned: success - type: complex + type: dict contains: cidr_block: description: The primary CIDR for the VPC. returned: when connection is not in the deleted state. type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" cidr_block_set: description: A list of all CIDRs for the VPC. returned: when connection is not in the deleted state. - type: complex + type: list + elements: dict contains: cidr_block: description: A CIDR block used by the VPC returned: success type: str - example: '10.10.10.0/23' + sample: "10.10.10.0/23" owner_id: description: The AWS account that owns the VPC. returned: success type: str - example: 123456789012 + sample: "123456789012" peering_options: description: Additional peering configuration. returned: when connection is not in the deleted state. @@ -162,27 +165,27 @@ description: The AWS region that the VPC is in. returned: success type: str - example: us-east-1 + sample: "us-east-1" vpc_id: description: The ID of the VPC returned: success type: str - example: vpc-0123456789abcdef0 + sample: "vpc-0123456789abcdef0" status: description: Details of the current status of the connection. returned: success - type: complex + type: dict contains: code: description: A short code describing the status of the connection. returned: success type: str - example: active + sample: "active" message: description: Additional information about the status of the connection. returned: success type: str - example: Pending Acceptance by 123456789012 + sample: "Pending Acceptance by 123456789012" tags: description: Tags applied to the connection. returned: success @@ -191,41 +194,171 @@ description: The ID of the VPC peering connection. returned: success type: str - example: "pcx-0123456789abcdef0" + sample: "pcx-0123456789abcdef0" result: description: The result of the describe. returned: success type: list + elements: dict + contains: + accepter_vpc_info: + description: Information about the VPC which accepted the connection. + returned: success + type: dict + contains: + cidr_block: + description: The primary CIDR for the VPC. + returned: when connection is in the accepted state. + type: str + sample: "10.10.10.0/23" + cidr_block_set: + description: A list of all CIDRs for the VPC. + returned: when connection is in the accepted state. + type: list + elements: dict + contains: + cidr_block: + description: A CIDR block used by the VPC. + returned: success + type: str + sample: "10.10.10.0/23" + owner_id: + description: The AWS account that owns the VPC. + returned: success + type: str + sample: "123456789012" + peering_options: + description: Additional peering configuration. + returned: when connection is in the accepted state. + type: dict + contains: + allow_dns_resolution_from_remote_vpc: + description: Indicates whether a VPC can resolve public DNS hostnames to private IP addresses when queried from instances in a peer VPC. + returned: success + type: bool + allow_egress_from_local_classic_link_to_remote_vpc: + description: Indicates whether a local ClassicLink connection can communicate with the peer VPC over the VPC peering connection. + returned: success + type: bool + allow_egress_from_local_vpc_to_remote_classic_link: + description: Indicates whether a local VPC can communicate with a ClassicLink connection in the peer VPC over the VPC peering connection. + returned: success + type: bool + region: + description: The AWS region that the VPC is in. + returned: success + type: str + sample: "us-east-1" + vpc_id: + description: The ID of the VPC + returned: success + type: str + sample: "vpc-0123456789abcdef0" + requester_vpc_info: + description: Information about the VPC which requested the connection. + returned: success + type: dict + contains: + cidr_block: + description: The primary CIDR for the VPC. + returned: when connection is not in the deleted state. + type: str + sample: "10.10.10.0/23" + cidr_block_set: + description: A list of all CIDRs for the VPC. + returned: when connection is not in the deleted state. + type: list + elements: dict + contains: + cidr_block: + description: A CIDR block used by the VPC + returned: success + type: str + sample: "10.10.10.0/23" + owner_id: + description: The AWS account that owns the VPC. + returned: success + type: str + sample: "123456789012" + peering_options: + description: Additional peering configuration. + returned: when connection is not in the deleted state. + type: dict + contains: + allow_dns_resolution_from_remote_vpc: + description: Indicates whether a VPC can resolve public DNS hostnames to private IP addresses when queried from instances in a peer VPC. + returned: success + type: bool + allow_egress_from_local_classic_link_to_remote_vpc: + description: Indicates whether a local ClassicLink connection can communicate with the peer VPC over the VPC peering connection. + returned: success + type: bool + allow_egress_from_local_vpc_to_remote_classic_link: + description: Indicates whether a local VPC can communicate with a ClassicLink connection in the peer VPC over the VPC peering connection. + returned: success + type: bool + region: + description: The AWS region that the VPC is in. + returned: success + type: str + sample: "us-east-1" + vpc_id: + description: The ID of the VPC + returned: success + type: str + sample: "vpc-0123456789abcdef0" + status: + description: Details of the current status of the connection. + returned: success + type: dict + contains: + code: + description: A short code describing the status of the connection. + returned: success + type: str + sample: "active" + message: + description: Additional information about the status of the connection. + returned: success + type: str + sample: "Pending Acceptance by 123456789012" + tags: + description: Tags applied to the connection. + returned: success + type: dict + vpc_peering_connection_id: + description: The ID of the VPC peering connection. + returned: success + type: str + sample: "pcx-0123456789abcdef0" """ -try: - import botocore -except ImportError: - pass # Handled by AnsibleAWSModule + +from typing import Any +from typing import Dict +from typing import List from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible_collections.amazon.aws.plugins.module_utils.botocore import normalize_boto3_result -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_vpc_peering_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 get_vpc_peers(client, module): - params = dict() +def get_vpc_peers(client, module: AnsibleAWSModule) -> List[Dict[str, Any]]: + params: Dict = {} params["Filters"] = ansible_dict_to_boto3_filter_list(module.params.get("filters")) + if module.params.get("peer_connection_ids"): params["VpcPeeringConnectionIds"] = module.params.get("peer_connection_ids") - try: - result = client.describe_vpc_peering_connections(aws_retry=True, **params) - result = normalize_boto3_result(result) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe peering connections") - return result["VpcPeeringConnections"] + result = describe_vpc_peering_connections(client, **params) + + return normalize_boto3_result(result) def main(): @@ -239,13 +372,10 @@ def main(): supports_check_mode=True, ) - try: - ec2 = module.client("ec2", retry_decorator=AWSRetry.jittered_backoff()) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to connect to AWS") + client = module.client("ec2") # Turn the boto3 result in to ansible friendly_snaked_names - results = [camel_dict_to_snake_dict(peer) for peer in get_vpc_peers(ec2, module)] + results = [camel_dict_to_snake_dict(peer) for peer in get_vpc_peers(client, module)] # Turn the boto3 result in to ansible friendly tag dictionary for peer in results: diff --git a/tests/integration/targets/ec2_vpc_peer/tasks/main.yml b/tests/integration/targets/ec2_vpc_peer/tasks/main.yml index b39b69b74b0..859e482b319 100644 --- a/tests/integration/targets/ec2_vpc_peer/tasks/main.yml +++ b/tests/integration/targets/ec2_vpc_peer/tasks/main.yml @@ -1,5 +1,5 @@ --- -- name: ec2_vpc_igw tests +- name: EC2 VPC Peering Connection integration tests collections: - amazon.aws module_defaults: @@ -9,27 +9,29 @@ session_token: "{{ security_token | default(omit) }}" region: "{{ aws_region }}" block: - - name: get ARN of calling user + - name: Get ARN of calling user aws_caller_info: register: aws_caller_info + - name: Store Account ID for later use - set_fact: + ansible.builtin.set_fact: account_id: '{{ aws_caller_info.account }}' # ============================================================ - - name: Fetch Peers in check_mode - ec2_vpc_peering_info: + - name: Fetch EC2 VPC Peering Connections in check_mode + community.aws.ec2_vpc_peering_info: register: peers_info check_mode: True + - name: Assert success - assert: + ansible.builtin.assert: that: - peers_info is successful - '"result" in peers_info' # ============================================================ - - name: create VPC 1 - ec2_vpc_net: + - name: Create VPC 1 + amazon.aws.ec2_vpc_net: name: "{{ vpc_1_name }}" state: present cidr_block: "{{ vpc_1_cidr }}" @@ -37,13 +39,14 @@ Name: "{{ vpc_1_name }}" TestPrefex: "{{ resource_prefix }}" register: vpc_1_result + - name: Assert success - assert: + ansible.builtin.assert: that: - vpc_1_result is successful - - name: create VPC 2 - ec2_vpc_net: + - name: Create VPC 2 + amazon.aws.ec2_vpc_net: name: "{{ vpc_2_name }}" state: present cidr_block: "{{ vpc_2_cidr }}" @@ -51,8 +54,9 @@ Name: "{{ vpc_2_name }}" TestPrefex: "{{ resource_prefix }}" register: vpc_2_result + - name: Assert success - assert: + ansible.builtin.assert: that: - vpc_2_result is successful @@ -62,20 +66,35 @@ vpc_2: '{{ vpc_2_result.vpc.id }}' - name: Set a name to use with the connections - set_fact: + ansible.builtin.set_fact: connection_name: 'Peering connection for VPC {{ vpc_1 }} to VPC {{ vpc_2 }}' - - name: Create local account VPC peering Connection request - ec2_vpc_peer: + - name: Create local account EC2 VPC Peering Connection request (check_mode) + community.aws.ec2_vpc_peer: vpc_id: '{{ vpc_1 }}' peer_vpc_id: '{{ vpc_2 }}' state: present tags: Name: '{{ connection_name }}' + check_mode: true register: vpc_peer - name: Assert success - assert: + ansible.builtin.assert: + that: + - vpc_peer is changed + + - name: Create local account EC2 VPC Peering Connection request + community.aws.ec2_vpc_peer: + vpc_id: '{{ vpc_1 }}' + peer_vpc_id: '{{ vpc_2 }}' + state: present + tags: + Name: '{{ connection_name }}' + register: vpc_peer + + - name: Assert success + ansible.builtin.assert: that: - vpc_peer is changed - vpc_peer is successful @@ -84,48 +103,79 @@ - vpc_peer.peering_id.startswith('pcx-') - name: Store Connection ID - set_fact: + ansible.builtin.set_fact: peer_id_1: '{{ vpc_peer.peering_id }}' - - name: (re-) Create local account VPC peering Connection request (idempotency) - ec2_vpc_peer: + - name: Re-create local account EC2 VPC Peering Connection request (idempotency check_mode) + community.aws.ec2_vpc_peer: vpc_id: '{{ vpc_1 }}' peer_vpc_id: '{{ vpc_2 }}' state: present tags: Name: '{{ connection_name }}' + check_mode: true register: vpc_peer - name: Assert success - assert: + ansible.builtin.assert: + that: + - vpc_peer is not changed + + - name: Re-create local account EC2 VPC Peering Connection request (idempotency) + community.aws.ec2_vpc_peer: + vpc_id: '{{ vpc_1 }}' + peer_vpc_id: '{{ vpc_2 }}' + state: present + tags: + Name: '{{ connection_name }}' + register: vpc_peer + + - name: Assert success + ansible.builtin.assert: that: - vpc_peer is not changed - vpc_peer is successful - vpc_peer.peering_id == peer_id_1 - - name: (re-) Create local account VPC peering Connection request with accepter/requester reversed (idempotency) - ec2_vpc_peer: + - name: Create local account EC2 VPC Peering Connection request with accepter/requester reversed (idempotency check_mode) + community.aws.ec2_vpc_peer: vpc_id: '{{ vpc_2 }}' peer_vpc_id: '{{ vpc_1 }}' state: present tags: Name: '{{ connection_name }}' + check_mode: true register: vpc_peer - name: Assert success - assert: + ansible.builtin.assert: + that: + - vpc_peer is not changed + + - name: Create local account EC2 VPC Peering Connection request with accepter/requester reversed (idempotency) + community.aws.ec2_vpc_peer: + vpc_id: '{{ vpc_2 }}' + peer_vpc_id: '{{ vpc_1 }}' + state: present + tags: + Name: '{{ connection_name }}' + register: vpc_peer + + - name: Assert success + ansible.builtin.assert: that: - vpc_peer is not changed - vpc_peer is successful - vpc_peer.peering_id == peer_id_1 - - name: Get details on specific VPC peer - ec2_vpc_peering_info: + - name: Get details on specific EC2 VPC Peering Connection + community.aws.ec2_vpc_peering_info: peer_connection_ids: - '{{ peer_id_1 }}' register: peer_info + - name: Assert expected values - assert: + ansible.builtin.assert: that: - peer_info is successful - "'vpc_peering_connections' in peer_info" @@ -170,13 +220,14 @@ acceptor_details: '{{ peer_details["accepter_vpc_info"] }}' requester_details: '{{ peer_details["requester_vpc_info"] }}' - - name: Get all vpc peers with specific filters - ec2_vpc_peering_info: + - name: Get all EC2 VPC Peering Connections with specific filters + community.aws.ec2_vpc_peering_info: filters: status-code: ['pending-acceptance'] register: pending_vpc_peers + - name: Assert expected values - assert: + ansible.builtin.assert: that: # Not guaranteed to just be us, only assert the shape - pending_vpc_peers is successful @@ -210,8 +261,24 @@ acceptor_details: '{{ peer_details["accepter_vpc_info"] }}' requester_details: '{{ peer_details["requester_vpc_info"] }}' - - name: Update tags on the VPC Peering Connection - ec2_vpc_peer: + - name: Update tags on the EC2 VPC Peering Connection (check_mode) + community.aws.ec2_vpc_peer: + vpc_id: '{{ vpc_1 }}' + peer_vpc_id: '{{ vpc_2 }}' + state: present + tags: + Name: '{{ connection_name }}' + testPrefix: '{{ resource_prefix }}' + check_mode: true + register: tag_peer + + - name: Assert success + ansible.builtin.assert: + that: + - tag_peer is changed + + - name: Update tags on the EC2 VPC Peering Connection + community.aws.ec2_vpc_peer: vpc_id: '{{ vpc_1 }}' peer_vpc_id: '{{ vpc_2 }}' state: present @@ -219,36 +286,55 @@ Name: '{{ connection_name }}' testPrefix: '{{ resource_prefix }}' register: tag_peer + - name: Assert success - assert: + ansible.builtin.assert: that: - tag_peer is changed - tag_peer is successful - tag_peer.peering_id == peer_id_1 - - name: (re-) Update tags on the VPC Peering Connection (idempotency) - ec2_vpc_peer: + - name: Update tags on the EC2 VPC Peering Connection (idempotency check_mode) + community.aws.ec2_vpc_peer: vpc_id: '{{ vpc_1 }}' peer_vpc_id: '{{ vpc_2 }}' state: present tags: Name: '{{ connection_name }}' testPrefix: '{{ resource_prefix }}' + check_mode: true register: tag_peer + - name: Assert success - assert: + ansible.builtin.assert: + that: + - tag_peer is not changed + + - name: Update tags on the EC2 VPC Peering Connection (idempotency) + community.aws.ec2_vpc_peer: + vpc_id: '{{ vpc_1 }}' + peer_vpc_id: '{{ vpc_2 }}' + state: present + tags: + Name: '{{ connection_name }}' + testPrefix: '{{ resource_prefix }}' + register: tag_peer + + - name: Assert success + ansible.builtin.assert: that: - tag_peer is not changed - tag_peer is successful - tag_peer.peering_id == peer_id_1 - - name: Get details on specific VPC peer - ec2_vpc_peering_info: + - name: Get details on specific EC2 VPC Peering Connection + community.aws.ec2_vpc_peering_info: peer_connection_ids: - '{{ peer_id_1 }}' register: peer_info + - name: Assert expected tags - assert: + ansible.builtin.assert: that: - peer_info is successful - "'tags' in peer_details" @@ -259,14 +345,28 @@ vars: peer_details: '{{ peer_info.vpc_peering_connections[0] }}' - - name: Accept local VPC peering request - ec2_vpc_peer: + - name: Accept local EC2 VPC Peering request (check_mode) + community.aws.ec2_vpc_peer: peering_id: "{{ vpc_peer.peering_id }}" state: accept - wait: True + wait: true + check_mode: true + register: action_peer + + - name: Assert success + ansible.builtin.assert: + that: + - action_peer is changed + + - name: Accept local EC2 VPC Peering request + community.aws.ec2_vpc_peer: + peering_id: "{{ vpc_peer.peering_id }}" + state: accept + wait: true register: action_peer + - name: Assert success - assert: + ansible.builtin.assert: that: - action_peer is changed - action_peer is successful @@ -274,13 +374,14 @@ - action_peer.vpc_peering_connection.accepter_vpc_info.cidr_block == vpc_2_cidr - action_peer.vpc_peering_connection.vpc_peering_connection_id == peer_id_1 - - name: Get details on specific VPC peer - ec2_vpc_peering_info: + - name: Get details on specific EC2 VPC Peering Connection + community.aws.ec2_vpc_peering_info: peer_connection_ids: - '{{ peer_id_1 }}' register: peer_info + - name: Assert expected values - assert: + ansible.builtin.assert: that: - peer_info is successful - "'vpc_peering_connections' in peer_info" @@ -331,38 +432,65 @@ acceptor_details: '{{ peer_details["accepter_vpc_info"] }}' requester_details: '{{ peer_details["requester_vpc_info"] }}' - - name: (re-) Accept local VPC peering request (idempotency) - ec2_vpc_peer: + - name: Accept local EC2 VPC Peering request (idempotency check_mode) + community.aws.ec2_vpc_peer: peering_id: "{{ vpc_peer.peering_id }}" state: accept + check_mode: true register: action_peer + - name: Assert success - assert: + ansible.builtin.assert: + that: + - action_peer is not changed + + - name: Accept local EC2 VPC Peering request (idempotency) + community.aws.ec2_vpc_peer: + peering_id: "{{ vpc_peer.peering_id }}" + state: accept + register: action_peer + + - name: Assert success + ansible.builtin.assert: that: - action_peer is not changed - action_peer is successful - action_peer.peering_id == peer_id_1 - action_peer.vpc_peering_connection.vpc_peering_connection_id == peer_id_1 - - name: delete a local VPC peering Connection - ec2_vpc_peer: + - name: Delete a local EC2 VPC Peering Connection (check_mode) + community.aws.ec2_vpc_peer: + peering_id: "{{ vpc_peer.peering_id }}" + state: absent + check_mode: true + register: delete_peer + + - name: Assert success + ansible.builtin.assert: + that: + - delete_peer is changed + + - name: Delete a local EC2 VPC Peering Connection + community.aws.ec2_vpc_peer: peering_id: "{{ vpc_peer.peering_id }}" state: absent register: delete_peer + - name: Assert success - assert: + ansible.builtin.assert: that: - delete_peer is changed - delete_peer is successful - "'peering_id' in delete_peer" - - name: Get details on specific VPC peer - ec2_vpc_peering_info: + - name: Get details on specific EC2 VPC Peering Connection + community.aws.ec2_vpc_peering_info: peer_connection_ids: - '{{ peer_id_1}}' register: peer_info + - name: Assert expected values - assert: + ansible.builtin.assert: that: - peer_info is successful - "'vpc_peering_connections' in peer_info" @@ -405,27 +533,41 @@ acceptor_details: '{{ peer_details["accepter_vpc_info"] }}' requester_details: '{{ peer_details["requester_vpc_info"] }}' - - name: (re-) delete a local VPC peering Connection (idempotency) - ec2_vpc_peer: + - name: Delete a local EC2 VPC Peering Connection (idempotency check_mode) + community.aws.ec2_vpc_peer: + peering_id: "{{ vpc_peer.peering_id }}" + state: absent + check_mode: true + register: delete_peer + + - name: Assert success + ansible.builtin.assert: + that: + - delete_peer is not changed + + - name: Delete a local EC2 VPC Peering Connection (idempotency) + community.aws.ec2_vpc_peer: peering_id: "{{ vpc_peer.peering_id }}" state: absent register: delete_peer + - name: Assert success - assert: + ansible.builtin.assert: that: - delete_peer is not changed - delete_peer is successful - - name: Create local account VPC peering Connection - ec2_vpc_peer: + - name: Create local account EC2 VPC Peering Connection + community.aws.ec2_vpc_peer: vpc_id: '{{ vpc_1 }}' peer_vpc_id: '{{ vpc_2 }}' state: present tags: Name: 'Peering connection for VPC {{ vpc_1 }} to VPC {{ vpc_2 }}' register: vpc_peer2 + - name: Assert success - assert: + ansible.builtin.assert: that: - vpc_peer2 is changed - vpc_peer2 is successful @@ -433,50 +575,53 @@ - vpc_peer2.peering_id.startswith('pcx-') - name: Store Connection ID - set_fact: + ansible.builtin.set_fact: peer_id_2: '{{ vpc_peer2.peering_id }}' - - name: reject a local VPC peering Connection - ec2_vpc_peer: + - name: Reject a local EC2 VPC Peering Connection + community.aws.ec2_vpc_peer: peering_id: "{{ vpc_peer2.peering_id }}" state: reject wait: True register: reject_peer + - name: Assert success - assert: + ansible.builtin.assert: that: - reject_peer is changed - reject_peer is successful - reject_peer.peering_id == peer_id_2 - - name: (re-) reject a local VPC peering Connection - ec2_vpc_peer: + - name: Reject a local EC2 VPC Peering Connection (idempotency) + community.aws.ec2_vpc_peer: peering_id: "{{ vpc_peer2.peering_id }}" state: reject register: reject_peer + - name: Assert success - assert: + ansible.builtin.assert: that: - reject_peer is not changed - reject_peer is successful - reject_peer.peering_id == peer_id_2 - reject_peer.vpc_peering_connection.vpc_peering_connection_id == peer_id_2 - - name: delete a local VPC peering Connection - ec2_vpc_peer: + - name: Delete a local EC2 VPC Peering Connections + community.aws.ec2_vpc_peer: peering_id: "{{ vpc_peer2.peering_id }}" state: absent register: delete_peer + - name: Assert success - assert: + ansible.builtin.assert: that: - delete_peer is not changed - delete_peer is successful always: - - name: Find all VPC Peering connections for our VPCs - ec2_vpc_peering_info: + - name: Find all EC2 VPC Peering Connections for our VPCs + community.aws.ec2_vpc_peering_info: filters: accepter-vpc-info.vpc-id: '{{ item }}' register: peering_info @@ -484,7 +629,7 @@ - '{{ vpc_1 }}' - '{{ vpc_2 }}' - - set_fact: + - ansible.builtin.set_fact: vpc_peering_connection_ids: '{{ _vpc_peering_connections | map(attribute="vpc_peering_connection_id") | list }}' vars: _vpc_peering_connections: '{{ peering_info.results | map(attribute="vpc_peering_connections") | flatten }}' @@ -492,23 +637,19 @@ # ============================================================ - - name: Delete remaining Peering connections - ec2_vpc_peer: + - name: Delete remaining EC2 VPC Peering Connections + community.aws.ec2_vpc_peer: peering_id: "{{ item }}" state: absent ignore_errors: True loop: '{{ vpc_peering_connection_ids }}' - - name: tidy up VPC 2 - ec2_vpc_net: - name: "{{ vpc_2_name }}" + - name: Tidy up VPCs + amazon.aws.ec2_vpc_net: + name: "{{ item.name }}" state: absent - cidr_block: "{{ vpc_2_cidr }}" - ignore_errors: true - - - name: tidy up VPC 1 - ec2_vpc_net: - name: "{{ vpc_1_name }}" - state: absent - cidr_block: "{{ vpc_1_cidr }}" + cidr_block: "{{ item.cidr }}" ignore_errors: true + loop: + - { name: "{{ vpc_2_name }}", cidr: "{{ vpc_2_cidr }}"} + - { name: "{{ vpc_1_name }}", cidr: "{{ vpc_1_cidr }}"} From 86d268f3391b49c5767d2243de7a5f0d74e49cb5 Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:34:05 +0200 Subject: [PATCH 5/9] Prepare modules ec2_vpc_nacl and ec2_vpc_nacl_info for promotion (#2159) SUMMARY Use shared code from amazon.aws.plugins.module_utils.ec2 Refactor module ec2_vpc_nacl and ec2_vpc_nacl_info ISSUE TYPE Feature Pull Request Reviewed-by: Alina Buzachis Reviewed-by: Bikouo Aubin Reviewed-by: GomathiselviS --- ...0240924-refactor-ec2_vpc_nacl-modules.yaml | 4 + plugins/modules/ec2_vpc_nacl.py | 606 ++++++++---------- plugins/modules/ec2_vpc_nacl_info.py | 109 ++-- .../ec2_vpc_nacl/tasks/ingress_and_egress.yml | 75 +-- .../targets/ec2_vpc_nacl/tasks/ipv6.yml | 57 +- .../targets/ec2_vpc_nacl/tasks/main.yml | 101 ++- .../targets/ec2_vpc_nacl/tasks/subnet_ids.yml | 76 +-- .../ec2_vpc_nacl/tasks/subnet_names.yml | 59 +- .../targets/ec2_vpc_nacl/tasks/tags.yml | 224 +++---- 9 files changed, 635 insertions(+), 676 deletions(-) create mode 100644 changelogs/fragments/20240924-refactor-ec2_vpc_nacl-modules.yaml diff --git a/changelogs/fragments/20240924-refactor-ec2_vpc_nacl-modules.yaml b/changelogs/fragments/20240924-refactor-ec2_vpc_nacl-modules.yaml new file mode 100644 index 00000000000..475d00cb9b9 --- /dev/null +++ b/changelogs/fragments/20240924-refactor-ec2_vpc_nacl-modules.yaml @@ -0,0 +1,4 @@ +--- +minor_changes: + - ec2_vpc_nacl_info - Refactor module to use shared code from `amazon.aws.plugins.module_utils.ec2` (https://github.com/ansible-collections/community.aws/pull/2159). + - ec2_vpc_nacl - Refactor module to use shared code from `amazon.aws.plugins.module_utils.ec2` (https://github.com/ansible-collections/community.aws/pull/2159). diff --git a/plugins/modules/ec2_vpc_nacl.py b/plugins/modules/ec2_vpc_nacl.py index cf109de1c8b..bc92003d17c 100644 --- a/plugins/modules/ec2_vpc_nacl.py +++ b/plugins/modules/ec2_vpc_nacl.py @@ -15,13 +15,15 @@ name: description: - Tagged name identifying a network ACL. - - One and only one of the I(name) or I(nacl_id) is required. + - One and only one of the O(name) or O(nacl_id) is required. + - Mutually exclusive with O(nacl_id). required: false type: str nacl_id: description: - NACL id identifying a network ACL. - - One and only one of the I(name) or I(nacl_id) is required. + - One and only one of the O(name) or O(nacl_id) is required. + - Mutually exclusive with O(name). required: false type: str vpc_id: @@ -142,25 +144,27 @@ """ RETURN = r""" -task: - description: The result of the create, or delete action. - returned: success - type: dict nacl_id: - description: The id of the NACL (when creating or updating an ACL) + description: The id of the NACL (when creating or updating an ACL). returned: success type: str - sample: acl-123456789abcdef01 + sample: "acl-123456789abcdef01" """ -try: - import botocore -except ImportError: - pass # Handled by AnsibleAWSModule - +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_network_acl +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_network_acl_entry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_network_acl +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_network_acl_entry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_network_acls +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_subnets from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry -from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_specifications +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import replace_network_acl_association from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule @@ -170,70 +174,63 @@ # Utility methods -def icmp_present(entry): - if len(entry) == 6 and entry[1] in ["icmp", "ipv6-icmp"] or entry[1] in [1, 58]: - return True - - -def subnets_removed(nacl_id, subnets, client, module): - results = find_acl_by_id(nacl_id, client, module) - associations = results["NetworkAcls"][0]["Associations"] - subnet_ids = [assoc["SubnetId"] for assoc in associations] - return [subnet for subnet in subnet_ids if subnet not in subnets] +def icmp_present(entry: List[str]) -> bool: + return len(entry) == 6 and entry[1] in ["icmp", "ipv6-icmp"] or entry[1] in [1, 58] -def subnets_added(nacl_id, subnets, client, module): - results = find_acl_by_id(nacl_id, client, module) - associations = results["NetworkAcls"][0]["Associations"] - subnet_ids = [assoc["SubnetId"] for assoc in associations] - return [subnet for subnet in subnets if subnet not in subnet_ids] - - -def subnets_changed(nacl, client, module): +def subnets_changed(client, module: AnsibleAWSModule, nacl_id: str, subnets_ids: List[str]) -> bool: changed = False vpc_id = module.params.get("vpc_id") - nacl_id = nacl["NetworkAcls"][0]["NetworkAclId"] - subnets = subnets_to_associate(nacl, client, module) - if not subnets: - default_nacl_id = find_default_vpc_nacl(vpc_id, client, module)[0] - subnets = find_subnet_ids_by_nacl_id(nacl_id, client, module) - if subnets: - replace_network_acl_association(default_nacl_id, subnets, client, module) - changed = True - return changed - changed = False + + if not subnets_ids: + default_nacl_id = find_default_vpc_nacl(client, vpc_id) + # Find subnets by Network ACL ids + network_acls = describe_network_acls( + client, Filters=[{"Name": "association.network-acl-id", "Values": [nacl_id]}] + ) + subnets = [ + association["SubnetId"] + for nacl in network_acls + for association in nacl["Associations"] + if association["SubnetId"] + ] + changed = associate_nacl_to_subnets(client, module, default_nacl_id, subnets) return changed - subs_added = subnets_added(nacl_id, subnets, client, module) - if subs_added: - replace_network_acl_association(nacl_id, subs_added, client, module) - changed = True - subs_removed = subnets_removed(nacl_id, subnets, client, module) - if subs_removed: - default_nacl_id = find_default_vpc_nacl(vpc_id, client, module)[0] - replace_network_acl_association(default_nacl_id, subs_removed, client, module) - changed = True + + network_acls = describe_network_acls(client, NetworkAclIds=[nacl_id]) + current_subnets = [ + association["SubnetId"] + for nacl in network_acls + for association in nacl["Associations"] + if association["SubnetId"] + ] + subnets_added = [subnet for subnet in subnets_ids if subnet not in current_subnets] + subnets_removed = [subnet for subnet in current_subnets if subnet not in subnets_ids] + + if subnets_added: + changed |= associate_nacl_to_subnets(client, module, nacl_id, subnets_added) + if subnets_removed: + default_nacl_id = find_default_vpc_nacl(client, vpc_id) + changed |= associate_nacl_to_subnets(client, module, default_nacl_id, subnets_removed) + return changed -def nacls_changed(nacl, client, module): +def nacls_changed(client, module: AnsibleAWSModule, nacl_info: Dict[str, Any]) -> bool: changed = False - params = dict() - params["egress"] = module.params.get("egress") - params["ingress"] = module.params.get("ingress") - - nacl_id = nacl["NetworkAcls"][0]["NetworkAclId"] - nacl = describe_network_acl(client, module) - entries = nacl["NetworkAcls"][0]["Entries"] - egress = [rule for rule in entries if rule["Egress"] is True and rule["RuleNumber"] < 32767] - ingress = [rule for rule in entries if rule["Egress"] is False and rule["RuleNumber"] < 32767] - if rules_changed(egress, params["egress"], True, nacl_id, client, module): - changed = True - if rules_changed(ingress, params["ingress"], False, nacl_id, client, module): - changed = True + entries = nacl_info["Entries"] + nacl_id = nacl_info["NetworkAclId"] + aws_egress_rules = [rule for rule in entries if rule["Egress"] is True and rule["RuleNumber"] < 32767] + aws_ingress_rules = [rule for rule in entries if rule["Egress"] is False and rule["RuleNumber"] < 32767] + + # Egress Rules + changed |= rules_changed(client, nacl_id, module.params.get("egress"), aws_egress_rules, True, module.check_mode) + # Ingress Rules + changed |= rules_changed(client, nacl_id, module.params.get("ingress"), aws_ingress_rules, False, module.check_mode) return changed -def tags_changed(nacl_id, client, module): +def tags_changed(client, module: AnsibleAWSModule, nacl_id: str) -> bool: tags = module.params.get("tags") name = module.params.get("name") purge_tags = module.params.get("purge_tags") @@ -255,42 +252,84 @@ def tags_changed(nacl_id, client, module): ) -def rules_changed(aws_rules, param_rules, Egress, nacl_id, client, module): +def ansible_to_boto3_dict_rule(ansible_rule: List[Any], egress: bool) -> Dict[str, Any]: + boto3_rule = {} + if isinstance(ansible_rule, list): + boto3_rule["RuleNumber"] = ansible_rule[0] + boto3_rule["Protocol"] = str(PROTOCOL_NUMBERS[ansible_rule[1]]) + boto3_rule["RuleAction"] = ansible_rule[2] + boto3_rule["Egress"] = egress + if is_ipv6(ansible_rule[3]): + boto3_rule["Ipv6CidrBlock"] = ansible_rule[3] + else: + boto3_rule["CidrBlock"] = ansible_rule[3] + if icmp_present(ansible_rule): + boto3_rule["IcmpTypeCode"] = {"Type": int(ansible_rule[4]), "Code": int(ansible_rule[5])} + else: + if ansible_rule[6] or ansible_rule[7]: + boto3_rule["PortRange"] = {"From": ansible_rule[6], "To": ansible_rule[7]} + return boto3_rule + + +def find_added_rules(rules_a: List[Dict[str, Any]], rules_b: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + results = [] + # A rule is considered as a new rule if either the RuleNumber does exist in the list of + # current Rules stored in AWS or if the Rule differs with the Rule stored in AWS with the same RuleNumber + for a in rules_a: + if not any(a["RuleNumber"] == b["RuleNumber"] and a == b for b in rules_b): + results.append(a) + return results + + +def rules_changed( + client, + nacl_id: str, + ansible_rules: List[List[str]], + aws_rules: List[Dict[str, Any]], + egress: bool, + check_mode: bool, +) -> bool: + # transform rules: from ansible list to boto3 dict + ansible_rules = [ansible_to_boto3_dict_rule(r, egress) for r in ansible_rules] + + # find added rules + added_rules = find_added_rules(ansible_rules, aws_rules) + # find removed rules + removed_rules = find_added_rules(aws_rules, ansible_rules) + changed = False - rules = list() - for entry in param_rules: - rules.append(process_rule_entry(entry, Egress)) - if rules == aws_rules: - return changed - else: - removed_rules = [x for x in aws_rules if x not in rules] - if removed_rules: - params = dict() - for rule in removed_rules: - params["NetworkAclId"] = nacl_id - params["RuleNumber"] = rule["RuleNumber"] - params["Egress"] = Egress - delete_network_acl_entry(params, client, module) - changed = True - added_rules = [x for x in rules if x not in aws_rules] - if added_rules: - for rule in added_rules: - rule["NetworkAclId"] = nacl_id - create_network_acl_entry(rule, client, module) - changed = True + for rule in added_rules: + changed = True + if not check_mode: + rule_number = rule.pop("RuleNumber") + protocol = rule.pop("Protocol") + rule_action = rule.pop("RuleAction") + egress = rule.pop("Egress") + create_network_acl_entry( + client, + network_acl_id=nacl_id, + protocol=protocol, + egress=egress, + rule_action=rule_action, + rule_number=rule_number, + **rule, + ) + + # Removed Rules + for rule in removed_rules: + changed = True + if not check_mode: + delete_network_acl_entry(client, network_acl_id=nacl_id, rule_number=rule["RuleNumber"], egress=egress) + return changed -def is_ipv6(cidr): +def is_ipv6(cidr: str) -> bool: return ":" in cidr -def process_rule_entry(entry, Egress): - params = dict() - params["RuleNumber"] = entry[0] - params["Protocol"] = str(PROTOCOL_NUMBERS[entry[1]]) - params["RuleAction"] = entry[2] - params["Egress"] = Egress +def process_rule_entry(entry: List[Any]) -> Dict[str, Any]: + params = {} if is_ipv6(entry[3]): params["Ipv6CidrBlock"] = entry[3] else: @@ -300,275 +339,161 @@ def process_rule_entry(entry, Egress): else: if entry[6] or entry[7]: params["PortRange"] = {"From": entry[6], "To": entry[7]} - return params - -def restore_default_associations(assoc_ids, default_nacl_id, client, module): - if assoc_ids: - params = dict() - params["NetworkAclId"] = default_nacl_id[0] - for assoc_id in assoc_ids: - params["AssociationId"] = assoc_id - restore_default_acl_association(params, client, module) - return True - - -def construct_acl_entries(nacl, client, module): - for entry in module.params.get("ingress"): - params = process_rule_entry(entry, Egress=False) - params["NetworkAclId"] = nacl["NetworkAcl"]["NetworkAclId"] - create_network_acl_entry(params, client, module) - for rule in module.params.get("egress"): - params = process_rule_entry(rule, Egress=True) - params["NetworkAclId"] = nacl["NetworkAcl"]["NetworkAclId"] - create_network_acl_entry(params, client, module) + return params -# Module invocations -def setup_network_acl(client, module): +def add_network_acl_entries( + client, nacl_id: str, ansible_entries: List[List[str]], egress: bool, check_mode: bool +) -> bool: changed = False - nacl = describe_network_acl(client, module) - if not nacl["NetworkAcls"]: - tags = {} - if module.params.get("name"): - tags["Name"] = module.params.get("name") - tags.update(module.params.get("tags") or {}) - nacl = create_network_acl(module.params.get("vpc_id"), client, module, tags) - nacl_id = nacl["NetworkAcl"]["NetworkAclId"] - subnets = subnets_to_associate(nacl, client, module) - replace_network_acl_association(nacl_id, subnets, client, module) - construct_acl_entries(nacl, client, module) + for entry in ansible_entries: changed = True - return changed, nacl["NetworkAcl"]["NetworkAclId"] - else: - changed = False - nacl_id = nacl["NetworkAcls"][0]["NetworkAclId"] - changed |= subnets_changed(nacl, client, module) - changed |= nacls_changed(nacl, client, module) - changed |= tags_changed(nacl_id, client, module) - return changed, nacl_id + if not check_mode: + create_network_acl_entry( + client, + network_acl_id=nacl_id, + protocol=str(PROTOCOL_NUMBERS[entry[1]]), + egress=egress, + rule_action=entry[2], + rule_number=entry[0], + **process_rule_entry(entry), + ) + return changed -def remove_network_acl(client, module): +def associate_nacl_to_subnets(client, module: AnsibleAWSModule, nacl_id: str, subnets_ids: List[str]) -> bool: changed = False - result = dict() - nacl = describe_network_acl(client, module) - if nacl["NetworkAcls"]: - nacl_id = nacl["NetworkAcls"][0]["NetworkAclId"] - vpc_id = nacl["NetworkAcls"][0]["VpcId"] - associations = nacl["NetworkAcls"][0]["Associations"] - assoc_ids = [a["NetworkAclAssociationId"] for a in associations] - default_nacl_id = find_default_vpc_nacl(vpc_id, client, module) - if not default_nacl_id: - result = {vpc_id: "Default NACL ID not found - Check the VPC ID"} - return changed, result - if restore_default_associations(assoc_ids, default_nacl_id, client, module): - delete_network_acl(nacl_id, client, module) + if subnets_ids: + network_acls = describe_network_acls(client, Filters=[{"Name": "association.subnet-id", "Values": subnets_ids}]) + associations = [ + association["NetworkAclAssociationId"] + for nacl in network_acls + for association in nacl["Associations"] + if association["SubnetId"] in subnets_ids + ] + for association_id in associations: changed = True - result[nacl_id] = "Successfully deleted" - return changed, result - if not assoc_ids: - delete_network_acl(nacl_id, client, module) - changed = True - result[nacl_id] = "Successfully deleted" - return changed, result - return changed, result - + if not module.check_mode: + replace_network_acl_association(client, network_acl_id=nacl_id, association_id=association_id) + return changed -# Boto3 client methods -@AWSRetry.jittered_backoff() -def _create_network_acl(client, *args, **kwargs): - return client.create_network_acl(*args, **kwargs) +def ensure_present(client, module: AnsibleAWSModule) -> None: + changed = False + nacl = describe_network_acl(client, module) + nacl_id = None + subnets_ids = [] + subnets = module.params.get("subnets") + if subnets: + subnets_ids = find_subnets_ids(client, module, subnets) -def create_network_acl(vpc_id, client, module, tags): - params = dict(VpcId=vpc_id) - if tags: - params["TagSpecifications"] = boto3_tag_specifications(tags, ["network-acl"]) - try: + if not nacl: if module.check_mode: - nacl = dict(NetworkAcl=dict(NetworkAclId="nacl-00000000")) - else: - nacl = _create_network_acl(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - return nacl - + module.exit_json(changed=True, msg="Would have created Network ACL if not in check mode.") -@AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidNetworkAclID.NotFound"]) -def _create_network_acl_entry(client, *args, **kwargs): - return client.create_network_acl_entry(*args, **kwargs) - - -def create_network_acl_entry(params, client, module): - try: - if not module.check_mode: - _create_network_acl_entry(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) + # Create Network ACL + tags = {} + name = module.params.get("name") + vpc_id = module.params.get("vpc_id") + if name: + tags["Name"] = name + if module.params.get("tags"): + tags.update(module.params.get("tags")) + nacl = create_network_acl(client, vpc_id, tags) + changed = True + # Associate Subnets to Network ACL + nacl_id = nacl["NetworkAclId"] + changed |= associate_nacl_to_subnets(client, module, nacl_id, subnets_ids) -@AWSRetry.jittered_backoff() -def _delete_network_acl(client, *args, **kwargs): - return client.delete_network_acl(*args, **kwargs) + # Create Network ACL entries (ingress and egress) + changed |= add_network_acl_entries( + client, nacl_id, module.params.get("ingress"), egress=False, check_mode=module.check_mode + ) + changed |= add_network_acl_entries( + client, nacl_id, module.params.get("egress"), egress=True, check_mode=module.check_mode + ) + else: + nacl_id = nacl["NetworkAclId"] + changed |= subnets_changed(client, module, nacl_id, subnets_ids) + changed |= nacls_changed(client, module, nacl) + changed |= tags_changed(client, module, nacl_id) + module.exit_json(changed=changed, nacl_id=nacl_id) -def delete_network_acl(nacl_id, client, module): - try: - if not module.check_mode: - _delete_network_acl(client, NetworkAclId=nacl_id) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) +def ensure_absent(client, module: AnsibleAWSModule) -> None: + changed = False + result = {} + nacl = describe_network_acl(client, module) + if not nacl: + module.exit_json(changed=changed) -@AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidNetworkAclID.NotFound"]) -def _delete_network_acl_entry(client, *args, **kwargs): - return client.delete_network_acl_entry(*args, **kwargs) + nacl_id = nacl["NetworkAclId"] + vpc_id = nacl["VpcId"] + associations = nacl["Associations"] + assoc_ids = [a["NetworkAclAssociationId"] for a in associations] + # Find default NACL associated to the VPC + default_nacl_id = find_default_vpc_nacl(client, vpc_id) + if not default_nacl_id: + module.exit_json(changed=changed, msg="Default NACL ID not found - Check the VPC ID") -def delete_network_acl_entry(params, client, module): - try: + # Replace Network ACL association + for assoc_id in assoc_ids: + changed = True if not module.check_mode: - _delete_network_acl_entry(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff() -def _describe_network_acls(client, **kwargs): - return client.describe_network_acls(**kwargs) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidNetworkAclID.NotFound"]) -def _describe_network_acls_retry_missing(client, **kwargs): - return client.describe_network_acls(**kwargs) + replace_network_acl_association(client, network_acl_id=default_nacl_id, association_id=assoc_id) + # Delete Network ACL + changed = True + if module.check_mode: + module.exit_json(changed=changed, msg=f"Would have deleted Network ACL id '{nacl_id}' if not in check mode.") -def describe_acl_associations(subnets, client, module): - if not subnets: - return [] - try: - results = _describe_network_acls_retry_missing( - client, Filters=[{"Name": "association.subnet-id", "Values": subnets}] - ) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - associations = results["NetworkAcls"][0]["Associations"] - return [a["NetworkAclAssociationId"] for a in associations if a["SubnetId"] in subnets] - + changed = delete_network_acl(client, network_acl_id=nacl_id) + module.exit_json(changed=changed, msg=f"Network ACL id '{nacl_id}' successfully deleted.") -def describe_network_acl(client, module): - try: - if module.params.get("nacl_id"): - nacl = _describe_network_acls( - client, Filters=[{"Name": "network-acl-id", "Values": [module.params.get("nacl_id")]}] - ) - else: - nacl = _describe_network_acls(client, Filters=[{"Name": "tag:Name", "Values": [module.params.get("name")]}]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - return nacl - - -def find_acl_by_id(nacl_id, client, module): - try: - return _describe_network_acls_retry_missing(client, NetworkAclIds=[nacl_id]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -def find_default_vpc_nacl(vpc_id, client, module): - try: - response = _describe_network_acls_retry_missing(client, Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - nacls = response["NetworkAcls"] - return [n["NetworkAclId"] for n in nacls if n["IsDefault"] is True] +def describe_network_acl(client, module: AnsibleAWSModule) -> Optional[Dict[str, Any]]: + nacl_id = module.params.get("nacl_id") + name = module.params.get("name") -def find_subnet_ids_by_nacl_id(nacl_id, client, module): - try: - results = _describe_network_acls_retry_missing( - client, Filters=[{"Name": "association.network-acl-id", "Values": [nacl_id]}] - ) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - if results["NetworkAcls"]: - associations = results["NetworkAcls"][0]["Associations"] - return [s["SubnetId"] for s in associations if s["SubnetId"]] + if nacl_id: + filters = [{"Name": "network-acl-id", "Values": [nacl_id]}] else: - return [] - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidNetworkAclID.NotFound"]) -def _replace_network_acl_association(client, *args, **kwargs): - return client.replace_network_acl_association(*args, **kwargs) + filters = [{"Name": "tag:Name", "Values": [name]}] + network_acls = describe_network_acls(client, Filters=filters) + return None if not network_acls else network_acls[0] -def replace_network_acl_association(nacl_id, subnets, client, module): - params = dict() - params["NetworkAclId"] = nacl_id - for association in describe_acl_associations(subnets, client, module): - params["AssociationId"] = association - try: - if not module.check_mode: - _replace_network_acl_association(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidNetworkAclID.NotFound"]) -def _replace_network_acl_entry(client, *args, **kwargs): - return client.replace_network_acl_entry(*args, **kwargs) +def find_default_vpc_nacl(client, vpc_id: str) -> Optional[str]: + default_nacl_id = None + for nacl in describe_network_acls(client, Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]): + if nacl.get("IsDefault", False): + default_nacl_id = nacl["NetworkAclId"] + break + return default_nacl_id -def replace_network_acl_entry(entries, Egress, nacl_id, client, module): - for entry in entries: - params = entry - params["NetworkAclId"] = nacl_id - try: - if not module.check_mode: - _replace_network_acl_entry(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - +def find_subnets_ids(client, module: AnsibleAWSModule, subnets_ids_or_names: List[str]) -> List[str]: + subnets_ids = [] + subnets_names = [] -@AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidNetworkAclID.NotFound"]) -def _replace_network_acl_association(client, *args, **kwargs): - return client.replace_network_acl_association(*args, **kwargs) + # Find Subnets by ID + subnets = describe_subnets(client, Filters=[{"Name": "subnet-id", "Values": subnets_ids_or_names}]) + subnets_ids += [subnet["SubnetId"] for subnet in subnets] + subnets_names += [tag["Value"] for subnet in subnets for tag in subnet.get("Tags", []) if tag["Key"] == "Name"] + # Find Subnets by Name + subnets = describe_subnets(client, Filters=[{"Name": "tag:Name", "Values": subnets_ids_or_names}]) + subnets_ids += [subnet["SubnetId"] for subnet in subnets] + subnets_names += [tag["Value"] for subnet in subnets for tag in subnet.get("Tags", []) if tag["Key"] == "Name"] -def restore_default_acl_association(params, client, module): - try: - if not module.check_mode: - _replace_network_acl_association(client, **params) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff() -def _describe_subnets(client, *args, **kwargs): - return client.describe_subnets(*args, **kwargs) - - -def subnets_to_associate(nacl, client, module): - params = list(module.params.get("subnets")) - if not params: - return [] - all_found = [] - if any(x.startswith("subnet-") for x in params): - try: - subnets = _describe_subnets(client, Filters=[{"Name": "subnet-id", "Values": params}]) - all_found.extend(subnets.get("Subnets", [])) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - if len(params) != len(all_found): - try: - subnets = _describe_subnets(client, Filters=[{"Name": "tag:Name", "Values": params}]) - all_found.extend(subnets.get("Subnets", [])) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - return list(set(s["SubnetId"] for s in all_found if s.get("SubnetId"))) + unexisting_subnets = [s for s in subnets_ids_or_names if s not in subnets_names + subnets_ids] + if unexisting_subnets: + module.fail_json(msg=f"The following subnets do not exist: {unexisting_subnets}") + return subnets_ids def main(): @@ -576,30 +501,35 @@ def main(): vpc_id=dict(), name=dict(), nacl_id=dict(), - subnets=dict(required=False, type="list", default=list(), elements="str"), + subnets=dict(required=False, type="list", default=[], elements="str"), tags=dict(required=False, type="dict", aliases=["resource_tags"]), purge_tags=dict(required=False, type="bool", default=True), ingress=dict(required=False, type="list", default=list(), elements="list"), egress=dict(required=False, type="list", default=list(), elements="list"), state=dict(default="present", choices=["present", "absent"]), ) + + mutually_exclusive = [ + ["name", "nacl_id"], + ] + module = AnsibleAWSModule( argument_spec=argument_spec, supports_check_mode=True, required_one_of=[["name", "nacl_id"]], required_if=[["state", "present", ["vpc_id"]]], + mutually_exclusive=mutually_exclusive, ) - state = module.params.get("state").lower() - client = module.client("ec2") - invocations = { - "present": setup_network_acl, - "absent": remove_network_acl, - } - (changed, results) = invocations[state](client, module) - module.exit_json(changed=changed, nacl_id=results) + try: + if module.params.get("state") == "present": + ensure_present(client, module) + else: + ensure_absent(client, module) + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) if __name__ == "__main__": diff --git a/plugins/modules/ec2_vpc_nacl_info.py b/plugins/modules/ec2_vpc_nacl_info.py index d95508a894e..9e0bc4e7f05 100644 --- a/plugins/modules/ec2_vpc_nacl_info.py +++ b/plugins/modules/ec2_vpc_nacl_info.py @@ -102,15 +102,15 @@ sample: [[100, 'all', 'allow', '0.0.0.0/0', null, null, null, null]] """ -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule +from typing import Any +from typing import Dict +from typing import List +from typing import Union from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_network_acls 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 @@ -121,55 +121,60 @@ PROTOCOL_NAMES = {"-1": "all", "1": "icmp", "6": "tcp", "17": "udp"} -def list_ec2_vpc_nacls(connection, module): +def format_nacl(nacl: Dict[str, Any]) -> Dict[str, Any]: + # Turn the boto3 result into ansible friendly snake cases + nacl = camel_dict_to_snake_dict(nacl) + + # convert boto3 tags list into ansible dict + if "tags" in nacl: + nacl["tags"] = boto3_tag_list_to_ansible_dict(nacl["tags"], "key", "value") + + # Convert NACL entries + if "entries" in nacl: + nacl["egress"] = [ + nacl_entry_to_list(entry) for entry in nacl["entries"] if entry["rule_number"] < 32767 and entry["egress"] + ] + nacl["ingress"] = [ + nacl_entry_to_list(entry) + for entry in nacl["entries"] + if entry["rule_number"] < 32767 and not entry["egress"] + ] + del nacl["entries"] + + # Read subnets from NACL Associations + if "associations" in nacl: + nacl["subnets"] = [a["subnet_id"] for a in nacl["associations"]] + del nacl["associations"] + + # Read Network ACL id + if "network_acl_id" in nacl: + nacl["nacl_id"] = nacl["network_acl_id"] + del nacl["network_acl_id"] + + return nacl + + +def list_ec2_vpc_nacls(connection, module: AnsibleAWSModule) -> None: nacl_ids = module.params.get("nacl_ids") - filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) + filters = module.params.get("filters") - if nacl_ids is None: - nacl_ids = [] + params = {} + if filters: + params["Filters"] = ansible_dict_to_boto3_filter_list(filters) + if nacl_ids: + params["NetworkAclIds"] = nacl_ids try: - nacls = connection.describe_network_acls(aws_retry=True, NetworkAclIds=nacl_ids, Filters=filters) - except is_boto3_error_code("InvalidNetworkAclID.NotFound"): - module.fail_json(msg="Unable to describe ACL. NetworkAcl does not exist") - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Unable to describe network ACLs {nacl_ids}") - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_nacls = [] - for nacl in nacls["NetworkAcls"]: - snaked_nacls.append(camel_dict_to_snake_dict(nacl)) - - # Turn the boto3 result in to ansible friendly tag dictionary - for nacl in snaked_nacls: - if "tags" in nacl: - nacl["tags"] = boto3_tag_list_to_ansible_dict(nacl["tags"], "key", "value") - if "entries" in nacl: - nacl["egress"] = [ - nacl_entry_to_list(entry) - for entry in nacl["entries"] - if entry["rule_number"] < 32767 and entry["egress"] - ] - nacl["ingress"] = [ - nacl_entry_to_list(entry) - for entry in nacl["entries"] - if entry["rule_number"] < 32767 and not entry["egress"] - ] - del nacl["entries"] - if "associations" in nacl: - nacl["subnets"] = [a["subnet_id"] for a in nacl["associations"]] - del nacl["associations"] - if "network_acl_id" in nacl: - nacl["nacl_id"] = nacl["network_acl_id"] - del nacl["network_acl_id"] - - module.exit_json(nacls=snaked_nacls) - - -def nacl_entry_to_list(entry): + network_acls = describe_network_acls(connection, **params) + if not network_acls: + module.fail_json(msg="Unable to describe ACL. NetworkAcl does not exist") + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) + + module.exit_json(nacls=[format_nacl(nacl) for nacl in network_acls]) + + +def nacl_entry_to_list(entry: Dict[str, Any]) -> List[Union[str, int, None]]: # entry list format # [ rule_num, protocol name or number, allow or deny, ipv4/6 cidr, icmp type, icmp code, port from, port to] elist = [] @@ -217,7 +222,7 @@ def main(): supports_check_mode=True, ) - connection = module.client("ec2", retry_decorator=AWSRetry.jittered_backoff()) + connection = module.client("ec2") list_ec2_vpc_nacls(connection, module) diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/ingress_and_egress.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/ingress_and_egress.yml index 875e7f0b2d7..432aaf3ba99 100644 --- a/tests/integration/targets/ec2_vpc_nacl/tasks/ingress_and_egress.yml +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/ingress_and_egress.yml @@ -1,7 +1,8 @@ # ============================================================ -- block: - - name: create ingress and egress rules using subnet IDs - ec2_vpc_nacl: +- name: Test Ingress and Egress rules + block: + - name: Create ingress and egress rules using subnet IDs + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -16,20 +17,20 @@ state: 'present' register: nacl - - name: assert the network acl was created - assert: + - name: Assert the network acl was created + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') - - name: get network ACL facts - ec2_vpc_nacl_info: + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_facts - - name: assert the nacl has the correct attributes - assert: + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].ingress | length == 3 @@ -37,8 +38,8 @@ # ============================================================ - - name: remove an ingress rule - ec2_vpc_nacl: + - name: Remove an ingress rule + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -52,20 +53,20 @@ state: 'present' register: nacl - - name: assert the network acl changed - assert: + - name: Assert the network acl changed + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') - - name: get network ACL facts - ec2_vpc_nacl_info: + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_facts - - name: assert the nacl has the correct attributes - assert: + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].ingress | length == 2 @@ -73,8 +74,8 @@ # ============================================================ - - name: remove the egress rule - ec2_vpc_nacl: + - name: Remove the egress rule + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -87,20 +88,20 @@ state: 'present' register: nacl - - name: assert the network acl changed - assert: + - name: Assert the network acl changed + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') - - name: get network ACL facts - ec2_vpc_nacl_info: + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_facts - - name: assert the nacl has the correct attributes - assert: + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].ingress | length == 2 @@ -108,8 +109,8 @@ # ============================================================ - - name: add egress rules - ec2_vpc_nacl: + - name: Add egress rules + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -124,20 +125,20 @@ state: 'present' register: nacl - - name: assert the network acl changed - assert: + - name: Assert the network acl changed + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') - - name: get network ACL facts - ec2_vpc_nacl_info: + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_facts - - name: assert the nacl has the correct attributes - assert: + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].ingress | length == 2 @@ -145,14 +146,14 @@ # ============================================================ - - name: remove the network ACL - ec2_vpc_nacl: + - name: Remove the network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" state: absent register: nacl - - name: assert nacl was removed - assert: + - name: Assert nacl was removed + ansible.builtin.assert: that: - nacl.changed diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/ipv6.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/ipv6.yml index 1366971613a..2113fb4aa9c 100644 --- a/tests/integration/targets/ec2_vpc_nacl/tasks/ipv6.yml +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/ipv6.yml @@ -1,9 +1,10 @@ -- block: +- name: Test using IPv6 + block: # ============================================================ - - name: create ingress and egress rules using subnet names - ec2_vpc_nacl: + - name: Create ingress and egress rules using subnet names + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_ipv6_id }}" name: "{{ nacl_name }}" subnets: @@ -18,15 +19,18 @@ - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] state: 'present' register: nacl - - assert: + + - name: Assert that module returned the Network ACL id + ansible.builtin.assert: that: - nacl.nacl_id - - set_fact: + - name: Set fact for Network ACL ID + ansible.builtin.set_fact: nacl_id: "{{ nacl.nacl_id }}" - - name: add ipv6 entries - ec2_vpc_nacl: + - name: Add ipv6 entries + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_ipv6_id }}" name: "{{ nacl_name }}" subnets: @@ -45,26 +49,27 @@ state: 'present' register: nacl - - assert: + - name: Assert that module reported change while the Network ACL remained unchanged + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id == nacl_id - - name: get network ACL facts (test that it works with ipv6 entries) - ec2_vpc_nacl_info: + - name: Get network ACL facts (test that it works with ipv6 entries) + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl_id }}" register: nacl_facts - - name: assert the nacl has the correct attributes - assert: + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].ingress | length == 5 - nacl_facts.nacls[0].egress | length == 2 - - name: purge ingress entries - ec2_vpc_nacl: + - name: Purge ingress entries + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_ipv6_id }}" name: "{{ nacl_name }}" subnets: @@ -78,13 +83,14 @@ state: 'present' register: nacl - - assert: + - name: Assert that module reported change while the Network ACL remained unchanged + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id == nacl_id - - name: purge egress entries - ec2_vpc_nacl: + - name: Purge egress entries + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_ipv6_id }}" name: "{{ nacl_name }}" subnets: @@ -96,18 +102,19 @@ state: 'present' register: nacl - - assert: + - name: Assert that module reported change + ansible.builtin.assert: that: - nacl.changed - - name: get network ACL facts (test that removed entries are gone) - ec2_vpc_nacl_info: + - name: Get network ACL facts (test that removed entries are gone) + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl_id }}" register: nacl_facts - - name: assert the nacl has the correct attributes - assert: + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].ingress | length == 0 @@ -115,10 +122,10 @@ always: - - name: remove network ACL - ec2_vpc_nacl: + - name: Remove network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_ipv6_id }}" name: "{{ nacl_name }}" state: absent register: removed_acl - ignore_errors: yes + ignore_errors: true diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml index 36c7ab2d8bb..445161ccd82 100644 --- a/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml @@ -5,40 +5,39 @@ secret_key: "{{ aws_secret_key }}" session_token: "{{ security_token | default(omit) }}" region: "{{ aws_region }}" - collections: - - amazon.aws + block: # ============================================================ - - name: test without any parameters - ec2_vpc_nacl: + - name: Test without any parameters + community.aws.ec2_vpc_nacl: register: result - ignore_errors: yes + ignore_errors: true - - name: assert required parameters - assert: + - name: Assert required parameters + ansible.builtin.assert: that: - result.failed - "result.msg == 'one of the following is required: name, nacl_id'" - - name: get network ACL info without any parameters - ec2_vpc_nacl_info: + - name: Get network ACL info without any parameters + community.aws.ec2_vpc_nacl_info: register: nacl_facts - - name: assert we don't error + - name: Assert we don't error assert: that: - nacl_facts is succeeded - - name: get network ACL info with invalid ID - ec2_vpc_nacl_info: + - name: Get network ACL info with invalid ID + community.aws.ec2_vpc_nacl_info: nacl_ids: - 'acl-000000000000' register: nacl_facts - ignore_errors: yes + ignore_errors: true - - name: assert message mentions missing ACLs + - name: Assert message mentions missing ACLs assert: that: - nacl_facts is failed @@ -46,34 +45,34 @@ # ============================================================ - - name: fetch AZ availability - aws_az_info: + - name: Fetch AZ availability + amazon.aws.aws_az_info: register: az_info - name: Assert that we have multiple AZs available to us - assert: + ansible.builtin.assert: that: az_info.availability_zones | length >= 2 - - name: pick AZs - set_fact: + - name: Pick AZs + ansible.builtin.set_fact: az_one: '{{ az_info.availability_zones[0].zone_name }}' az_two: '{{ az_info.availability_zones[1].zone_name }}' # ============================================================ - - name: create a VPC - ec2_vpc_net: + - name: Create a VPC + amazon.aws.ec2_vpc_net: cidr_block: "{{ vpc_cidr }}" name: "{{ vpc_name }}" state: present register: vpc - name: Save VPC ID for later - set_fact: + ansible.builtin.set_fact: vpc_id: "{{ vpc.vpc.id }}" - - name: create subnets - ec2_vpc_subnet: + - name: Create subnets + amazon.aws.ec2_vpc_subnet: cidr: "{{ item.cidr }}" az: "{{ item.az }}" vpc_id: "{{ vpc_id }}" @@ -95,27 +94,29 @@ name: "{{ subnet_name }}-4" register: subnets - - name: set helpful facts about subnets - set_fact: + - name: Set helpful facts about subnets + ansible.builtin.set_fact: subnet_ids: "{{ subnets | community.general.json_query('results[*].subnet.id') }}" subnet_names: "{{ subnets | community.general.json_query('results[*].subnet.tags.Name') }}" - - name: create VPC for IPv6 tests - ec2_vpc_net: + - name: Create VPC for IPv6 tests + amazon.aws.ec2_vpc_net: cidr_block: "{{ vpc_ipv6_cidr }}" name: "{{ vpc_ipv6_name }}" state: present - ipv6_cidr: yes + ipv6_cidr: true register: vpc_result - - set_fact: + + - name: Set helpful IPv6 facts + ansible.builtin.set_fact: vpc_ipv6_id: "{{ vpc_result.vpc.id }}" vpc_ipv6_cidr_v6: "{{ _ipv6_cidr }}" subnet_ipv6: "{{ _ipv6_cidr | regex_replace('::/56', '::/64') }}" vars: _ipv6_cidr: "{{ vpc_result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block }}" - - name: create subnet with IPv6 - ec2_vpc_subnet: + - name: Create subnet with IPv6 + amazon.aws.ec2_vpc_subnet: cidr: "{{ vpc_ipv6_cidr }}" vpc_id: "{{ vpc_ipv6_id }}" ipv6_cidr: "{{ subnet_ipv6 }}" @@ -124,31 +125,29 @@ Name: "{{ subnet_name }}-ipv6" # ============================================================ - - - include_tasks: tasks/subnet_ids.yml - - - include_tasks: tasks/subnet_names.yml - - - include_tasks: tasks/tags.yml - - - include_tasks: tasks/ingress_and_egress.yml - - - include_tasks: tasks/ipv6.yml + - name: Run individual tasks + ansible.builtin.include_tasks: "tasks/{{ item }}.yml" + with_items: + - subnet_ids + - subnet_names + - tags + - ingress_and_egress + - ipv6 # ============================================================ always: - - name: remove network ACL - ec2_vpc_nacl: + - name: Remove network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" state: absent register: removed_acl - ignore_errors: yes + ignore_errors: true - - name: remove subnets - ec2_vpc_subnet: + - name: Remove subnets + amazon.aws.ec2_vpc_subnet: cidr: "{{ item.cidr }}" vpc_id: "{{ item.vpc_id | default(vpc_id) }}" state: absent @@ -159,14 +158,14 @@ - cidr: "{{ subnet_4 }}" - cidr: "{{ vpc_ipv6_cidr }}" vpc_id: "{{ vpc_ipv6_id }}" - ignore_errors: yes + ignore_errors: true register: removed_subnets - - name: remove the VPCs - ec2_vpc_net: + - name: Remove the VPCs + amazon.aws.ec2_vpc_net: vpc_id: "{{ item }}" state: absent - ignore_errors: yes + ignore_errors: true register: removed_vpc with_items: - '{{ vpc_id }}' diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_ids.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_ids.yml index 4e1affa1f34..3a367e84f89 100644 --- a/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_ids.yml +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_ids.yml @@ -1,7 +1,7 @@ # ============================================================ -- name: create ingress and egress rules using subnet IDs - ec2_vpc_nacl: +- name: Create ingress and egress rules using subnet IDs + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -16,23 +16,24 @@ state: 'present' register: nacl -- set_fact: +- name: Set helpful fact for Network ACL ID + ansible.builtin.set_fact: nacl_id: "{{ nacl.nacl_id }}" -- name: assert the network acl was created - assert: +- name: Assert the network acl was created + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') -- name: get network ACL facts - ec2_vpc_nacl_info: +- name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl_id }}" register: nacl_facts -- name: assert the nacl has the correct attributes - assert: +- name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].nacl_id == nacl_id @@ -44,8 +45,8 @@ # ============================================================ -- name: test idempotence - ec2_vpc_nacl: +- name: Test idempotence + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -60,28 +61,28 @@ state: 'present' register: nacl -- name: assert the network acl already existed - assert: +- name: Assert the network acl already existed + ansible.builtin.assert: that: - not nacl.changed - nacl.nacl_id == nacl_id - nacl.nacl_id.startswith('acl-') -- name: get network ACL facts - ec2_vpc_nacl_info: +- name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_facts_idem -- name: assert the facts are the same as before - assert: +- name: Assert the facts are the same as before + ansible.builtin.assert: that: - nacl_facts_idem == nacl_facts # ============================================================ -- name: remove a subnet from the network ACL - ec2_vpc_nacl: +- name: Remove a subnet from the network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: @@ -99,21 +100,21 @@ state: 'present' register: nacl -- name: assert the network ACL changed - assert: +- name: Assert the network ACL changed + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') - nacl.nacl_id == nacl_id -- name: get network ACL facts - ec2_vpc_nacl_info: +- name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_id: - "{{ nacl.nacl_id }}" register: nacl_facts -- name: assert the nacl has the correct attributes - assert: +- name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].nacl_id == nacl_id @@ -125,37 +126,38 @@ # ============================================================ -- name: remove the network ACL - ec2_vpc_nacl: +- name: Remove the network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" state: absent register: nacl -- name: assert nacl was removed - assert: +- name: Assert nacl was removed + ansible.builtin.assert: that: - nacl.changed -- name: re-remove the network ACL by name (test idempotency) - ec2_vpc_nacl: +- name: Re-remove the network ACL by name (test idempotency) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" state: absent register: nacl -- name: assert nacl was removed - assert: + +- name: Assert nacl was removed + ansible.builtin.assert: that: - nacl is not changed -- name: re-remove the network ACL by id (test idempotency) - ec2_vpc_nacl: +- name: Re-remove the network ACL by id (test idempotency) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" nacl_id: "{{ nacl_id }}" state: absent register: nacl -- name: assert nacl was removed - assert: +- name: Assert nacl was removed + ansible.builtin.assert: that: - nacl is not changed diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_names.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_names.yml index 4db7e1b2068..dc44fef804a 100644 --- a/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_names.yml +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_names.yml @@ -1,7 +1,7 @@ # ============================================================ -- name: create ingress and egress rules using subnet names - ec2_vpc_nacl: +- name: Create ingress and egress rules using subnet names + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_names }}" @@ -16,23 +16,24 @@ state: 'present' register: nacl -- set_fact: +- name: Set helpful fact for Network ACL ID + ansible.builtin.set_fact: nacl_id: "{{ nacl.nacl_id }}" -- name: assert the network acl was created - assert: +- name: Assert the network acl was created + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') -- name: get network ACL facts - ec2_vpc_nacl_info: +- name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl_id }}" register: nacl_facts -- name: assert the nacl has the correct attributes - assert: +- name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].nacl_id == nacl_id @@ -43,8 +44,8 @@ # ============================================================ -- name: test idempotence - ec2_vpc_nacl: +- name: Test idempotence + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_names }}" @@ -59,28 +60,28 @@ state: 'present' register: nacl -- name: assert the network acl already existed - assert: +- name: Assert the network acl already existed + ansible.builtin.assert: that: - not nacl.changed - nacl.nacl_id == nacl_id - nacl.nacl_id.startswith('acl-') -- name: get network ACL facts - ec2_vpc_nacl_info: +- name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_facts_idem -- name: assert the facts are the same as before - assert: +- name: Assert the facts are the same as before + ansible.builtin.assert: that: - nacl_facts_idem == nacl_facts # ============================================================ -- name: remove a subnet from the network ACL - ec2_vpc_nacl: +- name: Remove a subnet from the network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: @@ -98,21 +99,21 @@ state: 'present' register: nacl -- name: assert the network ACL changed - assert: +- name: Assert the network ACL changed + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id == nacl_id - nacl.nacl_id.startswith('acl-') -- name: get network ACL facts - ec2_vpc_nacl_info: +- name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_facts -- name: assert the nacl has the correct attributes - assert: +- name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_facts.nacls | length == 1 - nacl_facts.nacls[0].nacl_id == nacl_id @@ -123,14 +124,14 @@ # ============================================================ -- name: remove the network ACL - ec2_vpc_nacl: +- name: Remove the network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" state: absent register: nacl -- name: assert nacl was removed - assert: +- name: Assert nacl was removed + ansible.builtin.assert: that: - nacl.changed diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/tags.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/tags.yml index da3ad71dda3..0e69ce760ac 100644 --- a/tests/integration/targets/ec2_vpc_nacl/tasks/tags.yml +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/tags.yml @@ -1,4 +1,5 @@ -- vars: +- name: Run test from tags.yml + vars: first_tags: 'Key with Spaces': Value with spaces CamelCaseKey: CamelCaseValue @@ -30,40 +31,40 @@ # ============================================================ - - name: create a network ACL using subnet IDs - ec2_vpc_nacl: + - name: Create a network ACL using subnet IDs + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" state: 'present' register: nacl - - name: assert the network acl was created - assert: + - name: Assert the network acl was created + ansible.builtin.assert: that: - nacl.changed - nacl.nacl_id.startswith('acl-') - name: Store NACL ID - set_fact: + ansible.builtin.set_fact: nacl_id: '{{ nacl.nacl_id }}' - - name: get network ACL facts - ec2_vpc_nacl_info: + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl_id }}" register: nacl_info - - name: assert the nacl has the correct attributes - assert: + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: that: - nacl_info.nacls[0].nacl_id == nacl_id - nacl_info.nacls[0].tags == name_tags # ============================================================ - - name: (check) add tags - ec2_vpc_nacl: + - name: Add tags (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -72,36 +73,37 @@ register: nacl check_mode: True - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - - name: add tags - ec2_vpc_nacl: + - name: Add tags + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" tags: "{{ first_tags }}" state: 'present' register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify the tags were added - assert: + - name: Verify the tags were added + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - nacl_info.nacls[0].nacl_id == nacl_id - nacl_info.nacls[0].tags == ( first_tags | combine(name_tags) ) - - name: (check) add tags - IDEMPOTENCY - ec2_vpc_nacl: + - name: Add tags - IDEMPOTENCY (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -110,28 +112,29 @@ register: nacl check_mode: True - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id - - name: add tags - IDEMPOTENCY - ec2_vpc_nacl: + - name: Add tags - IDEMPOTENCY + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" tags: "{{ first_tags }}" state: 'present' register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id @@ -140,14 +143,14 @@ # ============================================================ - - name: get network ACL facts by filter - ec2_vpc_nacl_info: + - name: Get network ACL facts by filter + community.aws.ec2_vpc_nacl_info: filters: "tag:Name": "{{ nacl_name }}" register: nacl_info - - name: assert the facts are the same as before - assert: + - name: Assert the facts are the same as before + ansible.builtin.assert: that: - nacl_info.nacls | length == 1 - nacl.nacl_id == nacl_id @@ -155,8 +158,8 @@ # ============================================================ - - name: (check) modify tags with purge - ec2_vpc_nacl: + - name: Modify tags with purge (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -165,36 +168,37 @@ register: nacl check_mode: True - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - - name: modify tags with purge - ec2_vpc_nacl: + - name: Modify tags with purge + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" tags: "{{ second_tags }}" state: 'present' register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify the tags were added - assert: + - name: Verify the tags were added + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - nacl_info.nacls[0].nacl_id == nacl_id - nacl_info.nacls[0].tags == ( second_tags | combine(name_tags) ) - - name: (check) modify tags with purge - IDEMPOTENCY - ec2_vpc_nacl: + - name: Modify tags with purge - IDEMPOTENCY (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -203,28 +207,29 @@ register: nacl check_mode: True - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id - - name: modify tags with purge - IDEMPOTENCY - ec2_vpc_nacl: + - name: Modify tags with purge - IDEMPOTENCY + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" tags: "{{ second_tags }}" state: 'present' register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id @@ -233,8 +238,8 @@ # ============================================================ - - name: (check) modify tags without purge - ec2_vpc_nacl: + - name: Modify tags without purge (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -244,14 +249,14 @@ register: nacl check_mode: True - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - - name: modify tags without purge - ec2_vpc_nacl: + - name: Modify tags without purge + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -259,22 +264,23 @@ state: 'present' purge_tags: False register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify the tags were added - assert: + - name: Verify the tags were added + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - nacl_info.nacls[0].nacl_id == nacl_id - nacl_info.nacls[0].tags == ( final_tags | combine(name_tags) ) - - name: (check) modify tags without purge - IDEMPOTENCY - ec2_vpc_nacl: + - name: Modify tags without purge - IDEMPOTENCY (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -284,14 +290,14 @@ register: nacl check_mode: True - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id - - name: modify tags without purge - IDEMPOTENCY - ec2_vpc_nacl: + - name: Modify tags without purge - IDEMPOTENCY + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -299,14 +305,15 @@ state: 'present' purge_tags: False register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id @@ -315,8 +322,8 @@ # ============================================================ - - name: (check) No change to tags without setting tags - ec2_vpc_nacl: + - name: No change to tags without setting tags (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -324,27 +331,28 @@ register: nacl check_mode: True - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id - name: No change to tags without setting tags - ec2_vpc_nacl: + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" state: 'present' register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify the tags were added - assert: + - name: Verify the tags were added + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id @@ -353,8 +361,8 @@ # ============================================================ - - name: (check) remove non name tags - ec2_vpc_nacl: + - name: Remove non name tags (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -363,36 +371,37 @@ register: nacl check_mode: True - - name: assert would change - assert: + - name: Assert would change + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - - name: remove non name tags - ec2_vpc_nacl: + - name: Remove non name tags + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" tags: {} state: 'present' register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify the tags were added - assert: + - name: Verify the tags were added + ansible.builtin.assert: that: - nacl is changed - nacl.nacl_id == nacl_id - nacl_info.nacls[0].nacl_id == nacl_id - nacl_info.nacls[0].tags == name_tags - - name: (check) remove non name tags - IDEMPOTENCY - ec2_vpc_nacl: + - name: Remove non name tags - IDEMPOTENCY (check mode) + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" @@ -401,28 +410,29 @@ register: nacl check_mode: True - - name: assert would not change - assert: + - name: Assert would not change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id - - name: remove non name tags - IDEMPOTENCY - ec2_vpc_nacl: + - name: Remove non name tags - IDEMPOTENCY + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" subnets: "{{ subnet_ids }}" tags: {} state: 'present' register: nacl - - name: get network ACL facts - ec2_vpc_nacl_info: + + - name: Get network ACL facts + community.aws.ec2_vpc_nacl_info: nacl_ids: - "{{ nacl.nacl_id }}" register: nacl_info - - name: verify no change - assert: + - name: Verify no change + ansible.builtin.assert: that: - nacl is not changed - nacl.nacl_id == nacl_id @@ -432,14 +442,14 @@ # ============================================================ always: - - name: remove the network ACL - ec2_vpc_nacl: + - name: Remove the network ACL + community.aws.ec2_vpc_nacl: vpc_id: "{{ vpc_id }}" name: "{{ nacl_name }}" state: absent register: nacl - - name: assert nacl was removed - assert: + - name: Assert nacl was removed + ansible.builtin.assert: that: - nacl.changed From d2403ab9630a58ea37b8f7d60c0aab908ec1c83d Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Tue, 15 Oct 2024 17:35:31 +0200 Subject: [PATCH 6/9] Refactor ec2_vpc_vpn* modules (#2160) SUMMARY Refactor ec2_vpc_vpn* modules ISSUE TYPE Bugfix Pull Request Docs Pull Request Feature Pull Request New Module Pull Request COMPONENT NAME ec2_vpc_vpn ec2_vpc_vpn_info ADDITIONAL INFORMATION Reviewed-by: GomathiselviS Reviewed-by: Bikouo Aubin --- .../20240930-ec2_vpc_vpn_refactoring.yml | 3 + plugins/modules/ec2_vpc_vpn.py | 748 ++++++++++-------- plugins/modules/ec2_vpc_vpn_info.py | 154 ++-- .../targets/ec2_vpc_vpn/tasks/main.yml | 122 +-- .../targets/ec2_vpc_vpn/tasks/tags.yml | 228 +++--- .../unit/plugins/modules/test_ec2_vpc_vpn.py | 668 ++++++---------- 6 files changed, 946 insertions(+), 977 deletions(-) create mode 100644 changelogs/fragments/20240930-ec2_vpc_vpn_refactoring.yml 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 From 290e89a836d585a52256f6c2e0b0783421c4efc9 Mon Sep 17 00:00:00 2001 From: GomathiselviS Date: Wed, 16 Oct 2024 11:23:51 -0400 Subject: [PATCH 7/9] Prepare ec2_placement_group* module for promotion (#2167) SUMMARY This PR refactors ec2_placement_group*. Depends-On: ansible-collections/amazon.aws#2322 Refer: https://issues.redhat.com/browse/ACA-1886 ISSUE TYPE Bugfix Pull Request Docs Pull Request Feature Pull Request New Module Pull Request COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Bikouo Aubin Reviewed-by: GomathiselviS Reviewed-by: Alina Buzachis --- .../refactor_ec2_placement_group.yml | 2 + plugins/modules/ec2_placement_group.py | 125 +++++++------- plugins/modules/ec2_placement_group_info.py | 45 +++-- .../ec2_placement_group/tasks/env_cleanup.yml | 34 ++-- .../ec2_placement_group/tasks/env_setup.yml | 16 +- .../ec2_placement_group/tasks/main.yml | 155 +++++++++--------- 6 files changed, 187 insertions(+), 190 deletions(-) create mode 100644 changelogs/fragments/refactor_ec2_placement_group.yml diff --git a/changelogs/fragments/refactor_ec2_placement_group.yml b/changelogs/fragments/refactor_ec2_placement_group.yml new file mode 100644 index 00000000000..c4366ed2c1f --- /dev/null +++ b/changelogs/fragments/refactor_ec2_placement_group.yml @@ -0,0 +1,2 @@ +minor_changes: + - ec2_placement_group - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` and update ``RETURN`` block (https://github.com/ansible-collections/community.aws/pull/2167). diff --git a/plugins/modules/ec2_placement_group.py b/plugins/modules/ec2_placement_group.py index b045ea34b25..1a38252ac97 100644 --- a/plugins/modules/ec2_placement_group.py +++ b/plugins/modules/ec2_placement_group.py @@ -25,8 +25,8 @@ partition_count: description: - The number of partitions. - - Valid only when I(Strategy) is set to C(partition). - - Must be a value between C(1) and C(7). + - Valid only when O(strategy) is set to V(partition). + - Must be a value between V(1) and V(7). type: int version_added: 3.1.0 state: @@ -86,23 +86,42 @@ placement_group: description: Placement group attributes returned: when state != absent - type: complex + type: dict contains: + group_arn: + description: Placement Group ARN. + type: str + returned: always + sample: "arn:aws:ec2:us-east-1:123456789012:placement-group" + group_id: + description: Placement Group Id. + type: str + returned: always + sample: "pg-123456789012" name: - description: PG name + description: Placement Group name. + type: str + returned: always + sample: "my-cluster" + partition_count: + description: Partition Count. type: str - sample: my-cluster + returned: If applicable + sample: "my-cluster" state: - description: PG state + description: Placement Groupt state. type: str + returned: If applicable sample: "available" strategy: - description: PG strategy + description: Placement Group strategy. type: str + returned: If applicable sample: "cluster" tags: - description: Tags associated with the placement group + description: Tags associated with the placement group. type: dict + returned: If applicable version_added: 8.1.0 sample: tags: @@ -110,59 +129,45 @@ other: value2 """ -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule +from typing import Any +from typing import Dict -from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_ec2_placement_group +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_ec2_placement_group +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_ec2_placement_groups 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 boto3_tag_specifications from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule -@AWSRetry.exponential_backoff() -def search_placement_group(connection, module): +def search_placement_group(connection, name: str) -> Dict[str, Any]: """ Check if a placement group exists. """ - name = module.params.get("name") - try: - response = connection.describe_placement_groups(Filters=[{"Name": "group-name", "Values": [name]}]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg=f"Couldn't find placement group named [{name}]") + response = describe_ec2_placement_groups(connection, Filters=[{"Name": "group-name", "Values": [name]}]) - if len(response["PlacementGroups"]) != 1: + if len(response) != 1: return None else: - placement_group = response["PlacementGroups"][0] - return { - "name": placement_group["GroupName"], - "state": placement_group["State"], - "strategy": placement_group["Strategy"], - "tags": boto3_tag_list_to_ansible_dict(placement_group.get("Tags")), - } + return format_placement_group_information(response[0]) -@AWSRetry.exponential_backoff(catch_extra_error_codes=["InvalidPlacementGroup.Unknown"]) -def get_placement_group_information(connection, name): +def format_placement_group_information(response: Dict[str, Any]) -> Dict[str, Any]: """ - Retrieve information about a placement group. + Format placement group information """ - response = connection.describe_placement_groups(GroupNames=[name]) - placement_group = response["PlacementGroups"][0] - return { - "name": placement_group["GroupName"], - "state": placement_group["State"], - "strategy": placement_group["Strategy"], - "tags": boto3_tag_list_to_ansible_dict(placement_group.get("Tags")), - } - - -@AWSRetry.exponential_backoff() -def create_placement_group(connection, module): + + response = camel_dict_to_snake_dict(response, ignore_list=["Tags"]) + if "tags" in response: + response["tags"] = boto3_tag_list_to_ansible_dict(response.get("tags", [])) + response["name"] = response["group_name"] + return response + + +def create_placement_group(connection, module: AnsibleAWSModule) -> None: name = module.params.get("name") strategy = module.params.get("strategy") tags = module.params.get("tags") @@ -178,38 +183,26 @@ def create_placement_group(connection, module): params["TagSpecifications"] = boto3_tag_specifications(tags, types=["placement-group"]) if partition_count: params["PartitionCount"] = partition_count - params["DryRun"] = module.check_mode - - try: - connection.create_placement_group(**params) - except is_boto3_error_code("DryRunOperation"): + if module.check_mode: module.exit_json( changed=True, placement_group={ "name": name, - "state": "DryRun", "strategy": strategy, "tags": tags, }, + msg="EC2 placement group would be created if not in check mode", ) - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Couldn't create placement group [{name}]") - module.exit_json(changed=True, placement_group=get_placement_group_information(connection, name)) + response = create_ec2_placement_group(connection, **params) + module.exit_json(changed=True, placement_group=format_placement_group_information(response)) -@AWSRetry.exponential_backoff() -def delete_placement_group(connection, module): +def delete_placement_group(connection, module: AnsibleAWSModule) -> None: + if module.check_mode: + module.exit_json(changed=True, msg="VPC would be deleted if not in check mode") name = module.params.get("name") - - try: - connection.delete_placement_group(GroupName=name, DryRun=module.check_mode) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg=f"Couldn't delete placement group [{name}]") - + delete_ec2_placement_group(connection, name) module.exit_json(changed=True) @@ -227,9 +220,10 @@ def main(): connection = module.client("ec2") state = module.params.get("state") + name = module.params.get("name") + placement_group = search_placement_group(connection, name) if state == "present": - placement_group = search_placement_group(connection, module) if placement_group is None: create_placement_group(connection, module) else: @@ -243,7 +237,6 @@ def main(): ) elif state == "absent": - placement_group = search_placement_group(connection, module) if placement_group is None: module.exit_json(changed=False) else: diff --git a/plugins/modules/ec2_placement_group_info.py b/plugins/modules/ec2_placement_group_info.py index 74b32558246..8c67e2b5fa4 100644 --- a/plugins/modules/ec2_placement_group_info.py +++ b/plugins/modules/ec2_placement_group_info.py @@ -58,7 +58,7 @@ name: description: PG name type: str - sample: my-cluster + sample: "my-cluster" state: description: PG state type: str @@ -77,36 +77,28 @@ other: value2 """ -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 List +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_ec2_placement_groups +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict -from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule - -def get_placement_groups_details(connection, module): - names = module.params.get("names") - try: - if len(names) > 0: - response = connection.describe_placement_groups( - Filters=[ - { - "Name": "group-name", - "Values": names, - } - ] - ) - else: - response = connection.describe_placement_groups() - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg=f"Couldn't find placement groups named [{names}]") +def get_placement_groups_details(connection, names: List) -> Dict[str, Any]: + params = {} + if len(names) > 0: + params["Filters"] = [ + { + "Name": "group-name", + "Values": names, + } + ] + response = describe_ec2_placement_groups(connection, **params) results = [] - for placement_group in response["PlacementGroups"]: + for placement_group in response: results.append( { "name": placement_group["GroupName"], @@ -129,8 +121,9 @@ def main(): ) connection = module.client("ec2") + names = module.params.get("names") - placement_groups = get_placement_groups_details(connection, module) + placement_groups = get_placement_groups_details(connection, names) module.exit_json(changed=False, placement_groups=placement_groups) diff --git a/tests/integration/targets/ec2_placement_group/tasks/env_cleanup.yml b/tests/integration/targets/ec2_placement_group/tasks/env_cleanup.yml index ce626b69c3d..be84fe8f17a 100644 --- a/tests/integration/targets/ec2_placement_group/tasks/env_cleanup.yml +++ b/tests/integration/targets/ec2_placement_group/tasks/env_cleanup.yml @@ -1,5 +1,5 @@ -- name: remove any instances in the test VPC - ec2_instance: +- name: Remove any instances in the test VPC + amazon.aws.ec2_instance: filters: vpc_id: "{{ testing_vpc.vpc.id }}" state: absent @@ -9,13 +9,13 @@ retries: 10 - name: Get ENIs - ec2_eni_info: + amazon.aws.ec2_eni_info: filters: vpc-id: "{{ testing_vpc.vpc.id }}" register: enis -- name: delete all ENIs - ec2_eni: +- name: Delete all ENIs + amazon.aws.ec2_eni: eni_id: "{{ item.id }}" state: absent until: removed is not failed @@ -23,8 +23,8 @@ ignore_errors: yes retries: 10 -- name: remove the security group - ec2_security_group: +- name: Remove the security group + amazon.aws.ec2_security_group: name: "{{ resource_prefix }}-sg" description: a security group for ansible tests vpc_id: "{{ testing_vpc.vpc.id }}" @@ -34,8 +34,8 @@ ignore_errors: yes retries: 10 -- name: remove routing rules - ec2_vpc_route_table: +- name: Remove routing rules + amazon.aws.ec2_vpc_route_table: state: absent vpc_id: "{{ testing_vpc.vpc.id }}" tags: @@ -51,8 +51,8 @@ ignore_errors: yes retries: 10 -- name: remove internet gateway - ec2_vpc_igw: +- name: Remove internet gateway + amazon.aws.ec2_vpc_igw: vpc_id: "{{ testing_vpc.vpc.id }}" state: absent register: removed @@ -60,8 +60,8 @@ ignore_errors: yes retries: 10 -- name: remove subnet A - ec2_vpc_subnet: +- name: Remove subnet A + amazon.aws.ec2_vpc_subnet: state: absent vpc_id: "{{ testing_vpc.vpc.id }}" cidr: 10.22.32.0/24 @@ -70,8 +70,8 @@ ignore_errors: yes retries: 10 -- name: remove subnet B - ec2_vpc_subnet: +- name: Remove subnet B + amazon.aws.ec2_vpc_subnet: state: absent vpc_id: "{{ testing_vpc.vpc.id }}" cidr: 10.22.33.0/24 @@ -80,8 +80,8 @@ ignore_errors: yes retries: 10 -- name: remove the VPC - ec2_vpc_net: +- name: Remove the VPC + amazon.aws.ec2_vpc_net: name: "{{ resource_prefix }}-vpc" cidr_block: 10.22.32.0/23 state: absent diff --git a/tests/integration/targets/ec2_placement_group/tasks/env_setup.yml b/tests/integration/targets/ec2_placement_group/tasks/env_setup.yml index d48bae66c83..54fa62a35db 100644 --- a/tests/integration/targets/ec2_placement_group/tasks/env_setup.yml +++ b/tests/integration/targets/ec2_placement_group/tasks/env_setup.yml @@ -1,5 +1,5 @@ - name: Create VPC for use in testing - ec2_vpc_net: + amazon.aws.ec2_vpc_net: name: "{{ resource_prefix }}-vpc" cidr_block: 10.22.32.0/23 tags: @@ -8,7 +8,7 @@ register: testing_vpc - name: Create internet gateway for use in testing - ec2_vpc_igw: + amazon.aws.ec2_vpc_igw: vpc_id: "{{ testing_vpc.vpc.id }}" state: present tags: @@ -16,7 +16,7 @@ register: igw - name: Create default subnet in zone A - ec2_vpc_subnet: + amazon.aws.ec2_vpc_subnet: state: present vpc_id: "{{ testing_vpc.vpc.id }}" cidr: 10.22.32.0/24 @@ -26,7 +26,7 @@ register: testing_subnet_a - name: Create secondary subnet in zone B - ec2_vpc_subnet: + amazon.aws.ec2_vpc_subnet: state: present vpc_id: "{{ testing_vpc.vpc.id }}" cidr: 10.22.33.0/24 @@ -35,8 +35,8 @@ Name: "{{ resource_prefix }}-subnet-b" register: testing_subnet_b -- name: create routing rules - ec2_vpc_route_table: +- name: Create routing rules + amazon.aws.ec2_vpc_route_table: vpc_id: "{{ testing_vpc.vpc.id }}" tags: created: "{{ resource_prefix }}-route" @@ -47,8 +47,8 @@ - "{{ testing_subnet_a.subnet.id }}" - "{{ testing_subnet_b.subnet.id }}" -- name: create a security group with the vpc - ec2_security_group: +- name: Create a security group with the vpc + amazon.aws.ec2_security_group: name: "{{ resource_prefix }}-sg" description: a security group for ansible tests vpc_id: "{{ testing_vpc.vpc.id }}" diff --git a/tests/integration/targets/ec2_placement_group/tasks/main.yml b/tests/integration/targets/ec2_placement_group/tasks/main.yml index eec1b168ef0..130f347db97 100644 --- a/tests/integration/targets/ec2_placement_group/tasks/main.yml +++ b/tests/integration/targets/ec2_placement_group/tasks/main.yml @@ -12,7 +12,7 @@ block: - - name: set up environment for testing. + - name: Set up environment for testing. include_tasks: env_setup.yml - name: Create a placement group 1 - check_mode @@ -22,12 +22,11 @@ check_mode: true register: pg_1_create_check_mode - - assert: + - name: Assert that placement group data is returned (check mode) + ansible.builtin.assert: that: - pg_1_create_check_mode is changed - pg_1_create_check_mode.placement_group.name == resource_prefix ~ '-pg1' - - pg_1_create_check_mode.placement_group.state == "DryRun" - - '"ec2:CreatePlacementGroup" in pg_1_create_check_mode.resource_actions' - name: Create a placement group 1 community.aws.ec2_placement_group: @@ -35,10 +34,11 @@ state: present register: pg_1_create - - set_fact: + - ansible.builtin.set_fact: placement_group_names: "{{ placement_group_names + [pg_1_create.placement_group.name] }}" - - assert: + - name: Assert that placement group is created + ansible.builtin.assert: that: - pg_1_create is changed - pg_1_create.placement_group.name == resource_prefix ~ '-pg1' @@ -51,7 +51,8 @@ - '{{ resource_prefix }}-pg1' register: pg_1_info_result - - assert: + - name: Assert that placement group is created + ansible.builtin.assert: that: - pg_1_info_result is not changed - pg_1_info_result.placement_groups[0].name == resource_prefix ~ '-pg1' @@ -65,7 +66,8 @@ state: present register: pg_1_create - - assert: + - name: Assert that placement group is not created (idempotent) + ansible.builtin.assert: that: - pg_1_create is not changed - pg_1_create.placement_group.name == resource_prefix ~ '-pg1' @@ -79,12 +81,12 @@ check_mode: true register: pg_1_create_check_mode_idem - - assert: + - name: Assert that placement group is not created (idempotent - check_mode) + ansible.builtin.assert: that: - pg_1_create_check_mode_idem is not changed - pg_1_create_check_mode_idem.placement_group.name == resource_prefix ~ '-pg1' - pg_1_create_check_mode_idem.placement_group.state == "available" - - '"ec2:CreatePlacementGroup" not in pg_1_create_check_mode_idem.resource_actions' - name: Create a placement group 2 - check_mode community.aws.ec2_placement_group: @@ -94,12 +96,11 @@ check_mode: true register: pg_2_create_check_mode - - assert: + - name: Assert that placement group is created + ansible.builtin.assert: that: - pg_2_create_check_mode is changed - pg_2_create_check_mode.placement_group.name == resource_prefix ~ '-pg2' - - pg_2_create_check_mode.placement_group.state == "DryRun" - - '"ec2:CreatePlacementGroup" in pg_2_create_check_mode.resource_actions' - name: Create a placement group 2 with spread strategy community.aws.ec2_placement_group: @@ -108,14 +109,15 @@ strategy: spread register: pg_2_create - - assert: + - name: Assert that placement group is created + ansible.builtin.assert: that: - pg_2_create is changed - pg_2_create.placement_group.name == resource_prefix ~ '-pg2' - pg_2_create.placement_group.state == "available" - '"ec2:CreatePlacementGroup" in pg_2_create.resource_actions' - - set_fact: + - ansible.builtin.set_fact: placement_group_names: "{{ placement_group_names + [pg_2_create.placement_group.name] }}" - name: Gather information about placement group 2 @@ -124,7 +126,8 @@ - '{{ resource_prefix }}-pg2' register: pg_2_info_result - - assert: + - name: Assert that placement group is created + ansible.builtin.assert: that: - pg_2_info_result is not changed - pg_2_info_result.placement_groups[0].name == resource_prefix ~ '-pg2' @@ -139,7 +142,8 @@ strategy: spread register: pg_2_create - - assert: + - name: Assert that placement group exists (idempotent) + ansible.builtin.assert: that: - pg_2_create is not changed - pg_2_create.placement_group.name == resource_prefix ~ '-pg2' @@ -154,12 +158,12 @@ check_mode: true register: pg_2_create_check_mode_idem - - assert: + - name: Assert that placement group exists (idempotent - check_mode) + ansible.builtin.assert: that: - pg_2_create_check_mode_idem is not changed - pg_2_create_check_mode_idem.placement_group.name == resource_prefix ~ '-pg2' - pg_2_create_check_mode_idem.placement_group.state == "available" - - '"ec2:CreatePlacementGroup" not in pg_2_create_check_mode_idem.resource_actions' - name: Create a placement group 3 - check_mode community.aws.ec2_placement_group: @@ -170,12 +174,11 @@ check_mode: true register: pg_3_create_check_mode - - assert: + - name: Assert that placement group exists + ansible.builtin.assert: that: - pg_3_create_check_mode is changed - pg_3_create_check_mode.placement_group.name == resource_prefix ~ '-pg3' - - pg_3_create_check_mode.placement_group.state == "DryRun" - - '"ec2:CreatePlacementGroup" in pg_3_create_check_mode.resource_actions' - name: Create a placement group 3 with Partition strategy community.aws.ec2_placement_group: @@ -185,14 +188,15 @@ partition_count: 4 register: pg_3_create - - assert: + - name: Assert that placement group exists + ansible.builtin.assert: that: - pg_3_create is changed - pg_3_create.placement_group.name == resource_prefix ~ '-pg3' - pg_3_create.placement_group.state == "available" - '"ec2:CreatePlacementGroup" in pg_3_create.resource_actions' - - set_fact: + - ansible.builtin.set_fact: placement_group_names: "{{ placement_group_names + [pg_3_create.placement_group.name] }}" @@ -202,7 +206,8 @@ - '{{ resource_prefix }}-pg3' register: pg_3_info_result - - assert: + - name: Assert that placement group exists + ansible.builtin.assert: that: - pg_3_info_result is not changed - pg_3_info_result.placement_groups[0].name == resource_prefix ~ '-pg3' @@ -218,7 +223,8 @@ partition_count: 4 register: pg_3_create - - assert: + - name: Assert that placement group exists (idempotent) + ansible.builtin.assert: that: - pg_3_create is not changed - pg_3_create.placement_group.name == resource_prefix ~ '-pg3' @@ -234,12 +240,12 @@ check_mode: true register: pg_3_create_check_mode_idem - - assert: + - name: Assert that placement group exists (idempotent - check_mode) + ansible.builtin.assert: that: - pg_3_create_check_mode_idem is not changed - pg_3_create_check_mode_idem.placement_group.name == resource_prefix ~ '-pg3' - pg_3_create_check_mode_idem.placement_group.state == "available" - - '"ec2:CreatePlacementGroup" not in pg_3_create_check_mode_idem.resource_actions' - name: Create a placement group 4 with tags - check_mode community.aws.ec2_placement_group: @@ -252,14 +258,13 @@ check_mode: true register: pg_4_create_check_mode - - assert: + - name: Assert that placement group exists (check-mode) + ansible.builtin.assert: that: - pg_4_create_check_mode is changed - pg_4_create_check_mode.placement_group.name == resource_prefix ~ '-pg4' - - pg_4_create_check_mode.placement_group.state == "DryRun" - pg_4_create_check_mode.placement_group.tags.foo == "test1" - pg_4_create_check_mode.placement_group.tags.bar == "test2" - - '"ec2:CreatePlacementGroup" in pg_4_create_check_mode.resource_actions' - name: Create a placement group 4 with tags community.aws.ec2_placement_group: @@ -271,7 +276,8 @@ bar: test2 register: pg_4_create - - assert: + - name: Assert that placement group exists + ansible.builtin.assert: that: - pg_4_create is changed - pg_4_create.placement_group.name == resource_prefix ~ '-pg4' @@ -280,7 +286,7 @@ - pg_4_create.placement_group.tags.bar == "test2" - '"ec2:CreatePlacementGroup" in pg_4_create.resource_actions' - - set_fact: + - ansible.builtin.set_fact: placement_group_names: "{{ placement_group_names + [pg_4_create.placement_group.name] }}" - name: Gather information about placement group 4 @@ -289,7 +295,8 @@ - '{{ resource_prefix }}-pg4' register: pg_4_info_result - - assert: + - name: Assert that placement group exists + ansible.builtin.assert: that: - pg_4_info_result is not changed - pg_4_info_result.placement_groups[0].name == resource_prefix ~ '-pg4' @@ -309,7 +316,8 @@ bar: test2 register: pg_4_create - - assert: + - name: Assert that placement group exists (idempotent) + ansible.builtin.assert: that: - pg_4_create is not changed - pg_4_create.placement_group.name == resource_prefix ~ '-pg4' @@ -330,7 +338,8 @@ check_mode: true register: pg_4_create_check_mode_idem - - assert: + - name: Assert that placement group exists (idempotent - check-mode) + ansible.builtin.assert: that: - pg_4_create_check_mode_idem is not changed - pg_4_create_check_mode_idem.placement_group.name == resource_prefix ~ '-pg4' @@ -338,7 +347,6 @@ - pg_4_create_check_mode_idem.placement_group.strategy == "cluster" - pg_4_create_check_mode_idem.placement_group.tags.foo == "test1" - pg_4_create_check_mode_idem.placement_group.tags.bar == "test2" - - '"ec2:CreatePlacementGroup" not in pg_4_create_check_mode_idem.resource_actions' - name: List all placement groups. community.aws.ec2_placement_group_info: @@ -346,9 +354,6 @@ # Delete Placement Group ========================================== - # On using check_mode for delete placement group operation - # If operation would have succeeded, the error response is DryRunOperation. - # Otherwise, it is UnauthorizedOperation . - name: Delete a placement group 1 - check_mode community.aws.ec2_placement_group: name: '{{ resource_prefix }}-pg1' @@ -357,11 +362,10 @@ register: pg_1_delete_check_mode ignore_errors: true - - assert: + - name: Assert check mode (check mode) + ansible.builtin.assert: that: - - pg_1_delete_check_mode is not changed - - pg_1_delete_check_mode.error.code == 'DryRunOperation' - - '"ec2:DeletePlacementGroup" in pg_1_delete_check_mode.resource_actions' + - pg_1_delete_check_mode is changed - name: Delete a placement group 1 community.aws.ec2_placement_group: @@ -369,7 +373,8 @@ state: absent register: pg_1_delete - - assert: + - name: Assert that deletion is successful + ansible.builtin.assert: that: - pg_1_delete is changed - '"ec2:DeletePlacementGroup" in pg_1_delete.resource_actions' @@ -380,7 +385,8 @@ state: absent register: pg_1_delete - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_1_delete is not changed - '"ec2:DeletePlacementGroup" not in pg_1_delete.resource_actions' @@ -393,10 +399,10 @@ register: pg_1_delete_check_mode_idem ignore_errors: true - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_1_delete_check_mode_idem is not changed - - '"ec2:DeletePlacementGroup" not in pg_1_delete_check_mode_idem.resource_actions' - name: Delete a placement group 2 - check_mode community.aws.ec2_placement_group: @@ -406,11 +412,10 @@ register: pg_2_delete_check_mode ignore_errors: true - - assert: + - name: Assert that check mode is successful + ansible.builtin.assert: that: - - pg_2_delete_check_mode is not changed - - pg_2_delete_check_mode.error.code == 'DryRunOperation' - - '"ec2:DeletePlacementGroup" in pg_2_delete_check_mode.resource_actions' + - pg_2_delete_check_mode is changed - name: Delete a placement group 2 community.aws.ec2_placement_group: @@ -418,7 +423,8 @@ state: absent register: pg_2_delete - - assert: + - name: Assert that there is change + ansible.builtin.assert: that: - pg_2_delete is changed - '"ec2:DeletePlacementGroup" in pg_2_delete.resource_actions' @@ -429,7 +435,8 @@ state: absent register: pg_2_delete - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_2_delete is not changed - '"ec2:DeletePlacementGroup" not in pg_2_delete.resource_actions' @@ -442,10 +449,10 @@ register: pg_2_delete_check_mode_idem ignore_errors: true - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_2_delete_check_mode_idem is not changed - - '"ec2:DeletePlacementGroup" not in pg_2_delete_check_mode_idem.resource_actions' - name: Delete a placement group 3 - check_mode community.aws.ec2_placement_group: @@ -455,11 +462,10 @@ register: pg_3_delete_check_mode ignore_errors: true - - assert: + - name: Assert that there is change - check mode + ansible.builtin.assert: that: - - pg_3_delete_check_mode is not changed - - pg_3_delete_check_mode.error.code == 'DryRunOperation' - - '"ec2:DeletePlacementGroup" in pg_3_delete_check_mode.resource_actions' + - pg_3_delete_check_mode is changed - name: Delete a placement group 3 community.aws.ec2_placement_group: @@ -467,7 +473,8 @@ state: absent register: pg_3_delete - - assert: + - name: Assert that there is change + ansible.builtin.assert: that: - pg_3_delete is changed - '"ec2:DeletePlacementGroup" in pg_3_delete.resource_actions' @@ -477,8 +484,9 @@ name: '{{ resource_prefix }}-pg3' state: absent register: pg_3_delete - - - assert: + + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_3_delete is not changed - '"ec2:DeletePlacementGroup" not in pg_3_delete.resource_actions' @@ -491,10 +499,10 @@ register: pg_3_delete_check_mode_idem ignore_errors: true - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_3_delete_check_mode_idem is not changed - - '"ec2:DeletePlacementGroup" not in pg_3_delete_check_mode_idem.resource_actions' - name: Delete a placement group 4 - check_mode community.aws.ec2_placement_group: @@ -504,11 +512,10 @@ register: pg_4_delete_check_mode ignore_errors: true - - assert: + - name: Assert that there is change check mode + ansible.builtin.assert: that: - - pg_4_delete_check_mode is not changed - - pg_4_delete_check_mode.error.code == 'DryRunOperation' - - '"ec2:DeletePlacementGroup" in pg_4_delete_check_mode.resource_actions' + - pg_4_delete_check_mode is changed - name: Delete a placement group 4 @@ -517,7 +524,8 @@ state: absent register: pg_4_delete - - assert: + - name: Assert that there is change + ansible.builtin.assert: that: - pg_4_delete is changed - '"ec2:DeletePlacementGroup" in pg_4_delete.resource_actions' @@ -528,7 +536,8 @@ state: absent register: pg_4_delete - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_4_delete is not changed - '"ec2:DeletePlacementGroup" not in pg_4_delete.resource_actions' @@ -541,10 +550,10 @@ register: pg_4_delete_check_mode_idem ignore_errors: true - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - pg_4_delete_check_mode_idem is not changed - - '"ec2:DeletePlacementGroup" not in pg_4_delete_check_mode_idem.resource_actions' always: From 7dabfccc73b77239b95de01069890f3ee7bf95f0 Mon Sep 17 00:00:00 2001 From: GomathiselviS Date: Fri, 18 Oct 2024 06:05:47 -0400 Subject: [PATCH 8/9] ec2_transit_gateway_vpc_attachment - Prepare module for migration to amazon.aws (#2157) SUMMARY Refer: https://issues.redhat.com/browse/ACA-1868 This PR refactors and adds necessary documentation to ec2_transit_gateway_vpc_attachment and ec2_transit_gateway_vpc_attachment_info ISSUE TYPE Bugfix Pull Request Docs Pull Request Feature Pull Request New Module Pull Request COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis Reviewed-by: Bikouo Aubin --- ...20240924-fix-documentation-tgw-vpc-att.yml | 2 + plugins/module_utils/transitgateway.py | 718 +++++++++++------- .../ec2_transit_gateway_vpc_attachment.py | 257 +++---- ...ec2_transit_gateway_vpc_attachment_info.py | 122 +-- .../tasks/cleanup.yml | 44 +- .../tasks/complex.yml | 112 +-- .../tasks/setup.yml | 20 +- .../tasks/simple.yml | 655 +++++++++------- 8 files changed, 1094 insertions(+), 836 deletions(-) create mode 100644 changelogs/fragments/20240924-fix-documentation-tgw-vpc-att.yml diff --git a/changelogs/fragments/20240924-fix-documentation-tgw-vpc-att.yml b/changelogs/fragments/20240924-fix-documentation-tgw-vpc-att.yml new file mode 100644 index 00000000000..0fa478776bb --- /dev/null +++ b/changelogs/fragments/20240924-fix-documentation-tgw-vpc-att.yml @@ -0,0 +1,2 @@ +minor_changes: + - ec2_transit_gateway_vpc_attachment - Modify doumentation and refactor to adhere to coding guidelines (https://github.com/ansible-collections/community.aws/pull/2157). diff --git a/plugins/module_utils/transitgateway.py b/plugins/module_utils/transitgateway.py index 8a82a839ff1..a3454931205 100644 --- a/plugins/module_utils/transitgateway.py +++ b/plugins/module_utils/transitgateway.py @@ -5,221 +5,234 @@ from copy import deepcopy -from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +try: + from botocore.exceptions import BotoCoreError + from botocore.exceptions import ClientError +except ImportError: + pass + +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple + +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 create_transit_gateway_vpc_attachment +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_transit_gateway_vpc_attachment +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_subnets +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_transit_gateway_vpc_attachments +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import modify_transit_gateway_vpc_attachment +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 boto3_tag_specifications from ansible_collections.amazon.aws.plugins.module_utils.transformation import ansible_dict_to_boto3_filter_list +from ansible_collections.amazon.aws.plugins.module_utils.transformation import boto3_resource_to_ansible_dict +from ansible_collections.amazon.aws.plugins.module_utils.waiters import get_waiter + +from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule + + +def get_states() -> List[str]: + return [ + "available", + "deleting", + "failed", + "failing", + "initiatingRequest", + "modifying", + "pendingAcceptance", + "pending", + "rollingBack", + "rejected", + "rejecting", + ] + + +def subnets_to_vpc( + client, module: AnsibleAWSModule, subnets: List[str], subnet_details: Optional[List[Dict[str, Any]]] = None +) -> Optional[str]: + if not subnets: + return None + + if subnet_details is None: + try: + subnet_details = describe_subnets(client, SubnetIds=list(subnets)) + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) + + vpcs = [s.get("VpcId") for s in subnet_details] + if len(set(vpcs)) > 1: + module.fail_json( + msg="Attachment subnets may only be in one VPC, multiple VPCs found", + vpcs=list(set(vpcs)), + subnets=subnet_details, + ) + + return vpcs[0] + + +def find_existing_attachment( + client, module: AnsibleAWSModule, filters: Optional[Dict[str, Any]] = None, attachment_id: Optional[str] = None +) -> Optional[Dict[str, Any]]: + """Find an existing transit gateway attachment based on filters or attachment ID. + + Args: + client: The AWS client used to interact with the EC2 service. + module: The Ansible module instance used for error handling. + filters (Optional[Dict[str, Any]]): A dictionary of filters to apply when searching for attachments. + attachment_id (Optional[str]): The ID of a specific attachment to find. + + Returns: + Optional[Dict[str, Any]]: The found attachment details or None if not found. -from ansible_collections.community.aws.plugins.module_utils.ec2 import BaseEc2Manager -from ansible_collections.community.aws.plugins.module_utils.ec2 import Boto3Mixin -from ansible_collections.community.aws.plugins.module_utils.ec2 import Ec2WaiterFactory + Raises: + ValueError: If multiple attachments match the criteria. + """ + # Find an existing attachment based on filters + params = {} + if attachment_id: + params["TransitGatewayAttachmentIds"] = [attachment_id] + elif filters: + params["Filters"] = ansible_dict_to_boto3_filter_list(filters) + + try: + attachments = describe_transit_gateway_vpc_attachments(client, **params) + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) + + if len(attachments) > 1: + raise ValueError("Multiple matching attachments found, provide an ID.") + + return attachments[0] if attachments else None + + +class TransitGatewayAttachmentStateManager: + def __init__(self, client, module: AnsibleAWSModule, attachment_id: str) -> None: + self.client = client + self.module = module + self.attachment_id = attachment_id -class TgwWaiterFactory(Ec2WaiterFactory): @property - def _waiter_model_data(self): - data = super(TgwWaiterFactory, self)._waiter_model_data - # split the TGW waiters so we can keep them close to everything else. - tgw_data = dict( - tgw_attachment_available=dict( - operation="DescribeTransitGatewayAttachments", - delay=5, - maxAttempts=120, - acceptors=[ - dict( - state="success", - matcher="pathAll", - expected="available", - argument="TransitGatewayAttachments[].State", - ), - ], - ), - tgw_attachment_deleted=dict( - operation="DescribeTransitGatewayAttachments", - delay=5, - maxAttempts=120, - acceptors=[ - dict( - state="retry", - matcher="pathAll", - expected="deleting", - argument="TransitGatewayAttachments[].State", - ), - dict( - state="success", - matcher="pathAll", - expected="deleted", - argument="TransitGatewayAttachments[].State", - ), - dict( - state="success", - matcher="path", - expected=True, - argument="length(TransitGatewayAttachments[]) == `0`", - ), - dict(state="success", matcher="error", expected="InvalidRouteTableID.NotFound"), - ], - ), - ) - data.update(tgw_data) - return data - - -class TGWAttachmentBoto3Mixin(Boto3Mixin): - def __init__(self, module, **kwargs): - self.tgw_waiter_factory = TgwWaiterFactory(module) - super(TGWAttachmentBoto3Mixin, self).__init__(module, **kwargs) - - # Paginators can't be (easily) wrapped, so we wrap this method with the - # retry - retries the full fetch, but better than simply giving up. - @AWSRetry.jittered_backoff() - def _paginated_describe_transit_gateway_vpc_attachments(self, **params): - paginator = self.client.get_paginator("describe_transit_gateway_vpc_attachments") - return paginator.paginate(**params).build_full_result() - - @Boto3Mixin.aws_error_handler("describe transit gateway attachments") - def _describe_vpc_attachments(self, **params): - result = self._paginated_describe_transit_gateway_vpc_attachments(**params) - return result.get("TransitGatewayVpcAttachments", None) - - @Boto3Mixin.aws_error_handler("create transit gateway attachment") - def _create_vpc_attachment(self, **params): - result = self.client.create_transit_gateway_vpc_attachment(aws_retry=True, **params) - return result.get("TransitGatewayVpcAttachment", None) - - @Boto3Mixin.aws_error_handler("modify transit gateway attachment") - def _modify_vpc_attachment(self, **params): - result = self.client.modify_transit_gateway_vpc_attachment(aws_retry=True, **params) - return result.get("TransitGatewayVpcAttachment", None) - - @Boto3Mixin.aws_error_handler("delete transit gateway attachment") - def _delete_vpc_attachment(self, **params): - try: - result = self.client.delete_transit_gateway_vpc_attachment(aws_retry=True, **params) - except is_boto3_error_code("ResourceNotFoundException"): - return None - return result.get("TransitGatewayVpcAttachment", None) + def waiter_config(self) -> Dict[str, Any]: + params: Dict[str, Any] = {} - @Boto3Mixin.aws_error_handler("transit gateway attachment to finish deleting") - def _wait_tgw_attachment_deleted(self, **params): - waiter = self.tgw_waiter_factory.get_waiter("tgw_attachment_deleted") - waiter.wait(**params) + delay = min(5, self.module.params.get("wait_timeout")) + max_attempts = self.module.params.get("wait_timeout") // delay + config = dict(Delay=delay, MaxAttempts=max_attempts) + params["WaiterConfig"] = config - @Boto3Mixin.aws_error_handler("transit gateway attachment to become available") - def _wait_tgw_attachment_available(self, **params): - waiter = self.tgw_waiter_factory.get_waiter("tgw_attachment_available") - waiter.wait(**params) + return params - def _normalize_tgw_attachment(self, rtb): - return self._normalize_boto3_resource(rtb) + def create_attachment(self, params: Dict[str, Any]) -> str: + """ + Create a new transit gateway attachment. - def _get_tgw_vpc_attachment(self, **params): - # Only for use with a single attachment, use _describe_vpc_attachments for - # multiple tables. - attachments = self._describe_vpc_attachments(**params) + Args: + params (Dict[str, Any]): A dictionary containing the parameters needed to + create the transit gateway attachment. - if not attachments: - return None + Returns: + str: The ID of the newly created transit gateway attachment. - attachment = attachments[0] - return attachment + Raises: + AnsibleEC2Error: If there is an error while creating the VPC attachment, + it will fail the module and provide an error message. + """ + try: + tags = params.pop("Tags") + except KeyError: + tags = None + if tags: + params["TagSpecifications"] = boto3_tag_specifications(tags, types=["transit-gateway-attachment"]) -class BaseTGWManager(BaseEc2Manager): - @Boto3Mixin.aws_error_handler("connect to AWS") - def _create_client(self, client_name="ec2"): - if client_name == "ec2": - error_codes = ["IncorrectState"] - else: - error_codes = [] + try: + response = create_transit_gateway_vpc_attachment(self.client, **params) + except AnsibleEC2Error as e: + self.module.fail_json_aws_error(e) - retry_decorator = AWSRetry.jittered_backoff( - catch_extra_error_codes=error_codes, - ) - client = self.module.client(client_name, retry_decorator=retry_decorator) - return client - - -class TransitGatewayVpcAttachmentManager(TGWAttachmentBoto3Mixin, BaseTGWManager): - TAG_RESOURCE_TYPE = "transit-gateway-attachment" - - def __init__(self, module, id=None): - self._subnet_updates = dict() - super(TransitGatewayVpcAttachmentManager, self).__init__(module=module, id=id) - - def _get_id_params(self, id=None, id_list=False): - if not id: - id = self.resource_id - if not id: - # Users should never see this, but let's cover ourself - self.module.fail_json(msg="Attachment identifier parameter missing") - - if id_list: - return dict(TransitGatewayAttachmentIds=[id]) - return dict(TransitGatewayAttachmentId=id) - - def _extra_error_output(self): - output = super(TransitGatewayVpcAttachmentManager, self)._extra_error_output() - if self.resource_id: - output["TransitGatewayAttachmentId"] = self.resource_id - return output - - def _filter_immutable_resource_attributes(self, resource): - resource = super(TransitGatewayVpcAttachmentManager, self)._filter_immutable_resource_attributes(resource) - resource.pop("TransitGatewayId", None) - resource.pop("VpcId", None) - resource.pop("VpcOwnerId", None) - resource.pop("State", None) - resource.pop("SubnetIds", None) - resource.pop("CreationTime", None) - resource.pop("Tags", None) - return resource + self.attachment_id = response["TransitGatewayAttachmentId"] - def _set_option(self, name, value): - if value is None: + return response["TransitGatewayAttachmentId"] + + def delete_attachment(self) -> bool: + # Delete the transit gateway attachment + + if not self.attachment_id: return False - # For now VPC Attachment options are all enable/disable - if value: - value = "enable" - else: - value = "disable" - options = deepcopy(self._preupdate_resource.get("Options", dict())) - options.update(self._resource_updates.get("Options", dict())) - options[name] = value + if not self.module.check_mode: + try: + delete_transit_gateway_vpc_attachment(self.client, self.attachment_id) + except AnsibleEC2Error as e: + self.module.fail_json_aws_error(e) - return self._set_resource_value("Options", options) + return True - def set_dns_support(self, value): - return self._set_option("DnsSupport", value) + def wait_for_state_change(self, desired_state: str) -> None: + # Wait until attachment reaches the desired state + params = {"TransitGatewayAttachmentIds": [self.attachment_id]} + params.update(self.waiter_config) + try: + waiter = get_waiter(self.client, f"transit_gateway_vpc_attachment_{desired_state}") + waiter.wait(**params) + except (BotoCoreError, ClientError) as e: + self.module.fail_json_aws_error(e) - def set_multicast_support(self, value): - return self._set_option("MulticastSupport", value) - def set_ipv6_support(self, value): - return self._set_option("Ipv6Support", value) +class AttachmentConfigurationManager: + def __init__(self, client, module: AnsibleAWSModule, attachment_id: str, existing: Dict[str, Any]) -> None: + self.client = client + self.module = module + self.attachment_id = attachment_id - def set_appliance_mode_support(self, value): - return self._set_option("ApplianceModeSupport", value) + self.existing = existing or {} + self._resource_updates = {} + self._subnets_to_add = [] + self._subnets_to_remove = [] - def set_transit_gateway(self, tgw_id): - return self._set_resource_value("TransitGatewayId", tgw_id) + @property + def resource_updates(self) -> Dict[str, Any]: + return self._resource_updates - def set_vpc(self, vpc_id): - return self._set_resource_value("VpcId", vpc_id) + @property + def subnets_to_add(self) -> List[str]: + return self._subnets_to_add - def set_subnets(self, subnets=None, purge=True): + @property + def subnets_to_remove(self) -> List[str]: + return self._subnets_to_remove + + def set_subnets(self, subnets: Optional[List[str]] = None, purge: bool = True) -> None: + """ + Set or update the subnets associated with the transit gateway attachment. + + Args: + subnets (Optional[List[str]]): A list of subnet IDs to associate with + the attachment. + purge (bool): If True, the existing subnets will be replaced with the + specified subnets. + """ + # Set or update the subnets associated with the attachment if subnets is None: - return False + return - current_subnets = set(self._preupdate_resource.get("SubnetIds", [])) + current_subnets = set(self.existing.get("SubnetIds", [])) desired_subnets = set(subnets) if not purge: desired_subnets = desired_subnets.union(current_subnets) # We'll pull the VPC ID from the subnets, no point asking for # information we 'know'. - subnet_details = self._describe_subnets(SubnetIds=list(desired_subnets)) - vpc_id = self.subnets_to_vpc(desired_subnets, subnet_details) + try: + subnet_details = describe_subnets(self.client, SubnetIds=list(desired_subnets)) + except AnsibleEC2Error as e: + self.module.fail_json_aws_error(e) + vpc_id = subnets_to_vpc(self.client, self.module, desired_subnets, subnet_details) self._set_resource_value("VpcId", vpc_id, immutable=True) # Only one subnet per-AZ is permitted @@ -231,138 +244,269 @@ def set_subnets(self, subnets=None, purge=True): subnets=subnet_details, ) - subnets_to_add = list(desired_subnets.difference(current_subnets)) - subnets_to_remove = list(current_subnets.difference(desired_subnets)) - if not subnets_to_remove and not subnets_to_add: - return False - self._subnet_updates = dict(add=subnets_to_add, remove=subnets_to_remove) + self._subnets_to_add = list(desired_subnets.difference(current_subnets)) + self._subnets_to_remove = list(current_subnets.difference(desired_subnets)) self._set_resource_value("SubnetIds", list(desired_subnets)) - return True - def subnets_to_vpc(self, subnets, subnet_details=None): - if not subnets: - return None + def set_dns_support(self, value): + return self._set_option("DnsSupport", value) - if subnet_details is None: - subnet_details = self._describe_subnets(SubnetIds=list(subnets)) + def set_ipv6_support(self, value): + return self._set_option("Ipv6Support", value) - vpcs = [s.get("VpcId") for s in subnet_details] - if len(set(vpcs)) > 1: - self.module.fail_json( - msg="Attachment subnets may only be in one VPC, multiple VPCs found", - vpcs=list(set(vpcs)), - subnets=subnet_details, - ) + def set_appliance_mode_support(self, value): + return self._set_option("ApplianceModeSupport", value) - return vpcs[0] - - def _do_deletion_wait(self, id=None, **params): - all_params = self._get_id_params(id=id, id_list=True) - all_params.update(**params) - return self._wait_tgw_attachment_deleted(**all_params) - - def _do_creation_wait(self, id=None, **params): - all_params = self._get_id_params(id=id, id_list=True) - all_params.update(**params) - return self._wait_tgw_attachment_available(**all_params) - - def _do_update_wait(self, id=None, **params): - all_params = self._get_id_params(id=id, id_list=True) - all_params.update(**params) - return self._wait_tgw_attachment_available(**all_params) - - def _do_create_resource(self): - params = self._merge_resource_changes(filter_immutable=False, creation=True) - response = self._create_vpc_attachment(**params) - if response: - self.resource_id = response.get("TransitGatewayAttachmentId", None) - return response - - def _do_update_resource(self): - if self._preupdate_resource.get("State", None) == "pending": - # Resources generally don't like it if you try to update before creation - # is complete. If things are in a 'pending' state they'll often throw - # exceptions. - self._wait_for_creation() - elif self._preupdate_resource.get("State", None) == "deleting": - self.module.fail_json(msg="Deletion in progress, unable to update", route_tables=[self.original_resource]) + def set_transit_gateway(self, tgw_id: str): + return self._set_resource_value("TransitGatewayId", tgw_id) - updates = self._filter_immutable_resource_attributes(self._resource_updates) - subnets_to_add = self._subnet_updates.get("add", []) - subnets_to_remove = self._subnet_updates.get("remove", []) - if subnets_to_add: - updates["AddSubnetIds"] = subnets_to_add - if subnets_to_remove: - updates["RemoveSubnetIds"] = subnets_to_remove + def set_vpc(self, vpc_id: str): + return self._set_resource_value("VpcId", vpc_id) + + def set_tags(self, tags, purge_tags): + current_tags = boto3_tag_list_to_ansible_dict(self.existing.get("Tags", None)) - if not updates: + if purge_tags: + desired_tags = deepcopy(tags) + else: + desired_tags = {**current_tags, **tags} + + self._set_resource_value("Tags", desired_tags) + + def _get_resource_value(self, key, default=None): + default_value = self.existing.get(key, default) + return self._resource_updates.get(key, default_value) + + def _set_option(self, name: str, value: Optional[bool]) -> bool: + """ + Set a VPC attachment option to either enable or disable. + + Args: + name (str): The name of the option to be updated. + value (Optional[bool]): A boolean indicating whether to enable (True) + or disable (False) the specified option. If None, no action is + taken. + + Returns: + bool: Returns True if the option was successfully set, or False if + no update was made (because the value was None). + """ + if value is None: return False - if self.module.check_mode: - return True + # For now VPC Attachment options are all enable/disable + value = "enable" if value else "disable" + + options = deepcopy(self.existing.get("Options", dict())) + options.update(self._resource_updates.get("Options", dict())) + options[name] = value + + return self._set_resource_value("Options", options) + + def _set_resource_value(self, key, value, description: Optional[str] = None, immutable: bool = False) -> bool: + """ + Set a value for a resource attribute and track changes. + + Args: + key (str): The attribute key to be updated. + value (Any): The new value to set for the specified key. + description (Optional[str], optional): A human-readable description of the + resource attribute. + immutable (bool, optional): A flag indicating whether the attribute is + immutable. If True, and the resource exists, an error will be raised + if attempting to change the value. Defaults to False. + + Returns: + bool: Returns True if the value was successfully set, or False if no + update was made. + """ + if value is None or value == self._get_resource_value(key): + return False + + if immutable and self.existing: + description = description or key + self.module.fail_json(msg=f"{description} can not be updated after creation") + + self.resource_updates[key] = value - updates.update(self._get_id_params(id_list=False)) - self._modify_vpc_attachment(**updates) return True - def get_resource(self): - return self.get_attachment() + def filter_immutable_resource_attributes(self, resource: Dict[str, Any]) -> Dict[str, Any]: + """ + Filter out immutable resource attributes from the given resource dictionary. - def delete(self, id=None): - if id: - id_params = self._get_id_params(id=id, id_list=True) - result = self._get_tgw_vpc_attachment(**id_params) - else: - result = self._preupdate_resource + Args: + resource (Dict[str, Any]): A dictionary representing the resource, which + may contain various attributes, including both mutable and immutable ones. + + Returns: + Dict[str, Any]: A new dictionary containing only the mutable attributes + of the resource. + """ + immutable_options = ["TransitGatewayId", "VpcId", "VpcOwnerId", "State", "SubnetIds", "CreationTime", "Tags"] + return {key: value for key, value in resource.items() if key not in immutable_options} - self.updated_resource = dict() - if not result: +class TransitGatewayVpcAttachmentManager: + def __init__( + self, client, module: AnsibleAWSModule, existing: Dict[str, Any], attachment_id: Optional[str] = None + ) -> None: + self.client = client + self.module = module + self.attachment_id = attachment_id + self.existing = existing or {} + self.updated = {} + self.changed = False + + self.state_manager = TransitGatewayAttachmentStateManager(client, module, attachment_id) + self.config_manager = AttachmentConfigurationManager(client, module, attachment_id, existing) + + def merge_resource_changes(self, filter_immutable: bool = True) -> Dict[str, Any]: + """Merge existing resource attributes with updates, optionally filtering out immutable attributes. + + Args: + filter_immutable (bool): Whether to filter out immutable resource attributes. Defaults to True. + + Returns: + Dict[str, Any]: The merged resource attributes. + """ + resource = deepcopy(self.existing) + resource.update(self.config_manager.resource_updates) + + if filter_immutable: + resource = self.config_manager.filter_immutable_resource_attributes(resource) + + return resource + + def apply_configuration(self): + """Apply configuration changes to the transit gateway attachment. + + Returns: + bool: True if configuration changes were applied, False otherwise. + """ + # Apply any configuration changes to the attachment + if not self.attachment_id: return False - if result.get("State") == "deleting": - self._wait_for_deletion() + updates = self.config_manager.filter_immutable_resource_attributes(self.config_manager.resource_updates) + + subnets_to_add = self.config_manager.subnets_to_add + subnets_to_remove = self.config_manager.subnets_to_remove + + # Check if there are no changes to apply + if not updates and not subnets_to_add and not subnets_to_remove: return False - if self.module.check_mode: - self.changed = True - return True + if subnets_to_add: + updates["AddSubnetIds"] = subnets_to_add + if subnets_to_remove: + updates["RemoveSubnetIds"] = subnets_to_remove - id_params = self._get_id_params(id=id, id_list=False) + updates["TransitGatewayAttachmentId"] = self.attachment_id - result = self._delete_vpc_attachment(**id_params) + if not self.module.check_mode: + try: + modify_transit_gateway_vpc_attachment(self.client, **updates) + except AnsibleEC2Error as e: + self.module.fail_json_aws_error(e) + return True - self.changed |= bool(result) + def _set_configuration_parameters(self) -> None: + """Set configuration parameters for the transit gateway attachment.""" + self.config_manager.set_transit_gateway(self.module.params.get("transit_gateway")) + self.config_manager.set_subnets(self.module.params["subnets"], self.module.params.get("purge_subnets", True)) + self.config_manager.set_dns_support(self.module.params.get("dns_support")) + self.config_manager.set_ipv6_support(self.module.params.get("ipv6_support")) + self.config_manager.set_appliance_mode_support(self.module.params.get("appliance_mode_support")) + + def _prepare_tags(self) -> Tuple[Optional[Dict[str, str]], bool]: + """Prepare and return the tags and purge flag. + + Returns: + Tuple[Optional[Dict[str, str]], bool]: A tuple containing the tags dictionary and the purge flag. + """ + tags = self.module.params.get("tags") + purge_tags = self.module.params.get("purge_tags") + + if self.module.params.get("name"): + new_tags = {"Name": self.module.params["name"]} + if tags is None: + purge_tags = False + else: + new_tags.update(tags) + tags = new_tags + + return {} if tags is None else tags, purge_tags + + def _create_attachment(self) -> None: + """Create a new transit gateway attachment.""" + if not self.module.check_mode: + params = self.merge_resource_changes(filter_immutable=False) + self.attachment_id = self.state_manager.create_attachment(params) + + if self.module.params.get("wait"): + self.state_manager.wait_for_state_change("available") + + self.changed = True + + def _update_attachment(self, tags: Dict[str, Any], purge_tags: bool) -> None: + """Update an existing transit gateway attachment.""" + if self.existing.get("State") == "pending": + # Wait for resources to finish creating before updating + self.state_manager.wait_for_state_change("available") + elif self.existing.get("State") == "deleting": + self.module.fail_json(msg="Deletion in progress, unable to update", route_tables=[self.original_resource]) - self._wait_for_deletion() - return bool(result) + # Apply the configuration + if self.apply_configuration(): + self.changed = True + if self.module.params.get("wait"): + self.state_manager.wait_for_state_change("available") + + # Ensure tags are applied + self.changed |= ensure_ec2_tags( + self.client, + self.module, + self.attachment_id, + resource_type="transit-gateway-attachment", + tags=tags, + purge_tags=purge_tags, + ) - def list(self, filters=None, id=None): - params = dict() - if id: - params["TransitGatewayAttachmentIds"] = [id] - if filters: - params["Filters"] = ansible_dict_to_boto3_filter_list(filters) - attachments = self._describe_vpc_attachments(**params) - if not attachments: - return list() + def create_or_modify_attachment(self): + """Create or modify a transit gateway attachment based on the provided parameters.""" - return [self._normalize_tgw_attachment(a) for a in attachments] + # Set the configuration parameters + self._set_configuration_parameters() - def get_attachment(self, id=None): - # RouteTable needs a list, Association/Propagation needs a single ID - id_params = self._get_id_params(id=id, id_list=True) - id_param = self._get_id_params(id=id, id_list=False) - result = self._get_tgw_vpc_attachment(**id_params) + # Handle tags + tags, purge_tags = self._prepare_tags() - if not result: - return None + # Set tags in the configuration manager + self.config_manager.set_tags(tags, purge_tags) - if not id: - self._preupdate_resource = deepcopy(result) + if not self.existing: + self._create_attachment() + else: + self._update_attachment(tags, purge_tags) - attachment = self._normalize_tgw_attachment(result) - return attachment + # Handle check mode updates + if self.module.check_mode: + self.updated = camel_dict_to_snake_dict( + self.merge_resource_changes(filter_immutable=False), ignore_list=["Tags"] + ) + else: + self.updated = boto3_resource_to_ansible_dict( + find_existing_attachment(self.client, self.module, attachment_id=self.attachment_id) + ) - def _normalize_resource(self, resource): - return self._normalize_tgw_attachment(resource) + def delete_attachment(self): + """Delete attachment""" + if self.existing.get("State") == "deleting": + if self.module.params.get("wait"): + self.state_manager.wait_for_state_change("deleted") + self.change = False + else: + self.changed |= self.state_manager.delete_attachment() + if self.module.params.get("wait"): + self.state_manager.wait_for_state_change("deleted") diff --git a/plugins/modules/ec2_transit_gateway_vpc_attachment.py b/plugins/modules/ec2_transit_gateway_vpc_attachment.py index cfb6809a803..9ecdeb3b2bb 100644 --- a/plugins/modules/ec2_transit_gateway_vpc_attachment.py +++ b/plugins/modules/ec2_transit_gateway_vpc_attachment.py @@ -14,30 +14,30 @@ transit_gateway: description: - The ID of the Transit Gateway that the attachment belongs to. - - When creating a new attachment, I(transit_gateway) must be provided. - - At least one of I(name), I(transit_gateway) and I(id) must be provided. - - I(transit_gateway) is an immutable setting and can not be updated on an + - When creating a new attachment, O(transit_gateway) must be provided. + - At least one of O(name), O(transit_gateway) and O(id) must be provided. + - O(transit_gateway) is an immutable setting and can not be updated on an existing attachment. type: str required: false - aliases: ['transit_gateway_id'] + aliases: ["transit_gateway_id"] id: description: - The ID of the Transit Gateway Attachment. - - When I(id) is not set, a search using I(transit_gateway) and I(name) will be - performed. If multiple results are returned, the module will fail. - - At least one of I(name), I(transit_gateway) and I(id) must be provided. + - When O(id) is not set, a search using O(transit_gateway) and O(name) will be + performed. If multiple results are returned, the module will fail. + - At least one of O(name), O(transit_gateway) and O(id) must be provided. type: str required: false - aliases: ['attachment_id'] + aliases: ["attachment_id"] name: description: - - The C(Name) tag of the Transit Gateway attachment. - - Providing both I(id) and I(name) will set the C(Name) tag on an existing - attachment the matching I(id). - - Setting the C(Name) tag in I(tags) will also result in the C(Name) tag being + - The V(Name) tag of the Transit Gateway attachment. + - Providing both O(id) and O(name) will set the V(Name) tag on an existing + attachment the matching O(id). + - Setting the V(Name) tag in O(tags) will also result in the V(Name) tag being updated. - - At least one of I(name), I(transit_gateway) and I(id) must be provided. + - At least one of O(name), O(transit_gateway) and O(id) must be provided. type: str required: false state: @@ -45,7 +45,7 @@ - Create or remove the Transit Gateway attachment. type: str required: false - choices: ['present', 'absent'] + choices: ["present", "absent"] default: 'present' subnets: description: @@ -56,8 +56,8 @@ required: false purge_subnets: description: - - If I(purge_subnets=true), existing subnets will be removed from the - attachment as necessary to match exactly what is defined by I(subnets). + - If O(purge_subnets=true), existing subnets will be removed from the + attachment as necessary to match exactly what is defined by O(subnets). type: bool required: false default: true @@ -92,9 +92,11 @@ to reach the expected state. - Defaults to 600 seconds. type: int + default: 600 required: false author: - - "Mark Chappell (@tremble)" + - Mark Chappell (@tremble) + - Alina Buzachis (@alinabuzachis) extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules @@ -103,40 +105,40 @@ """ EXAMPLES = r""" -# Create a Transit Gateway attachment -- community.aws.ec2_transit_gateway_vpc_attachment: - state: present - transit_gateway: 'tgw-123456789abcdef01' - name: AnsibleTest-1 +- name: Create a Transit Gateway attachment + community.aws.ec2_transit_gateway_vpc_attachment: + state: "present" + transit_gateway: "tgw-123456789abcdef01" + name: "AnsibleTest-1" subnets: - - subnet-00000000000000000 - - subnet-11111111111111111 - - subnet-22222222222222222 + - "subnet-00000000000000000" + - "subnet-11111111111111111" + - "subnet-22222222222222222" ipv6_support: true purge_subnets: true dns_support: true appliance_mode_support: true tags: - TestTag: changed data in Test Tag + TestTag: "changed data in Test Tag" -# Set sub options on a Transit Gateway attachment -- community.aws.ec2_transit_gateway_vpc_attachment: - state: present - id: 'tgw-attach-0c0c5fd0b0f01d1c9' - name: AnsibleTest-1 +- name: Set sub options on a Transit Gateway attachment + community.aws.ec2_transit_gateway_vpc_attachment: + state: "present" + id: "tgw-attach-0c0c5fd0b0f01d1c9" + name: "AnsibleTest-1" ipv6_support: true purge_subnets: false dns_support: false appliance_mode_support: true -# Delete the transit gateway -- community.aws.ec2_transit_gateway_vpc_attachment: - state: absent - id: 'tgw-attach-0c0c5fd0b0f01d1c9' +- name: Delete the transit gateway + community.aws.ec2_transit_gateway_vpc_attachment: + state: "absent" + id: "tgw-attach-0c0c5fd0b0f01d1c9" """ RETURN = r""" -transit_gateway_attachments: +attachments: description: The attributes of the Transit Gateway attachments. type: list elements: dict @@ -147,7 +149,7 @@ - An ISO 8601 date time stamp of when the attachment was created. type: str returned: success - example: '2022-03-10T16:40:26+00:00' + sample: "2022-03-10T16:40:26+00:00" options: description: - Additional VPC attachment options. @@ -159,32 +161,38 @@ - Indicates whether appliance mode support is enabled. type: str returned: success - example: 'enable' + sample: "enable" dns_support: description: - Indicates whether DNS support is enabled. type: str returned: success - example: 'disable' + sample: "disable" ipv6_support: description: - Indicates whether IPv6 support is disabled. type: str returned: success - example: 'disable' + sample: "disable" + security_group_referencing_support: + description: + - Indicated weather security group referencing support is disabled. + type: str + returned: success + sample: "enable" state: description: - The state of the attachment. type: str returned: success - example: 'deleting' + sample: "deleting" subnet_ids: description: - The IDs of the subnets in use by the attachment. type: list elements: str returned: success - example: ['subnet-0123456789abcdef0', 'subnet-11111111111111111'] + sample: ["subnet-0123456789abcdef0", "subnet-11111111111111111"] tags: description: - A dictionary representing the resource tags. @@ -195,29 +203,92 @@ - The ID of the attachment. type: str returned: success - example: 'tgw-attach-0c0c5fd0b0f01d1c9' + sample: "tgw-attach-0c0c5fd0b0f01d1c9" transit_gateway_id: description: - The ID of the transit gateway that the attachment is connected to. type: str returned: success - example: 'tgw-0123456789abcdef0' + sample: "tgw-0123456789abcdef0" vpc_id: description: - The ID of the VPC that the attachment is connected to. type: str returned: success - example: 'vpc-0123456789abcdef0' + sample: "vpc-0123456789abcdef0" vpc_owner_id: description: - The ID of the account that the VPC belongs to. type: str returned: success - example: '123456789012' + sample: "1234567890122" """ +from typing import NoReturn + +from ansible_collections.amazon.aws.plugins.module_utils.transformation import boto3_resource_to_ansible_dict + from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule from ansible_collections.community.aws.plugins.module_utils.transitgateway import TransitGatewayVpcAttachmentManager +from ansible_collections.community.aws.plugins.module_utils.transitgateway import find_existing_attachment +from ansible_collections.community.aws.plugins.module_utils.transitgateway import get_states +from ansible_collections.community.aws.plugins.module_utils.transitgateway import subnets_to_vpc + + +def handle_vpc_attachments(client, module: AnsibleAWSModule) -> NoReturn: + """ + Handle the creation, modification, or deletion of VPC attachments + based on the parameters provided in the Ansible module. + + Args: + client: The AWS client to interact with EC2 services. + module: An instance of AnsibleAWSModule. + + Returns: + NoReturn: The function exits by calling module.exit_json() + with the results of the operation. + """ + attach_id = module.params.get("id", None) + attachment = None + + if not attach_id: + filters = {} + if module.params.get("transit_gateway"): + filters["transit-gateway-id"] = module.params["transit_gateway"] + if module.params.get("name"): + filters["tag:Name"] = module.params["name"] + if module.params.get("subnets"): + vpc_id = subnets_to_vpc(client, module, module.params["subnets"]) + filters["vpc-id"] = vpc_id + + # Attachments lurk in a 'deleted' state, for a while, ignore them so we + # can reuse the names + filters["state"] = get_states() + + attachment = find_existing_attachment(client, module, filters=filters) + if attachment: + attach_id = attachment["TransitGatewayAttachmentId"] + else: + attachment = find_existing_attachment(client, module, attachment_id=attach_id) + + manager = TransitGatewayVpcAttachmentManager(client, module, attachment, attachment_id=attach_id) + + if module.params["state"] == "absent": + manager.delete_attachment() + else: + manager.create_or_modify_attachment() + + results = dict( + changed=manager.changed, + attachments=[manager.updated], + ) + if manager.changed: + results["diff"] = dict( + before=boto3_resource_to_ansible_dict(manager.existing), + after=manager.updated, + ) + + module.exit_json(**results) def main(): @@ -234,7 +305,7 @@ def main(): dns_support=dict(type="bool", required=False), ipv6_support=dict(type="bool", required=False), wait=dict(type="bool", required=False, default=True), - wait_timeout=dict(type="int", required=False), + wait_timeout=dict(type="int", default=600, required=False), ) one_of = [ @@ -247,97 +318,9 @@ def main(): required_one_of=one_of, ) - attach_id = module.params.get("id", None) - tgw = module.params.get("transit_gateway", None) - name = module.params.get("name", None) - tags = module.params.get("tags", None) - purge_tags = module.params.get("purge_tags") - state = module.params.get("state") - subnets = module.params.get("subnets", None) - purge_subnets = module.params.get("purge_subnets") - - # When not provided with an ID see if one exists. - if not attach_id: - search_manager = TransitGatewayVpcAttachmentManager(module=module) - filters = dict() - if tgw: - filters["transit-gateway-id"] = tgw - if name: - filters["tag:Name"] = name - if subnets: - vpc_id = search_manager.subnets_to_vpc(subnets) - filters["vpc-id"] = vpc_id - - # Attachments lurk in a 'deleted' state, for a while, ignore them so we - # can reuse the names - filters["state"] = [ - "available", - "deleting", - "failed", - "failing", - "initiatingRequest", - "modifying", - "pendingAcceptance", - "pending", - "rollingBack", - "rejected", - "rejecting", - ] - attachments = search_manager.list(filters=filters) - if len(attachments) > 1: - module.fail_json("Multiple matching attachments found, provide an ID", attachments=attachments) - # If we find a match then we'll modify it by ID, otherwise we'll be - # creating a new RTB. - if attachments: - attach_id = attachments[0]["transit_gateway_attachment_id"] - - manager = TransitGatewayVpcAttachmentManager(module=module, id=attach_id) - manager.set_wait(module.params.get("wait", None)) - manager.set_wait_timeout(module.params.get("wait_timeout", None)) + client = module.client("ec2") - if state == "absent": - manager.delete() - else: - if not attach_id: - if not tgw: - module.fail_json( - "No existing attachment found. To create a new attachment" - " the `transit_gateway` parameter must be provided." - ) - if not subnets: - module.fail_json( - "No existing attachment found. To create a new attachment" - " the `subnets` parameter must be provided." - ) - - # name is just a special case of tags. - if name: - new_tags = dict(Name=name) - if tags is None: - purge_tags = False - else: - new_tags.update(tags) - tags = new_tags - - manager.set_transit_gateway(tgw) - manager.set_subnets(subnets, purge_subnets) - manager.set_tags(tags, purge_tags) - manager.set_dns_support(module.params.get("dns_support", None)) - manager.set_ipv6_support(module.params.get("ipv6_support", None)) - manager.set_appliance_mode_support(module.params.get("appliance_mode_support", None)) - manager.flush_changes() - - results = dict( - changed=manager.changed, - attachments=[manager.updated_resource], - ) - if manager.changed: - results["diff"] = dict( - before=manager.original_resource, - after=manager.updated_resource, - ) - - module.exit_json(**results) + handle_vpc_attachments(client, module) if __name__ == "__main__": diff --git a/plugins/modules/ec2_transit_gateway_vpc_attachment_info.py b/plugins/modules/ec2_transit_gateway_vpc_attachment_info.py index a665e4080cc..2ec87583a94 100644 --- a/plugins/modules/ec2_transit_gateway_vpc_attachment_info.py +++ b/plugins/modules/ec2_transit_gateway_vpc_attachment_info.py @@ -14,31 +14,32 @@ id: description: - The ID of the Transit Gateway Attachment. - - Mutually exclusive with I(name) and I(filters) + - Mutually exclusive with O(name) and O(filters). type: str required: false - aliases: ['attachment_id'] + aliases: ["attachment_id"] name: description: - - The C(Name) tag of the Transit Gateway attachment. + - The V(Name) tag of the Transit Gateway attachment. type: str required: false filters: description: - A dictionary of filters to apply. Each dict item consists of a filter key and a filter value. - - Setting a C(tag:Name) filter will override the I(name) parameter. + - Setting a V(tag:Name) filter will override the O(name) parameter. type: dict required: false include_deleted: description: - - If I(include_deleted=True), then attachments in a deleted state will + - If O(include_deleted=True), then attachments in a deleted state will also be returned. - - Setting a C(state) filter will override the I(include_deleted) parameter. + - Setting a V(state) filter will override the O(include_deleted) parameter. type: bool required: false default: false author: - - "Mark Chappell (@tremble)" + - Mark Chappell (@tremble) + - Alina Buzachis (@alinabuzachis) extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules @@ -46,23 +47,21 @@ """ EXAMPLES = r""" -# Describe a specific Transit Gateway attachment. -- community.aws.ec2_transit_gateway_vpc_attachment_info: - id: 'tgw-attach-0123456789abcdef0' +- name: Describe a specific Transit Gateway attachment + community.aws.ec2_transit_gateway_vpc_attachment_info: + id: "tgw-attach-0123456789abcdef0" -# Describe all attachments attached to a transit gateway. -- community.aws.ec2_transit_gateway_vpc_attachment_info: +- name: Describe all attachments attached to a transit gateway + community.aws.ec2_transit_gateway_vpc_attachment_info: filters: - transit-gateway-id: tgw-0fedcba9876543210' + transit-gateway-id: "tgw-0fedcba9876543210" -# Describe all attachments in an account. -- community.aws.ec2_transit_gateway_vpc_attachment_info: - filters: - transit-gateway-id: tgw-0fedcba9876543210' +- name: Describe all attachments in an account + community.aws.ec2_transit_gateway_vpc_attachment_info: """ RETURN = r""" -transit_gateway_attachments: +attachments: description: The attributes of the Transit Gateway attachments. type: list elements: dict @@ -73,7 +72,7 @@ - An ISO 8601 date time stamp of when the attachment was created. type: str returned: success - example: '2022-03-10T16:40:26+00:00' + sample: "2022-03-10T16:40:26+00:00" options: description: - Additional VPC attachment options. @@ -85,32 +84,38 @@ - Indicates whether appliance mode support is enabled. type: str returned: success - example: 'enable' + sample: "enable" dns_support: description: - Indicates whether DNS support is enabled. type: str returned: success - example: 'disable' + sample: "disable" ipv6_support: description: - Indicates whether IPv6 support is disabled. type: str returned: success - example: 'disable' + sample: "disable" + security_group_referencing_support: + description: + - Indicated weather security group referencing support is disabled. + type: str + returned: success + sample: "enable" state: description: - The state of the attachment. type: str returned: success - example: 'deleting' + sample: "deleting" subnet_ids: description: - The IDs of the subnets in use by the attachment. type: list elements: str returned: success - example: ['subnet-0123456789abcdef0', 'subnet-11111111111111111'] + sample: ["subnet-0123456789abcdef0", "subnet-11111111111111111"] tags: description: - A dictionary representing the resource tags. @@ -121,29 +126,38 @@ - The ID of the attachment. type: str returned: success - example: 'tgw-attach-0c0c5fd0b0f01d1c9' + sample: "tgw-attach-0c0c5fd0b0f01d1c9" transit_gateway_id: description: - The ID of the transit gateway that the attachment is connected to. type: str returned: success - example: 'tgw-0123456789abcdef0' + sample: "tgw-0123456789abcdef0" vpc_id: description: - The ID of the VPC that the attachment is connected to. type: str returned: success - example: 'vpc-0123456789abcdef0' + sample: "vpc-0123456789abcdef0" vpc_owner_id: description: - The ID of the account that the VPC belongs to. type: str returned: success - example: '123456789012' + sample: "123456789012" """ +from typing import Any +from typing import Dict +from typing import List + +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_transit_gateway_vpc_attachments +from ansible_collections.amazon.aws.plugins.module_utils.transformation import ansible_dict_to_boto3_filter_list +from ansible_collections.amazon.aws.plugins.module_utils.transformation import boto3_resource_to_ansible_dict + from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule -from ansible_collections.community.aws.plugins.module_utils.transitgateway import TransitGatewayVpcAttachmentManager +from ansible_collections.community.aws.plugins.module_utils.transitgateway import get_states def main(): @@ -162,39 +176,45 @@ def main(): module = AnsibleAWSModule( argument_spec=argument_spec, supports_check_mode=True, + mutually_exclusive=mutually_exclusive, ) - name = module.params.get("name", None) - id = module.params.get("id", None) - opt_filters = module.params.get("filters", None) + name = module.params.get("name") + attachment_id = module.params.get("id") + opt_filters = module.params.get("filters") + include_deleted = module.params.get("include_deleted") + + client = module.client("ec2") + + params: Dict[str, Any] = {} + filters: Dict[str, Any] = {} + attachments: List[Dict[str, Any]] = [] - search_manager = TransitGatewayVpcAttachmentManager(module=module) - filters = dict() + if attachment_id: + params["TransitGatewayAttachmentIds"] = [attachment_id] + # Add filter by name if provided if name: filters["tag:Name"] = name - if not module.params.get("include_deleted"): - # Attachments lurk in a 'deleted' state, for a while, ignore them so we - # can reuse the names - filters["state"] = [ - "available", - "deleting", - "failed", - "failing", - "initiatingRequest", - "modifying", - "pendingAcceptance", - "pending", - "rollingBack", - "rejected", - "rejecting", - ] + # Include only active states if "include_deleted" is False + if not include_deleted: + filters["state"] = get_states() + # Include any additional filters provided by the user if opt_filters: filters.update(opt_filters) - attachments = search_manager.list(filters=filters, id=id) + if filters: + params["Filters"] = ansible_dict_to_boto3_filter_list(filters) + + try: + result = describe_transit_gateway_vpc_attachments(client, **params) + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) + + if result: + attachments = [boto3_resource_to_ansible_dict(attachment) for attachment in result] module.exit_json(changed=False, attachments=attachments, filters=filters) diff --git a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/cleanup.yml b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/cleanup.yml index e59723bdc30..b917be3907a 100644 --- a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/cleanup.yml +++ b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/cleanup.yml @@ -1,29 +1,29 @@ --- -- name: 'Describe all attachments on our VPC' - ec2_transit_gateway_vpc_attachment_info: +- name: Describe all attachments on our VPC + community.aws.ec2_transit_gateway_vpc_attachment_info: filters: transit-gateway-id: '{{ tgw_id }}' register: info - ignore_errors: True + ignore_errors: true -- name: 'Start deletion of all attachments' - ec2_transit_gateway_vpc_attachment: +- name: Start deletion of all attachments + community.aws.ec2_transit_gateway_vpc_attachment: state: absent id: '{{ item.transit_gateway_attachment_id }}' - wait: False + wait: false loop: '{{ info.attachments }}' - ignore_errors: True + ignore_errors: true -- name: 'Wait for deletion of all attachments' - ec2_transit_gateway_vpc_attachment: +- name: Wait for deletion of all attachments + community.aws.ec2_transit_gateway_vpc_attachment: state: absent id: '{{ item.transit_gateway_attachment_id }}' - wait: True + wait: true loop: '{{ info.attachments }}' - ignore_errors: True + ignore_errors: true -- name: 'Delete subnets' - ec2_vpc_subnet: +- name: Delete subnets + amazon.aws.ec2_vpc_subnet: state: absent cidr: '{{ item.cidr }}' vpc_id: '{{ item.vpc_id }}' @@ -42,8 +42,8 @@ vpc_id: '{{ vpc_id_a }}' ignore_errors: True -- name: 'Create VPCs to attach to TGW' - ec2_vpc_net: +- name: Delete VPCs to attach to TGW + amazon.aws.ec2_vpc_net: state: absent cidr_block: '{{ item.cidr }}' name: '{{ item.name }}' @@ -52,13 +52,19 @@ name: '{{ vpc_name_a }}' - cidr: '{{ vpc_cidr_b }}' name: '{{ vpc_name_b }}' - ignore_errors: True + ignore_errors: true + +- name: Gather info about all transit gateways + community.aws.ec2_transit_gateway_info: + transit_gateway_ids: + - '{{ tgw_id }}' + - '{{ tgw_id_2 }}' -- name: 'Create Transit Gateways' - ec2_transit_gateway: +- name: Delete Transit Gateways + community.aws.ec2_transit_gateway: state: absent transit_gateway_id: '{{ item.tgw_id }}' loop: - tgw_id: '{{ tgw_id }}' - tgw_id: '{{ tgw_id_2 }}' - ignore_errors: True + ignore_errors: true diff --git a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/complex.yml b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/complex.yml index eda3ab2ace4..2a234bb165f 100644 --- a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/complex.yml +++ b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/complex.yml @@ -8,9 +8,9 @@ # Creation - block: - - name: '(CHECK_MODE) Create an attachment - complex parameters' - check_mode: True - ec2_transit_gateway_vpc_attachment: + - name: (CHECK_MODE) Create an attachment - complex parameters + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: @@ -19,11 +19,12 @@ tags: tagA: 'example Value' Tag_B: 'second value' - appliance_mode_support: True - ipv6_support: True + appliance_mode_support: true + ipv6_support: true register: complex_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - complex_attach is changed - '"attachments" in complex_attach' @@ -52,8 +53,8 @@ vars: attachment: '{{ complex_attach.attachments[0] }}' - - name: 'Create an attachment - complex parameters' - ec2_transit_gateway_vpc_attachment: + - name: Create an attachment - complex parameters + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: @@ -62,11 +63,12 @@ tags: tagA: 'example Value' Tag_B: 'second value' - appliance_mode_support: True - ipv6_support: True + appliance_mode_support: true + ipv6_support: true register: complex_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - complex_attach is changed - '"attachments" in complex_attach' @@ -108,9 +110,9 @@ set_fact: complex_attachment_id: '{{ complex_attach.attachments[0].transit_gateway_attachment_id }}' - - name: '(CHECK_MODE) Create an attachment - complex parameters -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + - name: (CHECK_MODE) Create an attachment - complex parameters -- IDEMPOTENCY + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: @@ -119,11 +121,12 @@ tags: tagA: 'example Value' Tag_B: 'second value' - appliance_mode_support: True - ipv6_support: True + appliance_mode_support: true + ipv6_support: true register: complex_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - complex_attach is not changed - '"attachments" in complex_attach' @@ -161,8 +164,8 @@ vars: attachment: '{{ complex_attach.attachments[0] }}' - - name: 'Create an attachment - complex parameters -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + - name: Create an attachment - complex parameters -- IDEMPOTENCY + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: @@ -171,11 +174,12 @@ tags: tagA: 'example Value' Tag_B: 'second value' - appliance_mode_support: True - ipv6_support: True + appliance_mode_support: true + ipv6_support: true register: complex_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - complex_attach is not changed - '"attachments" in complex_attach' @@ -216,23 +220,24 @@ # ============================================================================= # Update - - name: '(CHECK_MODE) Update an attachment - complex parameters' - check_mode: True - ec2_transit_gateway_vpc_attachment: + - name: (CHECK_MODE) Update an attachment - complex parameters + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_b_1 }}' - purge_subnets: True + purge_subnets: true tags: tagC: '3' Tag_D: 'Hello again dear world' - purge_tags: False - dns_support: False - ipv6_support: False + purge_tags: false + dns_support: false + ipv6_support: false register: complex_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - complex_attach is changed - '"attachments" in complex_attach' @@ -273,22 +278,23 @@ vars: attachment: '{{ complex_attach.attachments[0] }}' - - name: 'Update an attachment - complex parameters' - ec2_transit_gateway_vpc_attachment: + - name: Update an attachment - complex parameters + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_b_1 }}' - purge_subnets: True + purge_subnets: true tags: tagC: '3' Tag_D: 'Hello again dear world' - purge_tags: False - dns_support: False - ipv6_support: False + purge_tags: false + dns_support: false + ipv6_support: false register: complex_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - complex_attach is changed - '"attachments" in complex_attach' @@ -329,23 +335,24 @@ vars: attachment: '{{ complex_attach.attachments[0] }}' - - name: '(CHECK_MODE) Update an attachment - complex parameters -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + - name: (CHECK_MODE) Update an attachment - complex parameters -- IDEMPOTENCY + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_b_1 }}' - purge_subnets: True + purge_subnets: true tags: tagC: '3' Tag_D: 'Hello again dear world' - purge_tags: False - dns_support: False - ipv6_support: False + purge_tags: false + dns_support: false + ipv6_support: false register: complex_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - complex_attach is not changed - '"attachments" in complex_attach' @@ -386,22 +393,23 @@ vars: attachment: '{{ complex_attach.attachments[0] }}' - - name: 'Update an attachment - complex parameters -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + - name: Update an attachment - complex parameters -- IDEMPOTENCY + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name_complex }}' transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_b_1 }}' - purge_subnets: True + purge_subnets: true tags: tagC: '3' Tag_D: 'Hello again dear world' - purge_tags: False - dns_support: False - ipv6_support: False + purge_tags: false + dns_support: false + ipv6_support: false register: complex_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - complex_attach is not changed - '"attachments" in complex_attach' diff --git a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/setup.yml b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/setup.yml index 86d5aa51b5f..f3b3e86f387 100644 --- a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/setup.yml +++ b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/setup.yml @@ -1,5 +1,5 @@ --- -- name: 'Pick 2 AZs available for use' +- name: Pick 2 AZs available for use set_fact: subnet_az_a_1: '{{ ec2_availability_zone_names[0] }}' subnet_az_a_1a: '{{ ec2_availability_zone_names[0] }}' @@ -8,23 +8,23 @@ subnet_az_b_1: '{{ ec2_availability_zone_names[0] }}' subnet_az_b_2: '{{ ec2_availability_zone_names[1] }}' -- name: 'Create Transit Gateways' - ec2_transit_gateway: +- name: Create Transit Gateways + community.aws.ec2_transit_gateway: description: '{{ item.description }}' tags: Name: '{{ item.name }}' loop: - - description: 'Transit Gateway for testing ec2_transit_gateway_attachment' + - description: 'Transit Gateway for testing community.aws.ec2_transit_gateway_attachment' name: '{{ tgw_name }}' - - description: 'Second Transit Gateway for testing ec2_transit_gateway_attachment' + - description: 'Second Transit Gateway for testing community.aws.ec2_transit_gateway_attachment' name: '{{ tgw_name_2 }}' register: create_tgws -- name: 'Create VPCs to attach to TGW' - ec2_vpc_net: +- name: Create VPCs to attach to TGW + amazon.aws.ec2_vpc_net: cidr_block: '{{ item.cidr }}' name: '{{ item.name }}' - ipv6_cidr: True + ipv6_cidr: true loop: - cidr: '{{ vpc_cidr_a }}' name: '{{ vpc_name_a }}' @@ -51,8 +51,8 @@ vpc_ipv6_a: '{{ vpc_a.ipv6_cidr_block_association_set[0].ipv6_cidr_block }}' vpc_ipv6_b: '{{ vpc_b.ipv6_cidr_block_association_set[0].ipv6_cidr_block }}' -- name: 'Create subnets' - ec2_vpc_subnet: +- name: Create subnets + amazon.aws.ec2_vpc_subnet: az: '{{ item.az }}' cidr: '{{ item.cidr }}' ipv6_cidr: '{{ item.ipv6_cidr }}' diff --git a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/simple.yml b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/simple.yml index 0085813a322..2cee6627e2c 100644 --- a/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/simple.yml +++ b/tests/integration/targets/ec2_transit_gateway_vpc_attachment/tasks/simple.yml @@ -2,15 +2,16 @@ # ============================================================================= # Creation - block: - - name: '(CHECK_MODE) Create an attachment - minimal parameters' - check_mode: True - ec2_transit_gateway_vpc_attachment: + - name: (CHECK_MODE) Create an attachment - minimal parameters + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that attachment parameters are returned in CHECK_MODE + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -25,14 +26,15 @@ vars: attachment: '{{ simple_attach.attachments[0] }}' - - name: 'Create an attachment - minimal parameters' - ec2_transit_gateway_vpc_attachment: + - name: Create an attachment - minimal parameters + community.aws.ec2_transit_gateway_vpc_attachment: transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that the create attachment is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -64,18 +66,19 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: Save Attachment ID - set_fact: + ansible.builtin.set_fact: simple_attachment_id: '{{ simple_attach.attachments[0].transit_gateway_attachment_id }}' - - name: '(CHECK_MODE) Create an attachment - minimal parameters -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + - name: (CHECK_MODE) Create an attachment - minimal parameters -- IDEMPOTENCY + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -107,13 +110,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Create an attachment - minimal parameters -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: transit_gateway: '{{ tgw_id }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -146,15 +150,16 @@ # ===== - - name: '(CHECK_MODE) By Id - minimal parameters -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + - name: (CHECK_MODE) By Id - minimal parameters -- IDEMPOTENCY + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -186,13 +191,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'By Id - minimal parameters -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -227,13 +233,14 @@ # Set a name - name: '(CHECK_MODE) Set name' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' name: '{{ attachment_name }}' register: simple_attach - - assert: + - name: Assert that the attachment parameters are returned in CHECK_MODE + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -267,12 +274,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set name' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' name: '{{ attachment_name }}' register: simple_attach - - assert: + - name: Assert that 'Set name' is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -306,13 +314,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Set name -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' name: '{{ attachment_name }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -346,12 +355,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set name -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' name: '{{ attachment_name }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -387,14 +397,15 @@ # ===== - name: '(CHECK_MODE) By Name - minimal parameters -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -428,13 +439,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'By Name - minimal parameters -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' subnets: - '{{ subnet_id_a_1 }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -471,10 +483,11 @@ # Describe - name: 'Describe all attachments' - ec2_transit_gateway_vpc_attachment_info: + community.aws.ec2_transit_gateway_vpc_attachment_info: register: info - - assert: + - name: Assert that the transit_gateway_vpc_attachment_info is returned sucessfully + ansible.builtin.assert: that: - info is not changed - '"attachments" in info' @@ -497,12 +510,13 @@ attachment: '{{ info.attachments[0] }}' - name: 'Describe attachments on a specific VPC' - ec2_transit_gateway_vpc_attachment_info: + community.aws.ec2_transit_gateway_vpc_attachment_info: filters: transit-gateway-id: '{{ tgw_id }}' register: info - - assert: + - name: Assert that the returned info is correct + ansible.builtin.assert: that: - info is not changed - '"attachments" in info' @@ -526,11 +540,12 @@ attachment: '{{ info.attachments[0] }}' - name: 'Describe attachment with a specific name' - ec2_transit_gateway_vpc_attachment_info: + community.aws.ec2_transit_gateway_vpc_attachment_info: name: '{{ attachment_name }}' register: info - - assert: + - name: Assert that the returned info is correct + ansible.builtin.assert: that: - info is not changed - '"attachments" in info' @@ -564,11 +579,12 @@ attachment: '{{ info.attachments[0] }}' - name: 'Describe attachment by ID' - ec2_transit_gateway_vpc_attachment_info: + community.aws.ec2_transit_gateway_vpc_attachment_info: id: '{{ simple_attachment_id }}' register: info - - assert: + - name: Assert that the returned info is correct + ansible.builtin.assert: that: - info is not changed - '"attachments" in info' @@ -605,8 +621,8 @@ # Tag attachment - name: '(CHECK_MODE) Set tags' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue @@ -615,7 +631,8 @@ "Tag with Space": value with space register: simple_attach - - assert: + - name: Assert that 'Set tags' is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -657,7 +674,7 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set tags' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue @@ -666,7 +683,8 @@ "Tag with Space": value with space register: simple_attach - - assert: + - name: Assert that 'Set tags' is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -708,8 +726,8 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Set tags -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue @@ -718,7 +736,8 @@ "Tag with Space": value with space register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -760,7 +779,7 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set tags -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue @@ -769,7 +788,8 @@ "Tag with Space": value with space register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -811,11 +831,12 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Describe attachment with tags set' - ec2_transit_gateway_vpc_attachment_info: + community.aws.ec2_transit_gateway_vpc_attachment_info: id: '{{ simple_attachment_id }}' register: info - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - info is not changed - '"attachments" in info' @@ -859,12 +880,13 @@ # ===== - name: '(CHECK_MODE) No change to tags with name set -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -906,11 +928,12 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'No change to tags with name set -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -954,8 +977,8 @@ # ===== - name: '(CHECK_MODE) Update tags' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: @@ -963,7 +986,8 @@ "Tag with Space": value with space 2 register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1005,7 +1029,7 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update tags' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: @@ -1013,7 +1037,8 @@ "Tag with Space": value with space 2 register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1055,8 +1080,8 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Update tags -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: @@ -1064,7 +1089,8 @@ "Tag with Space": value with space 2 register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1106,7 +1132,7 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update tags -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: @@ -1114,7 +1140,8 @@ "Tag with Space": value with space 2 register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1158,15 +1185,16 @@ # ===== - name: '(CHECK_MODE) Remove tags' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue pascalCase: pascalCaseValue register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1204,14 +1232,15 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove tags' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue pascalCase: pascalCaseValue register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1249,15 +1278,16 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Remove tags -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue pascalCase: pascalCaseValue register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1295,14 +1325,15 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove tags -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: CamelCase: CamelCaseValue pascalCase: pascalCaseValue register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1342,15 +1373,16 @@ # ===== - name: '(CHECK_MODE) Add tags with no purge' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: AnotherTag: Another Value register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1390,14 +1422,15 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Add tags with no purge' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: AnotherTag: Another Value register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1437,15 +1470,16 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Add tags with no purge -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: AnotherTag: Another Value register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1485,14 +1519,15 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Add tags with no purge -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' purge_tags: False tags: AnotherTag: Another Value register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1534,13 +1569,14 @@ # ===== - name: '(CHECK_MODE) Remove all tags with name set' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: {} register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1574,12 +1610,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove all tags with name set' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: {} register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1613,13 +1650,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Remove all tags with name set -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: {} register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1653,12 +1691,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove all tags with name set -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: name: '{{ attachment_name }}' tags: {} register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1694,13 +1733,14 @@ # ===== - name: '(CHECK_MODE) Remove all tags including name' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' tags: {} register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1732,12 +1772,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove all tags including name' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' tags: {} register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1769,13 +1810,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Remove all tags including name -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' tags: {} register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1807,12 +1849,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove all tags including name -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' tags: {} register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1847,13 +1890,14 @@ # Options - name: '(CHECK_MODE) Set IPv6 support' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - ipv6_support: True + ipv6_support: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1885,12 +1929,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set IPv6 support' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - ipv6_support: True + ipv6_support: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -1922,13 +1967,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Set IPv6 support -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - ipv6_support: True + ipv6_support: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1960,12 +2006,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set IPv6 support -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - ipv6_support: True + ipv6_support: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -1999,13 +2046,14 @@ # ===== - name: '(CHECK_MODE) Set DNS support' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' dns_support: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2037,12 +2085,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set DNS support' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' dns_support: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2074,13 +2123,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Set DNS support -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' dns_support: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2112,12 +2162,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set DNS support -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' dns_support: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2151,13 +2202,14 @@ # ===== - name: '(CHECK_MODE) Set Appliance Mode support' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - appliance_mode_support: True + appliance_mode_support: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2189,12 +2241,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set Appliance Mode support' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - appliance_mode_support: True + appliance_mode_support: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2226,13 +2279,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Set Appliance Mode support -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - appliance_mode_support: True + appliance_mode_support: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2264,12 +2318,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Set Appliance Mode support -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - appliance_mode_support: True + appliance_mode_support: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2303,13 +2358,14 @@ # ===== - name: '(CHECK_MODE) Update IPv6 support' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' ipv6_support: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2341,12 +2397,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update IPv6 support' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' ipv6_support: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2378,13 +2435,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Update IPv6 support -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' ipv6_support: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2416,12 +2474,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update IPv6 support -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' ipv6_support: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2455,13 +2514,14 @@ # ===== - name: '(CHECK_MODE) Update DNS support' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - dns_support: True + dns_support: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2493,12 +2553,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update DNS support' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - dns_support: True + dns_support: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2530,13 +2591,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Update DNS support -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - dns_support: True + dns_support: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2568,12 +2630,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update DNS support -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' - dns_support: True + dns_support: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2607,13 +2670,14 @@ # ===== - name: '(CHECK_MODE) Update Appliance Mode support' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' appliance_mode_support: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2645,12 +2709,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update Appliance Mode support' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' appliance_mode_support: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2682,13 +2747,14 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Update Appliance Mode support -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' appliance_mode_support: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2720,12 +2786,13 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Update Appliance Mode support -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' appliance_mode_support: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -2760,135 +2827,144 @@ # Subnet Management - name: '(CHECK_MODE) Try to add subnet from a different VPC - no purge' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_b_2 }}' purge_subnets: False register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed - name: 'Try to add subnet from a different VPC - no purge' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_b_2 }}' purge_subnets: False register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed # ===== - name: '(CHECK_MODE) Try to add subnet from a different VPC - with purge' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_b_2 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed - name: 'Try to add subnet from a different VPC - with purge' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_b_2 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed # ===== - name: '(CHECK_MODE) Try to add subnet in the same AZ - no purge' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_b_1a }}' purge_subnets: False register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed - name: 'Try to add subnet in the same AZ - no purge' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1a }}' purge_subnets: False register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed # ===== - name: '(CHECK_MODE) Try to add subnet in the same AZ - with purge' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_1a }}' - purge_subnets: True + purge_subnets: true register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed - name: 'Try to add subnet in the same AZ - with purge' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_1a }}' - purge_subnets: True + purge_subnets: true register: simple_attach - ignore_errors: True + ignore_errors: true - - assert: + - name: Assert that the test failed + ansible.builtin.assert: that: - simple_attach is failed # ===== - name: '(CHECK_MODE) Add subnet - without purge' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' purge_subnets: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2921,14 +2997,15 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Add subnet - without purge' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' purge_subnets: False register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -2961,15 +3038,16 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Add subnet - without purge -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' purge_subnets: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3002,14 +3080,15 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Add subnet - without purge -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' purge_subnets: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3044,17 +3123,18 @@ # ===== - name: '(CHECK_MODE) Add subnet - with purge' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -3088,16 +3168,17 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Add subnet - with purge' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -3131,17 +3212,18 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Add subnet - with purge -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3175,16 +3257,17 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Add subnet - with purge -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3220,16 +3303,17 @@ # ===== - name: '(CHECK_MODE) Remove subnet' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -3262,15 +3346,16 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove subnet' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -3303,16 +3388,17 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Remove subnet -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3345,15 +3431,16 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove subnet -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_2 }}' - '{{ subnet_id_a_3 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3388,16 +3475,17 @@ # ===== - name: '(CHECK_MODE) Remove and add subnet' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -3430,15 +3518,16 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove and add subnet' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - '"attachments" in simple_attach' @@ -3471,16 +3560,17 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: '(CHECK_MODE) Remove and add subnet -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3513,15 +3603,16 @@ attachment: '{{ simple_attach.attachments[0] }}' - name: 'Remove and add subnet -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: id: '{{ simple_attachment_id }}' subnets: - '{{ subnet_id_a_1 }}' - '{{ subnet_id_a_2 }}' - purge_subnets: True + purge_subnets: true register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - '"attachments" in simple_attach' @@ -3557,55 +3648,59 @@ # Deletion - name: '(CHECK_MODE) Delete an attachment - minimal parameters' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: state: absent id: '{{ simple_attachment_id }}' - wait: False + wait: false register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - name: 'Delete an attachment - minimal parameters' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: state: absent id: '{{ simple_attachment_id }}' - wait: False + wait: false register: simple_attach - - assert: + - name: Assert that the test is successful + ansible.builtin.assert: that: - simple_attach is changed - name: '(CHECK_MODE) Delete an attachment - minimal parameters -- IDEMPOTENCY' - check_mode: True - ec2_transit_gateway_vpc_attachment: + check_mode: true + community.aws.ec2_transit_gateway_vpc_attachment: state: absent id: '{{ simple_attachment_id }}' wait: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed - name: 'Delete an attachment - minimal parameters -- IDEMPOTENCY' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: state: absent id: '{{ simple_attachment_id }}' wait: False register: simple_attach - - assert: + - name: Assert that there is no change + ansible.builtin.assert: that: - simple_attach is not changed always: - name: 'Delete attachment' - ec2_transit_gateway_vpc_attachment: + community.aws.ec2_transit_gateway_vpc_attachment: state: absent id: '{{ simple_attachment_id }}' wait: False - ignore_errors: True + ignore_errors: true From 40d61f0e959ce4684a067746e51bcaa1804fb280 Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:13:37 +0200 Subject: [PATCH 9/9] Prepare module ec2_launch_template for promotion (#2164) SUMMARY Depends-On: ansible-collections/amazon.aws#2319 Add some type hint for the module Use shared code from amazon.aws.plugins.module_utils.ec2 Add the possibility to delete specific version of a launch template Add support for tagging for launch template resource (Closes #176) Add the possibility to tag specific resources, not always instance and volume (Closes [#48](#48, Closes #2083) Support EBS Throughput (Closes #1944) Fix issue occurring when launch template contains more than 200 versions (Closes #2131) ISSUE TYPE Feature Pull Request COMPONENT NAME ec2_launch_template Reviewed-by: Alina Buzachis Reviewed-by: Bikouo Aubin Reviewed-by: GomathiselviS --- .../20240110-ec2_launch_template-refactor.yml | 9 + plugins/modules/ec2_launch_template.py | 1404 +++++++++++++---- .../targets/ec2_launch_template/aliases | 2 + .../ec2_launch_template/tasks/cpu_options.yml | 38 - .../ec2_launch_template/tasks/deletion.yml | 404 +++++ .../tasks/iam_instance_role.yml | 141 +- .../tasks/instance-metadata.yml | 30 - .../ec2_launch_template/tasks/main.yml | 6 +- .../tasks/network_interfaces.yml | 53 - .../ec2_launch_template/tasks/tagging.yml | 210 +++ .../tasks/tags_and_vpc_settings.yml | 208 --- .../tasks/template_data.yml | 145 ++ .../ec2_launch_template/tasks/versions.yml | 485 +++++- 13 files changed, 2397 insertions(+), 738 deletions(-) create mode 100644 changelogs/fragments/20240110-ec2_launch_template-refactor.yml delete mode 100644 tests/integration/targets/ec2_launch_template/tasks/cpu_options.yml create mode 100644 tests/integration/targets/ec2_launch_template/tasks/deletion.yml delete mode 100644 tests/integration/targets/ec2_launch_template/tasks/instance-metadata.yml delete mode 100644 tests/integration/targets/ec2_launch_template/tasks/network_interfaces.yml create mode 100644 tests/integration/targets/ec2_launch_template/tasks/tagging.yml delete mode 100644 tests/integration/targets/ec2_launch_template/tasks/tags_and_vpc_settings.yml create mode 100644 tests/integration/targets/ec2_launch_template/tasks/template_data.yml diff --git a/changelogs/fragments/20240110-ec2_launch_template-refactor.yml b/changelogs/fragments/20240110-ec2_launch_template-refactor.yml new file mode 100644 index 00000000000..d2e7293fa9a --- /dev/null +++ b/changelogs/fragments/20240110-ec2_launch_template-refactor.yml @@ -0,0 +1,9 @@ +--- +breaking_changes: + - ec2_launch_template - Tags defined using option ``tags`` are now applied to the launch template resources not the resource created using this launch template (https://github.com/ansible-collections/community.aws/issues/176). +minor_changes: + - ec2_launch_template - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` and update ``RETURN`` block (https://github.com/ansible-collections/community.aws/pull/2164). + - ec2_launch_template - Add the possibility to delete specific versions of a launch template using ``versions_to_delete`` (https://github.com/ansible-collections/community.aws/pull/2164). + - ec2_launch_template - Add suboption ``throughput`` to ``block_device_mappings`` argument (https://github.com/ansible-collections/community.aws/issues/1944). + - ec2_launch_template - Add option ``tag_specifications`` to define tags to be applied to the resources created with the launch template (https://github.com/ansible-collections/community.aws/issues/176). + - ec2_launch_template - Add support ``purge_tags`` parameter (https://github.com/ansible-collections/community.aws/issues/176). diff --git a/plugins/modules/ec2_launch_template.py b/plugins/modules/ec2_launch_template.py index 9fd32711f91..aa055535a9a 100644 --- a/plugins/modules/ec2_launch_template.py +++ b/plugins/modules/ec2_launch_template.py @@ -21,6 +21,7 @@ template_id: description: - The ID for the launch template, can be used for all cases except creating a new Launch Template. + - At least one of O(template_id) and O(template_name) must be specified. aliases: [id] type: str template_name: @@ -31,6 +32,7 @@ nothing happens. - If a launch template with the specified name already exists and the configuration has changed, a new version of the launch template is created. + - At least one of O(template_id) and O(template_name) must be specified. aliases: [name] type: str default_version: @@ -44,6 +46,15 @@ - The description of a launch template version. default: "" type: str + versions_to_delete: + description: + - The version numbers of a launch template versions to delete. + - Use O(default_version) to specify a new default version when deleting the current default version. + - By default, the latest version will be made the default. + - Ignored when O(state=present). + type: list + elements: int + version_added: 9.0.0 state: description: - Whether the launch template should exist or not. @@ -63,7 +74,7 @@ elements: dict suboptions: device_name: - description: The device name (for example, /dev/sdh or xvdh). + description: The device name (for example, V(/dev/sdh) or V(xvdh)). type: str no_device: description: Suppresses the specified device included in the block device mapping of the AMI. @@ -119,6 +130,12 @@ volume_type: description: The volume type type: str + throughput: + description: > + The throughput to provision for a gp3 volume, with a maximum of 1,000 MiB/s. + Valid Range - Minimum value of V(125). Maximum value of V(1000). + type: int + version_added: 9.0.0 cpu_options: description: - Choose CPU settings for the EC2 instances that will be created with this template. @@ -131,24 +148,21 @@ threads_per_core: description: > The number of threads per CPU core. To disable Intel Hyper-Threading - Technology for the instance, specify a value of 1. Otherwise, specify - the default value of 2. + Technology for the instance, specify a value of V(1). Otherwise, specify + the default value of V(2). type: int credit_specification: description: The credit option for CPU usage of the instance. Valid for T2 or T3 instances only. type: dict suboptions: cpu_credits: - description: > - The credit option for CPU usage of a T2 or T3 instance. Valid values - are C(standard) and C(unlimited). + description: + - The credit option for CPU usage of a T2 or T3 instance. Valid values are C(standard) and C(unlimited). type: str disable_api_termination: - description: > - This helps protect instances from accidental termination. If set to true, - you can't terminate the instance using the Amazon EC2 console, CLI, or - API. To change this attribute to false after launch, use - I(ModifyInstanceAttribute). + description: + - This helps protect instances from accidental termination. + - If set to V(true), you can't terminate the instance using the Amazon EC2 console, CLI, or API. type: bool ebs_optimized: description: > @@ -187,20 +201,19 @@ type: dict suboptions: market_type: - description: The market type. This should always be 'spot'. + description: The market type. This should always be V(spot). type: str spot_options: description: Spot-market specific settings. type: dict suboptions: block_duration_minutes: - description: > - The required duration for the Spot Instances (also known as Spot - blocks), in minutes. This value must be a multiple of 60 (60, - 120, 180, 240, 300, or 360). + description: + - The required duration for the Spot Instances (also known as Spot blocks), in minutes. + - This value must be a multiple of V(60) (V(60), V(120), V(180), V(240), V(300), or V(360)). type: int instance_interruption_behavior: - description: The behavior when a Spot Instance is interrupted. The default is C(terminate). + description: The behavior when a Spot Instance is interrupted. The default is V(terminate). choices: [hibernate, stop, terminate] type: str max_price: @@ -211,22 +224,21 @@ choices: [one-time, persistent] type: str instance_type: - description: > - The instance type, such as C(c5.2xlarge). For a full list of instance types, see - U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html). + description: + - The instance type, such as V(c5.2xlarge). For a full list of instance types, see + U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html). type: str kernel_id: - description: > - The ID of the kernel. We recommend that you use PV-GRUB instead of - kernels and RAM disks. For more information, see - U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedkernels.html) + description: + - The ID of the kernel. + - We recommend that you use PV-GRUB instead of kernels and RAM disks. For more information, see + U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/UserProvidedkernels.html) type: str key_name: description: - The name of the key pair. You can create a key pair using M(amazon.aws.ec2_key). - If you do not specify a key pair, you can't connect to the instance - unless you choose an AMI that is configured to allow users another way to - log in. + unless you choose an AMI that is configured to allow users another way to log in. type: str monitoring: description: Settings for instance monitoring. @@ -257,16 +269,15 @@ type: list elements: str ipv6_address_count: - description: > - The number of IPv6 addresses to assign to a network interface. Amazon - EC2 automatically selects the IPv6 addresses from the subnet range. - You can't use this option if specifying the I(ipv6_addresses) option. + description: + - The number of IPv6 addresses to assign to a network interface. + - Amazon EC2 automatically selects the IPv6 addresses from the subnet range. + - You can't use this option if specifying the O(network_interfaces.ipv6_addresses) option. type: int ipv6_addresses: - description: > - A list of one or more specific IPv6 addresses from the IPv6 CIDR - block range of your subnet. You can't use this option if you're - specifying the I(ipv6_address_count) option. + description: + - A list of one or more specific IPv6 addresses from the IPv6 CIDR block range of your subnet. + - You can't use this option if you're specifying the O(network_interfaces.ipv6_address_count) option. type: list elements: str network_interface_id: @@ -312,24 +323,41 @@ security_groups: description: > A list of security group names (Default VPC or EC2-Classic) that the new instances will be added to. - For any VPC other than Default, you must use I(security_group_ids). + For any VPC other than Default, you must use O(security_group_ids). type: list elements: str source_version: - description: > - The version number of the launch template version on which to base the new version. - The new version inherits the same launch parameters as the source version, except for parameters that you explicity specify. - Snapshots applied to the block device mapping are ignored when creating a new version unless they are explicitly included. + description: + - The version number of the launch template version on which to base the new version. + - The new version inherits the same launch parameters as the source version, except for parameters that you explicity specify. + - Snapshots applied to the O(block_device_mappings) are ignored when creating a new version unless they are explicitly included. type: str default: latest version_added: 4.1.0 - tags: - type: dict + tag_specifications: description: - - A set of key-value pairs to be applied to resources when this Launch Template is used. - - "Tag key constraints: Tag keys are case-sensitive and accept a maximum of 127 Unicode characters. May not begin with I(aws:)" - - "Tag value constraints: Tag values are case-sensitive and accept a maximum of 255 Unicode characters." - aliases: ['resource_tags'] + - The tags to apply to the resources when this Launch template is used. + type: list + elements: dict + version_added: 9.0.0 + suboptions: + resource_type: + description: + - The type of resource to tag. + - If the instance does not include the resource type that you specify, the instance launch fails. + type: str + default: instance + choices: + - instance + - volume + - network-interface + - spot-instances-request + tags: + description: + - A set of key-value pairs to be applied to the resource type. + - "Tag key constraints: Tag keys are case-sensitive and accept a maximum of 127 Unicode characters. May not begin with I(aws:)" + - "Tag value constraints: Tag values are case-sensitive and accept a maximum of 255 Unicode characters." + type: dict user_data: description: > The Base64-encoded user data to make available to the instance. For more information, see the Linux @@ -347,40 +375,43 @@ suboptions: http_endpoint: type: str - description: > - This parameter enables or disables the HTTP metadata endpoint on your instances. + description: This parameter enables or disables the HTTP metadata endpoint on your instances. choices: [enabled, disabled] default: 'enabled' http_put_response_hop_limit: type: int - description: > - The desired HTTP PUT response hop limit for instance metadata requests. - The larger the number, the further instance metadata requests can travel. + description: + - The desired HTTP PUT response hop limit for instance metadata requests. + - The larger the number, the further instance metadata requests can travel. default: 1 http_tokens: type: str - description: > - The state of token usage for your instance metadata requests. + description: The state of token usage for your instance metadata requests. choices: [optional, required] default: 'optional' http_protocol_ipv6: version_added: 3.1.0 type: str - description: > - - Wether the instance metadata endpoint is available via IPv6 (C(enabled)) or not (C(disabled)). + description: + - Whether the instance metadata endpoint is available via IPv6. choices: [enabled, disabled] default: 'disabled' instance_metadata_tags: version_added: 3.1.0 type: str description: - - Wether the instance tags are availble (C(enabled)) via metadata endpoint or not (C(disabled)). + - Whether the instance tags are availble (V(enabled)) via metadata endpoint or not (V(disabled)). choices: [enabled, disabled] default: 'disabled' +notes: + - The O(tags) option used has been in release 9.0.0 to be applied to the launch template resource instead of launch template resource. + - Use O(tag_specifications) to define tags to be applied to resources when this Launch Template is used. + - Support for O(purge_tags) was added in release 9.0.0. extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules - amazon.aws.boto3 +- amazon.aws.tags """ EXAMPLES = r""" @@ -406,122 +437,751 @@ name: "my_template" state: absent -# This module does not yet allow deletion of specific versions of launch templates +- name: Delete a specific version of an ec2 launch template + community.aws.ec2_launch_template: + name: "my_template" + versions_to_delete: + - 2 + state: absent + +- name: Delete a specific version of an ec2 launch template and change the default version + community.aws.ec2_launch_template: + name: "my_template" + versions_to_delete: + - 1 + default_version: 2 + state: absent + +- name: Create an ec2 launch template with specific tags + community.aws.ec2_launch_template: + name: "my_template" + image_id: "ami-04b762b4289fba92b" + instance_type: t2.micro + disable_api_termination: true + tags: + Some: tag + Another: tag + +- name: Create an ec2 launch template with different tag for volume and instance + community.aws.ec2_launch_template: + name: "my_template" + image_id: "ami-04b762b4289fba92b" + instance_type: t2.micro + block_device_mappings: + - device_name: /dev/sdb + ebs: + volume_size: 20 + delete_on_termination: true + volume_type: standard + tag_specifications: + - resource_type: instance + tags: + OsType: Linux + - resource_type: volume + tags: + foo: bar """ RETURN = r""" latest_version: - description: Latest available version of the launch template - returned: when state=present + description: The latest available version number of the launch template. + returned: when RV(latest_template) has a version number. type: int default_version: description: The version that will be used if only the template name is specified. Often this is the same as the latest version, but not always. - returned: when state=present + returned: when RV(default_template) has a version number. type: int +template: + description: Latest available version of the launch template. + returned: when O(state=present) + type: complex + contains: + launch_template_id: + description: The ID of the launch template. + type: str + returned: always + launch_template_name: + description: The name of the launch template. + type: str + returned: always + create_time: + description: The time launch template was created. + type: str + returned: always + created_by: + description: The principal that created the launch template. + type: str + returned: always + default_version_number: + description: The version number of the default version of the launch template. + type: int + returned: always + latest_version_number: + description: The version number of the latest version of the launch template. + type: int + returned: always + tags: + description: A dictionary of tags assigned to image. + returned: when AMI is created or already exists + type: dict + sample: { + "Env": "devel", + "Name": "nat-server" + } +versions: + description: All available versions of the launch template. + returned: when O(state=present) + type: list + elements: dict + contains: + launch_template_id: + description: The ID of the launch template. + type: str + returned: always + launch_template_name: + description: The name of the launch template. + type: str + returned: always + create_time: + description: The time the version was created. + type: str + returned: always + created_by: + description: The principal that created the version. + type: str + returned: always + default_version: + description: Indicates whether the version is the default version. + type: bool + returned: always + version_number: + description: The version number. + type: int + returned: always + version_description: + description: The description for the version. + type: str + returned: always + launch_template_data: + description: Information about the launch template. + returned: always + type: dict + contains: + kernel_id: + description: + - The ID of the kernel. + returned: if applicable + type: str + image_id: + description: The ID of the AMI or a Systems Manager parameter. + type: str + returned: if applicable + instance_type: + description: The instance type. + type: str + returned: if applicable + key_name: + description: The name of the key pair. + type: str + returned: if applicable + monitoring: + description: The monitoring for the instance. + type: dict + returned: if applicable + contains: + enabled: + description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled. + type: bool + returned: always + placement: + description: The placement of the instance. + type: dict + returned: if applicable + contains: + availability_zone: + description: The Availability Zone of the instance. + type: str + returned: if applicable + affinity: + description: The affinity setting for the instance on the Dedicated Host. + type: str + returned: if applicable + group_name: + description: The name of the placement group for the instance. + type: str + returned: if applicable + host_id: + description: The ID of the Dedicated Host for the instance. + type: str + returned: if applicable + tenancy: + description: The tenancy of the instance. + type: str + returned: if applicable + host_resource_group_arn: + description: The ARN of the host resource group in which to launch the instances. + type: str + returned: if applicable + partition_number: + description: The number of the partition the instance should launch in. + type: int + returned: if applicable + group_id: + description: The Group ID of the placement group. + type: str + returned: if applicable + ebs_optimized: + description: + - Indicates whether the instance is optimized for Amazon EBS I/O. + type: bool + returned: always + iam_instance_profile: + description: + - The IAM instance profile. + type: dict + returned: if application + contains: + arn: + description: The Amazon Resource Name (ARN) of the instance profile. + type: str + returned: always + name: + description: The name of the instance profile. + type: str + returned: always + block_device_mappings: + description: The block device mappings. + type: list + elements: dict + returned: if applicable + contains: + device_name: + description: The device name. + type: str + returned: always + virtual_name: + description: The virtual device name. + type: str + returned: always + ebs: + description: Information about the block device for an EBS volume. + type: str + returned: if applicable + contains: + encrypted: + description: Indicates whether the EBS volume is encrypted. + type: bool + returned: always + delete_on_termination: + description: Indicates whether the EBS volume is deleted on instance termination. + type: bool + returned: always + iops: + description: The number of I/O operations per second (IOPS) that the volume supports. + type: int + returned: always + kms_key_id: + description: The ARN of the Key Management Service (KMS) CMK used for encryption. + type: int + returned: always + snapshot_id: + description: The ID of the snapshot. + type: str + returned: always + volume_size: + description: The size of the volume, in GiB. + type: int + returned: always + volume_type: + description: The volume type. + type: str + returned: always + throughput: + description: The throughput that the volume supports, in MiB/s. + type: int + returned: always + no_device: + description: To omit the device from the block device mapping, specify an empty string. + type: str + network_interfaces: + description: The network interfaces. + type: list + elements: dict + returned: if applicable + contains: + associate_carrier_ip_address: + description: Indicates whether to associate a Carrier IP address with eth0 for a new network interface. + type: bool + returned: always + associate_public_ip_address: + description: Indicates whether to associate a public IPv4 address with eth0 for a new network interface. + type: bool + returned: always + delete_on_termination: + description: Indicates whether the network interface is deleted when the instance is terminated. + type: bool + returned: always + description: + description: A description for the network interface. + type: str + returned: always + device_index: + description: The device index for the network interface attachment. + type: int + returned: always + groups: + description: The IDs of one or more security groups. + type: list + elements: str + returned: if applicable + interface_type: + description: The type of network interface. + type: str + returned: always + ipv6_address_count: + description: The number of IPv6 addresses for the network interface. + type: int + returned: if applicable + ipv6_addresses: + description: The IPv6 addresses for the network interface. + returned: if applicable + type: list + elements: dict + contains: + ipv6_address: + description: The IPv6 address. + type: str + returned: always + is_primary_ipv6: + description: Determines if an IPv6 address associated with a network interface is the primary IPv6 address. + type: bool + returned: always + network_interface_id: + description: The ID of the network interface. + type: str + returned: always + private_ip_address: + description: The primary private IPv4 address of the network interface. + type: str + returned: if applicable + private_ip_addresses: + description: A list of private IPv4 addresses. + type: list + elements: str + returned: if applicable + contains: + primary: + description: Indicates whether the private IPv4 address is the primary private IPv4 address. + type: bool + returned: always + private_ip_address: + description: The private IPv4 address. + type: bool + returned: always + secondary_private_ip_address_count: + description: The number of secondary private IPv4 addresses for the network interface. + type: int + returned: if applicable + subnet_id: + description: The ID of the subnet for the network interface. + type: str + returned: always + network_card_index: + description: The index of the network card. + type: int + returned: if applicable + ipv4_prefixes: + description: A list of IPv4 prefixes assigned to the network interface. + type: list + elements: dict + returned: if applicable + contains: + ipv4_prefix: + description: The IPv4 delegated prefixes assigned to the network interface. + type: str + returned: always + ipv4_prefix_count: + description: The number of IPv4 prefixes that Amazon Web Services automatically assigned to the network interface. + type: int + returned: if applicable + ipv6_prefixes: + description: A list of IPv6 prefixes assigned to the network interface. + type: list + elements: dict + returned: if applicable + contains: + ipv6_prefix: + description: The IPv6 delegated prefixes assigned to the network interface. + type: str + returned: always + ipv6_prefix_count: + description: The number of IPv6 prefixes that Amazon Web Services automatically assigned to the network interface. + type: int + returned: if applicable + primary_ipv6: + description: The primary IPv6 address of the network interface. + type: str + returned: if applicable + ena_srd_specification: + description: Contains the ENA Express settings for instances launched from your launch template. + type: dict + returned: if applicable + contains: + ena_srd_enabled: + description: Indicates whether ENA Express is enabled for the network interface. + type: bool + returned: always + ena_srd_udp_specification: + description: Configures ENA Express for UDP network traffic. + type: dict + returned: always + contains: + ena_srd_udp_enabled: + description: Indicates whether UDP traffic to and from the instance uses ENA Express. + type: bool + returned: always + connection_tracking_specification: + description: + - A security group connection tracking specification that enables you to set the timeout + for connection tracking on an Elastic network interface. + type: dict + returned: if applicable + contains: + tcp_established_timeout: + description: Timeout (in seconds) for idle TCP connections in an established state. + type: int + returned: always + udp_timeout: + description: + - Timeout (in seconds) for idle UDP flows that have seen traffic only in a single direction + or a single request-response transaction. + type: int + returned: always + udp_stream_timeout: + description: + - Timeout (in seconds) for idle UDP flows classified as streams which have seen more + than one request-response transaction. + type: int + returned: always + ram_disk_id: + description: The ID of the RAM disk, if applicable. + type: str + returned: if applicable + disable_api_termination: + description: If set to true, indicates that the instance cannot be terminated using the Amazon EC2 console, command line tool, or API. + type: bool + returned: if applicable + instance_initiated_shutdown_behavior: + description: Indicates whether an instance stops or terminates when you initiate shutdown from the instance. + type: str + returned: if applicable + user_data: + description: The user data for the instance. + type: str + returned: if applicable + tag_specifications: + description: The tags that are applied to the resources that are created during instance launch. + type: list + elements: dict + returned: if applicable + contains: + resource_type: + description: The type of resource to tag. + type: str + returned: always + tags: + description: The tags for the resource. + type: list + elements: dict + contains: + key: + description: The key of the tag. + type: str + returned: always + value: + description: The value of the tag. + type: str + returned: always + enclave_options: + description: Indicates whether the instance is enabled for Amazon Web Services Nitro Enclaves. + type: dict + returned: if applicable + contains: + enabled: + description: If this parameter is set to true, the instance is enabled for Amazon Web Services Nitro Enclaves. + type: bool + returned: always + metadata_options: + description: The metadata options for the instance. + type: dict + returned: if applicable + contains: + state: + description: The state of the metadata option changes. + type: str + returned: if applicable + http_tokens: + description: Indicates whether IMDSv2 is required. + type: str + returned: if applicable + http_put_response_hop_limit: + description: The desired HTTP PUT response hop limit for instance metadata requests. + type: int + returned: if applicable + http_endpoint: + description: Enables or disables the HTTP metadata endpoint on your instances. + type: str + returned: if applicable + http_protocol_ipv6: + description: Enables or disables the IPv6 endpoint for the instance metadata service. + type: str + returned: if applicable + instance_metadata_tags: + description: Set to enabled to allow access to instance tags from the instance metadata. + type: str + returned: if applicable + cpu_options: + description: The CPU options for the instance. + type: dict + returned: if applicable + contains: + core_count: + description: The number of CPU cores for the instance. + type: int + returned: if applicable + threads_per_core: + description: The number of threads per CPU core. + type: int + returned: if applicable + amd_sev_snp: + description: Indicates whether the instance is enabled for AMD SEV-SNP. + type: int + returned: if applicable + security_group_ids: + description: The security group IDs. + type: list + elements: str + returned: if applicable + security_groups: + description: The security group names. + type: list + elements: str + returned: if applicable + sample: { + "block_device_mappings": [ + { + "device_name": "/dev/sdb", + "ebs": { + "delete_on_termination": true, + "encrypted": true, + "volumeSize": 5 + } + } + ], + "ebs_optimized": false, + "image_id": "ami-0231217be14a6f3ba", + "instance_type": "t2.micro", + "network_interfaces": [ + { + "associate_public_ip_address": false, + "device_index": 0, + "ipv6_addresses": [ + { + "ipv6_address": "2001:0:130F:0:0:9C0:876A:130B" + } + ] + } + ] + } +latest_template: + description: The latest available version of the launch template. + returned: when O(state=present) + type: complex + contains: + launch_template_id: + description: The ID of the launch template. + type: str + returned: always + launch_template_name: + description: The name of the launch template. + type: str + returned: always + create_time: + description: The time launch template was created. + type: str + returned: always + created_by: + description: The principal that created the launch template. + type: str + returned: always + default_version_number: + description: The version number of the default version of the launch template. + type: int + returned: always + latest_version_number: + description: The version number of the latest version of the launch template. + type: int + returned: always + tags: + description: A dictionary of tags assigned to image. + returned: when AMI is created or already exists + type: dict + sample: { + "Env": "devel", + "Name": "nat-server" + } +default_template: + description: + - The launch template version that will be used if only the template name is specified. + - Often this is the same as the latest version, but not always. + returned: when O(state=present) + type: complex + contains: + launch_template_id: + description: The ID of the launch template. + type: str + returned: always + launch_template_name: + description: The name of the launch template. + type: str + returned: always + create_time: + description: The time launch template was created. + type: str + returned: always + created_by: + description: The principal that created the launch template. + type: str + returned: always + default_version_number: + description: The version number of the default version of the launch template. + type: int + returned: always + latest_version_number: + description: The version number of the latest version of the launch template. + type: int + returned: always + tags: + description: A dictionary of tags assigned to image. + returned: when AMI is created or already exists + type: dict + sample: { + "Env": "devel", + "Name": "nat-server" + } +deleted_template: + description: information about a launch template deleted. + returned: when O(state=absent) + type: complex + contains: + launch_template_id: + description: The ID of the launch template. + type: str + returned: always + launch_template_name: + description: The name of the launch template. + type: str + returned: always + create_time: + description: The time launch template was created. + type: str + returned: always + created_by: + description: The principal that created the launch template. + type: str + returned: always + default_version_number: + description: The version number of the default version of the launch template. + type: int + returned: always + latest_version_number: + description: The version number of the latest version of the launch template. + type: int + returned: always + tags: + description: A dictionary of tags assigned to image. + returned: when AMI is created or already exists + type: dict + sample: { + "Env": "devel", + "Name": "nat-server" + } +deleted_versions: + description: Information about deleted launch template versions. + returned: when O(state=absent) + type: list + elements: dict + contains: + launch_template_id: + description: The ID of the launch template. + type: str + returned: always + launch_template_name: + description: The name of the launch template. + type: str + returned: always + version_number: + description: The version number of the launch template. + type: int + returned: always """ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple from uuid import uuid4 -try: - from botocore.exceptions import BotoCoreError - from botocore.exceptions import ClientError - from botocore.exceptions import WaiterError -except ImportError: - pass # caught by AnsibleAWSModule - from ansible.module_utils._text import to_text from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict -from ansible_collections.amazon.aws.plugins.module_utils.arn import validate_aws_arn -from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -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.botocore import normalize_boto3_result +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_launch_template +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_launch_template_version +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_launch_template +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_launch_template_versions +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_launch_template_versions +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_launch_templates +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import determine_iam_arn_from_name +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import modify_launch_template 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 boto3_tag_specifications from ansible_collections.amazon.aws.plugins.module_utils.transformation import scrub_none_parameters from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule -def determine_iam_role(module, name_or_arn): - if validate_aws_arn(name_or_arn, service="iam", resource_type="instance-profile"): - return {"arn": name_or_arn} - iam = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) - try: - role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True) - return {"arn": role["InstanceProfile"]["Arn"]} - except is_boto3_error_code("NoSuchEntity") as e: - module.fail_json_aws(e, msg=f"Could not find instance_role {name_or_arn}") - except (BotoCoreError, ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws( - e, - msg=f"An error occurred while searching for instance_role {name_or_arn}. Please try supplying the full ARN.", - ) - - -def existing_templates(module): - ec2 = module.client("ec2", retry_decorator=AWSRetry.jittered_backoff()) - matches = None - try: - if module.params.get("template_id"): - matches = ec2.describe_launch_templates( - LaunchTemplateIds=[module.params.get("template_id")], aws_retry=True - ) - elif module.params.get("template_name"): - matches = ec2.describe_launch_templates( - LaunchTemplateNames=[module.params.get("template_name")], aws_retry=True - ) - except is_boto3_error_code("InvalidLaunchTemplateName.NotFoundException") as e: - # no named template was found, return nothing/empty versions - return None, [] - except is_boto3_error_code("InvalidLaunchTemplateId.Malformed") as e: # pylint: disable=duplicate-except - module.fail_json_aws( - e, - msg=( - f"Launch template with ID {module.params.get('launch_template_id')} is not a valid ID. It should start" - " with `lt-....`" - ), - ) - except is_boto3_error_code("InvalidLaunchTemplateId.NotFoundException") as e: # pylint: disable=duplicate-except - module.fail_json_aws( - e, - msg=( - f"Launch template with ID {module.params.get('launch_template_id')} could not be found, please supply a" - " name instead so that a new template can be created" - ), - ) - except (ClientError, BotoCoreError, WaiterError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Could not check existing launch templates. This may be an IAM permission problem.") +def find_existing(client, module: AnsibleAWSModule) -> Tuple[Optional[Dict[str, Any]], Optional[List[Dict[str, Any]]]]: + launch_template = None + launch_template_versions = [] + params = {} + template_id = module.params.get("template_id") + template_name = module.params.get("template_name") + if template_id: + params["launch_template_ids"] = [template_id] else: - template = matches["LaunchTemplates"][0] - template_id, template_version, template_default = ( - template["LaunchTemplateId"], - template["LatestVersionNumber"], - template["DefaultVersionNumber"], + params["launch_template_names"] = [template_name] + launch_templates = describe_launch_templates(client, **params) + if launch_templates: + launch_template = launch_templates[0] + launch_template_versions = describe_launch_template_versions( + client, LaunchTemplateId=launch_template["LaunchTemplateId"] ) - try: - return ( - template, - ec2.describe_launch_template_versions(LaunchTemplateId=template_id, aws_retry=True)[ - "LaunchTemplateVersions" - ], - ) - except (ClientError, BotoCoreError, WaiterError) as e: - module.fail_json_aws( - e, - msg=f"Could not find launch template versions for {template['LaunchTemplateName']} (ID: {template_id}).", - ) + return normalize_boto3_result(launch_template), normalize_boto3_result(launch_template_versions) -def params_to_launch_data(module, template_params): - if template_params.get("tags"): - tag_list = ansible_dict_to_boto3_tag_list(template_params.get("tags")) - template_params["tag_specifications"] = [ - {"resource_type": r_type, "tags": tag_list} for r_type in ("instance", "volume") - ] - del template_params["tags"] - if module.params.get("iam_instance_profile"): - template_params["iam_instance_profile"] = determine_iam_role(module, module.params["iam_instance_profile"]) +def params_to_launch_data( + template_params: Dict[str, Any], iam_instance_profile_arn: Optional[str] = None +) -> Dict[str, Any]: + if iam_instance_profile_arn: + template_params["iam_instance_profile"] = {"arn": iam_instance_profile_arn} + for interface in template_params.get("network_interfaces") or []: + if interface.get("ipv6_addresses"): + interface["ipv6_addresses"] = [{"ipv6_address": x} for x in interface["ipv6_addresses"]] params = snake_dict_to_camel_dict( dict((k, v) for k, v in template_params.items() if v is not None), capitalize_first=True, @@ -529,170 +1189,279 @@ def params_to_launch_data(module, template_params): return params -def delete_template(module): - ec2 = module.client("ec2", retry_decorator=AWSRetry.jittered_backoff()) - template, template_versions = existing_templates(module) +def validate_string_as_int(module: AnsibleAWSModule, version: str, param_name: str) -> int: + try: + return int(version) + except ValueError: + module.fail_json(msg=f'{param_name} param was not a valid integer, got "{version}"') + + +def validate_version_deletion( + module: AnsibleAWSModule, launch_template_id: str, existing_versions: List[Dict[str, Any]] +) -> Tuple[List[str], Optional[int]]: + versions_to_delete = module.params.get("versions_to_delete") + launch_template_versions_to_delete = [] + default_version_to_set = None + if versions_to_delete: + unique_versions_to_delete = list(set(versions_to_delete)) + launch_template_versions_to_delete = [ + t["VersionNumber"] for t in existing_versions if t["VersionNumber"] in unique_versions_to_delete + ] + if len(launch_template_versions_to_delete) != len(unique_versions_to_delete): + missing = [m for m in unique_versions_to_delete if m not in launch_template_versions_to_delete] + module.fail_json( + msg=f"The following versions {missing} do not exist for launch template id '{launch_template_id}'." + ) + + remaining_versions = [ + t["VersionNumber"] + for t in existing_versions + if t["VersionNumber"] not in launch_template_versions_to_delete + ] + + # Find the default version + default_version = module.params.get("default_version") + if default_version in (None, ""): + default_version_int = [t["VersionNumber"] for t in existing_versions if t["DefaultVersion"]][0] + elif default_version == "latest": + default_version_int = max(remaining_versions, default=None) + default_version_to_set = default_version_int + else: + default_version_int = validate_string_as_int(module, default_version, "default_version") + default_version_to_set = default_version_int + + # Ensure we are not deleting the default version + if default_version_int in launch_template_versions_to_delete or not remaining_versions: + module.fail_json(msg="Cannot delete the launch template default version.") + + if default_version_to_set and default_version_to_set not in remaining_versions: + module.fail_json( + msg=f"Could not set version '{default_version_to_set}' as default, " + "the launch template version was not found for the specified launch template id '{launch_template_id}'." + ) + else: + # By default delete all non default version before the launch template deletion + launch_template_versions_to_delete = [t["VersionNumber"] for t in existing_versions if not t["DefaultVersion"]] + + return [to_text(v) for v in launch_template_versions_to_delete], default_version_to_set + + +def ensure_absent( + client, module: AnsibleAWSModule, existing: Optional[Dict[str, Any]], existing_versions: List[Dict[str, Any]] +) -> None: deleted_versions = [] - if template or template_versions: - non_default_versions = [to_text(t["VersionNumber"]) for t in template_versions if not t["DefaultVersion"]] - if non_default_versions: - try: - v_resp = ec2.delete_launch_template_versions( - LaunchTemplateId=template["LaunchTemplateId"], - Versions=non_default_versions, - aws_retry=True, + deleted_template = {} + changed = False + + if existing: + launch_template_id = existing["LaunchTemplateId"] + v_to_delete, v_default = validate_version_deletion(module, launch_template_id, existing_versions) + + # Update default version + if v_default: + changed = True + if not module.check_mode: + modify_launch_template( + client, + LaunchTemplateId=launch_template_id, + ClientToken=uuid4().hex, + DefaultVersion=to_text(v_default), ) - if v_resp["UnsuccessfullyDeletedLaunchTemplateVersions"]: + # Delete versions + if v_to_delete: + changed = True + if not module.check_mode: + response = delete_launch_template_versions( + client, launch_template_id=launch_template_id, versions=v_to_delete + ) + if response["UnsuccessfullyDeletedLaunchTemplateVersions"]: module.warn( - f"Failed to delete template versions {v_resp['UnsuccessfullyDeletedLaunchTemplateVersions']} on" - f" launch template {template['LaunchTemplateId']}" + f"Failed to delete template versions {response['UnsuccessfullyDeletedLaunchTemplateVersions']} on" + f" launch template {launch_template_id}" ) deleted_versions = [ - camel_dict_to_snake_dict(v) for v in v_resp["SuccessfullyDeletedLaunchTemplateVersions"] + camel_dict_to_snake_dict(v) for v in response["SuccessfullyDeletedLaunchTemplateVersions"] ] - except (ClientError, BotoCoreError) as e: - module.fail_json_aws( - e, - msg=f"Could not delete existing versions of the launch template {template['LaunchTemplateId']}", - ) - try: - resp = ec2.delete_launch_template( - LaunchTemplateId=template["LaunchTemplateId"], - aws_retry=True, - ) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg=f"Could not delete launch template {template['LaunchTemplateId']}") - return { - "deleted_versions": deleted_versions, - "deleted_template": camel_dict_to_snake_dict(resp["LaunchTemplate"]), - "changed": True, - } - else: - return {"changed": False} + # Delete the launch template when a list of versions was not specified + if not module.params.get("versions_to_delete"): + changed = True + if not module.check_mode: + deleted_template = delete_launch_template(client, launch_template_id=launch_template_id) + deleted_template = camel_dict_to_snake_dict(deleted_template, ignore_list=["Tags"]) + if "tags" in deleted_template: + deleted_template["tags"] = boto3_tag_list_to_ansible_dict(deleted_template.get("tags")) + + module.exit_json(changed=changed, deleted_versions=deleted_versions, deleted_template=deleted_template) + + +def add_launch_template_version( + client, + module: AnsibleAWSModule, + launch_template_id: str, + launch_template_data: Dict[str, Any], + existing_versions: List[Dict[str, Any]], + most_recent_version_number: str, +) -> int: + source_version = module.params.get("source_version") + version_description = module.params.get("version_description") + + params = { + "LaunchTemplateId": launch_template_id, + "ClientToken": uuid4().hex, + "VersionDescription": version_description, + } + + if source_version == "latest": + params.update({"SourceVersion": most_recent_version_number}) + elif source_version not in (None, ""): + # Source version passed as int + source_version_int = validate_string_as_int(module, source_version, "source_version") + # get source template version + next_source_version = next( + (v for v in existing_versions if v["VersionNumber"] == source_version_int), + None, + ) + if next_source_version is None: + module.fail_json(msg=f'source_version does not exist, got "{source_version}"') + params.update({"SourceVersion": str(next_source_version["VersionNumber"])}) + + if module.check_mode: + module.exit_json(changed=True, msg="Would have created launch template version if not in check mode.") -def create_or_update(module, template_options): - ec2 = module.client( - "ec2", retry_decorator=AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidLaunchTemplateId.NotFound"]) + # Create Launch template version + launch_template_version = create_launch_template_version( + client, launch_template_data=launch_template_data, **params ) - template, template_versions = existing_templates(module) - out = {} - lt_data = params_to_launch_data(module, dict((k, v) for k, v in module.params.items() if k in template_options)) - lt_data = scrub_none_parameters(lt_data, descend_into_lists=True) - - if not (template or template_versions): - # create a full new one - try: - resp = ec2.create_launch_template( - LaunchTemplateName=module.params["template_name"], - LaunchTemplateData=lt_data, - ClientToken=uuid4().hex, - aws_retry=True, - ) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create launch template") - template, template_versions = existing_templates(module) - out["changed"] = True - elif template and template_versions: - most_recent = sorted(template_versions, key=lambda x: x["VersionNumber"])[-1] - if lt_data == most_recent["LaunchTemplateData"] and module.params["version_description"] == most_recent.get( - "VersionDescription", "" - ): - out["changed"] = False - return out - try: - if module.params.get("source_version") in (None, ""): - resp = ec2.create_launch_template_version( - LaunchTemplateId=template["LaunchTemplateId"], - LaunchTemplateData=lt_data, - ClientToken=uuid4().hex, - VersionDescription=str(module.params["version_description"]), - aws_retry=True, - ) - elif module.params.get("source_version") == "latest": - resp = ec2.create_launch_template_version( - LaunchTemplateId=template["LaunchTemplateId"], - LaunchTemplateData=lt_data, - ClientToken=uuid4().hex, - SourceVersion=str(most_recent["VersionNumber"]), - VersionDescription=str(module.params["version_description"]), - aws_retry=True, - ) - else: - try: - int(module.params.get("source_version")) - except ValueError: - module.fail_json( - msg=f"source_version param was not a valid integer, got \"{module.params.get('source_version')}\"" - ) - # get source template version - source_version = next( - (v for v in template_versions if v["VersionNumber"] == int(module.params.get("source_version"))), - None, - ) - if source_version is None: - module.fail_json( - msg=f"source_version does not exist, got \"{module.params.get('source_version')}\"" - ) - resp = ec2.create_launch_template_version( - LaunchTemplateId=template["LaunchTemplateId"], - LaunchTemplateData=lt_data, - ClientToken=uuid4().hex, - SourceVersion=str(source_version["VersionNumber"]), - VersionDescription=str(module.params["version_description"]), - aws_retry=True, - ) + return launch_template_version["VersionNumber"] - if module.params.get("default_version") in (None, ""): - # no need to do anything, leave the existing version as default - pass - elif module.params.get("default_version") == "latest": - set_default = ec2.modify_launch_template( - LaunchTemplateId=template["LaunchTemplateId"], - DefaultVersion=to_text(resp["LaunchTemplateVersion"]["VersionNumber"]), - ClientToken=uuid4().hex, - aws_retry=True, - ) - else: - try: - int(module.params.get("default_version")) - except ValueError: - module.fail_json( - msg=f"default_version param was not a valid integer, got \"{module.params.get('default_version')}\"" - ) - set_default = ec2.modify_launch_template( - LaunchTemplateId=template["LaunchTemplateId"], - DefaultVersion=to_text(int(module.params.get("default_version"))), + +def ensure_default_version( + client, + module: AnsibleAWSModule, + launch_template_id: str, + current_default_version_number: int, + most_recent_version_number: int, +) -> bool: + # Modify default version + default_version = module.params.get("default_version") + changed = False + if default_version not in (None, ""): + if default_version == "latest": + default_version = to_text(most_recent_version_number) + else: + default_version = to_text(validate_string_as_int(module, default_version, "default_version")) + if to_text(current_default_version_number) != default_version: + changed = True + if not module.check_mode: + modify_launch_template( + client, + LaunchTemplateId=launch_template_id, ClientToken=uuid4().hex, - aws_retry=True, + DefaultVersion=default_version, ) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create subsequent launch template version") - template, template_versions = existing_templates(module) - out["changed"] = True - return out + return changed -def format_module_output(module): - output = {} - template, template_versions = existing_templates(module) - template = camel_dict_to_snake_dict(template) +def format_module_output(client, module: AnsibleAWSModule) -> Dict[str, Any]: + # Describe launch template + template, template_versions = find_existing(client, module) + template = camel_dict_to_snake_dict(template, ignore_list=["Tags"]) + if "tags" in template: + template["tags"] = boto3_tag_list_to_ansible_dict(template.get("tags")) template_versions = [camel_dict_to_snake_dict(v) for v in template_versions] - for v in template_versions: - for ts in v["launch_template_data"].get("tag_specifications") or []: - ts["tags"] = boto3_tag_list_to_ansible_dict(ts.pop("tags")) - output.update(dict(template=template, versions=template_versions)) - output["default_template"] = [v for v in template_versions if v.get("default_version")][0] - output["latest_template"] = [ - v - for v in template_versions - if (v.get("version_number") and int(v["version_number"]) == int(template["latest_version_number"])) - ][0] - if "version_number" in output["default_template"]: - output["default_version"] = output["default_template"]["version_number"] - if "version_number" in output["latest_template"]: - output["latest_version"] = output["latest_template"]["version_number"] - return output + result = { + "template": template, + "versions": template_versions, + "default_template": [v for v in template_versions if v.get("default_version")][0], + "latest_template": [ + v + for v in template_versions + if (v.get("version_number") and int(v["version_number"]) == int(template["latest_version_number"])) + ][0], + } + if "version_number" in result["default_template"]: + result["default_version"] = result["default_template"]["version_number"] + if "version_number" in result["latest_template"]: + result["latest_version"] = result["latest_template"]["version_number"] + return result + + +def ensure_present( + client, + module: AnsibleAWSModule, + template_options: Dict[str, Any], + existing: Optional[Dict[str, Any]], + existing_versions: List[Dict[str, Any]], +) -> None: + template_name = module.params["template_name"] + tags = module.params["tags"] + tag_specifications = module.params.get("tag_specifications") + version_description = module.params.get("version_description") + iam_instance_profile = module.params.get("iam_instance_profile") + # IAM instance profile + if iam_instance_profile: + iam_instance_profile = determine_iam_arn_from_name(module.client("iam"), iam_instance_profile) + # Convert Launch template data + launch_template_data = params_to_launch_data( + dict((k, v) for k, v in module.params.items() if k in template_options), iam_instance_profile + ) + # Tag specifications + if tag_specifications: + boto3_tag_specs = [] + for tag_spec in tag_specifications: + boto3_tag_specs.extend(boto3_tag_specifications(tag_spec["tags"], types=tag_spec["resource_type"])) + launch_template_data["TagSpecifications"] = boto3_tag_specs + launch_template_data = scrub_none_parameters(launch_template_data, descend_into_lists=True) + changed = False + + if not (existing or existing_versions): + # Create Launch template + if module.check_mode: + module.exit_json(changed=True, msg="Would have created launch template if not in check mode.") + create_launch_template( + client, + launch_template_name=template_name, + launch_template_data=launch_template_data, + tags=tags, + ClientToken=uuid4().hex, + VersionDescription=version_description, + ) + changed = True + else: + launch_template_id = existing["LaunchTemplateId"] + default_version_number = existing["DefaultVersionNumber"] + most_recent = sorted(existing_versions, key=lambda x: x["VersionNumber"])[-1] + most_recent_version_number = most_recent["VersionNumber"] + if not ( + launch_template_data == most_recent["LaunchTemplateData"] + and version_description == most_recent.get("VersionDescription", "") + ): + changed = True + most_recent_version_number = add_launch_template_version( + client, + module, + launch_template_id, + launch_template_data, + existing_versions, + str(most_recent["VersionNumber"]), + ) + + # Ensure default version + changed |= ensure_default_version( + client, module, launch_template_id, default_version_number, most_recent_version_number + ) + # Ensure tags + changed |= ensure_ec2_tags( + client, + module, + launch_template_id, + resource_type="launch-template", + tags=tags, + purge_tags=module.params["purge_tags"], + ) + + module.exit_json(changed=changed, **format_module_output(client, module)) def main(): @@ -712,6 +1481,7 @@ def main(): snapshot_id=dict(), volume_size=dict(type="int"), volume_type=dict(), + throughput=dict(type="int"), ), ), no_device=dict(), @@ -738,7 +1508,6 @@ def main(): type="list", elements="dict", ), - iam_instance_profile=dict(), image_id=dict(), instance_initiated_shutdown_behavior=dict(choices=["stop", "terminate"]), instance_market_options=dict( @@ -802,42 +1571,55 @@ def main(): ram_disk_id=dict(), security_group_ids=dict(type="list", elements="str"), security_groups=dict(type="list", elements="str"), - tags=dict(type="dict", aliases=["resource_tags"]), user_data=dict(), ) - arg_spec = dict( + argument_spec = dict( state=dict(choices=["present", "absent"], default="present"), template_name=dict(aliases=["name"]), template_id=dict(aliases=["id"]), default_version=dict(default="latest"), source_version=dict(default="latest"), version_description=dict(default=""), + iam_instance_profile=dict(), + tags=dict(type="dict", aliases=["resource_tags"]), + purge_tags=dict(type="bool", default=True), + versions_to_delete=dict(type="list", elements="int"), + tag_specifications=dict( + type="list", + elements="dict", + options=dict( + resource_type=dict( + type="str", + default="instance", + choices=["instance", "volume", "network-interface", "spot-instances-request"], + ), + tags=dict(type="dict"), + ), + ), ) - arg_spec.update(template_options) + argument_spec.update(template_options) module = AnsibleAWSModule( - argument_spec=arg_spec, + argument_spec=argument_spec, required_one_of=[ ("template_name", "template_id"), ], supports_check_mode=True, ) - for interface in module.params.get("network_interfaces") or []: - if interface.get("ipv6_addresses"): - interface["ipv6_addresses"] = [{"ipv6_address": x} for x in interface["ipv6_addresses"]] - - if module.params.get("state") == "present": - out = create_or_update(module, template_options) - out.update(format_module_output(module)) - elif module.params.get("state") == "absent": - out = delete_template(module) - else: - module.fail_json(msg=f"Unsupported value \"{module.params.get('state')}\" for `state` parameter") + state = module.params.get("state") + client = module.client("ec2") + launch_template, launch_template_versions = find_existing(client, module) - module.exit_json(**out) + try: + if state == "present": + ensure_present(client, module, template_options, launch_template, launch_template_versions) + else: + ensure_absent(client, module, launch_template, launch_template_versions) + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) if __name__ == "__main__": diff --git a/tests/integration/targets/ec2_launch_template/aliases b/tests/integration/targets/ec2_launch_template/aliases index 4ef4b2067d0..42f0f3c880b 100644 --- a/tests/integration/targets/ec2_launch_template/aliases +++ b/tests/integration/targets/ec2_launch_template/aliases @@ -1 +1,3 @@ cloud/aws +time=3m +ec2_launch_template_info \ No newline at end of file diff --git a/tests/integration/targets/ec2_launch_template/tasks/cpu_options.yml b/tests/integration/targets/ec2_launch_template/tasks/cpu_options.yml deleted file mode 100644 index 92d7fac5fe1..00000000000 --- a/tests/integration/targets/ec2_launch_template/tasks/cpu_options.yml +++ /dev/null @@ -1,38 +0,0 @@ -- block: - - name: delete a non-existent template - ec2_launch_template: - name: "{{ resource_prefix }}-not-a-real-template" - state: absent - register: del_fake_lt - ignore_errors: true - - assert: - that: - - del_fake_lt is not failed - - name: create c4.large instance with cpu_options - ec2_launch_template: - name: "{{ resource_prefix }}-c4large-1-threads-per-core" - image_id: "{{ ec2_ami_id }}" - tags: - TestId: "{{ resource_prefix }}" - instance_type: c4.large - cpu_options: - core_count: 1 - threads_per_core: 1 - register: lt - - - name: instance with cpu_options created with the right options - assert: - that: - - lt is success - - lt is changed - - "lt.latest_template.launch_template_data.cpu_options.core_count == 1" - - "lt.latest_template.launch_template_data.cpu_options.threads_per_core == 1" - always: - - name: delete the template - ec2_launch_template: - name: "{{ resource_prefix }}-c4large-1-threads-per-core" - state: absent - register: del_lt - retries: 10 - until: del_lt is not failed - ignore_errors: true diff --git a/tests/integration/targets/ec2_launch_template/tasks/deletion.yml b/tests/integration/targets/ec2_launch_template/tasks/deletion.yml new file mode 100644 index 00000000000..3936888dc53 --- /dev/null +++ b/tests/integration/targets/ec2_launch_template/tasks/deletion.yml @@ -0,0 +1,404 @@ +- name: Test deletion of launch template + vars: + deletion_launch_template_name: "{{ resource_prefix }}-deletion" + deletion_launch_template_name_2: "{{ resource_prefix }}-deletion-2" + test_ec2_instance_types: + - t2.micro + - t2.small + - t2.medium + - t2.large + - t2.xlarge + block: + - name: Create multiple versions of the launch template + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + instance_type: "{{ item }}" + with_items: "{{ test_ec2_instance_types }}" + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Validate Launch template details + ansible.builtin.assert: + that: + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 5 + - _templates.launch_templates[0].latest_version_number == 5 + - _templates.launch_templates[0].default_version_number == 5 + + #==================================================================== + # Validate deletion errors + #==================================================================== + - name: Delete a non-existent template + community.aws.ec2_launch_template: + name: "{{ resource_prefix }}-not-a-real-template" + state: absent + register: delete_fake_template + ignore_errors: true + + - name: Ensure module did not failed while trying to delete non-existent template + assert: + that: + - delete_fake_template is not failed + + - name: Trying to delete the default version + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: "{{ range(1, 6) }}" + ignore_errors: true + register: delete_default_v + + - name: Ensure the module failed with proper message + ansible.builtin.assert: + that: + - delete_default_v is failed + - 'delete_default_v.msg == "Cannot delete the launch template default version."' + + - name: Trying to delete a version and a non-existing version as default + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: "{{ range(3, 6) }}" + default_version: 6 + ignore_errors: true + register: delete_set_non_existing_v + + - name: Ensure the module failed with proper message + ansible.builtin.assert: + that: + - delete_set_non_existing_v is failed + - error_m in delete_set_non_existing_v.msg + vars: + error_m: "Could not set version '6' as default, the launch template version was not found" + + - name: Trying to delete non-existing version (should failed) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: 10 + ignore_errors: true + register: delete_non_existing_v + + - name: Ensure the module failed with proper message + ansible.builtin.assert: + that: + - delete_non_existing_v is failed + - error_m in delete_non_existing_v.msg + vars: + error_m: "The following versions [10] do not exist for launch template id" + + #==================================================================== + # Delete default version and set latest version as new default + #==================================================================== + - name: Delete default version and set a new default version (check mode) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: 5 + check_mode: true + register: delete_default_v_check_mode + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while no change was made + ansible.builtin.assert: + that: + - delete_default_v_check_mode is changed + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 5 + - _templates.launch_templates[0].latest_version_number == 5 + - _templates.launch_templates[0].default_version_number == 5 + + - name: Delete default version and set a new default version + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: 5 + register: delete_default_v + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Validate deletion result + ansible.builtin.assert: + that: + - delete_default_v is changed + - delete_default_v.deleted_template == {} + - delete_default_v.deleted_versions | length == 1 + - delete_default_v.deleted_versions.0.version_number == 5 + - delete_default_v.deleted_versions.0.launch_template_name == deletion_launch_template_name + - delete_default_v.deleted_versions.0.launch_template_id == _templates.launch_templates[0].launch_template_id + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 4 + - _templates.launch_templates[0].latest_version_number == 4 + - _templates.launch_templates[0].default_version_number == 4 + + #==================================================================== + # Delete a single version and set a new default version different from the latest + #==================================================================== + - name: Delete a single version and set a new default version different from the latest (check mode) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: 3 + default_version: 1 + check_mode: true + register: delete_single_v_check_mode + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while no change was made + ansible.builtin.assert: + that: + - delete_default_v_check_mode is changed + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 4 + - _templates.launch_templates[0].latest_version_number == 4 + - _templates.launch_templates[0].default_version_number == 4 + + - name: Delete a single version and set a new default version different from the latest + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: 3 + default_version: 1 + register: delete_single_v + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Validate deletion result + ansible.builtin.assert: + that: + - delete_single_v is changed + - delete_single_v.deleted_template == {} + - delete_single_v.deleted_versions | length == 1 + - delete_single_v.deleted_versions.0.version_number == 3 + - delete_single_v.deleted_versions.0.launch_template_name == deletion_launch_template_name + - delete_single_v.deleted_versions.0.launch_template_id == _templates.launch_templates[0].launch_template_id + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 3 + - _templates.launch_templates[0].latest_version_number == 4 + - _templates.launch_templates[0].default_version_number == 1 + + #==================================================================== + # Delete multiple versions + #==================================================================== + - name: Delete multiple versions (check mode) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: [1, 2] + check_mode: true + register: delete_multiple_v_check_mode + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while no change was made + ansible.builtin.assert: + that: + - delete_multiple_v_check_mode is changed + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 3 + - _templates.launch_templates[0].latest_version_number == 4 + - _templates.launch_templates[0].default_version_number == 1 + + - name: Delete multiple versions + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + versions_to_delete: [1, 2] + register: delete_multiple_v + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while no change was made + ansible.builtin.assert: + that: + - delete_multiple_v is changed + - delete_multiple_v.deleted_template == {} + - delete_multiple_v.deleted_versions | length == 2 + - delete_multiple_v.deleted_versions | map(attribute='launch_template_name') | unique | list == [deletion_launch_template_name] + - delete_multiple_v.deleted_versions | map(attribute='launch_template_id') | unique | list == [_templates.launch_templates[0].launch_template_id] + - delete_multiple_v.deleted_versions | map(attribute='version_number') | sort | list == [1, 2] + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 1 + - _templates.launch_templates[0].latest_version_number == 4 + - _templates.launch_templates[0].default_version_number == 4 + + #==================================================================== + # Delete launch template + #==================================================================== + - name: Delete launch template (check mode) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + check_mode: true + register: delete_template_check_mode + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while no change was made + ansible.builtin.assert: + that: + - delete_template_check_mode is changed + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 1 + - _templates.launch_templates[0].latest_version_number == 4 + - _templates.launch_templates[0].default_version_number == 4 + + - name: Delete launch template + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + register: delete_template + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name }}" + register: _templates_v + + - name: Ensure the launch template was deleted + ansible.builtin.assert: + that: + - delete_template is changed + - delete_template.deleted_template.launch_template_id == _templates.launch_templates[0].launch_template_id + - delete_template.deleted_template.latest_version_number == _templates.launch_templates[0].latest_version_number + - delete_template.deleted_template.default_version_number == _templates.launch_templates[0].default_version_number + - delete_template.deleted_versions | length == 0 + - _templates_v.launch_templates | length == 0 + + - name: Delete launch template once again (idempotency) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name }}" + state: absent + register: delete_template_idempotency + + - name: Ensure module idempotency + ansible.builtin.assert: + that: + - delete_template_idempotency is not changed + - delete_template_idempotency.deleted_template == {} + - delete_template_idempotency.deleted_versions == [] + + #==================================================================== + # Delete launch template with multiple versions + #==================================================================== + - name: Create multiple versions of the launch template + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name_2 }}" + instance_type: "{{ item }}" + with_items: "{{ test_ec2_instance_types }}" + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name_2 }}" + register: _templates + + - name: Validate Launch template details + ansible.builtin.assert: + that: + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 5 + - _templates.launch_templates[0].latest_version_number == 5 + - _templates.launch_templates[0].default_version_number == 5 + + - name: Delete launch template with multiple versions (check mode) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name_2 }}" + state: absent + check_mode: true + register: delete_template_check_mode + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name_2 }}" + register: _templates + + - name: Ensure module reported change while no change was made + ansible.builtin.assert: + that: + - delete_template_check_mode is changed + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 5 + + - name: Delete launch template with multiple versions + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name_2 }}" + state: absent + register: delete_template + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ deletion_launch_template_name_2 }}" + register: _templates_v + + - name: Ensure the launch template was deleted + ansible.builtin.assert: + that: + - delete_template is changed + - delete_template.deleted_template.launch_template_id == _templates.launch_templates[0].launch_template_id + - delete_template.deleted_template.latest_version_number == _templates.launch_templates[0].latest_version_number + - delete_template.deleted_template.default_version_number == _templates.launch_templates[0].default_version_number + - delete_template.deleted_versions | length == 4 + - delete_template.deleted_versions | map(attribute='launch_template_name') | unique | list == [deletion_launch_template_name_2] + - delete_template.deleted_versions | map(attribute='launch_template_id') | unique | list == [_templates.launch_templates[0].launch_template_id] + - delete_template.deleted_versions | map(attribute='version_number') | sort | list == [1, 2, 3, 4] + - _templates_v.launch_templates | length == 0 + + - name: Delete launch template with multiple versions once again (idempotency) + community.aws.ec2_launch_template: + name: "{{ deletion_launch_template_name_2 }}" + state: absent + register: delete_template_idempotency + + - name: Ensure module idempotency + ansible.builtin.assert: + that: + - delete_template_idempotency is not changed + - delete_template_idempotency.deleted_template == {} + - delete_template_idempotency.deleted_versions == [] + + always: + - name: Delete the launch template + community.aws.ec2_launch_template: + name: "{{ item }}" + state: absent + with_items: + - "{{ deletion_launch_template_name }}" + - "{{ deletion_launch_template_name_2 }}" + ignore_errors: true diff --git a/tests/integration/targets/ec2_launch_template/tasks/iam_instance_role.yml b/tests/integration/targets/ec2_launch_template/tasks/iam_instance_role.yml index ad797fabb79..08a39dff855 100644 --- a/tests/integration/targets/ec2_launch_template/tasks/iam_instance_role.yml +++ b/tests/integration/targets/ec2_launch_template/tasks/iam_instance_role.yml @@ -1,6 +1,9 @@ -- block: +- name: Test using IAM instance profile + vars: + test_launch_template_name: "{{ resource_prefix }}-test-instance-role" + block: - name: Create IAM role for test - iam_role: + amazon.aws.iam_role: name: "{{ test_role_name }}-1" assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" state: present @@ -10,7 +13,7 @@ register: iam_role - name: Create second IAM role for test - iam_role: + amazon.aws.iam_role: name: "{{ test_role_name }}-2" assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" state: present @@ -20,115 +23,181 @@ register: iam_role_2 - name: Make instance with an instance_role - ec2_launch_template: - name: "{{ resource_prefix }}-test-instance-role" + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" instance_type: t2.micro iam_instance_profile: "{{ test_role_name }}-1" register: template_with_role - - assert: + - name: Get launch template details + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure the launch template was created with IAM instance profile + ansible.builtin.assert: that: - 'template_with_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/")' + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].versions | length == 1 + - _template_info.launch_templates[0].versions[0].launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/") + - _template_info.launch_templates[0].versions[0].launch_template_data.instance_type == "t2.micro" + - _template_info.launch_templates[0].versions[0].launch_template_data.image_id == ec2_ami_id - name: Create template again, with no change to instance_role - ec2_launch_template: - name: "{{ resource_prefix }}-test-instance-role" + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" instance_type: t2.micro iam_instance_profile: "{{ test_role_name }}-1" register: template_with_role - - assert: + - name: Get launch template details + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Validate idempotency + ansible.builtin.assert: that: - 'template_with_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/")' - 'template_with_role is not changed' + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].versions | length == 1 + - _template_info.launch_templates[0].versions[0].launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/") + - _template_info.launch_templates[0].versions[0].launch_template_data.instance_type == "t2.micro" + - _template_info.launch_templates[0].versions[0].launch_template_data.image_id == ec2_ami_id - name: Update instance with new instance_role - ec2_launch_template: - name: "{{ resource_prefix }}-test-instance-role" + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" instance_type: t2.micro iam_instance_profile: "{{ test_role_name }}-2" register: template_with_updated_role - - assert: + - name: Get launch template details + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure that the launch template was updated with new IAM instance profile + ansible.builtin.assert: that: - 'template_with_updated_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role_2.iam_role.arn.replace(":role/", ":instance-profile/")' - 'template_with_updated_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role_2.iam_role.arn.replace(":role/", ":instance-profile/")' - 'template_with_role.default_template.version_number < template_with_updated_role.default_template.version_number' - 'template_with_updated_role is changed' - 'template_with_updated_role is not failed' + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].versions | length == 2 + - template_version.launch_template_data.iam_instance_profile.arn == iam_role_2.iam_role.arn.replace(":role/", ":instance-profile/") + - template_version.launch_template_data.instance_type == "t2.micro" + - template_version.launch_template_data.image_id == ec2_ami_id + vars: + template_version: "{{ _template_info.launch_templates[0].versions | selectattr('version_number', 'equalto', 2) | list | first }}" - name: Re-set with same new instance_role - ec2_launch_template: - name: "{{ resource_prefix }}-test-instance-role" + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" instance_type: t2.micro iam_instance_profile: "{{ test_role_name }}-2" register: template_with_updated_role - - assert: + - name: Get launch template details + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure that module did not reported change + ansible.builtin.assert: that: - 'template_with_updated_role is not changed' - 'template_with_updated_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role_2.iam_role.arn.replace(":role/", ":instance-profile/")' + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].versions | length == 2 - name: Update instance with original instance_role (pass profile ARN) - ec2_launch_template: - name: "{{ resource_prefix }}-test-instance-role" + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" instance_type: t2.micro # By default an instance profile will be created with the same name as the role iam_instance_profile: '{{ iam_role.iam_role.arn.replace(":role/", ":instance-profile/") }}' register: template_with_updated_role - - assert: + - name: Get launch template details + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Validate that the launch template was updated + ansible.builtin.assert: that: - 'template_with_updated_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/")' - 'template_with_updated_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/")' - 'template_with_role.default_template.version_number < template_with_updated_role.default_template.version_number' - 'template_with_updated_role is changed' - 'template_with_updated_role is not failed' + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].versions | length == 3 + - template_version.launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/") + - template_version.launch_template_data.instance_type == "t2.micro" + - template_version.launch_template_data.image_id == ec2_ami_id + vars: + template_version: "{{ _template_info.launch_templates[0].versions | selectattr('version_number', 'equalto', 3) | list | first }}" - name: Re-set with same new instance_role (pass profile ARN) - ec2_launch_template: - name: "{{ resource_prefix }}-test-instance-role" + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" instance_type: t2.micro iam_instance_profile: '{{ iam_role.iam_role.arn.replace(":role/", ":instance-profile/") }}' register: template_with_updated_role - - assert: + - name: Get launch template details + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Assert that the template was not updated + ansible.builtin.assert: that: - 'template_with_updated_role is not changed' - 'template_with_updated_role.default_template.launch_template_data.iam_instance_profile.arn == iam_role.iam_role.arn.replace(":role/", ":instance-profile/")' + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].versions | length == 3 always: - - name: delete launch template - ec2_launch_template: - name: "{{ resource_prefix }}-test-instance-role" + - name: Delete launch template + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" state: absent register: lt_removed - until: lt_removed is not failed - ignore_errors: yes - retries: 10 + ignore_errors: true + - name: Delete IAM role for test - iam_role: + amazon.aws.iam_role: name: "{{ test_role_name }}-1" assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" state: absent - delete_instance_profile: yes + delete_instance_profile: true register: iam_removed - until: iam_removed is not failed - ignore_errors: yes - retries: 10 + ignore_errors: true + - name: Delete IAM role for test iam_role: name: "{{ test_role_name }}-2" assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" state: absent - delete_instance_profile: yes + delete_instance_profile: true register: iam_2_removed - until: iam_2_removed is not failed - ignore_errors: yes - retries: 10 + ignore_errors: true diff --git a/tests/integration/targets/ec2_launch_template/tasks/instance-metadata.yml b/tests/integration/targets/ec2_launch_template/tasks/instance-metadata.yml deleted file mode 100644 index 7648f00efb8..00000000000 --- a/tests/integration/targets/ec2_launch_template/tasks/instance-metadata.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -- name: instance_metadata_tags - block: - - name: metadata_options - ec2_launch_template: - name: "{{ resource_prefix }}-test-metadata" - metadata_options: - http_put_response_hop_limit: 1 - http_tokens: required - http_protocol_ipv6: enabled - instance_metadata_tags: enabled - state: present - register: metadata_options_launch_template - - name: instance with metadata_options created with the right options - assert: - that: - - metadata_options_launch_template is changed - - "metadata_options_launch_template.latest_template.launch_template_data.metadata_options.http_put_response_hop_limit == 1" - - "metadata_options_launch_template.latest_template.launch_template_data.metadata_options.http_tokens == 'required'" - - "metadata_options_launch_template.latest_template.launch_template_data.metadata_options.http_protocol_ipv6 == 'enabled'" - - "metadata_options_launch_template.latest_template.launch_template_data.metadata_options.instance_metadata_tags == 'enabled'" - always: - - name: delete the template - ec2_launch_template: - name: "{{ resource_prefix }}-test-metadata" - state: absent - register: del_lt - retries: 10 - until: del_lt is not failed - ignore_errors: true diff --git a/tests/integration/targets/ec2_launch_template/tasks/main.yml b/tests/integration/targets/ec2_launch_template/tasks/main.yml index e89dfceb557..c8ea5f055da 100644 --- a/tests/integration/targets/ec2_launch_template/tasks/main.yml +++ b/tests/integration/targets/ec2_launch_template/tasks/main.yml @@ -6,8 +6,8 @@ session_token: "{{ security_token | default(omit) }}" region: "{{ aws_region }}" block: - - include_tasks: cpu_options.yml + - include_tasks: template_data.yml + - include_tasks: tagging.yml - include_tasks: iam_instance_role.yml - include_tasks: versions.yml - - include_tasks: instance-metadata.yml - - include_tasks: network_interfaces.yml + - include_tasks: deletion.yml diff --git a/tests/integration/targets/ec2_launch_template/tasks/network_interfaces.yml b/tests/integration/targets/ec2_launch_template/tasks/network_interfaces.yml deleted file mode 100644 index a2ca0e5f6b9..00000000000 --- a/tests/integration/targets/ec2_launch_template/tasks/network_interfaces.yml +++ /dev/null @@ -1,53 +0,0 @@ -- block: - - name: network_interfaces - ec2_launch_template: - name: "{{ resource_prefix }}-test-nic" - state: present - network_interfaces: - - device_index: 0 - associate_public_ip_address: false - delete_on_termination: true - - device_index: 1 - associate_public_ip_address: true - delete_on_termination: false - ipv6_address_count: 1 - register: nic_template - - name: instance with network_interfaces created with the right settings - assert: - that: - - nic_template is changed - - nic_template.default_template.launch_template_data.network_interfaces[0].associate_public_ip_address == False - - nic_template.default_template.launch_template_data.network_interfaces[0].delete_on_termination == True - - nic_template.default_template.launch_template_data.network_interfaces[0].device_index == 0 - - nic_template.default_template.launch_template_data.network_interfaces[1].associate_public_ip_address == True - - nic_template.default_template.launch_template_data.network_interfaces[1].delete_on_termination == False - - nic_template.default_template.launch_template_data.network_interfaces[1].device_index == 1 - - nic_template.default_template.launch_template_data.network_interfaces[1].ipv6_address_count == 1 - - - name: network_interfaces - ec2_launch_template: - name: "{{ resource_prefix }}-test-nic" - state: present - network_interfaces: - - device_index: 0 - associate_public_ip_address: false - delete_on_termination: true - - device_index: 1 - associate_public_ip_address: true - delete_on_termination: false - ipv6_address_count: 1 - register: nic_template - - name: instance with network_interfaces created with the right settings - assert: - that: - - nic_template is not changed - - always: - - name: delete the template - ec2_launch_template: - name: "{{ resource_prefix }}-test-nic" - state: absent - register: del_lt - retries: 10 - until: del_lt is not failed - ignore_errors: true diff --git a/tests/integration/targets/ec2_launch_template/tasks/tagging.yml b/tests/integration/targets/ec2_launch_template/tasks/tagging.yml new file mode 100644 index 00000000000..b4d38da70c9 --- /dev/null +++ b/tests/integration/targets/ec2_launch_template/tasks/tagging.yml @@ -0,0 +1,210 @@ +--- +- name: Test tagging + vars: + test_launch_template_name: "{{ resource_prefix }}-tagging" + launch_template_instance_tags: + - key: foo + value: bar + - key: environment + value: test + launch_template_network_tags: + - key: owner + value: ansible + ansible_instance_tags: + foo: bar + environment: test + ansible_network_tags: + owner: ansible + block: + # Create launch template with tags + - name: Create Launch template with tags + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + instance_type: t2.micro + network_interfaces: + - associate_public_ip_address: false + delete_on_termination: true + device_index: 0 + tag_specifications: + - resource_type: instance + tags: "{{ ansible_instance_tags }}" + - resource_type: network-interface + tags: "{{ ansible_network_tags }}" + tags: + ResourcePrefix: "{{ resource_prefix }}" + InstanceType: "t2.micro" + register: _create_with_tags + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure the launch template was created with tags + ansible.builtin.assert: + that: + - _create_with_tags is changed + - '"tags" in _create_with_tags.template' + - _create_with_tags.template.tags.InstanceType == "t2.micro" + - _create_with_tags.template.tags.ResourcePrefix == resource_prefix + - _templates.launch_templates[0].tags.InstanceType == "t2.micro" + - _templates.launch_templates[0].tags.ResourcePrefix == resource_prefix + - _templates.launch_templates[0].versions | length == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | length == 2 + - instance_tags.tags == launch_template_instance_tags + - network_interface_tags.tags == launch_template_network_tags + vars: + instance_tags: "{{ _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | selectattr('resource_type', 'equalto', 'instance') | list | first }}" + network_interface_tags: "{{ _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | selectattr('resource_type', 'equalto', 'network-interface') | list | first }}" + + # Create launch template once again with same tags (expected no change) + - name: Create launch template once again with same tags (expected no change) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + instance_type: t2.micro + network_interfaces: + - associate_public_ip_address: false + delete_on_termination: true + device_index: 0 + tag_specifications: + - resource_type: instance + tags: "{{ ansible_instance_tags }}" + - resource_type: network-interface + tags: "{{ ansible_network_tags }}" + tags: + ResourcePrefix: "{{ resource_prefix }}" + InstanceType: "t2.micro" + register: _create_with_tags_idempotency + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure the launch template was created with tags + ansible.builtin.assert: + that: + - _create_with_tags_idempotency is not changed + - '"tags" in _create_with_tags_idempotency.template' + - _create_with_tags_idempotency.template.tags.InstanceType == "t2.micro" + - _create_with_tags_idempotency.template.tags.ResourcePrefix == resource_prefix + - _templates.launch_templates[0].tags.InstanceType == "t2.micro" + - _templates.launch_templates[0].tags.ResourcePrefix == resource_prefix + - _templates.launch_templates[0].versions | length == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | length == 2 + - instance_tags.tags == launch_template_instance_tags + - network_interface_tags.tags == launch_template_network_tags + vars: + instance_tags: "{{ _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | selectattr('resource_type', 'equalto', 'instance') | list | first }}" + network_interface_tags: "{{ _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | selectattr('resource_type', 'equalto', 'network-interface') | list | first }}" + + # Add new tag + - name: Add new tag with purge_tags=false + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + instance_type: t2.micro + network_interfaces: + - associate_public_ip_address: false + delete_on_termination: true + device_index: 0 + tag_specifications: + - resource_type: instance + tags: "{{ ansible_instance_tags }}" + - resource_type: network-interface + tags: "{{ ansible_network_tags }}" + tags: + Phase: integration + purge_tags: false + register: _add_tag + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure the launch template was created with tags + ansible.builtin.assert: + that: + - _add_tag is changed + - '"tags" in _add_tag.template' + - _add_tag.template.tags.InstanceType == "t2.micro" + - _add_tag.template.tags.ResourcePrefix == resource_prefix + - _add_tag.template.tags.Phase == "integration" + - _templates.launch_templates[0].tags.InstanceType == "t2.micro" + - _templates.launch_templates[0].tags.ResourcePrefix == resource_prefix + - _templates.launch_templates[0].tags.Phase == "integration" + - _templates.launch_templates[0].versions | length == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | length == 2 + - instance_tags.tags == launch_template_instance_tags + - network_interface_tags.tags == launch_template_network_tags + vars: + instance_tags: "{{ _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | selectattr('resource_type', 'equalto', 'instance') | list | first }}" + network_interface_tags: "{{ _templates.launch_templates[0].versions[0].launch_template_data.tag_specifications | selectattr('resource_type', 'equalto', 'network-interface') | list | first }}" + + # Add new launch template version and update tags + - name: Add new launch template version and update tags + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + instance_type: t3.micro + tags: + Team: Ansible + purge_tags: true + source_version: "" + register: _add_tag_and_version + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure the launch template was created with tags + ansible.builtin.assert: + that: + - _add_tag_and_version is changed + - '"tags" in _add_tag.template' + - '"InstanceType" not in _add_tag_and_version.template.tags' + - '"ResourcePrefix" not in _add_tag_and_version.template.tags' + - '"Phase" not in _add_tag_and_version.template.tags' + - _add_tag_and_version.template.tags.Team == "Ansible" + - '"InstanceType" not in _templates.launch_templates[0].tags' + - '"ResourcePrefix" not in _templates.launch_templates[0].tags' + - '"Phase" not in _templates.launch_templates[0].tags' + - _templates.launch_templates[0].tags.Team == "Ansible" + - _templates.launch_templates[0].versions | length == 2 + - '"tag_specifications" not in latest_version_template_data.launch_template_data' + vars: + latest_version_template_data: '{{ _templates.launch_templates[0].versions | selectattr("version_number", "equalto", 2) | list | first }}' + + # Purge tags + - name: Purge all tags from launch template + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + instance_type: t3.micro + tags: {} + purge_tags: true + register: _purge_tags + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure the launch template was created with tags + ansible.builtin.assert: + that: + - _purge_tags is changed + - '"tags" not in _purge_tags.template' + - _templates.launch_templates[0].tags == {} + - _templates.launch_templates[0].versions | length == 2 + + always: + - name: Delete launch template + community.aws.ec2_launch_template: + state: absent + name: "{{ test_launch_template_name }}" + ignore_errors: true diff --git a/tests/integration/targets/ec2_launch_template/tasks/tags_and_vpc_settings.yml b/tests/integration/targets/ec2_launch_template/tasks/tags_and_vpc_settings.yml deleted file mode 100644 index 41ff9082b76..00000000000 --- a/tests/integration/targets/ec2_launch_template/tasks/tags_and_vpc_settings.yml +++ /dev/null @@ -1,208 +0,0 @@ -- block: - # ============================================================ - # set up VPC - - name: Create VPC for use in testing - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc" - cidr_block: 10.99.0.0/16 - tags: - Name: Ansible ec2_instance Testing VPC - tenancy: default - register: testing_vpc - - - name: Create default subnet in zone A - ec2_vpc_subnet: - state: present - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: 10.99.0.0/24 - az: "{{ aws_region }}a" - resource_tags: - Name: "{{ resource_prefix }}-subnet-a" - register: testing_subnet_a - - - name: Create secondary subnet in zone B - ec2_vpc_subnet: - state: present - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: 10.99.1.0/24 - az: "{{ aws_region }}b" - resource_tags: - Name: "{{ resource_prefix }}-subnet-b" - register: testing_subnet_b - - - name: create a security group with the vpc - ec2_security_group: - name: "{{ resource_prefix }}-sg" - description: a security group for ansible tests - vpc_id: "{{ testing_vpc.vpc.id }}" - rules: - - proto: tcp - ports: [22, 80] - cidr_ip: 0.0.0.0/0 - register: sg - # TODO: switch these tests from instances - - assert: - that: - - 1 == 0 - # ============================================================ - # start subnet/sg testing - - name: Make instance in the testing subnet created in the test VPC - ec2_instance: - name: "{{ resource_prefix }}-test-basic-vpc-create" - image_id: "{{ ec2_ami_id }}" - user_data: | - #cloud-config - package_upgrade: true - package_update: true - tags: - TestId: "{{ resource_prefix }}" - Something: else - security_groups: "{{ sg.group_id }}" - network: - source_dest_check: false - vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" - instance_type: t2.micro - volumes: - - device_name: /dev/sda1 - ebs: - delete_on_termination: true - register: in_test_vpc - - - name: Try to re-make the instance, hopefully this shows changed=False - ec2_instance: - name: "{{ resource_prefix }}-test-basic-vpc-create" - image_id: "{{ ec2_ami_id }}" - user_data: | - #cloud-config - package_upgrade: true - package_update: true - tags: - TestId: "{{ resource_prefix }}" - Something: else - security_groups: "{{ sg.group_id }}" - vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" - instance_type: t2.micro - register: remake_in_test_vpc - - name: "Remaking the same instance resulted in no changes" - assert: - that: not remake_in_test_vpc.changed - - name: check that instance IDs match anyway - assert: - that: 'remake_in_test_vpc.instance_ids[0] == in_test_vpc.instance_ids[0]' - - name: check that source_dest_check was set to false - assert: - that: 'not remake_in_test_vpc.instances[0].source_dest_check' - - - name: Alter it by adding tags - ec2_instance: - name: "{{ resource_prefix }}-test-basic-vpc-create" - image_id: "{{ ec2_ami_id }}" - tags: - TestId: "{{ resource_prefix }}" - Another: thing - security_groups: "{{ sg.group_id }}" - vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" - instance_type: t2.micro - register: add_another_tag - - - ec2_instance_info: - instance_ids: "{{ add_another_tag.instance_ids }}" - register: check_tags - - name: "Remaking the same instance resulted in no changes" - assert: - that: - - check_tags.instances[0].tags.Another == 'thing' - - check_tags.instances[0].tags.Something == 'else' - - - name: Purge a tag - ec2_instance: - name: "{{ resource_prefix }}-test-basic-vpc-create" - image_id: "{{ ec2_ami_id }}" - purge_tags: true - tags: - TestId: "{{ resource_prefix }}" - Another: thing - security_groups: "{{ sg.group_id }}" - vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" - instance_type: t2.micro - - ec2_instance_info: - instance_ids: "{{ add_another_tag.instance_ids }}" - register: check_tags - - name: "Remaking the same instance resulted in no changes" - assert: - that: - - "'Something' not in check_tags.instances[0].tags" - - - name: Terminate instance - ec2_instance: - filters: - tag:TestId: "{{ resource_prefix }}" - state: absent - register: result - - assert: - that: result.changed - - - name: Terminate instance - ec2_instance: - instance_ids: "{{ in_test_vpc.instance_ids }}" - state: absent - register: result - - assert: - that: not result.changed - - - name: check that subnet-default public IP rule was followed - assert: - that: - - in_test_vpc.instances[0].public_dns_name == "" - - in_test_vpc.instances[0].private_ip_address.startswith("10.22.33") - - in_test_vpc.instances[0].subnet_id == testing_subnet_b.subnet.id - - name: check that tags were applied - assert: - that: - - in_test_vpc.instances[0].tags.Name.startswith(resource_prefix) - - in_test_vpc.instances[0].state.name == 'running' - - always: - - name: remove the security group - ec2_security_group: - name: "{{ resource_prefix }}-sg" - description: a security group for ansible tests - vpc_id: "{{ testing_vpc.vpc.id }}" - state: absent - register: removed - until: removed is not failed - ignore_errors: yes - retries: 10 - - - name: remove subnet A - ec2_vpc_subnet: - state: absent - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: 10.99.0.0/24 - register: removed - until: removed is not failed - ignore_errors: yes - retries: 10 - - - name: remove subnet B - ec2_vpc_subnet: - state: absent - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: 10.99.1.0/24 - register: removed - until: removed is not failed - ignore_errors: yes - retries: 10 - - - name: remove the VPC - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc" - cidr_block: 10.99.0.0/16 - state: absent - tags: - Name: Ansible Testing VPC - tenancy: default - register: removed - until: removed is not failed - ignore_errors: yes - retries: 10 diff --git a/tests/integration/targets/ec2_launch_template/tasks/template_data.yml b/tests/integration/targets/ec2_launch_template/tasks/template_data.yml new file mode 100644 index 00000000000..cc9a4962f62 --- /dev/null +++ b/tests/integration/targets/ec2_launch_template/tasks/template_data.yml @@ -0,0 +1,145 @@ +- name: Test launch template data + vars: + test_launch_template_name: "{{ resource_prefix }}-template-data" + block: + # Launch template meta data + - name: Create launch template (check mode) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + image_id: "{{ ec2_ami_id }}" + instance_type: c4.large + cpu_options: + core_count: 1 + threads_per_core: 1 + network_interfaces: + - device_index: 0 + associate_public_ip_address: false + delete_on_termination: true + - device_index: 1 + associate_public_ip_address: true + delete_on_termination: false + ipv6_address_count: 1 + metadata_options: + http_put_response_hop_limit: 1 + http_tokens: required + http_protocol_ipv6: enabled + instance_metadata_tags: enabled + register: _create_check + check_mode: true + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while the template was not created + ansible.builtin.assert: + that: + - _create_check is changed + - _templates.launch_templates | length == 0 + + - name: Create launch template + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + image_id: "{{ ec2_ami_id }}" + instance_type: c4.large + cpu_options: + core_count: 1 + threads_per_core: 3 + network_interfaces: + - device_index: 0 + associate_public_ip_address: false + delete_on_termination: true + - device_index: 1 + associate_public_ip_address: true + delete_on_termination: false + ipv6_address_count: 1 + metadata_options: + http_put_response_hop_limit: 1 + http_tokens: required + http_protocol_ipv6: enabled + instance_metadata_tags: enabled + register: _create + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while the template was not created + ansible.builtin.assert: + that: + - _create is changed + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.cpu_options.core_count == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.cpu_options.threads_per_core == 3 + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.http_put_response_hop_limit == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.http_tokens == 'required' + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.http_protocol_ipv6 == 'enabled' + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.instance_metadata_tags == 'enabled' + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[0].associate_public_ip_address == False + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[0].delete_on_termination == True + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[0].device_index == 0 + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].associate_public_ip_address == True + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].delete_on_termination == False + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].device_index == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].ipv6_address_count == 1 + + - name: Create launch template once again with same parameters (idempotency) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + image_id: "{{ ec2_ami_id }}" + instance_type: c4.large + cpu_options: + core_count: 1 + threads_per_core: 3 + network_interfaces: + - device_index: 0 + associate_public_ip_address: false + delete_on_termination: true + - device_index: 1 + associate_public_ip_address: true + delete_on_termination: false + ipv6_address_count: 1 + metadata_options: + http_put_response_hop_limit: 1 + http_tokens: required + http_protocol_ipv6: enabled + instance_metadata_tags: enabled + register: _create_idempotency + + - name: Retrieve Launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _templates + + - name: Ensure module reported change while the template was not created + ansible.builtin.assert: + that: + - _create_idempotency is not changed + - _templates.launch_templates | length == 1 + - _templates.launch_templates[0].versions | length == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.cpu_options.core_count == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.cpu_options.threads_per_core == 3 + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.http_put_response_hop_limit == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.http_tokens == 'required' + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.http_protocol_ipv6 == 'enabled' + - _templates.launch_templates[0].versions[0].launch_template_data.metadata_options.instance_metadata_tags == 'enabled' + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[0].associate_public_ip_address == False + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[0].delete_on_termination == True + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[0].device_index == 0 + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].associate_public_ip_address == True + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].delete_on_termination == False + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].device_index == 1 + - _templates.launch_templates[0].versions[0].launch_template_data.network_interfaces[1].ipv6_address_count == 1 + + always: + - name: delete the template + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + state: absent + ignore_errors: true diff --git a/tests/integration/targets/ec2_launch_template/tasks/versions.yml b/tests/integration/targets/ec2_launch_template/tasks/versions.yml index a9e40cd0843..666c5653836 100644 --- a/tests/integration/targets/ec2_launch_template/tasks/versions.yml +++ b/tests/integration/targets/ec2_launch_template/tasks/versions.yml @@ -1,95 +1,462 @@ -- block: - - name: create simple instance template - ec2_launch_template: - name: "{{ resource_prefix }}-simple" +- name: Test launch template versioning + vars: + test_launch_template_name: "{{ resource_prefix }}-versioning" + block: + #===================================================================== + # Create the launch template + #===================================================================== + - name: Create a launch template (check mode) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" tags: TestId: "{{ resource_prefix }}" instance_type: c4.large - register: lt + register: _create_check + check_mode: true - - name: instance with cpu_options created with the right options - assert: + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure module reported changed while the template was not created + ansible.builtin.assert: + that: + - _create_check is changed + - '"default_version" not in _create_check' + - '"latest_version" not in _create_check' + - _template_info.launch_templates | length == 0 + + - name: Create a launch template + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: c4.large + register: _create + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info + + - name: Ensure the launch template was created with the right version + ansible.builtin.assert: that: - - lt is success - - lt is changed - - lt.default_version == 1 - - lt.latest_version == 1 + - _create is changed + - _create.default_version == 1 + - _create.latest_version == 1 + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 1 + - _template_info.launch_templates[0].latest_version_number == 1 + - _template_info.launch_templates[0].versions | length == 1 + - _template_info.launch_templates[0].versions.0.launch_template_data.image_id == ec2_ami_id + - _template_info.launch_templates[0].versions.0.launch_template_data.instance_type == "c4.large" - - name: update simple instance template - ec2_launch_template: - name: "{{ resource_prefix }}-simple" + - name: Create the same launch template once again + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: c4.large + register: _create_idempotency + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure the module did not reported change (idempotency) + ansible.builtin.assert: + that: + - _create_idempotency is not changed + - _create.default_version == 1 + - _create.latest_version == 1 + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 1 + - _template_info.launch_templates[0].latest_version_number == 1 + - _template_info.launch_templates[0].versions | length == 1 + - _template_info.launch_templates[0].versions.0.launch_template_data.image_id == ec2_ami_id + - _template_info.launch_templates[0].versions.0.launch_template_data.instance_type == "c4.large" + + #===================================================================== + # Create a new version of the launch template (set first version as default) + #===================================================================== + - name: Create a new version of the launch template (check mode) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" default_version: 1 image_id: "{{ ec2_ami_id }}" tags: TestId: "{{ resource_prefix }}" instance_type: m5.large - register: lt + register: _update_check + check_mode: true + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info - - name: instance with cpu_options created with the right options - assert: + - name: Ensure the module reported change in check mode + ansible.builtin.assert: that: - - lt is success - - lt is changed - - lt.default_version == 1 - - lt.latest_version == 2 + - _update_check is changed + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 1 + - _template_info.launch_templates[0].latest_version_number == 1 + - _template_info.launch_templates[0].versions | length == 1 - - name: update simple instance template - ec2_launch_template: - name: "{{ resource_prefix }}-simple" + - name: Create a new version of the launch template + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + default_version: 1 + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: m5.large + register: _update + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure the launch template latest version has changed + ansible.builtin.assert: + that: + - _update is changed + - _update.default_version == 1 + - _update.latest_version == 2 + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 1 + - _template_info.launch_templates[0].latest_version_number == 2 + - _template_info.launch_templates[0].versions | length == 2 + - created_template.launch_template_data.image_id == ec2_ami_id + - created_template.launch_template_data.instance_type == "m5.large" + vars: + created_template: "{{ _template_info.launch_templates[0].versions | selectattr('version_number', 'equalto', 2) | first }}" + + - name: Create a new version of the launch template (idempotency) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + default_version: 1 + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: m5.large + register: _update_idempotency + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure the module did not reported change (idempotency) + ansible.builtin.assert: + that: + - _update_idempotency is not changed + - _update_idempotency.default_version == 1 + - _update_idempotency.latest_version == 2 + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 1 + - _template_info.launch_templates[0].latest_version_number == 2 + - _template_info.launch_templates[0].versions | length == 2 + + #===================================================================== + # Set the latest version of the launch template as default + #===================================================================== + - name: Set the latest version of the launch template as default (check mode) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + default_version: latest + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: m5.large + register: _set_version_check + check_mode: true + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure the module reported change in check mode + ansible.builtin.assert: + that: + - _set_version_check is changed + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 1 + - _template_info.launch_templates[0].latest_version_number == 2 + - _template_info.launch_templates[0].versions | length == 2 + + - name: Set the latest version of the launch template as default + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + default_version: latest + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: m5.large + register: _set_version + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure the launch template latest version has changed + ansible.builtin.assert: + that: + - _set_version is changed + - _set_version.default_version == 2 + - _set_version.latest_version == 2 + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 2 + - _template_info.launch_templates[0].latest_version_number == 2 + - _template_info.launch_templates[0].versions | length == 2 + - created_template.launch_template_data.image_id == ec2_ami_id + - created_template.launch_template_data.instance_type == "m5.large" + vars: + created_template: "{{ _template_info.launch_templates[0].versions | selectattr('version_number', 'equalto', 2) | first }}" + + - name: Set the latest version of the launch template as default (idempotency) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + default_version: latest + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: m5.large + register: _set_version_idempotency + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + filters: + launch-template-name: "{{ test_launch_template_name }}" + register: _template_info + + - name: Ensure the module did not reported change (idempotency) + ansible.builtin.assert: + that: + - _set_version_idempotency is not changed + - _set_version_idempotency.default_version == 2 + - _set_version_idempotency.latest_version == 2 + - _template_info.launch_templates[0].default_version_number == 2 + - _template_info.launch_templates[0].latest_version_number == 2 + - _template_info.launch_templates[0].versions | length == 2 + + #===================================================================== + # Create another version + #===================================================================== + - name: Create a new launch template version (check mode) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: t3.medium + register: _another_version_check + check_mode: true + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info + + - name: Ensure the module reported change in check_mode + ansible.builtin.assert: + that: + - _another_version_check is changed + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 2 + - _template_info.launch_templates[0].latest_version_number == 2 + - _template_info.launch_templates[0].versions | length == 2 + + - name: Create a new launch template version + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" image_id: "{{ ec2_ami_id }}" tags: TestId: "{{ resource_prefix }}" instance_type: t3.medium - register: lt + register: _another_version + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info - - name: instance with cpu_options created with the right options - assert: + - name: Ensure the launch template latest version has changed + ansible.builtin.assert: that: - - lt is success - - lt is changed - - lt.default_version == 3 - - lt.latest_version == 3 + - _another_version is changed + - _another_version.default_version == 3 + - _another_version.latest_version == 3 + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 3 + - _template_info.launch_templates[0].latest_version_number == 3 + - _template_info.launch_templates[0].versions | length == 3 + - created_template.launch_template_data.image_id == ec2_ami_id + - created_template.launch_template_data.instance_type == "t3.medium" + vars: + created_template: "{{ _template_info.launch_templates[0].versions | selectattr('version_number', 'equalto', 3) | first }}" - - name: create new template version based on an old version - ec2_launch_template: - name: "{{ resource_prefix }}-simple" + - name: Create a new launch template version (idempotency) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + image_id: "{{ ec2_ami_id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: t3.medium + register: _another_version_idempotency + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info + + - name: Ensure the module did not reported change (idempotency) + ansible.builtin.assert: + that: + - _another_version_idempotency is not changed + - _another_version_idempotency.default_version == 3 + - _another_version_idempotency.latest_version == 3 + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 3 + - _template_info.launch_templates[0].latest_version_number == 3 + - _template_info.launch_templates[0].versions | length == 3 + + #===================================================================== + # Create another version based on an old version + #===================================================================== + - name: Create new template version based on an old version (check mode) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + cpu_options: + core_count: 1 + threads_per_core: 1 + source_version: 1 + register: _version_based_on_old_version_check + check_mode: true + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info + + - name: Ensure module reported change in check mode + ansible.builtin.assert: + that: + - _version_based_on_old_version_check is changed + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 3 + - _template_info.launch_templates[0].latest_version_number == 3 + - _template_info.launch_templates[0].versions | length == 3 + + - name: Create new template version based on an old version + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" cpu_options: core_count: 1 threads_per_core: 1 source_version: 1 - register: lt + register: _version_based_on_old_version + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info - - name: instance with cpu_options created with the right options - assert: + - name: Ensure the new launch template has been created with the right options + ansible.builtin.assert: that: - - lt is success - - lt is changed - - lt.default_version == 4 - - lt.latest_version == 4 - - lt.latest_template.launch_template_data.instance_type == "c4.large" + - _version_based_on_old_version is changed + - _version_based_on_old_version.default_version == 4 + - _version_based_on_old_version.latest_version == 4 + - _version_based_on_old_version.latest_template.launch_template_data.instance_type == "c4.large" + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 4 + - _template_info.launch_templates[0].latest_version_number == 4 + - _template_info.launch_templates[0].versions | length == 4 + - created_template.launch_template_data.image_id == ec2_ami_id + - created_template.launch_template_data.cpu_options.core_count == 1 + - created_template.launch_template_data.cpu_options.threads_per_core == 1 + vars: + created_template: "{{ _template_info.launch_templates[0].versions | selectattr('version_number', 'equalto', 4) | first }}" - - name: update simple instance template - ec2_launch_template: - name: "{{ resource_prefix }}-simple" + #===================================================================== + # Create another version with updated description + #===================================================================== + - name: Create a launch template version with another description (check mode) + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" version_description: "Fix something." - register: lt + register: _version_description_check + check_mode: true + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info + + - name: Ensure module reported change in check mode + ansible.builtin.assert: + that: + - _version_description_check is changed + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 4 + - _template_info.launch_templates[0].latest_version_number == 4 + - _template_info.launch_templates[0].versions | length == 4 + + - name: Create a launch template version with another description + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" + version_description: "Fix something." + register: _version_description + + - name: Read launch template information + amazon.aws.ec2_launch_template_info: + launch_template_ids: + - "{{ _create.template.launch_template_id }}" + register: _template_info - - name: instance with cpu_options created with the right options - assert: + - name: Ensure module reported change + ansible.builtin.assert: that: - - lt is success - - lt is changed - - lt.default_version == 5 - - lt.latest_version == 5 - - lt.latest_template.version_description == "Fix something." + - _version_description is changed + - _version_description.default_version == 5 + - _version_description.latest_version == 5 + - _version_description.latest_template.version_description == "Fix something." + - _template_info.launch_templates | length == 1 + - _template_info.launch_templates[0].default_version_number == 5 + - _template_info.launch_templates[0].latest_version_number == 5 + - _template_info.launch_templates[0].versions | length == 5 + - created_template.version_description == "Fix something." + vars: + created_template: "{{ _template_info.launch_templates[0].versions | selectattr('version_number', 'equalto', 5) | first }}" always: - - name: delete the template - ec2_launch_template: - name: "{{ resource_prefix }}-simple" + - name: Delete the template + community.aws.ec2_launch_template: + name: "{{ test_launch_template_name }}" state: absent - register: del_lt - retries: 10 - until: del_lt is not failed ignore_errors: true