$ 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
$ 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
'--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?
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()
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):
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')
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
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 = {
# 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(
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']
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)
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)
@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)
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))
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)
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
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,
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)
@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
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']
@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
@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
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
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)
})
@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
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,
@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,
})
@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
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)
}
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';
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();
});
--- /dev/null
+export class RgwDaemon {
+ id: string;
+ version: string;
+ server_hostname: string;
+ zonegroup_name: string;
+ zone_name: string;
+ default: boolean;
+}
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';
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',
}
getDaemonList(context: CdTableFetchDataContext) {
- this.rgwDaemonService.list().subscribe(
+ this.rgwDaemonService.daemons$.pipe(take(1)).subscribe(
(resp: object[]) => {
this.daemons = resp;
},
const routes: Routes = [
{
- path: '',
- redirectTo: 'daemon',
- pathMatch: 'full'
+ path: '' // Required for a clean reload on daemon selection.
},
{ path: 'daemon', component: RgwDaemonListComponent, data: { breadcrumbs: 'Daemons' } },
{
--- /dev/null
+<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>
--- /dev/null
+@use './src/styles/vendor/variables' as vv;
+
+.cd-context-bar {
+ border-bottom: 1px solid vv.$gray-300;
+}
--- /dev/null
+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 ) ');
+ }));
+});
--- /dev/null
+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]);
+ });
+ }
+}
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';
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,
<cd-navigation>
<div class="container-fluid h-100"
[ngClass]="{'dashboard':isDashboardPage()} ">
+ <cd-context></cd-context>
<cd-breadcrumbs></cd-breadcrumbs>
<router-outlet></router-outlet>
</div>
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', () => {
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');
});
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');
});
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');
});
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);
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({
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(
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')
+ }
+ })
+ });
});
}
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(
-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({
})
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);
+ })
+ );
+ }
}
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', () => {
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`
+ );
});
});
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({
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 });
+ });
}
}
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', () => {
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([]);
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' });
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');
});
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({
export class RgwUserService {
private url = 'api/rgw/user';
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
/**
* Get the list of users.
* @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 });
+ });
}
/**
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(() => {
}
}
+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,
name: stats
schema:
type: boolean
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'200':
content:
properties:
bucket:
type: string
+ daemon_name:
+ type: string
lock_enabled:
default: 'false'
type: string
name: purge_objects
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'202':
content:
required: true
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'200':
content:
properties:
bucket_id:
type: string
+ daemon_name:
+ type: string
lock_mode:
type: string
lock_retention_period_days:
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':
name: query
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'200':
content:
- Rgw
/api/rgw/user:
get:
- parameters: []
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'200':
content:
properties:
access_key:
type: string
+ daemon_name:
+ type: string
display_name:
type: string
email:
- RgwUser
/api/rgw/user/get_emails:
get:
- parameters: []
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'200':
content:
required: true
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'202':
content:
required: true
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'200':
content:
application/json:
schema:
properties:
+ daemon_name:
+ type: string
display_name:
type: string
email:
required: true
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'202':
content:
application/json:
schema:
properties:
+ daemon_name:
+ type: string
perm:
type: string
type:
name: access_key
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'202':
content:
properties:
access_key:
type: string
+ daemon_name:
+ type: string
generate_key:
default: 'true'
type: string
required: true
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'200':
content:
application/json:
schema:
properties:
+ daemon_name:
+ type: string
enabled:
type: string
max_objects:
type: string
access_key:
type: string
+ daemon_name:
+ type: string
generate_secret:
default: 'true'
type: string
name: purge_keys
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: daemon_name
+ schema:
+ type: string
responses:
'202':
content:
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:
'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.
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:
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))
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):
}
)
- 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()
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
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):
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
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
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']), ''
'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'])
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'])
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):
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 = {
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):
@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):