diff --git a/src/pybind/mgr/dashboard_v2/controllers/monitor.py b/src/pybind/mgr/dashboard_v2/controllers/monitor.py new file mode 100644 index 00000000000..ef245d54580 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/controllers/monitor.py @@ -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 + } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts index 96afda40669..b5c2dc06bbb 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts @@ -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 { } diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts index c05675e74d2..463f0106b8c 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/cluster.module.ts @@ -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 {} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts new file mode 100644 index 00000000000..1d5f7de97b7 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts @@ -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(); + })); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts new file mode 100644 index 00000000000..5a61870660c --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts @@ -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'); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html new file mode 100644 index 00000000000..5c8f0fcc399 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html @@ -0,0 +1,80 @@ + + +
+
+
+ Status + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Cluster ID: + {{ mon_status.monmap.fsid }} +
+ monmap modified: + {{ mon_status.monmap.modified }} +
+ monmap epoch: + {{ mon_status.monmap.epoch }} +
+ quorum con: + {{ mon_status.features.quorum_con }} +
+ quorum mon: + {{ mon_status.features.quorum_mon }} +
+ required con: + {{ mon_status.features.required_con }} +
+ required mon: + {{ mon_status.features.required_mon }} +
+
+
+ +
+
+ In Quorum + + + + Not In Quorum + + +
+
+
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss new file mode 100644 index 00000000000..69d5ebbc888 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss @@ -0,0 +1,3 @@ +.name { + font-weight: bolder; +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts new file mode 100644 index 00000000000..906581e76b9 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts @@ -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; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + imports: [AppModule] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); +}); diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts new file mode 100644 index 00000000000..fd2e23edc5e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.ts @@ -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; + }); + } +} diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html index 98489f2d250..f3d14445cff 100644 --- a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -66,6 +66,14 @@ routerLink="/hosts">Hosts + +
  • + Monitors + +
  • diff --git a/src/pybind/mgr/dashboard_v2/tests/test_monitor.py b/src/pybind/mgr/dashboard_v2/tests/test_monitor.py new file mode 100644 index 00000000000..3dc0fe4b693 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/tests/test_monitor.py @@ -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'])