]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/rgw.py
import 15.2.0 Octopus source
[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
9 from . import ApiController, BaseController, RESTController, Endpoint, \
10 ReadPermission
11 from ..exceptions import DashboardException
12 from ..rest_client import RequestException
13 from ..security import Scope, Permission
14 from ..services.auth import AuthManager, JwtManager
15 from ..services.ceph_service import CephService
16 from ..services.rgw_client import RgwClient
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')
25
26
27 @ApiController('/rgw', Scope.RGW)
28 class Rgw(BaseController):
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:
51 status['message'] = str(ex) # type: ignore
52 return status
53
54
55 @ApiController('/rgw/daemon', Scope.RGW)
56 class RgwDaemon(RESTController):
57 def list(self):
58 # type: () -> List[dict]
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):
76 # type: (str) -> dict
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):
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)
107 if json_response:
108 result = json_str_to_object(result)
109 return result
110 except (DashboardException, RequestException) as e:
111 raise DashboardException(e, http_status_code=500, component='rgw')
112
113
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
128 @ApiController('/rgw/bucket', Scope.RGW)
129 class RgwBucket(RgwRESTController):
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
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
169 @staticmethod
170 def strip_tenant_from_bucket_name(bucket_name):
171 # type (str) -> str
172 """
173 >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name')
174 'bucket-name'
175 >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name')
176 'bucket-name'
177 """
178 return bucket_name[bucket_name.find('/') + 1:]
179
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)
194 return bucket_name
195
196 def list(self):
197 # type: () -> List[str]
198 return self.proxy('GET', 'bucket')
199
200 def get(self, bucket):
201 # type: (str) -> dict
202 result = self.proxy('GET', 'bucket', {'bucket': bucket})
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
215 return self._append_bid(result)
216
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)
222 try:
223 rgw_client = RgwClient.instance(uid)
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
232 except RequestException as e:
233 raise DashboardException(e, http_status_code=500, component='rgw')
234
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
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):
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
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
300 def list(self):
301 # type: () -> List[str]
302 users = [] # type: List[str]
303 marker = None
304 while True:
305 params = {} # type: dict
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):
320 # type: (str) -> dict
321 result = self.proxy('GET', 'user', {'uid': uid})
322 if not self._keys_allowed():
323 del result['keys']
324 del result['swift_keys']
325 return self._append_uid(result)
326
327 @Endpoint()
328 @ReadPermission
329 def get_emails(self):
330 # type: () -> List[str]
331 emails = []
332 for uid in json.loads(self.list()): # type: ignore
333 user = json.loads(self.get(uid)) # type: ignore
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)