From: Stephan Müller Date: Fri, 26 Jan 2018 09:18:48 +0000 (+0100) Subject: mgr/dashboard_v2: OSD pages X-Git-Tag: v13.0.2~84^2~22 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=ed58749063b4468e57f1142bf7e84eb53e251714;p=ceph.git mgr/dashboard_v2: OSD pages 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 --- 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 00000000000..5f89fa03285 --- /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 9e9ae715abc..7e7e84106cb 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 3211acdc618..e1ac1f7198f 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 7509789a3b9..0f74b8234a6 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 dad1d6c59b6..8f5b7b44ba9 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 00000000000..ac1cde1fa50 --- /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 00000000000..e69de29bb2d 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 00000000000..45b4b15ff8b --- /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 00000000000..28f22f3f67c --- /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 00000000000..e8b9a0ce330 --- /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 00000000000..e69de29bb2d 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 00000000000..4871fd62c03 --- /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 00000000000..a179ba1601e --- /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 00000000000..080f121f3bf --- /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 00000000000..e69de29bb2d 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 00000000000..7ff7d646a28 --- /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 00000000000..c3f06450659 --- /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 00000000000..115d6a4dbd9 --- /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 00000000000..cf9adf1b5fd --- /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 1b7d6dfb2f1..378e70a9571 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 9445a5f26c1..a8fda111faf 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 5ecba71dfcc..c2e9684aafc 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 7c85bbe08c7..7c1c2162f58 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 4e453253427..7651338d9c0 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 00000000000..b4da5b564c3 --- /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'])