]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Migrate from single site to multi-site
authorAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Thu, 20 Apr 2023 05:22:41 +0000 (10:52 +0530)
committerAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Thu, 10 Aug 2023 12:20:42 +0000 (17:50 +0530)
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
(cherry picked from commit 732cb8ef4fd6c77bd5e27f049dbabc10fbf2514a)

12 files changed:
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.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts
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
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts
src/pybind/mgr/dashboard/services/rgw_client.py

index 80f6b6ad575e86b2fdb8a87804db830e8ee1e538..01644afb47fccaa175d915ad760153afbb67cc80 100644 (file)
@@ -12,6 +12,7 @@ from ..rest_client import RequestException
 from ..security import Permission, Scope
 from ..services.auth import AuthManager, JwtManager
 from ..services.ceph_service import CephService
+from ..services.orchestrator import OrchClient
 from ..services.rgw_client import NoRgwDaemonsException, RgwClient
 from ..tools import json_str_to_object, str_to_bool
 from . import APIDoc, APIRouter, BaseController, CreatePermission, \
@@ -109,6 +110,23 @@ class RgwStatus(BaseController):
             raise DashboardException(e, http_status_code=404, component='rgw')  # noqa: E501 pylint: disable=line-too-long
         return result
 
+    @Endpoint(method='PUT')
+    # pylint: disable=W0102
+    def migrate(self, realm_name=None, zonegroup_name=None, zone_name=None,
+                zonegroup_endpoints: List[str] = [], zone_endpoints: List[str] = [],
+                user=None, daemon_name: Optional[str] = None):
+        try:
+            instance = RgwClient.admin_instance(daemon_name=daemon_name)
+            result = instance.migrate_to_multisite(realm_name, zonegroup_name, zone_name,
+                                                   zonegroup_endpoints, zone_endpoints, user)
+            orch = OrchClient.instance()
+            daemons = orch.services.list_daemons(service_name='rgw')
+            for daemon in daemons:
+                orch.daemons.action(action='reload', daemon_name=daemon.daemon_id)
+        except NoRgwDaemonsException as e:
+            raise DashboardException(e, http_status_code=404, component='rgw')
+        return result
+
 
 @APIRouter('/rgw/daemon', Scope.RGW)
 @APIDoc("RGW Daemon Management API", "RgwDaemon")
index 3c65390a177e944121230070859cdcd8e7fc31b5..ae5b01df64b121803aec7f98f996f66cc198a975 100644 (file)
@@ -1,11 +1,19 @@
 <div class="row">
   <div class="col-sm-12 col-lg-12">
     <div>
-      <cd-table-actions class="btn-group mb-4"
+      <cd-table-actions class="btn-group mb-4 me-2"
                         [permission]="permission"
                         [selection]="selection"
                         [tableActions]="createTableActions">
       </cd-table-actions>
+      <span *ngIf="showMigrateAction">
+        <cd-table-actions class="btn-group mb-4 me-2 secondary"
+                          [permission]="permission"
+                          [btnColor]="'light'"
+                          [selection]="selection"
+                          [tableActions]="migrateTableAction">
+        </cd-table-actions>
+      </span>
     </div>
     <div class="card">
       <div class="card-header"
index a0edcd95b49aa439cb79269aa113e9880c9f2750..51136e5d107f6002a2054ed02c7efc1ef350401d 100644 (file)
@@ -24,6 +24,7 @@ import { ModalService } from '~/app/shared/services/modal.service';
 import { NotificationService } from '~/app/shared/services/notification.service';
 import { TimerService } from '~/app/shared/services/timer.service';
 import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite';
+import { RgwMultisiteMigrateComponent } from '../rgw-multisite-migrate/rgw-multisite-migrate.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 { RgwMultisiteRealmFormComponent } from '../rgw-multisite-realm-form/rgw-multisite-realm-form.component';
@@ -42,13 +43,15 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
 
   messages = {
     noDefaultRealm: $localize`Please create a default realm first to enable this feature`,
-    noMasterZone: $localize`Please create a master zone for each zonegroups to enable this feature`
+    noMasterZone: $localize`Please create a master zone for each zonegroup to enable this feature`,
+    disableMigrate: $localize`Deployment is already migrated to multi-site system.`
   };
 
   icons = Icons;
   permission: Permission;
   selection = new CdTableSelection();
   createTableActions: CdTableAction[];
+  migrateTableAction: CdTableAction[];
   loadingIndicator = true;
   nodes: object[] = [];
   treeOptions: ITreeOptions = {
@@ -77,6 +80,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
   multisiteInfo: object[] = [];
   defaultsInfo: string[] = [];
   title: string = 'Edit';
+  showMigrateAction: boolean = false;
 
   constructor(
     private modalService: ModalService,
@@ -109,7 +113,14 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
       name: this.actionLabels.CREATE + ' Zone',
       click: () => this.openModal('zone')
     };
+    const migrateMultsiteAction: CdTableAction = {
+      permission: 'read',
+      icon: Icons.exchange,
+      name: this.actionLabels.MIGRATE,
+      click: () => this.openMigrateModal()
+    };
     this.createTableActions = [createRealmAction, createZonegroupAction, createZoneAction];
+    this.migrateTableAction = [migrateMultsiteAction];
   }
 
   openModal(entity: any, edit = false) {
@@ -137,6 +148,15 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
     }
   }
 
+  openMigrateModal() {
+    const initialState = {
+      multisiteInfo: this.multisiteInfo
+    };
+    this.bsModalRef = this.modalService.show(RgwMultisiteMigrateComponent, initialState, {
+      size: 'lg'
+    });
+  }
+
   ngOnInit() {
     const observables = [
       this.rgwRealmService.getAllRealmsInfo(),
@@ -257,6 +277,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
     }
     this.realmIds = [];
     this.zoneIds = [];
+    this.getDisableMigrate();
     return allNodes;
   }
 
@@ -312,6 +333,21 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
     }
   }
 
+  getDisableMigrate() {
+    if (
+      this.realms.length === 0 &&
+      this.zonegroups.length === 1 &&
+      this.zonegroups[0].name === 'default' &&
+      this.zones.length === 1 &&
+      this.zones[0].name === 'default'
+    ) {
+      this.showMigrateAction = true;
+    } else {
+      this.showMigrateAction = false;
+    }
+    return this.showMigrateAction;
+  }
+
   delete(node: TreeNode) {
     if (node.data.type === 'realm') {
       this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html
new file mode 100644 (file)
index 0000000..ceb0afc
--- /dev/null
@@ -0,0 +1,137 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">Migrate Single Site to Multi-site
+    <cd-helper>
+      <span>Migrate from a single-site deployment with a default zonegroup and zone to a multi-site system</span>
+    </cd-helper>
+  </ng-container>
+
+  <ng-container class="modal-content">
+    <form name="multisiteMigrateForm"
+          #formDir="ngForm"
+          [formGroup]="multisiteMigrateForm"
+          novalidate>
+    <div class="modal-body">
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="realmName"
+               i18n>Realm Name</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="Realm name..."
+                 id="realmName"
+                 name="realmName"
+                 formControlName="realmName">
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('realmName', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('realmName', formDir, 'uniqueName')"
+                i18n>The chosen realm name is already in use.</span>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zonegroupName"
+               i18n>Rename default zonegroup</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="Zonegroup name..."
+                 id="zonegroupName"
+                 name="zonegroupName"
+                 formControlName="zonegroupName">
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zonegroupName', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zonegroupName', formDir, 'uniqueName')"
+                i18n>The chosen zonegroup name is already in use.</span>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zonegroup_endpoints"
+               i18n>Zonegroup Endpoints</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="e.g, http://ceph-node-00.com:80"
+                 id="zonegroup_endpoints"
+                 name="zonegroup_endpoints"
+                 formControlName="zonegroup_endpoints">
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
+                i18n>Please enter a valid IP address.</span>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zoneName"
+               i18n>Rename default zone</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="Zone name..."
+                 id="zoneName"
+                 name="zoneName"
+                 formControlName="zoneName">
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zoneName', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zoneName', formDir, 'uniqueName')"
+                i18n>The chosen zone name is already in use.</span>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label required"
+               for="zone_endpoints"
+               i18n>Zone Endpoints</label>
+        <div class="cd-col-form-input">
+          <input class="form-control"
+                 type="text"
+                 placeholder="e.g, http://ceph-node-00.com:80"
+                 id="zone_endpoints"
+                 name="zone_endpoints"
+                 formControlName="zone_endpoints">
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'required')"
+                i18n>This field is required.</span>
+          <span class="invalid-feedback"
+                *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'endpoint')"
+                i18n>Please enter a valid IP address.</span>
+        </div>
+      </div>
+      <div class="form-group row">
+        <label class="cd-col-form-label"
+               for="users"
+               i18n>System User</label>
+        <div class="cd-col-form-input">
+          <select id="users"
+                  name="users"
+                  class="form-select"
+                  formControlName="users">
+          <option i18n
+                  *ngIf="users === null"
+                  [ngValue]="null">Loading...</option>
+          <option i18n
+                  *ngIf="users !== null"
+                  [ngValue]="null">-- Select a user --</option>
+          <option *ngFor="let user of users"
+                  [value]="user.user_id">{{ user.user_id }}</option>
+          </select>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <cd-form-button-panel (submitActionEvent)="submit()"
+                            [form]="multisiteMigrateForm"></cd-form-button-panel>
+    </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.spec.ts
new file mode 100644 (file)
index 0000000..77d924d
--- /dev/null
@@ -0,0 +1,38 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('RgwMultisiteMigrateComponent', () => {
+  let component: RgwMultisiteMigrateComponent;
+  let fixture: ComponentFixture<RgwMultisiteMigrateComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        ReactiveFormsModule,
+        RouterTestingModule,
+        HttpClientTestingModule,
+        ToastrModule.forRoot()
+      ],
+      declarations: [RgwMultisiteMigrateComponent],
+      providers: [NgbActiveModal]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwMultisiteMigrateComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts
new file mode 100644 (file)
index 0000000..786a30f
--- /dev/null
@@ -0,0 +1,202 @@
+import { Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite';
+import { ModalService } from '~/app/shared/services/modal.service';
+
+@Component({
+  selector: 'cd-rgw-multisite-migrate',
+  templateUrl: './rgw-multisite-migrate.component.html',
+  styleUrls: ['./rgw-multisite-migrate.component.scss']
+})
+export class RgwMultisiteMigrateComponent implements OnInit {
+  readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
+  readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
+  readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+
+  @Output()
+  submitAction = new EventEmitter();
+
+  multisiteMigrateForm: CdFormGroup;
+  zoneNames: string[];
+  realmList: RgwRealm[];
+  multisiteInfo: object[] = [];
+  realmNames: string[];
+  zonegroupList: RgwZonegroup[];
+  zonegroupNames: string[];
+  zoneList: RgwZone[];
+  realm: RgwRealm;
+  zonegroup: RgwZonegroup;
+  zone: RgwZone;
+  newZonegroupName: any;
+  newZoneName: any;
+  bsModalRef: NgbModalRef;
+  users: any;
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    public actionLabels: ActionLabelsI18n,
+    public rgwMultisiteService: RgwMultisiteService,
+    public rgwZoneService: RgwZoneService,
+    public notificationService: NotificationService,
+    public rgwZonegroupService: RgwZonegroupService,
+    public rgwRealmService: RgwRealmService,
+    public modalService: ModalService
+  ) {
+    this.createForm();
+  }
+
+  createForm() {
+    this.multisiteMigrateForm = new CdFormGroup({
+      realmName: new FormControl(null, {
+        validators: [
+          Validators.required,
+          CdValidators.custom('uniqueName', (realmName: string) => {
+            return this.realmNames && this.zoneNames.indexOf(realmName) !== -1;
+          })
+        ]
+      }),
+      zonegroupName: new FormControl(null, {
+        validators: [
+          Validators.required,
+          CdValidators.custom('uniqueName', (zonegroupName: string) => {
+            return this.zonegroupNames && this.zoneNames.indexOf(zonegroupName) !== -1;
+          })
+        ]
+      }),
+      zoneName: new FormControl(null, {
+        validators: [
+          Validators.required,
+          CdValidators.custom('uniqueName', (zoneName: string) => {
+            return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1;
+          })
+        ]
+      }),
+      zone_endpoints: new FormControl([], {
+        validators: [
+          CdValidators.custom('endpoint', (value: string) => {
+            if (_.isEmpty(value)) {
+              return false;
+            } else {
+              if (value.includes(',')) {
+                value.split(',').forEach((url: string) => {
+                  return (
+                    !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
+                  );
+                });
+              } else {
+                return (
+                  !this.endpoints.test(value) &&
+                  !this.ipv4Rgx.test(value) &&
+                  !this.ipv6Rgx.test(value)
+                );
+              }
+              return false;
+            }
+          }),
+          Validators.required
+        ]
+      }),
+      zonegroup_endpoints: new FormControl(
+        [],
+        [
+          CdValidators.custom('endpoint', (value: string) => {
+            if (_.isEmpty(value)) {
+              return false;
+            } else {
+              if (value.includes(',')) {
+                value.split(',').forEach((url: string) => {
+                  return (
+                    !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
+                  );
+                });
+              } else {
+                return (
+                  !this.endpoints.test(value) &&
+                  !this.ipv4Rgx.test(value) &&
+                  !this.ipv6Rgx.test(value)
+                );
+              }
+              return false;
+            }
+          }),
+          Validators.required
+        ]
+      ),
+      users: new FormControl(null)
+    });
+  }
+
+  ngOnInit(): void {
+    this.realmList =
+      this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms')
+        ? this.multisiteInfo[0]['realms']
+        : [];
+    this.realmNames = this.realmList.map((realm) => {
+      return realm['name'];
+    });
+    this.zonegroupList =
+      this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups')
+        ? this.multisiteInfo[1]['zonegroups']
+        : [];
+    this.zonegroupNames = this.zonegroupList.map((zonegroup) => {
+      return zonegroup['name'];
+    });
+    this.zoneList =
+      this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones')
+        ? this.multisiteInfo[2]['zones']
+        : [];
+    this.zoneNames = this.zoneList.map((zone) => {
+      return zone['name'];
+    });
+    this.rgwZoneService.getUserList('default').subscribe((users: any) => {
+      this.users = users.filter((user: any) => user['system'] === true);
+    });
+  }
+
+  submit() {
+    const values = this.multisiteMigrateForm.value;
+    this.realm = new RgwRealm();
+    this.realm.name = values['realmName'];
+    this.zonegroup = new RgwZonegroup();
+    this.zonegroup.name = values['zonegroupName'];
+    this.zonegroup.endpoints = this.checkUrlArray(values['zonegroup_endpoints']);
+    this.zone = new RgwZone();
+    this.zone.name = values['zoneName'];
+    this.zone.endpoints = this.checkUrlArray(values['zone_endpoints']);
+    const user = values['users'];
+    this.rgwMultisiteService.migrate(this.realm, this.zonegroup, this.zone, user).subscribe(
+      () => {
+        this.notificationService.show(
+          NotificationType.success,
+          $localize`${this.actionLabels.MIGRATE} done successfully`
+        );
+        this.submitAction.emit();
+        this.activeModal.close();
+      },
+      () => {
+        this.notificationService.show(NotificationType.error, $localize`Migration failed`);
+      }
+    );
+  }
+
+  checkUrlArray(endpoints: string) {
+    let endpointsArray = [];
+    if (endpoints.includes(',')) {
+      endpointsArray = endpoints.split(',');
+    } else {
+      endpointsArray.push(endpoints);
+    }
+    return endpointsArray;
+  }
+}
index 7a0c7ecb9835be7830776ec4effe9d95c1a3dbc5..1fe980e4cd5b0c7ad32533a5b34d20ccbe397f19 100644 (file)
@@ -211,7 +211,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
       this.onZoneGroupChange(this.info.data.parent);
       setTimeout(() => {
         this.getUserInfo(zone);
-      }, 1500);
+      }, 1000);
     }
     if (
       this.multisiteZoneForm.getValue('selectedZonegroup') !==
@@ -330,7 +330,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
       this.zone.name = this.info.data.name;
       this.zone.endpoints =
         values['zone_endpoints'] === this.info.data.endpoints
-          ? values['zonegroup_endpoints']
+          ? values['zone_endpoints']
           : this.checkUrlArray(values['zone_endpoints']);
       this.rgwZoneService
         .update(
index 671178b6d74c47cc725636ce86e0ab691efe7e62..520cd349fb9f97a9e6581f50c6355f4a521c6e7d 100644 (file)
@@ -37,6 +37,7 @@ import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form/rgw-mul
 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';
+import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate/rgw-multisite-migrate.component';
 
 @NgModule({
   imports: [
@@ -83,7 +84,8 @@ import { RgwSystemUserComponent } from './rgw-system-user/rgw-system-user.compon
     RgwMultisiteZoneFormComponent,
     RgwMultisiteZoneDeletionFormComponent,
     RgwMultisiteZonegroupDeletionFormComponent,
-    RgwSystemUserComponent
+    RgwSystemUserComponent,
+    RgwMultisiteMigrateComponent
   ]
 })
 export class RgwModule {}
index 400a273cb039a78874227eaebf7059d9d863cfe4..0a601bf0fa13c4ff8cdf1758b4c5122775aa62f9 100644 (file)
@@ -1,6 +1,7 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { RgwDaemonService } from './rgw-daemon.service';
+import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite';
 
 @Injectable({
   providedIn: 'root'
@@ -15,4 +16,18 @@ export class RgwMultisiteService {
       return this.http.get(`${this.url}/sync_status`);
     });
   }
+
+  migrate(realm: RgwRealm, zonegroup: RgwZonegroup, zone: RgwZone, user: string) {
+    return this.rgwDaemonService.request((requestBody: any) => {
+      requestBody = {
+        realm_name: realm.name,
+        zonegroup_name: zonegroup.name,
+        zone_name: zone.name,
+        zonegroup_endpoints: zonegroup.endpoints,
+        zone_endpoints: zone.endpoints,
+        user: user
+      };
+      return this.http.put(`${this.url}/migrate`, requestBody);
+    });
+  }
 }
index cdc81d6d1ed7445e694bf1075244d29558c2f06b..aec37abad607d52b472ddfcaa551a516b7d6070a 100644 (file)
@@ -139,6 +139,7 @@ export class ActionLabelsI18n {
   RESYNC: string;
   EXPORT: string;
   IMPORT: any;
+  MIGRATE: string;
 
   constructor() {
     /* Create a new item */
@@ -148,6 +149,8 @@ export class ActionLabelsI18n {
 
     this.IMPORT = $localize`Import`;
 
+    this.MIGRATE = $localize`Migrate to Multi-Site`;
+
     /* Destroy an existing item */
     this.DELETE = $localize`Delete`;
 
index ee7652187093a1d467d17a4978fc68e740a42f39..4a70f6731083763e7ef7a0caa1285f344d5acd76 100644 (file)
@@ -1260,6 +1260,91 @@ class RgwClient(RestClient):
                     raise DashboardException(error, http_status_code=500, component='rgw')
         return all_users_info
 
+    def migrate_to_multisite(self, realm_name: str, zonegroup_name: str, zone_name: str,
+                             zonegroup_endpoints: List[str], zone_endpoints: List[str], user: str):
+        rgw_realm_create_cmd = ['realm', 'create', '--rgw-realm', realm_name, '--default']
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(rgw_realm_create_cmd, False)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to create realm',
+                                         http_status_code=500, component='rgw')
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+
+        rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', 'default',
+                                  '--zonegroup-new-name', 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(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')
+
+        rgw_zone_edit_cmd = ['zone', 'rename', '--rgw-zone',
+                             'default', '--zone-new-name', zone_name,
+                             '--rgw-zonegroup', zonegroup_name]
+        try:
+            exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_edit_cmd, False)
+            if exit_code > 0:
+                raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(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')
+
+        rgw_zonegroup_modify_cmd = ['zonegroup', 'modify',
+                                    '--rgw-realm', realm_name,
+                                    '--rgw-zonegroup', zonegroup_name]
+        if zonegroup_endpoints:
+            if len(zonegroup_endpoints) > 1:
+                endpoint = ','.join(str(e) for e in zonegroup_endpoints)
+            else:
+                endpoint = zonegroup_endpoints[0]
+            rgw_zonegroup_modify_cmd.append('--endpoints')
+            rgw_zonegroup_modify_cmd.append(endpoint)
+        rgw_zonegroup_modify_cmd.append('--master')
+        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')
+
+        rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-realm', realm_name,
+                               '--rgw-zonegroup', zonegroup_name,
+                               '--rgw-zone', zone_name]
+        if zone_endpoints:
+            if len(zone_endpoints) > 1:
+                endpoint = ','.join(str(e) for e in zone_endpoints)
+            else:
+                endpoint = zone_endpoints[0]
+            rgw_zone_modify_cmd.append('--endpoints')
+            rgw_zone_modify_cmd.append(endpoint)
+        rgw_zone_modify_cmd.append('--master')
+        rgw_zone_modify_cmd.append('--default')
+        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')
+
+        if user:
+            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()
+
     @RestClient.api_get('/{bucket_name}?versioning')
     def get_bucket_versioning(self, bucket_name, request=None):
         """