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 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
)
195 def list(self
, stats
=False):
196 # type: (bool) -> List[Any]
197 query_params
= '?stats' if stats
else ''
198 result
= self
.proxy('GET', 'bucket{}'.format(query_params
))
201 result
= [self
._append
_bid
(bucket
) for bucket
in result
]
205 def get(self
, bucket
):
206 # type: (str) -> dict
207 result
= self
.proxy('GET', 'bucket', {'bucket': bucket
})
208 bucket_name
= RgwBucket
.get_s3_bucket_name(result
['bucket'],
211 # Append the versioning configuration.
212 versioning
= self
._get
_versioning
(result
['owner'], bucket_name
)
213 result
['versioning'] = versioning
['Status']
214 result
['mfa_delete'] = versioning
['MfaDelete']
216 # Append the locking configuration.
217 locking
= self
._get
_locking
(result
['owner'], bucket_name
)
218 result
.update(locking
)
220 return self
._append
_bid
(result
)
223 def create(self
, bucket
, uid
, zonegroup
=None, placement_target
=None,
224 lock_enabled
='false', lock_mode
=None,
225 lock_retention_period_days
=None,
226 lock_retention_period_years
=None):
227 lock_enabled
= str_to_bool(lock_enabled
)
229 rgw_client
= RgwClient
.instance(uid
)
230 result
= rgw_client
.create_bucket(bucket
, zonegroup
,
234 self
._set
_locking
(uid
, bucket
, lock_mode
,
235 lock_retention_period_days
,
236 lock_retention_period_years
)
238 except RequestException
as e
: # pragma: no cover - handling is too obvious
239 raise DashboardException(e
, http_status_code
=500, component
='rgw')
242 def set(self
, bucket
, bucket_id
, uid
, versioning_state
=None,
243 mfa_delete
=None, mfa_token_serial
=None, mfa_token_pin
=None,
244 lock_mode
=None, lock_retention_period_days
=None,
245 lock_retention_period_years
=None):
246 # When linking a non-tenant-user owned bucket to a tenanted user, we
247 # need to prefix bucket name with '/'. e.g. photos -> /photos
248 if '$' in uid
and '/' not in bucket
:
249 bucket
= '/{}'.format(bucket
)
251 # Link bucket to new user:
252 result
= self
.proxy('PUT',
255 'bucket-id': bucket_id
,
260 uid_tenant
= uid
[:uid
.find('$')] if uid
.find('$') >= 0 else None
261 bucket_name
= RgwBucket
.get_s3_bucket_name(bucket
, uid_tenant
)
264 self
._set
_versioning
(uid
, bucket_name
, versioning_state
,
265 mfa_delete
, mfa_token_serial
, mfa_token_pin
)
267 # Update locking if it is enabled.
268 locking
= self
._get
_locking
(uid
, bucket_name
)
269 if locking
['lock_enabled']:
270 self
._set
_locking
(uid
, bucket_name
, lock_mode
,
271 lock_retention_period_days
,
272 lock_retention_period_years
)
274 return self
._append
_bid
(result
)
276 def delete(self
, bucket
, purge_objects
='true'):
277 return self
.proxy('DELETE', 'bucket', {
279 'purge-objects': purge_objects
280 }, json_response
=False)
283 @ApiController('/rgw/user', Scope
.RGW
)
284 class RgwUser(RgwRESTController
):
285 def _append_uid(self
, user
):
287 Append the user identifier that looks like [<tenant>$]<user>.
288 See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for
290 :param user: The user parameters.
292 :return: The modified user parameters including the 'uid' parameter.
295 if isinstance(user
, dict):
296 user
['uid'] = '{}${}'.format(user
['tenant'], user
['user_id']) \
297 if user
['tenant'] else user
['user_id']
302 permissions
= AuthManager
.get_user(JwtManager
.get_username()).permissions_dict()
303 edit_permissions
= [Permission
.CREATE
, Permission
.UPDATE
, Permission
.DELETE
]
304 return Scope
.RGW
in permissions
and Permission
.READ
in permissions
[Scope
.RGW
] \
305 and len(set(edit_permissions
).intersection(set(permissions
[Scope
.RGW
]))) > 0
308 # type: () -> List[str]
309 users
= [] # type: List[str]
312 params
= {} # type: dict
314 params
['marker'] = marker
315 result
= self
.proxy('GET', 'user?list', params
)
316 users
.extend(result
['keys'])
317 if not result
['truncated']:
319 # Make sure there is a marker.
320 assert result
['marker']
321 # Make sure the marker has changed.
322 assert marker
!= result
['marker']
323 marker
= result
['marker']
327 # type: (str) -> dict
328 result
= self
.proxy('GET', 'user', {'uid': uid
})
329 if not self
._keys
_allowed
():
331 del result
['swift_keys']
332 return self
._append
_uid
(result
)
336 def get_emails(self
):
337 # type: () -> List[str]
339 for uid
in json
.loads(self
.list()): # type: ignore
340 user
= json
.loads(self
.get(uid
)) # type: ignore
342 emails
.append(user
["email"])
346 def create(self
, uid
, display_name
, email
=None, max_buckets
=None,
347 suspended
=None, generate_key
=None, access_key
=None,
349 params
= {'uid': uid
}
350 if display_name
is not None:
351 params
['display-name'] = display_name
352 if email
is not None:
353 params
['email'] = email
354 if max_buckets
is not None:
355 params
['max-buckets'] = max_buckets
356 if suspended
is not None:
357 params
['suspended'] = suspended
358 if generate_key
is not None:
359 params
['generate-key'] = generate_key
360 if access_key
is not None:
361 params
['access-key'] = access_key
362 if secret_key
is not None:
363 params
['secret-key'] = secret_key
364 result
= self
.proxy('PUT', 'user', params
)
365 return self
._append
_uid
(result
)
368 def set(self
, uid
, display_name
=None, email
=None, max_buckets
=None,
370 params
= {'uid': uid
}
371 if display_name
is not None:
372 params
['display-name'] = display_name
373 if email
is not None:
374 params
['email'] = email
375 if max_buckets
is not None:
376 params
['max-buckets'] = max_buckets
377 if suspended
is not None:
378 params
['suspended'] = suspended
379 result
= self
.proxy('POST', 'user', params
)
380 return self
._append
_uid
(result
)
382 def delete(self
, uid
):
384 instance
= RgwClient
.admin_instance()
385 # Ensure the user is not configured to access the RGW Object Gateway.
386 if instance
.userid
== uid
:
387 raise DashboardException(msg
='Unable to delete "{}" - this user '
388 'account is required for managing the '
389 'Object Gateway'.format(uid
))
390 # Finally redirect request to the RGW proxy.
391 return self
.proxy('DELETE', 'user', {'uid': uid
}, json_response
=False)
392 except (DashboardException
, RequestException
) as e
: # pragma: no cover
393 raise DashboardException(e
, component
='rgw')
395 # pylint: disable=redefined-builtin
396 @RESTController.Resource(method
='POST', path
='/capability', status
=201)
398 def create_cap(self
, uid
, type, perm
):
399 return self
.proxy('PUT', 'user?caps', {
401 'user-caps': '{}={}'.format(type, perm
)
404 # pylint: disable=redefined-builtin
405 @RESTController.Resource(method
='DELETE', path
='/capability', status
=204)
406 def delete_cap(self
, uid
, type, perm
):
407 return self
.proxy('DELETE', 'user?caps', {
409 'user-caps': '{}={}'.format(type, perm
)
412 @RESTController.Resource(method
='POST', path
='/key', status
=201)
414 def create_key(self
, uid
, key_type
='s3', subuser
=None, generate_key
='true',
415 access_key
=None, secret_key
=None):
416 params
= {'uid': uid
, 'key-type': key_type
, 'generate-key': generate_key
}
417 if subuser
is not None:
418 params
['subuser'] = subuser
419 if access_key
is not None:
420 params
['access-key'] = access_key
421 if secret_key
is not None:
422 params
['secret-key'] = secret_key
423 return self
.proxy('PUT', 'user?key', params
)
425 @RESTController.Resource(method
='DELETE', path
='/key', status
=204)
426 def delete_key(self
, uid
, key_type
='s3', subuser
=None, access_key
=None):
427 params
= {'uid': uid
, 'key-type': key_type
}
428 if subuser
is not None:
429 params
['subuser'] = subuser
430 if access_key
is not None:
431 params
['access-key'] = access_key
432 return self
.proxy('DELETE', 'user?key', params
, json_response
=False)
434 @RESTController.Resource(method
='GET', path
='/quota')
435 def get_quota(self
, uid
):
436 return self
.proxy('GET', 'user?quota', {'uid': uid
})
438 @RESTController.Resource(method
='PUT', path
='/quota')
440 def set_quota(self
, uid
, quota_type
, enabled
, max_size_kb
, max_objects
):
441 return self
.proxy('PUT', 'user?quota', {
443 'quota-type': quota_type
,
445 'max-size-kb': max_size_kb
,
446 'max-objects': max_objects
447 }, json_response
=False)
449 @RESTController.Resource(method
='POST', path
='/subuser', status
=201)
451 def create_subuser(self
, uid
, subuser
, access
, key_type
='s3',
452 generate_secret
='true', access_key
=None,
454 return self
.proxy('PUT', 'user', {
457 'key-type': key_type
,
459 'generate-secret': generate_secret
,
460 'access-key': access_key
,
461 'secret-key': secret_key
464 @RESTController.Resource(method
='DELETE', path
='/subuser/{subuser}', status
=204)
465 def delete_subuser(self
, uid
, subuser
, purge_keys
='true'):
467 :param purge_keys: Set to False to do not purge the keys.
468 Note, this only works for s3 subusers.
470 return self
.proxy('DELETE', 'user', {
473 'purge-keys': purge_keys
474 }, json_response
=False)