Add support for BlockDeviceMappings argument (#2949)
* Add support for BlockDeviceMappings argument upon run_instances execution * Remove redundant check for Ebs existence
This commit is contained in:
parent
9618e29ba9
commit
1e0a7380d5
@ -560,8 +560,10 @@ class Instance(TaggedEC2Resource, BotoInstance):
|
|||||||
# worst case we'll get IP address exaustion... rarely
|
# worst case we'll get IP address exaustion... rarely
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_block_device(self, size, device_path):
|
def add_block_device(self, size, device_path, snapshot_id=None, encrypted=False):
|
||||||
volume = self.ec2_backend.create_volume(size, self.region_name)
|
volume = self.ec2_backend.create_volume(
|
||||||
|
size, self.region_name, snapshot_id, encrypted
|
||||||
|
)
|
||||||
self.ec2_backend.attach_volume(volume.id, self.id, device_path)
|
self.ec2_backend.attach_volume(volume.id, self.id, device_path)
|
||||||
|
|
||||||
def setup_defaults(self):
|
def setup_defaults(self):
|
||||||
@ -891,8 +893,12 @@ class InstanceBackend(object):
|
|||||||
new_instance.add_tags(instance_tags)
|
new_instance.add_tags(instance_tags)
|
||||||
if "block_device_mappings" in kwargs:
|
if "block_device_mappings" in kwargs:
|
||||||
for block_device in kwargs["block_device_mappings"]:
|
for block_device in kwargs["block_device_mappings"]:
|
||||||
|
device_name = block_device["DeviceName"]
|
||||||
|
volume_size = block_device["Ebs"].get("VolumeSize")
|
||||||
|
snapshot_id = block_device["Ebs"].get("SnapshotId")
|
||||||
|
encrypted = block_device["Ebs"].get("Encrypted", False)
|
||||||
new_instance.add_block_device(
|
new_instance.add_block_device(
|
||||||
block_device["Ebs"]["VolumeSize"], block_device["DeviceName"]
|
volume_size, device_name, snapshot_id, encrypted
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
new_instance.setup_defaults()
|
new_instance.setup_defaults()
|
||||||
|
@ -4,10 +4,16 @@ from boto.ec2.instancetype import InstanceType
|
|||||||
from moto.autoscaling import autoscaling_backends
|
from moto.autoscaling import autoscaling_backends
|
||||||
from moto.core.responses import BaseResponse
|
from moto.core.responses import BaseResponse
|
||||||
from moto.core.utils import camelcase_to_underscores
|
from moto.core.utils import camelcase_to_underscores
|
||||||
from moto.ec2.utils import filters_from_querystring, dict_from_querystring
|
from moto.ec2.exceptions import MissingParameterError
|
||||||
|
from moto.ec2.utils import (
|
||||||
|
filters_from_querystring,
|
||||||
|
dict_from_querystring,
|
||||||
|
)
|
||||||
from moto.elbv2 import elbv2_backends
|
from moto.elbv2 import elbv2_backends
|
||||||
from moto.core import ACCOUNT_ID
|
from moto.core import ACCOUNT_ID
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
class InstanceResponse(BaseResponse):
|
class InstanceResponse(BaseResponse):
|
||||||
def describe_instances(self):
|
def describe_instances(self):
|
||||||
@ -44,40 +50,31 @@ class InstanceResponse(BaseResponse):
|
|||||||
owner_id = self._get_param("OwnerId")
|
owner_id = self._get_param("OwnerId")
|
||||||
user_data = self._get_param("UserData")
|
user_data = self._get_param("UserData")
|
||||||
security_group_names = self._get_multi_param("SecurityGroup")
|
security_group_names = self._get_multi_param("SecurityGroup")
|
||||||
security_group_ids = self._get_multi_param("SecurityGroupId")
|
kwargs = {
|
||||||
nics = dict_from_querystring("NetworkInterface", self.querystring)
|
"instance_type": self._get_param("InstanceType", if_none="m1.small"),
|
||||||
instance_type = self._get_param("InstanceType", if_none="m1.small")
|
"placement": self._get_param("Placement.AvailabilityZone"),
|
||||||
placement = self._get_param("Placement.AvailabilityZone")
|
"region_name": self.region,
|
||||||
subnet_id = self._get_param("SubnetId")
|
"subnet_id": self._get_param("SubnetId"),
|
||||||
private_ip = self._get_param("PrivateIpAddress")
|
"owner_id": owner_id,
|
||||||
associate_public_ip = self._get_param("AssociatePublicIpAddress")
|
"key_name": self._get_param("KeyName"),
|
||||||
key_name = self._get_param("KeyName")
|
"security_group_ids": self._get_multi_param("SecurityGroupId"),
|
||||||
ebs_optimized = self._get_param("EbsOptimized") or False
|
"nics": dict_from_querystring("NetworkInterface", self.querystring),
|
||||||
instance_initiated_shutdown_behavior = self._get_param(
|
"private_ip": self._get_param("PrivateIpAddress"),
|
||||||
|
"associate_public_ip": self._get_param("AssociatePublicIpAddress"),
|
||||||
|
"tags": self._parse_tag_specification("TagSpecification"),
|
||||||
|
"ebs_optimized": self._get_param("EbsOptimized") or False,
|
||||||
|
"instance_initiated_shutdown_behavior": self._get_param(
|
||||||
"InstanceInitiatedShutdownBehavior"
|
"InstanceInitiatedShutdownBehavior"
|
||||||
)
|
),
|
||||||
tags = self._parse_tag_specification("TagSpecification")
|
}
|
||||||
region_name = self.region
|
|
||||||
|
mappings = self._parse_block_device_mapping()
|
||||||
|
if mappings:
|
||||||
|
kwargs["block_device_mappings"] = mappings
|
||||||
|
|
||||||
if self.is_not_dryrun("RunInstance"):
|
if self.is_not_dryrun("RunInstance"):
|
||||||
new_reservation = self.ec2_backend.add_instances(
|
new_reservation = self.ec2_backend.add_instances(
|
||||||
image_id,
|
image_id, min_count, user_data, security_group_names, **kwargs
|
||||||
min_count,
|
|
||||||
user_data,
|
|
||||||
security_group_names,
|
|
||||||
instance_type=instance_type,
|
|
||||||
placement=placement,
|
|
||||||
region_name=region_name,
|
|
||||||
subnet_id=subnet_id,
|
|
||||||
owner_id=owner_id,
|
|
||||||
key_name=key_name,
|
|
||||||
security_group_ids=security_group_ids,
|
|
||||||
nics=nics,
|
|
||||||
private_ip=private_ip,
|
|
||||||
associate_public_ip=associate_public_ip,
|
|
||||||
tags=tags,
|
|
||||||
ebs_optimized=ebs_optimized,
|
|
||||||
instance_initiated_shutdown_behavior=instance_initiated_shutdown_behavior,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
template = self.response_template(EC2_RUN_INSTANCES)
|
template = self.response_template(EC2_RUN_INSTANCES)
|
||||||
@ -272,6 +269,58 @@ class InstanceResponse(BaseResponse):
|
|||||||
)
|
)
|
||||||
return EC2_MODIFY_INSTANCE_ATTRIBUTE
|
return EC2_MODIFY_INSTANCE_ATTRIBUTE
|
||||||
|
|
||||||
|
def _parse_block_device_mapping(self):
|
||||||
|
device_mappings = self._get_list_prefix("BlockDeviceMapping")
|
||||||
|
mappings = []
|
||||||
|
for device_mapping in device_mappings:
|
||||||
|
self._validate_block_device_mapping(device_mapping)
|
||||||
|
device_template = deepcopy(BLOCK_DEVICE_MAPPING_TEMPLATE)
|
||||||
|
device_template["VirtualName"] = device_mapping.get("virtual_name")
|
||||||
|
device_template["DeviceName"] = device_mapping.get("device_name")
|
||||||
|
device_template["Ebs"]["SnapshotId"] = device_mapping.get(
|
||||||
|
"ebs._snapshot_id"
|
||||||
|
)
|
||||||
|
device_template["Ebs"]["VolumeSize"] = device_mapping.get(
|
||||||
|
"ebs._volume_size"
|
||||||
|
)
|
||||||
|
device_template["Ebs"]["DeleteOnTermination"] = device_mapping.get(
|
||||||
|
"ebs._delete_on_termination", False
|
||||||
|
)
|
||||||
|
device_template["Ebs"]["VolumeType"] = device_mapping.get(
|
||||||
|
"ebs._volume_type"
|
||||||
|
)
|
||||||
|
device_template["Ebs"]["Iops"] = device_mapping.get("ebs._iops")
|
||||||
|
device_template["Ebs"]["Encrypted"] = device_mapping.get(
|
||||||
|
"ebs._encrypted", False
|
||||||
|
)
|
||||||
|
mappings.append(device_template)
|
||||||
|
|
||||||
|
return mappings
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_block_device_mapping(device_mapping):
|
||||||
|
|
||||||
|
if not any(mapping for mapping in device_mapping if mapping.startswith("ebs.")):
|
||||||
|
raise MissingParameterError("ebs")
|
||||||
|
if (
|
||||||
|
"ebs._volume_size" not in device_mapping
|
||||||
|
and "ebs._snapshot_id" not in device_mapping
|
||||||
|
):
|
||||||
|
raise MissingParameterError("size or snapshotId")
|
||||||
|
|
||||||
|
|
||||||
|
BLOCK_DEVICE_MAPPING_TEMPLATE = {
|
||||||
|
"VirtualName": None,
|
||||||
|
"DeviceName": None,
|
||||||
|
"Ebs": {
|
||||||
|
"SnapshotId": None,
|
||||||
|
"VolumeSize": None,
|
||||||
|
"DeleteOnTermination": None,
|
||||||
|
"VolumeType": None,
|
||||||
|
"Iops": None,
|
||||||
|
"Encrypted": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
EC2_RUN_INSTANCES = (
|
EC2_RUN_INSTANCES = (
|
||||||
"""<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
"""<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
|
||||||
|
@ -1126,6 +1126,111 @@ def test_run_instance_with_keypair():
|
|||||||
instance.key_name.should.equal("keypair_name")
|
instance.key_name.should.equal("keypair_name")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
def test_run_instance_with_block_device_mappings():
|
||||||
|
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
"MinCount": 1,
|
||||||
|
"MaxCount": 1,
|
||||||
|
"ImageId": "ami-d3adb33f",
|
||||||
|
"KeyName": "the_key",
|
||||||
|
"InstanceType": "t1.micro",
|
||||||
|
"BlockDeviceMappings": [{"DeviceName": "/dev/sda2", "Ebs": {"VolumeSize": 50}}],
|
||||||
|
}
|
||||||
|
|
||||||
|
ec2_client.run_instances(**kwargs)
|
||||||
|
|
||||||
|
instances = ec2_client.describe_instances()
|
||||||
|
volume = instances["Reservations"][0]["Instances"][0]["BlockDeviceMappings"][0][
|
||||||
|
"Ebs"
|
||||||
|
]
|
||||||
|
|
||||||
|
volumes = ec2_client.describe_volumes(VolumeIds=[volume["VolumeId"]])
|
||||||
|
volumes["Volumes"][0]["Size"].should.equal(50)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
def test_run_instance_with_block_device_mappings_missing_ebs():
|
||||||
|
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
"MinCount": 1,
|
||||||
|
"MaxCount": 1,
|
||||||
|
"ImageId": "ami-d3adb33f",
|
||||||
|
"KeyName": "the_key",
|
||||||
|
"InstanceType": "t1.micro",
|
||||||
|
"BlockDeviceMappings": [{"DeviceName": "/dev/sda2"}],
|
||||||
|
}
|
||||||
|
with assert_raises(ClientError) as ex:
|
||||||
|
ec2_client.run_instances(**kwargs)
|
||||||
|
|
||||||
|
ex.exception.response["Error"]["Code"].should.equal("MissingParameter")
|
||||||
|
ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.exception.response["Error"]["Message"].should.equal(
|
||||||
|
"The request must contain the parameter ebs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
def test_run_instance_with_block_device_mappings_missing_size():
|
||||||
|
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
"MinCount": 1,
|
||||||
|
"MaxCount": 1,
|
||||||
|
"ImageId": "ami-d3adb33f",
|
||||||
|
"KeyName": "the_key",
|
||||||
|
"InstanceType": "t1.micro",
|
||||||
|
"BlockDeviceMappings": [
|
||||||
|
{"DeviceName": "/dev/sda2", "Ebs": {"VolumeType": "standard"}}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with assert_raises(ClientError) as ex:
|
||||||
|
ec2_client.run_instances(**kwargs)
|
||||||
|
|
||||||
|
ex.exception.response["Error"]["Code"].should.equal("MissingParameter")
|
||||||
|
ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.exception.response["Error"]["Message"].should.equal(
|
||||||
|
"The request must contain the parameter size or snapshotId"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
def test_run_instance_with_block_device_mappings_from_snapshot():
|
||||||
|
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
||||||
|
ec2_resource = boto3.resource("ec2", region_name="us-east-1")
|
||||||
|
volume_details = {
|
||||||
|
"AvailabilityZone": "1a",
|
||||||
|
"Size": 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
volume = ec2_resource.create_volume(**volume_details)
|
||||||
|
snapshot = volume.create_snapshot()
|
||||||
|
kwargs = {
|
||||||
|
"MinCount": 1,
|
||||||
|
"MaxCount": 1,
|
||||||
|
"ImageId": "ami-d3adb33f",
|
||||||
|
"KeyName": "the_key",
|
||||||
|
"InstanceType": "t1.micro",
|
||||||
|
"BlockDeviceMappings": [
|
||||||
|
{"DeviceName": "/dev/sda2", "Ebs": {"SnapshotId": snapshot.snapshot_id}}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
ec2_client.run_instances(**kwargs)
|
||||||
|
|
||||||
|
instances = ec2_client.describe_instances()
|
||||||
|
volume = instances["Reservations"][0]["Instances"][0]["BlockDeviceMappings"][0][
|
||||||
|
"Ebs"
|
||||||
|
]
|
||||||
|
|
||||||
|
volumes = ec2_client.describe_volumes(VolumeIds=[volume["VolumeId"]])
|
||||||
|
|
||||||
|
volumes["Volumes"][0]["Size"].should.equal(30)
|
||||||
|
volumes["Volumes"][0]["SnapshotId"].should.equal(snapshot.snapshot_id)
|
||||||
|
|
||||||
|
|
||||||
@mock_ec2_deprecated
|
@mock_ec2_deprecated
|
||||||
def test_describe_instance_status_no_instances():
|
def test_describe_instance_status_no_instances():
|
||||||
conn = boto.connect_ec2("the_key", "the_secret")
|
conn = boto.connect_ec2("the_key", "the_secret")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user