]>
Commit | Line | Data |
---|---|---|
7c673cae FG |
1 | |
2 | ||
3 | """ | |
4 | Helpers for writing django views and rest_framework ViewSets that get | |
5 | their data from calls into the C++ side of ceph-mgr | |
6 | """ | |
7 | ||
8 | ||
9 | from rest_framework import viewsets, status | |
10 | from rest_framework.views import APIView | |
11 | ||
12 | from rest_framework.response import Response | |
13 | ||
14 | from rest.app.manager.osd_request_factory import OsdRequestFactory | |
15 | from rest.app.manager.pool_request_factory import PoolRequestFactory | |
16 | from rest.app.types import OsdMap, OSD, OSD_MAP, POOL, CRUSH_RULE, NotFound | |
17 | from rest.module import global_instance as rest_plugin | |
18 | ||
19 | from rest.logger import logger | |
20 | log = logger() | |
21 | ||
22 | ||
23 | class DataObject(object): | |
24 | """ | |
25 | A convenience for converting dicts from the backend into | |
26 | objects, because django_rest_framework expects objects | |
27 | """ | |
28 | def __init__(self, data): | |
29 | self.__dict__.update(data) | |
30 | ||
31 | ||
32 | class MgrClient(object): | |
33 | cluster_monitor = None | |
34 | ||
35 | def __init__(self): | |
36 | self._request_factories = { | |
37 | OSD: OsdRequestFactory, | |
38 | POOL: PoolRequestFactory | |
39 | } | |
40 | ||
41 | def get_sync_object(self, object_type, path=None): | |
42 | return rest_plugin().get_sync_object(object_type, path) | |
43 | ||
44 | def get_metadata(self, svc_type, svc_id): | |
45 | return rest_plugin().get_metadata(svc_type, svc_id) | |
46 | ||
47 | def get(self, object_type, object_id): | |
48 | """ | |
49 | Get one object from a particular cluster. | |
50 | """ | |
51 | ||
52 | if object_type == OSD: | |
53 | return self._osd_resolve(object_id) | |
54 | elif object_type == POOL: | |
55 | return self._pool_resolve(object_id) | |
56 | else: | |
57 | raise NotImplementedError(object_type) | |
58 | ||
59 | def get_valid_commands(self, object_type, object_ids): | |
60 | """ | |
61 | Determine what commands can be run on OSD object_ids | |
62 | """ | |
63 | if object_type != OSD: | |
64 | raise NotImplementedError(object_type) | |
65 | ||
66 | try: | |
67 | valid_commands = self.get_request_factory( | |
68 | object_type).get_valid_commands(object_ids) | |
69 | except KeyError as e: | |
70 | raise NotFound(object_type, str(e)) | |
71 | ||
72 | return valid_commands | |
73 | ||
74 | def _osd_resolve(self, osd_id): | |
75 | osdmap = self.get_sync_object(OsdMap) | |
76 | ||
77 | try: | |
78 | return osdmap.osds_by_id[osd_id] | |
79 | except KeyError: | |
80 | raise NotFound(OSD, osd_id) | |
81 | ||
82 | def _pool_resolve(self, pool_id): | |
83 | osdmap = self.get_sync_object(OsdMap) | |
84 | ||
85 | try: | |
86 | return osdmap.pools_by_id[pool_id] | |
87 | except KeyError: | |
88 | raise NotFound(POOL, pool_id) | |
89 | ||
90 | def list_requests(self, filter_args): | |
91 | state = filter_args.get('state', None) | |
92 | fsid = filter_args.get('fsid', None) | |
93 | requests = rest_plugin().requests.get_all() | |
94 | return sorted([self._dump_request(r) | |
95 | for r in requests | |
96 | if (state is None or r.state == state) and (fsid is None or r.fsid == fsid)], | |
97 | lambda a, b: cmp(b['requested_at'], a['requested_at'])) | |
98 | ||
99 | def _dump_request(self, request): | |
100 | """UserRequest to JSON-serializable form""" | |
101 | return { | |
102 | 'id': request.id, | |
103 | 'state': request.state, | |
104 | 'error': request.error, | |
105 | 'error_message': request.error_message, | |
106 | 'status': request.status, | |
107 | 'headline': request.headline, | |
108 | 'requested_at': request.requested_at, | |
109 | 'completed_at': request.completed_at | |
110 | } | |
111 | ||
112 | def get_request(self, request_id): | |
113 | """ | |
114 | Get a JSON representation of a UserRequest | |
115 | """ | |
116 | try: | |
117 | return self._dump_request(rest_plugin().requests.get_by_id(request_id)) | |
118 | except KeyError: | |
119 | raise NotFound('request', request_id) | |
120 | ||
121 | def cancel_request(self, request_id): | |
122 | try: | |
123 | rest_plugin().requests.cancel(request_id) | |
124 | return self.get_request(request_id) | |
125 | except KeyError: | |
126 | raise NotFound('request', request_id) | |
127 | ||
128 | def list(self, object_type, list_filter): | |
129 | """ | |
130 | Get many objects | |
131 | """ | |
132 | ||
133 | osd_map = self.get_sync_object(OsdMap).data | |
134 | if osd_map is None: | |
135 | return [] | |
136 | if object_type == OSD: | |
137 | result = osd_map['osds'] | |
138 | if 'id__in' in list_filter: | |
139 | result = [o for o in result if o['osd'] in list_filter['id__in']] | |
140 | if 'pool' in list_filter: | |
141 | try: | |
142 | osds_in_pool = self.get_sync_object(OsdMap).osds_by_pool[list_filter['pool']] | |
143 | except KeyError: | |
144 | raise NotFound("Pool {0} does not exist".format(list_filter['pool'])) | |
145 | else: | |
146 | result = [o for o in result if o['osd'] in osds_in_pool] | |
147 | ||
148 | return result | |
149 | elif object_type == POOL: | |
150 | return osd_map['pools'] | |
151 | elif object_type == CRUSH_RULE: | |
152 | return osd_map['crush']['rules'] | |
153 | else: | |
154 | raise NotImplementedError(object_type) | |
155 | ||
156 | def request_delete(self, obj_type, obj_id): | |
157 | return self._request('delete', obj_type, obj_id) | |
158 | ||
159 | def request_create(self, obj_type, attributes): | |
160 | return self._request('create', obj_type, attributes) | |
161 | ||
162 | def request_update(self, command, obj_type, obj_id, attributes): | |
163 | return self._request(command, obj_type, obj_id, attributes) | |
164 | ||
165 | def request_apply(self, obj_type, obj_id, command): | |
166 | return self._request(command, obj_type, obj_id) | |
167 | ||
168 | def update(self, object_type, object_id, attributes): | |
169 | """ | |
170 | Modify an object in a cluster. | |
171 | """ | |
172 | ||
173 | if object_type == OSD: | |
174 | # Run a resolve to throw exception if it's unknown | |
175 | self._osd_resolve(object_id) | |
176 | if 'id' not in attributes: | |
177 | attributes['id'] = object_id | |
178 | ||
179 | return self.request_update('update', OSD, object_id, attributes) | |
180 | elif object_type == POOL: | |
181 | self._pool_resolve(object_id) | |
182 | if 'id' not in attributes: | |
183 | attributes['id'] = object_id | |
184 | ||
185 | return self.request_update('update', POOL, object_id, attributes) | |
186 | elif object_type == OSD_MAP: | |
187 | return self.request_update('update_config', OSD, object_id, attributes) | |
188 | ||
189 | else: | |
190 | raise NotImplementedError(object_type) | |
191 | ||
192 | def get_request_factory(self, object_type): | |
193 | try: | |
194 | return self._request_factories[object_type]() | |
195 | except KeyError: | |
196 | raise ValueError("{0} is not one of {1}".format(object_type, self._request_factories.keys())) | |
197 | ||
198 | def _request(self, method, obj_type, *args, **kwargs): | |
199 | """ | |
200 | Create and submit UserRequest for an apply, create, update or delete. | |
201 | """ | |
202 | ||
203 | request_factory = self.get_request_factory(obj_type) | |
204 | request = getattr(request_factory, method)(*args, **kwargs) | |
205 | ||
206 | if request: | |
207 | # sleeps permitted during terminal phase of submitting, because we're | |
208 | # doing I/O to the salt master to kick off | |
209 | rest_plugin().requests.submit(request) | |
210 | return { | |
211 | 'request_id': request.id | |
212 | } | |
213 | else: | |
214 | return None | |
215 | ||
216 | def server_get(self, fqdn): | |
217 | return rest_plugin().get_server(fqdn) | |
218 | ||
219 | def server_list(self): | |
220 | return rest_plugin().list_servers() | |
221 | ||
222 | ||
223 | from rest_framework.permissions import IsAuthenticated, BasePermission | |
224 | ||
225 | ||
226 | class IsRoleAllowed(BasePermission): | |
227 | def has_permission(self, request, view): | |
228 | return True | |
229 | ||
230 | # TODO: reinstate read vs. read/write limitations on API keys | |
231 | # has_permission = False | |
232 | # if request.user.groups.filter(name='readonly').exists(): | |
233 | # has_permission = request.method in SAFE_METHODS | |
234 | # view.headers['Allow'] = ', '.join(SAFE_METHODS) | |
235 | # elif request.user.groups.filter(name='read/write').exists(): | |
236 | # has_permission = True | |
237 | # elif request.user.is_superuser: | |
238 | # has_permission = True | |
239 | # | |
240 | # return has_permission | |
241 | ||
242 | ||
243 | class RPCView(APIView): | |
244 | serializer_class = None | |
245 | log = log | |
246 | permission_classes = [IsAuthenticated, IsRoleAllowed] | |
247 | ||
248 | def get_authenticators(self): | |
249 | return rest_plugin().get_authenticators() | |
250 | ||
251 | def __init__(self, *args, **kwargs): | |
252 | super(RPCView, self).__init__(*args, **kwargs) | |
253 | self.client = MgrClient() | |
254 | ||
255 | @property | |
256 | def help(self): | |
257 | return self.__doc__ | |
258 | ||
259 | @property | |
260 | def help_summary(self): | |
261 | return "" | |
262 | ||
263 | def handle_exception(self, exc): | |
264 | try: | |
265 | return super(RPCView, self).handle_exception(exc) | |
266 | except NotFound as e: | |
267 | return Response(str(e), status=status.HTTP_404_NOT_FOUND) | |
268 | ||
269 | def metadata(self, request): | |
270 | ret = super(RPCView, self).metadata(request) | |
271 | ||
272 | actions = {} | |
273 | # TODO: get the fields marked up with whether they are: | |
274 | # - [allowed|required|forbidden] during [creation|update] (6 possible kinds of field) | |
275 | # e.g. on a pool | |
276 | # id is forbidden during creation and update | |
277 | # pg_num is required during create and optional during update | |
278 | # pgp_num is optional during create or update | |
279 | # nothing is required during update | |
280 | if hasattr(self, 'update'): | |
281 | if self.serializer_class: | |
282 | actions['PATCH'] = self.serializer_class().metadata() | |
283 | if hasattr(self, 'create'): | |
284 | if self.serializer_class: | |
285 | actions['POST'] = self.serializer_class().metadata() | |
286 | ret['actions'] = actions | |
287 | ||
288 | return ret | |
289 | ||
290 | ||
291 | class RPCViewSet(viewsets.ViewSetMixin, RPCView): | |
292 | pass |