From 148de0e5626790c7081a2beb8296422eb7b5ac91 Mon Sep 17 00:00:00 2001 From: Laurie O Date: Tue, 4 Oct 2022 19:48:10 +1000 Subject: [PATCH] EC2: Support all (current) instance-type filters (#5526) --- moto/ec2/exceptions.py | 4 +- moto/ec2/models/instance_types.py | 111 ++++++++++++++++++++++---- tests/test_ec2/test_instance_types.py | 31 +++++++ 3 files changed, 129 insertions(+), 17 deletions(-) diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 7a2ee1f04..da20305c7 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -427,8 +427,8 @@ class InvalidServiceName(EC2ClientError): class InvalidFilter(EC2ClientError): - def __init__(self, filter_name): - super().__init__("InvalidFilter", f"The filter '{filter_name}' is invalid") + def __init__(self, filter_name, error_type="InvalidFilter"): + super().__init__(error_type, f"The filter '{filter_name}' is invalid") class InvalidNextToken(EC2ClientError): diff --git a/moto/ec2/models/instance_types.py b/moto/ec2/models/instance_types.py index 225341307..3d17ec9f5 100644 --- a/moto/ec2/models/instance_types.py +++ b/moto/ec2/models/instance_types.py @@ -4,7 +4,7 @@ from os import listdir from ..utils import generic_filter from moto.utilities.utils import load_resource -from ..exceptions import FilterNotImplementedError, InvalidInstanceTypeError +from ..exceptions import InvalidFilter, InvalidInstanceTypeError INSTANCE_TYPES = load_resource(__name__, "../resources/instance_types.json") INSTANCE_FAMILIES = list(set([i.split(".")[0] for i in INSTANCE_TYPES.keys()])) @@ -23,6 +23,87 @@ for location_type in listdir(root / offerings_path): class InstanceType(dict): + _filter_attributes = { + "auto-recovery-supported": ["AutoRecoverySupported"], + "bare-metal": ["BareMetal"], + "burstable-performance-supported": ["BurstablePerformanceSupported"], + "current-generation": ["CurrentGeneration"], + "ebs-info.ebs-optimized-info.baseline-bandwidth-in-mbps": [ + "EbsInfo", "EbsOptimizedInfo", "BaselineBandwidthInMbps" + ], + "ebs-info.ebs-optimized-info.baseline-iops": [ + "EbsInfo", "EbsOptimizedInfo", "BaselineIops" + ], + "ebs-info.ebs-optimized-info.baseline-throughput-in-mbps": [ + "EbsInfo", "EbsOptimizedInfo", "BaselineThroughputInMBps" + ], + "ebs-info.ebs-optimized-info.maximum-bandwidth-in-mbps": [ + "EbsInfo", "EbsOptimizedInfo", "MaximumBandwidthInMbps" + ], + "ebs-info.ebs-optimized-info.maximum-iops": [ + "EbsInfo", "EbsOptimizedInfo", "MaximumIops" + ], + "ebs-info.ebs-optimized-info.maximum-throughput-in-mbps": [ + "EbsInfo", "EbsOptimizedInfo", "MaximumThroughputInMBps" + ], + "ebs-info.ebs-optimized-support": ["EbsInfo", "EbsOptimizedSupport"], + "ebs-info.encryption-support": ["EbsInfo", "EncryptionSupport"], + "ebs-info.nvme-support": ["EbsInfo", "NvmeSupport"], + "free-tier-eligible": ["FreeTierEligible"], + "hibernation-supported": ["HibernationSupported"], + "hypervisor": ["Hypervisor"], + "instance-storage-info.disk.count": ["InstanceStorageInfo", "Disks", "Count"], + "instance-storage-info.disk.size-in-gb": [ + "InstanceStorageInfo", "Disks", "SizeInGB" + ], + "instance-storage-info.disk.type": ["InstanceStorageInfo", "Disks", "Type"], + "instance-storage-info.encryption-support": [ + "InstanceStorageInfo", "EncryptionSupport" + ], + "instance-storage-info.nvme-support": ["InstanceStorageInfo", "NvmeSupport"], + "instance-storage-info.total-size-in-gb": [ + "InstanceStorageInfo", "TotalSizeInGB" + ], + "instance-storage-supported": ["InstanceStorageSupported"], + "instance-type": ["InstanceType"], + "memory-info.size-in-mib": ["MemoryInfo", "SizeInMiB"], + "network-info.efa-info.maximum-efa-interfaces": [ + "NetworkInfo", "EfaInfo", "MaximumEfaInterfaces" + ], + "network-info.efa-supported": ["NetworkInfo", "EfaSupported"], + "network-info.ena-support": ["NetworkInfo", "EnaSupport"], + "network-info.encryption-in-transit-supported": [ + "NetworkInfo", "EncryptionInTransitSupported" + ], + "network-info.ipv4-addresses-per-interface": [ + "NetworkInfo", "Ipv4AddressesPerInterface" + ], + "network-info.ipv6-addresses-per-interface": [ + "NetworkInfo", "Ipv6AddressesPerInterface" + ], + "network-info.ipv6-supported": ["NetworkInfo", "Ipv6Supported"], + "network-info.maximum-network-cards": ["NetworkInfo", "MaximumNetworkCards"], + "network-info.maximum-network-interfaces": [ + "NetworkInfo", "MaximumNetworkInterfaces" + ], + "network-info.network-performance": ["NetworkInfo", "NetworkPerformance"], + "processor-info.supported-architecture": [ + "ProcessorInfo", "SupportedArchitectures" + ], + "processor-info.sustained-clock-speed-in-ghz": [ + "ProcessorInfo", "SustainedClockSpeedInGhz" + ], + "supported-boot-mode": ["SupportedBootModes"], + "supported-root-device-type": ["SupportedRootDeviceTypes"], + "supported-usage-class": ["SupportedUsageClasses"], + "supported-virtualization-type": ["SupportedVirtualizationTypes"], + "vcpu-info.default-cores": ["VCpuInfo", "DefaultCores"], + "vcpu-info.default-threads-per-core": ["VCpuInfo", "DefaultThreadsPerCore"], + "vcpu-info.default-vcpus": ["VCpuInfo", "DefaultVCpus"], + "vcpu-info.valid-cores": ["VCpuInfo", "ValidCores"], + "vcpu-info.valid-threads-per-core": ["VCpuInfo", "ValidThreadsPerCore"], + } # fmt: skip + def __init__(self, name): self.name = name self.update(INSTANCE_TYPES[name]) @@ -37,20 +118,20 @@ class InstanceType(dict): return "" % self.name def get_filter_value(self, filter_name): - if filter_name in ("instance-type"): - return self.get("InstanceType") - elif filter_name in ("vcpu-info.default-vcpus"): - return str(self.get("VCpuInfo").get("DefaultVCpus")) - elif filter_name in ("memory-info.size-in-mib"): - return str(self.get("MemoryInfo").get("SizeInMiB")) - elif filter_name in ("bare-metal"): - return str(self.get("BareMetal")).lower() - elif filter_name in ("burstable-performance-supported"): - return str(self.get("BurstablePerformanceSupported")).lower() - elif filter_name in ("current-generation"): - return str(self.get("CurrentGeneration")).lower() - else: - return FilterNotImplementedError(filter_name, "DescribeInstanceTypes") + def stringify(v): + if isinstance(v, (bool, int)): + return str(v).lower() + elif isinstance(v, list): + return [stringify(i) for i in v] + return v + + path = self._filter_attributes.get(filter_name) + if not path: + raise InvalidFilter(filter_name, error_type="InvalidParameterValue") + value = self + for key in path: + value = (value or {}).get(key) + return stringify(value) class InstanceTypeBackend: diff --git a/tests/test_ec2/test_instance_types.py b/tests/test_ec2/test_instance_types.py index 778939348..28c603b87 100644 --- a/tests/test_ec2/test_instance_types.py +++ b/tests/test_ec2/test_instance_types.py @@ -180,3 +180,34 @@ def test_describe_instance_types_filter_by_current_generation(): # not contain types.should_not.contain("t1.micro") + + +@mock_ec2 +def test_describe_instance_types_small_instances(): + client = boto3.client("ec2", "us-east-1") + instance_types = client.describe_instance_types(Filters=[ + {"Name": "bare-metal", "Values": ["false"]}, + {"Name": "current-generation", "Values": ["true"]}, + {"Name": "vcpu-info.default-cores", "Values": ["1"]}, + {"Name": "memory-info.size-in-mib", "Values": ["512", "1024"]}, + {"Name": "vcpu-info.valid-threads-per-core", "Values": ["1"]}, + ]) # fmt: skip + + types = set(t["InstanceType"] for t in instance_types["InstanceTypes"]) + types.should.equal({"t3.nano", "t3.micro", "t3a.nano", "t3a.micro"}) + + +@mock_ec2 +def test_describe_instance_types_invalid_filter(): + client = boto3.client("ec2", "us-east-1") + + with pytest.raises(ClientError) as exc_info: + client.describe_instance_types( + Filters=[{"Name": "spam", "Values": ["eggs"]}], + ) + + exc_info.value.response["Error"]["Code"].should.equal("InvalidParameterValue") + exc_info.value.response["Error"]["Message"].split(":")[0].should.equal( + "The filter 'spam' is invalid", + ) + exc_info.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)