diff --git a/heat/engine/resources/autoscaling.py b/heat/engine/resources/autoscaling.py index 79ad56ae3..04987b889 100644 --- a/heat/engine/resources/autoscaling.py +++ b/heat/engine/resources/autoscaling.py @@ -14,6 +14,7 @@ # under the License. import copy + from heat.common import exception from heat.engine import resource from heat.engine import signal_responder @@ -21,6 +22,7 @@ from heat.engine import signal_responder from heat.openstack.common import log as logging from heat.openstack.common import timeutils from heat.engine.properties import Properties +from heat.engine import properties from heat.engine import stack_resource logger = logging.getLogger(__name__) @@ -73,12 +75,43 @@ class InstanceGroup(stack_resource.StackResource): 'Schema': {'Type': 'Map', 'Schema': tags_schema}} } - update_allowed_keys = ('Properties',) + update_allowed_keys = ('Properties', 'UpdatePolicy',) update_allowed_properties = ('Size', 'LaunchConfigurationName',) attributes_schema = { "InstanceList": ("A comma-delimited list of server ip addresses. " "(Heat extension)") } + rolling_update_schema = { + 'MinInstancesInService': properties.Schema(properties.NUMBER, + default=0), + 'MaxBatchSize': properties.Schema(properties.NUMBER, + default=1), + 'PauseTime': properties.Schema(properties.STRING, + default='PT0S') + } + update_policy_schema = { + 'RollingUpdate': properties.Schema(properties.MAP, + schema=rolling_update_schema) + } + + def __init__(self, name, json_snippet, stack): + """ + UpdatePolicy is currently only specific to InstanceGroup and + AutoScalingGroup. Therefore, init is overridden to parse for the + UpdatePolicy. + """ + super(InstanceGroup, self).__init__(name, json_snippet, stack) + self.update_policy = Properties(self.update_policy_schema, + self.t.get('UpdatePolicy', {}), + parent_name=self.name) + + def validate(self): + """ + Add validation for update_policy + """ + super(InstanceGroup, self).validate() + if self.update_policy: + self.update_policy.validate() def get_instance_names(self): """Get a list of resource names of the instances in this InstanceGroup. @@ -120,6 +153,14 @@ class InstanceGroup(stack_resource.StackResource): If Properties has changed, update self.properties, so we get the new values during any subsequent adjustment. """ + if tmpl_diff: + # parse update policy + if 'UpdatePolicy' in tmpl_diff: + self.update_policy = Properties( + self.update_policy_schema, + json_snippet.get('UpdatePolicy', {}), + parent_name=self.name) + if prop_diff: self.properties = Properties(self.properties_schema, json_snippet.get('Properties', {}), @@ -238,10 +279,22 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin): 'Tags': {'Type': 'List', 'Schema': {'Type': 'Map', 'Schema': tags_schema}} } + rolling_update_schema = { + 'MinInstancesInService': properties.Schema(properties.NUMBER, + default=0), + 'MaxBatchSize': properties.Schema(properties.NUMBER, + default=1), + 'PauseTime': properties.Schema(properties.STRING, + default='PT0S') + } + update_policy_schema = { + 'AutoScalingRollingUpdate': properties.Schema( + properties.MAP, schema=rolling_update_schema) + } # 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_keys = ('Properties', 'UpdatePolicy',) update_allowed_properties = ('LaunchConfigurationName', 'MaxSize', 'MinSize', 'Cooldown', 'DesiredCapacity',) @@ -267,6 +320,14 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin): If Properties has changed, update self.properties, so we get the new values during any subsequent adjustment. """ + if tmpl_diff: + # parse update policy + if 'UpdatePolicy' in tmpl_diff: + self.update_policy = Properties( + self.update_policy_schema, + json_snippet.get('UpdatePolicy', {}), + parent_name=self.name) + if prop_diff: self.properties = Properties(self.properties_schema, json_snippet.get('Properties', {}), diff --git a/heat/tests/test_autoscaling_update_policy.py b/heat/tests/test_autoscaling_update_policy.py new file mode 100644 index 000000000..4fceef1f6 --- /dev/null +++ b/heat/tests/test_autoscaling_update_policy.py @@ -0,0 +1,382 @@ +# 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 exception +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 import utils + + +asg_tmpl_without_updt_policy = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create autoscaling group.", + "Parameters" : {}, + "Resources" : { + "WebServerGroup" : { + "Type" : "AWS::AutoScaling::AutoScalingGroup", + "Properties" : { + "AvailabilityZones" : ["nova"], + "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, + "MinSize" : "1", + "MaxSize" : "10" + } + }, + "LaunchConfig" : { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId" : "foo", + "InstanceType" : "m1.medium", + "KeyName" : "test", + "SecurityGroups" : [ "sg-1" ], + "UserData" : "jsconfig data" + } + } + } +} +''' + +asg_tmpl_with_bad_updt_policy = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create autoscaling group.", + "Parameters" : {}, + "Resources" : { + "WebServerGroup" : { + "UpdatePolicy": { + "foo": { + } + }, + "Type" : "AWS::AutoScaling::AutoScalingGroup", + "Properties" : { + "AvailabilityZones" : ["nova"], + "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, + "MinSize" : "1", + "MaxSize" : "10" + } + }, + "LaunchConfig" : { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId" : "foo", + "InstanceType" : "m1.medium", + "KeyName" : "test", + "SecurityGroups" : [ "sg-1" ], + "UserData" : "jsconfig data" + } + } + } +} +''' + +asg_tmpl_with_default_updt_policy = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create autoscaling group.", + "Parameters" : {}, + "Resources" : { + "WebServerGroup" : { + "UpdatePolicy" : { + "AutoScalingRollingUpdate" : { + } + }, + "Type" : "AWS::AutoScaling::AutoScalingGroup", + "Properties" : { + "AvailabilityZones" : ["nova"], + "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, + "MinSize" : "1", + "MaxSize" : "10" + } + }, + "LaunchConfig" : { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId" : "foo", + "InstanceType" : "m1.medium", + "KeyName" : "test", + "SecurityGroups" : [ "sg-1" ], + "UserData" : "jsconfig data" + } + } + } +} +''' + +asg_tmpl_with_updt_policy_1 = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create autoscaling group.", + "Parameters" : {}, + "Resources" : { + "WebServerGroup" : { + "UpdatePolicy" : { + "AutoScalingRollingUpdate" : { + "MinInstancesInService" : "1", + "MaxBatchSize" : "3", + "PauseTime" : "PT30S" + } + }, + "Type" : "AWS::AutoScaling::AutoScalingGroup", + "Properties" : { + "AvailabilityZones" : ["nova"], + "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, + "MinSize" : "1", + "MaxSize" : "10" + } + }, + "LaunchConfig" : { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId" : "foo", + "InstanceType" : "m1.medium", + "KeyName" : "test", + "SecurityGroups" : [ "sg-1" ], + "UserData" : "jsconfig data" + } + } + } +} +''' + +asg_tmpl_with_updt_policy_2 = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create autoscaling group.", + "Parameters" : {}, + "Resources" : { + "WebServerGroup" : { + "UpdatePolicy" : { + "AutoScalingRollingUpdate" : { + "MinInstancesInService" : "1", + "MaxBatchSize" : "5", + "PauseTime" : "PT30S" + } + }, + "Type" : "AWS::AutoScaling::AutoScalingGroup", + "Properties" : { + "AvailabilityZones" : ["nova"], + "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, + "MinSize" : "1", + "MaxSize" : "10" + } + }, + "LaunchConfig" : { + "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_parse_without_update_policy(self): + tmpl = template_format.parse(asg_tmpl_without_updt_policy) + stack = utils.parse_stack(tmpl) + grp = stack.resources['WebServerGroup'] + self.assertFalse(grp.update_policy['AutoScalingRollingUpdate']) + + def test_parse_with_update_policy(self): + tmpl = template_format.parse(asg_tmpl_with_updt_policy_1) + stack = utils.parse_stack(tmpl) + grp = stack.resources['WebServerGroup'] + self.assertTrue(grp.update_policy) + self.assertTrue(len(grp.update_policy) == 1) + self.assertTrue('AutoScalingRollingUpdate' in grp.update_policy) + policy = grp.update_policy['AutoScalingRollingUpdate'] + self.assertTrue(policy and len(policy) > 0) + self.assertEqual(int(policy['MinInstancesInService']), 1) + self.assertEqual(int(policy['MaxBatchSize']), 3) + self.assertEqual(policy['PauseTime'], 'PT30S') + + def test_parse_with_default_update_policy(self): + tmpl = template_format.parse(asg_tmpl_with_default_updt_policy) + stack = utils.parse_stack(tmpl) + grp = stack.resources['WebServerGroup'] + self.assertTrue(grp.update_policy) + self.assertTrue(len(grp.update_policy) == 1) + self.assertTrue('AutoScalingRollingUpdate' in grp.update_policy) + policy = grp.update_policy['AutoScalingRollingUpdate'] + self.assertTrue(policy and len(policy) > 0) + self.assertEqual(int(policy['MinInstancesInService']), 0) + self.assertEqual(int(policy['MaxBatchSize']), 1) + self.assertEqual(policy['PauseTime'], 'PT0S') + + def test_parse_with_bad_update_policy(self): + tmpl = template_format.parse(asg_tmpl_with_bad_updt_policy) + stack = utils.parse_stack(tmpl) + self.assertRaises(exception.StackValidationFailed, stack.validate) + + def validate_update_policy_diff(self, current, updated): + + # load current stack + current_tmpl = template_format.parse(current) + current_stack = utils.parse_stack(current_tmpl) + + # get the json snippet for the current InstanceGroup resource + current_grp = current_stack.resources['WebServerGroup'] + current_snippets = dict((r.name, r.parsed_template()) + for r in current_stack) + current_grp_json = current_snippets[current_grp.name] + + # load the updated stack + updated_tmpl = template_format.parse(updated) + updated_stack = utils.parse_stack(updated_tmpl) + + # get the updated json snippet for the InstanceGroup resource in the + # context of the current stack + updated_grp = updated_stack.resources['WebServerGroup'] + updated_grp_json = current_stack.resolve_runtime_data(updated_grp.t) + + # identify the template difference + tmpl_diff = updated_grp.update_template_diff( + updated_grp_json, current_grp_json) + updated_policy = (updated_grp.t['UpdatePolicy'] + if 'UpdatePolicy' in updated_grp.t else None) + expected = {u'UpdatePolicy': updated_policy} + self.assertEqual(tmpl_diff, expected) + + def test_update_policy_added(self): + self.validate_update_policy_diff(asg_tmpl_without_updt_policy, + asg_tmpl_with_updt_policy_1) + + def test_update_policy_updated(self): + self.validate_update_policy_diff(asg_tmpl_with_updt_policy_1, + asg_tmpl_with_updt_policy_2) + + def test_update_policy_removed(self): + self.validate_update_policy_diff(asg_tmpl_with_updt_policy_1, + asg_tmpl_without_updt_policy) + + def test_autoscaling_group_update(self): + + # setup stack from the initial template + tmpl = template_format.parse(asg_tmpl_with_updt_policy_1) + stack = utils.parse_stack(tmpl) + nested = stack.resources['WebServerGroup'].nested() + + # test stack create + # test the number of instance creation + # test that physical resource name of launch configuration is used + size = int(stack.resources['WebServerGroup'].properties['MinSize']) + self._stub_create(size) + self.m.ReplayAll() + stack.create() + self.m.VerifyAll() + self.assertEqual(stack.state, ('CREATE', 'COMPLETE')) + conf = stack.resources['LaunchConfig'] + conf_name_pattern = '%s-LaunchConfig-[a-zA-Z0-9]+$' % stack.name + regex_pattern = re.compile(conf_name_pattern) + self.assertTrue(regex_pattern.match(conf.FnGetRefId())) + nested = stack.resources['WebServerGroup'].nested() + self.assertTrue(len(nested.resources), size) + + # test stack update + # test that update policy is updated + # test that launch configuration is replaced + current_grp = stack.resources['WebServerGroup'] + self.assertTrue('AutoScalingRollingUpdate' + in current_grp.update_policy) + current_policy = current_grp.update_policy['AutoScalingRollingUpdate'] + self.assertTrue(current_policy and len(current_policy) > 0) + self.assertEqual(int(current_policy['MaxBatchSize']), 3) + conf_name = self.get_launch_conf_name(stack, 'WebServerGroup') + updated_tmpl = template_format.parse(asg_tmpl_with_updt_policy_2) + updated_stack = utils.parse_stack(updated_tmpl) + stack.update(updated_stack) + self.assertEqual(stack.state, ('UPDATE', 'COMPLETE')) + updated_grp = stack.resources['WebServerGroup'] + self.assertTrue('AutoScalingRollingUpdate' + in updated_grp.update_policy) + updated_policy = updated_grp.update_policy['AutoScalingRollingUpdate'] + self.assertTrue(updated_policy and len(updated_policy) > 0) + self.assertEqual(int(updated_policy['MaxBatchSize']), 5) + updated_conf_name = self.get_launch_conf_name(stack, 'WebServerGroup') + self.assertNotEqual(conf_name, updated_conf_name) + + def test_autoscaling_group_update_policy_removed(self): + + # setup stack from the initial template + tmpl = template_format.parse(asg_tmpl_with_updt_policy_1) + stack = utils.parse_stack(tmpl) + nested = stack.resources['WebServerGroup'].nested() + + # test stack create + # test the number of instance creation + # test that physical resource name of launch configuration is used + size = int(stack.resources['WebServerGroup'].properties['MinSize']) + self._stub_create(size) + self.m.ReplayAll() + stack.create() + self.m.VerifyAll() + self.assertEqual(stack.state, ('CREATE', 'COMPLETE')) + conf = stack.resources['LaunchConfig'] + conf_name_pattern = '%s-LaunchConfig-[a-zA-Z0-9]+$' % stack.name + regex_pattern = re.compile(conf_name_pattern) + self.assertTrue(regex_pattern.match(conf.FnGetRefId())) + nested = stack.resources['WebServerGroup'].nested() + self.assertTrue(len(nested.resources), size) + + # test stack update + # test that update policy is removed + current_grp = stack.resources['WebServerGroup'] + self.assertTrue('AutoScalingRollingUpdate' + in current_grp.update_policy) + current_policy = current_grp.update_policy['AutoScalingRollingUpdate'] + self.assertTrue(current_policy and len(current_policy) > 0) + self.assertEqual(int(current_policy['MaxBatchSize']), 3) + updated_tmpl = template_format.parse(asg_tmpl_without_updt_policy) + updated_stack = utils.parse_stack(updated_tmpl) + stack.update(updated_stack) + self.assertEqual(stack.state, ('UPDATE', 'COMPLETE')) + updated_grp = stack.resources['WebServerGroup'] + self.assertFalse(updated_grp.update_policy['AutoScalingRollingUpdate']) diff --git a/heat/tests/test_instance_group_update_policy.py b/heat/tests/test_instance_group_update_policy.py index 2ac000b15..bdffeeb47 100644 --- a/heat/tests/test_instance_group_update_policy.py +++ b/heat/tests/test_instance_group_update_policy.py @@ -14,15 +14,16 @@ import re +from heat.common import exception 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 +from heat.tests import utils -ig_template_before = ''' +ig_tmpl_without_updt_policy = ''' { "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "Template to create multiple instances.", @@ -50,13 +51,118 @@ ig_template_before = ''' } ''' -ig_template_after = ''' +ig_tmpl_with_bad_updt_policy = ''' { "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "Template to create multiple instances.", "Parameters" : {}, "Resources" : { "JobServerGroup" : { + "UpdatePolicy" : { + "RollingUpdate": "foo" + }, + "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_tmpl_with_default_updt_policy = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create multiple instances.", + "Parameters" : {}, + "Resources" : { + "JobServerGroup" : { + "UpdatePolicy" : { + "RollingUpdate" : { + } + }, + "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_tmpl_with_updt_policy_1 = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create multiple instances.", + "Parameters" : {}, + "Resources" : { + "JobServerGroup" : { + "UpdatePolicy" : { + "RollingUpdate" : { + "MinInstancesInService" : "1", + "MaxBatchSize" : "3", + "PauseTime" : "PT30S" + } + }, + "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_tmpl_with_updt_policy_2 = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to create multiple instances.", + "Parameters" : {}, + "Resources" : { + "JobServerGroup" : { + "UpdatePolicy" : { + "RollingUpdate" : { + "MinInstancesInService" : "1", + "MaxBatchSize" : "5", + "PauseTime" : "PT30S" + } + }, "Type" : "OS::Heat::InstanceGroup", "Properties" : { "LaunchConfigurationName" : { "Ref" : "JobServerConfig" }, @@ -107,11 +213,90 @@ class InstanceGroupTest(HeatTestCase): def get_launch_conf_name(self, stack, ig_name): return stack.resources[ig_name].properties['LaunchConfigurationName'] - def test_instance_group(self): + def test_parse_without_update_policy(self): + tmpl = template_format.parse(ig_tmpl_without_updt_policy) + stack = utils.parse_stack(tmpl) + grp = stack.resources['JobServerGroup'] + self.assertFalse(grp.update_policy['RollingUpdate']) + + def test_parse_with_update_policy(self): + tmpl = template_format.parse(ig_tmpl_with_updt_policy_1) + stack = utils.parse_stack(tmpl) + grp = stack.resources['JobServerGroup'] + self.assertTrue(grp.update_policy) + self.assertTrue(len(grp.update_policy) == 1) + self.assertTrue('RollingUpdate' in grp.update_policy) + policy = grp.update_policy['RollingUpdate'] + self.assertTrue(policy and len(policy) > 0) + self.assertEqual(int(policy['MinInstancesInService']), 1) + self.assertEqual(int(policy['MaxBatchSize']), 3) + self.assertEqual(policy['PauseTime'], 'PT30S') + + def test_parse_with_default_update_policy(self): + tmpl = template_format.parse(ig_tmpl_with_default_updt_policy) + stack = utils.parse_stack(tmpl) + grp = stack.resources['JobServerGroup'] + self.assertTrue(grp.update_policy) + self.assertTrue(len(grp.update_policy) == 1) + self.assertTrue('RollingUpdate' in grp.update_policy) + policy = grp.update_policy['RollingUpdate'] + self.assertTrue(policy and len(policy) > 0) + self.assertEqual(int(policy['MinInstancesInService']), 0) + self.assertEqual(int(policy['MaxBatchSize']), 1) + self.assertEqual(policy['PauseTime'], 'PT0S') + + def test_parse_with_bad_update_policy(self): + tmpl = template_format.parse(ig_tmpl_with_bad_updt_policy) + stack = utils.parse_stack(tmpl) + self.assertRaises(exception.StackValidationFailed, stack.validate) + + def validate_update_policy_diff(self, current, updated): + + # load current stack + current_tmpl = template_format.parse(current) + current_stack = utils.parse_stack(current_tmpl) + + # get the json snippet for the current InstanceGroup resource + current_grp = current_stack.resources['JobServerGroup'] + current_snippets = dict((r.name, r.parsed_template()) + for r in current_stack) + current_grp_json = current_snippets[current_grp.name] + + # load the updated stack + updated_tmpl = template_format.parse(updated) + updated_stack = utils.parse_stack(updated_tmpl) + + # get the updated json snippet for the InstanceGroup resource in the + # context of the current stack + updated_grp = updated_stack.resources['JobServerGroup'] + updated_grp_json = current_stack.resolve_runtime_data(updated_grp.t) + + # identify the template difference + tmpl_diff = updated_grp.update_template_diff( + updated_grp_json, current_grp_json) + updated_policy = (updated_grp.t['UpdatePolicy'] + if 'UpdatePolicy' in updated_grp.t else None) + expected = {u'UpdatePolicy': updated_policy} + self.assertEqual(tmpl_diff, expected) + + def test_update_policy_added(self): + self.validate_update_policy_diff(ig_tmpl_without_updt_policy, + ig_tmpl_with_updt_policy_1) + + def test_update_policy_updated(self): + self.validate_update_policy_diff(ig_tmpl_with_updt_policy_1, + ig_tmpl_with_updt_policy_2) + + def test_update_policy_removed(self): + self.validate_update_policy_diff(ig_tmpl_with_updt_policy_1, + ig_tmpl_without_updt_policy) + + def test_instance_group_update(self): # setup stack from the initial template - tmpl = template_format.parse(ig_template_before) - stack = parse_stack(tmpl) + tmpl = template_format.parse(ig_tmpl_with_updt_policy_1) + stack = utils.parse_stack(tmpl) + nested = stack.resources['JobServerGroup'].nested() # test stack create # test the number of instance creation @@ -121,17 +306,68 @@ class InstanceGroupTest(HeatTestCase): self.m.ReplayAll() stack.create() self.m.VerifyAll() - self.assertEqual(stack.status, stack.COMPLETE) + self.assertEqual(stack.state, ('CREATE', '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())) + nested = stack.resources['JobServerGroup'].nested() + self.assertTrue(len(nested.resources), size) # test stack update + # test that update policy is updated # test that launch configuration is replaced + current_grp = stack.resources['JobServerGroup'] + self.assertTrue('RollingUpdate' in current_grp.update_policy) + current_policy = current_grp.update_policy['RollingUpdate'] + self.assertTrue(current_policy and len(current_policy) > 0) + self.assertEqual(int(current_policy['MaxBatchSize']), 3) conf_name = self.get_launch_conf_name(stack, 'JobServerGroup') - updated_tmpl = template_format.parse(ig_template_after) - updated_stack = parse_stack(updated_tmpl) + updated_tmpl = template_format.parse(ig_tmpl_with_updt_policy_2) + updated_stack = utils.parse_stack(updated_tmpl) stack.update(updated_stack) + self.assertEqual(stack.state, ('UPDATE', 'COMPLETE')) + updated_grp = stack.resources['JobServerGroup'] + self.assertTrue('RollingUpdate' in updated_grp.update_policy) + updated_policy = updated_grp.update_policy['RollingUpdate'] + self.assertTrue(updated_policy and len(updated_policy) > 0) + self.assertEqual(int(updated_policy['MaxBatchSize']), 5) updated_conf_name = self.get_launch_conf_name(stack, 'JobServerGroup') self.assertNotEqual(conf_name, updated_conf_name) + + def test_instance_group_update_policy_removed(self): + + # setup stack from the initial template + tmpl = template_format.parse(ig_tmpl_with_updt_policy_1) + stack = utils.parse_stack(tmpl) + nested = stack.resources['JobServerGroup'].nested() + + # 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.state, ('CREATE', '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())) + nested = stack.resources['JobServerGroup'].nested() + self.assertTrue(len(nested.resources), size) + + # test stack update + # test that update policy is removed + current_grp = stack.resources['JobServerGroup'] + self.assertTrue('RollingUpdate' in current_grp.update_policy) + current_policy = current_grp.update_policy['RollingUpdate'] + self.assertTrue(current_policy and len(current_policy) > 0) + self.assertEqual(int(current_policy['MaxBatchSize']), 3) + updated_tmpl = template_format.parse(ig_tmpl_without_updt_policy) + updated_stack = utils.parse_stack(updated_tmpl) + stack.update(updated_stack) + self.assertEqual(stack.state, ('UPDATE', 'COMPLETE')) + updated_grp = stack.resources['JobServerGroup'] + self.assertFalse(updated_grp.update_policy['RollingUpdate'])