Propose a spec for Rally-as-a-Service
This spec covers a bunch of proposed patches: * https://review.openstack.org/#/c/336636/ * https://review.openstack.org/#/c/182245/ * https://review.openstack.org/#/c/279194/ Change-Id: I60b4b9a2eba140828b938814db8aba3ac7d78eef
This commit is contained in:
parent
680995e738
commit
1234ab5bca
360
doc/specs/in-progress/raas.rst
Normal file
360
doc/specs/in-progress/raas.rst
Normal file
@ -0,0 +1,360 @@
|
||||
..
|
||||
This work is licensed under a Creative Commons Attribution 3.0 Unported
|
||||
License.
|
||||
|
||||
http://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
||||
..
|
||||
This template should be in ReSTructured text. The filename in the git
|
||||
repository should match the launchpad URL, for example a URL of
|
||||
https://blueprints.launchpad.net/heat/+spec/awesome-thing should be named
|
||||
awesome-thing.rst . Please do not delete any of the sections in this
|
||||
template. If you have nothing to say for a whole section, just write: None
|
||||
For help with syntax, see http://sphinx-doc.org/rest.html
|
||||
To test out your formatting, see http://www.tele3.cz/jbar/rest/rest.html
|
||||
|
||||
==================
|
||||
Rally-as-a-Service
|
||||
==================
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
Having Rally Web Service that gives access to Rally functionality via HTTP is a
|
||||
highly desired feature.
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
Enhance Rally API
|
||||
-----------------
|
||||
|
||||
Using Rally as a library (python client) seems to be a convenient way to
|
||||
automate its usage in different applications. The full power of Rally, however,
|
||||
can be now accessed only through its command-line interface.
|
||||
The current Rally API is not powerful enough to be used for Rally-as-a-Service.
|
||||
|
||||
Move all features from CLI to API
|
||||
"""""""""""""""""""""""""""""""""
|
||||
|
||||
Rally API should provide the same features which are available in CLI.
|
||||
|
||||
To achieve that all direct DB calls and Rally objects should be removed from
|
||||
CLI layer. The CLI implementation should be restricted to pure API method
|
||||
calls, and the API should cover all stuff that is needed for CLI (processing
|
||||
results, making reports, etc.).
|
||||
|
||||
Make API return serializable objects
|
||||
""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Rally API should always return something that can be easily serialized and sent
|
||||
over HTTP. It is required change, since we do not want to duplicate code which
|
||||
is used by CLI and which will be used by Rally-as-a-Service. Both of these
|
||||
entities should wrap the same thing - Rally API.
|
||||
|
||||
Move from a classmethod model to a instancemethod model in the API
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Each of API method should not be a single function - classmethod.
|
||||
The instancemethod model should establish a right way of communication between
|
||||
different API methods and provide an access to API preferences.
|
||||
|
||||
Also, it would be nice to create a base class for single API group.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class APIGroup(object):
|
||||
def __init__(self, api):
|
||||
"""Initialize API group.
|
||||
|
||||
:param api: an instance of rally.api.API object
|
||||
"""
|
||||
self.api = api
|
||||
|
||||
class _Task(APIGroup):
|
||||
def start(self, deployment, config, task=None,
|
||||
abort_on_sla_failure=False):
|
||||
deployment = self.api.deployment._get(deployment)
|
||||
|
||||
...
|
||||
|
||||
|
||||
Wrap each API method
|
||||
""""""""""""""""""""
|
||||
|
||||
Since usage of the API via HTTP should be similar to the direct usage, we need
|
||||
to wrap each of API methods by the specific decorator which will decide to send
|
||||
a http request or make a direct call to the API.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from rally import exceptions
|
||||
|
||||
def api_wrapper(path, method):
|
||||
def decorator(func)
|
||||
def inner(self, *args, **kwargs):
|
||||
if args:
|
||||
raise TypeError("It is restricted to use positional
|
||||
arguments for API calls.")
|
||||
|
||||
if self.api.endpoint_url:
|
||||
# it's a call to the remote Rally instance
|
||||
return self._request(path, method, **kwargs)
|
||||
else:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# NOTE(andreykurilin): we need to use the same error
|
||||
# handling things as it is done in dispatcher, so
|
||||
# one error will have the same representation in
|
||||
# both cases - via direct use and via HTTP
|
||||
raise exceptions.make_exception(e)
|
||||
|
||||
|
||||
inner.path = path
|
||||
inner.method = method
|
||||
|
||||
return inner
|
||||
return decorator
|
||||
|
||||
|
||||
The specific ``_request`` method for handling all communication details,
|
||||
serialization and errors should be implemented in the common class APIGroup.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import collections
|
||||
import requests
|
||||
|
||||
from rally import exceptions
|
||||
|
||||
class APIGroup(object):
|
||||
|
||||
def _request(self, path, method, **kwargs):
|
||||
response = request.request(method, path, json=kwargs)
|
||||
if response.status_code != 200:
|
||||
raise exceptions.find_exception(response)
|
||||
|
||||
# use OrderedDict by default for all cases
|
||||
return response.json(
|
||||
object_pairs_hook=collections.OrderedDict)["result"]
|
||||
|
||||
|
||||
Rally-as-a-Service implementation
|
||||
---------------------------------
|
||||
|
||||
The code base of Rally-as-a-Service should be located in ``rally.aas`` module.
|
||||
|
||||
The application should discover all API methods and check their properties to
|
||||
identify methods that should be available via HTTP.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from rally import api
|
||||
|
||||
def discover_routes(rapi):
|
||||
"""
|
||||
|
||||
:param rapi: an instance of rally.api.API
|
||||
"""
|
||||
|
||||
routes = []
|
||||
for group, obj in vars(rapi)):
|
||||
if not isinstance(obj, APIGroup):
|
||||
continue
|
||||
|
||||
for name, method in vars(obj):
|
||||
if name.startswith("_"):
|
||||
# do not touch private methods
|
||||
continue
|
||||
if hasattr(method, "path") and hasattr(method, "method"):
|
||||
routes.append({"path": "%s/%s" % (group, method.path),
|
||||
"method": method.method,
|
||||
"handler": method})
|
||||
return routes
|
||||
|
||||
|
||||
Since we have custom data, errors and etc, we need custom preparation method
|
||||
too.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import json
|
||||
|
||||
def dispatch(func, kwargs):
|
||||
"""
|
||||
:param func: method to call
|
||||
"""
|
||||
response = {}
|
||||
status_code = 200
|
||||
try:
|
||||
response["result"] = func(**kwargs)
|
||||
except Exception as e:
|
||||
status_code = getattr(e, "http_code", 500)
|
||||
response["error"] = {"name": e.__name__,
|
||||
"msg": str(e),
|
||||
"args": getattr(a, args)}
|
||||
return json.dumps(response, sort_keys=False), status_code
|
||||
|
||||
|
||||
|
||||
Most of the routing and dispatching things will be done via our specific
|
||||
methods and decorators, so our requirements to web framework are simple - we do
|
||||
not need much from it.
|
||||
|
||||
Let's start from `Flask <http://flask.pocoo.org/>`_ web framework. It is quite
|
||||
simple, lightweight and compatible with WSGI. In future, it should not be too
|
||||
difficult to switch from it.
|
||||
|
||||
Since there are a lot of blocking calls in Rally, only read-only methods (
|
||||
"GET" method type) should be allowed at first implementation of
|
||||
Rally-as-a-Service.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import flask
|
||||
|
||||
|
||||
class Application(object):
|
||||
|
||||
API_PATH_TEMPLATE = "/api/v%(version)s/%(path)s"
|
||||
|
||||
def __init__(self, rapi):
|
||||
self.rapi = rapi
|
||||
self.app = flask.Flask("OpenStack Rally")
|
||||
self.app.add_url_rule("<path:path>", methods=["GET"],
|
||||
view_func=self)
|
||||
self._routes = dict(
|
||||
[(PATH_TEMPLATE % {"version": rapi.get_api_version(),
|
||||
"path": path}, handler)
|
||||
for path, handler in discover_routes().items()])
|
||||
|
||||
def __call__(self, path):
|
||||
if path not in self._routes:
|
||||
# redirect to 404
|
||||
return dispatch(self._routes[path], flask.request.data)
|
||||
|
||||
|
||||
def start(self, ip, port):
|
||||
self.app.start(ip, port)
|
||||
|
||||
|
||||
Routing convention
|
||||
""""""""""""""""""
|
||||
|
||||
The routes for each API method should match next format:
|
||||
|
||||
``/api/v<VERSION_OF_API>/<API_GROUP>/<METHOD_NAME>``
|
||||
|
||||
, where
|
||||
|
||||
* ``<VERSION_OF_API>`` is a version of API. We do not provide versioning of
|
||||
API, so let's put "1" for now.
|
||||
* ``<API_GROUP>`` can be task, deployment, verification and etc
|
||||
* ``<METHOD_NAME>`` should represent the name of method to call.
|
||||
|
||||
Example of possible path: ``/api/v1/task/validate``
|
||||
|
||||
Exception refactoring
|
||||
---------------------
|
||||
|
||||
To make existing exception classes from ``rally.exceptions`` module usable in
|
||||
case of RaaS, they should:
|
||||
|
||||
* store initialization arguments, so it will be possible to re-create object
|
||||
* contain error code as a property.
|
||||
|
||||
Serialization/De-serialization of exceptions
|
||||
""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Exceptions should serializable as other return data. Serialization mechanism is
|
||||
described with ``dispatch`` method.
|
||||
|
||||
De-serialization should look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
exception_map = dict((e.error_code, e)
|
||||
for e in RallyException.subclasses())
|
||||
|
||||
def find_exception(response):
|
||||
"""Discover a proper exception class based on response object"""
|
||||
exc_class = exception_map.get(response.status_code, RallyException)
|
||||
error_data = response.json()["error"]
|
||||
if error_data["args"]:
|
||||
return exc_class(error_data["args"])
|
||||
return exc_class(error_data["msg"])
|
||||
|
||||
|
||||
As it was mentioned previously, exception objects should be the same in case of
|
||||
direct and HTTP communications. To make it possible specific check function
|
||||
should be implemented like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def make_exception(exc):
|
||||
"""Check a class of exception and convert it to rally-like if needed"""
|
||||
if isinstance(exc, RallyException):
|
||||
return exc
|
||||
return RallyException(str(exc))
|
||||
|
||||
|
||||
Command Line Interface
|
||||
----------------------
|
||||
|
||||
CLI should be extended by specific global argument ``--endpoint-url`` for
|
||||
using remote mode.
|
||||
|
||||
Rally-as-a-Service itself should be started via new command:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ rally-manage service start
|
||||
|
||||
Rally Web Portal
|
||||
----------------
|
||||
|
||||
Web Portal for Rally can be a good addition. It's implementation can be done
|
||||
on the top of Rally-as-a-Service which should handle all HTTP stuff.
|
||||
|
||||
Since read-only mode of RaaS will be enable from first stages, Web Portal
|
||||
can be started from providing tables with results of Tasks, Verifications. That
|
||||
tables should be able to filter results by different fields (tags, time,
|
||||
deployment, etc.) and make regular or trends reports for selected results.
|
||||
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
n/a
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Primary assignee(s):
|
||||
|
||||
Andrey Kurilin <andr.kurilin@gmail.com>
|
||||
Hai Shi <shihai1992@gmail.com>
|
||||
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
* Make return data of Verify/Verification API serializable
|
||||
* Make return data of Task API serializable
|
||||
* Make return data of Deployment API serializable
|
||||
* Implement the base class for API groups and port Deployment, Task, Verify,
|
||||
Verification APIs on it
|
||||
* Refactor exceptions
|
||||
* Implement `api_wrapper` decorator and wrap all methods of each API groups
|
||||
* Implement base logic for as-a-Service
|
||||
* Extend CLI
|
||||
* Add simple pages for Web Portal
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
n/a
|
Loading…
Reference in New Issue
Block a user