Add support for Launch Templates in Auto Scaling Groups (#3236)

* Add support for Launch Templates in Auto Scaling Groups

* Use named parameters, simplify parameter validation
This commit is contained in:
Kevin Frommelt 2020-08-26 09:15:07 -05:00 committed by GitHub
commit 55b02c6ee9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 774 additions and 42 deletions

View file

@ -21,3 +21,8 @@ class InvalidInstanceError(AutoscalingClientError):
super(InvalidInstanceError, self).__init__(
"ValidationError", "Instance [{0}] is invalid.".format(instance_id)
)
class ValidationError(AutoscalingClientError):
def __init__(self, message):
super(ValidationError, self).__init__("ValidationError", message)

View file

@ -7,6 +7,7 @@ from moto.ec2.exceptions import InvalidInstanceIdError
from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel, CloudFormationModel
from moto.core.utils import camelcase_to_underscores
from moto.ec2 import ec2_backends
from moto.elb import elb_backends
from moto.elbv2 import elbv2_backends
@ -15,6 +16,7 @@ from .exceptions import (
AutoscalingClientError,
ResourceContentionError,
InvalidInstanceError,
ValidationError,
)
# http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/AS_Concepts.html#Cooldown
@ -233,6 +235,7 @@ class FakeAutoScalingGroup(CloudFormationModel):
max_size,
min_size,
launch_config_name,
launch_template,
vpc_zone_identifier,
default_cooldown,
health_check_period,
@ -242,10 +245,12 @@ class FakeAutoScalingGroup(CloudFormationModel):
placement_group,
termination_policies,
autoscaling_backend,
ec2_backend,
tags,
new_instances_protected_from_scale_in=False,
):
self.autoscaling_backend = autoscaling_backend
self.ec2_backend = ec2_backend
self.name = name
self._set_azs_and_vpcs(availability_zones, vpc_zone_identifier)
@ -253,10 +258,10 @@ class FakeAutoScalingGroup(CloudFormationModel):
self.max_size = max_size
self.min_size = min_size
self.launch_config = self.autoscaling_backend.launch_configurations[
launch_config_name
]
self.launch_config_name = launch_config_name
self.launch_template = None
self.launch_config = None
self._set_launch_configuration(launch_config_name, launch_template)
self.default_cooldown = (
default_cooldown if default_cooldown else DEFAULT_COOLDOWN
@ -310,6 +315,34 @@ class FakeAutoScalingGroup(CloudFormationModel):
self.availability_zones = availability_zones
self.vpc_zone_identifier = vpc_zone_identifier
def _set_launch_configuration(self, launch_config_name, launch_template):
if launch_config_name:
self.launch_config = self.autoscaling_backend.launch_configurations[
launch_config_name
]
self.launch_config_name = launch_config_name
if launch_template:
launch_template_id = launch_template.get("launch_template_id")
launch_template_name = launch_template.get("launch_template_name")
if not (launch_template_id or launch_template_name) or (
launch_template_id and launch_template_name
):
raise ValidationError(
"Valid requests must contain either launchTemplateId or LaunchTemplateName"
)
if launch_template_id:
self.launch_template = self.ec2_backend.get_launch_template(
launch_template_id
)
elif launch_template_name:
self.launch_template = self.ec2_backend.get_launch_template_by_name(
launch_template_name
)
self.launch_template_version = int(launch_template["version"])
@staticmethod
def __set_string_propagate_at_launch_booleans_on_tags(tags):
bool_to_string = {True: "true", False: "false"}
@ -334,6 +367,10 @@ class FakeAutoScalingGroup(CloudFormationModel):
properties = cloudformation_json["Properties"]
launch_config_name = properties.get("LaunchConfigurationName")
launch_template = {
camelcase_to_underscores(k): v
for k, v in properties.get("LaunchTemplate", {}).items()
}
load_balancer_names = properties.get("LoadBalancerNames", [])
target_group_arns = properties.get("TargetGroupARNs", [])
@ -345,6 +382,7 @@ class FakeAutoScalingGroup(CloudFormationModel):
max_size=properties.get("MaxSize"),
min_size=properties.get("MinSize"),
launch_config_name=launch_config_name,
launch_template=launch_template,
vpc_zone_identifier=(
",".join(properties.get("VPCZoneIdentifier", [])) or None
),
@ -393,6 +431,38 @@ class FakeAutoScalingGroup(CloudFormationModel):
def physical_resource_id(self):
return self.name
@property
def image_id(self):
if self.launch_template:
version = self.launch_template.get_version(self.launch_template_version)
return version.image_id
return self.launch_config.image_id
@property
def instance_type(self):
if self.launch_template:
version = self.launch_template.get_version(self.launch_template_version)
return version.instance_type
return self.launch_config.instance_type
@property
def user_data(self):
if self.launch_template:
version = self.launch_template.get_version(self.launch_template_version)
return version.user_data
return self.launch_config.user_data
@property
def security_groups(self):
if self.launch_template:
version = self.launch_template.get_version(self.launch_template_version)
return version.security_groups
return self.launch_config.security_groups
def update(
self,
availability_zones,
@ -400,6 +470,7 @@ class FakeAutoScalingGroup(CloudFormationModel):
max_size,
min_size,
launch_config_name,
launch_template,
vpc_zone_identifier,
default_cooldown,
health_check_period,
@ -421,11 +492,8 @@ class FakeAutoScalingGroup(CloudFormationModel):
if max_size is not None and max_size < len(self.instance_states):
desired_capacity = max_size
if launch_config_name:
self.launch_config = self.autoscaling_backend.launch_configurations[
launch_config_name
]
self.launch_config_name = launch_config_name
self._set_launch_configuration(launch_config_name, launch_template)
if health_check_period is not None:
self.health_check_period = health_check_period
if health_check_type is not None:
@ -489,12 +557,13 @@ class FakeAutoScalingGroup(CloudFormationModel):
def replace_autoscaling_group_instances(self, count_needed, propagated_tags):
propagated_tags[ASG_NAME_TAG] = self.name
reservation = self.autoscaling_backend.ec2_backend.add_instances(
self.launch_config.image_id,
self.image_id,
count_needed,
self.launch_config.user_data,
self.launch_config.security_groups,
instance_type=self.launch_config.instance_type,
self.user_data,
self.security_groups,
instance_type=self.instance_type,
tags={"instance": propagated_tags},
placement=random.choice(self.availability_zones),
)
@ -586,6 +655,7 @@ class AutoScalingBackend(BaseBackend):
max_size,
min_size,
launch_config_name,
launch_template,
vpc_zone_identifier,
default_cooldown,
health_check_period,
@ -609,7 +679,19 @@ class AutoScalingBackend(BaseBackend):
health_check_period = 300
else:
health_check_period = make_int(health_check_period)
if launch_config_name is None and instance_id is not None:
# TODO: Add MixedInstancesPolicy once implemented.
# Verify only a single launch config-like parameter is provided.
params = [launch_config_name, launch_template, instance_id]
num_params = sum([1 for param in params if param])
if num_params != 1:
raise ValidationError(
"Valid requests must contain either LaunchTemplate, LaunchConfigurationName, "
"InstanceId or MixedInstancesPolicy parameter."
)
if instance_id:
try:
instance = self.ec2_backend.get_instance(instance_id)
launch_config_name = name
@ -626,6 +708,7 @@ class AutoScalingBackend(BaseBackend):
max_size=max_size,
min_size=min_size,
launch_config_name=launch_config_name,
launch_template=launch_template,
vpc_zone_identifier=vpc_zone_identifier,
default_cooldown=default_cooldown,
health_check_period=health_check_period,
@ -635,6 +718,7 @@ class AutoScalingBackend(BaseBackend):
placement_group=placement_group,
termination_policies=termination_policies,
autoscaling_backend=self,
ec2_backend=self.ec2_backend,
tags=tags,
new_instances_protected_from_scale_in=new_instances_protected_from_scale_in,
)
@ -652,6 +736,7 @@ class AutoScalingBackend(BaseBackend):
max_size,
min_size,
launch_config_name,
launch_template,
vpc_zone_identifier,
default_cooldown,
health_check_period,
@ -660,19 +745,28 @@ class AutoScalingBackend(BaseBackend):
termination_policies,
new_instances_protected_from_scale_in=None,
):
# TODO: Add MixedInstancesPolicy once implemented.
# Verify only a single launch config-like parameter is provided.
if launch_config_name and launch_template:
raise ValidationError(
"Valid requests must contain either LaunchTemplate, LaunchConfigurationName "
"or MixedInstancesPolicy parameter."
)
group = self.autoscaling_groups[name]
group.update(
availability_zones,
desired_capacity,
max_size,
min_size,
launch_config_name,
vpc_zone_identifier,
default_cooldown,
health_check_period,
health_check_type,
placement_group,
termination_policies,
availability_zones=availability_zones,
desired_capacity=desired_capacity,
max_size=max_size,
min_size=min_size,
launch_config_name=launch_config_name,
launch_template=launch_template,
vpc_zone_identifier=vpc_zone_identifier,
default_cooldown=default_cooldown,
health_check_period=health_check_period,
health_check_type=health_check_type,
placement_group=placement_group,
termination_policies=termination_policies,
new_instances_protected_from_scale_in=new_instances_protected_from_scale_in,
)
return group

View file

@ -81,6 +81,7 @@ class AutoScalingResponse(BaseResponse):
min_size=self._get_int_param("MinSize"),
instance_id=self._get_param("InstanceId"),
launch_config_name=self._get_param("LaunchConfigurationName"),
launch_template=self._get_dict_param("LaunchTemplate."),
vpc_zone_identifier=self._get_param("VPCZoneIdentifier"),
default_cooldown=self._get_int_param("DefaultCooldown"),
health_check_period=self._get_int_param("HealthCheckGracePeriod"),
@ -197,6 +198,7 @@ class AutoScalingResponse(BaseResponse):
max_size=self._get_int_param("MaxSize"),
min_size=self._get_int_param("MinSize"),
launch_config_name=self._get_param("LaunchConfigurationName"),
launch_template=self._get_dict_param("LaunchTemplate."),
vpc_zone_identifier=self._get_param("VPCZoneIdentifier"),
default_cooldown=self._get_int_param("DefaultCooldown"),
health_check_period=self._get_int_param("HealthCheckGracePeriod"),
@ -573,14 +575,31 @@ DESCRIBE_AUTOSCALING_GROUPS_TEMPLATE = """<DescribeAutoScalingGroupsResponse xml
<HealthCheckType>{{ group.health_check_type }}</HealthCheckType>
<CreatedTime>2013-05-06T17:47:15.107Z</CreatedTime>
<EnabledMetrics/>
{% if group.launch_config_name %}
<LaunchConfigurationName>{{ group.launch_config_name }}</LaunchConfigurationName>
{% elif group.launch_template %}
<LaunchTemplate>
<LaunchTemplateId>{{ group.launch_template.id }}</LaunchTemplateId>
<Version>{{ group.launch_template_version }}</Version>
<LaunchTemplateName>{{ group.launch_template.name }}</LaunchTemplateName>
</LaunchTemplate>
{% endif %}
<Instances>
{% for instance_state in group.instance_states %}
<member>
<HealthStatus>{{ instance_state.health_status }}</HealthStatus>
<AvailabilityZone>{{ instance_state.instance.placement }}</AvailabilityZone>
<InstanceId>{{ instance_state.instance.id }}</InstanceId>
<InstanceType>{{ instance_state.instance.instance_type }}</InstanceType>
{% if group.launch_config_name %}
<LaunchConfigurationName>{{ group.launch_config_name }}</LaunchConfigurationName>
{% elif group.launch_template %}
<LaunchTemplate>
<LaunchTemplateId>{{ group.launch_template.id }}</LaunchTemplateId>
<Version>{{ group.launch_template_version }}</Version>
<LaunchTemplateName>{{ group.launch_template.name }}</LaunchTemplateName>
</LaunchTemplate>
{% endif %}
<LifecycleState>{{ instance_state.lifecycle_state }}</LifecycleState>
<ProtectedFromScaleIn>{{ instance_state.protected_from_scale_in|string|lower }}</ProtectedFromScaleIn>
</member>
@ -666,7 +685,16 @@ DESCRIBE_AUTOSCALING_INSTANCES_TEMPLATE = """<DescribeAutoScalingInstancesRespon
<AutoScalingGroupName>{{ instance_state.instance.autoscaling_group.name }}</AutoScalingGroupName>
<AvailabilityZone>{{ instance_state.instance.placement }}</AvailabilityZone>
<InstanceId>{{ instance_state.instance.id }}</InstanceId>
<InstanceType>{{ instance_state.instance.instance_type }}</InstanceType>
{% if instance_state.instance.autoscaling_group.launch_config_name %}
<LaunchConfigurationName>{{ instance_state.instance.autoscaling_group.launch_config_name }}</LaunchConfigurationName>
{% elif instance_state.instance.autoscaling_group.launch_template %}
<LaunchTemplate>
<LaunchTemplateId>{{ instance_state.instance.autoscaling_group.launch_template.id }}</LaunchTemplateId>
<Version>{{ instance_state.instance.autoscaling_group.launch_template_version }}</Version>
<LaunchTemplateName>{{ instance_state.instance.autoscaling_group.launch_template.name }}</LaunchTemplateName>
</LaunchTemplate>
{% endif %}
<LifecycleState>{{ instance_state.lifecycle_state }}</LifecycleState>
<ProtectedFromScaleIn>{{ instance_state.protected_from_scale_in|string|lower }}</ProtectedFromScaleIn>
</member>

View file

@ -538,8 +538,8 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
returns
{
"SlaveInstanceType": "m1.small",
"InstanceCount": "1",
"slave_instance_type": "m1.small",
"instance_count": "1",
}
"""
params = {}

View file

@ -5386,6 +5386,22 @@ class LaunchTemplateVersion(object):
self.description = description
self.create_time = utc_date_and_time()
@property
def image_id(self):
return self.data.get("ImageId", "")
@property
def instance_type(self):
return self.data.get("InstanceType", "")
@property
def security_groups(self):
return self.data.get("SecurityGroups", [])
@property
def user_data(self):
return self.data.get("UserData", "")
class LaunchTemplate(TaggedEC2Resource):
def __init__(self, backend, name, template_data, version_description):