diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index efbbeb6fe..ccc7c7a37 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -214,6 +214,7 @@ class NetworkInterface(TaggedEC2Resource):
ec2_backend,
subnet,
private_ip_address,
+ private_ip_addresses=None,
device_index=0,
public_ip_auto_assign=True,
group_ids=None,
@@ -223,6 +224,7 @@ class NetworkInterface(TaggedEC2Resource):
self.id = random_eni_id()
self.device_index = device_index
self.private_ip_address = private_ip_address or random_private_ip()
+ self.private_ip_addresses = private_ip_addresses
self.subnet = subnet
self.instance = None
self.attachment_id = None
@@ -341,12 +343,19 @@ class NetworkInterfaceBackend(object):
super(NetworkInterfaceBackend, self).__init__()
def create_network_interface(
- self, subnet, private_ip_address, group_ids=None, description=None, **kwargs
+ self,
+ subnet,
+ private_ip_address,
+ private_ip_addresses=None,
+ group_ids=None,
+ description=None,
+ **kwargs
):
eni = NetworkInterface(
self,
subnet,
private_ip_address,
+ private_ip_addresses,
group_ids=group_ids,
description=description,
**kwargs
@@ -2819,6 +2828,9 @@ class Subnet(TaggedEC2Resource):
self.vpc_id = vpc_id
self.cidr_block = cidr_block
self.cidr = ipaddress.IPv4Network(six.text_type(self.cidr_block), strict=False)
+ self._available_ip_addresses = (
+ ipaddress.IPv4Network(six.text_type(self.cidr_block)).num_addresses - 5
+ )
self._availability_zone = availability_zone
self.default_for_az = default_for_az
self.map_public_ip_on_launch = map_public_ip_on_launch
@@ -2854,6 +2866,21 @@ class Subnet(TaggedEC2Resource):
return subnet
+ @property
+ def available_ip_addresses(self):
+ enis = [
+ eni
+ for eni in self.ec2_backend.get_all_network_interfaces()
+ if eni.subnet.id == self.id
+ ]
+ addresses_taken = [
+ eni.private_ip_address for eni in enis if eni.private_ip_address
+ ]
+ for eni in enis:
+ if eni.private_ip_addresses:
+ addresses_taken.extend(eni.private_ip_addresses)
+ return str(self._available_ip_addresses - len(addresses_taken))
+
@property
def availability_zone(self):
return self._availability_zone.name
diff --git a/moto/ec2/responses/elastic_network_interfaces.py b/moto/ec2/responses/elastic_network_interfaces.py
index fa014b219..6761b294e 100644
--- a/moto/ec2/responses/elastic_network_interfaces.py
+++ b/moto/ec2/responses/elastic_network_interfaces.py
@@ -7,12 +7,13 @@ class ElasticNetworkInterfaces(BaseResponse):
def create_network_interface(self):
subnet_id = self._get_param("SubnetId")
private_ip_address = self._get_param("PrivateIpAddress")
+ private_ip_addresses = self._get_multi_param("PrivateIpAddresses")
groups = self._get_multi_param("SecurityGroupId")
subnet = self.ec2_backend.get_subnet(subnet_id)
description = self._get_param("Description")
if self.is_not_dryrun("CreateNetworkInterface"):
eni = self.ec2_backend.create_network_interface(
- subnet, private_ip_address, groups, description
+ subnet, private_ip_address, private_ip_addresses, groups, description
)
template = self.response_template(CREATE_NETWORK_INTERFACE_RESPONSE)
return template.render(eni=eni)
diff --git a/moto/ec2/responses/subnets.py b/moto/ec2/responses/subnets.py
index c42583f23..e11984e52 100644
--- a/moto/ec2/responses/subnets.py
+++ b/moto/ec2/responses/subnets.py
@@ -53,7 +53,7 @@ CREATE_SUBNET_RESPONSE = """
pending
{{ subnet.vpc_id }}
{{ subnet.cidr_block }}
- 251
+ {{ subnet.available_ip_addresses }}
{{ subnet._availability_zone.name }}
{{ subnet._availability_zone.zone_id }}
{{ subnet.default_for_az }}
@@ -81,7 +81,7 @@ DESCRIBE_SUBNETS_RESPONSE = """
available
{{ subnet.vpc_id }}
{{ subnet.cidr_block }}
- 251
+ {{ subnet.available_ip_addresses }}
{{ subnet._availability_zone.name }}
{{ subnet._availability_zone.zone_id }}
{{ subnet.default_for_az }}
diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py
index f5f1af433..7bb57aab4 100644
--- a/tests/test_ec2/test_subnets.py
+++ b/tests/test_ec2/test_subnets.py
@@ -11,6 +11,7 @@ from boto.exception import EC2ResponseError
from botocore.exceptions import ParamValidationError, ClientError
import json
import sure # noqa
+import random
from moto import mock_cloudformation_deprecated, mock_ec2, mock_ec2_deprecated
@@ -474,3 +475,127 @@ def test_create_subnets_with_overlapping_cidr_blocks():
subnet_cidr_block
)
)
+
+
+@mock_ec2
+def test_available_ip_addresses_in_subnet():
+ ec2 = boto3.resource("ec2", region_name="us-west-1")
+ client = boto3.client("ec2", region_name="us-west-1")
+
+ vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
+ cidr_range_addresses = [
+ ("10.0.0.0/16", 65531),
+ ("10.0.0.0/17", 32763),
+ ("10.0.0.0/18", 16379),
+ ("10.0.0.0/19", 8187),
+ ("10.0.0.0/20", 4091),
+ ("10.0.0.0/21", 2043),
+ ("10.0.0.0/22", 1019),
+ ("10.0.0.0/23", 507),
+ ("10.0.0.0/24", 251),
+ ("10.0.0.0/25", 123),
+ ("10.0.0.0/26", 59),
+ ("10.0.0.0/27", 27),
+ ("10.0.0.0/28", 11),
+ ]
+ for (cidr, expected_count) in cidr_range_addresses:
+ validate_subnet_details(client, vpc, cidr, expected_count)
+
+
+@mock_ec2
+def test_available_ip_addresses_in_subnet_with_enis():
+ ec2 = boto3.resource("ec2", region_name="us-west-1")
+ client = boto3.client("ec2", region_name="us-west-1")
+
+ vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
+ # Verify behaviour for various CIDR ranges (...)
+ # Don't try to assign ENIs to /27 and /28, as there are not a lot of IP addresses to go around
+ cidr_range_addresses = [
+ ("10.0.0.0/16", 65531),
+ ("10.0.0.0/17", 32763),
+ ("10.0.0.0/18", 16379),
+ ("10.0.0.0/19", 8187),
+ ("10.0.0.0/20", 4091),
+ ("10.0.0.0/21", 2043),
+ ("10.0.0.0/22", 1019),
+ ("10.0.0.0/23", 507),
+ ("10.0.0.0/24", 251),
+ ("10.0.0.0/25", 123),
+ ("10.0.0.0/26", 59),
+ ]
+ for (cidr, expected_count) in cidr_range_addresses:
+ validate_subnet_details_after_creating_eni(client, vpc, cidr, expected_count)
+
+
+def validate_subnet_details(client, vpc, cidr, expected_ip_address_count):
+ subnet = client.create_subnet(
+ VpcId=vpc.id, CidrBlock=cidr, AvailabilityZone="us-west-1b"
+ )["Subnet"]
+ subnet["AvailableIpAddressCount"].should.equal(expected_ip_address_count)
+ client.delete_subnet(SubnetId=subnet["SubnetId"])
+
+
+def validate_subnet_details_after_creating_eni(
+ client, vpc, cidr, expected_ip_address_count
+):
+ subnet = client.create_subnet(
+ VpcId=vpc.id, CidrBlock=cidr, AvailabilityZone="us-west-1b"
+ )["Subnet"]
+ # Create a random number of Elastic Network Interfaces
+ nr_of_eni_to_create = random.randint(0, 5)
+ ip_addresses_assigned = 0
+ enis_created = []
+ for i in range(0, nr_of_eni_to_create):
+ # Create a random number of IP addresses per ENI
+ nr_of_ip_addresses = random.randint(1, 5)
+ if nr_of_ip_addresses == 1:
+ # Pick the first available IP address (First 4 are reserved by AWS)
+ private_address = "10.0.0." + str(ip_addresses_assigned + 4)
+ eni = client.create_network_interface(
+ SubnetId=subnet["SubnetId"], PrivateIpAddress=private_address
+ )["NetworkInterface"]
+ enis_created.append(eni)
+ ip_addresses_assigned = ip_addresses_assigned + 1
+ else:
+ # Assign a list of IP addresses
+ private_addresses = [
+ "10.0.0." + str(4 + ip_addresses_assigned + i)
+ for i in range(0, nr_of_ip_addresses)
+ ]
+ eni = client.create_network_interface(
+ SubnetId=subnet["SubnetId"],
+ PrivateIpAddresses=[
+ {"PrivateIpAddress": address} for address in private_addresses
+ ],
+ )["NetworkInterface"]
+ enis_created.append(eni)
+ ip_addresses_assigned = ip_addresses_assigned + nr_of_ip_addresses + 1 #
+ # Verify that the nr of available IP addresses takes these ENIs into account
+ updated_subnet = client.describe_subnets(SubnetIds=[subnet["SubnetId"]])["Subnets"][
+ 0
+ ]
+ private_addresses = [
+ eni["PrivateIpAddress"] for eni in enis_created if eni["PrivateIpAddress"]
+ ]
+ for eni in enis_created:
+ private_addresses.extend(
+ [address["PrivateIpAddress"] for address in eni["PrivateIpAddresses"]]
+ )
+ error_msg = (
+ "Nr of IP addresses for Subnet with CIDR {0} is incorrect. Expected: {1}, Actual: {2}. "
+ "Addresses: {3}"
+ )
+ with sure.ensure(
+ error_msg,
+ cidr,
+ str(expected_ip_address_count),
+ updated_subnet["AvailableIpAddressCount"],
+ str(private_addresses),
+ ):
+ updated_subnet["AvailableIpAddressCount"].should.equal(
+ expected_ip_address_count - ip_addresses_assigned
+ )
+ # Clean up, as we have to create a few more subnets that shouldn't interfere with each other
+ for eni in enis_created:
+ client.delete_network_interface(NetworkInterfaceId=eni["NetworkInterfaceId"])
+ client.delete_subnet(SubnetId=subnet["SubnetId"])