Merge "Add force to action_update"
This commit is contained in:
commit
0700a8bf29
|
@ -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
|
||||||
---------------
|
---------------
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -2315,8 +2315,11 @@ 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)
|
||||||
LOG.info("Signaling action '%s' to Cancel.", req.identity)
|
if req.force:
|
||||||
action.signal_cancel()
|
action.force_cancel()
|
||||||
|
else:
|
||||||
|
LOG.info("Signaling action '%s' to Cancel.", req.identity)
|
||||||
|
action.signal_cancel()
|
||||||
else:
|
else:
|
||||||
msg = ("Unknown status %(status)s for action %(action)s" %
|
msg = ("Unknown status %(status)s for action %(action)s" %
|
||||||
{"status": req.status, "action": req.identity})
|
{"status": req.status, "action": req.identity})
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -606,6 +606,55 @@ class ActionBaseTest(base.SenlinTestCase):
|
||||||
action.set_status.assert_not_called()
|
action.set_status.assert_not_called()
|
||||||
mock_signal.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):
|
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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue