diff --git a/changelogs/fragments/migrate_ec2_vpc_nacl_ec2_vpc_nacl_info.yaml b/changelogs/fragments/migrate_ec2_vpc_nacl_ec2_vpc_nacl_info.yaml new file mode 100644 index 00000000000..e6259b3f59c --- /dev/null +++ b/changelogs/fragments/migrate_ec2_vpc_nacl_ec2_vpc_nacl_info.yaml @@ -0,0 +1,8 @@ +--- +major_changes: + - ec2_vpc_nacl - 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.ec2_vpc_nacl`` (https://github.com/ansible-collections/amazon.aws/pull/2339). + - ec2_vpc_nacl_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.ec2_vpc_nacl_info`` (https://github.com/ansible-collections/amazon.aws/pull/2339). diff --git a/meta/runtime.yml b/meta/runtime.yml index b2c9bc08ab5..1e3c53d502f 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -67,6 +67,8 @@ action_groups: - ec2_vpc_endpoint_service_info - ec2_vpc_igw - ec2_vpc_igw_info + - ec2_vpc_nacl + - ec2_vpc_nacl_info - ec2_vpc_nat_gateway - ec2_vpc_nat_gateway_info - ec2_vpc_net diff --git a/plugins/modules/ec2_vpc_nacl.py b/plugins/modules/ec2_vpc_nacl.py new file mode 100644 index 00000000000..7abe96d301f --- /dev/null +++ b/plugins/modules/ec2_vpc_nacl.py @@ -0,0 +1,536 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: 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 +short_description: create and delete Network ACLs +version_added: 1.0.0 +version_added_collection: community.aws +description: + - Read the AWS documentation for Network ACLS + U(https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_ACLs.html). +options: + name: + description: + - Tagged name identifying a network ACL. + - 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 O(name) or O(nacl_id) is required. + - Mutually exclusive with O(name). + required: false + type: str + vpc_id: + description: + - VPC id of the requesting VPC. + - Required when state present. + required: false + type: str + subnets: + description: + - The list of subnets that should be associated with the network ACL. + - Must be specified as a list + - Each subnet can be specified as subnet ID, or its tagged name. + required: false + type: list + elements: str + default: [] + egress: + description: + - A list of rules for outgoing traffic. Each rule must be specified as a list. + Each rule may contain the rule number (integer 1-32766), protocol (one of ['tcp', 'udp', 'icmp', 'ipv6-icmp', '-1', 'all']), + the rule action ('allow' or 'deny') the CIDR of the IPv4 or IPv6 network range to allow or deny, + the ICMP type (-1 means all types), the ICMP code (-1 means all codes), the last port in the range for + TCP or UDP protocols, and the first port in the range for TCP or UDP protocols. + See examples. + default: [] + required: false + type: list + elements: list + ingress: + description: + - List of rules for incoming traffic. Each rule must be specified as a list. + Each rule may contain the rule number (integer 1-32766), protocol (one of ['tcp', 'udp', 'icmp', 'ipv6-icmp', '-1', 'all']), + the rule action ('allow' or 'deny') the CIDR of the IPv4 or IPv6 network range to allow or deny, + the ICMP type (-1 means all types), the ICMP code (-1 means all codes), the last port in the range for + TCP or UDP protocols, and the first port in the range for TCP or UDP protocols. + See examples. + default: [] + required: false + type: list + elements: list + state: + description: + - Creates or modifies an existing NACL. + - Deletes a NACL and reassociates subnets to the default NACL. + required: false + type: str + choices: ['present', 'absent'] + default: present +author: + - Mike Mochan (@mmochan) +notes: + - Support for I(purge_tags) was added in release 4.0.0. +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 + - amazon.aws.tags +""" + +EXAMPLES = r""" +# Complete example to create and delete a network ACL +# that allows SSH, HTTP and ICMP in, and all traffic out. +- name: "Create and associate production DMZ network ACL with DMZ subnets" + amazon.aws.ec2_vpc_nacl: + vpc_id: vpc-12345678 + name: prod-dmz-nacl + region: ap-southeast-2 + subnets: ['prod-dmz-1', 'prod-dmz-2'] + tags: + CostCode: CC1234 + Project: phoenix + Description: production DMZ + ingress: + # rule no, protocol, allow/deny, cidr, icmp_type, icmp_code, + # port from, port to + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [205, 'tcp', 'allow', '::/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + - [305, 'ipv6-icmp', 'allow', '::/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + - [105, 'all', 'allow', '::/0', null, null, null, null] + state: 'present' + +- name: "Remove the ingress and egress rules - defaults to deny all" + amazon.aws.ec2_vpc_nacl: + vpc_id: vpc-12345678 + name: prod-dmz-nacl + region: ap-southeast-2 + subnets: + - prod-dmz-1 + - prod-dmz-2 + tags: + CostCode: CC1234 + Project: phoenix + Description: production DMZ + state: present + +- name: "Remove the NACL subnet associations and tags" + amazon.aws.ec2_vpc_nacl: + vpc_id: 'vpc-12345678' + name: prod-dmz-nacl + region: ap-southeast-2 + state: present + +- name: "Delete nacl and subnet associations" + amazon.aws.ec2_vpc_nacl: + vpc_id: vpc-12345678 + name: prod-dmz-nacl + state: absent + +- name: "Delete nacl by its id" + amazon.aws.ec2_vpc_nacl: + nacl_id: acl-33b4ee5b + state: absent +""" + +RETURN = r""" +nacl_id: + description: The id of the NACL (when creating or updating an ACL). + returned: success + type: str + sample: "acl-123456789abcdef01" +""" + +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.ec2 import replace_network_acl_association +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + +# VPC-supported IANA protocol numbers +# http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml +PROTOCOL_NUMBERS = {"all": -1, "icmp": 1, "tcp": 6, "udp": 17, "ipv6-icmp": 58} + + +# Utility methods +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_changed(client, module: AnsibleAWSModule, nacl_id: str, subnets_ids: List[str]) -> bool: + changed = False + vpc_id = module.params.get("vpc_id") + + 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 + + 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(client, module: AnsibleAWSModule, nacl_info: Dict[str, Any]) -> bool: + changed = False + 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(client, module: AnsibleAWSModule, nacl_id: str) -> bool: + tags = module.params.get("tags") + name = module.params.get("name") + purge_tags = module.params.get("purge_tags") + + if name is None and tags is None: + return False + + if module.params.get("tags") is None: + # Only purge tags if tags is explicitly set to {} and purge_tags is True + purge_tags = False + + new_tags = dict() + if module.params.get("name") is not None: + new_tags["Name"] = module.params.get("name") + new_tags.update(module.params.get("tags") or {}) + + return ensure_ec2_tags( + client, module, nacl_id, tags=new_tags, purge_tags=purge_tags, retry_codes=["InvalidNetworkAclID.NotFound"] + ) + + +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 + 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: str) -> bool: + return ":" in cidr + + +def process_rule_entry(entry: List[Any]) -> Dict[str, Any]: + params = {} + if is_ipv6(entry[3]): + params["Ipv6CidrBlock"] = entry[3] + else: + params["CidrBlock"] = entry[3] + if icmp_present(entry): + params["IcmpTypeCode"] = {"Type": int(entry[4]), "Code": int(entry[5])} + else: + if entry[6] or entry[7]: + params["PortRange"] = {"From": entry[6], "To": entry[7]} + + return params + + +def add_network_acl_entries( + client, nacl_id: str, ansible_entries: List[List[str]], egress: bool, check_mode: bool +) -> bool: + changed = False + for entry in ansible_entries: + changed = True + 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 associate_nacl_to_subnets(client, module: AnsibleAWSModule, nacl_id: str, subnets_ids: List[str]) -> bool: + changed = False + 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 + if not module.check_mode: + replace_network_acl_association(client, network_acl_id=nacl_id, association_id=association_id) + return changed + + +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) + + if not nacl: + if module.check_mode: + module.exit_json(changed=True, msg="Would have created Network ACL if not in check mode.") + + # 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) + + # 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 ensure_absent(client, module: AnsibleAWSModule) -> None: + changed = False + result = {} + nacl = describe_network_acl(client, module) + if not nacl: + module.exit_json(changed=changed) + + 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") + + # Replace Network ACL association + for assoc_id in assoc_ids: + changed = True + if not module.check_mode: + 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.") + + 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: AnsibleAWSModule) -> Optional[Dict[str, Any]]: + nacl_id = module.params.get("nacl_id") + name = module.params.get("name") + + if nacl_id: + filters = [{"Name": "network-acl-id", "Values": [nacl_id]}] + else: + 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 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 find_subnets_ids(client, module: AnsibleAWSModule, subnets_ids_or_names: List[str]) -> List[str]: + subnets_ids = [] + subnets_names = [] + + # 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"] + + 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(): + argument_spec = dict( + vpc_id=dict(), + name=dict(), + nacl_id=dict(), + 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, + ) + + client = module.client("ec2") + + 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__": + main() diff --git a/plugins/modules/ec2_vpc_nacl_info.py b/plugins/modules/ec2_vpc_nacl_info.py new file mode 100644 index 00000000000..88bef9360d0 --- /dev/null +++ b/plugins/modules/ec2_vpc_nacl_info.py @@ -0,0 +1,231 @@ +#!/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 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 = [] + + 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/aliases b/tests/integration/targets/ec2_vpc_nacl/aliases new file mode 100644 index 00000000000..04109c2687b --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/aliases @@ -0,0 +1,3 @@ +cloud/aws + +ec2_vpc_nacl_info diff --git a/tests/integration/targets/ec2_vpc_nacl/defaults/main.yml b/tests/integration/targets/ec2_vpc_nacl/defaults/main.yml new file mode 100644 index 00000000000..5ac931209fb --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/defaults/main.yml @@ -0,0 +1,12 @@ +--- +vpc_name: '{{ resource_prefix }}-ec2-vpc-nacl' +nacl_name: '{{ resource_prefix }}-ec2-vpc-nacl' +subnet_name: '{{ resource_prefix }}-ec2-vpc-nacl' +vpc_cidr: '10.{{ 256 | random(seed=resource_prefix) }}.0.0/16' +subnet_1: '10.{{ 256 | random(seed=resource_prefix) }}.1.0/24' +subnet_2: '10.{{ 256 | random(seed=resource_prefix) }}.2.0/24' +subnet_3: '10.{{ 256 | random(seed=resource_prefix) }}.3.0/24' +subnet_4: '10.{{ 256 | random(seed=resource_prefix) }}.4.0/24' + +vpc_ipv6_cidr: '10.{{ 256 | random(seed=resource_prefix) }}.5.0/25' +vpc_ipv6_name: '{{ vpc_name }}-ipv6' diff --git a/tests/integration/targets/ec2_vpc_nacl/meta/main.yml b/tests/integration/targets/ec2_vpc_nacl/meta/main.yml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] 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 new file mode 100644 index 00000000000..584a9bcec0f --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/ingress_and_egress.yml @@ -0,0 +1,159 @@ +# ============================================================ +- name: Test Ingress and Egress rules + block: + - name: Create ingress and egress rules using subnet IDs + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + + - name: Assert the network acl was created + ansible.builtin.assert: + that: + - nacl.changed + - nacl.nacl_id.startswith('acl-') + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_facts + + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: + that: + - nacl_facts.nacls | length == 1 + - nacl_facts.nacls[0].ingress | length == 3 + - nacl_facts.nacls[0].egress | length == 1 + + # ============================================================ + + - name: Remove an ingress rule + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + + - name: Assert the network acl changed + ansible.builtin.assert: + that: + - nacl.changed + - nacl.nacl_id.startswith('acl-') + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_facts + + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: + that: + - nacl_facts.nacls | length == 1 + - nacl_facts.nacls[0].ingress | length == 2 + - nacl_facts.nacls[0].egress | length == 1 + + # ============================================================ + + - name: Remove the egress rule + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + egress: [] + state: 'present' + register: nacl + + - name: Assert the network acl changed + ansible.builtin.assert: + that: + - nacl.changed + - nacl.nacl_id.startswith('acl-') + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_facts + + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: + that: + - nacl_facts.nacls | length == 1 + - nacl_facts.nacls[0].ingress | length == 2 + - nacl_facts.nacls[0].egress | length == 0 + + # ============================================================ + + - name: Add egress rules + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + egress: + - [100, 'tcp', 'allow', '10.0.0.0/24', null, null, 22, 22] + - [200, 'udp', 'allow', '10.0.0.0/24', null, null, 22, 22] + state: 'present' + register: nacl + + - name: Assert the network acl changed + ansible.builtin.assert: + that: + - nacl.changed + - nacl.nacl_id.startswith('acl-') + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_facts + + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: + that: + - nacl_facts.nacls | length == 1 + - nacl_facts.nacls[0].ingress | length == 2 + - nacl_facts.nacls[0].egress | length == 2 + + # ============================================================ + + - name: Remove the network ACL + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + state: absent + register: nacl + + - 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 new file mode 100644 index 00000000000..5ac3819723e --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/ipv6.yml @@ -0,0 +1,131 @@ +- name: Test using IPv6 + block: + + # ============================================================ + + - name: Create ingress and egress rules using subnet names + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_ipv6_id }}" + name: "{{ nacl_name }}" + subnets: + - "{{ subnet_name }}-ipv6" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + + - name: Assert that module returned the Network ACL id + ansible.builtin.assert: + that: + - nacl.nacl_id + + - name: Set fact for Network ACL ID + ansible.builtin.set_fact: + nacl_id: "{{ nacl.nacl_id }}" + + - name: Add ipv6 entries + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_ipv6_id }}" + name: "{{ nacl_name }}" + subnets: + - "{{ subnet_name }}-ipv6" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [205, 'tcp', 'allow', '::/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + - [305, 'ipv6-icmp', 'allow', '::/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + - [105, 'all', 'allow', '::/0', null, null, null, null] + state: 'present' + register: nacl + + - 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) + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl_id }}" + register: nacl_facts + + - 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 + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_ipv6_id }}" + name: "{{ nacl_name }}" + subnets: + - "{{ subnet_name }}-ipv6" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: [] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + - [105, 'all', 'allow', '::/0', null, null, null, null] + state: 'present' + register: nacl + + - 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 + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_ipv6_id }}" + name: "{{ nacl_name }}" + subnets: + - "{{ subnet_name }}-ipv6" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: [] + egress: [] + state: 'present' + register: nacl + + - name: Assert that module reported change + ansible.builtin.assert: + that: + - nacl.changed + + - name: Get network ACL facts (test that removed entries are gone) + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl_id }}" + register: nacl_facts + + - name: Assert the nacl has the correct attributes + ansible.builtin.assert: + that: + - nacl_facts.nacls | length == 1 + - nacl_facts.nacls[0].ingress | length == 0 + - nacl_facts.nacls[0].egress | length == 0 + + always: + + - name: Remove network ACL + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_ipv6_id }}" + name: "{{ nacl_name }}" + state: absent + register: removed_acl + 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 new file mode 100644 index 00000000000..0225056152b --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/main.yml @@ -0,0 +1,174 @@ +--- +- 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 + 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 + assert: + that: + - nacl_facts is failed + - '"does not exist" in nacl_facts.msg' + + # ============================================================ + + - 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 }}' + + # ============================================================ diff --git a/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_ids.yml b/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_ids.yml new file mode 100644 index 00000000000..5aaed181bef --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_ids.yml @@ -0,0 +1,163 @@ +# ============================================================ + +- name: Create ingress and egress rules using subnet IDs + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + +- name: Set helpful fact for Network ACL ID + ansible.builtin.set_fact: + nacl_id: "{{ nacl.nacl_id }}" + +- name: Assert the network acl was created + ansible.builtin.assert: + that: + - nacl.changed + - nacl.nacl_id.startswith('acl-') + +- name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl_id }}" + register: nacl_facts + +- 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 + - nacl_facts.nacls[0].subnets | length == 4 + - nacl_facts.nacls[0].subnets | sort == subnet_ids | sort + - nacl_facts.nacls[0].ingress | length == 3 + - nacl_facts.nacls[0].egress | length == 1 + - nacl_facts.nacls[0].tags.Name == nacl_name + +# ============================================================ + +- name: Test idempotence + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + +- 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 + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_facts_idem + +- 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 + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: + - "{{ subnet_ids[0] }}" + - "{{ subnet_ids[1] }}" + - "{{ subnet_ids[2] }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + +- 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 + amazon.aws.ec2_vpc_nacl_info: + nacl_id: + - "{{ nacl.nacl_id }}" + register: nacl_facts + +- 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 + - nacl_facts.nacls[0].subnets | length == 3 + - subnet_ids[3] not in nacl_facts.nacls[0].subnets + - nacl_facts.nacls[0].ingress | length == 3 + - nacl_facts.nacls[0].egress | length == 1 + - nacl_facts.nacls[0].tags.Name == nacl_name + +# ============================================================ + +- name: Remove the network ACL + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + state: absent + register: nacl + +- name: Assert nacl was removed + ansible.builtin.assert: + that: + - nacl.changed + +- name: Re-remove the network ACL by name (test idempotency) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + state: absent + register: nacl + +- name: Assert nacl was removed + ansible.builtin.assert: + that: + - nacl is not changed + +- name: Re-remove the network ACL by id (test idempotency) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + nacl_id: "{{ nacl_id }}" + state: absent + register: nacl + +- 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 new file mode 100644 index 00000000000..78831afface --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/subnet_names.yml @@ -0,0 +1,137 @@ +# ============================================================ + +- name: Create ingress and egress rules using subnet names + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_names }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + +- name: Set helpful fact for Network ACL ID + ansible.builtin.set_fact: + nacl_id: "{{ nacl.nacl_id }}" + +- name: Assert the network acl was created + ansible.builtin.assert: + that: + - nacl.changed + - nacl.nacl_id.startswith('acl-') + +- name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl_id }}" + register: nacl_facts + +- 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 + - nacl_facts.nacls[0].subnets | length == 4 + - nacl_facts.nacls[0].ingress | length == 3 + - nacl_facts.nacls[0].egress | length == 1 + - nacl_facts.nacls[0].tags.Name == nacl_name + +# ============================================================ + +- name: Test idempotence + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_names }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + +- 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 + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_facts_idem + +- 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 + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: + - "{{ subnet_names[0] }}" + - "{{ subnet_names[1] }}" + - "{{ subnet_names[2] }}" + tags: + Created_by: "Ansible test {{ resource_prefix }}" + ingress: + - [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22] + - [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80] + - [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8] + egress: + - [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + state: 'present' + register: nacl + +- 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 + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_facts + +- 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 + - nacl_facts.nacls[0].subnets | length == 3 + - nacl_facts.nacls[0].ingress | length == 3 + - nacl_facts.nacls[0].egress | length == 1 + - nacl_facts.nacls[0].tags.Name == nacl_name + +# ============================================================ + +- name: Remove the network ACL + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + state: absent + register: nacl + +- 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 new file mode 100644 index 00000000000..556ab45494a --- /dev/null +++ b/tests/integration/targets/ec2_vpc_nacl/tasks/tags.yml @@ -0,0 +1,455 @@ +- name: Run test from tags.yml + vars: + first_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + second_tags: + 'New Key with Spaces': Value with spaces + NewCamelCaseKey: CamelCaseValue + newPascalCaseKey: pascalCaseValue + new_snake_case_key: snake_case_value + third_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + 'New Key with Spaces': Updated Value with spaces + final_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + 'New Key with Spaces': Updated Value with spaces + NewCamelCaseKey: CamelCaseValue + newPascalCaseKey: pascalCaseValue + new_snake_case_key: snake_case_value + name_tags: + Name: '{{ nacl_name }}' + block: + + # ============================================================ + + - name: Create a network ACL using subnet IDs + amazon.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 + ansible.builtin.assert: + that: + - nacl.changed + - nacl.nacl_id.startswith('acl-') + + - name: Store NACL ID + ansible.builtin.set_fact: + nacl_id: '{{ nacl.nacl_id }}' + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl_id }}" + register: nacl_info + + - 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: Add tags (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ first_tags }}" + state: 'present' + register: nacl + check_mode: True + + - name: Assert would change + ansible.builtin.assert: + that: + - nacl is changed + - nacl.nacl_id == nacl_id + + - name: Add tags + amazon.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 + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - 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: Add tags - IDEMPOTENCY (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ first_tags }}" + state: 'present' + register: nacl + check_mode: True + + - name: Assert would not change + ansible.builtin.assert: + that: + - nacl is not changed + - nacl.nacl_id == nacl_id + + - name: Add tags - IDEMPOTENCY + amazon.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 + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - name: Verify no change + ansible.builtin.assert: + that: + - nacl is not 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: Get network ACL facts by filter + amazon.aws.ec2_vpc_nacl_info: + filters: + "tag:Name": "{{ nacl_name }}" + register: nacl_info + + - name: Assert the facts are the same as before + ansible.builtin.assert: + that: + - nacl_info.nacls | length == 1 + - nacl.nacl_id == nacl_id + - nacl_info.nacls[0].nacl_id == nacl_id + + # ============================================================ + + - name: Modify tags with purge (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ second_tags }}" + state: 'present' + register: nacl + check_mode: True + + - name: Assert would change + ansible.builtin.assert: + that: + - nacl is changed + - nacl.nacl_id == nacl_id + + - name: Modify tags with purge + amazon.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 + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - 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: Modify tags with purge - IDEMPOTENCY (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ second_tags }}" + state: 'present' + register: nacl + check_mode: True + + - name: Assert would not change + ansible.builtin.assert: + that: + - nacl is not changed + - nacl.nacl_id == nacl_id + + - name: Modify tags with purge - IDEMPOTENCY + amazon.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 + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - name: Verify no change + ansible.builtin.assert: + that: + - nacl is not 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: Modify tags without purge (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ third_tags }}" + state: 'present' + purge_tags: False + register: nacl + check_mode: True + + - name: Assert would change + ansible.builtin.assert: + that: + - nacl is changed + - nacl.nacl_id == nacl_id + + - name: Modify tags without purge + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ third_tags }}" + state: 'present' + purge_tags: False + register: nacl + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - 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: Modify tags without purge - IDEMPOTENCY (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ third_tags }}" + state: 'present' + purge_tags: False + register: nacl + check_mode: True + + - name: Assert would not change + ansible.builtin.assert: + that: + - nacl is not changed + - nacl.nacl_id == nacl_id + + - name: Modify tags without purge - IDEMPOTENCY + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: "{{ third_tags }}" + state: 'present' + purge_tags: False + register: nacl + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - name: Verify no change + ansible.builtin.assert: + that: + - nacl is not 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: No change to tags without setting tags (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + state: 'present' + register: nacl + check_mode: True + + - 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 + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + state: 'present' + register: nacl + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - name: Verify the tags were added + ansible.builtin.assert: + that: + - nacl is not 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: Remove non name tags (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: {} + state: 'present' + register: nacl + check_mode: True + + - name: Assert would change + ansible.builtin.assert: + that: + - nacl is changed + - nacl.nacl_id == nacl_id + + - name: Remove non name tags + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: {} + state: 'present' + register: nacl + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - 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: Remove non name tags - IDEMPOTENCY (check mode) + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: {} + state: 'present' + register: nacl + check_mode: True + + - name: Assert would not change + ansible.builtin.assert: + that: + - nacl is not changed + - nacl.nacl_id == nacl_id + + - name: Remove non name tags - IDEMPOTENCY + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + subnets: "{{ subnet_ids }}" + tags: {} + state: 'present' + register: nacl + + - name: Get network ACL facts + amazon.aws.ec2_vpc_nacl_info: + nacl_ids: + - "{{ nacl.nacl_id }}" + register: nacl_info + + - name: Verify no change + ansible.builtin.assert: + that: + - nacl is not changed + - nacl.nacl_id == nacl_id + - nacl_info.nacls[0].nacl_id == nacl_id + - nacl_info.nacls[0].tags == name_tags + + # ============================================================ + + always: + - name: Remove the network ACL + amazon.aws.ec2_vpc_nacl: + vpc_id: "{{ vpc_id }}" + name: "{{ nacl_name }}" + state: absent + register: nacl + + - name: Assert nacl was removed + ansible.builtin.assert: + that: + - nacl.changed