Add instance type validation on add instance method (#5132)

This commit is contained in:
szopen321 2022-05-18 19:51:51 +02:00 committed by GitHub
parent 6ae0aa5272
commit dbcee3c196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 82 additions and 20 deletions

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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()

View File

@ -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=[],

View File

@ -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"),

View File

@ -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
) )

View File

@ -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,

View File

@ -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():
""" """

View File

@ -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")

View File

@ -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",