]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Dashboard RGW multisite configuration 49953/head
authorAashish Sharma <aasharma@redhat.com>
Wed, 1 Feb 2023 05:26:31 +0000 (10:56 +0530)
committeravanthakkar <avanjohn@gmail.com>
Thu, 9 Mar 2023 09:18:56 +0000 (14:48 +0530)
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
35 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts
src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index 710a4980f7cd3b08bd08250e20fd8b871ac9995a..909bdecba5ad053ec2623ec55b4e4dfb0265bb79 100644 (file)
@@ -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')
index 6880a1561c1d9d281d264a41dfd286be8344d484..3059acb0aa718697b33bb4a5b218c37a41c7a54e 100644 (file)
@@ -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 (file)
index 0000000..9c324f4
--- /dev/null
@@ -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 (file)
index 0000000..0e2e2ac
--- /dev/null
@@ -0,0 +1,49 @@
+<div class="row">
+  <div class="col-sm-12 col-lg-12">
+    <div>
+      <cd-table-actions class="btn-group mb-4"
+                        [permission]="permission"
+                        [selection]="selection"
+                        [tableActions]="createTableActions">
+      </cd-table-actions>
+    </div>
+    <div class="card">
+      <div class="card-header"
+           i18n>Multi-site Topology viewer</div>
+      <div class="card-body">
+        <div class="row">
+          <div class="col-sm-6 col-lg-6 tree-container">
+            <i *ngIf="loadingIndicator"
+               [ngClass]="[icons.large, icons.spinner, icons.spin]"></i>
+            <tree-root #tree
+                       [nodes]="nodes"
+                       [options]="treeOptions"
+                       (updateData)="onUpdateData()">
+              <ng-template #treeNodeTemplate
+                           let-node>
+                <span *ngIf="node.data.name"
+                      class="me-3">
+                  <i [ngClass]="node.data.icon"></i>
+                    {{ node.data.name }}
+                </span>
+                <span class="badge badge-success me-2"
+                      *ngIf="node.data.is_default">
+                  default
+                </span>
+                <span class="badge badge-info me-2"
+                      *ngIf="node.data.is_master">
+                  master
+                </span>
+              </ng-template>
+            </tree-root>
+          </div>
+          <div class="col-sm-6 col-lg-6 metadata"
+               *ngIf="metadata">
+            <legend>{{ metadataTitle }}</legend>
+            <cd-table-key-value [data]="metadata"></cd-table-key-value>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
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 (file)
index 0000000..bbedb62
--- /dev/null
@@ -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 (file)
index 0000000..1b0c0d6
--- /dev/null
@@ -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<RgwMultisiteDetailsComponent>;
+  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 (file)
index 0000000..f11c00d
--- /dev/null
@@ -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 (file)
index 0000000..a3f95d9
--- /dev/null
@@ -0,0 +1,48 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="multisiteRealmForm"
+          #formDir="ngForm"
+          [formGroup]="multisiteRealmForm"
+          novalidate>
+    <div class="modal-body">
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="realmName"
+               i18n>Realm Name</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="Realm name..."
+                 id="realmName"
+                 name="realmName"
+                 formControlName="realmName">
+          <span class="invalid-feedback"
+                *ngIf="multisiteRealmForm.showError('realmName', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteRealmForm.showError('realmName', formDir, 'uniqueName')"
+                i18n>The chosen realm name is already in use.</span>
+          <div class="custom-control custom-checkbox">
+            <input class="form-check-input"
+                   id="default_realm"
+                   name="default_realm"
+                   formControlName="default_realm"
+                   type="checkbox">
+            <label class="form-check-label"
+                   for="default_realm"
+                   i18n>Default</label>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <cd-form-button-panel (submitActionEvent)="submit()"
+                            [form]="multisiteRealmForm"
+                            [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+    </div>
+    </form>
+  </ng-container>
+</cd-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..ec3c052
--- /dev/null
@@ -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<RgwMultisiteRealmFormComponent>;
+
+  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 (file)
index 0000000..18c52a5
--- /dev/null
@@ -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 (file)
index 0000000..6e09131
--- /dev/null
@@ -0,0 +1,112 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="multisiteZoneForm"
+          #formDir="ngForm"
+          [formGroup]="multisiteZoneForm"
+          novalidate>
+    <div class="modal-body">
+      <div class="form-group row">
+        <label class="cd-col-form-label"
+               for="selectedZonegroup"
+               i18n>Select Zonegroup</label>
+        <div class="cd-col-form-input">
+          <select class="form-select"
+                  id="selectedZonegroup"
+                  formControlName="selectedZonegroup"
+                  name="selectedZonegroup">
+            <option *ngFor="let zonegroupName of zonegroupList"
+                    [value]="zonegroupName.name"
+                    [selected]="zonegroupName.name === multisiteZoneForm.getValue('selectedZonegroup')">
+            {{ zonegroupName.name }}
+            </option>
+          </select>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zonegroupName"
+               i18n>ZoneName</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="Zone name..."
+                 id="zoneName"
+                 name="zoneName"
+                 formControlName="zoneName">
+          <span class="invalid-feedback"
+                *ngIf="multisiteZoneForm.showError('zoneName', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteZoneForm.showError('zoneName', formDir, 'uniqueName')"
+                i18n>The chosen zone name is already in use.</span>
+          <div class="custom-control custom-checkbox">
+            <input class="form-check-input"
+                   id="default_zone"
+                   name="default_zone"
+                   formControlName="default_zone"
+                   type="checkbox">
+            <label class="form-check-label"
+                   for="default_zone"
+                   i18n>Default</label><br>
+            <input class="form-check-input"
+                   id="master_zone"
+                   name="master_zone"
+                   formControlName="master_zone"
+                   type="checkbox">
+            <label class="form-check-label"
+                   for="master_zone"
+                   i18n>Master</label>
+          </div>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zone_endpoints"
+               i18n>Endpoints</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="e.g, http://ceph-node-00.com:80"
+                 id="zone_endpoints"
+                 name="zone_endpoints"
+                 formControlName="zone_endpoints">
+          <span class="invalid-feedback"
+                *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'endpoint')"
+                i18n>Please enter a valid IP address.</span>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label"
+               for="users"
+               i18n>System User</label>
+        <div class="cd-col-form-input">
+          <select id="users"
+                  name="users"
+                  class="form-select"
+                  formControlName="users">
+          <option i18n
+                  *ngIf="users === null"
+                  [ngValue]="null">Loading...</option>
+          <option i18n
+                  *ngIf="users !== null"
+                  [ngValue]="null">-- Select a user --</option>
+          <option *ngFor="let user of users"
+                  [value]="user.user_id">{{ user.user_id }}</option>
+          </select>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <cd-form-button-panel (submitActionEvent)="submit()"
+                            [form]="multisiteZoneForm"
+                            [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+    </div>
+    </form>
+  </ng-container>
+</cd-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..977cf2d
--- /dev/null
@@ -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<RgwMultisiteZoneFormComponent>;
+
+  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 (file)
index 0000000..fa89a16
--- /dev/null
@@ -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 (file)
index 0000000..6dfc9eb
--- /dev/null
@@ -0,0 +1,94 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="multisiteZonegroupForm"
+          #formDir="ngForm"
+          [formGroup]="multisiteZonegroupForm"
+          novalidate>
+    <div class="modal-body">
+      <div class="form-group row">
+        <label class="cd-col-form-label"
+               for="selectedRealm"
+               i18n>Select Realm</label>
+        <div class="cd-col-form-input">
+          <select class="form-select"
+                  id="selectedRealm"
+                  formControlName="selectedRealm"
+                  name="selectedRealm">
+          <option ngValue=""
+                  i18n>-- Select a realm --</option>
+          <option *ngFor="let realmName of realmList"
+                  [value]="realmName.name"
+                  [selected]="realmName.name === multisiteZonegroupForm.getValue('selectedRealm')">
+                {{ realmName.name }}
+          </option>
+          </select>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zonegroupName"
+               i18n>Zonegroup Name</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="Zonegroup name..."
+                 id="zonegroupName"
+                 name="zonegroupName"
+                 formControlName="zonegroupName">
+          <span class="invalid-feedback"
+                *ngIf="multisiteZonegroupForm.showError('zonegroupName', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteZonegroupForm.showError('zonegroupName', formDir, 'uniqueName')"
+                i18n>The chosen zonegroup name is already in use.</span>
+        <div class="custom-control custom-checkbox">
+          <input class="form-check-input"
+                 id="default_zonegroup"
+                 name="default_zonegroup"
+                 formControlName="default_zonegroup"
+                 type="checkbox">
+          <label class="form-check-label"
+                 for="default_zonegroup"
+                 i18n>Default</label><br>
+          <input class="form-check-input"
+                 id="master_zonegroup"
+                 name="master_zonegroup"
+                 formControlName="master_zonegroup"
+                 type="checkbox">
+          <label class="form-check-label"
+                 for="master_zonegroup"
+                 i18n>Master</label>
+        </div>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zonegroup_endpoints"
+               i18n>Endpoints</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="e.g, http://ceph-node-00.com:80"
+                 id="zonegroup_endpoints"
+                 name="zonegroup_endpoints"
+                 formControlName="zonegroup_endpoints">
+        <span class="invalid-feedback"
+              *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'required')"
+              i18n>This field is required.</span>
+        <span class="invalid-feedback"
+              *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
+              i18n>Please enter a valid IP address.</span>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <cd-form-button-panel (submitActionEvent)="submit()"
+                            [form]="multisiteZonegroupForm"
+                            [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+    </div>
+    </form>
+  </ng-container>
+</cd-modal>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..bbd8257
--- /dev/null
@@ -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<RgwMultisiteZonegroupFormComponent>;
+
+  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 (file)
index 0000000..792674d
--- /dev/null
@@ -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;
+  }
+}
index edffa8e856b756a7c9a96df8f3d9102474bcfe70..6bd374c3baebb75d29ad96ef700d4da08ec1e41d 100644 (file)
@@ -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 }]
   }
 ];
 
index 164c181dab4bb76ad865ad9ed1c2dea5746a23bf..674aaf983494fb2ee011db03ec83c8e68e5122ef 100644 (file)
       <div class="mt-4">
         <div class="text-center"
              *ngIf="(buttonName && buttonRoute) || uiConfig; else dashboardButton">
-          <button class="btn btn-primary"
+          <button class="btn btn-primary ms-1"
                   [routerLink]="buttonRoute"
                   *ngIf="!uiConfig; else configureButtonTpl"
                   i18n>{{ buttonName }}</button>
+          <button class="btn btn-light ms-1"
+                  [routerLink]="secondaryButtonRoute"
+                  *ngIf="secondaryButtonName && secondaryButtonRoute"
+                  i18n>{{ secondaryButtonName }}</button>
         </div>
       </div>
     </div>
index d26bc6db43b764ee518383e4cfb866cd80814afa..ce959e13d0bd11064a80d3d1a82648d2b0fee6b3 100644 (file)
@@ -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) {
index dd72a2493ce1ef167370db973cbd4769fb816a22..0ea5e8dc3e38871957af8164b5d5c89cbe82b564 100644 (file)
             <a i18n
                routerLink="/rgw/bucket">Buckets</a>
           </li>
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_rgw_buckets">
+            <a i18n
+               routerLink="/rgw/multisite">Multisite</a>
+          </li>
         </ul>
       </li>
     </ng-container>
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 (file)
index 0000000..3595514
--- /dev/null
@@ -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 (file)
index 0000000..db6733b
--- /dev/null
@@ -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<object> {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get<object>(`${this.url}`);
+    });
+  }
+
+  get(realm: RgwRealm): Observable<RgwRealm> {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get(`${this.url}/${realm.name}`);
+    });
+  }
+
+  getAllRealmsInfo(): Observable<object> {
+    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 (file)
index 0000000..24cbcc5
--- /dev/null
@@ -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 (file)
index 0000000..b6c1d36
--- /dev/null
@@ -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<string>,
+    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<object> {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get<object>(`${this.url}`);
+    });
+  }
+
+  get(zone: RgwZone): Observable<RgwZone> {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get(`${this.url}/${zone.name}`);
+    });
+  }
+
+  getAllZonesInfo(): Observable<object> {
+    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 (file)
index 0000000..aec80e0
--- /dev/null
@@ -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 (file)
index 0000000..b8839fa
--- /dev/null
@@ -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<object> {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get<object>(`${this.url}`);
+    });
+  }
+
+  get(zonegroup: RgwZonegroup): Observable<RgwZonegroup> {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get(`${this.url}/${zonegroup.name}`);
+    });
+  }
+
+  getAllZonegroupsInfo(): Observable<object> {
+    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;
+  }
+}
index 4248be8f59c20121fec34d7f3f341f9f6754e113..cdc81d6d1ed7445e694bf1075244d29558c2f06b 100644 (file)
@@ -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;
+  }
+}
index a08bfcecc3603f31e6f05b6245fe4fdb2f78e65f..5dcf82d3c8e2430b2405fb554e68922703e0de94 100644 (file)
@@ -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
index df6f4854e1967d45090387c48ffdd0fb65521016..a4d50287595547b3c9409967994af32ad27bc9b6 100644 (file)
@@ -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,
index 941f639a363c14a022aa109a3fe0959b53df1977..58c50f14d9a31299ee9134ce99ab675d8c7ef54f 100644 (file)
@@ -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;
index e8d18474b85b7c0765ab973b072a5495ec5497ec..3b1e4de123728ba39585140d30cce58c440a419b 100644 (file)
@@ -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
index bf65dd2efe46153f7e5d6f7d9da1182a4fc785d5..d4008faacb33c187a974bd03cb7218b797cb055c 100644 (file)
@@ -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):