]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/docs.py
update sources to ceph Nautilus 14.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / docs.py
1 # -*- coding: utf-8 -*-
2 from __future__ import absolute_import
3
4 import cherrypy
5
6 from . import Controller, BaseController, Endpoint, ENDPOINT_MAP
7 from .. import logger, mgr
8
9 from ..tools import str_to_bool
10
11
12 @Controller('/docs', secure=False)
13 class Docs(BaseController):
14
15 @classmethod
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.
24 list_of_ctrl = set()
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)
29
30 TAG_MAP = {}
31 for ctrl in list_of_ctrl:
32 tag_name = ctrl.__name__
33 tag_descr = ""
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
40
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'])
44 return tags
45
46 @classmethod
47 def _get_tag(cls, endpoint):
48 """ Returns the name of a tag to assign to a path. """
49 ctrl = endpoint.ctrl
50 func = endpoint.func
51 tag = ctrl.__name__
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']
56 return tag
57
58 @classmethod
59 def _gen_type(cls, param):
60 # pylint: disable=too-many-return-statements
61 """
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.
65 """
66 param_name = param['name']
67 def_value = param['default'] if 'default' in param else None
68 if param_name.startswith("is_"):
69 return "boolean"
70 if "size" in param_name:
71 return "integer"
72 if "count" in param_name:
73 return "integer"
74 if "num" in param_name:
75 return "integer"
76 if isinstance(def_value, bool):
77 return "boolean"
78 if isinstance(def_value, int):
79 return "integer"
80 return "string"
81
82 @classmethod
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:
93 type_as_str = 'array'
94 elif type_as_type is float:
95 type_as_str = 'number'
96 else:
97 type_as_str = 'object'
98 return type_as_str
99
100 @classmethod
101 def _add_param_info(cls, parameters, p_info):
102 # Cases to consider:
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?
107 """
108 Adds explicitly described information for parameters of an endpoint.
109
110 There are two cases:
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
115 to be added.
116 """
117 for p in p_info:
118 if not p['nested']:
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'])
125 else:
126 nested_p = {
127 'name': p['name'],
128 'type': p['type'],
129 'description': p['description'],
130 'required': p['required'],
131 }
132 if 'default' in p:
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)
137
138 return parameters
139
140 @classmethod
141 def _gen_schema_for_content(cls, params):
142 """
143 Generates information to the content-object in OpenAPI Spec.
144 Used to for request body and responses.
145 """
146 required_params = []
147 properties = {}
148
149 for param in params:
150 if param['required']:
151 required_params.append(param['name'])
152
153 props = {}
154 if 'type' in param:
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'])
159 else: # dict in dict
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])}
164 else:
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
171
172 schema = {
173 'type': 'object',
174 'properties': properties,
175 }
176 if required_params:
177 schema['required'] = required_params
178 return schema
179
180 @classmethod
181 def _gen_responses(cls, method, resp_object=None):
182 resp = {
183 '400': {
184 "description": "Operation exception. Please check the "
185 "response body for details."
186 },
187 '401': {
188 "description": "Unauthenticated access. Please login first."
189 },
190 '403': {
191 "description": "Unauthorized access. Please check your "
192 "permissions."
193 },
194 '500': {
195 "description": "Unexpected error. Please check the "
196 "response body for the stack trace."
197 }
198 }
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."}
210
211 if resp_object:
212 for status_code, response_body in resp_object.items():
213 resp[status_code].update({
214 'content': {
215 'application/json': {
216 'schema': cls._gen_schema_for_content(response_body)}}})
217
218 return resp
219
220 @classmethod
221 def _gen_params(cls, params, location):
222 parameters = []
223 for param in params:
224 if 'type' in param:
225 _type = cls._type_to_str(param['type'])
226 else:
227 _type = cls._gen_type(param)
228 if 'description' in param:
229 descr = param['description']
230 else:
231 descr = "*No description available*"
232 res = {
233 'name': param['name'],
234 'in': location,
235 'schema': {
236 'type': _type
237 },
238 'description': descr
239 }
240 if param['required']:
241 res['required'] = True
242 elif param['default'] is None:
243 res['allowEmptyValue'] = True
244 else:
245 res['default'] = param['default']
246 parameters.append(res)
247
248 return parameters
249
250 @classmethod
251 def _gen_paths(cls, all_endpoints, baseUrl):
252 METHOD_ORDER = ['get', 'post', 'put', 'delete']
253 paths = {}
254 for path, endpoints in sorted(list(ENDPOINT_MAP.items()),
255 key=lambda p: p[0]):
256 methods = {}
257 skip = False
258
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:
263 skip = True
264 break
265
266 method = endpoint.method
267 func = endpoint.func
268
269 summary = "No description available"
270 resp = {}
271 p_info = []
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']
277 params = []
278 if endpoint.path_params:
279 params.extend(
280 cls._gen_params(
281 cls._add_param_info(endpoint.path_params, p_info), 'path'))
282 if endpoint.query_params:
283 params.extend(
284 cls._gen_params(
285 cls._add_param_info(endpoint.query_params, p_info), 'query'))
286
287 methods[method.lower()] = {
288 'tags': [cls._get_tag(endpoint)],
289 'summary': summary,
290 'description': func.__doc__,
291 'parameters': params,
292 'responses': cls._gen_responses(method, resp)
293 }
294
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'] = {
299 'content': {
300 'application/json': {
301 'schema': cls._gen_schema_for_content(body_params)}}}
302
303 if endpoint.is_secure:
304 methods[method.lower()]['security'] = [{'jwt': []}]
305
306 if not skip:
307 paths[path[len(baseUrl):]] = methods
308
309 return paths
310
311 def _gen_spec(self, all_endpoints=False, base_url=""):
312 if all_endpoints:
313 base_url = ""
314
315 host = cherrypy.request.base
316 host = host[host.index(':')+3:]
317 logger.debug("DOCS: Host: %s", host)
318
319 paths = self._gen_paths(all_endpoints, base_url)
320
321 if not base_url:
322 base_url = "/"
323
324 scheme = 'https'
325 ssl = str_to_bool(mgr.get_localized_module_option('ssl', True))
326 if not ssl:
327 scheme = 'http'
328
329 spec = {
330 'openapi': "3.0.0",
331 'info': {
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 "
337 "own risk.",
338 'version': "v1",
339 'title': "Ceph-Dashboard REST API"
340 },
341 'host': host,
342 'basePath': base_url,
343 'servers': [{'url': "{}{}".format(cherrypy.request.base, base_url)}],
344 'tags': self._gen_tags(all_endpoints),
345 'schemes': [scheme],
346 'paths': paths,
347 'components': {
348 'securitySchemes': {
349 'jwt': {
350 'type': 'http',
351 'scheme': 'bearer',
352 'bearerFormat': 'JWT'
353 }
354 }
355 }
356 }
357
358 return spec
359
360 @Endpoint(path="api.json")
361 def api_json(self):
362 return self._gen_spec(False, "/api")
363
364 @Endpoint(path="api-all.json")
365 def api_all_json(self):
366 return self._gen_spec(True, "/api")
367
368 def _swagger_ui_page(self, all_endpoints=False, token=None):
369 base = cherrypy.request.base
370 if all_endpoints:
371 spec_url = "{}/docs/api-all.json".format(base)
372 else:
373 spec_url = "{}/docs/api.json".format(base)
374
375 auth_header = cherrypy.request.headers.get('authorization')
376 jwt_token = ""
377 if auth_header is not None:
378 scheme, params = auth_header.split(' ', 1)
379 if scheme.lower() == 'bearer':
380 jwt_token = params
381 else:
382 if token is not None:
383 jwt_token = token
384
385 api_key_callback = """, onComplete: () => {{
386 ui.preauthorizeApiKey('jwt', '{}');
387 }}
388 """.format(jwt_token)
389
390 page = """
391 <!DOCTYPE html>
392 <html>
393 <head>
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"
398 rel="stylesheet">
399 <link rel="stylesheet" type="text/css"
400 href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" >
401 <style>
402 html
403 {{
404 box-sizing: border-box;
405 overflow: -moz-scrollbars-vertical;
406 overflow-y: scroll;
407 }}
408 *,
409 *:before,
410 *:after
411 {{
412 box-sizing: inherit;
413 }}
414 body {{
415 margin:0;
416 background: #fafafa;
417 }}
418 </style>
419 </head>
420 <body>
421 <div id="swagger-ui"></div>
422 <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js">
423 </script>
424 <script>
425 window.onload = function() {{
426 const ui = SwaggerUIBundle({{
427 url: '{}',
428 dom_id: '#swagger-ui',
429 presets: [
430 SwaggerUIBundle.presets.apis
431 ],
432 layout: "BaseLayout"
433 {}
434 }})
435 window.ui = ui
436 }}
437 </script>
438 </body>
439 </html>
440 """.format(spec_url, api_key_callback)
441
442 return page
443
444 @Endpoint(json_response=False)
445 def __call__(self, all_endpoints=False):
446 return self._swagger_ui_page(all_endpoints)
447
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)