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 <ali.abdelal@nokia.com>
This commit is contained in:
ali 2020-01-02 13:35:59 +00:00
parent 6e9a70db25
commit 20c3408692
26 changed files with 538 additions and 155 deletions

View File

@ -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, "

View File

@ -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
)

View File

@ -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=''
)

View File

@ -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()

View File

@ -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,

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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.
"""

View File

@ -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()

View File

@ -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 '<unknown>'
)
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):

View File

@ -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:

View File

@ -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 "

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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(

View File

@ -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': '<default-project>',
'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': '<default-project>',
'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': '<default-project>',
'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"
", <default-project>",
"Duplicate entry for Action ['name', 'namespace', 'project_id']:"
" action1, , <default-project>",
db_api.create_action_definition,
ACTION_DEFINITIONS[0]
)

View File

@ -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)

View File

@ -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',

View File

@ -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)

View File

@ -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
)

View File

@ -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))

View File

@ -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.