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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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