]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard_v2: OSD pages
authorStephan Müller <smueller@suse.com>
Fri, 26 Jan 2018 09:18:48 +0000 (10:18 +0100)
committerRicardo Dias <rdias@suse.com>
Mon, 5 Mar 2018 13:07:15 +0000 (13:07 +0000)
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>
25 files changed:
src/pybind/mgr/dashboard_v2/controllers/osd.py [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/ceph.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd-performance-histogram/osd-performance-histogram.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/osd/osd.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/enum/cell-template.enum.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts
src/pybind/mgr/dashboard_v2/tests/test_osd.py [new file with mode: 0644]

diff --git a/src/pybind/mgr/dashboard_v2/controllers/osd.py b/src/pybind/mgr/dashboard_v2/controllers/osd.py
new file mode 100644 (file)
index 0000000..5f89fa0
--- /dev/null
@@ -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,
+        }
index 9e9ae715abc0681c6fbb7d4ff27377f9985a0baf..7e7e84106cb63ab586e0af128f5a4aef0bdafe51 100644 (file)
@@ -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'}
 ];
 
index 3211acdc61841eebf6a0a7c491961da8a4f94d67..e1ac1f7198fd80624d352912f394911f6e54cfea 100644 (file)
@@ -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
   ],
index 7509789a3b91c31294e68ed78200bf28b0ee5589..0f74b8234a6e9937e343362ab8b7a71f67cd32a4 100644 (file)
@@ -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
index dad1d6c59b655ad944c6633db115e142d419ea74..8f5b7b44ba928844822fb9f50ed7c1f03ceb26d1 100644 (file)
@@ -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 (file)
index 0000000..ac1cde1
--- /dev/null
@@ -0,0 +1,42 @@
+<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>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..45b4b15
--- /dev/null
@@ -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<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();
+  });
+});
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 (file)
index 0000000..28f22f3
--- /dev/null
@@ -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 (file)
index 0000000..e8b9a0c
--- /dev/null
@@ -0,0 +1,21 @@
+<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>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..4871fd6
--- /dev/null
@@ -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<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();
+  });
+});
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 (file)
index 0000000..a179ba1
--- /dev/null
@@ -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<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')];
+  }
+}
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 (file)
index 0000000..080f121
--- /dev/null
@@ -0,0 +1,9 @@
+<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>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..7ff7d64
--- /dev/null
@@ -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<OsdPerformanceHistogramComponent>;
+
+  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 (file)
index 0000000..c3f0645
--- /dev/null
@@ -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 (file)
index 0000000..115d6a4
--- /dev/null
@@ -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 (file)
index 0000000..cf9adf1
--- /dev/null
@@ -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}`);
+  }
+}
index 1b7d6dfb2f166fdc8fa316737c4cf5ce44d382f3..378e70a95714f3d5898c7aab4c238cdb60336f85 100644 (file)
@@ -56,7 +56,6 @@
                routerLink="/hosts">Hosts
             </a>
           </li>
-
           <li routerLinkActive="active"
               class="tc_submenuitem tc_submenuitem_cluster_monitor">
             <a i18n
@@ -64,7 +63,6 @@
                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>
 
index 9445a5f26c121ddf33a1f77e48c35c4918066067..a8fda111faf8686468eabf4c7a7f8a889a7cded9 100644 (file)
@@ -86,3 +86,9 @@
              let-value="value">
   <a [routerLink]="[row.cdLink]">{{ value }}</a>
 </ng-template>
+
+<ng-template #perSecondTpl
+             let-row="row"
+             let-value="value">
+  {{ value }} /s
+</ng-template>
index 5ecba71dfcc26ca85feeba2296498494f3336f0f..c2e9684aafc74b16864d58fc04b50f026f3cc3fa 100644 (file)
@@ -29,6 +29,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges {
   @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[] = [];
@@ -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) {
index 7c85bbe08c7e4b1e7c4945d23d7f7d812787db54..7c1c2162f58d114fa2b90a0dab3213ce50d17c0e 100644 (file)
@@ -1,5 +1,6 @@
 export enum CellTemplate {
   bold = 'bold',
   sparkline = 'sparkline',
+  perSecond = 'perSecond',
   routerLink = 'routerLink'
 }
index 4e453253427fdee397226e641d551c79e2f43d0b..7651338d9c0bc7eea46d8945f92730ca1ad10f99 100644 (file)
@@ -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 (file)
index 0000000..b4da5b5
--- /dev/null
@@ -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'])