diff --git a/doc/specs/in-progress/raas.rst b/doc/specs/in-progress/raas.rst new file mode 100644 index 00000000..e14fb7ef --- /dev/null +++ b/doc/specs/in-progress/raas.rst @@ -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 `_ 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("", 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//`` + +, where + +* ```` is a version of API. We do not provide versioning of + API, so let's put "1" for now. +* ```` can be task, deployment, verification and etc +* ```` 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 + Hai Shi + + +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