From 69ebb38db23485dd10e2165bb2f508f34e5bf537 Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Fri, 25 Jan 2013 11:56:29 +1100 Subject: [PATCH] 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 --- heat/engine/resources/autoscaling.py | 125 ++++++++++++------ heat/tests/test_instance_group.py | 83 ++++++++++++ ...ppetit.template => InstanceGroup.template} | 5 +- 3 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 heat/tests/test_instance_group.py rename templates/{ppetit.template => InstanceGroup.template} (92%) diff --git a/heat/engine/resources/autoscaling.py b/heat/engine/resources/autoscaling.py index 0ce69609..caf500df 100644 --- a/heat/engine/resources/autoscaling.py +++ b/heat/engine/resources/autoscaling.py @@ -21,7 +21,7 @@ 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', @@ -31,44 +31,32 @@ class AutoScalingGroup(resource.Resource): 'Type': 'List'}, 'LaunchConfigurationName': {'Required': True, 'Type': 'String'}, - 'MaxSize': {'Required': True, - 'Type': 'String'}, - 'MinSize': {'Required': True, - 'Type': 'String'}, - 'Cooldown': {'Type': 'String'}, - 'DesiredCapacity': {'Type': 'Number'}, - 'HealthCheckGracePeriod': {'Type': 'Integer', - 'Implemented': False}, - 'HealthCheckType': {'Type': 'String', - 'AllowedValues': ['EC2', 'ELB'], - 'Implemented': False}, + 'Size': {'Required': True, + 'Type': 'Number'}, 'LoadBalancerNames': {'Type': 'List'}, - 'Tags': {'Type': 'List', 'Schema': {'Type': 'Map', - 'Schema': tags_schema}} + 'Tags': {'Type': 'List', + 'Schema': {'Type': 'Map', + 'Schema': tags_schema}} } def __init__(self, name, json_snippet, stack): - super(AutoScalingGroup, self).__init__(name, json_snippet, stack) + super(InstanceGroup, self).__init__(name, json_snippet, stack) # resource_id is a list of resources def handle_create(self): - - if self.properties['DesiredCapacity']: - num_to_create = int(self.properties['DesiredCapacity']) - else: - num_to_create = int(self.properties['MinSize']) - - self.adjust(num_to_create, - adjustment_type='ExactCapacity') + 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 AutoScalingGroupInstance(Instance): + class GroupedInstance(Instance): ''' Subclass instance.Instance to supress event transitions, since the scaling-group instances are not "real" resources, ie defined in the @@ -80,7 +68,7 @@ class AutoScalingGroup(resource.Resource): conf = self.properties['LaunchConfigurationName'] instance_definition = self.stack.t['Resources'][conf] - return AutoScalingGroupInstance(name, instance_definition, self.stack) + return GroupedInstance(name, instance_definition, self.stack) def handle_delete(self): if self.resource_id is not None: @@ -91,27 +79,12 @@ class AutoScalingGroup(resource.Resource): inst = self._make_instance(victim) inst.destroy() - def adjust(self, adjustment, adjustment_type='ChangeInCapacity'): + 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 adjustment_type == 'ChangeInCapacity': - new_capacity = capacity + adjustment - elif adjustment_type == 'ExactCapacity': - new_capacity = adjustment - else: - # PercentChangeInCapacity - new_capacity = capacity + (capacity * adjustment / 100) - - if new_capacity > int(self.properties['MaxSize']): - logger.warn('can not exceed %s' % self.properties['MaxSize']) - return - if new_capacity < int(self.properties['MinSize']): - 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 @@ -151,6 +124,75 @@ class AutoScalingGroup(resource.Resource): return unicode(self.name) +class AutoScalingGroup(InstanceGroup): + tags_schema = {'Key': {'Type': 'String', + 'Required': True}, + 'Value': {'Type': 'String', + 'Required': True}} + properties_schema = { + 'AvailabilityZones': {'Required': True, + 'Type': 'List'}, + 'LaunchConfigurationName': {'Required': True, + 'Type': 'String'}, + 'MaxSize': {'Required': True, + 'Type': 'String'}, + 'MinSize': {'Required': True, + 'Type': 'String'}, + 'Cooldown': {'Type': 'String'}, + 'DesiredCapacity': {'Type': 'Number'}, + 'HealthCheckGracePeriod': {'Type': 'Integer', + 'Implemented': False}, + 'HealthCheckType': {'Type': 'String', + 'AllowedValues': ['EC2', 'ELB'], + 'Implemented': False}, + 'LoadBalancerNames': {'Type': 'List'}, + 'Tags': {'Type': 'List', 'Schema': {'Type': 'Map', + 'Schema': tags_schema}} + } + + def __init__(self, name, json_snippet, stack): + super(AutoScalingGroup, self).__init__(name, json_snippet, stack) + # resource_id is a list of resources + + def handle_create(self): + + if self.properties['DesiredCapacity']: + num_to_create = int(self.properties['DesiredCapacity']) + else: + num_to_create = int(self.properties['MinSize']) + + self.resize(num_to_create) + + def handle_update(self): + return self.UPDATE_REPLACE + + def adjust(self, adjustment, adjustment_type='ChangeInCapacity'): + inst_list = [] + if self.resource_id is not None: + inst_list = sorted(self.resource_id.split(',')) + + capacity = len(inst_list) + if adjustment_type == 'ChangeInCapacity': + new_capacity = capacity + adjustment + elif adjustment_type == 'ExactCapacity': + new_capacity = adjustment + else: + # PercentChangeInCapacity + new_capacity = capacity + (capacity * adjustment / 100) + + if new_capacity > int(self.properties['MaxSize']): + logger.warn('can not exceed %s' % self.properties['MaxSize']) + return + if new_capacity < int(self.properties['MinSize']): + logger.warn('can not be less than %s' % self.properties['MinSize']) + return + + self.resize(new_capacity) + + def FnGetRefId(self): + return unicode(self.name) + + class LaunchConfiguration(resource.Resource): tags_schema = {'Key': {'Type': 'String', 'Required': True}, @@ -211,4 +253,5 @@ def resource_mapping(): 'AWS::AutoScaling::LaunchConfiguration': LaunchConfiguration, 'AWS::AutoScaling::AutoScalingGroup': AutoScalingGroup, 'AWS::AutoScaling::ScalingPolicy': ScalingPolicy, + 'OS::Heat::InstanceGroup': InstanceGroup, } diff --git a/heat/tests/test_instance_group.py b/heat/tests/test_instance_group.py new file mode 100644 index 00000000..5086afd0 --- /dev/null +++ b/heat/tests/test_instance_group.py @@ -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() diff --git a/templates/ppetit.template b/templates/InstanceGroup.template similarity index 92% rename from templates/ppetit.template rename to templates/InstanceGroup.template index 16217659..98510a8f 100644 --- a/templates/ppetit.template +++ b/templates/InstanceGroup.template @@ -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" : "" } } }, -- 2.45.2