mgr/dashboard_v2: add monitors page

Signed-off-by: Tiago Melo <tmelo@suse.com>
This commit is contained in:
Tiago Melo 2018-02-14 16:05:56 +00:00 committed by Ricardo Dias
parent 1a9619d9b3
commit d9a334d591
No known key found for this signature in database
GPG Key ID: 74390C579BD37B68
11 changed files with 298 additions and 10 deletions

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import json
import cherrypy
from ..tools import ApiController, AuthRequired, BaseController
@ApiController('monitor')
@AuthRequired()
class Monitor(BaseController):
@cherrypy.expose
@cherrypy.tools.json_out()
def default(self):
in_quorum, out_quorum = [], []
counters = ['mon.num_sessions']
mon_status_json = self.mgr.get("mon_status")
mon_status = json.loads(mon_status_json['json'])
for mon in mon_status["monmap"]["mons"]:
mon["stats"] = {}
for counter in counters:
data = self.mgr.get_counter("mon", mon["name"], counter)
if data is not None:
mon["stats"][counter.split(".")[1]] = data[counter]
else:
mon["stats"][counter.split(".")[1]] = []
if mon["rank"] in mon_status["quorum"]:
in_quorum.append(mon)
else:
out_quorum.append(mon)
return {
'mon_status': mon_status,
'in_quorum': in_quorum,
'out_quorum': out_quorum
}

View File

@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component';
import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
import {
PerformanceCounterComponent
@ -13,11 +14,8 @@ import { AuthGuardService } from './shared/services/auth-guard.service';
const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuardService]
},
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
{ path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
{ path: 'login', component: LoginComponent },
{ path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
{
@ -30,11 +28,12 @@ const routes: Routes = [
path: 'perf_counters/:type/:id',
component: PerformanceCounterComponent,
canActivate: [AuthGuardService]
}
},
{ path: 'monitor', component: MonitorComponent, canActivate: [AuthGuardService] }
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule {}
export class AppRoutingModule { }

View File

@ -4,6 +4,8 @@ import { NgModule } from '@angular/core';
import { ComponentsModule } from '../../shared/components/components.module';
import { SharedModule } from '../../shared/shared.module';
import { HostsComponent } from './hosts/hosts.component';
import { MonitorService } from './monitor.service';
import { MonitorComponent } from './monitor/monitor.component';
import { ServiceListPipe } from './service-list.pipe';
@NgModule({
@ -14,10 +16,12 @@ import { ServiceListPipe } from './service-list.pipe';
],
declarations: [
HostsComponent,
ServiceListPipe
ServiceListPipe,
MonitorComponent,
],
providers: [
ServiceListPipe
ServiceListPipe,
MonitorService
]
})
export class ClusterModule { }
export class ClusterModule {}

View File

@ -0,0 +1,21 @@
import { HttpClientModule } from '@angular/common/http';
import {
HttpClientTestingModule,
HttpTestingController
} from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { MonitorService } from './monitor.service';
describe('MonitorService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MonitorService],
imports: [HttpClientTestingModule, HttpClientModule]
});
});
it('should be created', inject([MonitorService], (service: MonitorService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,11 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable()
export class MonitorService {
constructor(private http: HttpClient) {}
getMonitor() {
return this.http.get('/api/monitor');
}
}

View File

@ -0,0 +1,80 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">Cluster</li>
<li class="breadcrumb-item active">Monitors</li>
</ol>
</nav>
<div class="row">
<div class="col-md-4">
<fieldset>
<legend>Status</legend>
<table class="table table-striped"
*ngIf="mon_status">
<tr>
<td>
<span class="name">Cluster ID: </span>
</td>
<td>{{ mon_status.monmap.fsid }}
</td>
</tr>
<tr>
<td>
<span class="name">monmap modified: </span>
</td>
<td> {{ mon_status.monmap.modified }}
</td>
</tr>
<tr>
<td>
<span class="name">monmap epoch: </span>
</td>
<td> {{ mon_status.monmap.epoch }}
</td>
</tr>
<tr>
<td>
<span class="name">quorum con: </span>
</td>
<td> {{ mon_status.features.quorum_con }}
</td>
</tr>
<tr>
<td>
<span class="name">quorum mon: </span>
</td>
<td> {{ mon_status.features.quorum_mon }}
</td>
</tr>
<tr>
<td>
<span class="name">required con: </span>
</td>
<td> {{ mon_status.features.required_con }}
</td>
</tr>
<tr>
<td>
<span class="name">required mon: </span>
</td>
<td> {{ mon_status.features.required_mon }}
</td>
</tr>
</table>
</fieldset>
</div>
<div class="col-md-8">
<fieldset>
<legend class="in-quorum">In Quorum</legend>
<cd-table [data]="inQuorum.data"
[columns]="inQuorum.columns">
</cd-table>
<legend class="in-quorum">Not In Quorum</legend>
<cd-table [data]="notInQuorum.data"
[columns]="notInQuorum.columns">
</cd-table>
</fieldset>
</div>
</div>

View File

@ -0,0 +1,3 @@
.name {
font-weight: bolder;
}

View File

@ -0,0 +1,23 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppModule } from '../../../app.module';
import { MonitorComponent } from './monitor.component';
describe('MonitorComponent', () => {
let component: MonitorComponent;
let fixture: ComponentFixture<MonitorComponent>;
beforeEach(
async(() => {
TestBed.configureTestingModule({
imports: [AppModule]
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(MonitorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
});

View File

@ -0,0 +1,80 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import * as _ from 'lodash';
import { CellTemplate } from '../../../shared/enum/cell-template.enum';
import { MonitorService } from '../monitor.service';
@Component({
selector: 'cd-monitor',
templateUrl: './monitor.component.html',
styleUrls: ['./monitor.component.scss']
})
export class MonitorComponent implements OnInit, OnDestroy {
mon_status: any;
inQuorum: any;
notInQuorum: any;
interval: any;
sparklineStyle = {
height: '30px',
width: '50%'
};
constructor(private monitorService: MonitorService) {}
ngOnInit() {
this.inQuorum = {
columns: [
{ prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
{ prop: 'rank', name: 'Rank' },
{ prop: 'public_addr', name: 'Public Address' },
{
prop: 'cdOpenSessions',
name: 'Open Sessions',
cellTransformation: CellTemplate.sparkline
}
],
data: []
};
this.notInQuorum = {
columns: [
{ prop: 'name', name: 'Name', cellTransformation: CellTemplate.routerLink },
{ prop: 'rank', name: 'Rank' },
{ prop: 'public_addr', name: 'Public Address' }
],
data: []
};
this.refresh();
this.interval = setInterval(() => {
this.refresh();
}, 5000);
}
ngOnDestroy() {
clearInterval(this.interval);
}
refresh() {
this.monitorService.getMonitor().subscribe((data: any) => {
data.in_quorum.map((row) => {
row.cdOpenSessions = row.stats.num_sessions.map(i => i[1]);
row.cdLink = '/perf_counters/mon/' + row.name;
return row;
});
data.out_quorum.map((row) => {
row.cdLink = '/perf_counters/mon/' + row.name;
return row;
});
this.inQuorum.data = [...data.in_quorum];
this.notInQuorum.data = [...data.out_quorum];
this.mon_status = data.mon_status;
});
}
}

View File

@ -66,6 +66,14 @@
routerLink="/hosts">Hosts
</a>
</li>
<li routerLinkActive="active"
class="tc_submenuitem tc_submenuitem_cluster_monitor">
<a i18n
class="dropdown-item"
routerLink="/monitor/"> Monitors
</a>
</li>
</ul>
</li>
<!-- Block -->

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from .helper import ControllerTestCase, authenticate
class MonitorTest(ControllerTestCase):
@authenticate
def test_monitor_default(self):
data = self._get("/api/monitor")
self.assertStatus(200)
self.assertIn('mon_status', data)
self.assertIn('in_quorum', data)
self.assertIn('out_quorum', data)
self.assertIsNotNone(data['mon_status'])
self.assertIsNotNone(data['in_quorum'])
self.assertIsNotNone(data['out_quorum'])