mirror of
https://github.com/ceph/ceph
synced 2024-12-30 07:23:11 +00:00
mgr/dashboard_v2: add monitors page
Signed-off-by: Tiago Melo <tmelo@suse.com>
This commit is contained in:
parent
1a9619d9b3
commit
d9a334d591
41
src/pybind/mgr/dashboard_v2/controllers/monitor.py
Normal file
41
src/pybind/mgr/dashboard_v2/controllers/monitor.py
Normal 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
|
||||
}
|
@ -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 { }
|
||||
|
@ -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 {}
|
||||
|
@ -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();
|
||||
}));
|
||||
});
|
@ -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');
|
||||
}
|
||||
}
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
.name {
|
||||
font-weight: bolder;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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 -->
|
||||
|
18
src/pybind/mgr/dashboard_v2/tests/test_monitor.py
Normal file
18
src/pybind/mgr/dashboard_v2/tests/test_monitor.py
Normal 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'])
|
Loading…
Reference in New Issue
Block a user