1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
9 from ..exceptions
import DashboardException
10 from ..rest_client
import RequestException
11 from ..security
import Permission
, Scope
12 from ..services
.auth
import AuthManager
, JwtManager
13 from ..services
.ceph_service
import CephService
14 from ..services
.rgw_client
import NoRgwDaemonsException
, RgwClient
15 from ..tools
import json_str_to_object
, str_to_bool
16 from . import ApiController
, BaseController
, ControllerDoc
, Endpoint
, \
17 EndpointDoc
, ReadPermission
, RESTController
, allow_empty_body
20 from typing
import Any
, List
, Optional
21 except ImportError: # pragma: no cover
22 pass # Just for type checking
24 logger
= logging
.getLogger("controllers.rgw")
27 "available": (bool, "Is RGW available?"),
28 "message": (str, "Descriptions")
32 "id": (str, "Daemon ID"),
33 "version": (str, "Ceph Version"),
34 "server_hostname": (str, ""),
35 "zonegroup_name": (str, "Zone Group"),
36 "zone_name": (str, "Zone")
40 "list_of_users": ([str], "list of rgw users")
44 @ApiController('/rgw', Scope
.RGW
)
45 @ControllerDoc("RGW Management API", "Rgw")
46 class Rgw(BaseController
):
49 @EndpointDoc("Display RGW Status",
50 responses
={200: RGW_SCHEMA
})
51 def status(self
) -> dict:
52 status
= {'available': False, 'message': None}
54 instance
= RgwClient
.admin_instance()
55 # Check if the service is online.
57 is_online
= instance
.is_service_online()
58 except RequestException
as e
:
59 # Drop this instance because the RGW client seems not to
60 # exist anymore (maybe removed via orchestrator). Removing
61 # the instance from the cache will result in the correct
62 # error message next time when the backend tries to
63 # establish a new connection (-> 'No RGW found' instead
64 # of 'RGW REST API failed request ...').
65 # Note, this only applies to auto-detected RGW clients.
66 RgwClient
.drop_instance(instance
)
69 msg
= 'Failed to connect to the Object Gateway\'s Admin Ops API.'
70 raise RequestException(msg
)
71 # Ensure the system flag is set for the API user ID.
72 if not instance
.is_system_user(): # pragma: no cover - no complexity there
73 msg
= 'The system flag is not set for user "{}".'.format(
75 raise RequestException(msg
)
76 status
['available'] = True
77 except (DashboardException
, RequestException
, NoRgwDaemonsException
) as ex
:
78 status
['message'] = str(ex
) # type: ignore
82 @ApiController('/rgw/daemon', Scope
.RGW
)
83 @ControllerDoc("RGW Daemon Management API", "RgwDaemon")
84 class RgwDaemon(RESTController
):
85 @EndpointDoc("Display RGW Daemons",
86 responses
={200: [RGW_DAEMON_SCHEMA
]})
87 def list(self
) -> List
[dict]:
88 daemons
: List
[dict] = []
90 instance
= RgwClient
.admin_instance()
91 except NoRgwDaemonsException
:
94 for hostname
, server
in CephService
.get_service_map('rgw').items():
95 for service
in server
['services']:
96 metadata
= service
['metadata']
98 # extract per-daemon service data and health
100 'id': metadata
['id'],
101 'version': metadata
['ceph_version'],
102 'server_hostname': hostname
,
103 'zonegroup_name': metadata
['zonegroup_name'],
104 'zone_name': metadata
['zone_name'],
105 'default': instance
.daemon
.name
== metadata
['id']
108 daemons
.append(daemon
)
110 return sorted(daemons
, key
=lambda k
: k
['id'])
112 def get(self
, svc_id
):
113 # type: (str) -> dict
119 service
= CephService
.get_service('rgw', svc_id
)
121 raise cherrypy
.NotFound('Service rgw {} is not available'.format(svc_id
))
123 metadata
= service
['metadata']
124 status
= service
['status']
127 status
= json
.loads(status
['json'])
129 logger
.warning('%s had invalid status json', service
['id'])
132 logger
.warning('%s has no key "json" in status', service
['id'])
134 daemon
['rgw_metadata'] = metadata
135 daemon
['rgw_status'] = status
139 class RgwRESTController(RESTController
):
140 def proxy(self
, daemon_name
, method
, path
, params
=None, json_response
=True):
142 instance
= RgwClient
.admin_instance(daemon_name
=daemon_name
)
143 result
= instance
.proxy(method
, path
, params
, None)
145 result
= json_str_to_object(result
)
147 except (DashboardException
, RequestException
) as e
:
148 http_status_code
= e
.status
if isinstance(e
, DashboardException
) else 500
149 raise DashboardException(e
, http_status_code
=http_status_code
, component
='rgw')
152 @ApiController('/rgw/site', Scope
.RGW
)
153 @ControllerDoc("RGW Site Management API", "RgwSite")
154 class RgwSite(RgwRESTController
):
155 def list(self
, query
=None, daemon_name
=None):
156 if query
== 'placement-targets':
157 return RgwClient
.admin_instance(daemon_name
=daemon_name
).get_placement_targets()
158 if query
== 'realms':
159 return RgwClient
.admin_instance(daemon_name
=daemon_name
).get_realms()
161 # @TODO: for multisite: by default, retrieve cluster topology/map.
162 raise DashboardException(http_status_code
=501, component
='rgw', msg
='Not Implemented')
165 @ApiController('/rgw/bucket', Scope
.RGW
)
166 @ControllerDoc("RGW Bucket Management API", "RgwBucket")
167 class RgwBucket(RgwRESTController
):
168 def _append_bid(self
, bucket
):
170 Append the bucket identifier that looks like [<tenant>/]<bucket>.
171 See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for
173 :param bucket: The bucket parameters.
175 :return: The modified bucket parameters including the 'bid' parameter.
178 if isinstance(bucket
, dict):
179 bucket
['bid'] = '{}/{}'.format(bucket
['tenant'], bucket
['bucket']) \
180 if bucket
['tenant'] else bucket
['bucket']
183 def _get_versioning(self
, owner
, daemon_name
, bucket_name
):
184 rgw_client
= RgwClient
.instance(owner
, daemon_name
)
185 return rgw_client
.get_bucket_versioning(bucket_name
)
187 def _set_versioning(self
, owner
, daemon_name
, bucket_name
, versioning_state
, mfa_delete
,
188 mfa_token_serial
, mfa_token_pin
):
189 bucket_versioning
= self
._get
_versioning
(owner
, daemon_name
, bucket_name
)
190 if versioning_state
!= bucket_versioning
['Status']\
191 or (mfa_delete
and mfa_delete
!= bucket_versioning
['MfaDelete']):
192 rgw_client
= RgwClient
.instance(owner
, daemon_name
)
193 rgw_client
.set_bucket_versioning(bucket_name
, versioning_state
, mfa_delete
,
194 mfa_token_serial
, mfa_token_pin
)
196 def _get_locking(self
, owner
, daemon_name
, bucket_name
):
197 rgw_client
= RgwClient
.instance(owner
, daemon_name
)
198 return rgw_client
.get_bucket_locking(bucket_name
)
200 def _set_locking(self
, owner
, daemon_name
, bucket_name
, mode
,
201 retention_period_days
, retention_period_years
):
202 rgw_client
= RgwClient
.instance(owner
, daemon_name
)
203 return rgw_client
.set_bucket_locking(bucket_name
, mode
,
204 retention_period_days
,
205 retention_period_years
)
208 def strip_tenant_from_bucket_name(bucket_name
):
211 >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name')
213 >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name')
216 return bucket_name
[bucket_name
.find('/') + 1:]
219 def get_s3_bucket_name(bucket_name
, tenant
=None):
220 # type (str, str) -> str
222 >>> RgwBucket.get_s3_bucket_name('bucket-name', 'tenant')
224 >>> RgwBucket.get_s3_bucket_name('tenant/bucket-name', 'tenant')
226 >>> RgwBucket.get_s3_bucket_name('bucket-name')
229 bucket_name
= RgwBucket
.strip_tenant_from_bucket_name(bucket_name
)
231 bucket_name
= '{}:{}'.format(tenant
, bucket_name
)
234 def list(self
, stats
=False, daemon_name
=None):
235 # type: (bool, Optional[str]) -> List[Any]
236 query_params
= '?stats' if stats
else ''
237 result
= self
.proxy(daemon_name
, 'GET', 'bucket{}'.format(query_params
))
240 result
= [self
._append
_bid
(bucket
) for bucket
in result
]
244 def get(self
, bucket
, daemon_name
=None):
245 # type: (str, Optional[str]) -> dict
246 result
= self
.proxy(daemon_name
, 'GET', 'bucket', {'bucket': bucket
})
247 bucket_name
= RgwBucket
.get_s3_bucket_name(result
['bucket'],
250 # Append the versioning configuration.
251 versioning
= self
._get
_versioning
(result
['owner'], daemon_name
, bucket_name
)
252 result
['versioning'] = versioning
['Status']
253 result
['mfa_delete'] = versioning
['MfaDelete']
255 # Append the locking configuration.
256 locking
= self
._get
_locking
(result
['owner'], daemon_name
, bucket_name
)
257 result
.update(locking
)
259 return self
._append
_bid
(result
)
262 def create(self
, bucket
, uid
, zonegroup
=None, placement_target
=None,
263 lock_enabled
='false', lock_mode
=None,
264 lock_retention_period_days
=None,
265 lock_retention_period_years
=None, daemon_name
=None):
266 lock_enabled
= str_to_bool(lock_enabled
)
268 rgw_client
= RgwClient
.instance(uid
, daemon_name
)
269 result
= rgw_client
.create_bucket(bucket
, zonegroup
,
273 self
._set
_locking
(uid
, daemon_name
, bucket
, lock_mode
,
274 lock_retention_period_days
,
275 lock_retention_period_years
)
277 except RequestException
as e
: # pragma: no cover - handling is too obvious
278 raise DashboardException(e
, http_status_code
=500, component
='rgw')
281 def set(self
, bucket
, bucket_id
, uid
, versioning_state
=None,
282 mfa_delete
=None, mfa_token_serial
=None, mfa_token_pin
=None,
283 lock_mode
=None, lock_retention_period_days
=None,
284 lock_retention_period_years
=None, daemon_name
=None):
285 # When linking a non-tenant-user owned bucket to a tenanted user, we
286 # need to prefix bucket name with '/'. e.g. photos -> /photos
287 if '$' in uid
and '/' not in bucket
:
288 bucket
= '/{}'.format(bucket
)
290 # Link bucket to new user:
291 result
= self
.proxy(daemon_name
,
295 'bucket-id': bucket_id
,
300 uid_tenant
= uid
[:uid
.find('$')] if uid
.find('$') >= 0 else None
301 bucket_name
= RgwBucket
.get_s3_bucket_name(bucket
, uid_tenant
)
303 locking
= self
._get
_locking
(uid
, daemon_name
, bucket_name
)
305 if versioning_state
== 'Suspended' and locking
['lock_enabled']:
306 raise DashboardException(msg
='Bucket versioning cannot be disabled/suspended '
307 'on buckets with object lock enabled ',
308 http_status_code
=409, component
='rgw')
309 self
._set
_versioning
(uid
, daemon_name
, bucket_name
, versioning_state
,
310 mfa_delete
, mfa_token_serial
, mfa_token_pin
)
312 # Update locking if it is enabled.
313 if locking
['lock_enabled']:
314 self
._set
_locking
(uid
, daemon_name
, bucket_name
, lock_mode
,
315 lock_retention_period_days
,
316 lock_retention_period_years
)
318 return self
._append
_bid
(result
)
320 def delete(self
, bucket
, purge_objects
='true', daemon_name
=None):
321 return self
.proxy(daemon_name
, 'DELETE', 'bucket', {
323 'purge-objects': purge_objects
324 }, json_response
=False)
327 @ApiController('/rgw/user', Scope
.RGW
)
328 @ControllerDoc("RGW User Management API", "RgwUser")
329 class RgwUser(RgwRESTController
):
330 def _append_uid(self
, user
):
332 Append the user identifier that looks like [<tenant>$]<user>.
333 See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
335 :param user: The user parameters.
337 :return: The modified user parameters including the 'uid' parameter.
340 if isinstance(user
, dict):
341 user
['uid'] = '{}${}'.format(user
['tenant'], user
['user_id']) \
342 if user
['tenant'] else user
['user_id']
347 permissions
= AuthManager
.get_user(JwtManager
.get_username()).permissions_dict()
348 edit_permissions
= [Permission
.CREATE
, Permission
.UPDATE
, Permission
.DELETE
]
349 return Scope
.RGW
in permissions
and Permission
.READ
in permissions
[Scope
.RGW
] \
350 and len(set(edit_permissions
).intersection(set(permissions
[Scope
.RGW
]))) > 0
352 @EndpointDoc("Display RGW Users",
353 responses
={200: RGW_USER_SCHEMA
})
354 def list(self
, daemon_name
=None):
355 # type: (Optional[str]) -> List[str]
356 users
= [] # type: List[str]
359 params
= {} # type: dict
361 params
['marker'] = marker
362 result
= self
.proxy(daemon_name
, 'GET', 'user?list', params
)
363 users
.extend(result
['keys'])
364 if not result
['truncated']:
366 # Make sure there is a marker.
367 assert result
['marker']
368 # Make sure the marker has changed.
369 assert marker
!= result
['marker']
370 marker
= result
['marker']
373 def get(self
, uid
, daemon_name
=None, stats
=True) -> dict:
374 query_params
= '?stats' if stats
else ''
375 result
= self
.proxy(daemon_name
, 'GET', 'user{}'.format(query_params
),
376 {'uid': uid
, 'stats': stats
})
377 if not self
._keys
_allowed
():
379 del result
['swift_keys']
380 return self
._append
_uid
(result
)
384 def get_emails(self
, daemon_name
=None):
385 # type: (Optional[str]) -> List[str]
387 for uid
in json
.loads(self
.list(daemon_name
)): # type: ignore
388 user
= json
.loads(self
.get(uid
, daemon_name
)) # type: ignore
390 emails
.append(user
["email"])
394 def create(self
, uid
, display_name
, email
=None, max_buckets
=None,
395 suspended
=None, generate_key
=None, access_key
=None,
396 secret_key
=None, daemon_name
=None):
397 params
= {'uid': uid
}
398 if display_name
is not None:
399 params
['display-name'] = display_name
400 if email
is not None:
401 params
['email'] = email
402 if max_buckets
is not None:
403 params
['max-buckets'] = max_buckets
404 if suspended
is not None:
405 params
['suspended'] = suspended
406 if generate_key
is not None:
407 params
['generate-key'] = generate_key
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 result
= self
.proxy(daemon_name
, 'PUT', 'user', params
)
413 return self
._append
_uid
(result
)
416 def set(self
, uid
, display_name
=None, email
=None, max_buckets
=None,
417 suspended
=None, daemon_name
=None):
418 params
= {'uid': uid
}
419 if display_name
is not None:
420 params
['display-name'] = display_name
421 if email
is not None:
422 params
['email'] = email
423 if max_buckets
is not None:
424 params
['max-buckets'] = max_buckets
425 if suspended
is not None:
426 params
['suspended'] = suspended
427 result
= self
.proxy(daemon_name
, 'POST', 'user', params
)
428 return self
._append
_uid
(result
)
430 def delete(self
, uid
, daemon_name
=None):
432 instance
= RgwClient
.admin_instance(daemon_name
=daemon_name
)
433 # Ensure the user is not configured to access the RGW Object Gateway.
434 if instance
.userid
== uid
:
435 raise DashboardException(msg
='Unable to delete "{}" - this user '
436 'account is required for managing the '
437 'Object Gateway'.format(uid
))
438 # Finally redirect request to the RGW proxy.
439 return self
.proxy(daemon_name
, 'DELETE', 'user', {'uid': uid
}, json_response
=False)
440 except (DashboardException
, RequestException
) as e
: # pragma: no cover
441 raise DashboardException(e
, component
='rgw')
443 # pylint: disable=redefined-builtin
444 @RESTController.Resource(method
='POST', path
='/capability', status
=201)
446 def create_cap(self
, uid
, type, perm
, daemon_name
=None):
447 return self
.proxy(daemon_name
, 'PUT', 'user?caps', {
449 'user-caps': '{}={}'.format(type, perm
)
452 # pylint: disable=redefined-builtin
453 @RESTController.Resource(method
='DELETE', path
='/capability', status
=204)
454 def delete_cap(self
, uid
, type, perm
, daemon_name
=None):
455 return self
.proxy(daemon_name
, 'DELETE', 'user?caps', {
457 'user-caps': '{}={}'.format(type, perm
)
460 @RESTController.Resource(method
='POST', path
='/key', status
=201)
462 def create_key(self
, uid
, key_type
='s3', subuser
=None, generate_key
='true',
463 access_key
=None, secret_key
=None, daemon_name
=None):
464 params
= {'uid': uid
, 'key-type': key_type
, 'generate-key': generate_key
}
465 if subuser
is not None:
466 params
['subuser'] = subuser
467 if access_key
is not None:
468 params
['access-key'] = access_key
469 if secret_key
is not None:
470 params
['secret-key'] = secret_key
471 return self
.proxy(daemon_name
, 'PUT', 'user?key', params
)
473 @RESTController.Resource(method
='DELETE', path
='/key', status
=204)
474 def delete_key(self
, uid
, key_type
='s3', subuser
=None, access_key
=None, daemon_name
=None):
475 params
= {'uid': uid
, 'key-type': key_type
}
476 if subuser
is not None:
477 params
['subuser'] = subuser
478 if access_key
is not None:
479 params
['access-key'] = access_key
480 return self
.proxy(daemon_name
, 'DELETE', 'user?key', params
, json_response
=False)
482 @RESTController.Resource(method
='GET', path
='/quota')
483 def get_quota(self
, uid
, daemon_name
=None):
484 return self
.proxy(daemon_name
, 'GET', 'user?quota', {'uid': uid
})
486 @RESTController.Resource(method
='PUT', path
='/quota')
488 def set_quota(self
, uid
, quota_type
, enabled
, max_size_kb
, max_objects
, daemon_name
=None):
489 return self
.proxy(daemon_name
, 'PUT', 'user?quota', {
491 'quota-type': quota_type
,
493 'max-size-kb': max_size_kb
,
494 'max-objects': max_objects
495 }, json_response
=False)
497 @RESTController.Resource(method
='POST', path
='/subuser', status
=201)
499 def create_subuser(self
, uid
, subuser
, access
, key_type
='s3',
500 generate_secret
='true', access_key
=None,
501 secret_key
=None, daemon_name
=None):
502 return self
.proxy(daemon_name
, 'PUT', 'user', {
505 'key-type': key_type
,
507 'generate-secret': generate_secret
,
508 'access-key': access_key
,
509 'secret-key': secret_key
512 @RESTController.Resource(method
='DELETE', path
='/subuser/{subuser}', status
=204)
513 def delete_subuser(self
, uid
, subuser
, purge_keys
='true', daemon_name
=None):
515 :param purge_keys: Set to False to do not purge the keys.
516 Note, this only works for s3 subusers.
518 return self
.proxy(daemon_name
, 'DELETE', 'user', {
521 'purge-keys': purge_keys
522 }, json_response
=False)