From 4d57690bf55d5b5fbb915d28d4e7b7735556d243 Mon Sep 17 00:00:00 2001 From: Tiago Melo Date: Fri, 11 May 2018 12:26:01 +0100 Subject: [PATCH] mgr/dashboard: Add UI for Cluster-wide OSD Flags configuration Signed-off-by: Tiago Melo --- .../src/app/ceph/cluster/cluster.module.ts | 6 +- .../osd-flags-modal.component.html | 48 ++++++ .../osd-flags-modal.component.scss | 0 .../osd-flags-modal.component.spec.ts | 98 ++++++++++++ .../osd-flags-modal.component.ts | 139 ++++++++++++++++++ .../osd/osd-list/osd-list.component.html | 10 +- .../osd/osd-list/osd-list.component.ts | 5 + .../src/app/shared/api/osd.service.spec.ts | 13 ++ .../src/app/shared/api/osd.service.ts | 8 + .../shared/components/components.module.ts | 10 +- 10 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 9d0460e2b3e3e..3893de00e6799 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -13,12 +13,13 @@ import { ConfigurationComponent } from './configuration/configuration.component' import { HostsComponent } from './hosts/hosts.component'; import { MonitorComponent } from './monitor/monitor.component'; import { OsdDetailsComponent } from './osd/osd-details/osd-details.component'; +import { OsdFlagsModalComponent } from './osd/osd-flags-modal/osd-flags-modal.component'; import { OsdListComponent } from './osd/osd-list/osd-list.component'; import { OsdPerformanceHistogramComponent } from './osd/osd-performance-histogram/osd-performance-histogram.component'; import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.component'; @NgModule({ - entryComponents: [OsdDetailsComponent, OsdScrubModalComponent], + entryComponents: [OsdDetailsComponent, OsdScrubModalComponent, OsdFlagsModalComponent], imports: [ CommonModule, PerformanceCounterModule, @@ -37,7 +38,8 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co OsdListComponent, OsdDetailsComponent, OsdPerformanceHistogramComponent, - OsdScrubModalComponent + OsdScrubModalComponent, + OsdFlagsModalComponent ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html new file mode 100644 index 0000000000000..bc1009b3afe62 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html @@ -0,0 +1,48 @@ + + Cluster-wide OSD Flags + + + +
+ + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts new file mode 100644 index 0000000000000..2d1d7daa73af1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts @@ -0,0 +1,98 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; + +import * as _ from 'lodash'; +import { ToastModule } from 'ng2-toastr'; +import { BsModalRef, ModalModule } from 'ngx-bootstrap'; + +import { NotificationType } from '../../../../shared/enum/notification-type.enum'; +import { NotificationService } from '../../../../shared/services/notification.service'; +import { SharedModule } from '../../../../shared/shared.module'; +import { configureTestBed } from '../../../../shared/unit-test-helper'; +import { OsdFlagsModalComponent } from './osd-flags-modal.component'; + +function getFlagsArray(component: OsdFlagsModalComponent) { + const allFlags = _.cloneDeep(component.allFlags); + allFlags['purged_snapdirs'].value = true; + allFlags['pause'].value = true; + return _.toArray(allFlags); +} + +describe('OsdFlagsModalComponent', () => { + let component: OsdFlagsModalComponent; + let fixture: ComponentFixture; + let httpTesting: HttpTestingController; + + configureTestBed({ + imports: [ + ReactiveFormsModule, + ModalModule.forRoot(), + SharedModule, + HttpClientTestingModule, + ToastModule.forRoot() + ], + declarations: [OsdFlagsModalComponent], + providers: [BsModalRef] + }); + + beforeEach(() => { + httpTesting = TestBed.get(HttpTestingController); + fixture = TestBed.createComponent(OsdFlagsModalComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should finish running ngOnInit', () => { + fixture.detectChanges(); + + const flags = getFlagsArray(component); + + const req = httpTesting.expectOne('api/osd/flags'); + req.flush(['purged_snapdirs', 'pause', 'foo']); + + expect(component.flags).toEqual(flags); + expect(component.unknownFlags).toEqual(['foo']); + }); + + describe('test submitAction', function() { + let notificationType: NotificationType; + let notificationService: NotificationService; + let bsModalRef: BsModalRef; + + beforeEach(() => { + notificationService = TestBed.get(NotificationService); + spyOn(notificationService, 'show').and.callFake((type) => { + notificationType = type; + }); + + bsModalRef = TestBed.get(BsModalRef); + spyOn(bsModalRef, 'hide').and.callThrough(); + component.unknownFlags = ['foo']; + }); + + it('should run submitAction', () => { + component.flags = getFlagsArray(component); + component.submitAction(); + const req = httpTesting.expectOne('api/osd/flags'); + req.flush(['purged_snapdirs', 'pause', 'foo']); + expect(req.request.body).toEqual({ flags: ['pause', 'purged_snapdirs', 'foo'] }); + + expect(notificationType).toBe(NotificationType.success); + expect(component.bsModalRef.hide).toHaveBeenCalledTimes(1); + }); + + it('should hide modal if request fails', () => { + component.flags = []; + component.submitAction(); + const req = httpTesting.expectOne('api/osd/flags'); + req.flush([], { status: 500, statusText: 'failure' }); + + expect(notificationService.show).toHaveBeenCalledTimes(0); + expect(component.bsModalRef.hide).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts new file mode 100644 index 0000000000000..de1e0467d5420 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts @@ -0,0 +1,139 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import * as _ from 'lodash'; +import { BsModalRef } from 'ngx-bootstrap'; + +import { OsdService } from '../../../../shared/api/osd.service'; +import { NotificationType } from '../../../../shared/enum/notification-type.enum'; +import { NotificationService } from '../../../../shared/services/notification.service'; + +@Component({ + selector: 'cd-osd-flags-modal', + templateUrl: './osd-flags-modal.component.html', + styleUrls: ['./osd-flags-modal.component.scss'] +}) +export class OsdFlagsModalComponent implements OnInit { + osdFlagsForm = new FormGroup({}); + + allFlags = { + noin: { + code: 'noin', + name: 'No In', + value: false, + description: 'OSDs that were previously marked out will not be marked back in when they start' + }, + noout: { + code: 'noout', + name: 'No Out', + value: false, + description: 'OSDs will not automatically be marked out after the configured interval' + }, + noup: { + code: 'noup', + name: 'No Up', + value: false, + description: 'OSDs are not allowed to start' + }, + nodown: { + code: 'nodown', + name: 'No Down', + value: false, + description: + 'OSD failure reports are being ignored, such that the monitors will not mark OSDs down' + }, + pause: { + code: 'pause', + name: 'Pause', + value: false, + description: 'Pauses reads and writes' + }, + noscrub: { + code: 'noscrub', + name: 'No Scrub', + value: false, + description: 'Scrubbing is disabled' + }, + 'nodeep-scrub': { + code: 'nodeep-scrub', + name: 'No Deep Scrub', + value: false, + description: 'Deep Scrubbing is disabled' + }, + nobackfill: { + code: 'nobackfill', + name: 'No Backfill', + value: false, + description: 'Backfilling of PGs is suspended' + }, + norecover: { + code: 'norecover', + name: 'No Recover', + value: false, + description: 'Recovery of PGs is suspended' + }, + sortbitwise: { + code: 'sortbitwise', + name: 'Bitwise Sort', + value: false, + description: 'Use bitwise sort', + disabled: true + }, + purged_snapdirs: { + code: 'purged_snapdirs', + name: 'Purged Snapdirs', + value: false, + description: 'OSDs have converted snapsets', + disabled: true + }, + recovery_deletes: { + code: 'recovery_deletes', + name: 'Recovery Deletes', + value: false, + description: 'Deletes performed during recovery instead of peering', + disabled: true + } + }; + flags: any[]; + unknownFlags: string[] = []; + + constructor( + public bsModalRef: BsModalRef, + private osdService: OsdService, + private notificationService: NotificationService + ) {} + + ngOnInit() { + this.osdService.getFlags().subscribe((res: string[]) => { + res.forEach((value) => { + if (this.allFlags[value]) { + this.allFlags[value].value = true; + } else { + this.unknownFlags.push(value); + } + }); + this.flags = _.toArray(this.allFlags); + }); + } + + submitAction() { + const newFlags = this.flags + .filter((flag) => flag.value) + .map((flag) => flag.code) + .concat(this.unknownFlags); + + this.osdService.updateFlags(newFlags).subscribe( + () => { + this.notificationService.show( + NotificationType.success, + 'OSD Flags were updated successfully.', + 'OSD Flags' + ); + this.bsModalRef.hide(); + }, + () => { + this.bsModalRef.hide(); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html index 426ec736978b7..733bcf26fb251 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html @@ -10,7 +10,7 @@ selectionType="single" (updateSelection)="updateSelection($event)" [updateSelectionOnRefresh]="false"> -
@@ -40,6 +40,14 @@
+ +
{ const req = httpTesting.expectOne('api/osd/foo/scrub?deep=false'); expect(req.request.method).toBe('POST'); }); + + it('should call getFlags', () => { + service.getFlags().subscribe(); + const req = httpTesting.expectOne('api/osd/flags'); + expect(req.request.method).toBe('GET'); + }); + + it('should call updateFlags', () => { + service.updateFlags(['foo']).subscribe(); + const req = httpTesting.expectOne('api/osd/flags'); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual({ flags: ['foo'] }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts index 29ffa721cb72a..4035de5ad7020 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts @@ -22,4 +22,12 @@ export class OsdService { scrub(id, deep) { return this.http.post(`${this.path}/${id}/scrub?deep=${deep}`, null); } + + getFlags() { + return this.http.get(`${this.path}/flags`); + } + + updateFlags(flags: string[]) { + return this.http.put(`${this.path}/flags`, { flags: flags }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index 8c6e87be16a63..62d4876a5a141 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -29,7 +29,7 @@ import { ViewCacheComponent } from './view-cache/view-cache.component'; ChartsModule, ReactiveFormsModule, PipesModule, - ModalModule.forRoot(), + ModalModule.forRoot() ], declarations: [ ViewCacheComponent, @@ -56,10 +56,6 @@ import { ViewCacheComponent } from './view-cache/view-cache.component'; UsageBarComponent, ModalComponent ], - entryComponents: [ - ModalComponent, - DeletionModalComponent, - ConfirmationModalComponent - ] + entryComponents: [ModalComponent, DeletionModalComponent, ConfirmationModalComponent] }) -export class ComponentsModule { } +export class ComponentsModule {} -- 2.39.5