Make a dedicated InstanceGroup

Make it the basis of the autoscaling group, the
main difference is you change the number of instances
via update (manually) or not at all.

This can be achieved using the autoscaling group, but
this is clearer from the user's perspective.

Implements blueprint static-inst-group

Change-Id: I72680e92183ba87a76efa64383269afb083a446a
This commit is contained in:
Angus Salkeld 2013-01-25 11:56:29 +11:00
parent 8d6aa11e70
commit 69ebb38db2
3 changed files with 192 additions and 67 deletions

View File

@ -21,7 +21,110 @@ from heat.openstack.common import log as logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AutoScalingGroup(resource.Resource): class InstanceGroup(resource.Resource):
tags_schema = {'Key': {'Type': 'String',
'Required': True},
'Value': {'Type': 'String',
'Required': True}}
properties_schema = {
'AvailabilityZones': {'Required': True,
'Type': 'List'},
'LaunchConfigurationName': {'Required': True,
'Type': 'String'},
'Size': {'Required': True,
'Type': 'Number'},
'LoadBalancerNames': {'Type': 'List'},
'Tags': {'Type': 'List',
'Schema': {'Type': 'Map',
'Schema': tags_schema}}
}
def __init__(self, name, json_snippet, stack):
super(InstanceGroup, self).__init__(name, json_snippet, stack)
# resource_id is a list of resources
def handle_create(self):
self.resize(int(self.properties['Size']))
def handle_update(self):
# TODO(asalkeld) if the only thing that has changed is the size then
# call resize. Maybe have an attribute of the properties that can mark
# it "update-able" so each resource doesn't have to figure this out.
return self.UPDATE_REPLACE
def _make_instance(self, name):
Instance = resource.get_class('AWS::EC2::Instance')
class GroupedInstance(Instance):
'''
Subclass instance.Instance to supress event transitions, since the
scaling-group instances are not "real" resources, ie defined in the
template, which causes problems for event handling since we can't
look up the resources via parser.Stack
'''
def state_set(self, new_state, reason="state changed"):
self._store_or_update(new_state, reason)
conf = self.properties['LaunchConfigurationName']
instance_definition = self.stack.t['Resources'][conf]
return GroupedInstance(name, instance_definition, self.stack)
def handle_delete(self):
if self.resource_id is not None:
inst_list = self.resource_id.split(',')
logger.debug('handle_delete %s' % str(inst_list))
for victim in inst_list:
logger.debug('handle_delete %s' % victim)
inst = self._make_instance(victim)
inst.destroy()
def resize(self, new_capacity):
inst_list = []
if self.resource_id is not None:
inst_list = sorted(self.resource_id.split(','))
capacity = len(inst_list)
if new_capacity == capacity:
logger.debug('no change in capacity %d' % capacity)
return
logger.debug('adjusting capacity from %d to %d' % (capacity,
new_capacity))
if new_capacity > capacity:
# grow
for x in range(capacity, new_capacity):
name = '%s-%d' % (self.name, x)
inst = self._make_instance(name)
inst_list.append(name)
self.resource_id_set(','.join(inst_list))
inst.create()
else:
# shrink (kill largest numbered first)
del_list = inst_list[new_capacity:]
for victim in reversed(del_list):
inst = self._make_instance(victim)
inst.destroy()
inst_list.remove(victim)
self.resource_id_set(','.join(inst_list))
# notify the LoadBalancer to reload it's config to include
# the changes in instances we have just made.
if self.properties['LoadBalancerNames']:
# convert the list of instance names into a list of instance id's
id_list = []
for inst_name in inst_list:
inst = self._make_instance(inst_name)
id_list.append(inst.FnGetRefId())
for lb in self.properties['LoadBalancerNames']:
self.stack[lb].reload(id_list)
def FnGetRefId(self):
return unicode(self.name)
class AutoScalingGroup(InstanceGroup):
tags_schema = {'Key': {'Type': 'String', tags_schema = {'Key': {'Type': 'String',
'Required': True}, 'Required': True},
'Value': {'Type': 'String', 'Value': {'Type': 'String',
@ -58,39 +161,11 @@ class AutoScalingGroup(resource.Resource):
else: else:
num_to_create = int(self.properties['MinSize']) num_to_create = int(self.properties['MinSize'])
self.adjust(num_to_create, self.resize(num_to_create)
adjustment_type='ExactCapacity')
def handle_update(self): def handle_update(self):
return self.UPDATE_REPLACE return self.UPDATE_REPLACE
def _make_instance(self, name):
Instance = resource.get_class('AWS::EC2::Instance')
class AutoScalingGroupInstance(Instance):
'''
Subclass instance.Instance to supress event transitions, since the
scaling-group instances are not "real" resources, ie defined in the
template, which causes problems for event handling since we can't
look up the resources via parser.Stack
'''
def state_set(self, new_state, reason="state changed"):
self._store_or_update(new_state, reason)
conf = self.properties['LaunchConfigurationName']
instance_definition = self.stack.t['Resources'][conf]
return AutoScalingGroupInstance(name, instance_definition, self.stack)
def handle_delete(self):
if self.resource_id is not None:
inst_list = self.resource_id.split(',')
logger.debug('handle_delete %s' % str(inst_list))
for victim in inst_list:
logger.debug('handle_delete %s' % victim)
inst = self._make_instance(victim)
inst.destroy()
def adjust(self, adjustment, adjustment_type='ChangeInCapacity'): def adjust(self, adjustment, adjustment_type='ChangeInCapacity'):
inst_list = [] inst_list = []
if self.resource_id is not None: if self.resource_id is not None:
@ -112,40 +187,7 @@ class AutoScalingGroup(resource.Resource):
logger.warn('can not be less than %s' % self.properties['MinSize']) logger.warn('can not be less than %s' % self.properties['MinSize'])
return return
if new_capacity == capacity: self.resize(new_capacity)
logger.debug('no change in capacity %d' % capacity)
return
logger.debug('adjusting capacity from %d to %d' % (capacity,
new_capacity))
if new_capacity > capacity:
# grow
for x in range(capacity, new_capacity):
name = '%s-%d' % (self.name, x)
inst = self._make_instance(name)
inst_list.append(name)
self.resource_id_set(','.join(inst_list))
inst.create()
else:
# shrink (kill largest numbered first)
del_list = inst_list[new_capacity:]
for victim in reversed(del_list):
inst = self._make_instance(victim)
inst.destroy()
inst_list.remove(victim)
self.resource_id_set(','.join(inst_list))
# notify the LoadBalancer to reload it's config to include
# the changes in instances we have just made.
if self.properties['LoadBalancerNames']:
# convert the list of instance names into a list of instance id's
id_list = []
for inst_name in inst_list:
inst = self._make_instance(inst_name)
id_list.append(inst.FnGetRefId())
for lb in self.properties['LoadBalancerNames']:
self.stack[lb].reload(id_list)
def FnGetRefId(self): def FnGetRefId(self):
return unicode(self.name) return unicode(self.name)
@ -211,4 +253,5 @@ def resource_mapping():
'AWS::AutoScaling::LaunchConfiguration': LaunchConfiguration, 'AWS::AutoScaling::LaunchConfiguration': LaunchConfiguration,
'AWS::AutoScaling::AutoScalingGroup': AutoScalingGroup, 'AWS::AutoScaling::AutoScalingGroup': AutoScalingGroup,
'AWS::AutoScaling::ScalingPolicy': ScalingPolicy, 'AWS::AutoScaling::ScalingPolicy': ScalingPolicy,
'OS::Heat::InstanceGroup': InstanceGroup,
} }

View File

@ -0,0 +1,83 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import unittest
import mox
from nose.plugins.attrib import attr
from heat.common import context
from heat.common import template_format
from heat.engine.resources import autoscaling as asc
from heat.engine.resources import loadbalancer
from heat.engine import parser
@attr(tag=['unit', 'resource'])
@attr(speed='fast')
class InstanceGroupTest(unittest.TestCase):
def setUp(self):
self.m = mox.Mox()
self.m.StubOutWithMock(loadbalancer.LoadBalancer, 'reload')
def tearDown(self):
self.m.UnsetStubs()
print "InstanceGroupTest teardown complete"
def load_template(self):
self.path = os.path.dirname(os.path.realpath(__file__)).\
replace('heat/tests', 'templates')
f = open("%s/InstanceGroup.template" % self.path)
t = template_format.parse(f.read())
f.close()
return t
def parse_stack(self, t):
ctx = context.RequestContext.from_dict({
'tenant': 'test_tenant',
'username': 'test_username',
'password': 'password',
'auth_url': 'http://localhost:5000/v2.0'})
template = parser.Template(t)
params = parser.Parameters('test_stack', template, {'KeyName': 'test'})
stack = parser.Stack(ctx, 'test_stack', template, params)
return stack
def create_instance_group(self, t, stack, resource_name):
resource = asc.InstanceGroup(resource_name,
t['Resources'][resource_name],
stack)
self.assertEqual(None, resource.validate())
self.assertEqual(None, resource.create())
self.assertEqual(asc.InstanceGroup.CREATE_COMPLETE, resource.state)
return resource
def test_instance_group(self):
t = self.load_template()
stack = self.parse_stack(t)
# start with min then delete
resource = self.create_instance_group(t, stack, 'JobServerGroup')
self.assertEqual('JobServerGroup', resource.FnGetRefId())
self.assertEqual('JobServerGroup-0', resource.resource_id)
self.assertEqual(asc.InstanceGroup.UPDATE_REPLACE,
resource.handle_update())
resource.delete()

View File

@ -31,11 +31,10 @@
"Resources" : { "Resources" : {
"JobServerGroup" : { "JobServerGroup" : {
"Type" : "AWS::AutoScaling::AutoScalingGroup", "Type" : "OS::Heat::InstanceGroup",
"Properties" : { "Properties" : {
"LaunchConfigurationName" : { "Ref" : "JobServerConfig" }, "LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
"MinSize" : {"Ref": "NumInstances"}, "Size" : {"Ref": "NumInstances"},
"MaxSize" : {"Ref": "NumInstances"},
"AvailabilityZones" : { "Fn::GetAZs" : "" } "AvailabilityZones" : { "Fn::GetAZs" : "" }
} }
}, },