Fix dynamic actions further

* Reworked /code_sources and /dynamic_actions API endpoints to
  simplify them. For now they don't work with multiple objects and
  they are consistent with other endpoints. If needed, we'll add
  support for multiple objects (i.e. adding multiple dynamic actions
  with a single request) later in a backwards compatible manner.
* Simplified unit tests.
* Got rid of services/*.py modules since they didn't do anything
  useful. They were just wrappers around DB API calls.

Change-Id: Ib5a53f1f1a185f0395ffae1ab0c401633fcdd0fc
This commit is contained in:
Renat Akhmerov 2020-11-24 17:03:05 +07:00
parent f78f33507e
commit a73fe5b8a3
16 changed files with 369 additions and 533 deletions

View File

@ -137,7 +137,7 @@ def _get_python_module(code_source_id, namespace=''):
namespace=namespace
)
mod = _load_python_module(code_source.name, code_source.src)
mod = _load_python_module(code_source.name, code_source.content)
return mod, code_source.version
@ -177,7 +177,7 @@ class DynamicActionProvider(ml_actions.ActionProvider):
# Reload module.
code_src = db_api.get_code_source(code_src_id)
module = _load_python_module(code_src.name, code_src.src)
module = _load_python_module(code_src.name, code_src.content)
self._code_sources[code_src_id] = (module, code_src.version)
else:

View File

@ -60,6 +60,7 @@ class Resource(wtypes.Base):
res = "%s [" % type(self).__name__
first = True
for attr in self._wsme_attributes:
if not first:
res += ', '

View File

@ -28,7 +28,6 @@ from mistral.api.hooks import content_type as ct_hook
from mistral import context
from mistral.db.v2 import api as db_api
from mistral.services import code_sources
from mistral.utils import filter_utils
from mistral.utils import rest_utils
@ -40,32 +39,76 @@ class CodeSourcesController(rest.RestController, hooks.HookController):
__hooks__ = [ct_hook.ContentTypeHook("application/json", ['POST', 'PUT'])]
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="multipart/form-data")
def post(self, namespace='', **files):
"""Creates new Code Sources.
@pecan.expose(content_type="text/plain")
def post(self, name, scope='private', namespace=''):
"""Creates new code sources.
:param name: Code source name (i.e. the name of the module).
:param scope: Optional. Scope (private or public).
:param namespace: Optional. The namespace to create the code sources
in.
:params **files: a list of files to create code sources from,
the variable name of the file will be the module name
"""
acl.enforce('code_sources:create', context.ctx())
# Extract content directly from the request.
content = pecan.request.text
LOG.debug(
'Creating Code Sources with names: %s in namespace:[%s]',
files.keys(),
'Creating code source [names=%s, scope=%s, namespace=%s]',
name,
scope,
namespace
)
code_sources_db = code_sources.create_code_sources(namespace, **files)
db_model = rest_utils.rest_retry_on_db_error(
db_api.create_code_source)(
{
'name': name,
'content': content,
'namespace': namespace,
'scope': scope,
'version': 1,
}
)
code_sources_list = [
resources.CodeSource.from_db_model(db_cs)
for db_cs in code_sources_db
]
pecan.response.status = 201
return resources.CodeSources(code_sources=code_sources_list).to_json()
return resources.CodeSource.from_db_model(db_model).to_json()
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain")
def put(self, identifier, scope='private', namespace=''):
"""Update code source.
:param identifier: Identifier (name or ID) of the code source.
:param scope: Scope (private or public) of the code source.
:param namespace: Optional. The namespace of the code source.
"""
acl.enforce('code_sources:update', context.ctx())
LOG.debug(
'Updating code source [identifier(name or id)=%s, scope=%s,'
' namespace=%s]',
identifier,
scope,
namespace
)
content = pecan.request.text
db_model = rest_utils.rest_retry_on_db_error(
db_api.update_code_source
)(
identifier=identifier,
namespace=namespace,
values={
'scope': scope,
'content': content
}
)
return resources.CodeSource.from_db_model(db_model).to_json()
@wsme_pecan.wsexpose(resources.CodeSources, types.uuid, int,
types.uniquelist, types.list, types.uniquelist,
@ -73,8 +116,7 @@ class CodeSourcesController(rest.RestController, hooks.HookController):
resources.SCOPE_TYPES, types.uuid, wtypes.text,
wtypes.text, bool, wtypes.text)
def get_all(self, marker=None, limit=None, sort_keys='created_at',
sort_dirs='asc', fields='', name=None,
tags=None, scope=None,
sort_dirs='asc', fields='', name=None, tags=None, scope=None,
project_id=None, created_at=None, updated_at=None,
all_projects=False, namespace=None):
"""Return a list of Code Sources.
@ -95,9 +137,6 @@ class CodeSourcesController(rest.RestController, hooks.HookController):
:param name: Optional. Keep only resources with a specific name.
:param namespace: Optional. Keep only resources with a specific
namespace
:param input: Optional. Keep only resources with a specific input.
:param definition: Optional. Keep only resources with a specific
definition.
:param tags: Optional. Keep only resources containing specific tags.
:param scope: Optional. Keep only resources with a specific scope.
:param project_id: Optional. The same as the requester project_id
@ -150,7 +189,7 @@ class CodeSourcesController(rest.RestController, hooks.HookController):
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(resources.CodeSource, wtypes.text, wtypes.text)
def get(self, identifier, namespace=''):
"""Return the named Code Source.
"""Return a code source.
:param identifier: Name or UUID of the code source to retrieve.
:param namespace: Optional. Namespace of the code source to retrieve.
@ -159,13 +198,13 @@ class CodeSourcesController(rest.RestController, hooks.HookController):
acl.enforce('code_sources:get', context.ctx())
LOG.debug(
'Fetch Code Source [identifier=%s], [namespace=%s]',
'Fetch code source [identifier=%s, namespace=%s]',
identifier,
namespace
)
db_model = rest_utils.rest_retry_on_db_error(
code_sources.get_code_source)(
db_api.get_code_source)(
identifier=identifier,
namespace=namespace
)
@ -175,7 +214,7 @@ class CodeSourcesController(rest.RestController, hooks.HookController):
@rest_utils.wrap_pecan_controller_exception
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204)
def delete(self, identifier, namespace=''):
"""Delete a Code Source.
"""Delete a code source.
:param identifier: Name or ID of Code Source to delete.
:param namespace: Optional. Namespace of the Code Source to delete.
@ -184,39 +223,12 @@ class CodeSourcesController(rest.RestController, hooks.HookController):
acl.enforce('code_sources:delete', context.ctx())
LOG.debug(
'Delete Code Source [identifier=%s, namespace=%s]',
'Delete code source [identifier=%s, namespace=%s]',
identifier,
namespace
)
rest_utils.rest_retry_on_db_error(
code_sources.delete_code_source
)(
rest_utils.rest_retry_on_db_error(db_api.delete_code_source)(
identifier=identifier,
namespace=namespace
)
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="multipart/form-data")
def put(self, namespace='', **files):
"""Update Code Sources.
:param namespace: Optional. The namespace of the code sources.
:params **files: a list of files to update code sources from.
"""
acl.enforce('code_sources:update', context.ctx())
LOG.debug(
'Updating Code Sources with names: %s in namespace:[%s]',
files.keys(),
namespace
)
code_sources_db = code_sources.update_code_sources(namespace, **files)
code_sources_list = [
resources.CodeSource.from_db_model(db_cs)
for db_cs in code_sources_db
]
return resources.CodeSources(code_sources=code_sources_list).to_json()

View File

@ -14,7 +14,6 @@
# limitations under the License.
from oslo_log import log as logging
import pecan
from pecan import hooks
from pecan import rest
from wsme import types as wtypes
@ -25,10 +24,9 @@ from mistral.api.controllers.v2 import resources
from mistral.api.controllers.v2 import types
from mistral.api.hooks import content_type as ct_hook
from mistral import context
from mistral.utils import safe_yaml
from mistral import exceptions as exc
from mistral.db.v2 import api as db_api
from mistral.services import dynamic_actions
from mistral.utils import filter_utils
from mistral.utils import rest_utils
@ -40,33 +38,94 @@ class DynamicActionsController(rest.RestController, hooks.HookController):
__hooks__ = [ct_hook.ContentTypeHook("application/json", ['POST', 'PUT'])]
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain")
def post(self, namespace=''):
"""Creates new dynamic actions.
@wsme_pecan.wsexpose(
resources.DynamicAction,
body=resources.DynamicAction,
status_code=201
)
def post(self, dyn_action):
"""Creates new dynamic action.
:param namespace: Optional. The namespace to create the actions in.
The text is allowed to have multiple actions. In such case, they all
will be created.
:param dyn_action: Dynamic action to create.
"""
acl.enforce('dynamic_actions:create', context.ctx())
actions = safe_yaml.load(pecan.request.text)
LOG.debug('Creating dynamic action [action=%s]', dyn_action)
LOG.debug(
'Creating dynamic actions with names: %s in namespace:[%s]',
actions,
namespace
if not dyn_action.code_source_id and not dyn_action.code_source_name:
raise exc.InputException(
"Either 'code_source_id' or 'code_source_name'"
" must be provided."
)
code_source = db_api.get_code_source(
dyn_action.code_source_id or dyn_action.code_source_name,
namespace=dyn_action.namespace
)
actions_db = dynamic_actions.create_dynamic_actions(actions, namespace)
# TODO(rakhmerov): Ideally we also need to check if the specified
# class exists in the specified code source. But probably it's not
# a controller responsibility.
actions_list = [
resources.DynamicAction.from_db_model(action)
for action in actions_db
]
db_model = rest_utils.rest_retry_on_db_error(
db_api.create_dynamic_action_definition
)(
{
'name': dyn_action.name,
'namespace': dyn_action.namespace,
'class_name': dyn_action.class_name,
'code_source_id': code_source.id,
'code_source_name': code_source.name
}
)
return resources.DynamicActions(dynamic_actions=actions_list).to_json()
return resources.DynamicAction.from_db_model(db_model)
@rest_utils.wrap_pecan_controller_exception
@wsme_pecan.wsexpose(
resources.DynamicAction,
body=resources.DynamicAction
)
def put(self, dyn_action):
"""Update dynamic action.
:param dyn_action: Dynamic action to create.
"""
acl.enforce('dynamic_actions:update', context.ctx())
LOG.debug('Updating dynamic action [action=%s]', dyn_action)
if not dyn_action.id and not dyn_action.name:
raise exc.InputException("Either 'name' or 'id' must be provided.")
values = {'class_name': dyn_action.class_name}
if dyn_action.scope:
values['scope'] = dyn_action.scope
# A client may also want to update a source code.
if dyn_action.code_source_id or dyn_action.code_source_name:
code_source = db_api.get_code_source(
dyn_action.code_source_id or dyn_action.code_source_name,
namespace=dyn_action.namespace
)
values['code_source_id'] = code_source.id
values['code_source_name'] = code_source.name
# TODO(rakhmerov): Ideally we also need to check if the specified
# class exists in the specified code source. But probably it's not
# a controller responsibility.
db_model = rest_utils.rest_retry_on_db_error(
db_api.update_dynamic_action_definition
)(
dyn_action.id or dyn_action.name,
values,
namespace=dyn_action.namespace
)
return resources.DynamicAction.from_db_model(db_model)
@wsme_pecan.wsexpose(resources.DynamicActions, types.uuid, int,
types.uniquelist, types.list, types.uniquelist,
@ -159,13 +218,14 @@ class DynamicActionsController(rest.RestController, hooks.HookController):
acl.enforce('dynamic_actions:get', context.ctx())
LOG.debug(
'Fetch Action [identifier=%s], [namespace=%s]',
'Fetch dynamic action [identifier=%s, namespace=%s]',
identifier,
namespace
)
db_model = rest_utils.rest_retry_on_db_error(
dynamic_actions.get_dynamic_action)(
db_api.get_dynamic_action_definition
)(
identifier=identifier,
namespace=namespace
)
@ -173,53 +233,25 @@ class DynamicActionsController(rest.RestController, hooks.HookController):
return resources.DynamicAction.from_db_model(db_model)
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="multipart/form-data")
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204)
def delete(self, identifier, namespace=''):
"""Delete an Action.
"""Delete a dynamic action.
:param identifier: Name or ID of Action to delete.
:param namespace: Optional. Namespace of the Action to delete.
:param identifier: Name or ID of the action to delete.
:param namespace: Optional. Namespace of the action to delete.
"""
acl.enforce('dynamic_actions:delete', context.ctx())
LOG.debug(
'Delete Action [identifier=%s, namespace=%s]',
'Delete dynamic action [identifier=%s, namespace=%s]',
identifier,
namespace
)
rest_utils.rest_retry_on_db_error(
dynamic_actions.delete_dynamic_action)(
db_api.delete_dynamic_action_definition
)(
identifier=identifier,
namespace=namespace
)
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain")
def put(self, namespace=''):
"""Update Actions.
:param namespace: Optional. The namespace to update the actions in.
The text is allowed to have multiple Actions, In such case,
they all will be updated.
"""
acl.enforce('dynamic_actions:update', context.ctx())
actions = safe_yaml.load(pecan.request.text)
LOG.debug(
'Updating Actions with names: %s in namespace:[%s]',
actions.keys(),
namespace
)
actions_db = dynamic_actions.update_dynamic_actions(actions, namespace)
actions_list = [
resources.DynamicAction.from_db_model(action)
for action in actions_db
]
return resources.DynamicActions(dynamic_actions=actions_list).to_json()

View File

@ -190,16 +190,17 @@ class CodeSource(resource.Resource, ScopedResource):
id = wtypes.text
name = wtypes.text
src = wtypes.text
scope = SCOPE_TYPES
namespace = wtypes.text
content = wtypes.text
version = wtypes.IntegerType(minimum=1)
project_id = wsme.wsattr(wtypes.text, readonly=True)
scope = SCOPE_TYPES
actions = [wtypes.text]
created_at = wtypes.text
updated_at = wtypes.text
namespace = wtypes.text
@classmethod
def sample(cls):
@ -216,24 +217,6 @@ class CodeSource(resource.Resource, ScopedResource):
namespace=''
)
@classmethod
def from_db_model(cls, db_model):
return CodeSource(
id=getattr(db_model, 'id', db_model.name),
name=db_model.name,
version=db_model.version,
src=db_model.src,
namespace=db_model.namespace,
project_id=db_model.project_id,
scope=db_model.scope,
created_at=utils.datetime_to_str(
getattr(db_model, 'created_at', '')
),
updated_at=utils.datetime_to_str(
getattr(db_model, 'updated_at', '')
)
)
class CodeSources(resource.ResourceList):
"""A collection of CodeSources."""
@ -264,13 +247,16 @@ class DynamicAction(resource.Resource, ScopedResource):
id = wtypes.text
name = wtypes.text
namespace = wsme.wsattr(wtypes.text, default='')
code_source_id = wtypes.text
code_source_name = wtypes.text
class_name = wtypes.text
project_id = wsme.wsattr(wtypes.text, readonly=True)
scope = SCOPE_TYPES
created_at = wtypes.text
updated_at = wtypes.text
namespace = wtypes.text
@classmethod
def sample(cls):
@ -279,6 +265,7 @@ class DynamicAction(resource.Resource, ScopedResource):
name='actionName',
class_name='className',
code_source_id='233e4567-354b-12d3-4444-426655444444',
code_source_name='my_sample_module',
scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7',
created_at='1970-01-01T00:00:00.000000',
@ -286,24 +273,6 @@ class DynamicAction(resource.Resource, ScopedResource):
namespace=''
)
@classmethod
def from_db_model(cls, db_model):
return DynamicAction(
id=getattr(db_model, 'id', db_model.name),
name=db_model.name,
code_source_id=db_model.code_source_id,
class_name=db_model.class_name,
namespace=db_model.namespace,
project_id=db_model.project_id,
scope=db_model.scope,
created_at=utils.datetime_to_str(
getattr(db_model, 'created_at', '')
),
updated_at=utils.datetime_to_str(
getattr(db_model, 'updated_at', '')
)
)
class DynamicActions(resource.ResourceList):
"""A collection of DynamicActions."""

View File

@ -40,7 +40,8 @@ class WorkbooksController(rest.RestController, hooks.HookController):
__hooks__ = [ct_hook.ContentTypeHook("application/json", ['POST', 'PUT'])]
validate = validation.SpecValidationController(
spec_parser.get_workbook_spec_from_yaml)
spec_parser.get_workbook_spec_from_yaml
)
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(resources.Workbook, wtypes.text, wtypes.text)
@ -56,9 +57,12 @@ class WorkbooksController(rest.RestController, hooks.HookController):
# Use retries to prevent possible failures.
r = rest_utils.create_db_retry_object()
db_model = r.call(db_api.get_workbook,
name,
namespace=namespace)
db_model = r.call(
db_api.get_workbook,
name,
namespace=namespace
)
return resources.Workbook.from_db_model(db_model)
@ -68,9 +72,6 @@ class WorkbooksController(rest.RestController, hooks.HookController):
"""Update a workbook.
:param namespace: Optional. Namespace of workbook to update.
:param validate: Optional. If set to False, disables validation of
the workflow YAML definition syntax, but only if allowed in the
service configuration. By default, validation is enabled.
"""
acl.enforce('workbooks:update', context.ctx())

View File

@ -47,7 +47,8 @@ class WorkflowsController(rest.RestController, hooks.HookController):
__hooks__ = [ct_hook.ContentTypeHook("application/json", ['POST', 'PUT'])]
validate = validation.SpecValidationController(
spec_parser.get_workflow_list_spec_from_yaml)
spec_parser.get_workflow_list_spec_from_yaml
)
@pecan.expose()
def _lookup(self, identifier, sub_resource, *remainder):

View File

@ -38,7 +38,7 @@ def upgrade():
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('project_id', sa.String(length=80), nullable=True),
sa.Column('namespace', sa.String(length=255), nullable=True),
sa.Column('src', sa.TEXT, nullable=False),
sa.Column('content', sa.TEXT, nullable=False),
sa.Column('version', sa.Integer, nullable=False),
sa.Column('tags', st.JsonEncoded(), nullable=True),
sa.Column('scope', sa.String(length=80), nullable=True),
@ -61,6 +61,7 @@ def upgrade():
sa.Column('scope', sa.String(length=80), nullable=True),
sa.Column('project_id', sa.String(length=80), nullable=True),
sa.Column('code_source_id', sa.String(length=36), nullable=False),
sa.Column('code_source_name', sa.String(length=255), nullable=False),
sa.Column('namespace', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),

View File

@ -184,10 +184,6 @@ def create_dynamic_action_definition(values):
return IMPL.create_dynamic_action_definition(values)
def delete_dynamic_action_definition(identifier, namespace=''):
return IMPL.delete_dynamic_action_definition(identifier, namespace)
def update_dynamic_action_definition(identifier, values, namespace=''):
return IMPL.update_dynamic_action_definition(identifier, values, namespace)
@ -204,12 +200,24 @@ def get_dynamic_action_definitions(limit=None, marker=None, sort_keys=None,
)
def delete_dynamic_action_definition(identifier, namespace=''):
return IMPL.delete_dynamic_action_definition(identifier, namespace)
def delete_dynamic_action_definitions(**kwargs):
return IMPL.delete_dynamic_action_definitions(**kwargs)
# Code sources.
def get_code_source(identifier, namespace='', fields=()):
return IMPL.get_code_source(identifier, fields, namespace=namespace)
def load_code_source(identifier, namespace='', fields=()):
return IMPL.load_code_source(identifier, fields, namespace=namespace)
def create_code_source(values):
return IMPL.create_code_source(values)
@ -218,10 +226,6 @@ def update_code_source(identifier, values, namespace=''):
return IMPL.update_code_source(identifier, values, namespace=namespace)
def delete_code_source(name, namespace=''):
return IMPL.delete_code_source(name, namespace=namespace)
def get_code_sources(limit=None, marker=None, sort_keys=None,
sort_dirs=None, fields=None, **kwargs):
return IMPL.get_code_sources(
@ -234,6 +238,14 @@ def get_code_sources(limit=None, marker=None, sort_keys=None,
)
def delete_code_source(identifier, namespace=''):
return IMPL.delete_code_source(identifier, namespace=namespace)
def delete_code_sources(**kwargs):
return IMPL.delete_code_sources(**kwargs)
# Action definitions.
def get_action_definition_by_id(id, fields=()):

View File

@ -687,6 +687,16 @@ def get_code_source(identifier, fields=(), session=None, namespace=''):
return code_src
@b.session_aware()
def load_code_source(identifier, fields=(), session=None, namespace=''):
return _get_db_object_by_name_and_namespace_or_id(
models.CodeSource,
identifier,
namespace=namespace,
columns=fields
)
@b.session_aware()
def update_code_source(identifier, values, namespace='', session=None):
code_src = get_code_source(identifier, namespace=namespace)
@ -709,6 +719,11 @@ def delete_code_source(identifier, namespace='', session=None):
session.delete(code_src)
@b.session_aware()
def delete_code_sources(session=None, **kwargs):
return _delete_all(models.CodeSource, **kwargs)
# Dynamic actions.
@b.session_aware()
@ -785,6 +800,11 @@ def delete_dynamic_action_definition(identifier, namespace='', session=None):
session.delete(action_def)
@b.session_aware()
def delete_dynamic_action_definitions(session=None, **kwargs):
return _delete_all(models.DynamicActionDefinition, **kwargs)
# Action definitions.
@b.session_aware()

View File

@ -213,7 +213,7 @@ class CodeSource(mb.MistralSecureModelBase):
# Main properties.
id = mb.id_column()
name = sa.Column(sa.String(255))
src = sa.Column(sa.Text())
content = sa.Column(sa.Text())
version = sa.Column(sa.Integer())
namespace = sa.Column(sa.String(255), nullable=True)
tags = sa.Column(st.JsonListType())
@ -238,6 +238,7 @@ class DynamicActionDefinition(mb.MistralSecureModelBase):
name = sa.Column(sa.String(255))
namespace = sa.Column(sa.String(255), nullable=True)
class_name = sa.Column(sa.String(255))
code_source_name = sa.Column(sa.String(255))
DynamicActionDefinition.code_source_id = sa.Column(
@ -246,6 +247,12 @@ DynamicActionDefinition.code_source_id = sa.Column(
nullable=False
)
DynamicActionDefinition.code_source = relationship(
CodeSource,
remote_side=CodeSource.id,
lazy='select'
)
# Execution objects.

View File

@ -1,95 +0,0 @@
# 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.
from oslo_log import log as logging
from mistral.db.v2 import api as db_api
LOG = logging.getLogger(__name__)
_SYSTEM_PROVIDER = None
_TEST_PROVIDER = None
def create_code_source(name, src_code, namespace='', version=1):
with db_api.transaction():
return db_api.create_code_source({
'name': name,
'namespace': namespace,
'version': version,
'src': src_code,
})
def create_code_sources(namespace='', **files):
return _update_or_create_code_sources(
create_code_source,
namespace,
**files
)
def _update_or_create_code_sources(operation, namespace='', **files):
code_sources = []
for file in files:
filename = files[file].name
file_content = files[file].file.read().decode()
code_sources.append(
operation(
filename,
file_content,
namespace
)
)
return code_sources
def update_code_sources(namespace='', **files):
return _update_or_create_code_sources(
update_code_source,
namespace,
**files
)
def update_code_source(identifier, src_code, namespace=''):
with db_api.transaction():
return db_api.update_code_source(
identifier=identifier,
namespace=namespace,
values={'src': src_code}
)
def delete_code_source(identifier, namespace=''):
with db_api.transaction():
db_api.delete_code_source(identifier, namespace=namespace)
def delete_code_sources(code_sources, namespace=''):
with db_api.transaction():
for code_source in code_sources:
db_api.delete_code_source(code_source, namespace=namespace)
def get_code_source(identifier, namespace='', fields=()):
return db_api.get_code_source(
identifier,
namespace=namespace,
fields=fields
)

View File

@ -1,83 +0,0 @@
# 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.
from oslo_log import log as logging
from mistral.db.v2 import api as db_api
LOG = logging.getLogger(__name__)
def create_dynamic_actions(action_list, namespace=''):
created_actions = []
with db_api.transaction():
for action in action_list:
created_actions.append(
db_api.create_dynamic_action_definition({
'name': action['name'],
'class_name': action['class_name'],
'namespace': namespace,
'code_source_id': action['code_source_id']
})
)
return created_actions
def delete_dynamic_action(identifier, namespace=''):
with db_api.transaction():
return db_api.delete_dynamic_action_definition(
identifier,
namespace
)
def get_dynamic_actions(limit=None, marker=None, sort_keys=None,
sort_dirs=None, fields=None, **kwargs):
with db_api.transaction():
return db_api.get_dynamic_action_definitions(
limit=limit,
marker=marker,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
**kwargs
)
def get_dynamic_action(identifier, namespace=''):
with db_api.transaction():
return db_api.get_dynamic_action_definition(
identifier,
namespace=namespace
)
def update_dynamic_action(identifier, values, namespace=''):
with db_api.transaction():
return db_api.update_dynamic_action_definition(
identifier,
values,
namespace
)
def update_dynamic_actions(actions, namespace=''):
return [
update_dynamic_action(name, values, namespace)
for name, values in actions.items()
]

View File

@ -13,8 +13,7 @@
# limitations under the License.
from mistral.actions import dynamic_action
from mistral.services import code_sources as code_sources_service
from mistral.services import dynamic_actions as dynamic_actions_service
from mistral.db.v2 import api as db_api
from mistral.tests.unit import base
DUMMY_CODE_SOURCE = """from mistral_lib import actions
@ -37,33 +36,36 @@ NAMESPACE = "ns"
class DynamicActionProviderTest(base.DbTestCase):
def _create_code_source(self, namespace=''):
return code_sources_service.create_code_source(
name='code_source',
src_code=DUMMY_CODE_SOURCE,
namespace=namespace
return db_api.create_code_source(
{
'name': 'code_source',
'content': DUMMY_CODE_SOURCE,
'namespace': namespace,
'version': 0
}
)
def _delete_code_source(self):
return code_sources_service.delete_code_source(
identifier='code_source',
)
return db_api.delete_code_source('code_source')
def _create_dynamic_actions(self, code_source_id, namespace=''):
actions = [
def _create_dynamic_actions(self, code_source, namespace=''):
db_api.create_dynamic_action_definition(
{
"name": "dummy_action",
"namespace": namespace,
"class_name": "DummyAction",
"code_source_id": code_source_id
},
"code_source_id": code_source.id,
"code_source_name": code_source.name
}
)
db_api.create_dynamic_action_definition(
{
"name": "dummy_action2",
"namespace": namespace,
"class_name": "DummyAction2",
"code_source_id": code_source_id
}]
dynamic_actions_service.create_dynamic_actions(
actions,
namespace=namespace
"code_source_id": code_source.id,
"code_source_name": code_source.name
}
)
def test_dynamic_actions(self):
@ -75,7 +77,7 @@ class DynamicActionProviderTest(base.DbTestCase):
code_source = self._create_code_source()
self._create_dynamic_actions(code_source_id=code_source['id'])
self._create_dynamic_actions(code_source)
action_descs = provider.find_all()
@ -92,7 +94,7 @@ class DynamicActionProviderTest(base.DbTestCase):
code_source = self._create_code_source()
self._create_dynamic_actions(code_source_id=code_source['id'])
self._create_dynamic_actions(code_source)
action_descs = provider.find_all()
@ -114,9 +116,10 @@ class DynamicActionProviderTest(base.DbTestCase):
code_source = self._create_code_source()
self._create_dynamic_actions(
code_source_id=code_source['id'],
code_source=code_source,
namespace=NAMESPACE
)
action_descs = provider.find_all(namespace=NAMESPACE)
self.assertEqual(2, len(action_descs))

View File

@ -13,11 +13,12 @@
# limitations under the License.
from mistral.db.v2 import api as db_api
from mistral.tests.unit.api import base
FILE_CONTENT = """test file"""
FILE_CONTENT = "test file"
UPDATED_FILE_CONTENT = """updated content"""
UPDATED_FILE_CONTENT = "updated content"
MODULE_NAME = 'modulename%s'
NAMESPACE = "NS"
@ -27,11 +28,9 @@ class TestCodeSourcesController(base.APITest):
def _create_code_source(self, module_name, file_content,
namespace=NAMESPACE, expect_errors=False):
return self.app.post(
'/v2/code_sources',
params={'namespace': namespace},
upload_files=[
(module_name, 'filename', file_content.encode())
],
'/v2/code_sources?name=%s&namespace=%s' % (module_name, namespace),
file_content,
headers={'Content-Type': 'text/plain'},
expect_errors=expect_errors
)
@ -40,79 +39,71 @@ class TestCodeSourcesController(base.APITest):
'/v2/code_sources/%s?namespace=%s' % (id, namespace)
)
def test_create_code_source(self):
def setUp(self):
super(TestCodeSourcesController, self).setUp()
self.addCleanup(db_api.delete_code_sources)
def test_post(self):
mod_name = MODULE_NAME % 'create'
resp = self._create_code_source(
mod_name,
FILE_CONTENT
)
resp = self._create_code_source(mod_name, FILE_CONTENT)
resp_json = resp.json
self.assertEqual(201, resp.status_int)
self.assertEqual(200, resp.status_int)
code_src = resp.json
code_sources = resp_json.get('code_sources')
self.assertEqual(mod_name, code_src['name'])
self.assertEqual(FILE_CONTENT, code_src['content'])
self.assertEqual(1, code_src['version'])
self.assertEqual(NAMESPACE, code_src['namespace'])
self.assertEqual(1, len(code_sources))
code_source = code_sources[0]
self.assertEqual(mod_name, code_source.get('name'))
self.assertEqual(FILE_CONTENT, code_source.get('src'))
self.assertEqual(1, code_source.get('version'))
self.assertEqual(NAMESPACE, code_source.get('namespace'))
self._delete_code_source(mod_name)
def test_update_code_source(self):
def test_put(self):
mod_name = MODULE_NAME % 'update'
self._create_code_source(mod_name, FILE_CONTENT)
resp = self.app.put(
'/v2/code_sources/',
params='namespace=%s' % NAMESPACE,
upload_files=[
(mod_name, 'filename', UPDATED_FILE_CONTENT.encode())
],
'/v2/code_sources?identifier=%s&namespace=%s' %
(mod_name, NAMESPACE),
UPDATED_FILE_CONTENT,
headers={'Content-Type': 'text/plain'}
)
resp_json = resp.json
self.assertEqual(200, resp.status_int)
code_sources = resp_json.get('code_sources')
code_src = resp.json
self.assertEqual(1, len(code_sources))
self.assertEqual(mod_name, code_src['name'])
self.assertEqual(UPDATED_FILE_CONTENT, code_src['content'])
self.assertEqual(2, code_src['version'])
self.assertEqual(NAMESPACE, code_src['namespace'])
code_source = code_sources[0]
self.assertEqual(200, resp.status_int)
self.assertEqual(mod_name, code_source.get('name'))
self.assertEqual(UPDATED_FILE_CONTENT, code_source.get('src'))
self.assertEqual(2, code_source.get('version'))
self.assertEqual(NAMESPACE, code_source.get('namespace'))
self._delete_code_source(mod_name)
def test_delete_code_source(self):
def test_delete(self):
mod_name = MODULE_NAME % 'delete'
resp = self._create_code_source(mod_name, FILE_CONTENT)
resp_json = resp.json
self.assertEqual(201, resp.status_int)
self.assertEqual(mod_name, resp.json['name'])
self.assertEqual(200, resp.status_int)
code_sources = resp_json.get('code_sources')
self.assertEqual(1, len(code_sources))
# Make sure the object is in DB.
self.assertIsNotNone(
db_api.load_code_source(mod_name, namespace=NAMESPACE)
)
self._delete_code_source(mod_name)
def test_create_duplicate_code_source(self):
# Make sure the object was deleted from DB.
self.assertIsNone(
db_api.load_code_source(mod_name, namespace=NAMESPACE)
)
def test_post_duplicate(self):
mod_name = MODULE_NAME % 'duplicate'
self._create_code_source(mod_name, FILE_CONTENT)
resp = self._create_code_source(
mod_name,
FILE_CONTENT, expect_errors=True
@ -120,28 +111,28 @@ class TestCodeSourcesController(base.APITest):
self.assertEqual(409, resp.status_int)
self.assertIn('Duplicate entry for CodeSource', resp)
self._delete_code_source(mod_name)
def test_get_code_source(self):
def test_get(self):
mod_name = MODULE_NAME % 'get'
self._create_code_source(mod_name, FILE_CONTENT)
resp = self.app.get(
'/v2/code_sources/%s' % mod_name,
params='namespace=%s' % NAMESPACE
'/v2/code_sources/%s?namespace=%s' % (mod_name, NAMESPACE)
)
resp_json = resp.json
self.assertEqual(200, resp.status_int)
self.assertEqual(mod_name, resp_json.get('name'))
self.assertEqual(FILE_CONTENT, resp_json.get('src'))
self.assertEqual(1, resp_json.get('version'))
self.assertEqual(NAMESPACE, resp_json.get('namespace'))
self.assertEqual(mod_name, resp_json['name'])
self.assertEqual(FILE_CONTENT, resp_json['content'])
self.assertEqual(1, resp_json['version'])
self.assertEqual(NAMESPACE, resp_json['namespace'])
self._delete_code_source(mod_name)
def test_get_all_code_source(self):
def test_get_all(self):
mod_name = MODULE_NAME % 'getall'
mod2_name = MODULE_NAME % '2getall'
@ -152,13 +143,11 @@ class TestCodeSourcesController(base.APITest):
'/v2/code_sources',
params='namespace=%s' % NAMESPACE
)
resp_json = resp.json
self.assertEqual(200, resp.status_int)
code_sources = resp_json.get('code_sources')
code_sources = resp_json['code_sources']
self.assertEqual(2, len(code_sources))
self._delete_code_source(mod_name)
self._delete_code_source(mod2_name)

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from mistral.db.v2 import api as db_api
from mistral.services import actions
from mistral.tests.unit.api import base
@ -33,70 +34,49 @@ class DummyAction2(actions.Action):
def test(self, context):
return None"""
CREATE_REQUEST = """
-
name: dummy_action
class_name: DummyAction
code_source_id: {}
"""
UPDATE_REQUEST = """
dummy_action:
class_name: NewDummyAction
code_source_id: {}
"""
class TestDynamicActionsController(base.APITest):
def setUp(self):
super(TestDynamicActionsController, self).setUp()
resp = self._create_code_source().json
self.code_source_id = resp.get('code_sources')[0].get('id')
self.addCleanup(self._delete_code_source)
def _create_code_source(self):
return self.app.post(
'/v2/code_sources',
upload_files=[
('test_dummy_module', 'filename', TEST_MODULE_TEXT.encode())
],
resp = self.app.post(
'/v2/code_sources?name=test_dummy_module',
TEST_MODULE_TEXT
)
def _create_dynamic_action(self, body):
return self.app.post(
self.code_source_id = resp.json['id']
self.addCleanup(db_api.delete_code_sources)
self.addCleanup(db_api.delete_dynamic_action_definitions)
def _create_dynamic_action(self):
return self.app.post_json(
'/v2/dynamic_actions',
body,
content_type="text/plain"
{
'name': 'dummy_action',
'class_name': 'DummyAction',
'code_source_id': self.code_source_id
}
)
def _delete_code_source(self):
return self.app.delete('/v2/code_sources/test_dummy_module')
def test_create_dynamic_action(self):
resp = self._create_dynamic_action(
CREATE_REQUEST.format(self.code_source_id)
def test_post(self):
resp = self.app.post_json(
'/v2/dynamic_actions',
{
'name': 'dummy_action',
'class_name': 'DummyAction',
'code_source_id': self.code_source_id
}
)
# Check the structure of the response.
resp_json = resp.json
self.assertEqual(201, resp.status_int)
self.assertEqual(200, resp.status_int)
dyn_action = resp.json
dynamic_actions = resp_json.get('dynamic_actions')
self.assertEqual(1, len(dynamic_actions))
dynamic_action = dynamic_actions[0]
self.assertEqual('dummy_action', dynamic_action.get('name'))
self.assertEqual('DummyAction', dynamic_action.get('class_name'))
self.assertEqual(
self.code_source_id,
dynamic_action.get('code_source_id')
)
self.assertEqual('dummy_action', dyn_action['name'])
self.assertEqual('DummyAction', dyn_action['class_name'])
self.assertEqual(self.code_source_id, dyn_action['code_source_id'])
# Make sure the action can be found via the system action provider
# and it's fully functioning.
@ -112,52 +92,42 @@ class TestDynamicActionsController(base.APITest):
self.assertEqual("Hello from the dummy action 1!", action.run(None))
# Delete the action
self.app.delete('/v2/dynamic_actions/dummy_action')
def test_put(self):
resp = self._create_dynamic_action()
def test_update_dynamic_action(self):
self._create_dynamic_action(
CREATE_REQUEST.format(self.code_source_id)
)
self.assertEqual(201, resp.status_int)
resp = self.app.put(
resp = self.app.put_json(
'/v2/dynamic_actions',
UPDATE_REQUEST.format(self.code_source_id),
content_type="text/plain"
)
resp_json = resp.json
self.assertEqual(200, resp.status_int)
dynamic_actions = resp_json.get('dynamic_actions')
self.assertEqual(1, len(dynamic_actions))
dynamic_action = dynamic_actions[0]
self.assertEqual('dummy_action', dynamic_action.get('name'))
self.assertEqual('NewDummyAction', dynamic_action.get('class_name'))
self.assertEqual(
self.code_source_id,
dynamic_action.get('code_source_id')
)
self.app.delete('/v2/dynamic_actions/dummy_action')
def test_get_dynamic_action(self):
resp = self._create_dynamic_action(
CREATE_REQUEST.format(self.code_source_id)
{
'name': 'dummy_action',
'class_name': 'NewDummyAction'
}
)
self.assertEqual(200, resp.status_int)
self.app.delete('/v2/dynamic_actions/dummy_action')
dyn_action = resp.json
def test_get_all_dynamic_actions(self):
self._create_dynamic_action(
CREATE_REQUEST.format(self.code_source_id)
)
self.assertEqual('dummy_action', dyn_action['name'])
self.assertEqual('NewDummyAction', dyn_action['class_name'])
def test_get(self):
self._create_dynamic_action()
resp = self.app.get('/v2/dynamic_actions/dummy_action')
self.assertEqual(200, resp.status_int)
dyn_action = resp.json
self.assertEqual('dummy_action', dyn_action['name'])
self.assertEqual('DummyAction', dyn_action['class_name'])
self.assertEqual(self.code_source_id, dyn_action['code_source_id'])
self.assertEqual('test_dummy_module', dyn_action['code_source_name'])
def test_get_all(self):
self._create_dynamic_action()
resp = self.app.get('/v2/dynamic_actions')
@ -169,15 +139,11 @@ class TestDynamicActionsController(base.APITest):
self.assertEqual(1, len(dynamic_actions))
self.app.delete('/v2/dynamic_actions/dummy_action')
def test_delete_dynamic_action(self):
resp = self._create_dynamic_action(
CREATE_REQUEST.format(self.code_source_id)
)
def test_delete(self):
resp = self._create_dynamic_action()
# Check the structure of the response
self.assertEqual(200, resp.status_int)
self.assertEqual(201, resp.status_int)
resp = self.app.get('/v2/dynamic_actions/dummy_action')