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
This commit is contained in:
Angus Salkeld 2013-03-06 16:28:00 +11:00
parent b8b5624d1b
commit bc1de6ae78
3 changed files with 258 additions and 7 deletions

View File

@ -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:

View File

@ -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 section is None:
template = self.t
if cached and self.cached_t:
t = self.cached_t
else:
template = self.t.get(section, default)
t = self.t
if section is None:
template = t
else:
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()))

View File

@ -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()