--- /dev/null
+# -*- 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
+ }
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
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] },
{
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 { }
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({
],
declarations: [
HostsComponent,
- ServiceListPipe
+ ServiceListPipe,
+ MonitorComponent,
],
providers: [
- ServiceListPipe
+ ServiceListPipe,
+ MonitorService
]
})
-export class ClusterModule { }
+export class ClusterModule {}
--- /dev/null
+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();
+ }));
+});
--- /dev/null
+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');
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+.name {
+ font-weight: bolder;
+}
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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;
+ });
+ }
+}
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 -->
--- /dev/null
+# -*- 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'])