Implement API call and RPC call for static actions
Static public methods can be called synchronously through the API call without creating environment, object instances and database records. It is proposed to make RPC call as the single request-responce for now. However async API and RPC calls may also be implemented later exploiting the same pattern as for calling instance methods. New call can be done through client method (see Ib6a60f8e33c5d3593a55db9f758e94e27f0a4445) Tempest and unit tests are added. APIImpact Implements: blueprint static-actions Change-Id: I17ab2eba0fd6c42309667f42d0644d21940ab02d
This commit is contained in:
parent
75bded129e
commit
7d186c191d
@ -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
|
Declaration of static methods and properties
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
|
|
||||||
Methods and properties are declared to be static by specifying ``Usage: Static``
|
Methods and properties are declared to be static by specifying
|
||||||
on them.
|
``Usage: Static`` on them.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@ -74,9 +74,10 @@ For example:
|
|||||||
|
|
||||||
Static properties are never initialized from object model but can be modified
|
Static properties are never initialized from object model but can be modified
|
||||||
from within MuranoPL code (i.e. they are not immutable).
|
from within MuranoPL code (i.e. they are not immutable).
|
||||||
Static methods cannot be executed as an `Action` from outside. Within static
|
Static methods also can be executed as an action from outside using
|
||||||
method `Body` ``$this`` (and ``$`` if not set to something else in expression)
|
``Scope: Public``. Within static method `Body` ``$this`` (and ``$`` if not
|
||||||
are set to type object rather than to instance, as it is for regular methods.
|
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
|
Static methods written in Python
|
||||||
|
@ -963,3 +963,77 @@ Json, describing action result is returned. Result type and value are provided.
|
|||||||
"isException": false,
|
"isException": false,
|
||||||
"result": ["item1", "item2"]
|
"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 request_statistics
|
||||||
from murano.api.v1 import services
|
from murano.api.v1 import services
|
||||||
from murano.api.v1 import sessions
|
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 template_applications
|
||||||
from murano.api.v1 import templates
|
from murano.api.v1 import templates
|
||||||
from murano.common import wsgi
|
from murano.common import wsgi
|
||||||
@ -206,6 +207,12 @@ class API(wsgi.Router):
|
|||||||
action='get_result',
|
action='get_result',
|
||||||
conditions={'method': ['GET']})
|
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()
|
catalog_resource = catalog.create_resource()
|
||||||
mapper.connect('/catalog/packages/categories',
|
mapper.connect('/catalog/packages/categories',
|
||||||
controller=catalog_resource,
|
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
|
self.server = None
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
endpoints = [TaskProcessingEndpoint()]
|
endpoints = [TaskProcessingEndpoint(), StaticActionEndpoint()]
|
||||||
|
|
||||||
transport = messaging.get_transport(CONF)
|
transport = messaging.get_transport(CONF)
|
||||||
s_target = target.Target('murano', 'tasks', server=str(uuid.uuid4()))
|
s_target = target.Target('murano', 'tasks', server=str(uuid.uuid4()))
|
||||||
@ -127,6 +127,25 @@ class TaskProcessingEndpoint(object):
|
|||||||
task_desc=jsonutils.dumps(result)))
|
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):
|
class TaskExecutor(object):
|
||||||
@property
|
@property
|
||||||
def action(self):
|
def action(self):
|
||||||
@ -288,3 +307,49 @@ class TaskExecutor(object):
|
|||||||
auth_utils.delete_trust(self._session.trust_id)
|
auth_utils.delete_trust(self._session.trust_id)
|
||||||
self._session.system_attributes['TrustId'] = None
|
self._session.system_attributes['TrustId'] = None
|
||||||
self._session.trust_id = 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):
|
def handle_task(self, task):
|
||||||
return self._client.cast({}, 'handle_task', task=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():
|
def api():
|
||||||
global TRANSPORT
|
global TRANSPORT
|
||||||
|
@ -77,6 +77,10 @@ class NoObjectFoundError(Exception):
|
|||||||
'Object "%s" is not found in object store' % object_id)
|
'Object "%s" is not found in object store' % object_id)
|
||||||
|
|
||||||
|
|
||||||
|
class MethodNotExposed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AmbiguousMethodName(Exception):
|
class AmbiguousMethodName(Exception):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super(AmbiguousMethodName, self).__init__(
|
super(AmbiguousMethodName, self).__init__(
|
||||||
|
@ -30,6 +30,7 @@ from murano.dsl import attribute_store
|
|||||||
from murano.dsl import constants
|
from murano.dsl import constants
|
||||||
from murano.dsl import dsl
|
from murano.dsl import dsl
|
||||||
from murano.dsl import dsl_types
|
from murano.dsl import dsl_types
|
||||||
|
from murano.dsl import exceptions as dsl_exceptions
|
||||||
from murano.dsl import helpers
|
from murano.dsl import helpers
|
||||||
from murano.dsl import object_store
|
from murano.dsl import object_store
|
||||||
from murano.dsl.principal_objects import stack_trace
|
from murano.dsl.principal_objects import stack_trace
|
||||||
@ -97,7 +98,8 @@ class MuranoDslExecutor(object):
|
|||||||
*args, **kwargs)
|
*args, **kwargs)
|
||||||
|
|
||||||
if context[constants.CTX_ACTIONS_ONLY] and not method.is_action:
|
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:
|
if method.is_static:
|
||||||
obj_context = self.create_object_context(
|
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
|
=: io.murano.apps
|
||||||
std: io.murano
|
std: io.murano
|
||||||
|
|
||||||
# Name: MockApp # use name from the manifest
|
# Write name into next line
|
||||||
|
Name: test_repository_class_xxxxxxxx
|
||||||
|
|
||||||
Extends: std:Application
|
Extends: std:Application
|
||||||
|
|
||||||
@ -11,13 +12,18 @@ Properties:
|
|||||||
userName:
|
userName:
|
||||||
Contract: $.string()
|
Contract: $.string()
|
||||||
|
|
||||||
|
greeting:
|
||||||
|
Usage: Static
|
||||||
|
Contract: $.string()
|
||||||
|
Default: 'Hello, '
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
testAction:
|
testAction:
|
||||||
Usage: Action
|
Scope: Public
|
||||||
Body:
|
Body:
|
||||||
- $this.find(std:Environment).reporter.report($this, 'Completed')
|
- $this.find(std:Environment).reporter.report($this, 'Completed')
|
||||||
getCredentials:
|
getCredentials:
|
||||||
Usage: Action
|
Scope: Public
|
||||||
Body:
|
Body:
|
||||||
- Return:
|
- Return:
|
||||||
credentials:
|
credentials:
|
||||||
@ -25,3 +31,20 @@ Methods:
|
|||||||
deploy:
|
deploy:
|
||||||
Body:
|
Body:
|
||||||
- $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit')
|
- $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))
|
resp, body = self.post(uri, json.dumps(body))
|
||||||
self.expected_success(200, resp.status)
|
self.expected_success(200, resp.status)
|
||||||
return self._parse_resp(body)
|
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,
|
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 a murano package
|
||||||
|
|
||||||
Composes package `app_name` with `manifest` file as a template for the
|
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
|
mfest_copy['Require'] = require
|
||||||
f.write(yaml.dump(mfest_copy, default_flow_style=False))
|
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'
|
name = app_name + '.zip'
|
||||||
|
|
||||||
if not archive_dir:
|
if not archive_dir:
|
||||||
@ -61,11 +69,12 @@ def compose_package(app_name, manifest, package_dir,
|
|||||||
return archive_path, name
|
return archive_path, name
|
||||||
|
|
||||||
|
|
||||||
def prepare_package(name, require=None):
|
def prepare_package(name, require=None, add_class_name=False):
|
||||||
"""Prepare package.
|
"""Prepare package.
|
||||||
|
|
||||||
:param name: Package name to compose
|
:param name: Package name to compose
|
||||||
:param require: Parameter 'require' for manifest
|
: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
|
:return: Path to archive, directory with archive, filename of archive
|
||||||
"""
|
"""
|
||||||
app_dir = acquire_package_directory()
|
app_dir = acquire_package_directory()
|
||||||
@ -73,7 +82,8 @@ def prepare_package(name, require=None):
|
|||||||
|
|
||||||
arc_path, filename = compose_package(
|
arc_path, filename = compose_package(
|
||||||
name, os.path.join(app_dir, 'manifest.yaml'),
|
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
|
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