]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/_rest_controller.py
update ceph source to reef 18.1.2
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / _rest_controller.py
1 import collections
2 import inspect
3 from functools import wraps
4 from typing import Optional
5
6 import cherrypy
7
8 from ..security import Permission
9 from ._base_controller import BaseController
10 from ._endpoint import Endpoint
11 from ._helpers import _get_function_params
12 from ._permissions import _set_func_permissions
13 from ._version import APIVersion
14
15
16 class RESTController(BaseController, skip_registry=True):
17 """
18 Base class for providing a RESTful interface to a resource.
19
20 To use this class, simply derive a class from it and implement the methods
21 you want to support. The list of possible methods are:
22
23 * list()
24 * bulk_set(data)
25 * create(data)
26 * bulk_delete()
27 * get(key)
28 * set(data, key)
29 * singleton_set(data)
30 * delete(key)
31
32 Test with curl:
33
34 curl -H "Content-Type: application/json" -X POST \
35 -d '{"username":"xyz","password":"xyz"}' https://127.0.0.1:8443/foo
36 curl https://127.0.0.1:8443/foo
37 curl https://127.0.0.1:8443/foo/0
38
39 """
40
41 # resource id parameter for using in get, set, and delete methods
42 # should be overridden by subclasses.
43 # to specify a composite id (two parameters) use '/'. e.g., "param1/param2".
44 # If subclasses don't override this property we try to infer the structure
45 # of the resource ID.
46 RESOURCE_ID: Optional[str] = None
47
48 _permission_map = {
49 'GET': Permission.READ,
50 'POST': Permission.CREATE,
51 'PUT': Permission.UPDATE,
52 'DELETE': Permission.DELETE
53 }
54
55 _method_mapping = collections.OrderedDict([
56 ('list', {'method': 'GET', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
57 ('create', {'method': 'POST', 'resource': False, 'status': 201, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
58 ('bulk_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
59 ('bulk_delete', {'method': 'DELETE', 'resource': False, 'status': 204, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
60 ('get', {'method': 'GET', 'resource': True, 'status': 200, 'version': APIVersion.DEFAULT}),
61 ('delete', {'method': 'DELETE', 'resource': True, 'status': 204, 'version': APIVersion.DEFAULT}), # noqa E501 #pylint: disable=line-too-long
62 ('set', {'method': 'PUT', 'resource': True, 'status': 200, 'version': APIVersion.DEFAULT}),
63 ('singleton_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': APIVersion.DEFAULT}) # noqa E501 #pylint: disable=line-too-long
64 ])
65
66 @classmethod
67 def infer_resource_id(cls):
68 if cls.RESOURCE_ID is not None:
69 return cls.RESOURCE_ID.split('/')
70 for k, v in cls._method_mapping.items():
71 func = getattr(cls, k, None)
72 while hasattr(func, "__wrapped__"):
73 assert func
74 func = func.__wrapped__
75 if v['resource'] and func:
76 path_params = cls.get_path_param_names()
77 params = _get_function_params(func)
78 return [p['name'] for p in params
79 if p['required'] and p['name'] not in path_params]
80 return None
81
82 @classmethod
83 def endpoints(cls):
84 result = super().endpoints()
85 res_id_params = cls.infer_resource_id()
86
87 for name, func in inspect.getmembers(cls, predicate=callable):
88 endpoint_params = {
89 'no_resource_id_params': False,
90 'status': 200,
91 'method': None,
92 'query_params': None,
93 'path': '',
94 'version': APIVersion.DEFAULT,
95 'sec_permissions': hasattr(func, '_security_permissions'),
96 'permission': None,
97 }
98 if name in cls._method_mapping:
99 cls._update_endpoint_params_method_map(
100 func, res_id_params, endpoint_params, name=name)
101
102 elif hasattr(func, "__collection_method__"):
103 cls._update_endpoint_params_collection_map(func, endpoint_params)
104
105 elif hasattr(func, "__resource_method__"):
106 cls._update_endpoint_params_resource_method(
107 res_id_params, endpoint_params, func)
108
109 else:
110 continue
111
112 if endpoint_params['no_resource_id_params']:
113 raise TypeError("Could not infer the resource ID parameters for"
114 " method {} of controller {}. "
115 "Please specify the resource ID parameters "
116 "using the RESOURCE_ID class property"
117 .format(func.__name__, cls.__name__))
118
119 if endpoint_params['method'] in ['GET', 'DELETE']:
120 params = _get_function_params(func)
121 if res_id_params is None:
122 res_id_params = []
123 if endpoint_params['query_params'] is None:
124 endpoint_params['query_params'] = [p['name'] for p in params # type: ignore
125 if p['name'] not in res_id_params]
126
127 func = cls._status_code_wrapper(func, endpoint_params['status'])
128 endp_func = Endpoint(endpoint_params['method'], path=endpoint_params['path'],
129 query_params=endpoint_params['query_params'],
130 version=endpoint_params['version'])(func) # type: ignore
131 if endpoint_params['permission']:
132 _set_func_permissions(endp_func, [endpoint_params['permission']])
133 result.append(cls.Endpoint(cls, endp_func))
134
135 return result
136
137 @classmethod
138 def _update_endpoint_params_resource_method(cls, res_id_params, endpoint_params, func):
139 if not res_id_params:
140 endpoint_params['no_resource_id_params'] = True
141 else:
142 path_params = ["{{{}}}".format(p) for p in res_id_params]
143 endpoint_params['path'] += "/{}".format("/".join(path_params))
144 if func.__resource_method__['path']:
145 endpoint_params['path'] += func.__resource_method__['path']
146 else:
147 endpoint_params['path'] += "/{}".format(func.__name__)
148 endpoint_params['status'] = func.__resource_method__['status']
149 endpoint_params['method'] = func.__resource_method__['method']
150 endpoint_params['version'] = func.__resource_method__['version']
151 endpoint_params['query_params'] = func.__resource_method__['query_params']
152 if not endpoint_params['sec_permissions']:
153 endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
154
155 @classmethod
156 def _update_endpoint_params_collection_map(cls, func, endpoint_params):
157 if func.__collection_method__['path']:
158 endpoint_params['path'] = func.__collection_method__['path']
159 else:
160 endpoint_params['path'] = "/{}".format(func.__name__)
161 endpoint_params['status'] = func.__collection_method__['status']
162 endpoint_params['method'] = func.__collection_method__['method']
163 endpoint_params['query_params'] = func.__collection_method__['query_params']
164 endpoint_params['version'] = func.__collection_method__['version']
165 if not endpoint_params['sec_permissions']:
166 endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
167
168 @classmethod
169 def _update_endpoint_params_method_map(cls, func, res_id_params, endpoint_params, name=None):
170 meth = cls._method_mapping[func.__name__ if not name else name] # type: dict
171
172 if meth['resource']:
173 if not res_id_params:
174 endpoint_params['no_resource_id_params'] = True
175 else:
176 path_params = ["{{{}}}".format(p) for p in res_id_params]
177 endpoint_params['path'] += "/{}".format("/".join(path_params))
178
179 endpoint_params['status'] = meth['status']
180 endpoint_params['method'] = meth['method']
181 if hasattr(func, "__method_map_method__"):
182 endpoint_params['version'] = func.__method_map_method__['version']
183 if not endpoint_params['sec_permissions']:
184 endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
185
186 @classmethod
187 def _status_code_wrapper(cls, func, status_code):
188 @wraps(func)
189 def wrapper(*vpath, **params):
190 cherrypy.response.status = status_code
191 return func(*vpath, **params)
192
193 return wrapper
194
195 @staticmethod
196 def Resource(method=None, path=None, status=None, query_params=None, # noqa: N802
197 version: Optional[APIVersion] = APIVersion.DEFAULT):
198 if not method:
199 method = 'GET'
200
201 if status is None:
202 status = 200
203
204 def _wrapper(func):
205 func.__resource_method__ = {
206 'method': method,
207 'path': path,
208 'status': status,
209 'query_params': query_params,
210 'version': version
211 }
212 return func
213 return _wrapper
214
215 @staticmethod
216 def MethodMap(resource=False, status=None,
217 version: Optional[APIVersion] = APIVersion.DEFAULT): # noqa: N802
218
219 if status is None:
220 status = 200
221
222 def _wrapper(func):
223 func.__method_map_method__ = {
224 'resource': resource,
225 'status': status,
226 'version': version
227 }
228 return func
229 return _wrapper
230
231 @staticmethod
232 def Collection(method=None, path=None, status=None, query_params=None, # noqa: N802
233 version: Optional[APIVersion] = APIVersion.DEFAULT):
234 if not method:
235 method = 'GET'
236
237 if status is None:
238 status = 200
239
240 def _wrapper(func):
241 func.__collection_method__ = {
242 'method': method,
243 'path': path,
244 'status': status,
245 'query_params': query_params,
246 'version': version
247 }
248 return func
249 return _wrapper