]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: add support for editing RGW zonegroup
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 11:57:09 +0000 (17:27 +0530)
Fixes: https://tracker.ceph.com/issues/59239
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
Co-authored-by: Aashish Sharma <aasharma@redhat.com>
(cherry picked from commit d9efaed62e7082ad1a61233515b05b7c15bdc28b)

src/pybind/mgr/dashboard/controllers/rgw.py
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.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.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.spec.ts
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/shared/api/rgw-zonegroup.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index 331d2067fd03ca50291459657c8c000164820692..4e01fb9ea3abc75936f5bbcf976d6d8b4b827f7d 100644 (file)
@@ -817,6 +817,21 @@ class RgwZonegroup(RESTController):
         except NoRgwDaemonsException as e:
             raise DashboardException(e, http_status_code=404, component='rgw')
 
+    @allow_empty_body
+    # pylint: disable=W0613,W0102
+    def set(self, zonegroup_name: str, realm_name: str, new_zonegroup_name: str, default: str = '',
+            master: str = '', zonegroup_endpoints: List[str] = [], add_zones: List[str] = [],
+            remove_zones: List[str] = [], placement_targets: List[Dict[str, str]] = [],
+            daemon_name=None):
+        try:
+            instance = RgwClient.admin_instance()
+            result = instance.edit_zonegroup(realm_name, zonegroup_name, new_zonegroup_name,
+                                             default, master, zonegroup_endpoints, add_zones,
+                                             remove_zones, placement_targets)
+            return result
+        except NoRgwDaemonsException as e:
+            raise DashboardException(e, http_status_code=404, component='rgw')
+
 
 @APIRouter('/rgw/zone', Scope.RGW)
 class RgwZone(RESTController):
index 3dcb290c64141099be18100e7fa1ba56976374cc..12ae56a2d0742cef7486dc2b45015c2465b5d252 100644 (file)
                       *ngIf="node.data.is_default">
                   default
                 </span>
-                <span class="badge badge-info me-2"
+                <span class="badge badge-warning me-2"
                       *ngIf="node.data.is_master">
                   master
                 </span>
                 <div class="btn-group align-inline-btns"
-                     *ngIf="node.isFocused && node.data.type === 'realm'"
+                     *ngIf="node.isFocused"
+                     [title]="title"
                      role="group">
                   <button type="button"
-                          title="Edit realm"
                           class="btn btn-light dropdown-toggle-split ms-1"
                           (click)="openModal(node, true)"
+                          [disabled]="getDisable()"
                           ngbDropdownToggle>
                     <i [ngClass]="[icons.edit]"></i>
                   </button>
index 0a8e598f6e73b1311e1ba066e079120df287e1f6..a0edcd95b49aa439cb79269aa113e9880c9f2750 100644 (file)
@@ -76,6 +76,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
   defaultZoneId = '';
   multisiteInfo: object[] = [];
   defaultsInfo: string[] = [];
+  title: string = 'Edit';
 
   constructor(
     private modalService: ModalService,
@@ -291,18 +292,21 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
   }
 
   getDisable() {
+    let isMasterZone = true;
     if (this.defaultRealmId === '') {
       return this.messages.noDefaultRealm;
     } else {
-      let isMasterZone = true;
       this.zonegroups.forEach((zgp: any) => {
         if (_.isEmpty(zgp.master_zone)) {
           isMasterZone = false;
         }
       });
       if (!isMasterZone) {
+        this.title =
+          'Please create a master zone for each existing zonegroup to enable this feature';
         return this.messages.noMasterZone;
       } else {
+        this.title = 'Edit';
         return false;
       }
     }
index adcadde8775ee264e056c45d51dbd4146c7ec322..b3848052b5b0d7f72ab91c7e706ba317cb2ceb48 100644 (file)
                  type="checkbox">
           <label class="form-check-label"
                  for="default_zonegroup"
-                 i18n>Default</label><br>
+                 i18n>Default</label>
+          <span *ngIf="disableDefault">
+          <cd-helper i18n>Zonegroup doesn't belong to the default realm.</cd-helper>
+          </span><br>
           <input class="form-check-input"
                  id="master_zonegroup"
                  name="master_zonegroup"
@@ -67,6 +70,9 @@
             Can be modified later by editing a zonegroup.
           </cd-helper>
           </span>
+          <span *ngIf="disableMaster">
+            <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>
         </div>
         </div>
       </div>
               i18n>Please enter a valid IP address.</span>
         </div>
       </div>
+      <div class="form-group row"
+           *ngIf="action === 'edit'">
+        <label i18n
+               for="zones"
+               class="cd-col-form-label">Zones</label>
+        <div class="cd-col-form-input">
+          <cd-select-badges id="zones"
+                            [data]="zonegroupZoneNames"
+                            [options]="labelsOption"
+                            [customBadges]="true">
+          </cd-select-badges><br>
+          <span class="invalid-feedback"
+                *ngIf="isRemoveMasterZone"
+                i18n>Cannot remove master zone.</span>
+        </div>
+      </div>
+      <div *ngIf="action === 'edit'">
+        <legend>Placement targets</legend>
+        <ng-container formArrayName="placementTargets">
+          <div *ngFor="let item of placementTargets.controls; let index = index; trackBy: trackByFn">
+            <div class="card"
+                 [formGroup]="item">
+              <div class="card-header">
+                {{ (index + 1) | ordinal }}
+                <span class="float-end clickable"
+                      name="remove_placement_target"
+                      (click)="removePlacementTarget(index)"
+                      ngbTooltip="Remove">&times;</span>
+              </div>
+
+              <div class="card-body">
+                <!-- Placement Id -->
+                <div class="form-group row">
+                  <label i18n
+                         class="cd-col-form-label required"
+                         for="placement_id">Placement Id</label>
+                  <div class="cd-col-form-input">
+                    <input type="text"
+                           class="form-control"
+                           name="placement_id"
+                           id="placement_id"
+                           formControlName="placement_id"
+                           placeholder="eg. default-placement">
+                    <span class="invalid-feedback">
+                      <span *ngIf="showError(index, 'placement_id', formDir, 'required')"
+                            i18n>This field is required.</span>
+                    </span>
+                  </div>
+                </div>
+
+                <!-- Tags-->
+                <div class="form-group row">
+                  <label i18n
+                         class="cd-col-form-label"
+                         for="tags">Tags</label>
+                  <div class="cd-col-form-input">
+                    <input type="text"
+                           class="form-control"
+                           name="tags"
+                           id="tags"
+                           formControlName="tags"
+                           placeholder="comma separated tags, eg. default-placement, ssd">
+                  </div>
+                </div>
+
+                <!-- Storage Class -->
+                <div class="form-group row">
+                  <label i18n
+                         class="cd-col-form-label"
+                         for="storage_class">Storage Class</label>
+                  <div class="cd-col-form-input">
+                    <input type="text"
+                           class="form-control"
+                           name="storage_class"
+                           id="storage_class"
+                           formControlName="storage_class"
+                           placeholder="eg. Standard-tier">
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </ng-container>
+        <button type="button"
+                id="add-plc"
+                class="btn btn-light float-end my-3"
+                (click)="addPlacementTarget()">
+          <i [ngClass]="[icons.add]"></i>
+          <ng-container i18n>Add placement target</ng-container>
+        </button>
+      </div>
     </div>
     <div class="modal-footer">
       <cd-form-button-panel (submitActionEvent)="submit()"
index bbd82577262cc5922571c638ae04771a225e4f19..bb7abdcbbebc0177d38afbbd2fc870912085d03c 100644 (file)
@@ -1,9 +1,15 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
 import { ToastrModule } from 'ngx-toastr';
+import { of as observableOf } from 'rxjs';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { NotificationService } from '~/app/shared/services/notification.service';
 import { SharedModule } from '~/app/shared/shared.module';
 
 import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-form.component';
@@ -11,6 +17,7 @@ import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-fo
 describe('RgwMultisiteZonegroupFormComponent', () => {
   let component: RgwMultisiteZonegroupFormComponent;
   let fixture: ComponentFixture<RgwMultisiteZonegroupFormComponent>;
+  let rgwZonegroupService: RgwZonegroupService;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -35,4 +42,62 @@ describe('RgwMultisiteZonegroupFormComponent', () => {
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  describe('submit form', () => {
+    let notificationService: NotificationService;
+
+    beforeEach(() => {
+      spyOn(TestBed.inject(Router), 'navigate').and.stub();
+      notificationService = TestBed.inject(NotificationService);
+      spyOn(notificationService, 'show');
+      rgwZonegroupService = TestBed.inject(RgwZonegroupService);
+    });
+
+    it('should validate name', () => {
+      component.action = 'create';
+      component.createForm();
+      const control = component.multisiteZonegroupForm.get('zonegroupName');
+      expect(_.isFunction(control.validator)).toBeTruthy();
+    });
+
+    it('should not validate name', () => {
+      component.action = 'edit';
+      component.createForm();
+      const control = component.multisiteZonegroupForm.get('zonegroupName');
+      expect(control.asyncValidator).toBeNull();
+    });
+
+    it('tests create success notification', () => {
+      spyOn(rgwZonegroupService, 'create').and.returnValue(observableOf([]));
+      component.action = 'create';
+      component.multisiteZonegroupForm.markAsDirty();
+      component.multisiteZonegroupForm._get('zonegroupName').setValue('zg-1');
+      component.multisiteZonegroupForm
+        ._get('zonegroup_endpoints')
+        .setValue('http://192.1.1.1:8004');
+      component.submit();
+      expect(notificationService.show).toHaveBeenCalledWith(
+        NotificationType.success,
+        "Zonegroup: 'zg-1' created successfully"
+      );
+    });
+
+    it('tests update success notification', () => {
+      spyOn(rgwZonegroupService, 'update').and.returnValue(observableOf([]));
+      component.action = 'edit';
+      component.info = {
+        data: { name: 'zg-1', zones: ['z1'] }
+      };
+      component.multisiteZonegroupForm._get('zonegroupName').setValue('zg-1');
+      component.multisiteZonegroupForm
+        ._get('zonegroup_endpoints')
+        .setValue('http://192.1.1.1:8004,http://192.12.12.12:8004');
+      component.multisiteZonegroupForm.markAsDirty();
+      component.submit();
+      expect(notificationService.show).toHaveBeenCalledWith(
+        NotificationType.success,
+        "Zonegroup: 'zg-1' updated successfully"
+      );
+    });
+  });
 });
index c36c636ce305bc4f990075e61649f7ac0012247e..2172a301e2cad09a0146b2662a4f49648d419384 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, OnInit } from '@angular/core';
-import { FormControl, Validators } from '@angular/forms';
+import { FormArray, FormBuilder, FormControl, NgForm, Validators } from '@angular/forms';
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
@@ -8,7 +8,9 @@ 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';
+import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
 
 @Component({
   selector: 'cd-rgw-multisite-zonegroup-form',
@@ -20,23 +22,39 @@ export class RgwMultisiteZonegroupFormComponent 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;
+  icons = Icons;
   multisiteZonegroupForm: CdFormGroup;
   editing = false;
   resource: string;
   realm: RgwRealm;
   zonegroup: RgwZonegroup;
+  info: any;
   defaultsInfo: string[] = [];
   multisiteInfo: object[] = [];
   realmList: RgwRealm[] = [];
   zonegroupList: RgwZonegroup[] = [];
   zonegroupNames: string[];
   isMaster = false;
+  placementTargets: FormArray;
+  newZonegroupName: string;
+  zonegroupZoneNames: string[];
+  labelsOption: Array<SelectOption> = [];
+  zoneList: RgwZone[] = [];
+  allZoneNames: string[];
+  zgZoneNames: string[];
+  zgZoneIds: string[];
+  removedZones: string[];
+  isRemoveMasterZone = false;
+  addedZones: string[];
+  disableDefault = false;
+  disableMaster = false;
 
   constructor(
     public activeModal: NgbActiveModal,
     public actionLabels: ActionLabelsI18n,
     public rgwZonegroupService: RgwZonegroupService,
-    public notificationService: NotificationService
+    public notificationService: NotificationService,
+    private formBuilder: FormBuilder
   ) {
     this.action = this.editing
       ? this.actionLabels.EDIT + this.resource
@@ -51,7 +69,11 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
         validators: [
           Validators.required,
           CdValidators.custom('uniqueName', (zonegroupName: string) => {
-            return this.zonegroupNames && this.zonegroupNames.indexOf(zonegroupName) !== -1;
+            return (
+              this.action === 'create' &&
+              this.zonegroupNames &&
+              this.zonegroupNames.indexOf(zonegroupName) !== -1
+            );
           })
         ]
       }),
@@ -79,11 +101,17 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
           }
         }),
         Validators.required
-      ])
+      ]),
+      placementTargets: this.formBuilder.array([])
     });
   }
 
   ngOnInit(): void {
+    _.forEach(this.multisiteZonegroupForm.get('placementTargets'), (placementTarget) => {
+      const fg = this.addPlacementTarget();
+      fg.patchValue(placementTarget);
+    });
+    this.placementTargets = this.multisiteZonegroupForm.get('placementTargets') as FormArray;
     this.realmList =
       this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms')
         ? this.multisiteInfo[0]['realms']
@@ -93,49 +121,150 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
         ? this.multisiteInfo[1]['zonegroups']
         : [];
     this.zonegroupList.forEach((zgp: any) => {
-      if (
-        zgp.is_master === true &&
-        !_.isEmpty(zgp.realm_id) &&
-        zgp.realm_id === this.defaultsInfo['defaultRealmName']
-      ) {
+      if (zgp.is_master === true && !_.isEmpty(zgp.realm_id)) {
         this.isMaster = true;
+        this.disableMaster = true;
       }
     });
     if (!this.isMaster) {
       this.multisiteZonegroupForm.get('master_zonegroup').setValue(true);
       this.multisiteZonegroupForm.get('master_zonegroup').disable();
     }
+    this.zoneList =
+      this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
+        ? this.multisiteInfo[2]['zones']
+        : [];
     this.zonegroupNames = this.zonegroupList.map((zonegroup) => {
       return zonegroup['name'];
     });
+    this.allZoneNames = this.zoneList.map((zone: RgwZone) => {
+      return zone['name'];
+    });
     if (this.action === 'create' && this.defaultsInfo['defaultRealmName'] !== null) {
       this.multisiteZonegroupForm
         .get('selectedRealm')
         .setValue(this.defaultsInfo['defaultRealmName']);
+      if (this.disableMaster) {
+        this.multisiteZonegroupForm.get('master_zonegroup').disable();
+      }
+    }
+    if (this.action === 'edit') {
+      this.multisiteZonegroupForm.get('zonegroupName').setValue(this.info.data.name);
+      this.multisiteZonegroupForm.get('selectedRealm').setValue(this.info.data.parent);
+      this.multisiteZonegroupForm.get('default_zonegroup').setValue(this.info.data.is_default);
+      this.multisiteZonegroupForm.get('master_zonegroup').setValue(this.info.data.is_master);
+      this.multisiteZonegroupForm.get('zonegroup_endpoints').setValue(this.info.data.endpoints);
+
+      if (this.info.data.is_default) {
+        this.multisiteZonegroupForm.get('default_zonegroup').disable();
+      }
+      if (
+        !this.info.data.is_default &&
+        this.multisiteZonegroupForm.getValue('selectedRealm') !==
+          this.defaultsInfo['defaultRealmName']
+      ) {
+        this.multisiteZonegroupForm.get('default_zonegroup').disable();
+        this.disableDefault = true;
+      }
+      if (this.info.data.is_master || this.disableMaster) {
+        this.multisiteZonegroupForm.get('master_zonegroup').disable();
+      }
+
+      this.zonegroupZoneNames = this.info.data.zones.map((zone: { [x: string]: any }) => {
+        return zone['name'];
+      });
+      this.zgZoneNames = this.info.data.zones.map((zone: { [x: string]: any }) => {
+        return zone['name'];
+      });
+      this.zgZoneIds = this.info.data.zones.map((zone: { [x: string]: any }) => {
+        return zone['id'];
+      });
+      const uniqueZones = new Set(this.allZoneNames);
+      this.labelsOption = Array.from(uniqueZones).map((zone) => {
+        return { enabled: true, name: zone, selected: false, description: null };
+      });
+
+      this.info.data.placement_targets.forEach((target: object) => {
+        const fg = this.addPlacementTarget();
+        let data = {
+          placement_id: target['name'],
+          tags: target['tags'].join(','),
+          storage_class:
+            typeof target['storage_classes'] === 'string'
+              ? target['storage_classes']
+              : target['storage_classes'].join(',')
+        };
+        fg.patchValue(data);
+      });
     }
   }
 
   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 });
-        }
+    const values = this.multisiteZonegroupForm.getRawValue();
+    if (this.action === 'create') {
+      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 });
+          }
+        );
+    } else if (this.action === 'edit') {
+      this.removedZones = _.difference(this.zgZoneNames, this.zonegroupZoneNames);
+      const masterZoneName = this.info.data.zones.filter(
+        (zone: any) => zone.id === this.info.data.master_zone
       );
+      this.isRemoveMasterZone = this.removedZones.includes(masterZoneName[0].name);
+      if (this.isRemoveMasterZone) {
+        this.multisiteZonegroupForm.setErrors({ cdSubmitButton: true });
+        return;
+      }
+      this.addedZones = _.difference(this.zonegroupZoneNames, this.zgZoneNames);
+      this.realm = new RgwRealm();
+      this.realm.name = values['selectedRealm'];
+      this.zonegroup = new RgwZonegroup();
+      this.zonegroup.name = this.info.data.name;
+      this.newZonegroupName = values['zonegroupName'];
+      this.zonegroup.endpoints =
+        values['zonegroup_endpoints'] === this.info.data.endpoints
+          ? values['zonegroup_endpoints']
+          : this.checkUrlArray(values['zonegroup_endpoints']);
+      this.zonegroup.placement_targets = values['placementTargets'];
+      this.rgwZonegroupService
+        .update(
+          this.realm,
+          this.zonegroup,
+          this.newZonegroupName,
+          values['default_zonegroup'],
+          values['master_zonegroup'],
+          this.removedZones,
+          this.addedZones
+        )
+        .subscribe(
+          () => {
+            this.notificationService.show(
+              NotificationType.success,
+              $localize`Zonegroup: '${values['zonegroupName']}' updated successfully`
+            );
+            this.activeModal.close();
+          },
+          () => {
+            this.multisiteZonegroupForm.setErrors({ cdSubmitButton: true });
+          }
+        );
+    }
   }
 
   checkUrlArray(endpoints: string) {
@@ -147,4 +276,34 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
     }
     return endpointsArray;
   }
+
+  addPlacementTarget() {
+    this.placementTargets = this.multisiteZonegroupForm.get('placementTargets') as FormArray;
+    const fg = new CdFormGroup({
+      placement_id: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      tags: new FormControl(''),
+      storage_class: new FormControl([])
+    });
+    this.placementTargets.push(fg);
+    return fg;
+  }
+
+  trackByFn(index: number) {
+    return index;
+  }
+
+  removePlacementTarget(index: number) {
+    this.placementTargets = this.multisiteZonegroupForm.get('placementTargets') as FormArray;
+    this.placementTargets.removeAt(index);
+  }
+
+  showError(index: number, control: string, formDir: NgForm, x: string) {
+    return (<any>this.multisiteZonegroupForm.controls.placementTargets).controls[index].showError(
+      control,
+      formDir,
+      x
+    );
+  }
 }
index c8bec7338374784cb350b61a52718b1dfdb64810..bb54606c2fef04ce9b12a1aa8cc7880d58477397 100644 (file)
@@ -26,6 +26,31 @@ export class RgwZonegroupService {
     });
   }
 
+  update(
+    realm: RgwRealm,
+    zonegroup: RgwZonegroup,
+    newZonegroupName: string,
+    defaultZonegroup: boolean,
+    master: boolean,
+    removedZones: string[],
+    addedZones: string[]
+  ) {
+    return this.rgwDaemonService.request((requestBody: any) => {
+      requestBody = {
+        zonegroup_name: zonegroup.name,
+        realm_name: realm.name,
+        new_zonegroup_name: newZonegroupName,
+        default: defaultZonegroup,
+        master: master,
+        zonegroup_endpoints: zonegroup.endpoints,
+        placement_targets: zonegroup.placement_targets,
+        remove_zones: removedZones,
+        add_zones: addedZones
+      };
+      return this.http.put(`${this.url}/${zonegroup.name}`, requestBody);
+    });
+  }
+
   list(): Observable<object> {
     return this.rgwDaemonService.request(() => {
       return this.http.get<object>(`${this.url}`);
@@ -66,6 +91,8 @@ export class RgwZonegroupService {
     nodes['type'] = 'zonegroup';
     nodes['endpoints'] = zonegroup.endpoints;
     nodes['master_zone'] = zonegroup.master_zone;
+    nodes['zones'] = zonegroup.zones;
+    nodes['placement_targets'] = zonegroup.placement_targets;
     return nodes;
   }
 }
index efb49555f06a0823a5595da9d34e54989a5e2897..255fd0dedfff817c7f4e40519ce267fb10ad5fb6 100644 (file)
@@ -9622,6 +9622,70 @@ paths:
       - jwt: []
       tags:
       - RgwZonegroup
+    put:
+      parameters:
+      - in: path
+        name: zonegroup_name
+        required: true
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                add_zones:
+                  default: []
+                  type: string
+                daemon_name:
+                  type: string
+                default:
+                  default: ''
+                  type: string
+                master:
+                  default: ''
+                  type: string
+                new_zonegroup_name:
+                  type: string
+                placement_targets:
+                  default: []
+                  type: string
+                realm_name:
+                  type: string
+                remove_zones:
+                  default: []
+                  type: string
+                zonegroup_endpoints:
+                  default: []
+                  type: string
+              required:
+              - realm_name
+              - new_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:
+      - RgwZonegroup
   /api/role:
     get:
       parameters: []
index 09719af20acfd396ce4f8cc828bc330a16fe38b2..fe432f079c3ca465b148a36037777d1923de0fc2 100644 (file)
@@ -667,7 +667,7 @@ class RgwClient(RestClient):
             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=400, component='rgw')
+                                         http_status_code=500, component='rgw')
         except SubprocessError as error:
             raise DashboardException(error, http_status_code=500, component='rgw')
 
@@ -721,6 +721,168 @@ class RgwClient(RestClient):
             raise DashboardException(error, http_status_code=500, component='rgw')
         return out
 
+    def modify_zonegroup(self, realm_name: str, zonegroup_name: str, default: str, master: str,
+                         endpoints: List[str]):
+        if realm_name:
+            rgw_zonegroup_modify_cmd = ['zonegroup', 'modify',
+                                        '--rgw-realm', realm_name,
+                                        '--rgw-zonegroup', zonegroup_name]
+        if endpoints:
+            if len(endpoints) > 1:
+                endpoint = ','.join(str(e) for e in endpoints)
+            else:
+                endpoint = endpoints[0]
+            rgw_zonegroup_modify_cmd.append('--endpoints')
+            rgw_zonegroup_modify_cmd.append(endpoint)
+        if master and str_to_bool(master):
+            rgw_zonegroup_modify_cmd.append('--master')
+        if default and str_to_bool(default):
+            rgw_zonegroup_modify_cmd.append('--default')
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_modify_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to modify zonegroup {}'.format(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 add_or_remove_zone(self, zonegroup_name: str, zone_name: str, action: str):
+        if action == 'add':
+            rgw_zonegroup_add_zone_cmd = ['zonegroup', 'add', '--rgw-zonegroup',
+                                          zonegroup_name, '--rgw-zone', zone_name]
+            try:
+                exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_add_zone_cmd)
+                if exit_code > 0:
+                    raise DashboardException(e=err, msg='Unable to add zone {} to zonegroup {}'.format(zone_name, 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()
+        if action == 'remove':
+            rgw_zonegroup_rm_zone_cmd = ['zonegroup', 'remove',
+                                         '--rgw-zonegroup', zonegroup_name, '--rgw-zone', zone_name]
+            try:
+                exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_rm_zone_cmd)
+                if exit_code > 0:
+                    raise DashboardException(e=err, msg='Unable to remove zone {} from zonegroup {}'.format(zone_name, 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 get_placement_targets_by_zonegroup(self, zonegroup_name: str):
+        rgw_get_placement_cmd = ['zonegroup', 'placement',
+                                 'list', '--rgw-zonegroup', zonegroup_name]
+        try:
+            exit_code, out, err = mgr.send_rgwadmin_command(rgw_get_placement_cmd)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to get placement targets',
+                                         http_status_code=500, component='rgw')
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+        return out
+
+    def add_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
+        rgw_add_placement_cmd = ['zonegroup', 'placement', 'add']
+        for placement_target in placement_targets:
+            cmd_add_placement_options = ['--rgw-zonegroup', zonegroup_name,
+                                         '--placement-id', placement_target['placement_id']]
+            if placement_target['tags']:
+                cmd_add_placement_options += ['--tags', placement_target['tags']]
+            rgw_add_placement_cmd += cmd_add_placement_options
+            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:
+                    cmd_add_placement_options = ['--storage-class', sc]
+                    try:
+                        exit_code, _, err = mgr.send_rgwadmin_command(
+                            rgw_add_placement_cmd + cmd_add_placement_options)
+                        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()
+                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']
+        for placement_target in placement_targets:
+            cmd_add_placement_options = ['--rgw-zonegroup', zonegroup_name,
+                                         '--placement-id', placement_target['placement_id']]
+            if placement_target['tags']:
+                cmd_add_placement_options += ['--tags', placement_target['tags']]
+            rgw_add_placement_cmd += cmd_add_placement_options
+            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:
+                    cmd_add_placement_options = []
+                    cmd_add_placement_options = ['--storage-class', sc]
+                    try:
+                        exit_code, _, err = mgr.send_rgwadmin_command(
+                            rgw_add_placement_cmd + cmd_add_placement_options)
+                        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()
+            else:
+                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()
+
+    # pylint: disable=W0102
+    def edit_zonegroup(self, realm_name: str, zonegroup_name: str, new_zonegroup_name: str,
+                       default: str = '', master: str = '', endpoints: List[str] = [],
+                       add_zones: List[str] = [], remove_zones: List[str] = [],
+                       placement_targets: List[Dict[str, str]] = []):
+        rgw_zonegroup_edit_cmd = []
+        if new_zonegroup_name != zonegroup_name:
+            rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', zonegroup_name,
+                                      '--zonegroup-new-name', new_zonegroup_name]
+            try:
+                exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_edit_cmd, False)
+                if exit_code > 0:
+                    raise DashboardException(e=err, msg='Unable to rename zonegroup to {}'.format(new_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()
+        self.modify_zonegroup(realm_name, new_zonegroup_name, default, master, endpoints)
+        if add_zones:
+            for zone_name in add_zones:
+                self.add_or_remove_zone(new_zonegroup_name, zone_name, 'add')
+        if remove_zones:
+            for zone_name in remove_zones:
+                self.add_or_remove_zone(new_zonegroup_name, zone_name, 'remove')
+        existing_placement_targets = self.get_placement_targets_by_zonegroup(new_zonegroup_name)
+        existing_placement_targets_ids = [pt['key'] for pt in existing_placement_targets]
+        if placement_targets:
+            for pt in placement_targets:
+                if pt['placement_id'] in existing_placement_targets_ids:
+                    self.modify_placement_targets(new_zonegroup_name, placement_targets)
+                else:
+                    self.add_placement_targets(new_zonegroup_name, placement_targets)
+
     def list_zonegroups(self):
         rgw_zonegroup_list = {}
         rgw_zonegroup_list_cmd = ['zonegroup', 'list']