1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
8 from . import ApiController
, BaseController
, RESTController
, Endpoint
, \
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 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 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(
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(
47 raise RequestException(msg
)
48 status
['available'] = True
49 except (RequestException
, LookupError) as ex
:
50 status
['message'] = str(ex
) # type: ignore
54 @ApiController('/rgw/daemon', Scope
.RGW
)
55 class RgwDaemon(RESTController
):
57 # type: () -> List[dict]
59 for hostname
, server
in CephService
.get_service_map('rgw').items():
60 for service
in server
['services']:
61 metadata
= service
['metadata']
63 # extract per-daemon service data and health
66 'version': metadata
['ceph_version'],
67 'server_hostname': hostname
70 daemons
.append(daemon
)
72 return sorted(daemons
, key
=lambda k
: k
['id'])
74 def get(self
, svc_id
):
81 service
= CephService
.get_service('rgw', svc_id
)
83 raise cherrypy
.NotFound('Service rgw {} is not available'.format(svc_id
))
85 metadata
= service
['metadata']
86 status
= service
['status']
89 status
= json
.loads(status
['json'])
91 logger
.warning('%s had invalid status json', service
['id'])
94 logger
.warning('%s has no key "json" in status', service
['id'])
96 daemon
['rgw_metadata'] = metadata
97 daemon
['rgw_status'] = status
101 class RgwRESTController(RESTController
):
102 def proxy(self
, method
, path
, params
=None, json_response
=True):
104 instance
= RgwClient
.admin_instance()
105 result
= instance
.proxy(method
, path
, params
, None)
107 result
= json_str_to_object(result
)
109 except (DashboardException
, RequestException
) as e
:
110 raise DashboardException(e
, http_status_code
=500, component
='rgw')
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()
121 # @TODO: for multisite: by default, retrieve cluster topology/map.
122 raise DashboardException(http_status_code
=501, component
='rgw', msg
='Not Implemented')
127 @ApiController('/rgw/bucket', Scope
.RGW
)
128 class RgwBucket(RgwRESTController
):
129 def _append_bid(self
, bucket
):
131 Append the bucket identifier that looks like [<tenant>/]<bucket>.
132 See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for
134 :param bucket: The bucket parameters.
136 :return: The modified bucket parameters including the 'bid' parameter.
139 if isinstance(bucket
, dict):
140 bucket
['bid'] = '{}/{}'.format(bucket
['tenant'], bucket
['bucket']) \
141 if bucket
['tenant'] else bucket
['bucket']
144 def _get_versioning(self
, owner
, bucket_name
):
145 rgw_client
= RgwClient
.instance(owner
)
146 return rgw_client
.get_bucket_versioning(bucket_name
)
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
)
157 def _get_locking(self
, owner
, bucket_name
):
158 rgw_client
= RgwClient
.instance(owner
)
159 return rgw_client
.get_bucket_locking(bucket_name
)
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
))
169 def strip_tenant_from_bucket_name(bucket_name
):
172 >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name')
174 >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name')
177 return bucket_name
[bucket_name
.find('/') + 1:]
180 def get_s3_bucket_name(bucket_name
, tenant
=None):
181 # type (str, str) -> str
183 >>> RgwBucket.get_s3_bucket_name('bucket-name', 'tenant')
185 >>> RgwBucket.get_s3_bucket_name('tenant/bucket-name', 'tenant')
187 >>> RgwBucket.get_s3_bucket_name('bucket-name')
190 bucket_name
= RgwBucket
.strip_tenant_from_bucket_name(bucket_name
)
192 bucket_name
= '{}:{}'.format(tenant
, bucket_name
)
196 # type: () -> List[str]
197 return self
.proxy('GET', 'bucket')
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'],
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']
210 # Append the locking configuration.
211 locking
= self
._get
_locking
(result
['owner'], bucket_name
)
212 result
.update(locking
)
214 return self
._append
_bid
(result
)
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
)
222 rgw_client
= RgwClient
.instance(uid
)
223 result
= rgw_client
.create_bucket(bucket
, zonegroup
,
227 self
._set
_locking
(uid
, bucket
, lock_mode
,
228 lock_retention_period_days
,
229 lock_retention_period_years
)
231 except RequestException
as e
: # pragma: no cover - handling is too obvious
232 raise DashboardException(e
, http_status_code
=500, component
='rgw')
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
)
243 # Link bucket to new user:
244 result
= self
.proxy('PUT',
247 'bucket-id': bucket_id
,
252 uid_tenant
= uid
[:uid
.find('$')] if uid
.find('$') >= 0 else None
253 bucket_name
= RgwBucket
.get_s3_bucket_name(bucket
, uid_tenant
)
256 self
._set
_versioning
(uid
, bucket_name
, versioning_state
,
257 mfa_delete
, mfa_token_serial
, mfa_token_pin
)
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
)
266 return self
._append
_bid
(result
)
268 def delete(self
, bucket
, purge_objects
='true'):
269 return self
.proxy('DELETE', 'bucket', {
271 'purge-objects': purge_objects
272 }, json_response
=False)
275 @ApiController('/rgw/user', Scope
.RGW
)
276 class RgwUser(RgwRESTController
):
277 def _append_uid(self
, user
):
279 Append the user identifier that looks like [<tenant>$]<user>.
280 See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
282 :param user: The user parameters.
284 :return: The modified user parameters including the 'uid' parameter.
287 if isinstance(user
, dict):
288 user
['uid'] = '{}${}'.format(user
['tenant'], user
['user_id']) \
289 if user
['tenant'] else user
['user_id']
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
300 # type: () -> List[str]
301 users
= [] # type: List[str]
304 params
= {} # type: dict
306 params
['marker'] = marker
307 result
= self
.proxy('GET', 'user?list', params
)
308 users
.extend(result
['keys'])
309 if not result
['truncated']:
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']
319 # type: (str) -> dict
320 result
= self
.proxy('GET', 'user', {'uid': uid
})
321 if not self
._keys
_allowed
():
323 del result
['swift_keys']
324 return self
._append
_uid
(result
)
328 def get_emails(self
):
329 # type: () -> List[str]
331 for uid
in json
.loads(self
.list()): # type: ignore
332 user
= json
.loads(self
.get(uid
)) # type: ignore
334 emails
.append(user
["email"])
337 def create(self
, uid
, display_name
, email
=None, max_buckets
=None,
338 suspended
=None, generate_key
=None, access_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
)
358 def set(self
, uid
, display_name
=None, email
=None, max_buckets
=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
)
372 def delete(self
, uid
):
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')
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', {
390 'user-caps': '{}={}'.format(type, perm
)
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', {
398 'user-caps': '{}={}'.format(type, perm
)
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
)
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)
422 @RESTController.Resource(method
='GET', path
='/quota')
423 def get_quota(self
, uid
):
424 return self
.proxy('GET', 'user?quota', {'uid': uid
})
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', {
430 'quota-type': quota_type
,
432 'max-size-kb': max_size_kb
,
433 'max-objects': max_objects
434 }, json_response
=False)
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,
440 return self
.proxy('PUT', 'user', {
443 'key-type': key_type
,
445 'generate-secret': generate_secret
,
446 'access-key': access_key
,
447 'secret-key': secret_key
450 @RESTController.Resource(method
='DELETE', path
='/subuser/{subuser}', status
=204)
451 def delete_subuser(self
, uid
, subuser
, purge_keys
='true'):
453 :param purge_keys: Set to False to do not purge the keys.
454 Note, this only works for s3 subusers.
456 return self
.proxy('DELETE', 'user', {
459 'purge-keys': purge_keys
460 }, json_response
=False)