1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
9 from . import ApiController
, BaseController
, RESTController
, Endpoint
, \
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
20 from typing
import List
22 pass # Just for type checking
24 logger
= logging
.getLogger('controllers.rgw')
27 @ApiController('/rgw', Scope
.RGW
)
28 class Rgw(BaseController
):
32 status
= {'available': False, 'message': None}
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(
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(
48 raise RequestException(msg
)
49 status
['available'] = True
50 except (RequestException
, LookupError) as ex
:
51 status
['message'] = str(ex
) # type: ignore
55 @ApiController('/rgw/daemon', Scope
.RGW
)
56 class RgwDaemon(RESTController
):
58 # type: () -> List[dict]
60 for hostname
, server
in CephService
.get_service_map('rgw').items():
61 for service
in server
['services']:
62 metadata
= service
['metadata']
64 # extract per-daemon service data and health
67 'version': metadata
['ceph_version'],
68 'server_hostname': hostname
71 daemons
.append(daemon
)
73 return sorted(daemons
, key
=lambda k
: k
['id'])
75 def get(self
, svc_id
):
82 service
= CephService
.get_service('rgw', svc_id
)
84 raise cherrypy
.NotFound('Service rgw {} is not available'.format(svc_id
))
86 metadata
= service
['metadata']
87 status
= service
['status']
90 status
= json
.loads(status
['json'])
92 logger
.warning('%s had invalid status json', service
['id'])
95 logger
.warning('%s has no key "json" in status', service
['id'])
97 daemon
['rgw_metadata'] = metadata
98 daemon
['rgw_status'] = status
102 class RgwRESTController(RESTController
):
103 def proxy(self
, method
, path
, params
=None, json_response
=True):
105 instance
= RgwClient
.admin_instance()
106 result
= instance
.proxy(method
, path
, params
, None)
108 result
= json_str_to_object(result
)
110 except (DashboardException
, RequestException
) as e
:
111 raise DashboardException(e
, http_status_code
=500, component
='rgw')
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()
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')
128 @ApiController('/rgw/bucket', Scope
.RGW
)
129 class RgwBucket(RgwRESTController
):
130 def _append_bid(self
, bucket
):
132 Append the bucket identifier that looks like [<tenant>/]<bucket>.
133 See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for
135 :param bucket: The bucket parameters.
137 :return: The modified bucket parameters including the 'bid' parameter.
140 if isinstance(bucket
, dict):
141 bucket
['bid'] = '{}/{}'.format(bucket
['tenant'], bucket
['bucket']) \
142 if bucket
['tenant'] else bucket
['bucket']
145 def _get_versioning(self
, owner
, bucket_name
):
146 rgw_client
= RgwClient
.instance(owner
)
147 return rgw_client
.get_bucket_versioning(bucket_name
)
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
)
158 def _get_locking(self
, owner
, bucket_name
):
159 rgw_client
= RgwClient
.instance(owner
)
160 return rgw_client
.get_bucket_locking(bucket_name
)
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
))
170 def strip_tenant_from_bucket_name(bucket_name
):
173 >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name')
175 >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name')
178 return bucket_name
[bucket_name
.find('/') + 1:]
181 def get_s3_bucket_name(bucket_name
, tenant
=None):
182 # type (str, str) -> str
184 >>> RgwBucket.get_s3_bucket_name('bucket-name', 'tenant')
186 >>> RgwBucket.get_s3_bucket_name('tenant/bucket-name', 'tenant')
188 >>> RgwBucket.get_s3_bucket_name('bucket-name')
191 bucket_name
= RgwBucket
.strip_tenant_from_bucket_name(bucket_name
)
193 bucket_name
= '{}:{}'.format(tenant
, bucket_name
)
197 # type: () -> List[str]
198 return self
.proxy('GET', 'bucket')
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'],
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']
211 # Append the locking configuration.
212 locking
= self
._get
_locking
(result
['owner'], bucket_name
)
213 result
.update(locking
)
215 return self
._append
_bid
(result
)
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
)
223 rgw_client
= RgwClient
.instance(uid
)
224 result
= rgw_client
.create_bucket(bucket
, zonegroup
,
228 self
._set
_locking
(uid
, bucket
, lock_mode
,
229 lock_retention_period_days
,
230 lock_retention_period_years
)
232 except RequestException
as e
:
233 raise DashboardException(e
, http_status_code
=500, component
='rgw')
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
)
244 # Link bucket to new user:
245 result
= self
.proxy('PUT',
248 'bucket-id': bucket_id
,
253 uid_tenant
= uid
[:uid
.find('$')] if uid
.find('$') >= 0 else None
254 bucket_name
= RgwBucket
.get_s3_bucket_name(bucket
, uid_tenant
)
257 self
._set
_versioning
(uid
, bucket_name
, versioning_state
,
258 mfa_delete
, mfa_token_serial
, mfa_token_pin
)
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
)
267 return self
._append
_bid
(result
)
269 def delete(self
, bucket
, purge_objects
='true'):
270 return self
.proxy('DELETE', 'bucket', {
272 'purge-objects': purge_objects
273 }, json_response
=False)
276 @ApiController('/rgw/user', Scope
.RGW
)
277 class RgwUser(RgwRESTController
):
278 def _append_uid(self
, user
):
280 Append the user identifier that looks like [<tenant>$]<user>.
281 See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
283 :param user: The user parameters.
285 :return: The modified user parameters including the 'uid' parameter.
288 if isinstance(user
, dict):
289 user
['uid'] = '{}${}'.format(user
['tenant'], user
['user_id']) \
290 if user
['tenant'] else user
['user_id']
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
301 # type: () -> List[str]
302 users
= [] # type: List[str]
305 params
= {} # type: dict
307 params
['marker'] = marker
308 result
= self
.proxy('GET', 'user?list', params
)
309 users
.extend(result
['keys'])
310 if not result
['truncated']:
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']
320 # type: (str) -> dict
321 result
= self
.proxy('GET', 'user', {'uid': uid
})
322 if not self
._keys
_allowed
():
324 del result
['swift_keys']
325 return self
._append
_uid
(result
)
329 def get_emails(self
):
330 # type: () -> List[str]
332 for uid
in json
.loads(self
.list()): # type: ignore
333 user
= json
.loads(self
.get(uid
)) # type: ignore
335 emails
.append(user
["email"])
338 def create(self
, uid
, display_name
, email
=None, max_buckets
=None,
339 suspended
=None, generate_key
=None, access_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
)
359 def set(self
, uid
, display_name
=None, email
=None, max_buckets
=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
)
373 def delete(self
, uid
):
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')
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', {
391 'user-caps': '{}={}'.format(type, perm
)
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', {
399 'user-caps': '{}={}'.format(type, perm
)
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
)
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)
423 @RESTController.Resource(method
='GET', path
='/quota')
424 def get_quota(self
, uid
):
425 return self
.proxy('GET', 'user?quota', {'uid': uid
})
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', {
431 'quota-type': quota_type
,
433 'max-size-kb': max_size_kb
,
434 'max-objects': max_objects
435 }, json_response
=False)
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,
441 return self
.proxy('PUT', 'user', {
444 'key-type': key_type
,
446 'generate-secret': generate_secret
,
447 'access-key': access_key
,
448 'secret-key': secret_key
451 @RESTController.Resource(method
='DELETE', path
='/subuser/{subuser}', status
=204)
452 def delete_subuser(self
, uid
, subuser
, purge_keys
='true'):
454 :param purge_keys: Set to False to do not purge the keys.
455 Note, this only works for s3 subusers.
457 return self
.proxy('DELETE', 'user', {
460 'purge-keys': purge_keys
461 }, json_response
=False)