From: guodan1 Date: Mon, 29 Oct 2018 03:04:28 +0000 (+0800) Subject: mgr/dashboard: Add CRUSH map viewer X-Git-Tag: v14.1.0~1001^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F24766%2Fhead;p=ceph.git mgr/dashboard: Add CRUSH map viewer Fixes: http://tracker.ceph.com/issues/35684 Signed-off-by: familyuu --- diff --git a/src/pybind/mgr/dashboard/frontend/angular.json b/src/pybind/mgr/dashboard/frontend/angular.json index 3b30176bfec9..3b5cc73ca70d 100644 --- a/src/pybind/mgr/dashboard/frontend/angular.json +++ b/src/pybind/mgr/dashboard/frontend/angular.json @@ -26,7 +26,8 @@ "node_modules/fork-awesome/css/fork-awesome.css", "node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css", "node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", - "src/styles.scss" + "src/styles.scss", + "node_modules/ng2-tree/styles.css" ], "scripts": [ "node_modules/chart.js/dist/Chart.bundle.js" diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json index 0527d5f1858a..6d7b8fcf8609 100644 --- a/src/pybind/mgr/dashboard/frontend/package-lock.json +++ b/src/pybind/mgr/dashboard/frontend/package-lock.json @@ -7913,6 +7913,11 @@ "version": "github:zzakir/ng2-toastr#0eafd72f1581058113ca338218d77f5c27f5b630", "from": "github:zzakir/ng2-toastr#0eafd72" }, + "ng2-tree": { + "version": "2.0.0-rc.11", + "resolved": "https://registry.npmjs.org/ng2-tree/-/ng2-tree-2.0.0-rc.11.tgz", + "integrity": "sha512-COGMatd+YrwJb3LSobagDC+t2PlSh4GkgG75Akh9QbXOSdFFPkbGmZvILg2xO4Hc+xicacvHp+6GINvjIeJwkA==" + }, "ngx-bootstrap": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-2.0.5.tgz", diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json index 11a082d694ed..3e8a3fed4515 100644 --- a/src/pybind/mgr/dashboard/frontend/package.json +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -62,6 +62,7 @@ "moment": "2.22.2", "ng2-charts": "1.6.0", "ng2-toastr": "zzakir/ng2-toastr#0eafd72", + "ng2-tree": "2.0.0-rc.11", "ngx-bootstrap": "2.0.5", "rxjs": "6.2.2", "rxjs-compat": "6.2.2", diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 542853fea045..79e7b5fa601c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -8,6 +8,7 @@ import { RbdImagesComponent } from './ceph/block/rbd-images/rbd-images.component import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component'; import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component'; import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component'; +import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component'; import { HostsComponent } from './ceph/cluster/hosts/hosts.component'; import { MonitorComponent } from './ceph/cluster/monitor/monitor.component'; import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component'; @@ -95,6 +96,12 @@ const routes: Routes = [ } ] }, + { + path: 'crush-map', + component: CrushmapComponent, + canActivate: [AuthGuardService], + data: { breadcrumbs: 'Cluster/CRUSH map' } + }, { path: 'perf_counters/:type/:id', component: PerformanceCounterComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 2d60ff99e64d..ba7bb2e4dfe6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; +import { TreeModule } from 'ng2-tree'; import { AlertModule } from 'ngx-bootstrap/alert'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { ModalModule } from 'ngx-bootstrap/modal'; @@ -14,6 +15,7 @@ import { PerformanceCounterModule } from '../performance-counter/performance-cou import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component'; import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component'; import { ConfigurationComponent } from './configuration/configuration.component'; +import { CrushmapComponent } from './crushmap/crushmap.component'; import { HostDetailsComponent } from './hosts/host-details/host-details.component'; import { HostsComponent } from './hosts/hosts.component'; import { MonitorComponent } from './monitor/monitor.component'; @@ -42,7 +44,8 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co BsDropdownModule.forRoot(), ModalModule.forRoot(), AlertModule.forRoot(), - TooltipModule.forRoot() + TooltipModule.forRoot(), + TreeModule ], declarations: [ HostsComponent, @@ -56,7 +59,8 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co HostDetailsComponent, ConfigurationDetailsComponent, ConfigurationFormComponent, - OsdReweightModalComponent + OsdReweightModalComponent, + CrushmapComponent ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html new file mode 100644 index 000000000000..d55b2270c294 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html @@ -0,0 +1,22 @@ +
+
+
+
+

+ {{ panelTitle }} +

+
+
+
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss new file mode 100644 index 000000000000..7bd19fca1671 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss @@ -0,0 +1,3 @@ +::ng-deep tree-internal .tree li { + cursor: pointer; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts new file mode 100644 index 000000000000..74dac5b774d0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts @@ -0,0 +1,88 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { of } from 'rxjs'; + +import { TreeModule } from 'ng2-tree'; +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { configureTestBed } from '../../../../testing/unit-test-helper'; +import { DashboardService } from '../../../shared/api/dashboard.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { CrushmapComponent } from './crushmap.component'; + +describe('CrushmapComponent', () => { + let component: CrushmapComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + configureTestBed({ + imports: [HttpClientTestingModule, TreeModule, TabsModule.forRoot(), SharedModule], + declarations: [CrushmapComponent], + providers: [DashboardService] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CrushmapComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display right title', () => { + fixture.detectChanges(); + const span = debugElement.nativeElement.querySelector('span'); + expect(span.textContent).toContain(component.panelTitle); + }); + + describe('test tree', () => { + let dashboardService: DashboardService; + const prepareGetHealth = (nodes: object[]) => { + spyOn(dashboardService, 'getHealth').and.returnValue( + of({ osd_map: { tree: { nodes: nodes } } }) + ); + fixture.detectChanges(); + }; + + beforeEach(() => { + dashboardService = debugElement.injector.get(DashboardService); + }); + + it('should display "No nodes!" if ceph tree nodes is empty array', () => { + prepareGetHealth([]); + expect(dashboardService.getHealth).toHaveBeenCalled(); + expect(component.tree.value).toEqual('No nodes!'); + }); + + describe('nodes not empty', () => { + beforeEach(() => { + prepareGetHealth([ + { children: [-2], type: 'root', name: 'default', id: -1 }, + { children: [1, 0, 2], type: 'host', name: 'my-host', id: -2 }, + { status: 'up', type: 'osd', name: 'osd.0', id: 0 }, + { status: 'down', type: 'osd', name: 'osd.1', id: 1 }, + { status: 'up', type: 'osd', name: 'osd.2', id: 2 } + ]); + }); + + it('should have tree structure derived from a root', () => { + expect(component.tree.value).toBe('default (root)'); + }); + + it('should have one host child with 3 osd children', () => { + expect(component.tree.children.length).toBe(1); + expect(component.tree.children[0].value).toBe('my-host (host)'); + expect(component.tree.children[0].children.length).toBe(3); + }); + + it('should have 3 osds in orderd', () => { + expect(component.tree.children[0].children[0].value).toBe('osd.0 (osd)--up'); + expect(component.tree.children[0].children[1].value).toBe('osd.1 (osd)--down'); + expect(component.tree.children[0].children[2].value).toBe('osd.2 (osd)--up'); + }); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts new file mode 100644 index 000000000000..9b6290f42429 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core'; + +import { NodeEvent, TreeModel } from 'ng2-tree'; + +import { DashboardService } from '../../../shared/api/dashboard.service'; + +@Component({ + selector: 'cd-crushmap', + templateUrl: './crushmap.component.html', + styleUrls: ['./crushmap.component.scss'] +}) +export class CrushmapComponent implements OnInit { + panelTitle: string; + tree: TreeModel; + metadata: any; + metadataKeyMap: { [key: number]: number } = {}; + + constructor(private dashboardService: DashboardService) { + this.panelTitle = 'CRUSH map viewer'; + } + + ngOnInit() { + this.dashboardService.getHealth().subscribe((data: any) => { + this.tree = this._abstractTreeData(data); + }); + } + + _abstractTreeData(data: any): TreeModel { + const nodes = data.osd_map.tree.nodes || []; + const treeNodeMap: { [key: number]: any } = {}; + + if (0 === nodes.length) { + return { + value: 'No nodes!', + settings: { static: true } + }; + } + + const rootNodeId = nodes[0].id || null; + nodes.reverse().forEach((node) => { + treeNodeMap[node.id] = this.generateTreeLeaf(node, treeNodeMap); + }); + + return treeNodeMap[rootNodeId]; + } + + private generateTreeLeaf(node: any, treeNodeMap) { + const id = node.id; + this.metadataKeyMap[id] = node; + const settings = { static: true }; + + let value: string = node.name + ' (' + node.type + ')'; + if (node.status) { + value += '--' + node.status; + } + + const children: any[] = []; + if (node.children) { + node.children.sort().forEach((childId) => { + children.push(treeNodeMap[childId]); + }); + + return { value, settings, id, children }; + } + + return { value, settings, id }; + } + + onNodeSelected(e: NodeEvent) { + this.metadata = this.metadataKeyMap[e.node.id]; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 34893e321f43..cbd1115ae701 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -82,6 +82,14 @@ routerLink="/configuration">Configuration +
  • + CRUSH map + +