1313from settings_library .ec2 import EC2Settings
1414from types_aiobotocore_ec2 import EC2Client
1515from 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
1822from ._error_handler import ec2_exception_handler
1923from ._errors import (
2024 EC2InstanceNotFoundError ,
2125 EC2InsufficientCapacityError ,
26+ EC2SubnetsNotEnoughIPsError ,
2227)
2328from ._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" ]]
0 commit comments