diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 8603603299..eecfd89930 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -662,7 +662,40 @@ class Stack(collections.Mapping): action) backup_stack = self._backup_stack(False) - if backup_stack is not None: + if backup_stack: + for key, backup_resource in backup_stack.resources.items(): + # If UpdateReplace is failed, we must restore backup_resource + # to existing_stack in case of it may have dependencies in + # these stacks. current_resource is the resource that just + # created and failed, so put into the backup_stack to delete + # anyway. + backup_resource_id = backup_resource.resource_id + current_resource = self.resources[key] + current_resource_id = current_resource.resource_id + if backup_resource_id: + child_failed = False + for child in self.dependencies[current_resource]: + # If child resource failed to update, current_resource + # should be replaced to resolve dependencies. But this + # is not fundamental solution. If there are update + # failer and success resources in the children, cannot + # delete the stack. + if (child.status == child.FAILED and child.action == + child.CREATE): + child_failed = True + if (current_resource.status == current_resource.FAILED or + child_failed): + # Stack class owns dependencies as set of resource's + # objects, so we switch members of the resource that is + # needed to delete it. + self.resources[key].resource_id = backup_resource_id + self.resources[ + key].properties = backup_resource.properties + backup_stack.resources[ + key].resource_id = current_resource_id + backup_stack.resources[ + key].properties = current_resource.properties + backup_stack.delete(backup=True) if backup_stack.status != backup_stack.COMPLETE: errs = backup_stack.status_reason diff --git a/heat/tests/generic_resource.py b/heat/tests/generic_resource.py index a7de46edfa..ef51b5a3d1 100644 --- a/heat/tests/generic_resource.py +++ b/heat/tests/generic_resource.py @@ -73,6 +73,20 @@ class ResourceWithProps(GenericResource): properties_schema = {'Foo': {'Type': 'String'}} +class ResourceWithResourceID(GenericResource): + properties_schema = {'ID': {'Type': 'String'}} + + def handle_create(self): + super(ResourceWithResourceID, self).handle_create() + self.resource_id_set(self.properties.get('ID')) + + def handle_delete(self): + self.mox_resource_id(self.resource_id) + + def mox_resource_id(self, resource_id): + pass + + class ResourceWithComplexAttributes(GenericResource): attributes_schema = {'list': 'A list', 'flat_dict': 'A flat dictionary', diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index f5aa9b5d7a..62754c45ff 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -1975,6 +1975,138 @@ class StackTest(HeatTestCase): self.stack.state) self.m.VerifyAll() + def test_update_modify_replace_failed_create_and_delete_1(self): + resource._register_class('ResourceWithResourceIDType', + generic_rsrc.ResourceWithResourceID) + tmpl = {'HeatTemplateFormatVersion': '2012-12-12', + 'Resources': {'AResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'a_res'}}, + 'BResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'b_res'}, + 'DependsOn': 'AResource'}}} + + self.stack = parser.Stack(self.ctx, 'update_test_stack', + template.Template(tmpl), + disable_rollback=True) + self.stack.store() + self.stack.create() + self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE), + self.stack.state) + + tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12', + 'Resources': {'AResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'xyz'}}, + 'BResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'b_res'}, + 'DependsOn': 'AResource'}}} + + updated_stack = parser.Stack(self.ctx, 'updated_stack', + template.Template(tmpl2)) + + # Calls to GenericResource.handle_update will raise + # resource.UpdateReplace because we've not specified the modified + # key/property in update_allowed_keys/update_allowed_properties + + # patch in a dummy handle_create making the replace fail creating + self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID, + 'handle_create') + generic_rsrc.ResourceWithResourceID.handle_create().AndRaise(Exception) + + self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID, + 'mox_resource_id') + # First, attempts to delete backup_stack. The create (xyz) has been + # failed, so it has no resource_id. + generic_rsrc.ResourceWithResourceID.mox_resource_id( + None).AndReturn(None) + # There are dependency AResource and BResource, so we must delete + # BResource, then delete AResource. + generic_rsrc.ResourceWithResourceID.mox_resource_id( + 'b_res').AndReturn(None) + generic_rsrc.ResourceWithResourceID.mox_resource_id( + 'a_res').AndReturn(None) + self.m.ReplayAll() + + self.stack.update(updated_stack) + self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED), + self.stack.state) + self.stack.delete() + self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE), + self.stack.state) + self.m.VerifyAll() + + def test_update_modify_replace_failed_create_and_delete_2(self): + resource._register_class('ResourceWithResourceIDType', + generic_rsrc.ResourceWithResourceID) + tmpl = {'HeatTemplateFormatVersion': '2012-12-12', + 'Resources': {'AResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'a_res'}}, + 'BResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'b_res'}, + 'DependsOn': 'AResource'}}} + + self.stack = parser.Stack(self.ctx, 'update_test_stack', + template.Template(tmpl), + disable_rollback=True) + self.stack.store() + self.stack.create() + self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE), + self.stack.state) + + tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12', + 'Resources': {'AResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'c_res'}}, + 'BResource': {'Type': + 'ResourceWithResourceIDType', + 'Properties': {'ID': 'xyz'}, + 'DependsOn': 'AResource'}}} + + updated_stack = parser.Stack(self.ctx, 'updated_stack', + template.Template(tmpl2)) + + # Calls to GenericResource.handle_update will raise + # resource.UpdateReplace because we've not specified the modified + # key/property in update_allowed_keys/update_allowed_properties + + # patch in a dummy handle_create making the replace fail creating + self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID, + 'handle_create') + generic_rsrc.ResourceWithResourceID.handle_create() + generic_rsrc.ResourceWithResourceID.handle_create().AndRaise(Exception) + + self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID, + 'mox_resource_id') + # First, attempts to delete backup_stack. The create (xyz) has been + # failed, so it has no resource_id. + generic_rsrc.ResourceWithResourceID.mox_resource_id( + None).AndReturn(None) + generic_rsrc.ResourceWithResourceID.mox_resource_id( + 'c_res').AndReturn(None) + # There are dependency AResource and BResource, so we must delete + # BResource, then delete AResource. + generic_rsrc.ResourceWithResourceID.mox_resource_id( + 'b_res').AndReturn(None) + generic_rsrc.ResourceWithResourceID.mox_resource_id( + 'a_res').AndReturn(None) + self.m.ReplayAll() + + self.stack.update(updated_stack) + # set resource_id for AResource because handle_create() is overwritten + # by the mox. + self.stack.resources['AResource'].resource_id_set('c_res') + self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED), + self.stack.state) + self.stack.delete() + self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE), + self.stack.state) + self.m.VerifyAll() + def test_update_add_failed_create(self): tmpl = {'HeatTemplateFormatVersion': '2012-12-12', 'Resources': {'AResource': {'Type': 'GenericResourceType'}}}