From a4f2eefe21f973d532a7e08d1ce4e977f39dfe88 Mon Sep 17 00:00:00 2001 From: Afreen Misbah Date: Wed, 17 Jul 2024 15:46:23 +0530 Subject: [PATCH] mgr/dashboard: Create and delete listeners Fixes https://tracker.ceph.com/issues/66996 - list listener under subsystems - delete listener - create listener Signed-off-by: Afreen Misbah --- .../src/app/ceph/block/block.module.ts | 11 +- .../nvmeof-listeners-form.component.html | 76 +++++++++++ .../nvmeof-listeners-form.component.scss | 0 .../nvmeof-listeners-form.component.spec.ts | 48 +++++++ .../nvmeof-listeners-form.component.ts | 115 ++++++++++++++++ .../nvmeof-listeners-list.component.html | 21 +++ .../nvmeof-listeners-list.component.scss | 0 .../nvmeof-listeners-list.component.spec.ts | 70 ++++++++++ .../nvmeof-listeners-list.component.ts | 123 ++++++++++++++++++ .../nvmeof-subsystems-details.component.html | 8 ++ .../nvmeof-subsystems-details.component.ts | 2 + .../src/app/shared/api/nvmeof.service.ts | 29 +++++ .../datatable/table/table.component.html | 10 ++ .../shared/datatable/table/table.component.ts | 3 + .../src/app/shared/enum/cell-template.enum.ts | 9 +- .../frontend/src/app/shared/models/nvmeof.ts | 9 ++ .../shared/services/task-message.service.ts | 13 +- .../mgr/dashboard/services/nvmeof_conf.py | 1 + 18 files changed, 545 insertions(+), 3 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.ts 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 8e926a40d99ed..21a0d5483ec79 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 @@ -43,6 +43,8 @@ import { NvmeofSubsystemsComponent } from './nvmeof-subsystems/nvmeof-subsystems import { NvmeofSubsystemsDetailsComponent } from './nvmeof-subsystems-details/nvmeof-subsystems-details.component'; import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component'; import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-subsystems-form.component'; +import { NvmeofListenersFormComponent } from './nvmeof-listeners-form/nvmeof-listeners-form.component'; +import { NvmeofListenersListComponent } from './nvmeof-listeners-list/nvmeof-listeners-list.component'; @NgModule({ imports: [ @@ -87,7 +89,9 @@ import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-s NvmeofSubsystemsComponent, NvmeofSubsystemsDetailsComponent, NvmeofTabsComponent, - NvmeofSubsystemsFormComponent + NvmeofSubsystemsFormComponent, + NvmeofListenersFormComponent, + NvmeofListenersListComponent ], exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent] }) @@ -244,6 +248,11 @@ const routes: Routes = [ path: `${URLVerbs.EDIT}/:subsystem_nqn`, component: NvmeofSubsystemsFormComponent, outlet: 'modal' + }, + { + path: `${URLVerbs.CREATE}/:subsystem_nqn/listener`, + component: NvmeofListenersFormComponent, + outlet: 'modal' } ] }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.html new file mode 100644 index 0000000000000..279d108d3fe20 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.html @@ -0,0 +1,76 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.spec.ts new file mode 100644 index 0000000000000..b115fd5b6f6fb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.spec.ts @@ -0,0 +1,48 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToastrModule } from 'ngx-toastr'; +import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; + +import { SharedModule } from '~/app/shared/shared.module'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { NvmeofListenersFormComponent } from './nvmeof-listeners-form.component'; + +describe('NvmeofListenersFormComponent', () => { + let component: NvmeofListenersFormComponent; + let fixture: ComponentFixture; + let nvmeofService: NvmeofService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NvmeofListenersFormComponent], + providers: [NgbActiveModal], + imports: [ + HttpClientTestingModule, + NgbTypeaheadModule, + ReactiveFormsModule, + RouterTestingModule, + SharedModule, + ToastrModule.forRoot() + ] + }).compileComponents(); + + fixture = TestBed.createComponent(NvmeofListenersFormComponent); + component = fixture.componentInstance; + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('should test form', () => { + beforeEach(() => { + nvmeofService = TestBed.inject(NvmeofService); + spyOn(nvmeofService, 'createListener').and.stub(); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts new file mode 100644 index 0000000000000..bc02ea99c98e6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts @@ -0,0 +1,115 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ListenerRequest, NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { Permission } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { FormatterService } from '~/app/shared/services/formatter.service'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { HostService } from '~/app/shared/api/host.service'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +@Component({ + selector: 'cd-nvmeof-listeners-form', + templateUrl: './nvmeof-listeners-form.component.html', + styleUrls: ['./nvmeof-listeners-form.component.scss'] +}) +export class NvmeofListenersFormComponent implements OnInit { + action: string; + permission: Permission; + hostPermission: Permission; + resource: string; + pageURL: string; + listenerForm: CdFormGroup; + subsystemNQN: string; + hosts: Array = null; + + constructor( + public actionLabels: ActionLabelsI18n, + private authStorageService: AuthStorageService, + private taskWrapperService: TaskWrapperService, + private nvmeofService: NvmeofService, + private hostService: HostService, + private router: Router, + private route: ActivatedRoute, + public activeModal: NgbActiveModal, + public formatterService: FormatterService, + public dimlessBinaryPipe: DimlessBinaryPipe + ) { + this.permission = this.authStorageService.getPermissions().nvmeof; + this.hostPermission = this.authStorageService.getPermissions().hosts; + this.resource = $localize`Listener`; + this.pageURL = 'block/nvmeof/subsystems'; + } + + setHosts() { + const hostContext = new CdTableFetchDataContext(() => undefined); + this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: any[]) => { + const nvmeofHosts = resp.filter((r) => + r.service_instances.some((si: any) => si.type === 'nvmeof') + ); + this.hosts = nvmeofHosts.map((h) => ({ hostname: h.hostname, addr: h.addr })); + }); + } + + ngOnInit() { + this.createForm(); + this.action = this.actionLabels.CREATE; + this.route.params.subscribe((params: { subsystem_nqn: string }) => { + this.subsystemNQN = params.subsystem_nqn; + }); + this.setHosts(); + } + + createForm() { + this.listenerForm = new CdFormGroup({ + host: new UntypedFormControl(null, { + validators: [Validators.required] + }), + trsvcid: new UntypedFormControl(4420, [ + Validators.required, + CdValidators.number(false), + Validators.max(65535) + ]) + }); + } + + buildRequest(): ListenerRequest { + const host = this.listenerForm.getValue('host'); + let trsvcid = Number(this.listenerForm.getValue('trsvcid')); + if (!trsvcid) trsvcid = 4420; + const request = { + host_name: host.hostname, + traddr: host.addr, + trsvcid + }; + return request; + } + + onSubmit() { + const component = this; + const taskUrl: string = `nvmeof/listener/${URLVerbs.CREATE}`; + const request = this.buildRequest(); + this.taskWrapperService + .wrapTaskAroundCall({ + task: new FinishedTask(taskUrl, { + nqn: this.subsystemNQN, + host_name: request.host_name + }), + call: this.nvmeofService.createListener(this.subsystemNQN, request) + }) + .subscribe({ + error() { + component.listenerForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.router.navigate([this.pageURL, { outlets: { modal: null } }]); + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html new file mode 100644 index 0000000000000..da0015541207d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.html @@ -0,0 +1,21 @@ + + + A listener defines the IP port on the gateway that is to process NVMe/TCP commands and I/O operations. + + + +
+ + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.spec.ts new file mode 100644 index 0000000000000..ecf8c4959af2e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.spec.ts @@ -0,0 +1,70 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { NvmeofListenersListComponent } from './nvmeof-listeners-list.component'; +import { HttpClientModule } from '@angular/common/http'; +import { RouterTestingModule } from '@angular/router/testing'; +import { SharedModule } from '~/app/shared/shared.module'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { of } from 'rxjs'; + +const mockListeners = [ + { + host_name: 'ceph-node-02', + trtype: 'TCP', + traddr: '192.168.100.102', + adrfam: 0, + trsvcid: 4421 + } +]; + +class MockNvmeOfService { + listListeners() { + return of(mockListeners); + } +} + +class MockAuthStorageService { + getPermissions() { + return { nvmeof: {} }; + } +} + +class MockModalService {} + +class MockTaskWrapperService {} + +describe('NvmeofListenersListComponent', () => { + let component: NvmeofListenersListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NvmeofListenersListComponent], + imports: [HttpClientModule, RouterTestingModule, SharedModule], + providers: [ + { provide: NvmeofService, useClass: MockNvmeOfService }, + { provide: AuthStorageService, useClass: MockAuthStorageService }, + { provide: ModalService, useClass: MockModalService }, + { provide: TaskWrapperService, useClass: MockTaskWrapperService } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(NvmeofListenersListComponent); + component = fixture.componentInstance; + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should retrieve subsystems', fakeAsync(() => { + component.listListeners(); + tick(); + expect(component.listeners).toEqual(mockListeners); + })); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.ts new file mode 100644 index 0000000000000..26b48d8aad9bf --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.ts @@ -0,0 +1,123 @@ +import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { NvmeofListener } from '~/app/shared/models/nvmeof'; +import { Permission } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; + +const BASE_URL = 'block/nvmeof/subsystems'; + +@Component({ + selector: 'cd-nvmeof-listeners-list', + templateUrl: './nvmeof-listeners-list.component.html', + styleUrls: ['./nvmeof-listeners-list.component.scss'] +}) +export class NvmeofListenersListComponent implements OnInit, OnChanges { + @Input() + subsystemNQN: string; + + listenerColumns: any; + tableActions: CdTableAction[]; + selection = new CdTableSelection(); + permission: Permission; + listeners: NvmeofListener[]; + + constructor( + public actionLabels: ActionLabelsI18n, + private modalService: ModalService, + private authStorageService: AuthStorageService, + private taskWrapper: TaskWrapperService, + private nvmeofService: NvmeofService, + private router: Router + ) { + this.permission = this.authStorageService.getPermissions().nvmeof; + } + + ngOnInit() { + this.listenerColumns = [ + { + name: $localize`Host`, + prop: 'host_name' + }, + { + name: $localize`Transport`, + prop: 'trtype' + }, + { + name: $localize`Address`, + prop: 'full_addr', + cellTransformation: CellTemplate.copy + } + ]; + this.tableActions = [ + { + name: this.actionLabels.CREATE, + permission: 'create', + icon: Icons.add, + click: () => + this.router.navigate([ + BASE_URL, + { outlets: { modal: [URLVerbs.CREATE, this.subsystemNQN, 'listener'] } } + ]), + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + }, + { + name: this.actionLabels.DELETE, + permission: 'delete', + icon: Icons.destroy, + click: () => this.deleteSubsystemModal() + } + ]; + } + + ngOnChanges() { + this.listListeners(); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + listListeners() { + this.nvmeofService + .listListeners(this.subsystemNQN) + .subscribe((listResponse: NvmeofListener[]) => { + this.listeners = listResponse.map((listener, index) => { + listener['id'] = index; + listener['full_addr'] = `${listener.traddr}:${listener.trsvcid}`; + return listener; + }); + }); + } + + deleteSubsystemModal() { + const listener = this.selection.first(); + this.modalService.show(CriticalConfirmationModalComponent, { + itemDescription: 'Listener', + actionDescription: 'delete', + itemNames: [`listener ${listener.host_name} (${listener.traddr}:${listener.trsvcid})`], + submitActionObservable: () => + this.taskWrapper.wrapTaskAroundCall({ + task: new FinishedTask('nvmeof/listener/delete', { + nqn: this.subsystemNQN, + host_name: listener.host_name + }), + call: this.nvmeofService.deleteListener( + this.subsystemNQN, + listener.host_name, + listener.traddr, + listener.trsvcid + ) + }) + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html index 56a05dfecda59..5b551bdb96611 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.html @@ -11,6 +11,14 @@ + + Listeners + + + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.ts index a79b01d6704dd..5e8abf9a4852f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.ts @@ -12,10 +12,12 @@ export class NvmeofSubsystemsDetailsComponent implements OnChanges { selectedItem: any; data: any; + subsystemNQN: string; ngOnChanges() { if (this.selection) { this.selectedItem = this.selection; + this.subsystemNQN = this.selectedItem.nqn; this.data = {}; this.data[$localize`Serial Number`] = this.selectedItem.serial_number; this.data[$localize`Model Number`] = this.selectedItem.model_number; 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 8d5b8a3830c81..d9375ba0de3c3 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 @@ -5,6 +5,12 @@ import _ from 'lodash'; import { Observable, of as observableOf } from 'rxjs'; import { catchError, mapTo } from 'rxjs/operators'; +export interface ListenerRequest { + host_name: string; + traddr: string; + trsvcid: number; +} + const BASE_URL = 'api/nvmeof'; @Injectable({ @@ -44,4 +50,27 @@ export class NvmeofService { }) ); } + + // listeners + listListeners(subsystemNQN: string) { + return this.http.get(`${BASE_URL}/subsystem/${subsystemNQN}/listener`); + } + + createListener(subsystemNQN: string, request: ListenerRequest) { + return this.http.post(`${BASE_URL}/subsystem/${subsystemNQN}/listener`, request, { + observe: 'response' + }); + } + + deleteListener(subsystemNQN: string, hostName: string, traddr: string, trsvcid: string) { + return this.http.delete( + `${BASE_URL}/subsystem/${subsystemNQN}/listener/${hostName}/${traddr}`, + { + observe: 'response', + params: { + trsvcid + } + } + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html index a856a4c487019..e567981899f94 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html @@ -370,3 +370,13 @@ + + + {{value}} + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts index 905646b55b8a8..80588cc5dc85c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts @@ -77,6 +77,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O pathTpl: TemplateRef; @ViewChild('tooltipTpl', { static: true }) tooltipTpl: TemplateRef; + @ViewChild('copyTpl', { static: true }) + copyTpl: TemplateRef; // This is the array with the items to be shown. @Input() @@ -615,6 +617,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O this.cellTemplates.timeAgo = this.timeAgoTpl; this.cellTemplates.path = this.pathTpl; this.cellTemplates.tooltip = this.tooltipTpl; + this.cellTemplates.copy = this.copyTpl; } useCustomClass(value: any): string { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts index 5c4072f7f1fc6..bda66f6004e6a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts @@ -72,5 +72,12 @@ export enum CellTemplate { // } // } */ - tooltip = 'tooltip' + tooltip = 'tooltip', + /* + This template is used to attach copy to clipboard functionality to the given column value + // { + // ... + // cellTransformation: CellTemplate.copy, + */ + copy = 'copy' } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts index e383d4a1dfca2..e86dae7dcd91a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts @@ -19,3 +19,12 @@ export interface NvmeofSubsystem { subtype: string; max_namespaces: number; } + +export interface NvmeofListener { + host_name: string; + trtype: string; + traddr: string; + adrfam: number; // 0: IPv4, 1: IPv6 + trsvcid: number; // 4420 + id?: number; // for table +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 0e966a9474b7b..4ae4a5bc028ae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -354,13 +354,20 @@ export class TaskMessageService { 'iscsi/target/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) => this.iscsiTarget(metadata) ), - // NVME/TCP tasks + // nvmeof 'nvmeof/subsystem/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.nvmeofSubsystem(metadata) ), 'nvmeof/subsystem/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) => this.nvmeofSubsystem(metadata) ), + 'nvmeof/listener/create': this.newTaskMessage(this.commonOperations.create, (metadata) => + this.nvmeofListener(metadata) + ), + 'nvmeof/listener/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) => + this.nvmeofListener(metadata) + ), + // nfs 'nfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.nfs(metadata) ), @@ -490,6 +497,10 @@ export class TaskMessageService { return $localize`subsystem '${metadata.nqn}'`; } + nvmeofListener(metadata: any) { + return $localize`listener '${metadata.host_name} on subsystem ${metadata.nqn}`; + } + nfs(metadata: any) { return $localize`NFS '${metadata.cluster_id}\:${ metadata.export_id ? metadata.export_id : metadata.path diff --git a/src/pybind/mgr/dashboard/services/nvmeof_conf.py b/src/pybind/mgr/dashboard/services/nvmeof_conf.py index 4d3f312eb485b..3f076d7b216ad 100644 --- a/src/pybind/mgr/dashboard/services/nvmeof_conf.py +++ b/src/pybind/mgr/dashboard/services/nvmeof_conf.py @@ -11,6 +11,7 @@ from ..services.orchestrator import OrchClient logger = logging.getLogger('nvmeof_conf') + class NvmeofGatewayAlreadyExists(Exception): def __init__(self, gateway_name): super(NvmeofGatewayAlreadyExists, self).__init__( -- 2.39.5