]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/docs.py
1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
3 from typing
import Any
, Dict
, Union
8 from . import Controller
, BaseController
, Endpoint
, ENDPOINT_MAP
, \
12 from ..tools
import str_to_bool
15 logger
= logging
.getLogger('controllers.docs')
18 @Controller('/docs', secure
=False)
19 class Docs(BaseController
):
22 def _gen_tags(cls
, all_endpoints
):
23 """ Generates a list of all tags and corresponding descriptions. """
24 # Scenarios to consider:
25 # * Intentionally make up a new tag name at controller => New tag name displayed.
26 # * Misspell or make up a new tag name at endpoint => Neither tag or endpoint displayed.
27 # * Misspell tag name at controller (when referring to another controller) =>
28 # Tag displayed but no endpoints assigned
29 # * Description for a tag added at multiple locations => Only one description displayed.
31 for endpoints
in ENDPOINT_MAP
.values():
32 for endpoint
in endpoints
:
33 if endpoint
.is_api
or all_endpoints
:
34 list_of_ctrl
.add(endpoint
.ctrl
)
36 tag_map
: Dict
[str, str] = {}
37 for ctrl
in list_of_ctrl
:
38 tag_name
= ctrl
.__name
__
40 if hasattr(ctrl
, 'doc_info'):
41 if ctrl
.doc_info
['tag']:
42 tag_name
= ctrl
.doc_info
['tag']
43 tag_descr
= ctrl
.doc_info
['tag_descr']
44 if tag_name
not in tag_map
or not tag_map
[tag_name
]:
45 tag_map
[tag_name
] = tag_descr
47 tags
= [{'name': k
, 'description': v
if v
else "*No description available*"}
48 for k
, v
in tag_map
.items()]
49 tags
.sort(key
=lambda e
: e
['name'])
53 def _get_tag(cls
, endpoint
):
54 """ Returns the name of a tag to assign to a path. """
58 if hasattr(func
, 'doc_info') and func
.doc_info
['tag']:
59 tag
= func
.doc_info
['tag']
60 elif hasattr(ctrl
, 'doc_info') and ctrl
.doc_info
['tag']:
61 tag
= ctrl
.doc_info
['tag']
65 def _gen_type(cls
, param
):
66 # pylint: disable=too-many-return-statements
68 Generates the type of parameter based on its name and default value,
69 using very simple heuristics.
70 Used if type is not explicitly defined.
72 param_name
= param
['name']
73 def_value
= param
['default'] if 'default' in param
else None
74 if param_name
.startswith("is_"):
76 if "size" in param_name
:
78 if "count" in param_name
:
80 if "num" in param_name
:
82 if isinstance(def_value
, bool):
84 if isinstance(def_value
, int):
89 # isinstance doesn't work: input is always <type 'type'>.
90 def _type_to_str(cls
, type_as_type
):
91 """ Used if type is explicitly defined. """
92 if type_as_type
is str:
93 type_as_str
= 'string'
94 elif type_as_type
is int:
95 type_as_str
= 'integer'
96 elif type_as_type
is bool:
97 type_as_str
= 'boolean'
98 elif type_as_type
is list or type_as_type
is tuple:
100 elif type_as_type
is float:
101 type_as_str
= 'number'
103 type_as_str
= 'object'
107 def _add_param_info(cls
, parameters
, p_info
):
109 # * Parameter name (if not nested) misspelt in decorator => parameter not displayed
110 # * Sometimes a parameter is used for several endpoints (e.g. fs_id in CephFS).
111 # Currently, there is no possibility of reuse. Should there be?
112 # But what if there are two parameters with same name but different functionality?
114 Adds explicitly described information for parameters of an endpoint.
117 * Either the parameter in p_info corresponds to an endpoint parameter. Implicit information
118 has higher priority, so only information that doesn't already exist is added.
119 * Or the parameter in p_info describes a nested parameter inside an endpoint parameter.
120 In that case there is no implicit information at all so all explicitly described info needs
125 for parameter
in parameters
:
126 if p
['name'] == parameter
['name']:
127 parameter
['type'] = p
['type']
128 parameter
['description'] = p
['description']
129 if 'nested_params' in p
:
130 parameter
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
135 'description': p
['description'],
136 'required': p
['required'],
139 nested_p
['default'] = p
['default']
140 if 'nested_params' in p
:
141 nested_p
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
142 parameters
.append(nested_p
)
147 def _gen_schema_for_content(cls
, params
):
149 Generates information to the content-object in OpenAPI Spec.
150 Used to for request body and responses.
156 if param
['required']:
157 required_params
.append(param
['name'])
161 props
['type'] = cls
._type
_to
_str
(param
['type'])
162 if 'nested_params' in param
:
163 if props
['type'] == 'array': # dict in array
164 props
['items'] = cls
._gen
_schema
_for
_content
(param
['nested_params'])
166 props
= cls
._gen
_schema
_for
_content
(param
['nested_params'])
167 elif props
['type'] == 'object': # e.g. [int]
168 props
['type'] = 'array'
169 props
['items'] = {'type': cls
._type
_to
_str
(param
['type'][0])}
171 props
['type'] = cls
._gen
_type
(param
)
172 if 'description' in param
:
173 props
['description'] = param
['description']
174 if 'default' in param
:
175 props
['default'] = param
['default']
176 properties
[param
['name']] = props
180 'properties': properties
,
183 schema
['required'] = required_params
187 def _gen_responses(cls
, method
, resp_object
=None):
188 resp
: Dict
[str, Dict
[str, Union
[str, Any
]]] = {
190 "description": "Operation exception. Please check the "
191 "response body for details."
194 "description": "Unauthenticated access. Please login first."
197 "description": "Unauthorized access. Please check your "
201 "description": "Unexpected error. Please check the "
202 "response body for the stack trace."
205 if method
.lower() == 'get':
206 resp
['200'] = {'description': "OK"}
207 if method
.lower() == 'post':
208 resp
['201'] = {'description': "Resource created."}
209 if method
.lower() == 'put':
210 resp
['200'] = {'description': "Resource updated."}
211 if method
.lower() == 'delete':
212 resp
['204'] = {'description': "Resource deleted."}
213 if method
.lower() in ['post', 'put', 'delete']:
214 resp
['202'] = {'description': "Operation is still executing."
215 " Please check the task queue."}
218 for status_code
, response_body
in resp_object
.items():
219 resp
[status_code
].update({
221 'application/json': {
222 'schema': cls
._gen
_schema
_for
_content
(response_body
)}}})
227 def _gen_params(cls
, params
, location
):
231 _type
= cls
._type
_to
_str
(param
['type'])
233 _type
= cls
._gen
_type
(param
)
234 if 'description' in param
:
235 descr
= param
['description']
237 descr
= "*No description available*"
239 'name': param
['name'],
246 if param
['required']:
247 res
['required'] = True
248 elif param
['default'] is None:
249 res
['allowEmptyValue'] = True
251 res
['default'] = param
['default']
252 parameters
.append(res
)
257 def _gen_paths(cls
, all_endpoints
):
258 method_order
= ['get', 'post', 'put', 'delete']
260 for path
, endpoints
in sorted(list(ENDPOINT_MAP
.items()),
265 endpoint_list
= sorted(endpoints
, key
=lambda e
:
266 method_order
.index(e
.method
.lower()))
267 for endpoint
in endpoint_list
:
268 if not endpoint
.is_api
and not all_endpoints
:
272 method
= endpoint
.method
275 summary
= "No description available"
278 if hasattr(func
, 'doc_info'):
279 if func
.doc_info
['summary']:
280 summary
= func
.doc_info
['summary']
281 resp
= func
.doc_info
['response']
282 p_info
= func
.doc_info
['parameters']
284 if endpoint
.path_params
:
287 cls
._add
_param
_info
(endpoint
.path_params
, p_info
), 'path'))
288 if endpoint
.query_params
:
291 cls
._add
_param
_info
(endpoint
.query_params
, p_info
), 'query'))
293 methods
[method
.lower()] = {
294 'tags': [cls
._get
_tag
(endpoint
)],
296 'description': func
.__doc
__,
297 'parameters': params
,
298 'responses': cls
._gen
_responses
(method
, resp
)
301 if method
.lower() in ['post', 'put']:
302 if endpoint
.body_params
:
303 body_params
= cls
._add
_param
_info
(endpoint
.body_params
, p_info
)
304 methods
[method
.lower()]['requestBody'] = {
306 'application/json': {
307 'schema': cls
._gen
_schema
_for
_content
(body_params
)}}}
309 if endpoint
.is_secure
:
310 methods
[method
.lower()]['security'] = [{'jwt': []}]
313 paths
[path
] = methods
317 def _gen_spec(self
, all_endpoints
=False, base_url
=""):
321 host
= cherrypy
.request
.base
322 host
= host
[host
.index(':')+3:]
323 logger
.debug("Host: %s", host
)
325 paths
= self
._gen
_paths
(all_endpoints
)
331 ssl
= str_to_bool(mgr
.get_localized_module_option('ssl', True))
338 'description': "Please note that this API is not an official "
339 "Ceph REST API to be used by third-party "
340 "applications. It's primary purpose is to serve"
341 " the requirements of the Ceph Dashboard and is"
342 " subject to change at any time. Use at your "
345 'title': "Ceph-Dashboard REST API"
348 'basePath': base_url
,
349 'servers': [{'url': "{}{}".format(cherrypy
.request
.base
, base_url
)}],
350 'tags': self
._gen
_tags
(all_endpoints
),
358 'bearerFormat': 'JWT'
366 @Endpoint(path
="api.json")
368 return self
._gen
_spec
(False, "/")
370 @Endpoint(path
="api-all.json")
371 def api_all_json(self
):
372 return self
._gen
_spec
(True, "/")
374 def _swagger_ui_page(self
, all_endpoints
=False, token
=None):
375 base
= cherrypy
.request
.base
377 spec_url
= "{}/docs/api-all.json".format(base
)
379 spec_url
= "{}/docs/api.json".format(base
)
381 auth_header
= cherrypy
.request
.headers
.get('authorization')
382 auth_cookie
= cherrypy
.request
.cookie
['token']
384 if auth_cookie
is not None:
385 jwt_token
= auth_cookie
.value
386 elif auth_header
is not None:
387 scheme
, params
= auth_header
.split(' ', 1)
388 if scheme
.lower() == 'bearer':
391 if token
is not None:
394 api_key_callback
= """, onComplete: () => {{
395 ui.preauthorizeApiKey('jwt', '{}');
397 """.format(jwt_token
)
403 <meta charset="UTF-8">
404 <meta name="referrer" content="no-referrer" />
405 <link rel="stylesheet" type="text/css"
406 href="/swagger-ui.css" >
410 box-sizing: border-box;
411 overflow: -moz-scrollbars-vertical;
427 <div id="swagger-ui"></div>
428 <script src="/swagger-ui-bundle.js">
431 window.onload = function() {{
432 const ui = SwaggerUIBundle({{
434 dom_id: '#swagger-ui',
436 SwaggerUIBundle.presets.apis
446 """.format(spec_url
, api_key_callback
)
450 @Endpoint(json_response
=False)
451 def __call__(self
, all_endpoints
=False):
452 return self
._swagger
_ui
_page
(all_endpoints
)
454 @Endpoint('POST', path
="/", json_response
=False,
455 query_params
="{all_endpoints}")
457 def _with_token(self
, token
, all_endpoints
=False):
458 return self
._swagger
_ui
_page
(all_endpoints
, token
)