Use `rally db` command instead of `rally-manage db` everywhere Change-Id: Ief8613d60b0c0a763bd9bf3086d7225f1dfe6905
11 KiB
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.
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, =False): abort_on_sla_failure= self.api.deployment._get(deployment) 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.
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.
import collections import requests from rally import exceptions class APIGroup(object): def _request(self, path, method, **kwargs): = request.request(method, path, json=kwargs) response if response.status_code != 200: raise exceptions.find_exception(response) # use OrderedDict by default for all cases return response.json( =collections.OrderedDict)["result"] object_pairs_hook
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.
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"): "path": "%s/%s" % (group, method.path), routes.append({"method": method.method, "handler": method}) return routes
Since we have custom data, errors and etc, we need custom preparation method too.
import json def dispatch(func, kwargs): """ :param func: method to call """ = {} response = 200 status_code try: "result"] = func(**kwargs) response[except Exception as e: = getattr(e, "http_code", 500) status_code "error"] = {"name": e.__name__, response["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 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.
import flask class Application(object): = "/api/v%(version)s/%(path)s" API_PATH_TEMPLATE def __init__(self, rapi): self.rapi = rapi self.app = flask.Flask("OpenStack Rally") self.app.add_url_rule("<path:path>", methods=["GET"], =self) view_funcself._routes = dict( % {"version": rapi.get_api_version(), [(PATH_TEMPLATE "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:
= dict((e.error_code, e) exception_map for e in RallyException.subclasses()) def find_exception(response): """Discover a proper exception class based on response object""" = exception_map.get(response.status_code, RallyException) exc_class = response.json()["error"] error_data 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:
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:
$ rally 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