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