]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/rgw.py
import 15.2.5
[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
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 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):
196 # type: () -> List[str]
197 return self.proxy('GET', 'bucket')
198
199 def get(self, bucket):
200 # type: (str) -> dict
201 result = self.proxy('GET', 'bucket', {'bucket': bucket})
202 bucket_name = RgwBucket.get_s3_bucket_name(result['bucket'],
203 result['tenant'])
204
205 # Append the versioning configuration.
206 versioning = self._get_versioning(result['owner'], bucket_name)
207 result['versioning'] = versioning['Status']
208 result['mfa_delete'] = versioning['MfaDelete']
209
210 # Append the locking configuration.
211 locking = self._get_locking(result['owner'], bucket_name)
212 result.update(locking)
213
214 return self._append_bid(result)
215
216 def create(self, bucket, uid, zonegroup=None, placement_target=None,
217 lock_enabled='false', lock_mode=None,
218 lock_retention_period_days=None,
219 lock_retention_period_years=None):
220 lock_enabled = str_to_bool(lock_enabled)
221 try:
222 rgw_client = RgwClient.instance(uid)
223 result = rgw_client.create_bucket(bucket, zonegroup,
224 placement_target,
225 lock_enabled)
226 if lock_enabled:
227 self._set_locking(uid, bucket, lock_mode,
228 lock_retention_period_days,
229 lock_retention_period_years)
230 return result
231 except RequestException as e: # pragma: no cover - handling is too obvious
232 raise DashboardException(e, http_status_code=500, component='rgw')
233
234 def set(self, bucket, bucket_id, uid, versioning_state=None,
235 mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
236 lock_mode=None, lock_retention_period_days=None,
237 lock_retention_period_years=None):
238 # When linking a non-tenant-user owned bucket to a tenanted user, we
239 # need to prefix bucket name with '/'. e.g. photos -> /photos
240 if '$' in uid and '/' not in bucket:
241 bucket = '/{}'.format(bucket)
242
243 # Link bucket to new user:
244 result = self.proxy('PUT',
245 'bucket', {
246 'bucket': bucket,
247 'bucket-id': bucket_id,
248 'uid': uid
249 },
250 json_response=False)
251
252 uid_tenant = uid[:uid.find('$')] if uid.find('$') >= 0 else None
253 bucket_name = RgwBucket.get_s3_bucket_name(bucket, uid_tenant)
254
255 if versioning_state:
256 self._set_versioning(uid, bucket_name, versioning_state,
257 mfa_delete, mfa_token_serial, mfa_token_pin)
258
259 # Update locking if it is enabled.
260 locking = self._get_locking(uid, bucket_name)
261 if locking['lock_enabled']:
262 self._set_locking(uid, bucket_name, lock_mode,
263 lock_retention_period_days,
264 lock_retention_period_years)
265
266 return self._append_bid(result)
267
268 def delete(self, bucket, purge_objects='true'):
269 return self.proxy('DELETE', 'bucket', {
270 'bucket': bucket,
271 'purge-objects': purge_objects
272 }, json_response=False)
273
274
275 @ApiController('/rgw/user', Scope.RGW)
276 class RgwUser(RgwRESTController):
277 def _append_uid(self, user):
278 """
279 Append the user identifier that looks like [<tenant>$]<user>.
280 See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
281 more information.
282 :param user: The user parameters.
283 :type user: dict
284 :return: The modified user parameters including the 'uid' parameter.
285 :rtype: dict
286 """
287 if isinstance(user, dict):
288 user['uid'] = '{}${}'.format(user['tenant'], user['user_id']) \
289 if user['tenant'] else user['user_id']
290 return user
291
292 @staticmethod
293 def _keys_allowed():
294 permissions = AuthManager.get_user(JwtManager.get_username()).permissions_dict()
295 edit_permissions = [Permission.CREATE, Permission.UPDATE, Permission.DELETE]
296 return Scope.RGW in permissions and Permission.READ in permissions[Scope.RGW] \
297 and len(set(edit_permissions).intersection(set(permissions[Scope.RGW]))) > 0
298
299 def list(self):
300 # type: () -> List[str]
301 users = [] # type: List[str]
302 marker = None
303 while True:
304 params = {} # type: dict
305 if marker:
306 params['marker'] = marker
307 result = self.proxy('GET', 'user?list', params)
308 users.extend(result['keys'])
309 if not result['truncated']:
310 break
311 # Make sure there is a marker.
312 assert result['marker']
313 # Make sure the marker has changed.
314 assert marker != result['marker']
315 marker = result['marker']
316 return users
317
318 def get(self, uid):
319 # type: (str) -> dict
320 result = self.proxy('GET', 'user', {'uid': uid})
321 if not self._keys_allowed():
322 del result['keys']
323 del result['swift_keys']
324 return self._append_uid(result)
325
326 @Endpoint()
327 @ReadPermission
328 def get_emails(self):
329 # type: () -> List[str]
330 emails = []
331 for uid in json.loads(self.list()): # type: ignore
332 user = json.loads(self.get(uid)) # type: ignore
333 if user["email"]:
334 emails.append(user["email"])
335 return emails
336
337 def create(self, uid, display_name, email=None, max_buckets=None,
338 suspended=None, generate_key=None, access_key=None,
339 secret_key=None):
340 params = {'uid': uid}
341 if display_name is not None:
342 params['display-name'] = display_name
343 if email is not None:
344 params['email'] = email
345 if max_buckets is not None:
346 params['max-buckets'] = max_buckets
347 if suspended is not None:
348 params['suspended'] = suspended
349 if generate_key is not None:
350 params['generate-key'] = generate_key
351 if access_key is not None:
352 params['access-key'] = access_key
353 if secret_key is not None:
354 params['secret-key'] = secret_key
355 result = self.proxy('PUT', 'user', params)
356 return self._append_uid(result)
357
358 def set(self, uid, display_name=None, email=None, max_buckets=None,
359 suspended=None):
360 params = {'uid': uid}
361 if display_name is not None:
362 params['display-name'] = display_name
363 if email is not None:
364 params['email'] = email
365 if max_buckets is not None:
366 params['max-buckets'] = max_buckets
367 if suspended is not None:
368 params['suspended'] = suspended
369 result = self.proxy('POST', 'user', params)
370 return self._append_uid(result)
371
372 def delete(self, uid):
373 try:
374 instance = RgwClient.admin_instance()
375 # Ensure the user is not configured to access the RGW Object Gateway.
376 if instance.userid == uid:
377 raise DashboardException(msg='Unable to delete "{}" - this user '
378 'account is required for managing the '
379 'Object Gateway'.format(uid))
380 # Finally redirect request to the RGW proxy.
381 return self.proxy('DELETE', 'user', {'uid': uid}, json_response=False)
382 except (DashboardException, RequestException) as e: # pragma: no cover
383 raise DashboardException(e, component='rgw')
384
385 # pylint: disable=redefined-builtin
386 @RESTController.Resource(method='POST', path='/capability', status=201)
387 def create_cap(self, uid, type, perm):
388 return self.proxy('PUT', 'user?caps', {
389 'uid': uid,
390 'user-caps': '{}={}'.format(type, perm)
391 })
392
393 # pylint: disable=redefined-builtin
394 @RESTController.Resource(method='DELETE', path='/capability', status=204)
395 def delete_cap(self, uid, type, perm):
396 return self.proxy('DELETE', 'user?caps', {
397 'uid': uid,
398 'user-caps': '{}={}'.format(type, perm)
399 })
400
401 @RESTController.Resource(method='POST', path='/key', status=201)
402 def create_key(self, uid, key_type='s3', subuser=None, generate_key='true',
403 access_key=None, secret_key=None):
404 params = {'uid': uid, 'key-type': key_type, 'generate-key': generate_key}
405 if subuser is not None:
406 params['subuser'] = subuser
407 if access_key is not None:
408 params['access-key'] = access_key
409 if secret_key is not None:
410 params['secret-key'] = secret_key
411 return self.proxy('PUT', 'user?key', params)
412
413 @RESTController.Resource(method='DELETE', path='/key', status=204)
414 def delete_key(self, uid, key_type='s3', subuser=None, access_key=None):
415 params = {'uid': uid, 'key-type': key_type}
416 if subuser is not None:
417 params['subuser'] = subuser
418 if access_key is not None:
419 params['access-key'] = access_key
420 return self.proxy('DELETE', 'user?key', params, json_response=False)
421
422 @RESTController.Resource(method='GET', path='/quota')
423 def get_quota(self, uid):
424 return self.proxy('GET', 'user?quota', {'uid': uid})
425
426 @RESTController.Resource(method='PUT', path='/quota')
427 def set_quota(self, uid, quota_type, enabled, max_size_kb, max_objects):
428 return self.proxy('PUT', 'user?quota', {
429 'uid': uid,
430 'quota-type': quota_type,
431 'enabled': enabled,
432 'max-size-kb': max_size_kb,
433 'max-objects': max_objects
434 }, json_response=False)
435
436 @RESTController.Resource(method='POST', path='/subuser', status=201)
437 def create_subuser(self, uid, subuser, access, key_type='s3',
438 generate_secret='true', access_key=None,
439 secret_key=None):
440 return self.proxy('PUT', 'user', {
441 'uid': uid,
442 'subuser': subuser,
443 'key-type': key_type,
444 'access': access,
445 'generate-secret': generate_secret,
446 'access-key': access_key,
447 'secret-key': secret_key
448 })
449
450 @RESTController.Resource(method='DELETE', path='/subuser/{subuser}', status=204)
451 def delete_subuser(self, uid, subuser, purge_keys='true'):
452 """
453 :param purge_keys: Set to False to do not purge the keys.
454 Note, this only works for s3 subusers.
455 """
456 return self.proxy('DELETE', 'user', {
457 'uid': uid,
458 'subuser': subuser,
459 'purge-keys': purge_keys
460 }, json_response=False)