]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: select any object gateway on local cluster. 38170/head
authorAlfonso Martínez <almartin@redhat.com>
Wed, 24 Feb 2021 07:20:53 +0000 (08:20 +0100)
committerAlfonso Martínez <almartin@redhat.com>
Wed, 24 Feb 2021 07:21:11 +0000 (08:21 +0100)
Dashboard backend settings:
- Refactoring: now accepting more than 1 type of value.
- RGW_API_ACCESS_KEY & RGW_API_SECRET_KEY accept string (backward compatibility: legacy behavior) as well as dictionary of strings for connecting multiple daemons.
- Ease of use: deprecated: mgr/dashboard/RGW_API_USER_ID: not useful anymore (kept for backward compatibility).

UI/UX:
- Created context component (to be shown only on rgw-related routes) for selecting operating daemon.
- Daemon selector only shown if there is more than 1 daemon running on a local cluster (to reduce cognitive load).

Fixes: https://tracker.ceph.com/issues/47375
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
28 files changed:
doc/mgr/dashboard.rst
qa/tasks/mgr/dashboard/test_rgw.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/controllers/settings.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-site.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
src/pybind/mgr/dashboard/frontend/src/testing/unit-test-helper.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py
src/pybind/mgr/dashboard/settings.py
src/pybind/mgr/dashboard/tests/test_rgw.py
src/pybind/mgr/dashboard/tests/test_rgw_client.py
src/pybind/mgr/dashboard/tests/test_settings.py

index 37f092c5ab1dbb88a588fc4814c634e3f000c9a8..efa458a7cf4c7d1226472169ef522623208f3799 100644 (file)
@@ -389,11 +389,22 @@ To obtain the credentials of an existing user via `radosgw-admin`::
 
   $ radosgw-admin user info --uid=<user_id>
 
-Finally, provide the credentials to the dashboard::
+In case of having several Object Gateways, you will need the required users' credentials
+to connect to each Object Gateway.
+Finally, provide these credentials to the dashboard::
 
+  $ echo -n "{'<daemon1.id>': '<user1-access-key>', '<daemon2.id>': '<user2-access-key>', ...}" > <file-containing-access-key>
+  $ echo -n "{'<daemon1.id>': '<user1-secret-key>', '<daemon2.id>': '<user2-secret-key>', ...}" > <file-containing-secret-key>
   $ ceph dashboard set-rgw-api-access-key -i <file-containing-access-key>
   $ ceph dashboard set-rgw-api-secret-key -i <file-containing-secret-key>
 
+.. note::
+
+  Legacy way of providing credentials (connect to single Object Gateway)::
+
+  $ echo -n "<access-key>" > <file-containing-access-key>
+  $ echo -n "<secret-key>" > <file-containing-secret-key>
+
 In a simple configuration with a single RGW endpoint, this is all you
 have to do to get the Object Gateway management functionality working. The
 dashboard will try to automatically determine the host and port
@@ -411,7 +422,6 @@ exist and you may find yourself in the situation that you have to use them::
 
   $ ceph dashboard set-rgw-api-scheme <scheme>  # http or https
   $ ceph dashboard set-rgw-api-admin-resource <admin_resource>
-  $ ceph dashboard set-rgw-api-user-id <user_id>
 
 If you are using a self-signed certificate in your Object Gateway setup,
 you should disable certificate verification in the dashboard to avoid refused
index 36227f9d22674578759102f05b92358949380467..17495bf6718fe67281d77f58f4e0fe781f66e86a 100644 (file)
@@ -31,7 +31,6 @@ class RgwTestCase(DashboardTestCase):
             '--system', '--access-key', 'admin', '--secret', 'admin'
         ])
         # Update the dashboard configuration.
-        cls._ceph_cmd(['dashboard', 'set-rgw-api-user-id', 'admin'])
         cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin')
         cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin')
         # Create a test user?
@@ -79,7 +78,6 @@ class RgwApiCredentialsTest(RgwTestCase):
         self._ceph_cmd(['mgr', 'module', 'disable', 'dashboard'])
         self._ceph_cmd(['mgr', 'module', 'enable', 'dashboard', '--force'])
         # Set the default credentials.
-        self._ceph_cmd(['dashboard', 'set-rgw-api-user-id', ''])
         self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin')
         self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin')
         super(RgwApiCredentialsTest, self).setUp()
@@ -101,16 +99,6 @@ class RgwApiCredentialsTest(RgwTestCase):
         self.assertIn('message', data)
         self.assertTrue(data['available'])
 
-    def test_invalid_user_id(self):
-        self._ceph_cmd(['dashboard', 'set-rgw-api-user-id', 'xyz'])
-        data = self._get('/api/rgw/status')
-        self.assertStatus(200)
-        self.assertIn('available', data)
-        self.assertIn('message', data)
-        self.assertFalse(data['available'])
-        self.assertIn('The user "xyz" is unknown to the Object Gateway.',
-                      data['message'])
-
 
 class RgwSiteTest(RgwTestCase):
 
@@ -471,6 +459,8 @@ class RgwDaemonTest(RgwTestCase):
         self.assertIn('id', data)
         self.assertIn('version', data)
         self.assertIn('server_hostname', data)
+        self.assertIn('zonegroup_name', data)
+        self.assertIn('zone_name', data)
 
     def test_get(self):
         data = self._get('/api/rgw/daemon')
index 23d1f5db62f610b969dcd7d7c37ec3003aab264f..9e41e5a4d6927658751a5270f170e4ed36215b6f 100644 (file)
@@ -17,7 +17,7 @@ from . import ApiController, BaseController, ControllerDoc, Endpoint, \
     EndpointDoc, ReadPermission, RESTController, allow_empty_body
 
 try:
-    from typing import Any, List
+    from typing import Any, List, Optional
 except ImportError:  # pragma: no cover
     pass  # Just for type checking
 
@@ -31,7 +31,9 @@ RGW_SCHEMA = {
 RGW_DAEMON_SCHEMA = {
     "id": (str, "Daemon ID"),
     "version": (str, "Ceph Version"),
-    "server_hostname": (str, "")
+    "server_hostname": (str, ""),
+    "zonegroup_name": (str, "Zone Group"),
+    "zone_name": (str, "Zone")
 }
 
 RGW_USER_SCHEMA = {
@@ -63,16 +65,11 @@ class Rgw(BaseController):
                 # 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)
+                RgwClient.drop_instance(instance)
                 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():  # pragma: no cover - no complexity there
                 msg = 'The system flag is not set for user "{}".'.format(
@@ -92,6 +89,7 @@ class RgwDaemon(RESTController):
     def list(self):
         # type: () -> List[dict]
         daemons = []
+        instance = RgwClient.admin_instance()
         for hostname, server in CephService.get_service_map('rgw').items():
             for service in server['services']:
                 metadata = service['metadata']
@@ -100,7 +98,10 @@ class RgwDaemon(RESTController):
                 daemon = {
                     'id': service['id'],
                     'version': metadata['ceph_version'],
-                    'server_hostname': hostname
+                    'server_hostname': hostname,
+                    'zonegroup_name': metadata['zonegroup_name'],
+                    'zone_name': metadata['zone_name'],
+                    'default': instance.daemon.name == service['id']
                 }
 
                 daemons.append(daemon)
@@ -135,9 +136,9 @@ class RgwDaemon(RESTController):
 
 
 class RgwRESTController(RESTController):
-    def proxy(self, method, path, params=None, json_response=True):
+    def proxy(self, daemon_name, method, path, params=None, json_response=True):
         try:
-            instance = RgwClient.admin_instance()
+            instance = RgwClient.admin_instance(daemon_name=daemon_name)
             result = instance.proxy(method, path, params, None)
             if json_response:
                 result = json_str_to_object(result)
@@ -149,16 +150,14 @@ class RgwRESTController(RESTController):
 @ApiController('/rgw/site', Scope.RGW)
 @ControllerDoc("RGW Site Management API", "RgwSite")
 class RgwSite(RgwRESTController):
-    def list(self, query=None):
+    def list(self, query=None, daemon_name=None):
         if query == 'placement-targets':
-            result = RgwClient.admin_instance().get_placement_targets()
-        elif query == 'realms':
-            result = RgwClient.admin_instance().get_realms()
-        else:
-            # @TODO: for multisite: by default, retrieve cluster topology/map.
-            raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
+            return RgwClient.admin_instance(daemon_name=daemon_name).get_placement_targets()
+        if query == 'realms':
+            return RgwClient.admin_instance(daemon_name=daemon_name).get_realms()
 
-        return result
+        # @TODO: for multisite: by default, retrieve cluster topology/map.
+        raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
 
 
 @ApiController('/rgw/bucket', Scope.RGW)
@@ -179,26 +178,26 @@ class RgwBucket(RgwRESTController):
                 if bucket['tenant'] else bucket['bucket']
         return bucket
 
-    def _get_versioning(self, owner, bucket_name):
-        rgw_client = RgwClient.instance(owner)
+    def _get_versioning(self, owner, daemon_name, bucket_name):
+        rgw_client = RgwClient.instance(owner, daemon_name)
         return rgw_client.get_bucket_versioning(bucket_name)
 
-    def _set_versioning(self, owner, bucket_name, versioning_state, mfa_delete,
+    def _set_versioning(self, owner, daemon_name, bucket_name, versioning_state, mfa_delete,
                         mfa_token_serial, mfa_token_pin):
-        bucket_versioning = self._get_versioning(owner, bucket_name)
+        bucket_versioning = self._get_versioning(owner, daemon_name, bucket_name)
         if versioning_state != bucket_versioning['Status']\
                 or (mfa_delete and mfa_delete != bucket_versioning['MfaDelete']):
-            rgw_client = RgwClient.instance(owner)
+            rgw_client = RgwClient.instance(owner, daemon_name)
             rgw_client.set_bucket_versioning(bucket_name, versioning_state, mfa_delete,
                                              mfa_token_serial, mfa_token_pin)
 
-    def _get_locking(self, owner, bucket_name):
-        rgw_client = RgwClient.instance(owner)
+    def _get_locking(self, owner, daemon_name, bucket_name):
+        rgw_client = RgwClient.instance(owner, daemon_name)
         return rgw_client.get_bucket_locking(bucket_name)
 
-    def _set_locking(self, owner, bucket_name, mode,
+    def _set_locking(self, owner, daemon_name, bucket_name, mode,
                      retention_period_days, retention_period_years):
-        rgw_client = RgwClient.instance(owner)
+        rgw_client = RgwClient.instance(owner, daemon_name)
         return rgw_client.set_bucket_locking(bucket_name, mode,
                                              int(retention_period_days),
                                              int(retention_period_years))
@@ -230,29 +229,29 @@ class RgwBucket(RgwRESTController):
             bucket_name = '{}:{}'.format(tenant, bucket_name)
         return bucket_name
 
-    def list(self, stats=False):
-        # type: (bool) -> List[Any]
+    def list(self, stats=False, daemon_name=None):
+        # type: (bool, Optional[str]) -> List[Any]
         query_params = '?stats' if stats else ''
-        result = self.proxy('GET', 'bucket{}'.format(query_params))
+        result = self.proxy(daemon_name, 'GET', 'bucket{}'.format(query_params))
 
         if stats:
             result = [self._append_bid(bucket) for bucket in result]
 
         return result
 
-    def get(self, bucket):
-        # type: (str) -> dict
-        result = self.proxy('GET', 'bucket', {'bucket': bucket})
+    def get(self, bucket, daemon_name=None):
+        # type: (str, Optional[str]) -> dict
+        result = self.proxy(daemon_name, 'GET', 'bucket', {'bucket': bucket})
         bucket_name = RgwBucket.get_s3_bucket_name(result['bucket'],
                                                    result['tenant'])
 
         # Append the versioning configuration.
-        versioning = self._get_versioning(result['owner'], bucket_name)
+        versioning = self._get_versioning(result['owner'], daemon_name, bucket_name)
         result['versioning'] = versioning['Status']
         result['mfa_delete'] = versioning['MfaDelete']
 
         # Append the locking configuration.
-        locking = self._get_locking(result['owner'], bucket_name)
+        locking = self._get_locking(result['owner'], daemon_name, bucket_name)
         result.update(locking)
 
         return self._append_bid(result)
@@ -261,15 +260,15 @@ class RgwBucket(RgwRESTController):
     def create(self, bucket, uid, zonegroup=None, placement_target=None,
                lock_enabled='false', lock_mode=None,
                lock_retention_period_days=None,
-               lock_retention_period_years=None):
+               lock_retention_period_years=None, daemon_name=None):
         lock_enabled = str_to_bool(lock_enabled)
         try:
-            rgw_client = RgwClient.instance(uid)
+            rgw_client = RgwClient.instance(uid, daemon_name)
             result = rgw_client.create_bucket(bucket, zonegroup,
                                               placement_target,
                                               lock_enabled)
             if lock_enabled:
-                self._set_locking(uid, bucket, lock_mode,
+                self._set_locking(uid, daemon_name, bucket, lock_mode,
                                   lock_retention_period_days,
                                   lock_retention_period_years)
             return result
@@ -280,14 +279,15 @@ class RgwBucket(RgwRESTController):
     def set(self, bucket, bucket_id, uid, versioning_state=None,
             mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None,
             lock_mode=None, lock_retention_period_days=None,
-            lock_retention_period_years=None):
+            lock_retention_period_years=None, daemon_name=None):
         # When linking a non-tenant-user owned bucket to a tenanted user, we
         # need to prefix bucket name with '/'. e.g. photos -> /photos
         if '$' in uid and '/' not in bucket:
             bucket = '/{}'.format(bucket)
 
         # Link bucket to new user:
-        result = self.proxy('PUT',
+        result = self.proxy(daemon_name,
+                            'PUT',
                             'bucket', {
                                 'bucket': bucket,
                                 'bucket-id': bucket_id,
@@ -299,20 +299,20 @@ class RgwBucket(RgwRESTController):
         bucket_name = RgwBucket.get_s3_bucket_name(bucket, uid_tenant)
 
         if versioning_state:
-            self._set_versioning(uid, bucket_name, versioning_state,
+            self._set_versioning(uid, daemon_name, bucket_name, versioning_state,
                                  mfa_delete, mfa_token_serial, mfa_token_pin)
 
         # Update locking if it is enabled.
-        locking = self._get_locking(uid, bucket_name)
+        locking = self._get_locking(uid, daemon_name, bucket_name)
         if locking['lock_enabled']:
-            self._set_locking(uid, bucket_name, lock_mode,
+            self._set_locking(uid, daemon_name, bucket_name, lock_mode,
                               lock_retention_period_days,
                               lock_retention_period_years)
 
         return self._append_bid(result)
 
-    def delete(self, bucket, purge_objects='true'):
-        return self.proxy('DELETE', 'bucket', {
+    def delete(self, bucket, purge_objects='true', daemon_name=None):
+        return self.proxy(daemon_name, 'DELETE', 'bucket', {
             'bucket': bucket,
             'purge-objects': purge_objects
         }, json_response=False)
@@ -345,15 +345,15 @@ class RgwUser(RgwRESTController):
 
     @EndpointDoc("Display RGW Users",
                  responses={200: RGW_USER_SCHEMA})
-    def list(self):
-        # type: () -> List[str]
+    def list(self, daemon_name=None):
+        # type: (Optional[str]) -> List[str]
         users = []  # type: List[str]
         marker = None
         while True:
             params = {}  # type: dict
             if marker:
                 params['marker'] = marker
-            result = self.proxy('GET', 'user?list', params)
+            result = self.proxy(daemon_name, 'GET', 'user?list', params)
             users.extend(result['keys'])
             if not result['truncated']:
                 break
@@ -364,9 +364,9 @@ class RgwUser(RgwRESTController):
             marker = result['marker']
         return users
 
-    def get(self, uid):
-        # type: (str) -> dict
-        result = self.proxy('GET', 'user', {'uid': uid})
+    def get(self, uid, daemon_name=None):
+        # type: (str, Optional[str]) -> dict
+        result = self.proxy(daemon_name, 'GET', 'user', {'uid': uid})
         if not self._keys_allowed():
             del result['keys']
             del result['swift_keys']
@@ -374,11 +374,11 @@ class RgwUser(RgwRESTController):
 
     @Endpoint()
     @ReadPermission
-    def get_emails(self):
-        # type: () -> List[str]
+    def get_emails(self, daemon_name=None):
+        # type: (Optional[str]) -> List[str]
         emails = []
-        for uid in json.loads(self.list()):  # type: ignore
-            user = json.loads(self.get(uid))  # type: ignore
+        for uid in json.loads(self.list(daemon_name)):  # type: ignore
+            user = json.loads(self.get(uid, daemon_name))  # type: ignore
             if user["email"]:
                 emails.append(user["email"])
         return emails
@@ -386,7 +386,7 @@ class RgwUser(RgwRESTController):
     @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):
+               secret_key=None, daemon_name=None):
         params = {'uid': uid}
         if display_name is not None:
             params['display-name'] = display_name
@@ -402,12 +402,12 @@ class RgwUser(RgwRESTController):
             params['access-key'] = access_key
         if secret_key is not None:
             params['secret-key'] = secret_key
-        result = self.proxy('PUT', 'user', params)
+        result = self.proxy(daemon_name, '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):
+            suspended=None, daemon_name=None):
         params = {'uid': uid}
         if display_name is not None:
             params['display-name'] = display_name
@@ -417,35 +417,35 @@ class RgwUser(RgwRESTController):
             params['max-buckets'] = max_buckets
         if suspended is not None:
             params['suspended'] = suspended
-        result = self.proxy('POST', 'user', params)
+        result = self.proxy(daemon_name, 'POST', 'user', params)
         return self._append_uid(result)
 
-    def delete(self, uid):
+    def delete(self, uid, daemon_name=None):
         try:
-            instance = RgwClient.admin_instance()
+            instance = RgwClient.admin_instance(daemon_name=daemon_name)
             # 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)
+            return self.proxy(daemon_name, 'DELETE', 'user', {'uid': uid}, json_response=False)
         except (DashboardException, RequestException) as e:  # pragma: no cover
             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', {
+    def create_cap(self, uid, type, perm, daemon_name=None):
+        return self.proxy(daemon_name, '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', {
+    def delete_cap(self, uid, type, perm, daemon_name=None):
+        return self.proxy(daemon_name, 'DELETE', 'user?caps', {
             'uid': uid,
             'user-caps': '{}={}'.format(type, perm)
         })
@@ -453,7 +453,7 @@ class RgwUser(RgwRESTController):
     @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):
+                   access_key=None, secret_key=None, daemon_name=None):
         params = {'uid': uid, 'key-type': key_type, 'generate-key': generate_key}
         if subuser is not None:
             params['subuser'] = subuser
@@ -461,25 +461,25 @@ class RgwUser(RgwRESTController):
             params['access-key'] = access_key
         if secret_key is not None:
             params['secret-key'] = secret_key
-        return self.proxy('PUT', 'user?key', params)
+        return self.proxy(daemon_name, '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):
+    def delete_key(self, uid, key_type='s3', subuser=None, access_key=None, daemon_name=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)
+        return self.proxy(daemon_name, '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})
+    def get_quota(self, uid, daemon_name=None):
+        return self.proxy(daemon_name, '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', {
+    def set_quota(self, uid, quota_type, enabled, max_size_kb, max_objects, daemon_name=None):
+        return self.proxy(daemon_name, 'PUT', 'user?quota', {
             'uid': uid,
             'quota-type': quota_type,
             'enabled': enabled,
@@ -491,8 +491,8 @@ class RgwUser(RgwRESTController):
     @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', {
+                       secret_key=None, daemon_name=None):
+        return self.proxy(daemon_name, 'PUT', 'user', {
             'uid': uid,
             'subuser': subuser,
             'key-type': key_type,
@@ -503,12 +503,12 @@ class RgwUser(RgwRESTController):
         })
 
     @RESTController.Resource(method='DELETE', path='/subuser/{subuser}', status=204)
-    def delete_subuser(self, uid, subuser, purge_keys='true'):
+    def delete_subuser(self, uid, subuser, purge_keys='true', daemon_name=None):
         """
         :param purge_keys: Set to False to do not purge the keys.
                            Note, this only works for s3 subusers.
         """
-        return self.proxy('DELETE', 'user', {
+        return self.proxy(daemon_name, 'DELETE', 'user', {
             'uid': uid,
             'subuser': subuser,
             'purge-keys': purge_keys
index cb16083ff11ea28e70f276c82e89e5abe25748e7..7d9ca9fb316a60e279347c94d49f4bfbe8c6e2a4 100644 (file)
@@ -72,11 +72,11 @@ class Settings(RESTController):
 
     def _get(self, name):
         with self._attribute_handler(name) as sname:
-            default, data_type = getattr(Options, sname)
+            setting = getattr(Options, sname)
         return {
             'name': sname,
-            'default': default,
-            'type': data_type.__name__,
+            'default': setting.default_value,
+            'type': setting.types_as_str(),
             'value': getattr(SettingsModule, sname)
         }
 
index 9911d18d8ed69e582db49c4c2aaca1a772517fca..660b9777a8c6da93b506d536f3d0bb682bfaffd2 100644 (file)
@@ -10,7 +10,7 @@ import { ToastrModule } from 'ngx-toastr';
 import { LoadingPanelComponent } from '~/app/shared/components/loading-panel/loading-panel.component';
 import { SharedModule } from '~/app/shared/shared.module';
 import { ActivatedRouteStub } from '~/testing/activated-route-stub';
-import { configureTestBed } from '~/testing/unit-test-helper';
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
 import { NFSClusterType } from '../nfs-cluster-type.enum';
 import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
 import { NfsFormComponent } from './nfs-form.component';
@@ -57,19 +57,20 @@ describe('NfsFormComponent', () => {
     httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
     httpTesting.expectOne('ui-api/nfs-ganesha/cephx/clients').flush(['admin', 'fs', 'rgw']);
     httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
-    httpTesting.expectOne('api/rgw/user').flush(['test', 'dev']);
+    RgwHelper.getCurrentDaemon();
+    httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`).flush(['test', 'dev']);
     const user_dev = {
       suspended: 0,
       user_id: 'dev',
       keys: ['a']
     };
-    httpTesting.expectOne('api/rgw/user/dev').flush(user_dev);
+    httpTesting.expectOne(`api/rgw/user/dev?${RgwHelper.DAEMON_QUERY_PARAM}`).flush(user_dev);
     const user_test = {
       suspended: 1,
       user_id: 'test',
       keys: ['a']
     };
-    httpTesting.expectOne('api/rgw/user/test').flush(user_test);
+    httpTesting.expectOne(`api/rgw/user/test?${RgwHelper.DAEMON_QUERY_PARAM}`).flush(user_test);
     httpTesting.verify();
   });
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts
new file mode 100644 (file)
index 0000000..25544a5
--- /dev/null
@@ -0,0 +1,8 @@
+export class RgwDaemon {
+  id: string;
+  version: string;
+  server_hostname: string;
+  zonegroup_name: string;
+  zone_name: string;
+  default: boolean;
+}
index 146d5dbf881b287bc08ea60ad75ce4d5d81c2741..cb8ce571335d44203285a5dace19af7efd1a557d 100644 (file)
@@ -1,5 +1,7 @@
 import { Component, OnInit } from '@angular/core';
 
+import { take } from 'rxjs/operators';
+
 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
@@ -42,6 +44,16 @@ export class RgwDaemonListComponent extends ListWithDetails implements OnInit {
         prop: 'server_hostname',
         flexGrow: 2
       },
+      {
+        name: $localize`Zone Group`,
+        prop: 'zonegroup_name',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Zone`,
+        prop: 'zone_name',
+        flexGrow: 2
+      },
       {
         name: $localize`Version`,
         prop: 'version',
@@ -55,7 +67,7 @@ export class RgwDaemonListComponent extends ListWithDetails implements OnInit {
   }
 
   getDaemonList(context: CdTableFetchDataContext) {
-    this.rgwDaemonService.list().subscribe(
+    this.rgwDaemonService.daemons$.pipe(take(1)).subscribe(
       (resp: object[]) => {
         this.daemons = resp;
       },
index 33c6e01560d260c60059ed753897342f63f30d5e..4abcd69796f32d51b3babcff07b7fa90414067a8 100644 (file)
@@ -63,9 +63,7 @@ export class RgwModule {}
 
 const routes: Routes = [
   {
-    path: '',
-    redirectTo: 'daemon',
-    pathMatch: 'full'
+    path: '' // Required for a clean reload on daemon selection.
   },
   { path: 'daemon', component: RgwDaemonListComponent, data: { breadcrumbs: 'Daemons' } },
   {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.html
new file mode 100644 (file)
index 0000000..63af29e
--- /dev/null
@@ -0,0 +1,27 @@
+<ng-container *ngIf="{ ftMap: featureToggleMap$ | async, daemons: rgwDaemonService.daemons$ | async, selectedDaemon: rgwDaemonService.selectedDaemon$ | async } as data">
+  <ng-container *ngIf="data.ftMap && data.ftMap.rgw && permissions.rgw.read && isRgwRoute && data.daemons.length > 1">
+    <div class="cd-context-bar pt-3 pb-3">
+      <span class="mr-1"
+            i18n>Selected Object Gateway:</span>
+      <div ngbDropdown
+           placement="bottom-left"
+           class="d-inline-block ml-2">
+        <button ngbDropdownToggle
+                class="btn btn-outline-info ctx-bar-selected-rgw-daemon"
+                i18n-title
+                title="Select Object Gateway">
+          {{ data.selectedDaemon.id }} ( {{ data.selectedDaemon.zonegroup_name }} )
+        </button>
+        <div ngbDropdownMenu>
+          <ng-container *ngFor="let daemon of data.daemons">
+            <button ngbDropdownItem
+                    class="ctx-bar-available-rgw-daemon"
+                    (click)="onDaemonSelection(daemon)">
+              {{ daemon.id }} ( {{ daemon.zonegroup_name }} )
+            </button>
+          </ng-container>
+        </div>
+      </div>
+    </div>
+  </ng-container>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.scss
new file mode 100644 (file)
index 0000000..0cd44f1
--- /dev/null
@@ -0,0 +1,5 @@
+@use './src/styles/vendor/variables' as vv;
+
+.cd-context-bar {
+  border-bottom: 1px solid vv.$gray-300;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.spec.ts
new file mode 100644 (file)
index 0000000..adffb6f
--- /dev/null
@@ -0,0 +1,106 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { of } from 'rxjs';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+  FeatureTogglesMap,
+  FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ContextComponent } from './context.component';
+
+describe('ContextComponent', () => {
+  let component: ContextComponent;
+  let fixture: ComponentFixture<ContextComponent>;
+  let router: Router;
+  let routerNavigateByUrlSpy: jasmine.Spy;
+  let routerNavigateSpy: jasmine.Spy;
+  let getPermissionsSpy: jasmine.Spy;
+  let getFeatureTogglesSpy: jasmine.Spy;
+  let ftMap: FeatureTogglesMap;
+  let httpTesting: HttpTestingController;
+
+  const getDaemonList = () => {
+    const daemonList: RgwDaemon[] = [];
+    for (let daemonIndex = 1; daemonIndex <= 3; daemonIndex++) {
+      const rgwDaemon = new RgwDaemon();
+      rgwDaemon.id = `daemon${daemonIndex}`;
+      rgwDaemon.default = daemonIndex === 2;
+      rgwDaemon.zonegroup_name = `zonegroup${daemonIndex}`;
+      daemonList.push(rgwDaemon);
+    }
+    return daemonList;
+  };
+
+  configureTestBed({
+    declarations: [ContextComponent],
+    imports: [HttpClientTestingModule, RouterTestingModule]
+  });
+
+  beforeEach(() => {
+    httpTesting = TestBed.inject(HttpTestingController);
+    router = TestBed.inject(Router);
+    routerNavigateByUrlSpy = spyOn(router, 'navigateByUrl');
+    routerNavigateByUrlSpy.and.returnValue(Promise.resolve(undefined));
+    routerNavigateSpy = spyOn(router, 'navigate');
+    getPermissionsSpy = spyOn(TestBed.inject(AuthStorageService), 'getPermissions');
+    getPermissionsSpy.and.returnValue(
+      new Permissions({ rgw: ['read', 'update', 'create', 'delete'] })
+    );
+    getFeatureTogglesSpy = spyOn(TestBed.inject(FeatureTogglesService), 'get');
+    ftMap = new FeatureTogglesMap();
+    ftMap.rgw = true;
+    getFeatureTogglesSpy.and.returnValue(of(ftMap));
+    fixture = TestBed.createComponent(ContextComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+    const req = httpTesting.expectOne('api/rgw/daemon');
+    req.flush(getDaemonList());
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should not show any info if not in RGW route', () => {
+    component.isRgwRoute = false;
+    expect(fixture.debugElement.nativeElement.textContent).toEqual('');
+  });
+
+  it('should select the default daemon', fakeAsync(() => {
+    component.isRgwRoute = true;
+    tick();
+    fixture.detectChanges();
+    const selectedDaemon = fixture.debugElement.nativeElement.querySelector(
+      '.ctx-bar-selected-rgw-daemon'
+    );
+    expect(selectedDaemon.textContent).toEqual(' daemon2 ( zonegroup2 ) ');
+
+    const availableDaemons = fixture.debugElement.nativeElement.querySelectorAll(
+      '.ctx-bar-available-rgw-daemon'
+    );
+    expect(availableDaemons.length).toEqual(getDaemonList().length);
+    expect(availableDaemons[0].textContent).toEqual(' daemon1 ( zonegroup1 ) ');
+  }));
+
+  it('should select the chosen daemon', fakeAsync(() => {
+    component.isRgwRoute = true;
+    component.onDaemonSelection(getDaemonList()[2]);
+    tick();
+    fixture.detectChanges();
+
+    expect(routerNavigateByUrlSpy).toHaveBeenCalledTimes(1);
+    expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
+
+    const selectedDaemon = fixture.debugElement.nativeElement.querySelector(
+      '.ctx-bar-selected-rgw-daemon'
+    );
+    expect(selectedDaemon.textContent).toEqual(' daemon3 ( zonegroup3 ) ');
+  }));
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts
new file mode 100644 (file)
index 0000000..5d1d1e3
--- /dev/null
@@ -0,0 +1,74 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Event, NavigationEnd, Router } from '@angular/router';
+
+import { NEVER, Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+  FeatureTogglesMap$,
+  FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { TimerService } from '~/app/shared/services/timer.service';
+
+@Component({
+  selector: 'cd-context',
+  templateUrl: './context.component.html',
+  styleUrls: ['./context.component.scss']
+})
+export class ContextComponent implements OnInit, OnDestroy {
+  readonly REFRESH_INTERVAL = 5000;
+  private subs = new Subscription();
+  private rgwUrlPrefix = '/rgw';
+  permissions: Permissions;
+  featureToggleMap$: FeatureTogglesMap$;
+  isRgwRoute = document.location.href.includes(this.rgwUrlPrefix);
+
+  constructor(
+    private authStorageService: AuthStorageService,
+    private featureToggles: FeatureTogglesService,
+    private router: Router,
+    private timerService: TimerService,
+    public rgwDaemonService: RgwDaemonService
+  ) {}
+
+  ngOnInit() {
+    this.permissions = this.authStorageService.getPermissions();
+    this.featureToggleMap$ = this.featureToggles.get();
+    // Check if route belongs to RGW:
+    this.subs.add(
+      this.router.events
+        .pipe(filter((event: Event) => event instanceof NavigationEnd))
+        .subscribe(() => (this.isRgwRoute = this.router.url.includes(this.rgwUrlPrefix)))
+    );
+    // Select default daemon:
+    this.rgwDaemonService.list().subscribe((daemons: RgwDaemon[]) => {
+      this.rgwDaemonService.selectDefaultDaemon(daemons);
+    });
+    // Set daemon list polling only when in RGW route:
+    this.subs.add(
+      this.timerService
+        .get(() => (this.isRgwRoute ? this.rgwDaemonService.list() : NEVER), this.REFRESH_INTERVAL)
+        .subscribe()
+    );
+  }
+
+  ngOnDestroy() {
+    this.subs.unsubscribe();
+  }
+
+  onDaemonSelection(daemon: RgwDaemon) {
+    this.rgwDaemonService.selectDaemon(daemon);
+    this.reloadData();
+  }
+
+  private reloadData() {
+    const currentUrl = this.router.url;
+    this.router.navigateByUrl(this.rgwUrlPrefix, { skipLocationChange: true }).finally(() => {
+      this.router.navigate([currentUrl]);
+    });
+  }
+}
index 0a5acf317a3a46d7397735161ee78d68c68011eb..005c8277877bc04b469788a1c10e957180b52e26 100644 (file)
@@ -2,9 +2,11 @@ import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
 import { RouterModule } from '@angular/router';
 
+import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
 import { BlockUIModule } from 'ng-block-ui';
 
-import { SharedModule } from '../shared/shared.module';
+import { ContextComponent } from '~/app/core/context/context.component';
+import { SharedModule } from '~/app/shared/shared.module';
 import { ErrorComponent } from './error/error.component';
 import { BlankLayoutComponent } from './layouts/blank-layout/blank-layout.component';
 import { LoginLayoutComponent } from './layouts/login-layout/login-layout.component';
@@ -12,9 +14,17 @@ import { WorkbenchLayoutComponent } from './layouts/workbench-layout/workbench-l
 import { NavigationModule } from './navigation/navigation.module';
 
 @NgModule({
-  imports: [BlockUIModule.forRoot(), CommonModule, NavigationModule, RouterModule, SharedModule],
+  imports: [
+    BlockUIModule.forRoot(),
+    CommonModule,
+    NavigationModule,
+    NgbDropdownModule,
+    RouterModule,
+    SharedModule
+  ],
   exports: [NavigationModule],
   declarations: [
+    ContextComponent,
     WorkbenchLayoutComponent,
     BlankLayoutComponent,
     LoginLayoutComponent,
index ae81cdee71d42020f36189228ca0ff7deb3d17b4..3979ad7a4a95e20cdcc26b3145fc7dfd7b7535bf 100644 (file)
@@ -2,6 +2,7 @@
   <cd-navigation>
     <div class="container-fluid h-100"
          [ngClass]="{'dashboard':isDashboardPage()} ">
+      <cd-context></cd-context>
       <cd-breadcrumbs></cd-breadcrumbs>
       <router-outlet></router-outlet>
     </div>
index aa33df3888d3daac5ce9501beee14989a7ef28b4..e0787d06a92ea89bc9db53e66b67433d4a4a2d0a 100644 (file)
@@ -1,7 +1,7 @@
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { TestBed } from '@angular/core/testing';
 
-import { configureTestBed } from '~/testing/unit-test-helper';
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
 import { RgwBucketService } from './rgw-bucket.service';
 
 describe('RgwBucketService', () => {
@@ -28,13 +28,15 @@ describe('RgwBucketService', () => {
 
   it('should call list', () => {
     service.list().subscribe();
-    const req = httpTesting.expectOne('api/rgw/bucket?stats=true');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/bucket?${RgwHelper.DAEMON_QUERY_PARAM}&stats=true`);
     expect(req.request.method).toBe('GET');
   });
 
   it('should call get', () => {
     service.get('foo').subscribe();
-    const req = httpTesting.expectOne('api/rgw/bucket/foo');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
   });
 
@@ -42,8 +44,9 @@ describe('RgwBucketService', () => {
     service
       .create('foo', 'bar', 'default', 'default-placement', false, 'COMPLIANCE', '10', '0')
       .subscribe();
+    RgwHelper.getCurrentDaemon();
     const req = httpTesting.expectOne(
-      'api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=10&lock_retention_period_years=0'
+      `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=10&lock_retention_period_years=0&${RgwHelper.DAEMON_QUERY_PARAM}`
     );
     expect(req.request.method).toBe('POST');
   });
@@ -52,21 +55,28 @@ describe('RgwBucketService', () => {
     service
       .update('foo', 'bar', 'baz', 'Enabled', 'Enabled', '1', '223344', 'GOVERNANCE', '0', '1')
       .subscribe();
+    RgwHelper.getCurrentDaemon();
     const req = httpTesting.expectOne(
-      'api/rgw/bucket/foo?bucket_id=bar&uid=baz&versioning_state=Enabled&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=0&lock_retention_period_years=1'
+      `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=0&lock_retention_period_years=1`
     );
     expect(req.request.method).toBe('PUT');
   });
 
   it('should call delete, with purgeObjects = true', () => {
     service.delete('foo').subscribe();
-    const req = httpTesting.expectOne('api/rgw/bucket/foo?purge_objects=true');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&purge_objects=true`
+    );
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call delete, with purgeObjects = false', () => {
     service.delete('foo', false).subscribe();
-    const req = httpTesting.expectOne('api/rgw/bucket/foo?purge_objects=false');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&purge_objects=false`
+    );
     expect(req.request.method).toBe('DELETE');
   });
 
@@ -75,7 +85,8 @@ describe('RgwBucketService', () => {
     service.exists('foo').subscribe((resp) => {
       result = resp;
     });
-    const req = httpTesting.expectOne('api/rgw/bucket');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/bucket?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
     req.flush(['foo', 'bar']);
     expect(result).toBe(true);
index baeaa3dde4837cfdbf68c5cdf70b575495e0e7c6..1d622518bba905978e9e62566a984f9acb1a0e8e 100644 (file)
@@ -5,7 +5,8 @@ import _ from 'lodash';
 import { of as observableOf } from 'rxjs';
 import { mergeMap } from 'rxjs/operators';
 
-import { cdEncode } from '../decorators/cd-encode';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
 
 @cdEncode
 @Injectable({
@@ -14,28 +15,33 @@ import { cdEncode } from '../decorators/cd-encode';
 export class RgwBucketService {
   private url = 'api/rgw/bucket';
 
-  constructor(private http: HttpClient) {}
+  constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
 
   /**
    * Get the list of buckets.
-   * @return {Observable<Object[]>}
+   * @return Observable<Object[]>
    */
   list() {
-    let params = new HttpParams();
-    params = params.append('stats', 'true');
-    return this.http.get(this.url, { params: params });
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.append('stats', 'true');
+      return this.http.get(this.url, { params: params });
+    });
   }
 
   /**
    * Get the list of bucket names.
-   * @return {Observable<string[]>}
+   * @return Observable<string[]>
    */
   enumerate() {
-    return this.http.get(this.url);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(this.url, { params: params });
+    });
   }
 
   get(bucket: string) {
-    return this.http.get(`${this.url}/${bucket}`);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(`${this.url}/${bucket}`, { params: params });
+    });
   }
 
   create(
@@ -48,19 +54,22 @@ export class RgwBucketService {
     lock_retention_period_days: string,
     lock_retention_period_years: string
   ) {
-    return this.http.post(this.url, null, {
-      params: new HttpParams({
-        fromObject: {
-          bucket,
-          uid,
-          zonegroup,
-          placement_target: placementTarget,
-          lock_enabled: String(lockEnabled),
-          lock_mode,
-          lock_retention_period_days,
-          lock_retention_period_years
-        }
-      })
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.post(this.url, null, {
+        params: new HttpParams({
+          fromObject: {
+            bucket,
+            uid,
+            zonegroup,
+            placement_target: placementTarget,
+            lock_enabled: String(lockEnabled),
+            lock_mode,
+            lock_retention_period_days,
+            lock_retention_period_years,
+            daemon_name: params.get('daemon_name')
+          }
+        })
+      });
     });
   }
 
@@ -76,29 +85,31 @@ export class RgwBucketService {
     lockRetentionPeriodDays: string,
     lockRetentionPeriodYears: string
   ) {
-    let params = new HttpParams();
-    params = params.append('bucket_id', bucketId);
-    params = params.append('uid', uid);
-    params = params.append('versioning_state', versioningState);
-    params = params.append('mfa_delete', mfaDelete);
-    params = params.append('mfa_token_serial', mfaTokenSerial);
-    params = params.append('mfa_token_pin', mfaTokenPin);
-    params = params.append('lock_mode', lockMode);
-    params = params.append('lock_retention_period_days', lockRetentionPeriodDays);
-    params = params.append('lock_retention_period_years', lockRetentionPeriodYears);
-    return this.http.put(`${this.url}/${bucket}`, null, { params: params });
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.append('bucket_id', bucketId);
+      params = params.append('uid', uid);
+      params = params.append('versioning_state', versioningState);
+      params = params.append('mfa_delete', mfaDelete);
+      params = params.append('mfa_token_serial', mfaTokenSerial);
+      params = params.append('mfa_token_pin', mfaTokenPin);
+      params = params.append('lock_mode', lockMode);
+      params = params.append('lock_retention_period_days', lockRetentionPeriodDays);
+      params = params.append('lock_retention_period_years', lockRetentionPeriodYears);
+      return this.http.put(`${this.url}/${bucket}`, null, { params: params });
+    });
   }
 
   delete(bucket: string, purgeObjects = true) {
-    let params = new HttpParams();
-    params = params.append('purge_objects', purgeObjects ? 'true' : 'false');
-    return this.http.delete(`${this.url}/${bucket}`, { params: params });
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.append('purge_objects', purgeObjects ? 'true' : 'false');
+      return this.http.delete(`${this.url}/${bucket}`, { params: params });
+    });
   }
 
   /**
    * Check if the specified bucket exists.
    * @param {string} bucket The bucket name to check.
-   * @return {Observable<boolean>}
+   * @return Observable<boolean>
    */
   exists(bucket: string) {
     return this.enumerate().pipe(
index ae2a5d697595e23edde4eb15930408e7f22c8417..67244e7de0e96d5ecaff65f1184b208709f4dfe1 100644 (file)
@@ -1,7 +1,11 @@
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
-import { cdEncode } from '../decorators/cd-encode';
+import { Observable, ReplaySubject } from 'rxjs';
+import { mergeMap, take, tap } from 'rxjs/operators';
+
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
 
 @cdEncode
 @Injectable({
@@ -9,14 +13,48 @@ import { cdEncode } from '../decorators/cd-encode';
 })
 export class RgwDaemonService {
   private url = 'api/rgw/daemon';
+  private daemons = new ReplaySubject<RgwDaemon[]>(1);
+  daemons$ = this.daemons.asObservable();
+  private selectedDaemon = new ReplaySubject<RgwDaemon>(1);
+  selectedDaemon$ = this.selectedDaemon.asObservable();
 
   constructor(private http: HttpClient) {}
 
-  list() {
-    return this.http.get(this.url);
+  list(): Observable<RgwDaemon[]> {
+    return this.http.get<RgwDaemon[]>(this.url).pipe(
+      tap((daemons: RgwDaemon[]) => {
+        this.daemons.next(daemons);
+      })
+    );
   }
 
   get(id: string) {
     return this.http.get(`${this.url}/${id}`);
   }
+
+  selectDaemon(daemon: RgwDaemon) {
+    this.selectedDaemon.next(daemon);
+  }
+
+  selectDefaultDaemon(daemons: RgwDaemon[]): RgwDaemon {
+    for (const daemon of daemons) {
+      if (daemon.default) {
+        this.selectDaemon(daemon);
+        return daemon;
+      }
+    }
+
+    throw new Error('No default RGW daemon found.');
+  }
+
+  request(next: (params: HttpParams) => Observable<any>) {
+    return this.selectedDaemon.pipe(
+      take(1),
+      mergeMap((daemon: RgwDaemon) => {
+        let params = new HttpParams();
+        params = params.append('daemon_name', daemon.id);
+        return next(params);
+      })
+    );
+  }
 }
index 5dcdbf4cf79886bf968564b076ee5b531d42b05f..d11501b35114f291e7f2ddb5423849c28bfd9b30 100644 (file)
@@ -1,7 +1,7 @@
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { TestBed } from '@angular/core/testing';
 
-import { configureTestBed } from '~/testing/unit-test-helper';
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
 import { RgwSiteService } from './rgw-site.service';
 
 describe('RgwSiteService', () => {
@@ -28,13 +28,17 @@ describe('RgwSiteService', () => {
 
   it('should contain site endpoint in GET request', () => {
     service.get().subscribe();
-    const req = httpTesting.expectOne(service['url']);
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`${service['url']}?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
   });
 
   it('should add query param in GET request', () => {
     const query = 'placement-targets';
     service.get(query).subscribe();
-    httpTesting.expectOne(`${service['url']}?query=placement-targets`);
+    RgwHelper.getCurrentDaemon();
+    httpTesting.expectOne(
+      `${service['url']}?${RgwHelper.DAEMON_QUERY_PARAM}&query=placement-targets`
+    );
   });
 });
index 8c761668831ff78bb4bb74f02e9e5943aa161f30..545179dcf1abe44f7ccc7d8ebc90fb237e89226e 100644 (file)
@@ -1,7 +1,8 @@
 import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
-import { cdEncode } from '../decorators/cd-encode';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
 
 @cdEncode
 @Injectable({
@@ -10,14 +11,14 @@ import { cdEncode } from '../decorators/cd-encode';
 export class RgwSiteService {
   private url = 'api/rgw/site';
 
-  constructor(private http: HttpClient) {}
+  constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
 
   get(query?: string) {
-    let params = new HttpParams();
-    if (query) {
-      params = params.append('query', query);
-    }
-
-    return this.http.get(this.url, { params: params });
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      if (query) {
+        params = params.append('query', query);
+      }
+      return this.http.get(this.url, { params: params });
+    });
   }
 }
index c2954eac585a2ef962c18b5bf659794afb995eb1..e85fe9d11381fd689b676c33d5aa3a82940f7a92 100644 (file)
@@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing';
 
 import { of as observableOf, throwError } from 'rxjs';
 
-import { configureTestBed } from '~/testing/unit-test-helper';
+import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper';
 import { RgwUserService } from './rgw-user.service';
 
 describe('RgwUserService', () => {
@@ -33,7 +33,8 @@ describe('RgwUserService', () => {
     service.list().subscribe((resp) => {
       result = resp;
     });
-    const req = httpTesting.expectOne('api/rgw/user');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
     req.flush([]);
     expect(result).toEqual([]);
@@ -44,16 +45,16 @@ describe('RgwUserService', () => {
     service.list().subscribe((resp) => {
       result = resp;
     });
-
-    let req = httpTesting.expectOne('api/rgw/user');
+    RgwHelper.getCurrentDaemon();
+    let req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
     req.flush(['foo', 'bar']);
 
-    req = httpTesting.expectOne('api/rgw/user/foo');
+    req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
     req.flush({ name: 'foo' });
 
-    req = httpTesting.expectOne('api/rgw/user/bar');
+    req = httpTesting.expectOne(`api/rgw/user/bar?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
     req.flush({ name: 'bar' });
 
@@ -62,79 +63,106 @@ describe('RgwUserService', () => {
 
   it('should call enumerate', () => {
     service.enumerate().subscribe();
-    const req = httpTesting.expectOne('api/rgw/user');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
   });
 
   it('should call get', () => {
     service.get('foo').subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
   });
 
   it('should call getQuota', () => {
     service.getQuota('foo').subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/quota');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/user/foo/quota?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('GET');
   });
 
   it('should call update', () => {
     service.update('foo', { xxx: 'yyy' }).subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo?xxx=yyy');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}&xxx=yyy`);
     expect(req.request.method).toBe('PUT');
   });
 
   it('should call updateQuota', () => {
     service.updateQuota('foo', { xxx: 'yyy' }).subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/quota?xxx=yyy');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/user/foo/quota?${RgwHelper.DAEMON_QUERY_PARAM}&xxx=yyy`
+    );
     expect(req.request.method).toBe('PUT');
   });
 
   it('should call create', () => {
     service.create({ foo: 'bar' }).subscribe();
-    const req = httpTesting.expectOne('api/rgw/user?foo=bar');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/user?${RgwHelper.DAEMON_QUERY_PARAM}&foo=bar`);
     expect(req.request.method).toBe('POST');
   });
 
   it('should call delete', () => {
     service.delete('foo').subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(`api/rgw/user/foo?${RgwHelper.DAEMON_QUERY_PARAM}`);
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call createSubuser', () => {
     service.createSubuser('foo', { xxx: 'yyy' }).subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/subuser?xxx=yyy');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/user/foo/subuser?${RgwHelper.DAEMON_QUERY_PARAM}&xxx=yyy`
+    );
     expect(req.request.method).toBe('POST');
   });
 
   it('should call deleteSubuser', () => {
     service.deleteSubuser('foo', 'bar').subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/subuser/bar');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/user/foo/subuser/bar?${RgwHelper.DAEMON_QUERY_PARAM}`
+    );
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call addCapability', () => {
     service.addCapability('foo', 'bar', 'baz').subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/capability?type=bar&perm=baz');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/user/foo/capability?${RgwHelper.DAEMON_QUERY_PARAM}&type=bar&perm=baz`
+    );
     expect(req.request.method).toBe('POST');
   });
 
   it('should call deleteCapability', () => {
     service.deleteCapability('foo', 'bar', 'baz').subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/capability?type=bar&perm=baz');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/user/foo/capability?${RgwHelper.DAEMON_QUERY_PARAM}&type=bar&perm=baz`
+    );
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call addS3Key', () => {
     service.addS3Key('foo', { xxx: 'yyy' }).subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/key?key_type=s3&xxx=yyy');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/user/foo/key?${RgwHelper.DAEMON_QUERY_PARAM}&key_type=s3&xxx=yyy`
+    );
     expect(req.request.method).toBe('POST');
   });
 
   it('should call deleteS3Key', () => {
     service.deleteS3Key('foo', 'bar').subscribe();
-    const req = httpTesting.expectOne('api/rgw/user/foo/key?key_type=s3&access_key=bar');
+    RgwHelper.getCurrentDaemon();
+    const req = httpTesting.expectOne(
+      `api/rgw/user/foo/key?${RgwHelper.DAEMON_QUERY_PARAM}&key_type=s3&access_key=bar`
+    );
     expect(req.request.method).toBe('DELETE');
   });
 
index f322a04fffd22af330b7f2f01e0bd4f909a2ec1a..66167bcabbd0777ff89778b0b04d778f99ed89ae 100644 (file)
@@ -5,7 +5,8 @@ import _ from 'lodash';
 import { forkJoin as observableForkJoin, Observable, of as observableOf } from 'rxjs';
 import { catchError, mapTo, mergeMap } from 'rxjs/operators';
 
-import { cdEncode } from '../decorators/cd-encode';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { cdEncode } from '~/app/shared/decorators/cd-encode';
 
 @cdEncode
 @Injectable({
@@ -14,7 +15,7 @@ import { cdEncode } from '../decorators/cd-encode';
 export class RgwUserService {
   private url = 'api/rgw/user';
 
-  constructor(private http: HttpClient) {}
+  constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
 
   /**
    * Get the list of users.
@@ -40,89 +41,109 @@ export class RgwUserService {
    * @return {Observable<string[]>}
    */
   enumerate() {
-    return this.http.get(this.url);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(this.url, { params: params });
+    });
   }
 
   enumerateEmail() {
-    return this.http.get(`${this.url}/get_emails`);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(`${this.url}/get_emails`, { params: params });
+    });
   }
 
   get(uid: string) {
-    return this.http.get(`${this.url}/${uid}`);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(`${this.url}/${uid}`, { params: params });
+    });
   }
 
   getQuota(uid: string) {
-    return this.http.get(`${this.url}/${uid}/quota`);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.get(`${this.url}/${uid}/quota`, { params: params });
+    });
   }
 
   create(args: Record<string, any>) {
-    let params = new HttpParams();
-    _.keys(args).forEach((key) => {
-      params = params.append(key, args[key]);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      _.keys(args).forEach((key) => {
+        params = params.append(key, args[key]);
+      });
+      return this.http.post(this.url, null, { params: params });
     });
-    return this.http.post(this.url, null, { params: params });
   }
 
   update(uid: string, args: Record<string, any>) {
-    let params = new HttpParams();
-    _.keys(args).forEach((key) => {
-      params = params.append(key, args[key]);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      _.keys(args).forEach((key) => {
+        params = params.append(key, args[key]);
+      });
+      return this.http.put(`${this.url}/${uid}`, null, { params: params });
     });
-    return this.http.put(`${this.url}/${uid}`, null, { params: params });
   }
 
   updateQuota(uid: string, args: Record<string, string>) {
-    let params = new HttpParams();
-    _.keys(args).forEach((key) => {
-      params = params.append(key, args[key]);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      _.keys(args).forEach((key) => {
+        params = params.append(key, args[key]);
+      });
+      return this.http.put(`${this.url}/${uid}/quota`, null, { params: params });
     });
-    return this.http.put(`${this.url}/${uid}/quota`, null, { params: params });
   }
 
   delete(uid: string) {
-    return this.http.delete(`${this.url}/${uid}`);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.delete(`${this.url}/${uid}`, { params: params });
+    });
   }
 
   createSubuser(uid: string, args: Record<string, string>) {
-    let params = new HttpParams();
-    _.keys(args).forEach((key) => {
-      params = params.append(key, args[key]);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      _.keys(args).forEach((key) => {
+        params = params.append(key, args[key]);
+      });
+      return this.http.post(`${this.url}/${uid}/subuser`, null, { params: params });
     });
-    return this.http.post(`${this.url}/${uid}/subuser`, null, { params: params });
   }
 
   deleteSubuser(uid: string, subuser: string) {
-    return this.http.delete(`${this.url}/${uid}/subuser/${subuser}`);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      return this.http.delete(`${this.url}/${uid}/subuser/${subuser}`, { params: params });
+    });
   }
 
   addCapability(uid: string, type: string, perm: string) {
-    let params = new HttpParams();
-    params = params.append('type', type);
-    params = params.append('perm', perm);
-    return this.http.post(`${this.url}/${uid}/capability`, null, { params: params });
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.append('type', type);
+      params = params.append('perm', perm);
+      return this.http.post(`${this.url}/${uid}/capability`, null, { params: params });
+    });
   }
 
   deleteCapability(uid: string, type: string, perm: string) {
-    let params = new HttpParams();
-    params = params.append('type', type);
-    params = params.append('perm', perm);
-    return this.http.delete(`${this.url}/${uid}/capability`, { params: params });
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.append('type', type);
+      params = params.append('perm', perm);
+      return this.http.delete(`${this.url}/${uid}/capability`, { params: params });
+    });
   }
 
   addS3Key(uid: string, args: Record<string, string>) {
-    let params = new HttpParams();
-    params = params.append('key_type', 's3');
-    _.keys(args).forEach((key) => {
-      params = params.append(key, args[key]);
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.append('key_type', 's3');
+      _.keys(args).forEach((key) => {
+        params = params.append(key, args[key]);
+      });
+      return this.http.post(`${this.url}/${uid}/key`, null, { params: params });
     });
-    return this.http.post(`${this.url}/${uid}/key`, null, { params: params });
   }
 
   deleteS3Key(uid: string, accessKey: string) {
-    let params = new HttpParams();
-    params = params.append('key_type', 's3');
-    params = params.append('access_key', accessKey);
-    return this.http.delete(`${this.url}/${uid}/key`, { params: params });
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.append('key_type', 's3');
+      params = params.append('access_key', accessKey);
+      return this.http.delete(`${this.url}/${uid}/key`, { params: params });
+    });
   }
 
   /**
index 3e3d27a7ce64158096840804abc58c3e46ec8643..c45f480a700e648b69be7b0db1ad3778d6957983 100644 (file)
@@ -9,24 +9,26 @@ import _ from 'lodash';
 import { configureTestSuite } from 'ng-bullet';
 import { of } from 'rxjs';
 
-import { InventoryDevice } from '../app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
-import { Pool } from '../app/ceph/pool/pool';
-import { OrchestratorService } from '../app/shared/api/orchestrator.service';
-import { TableActionsComponent } from '../app/shared/datatable/table-actions/table-actions.component';
-import { Icons } from '../app/shared/enum/icons.enum';
-import { CdFormGroup } from '../app/shared/forms/cd-form-group';
-import { CdTableAction } from '../app/shared/models/cd-table-action';
-import { CdTableSelection } from '../app/shared/models/cd-table-selection';
-import { CrushNode } from '../app/shared/models/crush-node';
-import { CrushRule, CrushRuleConfig } from '../app/shared/models/crush-rule';
-import { OrchestratorFeature } from '../app/shared/models/orchestrator.enum';
-import { Permission } from '../app/shared/models/permissions';
+import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
+import { Pool } from '~/app/ceph/pool/pool';
+import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CrushNode } from '~/app/shared/models/crush-node';
+import { CrushRule, CrushRuleConfig } from '~/app/shared/models/crush-rule';
+import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
+import { Permission } from '~/app/shared/models/permissions';
 import {
   AlertmanagerAlert,
   AlertmanagerNotification,
   AlertmanagerNotificationAlert,
   PrometheusRule
-} from '../app/shared/models/prometheus-alerts';
+} from '~/app/shared/models/prometheus-alerts';
 
 export function configureTestBed(configuration: any, entryComponents?: any) {
   configureTestSuite(() => {
@@ -385,6 +387,19 @@ export class IscsiHelper {
   }
 }
 
+export class RgwHelper {
+  static readonly DAEMON_NAME = 'daemon1';
+  static readonly DAEMON_QUERY_PARAM = `daemon_name=${RgwHelper.DAEMON_NAME}`;
+
+  static getCurrentDaemon() {
+    const rgwDaemon = new RgwDaemon();
+    rgwDaemon.id = this.DAEMON_NAME;
+    rgwDaemon.default = true;
+    const service = TestBed.inject(RgwDaemonService);
+    service.selectDaemon(rgwDaemon);
+  }
+}
+
 export class Mocks {
   static getCrushNode(
     name: string,
index cd2a570a8be2eec55375450fc42fc77cc66c6784..eba7eac1356c081f515b04756596bc11d8283c8a 100644 (file)
@@ -7404,6 +7404,11 @@ paths:
         name: stats
         schema:
           type: boolean
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -7432,6 +7437,8 @@ paths:
               properties:
                 bucket:
                   type: string
+                daemon_name:
+                  type: string
                 lock_enabled:
                   default: 'false'
                   type: string
@@ -7488,6 +7495,11 @@ paths:
         name: purge_objects
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '202':
           content:
@@ -7519,6 +7531,11 @@ paths:
         required: true
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -7552,6 +7569,8 @@ paths:
               properties:
                 bucket_id:
                   type: string
+                daemon_name:
+                  type: string
                 lock_mode:
                   type: string
                 lock_retention_period_days:
@@ -7615,11 +7634,19 @@ paths:
                     version:
                       description: Ceph Version
                       type: string
+                    zone_name:
+                      description: Zone
+                      type: string
+                    zonegroup_name:
+                      description: Zone Group
+                      type: string
                   type: object
                 required:
                 - id
                 - version
                 - server_hostname
+                - zonegroup_name
+                - zone_name
                 type: array
           description: OK
         '400':
@@ -7671,6 +7698,11 @@ paths:
         name: query
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -7726,7 +7758,12 @@ paths:
       - Rgw
   /api/rgw/user:
     get:
-      parameters: []
+      parameters:
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -7765,6 +7802,8 @@ paths:
               properties:
                 access_key:
                   type: string
+                daemon_name:
+                  type: string
                 display_name:
                   type: string
                 email:
@@ -7809,7 +7848,12 @@ paths:
       - RgwUser
   /api/rgw/user/get_emails:
     get:
-      parameters: []
+      parameters:
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -7837,6 +7881,11 @@ paths:
         required: true
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '202':
           content:
@@ -7868,6 +7917,11 @@ paths:
         required: true
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -7899,6 +7953,8 @@ paths:
           application/json:
             schema:
               properties:
+                daemon_name:
+                  type: string
                 display_name:
                   type: string
                 email:
@@ -7950,6 +8006,11 @@ paths:
         required: true
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '202':
           content:
@@ -7986,6 +8047,8 @@ paths:
           application/json:
             schema:
               properties:
+                daemon_name:
+                  type: string
                 perm:
                   type: string
                 type:
@@ -8041,6 +8104,11 @@ paths:
         name: access_key
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '202':
           content:
@@ -8079,6 +8147,8 @@ paths:
               properties:
                 access_key:
                   type: string
+                daemon_name:
+                  type: string
                 generate_key:
                   default: 'true'
                   type: string
@@ -8122,6 +8192,11 @@ paths:
         required: true
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -8153,6 +8228,8 @@ paths:
           application/json:
             schema:
               properties:
+                daemon_name:
+                  type: string
                 enabled:
                   type: string
                 max_objects:
@@ -8208,6 +8285,8 @@ paths:
                   type: string
                 access_key:
                   type: string
+                daemon_name:
+                  type: string
                 generate_secret:
                   default: 'true'
                   type: string
@@ -8267,6 +8346,11 @@ paths:
         name: purge_keys
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
       responses:
         '202':
           content:
index 54b26a7a3a1a3a9106464a0302794593f6d9f9f5..4c3e2821c9c5c4cb3b3be6cda9167e965613b0be 100644 (file)
@@ -11,7 +11,7 @@ from .. import mgr
 from ..awsauth import S3Auth
 from ..exceptions import DashboardException
 from ..rest_client import RequestException, RestClient
-from ..settings import Options, Settings
+from ..settings import Settings
 from ..tools import build_url, dict_contains_path, dict_get, json_str_to_object
 
 try:
@@ -30,7 +30,16 @@ class NoCredentialsException(RequestException):
             'the dashboard.')
 
 
-def _get_daemon_info() -> Dict[str, Any]:
+class RgwDaemon:
+    """Simple representation of a daemon."""
+    host: str
+    name: str
+    port: int
+    ssl: bool
+    zonegroup_name: str
+
+
+def _get_daemons() -> Dict[str, RgwDaemon]:
     """
     Retrieve RGW daemon info from MGR.
     Note, the service id of the RGW daemons may differ depending on the setup.
@@ -76,30 +85,31 @@ def _get_daemon_info() -> Dict[str, Any]:
     service_map = mgr.get('service_map')
     if not dict_contains_path(service_map, ['services', 'rgw', 'daemons']):
         raise LookupError('No RGW found')
-    daemon = None
-    daemons = service_map['services']['rgw']['daemons']
-    for key in daemons.keys():
-        if dict_contains_path(daemons[key], ['metadata', 'frontend_config#0']):
-            daemon = daemons[key]
-            break
-    if daemon is None:
+    daemons = {}
+    daemon_map = service_map['services']['rgw']['daemons']
+    for key in daemon_map.keys():
+        if dict_contains_path(daemon_map[key], ['metadata', 'frontend_config#0']):
+            daemon = _determine_rgw_addr(daemon_map[key])
+            daemon.name = key
+            daemon.zonegroup_name = daemon_map[key]['metadata']['zonegroup_name']
+            daemons[daemon.name] = daemon
+            logger.info('Found RGW daemon with configuration: host=%s, port=%d, ssl=%s',
+                        daemon.host, daemon.port, str(daemon.ssl))
+    if not daemons:
         raise LookupError('No RGW daemon found')
 
-    return daemon
+    return daemons
 
 
-def _determine_rgw_addr() -> Tuple[str, int, bool]:
+def _determine_rgw_addr(daemon_info: Dict[str, Any]) -> RgwDaemon:
     """
     Parse RGW daemon info to determine the configured host (IP address) and port.
     """
-    daemon = _get_daemon_info()
-    addr = _parse_addr(daemon['addr'])
-    port, ssl = _parse_frontend_config(daemon['metadata']['frontend_config#0'])
+    daemon = RgwDaemon()
+    daemon.host = _parse_addr(daemon_info['addr'])
+    daemon.port, daemon.ssl = _parse_frontend_config(daemon_info['metadata']['frontend_config#0'])
 
-    logger.info('Auto-detected RGW configuration: addr=%s, port=%d, ssl=%s',
-                addr, port, str(ssl))
-
-    return addr, port, ssl
+    return daemon
 
 
 def _parse_addr(value) -> str:
@@ -208,46 +218,28 @@ def _parse_frontend_config(config) -> Tuple[int, bool]:
 
 
 class RgwClient(RestClient):
-    _SYSTEM_USERID = None
-    _ADMIN_PATH = None
     _host = None
     _port = None
     _ssl = None
-    _user_instances = {}  # type: Dict[str, RgwClient]
+    _user_instances = {}  # type: Dict[str, Dict[str, RgwClient]]
+    _config_instances = {}  # type: Dict[str, RgwClient]
     _rgw_settings_snapshot = None
+    _daemons: Dict[str, RgwDaemon] = {}
+    daemon: RgwDaemon
+    got_keys_from_config: bool
+    userid: str
 
     @staticmethod
-    def _load_settings():
-        # The API access key and secret key are mandatory for a minimal configuration.
-        if not (Settings.RGW_API_ACCESS_KEY and Settings.RGW_API_SECRET_KEY):
-            logger.warning('No credentials found, please consult the '
-                           'documentation about how to enable RGW for the '
-                           'dashboard.')
-            raise NoCredentialsException()
-
-        if Options.has_default_value('RGW_API_HOST') and \
-                Options.has_default_value('RGW_API_PORT') and \
-                Options.has_default_value('RGW_API_SCHEME'):
-            host, port, ssl = _determine_rgw_addr()
-        else:
-            host = Settings.RGW_API_HOST
-            port = Settings.RGW_API_PORT
-            ssl = Settings.RGW_API_SCHEME == 'https'
-
-        RgwClient._host = host
-        RgwClient._port = port
-        RgwClient._ssl = ssl
-        RgwClient._ADMIN_PATH = Settings.RGW_API_ADMIN_RESOURCE
-
-        # Create an instance using the configured settings.
-        instance = RgwClient(Settings.RGW_API_USER_ID,
-                             Settings.RGW_API_ACCESS_KEY,
-                             Settings.RGW_API_SECRET_KEY)
-
-        RgwClient._SYSTEM_USERID = instance.userid
+    def _get_daemon_connection_info(daemon_name: str) -> dict:
+        try:
+            access_key = Settings.RGW_API_ACCESS_KEY[daemon_name]
+            secret_key = Settings.RGW_API_SECRET_KEY[daemon_name]
+        except TypeError:
+            # Legacy string values.
+            access_key = Settings.RGW_API_ACCESS_KEY
+            secret_key = Settings.RGW_API_SECRET_KEY
 
-        # Append the instance to the internal map.
-        RgwClient._user_instances[RgwClient._SYSTEM_USERID] = instance
+        return {'access_key': access_key, 'secret_key': secret_key}
 
     def _get_daemon_zone_info(self):  # type: () -> dict
         return json_str_to_object(self.proxy('GET', 'config?type=zone', None, None))
@@ -263,93 +255,126 @@ class RgwClient(RestClient):
                 Settings.RGW_API_SECRET_KEY,
                 Settings.RGW_API_ADMIN_RESOURCE,
                 Settings.RGW_API_SCHEME,
-                Settings.RGW_API_USER_ID,
                 Settings.RGW_API_SSL_VERIFY)
 
     @staticmethod
-    def instance(userid):
-        # type: (Optional[str]) -> RgwClient
+    def instance(userid: Optional[str] = None,
+                 daemon_name: Optional[str] = None) -> 'RgwClient':
+        # pylint: disable=too-many-branches
+        # The API access key and secret key are mandatory for a minimal configuration.
+        if not (Settings.RGW_API_ACCESS_KEY and Settings.RGW_API_SECRET_KEY):
+            logger.warning('No credentials found, please consult the '
+                           'documentation about how to enable RGW for the '
+                           'dashboard.')
+            raise NoCredentialsException()
+
+        if not RgwClient._daemons:
+            RgwClient._daemons = _get_daemons()
+
+        if not daemon_name:
+            # Select default daemon if configured in settings:
+            if Settings.RGW_API_HOST and Settings.RGW_API_PORT:
+                for daemon in RgwClient._daemons.values():
+                    if daemon.host == Settings.RGW_API_HOST \
+                            and daemon.port == Settings.RGW_API_PORT:
+                        daemon_name = daemon.name
+                        break
+                if not daemon_name:
+                    raise LookupError('No RGW daemon found with host: {}, port: {}'.format(
+                        Settings.RGW_API_HOST,
+                        Settings.RGW_API_PORT))
+            # Select 1st daemon:
+            else:
+                daemon_name = next(iter(RgwClient._daemons.keys()))
+
         # Discard all cached instances if any rgw setting has changed
         if RgwClient._rgw_settings_snapshot != RgwClient._rgw_settings():
             RgwClient._rgw_settings_snapshot = RgwClient._rgw_settings()
             RgwClient.drop_instance()
 
-        if not RgwClient._user_instances:
-            RgwClient._load_settings()
+        if daemon_name not in RgwClient._config_instances:
+            connection_info = RgwClient._get_daemon_connection_info(daemon_name)
+            RgwClient._config_instances[daemon_name] = RgwClient(connection_info['access_key'],
+                                                                 connection_info['secret_key'],
+                                                                 daemon_name)
 
-        if not userid:
-            userid = RgwClient._SYSTEM_USERID
+        if not userid or userid == RgwClient._config_instances[daemon_name].userid:
+            return RgwClient._config_instances[daemon_name]
 
-        if userid not in RgwClient._user_instances:
+        if daemon_name not in RgwClient._user_instances \
+                or userid not in RgwClient._user_instances[daemon_name]:
             # Get the access and secret keys for the specified user.
-            keys = RgwClient.admin_instance().get_user_keys(userid)
+            keys = RgwClient._config_instances[daemon_name].get_user_keys(userid)
             if not keys:
                 raise RequestException(
                     "User '{}' does not have any keys configured.".format(
                         userid))
+            instance = RgwClient(keys['access_key'],
+                                 keys['secret_key'],
+                                 daemon_name,
+                                 userid)
+            RgwClient._user_instances.update({daemon_name: {userid: instance}})
 
-            # Create an instance and append it to the internal map.
-            RgwClient._user_instances[userid] = RgwClient(userid,  # type: ignore
-                                                          keys['access_key'],
-                                                          keys['secret_key'])
-
-        return RgwClient._user_instances[userid]  # type: ignore
+        return RgwClient._user_instances[daemon_name][userid]
 
     @staticmethod
-    def admin_instance():
-        return RgwClient.instance(RgwClient._SYSTEM_USERID)
+    def admin_instance(daemon_name: Optional[str] = None) -> 'RgwClient':
+        return RgwClient.instance(daemon_name=daemon_name)
 
     @staticmethod
-    def drop_instance(userid: Optional[str] = None):
+    def drop_instance(instance: Optional['RgwClient'] = None):
         """
-        Drop a cached instance by name or all.
+        Drop a cached instance or all.
         """
-        if userid:
-            RgwClient._user_instances.pop(userid, None)
+        if instance:
+            if instance.got_keys_from_config:
+                del RgwClient._config_instances[instance.daemon.name]
+            else:
+                del RgwClient._user_instances[instance.daemon.name][instance.userid]
         else:
+            RgwClient._config_instances.clear()
             RgwClient._user_instances.clear()
 
     def _reset_login(self):
-        if self.userid != RgwClient._SYSTEM_USERID:
-            logger.info("Fetching new keys for user: %s", self.userid)
-            keys = RgwClient.admin_instance().get_user_keys(self.userid)
-            # pylint: disable=attribute-defined-outside-init
-            self.auth = S3Auth(keys['access_key'], keys['secret_key'],
-                               service_url=self.service_url)
-        else:
+        if self.got_keys_from_config:
             raise RequestException('Authentication failed for the "{}" user: wrong credentials'
                                    .format(self.userid), status_code=401)
+        logger.info("Fetching new keys for user: %s", self.userid)
+        keys = RgwClient.admin_instance(daemon_name=self.daemon.name).get_user_keys(self.userid)
+        self.auth = S3Auth(keys['access_key'], keys['secret_key'],
+                           service_url=self.service_url)
 
-    def __init__(self,  # pylint: disable-msg=R0913
-                 userid,
+    def __init__(self,
                  access_key,
                  secret_key,
-                 host=None,
-                 port=None,
-                 admin_path=None,
-                 ssl=False):
-
-        if not host and not RgwClient._host:
-            RgwClient._load_settings()
-        host = host if host else RgwClient._host
-        port = port if port else RgwClient._port
-        admin_path = admin_path if admin_path else RgwClient._ADMIN_PATH
-        ssl = ssl if ssl else RgwClient._ssl
+                 daemon_name,
+                 user_id=None):
+        daemon = RgwClient._daemons[daemon_name]
         ssl_verify = Settings.RGW_API_SSL_VERIFY
+        self.admin_path = Settings.RGW_API_ADMIN_RESOURCE
+        self.service_url = build_url(host=daemon.host, port=daemon.port)
+
+        self.auth = S3Auth(access_key, secret_key, service_url=self.service_url)
+        super(RgwClient, self).__init__(daemon.host,
+                                        daemon.port,
+                                        'RGW',
+                                        daemon.ssl,
+                                        self.auth,
+                                        ssl_verify=ssl_verify)
+        self.got_keys_from_config = not user_id
+        try:
+            self.userid = self._get_user_id(self.admin_path) if self.got_keys_from_config \
+                else user_id
+        except RequestException as error:
+            # Avoid dashboard GUI redirections caused by status code (403, ...):
+            http_status_code = 400 if 400 <= error.status_code < 500 else error.status_code
+            raise DashboardException(msg='Error connecting to Object Gateway.',
+                                     http_status_code=http_status_code,
+                                     component='rgw')
+        self.daemon = daemon
 
-        self.service_url = build_url(host=host, port=port)
-        self.admin_path = admin_path
-
-        s3auth = S3Auth(access_key, secret_key, service_url=self.service_url)
-        super(RgwClient, self).__init__(host, port, 'RGW', ssl, s3auth, ssl_verify=ssl_verify)
-
-        # If user ID is not set, then try to get it via the RGW Admin Ops API.
-        self.userid = userid if userid else self._get_user_id(self.admin_path)  # type: str
-
-        self._zonegroup_name: str = _get_daemon_info()['metadata']['zonegroup_name']
-
-        logger.info("Created new connection: user=%s, host=%s, port=%s, ssl=%d, sslverify=%d",
-                    self.userid, host, port, ssl, ssl_verify)
+        logger.info("Created new connection: daemon=%s, host=%s, port=%s, ssl=%d, sslverify=%d",
+                    daemon.name, daemon.host, daemon.port, daemon.ssl, ssl_verify)
 
     @RestClient.api_get('/', resp_structure='[0] > (ID & DisplayName)')
     def is_service_online(self, request=None):
@@ -495,7 +520,8 @@ class RgwClient(RestClient):
                 }
             )
 
-        return {'zonegroup': self._zonegroup_name, 'placement_targets': placement_targets}
+        return {'zonegroup': self.daemon.zonegroup_name,
+                'placement_targets': placement_targets}
 
     def get_realms(self):  # type: () -> List
         realms_info = self._get_realms_info()
index d42e6ed5b45593dc3ad2d55d8dc82269eb32c1cb..6c1b90374fb72660ce5ee69160ca1616b52b8e74 100644 (file)
@@ -3,12 +3,47 @@ from __future__ import absolute_import
 
 import errno
 import inspect
+from ast import literal_eval
+from typing import Any
 
 from mgr_module import CLICheckNonemptyFileInput
 
 from . import mgr
 
 
+class Setting:
+    """
+    Setting representation that allows to set a default value and a list of allowed data types.
+    :param default_value: The name of the bucket.
+    :param types: a list consisting of the primary/preferred type and, optionally,
+    secondary/legacy types for backward compatibility.
+    """
+
+    def __init__(self, default_value: Any, types: list):
+        if not isinstance(types, list):
+            raise ValueError('Setting types must be a list.')
+        default_value_type = type(default_value)
+        if default_value_type not in types:
+            raise ValueError('Default value type not allowed.')
+        self.default_value = default_value
+        self.types = types
+
+    def types_as_str(self):
+        return ','.join([x.__name__ for x in self.types])
+
+    def cast(self, value):
+        for type_index, setting_type in enumerate(self.types):
+            try:
+                if setting_type.__name__ == 'bool' and str(value).lower() == 'false':
+                    return False
+                elif setting_type.__name__ == 'dict':
+                    return literal_eval(value)
+                return setting_type(value)
+            except (SyntaxError, TypeError, ValueError) as error:
+                if type_index == len(self.types) - 1:
+                    raise error
+
+
 class Options(object):
     """
     If you need to store some configuration value please add the config option
@@ -19,93 +54,84 @@ class Options(object):
         GRAFANA_API_HOST = ('localhost', str)
         GRAFANA_API_PORT = (3000, int)
     """
-    ENABLE_BROWSABLE_API = (True, bool)
-    REST_REQUESTS_TIMEOUT = (45, int)
+    ENABLE_BROWSABLE_API = Setting(True, [bool])
+    REST_REQUESTS_TIMEOUT = Setting(45, [int])
 
     # AUTHENTICATION ATTEMPTS
-    ACCOUNT_LOCKOUT_ATTEMPTS = (10, int)
+    ACCOUNT_LOCKOUT_ATTEMPTS = Setting(10, [int])
 
     # API auditing
-    AUDIT_API_ENABLED = (False, bool)
-    AUDIT_API_LOG_PAYLOAD = (True, bool)
+    AUDIT_API_ENABLED = Setting(False, [bool])
+    AUDIT_API_LOG_PAYLOAD = Setting(True, [bool])
 
     # RGW settings
-    RGW_API_HOST = ('', str)
-    RGW_API_PORT = (80, int)
-    RGW_API_ACCESS_KEY = ('', str)
-    RGW_API_SECRET_KEY = ('', str)
-    RGW_API_ADMIN_RESOURCE = ('admin', str)
-    RGW_API_SCHEME = ('http', str)
-    RGW_API_USER_ID = ('', str)
-    RGW_API_SSL_VERIFY = (True, bool)
+    RGW_API_HOST = Setting('', [dict, str])
+    RGW_API_PORT = Setting(80, [dict, int])
+    RGW_API_ACCESS_KEY = Setting('', [dict, str])
+    RGW_API_SECRET_KEY = Setting('', [dict, str])
+    RGW_API_ADMIN_RESOURCE = Setting('admin', [str])
+    RGW_API_SCHEME = Setting('http', [str])
+    RGW_API_USER_ID = Setting('', [dict, str])
+    RGW_API_SSL_VERIFY = Setting(True, [bool])
 
     # Grafana settings
-    GRAFANA_API_URL = ('', str)
-    GRAFANA_FRONTEND_API_URL = ('', str)
-    GRAFANA_API_USERNAME = ('admin', str)
-    GRAFANA_API_PASSWORD = ('admin', str)
-    GRAFANA_API_SSL_VERIFY = (True, bool)
-    GRAFANA_UPDATE_DASHBOARDS = (False, bool)
+    GRAFANA_API_URL = Setting('', [str])
+    GRAFANA_FRONTEND_API_URL = Setting('', [str])
+    GRAFANA_API_USERNAME = Setting('admin', [str])
+    GRAFANA_API_PASSWORD = Setting('admin', [str])
+    GRAFANA_API_SSL_VERIFY = Setting(True, [bool])
+    GRAFANA_UPDATE_DASHBOARDS = Setting(False, [bool])
 
     # NFS Ganesha settings
-    GANESHA_CLUSTERS_RADOS_POOL_NAMESPACE = ('', str)
+    GANESHA_CLUSTERS_RADOS_POOL_NAMESPACE = Setting('', [str])
 
     # Prometheus settings
-    PROMETHEUS_API_HOST = ('', str)
-    PROMETHEUS_API_SSL_VERIFY = (True, bool)
-    ALERTMANAGER_API_HOST = ('', str)
-    ALERTMANAGER_API_SSL_VERIFY = (True, bool)
+    PROMETHEUS_API_HOST = Setting('', [str])
+    PROMETHEUS_API_SSL_VERIFY = Setting(True, [bool])
+    ALERTMANAGER_API_HOST = Setting('', [str])
+    ALERTMANAGER_API_SSL_VERIFY = Setting(True, [bool])
 
     # iSCSI management settings
-    ISCSI_API_SSL_VERIFICATION = (True, bool)
+    ISCSI_API_SSL_VERIFICATION = Setting(True, [bool])
 
     # user management settings
     # Time span of user passwords to expire in days.
     # The default value is '0' which means that user passwords are
     # never going to expire.
-    USER_PWD_EXPIRATION_SPAN = (0, int)
+    USER_PWD_EXPIRATION_SPAN = Setting(0, [int])
     # warning levels to notify the user that the password is going
     # to expire soon
-    USER_PWD_EXPIRATION_WARNING_1 = (10, int)
-    USER_PWD_EXPIRATION_WARNING_2 = (5, int)
+    USER_PWD_EXPIRATION_WARNING_1 = Setting(10, [int])
+    USER_PWD_EXPIRATION_WARNING_2 = Setting(5, [int])
 
     # Password policy
-    PWD_POLICY_ENABLED = (True, bool)
+    PWD_POLICY_ENABLED = Setting(True, [bool])
     # Individual checks
-    PWD_POLICY_CHECK_LENGTH_ENABLED = (True, bool)
-    PWD_POLICY_CHECK_OLDPWD_ENABLED = (True, bool)
-    PWD_POLICY_CHECK_USERNAME_ENABLED = (False, bool)
-    PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = (False, bool)
-    PWD_POLICY_CHECK_COMPLEXITY_ENABLED = (False, bool)
-    PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED = (False, bool)
-    PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED = (False, bool)
+    PWD_POLICY_CHECK_LENGTH_ENABLED = Setting(True, [bool])
+    PWD_POLICY_CHECK_OLDPWD_ENABLED = Setting(True, [bool])
+    PWD_POLICY_CHECK_USERNAME_ENABLED = Setting(False, [bool])
+    PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED = Setting(False, [bool])
+    PWD_POLICY_CHECK_COMPLEXITY_ENABLED = Setting(False, [bool])
+    PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED = Setting(False, [bool])
+    PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED = Setting(False, [bool])
     # Settings
-    PWD_POLICY_MIN_LENGTH = (8, int)
-    PWD_POLICY_MIN_COMPLEXITY = (10, int)
-    PWD_POLICY_EXCLUSION_LIST = (','.join(['osd', 'host',
-                                           'dashboard', 'pool',
-                                           'block', 'nfs',
-                                           'ceph', 'monitors',
-                                           'gateway', 'logs',
-                                           'crush', 'maps']),
-                                 str)
+    PWD_POLICY_MIN_LENGTH = Setting(8, [int])
+    PWD_POLICY_MIN_COMPLEXITY = Setting(10, [int])
+    PWD_POLICY_EXCLUSION_LIST = Setting(','.join(['osd', 'host', 'dashboard', 'pool',
+                                                  'block', 'nfs', 'ceph', 'monitors',
+                                                  'gateway', 'logs', 'crush', 'maps']),
+                                        [str])
 
     @staticmethod
     def has_default_value(name):
         return getattr(Settings, name, None) is None or \
-            getattr(Settings, name) == getattr(Options, name)[0]
+            getattr(Settings, name) == getattr(Options, name).default_value
 
 
 class SettingsMeta(type):
     def __getattr__(cls, attr):
-        default, stype = getattr(Options, attr)
-        if stype == bool and str(mgr.get_module_option(
-                attr,
-                default)).lower() == 'false':
-            value = False
-        else:
-            value = stype(mgr.get_module_option(attr, default))
-        return value
+        setting = getattr(Options, attr)
+        return setting.cast(mgr.get_module_option(attr, setting.default_value))
 
     def __setattr__(cls, attr, value):
         if not attr.startswith('_') and hasattr(Options, attr):
@@ -128,14 +154,14 @@ def _options_command_map():
         return not inspect.isroutine(member)
 
     cmd_map = {}
-    for option, value in inspect.getmembers(Options, filter_attr):
+    for option, setting in inspect.getmembers(Options, filter_attr):
         if option.startswith('_'):
             continue
         key_get = 'dashboard get-{}'.format(option.lower().replace('_', '-'))
         key_set = 'dashboard set-{}'.format(option.lower().replace('_', '-'))
         key_reset = 'dashboard reset-{}'.format(option.lower().replace('_', '-'))
         cmd_map[key_get] = {'name': option, 'type': None}
-        cmd_map[key_set] = {'name': option, 'type': value[1]}
+        cmd_map[key_set] = {'name': option, 'type': setting.types_as_str()}
         cmd_map[key_reset] = {'name': option, 'type': None}
     return cmd_map
 
@@ -191,11 +217,11 @@ def options_schema_list():
         return not inspect.isroutine(member)
 
     result = []
-    for option, value in inspect.getmembers(Options, filter_attr):
+    for option, setting in inspect.getmembers(Options, filter_attr):
         if option.startswith('_'):
             continue
-        result.append({'name': option, 'default': value[0],
-                       'type': value[1].__name__})
+        result.append({'name': option, 'default': setting.default_value,
+                       'type': setting.types_as_str()})
 
     return result
 
@@ -219,10 +245,8 @@ def handle_option_command(cmd, inbuf):
                 return value, stdout, stderr
         else:
             value = cmd['value']
-        value = opt['type'](value)
-        if opt['type'] == bool and cmd['value'].lower() == 'false':
-            value = False
-        setattr(Settings, opt['name'], value)
+        setting = getattr(Options, opt['name'])
+        setattr(Settings, opt['name'], setting.cast(value))
         return 0, 'Option {} updated'.format(opt['name']), ''
 
 
index 1a99db59287cda3720d948cfb0e007a00f27e3d1..5201f8d11156c79eb773885ac43326c51ca9d01d 100644 (file)
@@ -34,10 +34,10 @@ class RgwUserControllerTestCase(ControllerTestCase):
             'keys': ['test1', 'test2', 'test3'],
             'truncated': False
         }]
-        self._get('/test/api/rgw/user')
+        self._get('/test/api/rgw/user?daemon_name=dummy-daemon')
         self.assertStatus(200)
         mock_proxy.assert_has_calls([
-            mock.call('GET', 'user?list', {})
+            mock.call('dummy-daemon', 'GET', 'user?list', {})
         ])
         self.assertJsonBody(['test1', 'test2', 'test3'])
 
@@ -56,8 +56,8 @@ class RgwUserControllerTestCase(ControllerTestCase):
         self._get('/test/api/rgw/user')
         self.assertStatus(200)
         mock_proxy.assert_has_calls([
-            mock.call('GET', 'user?list', {}),
-            mock.call('GET', 'user?list', {'marker': 'foo:bar'})
+            mock.call(None, 'GET', 'user?list', {}),
+            mock.call(None, 'GET', 'user?list', {'marker': 'foo:bar'})
         ])
         self.assertJsonBody(['test1', 'test2', 'test3', 'admin'])
 
index 7cde2d4c3ff421341b435523aeb8b911fe1f5723..f12fa4aa8d5b6a3f2bf925ace6874f56750393fd 100644 (file)
@@ -3,34 +3,35 @@
 import unittest
 
 try:
-    from unittest.mock import patch
+    from unittest.mock import MagicMock, patch
 except ImportError:
-    from mock import patch  # type: ignore
+    from mock import MagicMock, patch  # type: ignore
 
-from ..services.rgw_client import RgwClient, _parse_frontend_config
+from ..services.rgw_client import NoCredentialsException, RgwClient, \
+    RgwDaemon, _parse_frontend_config
 from ..settings import Settings
 from . import KVStoreMockMixin  # pylint: disable=no-name-in-module
 
 
-def _dummy_daemon_info():
-    return {
-        'addr': '172.20.0.2:0/256594694',
-        'metadata': {
-            'zonegroup_name': 'zonegroup2-realm1'
-        }
-    }
+def _get_daemons_stub():
+    daemon = RgwDaemon()
+    daemon.host = 'rgw.1.myorg.com'
+    daemon.port = 8000
+    daemon.ssl = True
+    daemon.name = 'rgw.1.myorg.com'
+    daemon.zonegroup_name = 'zonegroup2-realm1'
+    return {daemon.name: daemon}
 
 
-@patch('dashboard.services.rgw_client._get_daemon_info', _dummy_daemon_info)
+@patch('dashboard.services.rgw_client._get_daemons', _get_daemons_stub)
+@patch('dashboard.services.rgw_client.RgwClient._get_user_id', MagicMock(
+    return_value='dummy_admin'))
 class RgwClientTest(unittest.TestCase, KVStoreMockMixin):
     def setUp(self):
-        RgwClient._user_instances.clear()  # pylint: disable=protected-access
         self.mock_kv_store()
         self.CONFIG_KEY_DICT.update({
             'RGW_API_ACCESS_KEY': 'klausmustermann',
             'RGW_API_SECRET_KEY': 'supergeheim',
-            'RGW_API_HOST': 'localhost',
-            'RGW_API_USER_ID': 'rgwadmin'
         })
 
     def test_ssl_verify(self):
@@ -43,6 +44,24 @@ class RgwClientTest(unittest.TestCase, KVStoreMockMixin):
         instance = RgwClient.admin_instance()
         self.assertFalse(instance.session.verify)
 
+    def test_no_credentials(self):
+        self.CONFIG_KEY_DICT.update({
+            'RGW_API_ACCESS_KEY': '',
+            'RGW_API_SECRET_KEY': '',
+        })
+        with self.assertRaises(NoCredentialsException) as cm:
+            RgwClient.admin_instance()
+        self.assertIn('No RGW credentials found', str(cm.exception))
+
+    def test_default_daemon_wrong_settings(self):
+        self.CONFIG_KEY_DICT.update({
+            'RGW_API_HOST': 'localhost',
+            'RGW_API_PORT': '7990',
+        })
+        with self.assertRaises(LookupError) as cm:
+            RgwClient.admin_instance()
+        self.assertIn('No RGW daemon found with host:', str(cm.exception))
+
     @patch.object(RgwClient, '_get_daemon_zone_info')
     def test_get_placement_targets_from_zone(self, zone_info):
         zone_info.return_value = {
index 240eafee348ab0dcb501a274a7bdc773b7eb13bf..e92b580e3a6277d206ca72c349fb8d0dd3c2b058 100644 (file)
@@ -15,10 +15,10 @@ from . import ControllerTestCase, KVStoreMockMixin  # pylint: disable=no-name-in
 class SettingsTest(unittest.TestCase, KVStoreMockMixin):
     @classmethod
     def setUpClass(cls):
+        setattr(settings.Options, 'GRAFANA_API_HOST', settings.Setting('localhost', [str]))
+        setattr(settings.Options, 'GRAFANA_API_PORT', settings.Setting(3000, [int]))
+        setattr(settings.Options, 'GRAFANA_ENABLED', settings.Setting(False, [bool]))
         # pylint: disable=protected-access
-        settings.Options.GRAFANA_API_HOST = ('localhost', str)
-        settings.Options.GRAFANA_API_PORT = (3000, int)
-        settings.Options.GRAFANA_ENABLED = (False, bool)
         settings._OPTIONS_COMMAND_MAP = settings._options_command_map()
 
     def setUp(self):
@@ -138,9 +138,8 @@ class SettingsControllerTest(ControllerTestCase, KVStoreMockMixin):
     @classmethod
     def setUpClass(cls):
         super().setUpClass()
-        # pylint: disable=protected-access
-        settings.Options.GRAFANA_API_HOST = ('localhost', str)
-        settings.Options.GRAFANA_ENABLED = (False, bool)
+        setattr(settings.Options, 'GRAFANA_API_HOST', settings.Setting('localhost', [str]))
+        setattr(settings.Options, 'GRAFANA_ENABLED', settings.Setting(False, [bool]))
 
     @classmethod
     def tearDownClass(cls):