]>
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
7 from . import Controller
, BaseController
, Endpoint
, ENDPOINT_MAP
10 from ..tools
import str_to_bool
13 logger
= logging
.getLogger('controllers.docs')
16 @Controller('/docs', secure
=False)
17 class Docs(BaseController
):
20 def _gen_tags(cls
, all_endpoints
):
21 """ Generates a list of all tags and corresponding descriptions. """
22 # Scenarios to consider:
23 # * Intentionally make up a new tag name at controller => New tag name displayed.
24 # * Misspell or make up a new tag name at endpoint => Neither tag or endpoint displayed.
25 # * Misspell tag name at controller (when referring to another controller) =>
26 # Tag displayed but no endpoints assigned
27 # * Description for a tag added at multiple locations => Only one description displayed.
29 for endpoints
in ENDPOINT_MAP
.values():
30 for endpoint
in endpoints
:
31 if endpoint
.is_api
or all_endpoints
:
32 list_of_ctrl
.add(endpoint
.ctrl
)
35 for ctrl
in list_of_ctrl
:
36 tag_name
= ctrl
.__name
__
38 if hasattr(ctrl
, 'doc_info'):
39 if ctrl
.doc_info
['tag']:
40 tag_name
= ctrl
.doc_info
['tag']
41 tag_descr
= ctrl
.doc_info
['tag_descr']
42 if tag_name
not in tag_map
or not tag_map
[tag_name
]:
43 tag_map
[tag_name
] = tag_descr
45 tags
= [{'name': k
, 'description': v
if v
else "*No description available*"}
46 for k
, v
in tag_map
.items()]
47 tags
.sort(key
=lambda e
: e
['name'])
51 def _get_tag(cls
, endpoint
):
52 """ Returns the name of a tag to assign to a path. """
56 if hasattr(func
, 'doc_info') and func
.doc_info
['tag']:
57 tag
= func
.doc_info
['tag']
58 elif hasattr(ctrl
, 'doc_info') and ctrl
.doc_info
['tag']:
59 tag
= ctrl
.doc_info
['tag']
63 def _gen_type(cls
, param
):
64 # pylint: disable=too-many-return-statements
66 Generates the type of parameter based on its name and default value,
67 using very simple heuristics.
68 Used if type is not explicitly defined.
70 param_name
= param
['name']
71 def_value
= param
['default'] if 'default' in param
else None
72 if param_name
.startswith("is_"):
74 if "size" in param_name
:
76 if "count" in param_name
:
78 if "num" in param_name
:
80 if isinstance(def_value
, bool):
82 if isinstance(def_value
, int):
87 # isinstance doesn't work: input is always <type 'type'>.
88 def _type_to_str(cls
, type_as_type
):
89 """ Used if type is explicitly defined. """
90 if type_as_type
is str:
91 type_as_str
= 'string'
92 elif type_as_type
is int:
93 type_as_str
= 'integer'
94 elif type_as_type
is bool:
95 type_as_str
= 'boolean'
96 elif type_as_type
is list or type_as_type
is tuple:
98 elif type_as_type
is float:
99 type_as_str
= 'number'
101 type_as_str
= 'object'
105 def _add_param_info(cls
, parameters
, p_info
):
107 # * Parameter name (if not nested) misspelt in decorator => parameter not displayed
108 # * Sometimes a parameter is used for several endpoints (e.g. fs_id in CephFS).
109 # Currently, there is no possibility of reuse. Should there be?
110 # But what if there are two parameters with same name but different functionality?
112 Adds explicitly described information for parameters of an endpoint.
115 * Either the parameter in p_info corresponds to an endpoint parameter. Implicit information
116 has higher priority, so only information that doesn't already exist is added.
117 * Or the parameter in p_info describes a nested parameter inside an endpoint parameter.
118 In that case there is no implicit information at all so all explicitly described info needs
123 for parameter
in parameters
:
124 if p
['name'] == parameter
['name']:
125 parameter
['type'] = p
['type']
126 parameter
['description'] = p
['description']
127 if 'nested_params' in p
:
128 parameter
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
133 'description': p
['description'],
134 'required': p
['required'],
137 nested_p
['default'] = p
['default']
138 if 'nested_params' in p
:
139 nested_p
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
140 parameters
.append(nested_p
)
145 def _gen_schema_for_content(cls
, params
):
147 Generates information to the content-object in OpenAPI Spec.
148 Used to for request body and responses.
154 if param
['required']:
155 required_params
.append(param
['name'])
159 props
['type'] = cls
._type
_to
_str
(param
['type'])
160 if 'nested_params' in param
:
161 if props
['type'] == 'array': # dict in array
162 props
['items'] = cls
._gen
_schema
_for
_content
(param
['nested_params'])
164 props
= cls
._gen
_schema
_for
_content
(param
['nested_params'])
165 elif props
['type'] == 'object': # e.g. [int]
166 props
['type'] = 'array'
167 props
['items'] = {'type': cls
._type
_to
_str
(param
['type'][0])}
169 props
['type'] = cls
._gen
_type
(param
)
170 if 'description' in param
:
171 props
['description'] = param
['description']
172 if 'default' in param
:
173 props
['default'] = param
['default']
174 properties
[param
['name']] = props
178 'properties': properties
,
181 schema
['required'] = required_params
185 def _gen_responses(cls
, method
, resp_object
=None):
188 "description": "Operation exception. Please check the "
189 "response body for details."
192 "description": "Unauthenticated access. Please login first."
195 "description": "Unauthorized access. Please check your "
199 "description": "Unexpected error. Please check the "
200 "response body for the stack trace."
203 if method
.lower() == 'get':
204 resp
['200'] = {'description': "OK"}
205 if method
.lower() == 'post':
206 resp
['201'] = {'description': "Resource created."}
207 if method
.lower() == 'put':
208 resp
['200'] = {'description': "Resource updated."}
209 if method
.lower() == 'delete':
210 resp
['204'] = {'description': "Resource deleted."}
211 if method
.lower() in ['post', 'put', 'delete']:
212 resp
['202'] = {'description': "Operation is still executing."
213 " Please check the task queue."}
216 for status_code
, response_body
in resp_object
.items():
217 resp
[status_code
].update({
219 'application/json': {
220 'schema': cls
._gen
_schema
_for
_content
(response_body
)}}})
225 def _gen_params(cls
, params
, location
):
229 _type
= cls
._type
_to
_str
(param
['type'])
231 _type
= cls
._gen
_type
(param
)
232 if 'description' in param
:
233 descr
= param
['description']
235 descr
= "*No description available*"
237 'name': param
['name'],
244 if param
['required']:
245 res
['required'] = True
246 elif param
['default'] is None:
247 res
['allowEmptyValue'] = True
249 res
['default'] = param
['default']
250 parameters
.append(res
)
255 def _gen_paths(cls
, all_endpoints
, base_url
):
256 method_order
= ['get', 'post', 'put', 'delete']
258 for path
, endpoints
in sorted(list(ENDPOINT_MAP
.items()),
263 endpoint_list
= sorted(endpoints
, key
=lambda e
:
264 method_order
.index(e
.method
.lower()))
265 for endpoint
in endpoint_list
:
266 if not endpoint
.is_api
and not all_endpoints
:
270 method
= endpoint
.method
273 summary
= "No description available"
276 if hasattr(func
, 'doc_info'):
277 if func
.doc_info
['summary']:
278 summary
= func
.doc_info
['summary']
279 resp
= func
.doc_info
['response']
280 p_info
= func
.doc_info
['parameters']
282 if endpoint
.path_params
:
285 cls
._add
_param
_info
(endpoint
.path_params
, p_info
), 'path'))
286 if endpoint
.query_params
:
289 cls
._add
_param
_info
(endpoint
.query_params
, p_info
), 'query'))
291 methods
[method
.lower()] = {
292 'tags': [cls
._get
_tag
(endpoint
)],
294 'description': func
.__doc
__,
295 'parameters': params
,
296 'responses': cls
._gen
_responses
(method
, resp
)
299 if method
.lower() in ['post', 'put']:
300 if endpoint
.body_params
:
301 body_params
= cls
._add
_param
_info
(endpoint
.body_params
, p_info
)
302 methods
[method
.lower()]['requestBody'] = {
304 'application/json': {
305 'schema': cls
._gen
_schema
_for
_content
(body_params
)}}}
307 if endpoint
.is_secure
:
308 methods
[method
.lower()]['security'] = [{'jwt': []}]
311 paths
[path
[len(base_url
):]] = methods
315 def _gen_spec(self
, all_endpoints
=False, base_url
=""):
319 host
= cherrypy
.request
.base
320 host
= host
[host
.index(':')+3:]
321 logger
.debug("Host: %s", host
)
323 paths
= self
._gen
_paths
(all_endpoints
, base_url
)
329 ssl
= str_to_bool(mgr
.get_localized_module_option('ssl', True))
336 'description': "Please note that this API is not an official "
337 "Ceph REST API to be used by third-party "
338 "applications. It's primary purpose is to serve"
339 " the requirements of the Ceph Dashboard and is"
340 " subject to change at any time. Use at your "
343 'title': "Ceph-Dashboard REST API"
346 'basePath': base_url
,
347 'servers': [{'url': "{}{}".format(cherrypy
.request
.base
, base_url
)}],
348 'tags': self
._gen
_tags
(all_endpoints
),
356 'bearerFormat': 'JWT'
364 @Endpoint(path
="api.json")
366 return self
._gen
_spec
(False, "/api")
368 @Endpoint(path
="api-all.json")
369 def api_all_json(self
):
370 return self
._gen
_spec
(True, "/api")
372 def _swagger_ui_page(self
, all_endpoints
=False, token
=None):
373 base
= cherrypy
.request
.base
375 spec_url
= "{}/docs/api-all.json".format(base
)
377 spec_url
= "{}/docs/api.json".format(base
)
379 auth_header
= cherrypy
.request
.headers
.get('authorization')
381 if auth_header
is not None:
382 scheme
, params
= auth_header
.split(' ', 1)
383 if scheme
.lower() == 'bearer':
386 if token
is not None:
389 api_key_callback
= """, onComplete: () => {{
390 ui.preauthorizeApiKey('jwt', '{}');
392 """.format(jwt_token
)
398 <meta charset="UTF-8">
399 <meta name="referrer" content="no-referrer" />
400 <link rel="stylesheet" type="text/css"
401 href="/swagger-ui.css" >
405 box-sizing: border-box;
406 overflow: -moz-scrollbars-vertical;
422 <div id="swagger-ui"></div>
423 <script src="/swagger-ui-bundle.js">
426 window.onload = function() {{
427 const ui = SwaggerUIBundle({{
429 dom_id: '#swagger-ui',
431 SwaggerUIBundle.presets.apis
441 """.format(spec_url
, api_key_callback
)
445 @Endpoint(json_response
=False)
446 def __call__(self
, all_endpoints
=False):
447 return self
._swagger
_ui
_page
(all_endpoints
)
449 @Endpoint('POST', path
="/", json_response
=False,
450 query_params
="{all_endpoints}")
451 def _with_token(self
, token
, all_endpoints
=False):
452 return self
._swagger
_ui
_page
(all_endpoints
, token
)