From dbcee3c1964d4f457b62a8bff0a304b062c6d6ea Mon Sep 17 00:00:00 2001 From: szopen321 <32538420+szopen321@users.noreply.github.com> Date: Wed, 18 May 2022 19:51:51 +0200 Subject: [PATCH] Add instance type validation on add instance method (#5132) --- moto/autoscaling/models.py | 1 + moto/batch/models.py | 1 + moto/ec2/models/instance_types.py | 35 ++++++++++++++++--------------- moto/ec2/models/instances.py | 27 +++++++++++++++++++++++- moto/ec2/models/spot_requests.py | 1 + moto/ec2/responses/instances.py | 1 + moto/emr/models.py | 1 + moto/opsworks/models.py | 1 + moto/settings.py | 4 ++++ tests/test_ec2/test_instances.py | 28 ++++++++++++++++++++++++- tests/test_emr/test_emr_boto3.py | 2 +- 11 files changed, 82 insertions(+), 20 deletions(-) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index a252fc531..296adb427 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -633,6 +633,7 @@ class FakeAutoScalingGroup(CloudFormationModel): tags={"instance": propagated_tags}, placement=random.choice(self.availability_zones), launch_config=self.launch_config, + is_instance_type_default=False, ) for instance in reservation.instances: instance.autoscaling_group = self diff --git a/moto/batch/models.py b/moto/batch/models.py index 37d72e5eb..2123ba0a9 100644 --- a/moto/batch/models.py +++ b/moto/batch/models.py @@ -1110,6 +1110,7 @@ class BatchBackend(BaseBackend): subnet_id=next(subnet_cycle), key_name=compute_resources.get("ec2KeyPair", "AWS_OWNED"), security_group_ids=compute_resources["securityGroupIds"], + is_instance_type_default=False, ) new_comp_env.add_instance(reservation.instances[0]) diff --git a/moto/ec2/models/instance_types.py b/moto/ec2/models/instance_types.py index 85577883b..5e2f7846d 100644 --- a/moto/ec2/models/instance_types.py +++ b/moto/ec2/models/instance_types.py @@ -45,24 +45,25 @@ class InstanceTypeOfferingBackend(object): location_type = location_type or "region" matches = INSTANCE_TYPE_OFFERINGS[location_type] matches = matches.get(self.region_name, []) + matches = [ + o for o in matches if self.matches_filters(o, filters or {}, location_type) + ] + return matches - def matches_filters(offering, filters): - def matches_filter(key, values): - if key == "location": - if location_type in ("availability-zone", "availability-zone-id"): - return offering.get("Location") in values - elif location_type == "region": - return any( - v for v in values if offering.get("Location").startswith(v) - ) - else: - return False - elif key == "instance-type": - return offering.get("InstanceType") in values + def matches_filters(self, offering, filters, location_type): + def matches_filter(key, values): + if key == "location": + if location_type in ("availability-zone", "availability-zone-id"): + return offering.get("Location") in values + elif location_type == "region": + return any( + v for v in values if offering.get("Location").startswith(v) + ) else: return False + elif key == "instance-type": + return offering.get("InstanceType") in values + else: + return False - return all([matches_filter(key, values) for key, values in filters.items()]) - - matches = [o for o in matches if matches_filters(o, filters or {})] - return matches + return all([matches_filter(key, values) for key, values in filters.items()]) diff --git a/moto/ec2/models/instances.py b/moto/ec2/models/instances.py index f7c93cbf1..dcc578d88 100644 --- a/moto/ec2/models/instances.py +++ b/moto/ec2/models/instances.py @@ -2,11 +2,15 @@ import copy import warnings from collections import OrderedDict from datetime import datetime +from moto import settings from moto.core import get_account_id from moto.core import CloudFormationModel from moto.core.utils import camelcase_to_underscores -from moto.ec2.models.instance_types import INSTANCE_TYPE_OFFERINGS +from moto.ec2.models.instance_types import ( + INSTANCE_TYPE_OFFERINGS, + InstanceTypeOfferingBackend, +) from moto.packages.boto.ec2.blockdevicemapping import BlockDeviceMapping from moto.packages.boto.ec2.instance import Instance as BotoInstance from moto.packages.boto.ec2.instance import Reservation @@ -15,6 +19,7 @@ from ..exceptions import ( AvailabilityZoneNotFromRegionError, EC2ClientError, InvalidInstanceIdError, + InvalidInstanceTypeError, InvalidParameterValueErrorUnknownAttribute, OperationNotPermitted4, ) @@ -275,6 +280,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): count=1, security_group_names=group_names, instance_type=properties.get("InstanceType", "m1.small"), + is_instance_type_default=not properties.get("InstanceType"), subnet_id=properties.get("SubnetId"), key_name=properties.get("KeyName"), private_ip=properties.get("PrivateIpAddress"), @@ -543,6 +549,7 @@ class InstanceBackend(object): def add_instances(self, image_id, count, user_data, security_group_names, **kwargs): location_type = "availability-zone" if kwargs.get("placement") else "region" + default_region = "us-east-1" valid_instance_types = INSTANCE_TYPE_OFFERINGS[location_type] if "region_name" in kwargs and kwargs.get("placement"): valid_availability_zones = { @@ -551,6 +558,24 @@ class InstanceBackend(object): } if kwargs["placement"] not in valid_availability_zones: raise AvailabilityZoneNotFromRegionError(kwargs["placement"]) + match_filters = InstanceTypeOfferingBackend().matches_filters + if not kwargs["is_instance_type_default"] and not any( + { + match_filters( + valid_instance, + {"instance-type": kwargs["instance_type"]}, + location_type, + ) + for valid_instance in valid_instance_types.get( + kwargs["region_name"] + if "region_name" in kwargs + else default_region, + {}, + ) + }, + ): + if settings.EC2_ENABLE_INSTANCE_TYPE_VALIDATION: + raise InvalidInstanceTypeError(kwargs["instance_type"]) new_reservation = Reservation() new_reservation.id = random_reservation_id() diff --git a/moto/ec2/models/spot_requests.py b/moto/ec2/models/spot_requests.py index 935518e92..765fda528 100644 --- a/moto/ec2/models/spot_requests.py +++ b/moto/ec2/models/spot_requests.py @@ -104,6 +104,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): count=1, user_data=self.user_data, instance_type=self.launch_specification.instance_type, + is_instance_type_default=not self.launch_specification.instance_type, subnet_id=self.launch_specification.subnet_id, key_name=self.launch_specification.key_name, security_group_names=[], diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index e3587d3fa..7f3a3d2cb 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -49,6 +49,7 @@ class InstanceResponse(EC2BaseResponse): security_group_names = self._get_multi_param("SecurityGroup") kwargs = { "instance_type": self._get_param("InstanceType", if_none="m1.small"), + "is_instance_type_default": not self._get_param("InstanceType"), "placement": self._get_param("Placement.AvailabilityZone"), "region_name": self.region, "subnet_id": self._get_param("SubnetId"), diff --git a/moto/emr/models.py b/moto/emr/models.py index 1fcdfa12f..34bce3fb6 100644 --- a/moto/emr/models.py +++ b/moto/emr/models.py @@ -435,6 +435,7 @@ class ElasticMapReduceBackend(BaseBackend): def add_instances(self, cluster_id, instances, instance_group): cluster = self.clusters[cluster_id] + instances["is_instance_type_default"] = not instances.get("instance_type") response = self.ec2_backend.add_instances( EXAMPLE_AMI_ID, instances["instance_count"], "", [], **instances ) diff --git a/moto/opsworks/models.py b/moto/opsworks/models.py index 271f4f79f..8ded0d766 100644 --- a/moto/opsworks/models.py +++ b/moto/opsworks/models.py @@ -101,6 +101,7 @@ class OpsworkInstance(BaseModel): security_group_names=[], security_group_ids=self.security_group_ids, instance_type=self.instance_type, + is_instance_type_default=not self.instance_type, key_name=self.ssh_keyname, ebs_optimized=self.ebs_optimized, subnet_id=self.subnet_id, diff --git a/moto/settings.py b/moto/settings.py index 3481ffd02..8a3bffaa8 100644 --- a/moto/settings.py +++ b/moto/settings.py @@ -18,6 +18,10 @@ S3_IGNORE_SUBDOMAIN_BUCKETNAME = os.environ.get( # How many seconds to wait before we "validate" a new certificate in ACM. ACM_VALIDATION_WAIT = int(os.environ.get("MOTO_ACM_VALIDATION_WAIT", "60")) +EC2_ENABLE_INSTANCE_TYPE_VALIDATION = bool( + os.environ.get("MOTO_EC2_ENABLE_INSTANCE_TYPE_VALIDATION", False) +) + def get_sf_execution_history_type(): """ diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index a2f1e0ab2..e0c34cb14 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -1,7 +1,7 @@ from botocore.exceptions import ClientError, ParamValidationError import pytest -from unittest import SkipTest +from unittest import SkipTest, mock import base64 import ipaddress @@ -1162,6 +1162,32 @@ def test_run_instance_with_placement(): instance.placement.should.have.key("AvailabilityZone").equal("us-east-1b") +@mock_ec2 +@mock.patch( + "moto.ec2.models.instances.settings.EC2_ENABLE_INSTANCE_TYPE_VALIDATION", + new_callable=mock.PropertyMock(return_value=True), +) +def test_run_instance_with_invalid_instance_type(m_flag): + if settings.TEST_SERVER_MODE: + raise SkipTest( + "It is not possible to set the environment variable in server mode" + ) + ec2 = boto3.resource("ec2", region_name="us-east-1") + with pytest.raises(ClientError) as ex: + ec2.create_instances( + ImageId=EXAMPLE_AMI_ID, + InstanceType="invalid_type", + MinCount=1, + MaxCount=1, + Placement={"AvailabilityZone": "us-east-1b"}, + ) + ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.value.response["Error"]["Message"].should.equal( + "The instance type 'invalid_type' does not exist" + ) + assert m_flag is True + + @mock_ec2 def test_run_instance_with_availability_zone_not_from_region(): ec2 = boto3.resource("ec2", region_name="us-east-1") diff --git a/tests/test_emr/test_emr_boto3.py b/tests/test_emr/test_emr_boto3.py index 62be08abe..5e1014ef1 100644 --- a/tests/test_emr/test_emr_boto3.py +++ b/tests/test_emr/test_emr_boto3.py @@ -48,7 +48,7 @@ input_instance_groups = [ { "InstanceCount": 6, "InstanceRole": "TASK", - "InstanceType": "c1.large", + "InstanceType": "c3.large", "Market": "SPOT", "Name": "task-1", "BidPrice": "0.07",