diff --git a/heat/common/grouputils.py b/heat/common/grouputils.py index 35bbd44e64..5cb8447769 100644 --- a/heat/common/grouputils.py +++ b/heat/common/grouputils.py @@ -30,17 +30,20 @@ def get_size(group, include_failed=False): return 0 -def get_members(group): +def get_members(group, include_failed=False): """Get a list of member resources managed by the specified group. Sort the list of instances first by created_time then by name. + If include_failed is set, failed members will be put first in the + list sorted by created_time then by name. """ resources = [] if group.nested(): resources = [r for r in six.itervalues(group.nested()) - if r.status != r.FAILED] + if include_failed or r.status != r.FAILED] - return sorted(resources, key=lambda r: (r.created_time, r.name)) + return sorted(resources, + key=lambda r: (r.status != r.FAILED, r.created_time, r.name)) def get_member_refids(group, exclude=None): @@ -100,10 +103,12 @@ def get_nested_attrs(stack, key, use_indices, *path): return get_rsrc_id(stack, key, use_indices, *path) -def get_member_definitions(group): +def get_member_definitions(group, include_failed=False): """Get member definitions in (name, ResourceDefinition) pair for group. The List is sorted first by created_time then by name. + If include_failed is set, failed members will be put first in the + List sorted by created_time then by name. """ return [(resource.name, resource.t) - for resource in get_members(group)] + for resource in get_members(group, include_failed)] diff --git a/heat/engine/resources/openstack/heat/instance_group.py b/heat/engine/resources/openstack/heat/instance_group.py index 470754adee..60be576d63 100644 --- a/heat/engine/resources/openstack/heat/instance_group.py +++ b/heat/engine/resources/openstack/heat/instance_group.py @@ -249,7 +249,8 @@ class InstanceGroup(stack_resource.StackResource): Also see heat.scaling.template.member_definitions. """ instance_definition = self._get_resource_definition() - old_resources = grouputils.get_member_definitions(self) + old_resources = grouputils.get_member_definitions(self, + include_failed=True) definitions = template.member_definitions( old_resources, instance_definition, num_instances, num_replace, short_id.generate_id) diff --git a/heat/tests/openstack/heat/test_instance_group.py b/heat/tests/openstack/heat/test_instance_group.py index c786af2cef..fed6717c75 100644 --- a/heat/tests/openstack/heat/test_instance_group.py +++ b/heat/tests/openstack/heat/test_instance_group.py @@ -347,15 +347,9 @@ class LoadbalancerReloadTest(common.HeatTestCase): {'Instances': ['aaaabbbbcccc']}) -class ReplaceTest(common.HeatTestCase): - scenarios = [ - ('1', dict(min_in_service=0, batch_size=1, updates=2)), - ('2', dict(min_in_service=0, batch_size=2, updates=1)), - ('3', dict(min_in_service=3, batch_size=1, updates=3)), - ('4', dict(min_in_service=3, batch_size=2, updates=2))] - +class InstanceGroupWithNestedStack(common.HeatTestCase): def setUp(self): - super(ReplaceTest, self).setUp() + super(InstanceGroupWithNestedStack, self).setUp() t = template_format.parse(inline_templates.as_template) self.stack = utils.parse_stack(t, params=inline_templates.as_params) lc = self.create_launch_config(t, self.stack) @@ -369,7 +363,6 @@ class ReplaceTest(common.HeatTestCase): self.group._lb_reload = mock.Mock() self.group.update_with_template = mock.Mock() self.group.check_update_complete = mock.Mock() - self.group._nested = self.get_fake_nested_stack() def create_launch_config(self, t, stack): self.stub_ImageConstraint_validate() @@ -381,22 +374,34 @@ class ReplaceTest(common.HeatTestCase): self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) return rsrc - def get_fake_nested_stack(self): - nested_t = ''' + def get_fake_nested_stack(self, size=1): + tmpl = ''' heat_template_version: 2013-05-23 description: AutoScaling Test resources: - one: - type: ResourceWithPropsAndAttrs - properties: - Foo: hello - two: - type: ResourceWithPropsAndAttrs - properties: - Foo: fee ''' + resource = ''' + r%(i)d: + type: ResourceWithPropsAndAttrs + properties: + Foo: bar%(i)d + ''' + resources = '\n'.join([resource % {'i': i + 1} for i in range(size)]) + nested_t = tmpl + resources return utils.parse_stack(template_format.parse(nested_t)) + +class ReplaceTest(InstanceGroupWithNestedStack): + scenarios = [ + ('1', dict(min_in_service=0, batch_size=1, updates=2)), + ('2', dict(min_in_service=0, batch_size=2, updates=1)), + ('3', dict(min_in_service=3, batch_size=1, updates=3)), + ('4', dict(min_in_service=3, batch_size=2, updates=2))] + + def setUp(self): + super(ReplaceTest, self).setUp() + self.group._nested = self.get_fake_nested_stack(2) + def test_rolling_updates(self): self.group._replace(self.min_in_service, self.batch_size, 0) self.assertEqual(self.updates, @@ -405,6 +410,35 @@ class ReplaceTest(common.HeatTestCase): len(self.group._lb_reload.call_args_list)) +class ResizeWithFailedInstancesTest(InstanceGroupWithNestedStack): + scenarios = [ + ('1', dict(size=3, failed=['r1'], content={'r2', 'r3', 'r4'})), + ('2', dict(size=3, failed=['r4'], content={'r1', 'r2', 'r3'})), + ('3', dict(size=2, failed=['r1', 'r2'], content={'r3', 'r4'})), + ('4', dict(size=2, failed=['r3', 'r4'], content={'r1', 'r2'})), + ('5', dict(size=2, failed=['r2', 'r3'], content={'r1', 'r4'})), + ('6', dict(size=3, failed=['r2', 'r3'], content={'r1', 'r3', 'r4'}))] + + def setUp(self): + super(ResizeWithFailedInstancesTest, self).setUp() + self.group._nested = self.get_fake_nested_stack(4) + self.nested = self.group.nested() + self.group.nested = mock.Mock(return_value=self.nested) + + def set_failed_instance(self, instance): + for r in six.itervalues(self.group.nested()): + if r.name == instance: + r.status = "FAILED" + + def test_resize(self): + for inst in self.failed: + self.set_failed_instance(inst) + self.group.resize(self.size) + tmpl = self.group.update_with_template.call_args[0][0] + resources = tmpl.resource_definitions(self.group.nested()) + self.assertEqual(set(resources.keys()), self.content) + + class TestGetBatches(common.HeatTestCase): scenarios = [