diff options
Diffstat (limited to '')
-rw-r--r-- | src/pybind/mgr/dashboard/controllers/rgw.py | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py new file mode 100644 index 00000000..085155aa --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -0,0 +1,385 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import json + +import cherrypy + +from . import ApiController, BaseController, RESTController, Endpoint, \ + ReadPermission, allow_empty_body +from .. import logger +from ..exceptions import DashboardException +from ..rest_client import RequestException +from ..security import Scope, Permission +from ..services.auth import AuthManager, JwtManager +from ..services.ceph_service import CephService +from ..services.rgw_client import RgwClient + + +@ApiController('/rgw', Scope.RGW) +class Rgw(BaseController): + + @Endpoint() + @ReadPermission + def status(self): + status = {'available': False, 'message': None} + try: + if not CephService.get_service_list('rgw'): + raise LookupError('No RGW service is running.') + instance = RgwClient.admin_instance() + # Check if the service is online. + try: + is_online = instance.is_service_online() + except RequestException as e: + # Drop this instance because the RGW client seems not to + # exist anymore (maybe removed via orchestrator). Removing + # the instance from the cache will result in the correct + # error message next time when the backend tries to + # establish a new connection (-> 'No RGW found' instead + # of 'RGW REST API failed request ...'). + # Note, this only applies to auto-detected RGW clients. + RgwClient.drop_instance(instance.userid) + raise e + if not is_online: + msg = 'Failed to connect to the Object Gateway\'s Admin Ops API.' + raise RequestException(msg) + # Ensure the API user ID is known by the RGW. + if not instance.user_exists(): + msg = 'The user "{}" is unknown to the Object Gateway.'.format( + instance.userid) + raise RequestException(msg) + # Ensure the system flag is set for the API user ID. + if not instance.is_system_user(): + msg = 'The system flag is not set for user "{}".'.format( + instance.userid) + raise RequestException(msg) + status['available'] = True + except (RequestException, LookupError) as ex: + status['message'] = str(ex) + return status + + +@ApiController('/rgw/daemon', Scope.RGW) +class RgwDaemon(RESTController): + + def list(self): + daemons = [] + for hostname, server in CephService.get_service_map('rgw').items(): + for service in server['services']: + metadata = service['metadata'] + + # extract per-daemon service data and health + daemon = { + 'id': service['id'], + 'version': metadata['ceph_version'], + 'server_hostname': hostname + } + + daemons.append(daemon) + + return sorted(daemons, key=lambda k: k['id']) + + def get(self, svc_id): + daemon = { + 'rgw_metadata': [], + 'rgw_id': svc_id, + 'rgw_status': [] + } + service = CephService.get_service('rgw', svc_id) + if not service: + raise cherrypy.NotFound('Service rgw {} is not available'.format(svc_id)) + + metadata = service['metadata'] + status = service['status'] + if 'json' in status: + try: + status = json.loads(status['json']) + except ValueError: + logger.warning('%s had invalid status json', service['id']) + status = {} + else: + logger.warning('%s has no key "json" in status', service['id']) + + daemon['rgw_metadata'] = metadata + daemon['rgw_status'] = status + return daemon + + +class RgwRESTController(RESTController): + + def proxy(self, method, path, params=None, json_response=True): + try: + instance = RgwClient.admin_instance() + result = instance.proxy(method, path, params, None) + if json_response and result != '': + result = json.loads(result.decode('utf-8')) + return result + except (DashboardException, RequestException) as e: + raise DashboardException(e, http_status_code=500, component='rgw') + + +@ApiController('/rgw/bucket', Scope.RGW) +class RgwBucket(RgwRESTController): + + def _append_bid(self, bucket): + """ + Append the bucket identifier that looks like [<tenant>/]<bucket>. + See http://docs.ceph.com/docs/nautilus/radosgw/multitenancy/ for + more information. + :param bucket: The bucket parameters. + :type bucket: dict + :return: The modified bucket parameters including the 'bid' parameter. + :rtype: dict + """ + if isinstance(bucket, dict): + bucket['bid'] = '{}/{}'.format(bucket['tenant'], bucket['bucket']) \ + if bucket['tenant'] else bucket['bucket'] + return bucket + + @staticmethod + def strip_tenant_from_bucket_name(bucket_name, uid): + # type (str, str) => str + """ + When linking a bucket to a new user belonging to same tenant + as the previous owner, tenant must be removed from the bucket name. + >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name', 'tenant$user1') + 'bucket-name' + >>> RgwBucket.strip_tenant_from_bucket_name('tenant/bucket-name', 'tenant2$user2') + 'tenant/bucket-name' + >>> RgwBucket.strip_tenant_from_bucket_name('bucket-name', 'user1') + 'bucket-name' + """ + bucket_tenant = bucket_name[:bucket_name.find('/')] if bucket_name.find('/') >= 0 else None + uid_tenant = uid[:uid.find('$')] if uid.find('$') >= 0 else None + if bucket_tenant and uid_tenant and bucket_tenant == uid_tenant: + return bucket_name[bucket_name.find('/') + 1:] + + return bucket_name + + def list(self, stats=False): + query_params = '?stats' if stats else '' + result = self.proxy('GET', 'bucket{}'.format(query_params)) + + if stats: + result = [self._append_bid(bucket) for bucket in result] + + return result + + def get(self, bucket): + result = self.proxy('GET', 'bucket', {'bucket': bucket}) + return self._append_bid(result) + + @allow_empty_body + def create(self, bucket, uid): + try: + rgw_client = RgwClient.instance(uid) + return rgw_client.create_bucket(bucket) + except RequestException as e: + raise DashboardException(e, http_status_code=500, component='rgw') + + @allow_empty_body + def set(self, bucket, bucket_id, uid): + result = self.proxy('PUT', 'bucket', { + 'bucket': RgwBucket.strip_tenant_from_bucket_name(bucket, uid), + 'bucket-id': bucket_id, + 'uid': uid + }, json_response=False) + return self._append_bid(result) + + def delete(self, bucket, purge_objects='true'): + return self.proxy('DELETE', 'bucket', { + 'bucket': bucket, + 'purge-objects': purge_objects + }, json_response=False) + + +@ApiController('/rgw/user', Scope.RGW) +class RgwUser(RgwRESTController): + + def _append_uid(self, user): + """ + Append the user identifier that looks like [<tenant>$]<user>. + See http://docs.ceph.com/docs/jewel/radosgw/multitenancy/ for + more information. + :param user: The user parameters. + :type user: dict + :return: The modified user parameters including the 'uid' parameter. + :rtype: dict + """ + if isinstance(user, dict): + user['uid'] = '{}${}'.format(user['tenant'], user['user_id']) \ + if user['tenant'] else user['user_id'] + return user + + @staticmethod + def _keys_allowed(): + permissions = AuthManager.get_user(JwtManager.get_username()).permissions_dict() + edit_permissions = [Permission.CREATE, Permission.UPDATE, Permission.DELETE] + return Scope.RGW in permissions and Permission.READ in permissions[Scope.RGW] \ + and len(set(edit_permissions).intersection(set(permissions[Scope.RGW]))) > 0 + + def list(self): + users = [] + marker = None + while True: + params = {} + if marker: + params['marker'] = marker + result = self.proxy('GET', 'user?list', params) + users.extend(result['keys']) + if not result['truncated']: + break + # Make sure there is a marker. + assert result['marker'] + # Make sure the marker has changed. + assert marker != result['marker'] + marker = result['marker'] + return users + + def get(self, uid): + result = self.proxy('GET', 'user', {'uid': uid}) + if not self._keys_allowed(): + del result['keys'] + del result['swift_keys'] + return self._append_uid(result) + + @Endpoint() + @ReadPermission + def get_emails(self): + emails = [] + for uid in json.loads(self.list()): + user = json.loads(self.get(uid)) + if user["email"]: + emails.append(user["email"]) + return emails + + @allow_empty_body + def create(self, uid, display_name, email=None, max_buckets=None, + suspended=None, generate_key=None, access_key=None, + secret_key=None): + params = {'uid': uid} + if display_name is not None: + params['display-name'] = display_name + if email is not None: + params['email'] = email + if max_buckets is not None: + params['max-buckets'] = max_buckets + if suspended is not None: + params['suspended'] = suspended + if generate_key is not None: + params['generate-key'] = generate_key + if access_key is not None: + params['access-key'] = access_key + if secret_key is not None: + params['secret-key'] = secret_key + result = self.proxy('PUT', 'user', params) + return self._append_uid(result) + + @allow_empty_body + def set(self, uid, display_name=None, email=None, max_buckets=None, + suspended=None): + params = {'uid': uid} + if display_name is not None: + params['display-name'] = display_name + if email is not None: + params['email'] = email + if max_buckets is not None: + params['max-buckets'] = max_buckets + if suspended is not None: + params['suspended'] = suspended + result = self.proxy('POST', 'user', params) + return self._append_uid(result) + + def delete(self, uid): + try: + instance = RgwClient.admin_instance() + # Ensure the user is not configured to access the RGW Object Gateway. + if instance.userid == uid: + raise DashboardException(msg='Unable to delete "{}" - this user ' + 'account is required for managing the ' + 'Object Gateway'.format(uid)) + # Finally redirect request to the RGW proxy. + return self.proxy('DELETE', 'user', {'uid': uid}, json_response=False) + except (DashboardException, RequestException) as e: + raise DashboardException(e, component='rgw') + + # pylint: disable=redefined-builtin + @RESTController.Resource(method='POST', path='/capability', status=201) + @allow_empty_body + def create_cap(self, uid, type, perm): + return self.proxy('PUT', 'user?caps', { + 'uid': uid, + 'user-caps': '{}={}'.format(type, perm) + }) + + # pylint: disable=redefined-builtin + @RESTController.Resource(method='DELETE', path='/capability', status=204) + def delete_cap(self, uid, type, perm): + return self.proxy('DELETE', 'user?caps', { + 'uid': uid, + 'user-caps': '{}={}'.format(type, perm) + }) + + @RESTController.Resource(method='POST', path='/key', status=201) + @allow_empty_body + def create_key(self, uid, key_type='s3', subuser=None, generate_key='true', + access_key=None, secret_key=None): + params = {'uid': uid, 'key-type': key_type, 'generate-key': generate_key} + if subuser is not None: + params['subuser'] = subuser + if access_key is not None: + params['access-key'] = access_key + if secret_key is not None: + params['secret-key'] = secret_key + return self.proxy('PUT', 'user?key', params) + + @RESTController.Resource(method='DELETE', path='/key', status=204) + def delete_key(self, uid, key_type='s3', subuser=None, access_key=None): + params = {'uid': uid, 'key-type': key_type} + if subuser is not None: + params['subuser'] = subuser + if access_key is not None: + params['access-key'] = access_key + return self.proxy('DELETE', 'user?key', params, json_response=False) + + @RESTController.Resource(method='GET', path='/quota') + def get_quota(self, uid): + return self.proxy('GET', 'user?quota', {'uid': uid}) + + @RESTController.Resource(method='PUT', path='/quota') + @allow_empty_body + def set_quota(self, uid, quota_type, enabled, max_size_kb, max_objects): + return self.proxy('PUT', 'user?quota', { + 'uid': uid, + 'quota-type': quota_type, + 'enabled': enabled, + 'max-size-kb': max_size_kb, + 'max-objects': max_objects + }, json_response=False) + + @RESTController.Resource(method='POST', path='/subuser', status=201) + @allow_empty_body + def create_subuser(self, uid, subuser, access, key_type='s3', + generate_secret='true', access_key=None, + secret_key=None): + return self.proxy('PUT', 'user', { + 'uid': uid, + 'subuser': subuser, + 'key-type': key_type, + 'access': access, + 'generate-secret': generate_secret, + 'access-key': access_key, + 'secret-key': secret_key + }) + + @RESTController.Resource(method='DELETE', path='/subuser/{subuser}', status=204) + def delete_subuser(self, uid, subuser, purge_keys='true'): + """ + :param purge_keys: Set to False to do not purge the keys. + Note, this only works for s3 subusers. + """ + return self.proxy('DELETE', 'user', { + 'uid': uid, + 'subuser': subuser, + 'purge-keys': purge_keys + }, json_response=False) |