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 <smueller@suse.com>
--- /dev/null
+# -*- 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,
+ }
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
{ 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'}
];
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';
AppComponent
],
imports: [
+ HttpClientModule,
BrowserModule,
BrowserAnimationsModule,
ToastModule.forRoot(),
CoreModule,
SharedModule,
CephModule,
+ AccordionModule.forRoot(),
+ TabsModule.forRoot(),
HttpClientModule,
BrowserAnimationsModule
],
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({
ClusterModule,
DashboardModule,
RgwModule,
+ PerformanceCounterModule,
BlockModule,
CephfsModule,
SharedModule
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
declarations: [
HostsComponent,
MonitorComponent,
- ConfigurationComponent
+ ConfigurationComponent,
+ OsdListComponent,
+ OsdDetailsComponent,
+ OsdPerformanceHistogramComponent
],
providers: [
- MonitorService
+ MonitorService,
+ OsdService
]
})
export class ClusterModule {}
--- /dev/null
+<accordion>
+ <accordion-group *ngFor="let osd of selected"
+ [heading]="osd.tree.name"
+ [isOpen]="true">
+ <div *ngIf="osd.loaded">
+ <tabset>
+ <tab heading="Attributes (OSD map)">
+ <cd-table-key-value [data]="osd.details.osd_map">
+ </cd-table-key-value>
+ </tab>
+ <tab heading="Metadata">
+ <cd-table-key-value *ngIf="osd.details.osd_metadata"
+ (fetchData)="osd.autoRefresh()"
+ [data]="osd.details.osd_metadata">
+ </cd-table-key-value>
+ </tab>
+ <tab heading="Performance counter">
+ <cd-table-performance-counter serviceType="osd"
+ [serviceId]="osd.id">
+ </cd-table-performance-counter>
+ </tab>
+ <tab heading="Histogram">
+ <h3 *ngIf="osd.histogram_failed">
+ Histogram not available -> <span class="text-warning">{{ osd.histogram_failed }}</span>
+ </h3>
+ <div class="row" *ngIf="osd.details.histogram">
+ <div class="col-md-6">
+ <h4>Writes</h4>
+ <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_w_latency_in_bytes_histogram">
+ </cd-osd-performance-histogram>
+ </div>
+ <div class="col-md-6">
+ <h4>Reads</h4>
+ <cd-osd-performance-histogram [histogram]="osd.details.histogram.osd.op_r_latency_out_bytes_histogram">
+ </cd-osd-performance-histogram>
+ </div>
+ </div>
+ </tab>
+ </tabset>
+ </div>
+ </accordion-group>
+</accordion>
--- /dev/null
+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<OsdDetailsComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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;
+ });
+ }
+
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">Cluster</li>
+ <li class="breadcrumb-item active">OSDs</li>
+ </ol>
+</nav>
+<cd-table [data]="osds"
+ (fetchData)="getOsdList()"
+ [columns]="columns"
+ [detailsComponent]="detailsComponent">
+</cd-table>
+<ng-template #statusColor
+ let-value="value">
+ <span *ngFor="let state of value; last as last">
+ <span [class.text-success]="'up' === state || 'in' === state"
+ [class.text-warning]="'down' === state || 'out' === state"
+ >
+ {{ state }}</span><span *ngIf="!last">, </span>
+ <!-- Has to be on the same line to prevent a space between state and comma. -->
+ </span>
+</ng-template>
--- /dev/null
+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<OsdListComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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<any>;
+ 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')];
+ }
+}
--- /dev/null
+<table>
+ <tr style="height: 10px;"
+ *ngFor="let row of valuesStyle">
+ <td style="width: 10px; height: 10px;"
+ *ngFor="let col of row"
+ [ngStyle]="col">
+ </td>
+ </tr>
+</table>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { OsdPerformanceHistogramComponent } from './osd-performance-histogram.component';
+
+describe('OsdPerformanceHistogramComponent', () => {
+ let component: OsdPerformanceHistogramComponent;
+ let fixture: ComponentFixture<OsdPerformanceHistogramComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ OsdPerformanceHistogramComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OsdPerformanceHistogramComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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();
+ }));
+});
--- /dev/null
+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}`);
+ }
+}
routerLink="/hosts">Hosts
</a>
</li>
-
<li routerLinkActive="active"
class="tc_submenuitem tc_submenuitem_cluster_monitor">
<a i18n
routerLink="/monitor/"> Monitors
</a>
</li>
-
<li routerLinkActive="active"
class="tc_submenuitem tc_submenuitem_configuration">
<a i18n
routerLink="/configuration">Configuration Doc.
</a>
</li>
+ <li routerLinkActive="active"
+ class="tc_submenuitem tc_submenuitem_hosts">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/osd">OSDs
+ </a>
+ </li>
</ul>
</li>
let-value="value">
<a [routerLink]="[row.cdLink]">{{ value }}</a>
</ng-template>
+
+<ng-template #perSecondTpl
+ let-row="row"
+ let-value="value">
+ {{ value }} /s
+</ng-template>
@ViewChild('tableCellBoldTpl') tableCellBoldTpl: TemplateRef<any>;
@ViewChild('sparklineTpl') sparklineTpl: TemplateRef<any>;
@ViewChild('routerLinkTpl') routerLinkTpl: TemplateRef<any>;
+ @ViewChild('perSecondTpl') perSecondTpl: TemplateRef<any>;
// This is the array with the items to be shown.
@Input() data: any[] = [];
this.cellTemplates.bold = this.tableCellBoldTpl;
this.cellTemplates.sparkline = this.sparklineTpl;
this.cellTemplates.routerLink = this.routerLinkTpl;
+ this.cellTemplates.perSecond = this.perSecondTpl;
}
ngOnChanges(changes) {
export enum CellTemplate {
bold = 'bold',
sparkline = 'sparkline',
+ perSecond = 'perSecond',
routerLink = 'routerLink'
}
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 {}
--- /dev/null
+# -*- 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'])