handle 'NoDevice' parameter in BlockDeviceMappings #4852 (#4853)

This commit is contained in:
Tim Snyder 2022-02-14 03:37:46 -06:00 committed by GitHub
parent 155f9f20eb
commit ea81377cd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 82 additions and 10 deletions

View File

@ -49,6 +49,11 @@ class InvalidDHCPOptionsIdError(EC2ClientError):
)
class InvalidRequest(EC2ClientError):
def __init__(self):
super().__init__("InvalidRequest", "The request received was invalid")
class InvalidParameterCombination(EC2ClientError):
def __init__(self, msg):
super().__init__("InvalidParameterCombination", msg)

View File

@ -1155,14 +1155,15 @@ class InstanceBackend(object):
"DeleteOnTermination", False
)
kms_key_id = block_device["Ebs"].get("KmsKeyId")
new_instance.add_block_device(
volume_size,
device_name,
snapshot_id,
encrypted,
delete_on_termination,
kms_key_id,
)
if block_device.get("NoDevice") != "":
new_instance.add_block_device(
volume_size,
device_name,
snapshot_id,
encrypted,
delete_on_termination,
kms_key_id,
)
else:
new_instance.setup_defaults()
if kwargs.get("instance_market_options"):

View File

@ -1,7 +1,11 @@
from moto.autoscaling import autoscaling_backends
from moto.core.responses import BaseResponse
from moto.core.utils import camelcase_to_underscores
from moto.ec2.exceptions import MissingParameterError, InvalidParameterCombination
from moto.ec2.exceptions import (
MissingParameterError,
InvalidParameterCombination,
InvalidRequest,
)
from moto.ec2.utils import (
filters_from_querystring,
dict_from_querystring,
@ -320,6 +324,7 @@ class InstanceResponse(BaseResponse):
device_mapping.get("ebs._encrypted", False)
)
device_template["Ebs"]["KmsKeyId"] = device_mapping.get("ebs._kms_key_id")
device_template["NoDevice"] = device_mapping.get("no_device")
mappings.append(device_template)
return mappings
@ -327,6 +332,22 @@ class InstanceResponse(BaseResponse):
@staticmethod
def _validate_block_device_mapping(device_mapping):
from botocore import __version__ as botocore_version
if "no_device" in device_mapping:
assert isinstance(
device_mapping["no_device"], str
), "botocore {} isn't limiting NoDevice to str type anymore, it is type:{}".format(
botocore_version, type(device_mapping["no_device"])
)
if device_mapping["no_device"] == "":
# the only legit value it can have is empty string
# and none of the other checks here matter if NoDevice
# is being used
return
else:
raise InvalidRequest()
if not any(mapping for mapping in device_mapping if mapping.startswith("ebs.")):
raise MissingParameterError("ebs")
if (
@ -349,6 +370,7 @@ class InstanceResponse(BaseResponse):
BLOCK_DEVICE_MAPPING_TEMPLATE = {
"VirtualName": None,
"DeviceName": None,
"NoDevice": None,
"Ebs": {
"SnapshotId": None,
"VolumeSize": None,

View File

@ -1,4 +1,4 @@
from botocore.exceptions import ClientError
from botocore.exceptions import ClientError, ParamValidationError
import pytest
from unittest import SkipTest
@ -174,6 +174,8 @@ def test_instance_terminate_keep_volumes_implicit():
for volume in instance.volumes.all():
instance_volume_ids.append(volume.volume_id)
instance_volume_ids.shouldnt.be.empty
instance.terminate()
instance.wait_until_terminated()
@ -1530,6 +1532,48 @@ def test_run_instance_with_block_device_mappings_missing_ebs():
)
@mock_ec2
def test_run_instance_with_block_device_mappings_using_no_device():
ec2_client = boto3.client("ec2", region_name="us-east-1")
kwargs = {
"MinCount": 1,
"MaxCount": 1,
"ImageId": EXAMPLE_AMI_ID,
"KeyName": "the_key",
"InstanceType": "t1.micro",
"BlockDeviceMappings": [{"DeviceName": "/dev/sda2", "NoDevice": ""}],
}
resp = ec2_client.run_instances(**kwargs)
instance_id = resp["Instances"][0]["InstanceId"]
instances = ec2_client.describe_instances(InstanceIds=[instance_id])
# Assuming that /dev/sda2 is not the root device and that there is a /dev/sda1, boto would
# create an instance with one block device instead of two. However, moto's modeling of
# BlockDeviceMappings is simplified, so we will accept that moto creates an instance without
# block devices for now
# instances["Reservations"][0]["Instances"][0].shouldnt.have.key("BlockDeviceMappings")
# moto gives the key with an empty list instead of not having it at all, that's also fine
instances["Reservations"][0]["Instances"][0]["BlockDeviceMappings"].should.be.empty
# passing None with NoDevice should raise ParamValidationError
kwargs["BlockDeviceMappings"][0]["NoDevice"] = None
with pytest.raises(ParamValidationError) as ex:
ec2_client.run_instances(**kwargs)
# passing a string other than "" with NoDevice should raise InvalidRequest
kwargs["BlockDeviceMappings"][0]["NoDevice"] = "yes"
with pytest.raises(ClientError) as ex:
ec2_client.run_instances(**kwargs)
ex.value.response["Error"]["Code"].should.equal("InvalidRequest")
ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.value.response["Error"]["Message"].should.equal(
"The request received was invalid"
)
@mock_ec2
def test_run_instance_with_block_device_mappings_missing_size():
ec2_client = boto3.client("ec2", region_name="us-east-1")