diff --git a/heat/engine/resources/autoscaling.py b/heat/engine/resources/autoscaling.py index d541de7ac..79ad56ae3 100644 --- a/heat/engine/resources/autoscaling.py +++ b/heat/engine/resources/autoscaling.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy from heat.common import exception from heat.engine import resource from heat.engine import signal_responder @@ -73,7 +74,7 @@ class InstanceGroup(stack_resource.StackResource): 'Schema': tags_schema}} } update_allowed_keys = ('Properties',) - update_allowed_properties = ('Size',) + update_allowed_properties = ('Size', 'LaunchConfigurationName',) attributes_schema = { "InstanceList": ("A comma-delimited list of server ip addresses. " "(Heat extension)") @@ -154,12 +155,12 @@ class InstanceGroup(stack_resource.StackResource): launch configuration. """ conf_name = self.properties['LaunchConfigurationName'] - instance_definition = self.stack.t['Resources'][conf_name].copy() + conf = self.stack.resource_by_refid(conf_name) + instance_definition = copy.deepcopy(conf.t) instance_definition['Type'] = 'AWS::EC2::Instance' instance_definition['Properties']['Tags'] = self._tags() # resolve references within the context of this stack. - static_parsed = self.stack.resolve_static_data(instance_definition) - fully_parsed = self.stack.resolve_runtime_data(static_parsed) + fully_parsed = self.stack.resolve_runtime_data(instance_definition) resources = {} for i in range(num_instances): @@ -241,7 +242,8 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin): # template keys and properties supported for handle_update, # note trailing comma is required for a single item to get a tuple update_allowed_keys = ('Properties',) - update_allowed_properties = ('MaxSize', 'MinSize', + update_allowed_properties = ('LaunchConfigurationName', + 'MaxSize', 'MinSize', 'Cooldown', 'DesiredCapacity',) def handle_create(self): @@ -364,6 +366,9 @@ class LaunchConfiguration(resource.Resource): 'Schema': tags_schema}}, } + def FnGetRefId(self): + return unicode(self.physical_resource_name()) + class ScalingPolicy(signal_responder.SignalResponder, CooldownMixin): properties_schema = { diff --git a/heat/tests/test_autoscaling.py b/heat/tests/test_autoscaling.py index 4759675d9..7ea0b1c71 100644 --- a/heat/tests/test_autoscaling.py +++ b/heat/tests/test_autoscaling.py @@ -106,18 +106,21 @@ class AutoScalingTest(HeatTestCase): self.fc = fakes.FakeKeystoneClient() def create_scaling_group(self, t, stack, resource_name): - rsrc = asc.AutoScalingGroup(resource_name, - t['Resources'][resource_name], - stack) + # create the launch configuration resource + conf = stack.resources['LaunchConfig'] + self.assertEqual(None, conf.validate()) + scheduler.TaskRunner(conf.create)() + self.assertEqual((conf.CREATE, conf.COMPLETE), conf.state) + + # create the group resource + rsrc = stack.resources[resource_name] self.assertEqual(None, rsrc.validate()) scheduler.TaskRunner(rsrc.create)() self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) return rsrc def create_scaling_policy(self, t, stack, resource_name): - rsrc = asc.ScalingPolicy(resource_name, - t['Resources'][resource_name], - stack) + rsrc = stack.resources[resource_name] self.assertEqual(None, rsrc.validate()) scheduler.TaskRunner(rsrc.create)() self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) @@ -231,7 +234,7 @@ class AutoScalingTest(HeatTestCase): self.assertEqual('WebServerGroup', rsrc.FnGetRefId()) self.assertEqual(['WebServerGroup-0'], rsrc.get_instance_names()) update_snippet = copy.deepcopy(rsrc.parsed_template()) - update_snippet['Properties']['LaunchConfigurationName'] = 'foo' + update_snippet['Properties']['AvailabilityZones'] = ['foo'] self.assertRaises(resource.UpdateReplace, rsrc.update, update_snippet) @@ -460,9 +463,13 @@ class AutoScalingTest(HeatTestCase): instance.Instance.handle_create().AndRaise(Exception) self.m.ReplayAll() - rsrc = asc.AutoScalingGroup('WebServerGroup', - t['Resources']['WebServerGroup'], - stack) + + conf = stack.resources['LaunchConfig'] + self.assertEqual(None, conf.validate()) + scheduler.TaskRunner(conf.create)() + self.assertEqual((conf.CREATE, conf.COMPLETE), conf.state) + + rsrc = stack.resources['WebServerGroup'] self.assertEqual(None, rsrc.validate()) self.assertRaises(exception.ResourceFailure, scheduler.TaskRunner(rsrc.create)) diff --git a/heat/tests/test_instance_group.py b/heat/tests/test_instance_group.py index c4f554e8f..9c4ac7eb5 100644 --- a/heat/tests/test_instance_group.py +++ b/heat/tests/test_instance_group.py @@ -16,7 +16,6 @@ import copy from heat.common import exception from heat.common import template_format -from heat.engine.resources import autoscaling as asc from heat.engine.resources import instance from heat.engine import resource from heat.engine import resources @@ -81,10 +80,10 @@ class InstanceGroupTest(HeatTestCase): instance_class.check_create_complete( cookie).MultipleTimes().AndReturn(True) - def create_instance_group(self, t, stack, resource_name): - rsrc = asc.InstanceGroup(resource_name, - t['Resources'][resource_name], - stack) + def create_resource(self, t, stack, resource_name): + # subsequent resources may need to reference previous created resources + # use the stack's resource objects instead of instantiating new ones + rsrc = stack.resources[resource_name] self.assertEqual(None, rsrc.validate()) scheduler.TaskRunner(rsrc.create)() self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) @@ -101,8 +100,8 @@ class InstanceGroupTest(HeatTestCase): instance.Instance.FnGetAtt('PublicIp').AndReturn('1.2.3.4') self.m.ReplayAll() - rsrc = self.create_instance_group(t, stack, 'JobServerGroup') - + conf = self.create_resource(t, stack, 'JobServerConfig') + rsrc = self.create_resource(t, stack, 'JobServerGroup') self.assertEqual('JobServerGroup', rsrc.FnGetRefId()) self.assertEqual('1.2.3.4', rsrc.FnGetAtt('InstanceList')) @@ -133,8 +132,8 @@ class InstanceGroupTest(HeatTestCase): self._stub_create(1, instance_class=MyInstance) self.m.ReplayAll() - - rsrc = self.create_instance_group(t, stack, 'JobServerGroup') + conf = self.create_resource(t, stack, 'JobServerConfig') + rsrc = self.create_resource(t, stack, 'JobServerGroup') self.assertEqual('JobServerGroup', rsrc.FnGetRefId()) rsrc.delete() self.m.VerifyAll() @@ -144,9 +143,8 @@ class InstanceGroupTest(HeatTestCase): t = template_format.parse(ig_template) stack = utils.parse_stack(t) - rsrc = asc.InstanceGroup('JobServerGroup', - t['Resources']['JobServerGroup'], - stack) + conf = self.create_resource(t, stack, 'JobServerConfig') + rsrc = stack.resources['JobServerGroup'] self.m.StubOutWithMock(instance.Instance, 'handle_create') not_found = exception.ImageNotFound(image_name='bla') @@ -170,7 +168,8 @@ class InstanceGroupTest(HeatTestCase): self._stub_create(2) self.m.ReplayAll() - rsrc = self.create_instance_group(t, stack, 'JobServerGroup') + conf = self.create_resource(t, stack, 'JobServerConfig') + rsrc = self.create_resource(t, stack, 'JobServerGroup') self.m.VerifyAll() self.m.UnsetStubs() @@ -206,7 +205,8 @@ class InstanceGroupTest(HeatTestCase): self._stub_create(2) self.m.ReplayAll() - rsrc = self.create_instance_group(t, stack, 'JobServerGroup') + conf = self.create_resource(t, stack, 'JobServerConfig') + rsrc = self.create_resource(t, stack, 'JobServerGroup') self.m.ReplayAll() @@ -226,12 +226,13 @@ class InstanceGroupTest(HeatTestCase): self._stub_create(2) self.m.ReplayAll() - rsrc = self.create_instance_group(t, stack, 'JobServerGroup') + conf = self.create_resource(t, stack, 'JobServerConfig') + rsrc = self.create_resource(t, stack, 'JobServerGroup') self.m.ReplayAll() update_snippet = copy.deepcopy(rsrc.parsed_template()) - update_snippet['Properties']['LaunchConfigurationName'] = 'wibble' + update_snippet['Properties']['AvailabilityZones'] = ['wibble'] self.assertRaises(resource.UpdateReplace, rsrc.update, update_snippet) diff --git a/heat/tests/test_instance_group_update_policy.py b/heat/tests/test_instance_group_update_policy.py new file mode 100644 index 000000000..2ac000b15 --- /dev/null +++ b/heat/tests/test_instance_group_update_policy.py @@ -0,0 +1,137 @@ +# 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 re + +from heat.common import template_format +from heat.engine.resources import instance +from heat.engine import parser +from heat.tests.common import HeatTestCase +from heat.tests.utils import setup_dummy_db +from heat.tests.utils import parse_stack + + +ig_template_before = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create multiple instances.", + "Parameters" : {}, + "Resources" : { + "JobServerGroup" : { + "Type" : "OS::Heat::InstanceGroup", + "Properties" : { + "LaunchConfigurationName" : { "Ref" : "JobServerConfig" }, + "Size" : "8", + "AvailabilityZones" : ["nova"] + } + }, + "JobServerConfig" : { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId" : "foo", + "InstanceType" : "m1.medium", + "KeyName" : "test", + "SecurityGroups" : [ "sg-1" ], + "UserData" : "jsconfig data" + } + } + } +} +''' + +ig_template_after = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create multiple instances.", + "Parameters" : {}, + "Resources" : { + "JobServerGroup" : { + "Type" : "OS::Heat::InstanceGroup", + "Properties" : { + "LaunchConfigurationName" : { "Ref" : "JobServerConfig" }, + "Size" : "8", + "AvailabilityZones" : ["nova"] + } + }, + "JobServerConfig" : { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId" : "foo", + "InstanceType" : "m1.large", + "KeyName" : "test", + "SecurityGroups" : [ "sg-1" ], + "UserData" : "jsconfig data" + } + } + } +} +''' + + +class InstanceGroupTest(HeatTestCase): + def setUp(self): + super(InstanceGroupTest, self).setUp() + setup_dummy_db() + + def _stub_create(self, num, instance_class=instance.Instance): + """ + Expect creation of C{num} number of Instances. + + :param instance_class: The resource class to expect to be created + instead of instance.Instance. + """ + + self.m.StubOutWithMock(parser.Stack, 'validate') + parser.Stack.validate() + + self.m.StubOutWithMock(instance_class, 'handle_create') + self.m.StubOutWithMock(instance_class, 'check_create_complete') + cookie = object() + for x in range(num): + instance_class.handle_create().AndReturn(cookie) + instance_class.check_create_complete(cookie).AndReturn(False) + instance_class.check_create_complete( + cookie).MultipleTimes().AndReturn(True) + + def get_launch_conf_name(self, stack, ig_name): + return stack.resources[ig_name].properties['LaunchConfigurationName'] + + def test_instance_group(self): + + # setup stack from the initial template + tmpl = template_format.parse(ig_template_before) + stack = parse_stack(tmpl) + + # test stack create + # test the number of instance creation + # test that physical resource name of launch configuration is used + size = int(stack.resources['JobServerGroup'].properties['Size']) + self._stub_create(size) + self.m.ReplayAll() + stack.create() + self.m.VerifyAll() + self.assertEqual(stack.status, stack.COMPLETE) + conf = stack.resources['JobServerConfig'] + conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack.name + regex_pattern = re.compile(conf_name_pattern) + self.assertTrue(regex_pattern.match(conf.FnGetRefId())) + + # test stack update + # test that launch configuration is replaced + conf_name = self.get_launch_conf_name(stack, 'JobServerGroup') + updated_tmpl = template_format.parse(ig_template_after) + updated_stack = parse_stack(updated_tmpl) + stack.update(updated_stack) + updated_conf_name = self.get_launch_conf_name(stack, 'JobServerGroup') + self.assertNotEqual(conf_name, updated_conf_name) diff --git a/heat/tests/test_server_tags.py b/heat/tests/test_server_tags.py index 742cc8a1d..e6b517e9a 100644 --- a/heat/tests/test_server_tags.py +++ b/heat/tests/test_server_tags.py @@ -15,7 +15,6 @@ import mox from heat.engine import environment from heat.tests.v1_1 import fakes -from heat.engine.resources import autoscaling from heat.engine.resources import instance as instances from heat.engine.resources import nova_utils from heat.common import template_format @@ -77,7 +76,7 @@ group_template = ''' "Type": "OS::Heat::InstanceGroup", "Properties": { "AvailabilityZones" : ["nova"], - "LaunchConfigurationName": "Config", + "LaunchConfigurationName": { "Ref": "Config" }, "Size" : "1" } } @@ -145,9 +144,14 @@ class ServerTagsTest(HeatTestCase): stack_id=uuidutils.generate_uuid()) t['Resources']['WebServer']['Properties']['Tags'] = intags - group = autoscaling.InstanceGroup('WebServer', - t['Resources']['WebServer'], - stack) + + # create the launch configuration + conf = stack.resources['Config'] + self.assertEqual(None, conf.validate()) + scheduler.TaskRunner(conf.create)() + self.assertEqual((conf.CREATE, conf.COMPLETE), conf.state) + + group = stack.resources['WebServer'] self.m.StubOutWithMock(instances.Instance, 'nova') instances.Instance.nova().MultipleTimes().AndReturn(self.fc)