]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: RBD flatten
authorRicardo Marques <rimarques@suse.com>
Fri, 20 Apr 2018 21:36:18 +0000 (22:36 +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>
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.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 127b1f59e72ff3d42f81debab7473219d49de9c7..1d5164b6ad3dfcfa3db2906ac6b9597e3c40cb7d 100644 (file)
@@ -59,6 +59,10 @@ class RbdTest(DashboardTestCase):
         return cls._task_put('/api/block/image/{}/{}'.format(pool, image),
                              {'name': name, 'size': size, 'features': features})
 
+    @classmethod
+    def flatten_image(cls, pool, image):
+        return cls._task_post('/api/block/image/{}/{}/flatten'.format(pool, image))
+
     @classmethod
     def create_snapshot(cls, pool, image, snapshot):
         return cls._task_post('/api/block/image/{}/{}/snap'.format(pool, image),
@@ -510,3 +514,26 @@ class RbdTest(DashboardTestCase):
         self.assertStatus(204)
         self.remove_image('rbd_iscsi', 'coimg-copy')
         self.assertStatus(204)
+
+    def test_flatten(self):
+        self.create_snapshot('rbd', 'img1', 'snapf')
+        self.update_snapshot('rbd', 'img1', 'snapf', None, True)
+        self.clone_image('rbd', 'img1', 'snapf', 'rbd_iscsi', 'img1_snapf_clone')
+
+        img = self._get('/api/block/image/rbd_iscsi/img1_snapf_clone')
+        self.assertStatus(200)
+        self.assertIsNotNone(img['parent'])
+
+        self.flatten_image('rbd_iscsi', 'img1_snapf_clone')
+        self.assertStatus(200)
+
+        img = self._get('/api/block/image/rbd_iscsi/img1_snapf_clone')
+        self.assertStatus(200)
+        self.assertIsNone(img['parent'])
+
+        self.update_snapshot('rbd', 'img1', 'snapf', None, False)
+        self.remove_snapshot('rbd', 'img1', 'snapf')
+        self.assertStatus(204)
+
+        self.remove_image('rbd_iscsi', 'img1_snapf_clone')
+        self.assertStatus(204)
index e177bd1ddcf3439f3e1f40a2c0bad8e19d97b0a1..424706ffa7efe6b548b5f02813811cba7213a80e 100644 (file)
@@ -349,6 +349,15 @@ class Rbd(RESTController):
 
         return _rbd_image_call(pool_name, image_name, _src_copy)
 
+    @RbdTask('flatten', ['{pool_name}', '{image_name}'], 2.0)
+    @RESTController.resource(['POST'])
+    def flatten(self, pool_name, image_name):
+
+        def _flatten(ioctx, image):
+            image.flatten()
+
+        return _rbd_image_call(pool_name, image_name, _flatten)
+
 
 @ApiController('block/image/:pool_name/:image_name/snap')
 @AuthRequired()
index 61eebd1682f3ab7e3e058e06434c3749a4dd42d1..477b80d4a0b63012ffcd30222a4d5dc7f2441043 100644 (file)
@@ -7,6 +7,9 @@ import { BsDropdownModule, ModalModule, TabsModule, TooltipModule } from 'ngx-bo
 import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
 
 import { SharedModule } from '../../shared/shared.module';
+import {
+  FlattenConfirmationModalComponent
+} from './flatten-confirmation-modal/flatten-confimation-modal.component';
 import { IscsiComponent } from './iscsi/iscsi.component';
 import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
 import { MirroringComponent } from './mirroring/mirroring.component';
@@ -23,7 +26,8 @@ import {
   entryComponents: [
     RbdDetailsComponent,
     RbdSnapshotFormComponent,
-    RollbackConfirmationModalComponent
+    RollbackConfirmationModalComponent,
+    FlattenConfirmationModalComponent
   ],
   imports: [
     CommonModule,
@@ -46,7 +50,8 @@ import {
     RbdFormComponent,
     RbdSnapshotListComponent,
     RbdSnapshotFormComponent,
-    RollbackConfirmationModalComponent
+    RollbackConfirmationModalComponent,
+    FlattenConfirmationModalComponent
   ]
 })
 export class BlockModule { }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.html
new file mode 100644 (file)
index 0000000..36d06d8
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="modal-header">
+  <h4 i18n
+      class="modal-title pull-left">RBD flatten</h4>
+  <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
+    <span aria-hidden="true">&times;</span>
+  </button>
+</div>
+<form name="flattenForm"
+      class="form-horizontal"
+      #formDir="ngForm"
+      [formGroup]="flattenForm"
+      novalidate>
+  <div class="modal-body">
+    You are about to flatten <strong>{{ child }}</strong>.
+    <br>
+    <br>
+    All blocks will be copied from parent <strong>{{ parent }}</strong> to child <strong>{{ child }}</strong>.
+  </div>
+  <div class="modal-footer">
+    <div class="button-group text-right">
+      <cd-submit-button i18n
+                        [form]="flattenForm"
+                        (submitAction)="submit()">
+        Flatten
+      </cd-submit-button>
+      <button i18n type="button" class="btn btn-sm btn-default" (click)="modalRef.hide()">Cancel</button>
+    </div>
+  </div>
+</form>
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..4d90410
--- /dev/null
@@ -0,0 +1,42 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { ToastModule } from 'ng2-toastr';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap';
+
+import { ApiModule } from '../../../shared/api/api.module';
+import { ServicesModule } from '../../../shared/services/services.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { FlattenConfirmationModalComponent } from './flatten-confimation-modal.component';
+
+describe('FlattenConfirmationModalComponent', () => {
+  let component: FlattenConfirmationModalComponent;
+  let fixture: ComponentFixture<FlattenConfirmationModalComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        ReactiveFormsModule,
+        HttpClientTestingModule,
+        SharedModule,
+        ServicesModule,
+        ApiModule,
+        ToastModule.forRoot()
+      ],
+      declarations: [ FlattenConfirmationModalComponent ],
+      providers: [ BsModalRef, BsModalService ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(FlattenConfirmationModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.ts
new file mode 100644 (file)
index 0000000..6d9174f
--- /dev/null
@@ -0,0 +1,36 @@
+import { Component, OnInit } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+
+import { BsModalRef } from 'ngx-bootstrap';
+import { Subject } from 'rxjs/Subject';
+
+@Component({
+  selector: 'cd-flatten-confimation-modal',
+  templateUrl: './flatten-confimation-modal.component.html',
+  styleUrls: ['./flatten-confimation-modal.component.scss']
+})
+export class FlattenConfirmationModalComponent implements OnInit {
+
+  child: string;
+  parent: string;
+
+  flattenForm: FormGroup;
+
+  public onSubmit: Subject<string>;
+
+  constructor(public modalRef: BsModalRef) {
+    this.createForm();
+  }
+
+  createForm() {
+    this.flattenForm = new FormGroup({});
+  }
+
+  ngOnInit() {
+    this.onSubmit = new Subject();
+  }
+
+  submit() {
+    this.onSubmit.next();
+  }
+}
index b7324eade489d5be777c8a2644eb5bed9e929144..1a3bbd06d9f7e550133de013b94f437a1bb0a82b 100644 (file)
             [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
           <a class="dropdown-item" routerLink="/rbd/copy/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}"><i class="fa fa-fw fa-copy"></i><span i18n>Copy</span></a>
         </li>
+        <li role="menuitem"
+            [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing || !selection.first().parent}">
+          <a class="dropdown-item" (click)="flattenRbdModal()"><i class="fa fa-fw fa-chain-broken"></i><span i18n>Flatten</span></a>
+        </li>
         <li role="menuitem"
             [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
           <a class="dropdown-item" (click)="deleteRbdModal()"><i class="fa fa-fw fa-trash-o"></i><span i18n>Delete</span></a>
index febdd22da6fa7b87937c2bf09b8e4d8fba08597b..41a0c03e59f7b9c91a7ad2da79dfc45d68c8f3a5 100644 (file)
@@ -22,6 +22,10 @@ import {
 import { SummaryService } from '../../../shared/services/summary.service';
 import { TaskManagerMessageService } from '../../../shared/services/task-manager-message.service';
 import { TaskManagerService } from '../../../shared/services/task-manager.service';
+import {
+  FlattenConfirmationModalComponent
+} from '../flatten-confirmation-modal/flatten-confimation-modal.component';
+import { RbdParentModel } from '../rbd-form/rbd-parent.model';
 import { RbdModel } from './rbd-model';
 
 @Component({
@@ -194,7 +198,11 @@ export class RbdListComponent implements OnInit, OnDestroy {
 
         } else if (executingTask.name === 'rbd/edit') {
           rbdExecuting.cdExecuting = 'updating';
+
+        } else if (executingTask.name === 'rbd/flatten') {
+          rbdExecuting.cdExecuting = 'flattening';
         }
+
       } else if (executingTask.name === 'rbd/create') {
         const rbdModel = new RbdModel();
         rbdModel.name = executingTask.metadata['image_name'];
@@ -267,4 +275,47 @@ export class RbdListComponent implements OnInit, OnDestroy {
       modalRef: this.modalRef
     });
   }
+
+  flattenRbd(poolName, imageName) {
+    const finishedTask = new FinishedTask();
+    finishedTask.name = 'rbd/flatten';
+    finishedTask.metadata = {'pool_name': poolName, 'image_name': imageName};
+    this.rbdService.flatten(poolName, imageName)
+      .toPromise().then((resp) => {
+        if (resp.status === 202) {
+          this.notificationService.show(NotificationType.info,
+            `RBD flatten in progress...`,
+            this.taskManagerMessageService.getDescription(finishedTask));
+          const executingTask = new ExecutingTask();
+          executingTask.name = finishedTask.name;
+          executingTask.metadata = finishedTask.metadata;
+          this.executingTasks.push(executingTask);
+          this.taskManagerService.subscribe(executingTask.name, executingTask.metadata,
+            (asyncFinishedTask: FinishedTask) => {
+              this.notificationService.notifyTask(asyncFinishedTask);
+            });
+        } else {
+          finishedTask.success = true;
+          this.notificationService.notifyTask(finishedTask);
+        }
+        this.modalRef.hide();
+        this.loadImages(null);
+      }).catch((resp) => {
+        finishedTask.success = false;
+        finishedTask.exception = resp.error;
+        this.notificationService.notifyTask(finishedTask);
+      });
+  }
+
+  flattenRbdModal() {
+    const poolName = this.selection.first().pool_name;
+    const imageName = this.selection.first().name;
+    this.modalRef = this.modalService.show(FlattenConfirmationModalComponent);
+    const parent: RbdParentModel = this.selection.first().parent;
+    this.modalRef.content.parent = `${parent.pool_name}/${parent.image_name}@${parent.snap_name}`;
+    this.modalRef.content.child = `${poolName}/${imageName}`;
+    this.modalRef.content.onSubmit.subscribe(() => {
+      this.flattenRbd(poolName, imageName);
+    });
+  }
 }
index 855f829f9db20ae821f16e97c3676c2b911c37d2..047a1a693e5fcb712179b54938a61003082e9f5b 100644 (file)
@@ -32,6 +32,11 @@ export class RbdService {
       { observe: 'response' });
   }
 
+  flatten(poolName, rbdName) {
+    return this.http.post(`api/block/image/${poolName}/${rbdName}/flatten`, null,
+      { observe: 'response' });
+  }
+
   createSnapshot(poolName, rbdName, snapshotName) {
     const request = {
       snapshot_name: snapshotName
index 6fcb041c13d23debcce27e2260ba214f591fcf9c..ab17f01d55965a8de8a5a089d3090ba7e1f41bbf 100644 (file)
@@ -75,6 +75,15 @@ export class TaskManagerMessageService {
         };
       }
     ),
+    'rbd/flatten': new TaskManagerMessage(
+      (metadata) => `Flatten RBD '${metadata.pool_name}/${metadata.image_name}'`,
+      (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'
+                     has been flattened successfully`,
+      () => {
+        return {
+        };
+      }
+    ),
     'rbd/snap/create': new TaskManagerMessage(
       (metadata) => `Create snapshot ` +
                     `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`,