From 3d6e9ac22b49dda306a63179b8097ebd057a3e91 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 1 Feb 2023 10:56:31 +0530 Subject: [PATCH] mgr/dashboard: Dashboard RGW multisite configuration Signed-off-by: Aashish Sharma --- src/pybind/mgr/dashboard/controllers/rgw.py | 152 +++++++ .../frontend/src/app/app-routing.module.ts | 2 +- .../src/app/ceph/rgw/models/rgw-multisite.ts | 46 +++ .../rgw-multisite-details.component.html | 49 +++ .../rgw-multisite-details.component.scss | 5 + .../rgw-multisite-details.component.spec.ts | 36 ++ .../rgw-multisite-details.component.ts | 291 +++++++++++++ .../rgw-multisite-realm-form.component.html | 48 +++ .../rgw-multisite-realm-form.component.scss | 0 ...rgw-multisite-realm-form.component.spec.ts | 38 ++ .../rgw-multisite-realm-form.component.ts | 80 ++++ .../rgw-multisite-zone-form.component.html | 112 +++++ .../rgw-multisite-zone-form.component.scss | 0 .../rgw-multisite-zone-form.component.spec.ts | 38 ++ .../rgw-multisite-zone-form.component.ts | 141 +++++++ ...gw-multisite-zonegroup-form.component.html | 94 +++++ ...gw-multisite-zonegroup-form.component.scss | 0 ...multisite-zonegroup-form.component.spec.ts | 38 ++ .../rgw-multisite-zonegroup-form.component.ts | 136 ++++++ .../frontend/src/app/ceph/rgw/rgw.module.ts | 39 +- .../src/app/core/error/error.component.html | 6 +- .../src/app/core/error/error.component.ts | 6 + .../navigation/navigation.component.html | 5 + .../app/shared/api/rgw-realm.service.spec.ts | 22 + .../src/app/shared/api/rgw-realm.service.ts | 58 +++ .../app/shared/api/rgw-zone.service.spec.ts | 22 + .../src/app/shared/api/rgw-zone.service.ts | 72 ++++ .../shared/api/rgw-zonegroup.service.spec.ts | 22 + .../app/shared/api/rgw-zonegroup.service.ts | 58 +++ .../src/app/shared/constants/app.constants.ts | 19 + .../src/app/shared/enum/icons.enum.ts | 1 + .../services/module-status-guard.service.ts | 3 + .../styles/defaults/_bootstrap-defaults.scss | 1 + src/pybind/mgr/dashboard/openapi.yaml | 387 ++++++++++++++++++ .../mgr/dashboard/services/rgw_client.py | 240 ++++++++++- 35 files changed, 2258 insertions(+), 9 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 710a4980f7cd3..909bdecba5ad0 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -77,6 +77,25 @@ class Rgw(BaseController): return status +@UIRouter('/rgw/multisite') +class RgwStatus(BaseController): + @Endpoint() + @ReadPermission + # pylint: disable=R0801 + def status(self): + status = {'available': True, 'message': None} + try: + instance = RgwClient.admin_instance() + is_multisite_configured = instance.get_multisite_status() + if not is_multisite_configured: + status['available'] = False + status['message'] = 'Multi-site provides disaster recovery and may also \ + serve as a foundation for content delivery networks' # type: ignore + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + return status + + @APIRouter('/rgw/daemon', Scope.RGW) @APIDoc("RGW Daemon Management API", "RgwDaemon") class RgwDaemon(RESTController): @@ -639,3 +658,136 @@ class RgwUserRole(NamedTuple): CreateDate: str MaxSessionDuration: int AssumeRolePolicyDocument: str + + +@APIRouter('/rgw/realm', Scope.RGW) +class RgwRealm(RESTController): + @allow_empty_body + # pylint: disable=W0613 + def create(self, realm_name, default, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.create_realm(realm_name, default) + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @allow_empty_body + # pylint: disable=W0613 + def list(self, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.list_realms() + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @allow_empty_body + # pylint: disable=W0613 + def get(self, realm_name, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.get_realm(realm_name) + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @Endpoint() + @ReadPermission + def get_all_realms_info(self): + try: + instance = RgwClient.admin_instance() + result = instance.get_all_realms_info() + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + +@APIRouter('/rgw/zonegroup', Scope.RGW) +class RgwZonegroup(RESTController): + @allow_empty_body + # pylint: disable=W0613 + def create(self, realm_name, zonegroup_name, default=None, master=None, + zonegroup_endpoints=None, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.create_zonegroup(realm_name, zonegroup_name, default, + master, zonegroup_endpoints) + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @allow_empty_body + # pylint: disable=W0613 + def list(self, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.list_zonegroups() + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @allow_empty_body + # pylint: disable=W0613 + def get(self, zonegroup_name, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.get_zonegroup(zonegroup_name) + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @Endpoint() + @ReadPermission + def get_all_zonegroups_info(self): + try: + instance = RgwClient.admin_instance() + result = instance.get_all_zonegroups_info() + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + +@APIRouter('/rgw/zone', Scope.RGW) +class RgwZone(RESTController): + @allow_empty_body + # pylint: disable=W0613 + def create(self, zone_name, zonegroup_name=None, default=False, master=False, + zone_endpoints=None, user=None, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.create_zone(zone_name, zonegroup_name, default, + master, zone_endpoints, user) + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @allow_empty_body + # pylint: disable=W0613 + def list(self, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.list_zones() + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @allow_empty_body + # pylint: disable=W0613 + def get(self, zone_name, daemon_name=None): + try: + instance = RgwClient.admin_instance() + result = instance.get_zone(zone_name) + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + @Endpoint() + @ReadPermission + def get_all_zones_info(self): + try: + instance = RgwClient.admin_instance() + result = instance.get_all_zones_info() + return result + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 6880a1561c1d9..3059acb0aa718 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -313,7 +313,7 @@ const routes: Routes = [ // Object Gateway { path: 'rgw', - canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService], + canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService], data: { moduleStatusGuardConfig: { uiApiPath: 'rgw', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts new file mode 100644 index 0000000000000..9c324f48727e2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts @@ -0,0 +1,46 @@ +export class RgwRealm { + id: string; + name: string; + current_period: string; + epoch: number; +} + +export class RgwZonegroup { + id: string; + name: string; + api_name: string; + is_master: boolean; + endpoints: string[]; + hostnames: string[]; + hostnames_s3website: string[]; + master_zone: string; + zones: RgwZone[]; + placement_targets: any[]; + default_placement: string; + realm_id: string; + sync_policy: object; + enabled_features: string[]; +} + +export class RgwZone { + id: string; + name: string; + domain_root: string; + control_pool: string; + gc_pool: string; + lc_pool: string; + log_pool: string; + intent_log_pool: string; + usage_log_pool: string; + roles_pool: string; + reshard_pool: string; + user_keys_pool: string; + user_email_pool: string; + user_swift_pool: string; + user_uid_pool: string; + otp_pool: string; + system_key: object; + placement_pools: any[]; + realm_id: string; + notif_pool: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html new file mode 100644 index 0000000000000..0e2e2accee8b1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html @@ -0,0 +1,49 @@ +
+
+
+ + +
+
+
Multi-site Topology viewer
+
+
+
+ + + + + + {{ node.data.name }} + + + default + + + master + + + +
+ +
+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss new file mode 100644 index 0000000000000..bbedb62466695 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss @@ -0,0 +1,5 @@ +@use './src/styles/vendor/variables' as vv; + +.tree-container { + height: calc(100vh - vv.$tree-container-height); +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts new file mode 100644 index 0000000000000..1b0c0d65a41a6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts @@ -0,0 +1,36 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TreeModule } from '@circlon/angular-tree-component'; +import { SharedModule } from '~/app/shared/shared.module'; + +import { RgwMultisiteDetailsComponent } from './rgw-multisite-details.component'; + +describe('RgwMultisiteDetailsComponent', () => { + let component: RgwMultisiteDetailsComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwMultisiteDetailsComponent], + imports: [HttpClientTestingModule, TreeModule, SharedModule] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwMultisiteDetailsComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display right title', () => { + const span = debugElement.nativeElement.querySelector('.card-header'); + expect(span.textContent).toBe('Multi-site Topology viewer'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts new file mode 100644 index 0000000000000..f11c00d56659a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts @@ -0,0 +1,291 @@ +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { + TreeComponent, + ITreeOptions, + TreeModel, + TreeNode, + TREE_ACTIONS +} from '@circlon/angular-tree-component'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { forkJoin, Subscription } from 'rxjs'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { ActionLabelsI18n, TimerServiceInterval } from '~/app/shared/constants/app.constants'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { Permission } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { TimerService } from '~/app/shared/services/timer.service'; +import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite'; +import { RgwMultisiteRealmFormComponent } from '../rgw-multisite-realm-form/rgw-multisite-realm-form.component'; +import { RgwMultisiteZoneFormComponent } from '../rgw-multisite-zone-form/rgw-multisite-zone-form.component'; +import { RgwMultisiteZonegroupFormComponent } from '../rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component'; + +@Component({ + selector: 'cd-rgw-multisite-details', + templateUrl: './rgw-multisite-details.component.html', + styleUrls: ['./rgw-multisite-details.component.scss'] +}) +export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { + private sub = new Subscription(); + + @ViewChild('tree') tree: TreeComponent; + + messages = { + noDefaultRealm: $localize`Please create a default realm first to enable this feature` + }; + + icons = Icons; + permission: Permission; + selection = new CdTableSelection(); + createTableActions: CdTableAction[]; + loadingIndicator = true; + nodes: object[] = []; + treeOptions: ITreeOptions = { + useVirtualScroll: true, + nodeHeight: 22, + levelPadding: 20, + actionMapping: { + mouse: { + click: this.onNodeSelected.bind(this) + } + } + }; + + realms: RgwRealm[] = []; + zonegroups: RgwZonegroup[] = []; + zones: RgwZone[] = []; + metadata: any; + metadataTitle: string; + bsModalRef: NgbModalRef; + realmIds: string[] = []; + zoneIds: string[] = []; + defaultRealmId = ''; + defaultZonegroupId = ''; + defaultZoneId = ''; + multisiteInfo: object[] = []; + defaultsInfo: string[] = []; + + constructor( + private modalService: ModalService, + private timerService: TimerService, + private authStorageService: AuthStorageService, + public actionLabels: ActionLabelsI18n, + public timerServiceVariable: TimerServiceInterval, + public rgwRealmService: RgwRealmService, + public rgwZonegroupService: RgwZonegroupService, + public rgwZoneService: RgwZoneService + ) { + this.permission = this.authStorageService.getPermissions().rgw; + const createRealmAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Realm', + click: () => this.openModal('realm') + }; + const createZonegroupAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Zonegroup', + click: () => this.openModal('zonegroup'), + disable: () => this.getDisable() + }; + const createZoneAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Zone', + click: () => this.openModal('zone') + }; + this.createTableActions = [createRealmAction, createZonegroupAction, createZoneAction]; + } + + openModal(entity: any, edit = false) { + const entityName = edit ? entity.data.name : entity; + const action = edit ? 'edit' : 'create'; + const initialState = { + resource: entityName, + action: action, + info: entity, + defaultsInfo: this.defaultsInfo, + multisiteInfo: this.multisiteInfo + }; + if (entityName === 'realm') { + this.bsModalRef = this.modalService.show(RgwMultisiteRealmFormComponent, initialState, { + size: 'lg' + }); + } else if (entityName === 'zonegroup') { + this.bsModalRef = this.modalService.show(RgwMultisiteZonegroupFormComponent, initialState, { + size: 'lg' + }); + } else { + this.bsModalRef = this.modalService.show(RgwMultisiteZoneFormComponent, initialState, { + size: 'lg' + }); + } + } + + ngOnInit() { + const observables = [ + this.rgwRealmService.getAllRealmsInfo(), + this.rgwZonegroupService.getAllZonegroupsInfo(), + this.rgwZoneService.getAllZonesInfo() + ]; + this.sub = this.timerService + .get(() => forkJoin(observables), this.timerServiceVariable.TIMER_SERVICE_PERIOD * 2) + .subscribe( + (multisiteInfo: [object, object, object]) => { + this.multisiteInfo = multisiteInfo; + this.loadingIndicator = false; + this.nodes = this.abstractTreeData(multisiteInfo); + }, + (_error) => {} + ); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } + + private abstractTreeData(multisiteInfo: [object, object, object]): any[] { + let allNodes: object[] = []; + let rootNodes = {}; + let firstChildNodes = {}; + let allFirstChildNodes = []; + let secondChildNodes = {}; + let allSecondChildNodes: {}[] = []; + this.realms = multisiteInfo[0]['realms']; + this.zonegroups = multisiteInfo[1]['zonegroups']; + this.zones = multisiteInfo[2]['zones']; + this.defaultRealmId = multisiteInfo[0]['default_realm']; + this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup']; + this.defaultZoneId = multisiteInfo[2]['default_zone']; + this.defaultsInfo = this.getDefaultsEntities( + this.defaultRealmId, + this.defaultZonegroupId, + this.defaultZoneId + ); + if (this.realms.length > 0) { + // get tree for realm -> zonegroup -> zone + for (const realm of this.realms) { + const result = this.rgwRealmService.getRealmTree(realm, this.defaultRealmId); + rootNodes = result['nodes']; + this.realmIds = this.realmIds.concat(result['realmIds']); + for (const zonegroup of this.zonegroups) { + if (zonegroup.realm_id === realm.id) { + firstChildNodes = this.rgwZonegroupService.getZonegroupTree( + zonegroup, + this.defaultZonegroupId, + realm + ); + for (const zone of zonegroup.zones) { + const zoneResult = this.rgwZoneService.getZoneTree( + zone, + this.defaultZoneId, + zonegroup, + realm + ); + secondChildNodes = zoneResult['nodes']; + this.zoneIds = this.zoneIds.concat(zoneResult['zoneIds']); + allSecondChildNodes.push(secondChildNodes); + secondChildNodes = {}; + } + firstChildNodes['children'] = allSecondChildNodes; + allSecondChildNodes = []; + allFirstChildNodes.push(firstChildNodes); + firstChildNodes = {}; + } + } + rootNodes['children'] = allFirstChildNodes; + allNodes.push(rootNodes); + firstChildNodes = {}; + secondChildNodes = {}; + rootNodes = {}; + allFirstChildNodes = []; + allSecondChildNodes = []; + } + } + if (this.zonegroups.length > 0) { + // get tree for zonegroup -> zone (standalone zonegroups that don't match a realm eg(initial default)) + for (const zonegroup of this.zonegroups) { + if (!this.realmIds.includes(zonegroup.realm_id)) { + rootNodes = this.rgwZonegroupService.getZonegroupTree(zonegroup, this.defaultZonegroupId); + for (const zone of zonegroup.zones) { + const zoneResult = this.rgwZoneService.getZoneTree(zone, this.defaultZoneId, zonegroup); + firstChildNodes = zoneResult['nodes']; + this.zoneIds = this.zoneIds.concat(zoneResult['zoneIds']); + allFirstChildNodes.push(firstChildNodes); + firstChildNodes = {}; + } + rootNodes['children'] = allFirstChildNodes; + allNodes.push(rootNodes); + firstChildNodes = {}; + rootNodes = {}; + allFirstChildNodes = []; + } + } + } + if (this.zones.length > 0) { + // get tree for standalone zones(zones that do not belong to a zonegroup) + for (const zone of this.zones) { + if (this.zoneIds.length > 0 && !this.zoneIds.includes(zone.id)) { + const zoneResult = this.rgwZoneService.getZoneTree(zone, this.defaultZoneId); + rootNodes = zoneResult['nodes']; + allNodes.push(rootNodes); + rootNodes = {}; + } + } + } + if (this.realms.length < 1 && this.zonegroups.length < 1 && this.zones.length < 1) { + return [ + { + name: 'No nodes!' + } + ]; + } + this.realmIds = []; + this.zoneIds = []; + return allNodes; + } + + getDefaultsEntities( + defaultRealmId: string, + defaultZonegroupId: string, + defaultZoneId: string + ): any { + const defaultRealm = this.realms.find((x: { id: string }) => x.id === defaultRealmId); + const defaultZonegroup = this.zonegroups.find( + (x: { id: string }) => x.id === defaultZonegroupId + ); + const defaultZone = this.zones.find((x: { id: string }) => x.id === defaultZoneId); + const defaultRealmName = defaultRealm !== undefined ? defaultRealm.name : null; + const defaultZonegroupName = defaultZonegroup !== undefined ? defaultZonegroup.name : null; + const defaultZoneName = defaultZone !== undefined ? defaultZone.name : null; + return { + defaultRealmName: defaultRealmName, + defaultZonegroupName: defaultZonegroupName, + defaultZoneName: defaultZoneName + }; + } + + onNodeSelected(tree: TreeModel, node: TreeNode) { + TREE_ACTIONS.ACTIVATE(tree, node, true); + this.metadataTitle = node.data.name; + this.metadata = node.data.info; + node.data.show = true; + } + + onUpdateData() { + this.tree.treeModel.expandAll(); + } + + getDisable() { + if (this.defaultRealmId === '') { + return this.messages.noDefaultRealm; + } else { + return false; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html new file mode 100644 index 0000000000000..a3f95d94979c5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html @@ -0,0 +1,48 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + + +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts new file mode 100644 index 0000000000000..ec3c052e4d177 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +import { RgwMultisiteRealmFormComponent } from './rgw-multisite-realm-form.component'; + +describe('RgwMultisiteRealmFormComponent', () => { + let component: RgwMultisiteRealmFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + providers: [NgbActiveModal], + declarations: [RgwMultisiteRealmFormComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwMultisiteRealmFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts new file mode 100644 index 0000000000000..18c52a56caa9b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts @@ -0,0 +1,80 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { RgwRealm } from '../models/rgw-multisite'; + +@Component({ + selector: 'cd-rgw-multisite-realm-form', + templateUrl: './rgw-multisite-realm-form.component.html', + styleUrls: ['./rgw-multisite-realm-form.component.scss'] +}) +export class RgwMultisiteRealmFormComponent implements OnInit { + action: string; + multisiteRealmForm: CdFormGroup; + editing = false; + resource: string; + multisiteInfo: object[] = []; + realm: RgwRealm; + realmList: RgwRealm[] = []; + realmNames: string[]; + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + public rgwRealmService: RgwRealmService, + public notificationService: NotificationService + ) { + this.action = this.editing + ? this.actionLabels.EDIT + this.resource + : this.actionLabels.CREATE + this.resource; + this.createForm(); + } + + createForm() { + this.multisiteRealmForm = new CdFormGroup({ + realmName: new FormControl(null, { + validators: [ + Validators.required, + CdValidators.custom('uniqueName', (realmName: string) => { + return this.realmNames && this.realmNames.indexOf(realmName) !== -1; + }) + ] + }), + default_realm: new FormControl(false) + }); + } + + ngOnInit(): void { + this.realmList = + this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms') + ? this.multisiteInfo[0]['realms'] + : []; + this.realmNames = this.realmList.map((realm) => { + return realm['name']; + }); + } + + submit() { + const values = this.multisiteRealmForm.value; + this.realm = new RgwRealm(); + this.realm.name = values['realmName']; + this.rgwRealmService.create(this.realm, values['default_realm']).subscribe( + () => { + this.notificationService.show( + NotificationType.success, + $localize`Realm: '${values['realmName']}' created successfully` + ); + this.activeModal.close(); + }, + () => { + this.multisiteRealmForm.setErrors({ cdSubmitButton: true }); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html new file mode 100644 index 0000000000000..6e091310971ea --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html @@ -0,0 +1,112 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + + +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts new file mode 100644 index 0000000000000..977cf2dfca51c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form.component'; + +describe('RgwMultisiteZoneFormComponent', () => { + let component: RgwMultisiteZoneFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + providers: [NgbActiveModal], + declarations: [RgwMultisiteZoneFormComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwMultisiteZoneFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts new file mode 100644 index 0000000000000..fa89a1699f5da --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts @@ -0,0 +1,141 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import _ from 'lodash'; +import { RgwUserService } from '~/app/shared/api/rgw-user.service'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite'; + +@Component({ + selector: 'cd-rgw-multisite-zone-form', + templateUrl: './rgw-multisite-zone-form.component.html', + styleUrls: ['./rgw-multisite-zone-form.component.scss'] +}) +export class RgwMultisiteZoneFormComponent implements OnInit { + readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/; + readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i; + readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; + action: string; + multisiteZoneForm: CdFormGroup; + editing = false; + resource: string; + realm: RgwRealm; + zonegroup: RgwZonegroup; + zone: RgwZone; + defaultsInfo: string[] = []; + multisiteInfo: object[] = []; + zonegroupList: RgwZonegroup[] = []; + zoneList: RgwZone[] = []; + zoneNames: string[]; + users: string[]; + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + public rgwZoneService: RgwZoneService, + public notificationService: NotificationService, + public rgwUserService: RgwUserService + ) { + this.action = this.editing + ? this.actionLabels.EDIT + this.resource + : this.actionLabels.CREATE + this.resource; + this.createForm(); + } + + createForm() { + this.multisiteZoneForm = new CdFormGroup({ + zoneName: new FormControl(null, { + validators: [ + Validators.required, + CdValidators.custom('uniqueName', (zoneName: string) => { + return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1; + }) + ] + }), + default_zone: new FormControl(false), + master_zone: new FormControl(false), + selectedZonegroup: new FormControl(null), + zone_endpoints: new FormControl(null, { + validators: [ + CdValidators.custom('endpoint', (value: string) => { + if (_.isEmpty(value)) { + return false; + } else { + if (value.includes(',')) { + value.split(',').forEach((url: string) => { + return ( + !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url) + ); + }); + } else { + return ( + !this.endpoints.test(value) && + !this.ipv4Rgx.test(value) && + !this.ipv6Rgx.test(value) + ); + } + return false; + } + }) + ] + }), + users: new FormControl(null) + }); + } + + ngOnInit(): void { + this.zonegroupList = + this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups') + ? this.multisiteInfo[1]['zonegroups'] + : []; + this.zoneList = + this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones') + ? this.multisiteInfo[2]['zones'] + : []; + this.zoneNames = this.zoneList.map((zone) => { + return zone['name']; + }); + if (this.action === 'create') { + this.multisiteZoneForm + .get('selectedZonegroup') + .setValue(this.defaultsInfo['defaultZonegroupName']); + } + this.rgwUserService.list().subscribe((users: any) => { + this.users = users.filter((user: any) => user.keys.length !== 0); + }); + } + + submit() { + const values = this.multisiteZoneForm.value; + this.zonegroup = new RgwZonegroup(); + this.zonegroup.name = values['selectedZonegroup']; + this.zone = new RgwZone(); + this.zone.name = values['zoneName']; + this.rgwZoneService + .create( + this.zone, + this.zonegroup, + values['default_zone'], + values['master_zone'], + values['zone_endpoints'], + values['users'] + ) + .subscribe( + () => { + this.notificationService.show( + NotificationType.success, + $localize`Zone: '${values['zoneName']}' created successfully` + ); + this.activeModal.close(); + }, + () => { + this.multisiteZoneForm.setErrors({ cdSubmitButton: true }); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html new file mode 100644 index 0000000000000..6dfc9ebbbead6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html @@ -0,0 +1,94 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + + +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts new file mode 100644 index 0000000000000..bbd82577262cc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-form.component'; + +describe('RgwMultisiteZonegroupFormComponent', () => { + let component: RgwMultisiteZonegroupFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + providers: [NgbActiveModal], + declarations: [RgwMultisiteZonegroupFormComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwMultisiteZonegroupFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts new file mode 100644 index 0000000000000..792674da3e484 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts @@ -0,0 +1,136 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import _ from 'lodash'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { RgwRealm, RgwZonegroup } from '../models/rgw-multisite'; + +@Component({ + selector: 'cd-rgw-multisite-zonegroup-form', + templateUrl: './rgw-multisite-zonegroup-form.component.html', + styleUrls: ['./rgw-multisite-zonegroup-form.component.scss'] +}) +export class RgwMultisiteZonegroupFormComponent implements OnInit { + readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/; + readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i; + readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; + action: string; + multisiteZonegroupForm: CdFormGroup; + editing = false; + resource: string; + realm: RgwRealm; + zonegroup: RgwZonegroup; + defaultsInfo: string[] = []; + multisiteInfo: object[] = []; + realmList: RgwRealm[] = []; + zonegroupList: RgwZonegroup[] = []; + zonegroupNames: string[]; + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + public rgwZonegroupService: RgwZonegroupService, + public notificationService: NotificationService + ) { + this.action = this.editing + ? this.actionLabels.EDIT + this.resource + : this.actionLabels.CREATE + this.resource; + this.createForm(); + } + + createForm() { + this.multisiteZonegroupForm = new CdFormGroup({ + default_zonegroup: new FormControl(false), + zonegroupName: new FormControl(null, { + validators: [ + Validators.required, + CdValidators.custom('uniqueName', (zonegroupName: string) => { + return this.zonegroupNames && this.zonegroupNames.indexOf(zonegroupName) !== -1; + }) + ] + }), + master_zonegroup: new FormControl(false), + selectedRealm: new FormControl(null), + zonegroup_endpoints: new FormControl(null, [ + CdValidators.custom('endpoint', (value: string) => { + if (_.isEmpty(value)) { + return false; + } else { + if (value.includes(',')) { + value.split(',').forEach((url: string) => { + return ( + !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url) + ); + }); + } else { + return ( + !this.endpoints.test(value) && + !this.ipv4Rgx.test(value) && + !this.ipv6Rgx.test(value) + ); + } + return false; + } + }), + Validators.required + ]) + }); + } + + ngOnInit(): void { + this.realmList = + this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms') + ? this.multisiteInfo[0]['realms'] + : []; + this.zonegroupList = + this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups') + ? this.multisiteInfo[1]['zonegroups'] + : []; + this.zonegroupNames = this.zonegroupList.map((zonegroup) => { + return zonegroup['name']; + }); + if (this.action === 'create' && this.defaultsInfo['defaultRealmName'] !== null) { + this.multisiteZonegroupForm + .get('selectedRealm') + .setValue(this.defaultsInfo['defaultRealmName']); + } + } + + submit() { + const values = this.multisiteZonegroupForm.value; + this.realm = new RgwRealm(); + this.realm.name = values['selectedRealm']; + this.zonegroup = new RgwZonegroup(); + this.zonegroup.name = values['zonegroupName']; + this.zonegroup.endpoints = this.checkUrlArray(values['zonegroup_endpoints']); + this.rgwZonegroupService + .create(this.realm, this.zonegroup, values['default_zonegroup'], values['master_zonegroup']) + .subscribe( + () => { + this.notificationService.show( + NotificationType.success, + $localize`Zonegroup: '${values['zonegroupName']}' created successfully` + ); + this.activeModal.close(); + }, + () => { + this.multisiteZonegroupForm.setErrors({ cdSubmitButton: true }); + } + ); + } + + checkUrlArray(endpoints: string) { + let endpointsArray = []; + if (endpoints.includes(',')) { + endpointsArray = endpoints.split(','); + } else { + endpointsArray.push(endpoints); + } + return endpointsArray; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index edffa8e856b75..6bd374c3baebb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -8,6 +8,7 @@ import { NgxPipeFunctionModule } from 'ngx-pipe-function'; import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants'; import { CRUDTableComponent } from '~/app/shared/datatable/crud-table/crud-table.component'; + import { SharedModule } from '~/app/shared/shared.module'; import { PerformanceCounterModule } from '../performance-counter/performance-counter.module'; import { RgwBucketDetailsComponent } from './rgw-bucket-details/rgw-bucket-details.component'; @@ -24,6 +25,14 @@ import { RgwUserS3KeyModalComponent } from './rgw-user-s3-key-modal/rgw-user-s3- import { RgwUserSubuserModalComponent } from './rgw-user-subuser-modal/rgw-user-subuser-modal.component'; import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-user-swift-key-modal.component'; import { RgwUserTabsComponent } from './rgw-user-tabs/rgw-user-tabs.component'; +import { RgwMultisiteDetailsComponent } from './rgw-multisite-details/rgw-multisite-details.component'; +import { TreeModule } from '@circlon/angular-tree-component'; +import { DataTableModule } from '~/app/shared/datatable/datatable.module'; +import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service'; +import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service'; +import { RgwMultisiteRealmFormComponent } from './rgw-multisite-realm-form/rgw-multisite-realm-form.component'; +import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component'; +import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form/rgw-multisite-zone-form.component'; @NgModule({ imports: [ @@ -35,7 +44,9 @@ import { RgwUserTabsComponent } from './rgw-user-tabs/rgw-user-tabs.component'; NgbNavModule, RouterModule, NgbTooltipModule, - NgxPipeFunctionModule + NgxPipeFunctionModule, + TreeModule, + DataTableModule ], exports: [ RgwDaemonListComponent, @@ -61,7 +72,11 @@ import { RgwUserTabsComponent } from './rgw-user-tabs/rgw-user-tabs.component'; RgwUserCapabilityModalComponent, RgwUserSubuserModalComponent, RgwConfigModalComponent, - RgwUserTabsComponent + RgwUserTabsComponent, + RgwMultisiteDetailsComponent, + RgwMultisiteRealmFormComponent, + RgwMultisiteZonegroupFormComponent, + RgwMultisiteZoneFormComponent ] }) export class RgwModule {} @@ -122,6 +137,26 @@ const routes: Routes = [ data: { breadcrumbs: ActionLabels.EDIT } } ] + }, + { + path: 'multisite', + canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService], + data: { + moduleStatusGuardConfig: { + uiApiPath: 'rgw/multisite', + redirectTo: 'error', + header: 'Multi-site not configured', + button_name: 'Add Multi-site Configuration', + button_route: '/rgw/multisite/create', + button_title: 'Add multi-site configuration (realms/zonegroups/zones)', + secondary_button_name: 'Import Multi-site Configuration', + secondary_button_route: 'rgw/multisite/import', + secondary_button_title: + 'Import multi-site configuration (import realm token from a secondary cluster)' + }, + breadcrumbs: 'Multisite' + }, + children: [{ path: '', component: RgwMultisiteDetailsComponent }] } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html index 164c181dab4bb..674aaf983494f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html @@ -23,10 +23,14 @@
- +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts index d26bc6db43b76..ce959e13d0bd1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts @@ -28,6 +28,9 @@ export class ErrorComponent implements OnDestroy, OnInit { buttonRoute: string; buttonName: string; buttonTitle: string; + secondaryButtonRoute: string; + secondaryButtonName: string; + secondaryButtonTitle: string; component: string; constructor( @@ -81,6 +84,9 @@ export class ErrorComponent implements OnDestroy, OnInit { this.buttonRoute = history.state.button_route; this.buttonName = history.state.button_name; this.buttonTitle = history.state.button_title; + this.secondaryButtonRoute = history.state.secondary_button_route; + this.secondaryButtonName = history.state.secondary_button_name; + this.secondaryButtonTitle = history.state.secondary_button_title; this.component = history.state.component; this.docUrl = this.docService.urlGenerator(this.section); } catch (error) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index dd72a2493ce1e..0ea5e8dc3e388 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -278,6 +278,11 @@ Buckets +
  • + Multisite +
  • diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts new file mode 100644 index 0000000000000..35955143648c6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts @@ -0,0 +1,22 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { configureTestBed } from '~/testing/unit-test-helper'; + +import { RgwRealmService } from './rgw-realm.service'; + +describe('RgwRealmService', () => { + let service: RgwRealmService; + + configureTestBed({ + imports: [HttpClientTestingModule] + }); + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RgwRealmService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts new file mode 100644 index 0000000000000..db6733b293fe6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts @@ -0,0 +1,58 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RgwRealm } from '~/app/ceph/rgw/models/rgw-multisite'; +import { Icons } from '../enum/icons.enum'; +import { RgwDaemonService } from './rgw-daemon.service'; + +@Injectable({ + providedIn: 'root' +}) +export class RgwRealmService { + private url = 'api/rgw/realm'; + + constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {} + + create(realm: RgwRealm, defaultRealm: boolean) { + return this.rgwDaemonService.request((params: HttpParams) => { + params = params.appendAll({ + realm_name: realm.name, + default: defaultRealm + }); + return this.http.post(`${this.url}`, null, { params: params }); + }); + } + + list(): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}`); + }); + } + + get(realm: RgwRealm): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}/${realm.name}`); + }); + } + + getAllRealmsInfo(): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}/get_all_realms_info`); + }); + } + + getRealmTree(realm: RgwRealm, defaultRealmId: string) { + let nodes = {}; + let realmIds = []; + nodes['id'] = realm.id; + realmIds.push(realm.id); + nodes['name'] = realm.name + ' (realm)'; + nodes['info'] = realm; + nodes['is_default'] = realm.id === defaultRealmId ? true : false; + nodes['icon'] = Icons.reweight; + return { + nodes: nodes, + realmIds: realmIds + }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts new file mode 100644 index 0000000000000..24cbcc5151856 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts @@ -0,0 +1,22 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { configureTestBed } from '~/testing/unit-test-helper'; + +import { RgwZoneService } from './rgw-zone.service'; + +describe('RgwZoneService', () => { + let service: RgwZoneService; + + configureTestBed({ + imports: [HttpClientTestingModule] + }); + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RgwZoneService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts new file mode 100644 index 0000000000000..b6c1d360a7a2b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts @@ -0,0 +1,72 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite'; +import { Icons } from '../enum/icons.enum'; +import { RgwDaemonService } from './rgw-daemon.service'; + +@Injectable({ + providedIn: 'root' +}) +export class RgwZoneService { + private url = 'api/rgw/zone'; + + constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {} + + create( + zone: RgwZone, + zonegroup: RgwZonegroup, + defaultZone: boolean, + master: boolean, + endpoints: Array, + user: string + ) { + return this.rgwDaemonService.request((params: HttpParams) => { + params = params.appendAll({ + zone_name: zone.name, + zonegroup_name: zonegroup.name, + default: defaultZone, + master: master, + zone_endpoints: endpoints, + user: user + }); + return this.http.post(`${this.url}`, null, { params: params }); + }); + } + + list(): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}`); + }); + } + + get(zone: RgwZone): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}/${zone.name}`); + }); + } + + getAllZonesInfo(): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}/get_all_zones_info`); + }); + } + + getZoneTree(zone: RgwZone, defaultZoneId: string, zonegroup?: RgwZonegroup, realm?: RgwRealm) { + let nodes = {}; + let zoneIds = []; + nodes['id'] = zone.id; + zoneIds.push(zone.id); + nodes['name'] = zone.name + ' (zone)'; + nodes['info'] = zone; + nodes['icon'] = Icons.deploy; + nodes['parent'] = zonegroup ? zonegroup.name : ''; + nodes['second_parent'] = realm ? realm.name : ''; + nodes['is_default'] = zone.id === defaultZoneId ? true : false; + nodes['is_master'] = zonegroup && zonegroup.master_zone === zone.id ? true : false; + return { + nodes: nodes, + zoneIds: zoneIds + }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts new file mode 100644 index 0000000000000..aec80e01706b5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts @@ -0,0 +1,22 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { configureTestBed } from '~/testing/unit-test-helper'; + +import { RgwZonegroupService } from './rgw-zonegroup.service'; + +describe('RgwZonegroupService', () => { + let service: RgwZonegroupService; + + configureTestBed({ + imports: [HttpClientTestingModule] + }); + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RgwZonegroupService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts new file mode 100644 index 0000000000000..b8839fa93fda7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts @@ -0,0 +1,58 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RgwRealm, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite'; +import { Icons } from '../enum/icons.enum'; +import { RgwDaemonService } from './rgw-daemon.service'; + +@Injectable({ + providedIn: 'root' +}) +export class RgwZonegroupService { + private url = 'api/rgw/zonegroup'; + + constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {} + + create(realm: RgwRealm, zonegroup: RgwZonegroup, defaultZonegroup: boolean, master: boolean) { + return this.rgwDaemonService.request((params: HttpParams) => { + params = params.appendAll({ + realm_name: realm.name, + zonegroup_name: zonegroup.name, + default: defaultZonegroup, + master: master, + zonegroup_endpoints: zonegroup.endpoints + }); + return this.http.post(`${this.url}`, null, { params: params }); + }); + } + + list(): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}`); + }); + } + + get(zonegroup: RgwZonegroup): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}/${zonegroup.name}`); + }); + } + + getAllZonegroupsInfo(): Observable { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}/get_all_zonegroups_info`); + }); + } + + getZonegroupTree(zonegroup: RgwZonegroup, defaultZonegroupId: string, realm?: RgwRealm) { + let nodes = {}; + nodes['id'] = zonegroup.id; + nodes['name'] = zonegroup.name + ' (zonegroup)'; + nodes['info'] = zonegroup; + nodes['icon'] = Icons.cubes; + nodes['is_master'] = zonegroup.is_master; + nodes['parent'] = realm ? realm.name : ''; + nodes['is_default'] = zonegroup.id === defaultZonegroupId ? true : false; + return nodes; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts index 4248be8f59c20..cdc81d6d1ed74 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts @@ -137,11 +137,17 @@ export class ActionLabelsI18n { REDEPLOY: string; RESTART: string; RESYNC: string; + EXPORT: string; + IMPORT: any; constructor() { /* Create a new item */ this.CREATE = $localize`Create`; + this.EXPORT = $localize`Export`; + + this.IMPORT = $localize`Import`; + /* Destroy an existing item */ this.DELETE = $localize`Delete`; @@ -224,6 +230,8 @@ export class SucceededActionLabelsI18n { CANCELED: string; PREVIEWED: string; MOVED: string; + EXPORT: string; + IMPORT: string; COPIED: string; CLONED: string; DEEP_SCRUBBED: string; @@ -303,3 +311,14 @@ export class SucceededActionLabelsI18n { this.RESTART = $localize`Restart`; } } + +@Injectable({ + providedIn: 'root' +}) +export class TimerServiceInterval { + TIMER_SERVICE_PERIOD: number; + + constructor() { + this.TIMER_SERVICE_PERIOD = 5000; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index a08bfcecc3603..5dcf82d3c8e24 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -71,6 +71,7 @@ export enum Icons { exit = 'fa fa-sign-out', // Exit restart = 'fa fa-history', // Restart deploy = 'fa fa-cube', // Deploy, Redeploy + cubes = 'fa fa-cubes', /* Icons for special effect */ large = 'fa fa-lg', // icon becomes 33% larger diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts index df6f4854e1967..a4d5028759554 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts @@ -83,6 +83,9 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild { button_name: config.button_name, button_route: config.button_route, button_title: config.button_title, + secondary_button_name: config.secondary_button_name, + secondary_button_route: config.secondary_button_route, + secondary_button_title: config.secondary_button_title, uiConfig: config.uiConfig, uiApiPath: config.uiApiPath, icon: Icons.wrench, diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss index 941f639a363c1..58c50f14d9a31 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss @@ -121,6 +121,7 @@ $screen-sm-min: 576px !default; $screen-md-min: 768px !default; $screen-lg-min: 992px !default; $screen-xl-min: 1200px !default; +$tree-container-height: 200px !default; $screen-xs-max: calc(#{$screen-sm-min} - 1px) !default; $screen-sm-max: calc(#{$screen-md-min} - 1px) !default; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index e8d18474b85b7..3b1e4de123728 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -8136,6 +8136,128 @@ paths: - jwt: [] tags: - RgwDaemon + /api/rgw/realm: + get: + parameters: + - allowEmptyValue: true + in: query + name: daemon_name + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwRealm + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + daemon_name: + type: string + default: + type: string + realm_name: + type: string + required: + - realm_name + - default + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwRealm + /api/rgw/realm/get_all_realms_info: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwRealm + /api/rgw/realm/{realm_name}: + get: + parameters: + - in: path + name: realm_name + required: true + schema: + type: string + - allowEmptyValue: true + in: query + name: daemon_name + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwRealm /api/rgw/site: get: parameters: @@ -8815,6 +8937,265 @@ paths: - jwt: [] tags: - RgwUser + /api/rgw/zone: + get: + parameters: + - allowEmptyValue: true + in: query + name: daemon_name + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZone + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + daemon_name: + type: string + default: + default: false + type: boolean + master: + default: false + type: boolean + user: + type: string + zone_endpoints: + type: string + zone_name: + type: string + zonegroup_name: + type: string + required: + - zone_name + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZone + /api/rgw/zone/get_all_zones_info: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZone + /api/rgw/zone/{zone_name}: + get: + parameters: + - in: path + name: zone_name + required: true + schema: + type: string + - allowEmptyValue: true + in: query + name: daemon_name + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZone + /api/rgw/zonegroup: + get: + parameters: + - allowEmptyValue: true + in: query + name: daemon_name + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZonegroup + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + daemon_name: + type: string + default: + type: string + master: + type: string + realm_name: + type: string + zonegroup_endpoints: + type: string + zonegroup_name: + type: string + required: + - realm_name + - zonegroup_name + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZonegroup + /api/rgw/zonegroup/get_all_zonegroups_info: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZonegroup + /api/rgw/zonegroup/{zonegroup_name}: + get: + parameters: + - in: path + name: zonegroup_name + required: true + schema: + type: string + - allowEmptyValue: true + in: query + name: daemon_name + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZonegroup /api/role: get: parameters: [] @@ -10856,10 +11237,16 @@ tags: name: RgwMirrorPerfCounter - description: Rgw Perf Counters Management API name: RgwPerfCounter +- description: '*No description available*' + name: RgwRealm - description: RGW Site Management API name: RgwSite - description: RGW User Management API name: RgwUser +- description: '*No description available*' + name: RgwZone +- description: '*No description available*' + name: RgwZonegroup - description: Role Management API name: Role - description: Service Management API diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index bf65dd2efe461..d4008faacb33c 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +# pylint: disable=C0302 +# pylint: disable=too-many-branches +# pylint: disable=too-many-lines import ipaddress import json @@ -579,18 +582,245 @@ class RgwClient(RestClient): realms_info = self._get_realms_info() if 'realms' in realms_info and realms_info['realms']: return realms_info['realms'] - return [] - def get_default_realm(self) -> str: + def get_default_realm(self): realms_info = self._get_realms_info() if 'default_info' in realms_info and realms_info['default_info']: realm_info = self._get_realm_info(realms_info['default_info']) if 'name' in realm_info and realm_info['name']: return realm_info['name'] - raise DashboardException(msg='Default realm not found.', - http_status_code=404, - component='rgw') + return None + + def create_realm(self, realm_name: str, default: bool): + rgw_realm_create_cmd = ['realm', 'create'] + cmd_create_realm_options = ['--rgw-realm', realm_name] + if default != 'false': + cmd_create_realm_options.append('--default') + rgw_realm_create_cmd += cmd_create_realm_options + try: + exit_code, _, _ = mgr.send_rgwadmin_command(rgw_realm_create_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to create realm', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + def list_realms(self): + rgw_realm_list = {} + rgw_realm_list_cmd = ['realm', 'list'] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_list_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to fetch realm list', + http_status_code=500, component='rgw') + rgw_realm_list = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return rgw_realm_list + + def get_realm(self, realm_name: str): + realm_info = {} + rgw_realm_info_cmd = ['realm', 'get', '--rgw-realm', realm_name] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_info_cmd) + if exit_code > 0: + raise DashboardException('Unable to get realm info', + http_status_code=500, component='rgw') + realm_info = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return realm_info + + def get_all_realms_info(self): + all_realms_info = {} + realms_info = [] + rgw_realm_list = self.list_realms() + if 'realms' in rgw_realm_list: + if rgw_realm_list['realms'] != []: + for rgw_realm in rgw_realm_list['realms']: + realm_info = self.get_realm(rgw_realm) + realms_info.append(realm_info) + all_realms_info['realms'] = realms_info # type: ignore + else: + all_realms_info['realms'] = [] # type: ignore + if 'default_info' in rgw_realm_list and rgw_realm_list['default_info'] != '': + all_realms_info['default_realm'] = rgw_realm_list['default_info'] # type: ignore + else: + all_realms_info['default_realm'] = '' # type: ignore + return all_realms_info + + def update_period(self): + rgw_update_period_cmd = ['period', 'update', '--commit'] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_update_period_cmd) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to update period', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + def create_zonegroup(self, realm_name: str, zonegroup_name: str, + default: bool, master: bool, endpoints: List[str]): + rgw_zonegroup_create_cmd = ['zonegroup', 'create'] + cmd_create_zonegroup_options = ['--rgw-zonegroup', zonegroup_name] + if realm_name != 'null': + cmd_create_zonegroup_options.append('--rgw-realm') + cmd_create_zonegroup_options.append(realm_name) + if default != 'false': + cmd_create_zonegroup_options.append('--default') + if master != 'false': + cmd_create_zonegroup_options.append('--master') + if endpoints != 'null': # type: ignore + if isinstance(endpoints, list) and len(endpoints) > 1: + endpoint = ','.join(endpoints) + else: + endpoint = endpoints # type: ignore + cmd_create_zonegroup_options.append('--endpoints') + cmd_create_zonegroup_options.append(endpoint) + rgw_zonegroup_create_cmd += cmd_create_zonegroup_options + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_create_cmd) + if exit_code > 0: + raise DashboardException('Unable to get realm info', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + self.update_period() + return out + + def list_zonegroups(self): + rgw_zonegroup_list = {} + rgw_zonegroup_list_cmd = ['zonegroup', 'list'] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_list_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to fetch zonegroup list', + http_status_code=500, component='rgw') + rgw_zonegroup_list = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return rgw_zonegroup_list + + def get_zonegroup(self, zonegroup_name: str): + zonegroup_info = {} + rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup', zonegroup_name] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_info_cmd) + if exit_code > 0: + raise DashboardException('Unable to get zonegroup info', + http_status_code=500, component='rgw') + zonegroup_info = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return zonegroup_info + + def get_all_zonegroups_info(self): + all_zonegroups_info = {} + zonegroups_info = [] + rgw_zonegroup_list = self.list_zonegroups() + if 'zonegroups' in rgw_zonegroup_list: + if rgw_zonegroup_list['zonegroups'] != []: + for rgw_zonegroup in rgw_zonegroup_list['zonegroups']: + zonegroup_info = self.get_zonegroup(rgw_zonegroup) + zonegroups_info.append(zonegroup_info) + all_zonegroups_info['zonegroups'] = zonegroups_info # type: ignore + else: + all_zonegroups_info['zonegroups'] = [] # type: ignore + if 'default_info' in rgw_zonegroup_list and rgw_zonegroup_list['default_info'] != '': + all_zonegroups_info['default_zonegroup'] = rgw_zonegroup_list['default_info'] + else: + all_zonegroups_info['default_zonegroup'] = '' # type: ignore + return all_zonegroups_info + + def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, user): + if user != 'null': + access_key, secret_key = _get_user_keys(user) + else: + access_key = None # type: ignore + secret_key = None # type: ignore + rgw_zone_create_cmd = ['zone', 'create'] + cmd_create_zone_options = ['--rgw-zone', zone_name] + if zonegroup_name != 'null': + cmd_create_zone_options.append('--rgw-zonegroup') + cmd_create_zone_options.append(zonegroup_name) + if default != 'false': + cmd_create_zone_options.append('--default') + if master != 'false': + cmd_create_zone_options.append('--master') + if endpoints != 'null': + cmd_create_zone_options.append('--endpoints') + cmd_create_zone_options.append(endpoints) + if access_key is not None: + cmd_create_zone_options.append('--access-key') + cmd_create_zone_options.append(access_key) + if secret_key is not None: + cmd_create_zone_options.append('--secret') + cmd_create_zone_options.append(secret_key) + rgw_zone_create_cmd += cmd_create_zone_options + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_create_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to create zone', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + self.update_period() + return out + + def list_zones(self): + rgw_zone_list = {} + rgw_zone_list_cmd = ['zone', 'list'] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_list_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to fetch zone list', + http_status_code=500, component='rgw') + rgw_zone_list = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return rgw_zone_list + + def get_zone(self, zone_name: str): + zone_info = {} + rgw_zone_info_cmd = ['zone', 'get', '--rgw-zone', zone_name] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_info_cmd) + if exit_code > 0: + raise DashboardException('Unable to get zone info', + http_status_code=500, component='rgw') + zone_info = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return zone_info + + def get_all_zones_info(self): + all_zones_info = {} + zones_info = [] + rgw_zone_list = self.list_zones() + if 'zones' in rgw_zone_list: + if rgw_zone_list['zones'] != []: + for rgw_zone in rgw_zone_list['zones']: + zone_info = self.get_zone(rgw_zone) + zones_info.append(zone_info) + all_zones_info['zones'] = zones_info # type: ignore + else: + all_zones_info['zones'] = [] + if 'default_info' in rgw_zone_list and rgw_zone_list['default_info'] != '': + all_zones_info['default_zone'] = rgw_zone_list['default_info'] # type: ignore + else: + all_zones_info['default_zone'] = '' # type: ignore + return all_zones_info + + def get_multisite_status(self): + is_multisite_configured = True + rgw_realm_list = self.list_realms() + rgw_zonegroup_list = self.list_zonegroups() + rgw_zone_list = self.list_zones() + if len(rgw_realm_list['realms']) < 1 and len(rgw_zonegroup_list['zonegroups']) < 1 \ + and len(rgw_zone_list['zones']) < 1: + is_multisite_configured = False + return is_multisite_configured @RestClient.api_get('/{bucket_name}?versioning') def get_bucket_versioning(self, bucket_name, request=None): -- 2.39.5