Support action_execution deletion
With this patch, users can perform operation as following: DELETE http://127.0.0.1:8989/v2/action_executions/<action_execution_id> NOTE: * Added a new config item 'allow_action_execution_deletion', with default value 'False'. * The 'allow_action_execution_deletion' item is set to 'True' for functional testing. * Only ad-hoc action-execution can be deleted. * Only completed action execution can be deleted. Implements: blueprint mistral-action-execution-deletion Closes-Bug: #1488157 Change-Id: I3729636911a42c273c5a7b2d7fbdaae0da398e31
This commit is contained in:
parent
7a8c29a2f1
commit
f2eaea5a12
@ -101,6 +101,7 @@ function configure_mistral {
|
||||
iniset $MISTRAL_CONF_FILE keystone_authtoken admin_tenant_name $SERVICE_TENANT_NAME
|
||||
iniset $MISTRAL_CONF_FILE keystone_authtoken admin_user $MISTRAL_ADMIN_USER
|
||||
iniset $MISTRAL_CONF_FILE keystone_authtoken admin_password $SERVICE_PASSWORD
|
||||
iniset $MISTRAL_CONF_FILE keystone_authtoken auth_uri "http://${KEYSTONE_AUTH_HOST}:5000/v3"
|
||||
|
||||
# Setup RabbitMQ credentials
|
||||
iniset $MISTRAL_CONF_FILE DEFAULT rabbit_userid $RABBIT_USERID
|
||||
@ -111,8 +112,8 @@ function configure_mistral {
|
||||
iniset $MISTRAL_CONF_FILE database max_overflow -1
|
||||
iniset $MISTRAL_CONF_FILE database max_pool_size 1000
|
||||
|
||||
# Configure keystone auth url
|
||||
iniset $MISTRAL_CONF_FILE keystone_authtoken auth_uri "http://${KEYSTONE_AUTH_HOST}:5000/v3"
|
||||
# Configure action execution deletion policy
|
||||
iniset $MISTRAL_CONF_FILE api allow_action_execution_deletion True
|
||||
}
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from pecan import rest
|
||||
from wsme import types as wtypes
|
||||
@ -191,6 +192,29 @@ class ActionExecutionsController(rest.RestController):
|
||||
|
||||
return _get_action_executions()
|
||||
|
||||
@rest_utils.wrap_wsme_controller_exception
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
|
||||
def delete(self, id):
|
||||
"""Delete the specified action_execution."""
|
||||
|
||||
LOG.info("Delete action_execution [id=%s]" % id)
|
||||
|
||||
if not cfg.CONF.api.allow_action_execution_deletion:
|
||||
raise exc.NotAllowedException("Action execution deletion is not "
|
||||
"allowed.")
|
||||
|
||||
action_ex = db_api.get_action_execution(id)
|
||||
|
||||
if action_ex.task_execution_id:
|
||||
raise exc.NotAllowedException("Only ad-hoc action execution can "
|
||||
"be deleted.")
|
||||
|
||||
if not states.is_completed(action_ex.state):
|
||||
raise exc.NotAllowedException("Only completed action execution "
|
||||
"can be deleted.")
|
||||
|
||||
return db_api.delete_action_execution(id)
|
||||
|
||||
|
||||
class TasksActionExecutionController(rest.RestController):
|
||||
@wsme_pecan.wsexpose(ActionExecutions, wtypes.text)
|
||||
|
@ -36,7 +36,10 @@ launch_opt = cfg.ListOpt(
|
||||
|
||||
api_opts = [
|
||||
cfg.StrOpt('host', default='0.0.0.0', help='Mistral API server host'),
|
||||
cfg.IntOpt('port', default=8989, help='Mistral API server port')
|
||||
cfg.IntOpt('port', default=8989, help='Mistral API server port'),
|
||||
cfg.BoolOpt('allow_action_execution_deletion', default=False,
|
||||
help='Enables the ability to delete action_execution which '
|
||||
'has no relationship with workflows.'),
|
||||
]
|
||||
|
||||
pecan_opts = [
|
||||
|
@ -127,3 +127,8 @@ class SizeLimitExceededException(MistralException):
|
||||
|
||||
class CoordinationException(MistralException):
|
||||
http_code = 500
|
||||
|
||||
|
||||
class NotAllowedException(MistralException):
|
||||
http_code = 403
|
||||
message = "Operation not allowed"
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
import json
|
||||
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
from tempest import test
|
||||
from tempest_lib import decorators
|
||||
from tempest_lib import exceptions
|
||||
@ -22,6 +24,9 @@ from mistral.tests.functional import base
|
||||
from mistral import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkbookTestsV2(base.TestCase):
|
||||
|
||||
_service = 'workflowv2'
|
||||
@ -976,15 +981,26 @@ class TasksTestsV2(base.TestCase):
|
||||
)
|
||||
|
||||
|
||||
# TODO(namkhotkin) Need more tests on action executions.
|
||||
class ActionExecutionTestsV2(base.TestCase):
|
||||
|
||||
_service = 'workflowv2'
|
||||
|
||||
@classmethod
|
||||
def resource_cleanup(cls):
|
||||
for action_ex in cls.client.action_executions:
|
||||
try:
|
||||
cls.client.delete_obj('action_executions', action_ex)
|
||||
except Exception as e:
|
||||
LOG.exception('Exception raised when deleting '
|
||||
'action_executions %s, error message: %s.'
|
||||
% (action_ex, six.text_type(e)))
|
||||
|
||||
cls.client.action_executions = []
|
||||
|
||||
super(ActionExecutionTestsV2, cls).resource_cleanup()
|
||||
|
||||
@test.attr(type='sanity')
|
||||
def test_run_action_execution(self):
|
||||
resp, body = self.client.post_json(
|
||||
'action_executions',
|
||||
resp, body = self.client.create_action_execution(
|
||||
{
|
||||
'name': 'std.echo',
|
||||
'input': '{"output": "Hello, Mistral!"}'
|
||||
@ -992,7 +1008,6 @@ class ActionExecutionTestsV2(base.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(201, resp.status)
|
||||
body = json.loads(body)
|
||||
output = json.loads(body['output'])
|
||||
self.assertDictEqual(
|
||||
{'result': 'Hello, Mistral!'},
|
||||
@ -1001,8 +1016,7 @@ class ActionExecutionTestsV2(base.TestCase):
|
||||
|
||||
@test.attr(type='sanity')
|
||||
def test_run_action_std_http(self):
|
||||
resp, body = self.client.post_json(
|
||||
'action_executions',
|
||||
resp, body = self.client.create_action_execution(
|
||||
{
|
||||
'name': 'std.http',
|
||||
'input': '{"url": "http://wiki.openstack.org"}'
|
||||
@ -1010,14 +1024,12 @@ class ActionExecutionTestsV2(base.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(201, resp.status)
|
||||
body = json.loads(body)
|
||||
output = json.loads(body['output'])
|
||||
self.assertTrue(output['result']['status'] in range(200, 307))
|
||||
|
||||
@test.attr(type='sanity')
|
||||
def test_run_action_std_http_error(self):
|
||||
resp, body = self.client.post_json(
|
||||
'action_executions',
|
||||
resp, body = self.client.create_action_execution(
|
||||
{
|
||||
'name': 'std.http',
|
||||
'input': '{"url": "http://www.google.ru/not-found-test"}'
|
||||
@ -1025,14 +1037,12 @@ class ActionExecutionTestsV2(base.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(201, resp.status)
|
||||
body = json.loads(body)
|
||||
output = json.loads(body['output'])
|
||||
self.assertEqual(404, output['result']['status'])
|
||||
|
||||
@test.attr(type='sanity')
|
||||
def test_create_action_execution(self):
|
||||
resp, body = self.client.post_json(
|
||||
'action_executions',
|
||||
resp, body = self.client.create_action_execution(
|
||||
{
|
||||
'name': 'std.echo',
|
||||
'input': '{"output": "Hello, Mistral!"}',
|
||||
@ -1041,9 +1051,6 @@ class ActionExecutionTestsV2(base.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(201, resp.status)
|
||||
|
||||
body = json.loads(body)
|
||||
|
||||
self.assertEqual('RUNNING', body['state'])
|
||||
|
||||
# We must reread action execution in order to get actual
|
||||
@ -1059,3 +1066,12 @@ class ActionExecutionTestsV2(base.TestCase):
|
||||
{'result': 'Hello, Mistral!'},
|
||||
output
|
||||
)
|
||||
|
||||
@test.attr(type='negative')
|
||||
def test_delete_nonexistent_action_execution(self):
|
||||
self.assertRaises(
|
||||
exceptions.NotFound,
|
||||
self.client.delete_obj,
|
||||
'action_executions',
|
||||
'nonexist'
|
||||
)
|
||||
|
@ -70,6 +70,7 @@ class MistralClientBase(rest_client.RestClient):
|
||||
self.workflows = []
|
||||
self.triggers = []
|
||||
self.actions = []
|
||||
self.action_executions = []
|
||||
|
||||
def get_list_obj(self, name):
|
||||
resp, body = self.get(name)
|
||||
@ -204,6 +205,15 @@ class MistralClientV2(MistralClientBase):
|
||||
|
||||
return [t for t in all_tasks if t['workflow_name'] == wf_name]
|
||||
|
||||
def create_action_execution(self, request_body):
|
||||
resp, body = self.post_json('action_executions', request_body)
|
||||
|
||||
params = json.loads(request_body.get('params', '{}'))
|
||||
if params.get('save_result', False):
|
||||
self.action_executions.append(json.loads(body)['id'])
|
||||
|
||||
return resp, json.loads(body)
|
||||
|
||||
|
||||
class AuthProv(auth.KeystoneV2AuthProvider):
|
||||
def __init__(self):
|
||||
|
@ -17,7 +17,9 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from mistral.db.v2 import api as db_api
|
||||
from mistral.db.v2.sqlalchemy import models
|
||||
@ -45,6 +47,34 @@ ACTION_EX_DB = models.ActionExecution(
|
||||
updated_at=datetime.datetime(1970, 1, 1)
|
||||
)
|
||||
|
||||
AD_HOC_ACTION_EX_DB = models.ActionExecution(
|
||||
id='123',
|
||||
state=states.SUCCESS,
|
||||
state_info=states.SUCCESS,
|
||||
tags=['foo', 'fee'],
|
||||
name='std.echo',
|
||||
description='something',
|
||||
accepted=True,
|
||||
input={},
|
||||
output={},
|
||||
created_at=datetime.datetime(1970, 1, 1),
|
||||
updated_at=datetime.datetime(1970, 1, 1)
|
||||
)
|
||||
|
||||
ACTION_EX_DB_NOT_COMPLETE = models.ActionExecution(
|
||||
id='123',
|
||||
state=states.RUNNING,
|
||||
state_info=states.RUNNING,
|
||||
tags=['foo', 'fee'],
|
||||
name='std.echo',
|
||||
description='something',
|
||||
accepted=False,
|
||||
input={},
|
||||
output={},
|
||||
created_at=datetime.datetime(1970, 1, 1),
|
||||
updated_at=datetime.datetime(1970, 1, 1)
|
||||
)
|
||||
|
||||
ACTION_EX = {
|
||||
'id': '123',
|
||||
'workflow_name': 'flow',
|
||||
@ -80,24 +110,39 @@ BROKEN_ACTION = copy.copy(ACTION_EX)
|
||||
BROKEN_ACTION['output'] = 'string not escaped'
|
||||
|
||||
MOCK_ACTION = mock.MagicMock(return_value=ACTION_EX_DB)
|
||||
MOCK_ACTION_NOT_COMPLETE = mock.MagicMock(
|
||||
return_value=ACTION_EX_DB_NOT_COMPLETE
|
||||
)
|
||||
MOCK_AD_HOC_ACTION = mock.MagicMock(return_value=AD_HOC_ACTION_EX_DB)
|
||||
MOCK_ACTIONS = mock.MagicMock(return_value=[ACTION_EX_DB])
|
||||
MOCK_EMPTY = mock.MagicMock(return_value=[])
|
||||
MOCK_NOT_FOUND = mock.MagicMock(side_effect=exc.NotFoundException())
|
||||
MOCK_DELETE = mock.MagicMock(return_value=None)
|
||||
|
||||
|
||||
class TestActionExecutionsController(base.FunctionalTest):
|
||||
def setUp(self):
|
||||
super(TestActionExecutionsController, self).setUp()
|
||||
|
||||
self.addCleanup(
|
||||
cfg.CONF.set_default,
|
||||
'allow_action_execution_deletion',
|
||||
False,
|
||||
group='api'
|
||||
)
|
||||
|
||||
@mock.patch.object(db_api, 'get_action_execution', MOCK_ACTION)
|
||||
def test_get(self):
|
||||
resp = self.app.get('/v2/action_executions/123')
|
||||
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(200, resp.status_int)
|
||||
self.assertDictEqual(ACTION_EX, resp.json)
|
||||
|
||||
@mock.patch.object(db_api, 'get_action_execution', MOCK_NOT_FOUND)
|
||||
def test_get_not_found(self):
|
||||
resp = self.app.get('/v2/action_executions/123', expect_errors=True)
|
||||
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
self.assertEqual(404, resp.status_int)
|
||||
|
||||
@mock.patch.object(rpc.EngineClient, 'start_action')
|
||||
def test_post(self, f):
|
||||
@ -112,7 +157,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
self.assertEqual(201, resp.status_int)
|
||||
|
||||
action_exec = ACTION_EX
|
||||
del action_exec['task_name']
|
||||
@ -136,7 +181,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
{'name': 'nova.servers_list'}
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 201)
|
||||
self.assertEqual(201, resp.status_int)
|
||||
self.assertEqual('{"result": "123"}', resp.json['output'])
|
||||
|
||||
f.assert_called_once_with('nova.servers_list', {}, description=None)
|
||||
@ -148,7 +193,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
expect_errors=True
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
self.assertEqual(400, resp.status_int)
|
||||
|
||||
def test_post_bad_input(self):
|
||||
resp = self.app.post_json(
|
||||
@ -157,7 +202,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
expect_errors=True
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
self.assertEqual(400, resp.status_int)
|
||||
|
||||
@mock.patch.object(rpc.EngineClient, 'on_action_complete')
|
||||
def test_put(self, f):
|
||||
@ -165,7 +210,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
|
||||
resp = self.app.put_json('/v2/action_executions/123', UPDATED_ACTION)
|
||||
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(200, resp.status_int)
|
||||
self.assertDictEqual(UPDATED_ACTION, resp.json)
|
||||
|
||||
f.assert_called_once_with(
|
||||
@ -179,7 +224,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
|
||||
resp = self.app.put_json('/v2/action_executions/123', ERROR_ACTION)
|
||||
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(200, resp.status_int)
|
||||
self.assertDictEqual(ERROR_ACTION, resp.json)
|
||||
|
||||
f.assert_called_once_with(
|
||||
@ -199,7 +244,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
expect_errors=True
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 404)
|
||||
self.assertEqual(404, resp.status_int)
|
||||
|
||||
def test_put_bad_result(self):
|
||||
resp = self.app.put_json(
|
||||
@ -208,7 +253,7 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
expect_errors=True
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
self.assertEqual(400, resp.status_int)
|
||||
|
||||
@mock.patch.object(rpc.EngineClient, 'on_action_complete')
|
||||
def test_put_without_result(self, f):
|
||||
@ -219,21 +264,69 @@ class TestActionExecutionsController(base.FunctionalTest):
|
||||
|
||||
resp = self.app.put_json('/v2/action_executions/123', action_ex)
|
||||
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(200, resp.status_int)
|
||||
|
||||
@mock.patch.object(db_api, 'get_action_executions', MOCK_ACTIONS)
|
||||
def test_get_all(self):
|
||||
resp = self.app.get('/v2/action_executions')
|
||||
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(200, resp.status_int)
|
||||
|
||||
self.assertEqual(len(resp.json['action_executions']), 1)
|
||||
self.assertEqual(1, len(resp.json['action_executions']))
|
||||
self.assertDictEqual(ACTION_EX, resp.json['action_executions'][0])
|
||||
|
||||
@mock.patch.object(db_api, 'get_action_executions', MOCK_EMPTY)
|
||||
def test_get_all_empty(self):
|
||||
resp = self.app.get('/v2/action_executions')
|
||||
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
self.assertEqual(200, resp.status_int)
|
||||
|
||||
self.assertEqual(len(resp.json['action_executions']), 0)
|
||||
self.assertEqual(0, len(resp.json['action_executions']))
|
||||
|
||||
@mock.patch.object(db_api, 'get_action_execution', MOCK_AD_HOC_ACTION)
|
||||
@mock.patch.object(db_api, 'delete_action_execution', MOCK_DELETE)
|
||||
def test_delete(self):
|
||||
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
|
||||
|
||||
resp = self.app.delete('/v2/action_executions/123')
|
||||
|
||||
self.assertEqual(204, resp.status_int)
|
||||
|
||||
@mock.patch.object(db_api, 'get_action_execution', MOCK_NOT_FOUND)
|
||||
def test_delete_not_found(self):
|
||||
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
|
||||
|
||||
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
|
||||
|
||||
self.assertEqual(404, resp.status_int)
|
||||
|
||||
def test_delete_not_allowed(self):
|
||||
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
|
||||
|
||||
self.assertEqual(403, resp.status_int)
|
||||
self.assertIn("Action execution deletion is not allowed", resp.body)
|
||||
|
||||
@mock.patch.object(db_api, 'get_action_execution', MOCK_ACTION)
|
||||
def test_delete_action_exeuction_with_task(self):
|
||||
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
|
||||
|
||||
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
|
||||
|
||||
self.assertEqual(403, resp.status_int)
|
||||
self.assertIn("Only ad-hoc action execution can be deleted", resp.body)
|
||||
|
||||
@mock.patch.object(
|
||||
db_api,
|
||||
'get_action_execution',
|
||||
MOCK_ACTION_NOT_COMPLETE
|
||||
)
|
||||
def test_delete_action_exeuction_not_complete(self):
|
||||
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
|
||||
|
||||
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
|
||||
|
||||
self.assertEqual(403, resp.status_int)
|
||||
self.assertIn(
|
||||
"Only completed action execution can be deleted",
|
||||
resp.body
|
||||
)
|
||||
|
@ -633,6 +633,18 @@ class ActionExecutionTest(SQLAlchemyTest):
|
||||
created.id
|
||||
)
|
||||
|
||||
def test_delete_other_tenant_action_execution(self):
|
||||
created = db_api.create_action_execution(ACTION_EXECS[0])
|
||||
|
||||
# Create a new user.
|
||||
auth_context.set_ctx(test_base.get_context(default=False))
|
||||
|
||||
self.assertRaises(
|
||||
exc.NotFoundException,
|
||||
db_api.delete_action_execution,
|
||||
created.id
|
||||
)
|
||||
|
||||
def test_trim_status_info(self):
|
||||
created = db_api.create_action_execution(ACTION_EXECS[0])
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user