]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: add support for editing RGW zone
authoravanthakkar <avanjohn@gmail.com>
Thu, 30 Mar 2023 17:18:52 +0000 (22:48 +0530)
committerAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Thu, 10 Aug 2023 12:03:28 +0000 (17:33 +0530)
Fixes: https://tracker.ceph.com/issues/59328
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
Co-authored-by: Aashish Sharma <aasharma@redhat.com>
(cherry picked from commit bcc92adb96d1ab8155d30cb51933b0d07b398cdc)

22 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.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/shared/api/rgw-multisite.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/requirements-test.txt
src/pybind/mgr/dashboard/services/rgw_client.py

index 4e01fb9ea3abc75936f5bbcf976d6d8b4b827f7d..80f6b6ad575e86b2fdb8a87804db830e8ee1e538 100644 (file)
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, NamedTuple, Optional, Union
 
 import cherrypy
 
+from .. import mgr
 from ..exceptions import DashboardException
 from ..rest_client import RequestException
 from ..security import Permission, Scope
@@ -13,11 +14,11 @@ from ..services.auth import AuthManager, JwtManager
 from ..services.ceph_service import CephService
 from ..services.rgw_client import NoRgwDaemonsException, RgwClient
 from ..tools import json_str_to_object, str_to_bool
-from . import APIDoc, APIRouter, BaseController, CRUDCollectionMethod, \
-    CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, RESTController, \
-    UIRouter, allow_empty_body
-from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, MethodType, \
-    TableAction, Validator, VerticalContainer
+from . import APIDoc, APIRouter, BaseController, CreatePermission, \
+    CRUDCollectionMethod, CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, \
+    RESTController, UIRouter, allow_empty_body
+from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, MethodType, TableAction, \
+    Validator, VerticalContainer
 from ._version import APIVersion
 
 logger = logging.getLogger("controllers.rgw")
@@ -97,6 +98,17 @@ class RgwStatus(BaseController):
             raise DashboardException(e, http_status_code=404, component='rgw')
         return status
 
+    @Endpoint()
+    @ReadPermission
+    # pylint: disable=R0801
+    def sync_status(self):
+        try:
+            instance = RgwClient.admin_instance()
+            result = instance.get_multisite_sync_status()
+        except NoRgwDaemonsException as e:
+            raise DashboardException(e, http_status_code=404, component='rgw')  # noqa: E501 pylint: disable=line-too-long
+        return result
+
 
 @APIRouter('/rgw/daemon', Scope.RGW)
 @APIDoc("RGW Daemon Management API", "RgwDaemon")
@@ -838,11 +850,13 @@ 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):
+               zone_endpoints=None, user=None, createSystemUser=False, daemon_name=None,
+               master_zone_of_master_zonegroup=None):
         try:
             instance = RgwClient.admin_instance(daemon_name=daemon_name)
             result = instance.create_zone(zone_name, zonegroup_name, default,
-                                          master, zone_endpoints, user)
+                                          master, zone_endpoints, user, createSystemUser,
+                                          master_zone_of_master_zonegroup)
             return result
         except NoRgwDaemonsException as e:
             raise DashboardException(e, http_status_code=404, component='rgw')
@@ -884,3 +898,53 @@ class RgwZone(RESTController):
             return result
         except NoRgwDaemonsException as e:
             raise DashboardException(e, http_status_code=404, component='rgw')
+
+    @allow_empty_body
+    # pylint: disable=W0613,W0102
+    def set(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '',
+            master: str = '', zone_endpoints: List[str] = [], user: str = '',
+            placement_target: str = '', data_pool: str = '', index_pool: str = '',
+            data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
+            compression: str = '', daemon_name=None, master_zone_of_master_zonegroup=None):
+        try:
+            instance = RgwClient.admin_instance(daemon_name=daemon_name)
+            result = instance.edit_zone(zone_name, new_zone_name, zonegroup_name, default,
+                                        master, zone_endpoints, user, placement_target,
+                                        data_pool, index_pool, data_extra_pool, storage_class,
+                                        data_pool_class, compression,
+                                        master_zone_of_master_zonegroup)
+            return result
+        except NoRgwDaemonsException as e:
+            raise DashboardException(e, http_status_code=404, component='rgw')
+
+    @Endpoint()
+    @ReadPermission
+    def get_pool_names(self):
+        pool_names = []
+        ret, out, _ = mgr.check_mon_command({
+            'prefix': 'osd lspools',
+            'format': 'json',
+        })
+        if ret == 0 and out is not None:
+            pool_names = json.loads(out)
+        return pool_names
+
+    @Endpoint('PUT')
+    @CreatePermission
+    def create_system_user(self, userName: str, zoneName: str, daemon_name=None):
+        try:
+            instance = RgwClient.admin_instance(daemon_name=daemon_name)
+            result = instance.create_system_user(userName, zoneName)
+            return result
+        except NoRgwDaemonsException as e:
+            raise DashboardException(e, http_status_code=404, component='rgw')
+
+    @Endpoint()
+    @ReadPermission
+    def get_user_list(self, daemon_name=None, zoneName=None):
+        try:
+            instance = RgwClient.admin_instance(daemon_name=daemon_name)
+            result = instance.get_user_list(zoneName)
+            return result
+        except NoRgwDaemonsException as e:
+            raise DashboardException(e, http_status_code=404, component='rgw')
index 9c324f48727e2cb5e001a2372f74936fa5d4273e..fb0ce154900d8047c367b1b911ffe8a9eb14d37e 100644 (file)
@@ -43,4 +43,5 @@ export class RgwZone {
   placement_pools: any[];
   realm_id: string;
   notif_pool: string;
+  endpoints: string[];
 }
index 12ae56a2d0742cef7486dc2b45015c2465b5d252..3c65390a177e944121230070859cdcd8e7fc31b5 100644 (file)
@@ -9,7 +9,7 @@
     </div>
     <div class="card">
       <div class="card-header"
-           i18n>Multi-site Topology viewer</div>
+           i18n>Topology Viewer</div>
       <div class="card-body">
         <div class="row">
           <div class="col-sm-6 col-lg-6 tree-container">
                   <i [ngClass]="node.data.icon"></i>
                     {{ node.data.name }}
                 </span>
-                <span *ngIf="node.data.type"
-                      class="badge badge-info me-3">
-                    {{ node.data.type }}
-                </span>
                 <span class="badge badge-success me-2"
                       *ngIf="node.data.is_default">
                   default
@@ -45,8 +41,7 @@
                   <button type="button"
                           class="btn btn-light dropdown-toggle-split ms-1"
                           (click)="openModal(node, true)"
-                          [disabled]="getDisable()"
-                          ngbDropdownToggle>
+                          [disabled]="getDisable()">
                     <i [ngClass]="[icons.edit]"></i>
                   </button>
                   <button type="button"
           <div class="col-sm-6 col-lg-6 metadata"
                *ngIf="metadata">
             <legend>{{ metadataTitle }}</legend>
-            <cd-table-key-value [data]="metadata"></cd-table-key-value>
+            <cd-table-key-value cdTableDetail
+                                [data]="metadata"
+                                [renderObjects]="true"
+                                [customCss]="customCss">
+            </cd-table-key-value>
           </div>
         </div>
       </div>
index 173bdb4bd820e1134019f10585d101f4ceaea509..7f1c0b19769b1a6ca4ae973d5e5166a50f98b19f 100644 (file)
@@ -32,6 +32,6 @@ describe('RgwMultisiteDetailsComponent', () => {
 
   it('should display right title', () => {
     const span = debugElement.nativeElement.querySelector('.card-header');
-    expect(span.textContent).toBe('Multi-site Topology viewer');
+    expect(span.textContent).toBe('Topology Viewer');
   });
 });
index 46e732659c18d1ad13051c724e7071822fcd1384..0bcf88b8cd2c03cea5fd4ad1c9bfbdd2e763fb03 100644 (file)
                    id="default_realm"
                    name="default_realm"
                    formControlName="default_realm"
+                   [attr.disabled]="action === 'edit' ? true: null"
                    type="checkbox">
             <label class="form-check-label"
                    for="default_realm"
                    i18n>Default</label>
-            <cd-helper *ngIf="multisiteRealmForm.get('default_realm').disabled">
-              <span i18n>You cannot unset the default flag. Please create another realm and set it as default.</span>
+            <cd-helper *ngIf="action === 'edit' && info.data.is_default">
+              <span i18n>You cannot unset the default flag.</span>
+            </cd-helper>
+            <cd-helper *ngIf="action === 'edit' && !info.data.is_default">
+              <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+            </cd-helper>
+            <cd-helper *ngIf="defaultRealmDisabled && action === 'create'">
+              <span i18n>Default realm already exists.</span>
             </cd-helper>
           </div>
         </div>
index b3d000c94fbde30c324c9b14fccc45faff65d61b..afbb29e8efedee7ea759f8b121c3ea93251152d9 100644 (file)
@@ -8,6 +8,7 @@ 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';
+import { DocService } from '~/app/shared/services/doc.service';
 
 @Component({
   selector: 'cd-rgw-multisite-realm-form',
@@ -23,14 +24,20 @@ export class RgwMultisiteRealmFormComponent implements OnInit {
   multisiteInfo: object[] = [];
   realm: RgwRealm;
   realmList: RgwRealm[] = [];
+  zonegroupList: RgwRealm[] = [];
   realmNames: string[];
   newRealmName: string;
+  isMaster: boolean;
+  defaultsInfo: string[];
+  defaultRealmDisabled = false;
+  docUrl: string;
 
   constructor(
     public activeModal: NgbActiveModal,
     public actionLabels: ActionLabelsI18n,
     public rgwRealmService: RgwRealmService,
-    public notificationService: NotificationService
+    public notificationService: NotificationService,
+    public docService: DocService
   ) {
     this.action = this.editing
       ? this.actionLabels.EDIT + this.resource
@@ -65,16 +72,30 @@ export class RgwMultisiteRealmFormComponent implements OnInit {
       return realm['name'];
     });
     if (this.action === 'edit') {
+      this.zonegroupList =
+        this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups')
+          ? this.multisiteInfo[1]['zonegroups']
+          : [];
       this.multisiteRealmForm.get('realmName').setValue(this.info.data.name);
       this.multisiteRealmForm.get('default_realm').setValue(this.info.data.is_default);
       if (this.info.data.is_default) {
         this.multisiteRealmForm.get('default_realm').disable();
       }
     }
+    this.zonegroupList.forEach((zgp: any) => {
+      if (zgp.is_master === true && zgp.realm_id === this.info.data.id) {
+        this.isMaster = true;
+      }
+    });
+    if (this.defaultsInfo && this.defaultsInfo['defaultRealmName'] !== null) {
+      this.multisiteRealmForm.get('default_realm').disable();
+      this.defaultRealmDisabled = true;
+    }
+    this.docUrl = this.docService.urlGenerator('rgw-multisite');
   }
 
   submit() {
-    const values = this.multisiteRealmForm.value;
+    const values = this.multisiteRealmForm.getRawValue();
     this.realm = new RgwRealm();
     if (this.action === 'create') {
       this.realm.name = values['realmName'];
index 715cb46b13fb8f8b65bb2960e41685f78a157765..0c4ec560d38f0013100cd6cb196a268b02fa92be 100644 (file)
@@ -15,6 +15,7 @@
         <div class="cd-col-form-input">
           <select class="form-select"
                   id="selectedZonegroup"
+                  [attr.disabled]="action === 'edit' ? true : null"
                   formControlName="selectedZonegroup"
                   name="selectedZonegroup"
                   (change)="onZoneGroupChange($event.target.value)">
@@ -29,7 +30,7 @@
       <div class="form-group row">
         <label class="cd-col-form-label required"
                for="zonegroupName"
-               i18n>ZoneName</label>
+               i18n>Zone Name</label>
         <div class="cd-col-form-input">
           <input class="form-control"
                  type="text"
                    id="default_zone"
                    name="default_zone"
                    formControlName="default_zone"
+                   [attr.disabled]="action === 'edit' ? true : null"
                    type="checkbox">
             <label class="form-check-label"
                    for="default_zone"
-                   i18n>Default</label><br>
+                   i18n>Default</label>
+            <span *ngIf="disableDefault && action === 'create'">
+              <cd-helper i18n>Default zone can only exist in a default zonegroup.
+              </cd-helper>
+            </span>
+            <span *ngIf="isDefaultZone">
+              <cd-helper i18n>You cannot unset the default flag.
+              </cd-helper>
+            </span>
+            <cd-helper *ngIf="action === 'edit' && !isDefaultZone">
+              <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+            </cd-helper><br>
+          </div>
+          <div class="custom-control custom-checkbox">
             <input class="form-check-input"
                    id="master_zone"
                    name="master_zone"
                    formControlName="master_zone"
+                   [attr.disabled]="action === 'edit' ? true : null"
                    type="checkbox">
             <label class="form-check-label"
                    for="master_zone"
                    i18n>Master</label>
+            <span *ngIf="disableMaster">
+              <cd-helper i18n>Master zone already exists for the selected zonegroup.
+              </cd-helper>
+            </span>
+            <span *ngIf="isMasterZone">
+              <cd-helper i18n>You cannot unset the master flag.
+              </cd-helper>
+            </span>
+            <cd-helper *ngIf="action === 'edit' && !isMasterZone">
+              <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+            </cd-helper>
           </div>
         </div>
       </div>
                 i18n>Please enter a valid IP address.</span>
         </div>
       </div>
-      <div class="form-group row">
+      <div class="form-group row"
+           *ngIf="action === 'edit'">
         <label class="cd-col-form-label"
                for="users"
                i18n>System User</label>
                   [ngValue]="null">-- Select a user --</option>
           <option *ngFor="let user of users"
                   [value]="user.user_id">{{ user.user_id }}</option>
-          </select>
+          </select><br><br>
+          <div *ngIf="info.data.zone_zonegroup.is_master && info.data.is_master">
+            <button type="button"
+                    class="btn btn-light"
+                    (click)="CreateSystemUser()">
+                    Create System User
+            </button>
+          </div>
+        </div>
+        <div *ngIf="action === 'edit'">
+          <legend>Placement Targets</legend>
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="placementTarget"
+                   i18n>Placement target</label>
+            <div class="cd-col-form-input">
+              <select class="form-select"
+                      id="placementTarget"
+                      formControlName="placementTarget"
+                      name="placementTarget"
+                      (change)="getZonePlacementData($event.target.value)">
+                <option *ngFor="let placement of placementTargets"
+                        [value]="placement.name"
+                        [selected]="placement.name === multisiteZoneForm.getValue('placementTarget')">
+                {{ placement.name }}
+                </option>
+              </select>
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="placementDataPool"
+                   i18n>Data pool</label>
+            <div class="cd-col-form-input">
+              <select class="form-select"
+                      id="placementDataPool"
+                      formControlName="placementDataPool"
+                      [value]="placementDataPool"
+                      name="placementDataPool">
+                <option *ngFor="let pool of poolList"
+                        [value]="pool.poolname"
+                        [selected]="pool.poolname === multisiteZoneForm.getValue('placementDataPool')">
+                {{ pool.poolname }}
+                </option>
+              </select>
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="placementIndexPool"
+                   i18n>Index pool</label>
+            <div class="cd-col-form-input">
+              <select class="form-select"
+                      id="placementIndexPool"
+                      formControlName="placementIndexPool"
+                      name="placementIndexPool">
+                <option *ngFor="let pool of poolList"
+                        [value]="pool.poolname"
+                        [selected]="pool.poolname === multisiteZoneForm.getValue('placementIndexPool')">
+                {{ pool.poolname }}
+                </option>
+              </select>
+            </div>
+          </div>
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="placementDataExtraPool"
+                   i18n>Data extra pool</label>
+            <div class="cd-col-form-input">
+              <select class="form-select"
+                      id="placementDataExtraPool"
+                      formControlName="placementDataExtraPool"
+                      name="placementDataExtraPool">
+                <option *ngFor="let pool of poolList"
+                        [value]="pool.poolname"
+                        [selected]="pool.poolname === multisiteZoneForm.getValue('placementDataExtraPool')">
+                {{ pool.poolname }}
+                </option>
+              </select>
+            </div>
+          </div>
+          <div>
+            <legend>Storage Classes</legend>
+            <div class="form-group row">
+              <label class="cd-col-form-label"
+                     for="storageClass"
+                     i18n>Storage Class</label>
+              <div class="cd-col-form-input">
+                <select class="form-select"
+                        id="storageClass"
+                        formControlName="storageClass"
+                        (change)="getStorageClassData($event.target.value)"
+                        name="storageClass">
+                  <option *ngFor="let str of storageClassList"
+                          [value]="str.value">
+                  {{ str.value }}
+                  </option>
+                </select>
+              </div>
+            </div>
+            <div class="form-group row">
+              <label class="cd-col-form-label"
+                     for="storageDataPool"
+                     i18n>Data pool</label>
+              <div class="cd-col-form-input">
+                <select class="form-select"
+                        id="storageDataPool"
+                        formControlName="storageDataPool"
+                        name="storageDataPool">
+                  <option *ngFor="let pool of poolList"
+                          [value]="pool.poolname"
+                          [selected]="pool.poolname === multisiteZoneForm.getValue('storageDataPool')">
+                  {{ pool.poolname }}
+                  </option>
+                </select>
+              </div>
+            </div>
+            <div class="form-group row">
+              <label class="cd-col-form-label"
+                     for="storageCompression"
+                     i18n>Compression</label>
+              <div class="cd-col-form-input">
+                <select class="form-select"
+                        id="storageCompression"
+                        formControlName="storageCompression"
+                        name="storageCompression">
+                  <option *ngFor="let compression of compressionTypes"
+                          [value]="compression">
+                  {{ compression }}
+                  </option>
+                </select>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
index 567c5f096dd9775fefc438d3ed3d578b6d728f95..7a0c7ecb9835be7830776ec4effe9d95c1a3dbc5 100644 (file)
@@ -1,7 +1,8 @@
 import { Component, OnInit } from '@angular/core';
 import { FormControl, Validators } from '@angular/forms';
-import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
 import { RgwUserService } from '~/app/shared/api/rgw-user.service';
 import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
@@ -11,6 +12,8 @@ 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';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { RgwSystemUserComponent } from '../rgw-system-user/rgw-system-user.component';
 
 @Component({
   selector: 'cd-rgw-multisite-zone-form',
@@ -22,6 +25,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
   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;
+  info: any;
   multisiteZoneForm: CdFormGroup;
   editing = false;
   resource: string;
@@ -33,15 +37,34 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
   zonegroupList: RgwZonegroup[] = [];
   zoneList: RgwZone[] = [];
   zoneNames: string[];
-  users: string[];
+  users: any;
+  placementTargets: any;
+  zoneInfo: RgwZone;
+  poolList: object[] = [];
+  storageClassList: object[] = [];
+  disableDefault: boolean = false;
+  disableMaster: boolean = false;
+  isMetadataSync: boolean = false;
+  isMasterZone: boolean;
+  isDefaultZone: boolean;
+  syncStatusTimedOut: boolean = false;
+  bsModalRef: NgbModalRef;
+  createSystemUser: boolean = false;
+  master_zone_of_master_zonegroup: RgwZone;
+  masterZoneUser: any;
+  access_key: any;
+  master_zonegroup_of_realm: RgwZonegroup;
+  compressionTypes = ['lz4', 'zlib', 'snappy'];
 
   constructor(
     public activeModal: NgbActiveModal,
     public actionLabels: ActionLabelsI18n,
+    public rgwMultisiteService: RgwMultisiteService,
     public rgwZoneService: RgwZoneService,
     public rgwZoneGroupService: RgwZonegroupService,
     public notificationService: NotificationService,
-    public rgwUserService: RgwUserService
+    public rgwUserService: RgwUserService,
+    public modalService: ModalService
   ) {
     this.action = this.editing
       ? this.actionLabels.EDIT + this.resource
@@ -55,14 +78,16 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
         validators: [
           Validators.required,
           CdValidators.custom('uniqueName', (zoneName: string) => {
-            return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1;
+            return (
+              this.action === 'create' && 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, {
+      zone_endpoints: new FormControl([], {
         validators: [
           CdValidators.custom('endpoint', (value: string) => {
             if (_.isEmpty(value)) {
@@ -83,10 +108,18 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
               }
               return false;
             }
-          })
+          }),
+          Validators.required
         ]
       }),
-      users: new FormControl(null)
+      users: new FormControl(null),
+      placementTarget: new FormControl(null),
+      placementDataPool: new FormControl(''),
+      placementIndexPool: new FormControl(null),
+      placementDataExtraPool: new FormControl(null),
+      storageClass: new FormControl(null),
+      storageDataPool: new FormControl(null),
+      storageCompression: new FormControl(null)
     });
   }
 
@@ -97,8 +130,38 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
       if (_.isEmpty(zonegroup.master_zone)) {
         this.multisiteZoneForm.get('master_zone').setValue(true);
         this.multisiteZoneForm.get('master_zone').disable();
+        this.disableMaster = false;
+      } else if (!_.isEmpty(zonegroup.master_zone) && this.action === 'create') {
+        this.multisiteZoneForm.get('master_zone').setValue(false);
+        this.multisiteZoneForm.get('master_zone').disable();
+        this.disableMaster = true;
+      }
+      const zonegroupInfo = this.zonegroupList.filter((zgroup: any) => zgroup.name === zg.name)[0];
+      if (zonegroupInfo) {
+        const realm_id = zonegroupInfo.realm_id;
+        this.master_zonegroup_of_realm = this.zonegroupList.filter(
+          (zg: any) => zg.realm_id === realm_id && zg.is_master === true
+        )[0];
+      }
+      if (this.master_zonegroup_of_realm) {
+        this.master_zone_of_master_zonegroup = this.zoneList.filter(
+          (zone: any) => zone.id === this.master_zonegroup_of_realm.master_zone
+        )[0];
+      }
+      if (this.master_zone_of_master_zonegroup) {
+        this.getUserInfo(this.master_zone_of_master_zonegroup);
+      }
+      if (zonegroupInfo.is_master && this.multisiteZoneForm.getValue('master_zone') === true) {
+        this.createSystemUser = true;
       }
     });
+    if (
+      this.multisiteZoneForm.getValue('selectedZonegroup') !==
+      this.defaultsInfo['defaultZonegroupName']
+    ) {
+      this.disableDefault = true;
+      this.multisiteZoneForm.get('default_zone').disable();
+    }
   }
 
   ngOnInit(): void {
@@ -114,42 +177,211 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
       return zone['name'];
     });
     if (this.action === 'create') {
+      if (this.defaultsInfo['defaultZonegroupName'] !== undefined) {
+        this.multisiteZoneForm
+          .get('selectedZonegroup')
+          .setValue(this.defaultsInfo['defaultZonegroupName']);
+        this.onZoneGroupChange(this.defaultsInfo['defaultZonegroupName']);
+      }
+    }
+    if (this.action === 'edit') {
+      this.placementTargets = this.info.parent ? this.info.parent.data.placement_targets : [];
+      this.rgwZoneService.getPoolNames().subscribe((pools: object[]) => {
+        this.poolList = pools;
+      });
+      this.multisiteZoneForm.get('zoneName').setValue(this.info.data.name);
+      this.multisiteZoneForm.get('selectedZonegroup').setValue(this.info.data.parent);
+      this.multisiteZoneForm.get('default_zone').setValue(this.info.data.is_default);
+      this.multisiteZoneForm.get('master_zone').setValue(this.info.data.is_master);
+      this.multisiteZoneForm.get('zone_endpoints').setValue(this.info.data.endpoints);
       this.multisiteZoneForm
-        .get('selectedZonegroup')
-        .setValue(this.defaultsInfo['defaultZonegroupName']);
-      this.onZoneGroupChange(this.defaultsInfo['defaultZonegroupName']);
+        .get('placementTarget')
+        .setValue(this.info.parent.data.default_placement);
+      this.getZonePlacementData(this.multisiteZoneForm.getValue('placementTarget'));
+      if (this.info.data.is_default) {
+        this.isDefaultZone = true;
+        this.multisiteZoneForm.get('default_zone').disable();
+      }
+      if (this.info.data.is_master) {
+        this.isMasterZone = true;
+        this.multisiteZoneForm.get('master_zone').disable();
+      }
+      const zone = new RgwZone();
+      zone.name = this.info.data.name;
+      this.onZoneGroupChange(this.info.data.parent);
+      setTimeout(() => {
+        this.getUserInfo(zone);
+      }, 1500);
+    }
+    if (
+      this.multisiteZoneForm.getValue('selectedZonegroup') !==
+      this.defaultsInfo['defaultZonegroupName']
+    ) {
+      this.disableDefault = true;
+      this.multisiteZoneForm.get('default_zone').disable();
     }
-    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'];
+  getUserInfo(zone: RgwZone) {
     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 });
+      .getUserList(this.master_zone_of_master_zonegroup.name)
+      .subscribe((users: any) => {
+        this.users = users.filter((user: any) => user.keys.length !== 0);
+        this.rgwZoneService.get(zone).subscribe((zone: RgwZone) => {
+          const access_key = zone.system_key['access_key'];
+          const user = this.users.filter((user: any) => user.keys[0].access_key === access_key);
+          if (user.length > 0) {
+            this.multisiteZoneForm.get('users').setValue(user[0].user_id);
+          }
+          return user[0].user_id;
+        });
+      });
+  }
+
+  getZonePlacementData(placementTarget: string) {
+    this.zone = new RgwZone();
+    this.zone.name = this.info.data.name;
+    if (this.placementTargets) {
+      this.placementTargets.forEach((placement: any) => {
+        if (placement.name === placementTarget) {
+          let storageClasses = placement.storage_classes;
+          this.storageClassList = Object.entries(storageClasses).map(([key, value]) => ({
+            key,
+            value
+          }));
         }
-      );
+      });
+    }
+    this.rgwZoneService.get(this.zone).subscribe((zoneInfo: RgwZone) => {
+      this.zoneInfo = zoneInfo;
+      if (this.zoneInfo && this.zoneInfo['placement_pools']) {
+        this.zoneInfo['placement_pools'].forEach((plc_pool) => {
+          if (plc_pool.key === placementTarget) {
+            let storageClasses = plc_pool.val.storage_classes;
+            let placementDataPool = storageClasses['STANDARD']
+              ? storageClasses['STANDARD']['data_pool']
+              : '';
+            let placementIndexPool = plc_pool.val.index_pool;
+            let placementDataExtraPool = plc_pool.val.data_extra_pool;
+            this.poolList.push({ poolname: placementDataPool });
+            this.poolList.push({ poolname: placementIndexPool });
+            this.poolList.push({ poolname: placementDataExtraPool });
+            this.multisiteZoneForm.get('storageClass').setValue(this.storageClassList[0]['value']);
+            this.multisiteZoneForm.get('storageDataPool').setValue(placementDataPool);
+            this.multisiteZoneForm.get('storageCompression').setValue(this.compressionTypes[0]);
+            this.multisiteZoneForm.get('placementDataPool').setValue(placementDataPool);
+            this.multisiteZoneForm.get('placementIndexPool').setValue(placementIndexPool);
+            this.multisiteZoneForm.get('placementDataExtraPool').setValue(placementDataExtraPool);
+          }
+        });
+      }
+    });
+  }
+
+  getStorageClassData(storageClass: string) {
+    let storageClassSelected = this.storageClassList.find((x) => x['value'] == storageClass)[
+      'value'
+    ];
+    this.poolList.push({ poolname: storageClassSelected.data_pool });
+    this.multisiteZoneForm.get('storageDataPool').setValue(storageClassSelected.data_pool);
+    this.multisiteZoneForm
+      .get('storageCompression')
+      .setValue(storageClassSelected.compression_type);
+  }
+
+  submit() {
+    const values = this.multisiteZoneForm.getRawValue();
+    if (this.action === 'create') {
+      this.zonegroup = new RgwZonegroup();
+      this.zonegroup.name = values['selectedZonegroup'];
+      this.zone = new RgwZone();
+      this.zone.name = values['zoneName'];
+      this.zone.endpoints = this.checkUrlArray(values['zone_endpoints']);
+      if (this.createSystemUser) {
+        values['users'] = values['zoneName'] + '_User';
+      }
+      this.rgwZoneService
+        .create(
+          this.zone,
+          this.zonegroup,
+          values['default_zone'],
+          values['master_zone'],
+          this.zone.endpoints,
+          values['users'],
+          this.createSystemUser,
+          this.master_zone_of_master_zonegroup
+        )
+        .subscribe(
+          () => {
+            this.notificationService.show(
+              NotificationType.success,
+              $localize`Zone: '${values['zoneName']}' created successfully`
+            );
+            this.activeModal.close();
+          },
+          () => {
+            this.multisiteZoneForm.setErrors({ cdSubmitButton: true });
+          }
+        );
+    } else if (this.action === 'edit') {
+      this.zonegroup = new RgwZonegroup();
+      this.zonegroup.name = values['selectedZonegroup'];
+      this.zone = new RgwZone();
+      this.zone.name = this.info.data.name;
+      this.zone.endpoints =
+        values['zone_endpoints'] === this.info.data.endpoints
+          ? values['zonegroup_endpoints']
+          : this.checkUrlArray(values['zone_endpoints']);
+      this.rgwZoneService
+        .update(
+          this.zone,
+          this.zonegroup,
+          values['zoneName'],
+          values['default_zone'],
+          values['master_zone'],
+          this.zone.endpoints,
+          values['users'],
+          values['placementTarget'],
+          values['placementDataPool'],
+          values['placementIndexPool'],
+          values['placementDataExtraPool'],
+          values['storageClass'],
+          values['storageDataPool'],
+          values['storageCompression'],
+          this.master_zone_of_master_zonegroup
+        )
+        .subscribe(
+          () => {
+            this.notificationService.show(
+              NotificationType.success,
+              $localize`Zone: '${values['zoneName']}' updated successfully`
+            );
+            this.activeModal.close();
+          },
+          () => {
+            this.multisiteZoneForm.setErrors({ cdSubmitButton: true });
+          }
+        );
+    }
+  }
+
+  checkUrlArray(endpoints: string) {
+    let endpointsArray = [];
+    if (endpoints.includes(',')) {
+      endpointsArray = endpoints.split(',');
+    } else {
+      endpointsArray.push(endpoints);
+    }
+    return endpointsArray;
+  }
+
+  CreateSystemUser() {
+    const initialState = {
+      zoneName: this.master_zone_of_master_zonegroup.name
+    };
+    this.bsModalRef = this.modalService.show(RgwSystemUserComponent, initialState);
+    this.bsModalRef.componentInstance.submitAction.subscribe(() => {
+      this.getUserInfo(this.master_zone_of_master_zonegroup);
+    });
   }
 }
index b3848052b5b0d7f72ab91c7e706ba317cb2ceb48..e028d513ffe5e688304ad2c2e17b55133fc87850 100644 (file)
                  id="default_zonegroup"
                  name="default_zonegroup"
                  formControlName="default_zonegroup"
+                 [attr.disabled]="action === 'edit' ? true : null"
                  type="checkbox">
           <label class="form-check-label"
                  for="default_zonegroup"
                  i18n>Default</label>
-          <span *ngIf="disableDefault">
-          <cd-helper i18n>Zonegroup doesn't belong to the default realm.</cd-helper>
-          </span><br>
+          <span *ngIf="disableDefault && action === 'create'">
+            <cd-helper i18n>Zonegroup doesn't belong to the default realm.</cd-helper>
+          </span>
+          <cd-helper *ngIf="action === 'edit' && !info.data.is_default">
+            <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+          </cd-helper>
+          <cd-helper *ngIf="action === 'edit' && info.data.is_default">
+            <span i18n>You cannot unset the default flag.</span>
+          </cd-helper><br>
           <input class="form-check-input"
                  id="master_zonegroup"
                  name="master_zonegroup"
                  formControlName="master_zonegroup"
+                 [attr.disabled]="action === 'edit' ? true : null"
                  type="checkbox">
           <label class="form-check-label"
                  for="master_zonegroup"
                  i18n>Master</label>
-          <span *ngIf="!isMaster">
-          <cd-helper i18n>RGW multi-site configuration must have a master zonegroup. Setting
-            the first zonegroup created as master, to avoid any errors on udating the period.
-            Can be modified later by editing a zonegroup.
-          </cd-helper>
-          </span>
-          <span *ngIf="disableMaster">
+          <span *ngIf="disableMaster && action === 'create'">
             <cd-helper i18n>Multiple master zonegroups can't be configured. If you want to create a new zonegroup and make it the master zonegroup, you must delete the default zonegroup.</cd-helper>
           </span>
+          <cd-helper *ngIf="action === 'edit' && !info.data.is_master">
+            <span i18n>Please consult the <a href="{{ docUrl }}">documentation</a> to follow the failover mechanism</span>
+          </cd-helper>
+          <cd-helper *ngIf="action === 'edit' && info.data.is_master">
+            <span i18n>You cannot unset the master flag.</span>
+          </cd-helper>
         </div>
         </div>
       </div>
index 2172a301e2cad09a0146b2662a4f49648d419384..be9e3f64859117a042fa2b55fca7d39b502a09d4 100644 (file)
@@ -137,9 +137,20 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
     this.zonegroupNames = this.zonegroupList.map((zonegroup) => {
       return zonegroup['name'];
     });
+    let allZonegroupZonesList = this.zonegroupList.map((zonegroup: RgwZonegroup) => {
+      return zonegroup['zones'];
+    });
+    const allZonegroupZonesInfo = allZonegroupZonesList.reduce(
+      (accumulator, value) => accumulator.concat(value),
+      []
+    );
+    const allZonegroupZonesNames = allZonegroupZonesInfo.map((zone) => {
+      return zone['name'];
+    });
     this.allZoneNames = this.zoneList.map((zone: RgwZone) => {
       return zone['name'];
     });
+    this.allZoneNames = _.difference(this.allZoneNames, allZonegroupZonesNames);
     if (this.action === 'create' && this.defaultsInfo['defaultRealmName'] !== null) {
       this.multisiteZonegroupForm
         .get('selectedRealm')
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html
new file mode 100644 (file)
index 0000000..86aa3d2
--- /dev/null
@@ -0,0 +1,37 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">Create System User</ng-container>
+
+  <ng-container class="modal-content">
+    <form name="multisiteSystemUserForm"
+          #formDir="ngForm"
+          [formGroup]="multisiteSystemUserForm"
+          novalidate>
+      <div class="modal-body">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="userName"
+                 i18n>User Name</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   placeholder="User name..."
+                   id="userName"
+                   name="userName"
+                   formControlName="userName">
+            <span class="invalid-feedback"
+                  *ngIf="multisiteSystemUserForm.showError('userName', formDir, 'required')"
+                  i18n>This field is required.</span>
+            <span class="invalid-feedback"
+                  *ngIf="multisiteSystemUserForm.showError('userName', formDir, 'uniqueName')"
+                  i18n>The chosen realm name is already in use.</span>
+          </div>
+        </div>
+      </div>
+      <div class="modal-footer">
+        <cd-form-button-panel (submitActionEvent)="submit()"
+                              [form]="multisiteSystemUserForm"></cd-form-button-panel>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts
new file mode 100644 (file)
index 0000000..b3d576a
--- /dev/null
@@ -0,0 +1,38 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSystemUserComponent } from './rgw-system-user.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('RgwSystemUserComponent', () => {
+  let component: RgwSystemUserComponent;
+  let fixture: ComponentFixture<RgwSystemUserComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        ReactiveFormsModule,
+        RouterTestingModule,
+        HttpClientTestingModule,
+        ToastrModule.forRoot()
+      ],
+      declarations: [RgwSystemUserComponent],
+      providers: [NgbActiveModal]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwSystemUserComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts
new file mode 100644 (file)
index 0000000..6ace671
--- /dev/null
@@ -0,0 +1,50 @@
+import { Component, EventEmitter, Output } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+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 { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+  selector: 'cd-rgw-system-user',
+  templateUrl: './rgw-system-user.component.html',
+  styleUrls: ['./rgw-system-user.component.scss']
+})
+export class RgwSystemUserComponent {
+  multisiteSystemUserForm: CdFormGroup;
+  zoneName: string;
+
+  @Output()
+  submitAction = new EventEmitter();
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    public actionLabels: ActionLabelsI18n,
+    public rgwZoneService: RgwZoneService,
+    public notificationService: NotificationService
+  ) {
+    this.createForm();
+  }
+
+  createForm() {
+    this.multisiteSystemUserForm = new CdFormGroup({
+      userName: new FormControl(null, {
+        validators: [Validators.required]
+      })
+    });
+  }
+
+  submit() {
+    const userName = this.multisiteSystemUserForm.getValue('userName');
+    this.rgwZoneService.createSystemUser(userName, this.zoneName).subscribe(() => {
+      this.submitAction.emit();
+      this.notificationService.show(
+        NotificationType.success,
+        $localize`User: '${this.multisiteSystemUserForm.getValue('userName')}' created successfully`
+      );
+      this.activeModal.close();
+    });
+  }
+}
index d3a48ad6b2244152e71f4d31390c8018d374997c..671178b6d74c47cc725636ce86e0ab691efe7e62 100644 (file)
@@ -36,6 +36,7 @@ import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-fo
 import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form/rgw-multisite-zone-form.component';
 import { RgwMultisiteZoneDeletionFormComponent } from './models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component';
 import { RgwMultisiteZonegroupDeletionFormComponent } from './models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component';
+import { RgwSystemUserComponent } from './rgw-system-user/rgw-system-user.component';
 
 @NgModule({
   imports: [
@@ -81,7 +82,8 @@ import { RgwMultisiteZonegroupDeletionFormComponent } from './models/rgw-multisi
     RgwMultisiteZonegroupFormComponent,
     RgwMultisiteZoneFormComponent,
     RgwMultisiteZoneDeletionFormComponent,
-    RgwMultisiteZonegroupDeletionFormComponent
+    RgwMultisiteZonegroupDeletionFormComponent,
+    RgwSystemUserComponent
   ]
 })
 export class RgwModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts
new file mode 100644 (file)
index 0000000..400a273
--- /dev/null
@@ -0,0 +1,18 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { RgwDaemonService } from './rgw-daemon.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RgwMultisiteService {
+  private url = 'ui-api/rgw/multisite';
+
+  constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {}
+
+  getMultisiteSyncStatus() {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get(`${this.url}/sync_status`);
+    });
+  }
+}
index a8530c9558e77fb3bf2f6307514ca9bdcd3f6f7b..ffff267ceb2bde51add445fe0fe73e299d36b59b 100644 (file)
@@ -19,8 +19,16 @@ export class RgwZoneService {
     defaultZone: boolean,
     master: boolean,
     endpoints: Array<string>,
-    user: string
+    user: string,
+    createSystemUser: boolean,
+    master_zone_of_master_zonegroup: RgwZone
   ) {
+    let master_zone_name = '';
+    if (master_zone_of_master_zonegroup !== undefined) {
+      master_zone_name = master_zone_of_master_zonegroup.name;
+    } else {
+      master_zone_name = '';
+    }
     return this.rgwDaemonService.request((params: HttpParams) => {
       params = params.appendAll({
         zone_name: zone.name,
@@ -28,7 +36,9 @@ export class RgwZoneService {
         default: defaultZone,
         master: master,
         zone_endpoints: endpoints,
-        user: user
+        user: user,
+        createSystemUser: createSystemUser,
+        master_zone_of_master_zonegroup: master_zone_name
       });
       return this.http.post(`${this.url}`, null, { params: params });
     });
@@ -63,17 +73,66 @@ export class RgwZoneService {
     });
   }
 
+  update(
+    zone: RgwZone,
+    zonegroup: RgwZonegroup,
+    newZoneName: string,
+    defaultZone?: boolean,
+    master?: boolean,
+    endpoints?: Array<string>,
+    user?: string,
+    placementTarget?: string,
+    dataPool?: string,
+    indexPool?: string,
+    dataExtraPool?: string,
+    storageClass?: string,
+    dataPoolClass?: string,
+    compression?: string,
+    master_zone_of_master_zonegroup?: RgwZone
+  ) {
+    let master_zone_name = '';
+    if (master_zone_of_master_zonegroup !== undefined) {
+      master_zone_name = master_zone_of_master_zonegroup.name;
+    } else {
+      master_zone_name = '';
+    }
+    return this.rgwDaemonService.request((requestBody: any) => {
+      requestBody = {
+        zone_name: zone.name,
+        zonegroup_name: zonegroup.name,
+        new_zone_name: newZoneName,
+        default: defaultZone,
+        master: master,
+        zone_endpoints: endpoints,
+        user: user,
+        placement_target: placementTarget,
+        data_pool: dataPool,
+        index_pool: indexPool,
+        data_extra_pool: dataExtraPool,
+        storage_class: storageClass,
+        data_pool_class: dataPoolClass,
+        compression: compression,
+        master_zone_of_master_zonegroup: master_zone_name
+      };
+      return this.http.put(`${this.url}/${zone.name}`, requestBody);
+    });
+  }
+
   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;
+    nodes['type'] = 'zone';
+    nodes['name'] = zone.name;
     nodes['info'] = zone;
     nodes['icon'] = Icons.deploy;
+    nodes['zone_zonegroup'] = zonegroup;
     nodes['parent'] = zonegroup ? zonegroup.name : '';
     nodes['second_parent'] = realm ? realm.name : '';
     nodes['is_default'] = zone.id === defaultZoneId ? true : false;
+    nodes['endpoints'] = zone.endpoints;
     nodes['is_master'] = zonegroup && zonegroup.master_zone === zone.id ? true : false;
     nodes['type'] = 'zone';
     return {
@@ -81,4 +140,29 @@ export class RgwZoneService {
       zoneIds: zoneIds
     };
   }
+
+  getPoolNames() {
+    return this.rgwDaemonService.request(() => {
+      return this.http.get(`${this.url}/get_pool_names`);
+    });
+  }
+
+  createSystemUser(userName: string, zone: string) {
+    return this.rgwDaemonService.request((requestBody: any) => {
+      requestBody = {
+        userName: userName,
+        zoneName: zone
+      };
+      return this.http.put(`${this.url}/create_system_user`, requestBody);
+    });
+  }
+
+  getUserList(zoneName: string) {
+    return this.rgwDaemonService.request((params: HttpParams) => {
+      params = params.appendAll({
+        zoneName: zoneName
+      });
+      return this.http.get(`${this.url}/get_user_list`, { params: params });
+    });
+  }
 }
index bb54606c2fef04ce9b12a1aa8cc7880d58477397..7c9b6b870cadc4fd3331be786a09d31e33b81976 100644 (file)
@@ -30,10 +30,10 @@ export class RgwZonegroupService {
     realm: RgwRealm,
     zonegroup: RgwZonegroup,
     newZonegroupName: string,
-    defaultZonegroup: boolean,
-    master: boolean,
-    removedZones: string[],
-    addedZones: string[]
+    defaultZonegroup?: boolean,
+    master?: boolean,
+    removedZones?: string[],
+    addedZones?: string[]
   ) {
     return this.rgwDaemonService.request((requestBody: any) => {
       requestBody = {
@@ -93,6 +93,7 @@ export class RgwZonegroupService {
     nodes['master_zone'] = zonegroup.master_zone;
     nodes['zones'] = zonegroup.zones;
     nodes['placement_targets'] = zonegroup.placement_targets;
+    nodes['default_placement'] = zonegroup.default_placement;
     return nodes;
   }
 }
index 22f2230cbd41677e4d8e3358cbbcebb50074a488..00c91abd092628b3b45ce39837632d66a67c5d05 100644 (file)
@@ -35,6 +35,7 @@ export class DocService {
       'nfs-ganesha': `${domain}mgr/dashboard/#configuring-nfs-ganesha-in-the-dashboard`,
       'rgw-nfs': `${domain}radosgw/nfs`,
       rgw: `${domain}mgr/dashboard/#enabling-the-object-gateway-management-frontend`,
+      'rgw-multisite': `${domain}/radosgw/multisite/#failover-and-disaster-recovery`,
       dashboard: `${domain}mgr/dashboard`,
       grafana: `${domain}mgr/dashboard/#enabling-the-embedding-of-grafana-dashboards`,
       orch: `${domain}mgr/orchestrator`,
index 255fd0dedfff817c7f4e40519ce267fb10ad5fb6..723acc09268e436511df1d2fd0f858d9cb76eaab 100644 (file)
@@ -9310,6 +9310,9 @@ paths:
           application/json:
             schema:
               properties:
+                createSystemUser:
+                  default: false
+                  type: boolean
                 daemon_name:
                   type: string
                 default:
@@ -9318,6 +9321,8 @@ paths:
                 master:
                   default: false
                   type: boolean
+                master_zone_of_master_zonegroup:
+                  type: string
                 user:
                   type: string
                 zone_endpoints:
@@ -9353,6 +9358,48 @@ paths:
       - jwt: []
       tags:
       - RgwZone
+  /api/rgw/zone/create_system_user:
+    put:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                daemon_name:
+                  type: string
+                userName:
+                  type: string
+                zoneName:
+                  type: string
+              required:
+              - userName
+              - zoneName
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '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: []
@@ -9375,6 +9422,60 @@ paths:
       - jwt: []
       tags:
       - RgwZone
+  /api/rgw/zone/get_pool_names:
+    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/get_user_list:
+    get:
+      parameters:
+      - allowEmptyValue: true
+        in: query
+        name: daemon_name
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: zoneName
+        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/zone/{zone_name}:
     delete:
       parameters:
@@ -9453,6 +9554,87 @@ paths:
       - jwt: []
       tags:
       - RgwZone
+    put:
+      parameters:
+      - in: path
+        name: zone_name
+        required: true
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                compression:
+                  default: ''
+                  type: string
+                daemon_name:
+                  type: string
+                data_extra_pool:
+                  default: ''
+                  type: string
+                data_pool:
+                  default: ''
+                  type: string
+                data_pool_class:
+                  default: ''
+                  type: string
+                default:
+                  default: ''
+                  type: string
+                index_pool:
+                  default: ''
+                  type: string
+                master:
+                  default: ''
+                  type: string
+                master_zone_of_master_zonegroup:
+                  type: string
+                new_zone_name:
+                  type: string
+                placement_target:
+                  default: ''
+                  type: string
+                storage_class:
+                  default: ''
+                  type: string
+                user:
+                  default: ''
+                  type: string
+                zone_endpoints:
+                  default: []
+                  type: string
+                zonegroup_name:
+                  type: string
+              required:
+              - new_zone_name
+              - zonegroup_name
+              type: object
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource updated.
+        '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/zonegroup:
     get:
       parameters:
index 4e925e8616f1ae63c5128278337c71aa7530bb92..d2566bab59f61a27474d227f62fa6f534fdd5697 100644 (file)
@@ -1,4 +1,4 @@
 pytest-cov
 pytest-instafail
 pyfakefs==4.5.0
-jsonschema==4.16.0
+jsonschema
index fe432f079c3ca465b148a36037777d1923de0fc2..ee7652187093a1d467d17a4978fc68e740a42f39 100644 (file)
@@ -8,6 +8,7 @@ import json
 import logging
 import os
 import re
+import subprocess
 import xml.etree.ElementTree as ET  # noqa: N814
 from subprocess import SubprocessError
 
@@ -791,6 +792,15 @@ class RgwClient(RestClient):
             if placement_target['tags']:
                 cmd_add_placement_options += ['--tags', placement_target['tags']]
             rgw_add_placement_cmd += cmd_add_placement_options
+            try:
+                exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
+                if exit_code > 0:
+                    raise DashboardException(e=err,
+                                             msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name),  # noqa E501  #pylint: disable=line-too-long
+                                             http_status_code=500, component='rgw')
+            except SubprocessError as error:
+                raise DashboardException(error, http_status_code=500, component='rgw')
+            self.update_period()
             storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else []  # noqa E501  #pylint: disable=line-too-long
             if storage_classes:
                 for sc in storage_classes:
@@ -805,16 +815,6 @@ class RgwClient(RestClient):
                     except SubprocessError as error:
                         raise DashboardException(error, http_status_code=500, component='rgw')
                     self.update_period()
-                return
-            try:
-                exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
-                if exit_code > 0:
-                    raise DashboardException(e=err,
-                                             msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name),  # noqa E501  #pylint: disable=line-too-long
-                                             http_status_code=500, component='rgw')
-            except SubprocessError as error:
-                raise DashboardException(error, http_status_code=500, component='rgw')
-        self.update_period()
 
     def modify_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
         rgw_add_placement_cmd = ['zonegroup', 'placement', 'modify']
@@ -944,9 +944,10 @@ class RgwClient(RestClient):
                 zone_info = self.get_zone(zone['name'])
                 self.delete_zone_pools(zone['name'], zone_info)
 
-    def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, user):
+    def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, user,
+                    createSystemUser, master_zone_of_master_zonegroup):
         if user != 'null':
-            access_key, secret_key = _get_user_keys(user)
+            access_key, secret_key = self.get_rgw_user_keys(user, master_zone_of_master_zonegroup)
         else:
             access_key = None  # type: ignore
             secret_key = None  # type: ignore
@@ -978,8 +979,133 @@ class RgwClient(RestClient):
             raise DashboardException(error, http_status_code=500, component='rgw')
 
         self.update_period()
+
+        if createSystemUser == 'true':
+            self.create_system_user(user, zone_name)
+            access_key, secret_key = self.get_rgw_user_keys(user, zone_name)
+            rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zone', zone_name,
+                                   '--access-key', access_key, '--secret', secret_key]
+            try:
+                exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
+                if exit_code > 0:
+                    raise DashboardException(e=err, msg='Unable to modify 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 get_rgw_user_keys(self, user, zone_name):
+        access_key = ''
+        secret_key = ''
+        rgw_user_info_cmd = ['user', 'info', '--uid', user, '--rgw-zone', zone_name]
+        try:
+            _, out, _ = mgr.send_rgwadmin_command(rgw_user_info_cmd)
+            if out:
+                access_key, secret_key = self.parse_secrets(user, out)
+        except SubprocessError as error:
+            logger.exception(error)
+
+        return access_key, secret_key
+
+    def parse_secrets(self, user, data):
+        for key in data.get('keys', []):
+            if key.get('user') == user:
+                access_key = key.get('access_key')
+                secret_key = key.get('secret_key')
+                return access_key, secret_key
+        return '', ''
+
+    def modify_zone(self, zone_name: str, zonegroup_name: str, default: str, master: str,
+                    endpoints: List[str], user: str, master_zone_of_master_zonegroup):
+        if user:
+            access_key, secret_key = self.get_rgw_user_keys(user, master_zone_of_master_zonegroup)
+        else:
+            access_key = None
+            secret_key = None
+        rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zonegroup',
+                               zonegroup_name, '--rgw-zone', zone_name]
+        if endpoints:
+            if len(endpoints) > 1:
+                endpoint = ','.join(str(e) for e in endpoints)
+            else:
+                endpoint = endpoints[0]
+            rgw_zone_modify_cmd.append('--endpoints')
+            rgw_zone_modify_cmd.append(endpoint)
+        if default and str_to_bool(default):
+            rgw_zone_modify_cmd.append('--default')
+        if master and str_to_bool(master):
+            rgw_zone_modify_cmd.append('--master')
+        if access_key is not None:
+            rgw_zone_modify_cmd.append('--access-key')
+            rgw_zone_modify_cmd.append(access_key)
+        if secret_key is not None:
+            rgw_zone_modify_cmd.append('--secret')
+            rgw_zone_modify_cmd.append(secret_key)
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to modify zone',
+                                         http_status_code=500, component='rgw')
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+        self.update_period()
+
+    def add_placement_targets_zone(self, zone_name: str, placement_target: str, data_pool: str,
+                                   index_pool: str, data_extra_pool: str):
+        rgw_zone_add_placement_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name,
+                                      '--placement-id', placement_target, '--data-pool', data_pool,
+                                      '--index-pool', index_pool,
+                                      '--data-extra-pool', data_extra_pool]
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_placement_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to add placement target {} to zone {}'.format(placement_target, zone_name),  # noqa E501 #pylint: disable=line-too-long
+                                         http_status_code=500, component='rgw')
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+        self.update_period()
+
+    def add_storage_class_zone(self, zone_name: str, placement_target: str, storage_class: str,
+                               data_pool: str, compression: str):
+        rgw_zone_add_storage_class_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name,
+                                          '--placement-id', placement_target,
+                                          '--storage-class', storage_class,
+                                          '--data-pool', data_pool,
+                                          '--compression', compression]
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_storage_class_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to add storage class {} to zone {}'.format(storage_class, zone_name),  # noqa E501 #pylint: disable=line-too-long
+                                         http_status_code=500, component='rgw')
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+        self.update_period()
+
+    def edit_zone(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '',
+                  master: str = '', endpoints: List[str] = [], user: str = '',
+                  placement_target: str = '', data_pool: str = '', index_pool: str = '',
+                  data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
+                  compression: str = '', master_zone_of_master_zonegroup=None):
+        if new_zone_name != zone_name:
+            rgw_zone_rename_cmd = ['zone', 'rename', '--rgw-zone',
+                                   zone_name, '--zone-new-name', new_zone_name]
+            try:
+                exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_rename_cmd, False)
+                if exit_code > 0:
+                    raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(new_zone_name),  # noqa E501 #pylint: disable=line-too-long
+                                             http_status_code=500, component='rgw')
+            except SubprocessError as error:
+                raise DashboardException(error, http_status_code=500, component='rgw')
+            self.update_period()
+        self.modify_zone(new_zone_name, zonegroup_name, default, master, endpoints, user,
+                         master_zone_of_master_zonegroup)
+        self.add_placement_targets_zone(new_zone_name, placement_target,
+                                        data_pool, index_pool, data_extra_pool)
+        self.add_storage_class_zone(new_zone_name, placement_target, storage_class,
+                                    data_pool_class, compression)
+
     def list_zones(self):
         rgw_zone_list = {}
         rgw_zone_list_cmd = ['zone', 'list']
@@ -1083,6 +1209,57 @@ class RgwClient(RestClient):
             is_multisite_configured = False
         return is_multisite_configured
 
+    def get_multisite_sync_status(self):
+        sync_status = ''
+        rgw_sync_status_cmd = ['sync', 'status']
+        try:
+            exit_code, out, _ = mgr.send_rgwadmin_command(rgw_sync_status_cmd, False)
+            if exit_code > 0:
+                raise DashboardException('Unable to get sync status',
+                                         http_status_code=500, component='rgw')
+            sync_status = out
+        except subprocess.TimeoutExpired:
+            sync_status = 'Timeout Expired'
+        return sync_status
+
+    def create_system_user(self, userName: str, zoneName: str):
+        rgw_user_create_cmd = ['user', 'create', '--uid', userName,
+                               '--display-name', userName, '--rgw-zone', zoneName, '--system']
+        try:
+            exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_create_cmd)
+            if exit_code > 0:
+                raise DashboardException(msg='Unable to create system user',
+                                         http_status_code=500, component='rgw')
+            return out
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+
+    def get_user_list(self, zoneName: str):
+        all_users_info = []
+        user_list = []
+        rgw_user_list_cmd = ['user', 'list', '--rgw-zone', zoneName]
+        try:
+            exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_list_cmd)
+            if exit_code > 0:
+                raise DashboardException('Unable to get user list',
+                                         http_status_code=500, component='rgw')
+            user_list = out
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+
+        if len(user_list) > 0:
+            for user_name in user_list:
+                rgw_user_info_cmd = ['user', 'info', '--uid', user_name, '--rgw-zone', zoneName]
+                try:
+                    exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_info_cmd)
+                    if exit_code > 0:
+                        raise DashboardException('Unable to get user info',
+                                                 http_status_code=500, component='rgw')
+                    all_users_info.append(out)
+                except SubprocessError as error:
+                    raise DashboardException(error, http_status_code=500, component='rgw')
+        return all_users_info
+
     @RestClient.api_get('/{bucket_name}?versioning')
     def get_bucket_versioning(self, bucket_name, request=None):
         """