Autoscaling - pass BlockDeviceMapping from launch_config/launch_template (#5044)

This commit is contained in:
Bert Blommers 2022-04-21 14:19:36 +00:00 committed by GitHub
parent f8c2b621db
commit 8da9666a90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 154 additions and 37 deletions

View File

@ -163,7 +163,8 @@ class FakeLaunchConfiguration(CloudFormationModel):
spot_price=None, spot_price=None,
ebs_optimized=instance.ebs_optimized, ebs_optimized=instance.ebs_optimized,
associate_public_ip_address=instance.associate_public_ip, associate_public_ip_address=instance.associate_public_ip,
block_device_mappings=instance.block_device_mapping, # We expect a dictionary in the same format as when the user calls it
block_device_mappings=instance.block_device_mapping.to_source_dict(),
) )
return config return config
@ -249,17 +250,16 @@ class FakeLaunchConfiguration(CloudFormationModel):
block_device_map = BlockDeviceMapping() block_device_map = BlockDeviceMapping()
for mapping in self.block_device_mapping_dict: for mapping in self.block_device_mapping_dict:
block_type = BlockDeviceType() block_type = BlockDeviceType()
mount_point = mapping.get("device_name") mount_point = mapping.get("DeviceName")
if "ephemeral" in mapping.get("virtual_name", ""): if mapping.get("VirtualName") and "ephemeral" in mapping.get("VirtualName"):
block_type.ephemeral_name = mapping.get("virtual_name") block_type.ephemeral_name = mapping.get("VirtualName")
else: else:
block_type.volume_type = mapping.get("ebs._volume_type") ebs = mapping.get("Ebs", {})
block_type.snapshot_id = mapping.get("ebs._snapshot_id") block_type.volume_type = ebs.get("VolumeType")
block_type.delete_on_termination = mapping.get( block_type.snapshot_id = ebs.get("SnapshotId")
"ebs._delete_on_termination" block_type.delete_on_termination = ebs.get("DeleteOnTermination")
) block_type.size = ebs.get("VolumeSize")
block_type.size = mapping.get("ebs._volume_size") block_type.iops = ebs.get("Iops")
block_type.iops = mapping.get("ebs._iops")
block_device_map[mount_point] = block_type block_device_map[mount_point] = block_type
return block_device_map return block_device_map
@ -620,6 +620,7 @@ class FakeAutoScalingGroup(CloudFormationModel):
instance_type=self.instance_type, instance_type=self.instance_type,
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,
) )
for instance in reservation.instances: for instance in reservation.instances:
instance.autoscaling_group = self instance.autoscaling_group = self

View File

@ -20,22 +20,23 @@ class AutoScalingResponse(BaseResponse):
instance_monitoring = True instance_monitoring = True
else: else:
instance_monitoring = False instance_monitoring = False
params = self._get_params()
self.autoscaling_backend.create_launch_configuration( self.autoscaling_backend.create_launch_configuration(
name=self._get_param("LaunchConfigurationName"), name=params.get("LaunchConfigurationName"),
image_id=self._get_param("ImageId"), image_id=params.get("ImageId"),
key_name=self._get_param("KeyName"), key_name=params.get("KeyName"),
ramdisk_id=self._get_param("RamdiskId"), ramdisk_id=params.get("RamdiskId"),
kernel_id=self._get_param("KernelId"), kernel_id=params.get("KernelId"),
security_groups=self._get_multi_param("SecurityGroups.member"), security_groups=self._get_multi_param("SecurityGroups.member"),
user_data=self._get_param("UserData"), user_data=params.get("UserData"),
instance_type=self._get_param("InstanceType"), instance_type=params.get("InstanceType"),
instance_monitoring=instance_monitoring, instance_monitoring=instance_monitoring,
instance_profile_name=self._get_param("IamInstanceProfile"), instance_profile_name=params.get("IamInstanceProfile"),
spot_price=self._get_param("SpotPrice"), spot_price=params.get("SpotPrice"),
ebs_optimized=self._get_param("EbsOptimized"), ebs_optimized=params.get("EbsOptimized"),
associate_public_ip_address=self._get_param("AssociatePublicIpAddress"), associate_public_ip_address=params.get("AssociatePublicIpAddress"),
block_device_mappings=self._get_list_prefix("BlockDeviceMappings.member"), block_device_mappings=params.get("BlockDeviceMappings"),
instance_id=self._get_param("InstanceId"), instance_id=params.get("InstanceId"),
) )
template = self.response_template(CREATE_LAUNCH_CONFIGURATION_TEMPLATE) template = self.response_template(CREATE_LAUNCH_CONFIGURATION_TEMPLATE)
return template.render() return template.render()

View File

@ -66,17 +66,8 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
launch_template_arg = kwargs.get("launch_template", {}) launch_template_arg = kwargs.get("launch_template", {})
if launch_template_arg and not image_id: if launch_template_arg and not image_id:
# the image id from the template should be used # the image id from the template should be used
template = ( template_version = ec2_backend._get_template_from_args(launch_template_arg)
ec2_backend.describe_launch_templates( self.image_id = template_version.image_id
template_ids=[launch_template_arg["LaunchTemplateId"]]
)[0]
if "LaunchTemplateId" in launch_template_arg
else ec2_backend.describe_launch_templates(
template_names=[launch_template_arg["LaunchTemplateName"]]
)[0]
)
version = launch_template_arg.get("Version", template.latest_version_number)
self.image_id = template.get_version(int(version)).image_id
else: else:
self.image_id = image_id self.image_id = image_id
@ -183,6 +174,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
encrypted=False, encrypted=False,
delete_on_termination=False, delete_on_termination=False,
kms_key_id=None, kms_key_id=None,
volume_type=None,
): ):
volume = self.ec2_backend.create_volume( volume = self.ec2_backend.create_volume(
size=size, size=size,
@ -190,6 +182,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
snapshot_id=snapshot_id, snapshot_id=snapshot_id,
encrypted=encrypted, encrypted=encrypted,
kms_key_id=kms_key_id, kms_key_id=kms_key_id,
volume_type=volume_type,
) )
self.ec2_backend.attach_volume( self.ec2_backend.attach_volume(
volume.id, self.id, device_path, delete_on_termination volume.id, self.id, device_path, delete_on_termination
@ -563,16 +556,30 @@ class InstanceBackend(object):
) )
new_reservation.instances.append(new_instance) new_reservation.instances.append(new_instance)
new_instance.add_tags(instance_tags) new_instance.add_tags(instance_tags)
block_device_mappings = None
if "block_device_mappings" in kwargs: if "block_device_mappings" in kwargs:
for block_device in kwargs["block_device_mappings"]: block_device_mappings = kwargs["block_device_mappings"]
elif kwargs.get("launch_template"):
template = self._get_template_from_args(kwargs["launch_template"])
block_device_mappings = template.data.get("BlockDeviceMapping")
elif kwargs.get("launch_config"):
block_device_mappings = kwargs[
"launch_config"
].block_device_mapping_dict
if block_device_mappings:
for block_device in block_device_mappings:
device_name = block_device["DeviceName"] device_name = block_device["DeviceName"]
volume_size = block_device["Ebs"].get("VolumeSize") volume_size = block_device["Ebs"].get("VolumeSize")
volume_type = block_device["Ebs"].get("VolumeType")
snapshot_id = block_device["Ebs"].get("SnapshotId") snapshot_id = block_device["Ebs"].get("SnapshotId")
encrypted = block_device["Ebs"].get("Encrypted", False) encrypted = block_device["Ebs"].get("Encrypted", False)
if isinstance(encrypted, str):
encrypted = encrypted.lower() == "true"
delete_on_termination = block_device["Ebs"].get( delete_on_termination = block_device["Ebs"].get(
"DeleteOnTermination", False "DeleteOnTermination", False
) )
kms_key_id = block_device["Ebs"].get("KmsKeyId") kms_key_id = block_device["Ebs"].get("KmsKeyId")
if block_device.get("NoDevice") != "": if block_device.get("NoDevice") != "":
new_instance.add_block_device( new_instance.add_block_device(
volume_size, volume_size,
@ -581,6 +588,7 @@ class InstanceBackend(object):
encrypted, encrypted,
delete_on_termination, delete_on_termination,
kms_key_id, kms_key_id,
volume_type=volume_type,
) )
else: else:
new_instance.setup_defaults() new_instance.setup_defaults()
@ -759,3 +767,17 @@ class InstanceBackend(object):
if filters is not None: if filters is not None:
reservations = filter_reservations(reservations, filters) reservations = filter_reservations(reservations, filters)
return reservations return reservations
def _get_template_from_args(self, launch_template_arg):
template = (
self.describe_launch_templates(
template_ids=[launch_template_arg["LaunchTemplateId"]]
)[0]
if "LaunchTemplateId" in launch_template_arg
else self.describe_launch_templates(
template_names=[launch_template_arg["LaunchTemplateName"]]
)[0]
)
version = launch_template_arg.get("Version", template.latest_version_number)
template_version = template.get_version(int(version))
return template_version

View File

@ -54,6 +54,7 @@ class BlockDeviceType(object):
self.volume_type = volume_type self.volume_type = volume_type
self.iops = iops self.iops = iops
self.encrypted = encrypted self.encrypted = encrypted
self.kms_key_id = None
# for backwards compatibility # for backwards compatibility
@ -81,3 +82,18 @@ class BlockDeviceMapping(dict):
self.connection = connection self.connection = connection
self.current_name = None self.current_name = None
self.current_value = None self.current_value = None
def to_source_dict(self):
return [
{
"DeviceName": device_name,
"Ebs": {
"DeleteOnTermination": block.delete_on_termination,
"Encrypted": block.encrypted,
"VolumeType": block.volume_type,
"VolumeSize": block.size,
},
"VirtualName": block.ephemeral_name,
}
for device_name, block in self.items()
]

View File

@ -2781,3 +2781,34 @@ def test_set_desired_capacity_without_protection(original, new):
group["DesiredCapacity"].should.equal(new) group["DesiredCapacity"].should.equal(new)
instances = client.describe_auto_scaling_instances()["AutoScalingInstances"] instances = client.describe_auto_scaling_instances()["AutoScalingInstances"]
instances.should.have.length_of(new) instances.should.have.length_of(new)
@mock_autoscaling
@mock_ec2
def test_create_template_with_block_device():
ec2_client = boto3.client("ec2", region_name="ap-southeast-2")
ec2_client.create_launch_template(
LaunchTemplateName="launchie",
LaunchTemplateData={
"ImageId": EXAMPLE_AMI_ID,
"BlockDeviceMappings": [
{
"DeviceName": "/dev/sda1",
"Ebs": {
"VolumeSize": 20,
"DeleteOnTermination": True,
"VolumeType": "gp3",
"Encrypted": True,
},
}
],
},
)
ec2_client.run_instances(
MaxCount=1, MinCount=1, LaunchTemplate={"LaunchTemplateName": "launchie"}
)
ec2_client = boto3.client("ec2", region_name="ap-southeast-2")
volumes = ec2_client.describe_volumes()["Volumes"]
volumes[0]["VolumeType"].should.equal("gp3")
volumes[0]["Size"].should.equal(20)

View File

@ -5,7 +5,7 @@ from botocore.exceptions import ClientError
import pytest import pytest
import sure # noqa # pylint: disable=unused-import import sure # noqa # pylint: disable=unused-import
from moto import mock_autoscaling from moto import mock_autoscaling, mock_ec2
from moto.core import ACCOUNT_ID from moto.core import ACCOUNT_ID
from tests import EXAMPLE_AMI_ID from tests import EXAMPLE_AMI_ID
@ -245,3 +245,49 @@ def test_invalid_launch_configuration_request_raises_error(request_params):
ex.value.response["Error"]["Message"].should.match( ex.value.response["Error"]["Message"].should.match(
r"^Valid requests must contain.*" r"^Valid requests must contain.*"
) )
@mock_autoscaling
@mock_ec2
def test_launch_config_with_block_device_mappings__volumes_are_created():
as_client = boto3.client("autoscaling", "us-east-2")
ec2_client = boto3.client("ec2", "us-east-2")
random_image_id = ec2_client.describe_images()["Images"][0]["ImageId"]
as_client.create_launch_configuration(
LaunchConfigurationName=f"lc-{random_image_id}",
ImageId=random_image_id,
InstanceType="t2.nano",
BlockDeviceMappings=[
{
"DeviceName": "/dev/sdf",
"Ebs": {
"VolumeSize": 10,
"VolumeType": "standard",
"Encrypted": False,
"DeleteOnTermination": True,
},
}
],
)
asg_name = f"asg-{random_image_id}"
as_client.create_auto_scaling_group(
AutoScalingGroupName=asg_name,
LaunchConfigurationName=f"lc-{random_image_id}",
MinSize=1,
MaxSize=1,
DesiredCapacity=1,
AvailabilityZones=["us-east-2b"],
)
instances = as_client.describe_auto_scaling_instances()["AutoScalingInstances"]
instance_id = instances[0]["InstanceId"]
volumes = ec2_client.describe_volumes(
Filters=[{"Name": "attachment.instance-id", "Values": [instance_id]}]
)["Volumes"]
volumes.should.have.length_of(1)
volumes[0].should.have.key("Size").equals(10)
volumes[0].should.have.key("Encrypted").equals(False)
volumes[0].should.have.key("VolumeType").equals("standard")