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