Add force to action_update

This patch adds the force query parameter to action_update.
When force is set to true on an action cancel request the
action status will be updated in the database to cancelled
rather than signaled to cancel. This will also update any
child actions to update their status in the database to
cancelled.

Change-Id: I45ab4a6ad57e3bfe791da1d2e70d3b2ed3ad9a21
This commit is contained in:
Jude Cross 2018-12-11 16:30:49 -08:00
parent 94c448276f
commit 44b5ae89f6
11 changed files with 154 additions and 9 deletions

View File

@ -163,6 +163,7 @@ Request Parameters
- action_id: action_id_url - action_id: action_id_url
- action: action - action: action
- status: action_status_update - status: action_status_update
- force: action_update_force_query
Request Example Request Example
--------------- ---------------

View File

@ -131,6 +131,12 @@ action_status_query:
description: | description: |
Filters the results by the ``status`` property of an action object. Filters the results by the ``status`` property of an action object.
action_update_force_query:
type: boolean
in: query
description: |
A boolean indicating if the action update request should be forced.
cluster_identity_query: cluster_identity_query:
type: string type: string
in: query in: query
@ -358,11 +364,11 @@ action_status:
A string representation of the current status of the action. A string representation of the current status of the action.
action_status_update: action_status_update:
type: object type: string
in: body in: body
required: True required: True
description: | description: |
A string representation of the action status to update CANCELLED is A string representation of the action status to update. CANCELLED is
the only valid status at this time. the only valid status at this time.
action_target: action_target:

View File

@ -107,6 +107,15 @@ class ActionController(wsgi.Controller):
if data is None: if data is None:
raise exc.HTTPBadRequest(_("Malformed request data, missing " raise exc.HTTPBadRequest(_("Malformed request data, missing "
"'action' key in request body.")) "'action' key in request body."))
force_update = req.params.get('force')
if force_update is not None:
force = util.parse_bool_param(consts.ACTION_UPDATE_FORCE,
force_update)
else:
force = False
data['force'] = force
data['identity'] = action_id data['identity'] = action_id
obj = util.parse_request('ActionUpdateRequest', req, data) obj = util.parse_request('ActionUpdateRequest', req, data)

View File

@ -268,6 +268,12 @@ ACTION_STATUSES = (
'SUSPENDED', 'SUSPENDED',
) )
ACTION_PARAMS = (
ACTION_UPDATE_FORCE,
) = (
'force',
)
EVENT_LEVELS = { EVENT_LEVELS = {
'CRITICAL': logging.CRITICAL, 'CRITICAL': logging.CRITICAL,
'ERROR': logging.ERROR, 'ERROR': logging.ERROR,

View File

@ -368,6 +368,38 @@ class Action(object):
action.set_status(action.RES_CANCEL, action.set_status(action.RES_CANCEL,
'Action execution cancelled') 'Action execution cancelled')
def force_cancel(self):
"""Force the action and any depended actions to cancel.
If the action or any depended actions are in status 'INIT', 'WAITING',
'READY', 'RUNNING', or 'WAITING_LIFECYCLE_COMPLETION' immediately
update their status to cancelled. This should only be used if an action
is stuck/dead and has no expectation of ever completing.
:raises: `ActionImmutable` if the action is in an unchangeable state
"""
expected = (self.INIT, self.WAITING, self.READY, self.RUNNING,
self.WAITING_LIFECYCLE_COMPLETION)
if self.status not in expected:
raise exception.ActionImmutable(id=self.id[:8], expected=expected,
actual=self.status)
LOG.debug('Forcing action %s to cancel.', self.id)
self.set_status(self.RES_CANCEL, 'Action execution force cancelled')
depended = dobj.Dependency.get_depended(self.context, self.id)
if not depended:
return
for child in depended:
# Force cancel all dependant actions
action = self.load(self.context, action_id=child)
if action.status in (action.INIT, action.WAITING, action.READY,
action.RUNNING,
action.WAITING_LIFECYCLE_COMPLETION):
LOG.debug('Forcing action %s to cancel.', action.id)
action.set_status(action.RES_CANCEL,
'Action execution force cancelled')
def execute(self, **kwargs): def execute(self, **kwargs):
"""Execute the action. """Execute the action.

View File

@ -2315,6 +2315,9 @@ class EngineService(service.Service):
if req.status == consts.ACTION_CANCELLED: if req.status == consts.ACTION_CANCELLED:
action = action_mod.Action.load(ctx, req.identity, action = action_mod.Action.load(ctx, req.identity,
project_safe=False) project_safe=False)
if req.force:
action.force_cancel()
else:
LOG.info("Signaling action '%s' to Cancel.", req.identity) LOG.info("Signaling action '%s' to Cancel.", req.identity)
action.signal_cancel() action.signal_cancel()
else: else:

View File

@ -75,5 +75,6 @@ class ActionUpdateRequest(base.SenlinObject):
fields = { fields = {
'identity': fields.StringField(), 'identity': fields.StringField(),
'status': fields.StringField() 'status': fields.StringField(),
'force': fields.BooleanField(default=False)
} }

View File

@ -336,7 +336,41 @@ class ActionControllerTest(shared.ControllerTest, base.SenlinTestCase):
'ActionUpdateRequest', req, 'ActionUpdateRequest', req,
{ {
'identity': aid, 'identity': aid,
'status': 'CANCELLED',
'force': False
})
mock_call.assert_called_once_with(req.context, 'action_update', obj)
@mock.patch.object(util, 'parse_bool_param')
@mock.patch.object(util, 'parse_request')
@mock.patch.object(rpc_client.EngineClient, 'call')
def test_action_update_force_cancel(self, mock_call, mock_parse,
mock_parse_bool, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'update', True)
aid = 'xxxx-yyyy-zzzz'
body = {
'action': {
'status': 'CANCELLED' 'status': 'CANCELLED'
}
}
params = {'force': 'True'}
req = self._patch(
'/actions/%(action_id)s' % {'action_id': aid},
jsonutils.dumps(body), version='1.12', params=params)
obj = mock.Mock()
mock_parse.return_value = obj
mock_parse_bool.return_value = True
self.assertRaises(exc.HTTPAccepted,
self.controller.update, req,
action_id=aid, body=body)
mock_parse.assert_called_once_with(
'ActionUpdateRequest', req,
{
'identity': aid,
'status': 'CANCELLED',
'force': True
}) })
mock_call.assert_called_once_with(req.context, 'action_update', obj) mock_call.assert_called_once_with(req.context, 'action_update', obj)

View File

@ -85,10 +85,14 @@ class ControllerTest(object):
return self._simple_request(path, params=params, method='DELETE') return self._simple_request(path, params=params, method='DELETE')
def _data_request(self, path, data, content_type='application/json', def _data_request(self, path, data, content_type='application/json',
method='POST', version=None): method='POST', version=None, params=None):
environ = self._environ(path) environ = self._environ(path)
environ['REQUEST_METHOD'] = method environ['REQUEST_METHOD'] = method
if params:
qs = "&".join(["=".join([k, str(params[k])]) for k in params])
environ['QUERY_STRING'] = qs
req = wsgi.Request(environ) req = wsgi.Request(environ)
req.context = utils.dummy_context('api_test_user', self.project) req.context = utils.dummy_context('api_test_user', self.project)
self.context = req.context self.context = req.context
@ -104,10 +108,10 @@ class ControllerTest(object):
return self._data_request(path, data, content_type, method='PUT', return self._data_request(path, data, content_type, method='PUT',
version=version) version=version)
def _patch(self, path, data, content_type='application/json', def _patch(self, path, data, params=None, content_type='application/json',
version=None): version=None):
return self._data_request(path, data, content_type, method='PATCH', return self._data_request(path, data, content_type, method='PATCH',
version=version) version=version, params=params)
def tearDown(self): def tearDown(self):
# Common tearDown to assert that policy enforcement happens for all # Common tearDown to assert that policy enforcement happens for all

View File

@ -606,6 +606,55 @@ class ActionBaseTest(base.SenlinTestCase):
action.set_status.assert_not_called() action.set_status.assert_not_called()
mock_signal.aseert_not_called() mock_signal.aseert_not_called()
@mock.patch.object(dobj.Dependency, 'get_depended')
def test_force_cancel(self, mock_dobj):
action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID)
action.load = mock.Mock()
action.set_status = mock.Mock()
mock_dobj.return_value = None
action.status = action.RUNNING
action.force_cancel()
action.load.assert_not_called()
action.set_status.assert_called_once_with(
action.RES_CANCEL, 'Action execution force cancelled')
@mock.patch.object(dobj.Dependency, 'get_depended')
def test_force_cancel_children(self, mock_dobj):
action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID)
child_status_mock = mock.Mock()
children = []
for child_id in CHILD_IDS:
child = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=child_id)
child.status = child.WAITING_LIFECYCLE_COMPLETION
child.set_status = child_status_mock
children.append(child)
mock_dobj.return_value = CHILD_IDS
action.set_status = mock.Mock()
action.load = mock.Mock()
action.load.side_effect = children
action.status = action.RUNNING
action.force_cancel()
mock_dobj.assert_called_once_with(action.context, action.id)
self.assertEqual(2, child_status_mock.call_count)
self.assertEqual(2, action.load.call_count)
@mock.patch.object(dobj.Dependency, 'get_depended')
def test_force_cancel_immutable(self, mock_dobj):
action = ab.Action(OBJID, 'OBJECT_ACTION', self.ctx, id=ACTION_ID)
action.load = mock.Mock()
action.set_status = mock.Mock()
mock_dobj.return_value = None
action.status = action.FAILED
self.assertRaises(exception.ActionImmutable, action.force_cancel)
action.load.assert_not_called()
action.set_status.assert_not_called()
def test_execute_default(self): def test_execute_default(self):
action = ab.Action.__new__(DummyAction, OBJID, 'BOOM', self.ctx) action = ab.Action.__new__(DummyAction, OBJID, 'BOOM', self.ctx)
self.assertRaises(NotImplementedError, self.assertRaises(NotImplementedError,

View File

@ -215,7 +215,7 @@ class ActionTest(base.SenlinTestCase):
mock_load.return_value = x_obj mock_load.return_value = x_obj
req = orao.ActionUpdateRequest(identity='ACTION_ID', req = orao.ActionUpdateRequest(identity='ACTION_ID',
status='CANCELLED') status='CANCELLED', force=False)
result = self.eng.action_update(self.ctx, req.obj_to_primitive()) result = self.eng.action_update(self.ctx, req.obj_to_primitive())
self.assertIsNone(result) self.assertIsNone(result)