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:
parent
6e9a70db25
commit
20c3408692
@ -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, "
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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=''
|
||||
)
|
||||
|
||||
|
||||
|
@ -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()
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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 "
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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]
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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))
|
||||
|
14
releasenotes/notes/namespace_for_adhoc_actions.yaml
Normal file
14
releasenotes/notes/namespace_for_adhoc_actions.yaml
Normal 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.
|
||||
|
Loading…
Reference in New Issue
Block a user