From 442346f0efbb5d7d3af3ebdf847586bbe3f93f6d Mon Sep 17 00:00:00 2001 From: Afreen Date: Fri, 31 May 2024 13:24:27 +0530 Subject: [PATCH] mgr/dashboard: Introduce NVMe/TCP navigation Fixes https://tracker.ceph.com/issues/66346 - adds NVMe/TCP tab under Block nav - adds overview page for NVMe/TCP nav - overview page lists gateways - add default error page when no nvmeof service running - added unit tests - fixes service page e2e test Signed-off-by: Afreen --- .../mgr/dashboard/controllers/nvmeof.py | 40 ++++++++++++-- .../frontend/src/app/app-routing.module.ts | 5 ++ .../src/app/ceph/block/block.module.ts | 27 +++++++++- .../nvmeof-gateway.component.html | 22 ++++++++ .../nvmeof-gateway.component.scss | 0 .../nvmeof-gateway.component.spec.ts | 53 +++++++++++++++++++ .../nvmeof-gateway.component.ts | 47 ++++++++++++++++ .../service-form/service-form.component.html | 3 +- .../service-form/service-form.component.ts | 21 ++++++-- .../breadcrumbs/breadcrumbs.component.spec.ts | 8 +-- .../breadcrumbs/breadcrumbs.component.ts | 2 +- .../navigation/navigation.component.html | 8 +++ .../src/app/shared/api/nvmeof.service.spec.ts | 33 ++++++++++++ .../src/app/shared/api/nvmeof.service.ts | 15 ++++++ .../src/app/shared/models/breadcrumbs.ts | 4 +- .../frontend/src/app/shared/models/nvmeof.ts | 10 ++++ .../src/app/shared/models/permission.spec.ts | 3 ++ .../src/app/shared/models/permissions.ts | 2 + 18 files changed, 288 insertions(+), 15 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts diff --git a/src/pybind/mgr/dashboard/controllers/nvmeof.py b/src/pybind/mgr/dashboard/controllers/nvmeof.py index 3d9e337870271..84d7a37952e72 100644 --- a/src/pybind/mgr/dashboard/controllers/nvmeof.py +++ b/src/pybind/mgr/dashboard/controllers/nvmeof.py @@ -1,16 +1,27 @@ # -*- coding: utf-8 -*- -from typing import Optional +import logging +from typing import Any, Dict, Optional +from .. import mgr from ..model import nvmeof as model from ..security import Scope +from ..services.orchestrator import OrchClient from ..tools import str_to_bool -from . import APIDoc, APIRouter, Endpoint, EndpointDoc, Param, ReadPermission, RESTController +from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, Param, \ + ReadPermission, RESTController, UIRouter + +logger = logging.getLogger(__name__) + +NVME_SCHEMA = { + "available": (bool, "Is NVMe/TCP available?"), + "message": (str, "Descriptions") +} try: from ..services.nvmeof_client import NVMeoFClient, empty_response, \ handle_nvmeof_error, map_collection, map_model -except ImportError: - pass +except ImportError as e: + logger.error("Failed to import NVMeoFClient and related components: %s", e) else: @APIRouter("/nvmeof/gateway", Scope.NVME_OF) @APIDoc("NVMe-oF Gateway Management API", "NVMe-oF Gateway") @@ -380,3 +391,24 @@ else: return NVMeoFClient().stub.list_connections( NVMeoFClient.pb2.list_connections_req(subsystem=nqn) ) + + +@UIRouter('/nvmeof', Scope.NVME_OF) +@APIDoc("NVMe/TCP Management API", "NVMe/TCP") +class NVMeoFStatus(BaseController): + @Endpoint() + @ReadPermission + @EndpointDoc("Display NVMe/TCP service Status", + responses={200: NVME_SCHEMA}) + def status(self) -> dict: + status: Dict[str, Any] = {'available': True, 'message': None} + orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator') + if orch_backend == 'cephadm': + orch = OrchClient.instance() + orch_status = orch.status() + if not orch_status['available']: + return status + if not orch.services.list_daemons(daemon_type='nvmeof'): + status["available"] = False + status["message"] = 'Create an NVMe/TCP service to get started.' + return status 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 8b09a5fc27d6b..2316896863e47 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 @@ -179,6 +179,11 @@ const routes: Routes = [ component: ServiceFormComponent, outlet: 'modal' }, + { + path: `${URLVerbs.CREATE}/:type`, + component: ServiceFormComponent, + outlet: 'modal' + }, { path: `${URLVerbs.EDIT}/:type/:name`, component: ServiceFormComponent, 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 b9995ac029de9..30a8d21d2c3f3 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 @@ -38,6 +38,7 @@ import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-list.component import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component'; import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component'; import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component'; +import { NvmeofGatewayComponent } from './nvmeof-gateway/nvmeof-gateway.component'; @NgModule({ imports: [ @@ -77,7 +78,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra RbdConfigurationListComponent, RbdConfigurationFormComponent, RbdTabsComponent, - RbdPerformanceComponent + RbdPerformanceComponent, + NvmeofGatewayComponent ], exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent] }) @@ -198,6 +200,29 @@ const routes: Routes = [ ] } ] + }, + // NVMe/TCP + { + path: 'nvmeof', + canActivate: [ModuleStatusGuardService], + data: { + breadcrumbs: true, + text: 'NVMe/TCP', + path: 'nvmeof', + disableSplit: true, + moduleStatusGuardConfig: { + uiApiPath: 'nvmeof', + redirectTo: 'error', + header: $localize`NVMe/TCP Gateway not configured`, + button_name: $localize`Configure NVMe/TCP`, + button_route: ['/services', { outlets: { modal: ['create', 'nvmeof'] } }], + uiConfig: false + } + }, + children: [ + { path: '', redirectTo: 'gateways', pathMatch: 'full' }, + { path: 'gateways', component: NvmeofGatewayComponent, data: { breadcrumbs: 'Gateways' } } + ] } ]; 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 new file mode 100644 index 0000000000000..3d27a77b1a846 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html @@ -0,0 +1,22 @@ + + + + Gateways + + The NVMe-oF gateway integrates Ceph with the NVMe over TCP (NVMe/TCP) protocol to provide an NVMe/TCP target that exports RADOS Block Device (RBD) images. + + +
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d 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 new file mode 100644 index 0000000000000..53187cd0f8d8c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.spec.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } 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'; + +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' + } +]; + +class MockNvmeOfService { + listGateways() { + return of(mockGateways); + } +} + +describe('NvmeofGatewayComponent', () => { + let component: NvmeofGatewayComponent; + let fixture: ComponentFixture; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + declarations: [NvmeofGatewayComponent], + imports: [HttpClientModule, SharedModule], + providers: [{ provide: NvmeofService, useClass: MockNvmeOfService }] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NvmeofGatewayComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should retrieve gateways', fakeAsync(() => { + component.getGateways(); + tick(); + expect(component.gateways).toEqual(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 new file mode 100644 index 0000000000000..7d5b3fbe9fe9e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; + +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { NvmeofGateway } from '~/app/shared/models/nvmeof'; + +import { NvmeofService } from '../../../shared/api/nvmeof.service'; + +@Component({ + selector: 'cd-nvmeof-gateway', + templateUrl: './nvmeof-gateway.component.html', + styleUrls: ['./nvmeof-gateway.component.scss'] +}) +export class NvmeofGatewayComponent extends ListWithDetails implements OnInit { + gateways: NvmeofGateway[] = []; + gatewayColumns: any; + selection = new CdTableSelection(); + + constructor(private nvmeofService: NvmeofService, public actionLabels: ActionLabelsI18n) { + super(); + } + + ngOnInit() { + this.gatewayColumns = [ + { + name: $localize`Name`, + prop: 'name' + }, + { + name: $localize`Address`, + prop: 'addr' + }, + { + name: $localize`Port`, + prop: 'port' + } + ]; + } + + getGateways() { + this.nvmeofService.listGateways().subscribe((gateways: NvmeofGateway[] | NvmeofGateway) => { + if (Array.isArray(gateways)) this.gateways = gateways; + else this.gateways = [gateways]; + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html index 62333d3391b74..7a439e23dfe70 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html @@ -222,7 +222,8 @@