From ed58749063b4468e57f1142bf7e84eb53e251714 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Stephan=20M=C3=BCller?= Date: Fri, 26 Jan 2018 10:18:48 +0100 Subject: [PATCH] mgr/dashboard_v2: OSD pages MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit You can now list all osds and see details for each OSD. The details can be viewed in tabs. You can view attributes, meta data and histogram of the selected OSD. Signed-off-by: Stephan Müller --- .../mgr/dashboard_v2/controllers/osd.py | 88 +++++++++++++++++++ .../frontend/src/app/app-routing.module.ts | 2 + .../frontend/src/app/app.module.ts | 4 + .../frontend/src/app/ceph/ceph.module.ts | 2 + .../src/app/ceph/cluster/cluster.module.ts | 22 ++++- .../osd-details/osd-details.component.html | 42 +++++++++ .../osd-details/osd-details.component.scss | 0 .../osd-details/osd-details.component.spec.ts | 45 ++++++++++ .../osd/osd-details/osd-details.component.ts | 37 ++++++++ .../osd/osd-list/osd-list.component.html | 21 +++++ .../osd/osd-list/osd-list.component.scss | 0 .../osd/osd-list/osd-list.component.spec.ts | 48 ++++++++++ .../osd/osd-list/osd-list.component.ts | 65 ++++++++++++++ .../osd-performance-histogram.component.html | 9 ++ .../osd-performance-histogram.component.scss | 0 ...sd-performance-histogram.component.spec.ts | 25 ++++++ .../osd-performance-histogram.component.ts | 61 +++++++++++++ .../app/ceph/cluster/osd/osd.service.spec.ts | 19 ++++ .../src/app/ceph/cluster/osd/osd.service.ts | 17 ++++ .../navigation/navigation.component.html | 9 +- .../datatable/table/table.component.html | 6 ++ .../shared/datatable/table/table.component.ts | 2 + .../src/app/shared/enum/cell-template.enum.ts | 1 + .../frontend/src/app/shared/shared.module.ts | 14 +-- src/pybind/mgr/dashboard_v2/tests/test_osd.py | 36 ++++++++ 25 files changed, 564 insertions(+), 11 deletions(-) create mode 100644 src/pybind/mgr/dashboard_v2/controllers/osd.py create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts create mode 100644 src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts create mode 100644 src/pybind/mgr/dashboard_v2/tests/test_osd.py diff --git a/src/pybind/mgr/dashboard_v2/controllers/osd.py b/src/pybind/mgr/dashboard_v2/controllers/osd.py new file mode 100644 index 0000000000000..5f89fa032856a --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/controllers/osd.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import json + +from mgr_module import CommandResult +from ..tools import ApiController, AuthRequired, RESTController +from .. import logger + + +@ApiController('osd') +@AuthRequired() +class Osd(RESTController): + def get_counter(self, daemon_name, stat): + return self.mgr.get_counter('osd', daemon_name, stat)[stat] + + def get_rate(self, daemon_name, stat): + data = self.get_counter(daemon_name, stat) + rate = 0 + if data and len(data) > 1: + rate = (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0]) + return rate + + def get_latest(self, daemon_name, stat): + data = self.get_counter(daemon_name, stat) + latest = 0 + if data and data[-1] and len(data[-1]) == 2: + latest = data[-1][1] + return latest + + def list(self): + osds = self.get_osd_map() + # Extending by osd stats information + for s in self.mgr.get('osd_stats')['osd_stats']: + osds[str(s['osd'])].update({'osd_stats': s}) + # Extending by osd node information + nodes = self.mgr.get('osd_map_tree')['nodes'] + osd_tree = [(str(o['id']), o) for o in nodes if o['id'] >= 0] + for o in osd_tree: + osds[o[0]].update({'tree': o[1]}) + # Extending by osd parent node information + hosts = [(h['name'], h) for h in nodes if h['id'] < 0] + for h in hosts: + for o_id in h[1]['children']: + if o_id >= 0: + osds[str(o_id)]['host'] = h[1] + # Extending by osd histogram data + for o_id in osds: + o = osds[o_id] + o['stats'] = {} + o['stats_history'] = {} + osd_spec = str(o['osd']) + for s in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']: + prop = s.split('.')[1] + o['stats'][prop] = self.get_rate(osd_spec, s) + o['stats_history'][prop] = self.get_counter(osd_spec, s) + # Gauge stats + for s in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']: + o['stats'][s.split('.')[1]] = self.get_latest(osd_spec, s) + return osds.values() + + def get_osd_map(self): + osds = {} + for osd in self.mgr.get('osd_map')['osds']: + osd['id'] = osd['osd'] + osds[str(osd['id'])] = osd + return osds + + def get(self, svc_id): + result = CommandResult('') + self.mgr.send_command(result, 'osd', svc_id, + json.dumps({ + 'prefix': 'perf histogram dump', + }), + '') + r, outb, outs = result.wait() + if r != 0: + histogram = None + logger.warning('Failed to load histogram for OSD %s', svc_id) + logger.debug(outs) + histogram = outs + else: + histogram = json.loads(outb) + return { + 'osd_map': self.get_osd_map()[svc_id], + 'osd_metadata': self.mgr.get_metadata('osd', svc_id), + 'histogram': histogram, + } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts index 9e9ae715abc06..7e7e84106cb63 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts @@ -8,6 +8,7 @@ import { ClientsComponent } from './ceph/cephfs/clients/clients.component'; import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component'; import { HostsComponent } from './ceph/cluster/hosts/hosts.component'; import { MonitorComponent } from './ceph/cluster/monitor/monitor.component'; +import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component'; import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; import { PerformanceCounterComponent @@ -40,6 +41,7 @@ const routes: Routes = [ { path: 'cephfs/:id', component: CephfsComponent, canActivate: [AuthGuardService] }, { path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] }, { path: '404', component: NotFoundComponent }, + { path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] }, { path: '**', redirectTo: '/404'} ]; diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts index 3211acdc61841..e1ac1f7198fd8 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts @@ -5,6 +5,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr'; +import { AccordionModule, TabsModule } from 'ngx-bootstrap'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { CephModule } from './ceph/ceph.module'; @@ -24,6 +25,7 @@ export class CustomOption extends ToastOptions { AppComponent ], imports: [ + HttpClientModule, BrowserModule, BrowserAnimationsModule, ToastModule.forRoot(), @@ -32,6 +34,8 @@ export class CustomOption extends ToastOptions { CoreModule, SharedModule, CephModule, + AccordionModule.forRoot(), + TabsModule.forRoot(), HttpClientModule, BrowserAnimationsModule ], diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts index 7509789a3b91c..0f74b8234a6e9 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts @@ -6,6 +6,7 @@ import { BlockModule } from './block/block.module'; import { CephfsModule } from './cephfs/cephfs.module'; import { ClusterModule } from './cluster/cluster.module'; import { DashboardModule } from './dashboard/dashboard.module'; +import { PerformanceCounterModule } from './performance-counter/performance-counter.module'; import { RgwModule } from './rgw/rgw.module'; @NgModule({ @@ -14,6 +15,7 @@ import { RgwModule } from './rgw/rgw.module'; ClusterModule, DashboardModule, RgwModule, + PerformanceCounterModule, BlockModule, CephfsModule, SharedModule diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts index dad1d6c59b655..8f5b7b44ba928 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts @@ -3,17 +3,31 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; +import { AccordionModule, TabsModule } from 'ngx-bootstrap'; import { ComponentsModule } from '../../shared/components/components.module'; import { SharedModule } from '../../shared/shared.module'; +import { PerformanceCounterModule } from '../performance-counter/performance-counter.module'; import { ConfigurationComponent } from './configuration/configuration.component'; import { HostsComponent } from './hosts/hosts.component'; import { MonitorService } from './monitor.service'; import { MonitorComponent } from './monitor/monitor.component'; +import { OsdDetailsComponent } from './osd/osd-details/osd-details.component'; +import { OsdListComponent } from './osd/osd-list/osd-list.component'; +import { + OsdPerformanceHistogramComponent +} from './osd/osd-performance-histogram/osd-performance-histogram.component'; +import { OsdService } from './osd/osd.service'; @NgModule({ + entryComponents: [ + OsdDetailsComponent + ], imports: [ CommonModule, + PerformanceCounterModule, ComponentsModule, + AccordionModule, + TabsModule, SharedModule, RouterModule, FormsModule @@ -21,10 +35,14 @@ import { MonitorComponent } from './monitor/monitor.component'; declarations: [ HostsComponent, MonitorComponent, - ConfigurationComponent + ConfigurationComponent, + OsdListComponent, + OsdDetailsComponent, + OsdPerformanceHistogramComponent ], providers: [ - MonitorService + MonitorService, + OsdService ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html new file mode 100644 index 0000000000000..ac1cde1fa5096 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html @@ -0,0 +1,42 @@ + + +
+ + + + + + + + + + + + + + +

+ Histogram not available -> {{ osd.histogram_failed }} +

+
+
+

Writes

+ + +
+
+

Reads

+ + +
+
+
+
+
+
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts new file mode 100644 index 0000000000000..45b4b15ff8b64 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts @@ -0,0 +1,45 @@ +import { HttpClientModule } from '@angular/common/http'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AccordionConfig, AccordionModule, TabsModule } from 'ngx-bootstrap'; + +import { DataTableModule } from '../../../../shared/datatable/datatable.module'; +import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module'; +import { + OsdPerformanceHistogramComponent +} from '../osd-performance-histogram/osd-performance-histogram.component'; +import { OsdService } from '../osd.service'; +import { OsdDetailsComponent } from './osd-details.component'; + +describe('OsdDetailsComponent', () => { + let component: OsdDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + AccordionModule, + TabsModule, + PerformanceCounterModule, + DataTableModule + ], + declarations: [ + OsdDetailsComponent, + OsdPerformanceHistogramComponent + ], + providers: [OsdService, AccordionConfig] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OsdDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts new file mode 100644 index 0000000000000..28f22f3f67c80 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts @@ -0,0 +1,37 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import * as _ from 'lodash'; + +import { OsdService } from '../osd.service'; + +@Component({ + selector: 'cd-osd-details', + templateUrl: './osd-details.component.html', + styleUrls: ['./osd-details.component.scss'] +}) +export class OsdDetailsComponent implements OnInit { + @Input() selected?: any[]; + + constructor(private osdService: OsdService) {} + + ngOnInit() { + _.each(this.selected, (osd) => { + this.refresh(osd); + osd.autoRefresh = () => { + this.refresh(osd); + }; + }); + } + + refresh(osd: any) { + this.osdService.getDetails(osd.tree.id).subscribe((data: any) => { + osd.details = data; + if (!_.isObject(data.histogram)) { + osd.histogram_failed = data.histogram; + osd.details.histogram = undefined; + } + osd.loaded = true; + }); + } + +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html new file mode 100644 index 0000000000000..e8b9a0ce3304e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html @@ -0,0 +1,21 @@ + + + + + + + {{ state }}, + + + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts new file mode 100644 index 0000000000000..4871fd62c03fa --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts @@ -0,0 +1,48 @@ +import { HttpClientModule } from '@angular/common/http'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AccordionModule, TabsModule } from 'ngx-bootstrap'; + +import { DataTableModule } from '../../../../shared/datatable/datatable.module'; +import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe'; +import { FormatterService } from '../../../../shared/services/formatter.service'; +import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module'; +import { OsdDetailsComponent } from '../osd-details/osd-details.component'; +import { + OsdPerformanceHistogramComponent +} from '../osd-performance-histogram/osd-performance-histogram.component'; +import { OsdService } from '../osd.service'; +import { OsdListComponent } from './osd-list.component'; + +describe('OsdListComponent', () => { + let component: OsdListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + AccordionModule, + PerformanceCounterModule, + TabsModule, + DataTableModule + ], + declarations: [ + OsdListComponent, + OsdDetailsComponent, + OsdPerformanceHistogramComponent + ], + providers: [OsdService, DimlessPipe, FormatterService] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OsdListComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts new file mode 100644 index 0000000000000..a179ba1601ec8 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; + +import { CellTemplate } from '../../../../shared/enum/cell-template.enum'; +import { CdTableColumn } from '../../../../shared/models/cd-table-column'; +import { DimlessPipe } from '../../../../shared/pipes/dimless.pipe'; +import { OsdService } from '../osd.service'; + +@Component({ + selector: 'cd-osd-list', + templateUrl: './osd-list.component.html', + styleUrls: ['./osd-list.component.scss'] +}) + +export class OsdListComponent implements OnInit { + @ViewChild('statusColor') statusColor: TemplateRef; + osds = []; + detailsComponent = 'OsdDetailsComponent'; + columns: CdTableColumn[]; + + constructor( + private osdService: OsdService, + private dimlessPipe: DimlessPipe + ) { } + + ngOnInit() { + this.columns = [ + {prop: 'host.name', name: 'Host'}, + {prop: 'id', name: 'ID', cellTransformation: CellTemplate.bold}, + {prop: 'collectedStates', name: 'Status', cellTemplate: this.statusColor}, + {prop: 'stats.numpg', name: 'PGs'}, + {prop: 'usedPercent', name: 'Usage'}, + { + prop: 'stats_history.out_bytes', + name: 'Read bytes', + cellTransformation: CellTemplate.sparkline + }, + { + prop: 'stats_history.in_bytes', + name: 'Writes bytes', + cellTransformation: CellTemplate.sparkline + }, + {prop: 'stats.op_r', name: 'Read ops', cellTransformation: CellTemplate.perSecond}, + {prop: 'stats.op_w', name: 'Write ops', cellTransformation: CellTemplate.perSecond} + ]; + } + + getOsdList() { + this.osdService.getList().subscribe((data: any[]) => { + this.osds = data; + data.map((osd) => { + osd.collectedStates = this.collectStates(osd); + osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map(i => i[1]); + osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map(i => i[1]); + osd.usedPercent = this.dimlessPipe.transform(osd.stats.stat_bytes_used) + ' / ' + + this.dimlessPipe.transform(osd.stats.stat_bytes); + return osd; + }); + }); + } + + collectStates(osd) { + const select = (onState, offState) => osd[onState] ? onState : offState; + return [select('up', 'down'), select('in', 'out')]; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html new file mode 100644 index 0000000000000..080f121f3bf5a --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html @@ -0,0 +1,9 @@ + + + + +
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts new file mode 100644 index 0000000000000..7ff7d646a2866 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component'; + +describe('OsdPerformanceHistogramComponent', () => { + let component: OsdPerformanceHistogramComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ OsdPerformanceHistogramComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OsdPerformanceHistogramComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts new file mode 100644 index 0000000000000..c3f06450659fb --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts @@ -0,0 +1,61 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +import * as _ from 'lodash'; + +@Component({ + selector: 'cd-osd-performance-histogram', + templateUrl: './osd-performance-histogram.component.html', + styleUrls: ['./osd-performance-histogram.component.scss'] +}) +export class OsdPerformanceHistogramComponent implements OnChanges { + @Input() histogram: any; + valuesStyle: any; + last = {}; + + constructor() { } + + ngOnChanges() { + this.render(); + } + + hexdigits(v): string { + const i = Math.floor(v * 255).toString(16); + return i.length === 1 ? '0' + i : i; + } + + hexcolor(r, g, b) { + return '#' + this.hexdigits(r) + this.hexdigits(g) + this.hexdigits(b); + } + + render() { + if (!this.histogram) { + return; + } + let sum = 0; + let max = 0; + + _.each(this.histogram.values, (row, i) => { + _.each(row, (col, j) => { + let val; + if (this.last && this.last[i] && this.last[i][j]) { + val = col - this.last[i][j]; + } else { + val = col; + } + sum += val; + max = Math.max(max, val); + }); + }); + + this.valuesStyle = this.histogram.values.map((row, i) => { + return row.map((col, j) => { + const val = this.last && this.last[i] && this.last[i][j] ? col - this.last[i][j] : col; + const g = max ? val / max : 0; + const r = 1 - g; + return {backgroundColor: this.hexcolor(r, g, 0)}; + }); + }); + + this.last = this.histogram.values; + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts new file mode 100644 index 0000000000000..115d6a4dbd960 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts @@ -0,0 +1,19 @@ +import { HttpClientModule } from '@angular/common/http'; +import { inject, TestBed } from '@angular/core/testing'; + +import { OsdService } from './osd.service'; + +describe('OsdService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [OsdService], + imports: [ + HttpClientModule, + ], + }); + }); + + it('should be created', inject([OsdService], (service: OsdService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts new file mode 100644 index 0000000000000..cf9adf1b5fd75 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts @@ -0,0 +1,17 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class OsdService { + private path = 'api/osd'; + + constructor (private http: HttpClient) {} + + getList () { + return this.http.get(`${this.path}`); + } + + getDetails(id: number) { + return this.http.get(`${this.path}/${id}`); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html index 1b7d6dfb2f166..378e70a95714f 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -56,7 +56,6 @@ routerLink="/hosts">Hosts -
  • Monitors
  • -
  • Configuration Doc.
  • +
  • + OSDs + +
  • diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html index 9445a5f26c121..a8fda111faf86 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html @@ -86,3 +86,9 @@ let-value="value"> {{ value }} + + + {{ value }} /s + diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts index 5ecba71dfcc26..c2e9684aafc74 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts @@ -29,6 +29,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges { @ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef; @ViewChild('sparklineTpl') sparklineTpl: TemplateRef; @ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef; + @ViewChild('perSecondTpl') perSecondTpl: TemplateRef; // This is the array with the items to be shown. @Input() data: any[] = []; @@ -115,6 +116,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges { this.cellTemplates.bold = this.tableCellBoldTpl; this.cellTemplates.sparkline = this.sparklineTpl; this.cellTemplates.routerLink = this.routerLinkTpl; + this.cellTemplates.perSecond = this.perSecondTpl; } ngOnChanges(changes) { diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts index 7c85bbe08c7e4..7c1c2162f58d1 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts @@ -1,5 +1,6 @@ export enum CellTemplate { bold = 'bold', sparkline = 'sparkline', + perSecond = 'perSecond', routerLink = 'routerLink' } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts index 4e453253427fd..7651338d9c0bc 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts @@ -21,23 +21,23 @@ import { ServicesModule } from './services/services.module'; ServicesModule, DataTableModule ], + declarations: [ + PasswordButtonDirective + ], exports: [ - PipesModule, ComponentsModule, + PipesModule, ServicesModule, PasswordButtonDirective, DataTableModule ], - declarations: [ - PasswordButtonDirective - ], providers: [ AuthService, AuthStorageService, AuthGuardService, - HostService, PoolService, - FormatterService - ] + FormatterService, + HostService + ], }) export class SharedModule {} diff --git a/src/pybind/mgr/dashboard_v2/tests/test_osd.py b/src/pybind/mgr/dashboard_v2/tests/test_osd.py new file mode 100644 index 0000000000000..b4da5b564c3f5 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/tests/test_osd.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .helper import ControllerTestCase, authenticate + + +class OsdTest(ControllerTestCase): + + def assert_in_and_not_none(self, data, properties): + for prop in properties: + self.assertIn(prop, data) + self.assertIsNotNone(data[prop]) + + @authenticate + def test_list(self): + data = self._get('/api/osd') + self.assertStatus(200) + + self.assertGreaterEqual(len(data), 1) + data = data[0] + self.assert_in_and_not_none(data, ['host', 'tree', 'state', 'stats', 'stats_history']) + self.assert_in_and_not_none(data['host'], ['name']) + self.assert_in_and_not_none(data['tree'], ['id']) + self.assert_in_and_not_none(data['stats'], ['numpg', 'stat_bytes_used', 'stat_bytes', + 'op_r', 'op_w']) + self.assert_in_and_not_none(data['stats_history'], ['op_out_bytes', 'op_in_bytes']) + + @authenticate + def test_details(self): + data = self._get('/api/osd/0') + self.assertStatus(200) + self.assert_in_and_not_none(data, ['osd_metadata', 'histogram']) + self.assert_in_and_not_none(data['histogram'], ['osd']) + self.assert_in_and_not_none(data['histogram']['osd'], ['op_w_latency_in_bytes_histogram', + 'op_r_latency_out_bytes_histogram']) -- 2.39.5