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,
OsdListComponent,
OsdDetailsComponent,
OsdPerformanceHistogramComponent,
- OsdScrubModalComponent
+ OsdScrubModalComponent,
+ OsdFlagsModalComponent
]
})
export class ClusterModule {}
--- /dev/null
+<cd-modal [modalRef]="bsModalRef">
+ <ng-container class="modal-title"
+ i18n>Cluster-wide OSD Flags
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form name="osdFlagsForm"
+ #formDir="ngForm"
+ [formGroup]="osdFlagsForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="checkbox checkbox-primary"
+ *ngFor="let flag of flags; let last = last">
+ <input type="checkbox"
+ [checked]="flag.value"
+ (change)="flag.value = !flag.value"
+ [name]="flag.code"
+ [id]="flag.code"
+ [disabled]="flag.disabled">
+ <label [for]="flag.code"
+ ng-class="['tc_' + key]">
+ <strong>{{ flag.name }}</strong>
+ <br>
+ <span class="text-muted">{{ flag.description }}</span>
+ </label>
+ <hr class="oa-hr-small"
+ *ngIf="!last">
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button (submitAction)="submitAction()"
+ [form]="osdFlagsForm"
+ i18n>
+ Submit
+ </cd-submit-button>
+
+ <button class="btn btn-link btn-sm"
+ (click)="bsModalRef.hide()"
+ i18n>
+ Cancel
+ </button>
+ </div>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+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<OsdFlagsModalComponent>;
+ 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);
+ });
+ });
+});
--- /dev/null
+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();
+ }
+ );
+ }
+}
selectionType="single"
(updateSelection)="updateSelection($event)"
[updateSelectionOnRefresh]="false">
- <div class="table-actions"
+ <div class="table-actions btn-toolbar"
*ngIf="permission.update">
<div class="btn-group"
dropdown>
</li>
</ul>
</div>
+
+ <button class="btn btn-sm btn-default btn-label tc_configureCluster"
+ type="button"
+ (click)="configureClusterAction()">
+ <i class="fa fa-fw fa-cog"
+ aria-hidden="true"></i>
+ <ng-container i18n>Set Cluster-wide OSD Flags</ng-container>
+ </button>
</div>
<cd-osd-details cdTableDetail
import { Permission } from '../../../../shared/models/permissions';
import { DimlessBinaryPipe } from '../../../../shared/pipes/dimless-binary.pipe';
import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
+import { OsdFlagsModalComponent } from '../osd-flags-modal/osd-flags-modal.component';
import { OsdScrubModalComponent } from '../osd-scrub-modal/osd-scrub-modal.component';
@Component({
this.bsModalRef = this.modalService.show(OsdScrubModalComponent, { initialState });
}
+
+ configureClusterAction() {
+ this.bsModalRef = this.modalService.show(OsdFlagsModalComponent, {});
+ }
}
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'] });
+ });
});
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 });
+ }
}
ChartsModule,
ReactiveFormsModule,
PipesModule,
- ModalModule.forRoot(),
+ ModalModule.forRoot()
],
declarations: [
ViewCacheComponent,
UsageBarComponent,
ModalComponent
],
- entryComponents: [
- ModalComponent,
- DeletionModalComponent,
- ConfirmationModalComponent
- ]
+ entryComponents: [ModalComponent, DeletionModalComponent, ConfirmationModalComponent]
})
-export class ComponentsModule { }
+export class ComponentsModule {}