Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mgr/dashboard: CRUSH map viewer #24766

Merged
merged 1 commit into from Nov 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/pybind/mgr/dashboard/frontend/angular.json
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions src/pybind/mgr/dashboard/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/pybind/mgr/dashboard/frontend/package.json
Expand Up @@ -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",
Expand Down
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -56,7 +59,8 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co
HostDetailsComponent,
ConfigurationDetailsComponent,
ConfigurationFormComponent,
OsdReweightModalComponent
OsdReweightModalComponent,
CrushmapComponent
]
})
export class ClusterModule {}
@@ -0,0 +1,22 @@
<div class="row">
<div class="col-sm-12 col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<span i18n>{{ panelTitle }}</span>
</h3>
</div>
<div class="panel-body">
<div class="col-sm-6 col-lg-6">
<tree [tree]="tree"
(nodeSelected)="onNodeSelected($event)"></tree>
</div>
<div class="col-sm-6 col-lg-6 metadata">
<cd-table-key-value *ngIf="metadata"
[data]="metadata">
</cd-table-key-value>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,3 @@
::ng-deep tree-internal .tree li {
cursor: pointer;
}
@@ -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<CrushmapComponent>;
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');
});
});
});
});
tspmelo marked this conversation as resolved.
Show resolved Hide resolved
@@ -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 {
Devp00l marked this conversation as resolved.
Show resolved Hide resolved
Devp00l marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a special Ceph convention? What about ods.1 (osd) - up or ods.1 (osd) [up]? Using colors might be the best solution in my opinion.

}

const children: any[] = [];
if (node.children) {
node.children.sort().forEach((childId) => {
children.push(treeNodeMap[childId]);
});

return { value, settings, id, children };
}

return { value, settings, id };
Devp00l marked this conversation as resolved.
Show resolved Hide resolved
}

onNodeSelected(e: NodeEvent) {
this.metadata = this.metadataKeyMap[e.node.id];
}
}
Expand Up @@ -82,6 +82,14 @@
routerLink="/configuration">Configuration
</a>
</li>
<li routerLinkActive="active"
class="tc_submenuitem tc_submenuitem_crush"
*ngIf="permissions.hosts.read && permissions.osd.read">
<a i18n
class="dropdown-item"
routerLink="/crush-map">CRUSH map
</a>
</li>
</ul>
</li>

Expand Down