1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
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
19 from typing
import Any
, List
20 except ImportError: # pragma: no cover
21 pass # Just for type checking
23 logger
= logging
.getLogger('controllers.rgw')
26 @ApiController('/rgw', Scope
.RGW
)
27 class Rgw(BaseController
):
31 status
= {'available': False, 'message': None}
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.
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
)
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(
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(
61 raise RequestException(msg
)
62 status
['available'] = True
63 except (RequestException
, LookupError) as ex
:
64 status
['message'] = str(ex
) # type: ignore
68 @ApiController('/rgw/daemon', Scope
.RGW
)
69 class RgwDaemon(RESTController
):
71 # type: () -> List[dict]
73 for hostname
, server
in CephService
.get_service_map('rgw').items():
74 for service
in server
['services']:
75 metadata
= service
['metadata']
77 # extract per-daemon service data and health
80 'version': metadata
['ceph_version'],
81 'server_hostname': hostname
84 daemons
.append(daemon
)
86 return sorted(daemons
, key
=lambda k
: k
['id'])
88 def get(self
, svc_id
):
95 service
= CephService
.get_service('rgw', svc_id
)
97 raise cherrypy
.NotFound('Service rgw {} is not available'.format(svc_id
))
99 metadata
= service
['metadata']
100 status
= service
['status']
103 status
= json
.loads(status
['json'])
105 logger
.warning('%s had invalid status json', service
['id'])
108 logger
.warning('%s has no key "json" in status', service
['id'])
110 daemon
['rgw_metadata'] = metadata
111 daemon
['rgw_status'] = status
115 class RgwRESTController(RESTController
):
116 def proxy(self
, method
, path
, params
=None, json_response
=True):
118 instance
= RgwClient
.admin_instance()
119 result
= instance
.proxy(method
, path
, params
, None)
121 result
= json_str_to_object(result
)
123 except (DashboardException
, RequestException
) as e
:
124 raise DashboardException(e
, http_status_code
=500, component
='rgw')
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()
135 # @TODO: for multisite: by default, retrieve cluster topology/map.
136 raise DashboardException(http_status_code
=501, component
='rgw', msg
='Not Implemented')
141 @ApiController('/rgw/bucket', Scope
.RGW
)
142 class RgwBucket(RgwRESTController
):
143 def _append_bid(self
, bucket
):
145 Append the bucket identifier that looks like [<tenant>/]<bucket>.
146 See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for
148 :param bucket: The bucket parameters.
150 :return: The modified bucket parameters including the 'bid' parameter.
153 if isinstance(bucket
, dict):
154 bucket
['bid'] = '{}/{}'.format(bucket
['tenant'], bucket
['bucket']) \
155 if bucket
['tenant'] else bucket
['bucket']
158 def _get_versioning(self
, owner
, bucket_name
):
159 rgw_client
= RgwClient
.instance(owner
)
160 return rgw_client
.get_bucket_versioning(bucket_name
)
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
)
171 def _get_locking(self
, owner
, bucket_name
):
172 rgw_client
= RgwClient
.instance(owner
)
173 return rgw_client
.get_bucket_locking(bucket_name
)
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
))
183 def strip_tenant_from_bucket_name(bucket_name
):
186 >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name')
188 >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name')
191 return bucket_name
[bucket_name
.find('/') + 1:]
194 def get_s3_bucket_name(bucket_name
, tenant
=None):
195 # type (str, str) -> str
197 >>> RgwBucket.get_s3_bucket_name('bucket-name', 'tenant')
199 >>> RgwBucket.get_s3_bucket_name('tenant/bucket-name', 'tenant')
201 >>> RgwBucket.get_s3_bucket_name('bucket-name')
204 bucket_name
= RgwBucket
.strip_tenant_from_bucket_name(bucket_name
)
206 bucket_name
= '{}:{}'.format(tenant
, bucket_name
)
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
))
215 result
= [self
._append
_bid
(bucket
) for bucket
in result
]
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'],
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']
230 # Append the locking configuration.
231 locking
= self
._get
_locking
(result
['owner'], bucket_name
)
232 result
.update(locking
)
234 return self
._append
_bid
(result
)
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
)
243 rgw_client
= RgwClient
.instance(uid
)
244 result
= rgw_client
.create_bucket(bucket
, zonegroup
,
248 self
._set
_locking
(uid
, bucket
, lock_mode
,
249 lock_retention_period_days
,
250 lock_retention_period_years
)
252 except RequestException
as e
: # pragma: no cover - handling is too obvious
253 raise DashboardException(e
, http_status_code
=500, component
='rgw')
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
)
265 # Link bucket to new user:
266 result
= self
.proxy('PUT',
269 'bucket-id': bucket_id
,
274 uid_tenant
= uid
[:uid
.find('$')] if uid
.find('$') >= 0 else None
275 bucket_name
= RgwBucket
.get_s3_bucket_name(bucket
, uid_tenant
)
278 self
._set
_versioning
(uid
, bucket_name
, versioning_state
,
279 mfa_delete
, mfa_token_serial
, mfa_token_pin
)
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
)
288 return self
._append
_bid
(result
)
290 def delete(self
, bucket
, purge_objects
='true'):
291 return self
.proxy('DELETE', 'bucket', {
293 'purge-objects': purge_objects
294 }, json_response
=False)
297 @ApiController('/rgw/user', Scope
.RGW
)
298 class RgwUser(RgwRESTController
):
299 def _append_uid(self
, user
):
301 Append the user identifier that looks like [<tenant>$]<user>.
302 See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
304 :param user: The user parameters.
306 :return: The modified user parameters including the 'uid' parameter.
309 if isinstance(user
, dict):
310 user
['uid'] = '{}${}'.format(user
['tenant'], user
['user_id']) \
311 if user
['tenant'] else user
['user_id']
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
322 # type: () -> List[str]
323 users
= [] # type: List[str]
326 params
= {} # type: dict
328 params
['marker'] = marker
329 result
= self
.proxy('GET', 'user?list', params
)
330 users
.extend(result
['keys'])
331 if not result
['truncated']:
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']
341 # type: (str) -> dict
342 result
= self
.proxy('GET', 'user', {'uid': uid
})
343 if not self
._keys
_allowed
():
345 del result
['swift_keys']
346 return self
._append
_uid
(result
)
350 def get_emails(self
):
351 # type: () -> List[str]
353 for uid
in json
.loads(self
.list()): # type: ignore
354 user
= json
.loads(self
.get(uid
)) # type: ignore
356 emails
.append(user
["email"])
360 def create(self
, uid
, display_name
, email
=None, max_buckets
=None,
361 suspended
=None, generate_key
=None, access_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
)
382 def set(self
, uid
, display_name
=None, email
=None, max_buckets
=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
)
396 def delete(self
, uid
):
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')
409 # pylint: disable=redefined-builtin
410 @RESTController.Resource(method
='POST', path
='/capability', status
=201)
412 def create_cap(self
, uid
, type, perm
):
413 return self
.proxy('PUT', 'user?caps', {
415 'user-caps': '{}={}'.format(type, perm
)
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', {
423 'user-caps': '{}={}'.format(type, perm
)
426 @RESTController.Resource(method
='POST', path
='/key', status
=201)
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
)
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)
448 @RESTController.Resource(method
='GET', path
='/quota')
449 def get_quota(self
, uid
):
450 return self
.proxy('GET', 'user?quota', {'uid': uid
})
452 @RESTController.Resource(method
='PUT', path
='/quota')
454 def set_quota(self
, uid
, quota_type
, enabled
, max_size_kb
, max_objects
):
455 return self
.proxy('PUT', 'user?quota', {
457 'quota-type': quota_type
,
459 'max-size-kb': max_size_kb
,
460 'max-objects': max_objects
461 }, json_response
=False)
463 @RESTController.Resource(method
='POST', path
='/subuser', status
=201)
465 def create_subuser(self
, uid
, subuser
, access
, key_type
='s3',
466 generate_secret
='true', access_key
=None,
468 return self
.proxy('PUT', 'user', {
471 'key-type': key_type
,
473 'generate-secret': generate_secret
,
474 'access-key': access_key
,
475 'secret-key': secret_key
478 @RESTController.Resource(method
='DELETE', path
='/subuser/{subuser}', status
=204)
479 def delete_subuser(self
, uid
, subuser
, purge_keys
='true'):
481 :param purge_keys: Set to False to do not purge the keys.
482 Note, this only works for s3 subusers.
484 return self
.proxy('DELETE', 'user', {
487 'purge-keys': purge_keys
488 }, json_response
=False)