From 20c340869241910fe1e2db2a861694a429f93204 Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 2 Jan 2020 13:35:59 +0000 Subject: [PATCH] Add namespaces to Ad-Hoc actions added namespace for the actions, actions can have the same name if they are not in the same namespace, when executing an action, if an action with that name is not found in the workflow namespace or given namespace mistral will look for that action in the default namespace. * action base can only be in the same namespace,or in the default namespace. * Namespaces are not part of the mistral DSL. * The default namespace is an empty string ''. * all actions will be in a namespace, if not specified, they will be under default namespace Depends-On: I61acaed1658d291798e10229e81136259fcdb627 Change-Id: I07862e30adf28404ec70a473571a9213e53d8a08 Partially-Implements: blueprint create-and-run-workflows-within-a-namespace Signed-off-by: ali --- mistral/api/controllers/v2/action.py | 56 ++++++++----- .../api/controllers/v2/action_execution.py | 5 +- mistral/api/controllers/v2/resources.py | 5 +- ..._namespace_column_to_action_definitions.py | 67 ++++++++++++++++ mistral/db/v2/api.py | 26 ++++--- mistral/db/v2/sqlalchemy/api.py | 51 +++++++----- mistral/db/v2/sqlalchemy/models.py | 11 +-- mistral/engine/action_handler.py | 15 ++-- mistral/engine/actions.py | 44 +++++++---- mistral/engine/base.py | 4 +- mistral/engine/default_engine.py | 17 ++-- mistral/engine/engine_server.py | 8 +- mistral/engine/tasks.py | 3 +- mistral/executors/default_executor.py | 1 - mistral/rpc/clients.py | 6 +- mistral/services/action_manager.py | 26 ++++--- mistral/services/actions.py | 37 +++++---- mistral/services/workbooks.py | 12 +-- .../unit/api/v2/test_action_executions.py | 13 +++- .../unit/db/v2/test_sqlalchemy_db_api.py | 14 ++-- .../tests/unit/engine/test_action_caching.py | 34 ++++---- .../tests/unit/engine/test_adhoc_actions.py | 75 +++++++++++++++++- mistral/tests/unit/engine/test_run_action.py | 78 ++++++++++++++++++- .../unit/services/test_action_service.py | 58 +++++++++++++- .../unit/services/test_workbook_service.py | 13 +++- .../notes/namespace_for_adhoc_actions.yaml | 14 ++++ 26 files changed, 538 insertions(+), 155 deletions(-) create mode 100644 mistral/db/sqlalchemy/migration/alembic_migrations/versions/037_add_namespace_column_to_action_definitions.py create mode 100644 releasenotes/notes/namespace_for_adhoc_actions.yaml diff --git a/mistral/api/controllers/v2/action.py b/mistral/api/controllers/v2/action.py index 660189707..32e00eaca 100644 --- a/mistral/api/controllers/v2/action.py +++ b/mistral/api/controllers/v2/action.py @@ -1,5 +1,6 @@ # Copyright 2014 - Mirantis, Inc. # Copyright 2015 Huawei Technologies Co., Ltd. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -46,11 +47,12 @@ class ActionsController(rest.RestController, hooks.HookController): spec_parser.get_action_list_spec_from_yaml) @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.Action, wtypes.text) - def get(self, identifier): + @wsme_pecan.wsexpose(resources.Action, wtypes.text, wtypes.text) + def get(self, identifier, namespace=''): """Return the named action. :param identifier: ID or name of the Action to get. + :param namespace: The namespace of the action. """ acl.enforce('actions:get', context.ctx()) @@ -60,17 +62,19 @@ class ActionsController(rest.RestController, hooks.HookController): # Use retries to prevent possible failures. db_model = rest_utils.rest_retry_on_db_error( db_api.get_action_definition - )(identifier) + )(identifier, namespace=namespace) return resources.Action.from_db_model(db_model) @rest_utils.wrap_pecan_controller_exception @pecan.expose(content_type="text/plain") - def put(self, identifier=None): + def put(self, identifier=None, namespace=''): """Update one or more actions. :param identifier: Optional. If provided, it's UUID or name of an action. Only one action can be updated with identifier param. + :param namespace: Optional. If provided, it's the namespace that + the action is under. NOTE: This text is allowed to have definitions of multiple actions. In this case they all will be updated. @@ -81,6 +85,7 @@ class ActionsController(rest.RestController, hooks.HookController): LOG.debug("Update action(s) [definition=%s]", definition) + namespace = namespace or '' scope = pecan.request.GET.get('scope', 'private') resources.Action.validate_scope(scope) if scope == 'public': @@ -92,7 +97,8 @@ class ActionsController(rest.RestController, hooks.HookController): return actions.update_actions( definition, scope=scope, - identifier=identifier + identifier=identifier, + namespace=namespace ) db_acts = _update_actions() @@ -105,13 +111,19 @@ class ActionsController(rest.RestController, hooks.HookController): @rest_utils.wrap_pecan_controller_exception @pecan.expose(content_type="text/plain") - def post(self): + def post(self, namespace=''): """Create a new action. + :param namespace: Optional. The namespace to create the ad-hoc action + in. actions with the same name can be added to a given + project if they are in two different namespaces. + (default namespace is '') + NOTE: This text is allowed to have definitions of multiple actions. In this case they all will be created. """ acl.enforce('actions:create', context.ctx()) + namespace = namespace or '' definition = pecan.request.text scope = pecan.request.GET.get('scope', 'private') @@ -126,7 +138,9 @@ class ActionsController(rest.RestController, hooks.HookController): @rest_utils.rest_retry_on_db_error def _create_action_definitions(): with db_api.transaction(): - return actions.create_actions(definition, scope=scope) + return actions.create_actions(definition, + scope=scope, + namespace=namespace) db_acts = _create_action_definitions() @@ -137,26 +151,27 @@ class ActionsController(rest.RestController, hooks.HookController): return resources.Actions(actions=action_list).to_json() @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) - def delete(self, identifier): + @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204) + def delete(self, identifier, namespace=''): """Delete the named action. :param identifier: Name or UUID of the action to delete. + :param namespace: The namespace of which the action is in. """ acl.enforce('actions:delete', context.ctx()) - LOG.debug("Delete action [identifier=%s]", identifier) @rest_utils.rest_retry_on_db_error def _delete_action_definition(): with db_api.transaction(): - db_model = db_api.get_action_definition(identifier) + db_model = db_api.get_action_definition(identifier, + namespace=namespace) if db_model.is_system: msg = "Attempt to delete a system action: %s" % identifier raise exc.DataAccessException(msg) - - db_api.delete_action_definition(identifier) + db_api.delete_action_definition(identifier, + namespace=namespace) _delete_action_definition() @@ -164,12 +179,13 @@ class ActionsController(rest.RestController, hooks.HookController): @wsme_pecan.wsexpose(resources.Actions, types.uuid, int, types.uniquelist, types.list, types.uniquelist, wtypes.text, wtypes.text, resources.SCOPE_TYPES, wtypes.text, - wtypes.text, wtypes.text, wtypes.text, wtypes.text, - wtypes.text) + wtypes.text, wtypes.text, wtypes.text, + wtypes.text, wtypes.text, wtypes.text) def get_all(self, marker=None, limit=None, sort_keys='name', - sort_dirs='asc', fields='', created_at=None, name=None, - scope=None, tags=None, updated_at=None, - description=None, definition=None, is_system=None, input=None): + sort_dirs='asc', fields='', created_at=None, + name=None, scope=None, tags=None, + updated_at=None, description=None, definition=None, + is_system=None, input=None, namespace=''): """Return all actions. :param marker: Optional. Pagination marker for large data sets. @@ -199,6 +215,7 @@ class ActionsController(rest.RestController, hooks.HookController): time and date. :param updated_at: Optional. Keep only resources with specific latest update time and date. + :param namespace: Optional. The namespace of the action. """ acl.enforce('actions:list', context.ctx()) @@ -211,7 +228,8 @@ class ActionsController(rest.RestController, hooks.HookController): description=description, definition=definition, is_system=is_system, - input=input + input=input, + namespace=namespace ) LOG.debug("Fetch actions. marker=%s, limit=%s, sort_keys=%s, " diff --git a/mistral/api/controllers/v2/action_execution.py b/mistral/api/controllers/v2/action_execution.py index 1708be855..33cce07be 100644 --- a/mistral/api/controllers/v2/action_execution.py +++ b/mistral/api/controllers/v2/action_execution.py @@ -1,5 +1,6 @@ # Copyright 2015 - Mirantis, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -144,16 +145,15 @@ class ActionExecutionsController(rest.RestController): :param action_ex: Action to execute """ acl.enforce('action_executions:create', context.ctx()) - LOG.debug( "Create action_execution [action_execution=%s]", action_ex ) - name = action_ex.name description = action_ex.description or None action_input = action_ex.input or {} params = action_ex.params or {} + namespace = action_ex.workflow_namespace or '' if not name: raise exc.InputException( @@ -164,6 +164,7 @@ class ActionExecutionsController(rest.RestController): name, action_input, description=description, + namespace=namespace, **params ) diff --git a/mistral/api/controllers/v2/resources.py b/mistral/api/controllers/v2/resources.py index df6b0ac33..5a24bba36 100644 --- a/mistral/api/controllers/v2/resources.py +++ b/mistral/api/controllers/v2/resources.py @@ -1,6 +1,7 @@ # Copyright 2013 - Mirantis, Inc. # Copyright 2018 - Extreme Networks, Inc. # Copyright 2019 - NetCracker Technology Corp. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -206,6 +207,7 @@ class Action(resource.Resource, ScopedResource): created_at = wtypes.text updated_at = wtypes.text + namespace = wtypes.text @classmethod def sample(cls): @@ -217,7 +219,8 @@ class Action(resource.Resource, ScopedResource): scope='private', project_id='a7eb669e9819420ea4bd1453e672c0a7', created_at='1970-01-01T00:00:00.000000', - updated_at='1970-01-01T00:00:00.000000' + updated_at='1970-01-01T00:00:00.000000', + namespace='' ) diff --git a/mistral/db/sqlalchemy/migration/alembic_migrations/versions/037_add_namespace_column_to_action_definitions.py b/mistral/db/sqlalchemy/migration/alembic_migrations/versions/037_add_namespace_column_to_action_definitions.py new file mode 100644 index 000000000..e2e5e2657 --- /dev/null +++ b/mistral/db/sqlalchemy/migration/alembic_migrations/versions/037_add_namespace_column_to_action_definitions.py @@ -0,0 +1,67 @@ +# Copyright 2020 Nokia Software. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""add namespace column to action definitions + +Revision ID: 037 +Revises: 036 +Create Date: 2020-1-6 10:22:20 + +""" + +# revision identifiers, used by Alembic. + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine import reflection +from sqlalchemy.sql import table, column + +revision = '037' +down_revision = '036' + + +def upgrade(): + op.add_column( + 'action_definitions_v2', + sa.Column('namespace', sa.String(length=255), nullable=True) + ) + + inspect = reflection.Inspector.from_engine(op.get_bind()) + + unique_constraints = [ + unique_constraint['name'] for unique_constraint in + inspect.get_unique_constraints('action_definitions_v2') + ] + if 'name' in unique_constraints: + op.drop_index('name', table_name='action_definitions_v2') + + if 'action_definitions_v2_name_project_id_key' in unique_constraints: + op.drop_constraint('action_definitions_v2_name_project_id_key', + table_name='action_definitions_v2') + + op.create_unique_constraint( + None, + 'action_definitions_v2', + ['name', 'namespace', 'project_id'] + ) + + action_def = table('action_definitions_v2', column('namespace')) + session = sa.orm.Session(bind=op.get_bind()) + with session.begin(subtransactions=True): + session.execute( + action_def.update().values(namespace='').where( + action_def.c.namespace is None)) # noqa + + session.commit() diff --git a/mistral/db/v2/api.py b/mistral/db/v2/api.py index b77789289..9d8f7d2d5 100644 --- a/mistral/db/v2/api.py +++ b/mistral/db/v2/api.py @@ -1,5 +1,6 @@ # Copyright 2015 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -185,22 +186,30 @@ def get_action_definition_by_id(id, fields=()): return IMPL.get_action_definition_by_id(id, fields=fields) -def get_action_definition(name, fields=()): - return IMPL.get_action_definition(name, fields=fields) +def get_action_definition(name, fields=(), namespace=''): + return IMPL.get_action_definition(name, fields=fields, namespace=namespace) -def load_action_definition(name, fields=()): +def load_action_definition(name, fields=(), namespace=''): """Unlike get_action_definition this method is allowed to return None.""" + key = '{}:{}'.format(name, namespace) if namespace else name with _ACTION_DEF_CACHE_LOCK: - action_def = _ACTION_DEF_CACHE.get(name) + action_def = _ACTION_DEF_CACHE.get(key) if action_def: return action_def - action_def = IMPL.load_action_definition(name, fields=fields) + action_def = IMPL.load_action_definition(name, fields=fields, + namespace=namespace,) + + # If action definition was not found in the workflow namespace, + # check in the default namespace + if not action_def: + action_def = IMPL.load_action_definition(name, fields=fields, + namespace='') with _ACTION_DEF_CACHE_LOCK: - _ACTION_DEF_CACHE[name] = ( + _ACTION_DEF_CACHE[key] = ( action_def.get_clone() if action_def else None ) @@ -230,8 +239,8 @@ def create_or_update_action_definition(name, values): return IMPL.create_or_update_action_definition(name, values) -def delete_action_definition(name): - return IMPL.delete_action_definition(name) +def delete_action_definition(name, namespace=''): + return IMPL.delete_action_definition(name, namespace=namespace) def delete_action_definitions(**kwargs): @@ -539,7 +548,6 @@ def load_environment(name): def get_environments(limit=None, marker=None, sort_keys=None, sort_dirs=None, **kwargs): - return IMPL.get_environments( limit=limit, marker=marker, diff --git a/mistral/db/v2/sqlalchemy/api.py b/mistral/db/v2/sqlalchemy/api.py index 03aafcf05..09f048791 100644 --- a/mistral/db/v2/sqlalchemy/api.py +++ b/mistral/db/v2/sqlalchemy/api.py @@ -1,6 +1,7 @@ # Copyright 2015 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +21,6 @@ import re import sys import threading - from oslo_config import cfg from oslo_db import exception as db_exc from oslo_db import sqlalchemy as oslo_sqlalchemy @@ -41,11 +41,9 @@ from mistral.services import security from mistral.workflow import states from mistral_lib import utils - CONF = cfg.CONF LOG = logging.getLogger(__name__) - _SCHEMA_LOCK = threading.RLock() _initialized = False @@ -338,7 +336,7 @@ def _get_db_object_by_name_and_namespace_or_id(model, identifier, def _get_db_object_by_name_and_namespace(model, name, - namespace, insecure=False, + namespace='', insecure=False, columns=()): query = ( b.model_query(model, columns=columns) @@ -654,26 +652,38 @@ def get_action_definition_by_id(id, fields=(), session=None): @b.session_aware() -def get_action_definition(identifier, fields=(), session=None): +def get_action_definition(identifier, fields=(), session=None, namespace=''): a_def = _get_db_object_by_name_and_namespace_or_id( models.ActionDefinition, identifier, + namespace=namespace, columns=fields ) + # If the action was not found in the given namespace, + # look in the default namespace + if not a_def: + a_def = _get_db_object_by_name_and_namespace_or_id( + models.ActionDefinition, + identifier, + namespace='', + columns=fields + ) if not a_def: raise exc.DBEntityNotFoundError( - "Action definition not found [action_name=%s]" % identifier + "Action definition not found [action_name=%s,namespace=%s]" + % (identifier, namespace) ) return a_def @b.session_aware() -def load_action_definition(name, fields=(), session=None): - return _get_db_object_by_name( +def load_action_definition(name, fields=(), session=None, namespace=''): + return _get_db_object_by_name_and_namespace( models.ActionDefinition, name, + namespace=namespace, columns=fields ) @@ -693,8 +703,8 @@ def create_action_definition(values, session=None): a_def.save(session=session) except db_exc.DBDuplicateEntry: raise exc.DBDuplicateEntryError( - "Duplicate entry for Action ['name', 'project_id']:" - " {}, {}".format(a_def.name, a_def.project_id) + "Duplicate entry for Action ['name', 'namespace', 'project_id']:" + " {}, {}, {}".format(a_def.name, a_def.namespace, a_def.project_id) ) return a_def @@ -702,7 +712,8 @@ def create_action_definition(values, session=None): @b.session_aware() def update_action_definition(identifier, values, session=None): - a_def = get_action_definition(identifier) + namespace = values.get('namespace', '') + a_def = get_action_definition(identifier, namespace=namespace) a_def.update(values.copy()) @@ -711,15 +722,19 @@ def update_action_definition(identifier, values, session=None): @b.session_aware() def create_or_update_action_definition(name, values, session=None): - if not _get_db_object_by_name(models.ActionDefinition, name): + namespace = values.get('namespace', '') + if not _get_db_object_by_name_and_namespace( + models.ActionDefinition, + name, + namespace=namespace): return create_action_definition(values) else: return update_action_definition(name, values) @b.session_aware() -def delete_action_definition(identifier, session=None): - a_def = get_action_definition(identifier) +def delete_action_definition(identifier, namespace='', session=None): + a_def = get_action_definition(identifier, namespace=namespace) session.delete(a_def) @@ -793,8 +808,8 @@ def update_action_execution_heartbeat(id, session=None): raise exc.DBEntityNotFoundError now = utils.utc_now_sec() - session.query(models.ActionExecution).\ - filter(models.ActionExecution.id == id).\ + session.query(models.ActionExecution). \ + filter(models.ActionExecution.id == id). \ update({'last_heartbeat': now}) @@ -1758,8 +1773,8 @@ def update_resource_member(resource_id, res_type, member_id, values, @b.session_aware() def delete_resource_member(resource_id, res_type, member_id, session=None): - query = _secure_query(models.ResourceMember).\ - filter_by(resource_type=res_type).\ + query = _secure_query(models.ResourceMember). \ + filter_by(resource_type=res_type). \ filter(_get_criterion(resource_id, member_id)) # TODO(kong): Check association with cron triggers when deleting a workflow diff --git a/mistral/db/v2/sqlalchemy/models.py b/mistral/db/v2/sqlalchemy/models.py index ce410afdc..834020738 100644 --- a/mistral/db/v2/sqlalchemy/models.py +++ b/mistral/db/v2/sqlalchemy/models.py @@ -1,5 +1,6 @@ # Copyright 2015 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -31,7 +32,6 @@ from mistral import exceptions as exc from mistral.services import security from mistral_lib import utils - # Definition objects. CONF = cfg.CONF @@ -174,9 +174,12 @@ class ActionDefinition(Definition): """Contains info about registered Actions.""" __tablename__ = 'action_definitions_v2' - + namespace = sa.Column(sa.String(255), nullable=True) __table_args__ = ( - sa.UniqueConstraint('name', 'project_id'), + sa.UniqueConstraint( + 'name', + 'namespace', + 'project_id'), sa.Index('%s_is_system' % __tablename__, 'is_system'), sa.Index('%s_action_class' % __tablename__, 'action_class'), sa.Index('%s_project_id' % __tablename__, 'project_id'), @@ -346,7 +349,6 @@ for cls in utils.iter_subclasses(Execution): retval=True ) - # Many-to-one for 'ActionExecution' and 'TaskExecution'. ActionExecution.task_execution_id = sa.Column( @@ -405,7 +407,6 @@ WorkflowExecution.root_execution = relationship( lazy='select' ) - # Many-to-one for 'TaskExecution' and 'WorkflowExecution'. TaskExecution.workflow_execution_id = sa.Column( diff --git a/mistral/engine/action_handler.py b/mistral/engine/action_handler.py index 4fa526b92..fd364d84d 100644 --- a/mistral/engine/action_handler.py +++ b/mistral/engine/action_handler.py @@ -1,5 +1,6 @@ # Copyright 2015 - Mirantis, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,7 +23,6 @@ from mistral.engine import actions from mistral.engine import task_handler from mistral import exceptions as exc - LOG = logging.getLogger(__name__) @@ -31,7 +31,6 @@ def on_action_complete(action_ex, result): task_ex = action_ex.task_execution action = _build_action(action_ex) - try: action.complete(result) except exc.MistralException as e: @@ -87,8 +86,10 @@ def _build_action(action_ex): adhoc_action_name = action_ex.runtime_context.get('adhoc_action_name') if adhoc_action_name: - action_def = actions.resolve_action_definition(adhoc_action_name) - + action_def = actions.resolve_action_definition( + adhoc_action_name, + namespace=action_ex.workflow_namespace + ) return actions.AdHocAction(action_def, action_ex=action_ex) action_def = actions.resolve_action_definition(action_ex.name) @@ -96,9 +97,9 @@ def _build_action(action_ex): return actions.PythonAction(action_def, action_ex=action_ex) -def build_action_by_name(action_name): - action_def = actions.resolve_action_definition(action_name) - +def build_action_by_name(action_name, namespace=''): + action_def = actions.resolve_action_definition(action_name, + namespace=namespace) action_cls = (actions.PythonAction if not action_def.spec else actions.AdHocAction) diff --git a/mistral/engine/actions.py b/mistral/engine/actions.py index 39f01d880..aeb963762 100644 --- a/mistral/engine/actions.py +++ b/mistral/engine/actions.py @@ -37,7 +37,6 @@ from mistral.workflow import states from mistral_lib import actions as ml_actions from mistral_lib import utils - LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -152,7 +151,8 @@ class Action(object): return True def _create_action_execution(self, input_dict, runtime_ctx, is_sync, - desc='', action_ex_id=None): + desc='', action_ex_id=None, namespace=''): + action_ex_id = action_ex_id or utils.generate_unicode_uuid() values = { @@ -162,6 +162,7 @@ class Action(object): 'state': states.RUNNING, 'input': input_dict, 'runtime_context': runtime_ctx, + 'workflow_namespace': namespace, 'description': desc, 'is_sync': is_sync } @@ -245,7 +246,6 @@ class PythonAction(Action): # to be updated with the action execution ID after the action execution # DB object is created. action_ex_id = utils.generate_unicode_uuid() - self._create_action_execution( self._prepare_input(input_dict), self._prepare_runtime_context(index, safe_rerun), @@ -253,7 +253,6 @@ class PythonAction(Action): desc=desc, action_ex_id=action_ex_id ) - execution_context = self._prepare_execution_context() # Register an asynchronous command to send the action to @@ -320,7 +319,8 @@ class PythonAction(Action): try: prepared_input_dict = self._prepare_input(input_dict) - a = a_m.get_action_class(self.action_def.name)( + a = a_m.get_action_class(self.action_def.name, + self.action_def.namespace)( **prepared_input_dict ) @@ -356,9 +356,9 @@ class PythonAction(Action): if self.action_ex: exc_ctx['action_execution_id'] = self.action_ex.id - exc_ctx['callback_url'] = ( - '/v2/action_executions/%s' % self.action_ex.id - ) + exc_ctx['callback_url'] = ('/v2/action_executions/%s' + % self.action_ex.id + ) return exc_ctx @@ -394,7 +394,8 @@ class AdHocAction(PythonAction): self.action_spec = spec_parser.get_action_spec(action_def.spec) base_action_def = db_api.load_action_definition( - self.action_spec.get_base() + self.action_spec.get_base(), + namespace=action_def.namespace ) if not base_action_def: @@ -539,10 +540,12 @@ class AdHocAction(PythonAction): base_name = base.spec['base'] try: - base = db_api.get_action_definition(base_name) + base = db_api.get_action_definition(base_name, + namespace=base.namespace) except exc.DBEntityNotFoundError: raise exc.InvalidActionException( - "Failed to find action [action_name=%s]" % base_name + "Failed to find action [action_name=%s namespace=%s] " + % (base_name, base.namespace) ) # if the action is repeated @@ -554,6 +557,13 @@ class AdHocAction(PythonAction): return base + def _create_action_execution(self, input_dict, runtime_ctx, is_sync, + desc='', action_ex_id=None): + super()._create_action_execution(input_dict, + runtime_ctx, is_sync, + desc, action_ex_id, + self.adhoc_action_def.namespace) + class WorkflowAction(Action): """Workflow action.""" @@ -650,12 +660,13 @@ class WorkflowAction(Action): def resolve_action_definition(action_spec_name, wf_name=None, - wf_spec_name=None): + wf_spec_name=None, namespace=''): """Resolve action definition accounting for ad-hoc action namespacing. :param action_spec_name: Action name according to a spec. :param wf_name: Workflow name. :param wf_spec_name: Workflow name according to a spec. + :param namespace: The namespace of the action. :return: Action definition (python or ad-hoc). """ @@ -671,14 +682,17 @@ def resolve_action_definition(action_spec_name, wf_name=None, action_full_name = "%s.%s" % (wb_name, action_spec_name) - action_db = db_api.load_action_definition(action_full_name) + action_db = db_api.load_action_definition(action_full_name, + namespace=namespace) if not action_db: - action_db = db_api.load_action_definition(action_spec_name) + action_db = db_api.load_action_definition(action_spec_name, + namespace=namespace) if not action_db: raise exc.InvalidActionException( - "Failed to find action [action_name=%s]" % action_spec_name + "Failed to find action [action_name=%s] in [namespace=%s]" % + (action_spec_name, namespace) ) return action_db diff --git a/mistral/engine/base.py b/mistral/engine/base.py index 9130d20dc..f0686be79 100644 --- a/mistral/engine/base.py +++ b/mistral/engine/base.py @@ -1,5 +1,6 @@ # Copyright 2014 - Mirantis, Inc. # Copyright 2017 - Brocade Communications Systems, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,12 +49,13 @@ class Engine(object): @abc.abstractmethod def start_action(self, action_name, action_input, - description=None, **params): + description=None, namespace='', **params): """Starts the specific action. :param action_name: Action name. :param action_input: Action input data as a dictionary. :param description: Execution description. + :param namespace: The namespace of the action. :param params: Additional options for action running. :return: Action execution object. """ diff --git a/mistral/engine/default_engine.py b/mistral/engine/default_engine.py index 951f95051..8b1da720d 100644 --- a/mistral/engine/default_engine.py +++ b/mistral/engine/default_engine.py @@ -2,6 +2,7 @@ # Copyright 2015 - StackStorm, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2018 - Extreme Networks, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -83,9 +84,10 @@ class DefaultEngine(base.Engine): @db_utils.retry_on_db_error @post_tx_queue.run def start_action(self, action_name, action_input, - description=None, **params): + description=None, namespace='', **params): with db_api.transaction(): - action = action_handler.build_action_by_name(action_name) + action = action_handler.build_action_by_name(action_name, + namespace=namespace) action.validate_input(action_input) @@ -102,7 +104,6 @@ class DefaultEngine(base.Engine): if not sync and (save or not is_action_sync): action.schedule(action_input, target, timeout=timeout) - return action.action_ex.get_clone() output = action.run( @@ -111,7 +112,6 @@ class DefaultEngine(base.Engine): save=False, timeout=timeout ) - state = states.SUCCESS if output.is_success() else states.ERROR if not save: @@ -122,9 +122,9 @@ class DefaultEngine(base.Engine): description=description, input=action_input, output=output.to_dict(), - state=state + state=state, + workflow_namespace=namespace ) - action_ex_id = u.generate_unicode_uuid() values = { @@ -134,7 +134,8 @@ class DefaultEngine(base.Engine): 'input': action_input, 'output': output.to_dict(), 'state': state, - 'is_sync': is_action_sync + 'is_sync': is_action_sync, + 'workflow_namespace': namespace } return db_api.create_action_execution(values) @@ -147,7 +148,6 @@ class DefaultEngine(base.Engine): with db_api.transaction(): if wf_action: action_ex = db_api.get_workflow_execution(action_ex_id) - # If result is None it means that it's a normal subworkflow # output and we just need to fetch it from the model. # This is just an optimization to not send data over RPC @@ -170,7 +170,6 @@ class DefaultEngine(base.Engine): action_ex = db_api.get_workflow_execution(action_ex_id) else: action_ex = db_api.get_action_execution(action_ex_id) - action_handler.on_action_update(action_ex, state) return action_ex.get_clone() diff --git a/mistral/engine/engine_server.py b/mistral/engine/engine_server.py index 5e7472e91..2be3390a7 100644 --- a/mistral/engine/engine_server.py +++ b/mistral/engine/engine_server.py @@ -157,22 +157,24 @@ class EngineServer(service_base.MistralService): ) def start_action(self, rpc_ctx, action_name, - action_input, description, params): + action_input, description, namespace, params): """Receives calls over RPC to start actions on engine. :param rpc_ctx: RPC request context. :param action_name: name of the Action. :param action_input: input dictionary for Action. :param description: description of new Action execution. + :param namespace: The namespace of the action. :param params: extra parameters to run Action. :return: Action execution. """ LOG.info( "Received RPC request 'start_action'[name=%s, input=%s, " - "description=%s, params=%s]", + "description=%s, namespace=%s params=%s]", action_name, utils.cut(action_input), description, + namespace, params ) @@ -180,6 +182,7 @@ class EngineServer(service_base.MistralService): action_name, action_input, description, + namespace=namespace, **params ) @@ -198,7 +201,6 @@ class EngineServer(service_base.MistralService): action_ex_id, result.cut_repr() if result else '' ) - return self.engine.on_action_complete(action_ex_id, result, wf_action) def on_action_update(self, rpc_ctx, action_ex_id, state, wf_action): diff --git a/mistral/engine/tasks.py b/mistral/engine/tasks.py index 518c7abec..2f89955c1 100644 --- a/mistral/engine/tasks.py +++ b/mistral/engine/tasks.py @@ -691,7 +691,8 @@ class RegularTask(Task): action_def = actions.resolve_action_definition( action_name, self.wf_ex.name, - self.wf_spec.get_name() + self.wf_spec.get_name(), + namespace=self.wf_ex.workflow_namespace ) if action_def.spec: diff --git a/mistral/executors/default_executor.py b/mistral/executors/default_executor.py index 2aa06f04b..7813c1d6c 100644 --- a/mistral/executors/default_executor.py +++ b/mistral/executors/default_executor.py @@ -88,7 +88,6 @@ class DefaultExecutor(base.Executor): return None return error_result - if redelivered and not safe_rerun: msg = ( "Request to run action %s was redelivered, but action %s " diff --git a/mistral/rpc/clients.py b/mistral/rpc/clients.py index c9674e2f1..345bf8d0c 100644 --- a/mistral/rpc/clients.py +++ b/mistral/rpc/clients.py @@ -2,6 +2,7 @@ # Copyright 2015 - StackStorm, Inc. # Copyright 2017 - Brocade Communications Systems, Inc. # Copyright 2018 - Extreme Networks, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -150,12 +151,13 @@ class EngineClient(eng.Engine): @base.wrap_messaging_exception def start_action(self, action_name, action_input, - description=None, **params): + description=None, namespace='', **params): """Starts action sending a request to engine over RPC. :param action_name: Action name. :param action_input: Action input data as a dictionary. :param description: Execution description. + :param namespace: The namespace of the action. :param params: Additional options for action running. :return: Action execution. """ @@ -165,6 +167,7 @@ class EngineClient(eng.Engine): action_name=action_name, action_input=action_input or {}, description=description, + namespace=namespace, params=params ) @@ -372,7 +375,6 @@ class ExecutorClient(exe.Executor): action will be interrupted :return: Action result. """ - rpc_kwargs = { 'action_ex_id': action_ex_id, 'action_cls_str': action_cls_str, diff --git a/mistral/services/action_manager.py b/mistral/services/action_manager.py index 35c8d62e9..b8b99bd6f 100644 --- a/mistral/services/action_manager.py +++ b/mistral/services/action_manager.py @@ -1,5 +1,6 @@ # Copyright 2014 - Mirantis, Inc. # Copyright 2014 - StackStorm, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -51,7 +52,7 @@ def get_registered_actions(**kwargs): def register_action_class(name, action_class_str, attributes, - description=None, input_str=None): + description=None, input_str=None, namespace=''): values = { 'name': name, 'action_class': action_class_str, @@ -59,7 +60,8 @@ def register_action_class(name, action_class_str, attributes, 'description': description, 'input': input_str, 'is_system': True, - 'scope': 'public' + 'scope': 'public', + 'namespace': namespace } try: @@ -81,7 +83,7 @@ def sync_db(): register_standard_actions() -def _register_dynamic_action_classes(): +def _register_dynamic_action_classes(namespace=''): for generator in generator_factory.all_generators(): actions = generator.create_actions() @@ -98,11 +100,12 @@ def _register_dynamic_action_classes(): action_class_str, attrs, action['description'], - action['arg_list'] + action['arg_list'], + namespace=namespace ) -def register_action_classes(): +def register_action_classes(namespace=''): mgr = extension.ExtensionManager( namespace='mistral.actions', invoke_on_load=False @@ -120,23 +123,24 @@ def register_action_classes(): action_class_str, attrs, description=description, - input_str=input_str + input_str=input_str, + namespace=namespace ) - _register_dynamic_action_classes() + _register_dynamic_action_classes(namespace=namespace) -def get_action_db(action_name): - return db_api.load_action_definition(action_name) +def get_action_db(action_name, namespace=''): + return db_api.load_action_definition(action_name, namespace=namespace) -def get_action_class(action_full_name): +def get_action_class(action_full_name, namespace=''): """Finds action class by full action name (i.e. 'namespace.action_name'). :param action_full_name: Full action name (that includes namespace). :return: Action class or None if not found. """ - action_db = get_action_db(action_full_name) + action_db = get_action_db(action_full_name, namespace) if action_db: return action_factory.construct_action_class( diff --git a/mistral/services/actions.py b/mistral/services/actions.py index 118f1bb31..7d4492abe 100644 --- a/mistral/services/actions.py +++ b/mistral/services/actions.py @@ -1,4 +1,5 @@ # Copyright 2015 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,18 +20,22 @@ from mistral import exceptions as exc from mistral.lang import parser as spec_parser -def create_actions(definition, scope='private'): +def create_actions(definition, scope='private', namespace=''): action_list_spec = spec_parser.get_action_list_spec_from_yaml(definition) db_actions = [] for action_spec in action_list_spec.get_actions(): - db_actions.append(create_action(action_spec, definition, scope)) + db_actions.append(create_action( + action_spec, + definition, + scope, + namespace)) return db_actions -def update_actions(definition, scope='private', identifier=None): +def update_actions(definition, scope='private', identifier=None, namespace=''): action_list_spec = spec_parser.get_action_list_spec_from_yaml(definition) actions = action_list_spec.get_actions() @@ -48,32 +53,35 @@ def update_actions(definition, scope='private', identifier=None): action_spec, definition, scope, - identifier=identifier + identifier=identifier, + namespace=namespace + )) return db_actions -def create_or_update_actions(definition, scope='private'): +def create_or_update_actions(definition, scope='private', namespace=''): action_list_spec = spec_parser.get_action_list_spec_from_yaml(definition) db_actions = [] for action_spec in action_list_spec.get_actions(): db_actions.append( - create_or_update_action(action_spec, definition, scope) + create_or_update_action(action_spec, definition, scope, namespace) ) return db_actions -def create_action(action_spec, definition, scope): +def create_action(action_spec, definition, scope, namespace): return db_api.create_action_definition( - _get_action_values(action_spec, definition, scope) + _get_action_values(action_spec, definition, scope, namespace) ) -def update_action(action_spec, definition, scope, identifier=None): +def update_action(action_spec, definition, scope, identifier=None, + namespace=''): action = db_api.load_action_definition(action_spec.get_name()) if action and action.is_system: @@ -82,7 +90,7 @@ def update_action(action_spec, definition, scope, identifier=None): action.name ) - values = _get_action_values(action_spec, definition, scope) + values = _get_action_values(action_spec, definition, scope, namespace) return db_api.update_action_definition( identifier if identifier else values['name'], @@ -90,7 +98,7 @@ def update_action(action_spec, definition, scope, identifier=None): ) -def create_or_update_action(action_spec, definition, scope): +def create_or_update_action(action_spec, definition, scope, namespace): action = db_api.load_action_definition(action_spec.get_name()) if action and action.is_system: @@ -99,7 +107,7 @@ def create_or_update_action(action_spec, definition, scope): action.name ) - values = _get_action_values(action_spec, definition, scope) + values = _get_action_values(action_spec, definition, scope, namespace) return db_api.create_or_update_action_definition(values['name'], values) @@ -117,7 +125,7 @@ def get_input_list(action_input): return input_list -def _get_action_values(action_spec, definition, scope): +def _get_action_values(action_spec, definition, scope, namespace=''): action_input = action_spec.to_dict().get('input', []) input_list = get_input_list(action_input) @@ -129,7 +137,8 @@ def _get_action_values(action_spec, definition, scope): 'spec': action_spec.to_dict(), 'is_system': False, 'input': ", ".join(input_list) if input_list else None, - 'scope': scope + 'scope': scope, + 'namespace': namespace } return values diff --git a/mistral/services/workbooks.py b/mistral/services/workbooks.py index 34b7ba904..df93750eb 100644 --- a/mistral/services/workbooks.py +++ b/mistral/services/workbooks.py @@ -1,4 +1,5 @@ # Copyright 2015 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,9 +58,9 @@ def update_workbook_v2(definition, namespace='', scope='private', return wb_db -def _on_workbook_update(wb_db, wb_spec, namespace): - # TODO(hardikj) Handle actions for namespace - db_actions = _create_or_update_actions(wb_db, wb_spec.get_actions()) +def _on_workbook_update(wb_db, wb_spec, namespace=''): + db_actions = _create_or_update_actions(wb_db, wb_spec.get_actions(), + namespace=namespace) db_wfs = _create_or_update_workflows( wb_db, wb_spec.get_workflows(), @@ -69,7 +70,7 @@ def _on_workbook_update(wb_db, wb_spec, namespace): return db_actions, db_wfs -def _create_or_update_actions(wb_db, actions_spec): +def _create_or_update_actions(wb_db, actions_spec, namespace): db_actions = [] if actions_spec: @@ -88,7 +89,8 @@ def _create_or_update_actions(wb_db, actions_spec): 'is_system': False, 'input': ', '.join(input_list) if input_list else None, 'scope': wb_db.scope, - 'project_id': wb_db.project_id + 'project_id': wb_db.project_id, + 'namespace': namespace } db_actions.append( diff --git a/mistral/tests/unit/api/v2/test_action_executions.py b/mistral/tests/unit/api/v2/test_action_executions.py index 2dad135b8..8a729e5f5 100644 --- a/mistral/tests/unit/api/v2/test_action_executions.py +++ b/mistral/tests/unit/api/v2/test_action_executions.py @@ -304,7 +304,8 @@ class TestActionExecutionsController(base.APITest): json.loads(action_exec['input']), description=None, save_result=True, - run_sync=True + run_sync=True, + namespace='' ) @mock.patch.object(rpc_clients.EngineClient, 'start_action') @@ -331,7 +332,8 @@ class TestActionExecutionsController(base.APITest): action_exec['name'], json.loads(action_exec['input']), description=None, - timeout=2 + timeout=2, + namespace='' ) @mock.patch.object(rpc_clients.EngineClient, 'start_action') @@ -358,7 +360,8 @@ class TestActionExecutionsController(base.APITest): action_exec['name'], json.loads(action_exec['input']), description=None, - save_result=True + save_result=True, + namespace='' ) @mock.patch.object(rpc_clients.EngineClient, 'start_action') @@ -374,7 +377,9 @@ class TestActionExecutionsController(base.APITest): self.assertEqual(201, resp.status_int) self.assertEqual('{"result": "123"}', resp.json['output']) - f.assert_called_once_with('nova.servers_list', {}, description=None) + f.assert_called_once_with('nova.servers_list', {}, + description=None, + namespace='') def test_post_bad_result(self): resp = self.app.post_json( diff --git a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py index 50f60c33b..c9df95047 100644 --- a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py +++ b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py @@ -1,5 +1,6 @@ # Copyright 2015 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -1085,7 +1086,8 @@ ACTION_DEFINITIONS = [ 'action_class': 'mypackage.my_module.Action1', 'attributes': None, 'project_id': '', - 'created_at': datetime.datetime(2016, 12, 1, 15, 0, 0) + 'created_at': datetime.datetime(2016, 12, 1, 15, 0, 0), + 'namespace': '' }, { 'name': 'action2', @@ -1094,7 +1096,8 @@ ACTION_DEFINITIONS = [ 'action_class': 'mypackage.my_module.Action2', 'attributes': None, 'project_id': '', - 'created_at': datetime.datetime(2016, 12, 1, 15, 1, 0) + 'created_at': datetime.datetime(2016, 12, 1, 15, 1, 0), + 'namespace': '' }, { 'name': 'action3', @@ -1104,7 +1107,8 @@ ACTION_DEFINITIONS = [ 'action_class': 'mypackage.my_module.Action3', 'attributes': None, 'project_id': '', - 'created_at': datetime.datetime(2016, 12, 1, 15, 2, 0) + 'created_at': datetime.datetime(2016, 12, 1, 15, 2, 0), + 'namespace': '' }, ] @@ -1154,8 +1158,8 @@ class ActionDefinitionTest(SQLAlchemyTest): self.assertRaisesWithMessage( exc.DBDuplicateEntryError, - "Duplicate entry for Action ['name', 'project_id']: action1" - ", ", + "Duplicate entry for Action ['name', 'namespace', 'project_id']:" + " action1, , ", db_api.create_action_definition, ACTION_DEFINITIONS[0] ) diff --git a/mistral/tests/unit/engine/test_action_caching.py b/mistral/tests/unit/engine/test_action_caching.py index 86c639f6b..8a1374b04 100644 --- a/mistral/tests/unit/engine/test_action_caching.py +++ b/mistral/tests/unit/engine/test_action_caching.py @@ -28,9 +28,7 @@ cfg.CONF.set_default('auth_enable', False, group='pecan') class LookupUtilsTest(base.EngineTestCase): - - def test_action_definition_cache_ttl(self): - action = """--- + ACTION = """--- version: '2.0' action1: @@ -39,7 +37,7 @@ class LookupUtilsTest(base.EngineTestCase): result: $ """ - wf_text = """--- + WF_TEXT = """--- version: '2.0' wf: @@ -61,16 +59,23 @@ class LookupUtilsTest(base.EngineTestCase): pause-before: true """ - wf_service.create_workflows(wf_text) + def test_action_definition_cache_ttl(self): + namespace = 'test_namespace' + wf_service.create_workflows(self.WF_TEXT, namespace=namespace) # Create an action. - db_actions = action_service.create_actions(action) + db_actions = action_service.create_actions(self.ACTION, + namespace=namespace) self.assertEqual(1, len(db_actions)) - self._assert_single_item(db_actions, name='action1') + self._assert_single_item(db_actions, + name='action1', + namespace=namespace) # Explicitly mark the action to be deleted after the test execution. - self.addCleanup(db_api.delete_action_definitions, name='action1') + self.addCleanup(db_api.delete_action_definitions, + name='action1', + namespace=namespace) # Reinitialise the cache with reduced action_definition_cache_time # to make sure the test environment is under control. @@ -84,13 +89,15 @@ class LookupUtilsTest(base.EngineTestCase): self.addCleanup(cache_patch.stop) # Start workflow. - wf_ex = self.engine.start_workflow('wf') + wf_ex = self.engine.start_workflow('wf', wf_namespace=namespace) self.await_workflow_paused(wf_ex.id) # Check that 'action1' 'echo' and 'noop' are cached. - self.assertEqual(3, len(db_api._ACTION_DEF_CACHE)) - self.assertIn('action1', db_api._ACTION_DEF_CACHE) + self.assertEqual(5, len(db_api._ACTION_DEF_CACHE)) + self.assertIn('action1:test_namespace', db_api._ACTION_DEF_CACHE) + self.assertIn('std.noop:test_namespace', db_api._ACTION_DEF_CACHE) + self.assertIn('std.echo:test_namespace', db_api._ACTION_DEF_CACHE) self.assertIn('std.noop', db_api._ACTION_DEF_CACHE) self.assertIn('std.echo', db_api._ACTION_DEF_CACHE) @@ -110,6 +117,7 @@ class LookupUtilsTest(base.EngineTestCase): self.await_workflow_success(wf_ex.id) # Check all actions are cached again. - self.assertEqual(2, len(db_api._ACTION_DEF_CACHE)) - self.assertIn('action1', db_api._ACTION_DEF_CACHE) + self.assertEqual(3, len(db_api._ACTION_DEF_CACHE)) + self.assertIn('action1:test_namespace', db_api._ACTION_DEF_CACHE) self.assertIn('std.echo', db_api._ACTION_DEF_CACHE) + self.assertIn('std.echo:test_namespace', db_api._ACTION_DEF_CACHE) diff --git a/mistral/tests/unit/engine/test_adhoc_actions.py b/mistral/tests/unit/engine/test_adhoc_actions.py index c962bc812..b497151a1 100644 --- a/mistral/tests/unit/engine/test_adhoc_actions.py +++ b/mistral/tests/unit/engine/test_adhoc_actions.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +16,7 @@ from oslo_config import cfg from mistral.db.v2 import api as db_api +from mistral import exceptions as exc from mistral.services import workbooks as wb_service from mistral.tests.unit.engine import base from mistral.workflow import states @@ -24,7 +26,6 @@ from mistral.workflow import states # the change in value is not permanent. cfg.CONF.set_default('auth_enable', False, group='pecan') - WORKBOOK = """ --- version: '2.0' @@ -305,6 +306,78 @@ class AdhocActionsTest(base.EngineTestCase): self.await_workflow_running(wf_ex.id) + def test_adhoc_action_difinition_with_namespace(self): + namespace = 'ad-hoc_test' + namespace2 = 'ad-hoc_test2' + wb_text = """--- + + version: '2.0' + + name: my_wb_namespace + + actions: + test_env: + base: std.echo + base-input: + output: '{{ env().foo }}' + + workflows: + wf_namespace: + type: direct + input: + - str1 + output: + workflow_result: '{{ _.printenv_result }}' + + tasks: + printenv: + action: test_env + publish: + printenv_result: '{{ task().result }}' + """ + + wb_service.create_workbook_v2(wb_text, namespace=namespace) + wb_service.create_workbook_v2(wb_text, namespace=namespace2) + + with db_api.transaction(): + action_def = db_api.get_action_definitions( + name='my_wb_namespace.test_env', ) + + self.assertEqual(2, len(action_def)) + + action_def = db_api.get_action_definitions( + name='my_wb_namespace.test_env', + namespace=namespace) + + self.assertEqual(1, len(action_def)) + + self.assertRaises(exc.DBEntityNotFoundError, + db_api.get_action_definition, + name='my_wb_namespace.test_env') + + def test_adhoc_action_execution_with_namespace(self): + namespace = 'ad-hoc_test' + + wb_service.create_workbook_v2(WORKBOOK, namespace=namespace) + wf_ex = self.engine.start_workflow( + 'my_wb.wf4', + wf_input={'str1': 'a'}, + env={'foo': 'bar'}, + wf_namespace=namespace + ) + + self.await_workflow_success(wf_ex.id) + + with db_api.transaction(): + action_execs = db_api.get_action_executions( + name='std.echo', + workflow_namespace=namespace) + self.assertEqual(1, len(action_execs)) + context = action_execs[0].runtime_context + self.assertEqual('my_wb.test_env', + context.get('adhoc_action_name')) + self.assertEqual(namespace, action_execs[0].workflow_namespace) + def test_adhoc_action_runtime_context_name(self): wf_ex = self.engine.start_workflow( 'my_wb.wf4', diff --git a/mistral/tests/unit/engine/test_run_action.py b/mistral/tests/unit/engine/test_run_action.py index 2ee7d3209..a16d19e6d 100644 --- a/mistral/tests/unit/engine/test_run_action.py +++ b/mistral/tests/unit/engine/test_run_action.py @@ -1,4 +1,5 @@ # Copyright 2015 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -100,6 +101,81 @@ class RunActionEngineTest(base.EngineTestCase): self.assertEqual('Hello!', action_ex.output['result']) self.assertEqual(states.SUCCESS, action_ex.state) + def test_run_action_with_namespace(self): + namespace = 'test_namespace' + action = """--- + version: '2.0' + + concat_namespace: + base: std.echo + base-input: + output: <% $.left %><% $.right %> + input: + - left + - right + + concat_namespace2: + base: concat_namespace + base-input: + left: <% $.left %><% $.center %> + right: <% $.right %> + input: + - left + - center + - right + """ + actions.create_actions(action, namespace=namespace) + + self.assertRaises(exc.InvalidActionException, + self.engine.start_action, + 'concat_namespace', + {'left': 'Hello, ', 'right': 'John Doe!'}, + save_result=True, + namespace='') + + action_ex = self.engine.start_action( + 'concat_namespace', + {'left': 'Hello, ', 'right': 'John Doe!'}, + save_result=True, + namespace=namespace + ) + + self.assertEqual(namespace, action_ex.workflow_namespace) + + self.await_action_success(action_ex.id) + + with db_api.transaction(): + action_ex = db_api.get_action_execution(action_ex.id) + self.assertEqual(states.SUCCESS, action_ex.state) + self.assertEqual({'result': u'Hello, John Doe!'}, action_ex.output) + + action_ex = self.engine.start_action( + 'concat_namespace2', + {'left': 'Hello, ', 'center': 'John', 'right': ' Doe!'}, + save_result=True, + namespace=namespace + ) + self.assertEqual(namespace, action_ex.workflow_namespace) + self.await_action_success(action_ex.id) + + with db_api.transaction(): + action_ex = db_api.get_action_execution(action_ex.id) + self.assertEqual(states.SUCCESS, action_ex.state) + self.assertEqual('Hello, John Doe!', action_ex.output['result']) + + def test_run_action_with_invalid_namespace(self): + # This test checks the case in which, the action with that name is + # not found with the given name, if an action was found with the + # same name in default namespace, that action will run. + + action_ex = self.engine.start_action( + 'concat', + {'left': 'Hello, ', 'right': 'John Doe!'}, + save_result=True, + namespace='namespace' + ) + self.assertIsNotNone(action_ex) + @mock.patch.object( std_actions.EchoAction, 'run', @@ -325,7 +401,7 @@ class RunActionEngineTest(base.EngineTestCase): self.engine.start_action('fake_action', {'input': 'Hello'}) self.assertEqual(1, def_mock.call_count) - def_mock.assert_called_with('fake_action') + def_mock.assert_called_with('fake_action', namespace='') self.assertEqual(0, validate_mock.call_count) diff --git a/mistral/tests/unit/services/test_action_service.py b/mistral/tests/unit/services/test_action_service.py index c7c8e289b..3b0a90000 100644 --- a/mistral/tests/unit/services/test_action_service.py +++ b/mistral/tests/unit/services/test_action_service.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,17 +16,16 @@ from oslo_config import cfg from mistral.db.v2 import api as db_api +from mistral.exceptions import DBEntityNotFoundError from mistral.lang import parser as spec_parser from mistral.services import actions as action_service from mistral.tests.unit import base from mistral_lib import utils - # Use the set_default method to set value otherwise in certain test cases # the change in value is not permanent. cfg.CONF.set_default('auth_enable', False, group='pecan') - ACTION_LIST = """ --- version: '2.0' @@ -54,6 +54,8 @@ action1: result: $ """ +NAMESPACE = 'test_namespace' + class ActionServiceTest(base.DbTestCase): def setUp(self): @@ -78,14 +80,35 @@ class ActionServiceTest(base.DbTestCase): # Action 2. action2_db = self._assert_single_item(db_actions, name='action2') + self.assertEqual('', action2_db.namespace) action2_spec = spec_parser.get_action_spec(action2_db.spec) self.assertEqual('action2', action2_spec.get_name()) self.assertEqual('std.echo', action1_spec.get_base()) self.assertDictEqual({'output': 'Hey'}, action2_spec.get_base_input()) + def test_create_actions_in_namespace(self): + db_actions = action_service.create_actions(ACTION_LIST, + namespace=NAMESPACE) + + self.assertEqual(2, len(db_actions)) + + action1_db = self._assert_single_item(db_actions, name='action1') + self.assertEqual(NAMESPACE, action1_db.namespace) + + action2_db = self._assert_single_item(db_actions, name='action2') + self.assertEqual(NAMESPACE, action2_db.namespace) + + self.assertRaises( + DBEntityNotFoundError, + db_api.get_action_definition, + name='action1', + namespace='' + ) + def test_update_actions(self): - db_actions = action_service.create_actions(ACTION_LIST) + db_actions = action_service.create_actions(ACTION_LIST, + namespace=NAMESPACE) self.assertEqual(2, len(db_actions)) @@ -97,7 +120,8 @@ class ActionServiceTest(base.DbTestCase): self.assertDictEqual({'output': 'Hi'}, action1_spec.get_base_input()) self.assertDictEqual({}, action1_spec.get_input()) - db_actions = action_service.update_actions(UPDATED_ACTION_LIST) + db_actions = action_service.update_actions(UPDATED_ACTION_LIST, + namespace=NAMESPACE) # Action 1. action1_db = self._assert_single_item(db_actions, name='action1') @@ -112,3 +136,29 @@ class ActionServiceTest(base.DbTestCase): action1_spec.get_input().get('param1'), utils.NotDefined ) + + self.assertRaises( + DBEntityNotFoundError, + action_service.update_actions, + UPDATED_ACTION_LIST, + namespace='' + ) + + def test_delete_action(self): + + # Create action. + action_service.create_actions(ACTION_LIST, namespace=NAMESPACE) + + action = db_api.get_action_definition('action1', namespace=NAMESPACE) + self.assertEqual(NAMESPACE, action.get('namespace')) + self.assertEqual('action1', action.get('name')) + + # Delete action. + db_api.delete_action_definition('action1', namespace=NAMESPACE) + + self.assertRaises( + DBEntityNotFoundError, + db_api.get_action_definition, + name='action1', + namespace=NAMESPACE + ) diff --git a/mistral/tests/unit/services/test_workbook_service.py b/mistral/tests/unit/services/test_workbook_service.py index 25abce86c..24fa56c76 100644 --- a/mistral/tests/unit/services/test_workbook_service.py +++ b/mistral/tests/unit/services/test_workbook_service.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -181,7 +182,10 @@ class WorkbookServiceTest(base.DbTestCase): self.assertIsNotNone(wb_db.spec) self.assertListEqual(['test'], wb_db.tags) - db_actions = db_api.get_action_definitions(name='my_wb.concat') + db_actions = db_api.get_action_definitions( + name='my_wb.concat', + namespace=namespace + ) self.assertEqual(1, len(db_actions)) @@ -279,7 +283,8 @@ class WorkbookServiceTest(base.DbTestCase): wb_service.create_workbook_v2(WORKBOOK, namespace=namespace) db_wfs = db_api.get_workflow_definitions() - db_actions = db_api.get_action_definitions(name='my_wb.concat') + db_actions = db_api.get_action_definitions(name='my_wb.concat', + namespace=namespace) self.assertEqual(2, len(db_wfs)) self.assertEqual(1, len(db_actions)) @@ -287,8 +292,8 @@ class WorkbookServiceTest(base.DbTestCase): db_api.delete_workbook('my_wb', namespace=namespace) db_wfs = db_api.get_workflow_definitions() - db_actions = db_api.get_action_definitions(name='my_wb.concat') - + db_actions = db_api.get_action_definitions(name='my_wb.concat', + namespace=namespace) # Deleting workbook shouldn't delete workflows and actions self.assertEqual(2, len(db_wfs)) self.assertEqual(1, len(db_actions)) diff --git a/releasenotes/notes/namespace_for_adhoc_actions.yaml b/releasenotes/notes/namespace_for_adhoc_actions.yaml new file mode 100644 index 000000000..d190b0df3 --- /dev/null +++ b/releasenotes/notes/namespace_for_adhoc_actions.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Add support for creating ad-hoc actions in a namespace. Creating actions + with same name is now possible inside the same project now. This feature + is backward compatible. + + All existing actions are assumed to be in the default namespace, + represented by an empty string. Also, if an action is created without a + namespace specified, it is assumed to be in the default namespace. + + If an ad-hoc action is created inside a workbook, then the namespace of the workbook + would be also it's namespace. +