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:
parent
8d6aa11e70
commit
69ebb38db2
@ -21,7 +21,110 @@ from heat.openstack.common import log as logging
|
||||
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',
|
||||
'Required': True},
|
||||
'Value': {'Type': 'String',
|
||||
@ -58,39 +161,11 @@ class AutoScalingGroup(resource.Resource):
|
||||
else:
|
||||
num_to_create = int(self.properties['MinSize'])
|
||||
|
||||
self.adjust(num_to_create,
|
||||
adjustment_type='ExactCapacity')
|
||||
self.resize(num_to_create)
|
||||
|
||||
def handle_update(self):
|
||||
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'):
|
||||
inst_list = []
|
||||
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'])
|
||||
return
|
||||
|
||||
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)
|
||||
self.resize(new_capacity)
|
||||
|
||||
def FnGetRefId(self):
|
||||
return unicode(self.name)
|
||||
@ -211,4 +253,5 @@ def resource_mapping():
|
||||
'AWS::AutoScaling::LaunchConfiguration': LaunchConfiguration,
|
||||
'AWS::AutoScaling::AutoScalingGroup': AutoScalingGroup,
|
||||
'AWS::AutoScaling::ScalingPolicy': ScalingPolicy,
|
||||
'OS::Heat::InstanceGroup': InstanceGroup,
|
||||
}
|
||||
|
83
heat/tests/test_instance_group.py
Normal file
83
heat/tests/test_instance_group.py
Normal 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()
|
@ -31,11 +31,10 @@
|
||||
|
||||
"Resources" : {
|
||||
"JobServerGroup" : {
|
||||
"Type" : "AWS::AutoScaling::AutoScalingGroup",
|
||||
"Type" : "OS::Heat::InstanceGroup",
|
||||
"Properties" : {
|
||||
"LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
|
||||
"MinSize" : {"Ref": "NumInstances"},
|
||||
"MaxSize" : {"Ref": "NumInstances"},
|
||||
"Size" : {"Ref": "NumInstances"},
|
||||
"AvailabilityZones" : { "Fn::GetAZs" : "" }
|
||||
}
|
||||
},
|
Loading…
Reference in New Issue
Block a user