From bc1de6ae780e10b4de2337646c4fda40380b2335 Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Wed, 6 Mar 2013 16:28:00 +1100 Subject: [PATCH] make parsed template snapshots before updating This is to make sure that as the update progresses and dependant resources are modified/replaced that we compare the original dynamic data with the current. bug 1134258 Change-Id: Ia4e474e914aa9e99e5e569c4f2276beb55798421 --- heat/engine/parser.py | 6 +- heat/engine/resource.py | 25 +++- heat/tests/test_parser.py | 232 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 6 deletions(-) diff --git a/heat/engine/parser.py b/heat/engine/parser.py index fa1c6e77..72c0bf94 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -317,6 +317,10 @@ class Stack(object): else: self.state_set(self.ROLLBACK_IN_PROGRESS, 'Stack rollback started') + # cache all the resources runtime data. + for r in self: + r.cache_template() + # Now make the resources match the new stack definition with eventlet.Timeout(self.timeout_mins * 60) as tmo: try: @@ -371,7 +375,7 @@ class Stack(object): # Compare resolved pre/post update resource snippets, # note the new resource snippet is resolved in the context # of the existing stack (which is the stack being updated) - old_snippet = self.resolve_runtime_data(self[res.name].t) + old_snippet = self[res.name].parsed_template(cached=True) new_snippet = self.resolve_runtime_data(res.t) if old_snippet != new_snippet: diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 7458e517..ee9c93f0 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -130,6 +130,7 @@ class Resource(object): self.name = name self.json_snippet = json_snippet self.t = stack.resolve_static_data(json_snippet) + self.cached_t = None self.properties = Properties(self.properties_schema, self.t.get('Properties', {}), self.stack.resolve_runtime_data, @@ -172,18 +173,29 @@ class Resource(object): return identifier.ResourceIdentifier(resource_name=self.name, **self.stack.identifier()) - def parsed_template(self, section=None, default={}): + def parsed_template(self, section=None, default={}, cached=False): ''' Return the parsed template data for the resource. May be limited to only one section of the data, in which case a default value may also be supplied. ''' + if cached and self.cached_t: + t = self.cached_t + else: + t = self.t if section is None: - template = self.t + template = t else: - template = self.t.get(section, default) + template = t.get(section, default) return self.stack.resolve_runtime_data(template) + def cache_template(self): + ''' + make a cache of the resource's parsed template + this can then be used via parsed_template(cached=True) + ''' + self.cached_t = self.stack.resolve_runtime_data(self.t) + def update_template_diff(self, json_snippet=None): ''' Returns the difference between json_template and self.t @@ -194,7 +206,8 @@ class Resource(object): update_allowed_set = set(self.update_allowed_keys) # Create a set containing the keys in both current and update template - current_template = self.parsed_template() + current_template = self.parsed_template(cached=True) + template_keys = set(current_template.keys()) new_template = self.stack.resolve_runtime_data(json_snippet) template_keys.update(set(new_template.keys())) @@ -221,7 +234,9 @@ class Resource(object): update_allowed_set = set(self.update_allowed_properties) # Create a set containing the keys in both current and update template - current_properties = self.parsed_template().get('Properties', {}) + tmpl = self.parsed_template(cached=True) + current_properties = tmpl.get('Properties', {}) + template_properties = set(current_properties.keys()) updated_properties = json_snippet.get('Properties', {}) template_properties.update(set(updated_properties.keys())) diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 5191ae4a..0e21d1f2 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -792,3 +792,235 @@ class StackTest(unittest.TestCase): self.m.VerifyAll() # Unset here so destroy() is not stubbed for stack.delete cleanup self.m.UnsetStubs() + + @stack_delete_after + def test_update_replace_by_reference(self): + ''' + assertion: + changes in dynamic attributes, due to other resources been updated + are not ignored and can cause dependant resources to be updated. + ''' + # patch in a dummy property schema for GenericResource + dummy_schema = {'Foo': {'Type': 'String'}} + resource.GenericResource.properties_schema = dummy_schema + tmpl = {'Resources': { + 'AResource': {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'abc'}}, + 'BResource': {'Type': 'GenericResourceType', + 'Properties': { + 'Foo': {'Ref': 'AResource'}}}}} + tmpl2 = {'Resources': { + 'AResource': {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'smelly'}}, + 'BResource': {'Type': 'GenericResourceType', + 'Properties': { + 'Foo': {'Ref': 'AResource'}}}}} + + self.stack = parser.Stack(self.ctx, 'update_test_stack', + template.Template(tmpl)) + self.stack.store() + self.stack.create() + self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE) + self.assertEqual(self.stack['AResource'].properties['Foo'], 'abc') + self.assertEqual(self.stack['BResource'].properties['Foo'], + 'AResource') + + self.m.StubOutWithMock(resource.GenericResource, 'handle_update') + resource.GenericResource.handle_update( + tmpl2['Resources']['AResource']).AndReturn( + resource.Resource.UPDATE_REPLACE) + + br2_snip = {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'inst-007'}} + resource.GenericResource.handle_update( + br2_snip).AndReturn( + resource.Resource.UPDATE_REPLACE) + + self.m.StubOutWithMock(resource.GenericResource, 'FnGetRefId') + resource.GenericResource.FnGetRefId().AndReturn( + 'AResource') + resource.GenericResource.FnGetRefId().MultipleTimes().AndReturn( + 'inst-007') + self.m.ReplayAll() + + updated_stack = parser.Stack(self.ctx, 'updated_stack', + template.Template(tmpl2)) + self.stack.update(updated_stack) + self.assertEqual(self.stack.state, parser.Stack.UPDATE_COMPLETE) + self.assertEqual(self.stack['AResource'].properties['Foo'], 'smelly') + self.assertEqual(self.stack['BResource'].properties['Foo'], 'inst-007') + self.m.VerifyAll() + + @stack_delete_after + def test_update_by_reference_and_rollback_1(self): + ''' + assertion: + check that rollback still works with dynamic metadata + this test fails the first instance + ''' + # patch in a dummy property schema for GenericResource + dummy_schema = {'Foo': {'Type': 'String'}} + resource.GenericResource.properties_schema = dummy_schema + tmpl = {'Resources': { + 'AResource': {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'abc'}}, + 'BResource': {'Type': 'GenericResourceType', + 'Properties': { + 'Foo': {'Ref': 'AResource'}}}}} + tmpl2 = {'Resources': { + 'AResource': {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'smelly'}}, + 'BResource': {'Type': 'GenericResourceType', + 'Properties': { + 'Foo': {'Ref': 'AResource'}}}}} + + self.stack = parser.Stack(self.ctx, 'update_test_stack', + template.Template(tmpl), + disable_rollback=False) + self.stack.store() + self.stack.create() + self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE) + self.assertEqual(self.stack['AResource'].properties['Foo'], 'abc') + self.assertEqual(self.stack['BResource'].properties['Foo'], + 'AResource') + + self.m.StubOutWithMock(resource.GenericResource, 'handle_update') + self.m.StubOutWithMock(resource.GenericResource, 'FnGetRefId') + self.m.StubOutWithMock(resource.GenericResource, 'handle_create') + + # mocks for first (failed update) + resource.GenericResource.handle_update( + tmpl2['Resources']['AResource']).AndReturn( + resource.Resource.UPDATE_REPLACE) + resource.GenericResource.FnGetRefId().AndReturn( + 'AResource') + + # mock to make the replace fail when creating the replacement resource + resource.GenericResource.handle_create().AndRaise(Exception) + + # mocks for second rollback update + resource.GenericResource.handle_update( + tmpl['Resources']['AResource']).AndReturn( + resource.Resource.UPDATE_REPLACE) + + resource.GenericResource.handle_create().AndReturn(None) + resource.GenericResource.FnGetRefId().MultipleTimes().AndReturn( + 'AResource') + + self.m.ReplayAll() + + updated_stack = parser.Stack(self.ctx, 'updated_stack', + template.Template(tmpl2), + disable_rollback=False) + self.stack.update(updated_stack) + self.assertEqual(self.stack.state, parser.Stack.ROLLBACK_COMPLETE) + self.assertEqual(self.stack['AResource'].properties['Foo'], 'abc') + + self.m.VerifyAll() + + @stack_delete_after + def test_update_by_reference_and_rollback_2(self): + ''' + assertion: + check that rollback still works with dynamic metadata + this test fails the second instance + ''' + # patch in a dummy property schema for GenericResource + dummy_schema = {'Foo': {'Type': 'String'}} + resource.GenericResource.properties_schema = dummy_schema + tmpl = {'Resources': { + 'AResource': {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'abc'}}, + 'BResource': {'Type': 'GenericResourceType', + 'Properties': { + 'Foo': {'Ref': 'AResource'}}}}} + tmpl2 = {'Resources': { + 'AResource': {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'smelly'}}, + 'BResource': {'Type': 'GenericResourceType', + 'Properties': { + 'Foo': {'Ref': 'AResource'}}}}} + + self.stack = parser.Stack(self.ctx, 'update_test_stack', + template.Template(tmpl), + disable_rollback=False) + self.stack.store() + self.stack.create() + self.assertEqual(self.stack.state, parser.Stack.CREATE_COMPLETE) + self.assertEqual(self.stack['AResource'].properties['Foo'], 'abc') + self.assertEqual(self.stack['BResource'].properties['Foo'], + 'AResource') + + self.m.StubOutWithMock(resource.GenericResource, 'handle_update') + self.m.StubOutWithMock(resource.GenericResource, 'FnGetRefId') + self.m.StubOutWithMock(resource.GenericResource, 'handle_create') + + # mocks for first and second (failed update) + resource.GenericResource.handle_update( + tmpl2['Resources']['AResource']).AndReturn( + resource.Resource.UPDATE_REPLACE) + br2_snip = {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'inst-007'}} + resource.GenericResource.handle_update( + br2_snip).AndReturn( + resource.Resource.UPDATE_REPLACE) + + resource.GenericResource.FnGetRefId().AndReturn( + 'AResource') + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + # self.state_set(self.UPDATE_IN_PROGRESS) + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + # self.state_set(self.DELETE_IN_PROGRESS) + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + # self.state_set(self.DELETE_COMPLETE) + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + # self.properties.validate() + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + # self.state_set(self.CREATE_IN_PROGRESS) + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + + # mock to make the replace fail when creating the second + # replacement resource + resource.GenericResource.handle_create().AndReturn(None) + resource.GenericResource.handle_create().AndRaise(Exception) + + # mocks for second rollback update + resource.GenericResource.handle_update( + tmpl['Resources']['AResource']).AndReturn( + resource.Resource.UPDATE_REPLACE) + br2_snip = {'Type': 'GenericResourceType', + 'Properties': {'Foo': 'AResource'}} + resource.GenericResource.handle_update( + br2_snip).AndReturn( + resource.Resource.UPDATE_REPLACE) + + # self.state_set(self.DELETE_IN_PROGRESS) + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + # self.state_set(self.DELETE_IN_PROGRESS) + resource.GenericResource.FnGetRefId().AndReturn( + 'inst-007') + + resource.GenericResource.handle_create().AndReturn(None) + resource.GenericResource.handle_create().AndReturn(None) + + # reverting to AResource + resource.GenericResource.FnGetRefId().MultipleTimes().AndReturn( + 'AResource') + + self.m.ReplayAll() + + updated_stack = parser.Stack(self.ctx, 'updated_stack', + template.Template(tmpl2), + disable_rollback=False) + self.stack.update(updated_stack) + self.assertEqual(self.stack.state, parser.Stack.ROLLBACK_COMPLETE) + self.assertEqual(self.stack['AResource'].properties['Foo'], 'abc') + + self.m.VerifyAll() -- 2.45.2