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 2014 - Mirantis, Inc.
# Copyright 2015 Huawei Technologies Co., Ltd. # Copyright 2015 Huawei Technologies Co., Ltd.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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) spec_parser.get_action_list_spec_from_yaml)
@rest_utils.wrap_wsme_controller_exception @rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(resources.Action, wtypes.text) @wsme_pecan.wsexpose(resources.Action, wtypes.text, wtypes.text)
def get(self, identifier): def get(self, identifier, namespace=''):
"""Return the named action. """Return the named action.
:param identifier: ID or name of the Action to get. :param identifier: ID or name of the Action to get.
:param namespace: The namespace of the action.
""" """
acl.enforce('actions:get', context.ctx()) acl.enforce('actions:get', context.ctx())
@ -60,17 +62,19 @@ class ActionsController(rest.RestController, hooks.HookController):
# Use retries to prevent possible failures. # Use retries to prevent possible failures.
db_model = rest_utils.rest_retry_on_db_error( db_model = rest_utils.rest_retry_on_db_error(
db_api.get_action_definition db_api.get_action_definition
)(identifier) )(identifier, namespace=namespace)
return resources.Action.from_db_model(db_model) return resources.Action.from_db_model(db_model)
@rest_utils.wrap_pecan_controller_exception @rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain") @pecan.expose(content_type="text/plain")
def put(self, identifier=None): def put(self, identifier=None, namespace=''):
"""Update one or more actions. """Update one or more actions.
:param identifier: Optional. If provided, it's UUID or name of an :param identifier: Optional. If provided, it's UUID or name of an
action. Only one action can be updated with identifier param. 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 NOTE: This text is allowed to have definitions
of multiple actions. In this case they all will be updated. 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) LOG.debug("Update action(s) [definition=%s]", definition)
namespace = namespace or ''
scope = pecan.request.GET.get('scope', 'private') scope = pecan.request.GET.get('scope', 'private')
resources.Action.validate_scope(scope) resources.Action.validate_scope(scope)
if scope == 'public': if scope == 'public':
@ -92,7 +97,8 @@ class ActionsController(rest.RestController, hooks.HookController):
return actions.update_actions( return actions.update_actions(
definition, definition,
scope=scope, scope=scope,
identifier=identifier identifier=identifier,
namespace=namespace
) )
db_acts = _update_actions() db_acts = _update_actions()
@ -105,13 +111,19 @@ class ActionsController(rest.RestController, hooks.HookController):
@rest_utils.wrap_pecan_controller_exception @rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain") @pecan.expose(content_type="text/plain")
def post(self): def post(self, namespace=''):
"""Create a new action. """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 NOTE: This text is allowed to have definitions
of multiple actions. In this case they all will be created. of multiple actions. In this case they all will be created.
""" """
acl.enforce('actions:create', context.ctx()) acl.enforce('actions:create', context.ctx())
namespace = namespace or ''
definition = pecan.request.text definition = pecan.request.text
scope = pecan.request.GET.get('scope', 'private') scope = pecan.request.GET.get('scope', 'private')
@ -126,7 +138,9 @@ class ActionsController(rest.RestController, hooks.HookController):
@rest_utils.rest_retry_on_db_error @rest_utils.rest_retry_on_db_error
def _create_action_definitions(): def _create_action_definitions():
with db_api.transaction(): 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() db_acts = _create_action_definitions()
@ -137,26 +151,27 @@ class ActionsController(rest.RestController, hooks.HookController):
return resources.Actions(actions=action_list).to_json() return resources.Actions(actions=action_list).to_json()
@rest_utils.wrap_wsme_controller_exception @rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204) @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204)
def delete(self, identifier): def delete(self, identifier, namespace=''):
"""Delete the named action. """Delete the named action.
:param identifier: Name or UUID of the action to delete. :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()) acl.enforce('actions:delete', context.ctx())
LOG.debug("Delete action [identifier=%s]", identifier) LOG.debug("Delete action [identifier=%s]", identifier)
@rest_utils.rest_retry_on_db_error @rest_utils.rest_retry_on_db_error
def _delete_action_definition(): def _delete_action_definition():
with db_api.transaction(): 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: if db_model.is_system:
msg = "Attempt to delete a system action: %s" % identifier msg = "Attempt to delete a system action: %s" % identifier
raise exc.DataAccessException(msg) raise exc.DataAccessException(msg)
db_api.delete_action_definition(identifier,
db_api.delete_action_definition(identifier) namespace=namespace)
_delete_action_definition() _delete_action_definition()
@ -164,12 +179,13 @@ class ActionsController(rest.RestController, hooks.HookController):
@wsme_pecan.wsexpose(resources.Actions, types.uuid, int, types.uniquelist, @wsme_pecan.wsexpose(resources.Actions, types.uuid, int, types.uniquelist,
types.list, types.uniquelist, wtypes.text, types.list, types.uniquelist, wtypes.text,
wtypes.text, resources.SCOPE_TYPES, 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', def get_all(self, marker=None, limit=None, sort_keys='name',
sort_dirs='asc', fields='', created_at=None, name=None, sort_dirs='asc', fields='', created_at=None,
scope=None, tags=None, updated_at=None, name=None, scope=None, tags=None,
description=None, definition=None, is_system=None, input=None): updated_at=None, description=None, definition=None,
is_system=None, input=None, namespace=''):
"""Return all actions. """Return all actions.
:param marker: Optional. Pagination marker for large data sets. :param marker: Optional. Pagination marker for large data sets.
@ -199,6 +215,7 @@ class ActionsController(rest.RestController, hooks.HookController):
time and date. time and date.
:param updated_at: Optional. Keep only resources with specific latest :param updated_at: Optional. Keep only resources with specific latest
update time and date. update time and date.
:param namespace: Optional. The namespace of the action.
""" """
acl.enforce('actions:list', context.ctx()) acl.enforce('actions:list', context.ctx())
@ -211,7 +228,8 @@ class ActionsController(rest.RestController, hooks.HookController):
description=description, description=description,
definition=definition, definition=definition,
is_system=is_system, is_system=is_system,
input=input input=input,
namespace=namespace
) )
LOG.debug("Fetch actions. marker=%s, limit=%s, sort_keys=%s, " LOG.debug("Fetch actions. marker=%s, limit=%s, sort_keys=%s, "

View File

@ -1,5 +1,6 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2016 - Brocade Communications Systems, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 :param action_ex: Action to execute
""" """
acl.enforce('action_executions:create', context.ctx()) acl.enforce('action_executions:create', context.ctx())
LOG.debug( LOG.debug(
"Create action_execution [action_execution=%s]", "Create action_execution [action_execution=%s]",
action_ex action_ex
) )
name = action_ex.name name = action_ex.name
description = action_ex.description or None description = action_ex.description or None
action_input = action_ex.input or {} action_input = action_ex.input or {}
params = action_ex.params or {} params = action_ex.params or {}
namespace = action_ex.workflow_namespace or ''
if not name: if not name:
raise exc.InputException( raise exc.InputException(
@ -164,6 +164,7 @@ class ActionExecutionsController(rest.RestController):
name, name,
action_input, action_input,
description=description, description=description,
namespace=namespace,
**params **params
) )

View File

@ -1,6 +1,7 @@
# Copyright 2013 - Mirantis, Inc. # Copyright 2013 - Mirantis, Inc.
# Copyright 2018 - Extreme Networks, Inc. # Copyright 2018 - Extreme Networks, Inc.
# Copyright 2019 - NetCracker Technology Corp. # Copyright 2019 - NetCracker Technology Corp.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 created_at = wtypes.text
updated_at = wtypes.text updated_at = wtypes.text
namespace = wtypes.text
@classmethod @classmethod
def sample(cls): def sample(cls):
@ -217,7 +219,8 @@ class Action(resource.Resource, ScopedResource):
scope='private', scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7', project_id='a7eb669e9819420ea4bd1453e672c0a7',
created_at='1970-01-01T00:00:00.000000', 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 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc. # Copyright 2015 - StackStorm, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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) return IMPL.get_action_definition_by_id(id, fields=fields)
def get_action_definition(name, fields=()): def get_action_definition(name, fields=(), namespace=''):
return IMPL.get_action_definition(name, fields=fields) 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.""" """Unlike get_action_definition this method is allowed to return None."""
key = '{}:{}'.format(name, namespace) if namespace else name
with _ACTION_DEF_CACHE_LOCK: with _ACTION_DEF_CACHE_LOCK:
action_def = _ACTION_DEF_CACHE.get(name) action_def = _ACTION_DEF_CACHE.get(key)
if action_def: if action_def:
return 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: with _ACTION_DEF_CACHE_LOCK:
_ACTION_DEF_CACHE[name] = ( _ACTION_DEF_CACHE[key] = (
action_def.get_clone() if action_def else None 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) return IMPL.create_or_update_action_definition(name, values)
def delete_action_definition(name): def delete_action_definition(name, namespace=''):
return IMPL.delete_action_definition(name) return IMPL.delete_action_definition(name, namespace=namespace)
def delete_action_definitions(**kwargs): def delete_action_definitions(**kwargs):
@ -539,7 +548,6 @@ def load_environment(name):
def get_environments(limit=None, marker=None, sort_keys=None, def get_environments(limit=None, marker=None, sort_keys=None,
sort_dirs=None, **kwargs): sort_dirs=None, **kwargs):
return IMPL.get_environments( return IMPL.get_environments(
limit=limit, limit=limit,
marker=marker, marker=marker,

View File

@ -1,6 +1,7 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc. # Copyright 2015 - StackStorm, Inc.
# Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2016 - Brocade Communications Systems, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -20,7 +21,6 @@ import re
import sys import sys
import threading import threading
from oslo_config import cfg from oslo_config import cfg
from oslo_db import exception as db_exc from oslo_db import exception as db_exc
from oslo_db import sqlalchemy as oslo_sqlalchemy from oslo_db import sqlalchemy as oslo_sqlalchemy
@ -41,11 +41,9 @@ from mistral.services import security
from mistral.workflow import states from mistral.workflow import states
from mistral_lib import utils from mistral_lib import utils
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
_SCHEMA_LOCK = threading.RLock() _SCHEMA_LOCK = threading.RLock()
_initialized = False _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, def _get_db_object_by_name_and_namespace(model, name,
namespace, insecure=False, namespace='', insecure=False,
columns=()): columns=()):
query = ( query = (
b.model_query(model, columns=columns) b.model_query(model, columns=columns)
@ -654,26 +652,38 @@ def get_action_definition_by_id(id, fields=(), session=None):
@b.session_aware() @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( a_def = _get_db_object_by_name_and_namespace_or_id(
models.ActionDefinition, models.ActionDefinition,
identifier, identifier,
namespace=namespace,
columns=fields 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: if not a_def:
raise exc.DBEntityNotFoundError( 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 return a_def
@b.session_aware() @b.session_aware()
def load_action_definition(name, fields=(), session=None): def load_action_definition(name, fields=(), session=None, namespace=''):
return _get_db_object_by_name( return _get_db_object_by_name_and_namespace(
models.ActionDefinition, models.ActionDefinition,
name, name,
namespace=namespace,
columns=fields columns=fields
) )
@ -693,8 +703,8 @@ def create_action_definition(values, session=None):
a_def.save(session=session) a_def.save(session=session)
except db_exc.DBDuplicateEntry: except db_exc.DBDuplicateEntry:
raise exc.DBDuplicateEntryError( raise exc.DBDuplicateEntryError(
"Duplicate entry for Action ['name', 'project_id']:" "Duplicate entry for Action ['name', 'namespace', 'project_id']:"
" {}, {}".format(a_def.name, a_def.project_id) " {}, {}, {}".format(a_def.name, a_def.namespace, a_def.project_id)
) )
return a_def return a_def
@ -702,7 +712,8 @@ def create_action_definition(values, session=None):
@b.session_aware() @b.session_aware()
def update_action_definition(identifier, values, session=None): 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()) a_def.update(values.copy())
@ -711,15 +722,19 @@ def update_action_definition(identifier, values, session=None):
@b.session_aware() @b.session_aware()
def create_or_update_action_definition(name, values, session=None): 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) return create_action_definition(values)
else: else:
return update_action_definition(name, values) return update_action_definition(name, values)
@b.session_aware() @b.session_aware()
def delete_action_definition(identifier, session=None): def delete_action_definition(identifier, namespace='', session=None):
a_def = get_action_definition(identifier) a_def = get_action_definition(identifier, namespace=namespace)
session.delete(a_def) session.delete(a_def)
@ -793,8 +808,8 @@ def update_action_execution_heartbeat(id, session=None):
raise exc.DBEntityNotFoundError raise exc.DBEntityNotFoundError
now = utils.utc_now_sec() now = utils.utc_now_sec()
session.query(models.ActionExecution).\ session.query(models.ActionExecution). \
filter(models.ActionExecution.id == id).\ filter(models.ActionExecution.id == id). \
update({'last_heartbeat': now}) update({'last_heartbeat': now})
@ -1758,8 +1773,8 @@ def update_resource_member(resource_id, res_type, member_id, values,
@b.session_aware() @b.session_aware()
def delete_resource_member(resource_id, res_type, member_id, session=None): def delete_resource_member(resource_id, res_type, member_id, session=None):
query = _secure_query(models.ResourceMember).\ query = _secure_query(models.ResourceMember). \
filter_by(resource_type=res_type).\ filter_by(resource_type=res_type). \
filter(_get_criterion(resource_id, member_id)) filter(_get_criterion(resource_id, member_id))
# TODO(kong): Check association with cron triggers when deleting a workflow # TODO(kong): Check association with cron triggers when deleting a workflow

View File

@ -1,5 +1,6 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc. # Copyright 2015 - StackStorm, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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.services import security
from mistral_lib import utils from mistral_lib import utils
# Definition objects. # Definition objects.
CONF = cfg.CONF CONF = cfg.CONF
@ -174,9 +174,12 @@ class ActionDefinition(Definition):
"""Contains info about registered Actions.""" """Contains info about registered Actions."""
__tablename__ = 'action_definitions_v2' __tablename__ = 'action_definitions_v2'
namespace = sa.Column(sa.String(255), nullable=True)
__table_args__ = ( __table_args__ = (
sa.UniqueConstraint('name', 'project_id'), sa.UniqueConstraint(
'name',
'namespace',
'project_id'),
sa.Index('%s_is_system' % __tablename__, 'is_system'), sa.Index('%s_is_system' % __tablename__, 'is_system'),
sa.Index('%s_action_class' % __tablename__, 'action_class'), sa.Index('%s_action_class' % __tablename__, 'action_class'),
sa.Index('%s_project_id' % __tablename__, 'project_id'), sa.Index('%s_project_id' % __tablename__, 'project_id'),
@ -346,7 +349,6 @@ for cls in utils.iter_subclasses(Execution):
retval=True retval=True
) )
# Many-to-one for 'ActionExecution' and 'TaskExecution'. # Many-to-one for 'ActionExecution' and 'TaskExecution'.
ActionExecution.task_execution_id = sa.Column( ActionExecution.task_execution_id = sa.Column(
@ -405,7 +407,6 @@ WorkflowExecution.root_execution = relationship(
lazy='select' lazy='select'
) )
# Many-to-one for 'TaskExecution' and 'WorkflowExecution'. # Many-to-one for 'TaskExecution' and 'WorkflowExecution'.
TaskExecution.workflow_execution_id = sa.Column( TaskExecution.workflow_execution_id = sa.Column(

View File

@ -1,5 +1,6 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2016 - Brocade Communications Systems, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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.engine import task_handler
from mistral import exceptions as exc from mistral import exceptions as exc
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -31,7 +31,6 @@ def on_action_complete(action_ex, result):
task_ex = action_ex.task_execution task_ex = action_ex.task_execution
action = _build_action(action_ex) action = _build_action(action_ex)
try: try:
action.complete(result) action.complete(result)
except exc.MistralException as e: 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') adhoc_action_name = action_ex.runtime_context.get('adhoc_action_name')
if 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) return actions.AdHocAction(action_def, action_ex=action_ex)
action_def = actions.resolve_action_definition(action_ex.name) 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) return actions.PythonAction(action_def, action_ex=action_ex)
def build_action_by_name(action_name): def build_action_by_name(action_name, namespace=''):
action_def = actions.resolve_action_definition(action_name) action_def = actions.resolve_action_definition(action_name,
namespace=namespace)
action_cls = (actions.PythonAction if not action_def.spec action_cls = (actions.PythonAction if not action_def.spec
else actions.AdHocAction) 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 actions as ml_actions
from mistral_lib import utils from mistral_lib import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
@ -152,7 +151,8 @@ class Action(object):
return True return True
def _create_action_execution(self, input_dict, runtime_ctx, is_sync, 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() action_ex_id = action_ex_id or utils.generate_unicode_uuid()
values = { values = {
@ -162,6 +162,7 @@ class Action(object):
'state': states.RUNNING, 'state': states.RUNNING,
'input': input_dict, 'input': input_dict,
'runtime_context': runtime_ctx, 'runtime_context': runtime_ctx,
'workflow_namespace': namespace,
'description': desc, 'description': desc,
'is_sync': is_sync 'is_sync': is_sync
} }
@ -245,7 +246,6 @@ class PythonAction(Action):
# to be updated with the action execution ID after the action execution # to be updated with the action execution ID after the action execution
# DB object is created. # DB object is created.
action_ex_id = utils.generate_unicode_uuid() action_ex_id = utils.generate_unicode_uuid()
self._create_action_execution( self._create_action_execution(
self._prepare_input(input_dict), self._prepare_input(input_dict),
self._prepare_runtime_context(index, safe_rerun), self._prepare_runtime_context(index, safe_rerun),
@ -253,7 +253,6 @@ class PythonAction(Action):
desc=desc, desc=desc,
action_ex_id=action_ex_id action_ex_id=action_ex_id
) )
execution_context = self._prepare_execution_context() execution_context = self._prepare_execution_context()
# Register an asynchronous command to send the action to # Register an asynchronous command to send the action to
@ -320,7 +319,8 @@ class PythonAction(Action):
try: try:
prepared_input_dict = self._prepare_input(input_dict) 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 **prepared_input_dict
) )
@ -356,9 +356,9 @@ class PythonAction(Action):
if self.action_ex: if self.action_ex:
exc_ctx['action_execution_id'] = self.action_ex.id exc_ctx['action_execution_id'] = self.action_ex.id
exc_ctx['callback_url'] = ( exc_ctx['callback_url'] = ('/v2/action_executions/%s'
'/v2/action_executions/%s' % self.action_ex.id % self.action_ex.id
) )
return exc_ctx return exc_ctx
@ -394,7 +394,8 @@ class AdHocAction(PythonAction):
self.action_spec = spec_parser.get_action_spec(action_def.spec) self.action_spec = spec_parser.get_action_spec(action_def.spec)
base_action_def = db_api.load_action_definition( 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: if not base_action_def:
@ -539,10 +540,12 @@ class AdHocAction(PythonAction):
base_name = base.spec['base'] base_name = base.spec['base']
try: try:
base = db_api.get_action_definition(base_name) base = db_api.get_action_definition(base_name,
namespace=base.namespace)
except exc.DBEntityNotFoundError: except exc.DBEntityNotFoundError:
raise exc.InvalidActionException( 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 # if the action is repeated
@ -554,6 +557,13 @@ class AdHocAction(PythonAction):
return base 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): class WorkflowAction(Action):
"""Workflow action.""" """Workflow action."""
@ -650,12 +660,13 @@ class WorkflowAction(Action):
def resolve_action_definition(action_spec_name, wf_name=None, 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. """Resolve action definition accounting for ad-hoc action namespacing.
:param action_spec_name: Action name according to a spec. :param action_spec_name: Action name according to a spec.
:param wf_name: Workflow name. :param wf_name: Workflow name.
:param wf_spec_name: Workflow name according to a spec. :param wf_spec_name: Workflow name according to a spec.
:param namespace: The namespace of the action.
:return: Action definition (python or ad-hoc). :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_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: 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: if not action_db:
raise exc.InvalidActionException( 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 return action_db

View File

@ -1,5 +1,6 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2017 - Brocade Communications Systems, Inc. # Copyright 2017 - Brocade Communications Systems, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -48,12 +49,13 @@ class Engine(object):
@abc.abstractmethod @abc.abstractmethod
def start_action(self, action_name, action_input, def start_action(self, action_name, action_input,
description=None, **params): description=None, namespace='', **params):
"""Starts the specific action. """Starts the specific action.
:param action_name: Action name. :param action_name: Action name.
:param action_input: Action input data as a dictionary. :param action_input: Action input data as a dictionary.
:param description: Execution description. :param description: Execution description.
:param namespace: The namespace of the action.
:param params: Additional options for action running. :param params: Additional options for action running.
:return: Action execution object. :return: Action execution object.
""" """

View File

@ -2,6 +2,7 @@
# Copyright 2015 - StackStorm, Inc. # Copyright 2015 - StackStorm, Inc.
# Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2016 - Brocade Communications Systems, Inc.
# Copyright 2018 - Extreme Networks, Inc. # Copyright 2018 - Extreme Networks, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 @db_utils.retry_on_db_error
@post_tx_queue.run @post_tx_queue.run
def start_action(self, action_name, action_input, def start_action(self, action_name, action_input,
description=None, **params): description=None, namespace='', **params):
with db_api.transaction(): 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) action.validate_input(action_input)
@ -102,7 +104,6 @@ class DefaultEngine(base.Engine):
if not sync and (save or not is_action_sync): if not sync and (save or not is_action_sync):
action.schedule(action_input, target, timeout=timeout) action.schedule(action_input, target, timeout=timeout)
return action.action_ex.get_clone() return action.action_ex.get_clone()
output = action.run( output = action.run(
@ -111,7 +112,6 @@ class DefaultEngine(base.Engine):
save=False, save=False,
timeout=timeout timeout=timeout
) )
state = states.SUCCESS if output.is_success() else states.ERROR state = states.SUCCESS if output.is_success() else states.ERROR
if not save: if not save:
@ -122,9 +122,9 @@ class DefaultEngine(base.Engine):
description=description, description=description,
input=action_input, input=action_input,
output=output.to_dict(), output=output.to_dict(),
state=state state=state,
workflow_namespace=namespace
) )
action_ex_id = u.generate_unicode_uuid() action_ex_id = u.generate_unicode_uuid()
values = { values = {
@ -134,7 +134,8 @@ class DefaultEngine(base.Engine):
'input': action_input, 'input': action_input,
'output': output.to_dict(), 'output': output.to_dict(),
'state': state, 'state': state,
'is_sync': is_action_sync 'is_sync': is_action_sync,
'workflow_namespace': namespace
} }
return db_api.create_action_execution(values) return db_api.create_action_execution(values)
@ -147,7 +148,6 @@ class DefaultEngine(base.Engine):
with db_api.transaction(): with db_api.transaction():
if wf_action: if wf_action:
action_ex = db_api.get_workflow_execution(action_ex_id) action_ex = db_api.get_workflow_execution(action_ex_id)
# If result is None it means that it's a normal subworkflow # If result is None it means that it's a normal subworkflow
# output and we just need to fetch it from the model. # output and we just need to fetch it from the model.
# This is just an optimization to not send data over RPC # 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) action_ex = db_api.get_workflow_execution(action_ex_id)
else: else:
action_ex = db_api.get_action_execution(action_ex_id) action_ex = db_api.get_action_execution(action_ex_id)
action_handler.on_action_update(action_ex, state) action_handler.on_action_update(action_ex, state)
return action_ex.get_clone() return action_ex.get_clone()

View File

@ -157,22 +157,24 @@ class EngineServer(service_base.MistralService):
) )
def start_action(self, rpc_ctx, action_name, 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. """Receives calls over RPC to start actions on engine.
:param rpc_ctx: RPC request context. :param rpc_ctx: RPC request context.
:param action_name: name of the Action. :param action_name: name of the Action.
:param action_input: input dictionary for Action. :param action_input: input dictionary for Action.
:param description: description of new Action execution. :param description: description of new Action execution.
:param namespace: The namespace of the action.
:param params: extra parameters to run Action. :param params: extra parameters to run Action.
:return: Action execution. :return: Action execution.
""" """
LOG.info( LOG.info(
"Received RPC request 'start_action'[name=%s, input=%s, " "Received RPC request 'start_action'[name=%s, input=%s, "
"description=%s, params=%s]", "description=%s, namespace=%s params=%s]",
action_name, action_name,
utils.cut(action_input), utils.cut(action_input),
description, description,
namespace,
params params
) )
@ -180,6 +182,7 @@ class EngineServer(service_base.MistralService):
action_name, action_name,
action_input, action_input,
description, description,
namespace=namespace,
**params **params
) )
@ -198,7 +201,6 @@ class EngineServer(service_base.MistralService):
action_ex_id, action_ex_id,
result.cut_repr() if result else '<unknown>' result.cut_repr() if result else '<unknown>'
) )
return self.engine.on_action_complete(action_ex_id, result, wf_action) 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): 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_def = actions.resolve_action_definition(
action_name, action_name,
self.wf_ex.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: if action_def.spec:

View File

@ -88,7 +88,6 @@ class DefaultExecutor(base.Executor):
return None return None
return error_result return error_result
if redelivered and not safe_rerun: if redelivered and not safe_rerun:
msg = ( msg = (
"Request to run action %s was redelivered, but action %s " "Request to run action %s was redelivered, but action %s "

View File

@ -2,6 +2,7 @@
# Copyright 2015 - StackStorm, Inc. # Copyright 2015 - StackStorm, Inc.
# Copyright 2017 - Brocade Communications Systems, Inc. # Copyright 2017 - Brocade Communications Systems, Inc.
# Copyright 2018 - Extreme Networks, Inc. # Copyright 2018 - Extreme Networks, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 @base.wrap_messaging_exception
def start_action(self, action_name, action_input, def start_action(self, action_name, action_input,
description=None, **params): description=None, namespace='', **params):
"""Starts action sending a request to engine over RPC. """Starts action sending a request to engine over RPC.
:param action_name: Action name. :param action_name: Action name.
:param action_input: Action input data as a dictionary. :param action_input: Action input data as a dictionary.
:param description: Execution description. :param description: Execution description.
:param namespace: The namespace of the action.
:param params: Additional options for action running. :param params: Additional options for action running.
:return: Action execution. :return: Action execution.
""" """
@ -165,6 +167,7 @@ class EngineClient(eng.Engine):
action_name=action_name, action_name=action_name,
action_input=action_input or {}, action_input=action_input or {},
description=description, description=description,
namespace=namespace,
params=params params=params
) )
@ -372,7 +375,6 @@ class ExecutorClient(exe.Executor):
action will be interrupted action will be interrupted
:return: Action result. :return: Action result.
""" """
rpc_kwargs = { rpc_kwargs = {
'action_ex_id': action_ex_id, 'action_ex_id': action_ex_id,
'action_cls_str': action_cls_str, 'action_cls_str': action_cls_str,

View File

@ -1,5 +1,6 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2014 - StackStorm, Inc. # Copyright 2014 - StackStorm, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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, def register_action_class(name, action_class_str, attributes,
description=None, input_str=None): description=None, input_str=None, namespace=''):
values = { values = {
'name': name, 'name': name,
'action_class': action_class_str, 'action_class': action_class_str,
@ -59,7 +60,8 @@ def register_action_class(name, action_class_str, attributes,
'description': description, 'description': description,
'input': input_str, 'input': input_str,
'is_system': True, 'is_system': True,
'scope': 'public' 'scope': 'public',
'namespace': namespace
} }
try: try:
@ -81,7 +83,7 @@ def sync_db():
register_standard_actions() register_standard_actions()
def _register_dynamic_action_classes(): def _register_dynamic_action_classes(namespace=''):
for generator in generator_factory.all_generators(): for generator in generator_factory.all_generators():
actions = generator.create_actions() actions = generator.create_actions()
@ -98,11 +100,12 @@ def _register_dynamic_action_classes():
action_class_str, action_class_str,
attrs, attrs,
action['description'], action['description'],
action['arg_list'] action['arg_list'],
namespace=namespace
) )
def register_action_classes(): def register_action_classes(namespace=''):
mgr = extension.ExtensionManager( mgr = extension.ExtensionManager(
namespace='mistral.actions', namespace='mistral.actions',
invoke_on_load=False invoke_on_load=False
@ -120,23 +123,24 @@ def register_action_classes():
action_class_str, action_class_str,
attrs, attrs,
description=description, 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): def get_action_db(action_name, namespace=''):
return db_api.load_action_definition(action_name) 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'). """Finds action class by full action name (i.e. 'namespace.action_name').
:param action_full_name: Full action name (that includes namespace). :param action_full_name: Full action name (that includes namespace).
:return: Action class or None if not found. :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: if action_db:
return action_factory.construct_action_class( return action_factory.construct_action_class(

View File

@ -1,4 +1,5 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 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) action_list_spec = spec_parser.get_action_list_spec_from_yaml(definition)
db_actions = [] db_actions = []
for action_spec in action_list_spec.get_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 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) action_list_spec = spec_parser.get_action_list_spec_from_yaml(definition)
actions = action_list_spec.get_actions() actions = action_list_spec.get_actions()
@ -48,32 +53,35 @@ def update_actions(definition, scope='private', identifier=None):
action_spec, action_spec,
definition, definition,
scope, scope,
identifier=identifier identifier=identifier,
namespace=namespace
)) ))
return db_actions 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) action_list_spec = spec_parser.get_action_list_spec_from_yaml(definition)
db_actions = [] db_actions = []
for action_spec in action_list_spec.get_actions(): for action_spec in action_list_spec.get_actions():
db_actions.append( db_actions.append(
create_or_update_action(action_spec, definition, scope) create_or_update_action(action_spec, definition, scope, namespace)
) )
return db_actions return db_actions
def create_action(action_spec, definition, scope): def create_action(action_spec, definition, scope, namespace):
return db_api.create_action_definition( 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()) action = db_api.load_action_definition(action_spec.get_name())
if action and action.is_system: if action and action.is_system:
@ -82,7 +90,7 @@ def update_action(action_spec, definition, scope, identifier=None):
action.name 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( return db_api.update_action_definition(
identifier if identifier else values['name'], 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()) action = db_api.load_action_definition(action_spec.get_name())
if action and action.is_system: if action and action.is_system:
@ -99,7 +107,7 @@ def create_or_update_action(action_spec, definition, scope):
action.name 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) return db_api.create_or_update_action_definition(values['name'], values)
@ -117,7 +125,7 @@ def get_input_list(action_input):
return input_list 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', []) action_input = action_spec.to_dict().get('input', [])
input_list = get_input_list(action_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(), 'spec': action_spec.to_dict(),
'is_system': False, 'is_system': False,
'input': ", ".join(input_list) if input_list else None, 'input': ", ".join(input_list) if input_list else None,
'scope': scope 'scope': scope,
'namespace': namespace
} }
return values return values

View File

@ -1,4 +1,5 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 return wb_db
def _on_workbook_update(wb_db, wb_spec, namespace): 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(),
db_actions = _create_or_update_actions(wb_db, wb_spec.get_actions()) namespace=namespace)
db_wfs = _create_or_update_workflows( db_wfs = _create_or_update_workflows(
wb_db, wb_db,
wb_spec.get_workflows(), wb_spec.get_workflows(),
@ -69,7 +70,7 @@ def _on_workbook_update(wb_db, wb_spec, namespace):
return db_actions, db_wfs 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 = [] db_actions = []
if actions_spec: if actions_spec:
@ -88,7 +89,8 @@ def _create_or_update_actions(wb_db, actions_spec):
'is_system': False, 'is_system': False,
'input': ', '.join(input_list) if input_list else None, 'input': ', '.join(input_list) if input_list else None,
'scope': wb_db.scope, 'scope': wb_db.scope,
'project_id': wb_db.project_id 'project_id': wb_db.project_id,
'namespace': namespace
} }
db_actions.append( db_actions.append(

View File

@ -304,7 +304,8 @@ class TestActionExecutionsController(base.APITest):
json.loads(action_exec['input']), json.loads(action_exec['input']),
description=None, description=None,
save_result=True, save_result=True,
run_sync=True run_sync=True,
namespace=''
) )
@mock.patch.object(rpc_clients.EngineClient, 'start_action') @mock.patch.object(rpc_clients.EngineClient, 'start_action')
@ -331,7 +332,8 @@ class TestActionExecutionsController(base.APITest):
action_exec['name'], action_exec['name'],
json.loads(action_exec['input']), json.loads(action_exec['input']),
description=None, description=None,
timeout=2 timeout=2,
namespace=''
) )
@mock.patch.object(rpc_clients.EngineClient, 'start_action') @mock.patch.object(rpc_clients.EngineClient, 'start_action')
@ -358,7 +360,8 @@ class TestActionExecutionsController(base.APITest):
action_exec['name'], action_exec['name'],
json.loads(action_exec['input']), json.loads(action_exec['input']),
description=None, description=None,
save_result=True save_result=True,
namespace=''
) )
@mock.patch.object(rpc_clients.EngineClient, 'start_action') @mock.patch.object(rpc_clients.EngineClient, 'start_action')
@ -374,7 +377,9 @@ class TestActionExecutionsController(base.APITest):
self.assertEqual(201, resp.status_int) self.assertEqual(201, resp.status_int)
self.assertEqual('{"result": "123"}', resp.json['output']) 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): def test_post_bad_result(self):
resp = self.app.post_json( resp = self.app.post_json(

View File

@ -1,5 +1,6 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc. # Copyright 2015 - StackStorm, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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', 'action_class': 'mypackage.my_module.Action1',
'attributes': None, 'attributes': None,
'project_id': '<default-project>', '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', 'name': 'action2',
@ -1094,7 +1096,8 @@ ACTION_DEFINITIONS = [
'action_class': 'mypackage.my_module.Action2', 'action_class': 'mypackage.my_module.Action2',
'attributes': None, 'attributes': None,
'project_id': '<default-project>', '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', 'name': 'action3',
@ -1104,7 +1107,8 @@ ACTION_DEFINITIONS = [
'action_class': 'mypackage.my_module.Action3', 'action_class': 'mypackage.my_module.Action3',
'attributes': None, 'attributes': None,
'project_id': '<default-project>', '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( self.assertRaisesWithMessage(
exc.DBDuplicateEntryError, exc.DBDuplicateEntryError,
"Duplicate entry for Action ['name', 'project_id']: action1" "Duplicate entry for Action ['name', 'namespace', 'project_id']:"
", <default-project>", " action1, , <default-project>",
db_api.create_action_definition, db_api.create_action_definition,
ACTION_DEFINITIONS[0] ACTION_DEFINITIONS[0]
) )

View File

@ -28,9 +28,7 @@ cfg.CONF.set_default('auth_enable', False, group='pecan')
class LookupUtilsTest(base.EngineTestCase): class LookupUtilsTest(base.EngineTestCase):
ACTION = """---
def test_action_definition_cache_ttl(self):
action = """---
version: '2.0' version: '2.0'
action1: action1:
@ -39,7 +37,7 @@ class LookupUtilsTest(base.EngineTestCase):
result: $ result: $
""" """
wf_text = """--- WF_TEXT = """---
version: '2.0' version: '2.0'
wf: wf:
@ -61,16 +59,23 @@ class LookupUtilsTest(base.EngineTestCase):
pause-before: true 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. # 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.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. # 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 # Reinitialise the cache with reduced action_definition_cache_time
# to make sure the test environment is under control. # to make sure the test environment is under control.
@ -84,13 +89,15 @@ class LookupUtilsTest(base.EngineTestCase):
self.addCleanup(cache_patch.stop) self.addCleanup(cache_patch.stop)
# Start workflow. # 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) self.await_workflow_paused(wf_ex.id)
# Check that 'action1' 'echo' and 'noop' are cached. # Check that 'action1' 'echo' and 'noop' are cached.
self.assertEqual(3, len(db_api._ACTION_DEF_CACHE)) self.assertEqual(5, len(db_api._ACTION_DEF_CACHE))
self.assertIn('action1', 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.noop', db_api._ACTION_DEF_CACHE)
self.assertIn('std.echo', 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) self.await_workflow_success(wf_ex.id)
# Check all actions are cached again. # Check all actions are cached again.
self.assertEqual(2, len(db_api._ACTION_DEF_CACHE)) self.assertEqual(3, len(db_api._ACTION_DEF_CACHE))
self.assertIn('action1', 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', 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 2014 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,6 +16,7 @@
from oslo_config import cfg from oslo_config import cfg
from mistral.db.v2 import api as db_api 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.services import workbooks as wb_service
from mistral.tests.unit.engine import base from mistral.tests.unit.engine import base
from mistral.workflow import states from mistral.workflow import states
@ -24,7 +26,6 @@ from mistral.workflow import states
# the change in value is not permanent. # the change in value is not permanent.
cfg.CONF.set_default('auth_enable', False, group='pecan') cfg.CONF.set_default('auth_enable', False, group='pecan')
WORKBOOK = """ WORKBOOK = """
--- ---
version: '2.0' version: '2.0'
@ -305,6 +306,78 @@ class AdhocActionsTest(base.EngineTestCase):
self.await_workflow_running(wf_ex.id) 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): def test_adhoc_action_runtime_context_name(self):
wf_ex = self.engine.start_workflow( wf_ex = self.engine.start_workflow(
'my_wb.wf4', 'my_wb.wf4',

View File

@ -1,4 +1,5 @@
# Copyright 2015 - Mirantis, Inc. # Copyright 2015 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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('Hello!', action_ex.output['result'])
self.assertEqual(states.SUCCESS, action_ex.state) 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( @mock.patch.object(
std_actions.EchoAction, std_actions.EchoAction,
'run', 'run',
@ -325,7 +401,7 @@ class RunActionEngineTest(base.EngineTestCase):
self.engine.start_action('fake_action', {'input': 'Hello'}) self.engine.start_action('fake_action', {'input': 'Hello'})
self.assertEqual(1, def_mock.call_count) 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) self.assertEqual(0, validate_mock.call_count)

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,17 +16,16 @@
from oslo_config import cfg from oslo_config import cfg
from mistral.db.v2 import api as db_api from mistral.db.v2 import api as db_api
from mistral.exceptions import DBEntityNotFoundError
from mistral.lang import parser as spec_parser from mistral.lang import parser as spec_parser
from mistral.services import actions as action_service from mistral.services import actions as action_service
from mistral.tests.unit import base from mistral.tests.unit import base
from mistral_lib import utils from mistral_lib import utils
# Use the set_default method to set value otherwise in certain test cases # Use the set_default method to set value otherwise in certain test cases
# the change in value is not permanent. # the change in value is not permanent.
cfg.CONF.set_default('auth_enable', False, group='pecan') cfg.CONF.set_default('auth_enable', False, group='pecan')
ACTION_LIST = """ ACTION_LIST = """
--- ---
version: '2.0' version: '2.0'
@ -54,6 +54,8 @@ action1:
result: $ result: $
""" """
NAMESPACE = 'test_namespace'
class ActionServiceTest(base.DbTestCase): class ActionServiceTest(base.DbTestCase):
def setUp(self): def setUp(self):
@ -78,14 +80,35 @@ class ActionServiceTest(base.DbTestCase):
# Action 2. # Action 2.
action2_db = self._assert_single_item(db_actions, name='action2') 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) action2_spec = spec_parser.get_action_spec(action2_db.spec)
self.assertEqual('action2', action2_spec.get_name()) self.assertEqual('action2', action2_spec.get_name())
self.assertEqual('std.echo', action1_spec.get_base()) self.assertEqual('std.echo', action1_spec.get_base())
self.assertDictEqual({'output': 'Hey'}, action2_spec.get_base_input()) 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): 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)) 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({'output': 'Hi'}, action1_spec.get_base_input())
self.assertDictEqual({}, action1_spec.get_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. # Action 1.
action1_db = self._assert_single_item(db_actions, name='action1') action1_db = self._assert_single_item(db_actions, name='action1')
@ -112,3 +136,29 @@ class ActionServiceTest(base.DbTestCase):
action1_spec.get_input().get('param1'), action1_spec.get_input().get('param1'),
utils.NotDefined 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 2014 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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.assertIsNotNone(wb_db.spec)
self.assertListEqual(['test'], wb_db.tags) 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)) self.assertEqual(1, len(db_actions))
@ -279,7 +283,8 @@ class WorkbookServiceTest(base.DbTestCase):
wb_service.create_workbook_v2(WORKBOOK, namespace=namespace) wb_service.create_workbook_v2(WORKBOOK, namespace=namespace)
db_wfs = db_api.get_workflow_definitions() 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(2, len(db_wfs))
self.assertEqual(1, len(db_actions)) self.assertEqual(1, len(db_actions))
@ -287,8 +292,8 @@ class WorkbookServiceTest(base.DbTestCase):
db_api.delete_workbook('my_wb', namespace=namespace) db_api.delete_workbook('my_wb', namespace=namespace)
db_wfs = db_api.get_workflow_definitions() 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 # Deleting workbook shouldn't delete workflows and actions
self.assertEqual(2, len(db_wfs)) self.assertEqual(2, len(db_wfs))
self.assertEqual(1, len(db_actions)) 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.