Merge "Implement API call and RPC call for static actions"
This commit is contained in:
commit
0ec885a972
@ -83,3 +83,54 @@ Response:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
==============
|
||||
Static actions
|
||||
==============
|
||||
|
||||
Static methods (:ref:`static_methods_and_properties`) can also be called
|
||||
through the API if they are exposed by specifying ``Scope: Public``, and the
|
||||
result of its execution will be returned.
|
||||
|
||||
Consider the following example of the static action that makes use both of
|
||||
static class property and user's input as an argument:
|
||||
|
||||
::
|
||||
|
||||
Name: Bar
|
||||
|
||||
Properties:
|
||||
greeting:
|
||||
Usage: Static
|
||||
Contract: $.string()
|
||||
Default: 'Hello, '
|
||||
|
||||
Methods:
|
||||
staticAction:
|
||||
Scope: Public
|
||||
Usage: Static
|
||||
Arguments:
|
||||
- myName:
|
||||
Contract: $.string().notNull()
|
||||
Body:
|
||||
- Return: concat($.greeting, $myName)
|
||||
|
||||
Request:
|
||||
``http://localhost:8082/v1/actions``
|
||||
|
||||
Request body:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
{
|
||||
"className": "ns.Bar",
|
||||
"methodName": "staticAction",
|
||||
"parameters": {"myName": "John"}
|
||||
}
|
||||
|
||||
Responce:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
"Hello, John"
|
||||
|
@ -54,8 +54,8 @@ particular version of the class).
|
||||
Declaration of static methods and properties
|
||||
--------------------------------------------
|
||||
|
||||
Methods and properties are declared to be static by specifying ``Usage: Static``
|
||||
on them.
|
||||
Methods and properties are declared to be static by specifying
|
||||
``Usage: Static`` on them.
|
||||
|
||||
For example:
|
||||
|
||||
@ -74,9 +74,10 @@ For example:
|
||||
|
||||
Static properties are never initialized from object model but can be modified
|
||||
from within MuranoPL code (i.e. they are not immutable).
|
||||
Static methods cannot be executed as an `Action` from outside. Within static
|
||||
method `Body` ``$this`` (and ``$`` if not set to something else in expression)
|
||||
are set to type object rather than to instance, as it is for regular methods.
|
||||
Static methods also can be executed as an action from outside using
|
||||
``Scope: Public``. Within static method `Body` ``$this`` (and ``$`` if not
|
||||
set to something else in expression) are set to type object rather than to
|
||||
instance, as it is for regular methods.
|
||||
|
||||
|
||||
Static methods written in Python
|
||||
|
@ -963,3 +963,77 @@ Json, describing action result is returned. Result type and value are provided.
|
||||
"isException": false,
|
||||
"result": ["item1", "item2"]
|
||||
}
|
||||
|
||||
|
||||
Static Actions API
|
||||
==================
|
||||
|
||||
Static actions are MuranoPL methods that can be called on a MuranoPL class
|
||||
without deploying actual applications and usually return a result.
|
||||
|
||||
Execute a static action
|
||||
-----------------------
|
||||
|
||||
Invoke public static method of the specified MuranoPL class.
|
||||
Input parameters may be provided if method requires them.
|
||||
|
||||
*Request*
|
||||
|
||||
**Content-Type**
|
||||
application/json
|
||||
|
||||
+----------------+-----------------------------------------------------------+------------------------------------+
|
||||
| Method | URI | Header |
|
||||
+================+===========================================================+====================================+
|
||||
| POST | /actions | |
|
||||
+----------------+-----------------------------------------------------------+------------------------------------+
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"className": "my.class.fqn",
|
||||
"methodName": "myMethod",
|
||||
"packageName": "optional.package.fqn",
|
||||
"classVersion": "1.2.3",
|
||||
"parameters": {
|
||||
"arg1": "value1",
|
||||
"arg2": "value2"
|
||||
}
|
||||
}
|
||||
|
||||
+-----------------+------------+-----------------------------------------------------------------------------+
|
||||
| Attribute | Type | Description |
|
||||
+=================+============+=============================================================================+
|
||||
| className | string | Fully qualified name of MuranoPL class with static method |
|
||||
+-----------------+------------+-----------------------------------------------------------------------------+
|
||||
| methodName | string | Name of the method to invoke |
|
||||
+-----------------+------------+-----------------------------------------------------------------------------+
|
||||
| packageName | string | Fully qualified name of a package with the MuranoPL class (optional) |
|
||||
+-----------------+------------+-----------------------------------------------------------------------------+
|
||||
| classVersion | string | Class version specification, "=0" by default |
|
||||
+-----------------+------------+-----------------------------------------------------------------------------+
|
||||
| parameters | object | Key-value pairs of method parameter names and their values, "{}" by default |
|
||||
+-----------------+------------+-----------------------------------------------------------------------------+
|
||||
|
||||
*Response*
|
||||
|
||||
JSON-serialized result of the static method execution.
|
||||
|
||||
HTTP codes:
|
||||
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| Code | Description |
|
||||
+================+===========================================================+
|
||||
| 200 | OK. Action was executed successfully |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 400 | Bad request. The format of the body is invalid, method |
|
||||
| | doesn't match provided arguments, mandatory arguments are |
|
||||
| | not provided |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 403 | User is not allowed to execute the action |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 404 | Not found. Specified class, package or method doesn't |
|
||||
| | exist or method is not exposed |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 503 | Unhandled exception in the action |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
|
@ -22,6 +22,7 @@ from murano.api.v1 import instance_statistics
|
||||
from murano.api.v1 import request_statistics
|
||||
from murano.api.v1 import services
|
||||
from murano.api.v1 import sessions
|
||||
from murano.api.v1 import static_actions
|
||||
from murano.api.v1 import template_applications
|
||||
from murano.api.v1 import templates
|
||||
from murano.common import wsgi
|
||||
@ -206,6 +207,12 @@ class API(wsgi.Router):
|
||||
action='get_result',
|
||||
conditions={'method': ['GET']})
|
||||
|
||||
static_actions_resource = static_actions.create_resource()
|
||||
mapper.connect('/actions',
|
||||
controller=static_actions_resource,
|
||||
action='execute',
|
||||
conditions={'method': ['POST']})
|
||||
|
||||
catalog_resource = catalog.create_resource()
|
||||
mapper.connect('/catalog/packages/categories',
|
||||
controller=catalog_resource,
|
||||
|
75
murano/api/v1/static_actions.py
Normal file
75
murano/api/v1/static_actions.py
Normal file
@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# 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 oslo_messaging.rpc import client
|
||||
from webob import exc
|
||||
|
||||
from murano.common.i18n import _LE, _
|
||||
from murano.common import policy
|
||||
from murano.common import wsgi
|
||||
from murano.services import static_actions
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller(object):
|
||||
|
||||
def execute(self, request, body):
|
||||
policy.check("execute_action", request.context, {})
|
||||
|
||||
class_name = body.get('className')
|
||||
method_name = body.get('methodName')
|
||||
if not class_name or not method_name:
|
||||
msg = _('Class name and method name must be specified for '
|
||||
'static action')
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
|
||||
args = body.get('parameters')
|
||||
pkg_name = body.get('packageName')
|
||||
class_version = body.get('classVersion', '=0')
|
||||
|
||||
LOG.debug('StaticAction:Execute <MethodName: {0}, '
|
||||
'ClassName: {1}>'.format(method_name, class_name))
|
||||
|
||||
credentials = {
|
||||
'token': request.context.auth_token,
|
||||
'tenant_id': request.context.tenant
|
||||
}
|
||||
|
||||
try:
|
||||
return static_actions.StaticActionServices.execute(
|
||||
method_name, class_name, pkg_name, class_version, args,
|
||||
credentials)
|
||||
except client.RemoteError as e:
|
||||
LOG.error(_LE('Exception during call of the method {method_name}: '
|
||||
'{exc}').format(method_name=method_name, exc=str(e)))
|
||||
if e.exc_type in (
|
||||
'NoClassFound', 'NoMethodFound', 'NoPackageFound',
|
||||
'NoPackageForClassFound', 'MethodNotExposed',
|
||||
'NoMatchingMethodException'):
|
||||
raise exc.HTTPNotFound(e.value)
|
||||
elif e.exc_type == 'ContractViolationException':
|
||||
raise exc.HTTPBadRequest(e.value)
|
||||
raise exc.HTTPServiceUnavailable(e.value)
|
||||
except ValueError as e:
|
||||
LOG.error(_LE('Exception during call of the method {method_name}: '
|
||||
'{exc}').format(method_name=method_name, exc=str(e)))
|
||||
raise exc.HTTPBadRequest(e.message)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(Controller())
|
@ -57,7 +57,7 @@ class EngineService(service.Service):
|
||||
self.server = None
|
||||
|
||||
def start(self):
|
||||
endpoints = [TaskProcessingEndpoint()]
|
||||
endpoints = [TaskProcessingEndpoint(), StaticActionEndpoint()]
|
||||
|
||||
transport = messaging.get_transport(CONF)
|
||||
s_target = target.Target('murano', 'tasks', server=str(uuid.uuid4()))
|
||||
@ -127,6 +127,25 @@ class TaskProcessingEndpoint(object):
|
||||
task_desc=jsonutils.dumps(result)))
|
||||
|
||||
|
||||
class StaticActionEndpoint(object):
|
||||
@classmethod
|
||||
def call_static_action(cls, context, task):
|
||||
s_task = token_sanitizer.TokenSanitizer().sanitize(task)
|
||||
LOG.info(_LI('Starting execution of static action: '
|
||||
'{task_desc}').format(task_desc=jsonutils.dumps(s_task)))
|
||||
|
||||
result = None
|
||||
reporter = status_reporter.StatusReporter(task['id'])
|
||||
|
||||
try:
|
||||
task_executor = StaticActionExecutor(task, reporter)
|
||||
result = task_executor.execute()
|
||||
return result
|
||||
finally:
|
||||
LOG.info(_LI('Finished execution of static action: '
|
||||
'{task_desc}').format(task_desc=jsonutils.dumps(result)))
|
||||
|
||||
|
||||
class TaskExecutor(object):
|
||||
@property
|
||||
def action(self):
|
||||
@ -288,3 +307,49 @@ class TaskExecutor(object):
|
||||
auth_utils.delete_trust(self._session.trust_id)
|
||||
self._session.system_attributes['TrustId'] = None
|
||||
self._session.trust_id = None
|
||||
|
||||
|
||||
class StaticActionExecutor(object):
|
||||
@property
|
||||
def action(self):
|
||||
return self._action
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self._session
|
||||
|
||||
def __init__(self, task, reporter=None):
|
||||
if reporter is None:
|
||||
reporter = status_reporter.StatusReporter(task['id'])
|
||||
self._action = task['action']
|
||||
self._session = execution_session.ExecutionSession()
|
||||
self._session.token = task['token']
|
||||
self._session.project_id = task['tenant_id']
|
||||
self._reporter = reporter
|
||||
self._model_policy_enforcer = enforcer.ModelPolicyEnforcer(
|
||||
self._session)
|
||||
|
||||
def execute(self):
|
||||
with package_loader.CombinedPackageLoader(self._session) as pkg_loader:
|
||||
get_plugin_loader().register_in_loader(pkg_loader)
|
||||
executor = dsl_executor.MuranoDslExecutor(pkg_loader,
|
||||
ContextManager())
|
||||
action_result = self._invoke(executor)
|
||||
action_result = serializer.serialize(action_result, executor)
|
||||
return action_result
|
||||
|
||||
def _invoke(self, mpl_executor):
|
||||
class_name = self.action['class_name']
|
||||
pkg_name = self.action['pkg_name']
|
||||
class_version = self.action['class_version']
|
||||
version_spec = helpers.parse_version_spec(class_version)
|
||||
if pkg_name:
|
||||
package = mpl_executor.package_loader.load_package(
|
||||
pkg_name, version_spec)
|
||||
else:
|
||||
package = mpl_executor.package_loader.load_class_package(
|
||||
class_name, version_spec)
|
||||
cls = package.find_class(class_name, search_requirements=False)
|
||||
method_name, kwargs = self.action['method'], self.action['args']
|
||||
|
||||
return cls.invoke(method_name, mpl_executor, None, (), kwargs)
|
||||
|
@ -40,6 +40,9 @@ class EngineClient(object):
|
||||
def handle_task(self, task):
|
||||
return self._client.cast({}, 'handle_task', task=task)
|
||||
|
||||
def call_static_action(self, task):
|
||||
return self._client.call({}, 'call_static_action', task=task)
|
||||
|
||||
|
||||
def api():
|
||||
global TRANSPORT
|
||||
|
@ -77,6 +77,10 @@ class NoObjectFoundError(Exception):
|
||||
'Object "%s" is not found in object store' % object_id)
|
||||
|
||||
|
||||
class MethodNotExposed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousMethodName(Exception):
|
||||
def __init__(self, name):
|
||||
super(AmbiguousMethodName, self).__init__(
|
||||
|
@ -30,6 +30,7 @@ from murano.dsl import attribute_store
|
||||
from murano.dsl import constants
|
||||
from murano.dsl import dsl
|
||||
from murano.dsl import dsl_types
|
||||
from murano.dsl import exceptions as dsl_exceptions
|
||||
from murano.dsl import helpers
|
||||
from murano.dsl import object_store
|
||||
from murano.dsl.principal_objects import stack_trace
|
||||
@ -97,7 +98,8 @@ class MuranoDslExecutor(object):
|
||||
*args, **kwargs)
|
||||
|
||||
if context[constants.CTX_ACTIONS_ONLY] and not method.is_action:
|
||||
raise Exception('{0} is not an action'.format(method.name))
|
||||
raise dsl_exceptions.MethodNotExposed(
|
||||
'{0} is not an action'.format(method.name))
|
||||
|
||||
if method.is_static:
|
||||
obj_context = self.create_object_context(
|
||||
|
36
murano/services/static_actions.py
Normal file
36
murano/services/static_actions.py
Normal file
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
|
||||
import uuid
|
||||
|
||||
from murano.common import rpc
|
||||
|
||||
|
||||
class StaticActionServices(object):
|
||||
|
||||
@staticmethod
|
||||
def execute(method_name, class_name, pkg_name, class_version, args,
|
||||
credentials):
|
||||
action = {
|
||||
'method': method_name,
|
||||
'args': args or {},
|
||||
'class_name': class_name,
|
||||
'pkg_name': pkg_name,
|
||||
'class_version': class_version
|
||||
}
|
||||
task = {
|
||||
'action': action,
|
||||
'token': credentials['token'],
|
||||
'tenant_id': credentials['tenant_id'],
|
||||
'id': str(uuid.uuid4())
|
||||
}
|
||||
return rpc.engine().call_static_action(task)
|
65
murano/tests/unit/api/v1/test_static_actions.py
Normal file
65
murano/tests/unit/api/v1/test_static_actions.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
import mock
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from murano.api.v1 import static_actions
|
||||
from murano.common import policy
|
||||
import murano.tests.unit.api.base as tb
|
||||
|
||||
|
||||
@mock.patch.object(policy, 'check')
|
||||
class TestStaticActionsApi(tb.ControllerTest, tb.MuranoApiTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestStaticActionsApi, self).setUp()
|
||||
self.controller = static_actions.Controller()
|
||||
|
||||
def test_execute_static_action(self, mock_policy_check):
|
||||
"""Test that action execution results in the correct rpc call."""
|
||||
self._set_policy_rules(
|
||||
{'execute_action': '@'}
|
||||
)
|
||||
|
||||
action = {
|
||||
'method': 'TestAction',
|
||||
'args': {'name': 'John'},
|
||||
'class_name': 'TestClass',
|
||||
'pkg_name': 'TestPackage',
|
||||
'class_version': '=0'
|
||||
}
|
||||
rpc_task = {
|
||||
'action': action,
|
||||
'token': None,
|
||||
'tenant_id': 'test_tenant',
|
||||
'id': mock.ANY
|
||||
}
|
||||
|
||||
request_data = {
|
||||
"className": 'TestClass',
|
||||
"methodName": 'TestAction',
|
||||
"packageName": 'TestPackage',
|
||||
"classVersion": '=0',
|
||||
"parameters": {'name': 'John'}
|
||||
}
|
||||
req = self._post('/actions', jsonutils.dump_as_bytes(request_data))
|
||||
try:
|
||||
self.controller.execute(req, request_data)
|
||||
except TypeError:
|
||||
pass
|
||||
self.mock_engine_rpc.call_static_action.assert_called_once_with(
|
||||
rpc_task)
|
@ -2,7 +2,8 @@ Namespaces:
|
||||
=: io.murano.apps
|
||||
std: io.murano
|
||||
|
||||
# Name: MockApp # use name from the manifest
|
||||
# Write name into next line
|
||||
Name: test_repository_class_xxxxxxxx
|
||||
|
||||
Extends: std:Application
|
||||
|
||||
@ -11,13 +12,18 @@ Properties:
|
||||
userName:
|
||||
Contract: $.string()
|
||||
|
||||
greeting:
|
||||
Usage: Static
|
||||
Contract: $.string()
|
||||
Default: 'Hello, '
|
||||
|
||||
Methods:
|
||||
testAction:
|
||||
Usage: Action
|
||||
Scope: Public
|
||||
Body:
|
||||
- $this.find(std:Environment).reporter.report($this, 'Completed')
|
||||
getCredentials:
|
||||
Usage: Action
|
||||
Scope: Public
|
||||
Body:
|
||||
- Return:
|
||||
credentials:
|
||||
@ -25,3 +31,20 @@ Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit')
|
||||
|
||||
staticAction:
|
||||
Scope: Public
|
||||
Usage: Static
|
||||
Arguments:
|
||||
- myName:
|
||||
Contract: $.string().notNull()
|
||||
Body:
|
||||
- Return: concat($.greeting, $myName)
|
||||
|
||||
staticNotAction:
|
||||
Usage: Static
|
||||
Arguments:
|
||||
- myName:
|
||||
Contract: $.string().notNull()
|
||||
Body:
|
||||
- Return: concat($.greeting, $myName)
|
||||
|
@ -365,3 +365,22 @@ class ApplicationCatalogClient(rest_client.RestClient):
|
||||
resp, body = self.post(uri, json.dumps(body))
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
# ----------------------------Static action methods----------------------------
|
||||
def call_static_action(self, class_name=None, method_name=None, args=None,
|
||||
package_name=None, class_version="=0"):
|
||||
uri = 'v1/actions'
|
||||
post_body = {
|
||||
'parameters': args or {},
|
||||
'packageName': package_name,
|
||||
'classVersion': class_version
|
||||
}
|
||||
if class_name:
|
||||
post_body['className'] = class_name
|
||||
if method_name:
|
||||
post_body['methodName'] = method_name
|
||||
|
||||
resp, body = self.post(uri, json.dumps(post_body))
|
||||
self.expected_success(200, resp.status)
|
||||
# _parse_resp() cannot be used because body is expected to be string
|
||||
return body
|
||||
|
@ -0,0 +1,65 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import testtools
|
||||
|
||||
from tempest import config
|
||||
|
||||
from murano_tempest_tests.tests.api.application_catalog import base
|
||||
from murano_tempest_tests import utils
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class TestStaticActions(base.BaseApplicationCatalogTest):
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
if CONF.application_catalog.glare_backend:
|
||||
msg = ("Murano using GLARE backend. "
|
||||
"Static actions tests will be skipped.")
|
||||
raise cls.skipException(msg)
|
||||
|
||||
super(TestStaticActions, cls).resource_setup()
|
||||
|
||||
application_name = utils.generate_name('test_repository_class')
|
||||
cls.abs_archive_path, dir_with_archive, archive_name = \
|
||||
utils.prepare_package(application_name, add_class_name=True)
|
||||
cls.package = cls.application_catalog_client.upload_package(
|
||||
application_name, archive_name, dir_with_archive,
|
||||
{"categories": [], "tags": [], 'is_public': False})
|
||||
|
||||
@classmethod
|
||||
def resource_cleanup(cls):
|
||||
super(TestStaticActions, cls).resource_cleanup()
|
||||
os.remove(cls.abs_archive_path)
|
||||
cls.application_catalog_client.delete_package(cls.package['id'])
|
||||
|
||||
@testtools.testcase.attr('smoke')
|
||||
def test_call_static_action_basic(self):
|
||||
action_result = self.application_catalog_client.call_static_action(
|
||||
class_name=self.package['class_definitions'][0],
|
||||
method_name='staticAction',
|
||||
args={'myName': 'John'})
|
||||
self.assertEqual('"Hello, John"', action_result)
|
||||
|
||||
@testtools.testcase.attr('smoke')
|
||||
def test_call_static_action_full(self):
|
||||
action_result = self.application_catalog_client.call_static_action(
|
||||
class_name=self.package['class_definitions'][0],
|
||||
method_name='staticAction',
|
||||
package_name=self.package['fully_qualified_name'],
|
||||
class_version="<1", args={'myName': 'John'})
|
||||
self.assertEqual('"Hello, John"', action_result)
|
@ -0,0 +1,103 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import testtools
|
||||
|
||||
from tempest import config
|
||||
from tempest.lib import exceptions
|
||||
|
||||
from murano_tempest_tests.tests.api.application_catalog import base
|
||||
from murano_tempest_tests import utils
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class TestStaticActionsNegative(base.BaseApplicationCatalogTest):
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
if CONF.application_catalog.glare_backend:
|
||||
msg = ("Murano using GLARE backend. "
|
||||
"Static actions tests will be skipped.")
|
||||
raise cls.skipException(msg)
|
||||
|
||||
super(TestStaticActionsNegative, cls).resource_setup()
|
||||
|
||||
application_name = utils.generate_name('test_repository_class')
|
||||
cls.abs_archive_path, dir_with_archive, archive_name = \
|
||||
utils.prepare_package(application_name, add_class_name=True)
|
||||
cls.package = cls.application_catalog_client.upload_package(
|
||||
application_name, archive_name, dir_with_archive,
|
||||
{"categories": [], "tags": [], 'is_public': False})
|
||||
|
||||
@classmethod
|
||||
def resource_cleanup(cls):
|
||||
super(TestStaticActionsNegative, cls).resource_cleanup()
|
||||
os.remove(cls.abs_archive_path)
|
||||
cls.application_catalog_client.delete_package(cls.package['id'])
|
||||
|
||||
@testtools.testcase.attr('negative')
|
||||
def test_call_static_action_no_args(self):
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.application_catalog_client.call_static_action)
|
||||
|
||||
@testtools.testcase.attr('negative')
|
||||
def test_call_static_action_wrong_class(self):
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
self.application_catalog_client.call_static_action,
|
||||
'wrong.class', 'staticAction',
|
||||
args={'myName': 'John'})
|
||||
|
||||
@testtools.testcase.attr('negative')
|
||||
def test_call_static_action_wrong_method(self):
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
self.application_catalog_client.call_static_action,
|
||||
class_name=self.package['class_definitions'][0],
|
||||
method_name='wrongMethod',
|
||||
args={'myName': 'John'})
|
||||
|
||||
@testtools.testcase.attr('negative')
|
||||
def test_call_static_action_session_method(self):
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
self.application_catalog_client.call_static_action,
|
||||
class_name=self.package['class_definitions'][0],
|
||||
method_name='staticNotAction',
|
||||
args={'myName': 'John'})
|
||||
|
||||
@testtools.testcase.attr('negative')
|
||||
def test_call_static_action_wrong_args(self):
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.application_catalog_client.call_static_action,
|
||||
class_name=self.package['class_definitions'][0],
|
||||
method_name='staticAction',
|
||||
args={'myEmail': 'John'})
|
||||
|
||||
@testtools.testcase.attr('negative')
|
||||
def test_call_static_action_wrong_package(self):
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
self.application_catalog_client.call_static_action,
|
||||
class_name=self.package['class_definitions'][0],
|
||||
method_name='staticAction',
|
||||
package_name='wrong.package',
|
||||
args={'myName': 'John'})
|
||||
|
||||
@testtools.testcase.attr('negative')
|
||||
def test_call_static_action_wrong_version_format(self):
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.application_catalog_client.call_static_action,
|
||||
class_name=self.package['class_definitions'][0],
|
||||
method_name='staticAction',
|
||||
class_version='aaa',
|
||||
args={'myName': 'John'})
|
@ -25,7 +25,7 @@ MANIFEST = {'Format': 'MuranoPL/1.0',
|
||||
|
||||
|
||||
def compose_package(app_name, manifest, package_dir,
|
||||
require=None, archive_dir=None):
|
||||
require=None, archive_dir=None, add_class_name=False):
|
||||
"""Composes a murano package
|
||||
|
||||
Composes package `app_name` with `manifest` file as a template for the
|
||||
@ -44,6 +44,14 @@ def compose_package(app_name, manifest, package_dir,
|
||||
mfest_copy['Require'] = require
|
||||
f.write(yaml.dump(mfest_copy, default_flow_style=False))
|
||||
|
||||
if add_class_name:
|
||||
class_file = os.path.join(package_dir, 'Classes', 'mock_muranopl.yaml')
|
||||
with open(class_file, 'r+') as f:
|
||||
line = ''
|
||||
while line != '# Write name into next line\n':
|
||||
line = f.readline()
|
||||
f.write('Name: {0}'.format(app_name))
|
||||
|
||||
name = app_name + '.zip'
|
||||
|
||||
if not archive_dir:
|
||||
@ -61,11 +69,12 @@ def compose_package(app_name, manifest, package_dir,
|
||||
return archive_path, name
|
||||
|
||||
|
||||
def prepare_package(name, require=None):
|
||||
def prepare_package(name, require=None, add_class_name=False):
|
||||
"""Prepare package.
|
||||
|
||||
:param name: Package name to compose
|
||||
:param require: Parameter 'require' for manifest
|
||||
:param add_class_name: Option to write class name to class file
|
||||
:return: Path to archive, directory with archive, filename of archive
|
||||
"""
|
||||
app_dir = acquire_package_directory()
|
||||
@ -73,7 +82,8 @@ def prepare_package(name, require=None):
|
||||
|
||||
arc_path, filename = compose_package(
|
||||
name, os.path.join(app_dir, 'manifest.yaml'),
|
||||
app_dir, require=require, archive_dir=target_arc_path)
|
||||
app_dir, require=require, archive_dir=target_arc_path,
|
||||
add_class_name=add_class_name)
|
||||
return arc_path, target_arc_path, filename
|
||||
|
||||
|
||||
|
7
releasenotes/notes/static-actions-61759be796299039.yaml
Normal file
7
releasenotes/notes/static-actions-61759be796299039.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- Static public methods can be called synchronously through the API call
|
||||
"http://murano-url:port/v1/actions" specifying class name, method name,
|
||||
method arguments and optionally package name and class version in the
|
||||
request body. This call does not create environment, object instances and
|
||||
database records.
|
Loading…
Reference in New Issue
Block a user