]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add UI for Cluster-wide OSD Flags configuration 22461/head
authorTiago Melo <tmelo@suse.com>
Fri, 11 May 2018 11:26:01 +0000 (12:26 +0100)
committerTiago Melo <tmelo@suse.com>
Mon, 2 Jul 2018 10:14:33 +0000 (11:14 +0100)
Signed-off-by: Tiago Melo <tmelo@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts

index 9d0460e2b3e3e1f0da78576d1300e3cbbe5acb5e..3893de00e67997f4dab63693d2079b5b0009b8b2 100644 (file)
@@ -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 (file)
index 0000000..bc1009b
--- /dev/null
@@ -0,0 +1,48 @@
+<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>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..2d1d7da
--- /dev/null
@@ -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<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);
+    });
+  });
+});
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 (file)
index 0000000..de1e046
--- /dev/null
@@ -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();
+      }
+    );
+  }
+}
index 426ec736978b7dbe6f9a740a259257f8c8e9ef53..733bcf26fb251f299dc8999ee7b8b3722bf106aa 100644 (file)
@@ -10,7 +10,7 @@
           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
index 80e3db42c77820e3a6d2917cf7a5e54140d158e6..899f935c5b851847e7b005c26149b1f53444d643 100644 (file)
@@ -10,6 +10,7 @@ import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 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({
@@ -97,4 +98,8 @@ export class OsdListComponent implements OnInit {
 
     this.bsModalRef = this.modalService.show(OsdScrubModalComponent, { initialState });
   }
+
+  configureClusterAction() {
+    this.bsModalRef = this.modalService.show(OsdFlagsModalComponent, {});
+  }
 }
index d8237e714678ba8ff131f150c85a7d02461fcfe4..ad004c2e8bcfffd009f46f9a4f2bc86d54163004 100644 (file)
@@ -49,4 +49,17 @@ describe('OsdService', () => {
     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'] });
+  });
 });
index 29ffa721cb72a473bb45cbaae4687102c58f11cb..4035de5ad702045e5203fd9e250062985bc0fa8e 100644 (file)
@@ -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 });
+  }
 }
index 8c6e87be16a6399ba65fa3f86d33a8eecc0be918..62d4876a5a141d0439f0d8348ce069567308c8a7 100644 (file)
@@ -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 {}