diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 6033387cf..cc8535e84 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -677,3 +677,21 @@ class InvalidTaggableResourceType(EC2ClientError): resource_type ), ) + + +class GenericInvalidParameterValueError(EC2ClientError): + def __init__(self, attribute, value): + super(GenericInvalidParameterValueError, self).__init__( + "InvalidParameterValue", + "invalid value for parameter {0}: {1}".format(attribute, value), + ) + + +class InvalidSubnetCidrBlockAssociationID(EC2ClientError): + def __init__(self, association_id): + super(InvalidSubnetCidrBlockAssociationID, self).__init__( + "InvalidSubnetCidrBlockAssociationID.NotFound", + "The subnet CIDR block with association ID '{0}' does not exist".format( + association_id + ), + ) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 509c2ad7c..c2fa8480c 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -38,6 +38,7 @@ from os import listdir from .exceptions import ( CidrLimitExceeded, + GenericInvalidParameterValueError, UnsupportedTenancy, DependencyViolationError, EC2ClientError, @@ -95,6 +96,7 @@ from .exceptions import ( InvalidVPCRangeError, InvalidVpnGatewayIdError, InvalidVpnConnectionIdError, + InvalidSubnetCidrBlockAssociationID, MalformedAMIIdError, MalformedDHCPOptionsIdError, MissingParameterError, @@ -172,6 +174,7 @@ from .utils import ( random_vpn_gateway_id, random_vpn_connection_id, random_customer_gateway_id, + random_subnet_ipv6_cidr_block_association_id, is_tag_filter, tag_filter_matches, rsa_public_key_parse, @@ -284,6 +287,7 @@ class NetworkInterface(TaggedEC2Resource, CloudFormationModel): self.instance = None self.attachment_id = None self.description = description + self.source_dest_check = True self.public_ip = None self.public_ip_auto_assign = public_ip_auto_assign @@ -478,10 +482,14 @@ class NetworkInterfaceBackend(object): found_eni.instance.detach_eni(found_eni) - def modify_network_interface_attribute(self, eni_id, group_ids): + def modify_network_interface_attribute( + self, eni_id, group_ids, source_dest_check=None + ): eni = self.get_network_interface(eni_id) groups = [self.get_security_group_from_id(group_id) for group_id in group_ids] eni._group_set = groups + if source_dest_check: + eni.source_dest_check = source_dest_check def get_all_network_interfaces(self, eni_ids=None, filters=None): enis = self.enis.values() @@ -3633,6 +3641,7 @@ class Subnet(TaggedEC2Resource, CloudFormationModel): subnet_id, vpc_id, cidr_block, + ipv6_cidr_block, availability_zone, default_for_az, map_public_ip_on_launch, @@ -3650,7 +3659,9 @@ class Subnet(TaggedEC2Resource, CloudFormationModel): self.default_for_az = default_for_az self.map_public_ip_on_launch = map_public_ip_on_launch self.assign_ipv6_address_on_creation = assign_ipv6_address_on_creation - self.ipv6_cidr_block_associations = [] + self.ipv6_cidr_block_associations = {} + if ipv6_cidr_block: + self.attach_ipv6_cidr_block_associations(ipv6_cidr_block) # Theory is we assign ip's as we go (as 16,777,214 usable IPs in a /8) self._subnet_ip_generator = self.cidr.hosts() @@ -3802,6 +3813,22 @@ class Subnet(TaggedEC2Resource, CloudFormationModel): except KeyError: pass # Unknown IP + def attach_ipv6_cidr_block_associations(self, ipv6_cidr_block): + association = { + "associationId": random_subnet_ipv6_cidr_block_association_id(), + "ipv6CidrBlock": ipv6_cidr_block, + "ipv6CidrBlockState": "associated", + } + self.ipv6_cidr_block_associations[ + association.get("associationId") + ] = association + return association + + def detach_subnet_cidr_block(self, association_id): + association = self.ipv6_cidr_block_associations.get(association_id) + association["ipv6CidrBlockState"] = "disassociated" + return association + class SubnetBackend(object): def __init__(self): @@ -3819,6 +3846,7 @@ class SubnetBackend(object): self, vpc_id, cidr_block, + ipv6_cidr_block=None, availability_zone=None, availability_zone_id=None, context=None, @@ -3852,6 +3880,10 @@ class SubnetBackend(object): if not subnet_in_vpc_cidr_range: raise InvalidSubnetRangeError(cidr_block) + # The subnet size must use a /64 prefix length. + if ipv6_cidr_block and "::/64" not in ipv6_cidr_block: + raise GenericInvalidParameterValueError("ipv6-cidr-block", ipv6_cidr_block) + for subnet in self.get_all_subnets(filters={"vpc-id": vpc_id}): if subnet.cidr.overlaps(subnet_cidr_block): raise InvalidSubnetConflictError(cidr_block) @@ -3895,6 +3927,7 @@ class SubnetBackend(object): subnet_id, vpc_id, cidr_block, + ipv6_cidr_block, availability_zone_data, default_for_az, map_public_ip_on_launch, @@ -3937,6 +3970,27 @@ class SubnetBackend(object): else: raise InvalidParameterValueError(attr_name) + def get_subnet_from_ipv6_association(self, association_id): + subnet = None + for s in self.get_all_subnets(): + if association_id in s.ipv6_cidr_block_associations: + subnet = s + return subnet + + def associate_subnet_cidr_block(self, subnet_id, ipv6_cidr_block): + subnet = self.get_subnet(subnet_id) + if not subnet: + raise InvalidSubnetIdError(subnet_id) + association = subnet.attach_ipv6_cidr_block_associations(ipv6_cidr_block) + return association + + def disassociate_subnet_cidr_block(self, association_id): + subnet = self.get_subnet_from_ipv6_association(association_id) + if not subnet: + raise InvalidSubnetCidrBlockAssociationID(association_id) + association = subnet.detach_subnet_cidr_block(association_id) + return subnet.id, association + class FlowLogs(TaggedEC2Resource, CloudFormationModel): def __init__( @@ -4750,12 +4804,13 @@ class RouteBackend(object): nat_gateway = None transit_gateway = None egress_only_igw = None + interface = None route_table = self.get_route_table(route_table_id) if interface_id: # for validating interface Id whether it is valid or not. - self.get_network_interface(interface_id) + interface = self.get_network_interface(interface_id) else: if gateway_id: @@ -4787,7 +4842,7 @@ class RouteBackend(object): nat_gateway=nat_gateway, egress_only_igw=egress_only_igw, transit_gateway=transit_gateway, - interface=None, + interface=interface, vpc_pcx=self.get_vpc_peering_connection(vpc_peering_connection_id) if vpc_peering_connection_id else None, diff --git a/moto/ec2/responses/elastic_network_interfaces.py b/moto/ec2/responses/elastic_network_interfaces.py index a46b4858c..f4dce86c7 100644 --- a/moto/ec2/responses/elastic_network_interfaces.py +++ b/moto/ec2/responses/elastic_network_interfaces.py @@ -66,8 +66,11 @@ class ElasticNetworkInterfaces(BaseResponse): def modify_network_interface_attribute(self): eni_id = self._get_param("NetworkInterfaceId") group_ids = self._get_multi_param("SecurityGroupId") + source_dest_check = self._get_param("SourceDestCheck") if self.is_not_dryrun("ModifyNetworkInterface"): - self.ec2_backend.modify_network_interface_attribute(eni_id, group_ids) + self.ec2_backend.modify_network_interface_attribute( + eni_id, group_ids, source_dest_check + ) return MODIFY_NETWORK_INTERFACE_ATTRIBUTE_RESPONSE def reset_network_interface_attribute(self): @@ -97,9 +100,7 @@ CREATE_NETWORK_INTERFACE_RESPONSE = """ {% if eni.private_ip_address %} {{ eni.private_ip_address }} {% endif %} - {% if eni.instance %} - {{ eni.instance.source_dest_check }} - {% endif %} + {{ eni.source_dest_check }} {% for group in eni.group_set %} @@ -132,7 +133,7 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """{{ eni.subnet.id }} {{ eni.subnet.vpc_id }} us-west-2a - {{ eni.description }} + {{ eni.description or "" }} 190610284047 false {% if eni.attachment_id %} @@ -145,9 +146,7 @@ DESCRIBE_NETWORK_INTERFACES_RESPONSE = """{{ eni.private_ip_address }} {% endif %} ip-10-0-0-134.us-west-2.compute.internal - {% if eni.instance %} - {{ eni.instance.source_dest_check }} - {% endif %} + {{ eni.source_dest_check }} {% for group in eni.group_set %} diff --git a/moto/ec2/responses/route_tables.py b/moto/ec2/responses/route_tables.py index 14c733445..137bc92e1 100644 --- a/moto/ec2/responses/route_tables.py +++ b/moto/ec2/responses/route_tables.py @@ -210,6 +210,10 @@ DESCRIBE_ROUTE_TABLES_RESPONSE = """ {{ route.transit_gateway.id }} active {% endif %} + {% if route.interface %} + {{ route.interface.id }} + active + {% endif %} {% endfor %} diff --git a/moto/ec2/responses/subnets.py b/moto/ec2/responses/subnets.py index f18aaf2ec..5698e1340 100644 --- a/moto/ec2/responses/subnets.py +++ b/moto/ec2/responses/subnets.py @@ -9,6 +9,7 @@ class Subnets(BaseResponse): def create_subnet(self): vpc_id = self._get_param("VpcId") cidr_block = self._get_param("CidrBlock") + ipv6_cidr_block = self._get_param("Ipv6CidrBlock") availability_zone = self._get_param("AvailabilityZone") availability_zone_id = self._get_param("AvailabilityZoneId") tags = self._get_multi_param("TagSpecification") @@ -22,6 +23,7 @@ class Subnets(BaseResponse): subnet = self.ec2_backend.create_subnet( vpc_id, cidr_block, + ipv6_cidr_block, availability_zone, availability_zone_id, context=self, @@ -55,6 +57,26 @@ class Subnets(BaseResponse): ) return MODIFY_SUBNET_ATTRIBUTE_RESPONSE + def associate_subnet_cidr_block(self): + ipv6_cidr_block = self._get_param("Ipv6CidrBlock") + subnet_id = self._get_param("SubnetId") + + association = self.ec2_backend.associate_subnet_cidr_block( + subnet_id, ipv6_cidr_block + ) + template = self.response_template(ASSOCIATE_SUBNET_CIDR_BLOCK_RESPONSE) + return template.render(subnet_id=subnet_id, association=association) + + def disassociate_subnet_cidr_block(self): + association_id = self._get_param("AssociationId") + + subnet_id, association = self.ec2_backend.disassociate_subnet_cidr_block( + association_id + ) + template = self.response_template(DISASSOCIATE_SUBNET_CIDR_BLOCK_RESPONSE) + result = template.render(subnet_id=subnet_id, association=association) + return result + CREATE_SUBNET_RESPONSE = """ @@ -71,7 +93,19 @@ CREATE_SUBNET_RESPONSE = """ {{ subnet.map_public_ip_on_launch }} {{ subnet.owner_id }} {{ 'false' if not subnet.assign_ipv6_address_on_creation or subnet.assign_ipv6_address_on_creation == 'false' else 'true'}} - {{ subnet.ipv6_cidr_block_associations }} + + {% for ipv6_association in subnet.ipv6_cidr_block_associations.values() %} + {% if ipv6_association.ipv6CidrBlockState == "associated" %} + + {{ ipv6_association.ipv6CidrBlock }} + {{ ipv6_association.associationId }} + + {{ ipv6_association.ipv6CidrBlockState }} + + + {% endif %} + {% endfor %} + arn:aws:ec2:{{ subnet._availability_zone.name[0:-1] }}:{{ subnet.owner_id }}:subnet/{{ subnet.id }} {% for tag in subnet.get_tags() %} @@ -109,7 +143,19 @@ DESCRIBE_SUBNETS_RESPONSE = """ {{ subnet.map_public_ip_on_launch }} {{ subnet.owner_id }} {{ 'false' if not subnet.assign_ipv6_address_on_creation or subnet.assign_ipv6_address_on_creation == 'false' else 'true'}} - {{ subnet.ipv6_cidr_block_associations }} + + {% for ipv6_association in subnet.ipv6_cidr_block_associations.values() %} + {% if ipv6_association.ipv6CidrBlockState == "associated" %} + + {{ ipv6_association.ipv6CidrBlock }} + {{ ipv6_association.associationId }} + + {{ ipv6_association.ipv6CidrBlockState }} + + + {% endif %} + {% endfor %} + arn:aws:ec2:{{ subnet._availability_zone.name[0:-1] }}:{{ subnet.owner_id }}:subnet/{{ subnet.id }} {% if subnet.get_tags() %} @@ -133,3 +179,31 @@ MODIFY_SUBNET_ATTRIBUTE_RESPONSE = """ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE true """ + +ASSOCIATE_SUBNET_CIDR_BLOCK_RESPONSE = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ subnet_id }} + + {{ association.ipv6CidrBlock }} + + {{ association.ipv6CidrBlockState }} + + {{ association.associationId }} + + +""" + +DISASSOCIATE_SUBNET_CIDR_BLOCK_RESPONSE = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ subnet_id }} + + {{ association.ipv6CidrBlock }} + + {{ association.ipv6CidrBlockState }} + + {{ association.associationId }} + + +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 5c0835bd5..72b2fce35 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -39,6 +39,7 @@ EC2_RESOURCE_TO_PREFIX = { "spot-instance-request": "sir", "spot-fleet-request": "sfr", "subnet": "subnet", + "subnet-ipv6-cidr-block-association": "subnet-cidr-assoc", "reservation": "r", "volume": "vol", "vpc": "vpc", @@ -107,6 +108,12 @@ def random_subnet_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX["subnet"]) +def random_subnet_ipv6_cidr_block_association_id(): + return random_id( + prefix=EC2_RESOURCE_TO_PREFIX["subnet-ipv6-cidr-block-association"] + ) + + def random_subnet_association_id(): return random_id(prefix=EC2_RESOURCE_TO_PREFIX["route-table-association"]) diff --git a/tests/terraform-tests.success.txt b/tests/terraform-tests.success.txt index 82172fd3c..be35edeb4 100644 --- a/tests/terraform-tests.success.txt +++ b/tests/terraform-tests.success.txt @@ -88,6 +88,7 @@ TestAccAWSRouteTable_IPv4_To_NatGateway TestAccAWSRouteTable_IPv4_To_TransitGateway TestAccAWSRouteTable_IPv4_To_VpcPeeringConnection TestAccAWSRouteTable_IPv6_To_EgressOnlyInternetGateway +TestAccAWSRouteTable_IPv6_To_NetworkInterface_Unattached TestAccAWSRouteTable_disappears TestAccAWSRouteTable_basic TestAccAWSSsmDocumentDataSource