]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard_v2: add monitors page
authorTiago Melo <tmelo@suse.com>
Wed, 14 Feb 2018 16:05:56 +0000 (16:05 +0000)
committerRicardo Dias <rdias@suse.com>
Mon, 5 Mar 2018 13:07:10 +0000 (13:07 +0000)
Signed-off-by: Tiago Melo <tmelo@suse.com>
src/pybind/mgr/dashboard_v2/controllers/monitor.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/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/ceph/cluster/monitor/monitor.component.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/tests/test_monitor.py [new file with mode: 0644]

diff --git a/src/pybind/mgr/dashboard_v2/controllers/monitor.py b/src/pybind/mgr/dashboard_v2/controllers/monitor.py
new file mode 100644 (file)
index 0000000..ef245d5
--- /dev/null
@@ -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
+        }
index 96afda406690c4994f3e06d0ddb0816cb120507c..b5c2dc06bbb261c21cdef721e971c7cd15b022c1 100644 (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 { }
index c05675e74d291dd7dec748e82fc3111c0e4a3f97..463f0106b8c3eaf222fce8ef85f2e790eeb47b83 100644 (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 {}
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 (file)
index 0000000..1d5f7de
--- /dev/null
@@ -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 (file)
index 0000000..5a61870
--- /dev/null
@@ -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 (file)
index 0000000..5c8f0fc
--- /dev/null
@@ -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>
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 (file)
index 0000000..69d5ebb
--- /dev/null
@@ -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 (file)
index 0000000..906581e
--- /dev/null
@@ -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();
+  });
+});
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 (file)
index 0000000..fd2e23e
--- /dev/null
@@ -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;
+    });
+  }
+}
index 98489f2d25068a516eabccc70e75a7bbfeec9dbf..f3d14445cffa4ba508c553a260db278bdfa4d339 100644 (file)
                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 -->
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 (file)
index 0000000..3dc0fe4
--- /dev/null
@@ -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'])