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