]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/docs.py
ab9f5687b532ba33424d12233d5e19bfde8cf12e
1 # -*- coding: utf-8 -*-
3 from typing
import Any
, Dict
, List
, Optional
, Union
8 from ..api
.doc
import Schema
, SchemaInput
, SchemaType
9 from . import ENDPOINT_MAP
, BaseController
, Endpoint
, Router
10 from ._version
import APIVersion
12 NO_DESCRIPTION_AVAILABLE
= "*No description available*"
14 logger
= logging
.getLogger('controllers.docs')
17 @Router('/docs', secure
=False)
18 class Docs(BaseController
):
21 def _gen_tags(cls
, all_endpoints
):
22 """ Generates a list of all tags and corresponding descriptions. """
23 # Scenarios to consider:
24 # * Intentionally make up a new tag name at controller => New tag name displayed.
25 # * Misspell or make up a new tag name at endpoint => Neither tag or endpoint displayed.
26 # * Misspell tag name at controller (when referring to another controller) =>
27 # Tag displayed but no endpoints assigned
28 # * Description for a tag added at multiple locations => Only one description displayed.
30 for endpoints
in ENDPOINT_MAP
.values():
31 for endpoint
in endpoints
:
32 if endpoint
.is_api
or all_endpoints
:
33 list_of_ctrl
.add(endpoint
.ctrl
)
35 tag_map
: Dict
[str, str] = {}
36 for ctrl
in list_of_ctrl
:
37 tag_name
= ctrl
.__name
__
39 if hasattr(ctrl
, 'doc_info'):
40 if ctrl
.doc_info
['tag']:
41 tag_name
= ctrl
.doc_info
['tag']
42 tag_descr
= ctrl
.doc_info
['tag_descr']
43 if tag_name
not in tag_map
or not tag_map
[tag_name
]:
44 tag_map
[tag_name
] = tag_descr
46 tags
= [{'name': k
, 'description': v
if v
else NO_DESCRIPTION_AVAILABLE
}
47 for k
, v
in tag_map
.items()]
48 tags
.sort(key
=lambda e
: e
['name'])
52 def _get_tag(cls
, endpoint
):
53 """ Returns the name of a tag to assign to a path. """
57 if hasattr(func
, 'doc_info') and func
.doc_info
['tag']:
58 tag
= func
.doc_info
['tag']
59 elif hasattr(ctrl
, 'doc_info') and ctrl
.doc_info
['tag']:
60 tag
= ctrl
.doc_info
['tag']
64 def _gen_type(cls
, param
):
65 # pylint: disable=too-many-return-statements
67 Generates the type of parameter based on its name and default value,
68 using very simple heuristics.
69 Used if type is not explicitly defined.
71 param_name
= param
['name']
72 def_value
= param
['default'] if 'default' in param
else None
73 if param_name
.startswith("is_"):
74 return str(SchemaType
.BOOLEAN
)
75 if "size" in param_name
:
76 return str(SchemaType
.INTEGER
)
77 if "count" in param_name
:
78 return str(SchemaType
.INTEGER
)
79 if "num" in param_name
:
80 return str(SchemaType
.INTEGER
)
81 if isinstance(def_value
, bool):
82 return str(SchemaType
.BOOLEAN
)
83 if isinstance(def_value
, int):
84 return str(SchemaType
.INTEGER
)
85 return str(SchemaType
.STRING
)
88 # isinstance doesn't work: input is always <type 'type'>.
89 def _type_to_str(cls
, type_as_type
):
90 """ Used if type is explicitly defined. """
91 if type_as_type
is str:
92 type_as_str
= str(SchemaType
.STRING
)
93 elif type_as_type
is int:
94 type_as_str
= str(SchemaType
.INTEGER
)
95 elif type_as_type
is bool:
96 type_as_str
= str(SchemaType
.BOOLEAN
)
97 elif type_as_type
is list or type_as_type
is tuple:
98 type_as_str
= str(SchemaType
.ARRAY
)
99 elif type_as_type
is float:
100 type_as_str
= str(SchemaType
.NUMBER
)
102 type_as_str
= str(SchemaType
.OBJECT
)
106 def _add_param_info(cls
, parameters
, p_info
):
108 # * Parameter name (if not nested) misspelt in decorator => parameter not displayed
109 # * Sometimes a parameter is used for several endpoints (e.g. fs_id in CephFS).
110 # Currently, there is no possibility of reuse. Should there be?
111 # But what if there are two parameters with same name but different functionality?
113 Adds explicitly described information for parameters of an endpoint.
116 * Either the parameter in p_info corresponds to an endpoint parameter. Implicit information
117 has higher priority, so only information that doesn't already exist is added.
118 * Or the parameter in p_info describes a nested parameter inside an endpoint parameter.
119 In that case there is no implicit information at all so all explicitly described info needs
124 for parameter
in parameters
:
125 if p
['name'] == parameter
['name']:
126 parameter
['type'] = p
['type']
127 parameter
['description'] = p
['description']
128 if 'nested_params' in p
:
129 parameter
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
134 'description': p
['description'],
135 'required': p
['required'],
138 nested_p
['default'] = p
['default']
139 if 'nested_params' in p
:
140 nested_p
['nested_params'] = cls
._add
_param
_info
([], p
['nested_params'])
141 parameters
.append(nested_p
)
146 def _gen_schema_for_content(cls
, params
: List
[Any
]) -> Dict
[str, Any
]:
148 Generates information to the content-object in OpenAPI Spec.
149 Used to for request body and responses.
153 schema_type
= SchemaType
.OBJECT
154 if isinstance(params
, SchemaInput
):
155 schema_type
= params
.type
156 params
= params
.params
159 if param
['required']:
160 required_params
.append(param
['name'])
164 props
['type'] = cls
._type
_to
_str
(param
['type'])
165 if 'nested_params' in param
:
166 if props
['type'] == str(SchemaType
.ARRAY
): # dict in array
167 props
['items'] = cls
._gen
_schema
_for
_content
(param
['nested_params'])
169 props
= cls
._gen
_schema
_for
_content
(param
['nested_params'])
170 elif props
['type'] == str(SchemaType
.OBJECT
): # e.g. [int]
171 props
['type'] = str(SchemaType
.ARRAY
)
172 props
['items'] = {'type': cls
._type
_to
_str
(param
['type'][0])}
174 props
['type'] = cls
._gen
_type
(param
)
175 if 'description' in param
:
176 props
['description'] = param
['description']
177 if 'default' in param
:
178 props
['default'] = param
['default']
179 properties
[param
['name']] = props
181 schema
= Schema(schema_type
=schema_type
, properties
=properties
,
182 required
=required_params
)
184 return schema
.as_dict()
187 def _gen_responses(cls
, method
, resp_object
=None,
188 version
: Optional
[APIVersion
] = 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."
208 version
= APIVersion
.DEFAULT
210 if method
.lower() == 'get':
211 resp
['200'] = {'description': "OK",
212 'content': {version
.to_mime_type():
214 if method
.lower() == 'post':
215 resp
['201'] = {'description': "Resource created.",
216 'content': {version
.to_mime_type():
218 if method
.lower() == 'put':
219 resp
['200'] = {'description': "Resource updated.",
220 'content': {version
.to_mime_type():
222 if method
.lower() == 'delete':
223 resp
['204'] = {'description': "Resource deleted.",
224 'content': {version
.to_mime_type():
226 if method
.lower() in ['post', 'put', 'delete']:
227 resp
['202'] = {'description': "Operation is still executing."
228 " Please check the task queue.",
229 'content': {version
.to_mime_type():
233 for status_code
, response_body
in resp_object
.items():
234 if status_code
in resp
:
235 resp
[status_code
].update(
237 {version
.to_mime_type():
238 {'schema': cls
._gen
_schema
_for
_content
(response_body
)}
244 def _gen_params(cls
, params
, location
):
248 _type
= cls
._type
_to
_str
(param
['type'])
250 _type
= cls
._gen
_type
(param
)
252 'name': param
['name'],
258 if param
.get('description'):
259 res
['description'] = param
['description']
260 if param
['required']:
261 res
['required'] = True
262 elif param
['default'] is None:
263 res
['allowEmptyValue'] = True
265 res
['default'] = param
['default']
266 parameters
.append(res
)
271 def gen_paths(cls
, all_endpoints
):
272 # pylint: disable=R0912
273 method_order
= ['get', 'post', 'put', 'delete']
275 for path
, endpoints
in sorted(list(ENDPOINT_MAP
.items()),
280 endpoint_list
= sorted(endpoints
, key
=lambda e
:
281 method_order
.index(e
.method
.lower()))
282 for endpoint
in endpoint_list
:
283 if not endpoint
.is_api
and not all_endpoints
:
287 method
= endpoint
.method
295 if hasattr(func
, '__method_map_method__'):
296 version
= func
.__method
_map
_method
__['version']
298 elif hasattr(func
, '__resource_method__'):
299 version
= func
.__resource
_method
__['version']
301 elif hasattr(func
, '__collection_method__'):
302 version
= func
.__collection
_method
__['version']
304 if hasattr(func
, 'doc_info'):
305 if func
.doc_info
['summary']:
306 summary
= func
.doc_info
['summary']
307 resp
= func
.doc_info
['response']
308 p_info
= func
.doc_info
['parameters']
310 if endpoint
.path_params
:
313 cls
._add
_param
_info
(endpoint
.path_params
, p_info
), 'path'))
314 if endpoint
.query_params
:
317 cls
._add
_param
_info
(endpoint
.query_params
, p_info
), 'query'))
319 methods
[method
.lower()] = {
320 'tags': [cls
._get
_tag
(endpoint
)],
321 'description': func
.__doc
__,
322 'parameters': params
,
323 'responses': cls
._gen
_responses
(method
, resp
, version
)
326 methods
[method
.lower()]['summary'] = summary
328 if method
.lower() in ['post', 'put']:
329 if endpoint
.body_params
:
330 body_params
= cls
._add
_param
_info
(endpoint
.body_params
, p_info
)
331 methods
[method
.lower()]['requestBody'] = {
333 'application/json': {
334 'schema': cls
._gen
_schema
_for
_content
(body_params
)}}}
336 if endpoint
.query_params
:
337 query_params
= cls
._add
_param
_info
(endpoint
.query_params
, p_info
)
338 methods
[method
.lower()]['requestBody'] = {
340 'application/json': {
341 'schema': cls
._gen
_schema
_for
_content
(query_params
)}}}
343 if endpoint
.is_secure
:
344 methods
[method
.lower()]['security'] = [{'jwt': []}]
347 paths
[path
] = methods
352 def _gen_spec(cls
, all_endpoints
=False, base_url
="", offline
=False):
356 host
= cherrypy
.request
.base
.split('://', 1)[1] if not offline
else 'example.com'
357 logger
.debug("Host: %s", host
)
359 paths
= cls
.gen_paths(all_endpoints
)
364 scheme
= 'https' if offline
or mgr
.get_localized_module_option('ssl') else 'http'
369 'description': "This is the official Ceph REST API",
371 'title': "Ceph REST API"
374 'basePath': base_url
,
375 'servers': [{'url': "{}{}".format(
376 cherrypy
.request
.base
if not offline
else '',
378 'tags': cls
._gen
_tags
(all_endpoints
),
386 'bearerFormat': 'JWT'
394 @Endpoint(path
="openapi.json", version
=None)
395 def open_api_json(self
):
396 return self
._gen
_spec
(False, "/")
398 @Endpoint(path
="api-all.json", version
=None)
399 def api_all_json(self
):
400 return self
._gen
_spec
(True, "/")
403 if __name__
== "__main__":
408 def fix_null_descr(obj
):
410 A hot fix for errors caused by null description values when generating
411 static documentation: better fix would be default values in source
412 to be 'None' strings: however, decorator changes didn't resolve
414 return {k
: fix_null_descr(v
) for k
, v
in obj
.items() if v
is not None} \
415 if isinstance(obj
, dict) else obj
417 Router
.generate_routes("/api")
419 with
open(sys
.argv
[1], 'w') as f
:
420 # pylint: disable=protected-access
422 fix_null_descr(Docs
._gen
_spec
(all_endpoints
=False, base_url
="/", offline
=True)),
425 sys
.exit("Output file name missing; correct syntax is: `cmd <file.yml>`")
426 except IsADirectoryError
:
427 sys
.exit("Specified output is a directory; correct syntax is: `cmd <file.yml>`")