#2002 - Take ElasticNetworkInterfaces into account when requesting available IP addresses
This commit is contained in:
parent
f58f645a8e
commit
179bdd6caa
@ -214,6 +214,7 @@ class NetworkInterface(TaggedEC2Resource):
|
|||||||
ec2_backend,
|
ec2_backend,
|
||||||
subnet,
|
subnet,
|
||||||
private_ip_address,
|
private_ip_address,
|
||||||
|
private_ip_addresses=None,
|
||||||
device_index=0,
|
device_index=0,
|
||||||
public_ip_auto_assign=True,
|
public_ip_auto_assign=True,
|
||||||
group_ids=None,
|
group_ids=None,
|
||||||
@ -223,6 +224,7 @@ class NetworkInterface(TaggedEC2Resource):
|
|||||||
self.id = random_eni_id()
|
self.id = random_eni_id()
|
||||||
self.device_index = device_index
|
self.device_index = device_index
|
||||||
self.private_ip_address = private_ip_address or random_private_ip()
|
self.private_ip_address = private_ip_address or random_private_ip()
|
||||||
|
self.private_ip_addresses = private_ip_addresses
|
||||||
self.subnet = subnet
|
self.subnet = subnet
|
||||||
self.instance = None
|
self.instance = None
|
||||||
self.attachment_id = None
|
self.attachment_id = None
|
||||||
@ -341,12 +343,19 @@ class NetworkInterfaceBackend(object):
|
|||||||
super(NetworkInterfaceBackend, self).__init__()
|
super(NetworkInterfaceBackend, self).__init__()
|
||||||
|
|
||||||
def create_network_interface(
|
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(
|
eni = NetworkInterface(
|
||||||
self,
|
self,
|
||||||
subnet,
|
subnet,
|
||||||
private_ip_address,
|
private_ip_address,
|
||||||
|
private_ip_addresses,
|
||||||
group_ids=group_ids,
|
group_ids=group_ids,
|
||||||
description=description,
|
description=description,
|
||||||
**kwargs
|
**kwargs
|
||||||
@ -2819,7 +2828,9 @@ class Subnet(TaggedEC2Resource):
|
|||||||
self.vpc_id = vpc_id
|
self.vpc_id = vpc_id
|
||||||
self.cidr_block = cidr_block
|
self.cidr_block = cidr_block
|
||||||
self.cidr = ipaddress.IPv4Network(six.text_type(self.cidr_block), strict=False)
|
self.cidr = ipaddress.IPv4Network(six.text_type(self.cidr_block), strict=False)
|
||||||
self.available_ip_addresses = str(ipaddress.IPv4Network(six.text_type(self.cidr_block)).num_addresses - 5)
|
self._available_ip_addresses = (
|
||||||
|
ipaddress.IPv4Network(six.text_type(self.cidr_block)).num_addresses - 5
|
||||||
|
)
|
||||||
self._availability_zone = availability_zone
|
self._availability_zone = availability_zone
|
||||||
self.default_for_az = default_for_az
|
self.default_for_az = default_for_az
|
||||||
self.map_public_ip_on_launch = map_public_ip_on_launch
|
self.map_public_ip_on_launch = map_public_ip_on_launch
|
||||||
@ -2855,6 +2866,21 @@ class Subnet(TaggedEC2Resource):
|
|||||||
|
|
||||||
return subnet
|
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
|
@property
|
||||||
def availability_zone(self):
|
def availability_zone(self):
|
||||||
return self._availability_zone.name
|
return self._availability_zone.name
|
||||||
|
@ -7,12 +7,13 @@ class ElasticNetworkInterfaces(BaseResponse):
|
|||||||
def create_network_interface(self):
|
def create_network_interface(self):
|
||||||
subnet_id = self._get_param("SubnetId")
|
subnet_id = self._get_param("SubnetId")
|
||||||
private_ip_address = self._get_param("PrivateIpAddress")
|
private_ip_address = self._get_param("PrivateIpAddress")
|
||||||
|
private_ip_addresses = self._get_multi_param("PrivateIpAddresses")
|
||||||
groups = self._get_multi_param("SecurityGroupId")
|
groups = self._get_multi_param("SecurityGroupId")
|
||||||
subnet = self.ec2_backend.get_subnet(subnet_id)
|
subnet = self.ec2_backend.get_subnet(subnet_id)
|
||||||
description = self._get_param("Description")
|
description = self._get_param("Description")
|
||||||
if self.is_not_dryrun("CreateNetworkInterface"):
|
if self.is_not_dryrun("CreateNetworkInterface"):
|
||||||
eni = self.ec2_backend.create_network_interface(
|
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)
|
template = self.response_template(CREATE_NETWORK_INTERFACE_RESPONSE)
|
||||||
return template.render(eni=eni)
|
return template.render(eni=eni)
|
||||||
|
@ -11,6 +11,7 @@ from boto.exception import EC2ResponseError
|
|||||||
from botocore.exceptions import ParamValidationError, ClientError
|
from botocore.exceptions import ParamValidationError, ClientError
|
||||||
import json
|
import json
|
||||||
import sure # noqa
|
import sure # noqa
|
||||||
|
import random
|
||||||
|
|
||||||
from moto import mock_cloudformation_deprecated, mock_ec2, mock_ec2_deprecated
|
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
|
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"])
|
||||||
|
Loading…
Reference in New Issue
Block a user