Skip to content

Commit aba0191

Browse files
committed
improve
1 parent b8659fc commit aba0191

File tree

3 files changed

+43
-37
lines changed

3 files changed

+43
-37
lines changed

packages/aws-library/src/aws_library/ec2/_client.py

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
from settings_library.ec2 import EC2Settings
1414
from types_aiobotocore_ec2 import EC2Client
1515
from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
16-
from types_aiobotocore_ec2.type_defs import FilterTypeDef, TagTypeDef
16+
from types_aiobotocore_ec2.type_defs import (
17+
FilterTypeDef,
18+
SubnetTypeDef,
19+
TagTypeDef,
20+
)
1721

1822
from ._error_handler import ec2_exception_handler
1923
from ._errors import (
2024
EC2InstanceNotFoundError,
2125
EC2InsufficientCapacityError,
26+
EC2SubnetsNotEnoughIPsError,
2227
)
2328
from ._models import (
2429
AWSTagKey,
@@ -135,7 +140,10 @@ async def launch_instances(
135140
max_total_number_of_instances -- The total maximum allowed number of instances for this given instance_config
136141
137142
Raises:
138-
EC2TooManyInstancesError:
143+
EC2TooManyInstancesError: max_total_number_of_instances would be exceeded
144+
EC2SubnetsNotEnoughIPsError: not enough IPs in the subnets
145+
EC2InsufficientCapacityError: not enough capacity in the subnets
146+
139147
140148
Returns:
141149
The created instance data infos
@@ -144,7 +152,8 @@ async def launch_instances(
144152
with log_context(
145153
_logger,
146154
logging.INFO,
147-
msg=f"launch {number_of_instances} AWS instance(s) {instance_config.type.name} with {instance_config.tags=}",
155+
msg=f"launch {number_of_instances} AWS instance(s) {instance_config.type.name}"
156+
f" with {instance_config.tags=} in {instance_config.subnet_ids=}",
148157
):
149158
# first check the max amount is not already reached
150159
await check_max_number_of_instances_not_exceeded(
@@ -154,25 +163,31 @@ async def launch_instances(
154163
max_total_number_of_instances=max_total_number_of_instances,
155164
)
156165

157-
subnet_descriptions = await self.client.describe_subnets(
166+
# NOTE: checking subnets capacity is not strictly needed as AWS will do it for us
167+
# but it gives us a chance to give early feedback to the user
168+
# and avoid trying to launch instances in subnets that are already full
169+
# and also allows to circumvent a moto bug that does not raise
170+
# InsufficientInstanceCapacity when a subnet is full
171+
subnets = await self.client.describe_subnets(
158172
SubnetIds=instance_config.subnet_ids
159173
)
160-
# check available IPs in subnets to give early feedback
161-
subnet_id_to_available_ips: dict[str, int] = {}
162-
for subnet in subnet_descriptions["Subnets"]:
163-
if "SubnetId" in subnet and "AvailableIpAddressCount" in subnet:
164-
subnet_id_to_available_ips[subnet["SubnetId"]] = subnet[
165-
"AvailableIpAddressCount"
166-
]
174+
assert "Subnets" in subnets # nosec
175+
subnet_id_to_subnet_map: dict[str, SubnetTypeDef] = {
176+
subnet["SubnetId"]: subnet for subnet in subnets["Subnets"]
177+
}
178+
# preserve the order of instance_config.subnet_ids
179+
180+
subnet_id_to_available_ips: dict[str, int] = {
181+
subnet_id: subnet_id_to_subnet_map[subnet_id]["AvailableIpAddressCount"] # type: ignore
182+
for subnet_id in instance_config.subnet_ids
183+
}
184+
167185
total_available_ips = sum(subnet_id_to_available_ips.values())
168186
if total_available_ips < min_number_of_instances:
169-
raise EC2InsufficientCapacityError(
170-
subnet_id=", ".join(instance_config.subnet_ids),
187+
raise EC2SubnetsNotEnoughIPsError(
188+
subnet_ids=instance_config.subnet_ids,
171189
instance_type=instance_config.type.name,
172-
details=(
173-
f"Not enough available IPs in subnets {subnet_id_to_available_ips}. "
174-
f"Total available IPs={total_available_ips} < min required instances={min_number_of_instances}"
175-
),
190+
available_ips=total_available_ips,
176191
)
177192
# now let's not try to run instances in subnets that have not enough IPs
178193
subnet_ids_with_capacity = [
@@ -187,7 +202,6 @@ async def launch_instances(
187202
]
188203

189204
# Try each subnet in order until one succeeds
190-
last_error = None
191205
for subnet_id in subnet_ids_with_capacity:
192206
try:
193207
_logger.debug(
@@ -227,27 +241,13 @@ async def launch_instances(
227241
# If we get here, the launch succeeded
228242
break
229243
except botocore.exceptions.ClientError as exc:
230-
# AI says run_instances may raise the following errors:
231-
# InsufficientInstanceCapacity, when a subnet does not have enough capacity
232-
# InstanceLimitExceeded, when the AWS account has reached its limit of instances for this region
233-
# InvalidAMIID.NotFound, when the AMI ID does not exist
234-
# InvalidSubnetID.NotFound, when the subnet does not exist
235-
# InvalidGroupId.NotFound, when a security group does not exist
236-
# InvalidKeyPair.NotFound, when the key pair does not exist
237-
# UnauthorizedOperation, when the credentials do not have permissions to launch instances
238-
# InvalidInstanceType, when the instance type is not valid
239-
240244
error_code = exc.response.get("Error", {}).get("Code")
241245
if error_code == "InsufficientInstanceCapacity":
242246
_logger.warning(
243247
"Insufficient capacity in subnet %s for instance type %s, trying next subnet",
244248
subnet_id,
245249
instance_config.type.name,
246250
)
247-
last_error = EC2InsufficientCapacityError(
248-
subnet_id=subnet_id,
249-
instance_type=instance_config.type.name,
250-
)
251251
continue
252252
# For any other ClientError, re-raise to let the decorator handle it
253253
raise
@@ -258,10 +258,9 @@ async def launch_instances(
258258
"All subnets failed with insufficient capacity for instance type %s",
259259
instance_config.type.name,
260260
)
261-
if last_error:
262-
raise last_error
261+
263262
raise EC2InsufficientCapacityError(
264-
subnet_id="all_configured_subnets",
263+
subnet_ids=instance_config.subnet_ids,
265264
instance_type=instance_config.type.name,
266265
)
267266
instance_ids = [i["InstanceId"] for i in instances["Instances"]]

packages/aws-library/src/aws_library/ec2/_errors.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,11 @@ class EC2TooManyInstancesError(EC2AccessError):
3939

4040

4141
class EC2InsufficientCapacityError(EC2AccessError):
42-
msg_template: str = "Insufficient capacity in {subnet_id} for {instance_type}"
42+
msg_template: str = "Insufficient capacity in {subnet_ids} for {instance_type}"
43+
44+
45+
class EC2SubnetsNotEnoughIPsError(EC2AccessError):
46+
msg_template: str = (
47+
"Not enough free IPs in subnet(s) {subnet_ids} for {num_instances} instances"
48+
". Only {available_ips} IPs available."
49+
)

packages/aws-library/tests/test_ec2_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ async def mock_run_instances(*args, **kwargs):
828828
# Verify the error contains the expected information
829829
assert hasattr(exc_info.value, "instance_type")
830830
assert exc_info.value.instance_type == fake_ec2_instance_type.name # type: ignore
831-
assert exc_info.value.subnet_id == aws_subnet_id # type: ignore
831+
assert exc_info.value.subnet_ids == [aws_subnet_id] # type: ignore
832832

833833
# Verify still only 2 instances exist (no new ones were created)
834834
await _assert_instances_in_ec2(

0 commit comments

Comments
 (0)