Merge "resource failure causes nested stacks to be rolled back"

This commit is contained in:
Jenkins 2015-08-21 01:37:26 +00:00 committed by Gerrit Code Review
commit 5135ad7307
5 changed files with 83 additions and 15 deletions

View File

@ -377,7 +377,7 @@ class StackController(object):
stack_identity = self._get_identity(con, stack_name)
try:
self.rpc_client.stack_cancel_update(
con, stack_identity=stack_identity)
con, stack_identity=stack_identity, cancel_with_rollback=True)
except Exception as ex:
return exception.map_remote_error(ex)

View File

@ -111,8 +111,10 @@ class StackResource(resource.Resource):
self.context.tenant_id,
self.physical_resource_name(),
self.resource_id)
self.rpc_client().stack_cancel_update(self.context,
stack_identity)
self.rpc_client().stack_cancel_update(
self.context,
stack_identity,
cancel_with_rollback=False)
def has_nested(self):
if self.nested() is not None:

View File

@ -61,6 +61,9 @@ LOG = logging.getLogger(__name__)
class ForcedCancel(BaseException):
"""Exception raised to cancel task execution."""
def __init__(self, with_rollback=True):
self.with_rollback = with_rollback
def __str__(self):
return "Operation cancelled"
@ -1197,20 +1200,27 @@ class Stack(collections.Mapping):
(self.status == self.FAILED))
def _update_exception_handler(self, exc, action, update_task):
require_rollback = False
'''
Handle exceptions in update_task. Decide if we should cancel tasks or
not. Also decide if we should rollback or not, depend on disable
rollback flag if force rollback flag not trigered.
:returns: a boolean for require rollback flag
'''
self.status_reason = six.text_type(exc)
self.status = self.FAILED
if action == self.UPDATE:
if not self.disable_rollback:
require_rollback = True
if isinstance(exc, ForcedCancel):
update_task.updater.cancel_all()
require_rollback = True
return require_rollback
if action != self.UPDATE:
return False
if isinstance(exc, ForcedCancel):
update_task.updater.cancel_all()
return exc.with_rollback or not self.disable_rollback
return not self.disable_rollback
def _message_parser(self, message):
if message == rpc_api.THREAD_CANCEL:
raise ForcedCancel()
raise ForcedCancel(with_rollback=False)
elif message == rpc_api.THREAD_CANCEL_WITH_ROLLBACK:
raise ForcedCancel(with_rollback=True)
def _delete_backup_stack(self, stack):
# Delete resources in the backup stack referred to by 'stack'

View File

@ -2260,7 +2260,10 @@ class StackTest(common.HeatTestCase):
self.assertTrue(res)
elif isinstance(exc, stack.ForcedCancel):
update_task.updater.cancel_all.assert_called_once_with()
self.assertTrue(res)
if exc.with_rollback or not disable_rollback:
self.assertTrue(res)
else:
self.assertFalse(res)
self.m.VerifyAll()
def test_update_exception_handler_resource_failure_no_rollback(self):
@ -2273,10 +2276,14 @@ class StackTest(common.HeatTestCase):
exc = exception.ResourceFailure(reason, None, action='UPDATE')
self.update_exception_handler(exc, disable_rollback=False)
def test_update_exception_handler_force_cancel(self):
exc = stack.ForcedCancel()
def test_update_exception_handler_force_cancel_with_rollback(self):
exc = stack.ForcedCancel(with_rollback=True)
self.update_exception_handler(exc, disable_rollback=False)
def test_update_exception_handler_force_cancel_no_rollback(self):
exc = stack.ForcedCancel(with_rollback=False)
self.update_exception_handler(exc, disable_rollback=True)
class StackKwargsForCloningTest(common.HeatTestCase):
scenarios = [

View File

@ -21,6 +21,7 @@ from heat.engine import resource
from heat.engine import scheduler
from heat.engine import stack
from heat.engine import template
from heat.rpc import api as rpc_api
from heat.tests import common
from heat.tests import generic_resource as generic_rsrc
from heat.tests import utils
@ -671,6 +672,54 @@ class StackUpdateTest(common.HeatTestCase):
[mock.call(None), mock.call('c_res'), mock.call('b_res'),
mock.call('a_res')])
def _update_force_cancel(self, state, disable_rollback=False,
cancel_message=rpc_api.THREAD_CANCEL):
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
self.stack = stack.Stack(self.ctx, 'update_test_stack',
template.Template(tmpl))
self.stack.store()
self.stack.create()
self.assertEqual((stack.Stack.CREATE, stack.Stack.COMPLETE),
self.stack.state)
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {
'AResource': {'Type': 'GenericResourceType'},
'BResource': {'Type': 'GenericResourceType'}}}
updated_stack = stack.Stack(self.ctx, 'updated_stack',
template.Template(tmpl2),
disable_rollback=disable_rollback)
evt_mock = mock.MagicMock()
evt_mock.ready.return_value = True
evt_mock.wait.return_value = cancel_message
self.stack.update(updated_stack, event=evt_mock)
self.assertEqual(state, self.stack.state)
evt_mock.ready.assert_called_once_with()
evt_mock.wait.assert_called_once_with()
def test_update_force_cancel_no_rollback(self):
self._update_force_cancel(
state=(stack.Stack.UPDATE, stack.Stack.FAILED),
disable_rollback=True,
cancel_message=rpc_api.THREAD_CANCEL)
def test_update_force_cancel_rollback(self):
self._update_force_cancel(
state=(stack.Stack.ROLLBACK, stack.Stack.COMPLETE),
disable_rollback=False,
cancel_message=rpc_api.THREAD_CANCEL)
def test_update_force_cancel_force_rollback(self):
self._update_force_cancel(
state=(stack.Stack.ROLLBACK, stack.Stack.COMPLETE),
disable_rollback=False,
cancel_message=rpc_api.THREAD_CANCEL_WITH_ROLLBACK)
def test_update_add_signal(self):
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}