From: Ricardo Marques Date: Fri, 20 Apr 2018 21:36:18 +0000 (+0100) Subject: mgr/dashboard: RBD flatten X-Git-Tag: v13.1.0~92^2~4 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=838c9ce9e3ee91c80151b26b9d13baee6918f317;p=ceph.git mgr/dashboard: RBD flatten Signed-off-by: Ricardo Marques --- diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py index 127b1f59e72f..1d5164b6ad3d 100644 --- a/qa/tasks/mgr/dashboard/test_rbd.py +++ b/qa/tasks/mgr/dashboard/test_rbd.py @@ -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) diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index e177bd1ddcf3..424706ffa7ef 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -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() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index 61eebd1682f3..477b80d4a0b6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -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 index 000000000000..36d06d833a8f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.html @@ -0,0 +1,30 @@ + +
+ + +
+ 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 index 000000000000..e69de29bb2d1 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 index 000000000000..4d904101312f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.spec.ts @@ -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; + + 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 index 000000000000..6d9174f80b3d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/flatten-confirmation-modal/flatten-confimation-modal.component.ts @@ -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; + + constructor(public modalRef: BsModalRef) { + this.createForm(); + } + + createForm() { + this.flattenForm = new FormGroup({}); + } + + ngOnInit() { + this.onSubmit = new Subject(); + } + + submit() { + this.onSubmit.next(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html index b7324eade489..1a3bbd06d9f7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html @@ -50,6 +50,10 @@ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}"> Copy +
  • + Flatten +
  • Delete diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts index febdd22da6fa..41a0c03e59f7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts @@ -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); + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts index 855f829f9db2..047a1a693e5f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager-message.service.ts index 6fcb041c13d2..ab17f01d5596 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-manager-message.service.ts @@ -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}'`,