]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/docs.py
e7ed9742ab9d25e91915283c588dd9b4cdedd2fc
1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
5 from typing
import Any
, Dict
, List
, Union
9 from .. import DEFAULT_VERSION
, mgr
10 from ..api
.doc
import Schema
, SchemaInput
, SchemaType
11 from . import ENDPOINT_MAP
, BaseController
, Controller
, Endpoint
13 NO_DESCRIPTION_AVAILABLE
= "*No description available*"
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_"):
75 return str(SchemaType
.BOOLEAN
)
76 if "size" in param_name
:
77 return str(SchemaType
.INTEGER
)
78 if "count" in param_name
:
79 return str(SchemaType
.INTEGER
)
80 if "num" in param_name
:
81 return str(SchemaType
.INTEGER
)
82 if isinstance(def_value
, bool):
83 return str(SchemaType
.BOOLEAN
)
84 if isinstance(def_value
, int):
85 return str(SchemaType
.INTEGER
)
86 return str(SchemaType
.STRING
)
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
= str(SchemaType
.STRING
)
94 elif type_as_type
is int:
95 type_as_str
= str(SchemaType
.INTEGER
)
96 elif type_as_type
is bool:
97 type_as_str
= str(SchemaType
.BOOLEAN
)
98 elif type_as_type
is list or type_as_type
is tuple:
99 type_as_str
= str(SchemaType
.ARRAY
)
100 elif type_as_type
is float:
101 type_as_str
= str(SchemaType
.NUMBER
)
103 type_as_str
= str(SchemaType
.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
: List
[Any
]) -> Dict
[str, Any
]:
149 Generates information to the content-object in OpenAPI Spec.
150 Used to for request body and responses.
154 schema_type
= SchemaType
.OBJECT
155 if isinstance(params
, SchemaInput
):
156 schema_type
= params
.type
157 params
= params
.params
160 if param
['required']:
161 required_params
.append(param
['name'])
165 props
['type'] = cls
._type
_to
_str
(param
['type'])
166 if 'nested_params' in param
:
167 if props
['type'] == str(SchemaType
.ARRAY
): # dict in array
168 props
['items'] = cls
._gen
_schema
_for
_content
(param
['nested_params'])
170 props
= cls
._gen
_schema
_for
_content
(param
['nested_params'])
171 elif props
['type'] == str(SchemaType
.OBJECT
): # e.g. [int]
172 props
['type'] = str(SchemaType
.ARRAY
)
173 props
['items'] = {'type': cls
._type
_to
_str
(param
['type'][0])}
175 props
['type'] = cls
._gen
_type
(param
)
176 if 'description' in param
:
177 props
['description'] = param
['description']
178 if 'default' in param
:
179 props
['default'] = param
['default']
180 properties
[param
['name']] = props
182 schema
= Schema(schema_type
=schema_type
, properties
=properties
,
183 required
=required_params
)
185 return schema
.as_dict()
188 def _gen_responses(cls
, method
, resp_object
=None):
189 resp
: Dict
[str, Dict
[str, Union
[str, Any
]]] = {
191 "description": "Operation exception. Please check the "
192 "response body for details."
195 "description": "Unauthenticated access. Please login first."
198 "description": "Unauthorized access. Please check your "
202 "description": "Unexpected error. Please check the "
203 "response body for the stack trace."
206 if method
.lower() == 'get':
207 resp
['200'] = {'description': "OK",
208 'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION
):
210 if method
.lower() == 'post':
211 resp
['201'] = {'description': "Resource created.",
212 'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION
):
214 if method
.lower() == 'put':
215 resp
['200'] = {'description': "Resource updated.",
216 'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION
):
218 if method
.lower() == 'delete':
219 resp
['204'] = {'description': "Resource deleted.",
220 'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION
):
222 if method
.lower() in ['post', 'put', 'delete']:
223 resp
['202'] = {'description': "Operation is still executing."
224 " Please check the task queue.",
225 'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION
):
229 for status_code
, response_body
in resp_object
.items():
230 if status_code
in resp
:
231 resp
[status_code
].update({
233 'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION
): {
234 'schema': cls
._gen
_schema
_for
_content
(response_body
)}}})
239 def _gen_params(cls
, params
, location
):
243 _type
= cls
._type
_to
_str
(param
['type'])
245 _type
= cls
._gen
_type
(param
)
247 'name': param
['name'],
253 if param
.get('description'):
254 res
['description'] = param
['description']
255 if param
['required']:
256 res
['required'] = True
257 elif param
['default'] is None:
258 res
['allowEmptyValue'] = True
260 res
['default'] = param
['default']
261 parameters
.append(res
)
266 def _gen_paths(cls
, all_endpoints
):
267 # pylint: disable=R0912
268 method_order
= ['get', 'post', 'put', 'delete']
270 for path
, endpoints
in sorted(list(ENDPOINT_MAP
.items()),
275 endpoint_list
= sorted(endpoints
, key
=lambda e
:
276 method_order
.index(e
.method
.lower()))
277 for endpoint
in endpoint_list
:
278 if not endpoint
.is_api
and not all_endpoints
:
282 method
= endpoint
.method
288 if hasattr(func
, 'doc_info'):
289 if func
.doc_info
['summary']:
290 summary
= func
.doc_info
['summary']
291 resp
= func
.doc_info
['response']
292 p_info
= func
.doc_info
['parameters']
294 if endpoint
.path_params
:
297 cls
._add
_param
_info
(endpoint
.path_params
, p_info
), 'path'))
298 if endpoint
.query_params
:
301 cls
._add
_param
_info
(endpoint
.query_params
, p_info
), 'query'))
303 methods
[method
.lower()] = {
304 'tags': [cls
._get
_tag
(endpoint
)],
305 'description': func
.__doc
__,
306 'parameters': params
,
307 'responses': cls
._gen
_responses
(method
, resp
)
310 methods
[method
.lower()]['summary'] = summary
312 if method
.lower() in ['post', 'put']:
313 if endpoint
.body_params
:
314 body_params
= cls
._add
_param
_info
(endpoint
.body_params
, p_info
)
315 methods
[method
.lower()]['requestBody'] = {
317 'application/json': {
318 'schema': cls
._gen
_schema
_for
_content
(body_params
)}}}
320 if endpoint
.query_params
:
321 query_params
= cls
._add
_param
_info
(endpoint
.query_params
, p_info
)
322 methods
[method
.lower()]['requestBody'] = {
324 'application/json': {
325 'schema': cls
._gen
_schema
_for
_content
(query_params
)}}}
327 if endpoint
.is_secure
:
328 methods
[method
.lower()]['security'] = [{'jwt': []}]
331 paths
[path
] = methods
336 def _gen_spec(cls
, all_endpoints
=False, base_url
="", offline
=False):
340 host
= cherrypy
.request
.base
.split('://', 1)[1] if not offline
else 'example.com'
341 logger
.debug("Host: %s", host
)
343 paths
= cls
._gen
_paths
(all_endpoints
)
348 scheme
= 'https' if offline
or mgr
.get_localized_module_option('ssl') else 'http'
353 'description': "This is the official Ceph REST API",
355 'title': "Ceph REST API"
358 'basePath': base_url
,
359 'servers': [{'url': "{}{}".format(
360 cherrypy
.request
.base
if not offline
else '',
362 'tags': cls
._gen
_tags
(all_endpoints
),
370 'bearerFormat': 'JWT'
378 @Endpoint(path
="api.json", version
=None)
380 return self
._gen
_spec
(False, "/")
382 @Endpoint(path
="api-all.json", version
=None)
383 def api_all_json(self
):
384 return self
._gen
_spec
(True, "/")
386 def _swagger_ui_page(self
, all_endpoints
=False):
387 base
= cherrypy
.request
.base
389 spec_url
= "{}/docs/api-all.json".format(base
)
391 spec_url
= "{}/docs/api.json".format(base
)
397 <meta charset="UTF-8">
398 <meta name="referrer" content="no-referrer" />
399 <link rel="stylesheet" type="text/css"
400 href="/swagger-ui.css" >
404 box-sizing: border-box;
405 overflow: -moz-scrollbars-vertical;
421 <div id="swagger-ui"></div>
422 <script src="/swagger-ui-bundle.js">
425 window.onload = function() {{
426 const ui = SwaggerUIBundle({{
428 dom_id: '#swagger-ui',
430 SwaggerUIBundle.presets.apis
443 @Endpoint(json_response
=False, version
=None)
444 def __call__(self
, all_endpoints
=False):
445 return self
._swagger
_ui
_page
(all_endpoints
)
448 if __name__
== "__main__":
453 from . import generate_routes
455 def fix_null_descr(obj
):
457 A hot fix for errors caused by null description values when generating
458 static documentation: better fix would be default values in source
459 to be 'None' strings: however, decorator changes didn't resolve
461 return {k
: fix_null_descr(v
) for k
, v
in obj
.items() if v
is not None} \
462 if isinstance(obj
, dict) else obj
464 generate_routes("/api")
466 with
open(sys
.argv
[1], 'w') as f
:
467 # pylint: disable=protected-access
469 fix_null_descr(Docs
._gen
_spec
(all_endpoints
=False, base_url
="/", offline
=True)),
472 sys
.exit("Output file name missing; correct syntax is: `cmd <file.yml>`")
473 except IsADirectoryError
:
474 sys
.exit("Specified output is a directory; correct syntax is: `cmd <file.yml>`")