| 
1 | 1 | import functools  | 
2 | 2 | import logging  | 
 | 3 | +import re  | 
3 | 4 | from collections.abc import Callable, Coroutine  | 
4 |  | -from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar  | 
 | 5 | +from typing import (  | 
 | 6 | +    TYPE_CHECKING,  | 
 | 7 | +    Any,  | 
 | 8 | +    Concatenate,  | 
 | 9 | +    Final,  | 
 | 10 | +    ParamSpec,  | 
 | 11 | +    TypeVar,  | 
 | 12 | +    cast,  | 
 | 13 | +)  | 
5 | 14 | 
 
  | 
6 | 15 | from botocore import exceptions as botocore_exc  | 
7 | 16 | 
 
  | 
8 | 17 | from ._errors import (  | 
9 | 18 |     EC2AccessError,  | 
10 | 19 |     EC2InstanceNotFoundError,  | 
11 | 20 |     EC2InstanceTypeInvalidError,  | 
 | 21 | +    EC2InsufficientCapacityError,  | 
12 | 22 |     EC2NotConnectedError,  | 
13 | 23 |     EC2RuntimeError,  | 
14 | 24 |     EC2TimeoutError,  | 
 | 
26 | 36 | Self = TypeVar("Self", bound="SimcoreEC2API")  | 
27 | 37 | 
 
  | 
28 | 38 | 
 
  | 
 | 39 | +_INSUFFICIENT_CAPACITY_ERROR_MSG_PATTERN: Final[re.Pattern] = re.compile(  | 
 | 40 | +    r"sufficient (?P<instance_type>\S+) capacity in the Availability Zone you requested "  | 
 | 41 | +    r"\((?P<failed_az>\S+)\)"  | 
 | 42 | +)  | 
 | 43 | + | 
 | 44 | + | 
29 | 45 | def _map_botocore_client_exception(  | 
30 | 46 |     botocore_error: botocore_exc.ClientError,  | 
31 | 47 |     *args,  # pylint: disable=unused-argument # noqa: ARG001  | 
32 | 48 |     **kwargs,  # pylint: disable=unused-argument # noqa: ARG001  | 
33 | 49 | ) -> EC2AccessError:  | 
34 |  | -    status_code = int(  | 
35 |  | -        botocore_error.response.get("ResponseMetadata", {}).get("HTTPStatusCode")  | 
36 |  | -        or botocore_error.response.get("Error", {}).get("Code", -1)  | 
 | 50 | +    # see https://boto3.amazonaws.com/v1/documentation/api/latest/guide/error-handling.html#parsing-error-responses-and-catching-exceptions-from-aws-services  | 
 | 51 | +    status_code = cast(  | 
 | 52 | +        int,  | 
 | 53 | +        botocore_error.response.get("ResponseMetadata", {}).get("HTTPStatusCode", "-1"),  | 
37 | 54 |     )  | 
 | 55 | +    error_code = botocore_error.response.get("Error", {}).get("Code", "Unknown")  | 
 | 56 | +    error_msg = botocore_error.response.get("Error", {}).get("Message", "Unknown")  | 
38 | 57 |     operation_name = botocore_error.operation_name  | 
39 |  | -    match status_code, operation_name:  | 
40 |  | -        case 400, "StartInstances":  | 
 | 58 | +    match error_code:  | 
 | 59 | +        case "InvalidInstanceID.NotFound":  | 
41 | 60 |             return EC2InstanceNotFoundError()  | 
42 |  | -        case 400, "StopInstances":  | 
43 |  | -            return EC2InstanceNotFoundError()  | 
44 |  | -        case 400, "TerminateInstances":  | 
45 |  | -            return EC2InstanceNotFoundError()  | 
46 |  | -        case 400, "DescribeInstanceTypes":  | 
 | 61 | +        case "InvalidInstanceType":  | 
47 | 62 |             return EC2InstanceTypeInvalidError()  | 
 | 63 | +        case "InsufficientInstanceCapacity":  | 
 | 64 | +            availability_zone = "unknown"  | 
 | 65 | +            instance_type = "unknown"  | 
 | 66 | +            if match := re.search(_INSUFFICIENT_CAPACITY_ERROR_MSG_PATTERN, error_msg):  | 
 | 67 | +                instance_type = match.group("instance_type")  | 
 | 68 | +                availability_zone = match.group("failed_az")  | 
 | 69 | + | 
 | 70 | +            raise EC2InsufficientCapacityError(  | 
 | 71 | +                availability_zone=availability_zone, instance_type=instance_type  | 
 | 72 | +            )  | 
48 | 73 |         case _:  | 
49 | 74 |             return EC2AccessError(  | 
 | 75 | +                status_code=status_code,  | 
50 | 76 |                 operation_name=operation_name,  | 
51 |  | -                code=status_code,  | 
52 |  | -                error=f"{botocore_error}",  | 
 | 77 | +                code=error_code,  | 
 | 78 | +                error=error_msg,  | 
53 | 79 |             )  | 
54 | 80 | 
 
  | 
55 | 81 | 
 
  | 
 | 
0 commit comments