]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: RBD snapshot clone (frontend)
authorRicardo Marques <rimarques@suse.com>
Thu, 19 Apr 2018 14:11:32 +0000 (15:11 +0100)
committerRicardo Marques <rimarques@suse.com>
Tue, 24 Apr 2018 15:50:37 +0000 (16:50 +0100)
Signed-off-by: Ricardo Marques <rimarques@suse.com>
13 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-mode.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager-message.service.ts

index f6b0ed126d2631dd4f80735af17a0cac03b35639..a015eebe05050f9c82b1b5d1fc83fef761f5b698 100644 (file)
@@ -49,6 +49,11 @@ const routes: Routes = [
   { path: 'rbd/add', component: RbdFormComponent, canActivate: [AuthGuardService] },
   { path: 'rbd/edit/:pool/:name', component: RbdFormComponent, canActivate: [AuthGuardService] },
   { path: 'pool', component: PoolListComponent, canActivate: [AuthGuardService] },
+  {
+    path: 'rbd/clone/:pool/:name/:snap',
+    component: RbdFormComponent,
+    canActivate: [AuthGuardService]
+  },
   {
     path: 'perf_counters/:type/:id',
     component: PerformanceCounterComponent,
index efca5704b5b8883badcd7c4c69e0167b68b8ec82..25a2d4507ce7444bd0ad67a0302f918dabe0e554 100644 (file)
@@ -1,4 +1,5 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
 
 import { TabsModule, TooltipModule } from 'ngx-bootstrap';
 
@@ -13,7 +14,7 @@ describe('RbdDetailsComponent', () => {
   beforeEach(async(() => {
     TestBed.configureTestingModule({
       declarations: [ RbdDetailsComponent, RbdSnapshotListComponent ],
-      imports: [ SharedModule, TabsModule.forRoot(), TooltipModule.forRoot(), ]
+      imports: [ SharedModule, TabsModule.forRoot(), TooltipModule.forRoot(), RouterTestingModule]
     })
     .compileComponents();
   }));
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts
new file mode 100644 (file)
index 0000000..826d4cc
--- /dev/null
@@ -0,0 +1,9 @@
+export class RbdFormCloneRequestModel {
+  child_pool_name: string;
+  child_image_name: string;
+  obj_size: number;
+  features: Array<string> = [];
+  stripe_unit: number;
+  stripe_count: number;
+  data_pool: string;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-mode.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-mode.enum.ts
new file mode 100644 (file)
index 0000000..c89005f
--- /dev/null
@@ -0,0 +1,4 @@
+export enum RbdFormMode {
+  editing = 'editing',
+  cloning = 'cloning'
+}
index 112d060502d26ef6b32d434b4fdbb8b96b3c6c50..7468e3a2b50d2be71dcd353bfaf5b6c690669892 100644 (file)
@@ -1,5 +1,7 @@
 import { RbdFormModel } from './rbd-form.model';
+import { RbdParentModel } from './rbd-parent.model';
 
 export class RbdFormResponseModel extends RbdFormModel {
   features_name: string[];
+  parent: RbdParentModel;
 }
index 19f745f5ec8cf4ad4461ccb34cb318fbb6503e55..71bcda73c69bb87c6832e643bfa9044363c6ab55 100644 (file)
@@ -4,7 +4,7 @@
     <li class="breadcrumb-item">
       <a routerLink="/block/rbd">Images</a></li>
     <li class="breadcrumb-item active"
-        i18n>{editing, select, 1 {Edit} other {Add}}</li>
+        i18n>{mode, select, editing {Edit} cloning {Clone} other {Add}}</li>
   </ol>
 </nav>
 
     <div class="panel panel-default">
       <div class="panel-heading">
         <h3 class="panel-title">
-          <span i18n>{editing, select, 1 {Edit} other {Add}}</span> RBD
+          <span i18n>{mode, select, editing {Edit} cloning {Clone} other {Add}}</span> RBD
         </h3>
       </div>
       <div class="panel-body">
 
+        <!-- Parent -->
+        <div class="form-group"
+             *ngIf="rbdForm.controls.parent.value">
+          <label i18n
+                 class="control-label col-sm-3"
+                 for="name">{mode, select, cloning {Clone from} other {Parent}}
+          </label>
+          <div class="col-sm-9">
+            <input class="form-control"
+                   type="text"
+                   id="parent"
+                   name="parent"
+                   formControlName="parent">
+            <hr>
+          </div>
+        </div>
+
         <!-- Name -->
         <div class="form-group"
              [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.name.dirty) && rbdForm.controls.name.invalid}">
           </label>
           <div class="col-sm-9">
             <div class="checkbox checkbox-primary"
-                 *ngIf="!editing">
+                 *ngIf="mode !== rbdFormMode.editing">
               <input type="checkbox"
                      id="default-features"
                      name="default-features"
                      for="default-features">Use default features</label>
             </div>
             <div *ngIf="!featuresFormGroups.value.defaultFeatures">
-              <br *ngIf="!editing">
+              <br *ngIf="mode !== rbdFormMode.editing">
               <div class="checkbox checkbox-primary"
                    *ngFor="let feature of featuresList">
                 <input type="checkbox"
           <cd-submit-button [form]="rbdForm"
                             type="button"
                             (submitAction)="submit()">
-            <span i18n>{editing, select, 1 {Update} other {Create}}</span> RBD
+            <span i18n>{mode, select, editing {Update} cloning {Clone} other {Create}}</span> RBD
           </cd-submit-button>
           <button i18n
                   type="button"
index 5e936f0a9e1efcec3b2853107759eaf0b3472511..5eb224eb7cd1a59de75d490c219303a426d8421c 100644 (file)
@@ -13,8 +13,10 @@ import { FormatterService } from '../../../shared/services/formatter.service';
 import { NotificationService } from '../../../shared/services/notification.service';
 import { TaskManagerMessageService } from '../../../shared/services/task-manager-message.service';
 import { TaskManagerService } from '../../../shared/services/task-manager.service';
+import { RbdFormCloneRequestModel } from './rbd-form-clone-request.model';
 import { RbdFormCreateRequestModel } from './rbd-form-create-request.model';
 import { RbdFormEditRequestModel } from './rbd-form-edit-request.model';
+import { RbdFormMode } from './rbd-form-mode.enum';
 import { RbdFormResponseModel } from './rbd-form-response.model';
 
 @Component({
@@ -46,9 +48,11 @@ export class RbdFormComponent implements OnInit {
 
   advancedEnabled = false;
 
-  editing = false;
+  public rbdFormMode = RbdFormMode;
+  mode: RbdFormMode;
 
   response: RbdFormResponseModel;
+  snapName: string;
 
   defaultObjectSize = '4MiB';
 
@@ -142,6 +146,7 @@ export class RbdFormComponent implements OnInit {
       'fast-diff': this.fastDiffFormControl,
     });
     this.rbdForm = new FormGroup({
+      parent: new FormControl(''),
       name: new FormControl('', {
         validators: [
           Validators.required
@@ -167,6 +172,7 @@ export class RbdFormComponent implements OnInit {
   }
 
   disableForEdit() {
+    this.rbdForm.get('parent').disable();
     this.rbdForm.get('pool').disable();
     this.rbdForm.get('useDataPool').disable();
     this.rbdForm.get('dataPool').disable();
@@ -175,19 +181,28 @@ export class RbdFormComponent implements OnInit {
     this.rbdForm.get('stripingCount').disable();
   }
 
+  disableForClone() {
+    this.rbdForm.get('parent').disable();
+    this.rbdForm.get('size').disable();
+  }
+
   ngOnInit() {
     if (this.router.url.startsWith('/rbd/edit')) {
-      this.editing = true;
-    }
-    if (this.editing) {
+      this.mode = this.rbdFormMode.editing;
       this.disableForEdit();
+    } else if (this.router.url.startsWith('/rbd/clone')) {
+      this.mode = this.rbdFormMode.cloning;
+      this.disableForClone();
+    }
+    if (this.mode === this.rbdFormMode.editing || this.mode === this.rbdFormMode.cloning) {
       this.routeParamsSubscribe = this.route.params.subscribe(
-        (params: { pool: string, name: string }) => {
+        (params: { pool: string, name: string, snap: string }) => {
           const poolName = params.pool;
           const rbdName = params.name;
+          this.snapName = params.snap;
           this.rbdService.get(poolName, rbdName)
             .subscribe((resp: RbdFormResponseModel) => {
-              this.setResponse(resp);
+              this.setResponse(resp, this.snapName);
             });
         }
       );
@@ -330,7 +345,7 @@ export class RbdFormComponent implements OnInit {
           this.deepBoxCheck(feature, checked);
         }
       }
-      if (this.editing && this.featuresFormGroups.get(feature).enabled) {
+      if (this.mode === this.rbdFormMode.editing && this.featuresFormGroups.get(feature).enabled) {
 
         if (this.response.features_name.indexOf(feature) !== -1 && !details.allowDisable) {
           this.featuresFormGroups.get(feature).disable();
@@ -359,9 +374,18 @@ export class RbdFormComponent implements OnInit {
     }
   }
 
-  setResponse(response: RbdFormResponseModel) {
+  setResponse(response: RbdFormResponseModel, snapName: string) {
     this.response = response;
-    this.rbdForm.get('name').setValue(response.name);
+    if (this.mode === this.rbdFormMode.cloning) {
+      this.rbdForm.get('parent').setValue(`${response.pool_name}/${response.name}@${snapName}`);
+    } else if (response.parent) {
+      const parent = response.parent;
+      this.rbdForm.get('parent')
+        .setValue(`${parent.pool_name}/${parent.image_name}@${parent.snap_name}`);
+    }
+    if (this.mode === this.rbdFormMode.editing) {
+      this.rbdForm.get('name').setValue(response.name);
+    }
     this.rbdForm.get('pool').setValue(response.pool_name);
     if (response.data_pool) {
       this.rbdForm.get('useDataPool').setValue(true);
@@ -443,6 +467,26 @@ export class RbdFormComponent implements OnInit {
     return request;
   }
 
+  cloneRequest(): RbdFormCloneRequestModel {
+    const request = new RbdFormCloneRequestModel();
+    request.child_pool_name = this.rbdForm.get('pool').value;
+    request.child_image_name = this.rbdForm.get('name').value;
+    request.obj_size = this.formatter.toBytes(this.rbdForm.get('obj_size').value);
+    if (!this.defaultFeaturesFormControl.value) {
+      _.forIn(this.features, (feature) => {
+        if (this.featuresFormGroups.get(feature.key).value) {
+          request.features.push(feature.key);
+        }
+      });
+    } else {
+      request.features = null;
+    }
+    request.stripe_unit = this.formatter.toBytes(this.rbdForm.get('stripingUnit').value);
+    request.stripe_count = this.rbdForm.get('stripingCount').value;
+    request.data_pool = this.rbdForm.get('dataPool').value;
+    return request;
+  }
+
   editAction() {
     const request = this.editRequest();
     const finishedTask = new FinishedTask();
@@ -474,9 +518,46 @@ export class RbdFormComponent implements OnInit {
       });
   }
 
+  cloneAction() {
+    const request = this.cloneRequest();
+    const finishedTask = new FinishedTask();
+    finishedTask.name = 'rbd/clone';
+    finishedTask.metadata = {
+      'parent_pool_name': this.response.pool_name,
+      'parent_image_name': this.response.name,
+      'parent_snap_name': this.snapName,
+      'child_pool_name': request.child_pool_name,
+      'child_image_name': request.child_image_name
+    };
+    this.rbdService
+      .cloneSnapshot(this.response.pool_name, this.response.name, this.snapName, request)
+      .subscribe((resp) => {
+        if (resp.status === 202) {
+          this.notificationService.show(NotificationType.info,
+            `RBD clone in progress...`,
+            this.taskManagerMessageService.getDescription(finishedTask));
+          this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata,
+            (asyncFinishedTask: FinishedTask) => {
+              this.notificationService.notifyTask(asyncFinishedTask);
+            });
+        } else {
+          finishedTask.success = true;
+          this.notificationService.notifyTask(finishedTask);
+        }
+        this.router.navigate(['/block/rbd']);
+      }, (resp) => {
+        this.rbdForm.setErrors({'cdSubmitButton': true});
+        finishedTask.success = false;
+        finishedTask.exception = resp.error;
+        this.notificationService.notifyTask(finishedTask);
+      });
+  }
+
   submit() {
-    if (this.editing) {
+    if (this.mode === this.rbdFormMode.editing) {
       this.editAction();
+    } else if (this.mode === this.rbdFormMode.cloning) {
+      this.cloneAction();
     } else {
       this.createAction();
     }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts
new file mode 100644 (file)
index 0000000..a10f8a3
--- /dev/null
@@ -0,0 +1,5 @@
+export class RbdParentModel {
+  image_name: string;
+  pool_name: string;
+  snap_name: string;
+}
index 6ac2fbb02abab4fb1b24aca8989c3bdcf39c74c7..e223ff9f7c6b49429db3dca669c69669cfea6e5d 100644 (file)
@@ -201,6 +201,13 @@ export class RbdListComponent implements OnInit, OnDestroy {
         rbdModel.pool_name = executingTask.metadata['pool_name'];
         rbdModel.cdExecuting = 'creating';
         resultRBDs.push(rbdModel);
+
+      } else if (executingTask.name === 'rbd/clone') {
+        const rbdModel = new RbdModel();
+        rbdModel.name = executingTask.metadata['child_image_name'];
+        rbdModel.pool_name = executingTask.metadata['child_pool_name'];
+        rbdModel.cdExecuting = 'cloning';
+        resultRBDs.push(rbdModel);
       }
     });
     return resultRBDs;
index 76a1d42cb1cd1ea78ebd93afe46353b9c886f5ab..944f755eed9c0890f330a2fb9e0efb398c86d31a 100644 (file)
             <span *ngIf="selection.first()?.is_protected"><i class="fa fa-fw fa-unlock"></i><span i18n>Unprotect</span></span>
           </a>
         </li>
+        <li role="menuitem"
+            [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
+          <a class="dropdown-item" routerLink="/rbd/clone/{{ poolName }}/{{ rbdName }}/{{ selection.first()?.name }}">
+            <i class="fa fa-fw fa-clone"></i><span i18n>Clone</span>
+          </a>
+        </li>
         <li role="menuitem"
             [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
           <a class="dropdown-item" (click)="rollbackModal()"><i class="fa fa-fw fa-undo"></i><span i18n>Rollback</span></a>
index c17eace27f82676bcd247ebbae2e37f3b3c2698d..54ebf3671aa427e8545ac5c3df4587347acf7ee2 100644 (file)
@@ -1,5 +1,6 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
 
 import { ToastModule } from 'ng2-toastr';
 import { BsModalRef, ModalModule } from 'ngx-bootstrap';
@@ -28,7 +29,8 @@ describe('RbdSnapshotListComponent', () => {
         ToastModule.forRoot(),
         ServicesModule,
         ApiModule,
-        HttpClientTestingModule
+        HttpClientTestingModule,
+        RouterTestingModule
       ],
       providers: [ AuthStorageService ]
     })
index c57f1ec6feb3fbf87a41f51e479f9842897f337e..f01cbaad6354dc83dabe9cebe47071623008a68b 100644 (file)
@@ -59,6 +59,12 @@ export class RbdService {
       { observe: 'response' });
   }
 
+  cloneSnapshot(poolName, rbdName, snapshotName, request) {
+    return this.http.post(
+      `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}/clone`, request,
+      { observe: 'response' });
+  }
+
   deleteSnapshot(poolName, rbdName, snapshotName) {
     return this.http.delete(
       `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`,
index cc4be50f0a024023f87bfc7c7c89fcae1127a3de..f4fa85dc0377e5b829413813449745d991a1d52f 100644 (file)
@@ -52,6 +52,18 @@ export class TaskManagerMessageService {
         };
       }
     ),
+    'rbd/clone': new TaskManagerMessage(
+      (metadata) => `Clone RBD '${metadata.child_pool_name}/${metadata.child_image_name}'`,
+      (metadata) => `RBD '${metadata.child_pool_name}/${metadata.child_image_name}'
+                     has been cloned successfully`,
+      (metadata) => {
+        return {
+          '17': `Name '${metadata.child_pool_name}/${metadata.child_image_name}' is already
+                 in use.`,
+          '22': `Snapshot must be protected.`
+        };
+      }
+    ),
     'rbd/snap/create': new TaskManagerMessage(
       (metadata) => `Create snapshot ` +
                     `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`,