From: Ricardo Dias Date: Thu, 24 May 2018 16:10:41 +0000 (+0100) Subject: mgr/dashboard: hacking.rst: controller creation instructions update X-Git-Tag: v14.0.1~1262^2~1 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=2ccb2dff0ddbaf2d6d523d4778a52ebdf66207ab;p=ceph.git mgr/dashboard: hacking.rst: controller creation instructions update Signed-off-by: Ricardo Dias --- diff --git a/src/pybind/mgr/dashboard/HACKING.rst b/src/pybind/mgr/dashboard/HACKING.rst index 3f3c414cffbce..ec6ea77387f0c 100644 --- a/src/pybind/mgr/dashboard/HACKING.rst +++ b/src/pybind/mgr/dashboard/HACKING.rst @@ -245,48 +245,191 @@ Or, ``source`` the script and run the tests manually:: How to add a new controller? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you want to add a new endpoint to the backend, you just need to add a -class derived from ``BaseController`` decorated with ``ApiController`` in a -Python file located under the ``controllers`` directory. The Dashboard module -will automatically load your new controller upon start. +A controller is a Python class that extends from the ``BaseController`` class +and is decorated with either the ``@Controller`` or ``@ApiController`` +decorators. The Python class must be stored inside a Python file located under +the ``controllers`` directory. The Dashboard module will automatically load +your new controller upon start. -For example create a file ``ping2.py`` under ``controllers`` directory with the -following code:: +The ``@ApiController`` decorator is a specialization of the ``@Controller`` +decorator, and should be used for controllers that provide an API-like REST +interface. For any other kinds of controllers the ``@Controller`` decorator +should be used. - import cherrypy - from ..tools import ApiController, BaseController +A controller has a URL prefix path associated that is specified in the +controller decorator, and all endpoints exposed by the controller will share +the same URL prefix path. - @ApiController('ping2') - class Ping2(BaseController): - @cherrypy.expose - def default(self, *args): - return "Hello" +A controller's endpoint is exposed by implementing a method on the controller +class decorated with the ``@Endpoint`` decorator. -Every path given in the ``ApiController`` decorator will automatically be -prefixed with ``api``. After reloading the Dashboard module you can access the -above mentioned controller by pointing your browser to -https://mgr_hostname:8080/api/ping2. +For example create a file ``ping.py`` under ``controllers`` directory with the +following code: -It is also possible to have nested controllers. The ``RgwController`` uses -this technique to make the daemons available through the URL -https://mgr_hostname:8080/api/rgw/daemon:: +.. code-block:: python - @ApiController('rgw') - @AuthRequired() - class Rgw(RESTController): - pass + from ..tools import Controller, ApiController, BaseController, Endpoint + + @Controller('/ping') + class Ping(BaseController): + @Endpoint() + def hello(self): + return {msg: "Hello"} + + @ApiController('/ping') + class ApiPing(BaseController): + @Endpoint() + def hello(self): + return {msg: "Hello"} + + +The ``hello`` endpoint of the ``Ping`` controller can be reached by the +following URL: https://mgr_hostname:8080/ping/hello using HTTP GET requests. +As you can see the controller URL path ``/ping`` is concatenated to the +method name ``hello`` to generate the endpoint's URL. + +In the case of the ``ApiPing`` controller, the ``hello`` endpoint can be +reached by the following URL: https://mgr_hostname:8080/api/ping/hello using +HTTP GET request. +The API controller URL path ``/ping`` is prefixed by the ``/api`` path and then +concatenated to the method name ``hello`` to generate the endpoint's URL. +Internally, the ``@ApiController`` is actually calling the ``@Controller`` +decorator by passing an additional decorator parameter called ``base_url``:: + + @ApiController('/ping') <=> @Controller('/ping', base_url="/api") + +The ``@Endpoint`` decorator also supports many parameters to customize the +endpoint: + +* ``method="GET"``: the HTTP method allowed to access this endpoint. +* ``path="/"``: the URL path of the endpoint, excluding the + controller URL path prefix. +* ``path_params=[]``: list of method parameter names that correspond to URL + path parameters. Can only be used when ``method in ['POST', 'PUT']``. +* ``query_params=[]``: list of method parameter names that correspond to URL + query parameters. +* ``json_response=True``: indicates if the endpoint response should be + serialized in JSON format. +* ``proxy=False``: indicates if the endpoint should be used as a proxy. + +An endpoint method may have parameters declared. Depending on the HTTP method +defined for the endpoint the method parameters might be considered either +path parameters, query parameters, or body parameters. + +For ``GET`` and ``DELETE`` methods, the method non-optional parameters are +considered path parameters by default. Optional parameters are considered +query parameters. By specifing the ``query_parameters`` in the endpoint +decorator it is possible to make a non-optional parameter to be a query +parameter. + +For ``POST`` and ``PUT`` methods, all method parameters are considered +body parameters by default. To override this default, one can use the +``path_params`` and ``query_params`` to specify which method parameters are +path and query parameters respectivelly. +Body parameters are decoded from the request body, either from a form format, or +from a dictionary in JSON format. + +Let's use an example to better understand the possible ways to custumize an +endpoint: +.. code-block:: python - @ApiController('rgw/daemon') - @AuthRequired() - class RgwDaemon(RESTController): + from ..tools import Controller, BaseController, Endpoint - def list(self): - pass + @Controller('/ping') + class Ping(BaseController): + + # URL: /ping/{key}?opt1=...&opt2=... + @Endpoint(path="/", query_params=['opt1']) + def index(self, key, opt1, opt2=None): + # ... + + # URL: /ping/{key}?opt1=...&opt2=... + @Endpoint(query_params=['opt1']) + def __call__(self, key, opt1, opt2=None): + # ... + + # URL: /ping/post/{key1}/{key2} + @Endpoint('POST', path_params=['key1', 'key2']) + def post(self, key1, key2, data1, data2=None): + # ... + +From the above example you can see that the path parameters are collected from +the URL by parsing the list of values separated by slashes ``/`` that come +after the URL path ``/ping`` for ``index`` method case, and ``/ping/post`` for +the ``post`` method case. + +In the above example we also see how the ``path`` option can be used to +override the generated endpoint URL in order to not use the method name in the +URL. In the ``index`` method we set the ``path`` to ``"/"`` to generate an +endpoint that is accessible by the root URL of the controller. + +An alternative approach to generate an endpoint that is accessible through just +the controller's path URL is by using the ``__call__`` method, as we show in +the above example. + +Defining path parameters in endpoints's URLs using python methods's parameters +is very easy but it is still a bit strict with respect to the position of these +parameters in the URL structure. +Sometimes we may want to explictly define a URL scheme that +contains path parameters mixed with static parts of the URL. +Our controller infrastructure also supports the declaration of URL paths with +explicit path parameters at both the controller level and method level. + +Consider the following example: + +.. code-block:: python + + from ..tools import Controller, BaseController, Endpoint + + @Controller('/ping/{node}/stats') + class Ping(BaseController): + + # URL: /ping/{node}/stats/{date}/latency?unit=... + @Endpoint(path="/{date}/latency") + def latency(self, node, date, unit="ms"): + # ... + +In this example we explicitly declare a path parameter ``{node}`` in the +controller URL path, and a path parameter ``{date}`` in the ``latency`` +method. The endpoint for the ``latency`` method is then accessible through +the URL: https://mgr_hostname:8080/ping/{node}/stats/{date}/latency . + +For a full set of examples on how to use the ``@Endpoint`` +decorator please check the unit test file: ``tests/test_controllers.py``. +There you will find many examples of how to customize endpoint methods. + + +Implementing Proxy Controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you might need to relay some requests from the Dashboard frontend +directly to an external service. +For that purpose we provide a decorator called ``@Proxy``. +(As a concrete example, check the ``controllers/rgw.py`` file where we +implemented an RGW Admin Ops proxy.) + + +The ``@Proxy`` decorator is a wrapper of the ``@Endpoint`` decorator that +already customizes the endpoint for working as a proxy. +A proxy endpoint works by capturing the URL path that follows the controller +URL prefix path, and does not do any decoding of the request body. + +Example: + +.. code-block:: python + + from ..tools import Controller, BaseController, Proxy + + @Controller('/foo/proxy') + class FooServiceProxy(BaseController): + @Proxy() + def proxy(self, path, **params): + # if requested URL is "/foo/proxy/access/service?opt=1" + # then path is "access/service" and params is {'opt': '1'} + # ... -Note that paths must be unique and that a path like ``rgw/daemon`` has to have -a parent ``rgw``. Otherwise it won't work. How does the RESTController work? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~