]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Make a dedicated InstanceGroup
authorAngus Salkeld <asalkeld@redhat.com>
Fri, 25 Jan 2013 00:56:29 +0000 (11:56 +1100)
committerAngus Salkeld <asalkeld@redhat.com>
Fri, 25 Jan 2013 01:50:48 +0000 (12:50 +1100)
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
heat/tests/test_instance_group.py [new file with mode: 0644]
templates/InstanceGroup.template [moved from templates/ppetit.template with 92% similarity]

index 0ce69609601917497358bb6f89f5c54843d43e5a..caf500dfb2840661201121ff5e52a0e0ac87aa44 100644 (file)
@@ -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 (file)
index 0000000..5086afd
--- /dev/null
@@ -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()
similarity index 92%
rename from templates/ppetit.template
rename to templates/InstanceGroup.template
index 1621765997957fadb0c41fbaec6ffe2dce3c8bae..98510a8f939b32b7d607872d923f775f977dddc2 100644 (file)
 
   "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" : "" }
       }
     },