From 696a41037f0bd502cc3dd4781195642c1ef54abf Mon Sep 17 00:00:00 2001 From: Afreen Misbah Date: Wed, 18 Sep 2024 19:24:37 +0530 Subject: [PATCH] mgr/dashboard: List gateways in a group - adds a group selection component for gateways - fetch gateway using daemons info - added tests fixes https://tracker.ceph.com/issues/68135 Signed-off-by: Afreen Misbah --- .../src/app/ceph/block/block.module.ts | 4 +- .../nvmeof-gateway.component.html | 23 +++ .../nvmeof-gateway.component.spec.ts | 137 +++++++++++++++--- .../nvmeof-gateway.component.ts | 103 +++++++++++-- .../src/app/shared/api/nvmeof.service.ts | 5 + .../src/app/shared/models/daemon.interface.ts | 2 + 6 files changed, 237 insertions(+), 37 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index 4f3531c3d5e..b6f04cadcc1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -53,6 +53,7 @@ import { NvmeofInitiatorsFormComponent } from './nvmeof-initiators-form/nvmeof-i import { ButtonModule, CheckboxModule, + ComboBoxModule, DatePickerModule, GridModule, IconModule, @@ -95,7 +96,8 @@ import Reset from '@carbon/icons/es/reset/32'; SelectModule, NumberModule, ModalModule, - DatePickerModule + DatePickerModule, + ComboBoxModule ], declarations: [ RbdListComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html index c466c8674cc..556033e89e5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html @@ -1,5 +1,20 @@ +
+ + + +
+ Gateways @@ -11,3 +26,11 @@ [columns]="gatewayColumns"> + + + + {{ row.status_desc }} + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts index 53187cd0f8d..1c8bf548566 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts @@ -1,26 +1,106 @@ -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { NvmeofGatewayComponent } from './nvmeof-gateway.component'; import { NvmeofService } from '../../../shared/api/nvmeof.service'; import { HttpClientModule } from '@angular/common/http'; import { SharedModule } from '~/app/shared/shared.module'; +import { ComboBoxModule, GridModule } from 'carbon-components-angular'; +import { NvmeofTabsComponent } from '../nvmeof-tabs/nvmeof-tabs.component'; +import { CephServiceService } from '~/app/shared/api/ceph-service.service'; + +const mockServiceDaemons = [ + { + daemon_type: 'nvmeof', + daemon_id: 'nvmeof.default.ceph-node-01.kdcguk', + daemon_name: 'nvmeof.nvmeof.default.ceph-node-01.kdcguk', + hostname: 'ceph-node-01', + container_id: '6fe5a9ae9c96', + container_image_id: '32a3d75b7c146d6c37b04ee3c9ba883ab88a8f7ae8f286de268d0f41ebd86a51', + container_image_name: 'quay.io/ceph/nvmeof:1.2.17', + container_image_digests: [ + 'quay.io/ceph/nvmeof@sha256:4308d05d3bb2167fc695d755316fec8d12ec3f00eb7639eeeabad38a5c4df0f9' + ], + memory_usage: 89443532, + cpu_percentage: '98.87%', + version: '1.2.17', + status: 1, + status_desc: 'running' + }, + { + daemon_type: 'nvmeof', + daemon_id: 'nvmeof.default.ceph-node-02.hybprc', + daemon_name: 'nvmeof.nvmeof.default.ceph-node-02.hybprc', + hostname: 'ceph-node-02', + container_id: '2b061130726b', + container_image_id: '32a3d75b7c146d6c37b04ee3c9ba883ab88a8f7ae8f286de268d0f41ebd86a51', + container_image_name: 'quay.io/ceph/nvmeof:1.2.17', + container_image_digests: [ + 'quay.io/ceph/nvmeof@sha256:4308d05d3bb2167fc695d755316fec8d12ec3f00eb7639eeeabad38a5c4df0f9' + ], + memory_usage: 89328189, + cpu_percentage: '98.89%', + version: '1.2.17', + status: 1, + status_desc: 'running' + } +]; const mockGateways = [ { - cli_version: '', - version: '1.2.5', - name: 'client.nvmeof.rbd.ceph-node-01.jnmnwa', - group: '', - addr: '192.168.100.101', - port: '5500', - load_balancing_group: 1, - spdk_version: '24.01' + id: 'client.nvmeof.nvmeof.default.ceph-node-01.kdcguk', + hostname: 'ceph-node-01', + status_desc: 'running', + status: 1 + }, + { + id: 'client.nvmeof.nvmeof.default.ceph-node-02.hybprc', + hostname: 'ceph-node-02', + status_desc: 'running', + status: 1 + } +]; + +const mockGwGroups = [ + { + content: 'default', + serviceName: 'nvmeof.rbd.default' + }, + { + content: 'foo', + serviceName: 'nvmeof.rbd.foo' } ]; +const mockServices = [ + [ + { + service_name: 'nvmeof.rbd.default', + service_type: 'nvmeof', + unmanaged: false, + spec: { + group: 'default' + } + }, + { + service_name: 'nvmeof.rbd.foo', + service_type: 'nvmeof', + unmanaged: false, + spec: { + group: 'foo' + } + } + ], + 2 +]; class MockNvmeOfService { - listGateways() { - return of(mockGateways); + listGatewayGroups() { + return of(mockServices); + } +} + +class MockCephServiceService { + getDaemons(_service: string) { + return of(mockServiceDaemons); } } @@ -28,26 +108,37 @@ describe('NvmeofGatewayComponent', () => { let component: NvmeofGatewayComponent; let fixture: ComponentFixture; - beforeEach(fakeAsync(() => { - TestBed.configureTestingModule({ - declarations: [NvmeofGatewayComponent], - imports: [HttpClientModule, SharedModule], - providers: [{ provide: NvmeofService, useClass: MockNvmeOfService }] + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NvmeofGatewayComponent, NvmeofTabsComponent], + imports: [HttpClientModule, SharedModule, ComboBoxModule, GridModule], + providers: [ + { provide: NvmeofService, useClass: MockNvmeOfService }, + { provide: CephServiceService, useClass: MockCephServiceService } + ] }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(NvmeofGatewayComponent); component = fixture.componentInstance; + component.ngOnInit(); + fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); - it('should retrieve gateways', fakeAsync(() => { - component.getGateways(); - tick(); - expect(component.gateways).toEqual(mockGateways); - })); + it('should load gateway groups correctly', () => { + expect(component.gwGroups.length).toBe(2); + expect(component.gwGroups).toStrictEqual(mockGwGroups); + }); + + it('should set service name of gateway groups correctly', () => { + expect(component.groupService).toBe(mockServices[0][0].service_name); + }); + + it('should set gateways correctly', () => { + expect(component.gateways.length).toBe(2); + expect(component.gateways).toStrictEqual(mockGateways); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts index 46600388bd9..c966da9b9c2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts @@ -1,10 +1,27 @@ -import { Component } from '@angular/core'; +import { Component, TemplateRef, ViewChild } from '@angular/core'; + +import _ from 'lodash'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; -import { NvmeofGateway } from '~/app/shared/models/nvmeof'; import { NvmeofService } from '../../../shared/api/nvmeof.service'; +import { CephServiceSpec } from '~/app/shared/models/service.interface'; +import { CephServiceService } from '~/app/shared/api/ceph-service.service'; +import { Daemon } from '~/app/shared/models/daemon.interface'; + +type ComboBoxItem = { + content: string; + serviceName: string; + selected?: boolean; +}; + +type Gateway = { + id: string; + hostname: string; + status: number; + status_desc: string; +}; @Component({ selector: 'cd-nvmeof-gateway', @@ -12,33 +29,93 @@ import { NvmeofService } from '../../../shared/api/nvmeof.service'; styleUrls: ['./nvmeof-gateway.component.scss'] }) export class NvmeofGatewayComponent { - gateways: NvmeofGateway[] = []; + @ViewChild('statusTpl', { static: true }) + statusTpl: TemplateRef; + + gateways: Gateway[] = []; gatewayColumns: any; selection = new CdTableSelection(); + gwGroups: ComboBoxItem[] = []; + groupService: string = null; - constructor(private nvmeofService: NvmeofService, public actionLabels: ActionLabelsI18n) {} + constructor( + private nvmeofService: NvmeofService, + private cephServiceService: CephServiceService, + public actionLabels: ActionLabelsI18n + ) {} ngOnInit() { + this.getGatewayGroups(); this.gatewayColumns = [ { - name: $localize`Name`, - prop: 'name' + name: $localize`Gateway ID`, + prop: 'id' }, { - name: $localize`Address`, - prop: 'addr' + name: $localize`Host name`, + prop: 'hostname' }, { - name: $localize`Port`, - prop: 'port' + name: $localize`Status`, + prop: 'status_desc', + cellTemplate: this.statusTpl } ]; } + // for Status column + getStatusClass(row: Gateway): string { + return _.get( + { + '-1': 'badge-danger', + '0': 'badge-warning', + '1': 'badge-success' + }, + row.status, + 'badge-dark' + ); + } + + // Gateways getGateways() { - this.nvmeofService.listGateways().subscribe((gateways: NvmeofGateway[] | NvmeofGateway) => { - if (Array.isArray(gateways)) this.gateways = gateways; - else this.gateways = [gateways]; + this.cephServiceService.getDaemons(this.groupService).subscribe((daemons: Daemon[]) => { + this.gateways = daemons.length + ? daemons.map((daemon: Daemon) => { + return { + id: `client.${daemon.daemon_name}`, + hostname: daemon.hostname, + status_desc: daemon.status_desc, + status: daemon.status + }; + }) + : []; + }); + } + + // Gateway groups + onGroupSelection(selected: ComboBoxItem) { + selected.selected = true; + this.groupService = selected.serviceName; + this.getGateways(); + } + + onGroupClear() { + this.groupService = null; + this.getGateways(); + } + + getGatewayGroups() { + this.nvmeofService.listGatewayGroups().subscribe((response: CephServiceSpec[][]) => { + this.gwGroups = response?.[0]?.length + ? response[0].map((group: CephServiceSpec) => { + return { + content: group?.spec?.group, + serviceName: group?.service_name + }; + }) + : []; + // Select first group if no group is selected + if (!this.groupService && this.gwGroups.length) this.onGroupSelection(this.gwGroups[0]); }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts index 7c72530e84a..a8baac4863f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts @@ -36,6 +36,11 @@ const UI_API_PATH = 'ui-api/nvmeof'; export class NvmeofService { constructor(private http: HttpClient) {} + // Gateway groups + listGatewayGroups() { + return this.http.get(`${API_PATH}/gateway/group`); + } + // Gateways listGateways() { return this.http.get(`${API_PATH}/gateway`); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts index c69a27851c6..440b276fafe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts @@ -5,6 +5,8 @@ export interface Daemon { container_image_name: string; daemon_id: string; daemon_type: string; + daemon_name: string; + hostname: string; version: string; status: number; status_desc: string; -- 2.39.5