Add instance type validation on add instance method (#5132)
This commit is contained in:
		
							parent
							
								
									6ae0aa5272
								
							
						
					
					
						commit
						dbcee3c196
					
				| @ -633,6 +633,7 @@ class FakeAutoScalingGroup(CloudFormationModel): | |||||||
|             tags={"instance": propagated_tags}, |             tags={"instance": propagated_tags}, | ||||||
|             placement=random.choice(self.availability_zones), |             placement=random.choice(self.availability_zones), | ||||||
|             launch_config=self.launch_config, |             launch_config=self.launch_config, | ||||||
|  |             is_instance_type_default=False, | ||||||
|         ) |         ) | ||||||
|         for instance in reservation.instances: |         for instance in reservation.instances: | ||||||
|             instance.autoscaling_group = self |             instance.autoscaling_group = self | ||||||
|  | |||||||
| @ -1110,6 +1110,7 @@ class BatchBackend(BaseBackend): | |||||||
|                     subnet_id=next(subnet_cycle), |                     subnet_id=next(subnet_cycle), | ||||||
|                     key_name=compute_resources.get("ec2KeyPair", "AWS_OWNED"), |                     key_name=compute_resources.get("ec2KeyPair", "AWS_OWNED"), | ||||||
|                     security_group_ids=compute_resources["securityGroupIds"], |                     security_group_ids=compute_resources["securityGroupIds"], | ||||||
|  |                     is_instance_type_default=False, | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|                 new_comp_env.add_instance(reservation.instances[0]) |                 new_comp_env.add_instance(reservation.instances[0]) | ||||||
|  | |||||||
| @ -45,24 +45,25 @@ class InstanceTypeOfferingBackend(object): | |||||||
|         location_type = location_type or "region" |         location_type = location_type or "region" | ||||||
|         matches = INSTANCE_TYPE_OFFERINGS[location_type] |         matches = INSTANCE_TYPE_OFFERINGS[location_type] | ||||||
|         matches = matches.get(self.region_name, []) |         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_filters(self, offering, filters, location_type): | ||||||
|             def matches_filter(key, values): |         def matches_filter(key, values): | ||||||
|                 if key == "location": |             if key == "location": | ||||||
|                     if location_type in ("availability-zone", "availability-zone-id"): |                 if location_type in ("availability-zone", "availability-zone-id"): | ||||||
|                         return offering.get("Location") in values |                     return offering.get("Location") in values | ||||||
|                     elif location_type == "region": |                 elif location_type == "region": | ||||||
|                         return any( |                     return any( | ||||||
|                             v for v in values if offering.get("Location").startswith(v) |                         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: |                 else: | ||||||
|                     return False |                     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()]) |         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 |  | ||||||
|  | |||||||
| @ -2,11 +2,15 @@ import copy | |||||||
| import warnings | import warnings | ||||||
| from collections import OrderedDict | from collections import OrderedDict | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  | from moto import settings | ||||||
| 
 | 
 | ||||||
| from moto.core import get_account_id | from moto.core import get_account_id | ||||||
| from moto.core import CloudFormationModel | from moto.core import CloudFormationModel | ||||||
| from moto.core.utils import camelcase_to_underscores | 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.blockdevicemapping import BlockDeviceMapping | ||||||
| from moto.packages.boto.ec2.instance import Instance as BotoInstance | from moto.packages.boto.ec2.instance import Instance as BotoInstance | ||||||
| from moto.packages.boto.ec2.instance import Reservation | from moto.packages.boto.ec2.instance import Reservation | ||||||
| @ -15,6 +19,7 @@ from ..exceptions import ( | |||||||
|     AvailabilityZoneNotFromRegionError, |     AvailabilityZoneNotFromRegionError, | ||||||
|     EC2ClientError, |     EC2ClientError, | ||||||
|     InvalidInstanceIdError, |     InvalidInstanceIdError, | ||||||
|  |     InvalidInstanceTypeError, | ||||||
|     InvalidParameterValueErrorUnknownAttribute, |     InvalidParameterValueErrorUnknownAttribute, | ||||||
|     OperationNotPermitted4, |     OperationNotPermitted4, | ||||||
| ) | ) | ||||||
| @ -275,6 +280,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel): | |||||||
|             count=1, |             count=1, | ||||||
|             security_group_names=group_names, |             security_group_names=group_names, | ||||||
|             instance_type=properties.get("InstanceType", "m1.small"), |             instance_type=properties.get("InstanceType", "m1.small"), | ||||||
|  |             is_instance_type_default=not properties.get("InstanceType"), | ||||||
|             subnet_id=properties.get("SubnetId"), |             subnet_id=properties.get("SubnetId"), | ||||||
|             key_name=properties.get("KeyName"), |             key_name=properties.get("KeyName"), | ||||||
|             private_ip=properties.get("PrivateIpAddress"), |             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): |     def add_instances(self, image_id, count, user_data, security_group_names, **kwargs): | ||||||
|         location_type = "availability-zone" if kwargs.get("placement") else "region" |         location_type = "availability-zone" if kwargs.get("placement") else "region" | ||||||
|  |         default_region = "us-east-1" | ||||||
|         valid_instance_types = INSTANCE_TYPE_OFFERINGS[location_type] |         valid_instance_types = INSTANCE_TYPE_OFFERINGS[location_type] | ||||||
|         if "region_name" in kwargs and kwargs.get("placement"): |         if "region_name" in kwargs and kwargs.get("placement"): | ||||||
|             valid_availability_zones = { |             valid_availability_zones = { | ||||||
| @ -551,6 +558,24 @@ class InstanceBackend(object): | |||||||
|             } |             } | ||||||
|             if kwargs["placement"] not in valid_availability_zones: |             if kwargs["placement"] not in valid_availability_zones: | ||||||
|                 raise AvailabilityZoneNotFromRegionError(kwargs["placement"]) |                 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 = Reservation() | ||||||
|         new_reservation.id = random_reservation_id() |         new_reservation.id = random_reservation_id() | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -104,6 +104,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource): | |||||||
|             count=1, |             count=1, | ||||||
|             user_data=self.user_data, |             user_data=self.user_data, | ||||||
|             instance_type=self.launch_specification.instance_type, |             instance_type=self.launch_specification.instance_type, | ||||||
|  |             is_instance_type_default=not self.launch_specification.instance_type, | ||||||
|             subnet_id=self.launch_specification.subnet_id, |             subnet_id=self.launch_specification.subnet_id, | ||||||
|             key_name=self.launch_specification.key_name, |             key_name=self.launch_specification.key_name, | ||||||
|             security_group_names=[], |             security_group_names=[], | ||||||
|  | |||||||
| @ -49,6 +49,7 @@ class InstanceResponse(EC2BaseResponse): | |||||||
|         security_group_names = self._get_multi_param("SecurityGroup") |         security_group_names = self._get_multi_param("SecurityGroup") | ||||||
|         kwargs = { |         kwargs = { | ||||||
|             "instance_type": self._get_param("InstanceType", if_none="m1.small"), |             "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"), |             "placement": self._get_param("Placement.AvailabilityZone"), | ||||||
|             "region_name": self.region, |             "region_name": self.region, | ||||||
|             "subnet_id": self._get_param("SubnetId"), |             "subnet_id": self._get_param("SubnetId"), | ||||||
|  | |||||||
| @ -435,6 +435,7 @@ class ElasticMapReduceBackend(BaseBackend): | |||||||
| 
 | 
 | ||||||
|     def add_instances(self, cluster_id, instances, instance_group): |     def add_instances(self, cluster_id, instances, instance_group): | ||||||
|         cluster = self.clusters[cluster_id] |         cluster = self.clusters[cluster_id] | ||||||
|  |         instances["is_instance_type_default"] = not instances.get("instance_type") | ||||||
|         response = self.ec2_backend.add_instances( |         response = self.ec2_backend.add_instances( | ||||||
|             EXAMPLE_AMI_ID, instances["instance_count"], "", [], **instances |             EXAMPLE_AMI_ID, instances["instance_count"], "", [], **instances | ||||||
|         ) |         ) | ||||||
|  | |||||||
| @ -101,6 +101,7 @@ class OpsworkInstance(BaseModel): | |||||||
|                 security_group_names=[], |                 security_group_names=[], | ||||||
|                 security_group_ids=self.security_group_ids, |                 security_group_ids=self.security_group_ids, | ||||||
|                 instance_type=self.instance_type, |                 instance_type=self.instance_type, | ||||||
|  |                 is_instance_type_default=not self.instance_type, | ||||||
|                 key_name=self.ssh_keyname, |                 key_name=self.ssh_keyname, | ||||||
|                 ebs_optimized=self.ebs_optimized, |                 ebs_optimized=self.ebs_optimized, | ||||||
|                 subnet_id=self.subnet_id, |                 subnet_id=self.subnet_id, | ||||||
|  | |||||||
| @ -18,6 +18,10 @@ S3_IGNORE_SUBDOMAIN_BUCKETNAME = os.environ.get( | |||||||
| # How many seconds to wait before we "validate" a new certificate in ACM. | # 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")) | 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(): | def get_sf_execution_history_type(): | ||||||
|     """ |     """ | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| from botocore.exceptions import ClientError, ParamValidationError | from botocore.exceptions import ClientError, ParamValidationError | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| from unittest import SkipTest | from unittest import SkipTest, mock | ||||||
| 
 | 
 | ||||||
| import base64 | import base64 | ||||||
| import ipaddress | import ipaddress | ||||||
| @ -1162,6 +1162,32 @@ def test_run_instance_with_placement(): | |||||||
|     instance.placement.should.have.key("AvailabilityZone").equal("us-east-1b") |     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 | @mock_ec2 | ||||||
| def test_run_instance_with_availability_zone_not_from_region(): | def test_run_instance_with_availability_zone_not_from_region(): | ||||||
|     ec2 = boto3.resource("ec2", region_name="us-east-1") |     ec2 = boto3.resource("ec2", region_name="us-east-1") | ||||||
|  | |||||||
| @ -48,7 +48,7 @@ input_instance_groups = [ | |||||||
|     { |     { | ||||||
|         "InstanceCount": 6, |         "InstanceCount": 6, | ||||||
|         "InstanceRole": "TASK", |         "InstanceRole": "TASK", | ||||||
|         "InstanceType": "c1.large", |         "InstanceType": "c3.large", | ||||||
|         "Market": "SPOT", |         "Market": "SPOT", | ||||||
|         "Name": "task-1", |         "Name": "task-1", | ||||||
|         "BidPrice": "0.07", |         "BidPrice": "0.07", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user