]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Add CRUSH map viewer
authorguodan1 <guodan1@lenovo.com>
Mon, 29 Oct 2018 03:04:28 +0000 (11:04 +0800)
committerguodan1 <guodan1@lenovo.com>
Tue, 6 Nov 2018 02:13:41 +0000 (10:13 +0800)
Fixes: http://tracker.ceph.com/issues/35684
Signed-off-by: familyuu <guodan1@lenovo.com>
src/pybind/mgr/dashboard/frontend/angular.json
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/crushmap/crushmap.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html

index 3b30176bfec96f958b26dcad8644999f5e3f7ead..3b5cc73ca70d438bd897e29543e48de831c60382 100644 (file)
@@ -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"
index 0527d5f1858ae86da56be83dcac449d172223289..6d7b8fcf8609c87e648bbf390a7e291d6d848fc1 100644 (file)
       "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",
index 11a082d694ed9273a49856c126582b1466b70766..3e8a3fed45159ab1d90759e9461bf15a6c82ab6e 100644 (file)
@@ -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",
index 542853fea04545e6b9270e3b9a67d68c5c9db937..79e7b5fa601c7335ba740bbe45ef90c44565d92b 100644 (file)
@@ -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,
index 2d60ff99e64dd4246cbd4dd23f9bf9b251f008c8..ba7bb2e4dfe628e02d05f96641a52e6bcdcc476f 100644 (file)
@@ -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 (file)
index 0000000..d55b227
--- /dev/null
@@ -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>
\ 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 (file)
index 0000000..7bd19fc
--- /dev/null
@@ -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 (file)
index 0000000..74dac5b
--- /dev/null
@@ -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');
+      });
+    });
+  });
+});
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 (file)
index 0000000..9b6290f
--- /dev/null
@@ -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];
+  }
+}
index 34893e321f43242813cd1fb664168c3c440b4b48..cbd1115ae701120c1126b936f2facbed10b6b10b 100644 (file)
                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>