From 4e9a5aa7a6e66c3466ff73a4223ce93bca6641f4 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Thu, 22 Mar 2018 12:41:45 +0000 Subject: [PATCH] mgr/dashboard: RBD management Signed-off-by: Ricardo Marques --- .../frontend/src/app/app-routing.module.ts | 7 +- .../src/app/ceph/block/block.module.ts | 35 +- .../pool-detail/pool-detail.component.html | 24 - .../pool-detail/pool-detail.component.ts | 96 ---- .../rbd-details/rbd-details.component.html | 136 +++++ .../rbd-details.component.scss} | 0 .../rbd-details/rbd-details.component.spec.ts | 30 ++ .../rbd-details/rbd-details.component.ts | 22 + .../rbd-form/rbd-form-create-request.model.ts | 5 + .../rbd-form/rbd-form-edit-request.model.ts | 5 + .../block/rbd-form/rbd-form-response.model.ts | 5 + .../block/rbd-form/rbd-form.component.html | 314 ++++++++++++ .../block/rbd-form/rbd-form.component.scss | 0 .../block/rbd-form/rbd-form.component.spec.ts | 42 ++ .../ceph/block/rbd-form/rbd-form.component.ts | 485 ++++++++++++++++++ .../app/ceph/block/rbd-form/rbd-form.model.ts | 9 + .../block/rbd-list/rbd-list.component.html | 68 +++ .../block/rbd-list/rbd-list.component.scss | 0 .../rbd-list.component.spec.ts} | 26 +- .../ceph/block/rbd-list/rbd-list.component.ts | 253 +++++++++ .../src/app/ceph/block/rbd-list/rbd-model.ts | 6 + .../rbd-snapshot-form.component.html | 50 ++ .../rbd-snapshot-form.component.scss | 0 .../rbd-snapshot-form.component.spec.ts | 43 ++ .../rbd-snapshot-form.component.ts | 115 +++++ .../rbd-snapshot-list.component.html | 53 ++ .../rbd-snapshot-list.component.scss | 0 .../rbd-snapshot-list.component.spec.ts | 44 ++ .../rbd-snapshot-list.component.ts | 238 +++++++++ .../rbd-snapshot-list/rbd-snapshot.model.ts | 9 + .../rollback-confimation-modal.component.html | 27 + .../rollback-confimation-modal.component.scss | 0 ...llback-confimation-modal.component.spec.ts | 42 ++ .../rollback-confimation-modal.component.ts | 35 ++ .../navigation/navigation.component.html | 26 +- .../frontend/src/app/shared/api/api.module.ts | 2 + .../src/app/shared/api/pool.service.ts | 5 +- .../src/app/shared/api/rbd.service.ts | 67 +++ .../src/app/shared/empty.pipe.spec.ts | 8 + .../frontend/src/app/shared/empty.pipe.ts | 14 + .../src/app/shared/enum/unix_errno.enum.ts | 4 + .../src/app/shared/pipes/pipes.module.ts | 10 +- .../services/task-manager-message.service.ts | 79 +++ .../frontend/src/app/shared/shared.module.ts | 8 + 44 files changed, 2287 insertions(+), 160 deletions(-) delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html rename src/pybind/mgr/dashboard/frontend/src/app/ceph/block/{pool-detail/pool-detail.component.scss => rbd-details/rbd-details.component.scss} (100%) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss rename src/pybind/mgr/dashboard/frontend/src/app/ceph/block/{pool-detail/pool-detail.component.spec.ts => rbd-list/rbd-list.component.spec.ts} (53%) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 8883796d367d..1b3c15bb2854 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -3,7 +3,8 @@ import { RouterModule, Routes } from '@angular/router'; import { IscsiComponent } from './ceph/block/iscsi/iscsi.component'; import { MirroringComponent } from './ceph/block/mirroring/mirroring.component'; -import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component'; +import { RbdFormComponent } from './ceph/block/rbd-form/rbd-form.component'; +import { RbdListComponent } from './ceph/block/rbd-list/rbd-list.component'; import { CephfsComponent } from './ceph/cephfs/cephfs/cephfs.component'; import { ClientsComponent } from './ceph/cephfs/clients/clients.component'; import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component'; @@ -31,7 +32,9 @@ const routes: Routes = [ canActivate: [AuthGuardService] }, { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] }, - { path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] }, + { path: 'block/rbd', component: RbdListComponent, canActivate: [AuthGuardService] }, + { path: 'rbd/add', component: RbdFormComponent, canActivate: [AuthGuardService] }, + { path: 'rbd/edit/:pool/:name', component: RbdFormComponent, canActivate: [AuthGuardService] }, { path: 'perf_counters/:type/:id', component: PerformanceCounterComponent, 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 bd799d176338..61eebd1682f3 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 @@ -1,29 +1,52 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { BsDropdownModule, ModalModule, TabsModule, TooltipModule } from 'ngx-bootstrap'; import { ProgressbarModule } from 'ngx-bootstrap/progressbar'; -import { TabsModule } from 'ngx-bootstrap/tabs'; import { SharedModule } from '../../shared/shared.module'; import { IscsiComponent } from './iscsi/iscsi.component'; import { MirrorHealthColorPipe } from './mirror-health-color.pipe'; import { MirroringComponent } from './mirroring/mirroring.component'; -import { PoolDetailComponent } from './pool-detail/pool-detail.component'; +import { RbdDetailsComponent } from './rbd-details/rbd-details.component'; +import { RbdFormComponent } from './rbd-form/rbd-form.component'; +import { RbdListComponent } from './rbd-list/rbd-list.component'; +import { RbdSnapshotFormComponent } from './rbd-snapshot-form/rbd-snapshot-form.component'; +import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component'; +import { + RollbackConfirmationModalComponent +} from './rollback-confirmation-modal/rollback-confimation-modal.component'; @NgModule({ + entryComponents: [ + RbdDetailsComponent, + RbdSnapshotFormComponent, + RollbackConfirmationModalComponent + ], imports: [ CommonModule, FormsModule, + ReactiveFormsModule, TabsModule.forRoot(), ProgressbarModule.forRoot(), - SharedModule + BsDropdownModule.forRoot(), + TooltipModule.forRoot(), + ModalModule.forRoot(), + SharedModule, + RouterModule ], declarations: [ - PoolDetailComponent, + RbdListComponent, IscsiComponent, MirroringComponent, - MirrorHealthColorPipe + MirrorHealthColorPipe, + RbdDetailsComponent, + RbdFormComponent, + RbdSnapshotListComponent, + RbdSnapshotFormComponent, + RollbackConfirmationModalComponent ] }) export class BlockModule { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html deleted file mode 100644 index 5a1b0bb5ebcf..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - {{ value.pool_name }}/{{ value.image_name }}@{{ value.snap_name }} - - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts deleted file mode 100644 index 018758a3a32b..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -import { PoolService } from '../../../shared/api/pool.service'; -import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; -import { CdTableColumn } from '../../../shared/models/cd-table-column'; -import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; -import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; - -@Component({ - selector: 'cd-pool-detail', - templateUrl: './pool-detail.component.html', - styleUrls: ['./pool-detail.component.scss'] -}) -export class PoolDetailComponent implements OnInit, OnDestroy { - @ViewChild('parentTpl') parentTpl: TemplateRef; - - name: string; - images: any; - columns: CdTableColumn[]; - retries: number; - routeParamsSubscribe: any; - viewCacheStatus: ViewCacheStatus; - - constructor( - private route: ActivatedRoute, - private poolService: PoolService, - dimlessBinaryPipe: DimlessBinaryPipe, - dimlessPipe: DimlessPipe - ) { - this.columns = [ - { - name: 'Name', - prop: 'name', - cellTemplate: this.parentTpl, - flexGrow: 2 - }, - { - name: 'Size', - prop: 'size', - flexGrow: 1, - cellClass: 'text-right', - pipe: dimlessBinaryPipe - }, - { - name: 'Objects', - prop: 'num_objs', - flexGrow: 1, - cellClass: 'text-right', - pipe: dimlessPipe - }, - { - name: 'Object size', - prop: 'obj_size', - flexGrow: 1, - cellClass: 'text-right', - pipe: dimlessBinaryPipe - }, - { - name: 'Features', - prop: 'features_name', - flexGrow: 3 - }, - { - name: 'Parent', - prop: 'parent', - cellTemplate: this.parentTpl, - flexGrow: 2 - } - ]; - } - - ngOnInit() { - this.routeParamsSubscribe = this.route.params.subscribe((params: { name: string }) => { - this.name = params.name; - this.images = []; - this.retries = 0; - }); - } - - ngOnDestroy() { - this.routeParamsSubscribe.unsubscribe(); - } - - loadImages() { - this.poolService.rbdPoolImages(this.name).then( - resp => { - this.viewCacheStatus = resp[0].status; - this.images = resp[0].value; - }, - () => { - this.viewCacheStatus = ViewCacheStatus.ValueException; - } - ); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html new file mode 100644 index 000000000000..655154a859c0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html @@ -0,0 +1,136 @@ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ selectedItem.name }}
Pool + {{ selectedItem.pool_name }}
Data Pool + {{ selectedItem.data_pool | empty }}
Created + {{ selectedItem.timestamp | cdDate }}
Size + {{ selectedItem.size | dimlessBinary }}
Objects + {{ selectedItem.num_objs | dimless }}
Object size + {{ selectedItem.obj_size | dimlessBinary }}
Features + + + {{ feature }} + +
Provisioned + + + N/A + + + {{ selectedItem.disk_usage | dimlessBinary }} + +
Total provisioned + + + N/A + + + {{ selectedItem.total_disk_usage | dimlessBinary }} + +
Striping unit + {{ selectedItem.stripe_unit | dimlessBinary }}
Striping count + {{ selectedItem.stripe_count }}
Parent + + {{ selectedItem.parent.pool_name }}/{{ selectedItem.parent.image_name }}@{{ selectedItem.parent.snap_name }} + - +
Block name prefix + {{ selectedItem.block_name_prefix }}
Order + {{ selectedItem.order }}
+
+ + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.scss similarity index 100% rename from src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.scss rename to src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.scss diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts new file mode 100644 index 000000000000..efca5704b5b8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.spec.ts @@ -0,0 +1,30 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabsModule, TooltipModule } from 'ngx-bootstrap'; + +import { SharedModule } from '../../../shared/shared.module'; +import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component'; +import { RbdDetailsComponent } from './rbd-details.component'; + +describe('RbdDetailsComponent', () => { + let component: RbdDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RbdDetailsComponent, RbdSnapshotListComponent ], + imports: [ SharedModule, TabsModule.forRoot(), TooltipModule.forRoot(), ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts new file mode 100644 index 000000000000..896e295a4cdc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.ts @@ -0,0 +1,22 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; + +@Component({ + selector: 'cd-rbd-details', + templateUrl: './rbd-details.component.html', + styleUrls: ['./rbd-details.component.scss'] +}) +export class RbdDetailsComponent implements OnChanges { + + @Input() selection: CdTableSelection; + selectedItem: any; + + constructor() { } + + ngOnChanges() { + if (this.selection.hasSelection) { + this.selectedItem = this.selection.first(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts new file mode 100644 index 000000000000..2a2366f7c02d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-create-request.model.ts @@ -0,0 +1,5 @@ +import { RbdFormModel } from './rbd-form.model'; + +export class RbdFormCreateRequestModel extends RbdFormModel { + features: Array = []; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts new file mode 100644 index 000000000000..39495630a4c5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts @@ -0,0 +1,5 @@ +export class RbdFormEditRequestModel { + name: string; + size: number; + features: Array = []; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts new file mode 100644 index 000000000000..112d060502d2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-response.model.ts @@ -0,0 +1,5 @@ +import { RbdFormModel } from './rbd-form.model'; + +export class RbdFormResponseModel extends RbdFormModel { + features_name: string[]; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html new file mode 100644 index 000000000000..19f745f5ec8c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html @@ -0,0 +1,314 @@ + + +
+
+
+
+

+ {editing, select, 1 {Edit} other {Add}} RBD +

+
+
+ + +
+ +
+ + + This field is required. + +
+
+ + +
+ +
+ + + This field is required. + +
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+ + + This field is required. + +
+
+ + +
+ +
+ + + This field is required. + + + You have to increase the size. + +
+
+ + +
+ +
+
+ + +
+
+
+
+ + + + +
+
+
+
+ + + +
+ + + + +
+ +
+ +
+
+ + +
+ +
+ + + This field is required because stripe count is defined! + + + Stripe unit is greater than object size. + +
+
+ + +
+ +
+ + + This field is required because stripe unit is defined! + + + Stripe count must be greater than 0. + +
+
+ +
+ +
+ +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts new file mode 100644 index 000000000000..8c7c3fc27afb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.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 { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; + +import { ApiModule } from '../../../shared/api/api.module'; +import { ComponentsModule } from '../../../shared/components/components.module'; +import { ServicesModule } from '../../../shared/services/services.module'; +import { RbdFormComponent } from './rbd-form.component'; + +describe('RbdFormComponent', () => { + let component: RbdFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + HttpClientTestingModule, + RouterTestingModule, + ComponentsModule, + ServicesModule, + ApiModule, + ToastModule.forRoot() + ], + declarations: [ RbdFormComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts new file mode 100644 index 000000000000..c6add11f11f2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts @@ -0,0 +1,485 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import * as _ from 'lodash'; + +import { PoolService } from '../../../shared/api/pool.service'; +import { RbdService } from '../../../shared/api/rbd.service'; +import { NotificationType } from '../../../shared/enum/notification-type.enum'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; +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 { RbdFormCreateRequestModel } from './rbd-form-create-request.model'; +import { RbdFormEditRequestModel } from './rbd-form-edit-request.model'; +import { RbdFormResponseModel } from './rbd-form-response.model'; + +@Component({ + selector: 'cd-rbd-form', + templateUrl: './rbd-form.component.html', + styleUrls: ['./rbd-form.component.scss'] +}) +export class RbdFormComponent implements OnInit { + + rbdForm: FormGroup; + featuresFormGroups: FormGroup; + defaultFeaturesFormControl: FormControl; + deepFlattenFormControl: FormControl; + layeringFormControl: FormControl; + exclusiveLockFormControl: FormControl; + objectMapFormControl: FormControl; + journalingFormControl: FormControl; + fastDiffFormControl: FormControl; + + pools: Array = null; + allPools: Array = null; + dataPools: Array = null; + allDataPools: Array = null; + features: any; + featuresList = []; + + routeParamsSubscribe: any; + pool: string; + + advancedEnabled = false; + + editing = false; + + response: RbdFormResponseModel; + + defaultObjectSize = '4MiB'; + + objectSizes: Array = [ + '4KiB', + '8KiB', + '16KiB', + '32KiB', + '64KiB', + '128KiB', + '256KiB', + '512KiB', + '1MiB', + '2MiB', + '4MiB', + '8MiB', + '16MiB', + '32MiB' + ]; + + constructor(private route: ActivatedRoute, + private router: Router, + private poolService: PoolService, + private rbdService: RbdService, + private formatter: FormatterService, + private dimlessBinaryPipe: DimlessBinaryPipe, + private taskManagerService: TaskManagerService, + private taskManagerMessageService: TaskManagerMessageService, + private notificationService: NotificationService) { + this.features = { + 'deep-flatten': { + desc: 'Deep flatten', + requires: null, + allowEnable: false, + allowDisable: true + }, + 'layering': { + desc: 'Layering', + requires: null, + allowEnable: false, + allowDisable: false + }, + 'exclusive-lock': { + desc: 'Exclusive lock', + requires: null, + allowEnable: true, + allowDisable: true + }, + 'object-map': { + desc: 'Object map (requires exclusive-lock)', + requires: 'exclusive-lock', + allowEnable: true, + allowDisable: true + }, + 'journaling': { + desc: 'Journaling (requires exclusive-lock)', + requires: 'exclusive-lock', + allowEnable: true, + allowDisable: true + }, + 'fast-diff': { + desc: 'Fast diff (requires object-map)', + requires: 'object-map', + allowEnable: true, + allowDisable: true + } + }; + this.createForm(); + for (const key of Object.keys(this.features)) { + const listItem = this.features[key]; + listItem.key = key; + this.featuresList.push(listItem); + } + } + + createForm() { + this.defaultFeaturesFormControl = new FormControl(true); + this.deepFlattenFormControl = new FormControl(false); + this.layeringFormControl = new FormControl(false); + this.exclusiveLockFormControl = new FormControl(false); + this.objectMapFormControl = new FormControl({value: false, disabled: true}); + this.journalingFormControl = new FormControl({value: false, disabled: true}); + this.fastDiffFormControl = new FormControl({value: false, disabled: true}); + this.featuresFormGroups = new FormGroup({ + defaultFeatures: this.defaultFeaturesFormControl, + 'deep-flatten': this.deepFlattenFormControl, + 'layering': this.layeringFormControl, + 'exclusive-lock': this.exclusiveLockFormControl, + 'object-map': this.objectMapFormControl, + 'journaling': this.journalingFormControl, + 'fast-diff': this.fastDiffFormControl, + }); + this.rbdForm = new FormGroup({ + name: new FormControl('', { + validators: [ + Validators.required + ] + }), + pool: new FormControl(null, { + validators: [ + Validators.required + ] + }), + useDataPool: new FormControl(false), + dataPool: new FormControl(null), + size: new FormControl(null, { + updateOn: 'blur' + }), + obj_size: new FormControl(this.defaultObjectSize), + features: this.featuresFormGroups, + stripingUnit: new FormControl(null), + stripingCount: new FormControl(null, { + updateOn: 'blur' + }) + }, this.validateRbdForm(this.formatter)); + } + + disableForEdit() { + this.rbdForm.get('pool').disable(); + this.rbdForm.get('useDataPool').disable(); + this.rbdForm.get('dataPool').disable(); + this.rbdForm.get('obj_size').disable(); + this.rbdForm.get('stripingUnit').disable(); + this.rbdForm.get('stripingCount').disable(); + } + + ngOnInit() { + if (this.router.url.startsWith('/rbd/edit')) { + this.editing = true; + } + if (this.editing) { + this.disableForEdit(); + this.routeParamsSubscribe = this.route.params.subscribe( + (params: { pool: string, name: string }) => { + const poolName = params.pool; + const rbdName = params.name; + this.rbdService.get(poolName, rbdName) + .subscribe((resp: RbdFormResponseModel) => { + this.setResponse(resp); + }); + } + ); + } + this.poolService.list(['pool_name', 'type', 'flags_names', 'application_metadata']).then( + resp => { + const pools = []; + const dataPools = []; + for (const pool of resp) { + if (!_.isUndefined(pool.application_metadata.rbd)) { + if (pool.type === 'replicated') { + pools.push(pool); + dataPools.push(pool); + } else if (pool.type === 'erasure' && + pool.flags_names.indexOf('ec_overwrites') !== -1) { + dataPools.push(pool); + } + } + } + this.pools = pools; + this.allPools = pools; + this.dataPools = dataPools; + this.allDataPools = dataPools; + if (this.pools.length === 1) { + const poolName = this.pools[0]['pool_name']; + this.rbdForm.get('pool').setValue(poolName); + this.onPoolChange(poolName); + } + } + ); + this.defaultFeaturesFormControl.valueChanges.subscribe((value) => { + this.watchDataFeatures(null, value); + }); + this.deepFlattenFormControl.valueChanges.subscribe((value) => { + this.watchDataFeatures('deep-flatten', value); + }); + this.layeringFormControl.valueChanges.subscribe((value) => { + this.watchDataFeatures('layering', value); + }); + this.exclusiveLockFormControl.valueChanges.subscribe((value) => { + this.watchDataFeatures('exclusive-lock', value); + }); + this.objectMapFormControl.valueChanges.subscribe((value) => { + this.watchDataFeatures('object-map', value); + }); + this.journalingFormControl.valueChanges.subscribe((value) => { + this.watchDataFeatures('journaling', value); + }); + this.fastDiffFormControl.valueChanges.subscribe((value) => { + this.watchDataFeatures('fast-diff', value); + }); + } + + onPoolChange(selectedPoolName) { + const newDataPools = this.allDataPools.filter((dataPool: any) => { + return dataPool.pool_name !== selectedPoolName; + }); + if (this.rbdForm.get('dataPool').value === selectedPoolName) { + this.rbdForm.get('dataPool').setValue(null); + } + this.dataPools = newDataPools; + } + + onUseDataPoolChange () { + if (!this.rbdForm.get('useDataPool').value) { + this.rbdForm.get('dataPool').setValue(null); + this.onDataPoolChange(null); + } + } + + onDataPoolChange(selectedDataPoolName) { + const newPools = this.allPools.filter((pool: any) => { + return pool.pool_name !== selectedDataPoolName; + }); + if (this.rbdForm.get('pool').value === selectedDataPoolName) { + this.rbdForm.get('pool').setValue(null); + } + this.pools = newPools; + } + + validateRbdForm(formatter: FormatterService) { + return (formGroup: FormGroup) => { + // Data Pool + const useDataPoolControl = formGroup.get('useDataPool'); + const dataPoolControl = formGroup.get('dataPool'); + let dataPoolControlErrors = null; + if (useDataPoolControl.value && dataPoolControl.value == null) { + dataPoolControlErrors = {'required': true}; + } + dataPoolControl.setErrors(dataPoolControlErrors); + // Size + const sizeControl = formGroup.get('size'); + const objectSizeControl = formGroup.get('obj_size'); + const objectSizeInBytes = formatter.toBytes( + objectSizeControl.value != null ? objectSizeControl.value : this.defaultObjectSize); + const stripingCountControl = formGroup.get('stripingCount'); + const stripingCount = stripingCountControl.value != null ? stripingCountControl.value : 1; + let sizeControlErrors = null; + if (sizeControl.value === null) { + sizeControlErrors = {'required': true}; + } else { + const sizeInBytes = formatter.toBytes(sizeControl.value); + if (stripingCount * objectSizeInBytes > sizeInBytes) { + sizeControlErrors = {'invalidSizeObject': true}; + } + } + sizeControl.setErrors(sizeControlErrors); + // Striping Unit + const stripingUnitControl = formGroup.get('stripingUnit'); + let stripingUnitControlErrors = null; + if (stripingUnitControl.value === null && stripingCountControl.value !== null) { + stripingUnitControlErrors = {'required': true}; + } else if (stripingUnitControl.value !== null) { + const stripingUnitInBytes = formatter.toBytes(stripingUnitControl.value); + if (stripingUnitInBytes > objectSizeInBytes) { + stripingUnitControlErrors = {'invalidStripingUnit': true}; + } + } + stripingUnitControl.setErrors(stripingUnitControlErrors); + // Striping Count + let stripingCountControlErrors = null; + if (stripingCountControl.value === null && stripingUnitControl.value !== null) { + stripingCountControlErrors = {'required': true}; + } else if (stripingCount < 1) { + stripingCountControlErrors = {'min': true}; + } + stripingCountControl.setErrors(stripingCountControlErrors); + }; + } + + deepBoxCheck(key, checked) { + _.forIn(this.features, (details, feature) => { + if (details.requires === key) { + if (checked) { + this.featuresFormGroups.get(feature).enable(); + } else { + this.featuresFormGroups.get(feature).disable(); + this.featuresFormGroups.get(feature).setValue(checked); + this.watchDataFeatures(feature, checked); + this.deepBoxCheck(feature, checked); + } + } + if (this.editing && this.featuresFormGroups.get(feature).enabled) { + + if (this.response.features_name.indexOf(feature) !== -1 && !details.allowDisable) { + this.featuresFormGroups.get(feature).disable(); + + } else if (this.response.features_name.indexOf(feature) === -1 && !details.allowEnable) { + this.featuresFormGroups.get(feature).disable(); + } + } + }); + } + + featureFormUpdate(key, checked) { + if (checked) { + const required = this.features[key].requires; + if (required && !this.featuresFormGroups.get(required).value) { + this.featuresFormGroups.get(key).setValue(false); + return; + } + } + this.deepBoxCheck(key, checked); + } + + watchDataFeatures(key, checked) { + if (!this.defaultFeaturesFormControl.value && key) { + this.featureFormUpdate(key, checked); + } + } + + setResponse(response: RbdFormResponseModel) { + this.response = response; + 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); + this.rbdForm.get('dataPool').setValue(response.data_pool); + } + this.rbdForm.get('size').setValue(this.dimlessBinaryPipe.transform(response.size)); + this.rbdForm.get('obj_size').setValue(this.dimlessBinaryPipe.transform(response.obj_size)); + const featuresControl = this.rbdForm.get('features'); + featuresControl.get('defaultFeatures').setValue(false); + _.forIn(this.features, (feature) => { + if (response.features_name.indexOf(feature.key) !== -1) { + featuresControl.get(feature.key).setValue(true); + } + }); + this.rbdForm.get('stripingUnit').setValue( + this.dimlessBinaryPipe.transform(response.stripe_unit)); + this.rbdForm.get('stripingCount').setValue(response.stripe_count); + } + + createRequest() { + const request = new RbdFormCreateRequestModel(); + request.pool_name = this.rbdForm.get('pool').value; + request.name = this.rbdForm.get('name').value; + request.size = this.formatter.toBytes(this.rbdForm.get('size').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; + } + + createAction() { + const request = this.createRequest(); + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/create'; + finishedTask.metadata = {'pool_name': request.pool_name, 'image_name': request.name}; + this.rbdService.create(request).toPromise().then((resp) => { + if (resp.status === 202) { + this.notificationService.show(NotificationType.info, + `RBD creation 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); + }); + } + + editRequest() { + const request = new RbdFormEditRequestModel(); + request.name = this.rbdForm.get('name').value; + request.size = this.formatter.toBytes(this.rbdForm.get('size').value); + if (!this.defaultFeaturesFormControl.value) { + _.forIn(this.features, (feature) => { + if (this.featuresFormGroups.get(feature.key).value) { + request.features.push(feature.key); + } + }); + } + return request; + } + + editAction() { + const request = this.editRequest(); + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/edit'; + finishedTask.metadata = { + 'pool_name': this.response.pool_name, + 'image_name': this.response.name + }; + this.rbdService.update(this.response.pool_name, this.response.name, request) + .toPromise().then((resp) => { + if (resp.status === 202) { + this.notificationService.show(NotificationType.info, + `RBD update 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']); + }).catch((resp) => { + this.rbdForm.setErrors({'cdSubmitButton': true}); + finishedTask.success = false; + finishedTask.exception = resp.error; + this.notificationService.notifyTask(finishedTask); + }); + } + + submit() { + if (this.editing) { + this.editAction(); + } else { + this.createAction(); + } + } + +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts new file mode 100644 index 000000000000..014b82716d3a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts @@ -0,0 +1,9 @@ +export class RbdFormModel { + name: string; + pool_name: string; + data_pool: string; + size: number; + obj_size: number; + stripe_unit: number; + stripe_count: number; +} 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 new file mode 100644 index 000000000000..c568ae0cc646 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html @@ -0,0 +1,68 @@ + + + + + +
+
+ + + + +
+
+ + +
+ + +
+
+ + + {{ value.pool_name }}/{{ value.image_name }}@{{ value.snap_name }} + - + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts similarity index 53% rename from src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts rename to src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts index aea790cf1da7..fa6c5bd18678 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/pool-detail/pool-detail.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts @@ -2,15 +2,24 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { AlertModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap'; +import { ToastModule } from 'ng2-toastr'; +import { + AlertModule, + BsDropdownModule, + ModalModule, + TabsModule, + TooltipModule +} from 'ngx-bootstrap'; import { ComponentsModule } from '../../../shared/components/components.module'; import { SharedModule } from '../../../shared/shared.module'; -import { PoolDetailComponent } from './pool-detail.component'; +import { RbdDetailsComponent } from '../rbd-details/rbd-details.component'; +import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component'; +import { RbdListComponent } from './rbd-list.component'; -describe('PoolDetailComponent', () => { - let component: PoolDetailComponent; - let fixture: ComponentFixture; +describe('RbdListComponent', () => { + let component: RbdListComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -18,18 +27,21 @@ describe('PoolDetailComponent', () => { SharedModule, BsDropdownModule.forRoot(), TabsModule.forRoot(), + ModalModule.forRoot(), + TooltipModule.forRoot(), + ToastModule.forRoot(), AlertModule.forRoot(), ComponentsModule, RouterTestingModule, HttpClientTestingModule ], - declarations: [ PoolDetailComponent ] + declarations: [ RbdListComponent, RbdDetailsComponent, RbdSnapshotListComponent ] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(PoolDetailComponent); + fixture = TestBed.createComponent(RbdListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); 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 new file mode 100644 index 000000000000..f90014efd587 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts @@ -0,0 +1,253 @@ +import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; + +import * as _ from 'lodash'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap'; + +import { RbdService } from '../../../shared/api/rbd.service'; +import { + DeleteConfirmationComponent +} from '../../../shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; +import { CellTemplate } from '../../../shared/enum/cell-template.enum'; +import { NotificationType } from '../../../shared/enum/notification-type.enum'; +import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; +import { CdTableColumn } from '../../../shared/models/cd-table-column'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { ExecutingTask } from '../../../shared/models/executing-task'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; +import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; +import { + NotificationService +} from '../../../shared/services/notification.service'; +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 { RbdModel } from './rbd-model'; + +@Component({ + selector: 'cd-rbd-list', + templateUrl: './rbd-list.component.html', + styleUrls: ['./rbd-list.component.scss'] +}) +export class RbdListComponent implements OnInit, OnDestroy { + + @ViewChild('usageTpl') usageTpl: TemplateRef; + @ViewChild('parentTpl') parentTpl: TemplateRef; + @ViewChild('nameTpl') nameTpl: TemplateRef; + + images: any; + executingTasks: ExecutingTask[] = []; + columns: CdTableColumn[]; + retries: number; + viewCacheStatusList: any[]; + selection = new CdTableSelection(); + + summaryDataSubscription = null; + + modalRef: BsModalRef; + + constructor(private rbdService: RbdService, + private dimlessBinaryPipe: DimlessBinaryPipe, + private dimlessPipe: DimlessPipe, + private summaryService: SummaryService, + private modalService: BsModalService, + private notificationService: NotificationService, + private taskManagerMessageService: TaskManagerMessageService, + private taskManagerService: TaskManagerService) { + } + + ngOnInit() { + this.columns = [ + { + name: 'Name', + prop: 'name', + flexGrow: 2, + cellTransformation: CellTemplate.executing + }, + { + name: 'Pool', + prop: 'pool_name', + flexGrow: 2 + }, + { + name: 'Size', + prop: 'size', + flexGrow: 1, + cellClass: 'text-right', + pipe: this.dimlessBinaryPipe + }, + { + name: 'Objects', + prop: 'num_objs', + flexGrow: 1, + cellClass: 'text-right', + pipe: this.dimlessPipe + }, + { + name: 'Object size', + prop: 'obj_size', + flexGrow: 1, + cellClass: 'text-right', + pipe: this.dimlessBinaryPipe + }, + { + name: 'Provisioned', + prop: 'disk_usage', + cellClass: 'text-center', + flexGrow: 1, + pipe: this.dimlessBinaryPipe + }, + { + name: 'Total provisioned', + prop: 'total_disk_usage', + cellClass: 'text-center', + flexGrow: 1, + pipe: this.dimlessBinaryPipe + }, + { + name: 'Parent', + prop: 'parent', + flexGrow: 2, + cellTemplate: this.parentTpl + } + ]; + + this.summaryService.get().then(resp => { + this.loadImages(resp.executing_tasks); + this.summaryDataSubscription = this.summaryService.summaryData$.subscribe((data: any) => { + this.loadImages(data.executing_tasks); + }); + }); + + } + + ngOnDestroy() { + if (this.summaryDataSubscription) { + this.summaryDataSubscription.unsubscribe(); + } + } + + loadImages(executingTasks) { + if (executingTasks === null) { + executingTasks = this.executingTasks; + } + this.rbdService.list() + .subscribe( + (resp: any[]) => { + let images = []; + const viewCacheStatusMap = {}; + resp.forEach(pool => { + if (_.isUndefined(viewCacheStatusMap[pool.status])) { + viewCacheStatusMap[pool.status] = []; + } + viewCacheStatusMap[pool.status].push(pool.pool_name); + images = images.concat(pool.value); + }); + const viewCacheStatusList = []; + _.forEach(viewCacheStatusMap, (value, key) => { + viewCacheStatusList.push({ + status: parseInt(key, 10), + statusFor: (value.length > 1 ? 'pools ' : 'pool ') + + '' + value.join(', ') + '' + }); + }); + this.viewCacheStatusList = viewCacheStatusList; + images.forEach(image => { + image.executingTasks = this._getExecutingTasks(executingTasks, + image.pool_name, image.name); + }); + this.images = this.merge(images, executingTasks); + this.executingTasks = executingTasks; + }, + () => { + this.viewCacheStatusList = [{status: ViewCacheStatus.ValueException}]; + } + ); + } + + _getExecutingTasks(executingTasks: ExecutingTask[], poolName, imageName): ExecutingTask[] { + const result: ExecutingTask[] = []; + executingTasks.forEach(executingTask => { + if (executingTask.name === 'rbd/snap/create' || + executingTask.name === 'rbd/snap/delete' || + executingTask.name === 'rbd/snap/edit' || + executingTask.name === 'rbd/snap/rollback') { + if (poolName === executingTask.metadata['pool_name'] && + imageName === executingTask.metadata['image_name']) { + result.push(executingTask); + } + } + }); + return result; + } + + private merge(rbds: RbdModel[], executingTasks: ExecutingTask[] = []) { + const resultRBDs = _.clone(rbds); + executingTasks.forEach((executingTask) => { + const rbdExecuting = resultRBDs.find((rbd) => { + return rbd.pool_name === executingTask.metadata['pool_name'] && + rbd.name === executingTask.metadata['image_name']; + }); + if (rbdExecuting) { + if (executingTask.name === 'rbd/delete') { + rbdExecuting.cdExecuting = 'deleting'; + + } else if (executingTask.name === 'rbd/edit') { + rbdExecuting.cdExecuting = 'updating'; + } + } else if (executingTask.name === 'rbd/create') { + const rbdModel = new RbdModel(); + rbdModel.name = executingTask.metadata['image_name']; + rbdModel.pool_name = executingTask.metadata['pool_name']; + rbdModel.cdExecuting = 'creating'; + resultRBDs.push(rbdModel); + } + }); + return resultRBDs; + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + deleteRbd(poolName: string, imageName: string) { + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/delete'; + finishedTask.metadata = {'pool_name': poolName, 'image_name': imageName}; + this.rbdService.delete(poolName, imageName) + .toPromise().then((resp) => { + if (resp.status === 202) { + this.notificationService.show(NotificationType.info, + `RBD deletion 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); + }); + } + + deleteRbdModal() { + const poolName = this.selection.first().pool_name; + const imageName = this.selection.first().name; + this.modalRef = this.modalService.show(DeleteConfirmationComponent); + this.modalRef.content.itemName = `${poolName}/${imageName}`; + this.modalRef.content.onSubmit.subscribe(() => { + this.deleteRbd(poolName, imageName); + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts new file mode 100644 index 000000000000..8be8dafdb1a5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts @@ -0,0 +1,6 @@ +export class RbdModel { + name: string; + pool_name: string; + + cdExecuting: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html new file mode 100644 index 000000000000..dc277020b0fe --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html @@ -0,0 +1,50 @@ + +
+ + + +
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts new file mode 100644 index 000000000000..3217fe39f4c3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.spec.ts @@ -0,0 +1,43 @@ +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 { ComponentsModule } from '../../../shared/components/components.module'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { ServicesModule } from '../../../shared/services/services.module'; +import { RbdSnapshotFormComponent } from './rbd-snapshot-form.component'; + +describe('RbdSnapshotFormComponent', () => { + let component: RbdSnapshotFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + ComponentsModule, + HttpClientTestingModule, + ServicesModule, + ApiModule, + ToastModule.forRoot() + ], + declarations: [ RbdSnapshotFormComponent ], + providers: [ BsModalRef, BsModalService, AuthStorageService ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdSnapshotFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts new file mode 100644 index 000000000000..42d19b320fa4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts @@ -0,0 +1,115 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap'; +import { Subject } from 'rxjs/Subject'; + +import { RbdService } from '../../../shared/api/rbd.service'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { + NotificationService +} from '../../../shared/services/notification.service'; +import { TaskManagerService } from '../../../shared/services/task-manager.service'; + +@Component({ + selector: 'cd-rbd-snapshot-form', + templateUrl: './rbd-snapshot-form.component.html', + styleUrls: ['./rbd-snapshot-form.component.scss'] +}) +export class RbdSnapshotFormComponent implements OnInit { + + poolName: string; + imageName: string; + snapName: string; + + snapshotForm: FormGroup; + + editing = false; + + public onSubmit: Subject; + + constructor(public modalRef: BsModalRef, + private rbdService: RbdService, + private taskManagerService: TaskManagerService, + private notificationService: NotificationService) { + this.createForm(); + } + + createForm() { + this.snapshotForm = new FormGroup({ + snapshotName: new FormControl('', { + validators: [ + Validators.required + ] + }) + }); + } + + ngOnInit() { + this.onSubmit = new Subject(); + } + + setSnapName(snapName) { + this.snapName = snapName; + this.snapshotForm.get('snapshotName').setValue(snapName); + this.editing = true; + } + + editAction() { + const snapshotName = this.snapshotForm.get('snapshotName').value; + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/snap/edit'; + finishedTask.metadata = { + 'pool_name': this.poolName, + 'image_name': this.imageName, + 'snapshot_name': snapshotName + }; + this.rbdService.renameSnapshot(this.poolName, this.imageName, this.snapName, snapshotName) + .toPromise().then((resp) => { + this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata, + (asyncFinishedTask: FinishedTask) => { + this.notificationService.notifyTask(asyncFinishedTask); + }); + this.modalRef.hide(); + this.onSubmit.next(this.snapName); + }).catch((resp) => { + this.snapshotForm.setErrors({'cdSubmitButton': true}); + finishedTask.success = false; + finishedTask.exception = resp.error; + this.notificationService.notifyTask(finishedTask); + }); + } + + createAction() { + const snapshotName = this.snapshotForm.get('snapshotName').value; + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/snap/create'; + finishedTask.metadata = { + 'pool_name': this.poolName, + 'image_name': this.imageName, + 'snapshot_name': snapshotName + }; + this.rbdService.createSnapshot(this.poolName, this.imageName, snapshotName) + .toPromise().then((resp) => { + this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata, + (asyncFinishedTask: FinishedTask) => { + this.notificationService.notifyTask(asyncFinishedTask); + }); + this.modalRef.hide(); + this.onSubmit.next(snapshotName); + }).catch((resp) => { + this.snapshotForm.setErrors({'cdSubmitButton': true}); + finishedTask.success = false; + finishedTask.exception = resp.error; + this.notificationService.notifyTask(finishedTask); + }); + } + + submit() { + if (this.editing) { + this.editAction(); + } else { + this.createAction(); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html new file mode 100644 index 000000000000..76a1d42cb1cd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html @@ -0,0 +1,53 @@ + +
+
+ + + + +
+
+
+ + + PROTECTED + UNPROTECTED + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts new file mode 100644 index 000000000000..e4ce34466017 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts @@ -0,0 +1,44 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { ModalModule } from 'ngx-bootstrap'; + +import { ApiModule } from '../../../shared/api/api.module'; +import { ComponentsModule } from '../../../shared/components/components.module'; +import { DataTableModule } from '../../../shared/datatable/datatable.module'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { ServicesModule } from '../../../shared/services/services.module'; +import { RbdSnapshotListComponent } from './rbd-snapshot-list.component'; + +describe('RbdSnapshotListComponent', () => { + let component: RbdSnapshotListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RbdSnapshotListComponent ], + imports: [ + DataTableModule, + ComponentsModule, + ModalModule.forRoot(), + ToastModule.forRoot(), + ServicesModule, + ApiModule, + HttpClientTestingModule + ], + providers: [ AuthStorageService ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdSnapshotListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts new file mode 100644 index 000000000000..9730579ed83b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts @@ -0,0 +1,238 @@ +import { + Component, + Input, + OnChanges, + OnInit, + TemplateRef, + ViewChild +} from '@angular/core'; + +import * as _ from 'lodash'; +import { ToastsManager } from 'ng2-toastr'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap'; + +import { + RbdService +} from '../../../shared/api/rbd.service'; +import { + DeleteConfirmationComponent +} from '../../../shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; +import { CellTemplate } from '../../../shared/enum/cell-template.enum'; +import { CdTableColumn } from '../../../shared/models/cd-table-column'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { ExecutingTask } from '../../../shared/models/executing-task'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe'; +import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; +import { + NotificationService +} from '../../../shared/services/notification.service'; +import { TaskManagerService } from '../../../shared/services/task-manager.service'; +import { RbdSnapshotFormComponent } from '../rbd-snapshot-form/rbd-snapshot-form.component'; +import { + RollbackConfirmationModalComponent +} from '../rollback-confirmation-modal/rollback-confimation-modal.component'; +import { RbdSnapshotModel } from './rbd-snapshot.model'; + +@Component({ + selector: 'cd-rbd-snapshot-list', + templateUrl: './rbd-snapshot-list.component.html', + styleUrls: ['./rbd-snapshot-list.component.scss'] +}) +export class RbdSnapshotListComponent implements OnInit, OnChanges { + + @Input() snapshots: RbdSnapshotModel[] = []; + @Input() poolName: string; + @Input() rbdName: string; + @Input() executingTasks: ExecutingTask[] = []; + + @ViewChild('nameTpl') nameTpl: TemplateRef; + @ViewChild('protectTpl') protectTpl: TemplateRef; + + data: RbdSnapshotModel[]; + + columns: CdTableColumn[]; + + modalRef: BsModalRef; + + selection = new CdTableSelection(); + + constructor(private modalService: BsModalService, + private dimlessBinaryPipe: DimlessBinaryPipe, + private cdDatePipe: CdDatePipe, + private rbdService: RbdService, + private toastr: ToastsManager, + private taskManagerService: TaskManagerService, + private notificationService: NotificationService) { } + + ngOnInit() { + this.columns = [ + { + name: 'Name', + prop: 'name', + cellTransformation: CellTemplate.executing, + flexGrow: 2 + }, + { + name: 'Size', + prop: 'size', + flexGrow: 1, + cellClass: 'text-right', + pipe: this.dimlessBinaryPipe + }, + { + name: 'Provisioned', + prop: 'disk_usage', + flexGrow: 1, + cellClass: 'text-right', + pipe: this.dimlessBinaryPipe + }, + { + name: 'State', + prop: 'is_protected', + flexGrow: 1, + cellClass: 'text-center', + cellTemplate: this.protectTpl + }, + { + name: 'Created', + prop: 'timestamp', + flexGrow: 1, + pipe: this.cdDatePipe + } + ]; + } + + ngOnChanges() { + this.data = this.merge(this.snapshots, this.executingTasks); + } + + private merge(snapshots: RbdSnapshotModel[], executingTasks: ExecutingTask[] = []) { + const resultSnapshots = _.clone(snapshots); + executingTasks.forEach((executingTask) => { + const snapshotExecuting = resultSnapshots.find((snapshot) => { + return snapshot.name === executingTask.metadata['snapshot_name']; + }); + if (snapshotExecuting) { + if (executingTask.name === 'rbd/snap/delete') { + snapshotExecuting.cdExecuting = 'deleting'; + + } else if (executingTask.name === 'rbd/snap/edit') { + snapshotExecuting.cdExecuting = 'updating'; + + } else if (executingTask.name === 'rbd/snap/rollback') { + snapshotExecuting.cdExecuting = 'rolling back'; + } + } else if (executingTask.name === 'rbd/snap/create') { + const rbdSnapshotModel = new RbdSnapshotModel(); + rbdSnapshotModel.name = executingTask.metadata['snapshot_name']; + rbdSnapshotModel.cdExecuting = 'creating'; + resultSnapshots.push(rbdSnapshotModel); + } + }); + return resultSnapshots; + } + + private openSnapshotModal(taskName: string, oldSnapshotName: string = null) { + this.modalRef = this.modalService.show(RbdSnapshotFormComponent); + this.modalRef.content.poolName = this.poolName; + this.modalRef.content.imageName = this.rbdName; + if (oldSnapshotName) { + this.modalRef.content.setSnapName(this.selection.first().name); + } + this.modalRef.content.onSubmit.subscribe((snapshotName: string) => { + const executingTask = new ExecutingTask(); + executingTask.name = taskName; + executingTask.metadata = {'snapshot_name': snapshotName}; + this.executingTasks.push(executingTask); + this.ngOnChanges(); + }); + } + + openCreateSnapshotModal() { + this.openSnapshotModal('rbd/snap/create'); + } + + openEditSnapshotModal() { + this.openSnapshotModal('rbd/snap/edit', this.selection.first().name); + } + + toggleProtection() { + const snapshotName = this.selection.first().name; + const isProtected = this.selection.first().is_protected; + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/snap/edit'; + finishedTask.metadata = { + 'pool_name': this.poolName, + 'image_name': this.rbdName, + 'snapshot_name': snapshotName + }; + this.rbdService.protectSnapshot(this.poolName, this.rbdName, snapshotName, !isProtected) + .toPromise().then((resp) => { + const executingTask = new ExecutingTask(); + executingTask.name = finishedTask.name; + executingTask.metadata = finishedTask.metadata; + this.executingTasks.push(executingTask); + this.ngOnChanges(); + this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata, + (asyncFinishedTask: FinishedTask) => { + this.notificationService.notifyTask(asyncFinishedTask); + }); + }).catch((resp) => { + finishedTask.success = false; + finishedTask.exception = resp.error; + this.notificationService.notifyTask(finishedTask); + }); + } + + private asyncTask(task: string, taskName: string, snapshotName: string) { + const finishedTask = new FinishedTask(); + finishedTask.name = taskName; + finishedTask.metadata = { + 'pool_name': this.poolName, + 'image_name': this.rbdName, + 'snapshot_name': snapshotName + }; + this.rbdService[task](this.poolName, this.rbdName, snapshotName) + .toPromise().then(() => { + const executingTask = new ExecutingTask(); + executingTask.name = finishedTask.name; + executingTask.metadata = finishedTask.metadata; + this.executingTasks.push(executingTask); + this.modalRef.hide(); + this.ngOnChanges(); + this.taskManagerService.subscribe(executingTask.name, executingTask.metadata, + (asyncFinishedTask: FinishedTask) => { + this.notificationService.notifyTask(asyncFinishedTask); + }); + }) + .catch((resp) => { + this.modalRef.hide(); + finishedTask.success = false; + finishedTask.exception = resp.error; + this.notificationService.notifyTask(finishedTask); + }); + } + + rollbackModal() { + const snapshotName = this.selection.selected[0].name; + this.modalRef = this.modalService.show(RollbackConfirmationModalComponent); + this.modalRef.content.snapName = `${this.poolName}/${this.rbdName}@${snapshotName}`; + this.modalRef.content.onSubmit.subscribe((itemName: string) => { + this.asyncTask('rollbackSnapshot', 'rbd/snap/rollback', snapshotName); + }); + } + + deleteSnapshotModal() { + const snapshotName = this.selection.selected[0].name; + this.modalRef = this.modalService.show(DeleteConfirmationComponent); + this.modalRef.content.itemName = snapshotName; + this.modalRef.content.onSubmit.subscribe((itemName: string) => { + this.asyncTask('deleteSnapshot', 'rbd/snap/delete', snapshotName); + }); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts new file mode 100644 index 000000000000..06fd28783ee9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot.model.ts @@ -0,0 +1,9 @@ +export class RbdSnapshotModel { + id: number; + name: string; + size: number; + timestamp: string; + is_protected: boolean; + + cdExecuting: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.html new file mode 100644 index 000000000000..e1ef7d46c784 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.html @@ -0,0 +1,27 @@ + +
+ + +
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.spec.ts new file mode 100644 index 000000000000..f041d4999e7f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-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 { RollbackConfirmationModalComponent } from './rollback-confimation-modal.component'; + +describe('RollbackConfirmationModalComponent', () => { + let component: RollbackConfirmationModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + HttpClientTestingModule, + SharedModule, + ServicesModule, + ApiModule, + ToastModule.forRoot() + ], + declarations: [ RollbackConfirmationModalComponent ], + providers: [ BsModalRef, BsModalService ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RollbackConfirmationModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.ts new file mode 100644 index 000000000000..596c6e942794 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rollback-confirmation-modal/rollback-confimation-modal.component.ts @@ -0,0 +1,35 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { BsModalRef } from 'ngx-bootstrap'; +import { Subject } from 'rxjs/Subject'; + +@Component({ + selector: 'cd-rollback-confimation-modal', + templateUrl: './rollback-confimation-modal.component.html', + styleUrls: ['./rollback-confimation-modal.component.scss'] +}) +export class RollbackConfirmationModalComponent implements OnInit { + + snapName: string; + + rollbackForm: FormGroup; + + public onSubmit: Subject; + + constructor(public modalRef: BsModalRef) { + this.createForm(); + } + + createForm() { + this.rollbackForm = new FormGroup({}); + } + + ngOnInit() { + this.onSubmit = new Subject(); + } + + submit() { + this.onSubmit.next(this.snapName); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 99bf8380912c..5ed75f6296b0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -93,6 +93,12 @@ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api.module.ts index dbb1af6d5a7d..61b1c32e7561 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/api.module.ts @@ -10,6 +10,7 @@ import { MonitorService } from './monitor.service'; import { OsdService } from './osd.service'; import { PoolService } from './pool.service'; import { RbdMirroringService } from './rbd-mirroring.service'; +import { RbdService } from './rbd.service'; import { RgwDaemonService } from './rgw-daemon.service'; import { TablePerformanceCounterService } from './table-performance-counter.service'; import { TcmuIscsiService } from './tcmu-iscsi.service'; @@ -26,6 +27,7 @@ import { TcmuIscsiService } from './tcmu-iscsi.service'; MonitorService, OsdService, PoolService, + RbdService, RbdMirroringService, RgwDaemonService, TablePerformanceCounterService, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts index 9589c6a8dd9d..96e9c8666c90 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/pool.service.ts @@ -7,8 +7,9 @@ export class PoolService { constructor(private http: HttpClient) { } - rbdPoolImages(pool) { - return this.http.get(`api/block/image?pool_name=${pool}`).toPromise().then((resp: any) => { + list(attrs = []) { + const attrsStr = attrs.join(','); + return this.http.get(`api/pool?attrs=${attrsStr}`).toPromise().then((resp: any) => { return resp; }); } 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 new file mode 100644 index 000000000000..c57f1ec6feb3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts @@ -0,0 +1,67 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class RbdService { + + constructor(private http: HttpClient) { + } + + create(rbd) { + return this.http.post('api/block/image', rbd, { observe: 'response' }); + } + + delete(poolName, rbdName) { + return this.http.delete(`api/block/image/${poolName}/${rbdName}`, { observe: 'response' }); + } + + update(poolName, rbdName, rbd) { + return this.http.put(`api/block/image/${poolName}/${rbdName}`, rbd, { observe: 'response' }); + } + + get(poolName, rbdName) { + return this.http.get(`api/block/image/${poolName}/${rbdName}`); + } + + list() { + return this.http.get('api/block/image'); + } + + createSnapshot(poolName, rbdName, snapshotName) { + const request = { + snapshot_name: snapshotName + }; + return this.http.post(`api/block/image/${poolName}/${rbdName}/snap`, request, + { observe: 'response' }); + } + + renameSnapshot(poolName, rbdName, snapshotName, newSnapshotName) { + const request = { + new_snap_name: newSnapshotName + }; + return this.http.put( + `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, request, + { observe: 'response' }); + } + + protectSnapshot(poolName, rbdName, snapshotName, isProtected) { + const request = { + is_protected: isProtected + }; + return this.http.put( + `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, request, + { observe: 'response' }); + } + + rollbackSnapshot(poolName, rbdName, snapshotName) { + return this.http.post( + `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}/rollback`, null, + { observe: 'response' }); + } + + deleteSnapshot(poolName, rbdName, snapshotName) { + return this.http.delete( + `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, + { observe: 'response' }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.spec.ts new file mode 100644 index 000000000000..9e5b0b2b5fff --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.spec.ts @@ -0,0 +1,8 @@ +import { EmptyPipe } from './empty.pipe'; + +describe('EmptyPipe', () => { + it('create an instance', () => { + const pipe = new EmptyPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.ts new file mode 100644 index 000000000000..5c7845ddf99e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/empty.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import * as _ from 'lodash'; + +@Pipe({ + name: 'empty' +}) +export class EmptyPipe implements PipeTransform { + + transform(value: any, args?: any): any { + return _.isUndefined(value) || _.isNull(value) ? '-' : value; + } + +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts new file mode 100644 index 000000000000..02bf31e3e14f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/unix_errno.enum.ts @@ -0,0 +1,4 @@ +// http://www.virtsync.com/c-error-codes-include-errno +export enum UnixErrno { + EEXIST = 17, // File exists +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts index bb0e238cc03f..e5b1a24fa8cb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts @@ -1,6 +1,7 @@ import { CommonModule, DatePipe } from '@angular/common'; import { NgModule } from '@angular/core'; +import { EmptyPipe } from '../empty.pipe'; import { CdDatePipe } from './cd-date.pipe'; import { CephShortVersionPipe } from './ceph-short-version.pipe'; import { DimlessBinaryPipe } from './dimless-binary.pipe'; @@ -20,7 +21,8 @@ import { RelativeDatePipe } from './relative-date.pipe'; RelativeDatePipe, ListPipe, FilterPipe, - CdDatePipe + CdDatePipe, + EmptyPipe ], exports: [ DimlessBinaryPipe, @@ -30,7 +32,8 @@ import { RelativeDatePipe } from './relative-date.pipe'; RelativeDatePipe, ListPipe, FilterPipe, - CdDatePipe + CdDatePipe, + EmptyPipe ], providers: [ DatePipe, @@ -39,7 +42,8 @@ import { RelativeDatePipe } from './relative-date.pipe'; DimlessPipe, RelativeDatePipe, ListPipe, - CdDatePipe + CdDatePipe, + EmptyPipe ] }) export class PipesModule {} 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 1eb70e1305c4..cc4be50f0a02 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 @@ -20,6 +20,85 @@ class TaskManagerMessage { export class TaskManagerMessageService { messages = { + 'rbd/create': new TaskManagerMessage( + (metadata) => `Create RBD '${metadata.pool_name}/${metadata.image_name}'`, + (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}' + has been created successfully`, + (metadata) => { + return { + '17': `Name '${metadata.pool_name}/${metadata.image_name}' is already + in use.` + }; + } + ), + 'rbd/edit': new TaskManagerMessage( + (metadata) => `Update RBD '${metadata.pool_name}/${metadata.image_name}'`, + (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}' + has been updated successfully`, + (metadata) => { + return { + '17': `Name '${metadata.pool_name}/${metadata.image_name}' is already + in use.` + }; + } + ), + 'rbd/delete': new TaskManagerMessage( + (metadata) => `Delete RBD '${metadata.pool_name}/${metadata.image_name}'`, + (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}' + has been deleted successfully`, + (metadata) => { + return { + '39': `RBD image contains snapshots.` + }; + } + ), + 'rbd/snap/create': new TaskManagerMessage( + (metadata) => `Create snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`, + (metadata) => `Snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` + + `has been created successfully`, + (metadata) => { + return { + '17': `Name '${metadata.snapshot_name}' is already in use.` + }; + } + ), + 'rbd/snap/edit': new TaskManagerMessage( + (metadata) => `Update snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`, + (metadata) => `Snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` + + `has been updated successfully`, + () => { + return { + '16': `Cannot unprotect snapshot because it contains child images.` + }; + } + ), + 'rbd/snap/delete': new TaskManagerMessage( + (metadata) => `Delete snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`, + (metadata) => `Snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` + + `has been deleted successfully`, + () => { + return { + '16': `Snapshot is protected.` + }; + } + ), + 'rbd/snap/rollback': new TaskManagerMessage( + (metadata) => `Rollback snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`, + (metadata) => `Snapshot ` + + `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` + + `has been rolled back successfully`, + () => { + return { + }; + } + ), }; defaultMessage = new TaskManagerMessage( diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts index 1956d75b593f..584ed0968170 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts @@ -7,6 +7,9 @@ import { DataTableModule } from './datatable/datatable.module'; import { DimlessBinaryDirective } from './directives/dimless-binary.directive'; import { PasswordButtonDirective } from './directives/password-button.directive'; import { PipesModule } from './pipes/pipes.module'; +import { AuthGuardService } from './services/auth-guard.service'; +import { AuthStorageService } from './services/auth-storage.service'; +import { FormatterService } from './services/formatter.service'; import { ServicesModule } from './services/services.module'; @NgModule({ @@ -30,6 +33,11 @@ import { ServicesModule } from './services/services.module'; DimlessBinaryDirective, DataTableModule, ApiModule + ], + providers: [ + AuthStorageService, + AuthGuardService, + FormatterService ] }) export class SharedModule {} -- 2.47.3