From d745dfd3d2eff02b628bd021374810a86ea73d4d Mon Sep 17 00:00:00 2001 From: DenverJ Date: Mon, 13 Apr 2020 10:50:01 +1000 Subject: [PATCH] Implement enter_standby, exit_standby and terminate_instance_in_auto_scaling_group --- moto/autoscaling/models.py | 67 ++- moto/autoscaling/responses.py | 144 ++++- tests/test_autoscaling/test_autoscaling.py | 584 ++++++++++++++++++++- 3 files changed, 777 insertions(+), 18 deletions(-) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 88577433e..b757672d0 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -267,6 +267,9 @@ class FakeAutoScalingGroup(BaseModel): self.tags = tags if tags else [] self.set_desired_capacity(desired_capacity) + def active_instances(self): + return [x for x in self.instance_states if x.lifecycle_state == "InService"] + def _set_azs_and_vpcs(self, availability_zones, vpc_zone_identifier, update=False): # for updates, if only AZs are provided, they must not clash with # the AZs of existing VPCs @@ -413,9 +416,11 @@ class FakeAutoScalingGroup(BaseModel): else: self.desired_capacity = new_capacity - curr_instance_count = len(self.instance_states) + curr_instance_count = len(self.active_instances()) if self.desired_capacity == curr_instance_count: + self.autoscaling_backend.update_attached_elbs(self.name) + self.autoscaling_backend.update_attached_target_groups(self.name) return if self.desired_capacity > curr_instance_count: @@ -442,6 +447,8 @@ class FakeAutoScalingGroup(BaseModel): self.instance_states = list( set(self.instance_states) - set(instances_to_remove) ) + self.autoscaling_backend.update_attached_elbs(self.name) + self.autoscaling_backend.update_attached_target_groups(self.name) def get_propagated_tags(self): propagated_tags = {} @@ -703,7 +710,7 @@ class AutoScalingBackend(BaseBackend): def detach_instances(self, group_name, instance_ids, should_decrement): group = self.autoscaling_groups[group_name] - original_size = len(group.instance_states) + original_size = group.desired_capacity detached_instances = [ x for x in group.instance_states if x.instance.id in instance_ids @@ -720,13 +727,8 @@ class AutoScalingBackend(BaseBackend): if should_decrement: group.desired_capacity = original_size - len(instance_ids) - else: - count_needed = len(instance_ids) - group.replace_autoscaling_group_instances( - count_needed, group.get_propagated_tags() - ) - self.update_attached_elbs(group_name) + group.set_desired_capacity(group.desired_capacity) return detached_instances def set_desired_capacity(self, group_name, desired_capacity): @@ -791,7 +793,9 @@ class AutoScalingBackend(BaseBackend): def update_attached_elbs(self, group_name): group = self.autoscaling_groups[group_name] - group_instance_ids = set(state.instance.id for state in group.instance_states) + group_instance_ids = set( + state.instance.id for state in group.active_instances() + ) # skip this if group.load_balancers is empty # otherwise elb_backend.describe_load_balancers returns all available load balancers @@ -908,15 +912,15 @@ class AutoScalingBackend(BaseBackend): autoscaling_group_name, autoscaling_group, ) in self.autoscaling_groups.items(): - original_instance_count = len(autoscaling_group.instance_states) + original_active_instance_count = len(autoscaling_group.active_instances()) autoscaling_group.instance_states = list( filter( lambda i_state: i_state.instance.id not in instance_ids, autoscaling_group.instance_states, ) ) - difference = original_instance_count - len( - autoscaling_group.instance_states + difference = original_active_instance_count - len( + autoscaling_group.active_instances() ) if difference > 0: autoscaling_group.replace_autoscaling_group_instances( @@ -924,6 +928,45 @@ class AutoScalingBackend(BaseBackend): ) self.update_attached_elbs(autoscaling_group_name) + def enter_standby_instances(self, group_name, instance_ids, should_decrement): + group = self.autoscaling_groups[group_name] + original_size = group.desired_capacity + standby_instances = [] + for instance_state in group.instance_states: + if instance_state.instance.id in instance_ids: + instance_state.lifecycle_state = "Standby" + standby_instances.append(instance_state) + if should_decrement: + group.desired_capacity = group.desired_capacity - len(instance_ids) + else: + group.set_desired_capacity(group.desired_capacity) + return standby_instances, original_size, group.desired_capacity + + def exit_standby_instances(self, group_name, instance_ids): + group = self.autoscaling_groups[group_name] + original_size = group.desired_capacity + standby_instances = [] + for instance_state in group.instance_states: + if instance_state.instance.id in instance_ids: + instance_state.lifecycle_state = "InService" + standby_instances.append(instance_state) + group.desired_capacity = group.desired_capacity + len(instance_ids) + return standby_instances, original_size, group.desired_capacity + + def terminate_instance(self, instance_id, should_decrement): + instance = self.ec2_backend.get_instance(instance_id) + instance_state = next( + instance_state + for group in self.autoscaling_groups.values() + for instance_state in group.instance_states + if instance_state.instance.id == instance.id + ) + group = instance.autoscaling_group + original_size = group.desired_capacity + self.detach_instances(group.name, [instance.id], should_decrement) + self.ec2_backend.terminate_instances([instance.id]) + return instance_state, original_size, group.desired_capacity + autoscaling_backends = {} for region, ec2_backend in ec2_backends.items(): diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py index 41c79edb4..06b68aa4b 100644 --- a/moto/autoscaling/responses.py +++ b/moto/autoscaling/responses.py @@ -1,7 +1,12 @@ from __future__ import unicode_literals +import datetime from moto.core.responses import BaseResponse -from moto.core.utils import amz_crc32, amzn_request_id +from moto.core.utils import ( + amz_crc32, + amzn_request_id, + iso_8601_datetime_with_milliseconds, +) from .models import autoscaling_backends @@ -291,6 +296,50 @@ class AutoScalingResponse(BaseResponse): template = self.response_template(DETACH_LOAD_BALANCERS_TEMPLATE) return template.render() + @amz_crc32 + @amzn_request_id + def enter_standby(self): + group_name = self._get_param("AutoScalingGroupName") + instance_ids = self._get_multi_param("InstanceIds.member") + should_decrement_string = self._get_param("ShouldDecrementDesiredCapacity") + if should_decrement_string == "true": + should_decrement = True + else: + should_decrement = False + ( + standby_instances, + original_size, + desired_capacity, + ) = self.autoscaling_backend.enter_standby_instances( + group_name, instance_ids, should_decrement + ) + template = self.response_template(ENTER_STANDBY_TEMPLATE) + return template.render( + standby_instances=standby_instances, + should_decrement=should_decrement, + original_size=original_size, + desired_capacity=desired_capacity, + timestamp=iso_8601_datetime_with_milliseconds(datetime.datetime.utcnow()), + ) + + @amz_crc32 + @amzn_request_id + def exit_standby(self): + group_name = self._get_param("AutoScalingGroupName") + instance_ids = self._get_multi_param("InstanceIds.member") + ( + standby_instances, + original_size, + desired_capacity, + ) = self.autoscaling_backend.exit_standby_instances(group_name, instance_ids) + template = self.response_template(EXIT_STANDBY_TEMPLATE) + return template.render( + standby_instances=standby_instances, + original_size=original_size, + desired_capacity=desired_capacity, + timestamp=iso_8601_datetime_with_milliseconds(datetime.datetime.utcnow()), + ) + def suspend_processes(self): autoscaling_group_name = self._get_param("AutoScalingGroupName") scaling_processes = self._get_multi_param("ScalingProcesses.member") @@ -310,6 +359,29 @@ class AutoScalingResponse(BaseResponse): template = self.response_template(SET_INSTANCE_PROTECTION_TEMPLATE) return template.render() + @amz_crc32 + @amzn_request_id + def terminate_instance_in_auto_scaling_group(self): + instance_id = self._get_param("InstanceId") + should_decrement_string = self._get_param("ShouldDecrementDesiredCapacity") + if should_decrement_string == "true": + should_decrement = True + else: + should_decrement = False + ( + instance, + original_size, + desired_capacity, + ) = self.autoscaling_backend.terminate_instance(instance_id, should_decrement) + template = self.response_template(TERMINATE_INSTANCES_TEMPLATE) + return template.render( + instance=instance, + should_decrement=should_decrement, + original_size=original_size, + desired_capacity=desired_capacity, + timestamp=iso_8601_datetime_with_milliseconds(datetime.datetime.utcnow()), + ) + CREATE_LAUNCH_CONFIGURATION_TEMPLATE = """ @@ -707,3 +779,73 @@ SET_INSTANCE_PROTECTION_TEMPLATE = """ + + + {% for instance in standby_instances %} + + 12345678-1234-1234-1234-123456789012 + {{ group_name }} + {% if should_decrement %} + At {{ timestamp }} instance {{ instance.instance.id }} was moved to standby in response to a user request, shrinking the capacity from {{ original_size }} to {{ desired_capacity }}. + {% else %} + At {{ timestamp }} instance {{ instance.instance.id }} was moved to standby in response to a user request. + {% endif %} + Moving EC2 instance to Standby: {{ instance.instance.id }} + 50 + {{ timestamp }} +
{"Subnet ID":"??","Availability Zone":"{{ instance.instance.placement }}"}
+ InProgress +
+ {% endfor %} +
+
+ + 7c6e177f-f082-11e1-ac58-3714bEXAMPLE + +""" + +EXIT_STANDBY_TEMPLATE = """ + + + {% for instance in standby_instances %} + + 12345678-1234-1234-1234-123456789012 + {{ group_name }} + Moving EC2 instance out of Standby: {{ instance.instance.id }} + 30 + At {{ timestamp }} instance {{ instance.instance.id }} was moved out of standby in response to a user request, increasing the capacity from {{ original_size }} to {{ desired_capacity }}. + {{ timestamp }} +
{"Subnet ID":"??","Availability Zone":"{{ instance.instance.placement }}"}
+ PreInService +
+ {% endfor %} +
+
+ + 7c6e177f-f082-11e1-ac58-3714bEXAMPLE + +
""" + +TERMINATE_INSTANCES_TEMPLATE = """ + + + 35b5c464-0b63-2fc7-1611-467d4a7f2497EXAMPLE + {{ group_name }} + {% if should_decrement %} + At {{ timestamp }} instance {{ instance.instance.id }} was taken out of service in response to a user request, shrinking the capacity from {{ original_size }} to {{ desired_capacity }}. + {% else %} + At {{ timestamp }} instance {{ instance.instance.id }} was taken out of service in response to a user request. + {% endif %} + Terminating EC2 instance: {{ instance.instance.id }} + 0 + {{ timestamp }} +
{"Subnet ID":"??","Availability Zone":"{{ instance.instance.placement }}"}
+ InProgress +
+
+ + a1ba8fb9-31d6-4d9a-ace1-a7f76749df11EXAMPLE + +
""" diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index 5cf3dc6ff..3a10f20ff 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -1102,8 +1102,6 @@ def test_detach_one_instance_decrement(): ec2_client = boto3.client("ec2", region_name="us-east-1") - response = ec2_client.describe_instances(InstanceIds=[instance_to_detach]) - response = client.detach_instances( AutoScalingGroupName="test_asg", InstanceIds=[instance_to_detach], @@ -1156,8 +1154,6 @@ def test_detach_one_instance(): ec2_client = boto3.client("ec2", region_name="us-east-1") - response = ec2_client.describe_instances(InstanceIds=[instance_to_detach]) - response = client.detach_instances( AutoScalingGroupName="test_asg", InstanceIds=[instance_to_detach], @@ -1178,6 +1174,516 @@ def test_detach_one_instance(): tags.should.have.length_of(2) +@mock_autoscaling +@mock_ec2 +def test_standby_one_instance_decrement(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=2, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby = response["AutoScalingGroups"][0]["Instances"][0]["InstanceId"] + instance_to_keep = response["AutoScalingGroups"][0]["Instances"][1]["InstanceId"] + + ec2_client = boto3.client("ec2", region_name="us-east-1") + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby], + ShouldDecrementDesiredCapacity=True, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(2) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(1) + + response = client.describe_auto_scaling_instances(InstanceIds=[instance_to_standby]) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + # test to ensure tag has been retained (standby instance is still part of the ASG) + response = ec2_client.describe_instances() + for reservation in response["Reservations"]: + for instance in reservation["Instances"]: + tags = instance["Tags"] + tags.should.have.length_of(2) + + +@mock_autoscaling +@mock_ec2 +def test_standby_one_instance(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=2, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby = response["AutoScalingGroups"][0]["Instances"][0]["InstanceId"] + instance_to_keep = response["AutoScalingGroups"][0]["Instances"][1]["InstanceId"] + + ec2_client = boto3.client("ec2", region_name="us-east-1") + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(3) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(2) + + response = client.describe_auto_scaling_instances(InstanceIds=[instance_to_standby]) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + # test to ensure tag has been retained (standby instance is still part of the ASG) + response = ec2_client.describe_instances() + for reservation in response["Reservations"]: + for instance in reservation["Instances"]: + tags = instance["Tags"] + tags.should.have.length_of(2) + + +@mock_elb +@mock_autoscaling +@mock_ec2 +def test_standby_elb_update(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=2, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + + elb_client = boto3.client("elb", region_name="us-east-1") + elb_client.create_load_balancer( + LoadBalancerName="my-lb", + Listeners=[{"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080}], + AvailabilityZones=["us-east-1a", "us-east-1b"], + ) + + response = client.attach_load_balancers( + AutoScalingGroupName="test_asg", LoadBalancerNames=["my-lb"] + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby = response["AutoScalingGroups"][0]["Instances"][0]["InstanceId"] + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(3) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(2) + + response = client.describe_auto_scaling_instances(InstanceIds=[instance_to_standby]) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + response = elb_client.describe_load_balancers(LoadBalancerNames=["my-lb"]) + list(response["LoadBalancerDescriptions"][0]["Instances"]).should.have.length_of(2) + + +@mock_autoscaling +@mock_ec2 +def test_standby_terminate_instance_decrement(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=3, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby_terminate = response["AutoScalingGroups"][0]["Instances"][0][ + "InstanceId" + ] + + ec2_client = boto3.client("ec2", region_name="us-east-1") + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby_terminate], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(3) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(2) + + response = client.describe_auto_scaling_instances( + InstanceIds=[instance_to_standby_terminate] + ) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + response = client.terminate_instance_in_auto_scaling_group( + InstanceId=instance_to_standby_terminate, ShouldDecrementDesiredCapacity=True + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + # AWS still decrements desired capacity ASG if requested, even if the terminated instance is in standby + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(1) + response["AutoScalingGroups"][0]["Instances"][0]["InstanceId"].should_not.equal( + instance_to_standby_terminate + ) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(1) + + response = ec2_client.describe_instances( + InstanceIds=[instance_to_standby_terminate] + ) + response["Reservations"][0]["Instances"][0]["State"]["Name"].should.equal( + "terminated" + ) + + +@mock_autoscaling +@mock_ec2 +def test_standby_terminate_instance_no_decrement(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=3, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby_terminate = response["AutoScalingGroups"][0]["Instances"][0][ + "InstanceId" + ] + + ec2_client = boto3.client("ec2", region_name="us-east-1") + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby_terminate], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(3) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(2) + + response = client.describe_auto_scaling_instances( + InstanceIds=[instance_to_standby_terminate] + ) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + response = client.terminate_instance_in_auto_scaling_group( + InstanceId=instance_to_standby_terminate, ShouldDecrementDesiredCapacity=False + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + group = response["AutoScalingGroups"][0] + group["Instances"].should.have.length_of(2) + instance_to_standby_terminate.shouldnt.be.within( + [x["InstanceId"] for x in group["Instances"]] + ) + group["DesiredCapacity"].should.equal(2) + + response = ec2_client.describe_instances( + InstanceIds=[instance_to_standby_terminate] + ) + response["Reservations"][0]["Instances"][0]["State"]["Name"].should.equal( + "terminated" + ) + + +@mock_autoscaling +@mock_ec2 +def test_standby_detach_instance_decrement(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=3, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby_detach = response["AutoScalingGroups"][0]["Instances"][0][ + "InstanceId" + ] + + ec2_client = boto3.client("ec2", region_name="us-east-1") + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby_detach], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(3) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(2) + + response = client.describe_auto_scaling_instances( + InstanceIds=[instance_to_standby_detach] + ) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + response = client.detach_instances( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby_detach], + ShouldDecrementDesiredCapacity=True, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + # AWS still decrements desired capacity ASG if requested, even if the detached instance was in standby + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(1) + response["AutoScalingGroups"][0]["Instances"][0]["InstanceId"].should_not.equal( + instance_to_standby_detach + ) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(1) + + response = ec2_client.describe_instances(InstanceIds=[instance_to_standby_detach]) + response["Reservations"][0]["Instances"][0]["State"]["Name"].should.equal("running") + + +@mock_autoscaling +@mock_ec2 +def test_standby_detach_instance_no_decrement(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=3, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby_detach = response["AutoScalingGroups"][0]["Instances"][0][ + "InstanceId" + ] + + ec2_client = boto3.client("ec2", region_name="us-east-1") + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby_detach], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(3) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(2) + + response = client.describe_auto_scaling_instances( + InstanceIds=[instance_to_standby_detach] + ) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + response = client.detach_instances( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby_detach], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + group = response["AutoScalingGroups"][0] + group["Instances"].should.have.length_of(2) + instance_to_standby_detach.shouldnt.be.within( + [x["InstanceId"] for x in group["Instances"]] + ) + group["DesiredCapacity"].should.equal(2) + + response = ec2_client.describe_instances(InstanceIds=[instance_to_standby_detach]) + response["Reservations"][0]["Instances"][0]["State"]["Name"].should.equal("running") + + +@mock_autoscaling +@mock_ec2 +def test_standby_exit_standby(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + MaxSize=3, + DesiredCapacity=2, + Tags=[ + { + "ResourceId": "test_asg", + "ResourceType": "auto-scaling-group", + "Key": "propogated-tag-key", + "Value": "propagate-tag-value", + "PropagateAtLaunch": True, + } + ], + VPCZoneIdentifier=mocked_networking["subnet1"], + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + instance_to_standby_exit_standby = response["AutoScalingGroups"][0]["Instances"][0][ + "InstanceId" + ] + + ec2_client = boto3.client("ec2", region_name="us-east-1") + + response = client.enter_standby( + AutoScalingGroupName="test_asg", + InstanceIds=[instance_to_standby_exit_standby], + ShouldDecrementDesiredCapacity=False, + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.have.length_of(3) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(2) + + response = client.describe_auto_scaling_instances( + InstanceIds=[instance_to_standby_exit_standby] + ) + response["AutoScalingInstances"][0]["LifecycleState"].should.equal("Standby") + + response = client.exit_standby( + AutoScalingGroupName="test_asg", InstanceIds=[instance_to_standby_exit_standby], + ) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + group = response["AutoScalingGroups"][0] + group["Instances"].should.have.length_of(3) + instance_to_standby_exit_standby.should.be.within( + [x["InstanceId"] for x in group["Instances"]] + ) + group["DesiredCapacity"].should.equal(3) + + response = ec2_client.describe_instances( + InstanceIds=[instance_to_standby_exit_standby] + ) + response["Reservations"][0]["Instances"][0]["State"]["Name"].should.equal("running") + + @mock_autoscaling @mock_ec2 def test_attach_one_instance(): @@ -1411,7 +1917,7 @@ def test_set_desired_capacity_down_boto3(): @mock_autoscaling @mock_ec2 -def test_terminate_instance_in_autoscaling_group(): +def test_terminate_instance_via_ec2_in_autoscaling_group(): mocked_networking = setup_networking() client = boto3.client("autoscaling", region_name="us-east-1") _ = client.create_launch_configuration( @@ -1440,3 +1946,71 @@ def test_terminate_instance_in_autoscaling_group(): for instance in response["AutoScalingGroups"][0]["Instances"] ) replaced_instance_id.should_not.equal(original_instance_id) + + +@mock_autoscaling +@mock_ec2 +def test_terminate_instance_in_auto_scaling_group_decrement(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + _ = client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + DesiredCapacity=1, + MaxSize=2, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=False, + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + original_instance_id = next( + instance["InstanceId"] + for instance in response["AutoScalingGroups"][0]["Instances"] + ) + client.terminate_instance_in_auto_scaling_group( + InstanceId=original_instance_id, ShouldDecrementDesiredCapacity=True + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + response["AutoScalingGroups"][0]["Instances"].should.equal([]) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(0) + + +@mock_autoscaling +@mock_ec2 +def test_terminate_instance_in_auto_scaling_group_no_decrement(): + mocked_networking = setup_networking() + client = boto3.client("autoscaling", region_name="us-east-1") + _ = client.create_launch_configuration( + LaunchConfigurationName="test_launch_configuration" + ) + _ = client.create_auto_scaling_group( + AutoScalingGroupName="test_asg", + LaunchConfigurationName="test_launch_configuration", + MinSize=0, + DesiredCapacity=1, + MaxSize=2, + VPCZoneIdentifier=mocked_networking["subnet1"], + NewInstancesProtectedFromScaleIn=False, + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + original_instance_id = next( + instance["InstanceId"] + for instance in response["AutoScalingGroups"][0]["Instances"] + ) + client.terminate_instance_in_auto_scaling_group( + InstanceId=original_instance_id, ShouldDecrementDesiredCapacity=False + ) + + response = client.describe_auto_scaling_groups(AutoScalingGroupNames=["test_asg"]) + replaced_instance_id = next( + instance["InstanceId"] + for instance in response["AutoScalingGroups"][0]["Instances"] + ) + replaced_instance_id.should_not.equal(original_instance_id) + response["AutoScalingGroups"][0]["DesiredCapacity"].should.equal(1)