rally-openstack/doc/specs/in-progress/raas.rst
Boris Pavlovic 4496b1480b Switch everything to rally db
Use `rally db` command instead of `rally-manage db` everywhere

Change-Id: Ief8613d60b0c0a763bd9bf3086d7225f1dfe6905
2017-08-29 13:25:41 -07:00

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,
              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.

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):
        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.

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.

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.

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:

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:

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