]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | from __future__ import absolute_import | |
3 | ||
9f95a23c | 4 | import logging |
11fdf7f2 TL |
5 | import json |
6 | ||
7 | import cherrypy | |
8 | ||
9 | from . import ApiController, BaseController, RESTController, Endpoint, \ | |
10 | ReadPermission | |
11fdf7f2 TL |
11 | from ..exceptions import DashboardException |
12 | from ..rest_client import RequestException | |
9f95a23c TL |
13 | from ..security import Scope, Permission |
14 | from ..services.auth import AuthManager, JwtManager | |
11fdf7f2 TL |
15 | from ..services.ceph_service import CephService |
16 | from ..services.rgw_client import RgwClient | |
9f95a23c TL |
17 | from ..tools import json_str_to_object, str_to_bool |
18 | ||
19 | try: | |
20 | from typing import List | |
21 | except ImportError: | |
22 | pass # Just for type checking | |
23 | ||
24 | logger = logging.getLogger('controllers.rgw') | |
11fdf7f2 TL |
25 | |
26 | ||
27 | @ApiController('/rgw', Scope.RGW) | |
28 | class Rgw(BaseController): | |
11fdf7f2 TL |
29 | @Endpoint() |
30 | @ReadPermission | |
31 | def status(self): | |
32 | status = {'available': False, 'message': None} | |
33 | try: | |
34 | instance = RgwClient.admin_instance() | |
35 | # Check if the service is online. | |
36 | if not instance.is_service_online(): | |
37 | msg = 'Failed to connect to the Object Gateway\'s Admin Ops API.' | |
38 | raise RequestException(msg) | |
39 | # Ensure the API user ID is known by the RGW. | |
40 | if not instance.user_exists(): | |
41 | msg = 'The user "{}" is unknown to the Object Gateway.'.format( | |
42 | instance.userid) | |
43 | raise RequestException(msg) | |
44 | # Ensure the system flag is set for the API user ID. | |
45 | if not instance.is_system_user(): | |
46 | msg = 'The system flag is not set for user "{}".'.format( | |
47 | instance.userid) | |
48 | raise RequestException(msg) | |
49 | status['available'] = True | |
50 | except (RequestException, LookupError) as ex: | |
9f95a23c | 51 | status['message'] = str(ex) # type: ignore |
11fdf7f2 TL |
52 | return status |
53 | ||
54 | ||
55 | @ApiController('/rgw/daemon', Scope.RGW) | |
56 | class RgwDaemon(RESTController): | |
11fdf7f2 | 57 | def list(self): |
9f95a23c | 58 | # type: () -> List[dict] |
11fdf7f2 TL |
59 | daemons = [] |
60 | for hostname, server in CephService.get_service_map('rgw').items(): | |
61 | for service in server['services']: | |
62 | metadata = service['metadata'] | |
63 | ||
64 | # extract per-daemon service data and health | |
65 | daemon = { | |
66 | 'id': service['id'], | |
67 | 'version': metadata['ceph_version'], | |
68 | 'server_hostname': hostname | |
69 | } | |
70 | ||
71 | daemons.append(daemon) | |
72 | ||
73 | return sorted(daemons, key=lambda k: k['id']) | |
74 | ||
75 | def get(self, svc_id): | |
9f95a23c | 76 | # type: (str) -> dict |
11fdf7f2 TL |
77 | daemon = { |
78 | 'rgw_metadata': [], | |
79 | 'rgw_id': svc_id, | |
80 | 'rgw_status': [] | |
81 | } | |
82 | service = CephService.get_service('rgw', svc_id) | |
83 | if not service: | |
84 | raise cherrypy.NotFound('Service rgw {} is not available'.format(svc_id)) | |
85 | ||
86 | metadata = service['metadata'] | |
87 | status = service['status'] | |
88 | if 'json' in status: | |
89 | try: | |
90 | status = json.loads(status['json']) | |
91 | except ValueError: | |
92 | logger.warning('%s had invalid status json', service['id']) | |
93 | status = {} | |
94 | else: | |
95 | logger.warning('%s has no key "json" in status', service['id']) | |
96 | ||
97 | daemon['rgw_metadata'] = metadata | |
98 | daemon['rgw_status'] = status | |
99 | return daemon | |
100 | ||
101 | ||
102 | class RgwRESTController(RESTController): | |
11fdf7f2 TL |
103 | def proxy(self, method, path, params=None, json_response=True): |
104 | try: | |
105 | instance = RgwClient.admin_instance() | |
106 | result = instance.proxy(method, path, params, None) | |
9f95a23c TL |
107 | if json_response: |
108 | result = json_str_to_object(result) | |
11fdf7f2 TL |
109 | return result |
110 | except (DashboardException, RequestException) as e: | |
111 | raise DashboardException(e, http_status_code=500, component='rgw') | |
112 | ||
113 | ||
9f95a23c TL |
114 | @ApiController('/rgw/site', Scope.RGW) |
115 | class RgwSite(RgwRESTController): | |
116 | def list(self, query=None): | |
117 | if query == 'placement-targets': | |
118 | instance = RgwClient.admin_instance() | |
119 | result = instance.get_placement_targets() | |
120 | else: | |
121 | # @TODO: (it'll be required for multisite workflows): | |
122 | # by default, retrieve cluster realms/zonegroups map. | |
123 | raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented') | |
124 | ||
125 | return result | |
126 | ||
127 | ||
11fdf7f2 TL |
128 | @ApiController('/rgw/bucket', Scope.RGW) |
129 | class RgwBucket(RgwRESTController): | |
11fdf7f2 TL |
130 | def _append_bid(self, bucket): |
131 | """ | |
132 | Append the bucket identifier that looks like [<tenant>/]<bucket>. | |
133 | See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for | |
134 | more information. | |
135 | :param bucket: The bucket parameters. | |
136 | :type bucket: dict | |
137 | :return: The modified bucket parameters including the 'bid' parameter. | |
138 | :rtype: dict | |
139 | """ | |
140 | if isinstance(bucket, dict): | |
141 | bucket['bid'] = '{}/{}'.format(bucket['tenant'], bucket['bucket']) \ | |
142 | if bucket['tenant'] else bucket['bucket'] | |
143 | return bucket | |
144 | ||
9f95a23c TL |
145 | def _get_versioning(self, owner, bucket_name): |
146 | rgw_client = RgwClient.instance(owner) | |
147 | return rgw_client.get_bucket_versioning(bucket_name) | |
148 | ||
149 | def _set_versioning(self, owner, bucket_name, versioning_state, mfa_delete, | |
150 | mfa_token_serial, mfa_token_pin): | |
151 | bucket_versioning = self._get_versioning(owner, bucket_name) | |
152 | if versioning_state != bucket_versioning['Status']\ | |
153 | or (mfa_delete and mfa_delete != bucket_versioning['MfaDelete']): | |
154 | rgw_client = RgwClient.instance(owner) | |
155 | rgw_client.set_bucket_versioning(bucket_name, versioning_state, mfa_delete, | |
156 | mfa_token_serial, mfa_token_pin) | |
157 | ||
158 | def _get_locking(self, owner, bucket_name): | |
159 | rgw_client = RgwClient.instance(owner) | |
160 | return rgw_client.get_bucket_locking(bucket_name) | |
161 | ||
162 | def _set_locking(self, owner, bucket_name, mode, | |
163 | retention_period_days, retention_period_years): | |
164 | rgw_client = RgwClient.instance(owner) | |
165 | return rgw_client.set_bucket_locking(bucket_name, mode, | |
166 | int(retention_period_days), | |
167 | int(retention_period_years)) | |
168 | ||
eafe8130 | 169 | @staticmethod |
9f95a23c TL |
170 | def strip_tenant_from_bucket_name(bucket_name): |
171 | # type (str) -> str | |
eafe8130 | 172 | """ |
9f95a23c | 173 | >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name') |
eafe8130 | 174 | 'bucket-name' |
9f95a23c | 175 | >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name') |
eafe8130 TL |
176 | 'bucket-name' |
177 | """ | |
9f95a23c | 178 | return bucket_name[bucket_name.find('/') + 1:] |
eafe8130 | 179 | |
9f95a23c TL |
180 | @staticmethod |
181 | def get_s3_bucket_name(bucket_name, tenant=None): | |
182 | # type (str, str) -> str | |
183 | """ | |
184 | >>> RgwBucket.get_s3_bucket_name('bucket-name', 'tenant') | |
185 | 'tenant:bucket-name' | |
186 | >>> RgwBucket.get_s3_bucket_name('tenant/bucket-name', 'tenant') | |
187 | 'tenant:bucket-name' | |
188 | >>> RgwBucket.get_s3_bucket_name('bucket-name') | |
189 | 'bucket-name' | |
190 | """ | |
191 | bucket_name = RgwBucket.strip_tenant_from_bucket_name(bucket_name) | |
192 | if tenant: | |
193 | bucket_name = '{}:{}'.format(tenant, bucket_name) | |
eafe8130 TL |
194 | return bucket_name |
195 | ||
11fdf7f2 | 196 | def list(self): |
9f95a23c | 197 | # type: () -> List[str] |
11fdf7f2 TL |
198 | return self.proxy('GET', 'bucket') |
199 | ||
200 | def get(self, bucket): | |
9f95a23c | 201 | # type: (str) -> dict |
11fdf7f2 | 202 | result = self.proxy('GET', 'bucket', {'bucket': bucket}) |
9f95a23c TL |
203 | bucket_name = RgwBucket.get_s3_bucket_name(result['bucket'], |
204 | result['tenant']) | |
205 | ||
206 | # Append the versioning configuration. | |
207 | versioning = self._get_versioning(result['owner'], bucket_name) | |
208 | result['versioning'] = versioning['Status'] | |
209 | result['mfa_delete'] = versioning['MfaDelete'] | |
210 | ||
211 | # Append the locking configuration. | |
212 | locking = self._get_locking(result['owner'], bucket_name) | |
213 | result.update(locking) | |
214 | ||
11fdf7f2 TL |
215 | return self._append_bid(result) |
216 | ||
9f95a23c TL |
217 | def create(self, bucket, uid, zonegroup=None, placement_target=None, |
218 | lock_enabled='false', lock_mode=None, | |
219 | lock_retention_period_days=None, | |
220 | lock_retention_period_years=None): | |
221 | lock_enabled = str_to_bool(lock_enabled) | |
11fdf7f2 TL |
222 | try: |
223 | rgw_client = RgwClient.instance(uid) | |
9f95a23c TL |
224 | result = rgw_client.create_bucket(bucket, zonegroup, |
225 | placement_target, | |
226 | lock_enabled) | |
227 | if lock_enabled: | |
228 | self._set_locking(uid, bucket, lock_mode, | |
229 | lock_retention_period_days, | |
230 | lock_retention_period_years) | |
231 | return result | |
11fdf7f2 TL |
232 | except RequestException as e: |
233 | raise DashboardException(e, http_status_code=500, component='rgw') | |
234 | ||
9f95a23c TL |
235 | def set(self, bucket, bucket_id, uid, versioning_state=None, |
236 | mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None, | |
237 | lock_mode=None, lock_retention_period_days=None, | |
238 | lock_retention_period_years=None): | |
239 | # When linking a non-tenant-user owned bucket to a tenanted user, we | |
240 | # need to prefix bucket name with '/'. e.g. photos -> /photos | |
241 | if '$' in uid and '/' not in bucket: | |
242 | bucket = '/{}'.format(bucket) | |
243 | ||
244 | # Link bucket to new user: | |
245 | result = self.proxy('PUT', | |
246 | 'bucket', { | |
247 | 'bucket': bucket, | |
248 | 'bucket-id': bucket_id, | |
249 | 'uid': uid | |
250 | }, | |
251 | json_response=False) | |
252 | ||
253 | uid_tenant = uid[:uid.find('$')] if uid.find('$') >= 0 else None | |
254 | bucket_name = RgwBucket.get_s3_bucket_name(bucket, uid_tenant) | |
255 | ||
256 | if versioning_state: | |
257 | self._set_versioning(uid, bucket_name, versioning_state, | |
258 | mfa_delete, mfa_token_serial, mfa_token_pin) | |
259 | ||
260 | # Update locking if it is enabled. | |
261 | locking = self._get_locking(uid, bucket_name) | |
262 | if locking['lock_enabled']: | |
263 | self._set_locking(uid, bucket_name, lock_mode, | |
264 | lock_retention_period_days, | |
265 | lock_retention_period_years) | |
266 | ||
11fdf7f2 TL |
267 | return self._append_bid(result) |
268 | ||
269 | def delete(self, bucket, purge_objects='true'): | |
270 | return self.proxy('DELETE', 'bucket', { | |
271 | 'bucket': bucket, | |
272 | 'purge-objects': purge_objects | |
273 | }, json_response=False) | |
274 | ||
275 | ||
276 | @ApiController('/rgw/user', Scope.RGW) | |
277 | class RgwUser(RgwRESTController): | |
11fdf7f2 TL |
278 | def _append_uid(self, user): |
279 | """ | |
280 | Append the user identifier that looks like [<tenant>$]<user>. | |
281 | See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for | |
282 | more information. | |
283 | :param user: The user parameters. | |
284 | :type user: dict | |
285 | :return: The modified user parameters including the 'uid' parameter. | |
286 | :rtype: dict | |
287 | """ | |
288 | if isinstance(user, dict): | |
289 | user['uid'] = '{}${}'.format(user['tenant'], user['user_id']) \ | |
290 | if user['tenant'] else user['user_id'] | |
291 | return user | |
292 | ||
9f95a23c TL |
293 | @staticmethod |
294 | def _keys_allowed(): | |
295 | permissions = AuthManager.get_user(JwtManager.get_username()).permissions_dict() | |
296 | edit_permissions = [Permission.CREATE, Permission.UPDATE, Permission.DELETE] | |
297 | return Scope.RGW in permissions and Permission.READ in permissions[Scope.RGW] \ | |
298 | and len(set(edit_permissions).intersection(set(permissions[Scope.RGW]))) > 0 | |
299 | ||
11fdf7f2 | 300 | def list(self): |
9f95a23c TL |
301 | # type: () -> List[str] |
302 | users = [] # type: List[str] | |
11fdf7f2 TL |
303 | marker = None |
304 | while True: | |
9f95a23c | 305 | params = {} # type: dict |
11fdf7f2 TL |
306 | if marker: |
307 | params['marker'] = marker | |
308 | result = self.proxy('GET', 'user?list', params) | |
309 | users.extend(result['keys']) | |
310 | if not result['truncated']: | |
311 | break | |
312 | # Make sure there is a marker. | |
313 | assert result['marker'] | |
314 | # Make sure the marker has changed. | |
315 | assert marker != result['marker'] | |
316 | marker = result['marker'] | |
317 | return users | |
318 | ||
319 | def get(self, uid): | |
9f95a23c | 320 | # type: (str) -> dict |
11fdf7f2 | 321 | result = self.proxy('GET', 'user', {'uid': uid}) |
9f95a23c TL |
322 | if not self._keys_allowed(): |
323 | del result['keys'] | |
324 | del result['swift_keys'] | |
11fdf7f2 TL |
325 | return self._append_uid(result) |
326 | ||
327 | @Endpoint() | |
328 | @ReadPermission | |
329 | def get_emails(self): | |
9f95a23c | 330 | # type: () -> List[str] |
11fdf7f2 | 331 | emails = [] |
9f95a23c TL |
332 | for uid in json.loads(self.list()): # type: ignore |
333 | user = json.loads(self.get(uid)) # type: ignore | |
11fdf7f2 TL |
334 | if user["email"]: |
335 | emails.append(user["email"]) | |
336 | return emails | |
337 | ||
338 | def create(self, uid, display_name, email=None, max_buckets=None, | |
339 | suspended=None, generate_key=None, access_key=None, | |
340 | secret_key=None): | |
341 | params = {'uid': uid} | |
342 | if display_name is not None: | |
343 | params['display-name'] = display_name | |
344 | if email is not None: | |
345 | params['email'] = email | |
346 | if max_buckets is not None: | |
347 | params['max-buckets'] = max_buckets | |
348 | if suspended is not None: | |
349 | params['suspended'] = suspended | |
350 | if generate_key is not None: | |
351 | params['generate-key'] = generate_key | |
352 | if access_key is not None: | |
353 | params['access-key'] = access_key | |
354 | if secret_key is not None: | |
355 | params['secret-key'] = secret_key | |
356 | result = self.proxy('PUT', 'user', params) | |
357 | return self._append_uid(result) | |
358 | ||
359 | def set(self, uid, display_name=None, email=None, max_buckets=None, | |
360 | suspended=None): | |
361 | params = {'uid': uid} | |
362 | if display_name is not None: | |
363 | params['display-name'] = display_name | |
364 | if email is not None: | |
365 | params['email'] = email | |
366 | if max_buckets is not None: | |
367 | params['max-buckets'] = max_buckets | |
368 | if suspended is not None: | |
369 | params['suspended'] = suspended | |
370 | result = self.proxy('POST', 'user', params) | |
371 | return self._append_uid(result) | |
372 | ||
373 | def delete(self, uid): | |
374 | try: | |
375 | instance = RgwClient.admin_instance() | |
376 | # Ensure the user is not configured to access the RGW Object Gateway. | |
377 | if instance.userid == uid: | |
378 | raise DashboardException(msg='Unable to delete "{}" - this user ' | |
379 | 'account is required for managing the ' | |
380 | 'Object Gateway'.format(uid)) | |
381 | # Finally redirect request to the RGW proxy. | |
382 | return self.proxy('DELETE', 'user', {'uid': uid}, json_response=False) | |
383 | except (DashboardException, RequestException) as e: | |
384 | raise DashboardException(e, component='rgw') | |
385 | ||
386 | # pylint: disable=redefined-builtin | |
387 | @RESTController.Resource(method='POST', path='/capability', status=201) | |
388 | def create_cap(self, uid, type, perm): | |
389 | return self.proxy('PUT', 'user?caps', { | |
390 | 'uid': uid, | |
391 | 'user-caps': '{}={}'.format(type, perm) | |
392 | }) | |
393 | ||
394 | # pylint: disable=redefined-builtin | |
395 | @RESTController.Resource(method='DELETE', path='/capability', status=204) | |
396 | def delete_cap(self, uid, type, perm): | |
397 | return self.proxy('DELETE', 'user?caps', { | |
398 | 'uid': uid, | |
399 | 'user-caps': '{}={}'.format(type, perm) | |
400 | }) | |
401 | ||
402 | @RESTController.Resource(method='POST', path='/key', status=201) | |
403 | def create_key(self, uid, key_type='s3', subuser=None, generate_key='true', | |
404 | access_key=None, secret_key=None): | |
405 | params = {'uid': uid, 'key-type': key_type, 'generate-key': generate_key} | |
406 | if subuser is not None: | |
407 | params['subuser'] = subuser | |
408 | if access_key is not None: | |
409 | params['access-key'] = access_key | |
410 | if secret_key is not None: | |
411 | params['secret-key'] = secret_key | |
412 | return self.proxy('PUT', 'user?key', params) | |
413 | ||
414 | @RESTController.Resource(method='DELETE', path='/key', status=204) | |
415 | def delete_key(self, uid, key_type='s3', subuser=None, access_key=None): | |
416 | params = {'uid': uid, 'key-type': key_type} | |
417 | if subuser is not None: | |
418 | params['subuser'] = subuser | |
419 | if access_key is not None: | |
420 | params['access-key'] = access_key | |
421 | return self.proxy('DELETE', 'user?key', params, json_response=False) | |
422 | ||
423 | @RESTController.Resource(method='GET', path='/quota') | |
424 | def get_quota(self, uid): | |
425 | return self.proxy('GET', 'user?quota', {'uid': uid}) | |
426 | ||
427 | @RESTController.Resource(method='PUT', path='/quota') | |
428 | def set_quota(self, uid, quota_type, enabled, max_size_kb, max_objects): | |
429 | return self.proxy('PUT', 'user?quota', { | |
430 | 'uid': uid, | |
431 | 'quota-type': quota_type, | |
432 | 'enabled': enabled, | |
433 | 'max-size-kb': max_size_kb, | |
434 | 'max-objects': max_objects | |
435 | }, json_response=False) | |
436 | ||
437 | @RESTController.Resource(method='POST', path='/subuser', status=201) | |
438 | def create_subuser(self, uid, subuser, access, key_type='s3', | |
439 | generate_secret='true', access_key=None, | |
440 | secret_key=None): | |
441 | return self.proxy('PUT', 'user', { | |
442 | 'uid': uid, | |
443 | 'subuser': subuser, | |
444 | 'key-type': key_type, | |
445 | 'access': access, | |
446 | 'generate-secret': generate_secret, | |
447 | 'access-key': access_key, | |
448 | 'secret-key': secret_key | |
449 | }) | |
450 | ||
451 | @RESTController.Resource(method='DELETE', path='/subuser/{subuser}', status=204) | |
452 | def delete_subuser(self, uid, subuser, purge_keys='true'): | |
453 | """ | |
454 | :param purge_keys: Set to False to do not purge the keys. | |
455 | Note, this only works for s3 subusers. | |
456 | """ | |
457 | return self.proxy('DELETE', 'user', { | |
458 | 'uid': uid, | |
459 | 'subuser': subuser, | |
460 | 'purge-keys': purge_keys | |
461 | }, json_response=False) |