Add attach_ and detach_instances methods to autoscaling service (#1264)
* add detach_instances functionality to autoscaling service * use ASG_NAME_TAG constant * cleanup models method a bit, add mocked DetachInstancesResult to response template * add attach_instances method
This commit is contained in:
parent
49ddb500a8
commit
5ef236e966
14
moto/autoscaling/exceptions.py
Normal file
14
moto/autoscaling/exceptions.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from moto.core.exceptions import RESTError
|
||||||
|
|
||||||
|
|
||||||
|
class AutoscalingClientError(RESTError):
|
||||||
|
code = 500
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceContentionError(AutoscalingClientError):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ResourceContentionError, self).__init__(
|
||||||
|
"ResourceContentionError",
|
||||||
|
"You already have a pending update to an Auto Scaling resource (for example, a group, instance, or load balancer).")
|
@ -5,6 +5,9 @@ from moto.core import BaseBackend, BaseModel
|
|||||||
from moto.ec2 import ec2_backends
|
from moto.ec2 import ec2_backends
|
||||||
from moto.elb import elb_backends
|
from moto.elb import elb_backends
|
||||||
from moto.elb.exceptions import LoadBalancerNotFoundError
|
from moto.elb.exceptions import LoadBalancerNotFoundError
|
||||||
|
from .exceptions import (
|
||||||
|
ResourceContentionError,
|
||||||
|
)
|
||||||
|
|
||||||
# http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/AS_Concepts.html#Cooldown
|
# http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/AS_Concepts.html#Cooldown
|
||||||
DEFAULT_COOLDOWN = 300
|
DEFAULT_COOLDOWN = 300
|
||||||
@ -259,6 +262,19 @@ class FakeAutoScalingGroup(BaseModel):
|
|||||||
# Need more instances
|
# Need more instances
|
||||||
count_needed = int(self.desired_capacity) - int(curr_instance_count)
|
count_needed = int(self.desired_capacity) - int(curr_instance_count)
|
||||||
|
|
||||||
|
propagated_tags = self.get_propagated_tags()
|
||||||
|
self.replace_autoscaling_group_instances(count_needed, propagated_tags)
|
||||||
|
else:
|
||||||
|
# Need to remove some instances
|
||||||
|
count_to_remove = curr_instance_count - self.desired_capacity
|
||||||
|
instances_to_remove = self.instance_states[:count_to_remove]
|
||||||
|
instance_ids_to_remove = [
|
||||||
|
instance.instance.id for instance in instances_to_remove]
|
||||||
|
self.autoscaling_backend.ec2_backend.terminate_instances(
|
||||||
|
instance_ids_to_remove)
|
||||||
|
self.instance_states = self.instance_states[count_to_remove:]
|
||||||
|
|
||||||
|
def get_propagated_tags(self):
|
||||||
propagated_tags = {}
|
propagated_tags = {}
|
||||||
for tag in self.tags:
|
for tag in self.tags:
|
||||||
# boto uses 'propagate_at_launch
|
# boto uses 'propagate_at_launch
|
||||||
@ -267,7 +283,9 @@ class FakeAutoScalingGroup(BaseModel):
|
|||||||
propagated_tags[tag['key']] = tag['value']
|
propagated_tags[tag['key']] = tag['value']
|
||||||
if 'PropagateAtLaunch' in tag and tag['PropagateAtLaunch']:
|
if 'PropagateAtLaunch' in tag and tag['PropagateAtLaunch']:
|
||||||
propagated_tags[tag['Key']] = tag['Value']
|
propagated_tags[tag['Key']] = tag['Value']
|
||||||
|
return propagated_tags
|
||||||
|
|
||||||
|
def replace_autoscaling_group_instances(self, count_needed, propagated_tags):
|
||||||
propagated_tags[ASG_NAME_TAG] = self.name
|
propagated_tags[ASG_NAME_TAG] = self.name
|
||||||
reservation = self.autoscaling_backend.ec2_backend.add_instances(
|
reservation = self.autoscaling_backend.ec2_backend.add_instances(
|
||||||
self.launch_config.image_id,
|
self.launch_config.image_id,
|
||||||
@ -280,15 +298,6 @@ class FakeAutoScalingGroup(BaseModel):
|
|||||||
for instance in reservation.instances:
|
for instance in reservation.instances:
|
||||||
instance.autoscaling_group = self
|
instance.autoscaling_group = self
|
||||||
self.instance_states.append(InstanceState(instance))
|
self.instance_states.append(InstanceState(instance))
|
||||||
else:
|
|
||||||
# Need to remove some instances
|
|
||||||
count_to_remove = curr_instance_count - self.desired_capacity
|
|
||||||
instances_to_remove = self.instance_states[:count_to_remove]
|
|
||||||
instance_ids_to_remove = [
|
|
||||||
instance.instance.id for instance in instances_to_remove]
|
|
||||||
self.autoscaling_backend.ec2_backend.terminate_instances(
|
|
||||||
instance_ids_to_remove)
|
|
||||||
self.instance_states = self.instance_states[count_to_remove:]
|
|
||||||
|
|
||||||
|
|
||||||
class AutoScalingBackend(BaseBackend):
|
class AutoScalingBackend(BaseBackend):
|
||||||
@ -409,6 +418,40 @@ class AutoScalingBackend(BaseBackend):
|
|||||||
instance_states.extend(group.instance_states)
|
instance_states.extend(group.instance_states)
|
||||||
return instance_states
|
return instance_states
|
||||||
|
|
||||||
|
def attach_instances(self, group_name, instance_ids):
|
||||||
|
group = self.autoscaling_groups[group_name]
|
||||||
|
original_size = len(group.instance_states)
|
||||||
|
|
||||||
|
if (original_size + len(instance_ids)) > group.max_size:
|
||||||
|
raise ResourceContentionError
|
||||||
|
else:
|
||||||
|
group.desired_capacity = original_size + len(instance_ids)
|
||||||
|
new_instances = [InstanceState(self.ec2_backend.get_instance(x)) for x in instance_ids]
|
||||||
|
for instance in new_instances:
|
||||||
|
self.ec2_backend.create_tags([instance.instance.id], {ASG_NAME_TAG: group.name})
|
||||||
|
group.instance_states.extend(new_instances)
|
||||||
|
self.update_attached_elbs(group.name)
|
||||||
|
|
||||||
|
def detach_instances(self, group_name, instance_ids, should_decrement):
|
||||||
|
group = self.autoscaling_groups[group_name]
|
||||||
|
original_size = len(group.instance_states)
|
||||||
|
|
||||||
|
detached_instances = [x for x in group.instance_states if x.instance.id in instance_ids]
|
||||||
|
for instance in detached_instances:
|
||||||
|
self.ec2_backend.delete_tags([instance.instance.id], {ASG_NAME_TAG: group.name})
|
||||||
|
|
||||||
|
new_instance_state = [x for x in group.instance_states if x.instance.id not in instance_ids]
|
||||||
|
group.instance_states = new_instance_state
|
||||||
|
|
||||||
|
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)
|
||||||
|
return detached_instances
|
||||||
|
|
||||||
def set_desired_capacity(self, group_name, desired_capacity):
|
def set_desired_capacity(self, group_name, desired_capacity):
|
||||||
group = self.autoscaling_groups[group_name]
|
group = self.autoscaling_groups[group_name]
|
||||||
group.set_desired_capacity(desired_capacity)
|
group.set_desired_capacity(desired_capacity)
|
||||||
|
@ -87,6 +87,27 @@ class AutoScalingResponse(BaseResponse):
|
|||||||
template = self.response_template(CREATE_AUTOSCALING_GROUP_TEMPLATE)
|
template = self.response_template(CREATE_AUTOSCALING_GROUP_TEMPLATE)
|
||||||
return template.render()
|
return template.render()
|
||||||
|
|
||||||
|
def attach_instances(self):
|
||||||
|
group_name = self._get_param('AutoScalingGroupName')
|
||||||
|
instance_ids = self._get_multi_param("InstanceIds.member")
|
||||||
|
self.autoscaling_backend.attach_instances(
|
||||||
|
group_name, instance_ids)
|
||||||
|
template = self.response_template(ATTACH_INSTANCES_TEMPLATE)
|
||||||
|
return template.render()
|
||||||
|
|
||||||
|
def detach_instances(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
|
||||||
|
detached_instances = self.autoscaling_backend.detach_instances(
|
||||||
|
group_name, instance_ids, should_decrement)
|
||||||
|
template = self.response_template(DETACH_INSTANCES_TEMPLATE)
|
||||||
|
return template.render(detached_instances=detached_instances)
|
||||||
|
|
||||||
def describe_auto_scaling_groups(self):
|
def describe_auto_scaling_groups(self):
|
||||||
names = self._get_multi_param("AutoScalingGroupNames.member")
|
names = self._get_multi_param("AutoScalingGroupNames.member")
|
||||||
token = self._get_param("NextToken")
|
token = self._get_param("NextToken")
|
||||||
@ -284,6 +305,40 @@ CREATE_AUTOSCALING_GROUP_TEMPLATE = """<CreateAutoScalingGroupResponse xmlns="ht
|
|||||||
</ResponseMetadata>
|
</ResponseMetadata>
|
||||||
</CreateAutoScalingGroupResponse>"""
|
</CreateAutoScalingGroupResponse>"""
|
||||||
|
|
||||||
|
ATTACH_INSTANCES_TEMPLATE = """<AttachInstancesResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||||
|
<AttachInstancesResult>
|
||||||
|
</AttachInstancesResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>8d798a29-f083-11e1-bdfb-cb223EXAMPLE</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</AttachInstancesResponse>"""
|
||||||
|
|
||||||
|
DETACH_INSTANCES_TEMPLATE = """<DetachInstancesResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||||
|
<DetachInstancesResult>
|
||||||
|
<Activities>
|
||||||
|
{% for instance in detached_instances %}
|
||||||
|
<member>
|
||||||
|
<ActivityId>5091cb52-547a-47ce-a236-c9ccbc2cb2c9EXAMPLE</ActivityId>
|
||||||
|
<AutoScalingGroupName>{{ group_name }}</AutoScalingGroupName>
|
||||||
|
<Cause>
|
||||||
|
At 2017-10-15T15:55:21Z instance {{ instance.instance.id }} was detached in response to a user request.
|
||||||
|
</Cause>
|
||||||
|
<Description>Detaching EC2 instance: {{ instance.instance.id }}</Description>
|
||||||
|
<StartTime>2017-10-15T15:55:21Z</StartTime>
|
||||||
|
<EndTime>2017-10-15T15:55:21Z</EndTime>
|
||||||
|
<StatusCode>InProgress</StatusCode>
|
||||||
|
<StatusMessage>InProgress</StatusMessage>
|
||||||
|
<Progress>50</Progress>
|
||||||
|
<Details>details</Details>
|
||||||
|
</member>
|
||||||
|
{% endfor %}
|
||||||
|
</Activities>
|
||||||
|
</DetachInstancesResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>8d798a29-f083-11e1-bdfb-cb223EXAMPLE</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</DetachInstancesResponse>"""
|
||||||
|
|
||||||
DESCRIBE_AUTOSCALING_GROUPS_TEMPLATE = """<DescribeAutoScalingGroupsResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
DESCRIBE_AUTOSCALING_GROUPS_TEMPLATE = """<DescribeAutoScalingGroupsResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
|
||||||
<DescribeAutoScalingGroupsResult>
|
<DescribeAutoScalingGroupsResult>
|
||||||
<AutoScalingGroups>
|
<AutoScalingGroups>
|
||||||
|
@ -653,3 +653,147 @@ def test_autoscaling_describe_policies_boto3():
|
|||||||
response['ScalingPolicies'].should.have.length_of(1)
|
response['ScalingPolicies'].should.have.length_of(1)
|
||||||
response['ScalingPolicies'][0][
|
response['ScalingPolicies'][0][
|
||||||
'PolicyName'].should.equal('test_policy_down')
|
'PolicyName'].should.equal('test_policy_down')
|
||||||
|
|
||||||
|
@mock_autoscaling
|
||||||
|
@mock_ec2
|
||||||
|
def test_detach_one_instance_decrement():
|
||||||
|
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': 'propogate-tag-value',
|
||||||
|
'PropagateAtLaunch': True
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
response = client.describe_auto_scaling_groups(
|
||||||
|
AutoScalingGroupNames=['test_asg']
|
||||||
|
)
|
||||||
|
instance_to_detach = 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 = ec2_client.describe_instances(InstanceIds=[instance_to_detach])
|
||||||
|
|
||||||
|
response = client.detach_instances(
|
||||||
|
AutoScalingGroupName='test_asg',
|
||||||
|
InstanceIds=[instance_to_detach],
|
||||||
|
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(1)
|
||||||
|
|
||||||
|
# test to ensure tag has been removed
|
||||||
|
response = ec2_client.describe_instances(InstanceIds=[instance_to_detach])
|
||||||
|
tags = response['Reservations'][0]['Instances'][0]['Tags']
|
||||||
|
tags.should.have.length_of(1)
|
||||||
|
|
||||||
|
# test to ensure tag is present on other instance
|
||||||
|
response = ec2_client.describe_instances(InstanceIds=[instance_to_keep])
|
||||||
|
tags = response['Reservations'][0]['Instances'][0]['Tags']
|
||||||
|
tags.should.have.length_of(2)
|
||||||
|
|
||||||
|
@mock_autoscaling
|
||||||
|
@mock_ec2
|
||||||
|
def test_detach_one_instance():
|
||||||
|
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': 'propogate-tag-value',
|
||||||
|
'PropagateAtLaunch': True
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
response = client.describe_auto_scaling_groups(
|
||||||
|
AutoScalingGroupNames=['test_asg']
|
||||||
|
)
|
||||||
|
instance_to_detach = 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 = ec2_client.describe_instances(InstanceIds=[instance_to_detach])
|
||||||
|
|
||||||
|
response = client.detach_instances(
|
||||||
|
AutoScalingGroupName='test_asg',
|
||||||
|
InstanceIds=[instance_to_detach],
|
||||||
|
ShouldDecrementDesiredCapacity=False
|
||||||
|
)
|
||||||
|
response['ResponseMetadata']['HTTPStatusCode'].should.equal(200)
|
||||||
|
|
||||||
|
response = client.describe_auto_scaling_groups(
|
||||||
|
AutoScalingGroupNames=['test_asg']
|
||||||
|
)
|
||||||
|
# test to ensure instance was replaced
|
||||||
|
response['AutoScalingGroups'][0]['Instances'].should.have.length_of(2)
|
||||||
|
|
||||||
|
response = ec2_client.describe_instances(InstanceIds=[instance_to_detach])
|
||||||
|
tags = response['Reservations'][0]['Instances'][0]['Tags']
|
||||||
|
tags.should.have.length_of(1)
|
||||||
|
|
||||||
|
response = ec2_client.describe_instances(InstanceIds=[instance_to_keep])
|
||||||
|
tags = response['Reservations'][0]['Instances'][0]['Tags']
|
||||||
|
tags.should.have.length_of(2)
|
||||||
|
|
||||||
|
@mock_autoscaling
|
||||||
|
@mock_ec2
|
||||||
|
def test_attach_one_instance():
|
||||||
|
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=4,
|
||||||
|
DesiredCapacity=2,
|
||||||
|
Tags=[
|
||||||
|
{'ResourceId': 'test_asg',
|
||||||
|
'ResourceType': 'auto-scaling-group',
|
||||||
|
'Key': 'propogated-tag-key',
|
||||||
|
'Value': 'propogate-tag-value',
|
||||||
|
'PropagateAtLaunch': True
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
response = client.describe_auto_scaling_groups(
|
||||||
|
AutoScalingGroupNames=['test_asg']
|
||||||
|
)
|
||||||
|
|
||||||
|
ec2 = boto3.resource('ec2', 'us-east-1')
|
||||||
|
instances_to_add = [x.id for x in ec2.create_instances(ImageId='', MinCount=1, MaxCount=1)]
|
||||||
|
|
||||||
|
response = client.attach_instances(
|
||||||
|
AutoScalingGroupName='test_asg',
|
||||||
|
InstanceIds=instances_to_add
|
||||||
|
)
|
||||||
|
response['ResponseMetadata']['HTTPStatusCode'].should.equal(200)
|
||||||
|
|
||||||
|
response = client.describe_auto_scaling_groups(
|
||||||
|
AutoScalingGroupNames=['test_asg']
|
||||||
|
)
|
||||||
|
response['AutoScalingGroups'][0]['Instances'].should.have.length_of(3)
|
||||||
|
Loading…
Reference in New Issue
Block a user