]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/docs.py
06d4f977698eda1230d3bba0976dd51fb448f874
1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
6 from . import Controller
, BaseController
, Endpoint
, ENDPOINT_MAP
7 from .. import logger
, mgr
9 from ..tools
import str_to_bool
12 @Controller('/docs', secure
=False)
13 class Docs(BaseController
):
16 def _gen_tags(cls
, all_endpoints
):
17 """ Generates a list of all tags and corresponding descriptions. """
18 # Scenarios to consider:
19 # * Intentionally make up a new tag name at controller => New tag name displayed.
20 # * Misspell or make up a new tag name at endpoint => Neither tag or endpoint displayed.
21 # * Misspell tag name at controller (when referring to another controller) =>
22 # Tag displayed but no endpoints assigned
23 # * Description for a tag added at multiple locations => Only one description displayed.
25 for endpoints
in ENDPOINT_MAP
.values():
26 for endpoint
in endpoints
:
27 if endpoint
.is_api
or all_endpoints
:
28 list_of_ctrl
.add(endpoint
.ctrl
)
31 for ctrl
in list_of_ctrl
:
32 tag_name
= ctrl
.__name
__
34 if hasattr(ctrl
, 'doc_info'):
35 if ctrl
.doc_info
['tag']:
36 tag_name
= ctrl
.doc_info
['tag']
37 tag_descr
= ctrl
.doc_info
['tag_descr']
38 if tag_name
not in TAG_MAP
or not TAG_MAP
[tag_name
]:
39 TAG_MAP
[tag_name
] = tag_descr
41 tags
= [{'name': k
, 'description': v
if v
else "*No description available*"}
42 for k
, v
in TAG_MAP
.items()]
43 tags
.sort(key
=lambda e
: e
['name'])
47 def _get_tag(cls
, endpoint
):
48 """ Returns the name of a tag to assign to a path. """
52 if hasattr(func
, 'doc_info') and func
.doc_info
['tag']:
53 tag
= func
.doc_info
['tag']
54 elif hasattr(ctrl
, 'doc_info') and ctrl
.doc_info
['tag']:
55 tag
= ctrl
.doc_info
['tag']
59 def _gen_type(cls
, param
):
60 # pylint: disable=too-many-return-statements
62 Generates the type of parameter based on its name and default value,
63 using very simple heuristics.
64 Used if type is not explicitly defined.
66 param_name
= param
['name']
67 def_value
= param
['default'] if 'default' in param
else None
68 if param_name
.startswith("is_"):
70 if "size" in param_name
:
72 if "count" in param_name
:
74 if "num" in param_name
:
76 if isinstance(def_value
, bool):
78 if isinstance(def_value
, int):
83 # isinstance doesn't work: input is always <type 'type'>.
84 def _type_to_str(cls
, type_as_type
):
85 """ Used if type is explicitly defined. """
86 if type_as_type
is str:
87 type_as_str
= 'string'
88 elif type_as_type
is int:
89 type_as_str
= 'integer'
90 elif type_as_type
is bool:
91 type_as_str
= 'boolean'
92 elif type_as_type
is list or type_as_type
is tuple:
94 elif type_as_type
is float:
95 type_as_str
= 'number'
97 type_as_str
= 'object'
101 def _add_param_info(cls
, parameters
, p_info
):
103 # * Parameter name (if not nested) misspelt in decorator => parameter not displayed
104 # * Sometimes a parameter is used for several endpoints (e.g. fs_id in CephFS).
105 # Currently, there is no possibility of reuse. Should there be?
106 # But what if there are two parameters with same name but different functionality?
108 Adds explicitly described information for parameters of an endpoint.
111 * Either the parameter in p_info corresponds to an endpoint parameter. Implicit information
112 has higher priority, so only information that doesn't already exist is added.
113 * Or the parameter in p_info describes a nested parameter inside an endpoint parameter.
114 In that case there is no implicit information at all so all explicitly described info needs
119 for parameter
in parameters
:
120 if p
['name'] == parameter
['name']:
121 parameter
['type'] = p
['type']
122 parameter
['description'] = p
['description']
123 if 'nested_params' in p
:
124 parameter
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
129 'description': p
['description'],
130 'required': p
['required'],
133 nested_p
['default'] = p
['default']
134 if 'nested_params' in p
:
135 nested_p
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
136 parameters
.append(nested_p
)
141 def _gen_schema_for_content(cls
, params
):
143 Generates information to the content-object in OpenAPI Spec.
144 Used to for request body and responses.
150 if param
['required']:
151 required_params
.append(param
['name'])
155 props
['type'] = cls
._type
_to
_str
(param
['type'])
156 if 'nested_params' in param
:
157 if props
['type'] == 'array': # dict in array
158 props
['items'] = cls
._gen
_schema
_for
_content
(param
['nested_params'])
160 props
= cls
._gen
_schema
_for
_content
(param
['nested_params'])
161 elif props
['type'] == 'object': # e.g. [int]
162 props
['type'] = 'array'
163 props
['items'] = {'type': cls
._type
_to
_str
(param
['type'][0])}
165 props
['type'] = cls
._gen
_type
(param
)
166 if 'description' in param
:
167 props
['description'] = param
['description']
168 if 'default' in param
:
169 props
['default'] = param
['default']
170 properties
[param
['name']] = props
174 'properties': properties
,
177 schema
['required'] = required_params
181 def _gen_responses(cls
, method
, resp_object
=None):
184 "description": "Operation exception. Please check the "
185 "response body for details."
188 "description": "Unauthenticated access. Please login first."
191 "description": "Unauthorized access. Please check your "
195 "description": "Unexpected error. Please check the "
196 "response body for the stack trace."
199 if method
.lower() == 'get':
200 resp
['200'] = {'description': "OK"}
201 if method
.lower() == 'post':
202 resp
['201'] = {'description': "Resource created."}
203 if method
.lower() == 'put':
204 resp
['200'] = {'description': "Resource updated."}
205 if method
.lower() == 'delete':
206 resp
['204'] = {'description': "Resource deleted."}
207 if method
.lower() in ['post', 'put', 'delete']:
208 resp
['202'] = {'description': "Operation is still executing."
209 " Please check the task queue."}
212 for status_code
, response_body
in resp_object
.items():
213 resp
[status_code
].update({
215 'application/json': {
216 'schema': cls
._gen
_schema
_for
_content
(response_body
)}}})
221 def _gen_params(cls
, params
, location
):
225 _type
= cls
._type
_to
_str
(param
['type'])
227 _type
= cls
._gen
_type
(param
)
228 if 'description' in param
:
229 descr
= param
['description']
231 descr
= "*No description available*"
233 'name': param
['name'],
240 if param
['required']:
241 res
['required'] = True
242 elif param
['default'] is None:
243 res
['allowEmptyValue'] = True
245 res
['default'] = param
['default']
246 parameters
.append(res
)
251 def _gen_paths(cls
, all_endpoints
, baseUrl
):
252 METHOD_ORDER
= ['get', 'post', 'put', 'delete']
254 for path
, endpoints
in sorted(list(ENDPOINT_MAP
.items()),
259 endpoint_list
= sorted(endpoints
, key
=lambda e
:
260 METHOD_ORDER
.index(e
.method
.lower()))
261 for endpoint
in endpoint_list
:
262 if not endpoint
.is_api
and not all_endpoints
:
266 method
= endpoint
.method
269 summary
= "No description available"
272 if hasattr(func
, 'doc_info'):
273 if func
.doc_info
['summary']:
274 summary
= func
.doc_info
['summary']
275 resp
= func
.doc_info
['response']
276 p_info
= func
.doc_info
['parameters']
278 if endpoint
.path_params
:
281 cls
._add
_param
_info
(endpoint
.path_params
, p_info
), 'path'))
282 if endpoint
.query_params
:
285 cls
._add
_param
_info
(endpoint
.query_params
, p_info
), 'query'))
287 methods
[method
.lower()] = {
288 'tags': [cls
._get
_tag
(endpoint
)],
290 'description': func
.__doc
__,
291 'parameters': params
,
292 'responses': cls
._gen
_responses
(method
, resp
)
295 if method
.lower() in ['post', 'put']:
296 if endpoint
.body_params
:
297 body_params
= cls
._add
_param
_info
(endpoint
.body_params
, p_info
)
298 methods
[method
.lower()]['requestBody'] = {
300 'application/json': {
301 'schema': cls
._gen
_schema
_for
_content
(body_params
)}}}
303 if endpoint
.is_secure
:
304 methods
[method
.lower()]['security'] = [{'jwt': []}]
307 paths
[path
[len(baseUrl
):]] = methods
311 def _gen_spec(self
, all_endpoints
=False, base_url
=""):
315 host
= cherrypy
.request
.base
316 host
= host
[host
.index(':')+3:]
317 logger
.debug("DOCS: Host: %s", host
)
319 paths
= self
._gen
_paths
(all_endpoints
, base_url
)
325 ssl
= str_to_bool(mgr
.get_localized_module_option('ssl', True))
332 'description': "Please note that this API is not an official "
333 "Ceph REST API to be used by third-party "
334 "applications. It's primary purpose is to serve"
335 " the requirements of the Ceph Dashboard and is"
336 " subject to change at any time. Use at your "
339 'title': "Ceph-Dashboard REST API"
342 'basePath': base_url
,
343 'servers': [{'url': "{}{}".format(cherrypy
.request
.base
, base_url
)}],
344 'tags': self
._gen
_tags
(all_endpoints
),
352 'bearerFormat': 'JWT'
360 @Endpoint(path
="api.json")
362 return self
._gen
_spec
(False, "/api")
364 @Endpoint(path
="api-all.json")
365 def api_all_json(self
):
366 return self
._gen
_spec
(True, "/api")
368 def _swagger_ui_page(self
, all_endpoints
=False, token
=None):
369 base
= cherrypy
.request
.base
371 spec_url
= "{}/docs/api-all.json".format(base
)
373 spec_url
= "{}/docs/api.json".format(base
)
375 auth_header
= cherrypy
.request
.headers
.get('authorization')
377 if auth_header
is not None:
378 scheme
, params
= auth_header
.split(' ', 1)
379 if scheme
.lower() == 'bearer':
382 if token
is not None:
385 api_key_callback
= """, onComplete: () => {{
386 ui.preauthorizeApiKey('jwt', '{}');
388 """.format(jwt_token
)
394 <meta charset="UTF-8">
395 <meta name="referrer" content="no-referrer" />
396 <link href="https://fonts.googleapis.com/css?family=Open+Sans:400, \
397 700|Source+Code+Pro:300,600|Titillium+Web:400,600,700"
399 <link rel="stylesheet" type="text/css"
400 href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" >
404 box-sizing: border-box;
405 overflow: -moz-scrollbars-vertical;
421 <div id="swagger-ui"></div>
422 <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js">
425 window.onload = function() {{
426 const ui = SwaggerUIBundle({{
428 dom_id: '#swagger-ui',
430 SwaggerUIBundle.presets.apis
440 """.format(spec_url
, api_key_callback
)
444 @Endpoint(json_response
=False)
445 def __call__(self
, all_endpoints
=False):
446 return self
._swagger
_ui
_page
(all_endpoints
)
448 @Endpoint('POST', path
="/", json_response
=False,
449 query_params
="{all_endpoints}")
450 def _with_token(self
, token
, all_endpoints
=False):
451 return self
._swagger
_ui
_page
(all_endpoints
, token
)