]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Swagger-UI based Dashboard REST API page 22282/head
authorRicardo Dias <rdias@suse.com>
Thu, 3 May 2018 14:26:37 +0000 (15:26 +0100)
committerRicardo Dias <rdias@suse.com>
Wed, 6 Jun 2018 15:53:13 +0000 (16:53 +0100)
Fixes: http://tracker.ceph.com/issues/23898
Signed-off-by: Ricardo Dias <rdias@suse.com>
src/pybind/mgr/dashboard/controllers/docs.py [new file with mode: 0644]

diff --git a/src/pybind/mgr/dashboard/controllers/docs.py b/src/pybind/mgr/dashboard/controllers/docs.py
new file mode 100644 (file)
index 0000000..b61bea9
--- /dev/null
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import cherrypy
+
+from . import Controller, BaseController, AuthRequired, Endpoint, ENDPOINT_MAP
+from .. import logger
+
+
+@Controller('/docs')
+@AuthRequired()
+class Docs(BaseController):
+
+    @classmethod
+    def _gen_tags(cls, all_endpoints):
+        ctrl_names = set()
+        for endpoints in ENDPOINT_MAP.values():
+            for endpoint in endpoints:
+                if endpoint.is_api or all_endpoints:
+                    ctrl_names.add(endpoint.group)
+
+        return [{'name': name, 'description': ""}
+                for name in sorted(ctrl_names)]
+
+    @classmethod
+    def _gen_type(cls, param):
+        # pylint: disable=too-many-return-statements
+        """
+        Generates the type of parameter based on its name and default value,
+        using very simple heuristics.
+        """
+        param_name = param['name']
+        def_value = param['default'] if 'default' in param else None
+        if param_name.startswith("is_"):
+            return "boolean"
+        elif "size" in param_name:
+            return "integer"
+        elif "count" in param_name:
+            return "integer"
+        elif "num" in param_name:
+            return "integer"
+        elif isinstance(def_value, bool):
+            return "boolean"
+        elif isinstance(def_value, int):
+            return "integer"
+        return "string"
+
+    @classmethod
+    def _gen_body_param(cls, body_params):
+        required = [p['name'] for p in body_params if p['required']]
+
+        props = {}
+        for p in body_params:
+            props[p['name']] = {
+                'type': cls._gen_type(p)
+            }
+            if 'default' in p:
+                props[p['name']]['default'] = p['default']
+
+        if not props:
+            return None
+
+        return {
+            'in': "body",
+            'name': "body",
+            'description': "",
+            'required': True,
+            'schema': {
+                'type': "object",
+                'required': required,
+                'properties': props
+            }
+        }
+
+    @classmethod
+    def _gen_responses_descriptions(cls, method):
+        resp = {
+            '400': {
+                "description": "Operation exception. Please check the "
+                               "response body for details."
+            },
+            '401': {
+                "description": "Unauthenticated access. Please login first."
+            },
+            '403': {
+                "description": "Unauthorized access. Please check your "
+                               "permissions."
+            },
+            '500': {
+                "description": "Unexpected error. Please check the "
+                               "response body for the stack trace."
+            }
+        }
+        if method.lower() == 'get':
+            resp['200'] = {'description': "OK"}
+        if method.lower() == 'post':
+            resp['201'] = {'description': "Resource created."}
+        if method.lower() == 'put':
+            resp['200'] = {'description': "Resource updated."}
+        if method.lower() == 'delete':
+            resp['204'] = {'description': "Resource deleted."}
+        if method.lower() in ['post', 'put', 'delete']:
+            resp['202'] = {'description': "Operation is still executing."
+                                          " Please check the task queue."}
+
+        return resp
+
+    @classmethod
+    def _gen_param(cls, param, ptype):
+        res = {
+            'name': param['name'],
+            'in': ptype,
+            'type': cls._gen_type(param)
+        }
+        if param['required']:
+            res['required'] = True
+        elif param['default'] is None:
+            res['allowEmptyValue'] = True
+        else:
+            res['default'] = param['default']
+        return res
+
+    def _gen_spec(self, all_endpoints=False, baseUrl=""):
+        if all_endpoints:
+            baseUrl = ""
+        METHOD_ORDER = ['get', 'post', 'put', 'delete']
+        host = cherrypy.request.base
+        host = host[host.index(':')+3:]
+        logger.debug("DOCS: Host: %s", host)
+
+        paths = {}
+        for path, endpoints in sorted(list(ENDPOINT_MAP.items()),
+                                      key=lambda p: p[0]):
+            methods = {}
+            skip = False
+
+            endpoint_list = sorted(endpoints, key=lambda e:
+                                   METHOD_ORDER.index(e.method.lower()))
+            for endpoint in endpoint_list:
+                if not endpoint.is_api and not all_endpoints:
+                    skip = True
+                    break
+
+                method = endpoint.method
+                params = []
+                params.extend([self._gen_param(p, 'path')
+                               for p in endpoint.path_params])
+                params.extend([self._gen_param(p, 'query')
+                               for p in endpoint.query_params])
+
+                if method.lower() in ['post', 'put']:
+                    body_params = self._gen_body_param(endpoint.body_params)
+                    if body_params:
+                        params.append(body_params)
+
+                methods[method.lower()] = {
+                    'tags': [endpoint.group],
+                    'summary': "",
+                    'consumes': [
+                        "application/json"
+                    ],
+                    'produces': [
+                        "application/json"
+                    ],
+                    'parameters': params,
+                    'responses': self._gen_responses_descriptions(method),
+                    "security": [""]
+                }
+
+            if not skip:
+                paths[path[len(baseUrl):]] = methods
+
+        if not baseUrl:
+            baseUrl = "/"
+        spec = {
+            'swagger': "2.0",
+            'info': {
+                'description': "Please note that this API is not an official "
+                               "Ceph REST API to be used by third-party "
+                               "applications. It's primary purpose is to serve"
+                               " the requirements of the Ceph Dashboard and is"
+                               " subject to change at any time. Use at your "
+                               "own risk.",
+                'version': "v1",
+                'title': "Ceph-Dashboard REST API"
+            },
+            'host': host,
+            'basePath': baseUrl,
+            'tags': self._gen_tags(all_endpoints),
+            'schemes': ["https"],
+            'paths': paths
+        }
+
+        return spec
+
+    @Endpoint(path="api.json")
+    def api_json(self):
+        return self._gen_spec(False, "/api")
+
+    @Endpoint(path="api-all.json")
+    def api_all_json(self):
+        return self._gen_spec(True, "/api")
+
+    @Endpoint(json_response=False)
+    def __call__(self, all_endpoints=False):
+        base = cherrypy.request.base
+        if all_endpoints:
+            spec_url = "{}/docs/api-all.json".format(base)
+        else:
+            spec_url = "{}/docs/api.json".format(base)
+        page = """
+        <!DOCTYPE html>
+        <html>
+        <head>
+            <meta charset="UTF-8">
+            <meta name="referrer" content="no-referrer" />
+            <link href="https://fonts.googleapis.com/css?family=Open+Sans:400, \
+                        700|Source+Code+Pro:300,600|Titillium+Web:400,600,700"
+                  rel="stylesheet">
+            <link rel="stylesheet" type="text/css"
+                  href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" >
+            <style>
+                html
+                {{
+                    box-sizing: border-box;
+                    overflow: -moz-scrollbars-vertical;
+                    overflow-y: scroll;
+                }}
+                *,
+                *:before,
+                *:after
+                {{
+                    box-sizing: inherit;
+                }}
+
+                body {{
+                    margin:0;
+                    background: #fafafa;
+                }}
+            </style>
+        </head>
+        <body>
+
+        <div id="swagger-ui"></div>
+
+        <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js">
+        </script>
+        <script>
+            window.onload = function() {{
+                const ui = SwaggerUIBundle({{
+                    url: '{}',
+                    dom_id: '#swagger-ui',
+                    presets: [
+                        SwaggerUIBundle.presets.apis
+                    ],
+                    layout: "BaseLayout"
+                }})
+                window.ui = ui
+            }}
+        </script>
+        </body>
+        </html>
+        """.format(spec_url)
+
+        return page