Save previous template on non convergence updates

We already store the previous template for convergence based
updates, but for non-convergence updates we can reuse the same
column to enable recovery from a failed update when using
PATCH updates, e.g where the error is in the environment or
nested templates and thus the top-level template may not be
provided & it's valid to use the previously stored one.

Change-Id: I79248bedb01260a0c4b1edf183db55fc0bffbdf6
This commit is contained in:
Steven Hardy 2015-09-07 16:09:58 +01:00 committed by Steve Baker
parent fc77e43297
commit 53c4707bf6
4 changed files with 64 additions and 9 deletions

View File

@ -893,6 +893,7 @@ class Stack(collections.Mapping):
elif create_if_missing: elif create_if_missing:
kwargs = self.get_kwargs_for_cloning() kwargs = self.get_kwargs_for_cloning()
kwargs['owner_id'] = self.id kwargs['owner_id'] = self.id
del(kwargs['prev_raw_template_id'])
prev = type(self)(self.context, self.name, copy.deepcopy(self.t), prev = type(self)(self.context, self.name, copy.deepcopy(self.t),
**kwargs) **kwargs)
prev.store(backup=True) prev.store(backup=True)
@ -1126,8 +1127,19 @@ class Stack(collections.Mapping):
'State invalid for %s' % action) 'State invalid for %s' % action)
return return
self.state_set(action, self.IN_PROGRESS, # Save a copy of the new template. To avoid two DB writes
'Stack %s started' % action) # we store the ID at the same time as the action/status
prev_tmpl_id = self.prev_raw_template_id
bu_tmpl = copy.deepcopy(newstack.t)
self.prev_raw_template_id = bu_tmpl.store()
self.action = action
self.status = self.IN_PROGRESS
self.status_reason = 'Stack %s started' % action
notification.send(self)
self._add_event(self.action, self.status, self.status_reason)
self.store()
if prev_tmpl_id is not None:
raw_template_object.RawTemplate.delete(self.context, prev_tmpl_id)
if action == self.UPDATE: if action == self.UPDATE:
# Oldstack is useless when the action is not UPDATE , so we don't # Oldstack is useless when the action is not UPDATE , so we don't

View File

@ -114,6 +114,7 @@ class ServiceStackUpdateTest(common.HeatTestCase):
self.assertEqual({'KeyName': 'test'}, stk.t.env.params) self.assertEqual({'KeyName': 'test'}, stk.t.env.params)
with mock.patch('heat.engine.stack.Stack') as mock_stack: with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
mock_stack.load.return_value = stk mock_stack.load.return_value = stk
mock_stack.validate.return_value = None mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(), result = self.man.update_stack(self.ctx, stk.identifier(),
@ -148,6 +149,7 @@ class ServiceStackUpdateTest(common.HeatTestCase):
stk.t.env.params) stk.t.env.params)
with mock.patch('heat.engine.stack.Stack') as mock_stack: with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
mock_stack.load.return_value = stk mock_stack.load.return_value = stk
mock_stack.validate.return_value = None mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(), result = self.man.update_stack(self.ctx, stk.identifier(),
@ -209,6 +211,7 @@ class ServiceStackUpdateTest(common.HeatTestCase):
'newfoo2.yaml': 'newfoo', 'newfoo2.yaml': 'newfoo',
'myother.yaml': 'myother'} 'myother.yaml': 'myother'}
with mock.patch('heat.engine.stack.Stack') as mock_stack: with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
mock_stack.load.return_value = stk mock_stack.load.return_value = stk
mock_stack.validate.return_value = None mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(), result = self.man.update_stack(self.ctx, stk.identifier(),
@ -250,6 +253,7 @@ class ServiceStackUpdateTest(common.HeatTestCase):
'parameters': {}, 'parameters': {},
'resource_registry': {'resources': {}}} 'resource_registry': {'resources': {}}}
with mock.patch('heat.engine.stack.Stack') as mock_stack: with mock.patch('heat.engine.stack.Stack') as mock_stack:
stk.update = mock.Mock()
mock_stack.load.return_value = stk mock_stack.load.return_value = stk
mock_stack.validate.return_value = None mock_stack.validate.return_value = None
result = self.man.update_stack(self.ctx, stk.identifier(), result = self.man.update_stack(self.ctx, stk.identifier(),

View File

@ -523,6 +523,39 @@ class StackTest(common.HeatTestCase):
self.stack.update(newstack) self.stack.update(newstack)
self.assertIsNotNone(self.stack.updated_time) self.assertIsNotNone(self.stack.updated_time)
def test_update_prev_raw_template(self):
self.stack = stack.Stack(self.ctx, 'updated_time_test',
self.tmpl)
self.assertIsNone(self.stack.updated_time)
self.stack.store()
self.stack.create()
self.assertIsNone(self.stack.prev_raw_template_id)
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {'R1': {'Type': 'GenericResourceType'}}}
newstack = stack.Stack(self.ctx, 'updated_time_test',
template.Template(tmpl))
self.stack.update(newstack)
self.assertIsNotNone(self.stack.prev_raw_template_id)
prev_t = template.Template.load(self.ctx,
self.stack.prev_raw_template_id)
self.assertEqual(tmpl, prev_t.t)
prev_id = self.stack.prev_raw_template_id
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {'R2': {'Type': 'GenericResourceType'}}}
newstack2 = stack.Stack(self.ctx, 'updated_time_test',
template.Template(tmpl2))
self.stack.update(newstack2)
self.assertIsNotNone(self.stack.prev_raw_template_id)
self.assertNotEqual(prev_id, self.stack.prev_raw_template_id)
prev_t2 = template.Template.load(self.ctx,
self.stack.prev_raw_template_id)
self.assertEqual(tmpl2, prev_t2.t)
self.assertRaises(exception.NotFound,
template.Template.load, self.ctx, prev_id)
def test_access_policy_update(self): def test_access_policy_update(self):
tmpl = {'HeatTemplateFormatVersion': '2012-12-12', tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': { 'Resources': {

View File

@ -23,11 +23,13 @@ from heat.engine import scheduler
from heat.engine import service from heat.engine import service
from heat.engine import stack from heat.engine import stack
from heat.engine import template from heat.engine import template
from heat.objects import stack as stack_object
from heat.rpc import api as rpc_api from heat.rpc import api as rpc_api
from heat.tests import common from heat.tests import common
from heat.tests import generic_resource as generic_rsrc from heat.tests import generic_resource as generic_rsrc
from heat.tests import utils from heat.tests import utils
empty_template = template_format.parse('''{ empty_template = template_format.parse('''{
"HeatTemplateFormatVersion" : "2012-12-12", "HeatTemplateFormatVersion" : "2012-12-12",
}''') }''')
@ -854,17 +856,21 @@ class StackUpdateTest(common.HeatTestCase):
mock_create = self.patchobject(generic_rsrc.ResourceWithProps, mock_create = self.patchobject(generic_rsrc.ResourceWithProps,
'handle_create', side_effect=Exception) 'handle_create', side_effect=Exception)
with mock.patch.object(self.stack, 'state_set', with mock.patch.object(stack_object.Stack,
side_effect=self.stack.state_set) as mock_state: 'update_by_id') as mock_db_update:
self.stack.update(updated_stack) self.stack.update(updated_stack)
self.assertEqual((stack.Stack.ROLLBACK, stack.Stack.COMPLETE), self.assertEqual((stack.Stack.ROLLBACK, stack.Stack.COMPLETE),
self.stack.state) self.stack.state)
self.assertEqual('abc', self.stack['AResource'].properties['Foo']) self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
self.assertEqual(2, mock_state.call_count) self.assertEqual(5, mock_db_update.call_count)
self.assertEqual(('UPDATE', 'IN_PROGRESS'), self.assertEqual('UPDATE',
mock_state.call_args_list[0][0][:2]) mock_db_update.call_args_list[0][0][2]['action'])
self.assertEqual(('ROLLBACK', 'IN_PROGRESS'), self.assertEqual('IN_PROGRESS',
mock_state.call_args_list[1][0][:2]) mock_db_update.call_args_list[0][0][2]['status'])
self.assertEqual('ROLLBACK',
mock_db_update.call_args_list[1][0][2]['action'])
self.assertEqual('IN_PROGRESS',
mock_db_update.call_args_list[1][0][2]['status'])
mock_create.assert_called_once_with() mock_create.assert_called_once_with()