Merge "Add force to action_update"

This commit is contained in:
Zuul 2019-01-15 01:30:24 +00:00 committed by Gerrit Code Review
commit 0700a8bf29
11 changed files with 154 additions and 9 deletions

View File

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

View File

@ -131,6 +131,12 @@ action_status_query:
description: |
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:
type: string
in: query
@ -358,11 +364,11 @@ action_status:
A string representation of the current status of the action.
action_status_update:
type: object
type: string
in: body
required: True
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.
action_target:

View File

@ -107,6 +107,15 @@ class ActionController(wsgi.Controller):
if data is None:
raise exc.HTTPBadRequest(_("Malformed request data, missing "
"'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
obj = util.parse_request('ActionUpdateRequest', req, data)

View File

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

View File

@ -368,6 +368,38 @@ class Action(object):
action.set_status(action.RES_CANCEL,
'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):
"""Execute the action.

View File

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

View File

@ -75,5 +75,6 @@ class ActionUpdateRequest(base.SenlinObject):
fields = {
'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,
{
'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'
}
}
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)

View File

@ -85,10 +85,14 @@ class ControllerTest(object):
return self._simple_request(path, params=params, method='DELETE')
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['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.context = utils.dummy_context('api_test_user', self.project)
self.context = req.context
@ -104,10 +108,10 @@ class ControllerTest(object):
return self._data_request(path, data, content_type, method='PUT',
version=version)
def _patch(self, path, data, content_type='application/json',
def _patch(self, path, data, params=None, content_type='application/json',
version=None):
return self._data_request(path, data, content_type, method='PATCH',
version=version)
version=version, params=params)
def tearDown(self):
# 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()
mock_signal.assert_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):
action = ab.Action.__new__(DummyAction, OBJID, 'BOOM', self.ctx)
self.assertRaises(NotImplementedError,

View File

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