]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: hacking.rst: controller creation instructions update
authorRicardo Dias <rdias@suse.com>
Thu, 24 May 2018 16:10:41 +0000 (17:10 +0100)
committerRicardo Dias <rdias@suse.com>
Mon, 28 May 2018 10:35:10 +0000 (11:35 +0100)
Signed-off-by: Ricardo Dias <rdias@suse.com>
src/pybind/mgr/dashboard/HACKING.rst

index 3f3c414cffbceb213d68715ca2bb155f8ffdd9f6..ec6ea77387f0cc54263605472220f8d24f49ebf1 100644 (file)
@@ -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="/<method_name>"``: 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?
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~