Merge "Add namespaces to Ad-Hoc actions"
This commit is contained in:
commit
19792f1f70
|
@ -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.
|
||||
|
@ -177,7 +178,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))
|
||||
|
||||
|
@ -291,7 +295,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))
|
||||
|
@ -299,8 +304,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))
|
||||
|
|
|
@ -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