From 03d28236bedf107ca107a2dd9e698457fb6785de Mon Sep 17 00:00:00 2001 From: Brant Evans Date: Tue, 14 Jan 2025 08:58:52 -0700 Subject: [PATCH] ec2_vpc_nacl_info - Fix failure when listing NetworkACLs (#2425) (#2429) SUMMARY Only fail listing NetworkACLs if specific ACLs were requested and not found. Fixes: #2425 ISSUE TYPE Bugfix Pull Request COMPONENT NAME ec2_vpc_nacl_info ADDITIONAL INFORMATION Reviewed-by: Mark Chappell (cherry picked from commit 7b5212d38d6f0f80064cdea1dc20408b5caffab2) --- ..._nacl_info-fix-issue-returning-results.yml | 3 + plugins/modules/ec2_vpc_nacl_info.py | 234 ++++++++++++++++++ .../targets/ec2_vpc_nacl/tasks/main.yml | 198 +++++++++++++++ 3 files changed, 435 insertions(+) create mode 100644 changelogs/fragments/20241219-ec2_vpc_nacl_info-fix-issue-returning-results.yml create mode 100644 plugins/modules/ec2_vpc_nacl_info.py create mode 100644 tests/integration/targets/ec2_vpc_nacl/tasks/main.yml diff --git a/changelogs/fragments/20241219-ec2_vpc_nacl_info-fix-issue-returning-results.yml b/changelogs/fragments/20241219-ec2_vpc_nacl_info-fix-issue-returning-results.yml new file mode 100644 index 00000000000..3f0f9cac84f --- /dev/null +++ b/changelogs/fragments/20241219-ec2_vpc_nacl_info-fix-issue-returning-results.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - ec2_vpc_nacl_info - Fix failure when listing NetworkACLs and no ACLs are found (https://github.com/ansible-collections/amazon.aws/issues/2425). diff --git a/plugins/modules/ec2_vpc_nacl_info.py b/plugins/modules/ec2_vpc_nacl_info.py new file mode 100644 index 00000000000..f086d3ada78 --- /dev/null +++ b/plugins/modules/ec2_vpc_nacl_info.py @@ -0,0 +1,234 @@ +#!/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: ec2_vpc_nacl_info +version_added: 1.0.0 +version_added_collection: community.aws +short_description: Gather information about Network ACLs in an AWS VPC +description: + - Gather information about Network ACLs in an AWS VPC. +author: + - "Brad Davidson (@brandond)" +options: + nacl_ids: + description: + - A list of Network ACL IDs to retrieve information about. + required: false + default: [] + aliases: [nacl_id] + type: list + elements: str + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See + U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkAcls.html) for possible filters. Filter + names and values are case sensitive. + required: false + default: {} + type: dict +notes: + - By default, the module will return all Network ACLs. + +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. + +# Gather information about all Network ACLs: +- name: Get All NACLs + amazon.aws.ec2_vpc_nacl_info: + region: us-west-2 + register: all_nacls + +# Retrieve default Network ACLs: +- name: Get Default NACLs + amazon.aws.ec2_vpc_nacl_info: + region: us-west-2 + filters: + 'default': 'true' + register: default_nacls +""" + +RETURN = r""" +nacls: + description: Returns an array of complex objects as described below. + returned: success + type: complex + contains: + nacl_id: + description: The ID of the Network Access Control List. + returned: always + type: str + vpc_id: + description: The ID of the VPC that the NACL is attached to. + returned: always + type: str + is_default: + description: True if the NACL is the default for its VPC. + returned: always + type: bool + tags: + description: A dict of tags associated with the NACL. + returned: always + type: dict + subnets: + description: A list of subnet IDs that are associated with the NACL. + returned: always + type: list + elements: str + ingress: + description: + - A list of NACL ingress rules. + - The rule format is C([rule no, protocol, allow/deny, v4 or v6 cidr, icmp_type, icmp_code, port from, port to]). + returned: always + type: list + elements: list + sample: [[100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22]] + egress: + description: + - A list of NACL egress rules. + - The rule format is C([rule no, protocol, allow/deny, v4 or v6 cidr, icmp_type, icmp_code, port from, port to]). + returned: always + type: list + elements: list + sample: [[100, 'all', 'allow', '0.0.0.0/0', null, null, null, null]] +""" + +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.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_network_acls +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.amazon.aws.plugins.module_utils.transformation import ansible_dict_to_boto3_filter_list + +# VPC-supported IANA protocol numbers +# http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml +PROTOCOL_NAMES = {"-1": "all", "1": "icmp", "6": "tcp", "17": "udp"} + + +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 = module.params.get("filters") + + params = {} + if filters: + params["Filters"] = ansible_dict_to_boto3_filter_list(filters) + if nacl_ids: + params["NetworkAclIds"] = nacl_ids + + try: + network_acls = describe_network_acls(connection, **params) + if nacl_ids and not len(nacl_ids) == len(network_acls): + if len(nacl_ids) == 1: + module.fail_json(msg="Unable to describe ACL. NetworkAcl does not exist.") + else: + module.fail_json(msg="Unable to describe all ACLs. One or more NetworkAcls 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 = [] + + elist.append(entry["rule_number"]) + + if entry.get("protocol") in PROTOCOL_NAMES: + elist.append(PROTOCOL_NAMES[entry["protocol"]]) + else: + elist.append(entry.get("protocol")) + + elist.append(entry["rule_action"]) + + if entry.get("cidr_block"): + elist.append(entry["cidr_block"]) + elif entry.get("ipv6_cidr_block"): + elist.append(entry["ipv6_cidr_block"]) + else: + elist.append(None) + + elist = elist + [None, None, None, None] + + if entry["protocol"] in ("1", "58"): + elist[4] = entry.get("icmp_type_code", {}).get("type") + elist[5] = entry.get("icmp_type_code", {}).get("code") + + if entry["protocol"] not in ("1", "6", "17", "58"): + elist[6] = 0 + elist[7] = 65535 + elif "port_range" in entry: + elist[6] = entry["port_range"]["from"] + elist[7] = entry["port_range"]["to"] + + return elist + + +def main(): + argument_spec = dict( + nacl_ids=dict(default=[], type="list", aliases=["nacl_id"], elements="str"), + filters=dict(default={}, type="dict"), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + connection = module.client("ec2") + + list_ec2_vpc_nacls(connection, module) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml new file mode 100644 index 00000000000..69c8db8140d --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml @@ -0,0 +1,198 @@ +--- +- module_defaults: + group/aws: + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + session_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + + block: + # ============================================================ + + - name: Test without any parameters + amazon.aws.ec2_vpc_nacl: + register: result + ignore_errors: true + + - 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 + amazon.aws.ec2_vpc_nacl_info: + register: nacl_facts + + - name: Assert we don't error + ansible.builtin.assert: + that: + - nacl_facts is succeeded + + - name: Get network ACL info with invalid ID + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "acl-000000000000" + register: nacl_facts + ignore_errors: true + + - name: Assert message mentions missing ACLs + ansible.builtin.assert: + that: + - nacl_facts is failed + - '"does not exist" in nacl_facts.msg' + - '"One or more" not in nacl_facts.msg' + + - name: Get network multiple ACLs info with invalid ID + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - 'acl-000000000000' + - 'acl-000000000001' + register: nacl_facts + ignore_errors: true + + - name: Assert message mentions missing ACLs + assert: + that: + - nacl_facts is failed + - '"does not exist" in nacl_facts.msg' + - '"One or more" in nacl_facts.msg' + + - name: Get network ACL info with filters + amazon.aws.ec2_vpc_nacl_info: + filters: + default: false + register: nacl_facts + + - name: Assert error is not returned + ansible.builtin.assert: + that: + - nacl_facts is succeeded + # ============================================================ + + - name: Fetch AZ availability + amazon.aws.aws_az_info: + register: az_info + + - name: Assert that we have multiple AZs available to us + ansible.builtin.assert: + that: az_info.availability_zones | length >= 2 + + - 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 + amazon.aws.ec2_vpc_net: + cidr_block: "{{ vpc_cidr }}" + name: "{{ vpc_name }}" + state: present + register: vpc + + - name: Save VPC ID for later + ansible.builtin.set_fact: + vpc_id: "{{ vpc.vpc.id }}" + + - name: Create subnets + amazon.aws.ec2_vpc_subnet: + cidr: "{{ item.cidr }}" + az: "{{ item.az }}" + vpc_id: "{{ vpc_id }}" + state: present + tags: + Name: "{{ item.name }}" + with_items: + - cidr: "{{ subnet_1 }}" + az: "{{ az_one }}" + name: "{{ subnet_name }}-1" + - cidr: "{{ subnet_2 }}" + az: "{{ az_two }}" + name: "{{ subnet_name }}-2" + - cidr: "{{ subnet_3 }}" + az: "{{ az_one }}" + name: "{{ subnet_name }}-3" + - cidr: "{{ subnet_4 }}" + az: "{{ az_two }}" + name: "{{ subnet_name }}-4" + register: subnets + + - 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 + amazon.aws.ec2_vpc_net: + cidr_block: "{{ vpc_ipv6_cidr }}" + name: "{{ vpc_ipv6_name }}" + state: present + ipv6_cidr: true + register: vpc_result + + - 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 + amazon.aws.ec2_vpc_subnet: + cidr: "{{ vpc_ipv6_cidr }}" + vpc_id: "{{ vpc_ipv6_id }}" + ipv6_cidr: "{{ subnet_ipv6 }}" + state: present + tags: + Name: "{{ subnet_name }}-ipv6" + + # ============================================================ + - 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 + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + state: absent + register: removed_acl + ignore_errors: true + + - name: Remove subnets + amazon.aws.ec2_vpc_subnet: + cidr: "{{ item.cidr }}" + vpc_id: "{{ item.vpc_id | default(vpc_id) }}" + state: absent + with_items: + - cidr: "{{ subnet_1 }}" + - cidr: "{{ subnet_2 }}" + - cidr: "{{ subnet_3 }}" + - cidr: "{{ subnet_4 }}" + - cidr: "{{ vpc_ipv6_cidr }}" + vpc_id: "{{ vpc_ipv6_id }}" + ignore_errors: true + register: removed_subnets + + - name: Remove the VPCs + amazon.aws.ec2_vpc_net: + vpc_id: "{{ item }}" + state: absent + ignore_errors: true + register: removed_vpc + with_items: + - "{{ vpc_id }}" + - "{{ vpc_ipv6_id }}" + + # ============================================================