From: pujaoshahu Date: Fri, 13 Feb 2026 09:15:19 +0000 (+0530) Subject: mgr/dashboard: Fix nvmeof edit host key in subsystem resources page X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=3e7e89b04ced0b5a6b594d74141095f3ee32fe39;p=ceph.git mgr/dashboard: Fix nvmeof edit host key in subsystem resources page Fixes: https://tracker.ceph.com/issues/74881 Signed-off-by: pujaoshahu (cherry picked from commit 392f7008b26c08d7e5e18ee0f5d2ff22a0afe40f) Conflicts: src/pybind/mgr/dashboard/controllers/nvmeof.py src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts src/pybind/mgr/dashboard/openapi.yaml --- diff --git a/src/pybind/mgr/dashboard/controllers/nvmeof.py b/src/pybind/mgr/dashboard/controllers/nvmeof.py index e52d7e566ae9..79411e61ba89 100644 --- a/src/pybind/mgr/dashboard/controllers/nvmeof.py +++ b/src/pybind/mgr/dashboard/controllers/nvmeof.py @@ -19,6 +19,7 @@ from . import APIDoc, APIRouter, BaseController, CreatePermission, \ logger = logging.getLogger(__name__) + NVME_SCHEMA = { "available": (bool, "Is NVMe/TCP available?"), "message": (str, "Descriptions") @@ -230,8 +231,8 @@ else: "max_namespaces": Param(int, "Maximum number of namespaces", True, None), "no_group_append": Param(int, "Do not append gateway group name to the NQN", True, False), - "serial_number": Param(int, "Subsystem serial number", True, None), - "dhchap_key": Param(int, "Subsystem DH-HMAC-CHAP key", True, None), + "serial_number": Param(str, "Subsystem serial number", True, None), + "dhchap_key": Param(str, "Subsystem DH-HMAC-CHAP key", True, None), "gw_group": Param(str, "NVMeoF gateway group", True, None), "traddr": Param(str, "NVMeoF gateway address", True, None), }, @@ -246,7 +247,8 @@ else: NVMeoFClient.pb2.create_subsystem_req( subsystem_nqn=nqn, serial_number=serial_number, max_namespaces=max_namespaces, enable_ha=enable_ha, - no_group_append=no_group_append, dhchap_key=dhchap_key + no_group_append=no_group_append, + dhchap_key=dhchap_key ) ) @@ -287,7 +289,8 @@ else: traddr: Optional[str] = None): return NVMeoFClient(gw_group=gw_group, traddr=traddr).stub.change_subsystem_key( NVMeoFClient.pb2.change_subsystem_key_req( - subsystem_nqn=nqn, dhchap_key=dhchap_key + subsystem_nqn=nqn, + dhchap_key=dhchap_key ) ) @@ -1223,6 +1226,8 @@ else: NVMeoFClient.pb2.remove_host_req(subsystem_nqn=nqn, host_nqn=host_nqn) ) + @Endpoint('PUT', '{host_nqn}/change_key') + @UpdatePermission @empty_response @Endpoint('PUT', '{host_nqn}/change_key') @UpdatePermission 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 ea6ac83312cb..89a0cfd07434 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,8 @@ import { NvmeofSubsystemsStepThreeComponent } from './nvmeof-subsystems-form/nvm import { NvmeofSubsystemsStepTwoComponent } from './nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component'; import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway-node.component'; import { NvmeofGroupFormComponent } from './nvmeof-group-form/nvmeof-group-form.component'; +import { NvmeofEditHostKeyModalComponent } from './nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component'; + import { ButtonModule, CheckboxModule, @@ -174,7 +176,8 @@ import { NvmeSubsystemViewComponent } from './nvme-subsystem-view/nvme-subsystem NvmeGatewayViewComponent, NvmeofGatewaySubsystemComponent, NvmeofGatewayNodeAddModalComponent, - NvmeSubsystemViewComponent + NvmeSubsystemViewComponent, + NvmeofEditHostKeyModalComponent ], exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.html new file mode 100644 index 000000000000..5357ece4cad8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.html @@ -0,0 +1,89 @@ + + +

+ {{'Subsystem'}} +

+ + + +

Edit Host Key

+
+ + + +

Confirm changes

+
+
+ +
+ + +

Update DHCHAP authentication key for the selected host.

+
+ + DHCHAP Key | {{ hostNQN }} + + + + This field is required. + Invalid DH-HMAC-CHAP key. Please enter a valid Base64 encoded key. + +
+
+ + + +

Modifying or removing the Host key can invalidate existing NVMe sessions and interrupt secure communication with hosts. Ensure new keys are configured on all connected systems before continuing.

+
+
+ + + + + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.spec.ts new file mode 100644 index 000000000000..0fdf214bfbbf --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.spec.ts @@ -0,0 +1,207 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { ToastrModule } from 'ngx-toastr'; +import { of, throwError } from 'rxjs'; + +import { SharedModule } from '~/app/shared/shared.module'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { NvmeofEditHostKeyModalComponent } from './nvmeof-edit-host-key-modal.component'; + +describe('NvmeofEditHostKeyModalComponent', () => { + let component: NvmeofEditHostKeyModalComponent; + let fixture: ComponentFixture; + let nvmeofService: NvmeofService; + let taskWrapperService: TaskWrapperService; + + const mockSubsystemNQN = 'nqn.2014-08.org.nvmexpress:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6'; + const mockHostNQN = 'nqn.2014-08.org.nvmexpress:uuid:12345678-1234-1234-1234-1234567890ab'; + const mockGroup = 'default'; + + const nvmeofServiceSpy = { + updateHostKey: jasmine.createSpy('updateHostKey').and.returnValue(of(null)) + }; + + const taskWrapperServiceSpy = { + wrapTaskAroundCall: jasmine.createSpy('wrapTaskAroundCall').and.callFake(({ call }) => call) + }; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [NvmeofEditHostKeyModalComponent], + imports: [ + ReactiveFormsModule, + HttpClientTestingModule, + RouterTestingModule, + SharedModule, + ToastrModule.forRoot() + ], + providers: [ + { provide: NvmeofService, useValue: nvmeofServiceSpy }, + { provide: TaskWrapperService, useValue: taskWrapperServiceSpy }, + { provide: 'subsystemNQN', useValue: mockSubsystemNQN }, + { provide: 'hostNQN', useValue: mockHostNQN }, + { provide: 'group', useValue: mockGroup }, + { provide: 'dhchapKey', useValue: '' } + ] + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(NvmeofEditHostKeyModalComponent); + component = fixture.componentInstance; + nvmeofService = TestBed.inject(NvmeofService); + taskWrapperService = TestBed.inject(TaskWrapperService); + nvmeofServiceSpy.updateHostKey.calls.reset(); + taskWrapperServiceSpy.wrapTaskAroundCall.calls.reset(); + nvmeofServiceSpy.updateHostKey.and.returnValue(of(null)); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize the form with empty dhchapKey', () => { + expect(component.editForm).toBeDefined(); + expect(component.editForm.get('dhchapKey').value).toBe(''); + expect(component.editForm.valid).toBe(false); + }); + + it('should inject subsystemNQN, hostNQN and group', () => { + expect(component.subsystemNQN).toBe(mockSubsystemNQN); + expect(component.hostNQN).toBe(mockHostNQN); + expect(component.group).toBe(mockGroup); + }); + + it('should display the host NQN in the form label', () => { + const label = fixture.debugElement.query(By.css('cds-text-label')); + expect(label.nativeElement.textContent).toContain(mockHostNQN); + }); + + it('should not submit if form is invalid', () => { + component.onSubmit(); + expect(nvmeofService.updateHostKey).not.toHaveBeenCalled(); + }); + + it('should submit successfully when form is valid', () => { + const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY='; + component.editForm.patchValue({ dhchapKey: mockKey }); + expect(component.editForm.valid).toBe(true); + + spyOn(component, 'closeModal'); + component.onSubmit(); + + expect(nvmeofService.updateHostKey).toHaveBeenCalledWith(mockSubsystemNQN, { + host_nqn: mockHostNQN, + dhchap_key: mockKey, + gw_group: mockGroup + }); + expect(taskWrapperService.wrapTaskAroundCall).toHaveBeenCalled(); + expect(component.closeModal).toHaveBeenCalled(); + }); + + it('should call updateHostKey with correct task metadata on submit', () => { + const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY='; + component.editForm.patchValue({ dhchapKey: mockKey }); + spyOn(component, 'closeModal'); + + component.onSubmit(); + + const callArgs = taskWrapperServiceSpy.wrapTaskAroundCall.calls.mostRecent().args[0]; + expect(callArgs.task.name).toBe('nvmeof/initiator/edit'); + expect(callArgs.task.metadata).toEqual({ + nqn: mockSubsystemNQN + }); + }); + + it('should handle error during submission', () => { + const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY='; + component.editForm.patchValue({ dhchapKey: mockKey }); + spyOn(component.editForm, 'setErrors'); + + nvmeofServiceSpy.updateHostKey.and.returnValue(throwError(() => ({ status: 500 }))); + + component.onSubmit(); + + expect(nvmeofService.updateHostKey).toHaveBeenCalled(); + expect(component.editForm.setErrors).toHaveBeenCalledTimes(1); + expect(component.editForm.setErrors).toHaveBeenCalledWith({ cdSubmitButton: true }); + }); + + it('should not close modal on error', () => { + const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY='; + component.editForm.patchValue({ dhchapKey: mockKey }); + spyOn(component, 'closeModal'); + + nvmeofServiceSpy.updateHostKey.and.returnValue(throwError(() => ({ status: 400 }))); + + component.onSubmit(); + + expect(component.closeModal).not.toHaveBeenCalled(); + }); + + it('should be valid with 43-character unpadded Base64 key with DHHC-1: prefix and trailing colon', () => { + const prefixedSuffixedKey = 'DHHC-1:00:Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY:'; + component.editForm.patchValue({ dhchapKey: prefixedSuffixedKey }); + expect(component.editForm.get('dhchapKey').valid).toBe(true); + expect(component.editForm.valid).toBe(true); + }); + + describe('Save button click', () => { + it('should trigger onSubmit and call updateHostKey when Save is clicked with valid form', () => { + const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY='; + component.editForm.patchValue({ dhchapKey: mockKey }); + fixture.detectChanges(); + + spyOn(component, 'closeModal'); + + // Step 1: Save (Confirmation) + fixture.debugElement + .query(By.css('cd-form-button-panel')) + .triggerEventHandler('submitActionEvent', null); + fixture.detectChanges(); + expect(component.showConfirmation).toBe(true); + + // Step 2: Save changes (Actual submit) + fixture.debugElement + .query(By.css('cd-form-button-panel')) + .triggerEventHandler('submitActionEvent', null); + fixture.detectChanges(); + + expect(nvmeofService.updateHostKey).toHaveBeenCalledWith(mockSubsystemNQN, { + host_nqn: mockHostNQN, + dhchap_key: mockKey, + gw_group: mockGroup + }); + expect(taskWrapperService.wrapTaskAroundCall).toHaveBeenCalled(); + expect(component.closeModal).toHaveBeenCalled(); + }); + + it('should not call updateHostKey when Save is clicked with empty form', () => { + fixture.detectChanges(); + + const submitPanel = fixture.debugElement.query(By.css('cd-form-button-panel')); + submitPanel.triggerEventHandler('submitActionEvent', null); + fixture.detectChanges(); + + expect(nvmeofService.updateHostKey).not.toHaveBeenCalled(); + }); + + it('should call closeModal when Cancel is clicked', () => { + spyOn(component, 'closeModal'); + fixture.detectChanges(); + + const submitPanel = fixture.debugElement.query(By.css('cd-form-button-panel')); + submitPanel.triggerEventHandler('backActionEvent', null); + fixture.detectChanges(); + + expect(component.closeModal).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.ts new file mode 100644 index 000000000000..08dcf66ca899 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component.ts @@ -0,0 +1,76 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { UntypedFormControl, Validators } from '@angular/forms'; +import { BaseModal } from 'carbon-components-angular'; +import { NvmeofService } from '~/app/shared/api/nvmeof.service'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; + +@Component({ + selector: 'cd-nvmeof-edit-host-key-modal', + templateUrl: './nvmeof-edit-host-key-modal.component.html', + styleUrls: ['./nvmeof-edit-host-key-modal.component.scss'], + standalone: false +}) +export class NvmeofEditHostKeyModalComponent extends BaseModal implements OnInit { + editForm: CdFormGroup; + showConfirmation = false; + + constructor( + @Inject('subsystemNQN') public subsystemNQN: string, + @Inject('hostNQN') public hostNQN: string, + @Inject('group') public group: string, + @Inject('dhchapKey') public existingDhchapKey: string, + private nvmeofService: NvmeofService, + private taskWrapper: TaskWrapperService + ) { + super(); + } + + ngOnInit() { + this.editForm = new CdFormGroup({ + dhchapKey: new UntypedFormControl(this.existingDhchapKey || '', [ + Validators.required, + CdValidators.base64() + ]) + }); + } + + onSave() { + if (this.editForm.invalid) { + return; + } + this.showConfirmation = true; + } + + goBack() { + this.showConfirmation = false; + } + + onSubmit() { + if (this.editForm.invalid) { + return; + } + const dhchapKey = this.editForm.getValue('dhchapKey'); + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('nvmeof/initiator/edit', { + nqn: this.subsystemNQN + }), + call: this.nvmeofService.updateHostKey(this.subsystemNQN, { + host_nqn: this.hostNQN, + dhchap_key: dhchapKey, + gw_group: this.group + }) + }) + .subscribe({ + error: () => { + this.editForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.closeModal(); + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html index 9c498ec9da05..942051ef37ab 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html @@ -89,6 +89,38 @@ + +
+
+ Enable encryption + +
+
+ + + @if (groupForm.controls.enableEncryption.value) { +
+
+ Encryption configuration + + +
+
+ } +
{ ReactiveFormsModule, RouterTestingModule, SharedModule, + CheckboxModule, GridModule, InputModule, SelectModule, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts index f91d156d68bf..7fd270e51ec7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts @@ -71,7 +71,9 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { pool: new UntypedFormControl('rbd', { validators: [Validators.required] }), - unmanaged: new UntypedFormControl(false) + unmanaged: new UntypedFormControl(false), + enableEncryption: new UntypedFormControl(false), + encryptionConfig: new UntypedFormControl(null) }); } @@ -146,7 +148,7 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { let taskUrl = `service/${URLVerbs.CREATE}`; const serviceName = `${formValues.pool}.${formValues.groupName}`; - const serviceSpec = { + const serviceSpec: Record = { service_type: 'nvmeof', service_id: serviceName, pool: formValues.pool, @@ -157,6 +159,10 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { unmanaged: formValues.unmanaged }; + if (formValues.enableCds && formValues.cdsInput) { + serviceSpec['encryption_key'] = formValues.cdsInput; + } + this.taskWrapperService .wrapTaskAroundCall({ task: new FinishedTask(taskUrl, { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html index e565c1ddb93e..97a520db0c6a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.html @@ -14,12 +14,10 @@ Allowing all hosts grants access to every initiator on the network. Authentication is not supported in this mode, which may expose the subsystem to unauthorized access.

- - Edit host access - + Edit host access diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts index b59fb002656c..e93857c53529 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts @@ -17,6 +17,7 @@ import { NvmeofSubsystemAuthType } from '~/app/shared/enum/nvmeof.enum'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { NvmeofEditHostKeyModalComponent } from '../nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component'; const BASE_URL = 'block/nvmeof/subsystems'; @@ -97,6 +98,14 @@ export class NvmeofInitiatorsListComponent implements OnInit { canBePrimary: (selection: CdTableSelection) => !selection.hasSelection, disable: () => this.hasAllHostsAllowed() }, + { + name: $localize`Edit host key`, + permission: 'update', + icon: Icons.edit, + click: () => this.editHostKeyModal(), + disable: () => this.selection.selected.length !== 1, + canBePrimary: (selection: CdTableSelection) => selection.selected.length === 1 + }, { name: this.actionLabels.REMOVE, permission: 'delete', @@ -115,6 +124,17 @@ export class NvmeofInitiatorsListComponent implements OnInit { } } + editHostKeyModal() { + const selected = this.selection.selected[0]; + if (!selected) return; + this.modalService.show(NvmeofEditHostKeyModalComponent, { + subsystemNQN: this.subsystemNQN, + hostNQN: selected.nqn, + group: this.group, + dhchapKey: selected.dhchap_key || '' + }); + } + getAllowAllHostIndex() { return this.selection.selected.findIndex((selected) => selected.nqn === '*'); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts index 0f7237ec80fd..866452eb9d11 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts @@ -80,7 +80,6 @@ export class NvmeofSubsystemsFormComponent implements OnInit { host_nqn: payload.hostType === HOST_TYPE.ALL ? '*' : payload.addedHosts.join(','), gw_group: this.group }; - this.nvmeofService .createSubsystem({ nqn: payload.nqn, 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 8937eb5026b7..aae1b2c4d817 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 @@ -44,6 +44,7 @@ export type NamespaceUpdateRequest = NvmeofRequest & { export type InitiatorRequest = NvmeofRequest & { host_nqn: string; + dhchap_key?: string; }; const API_PATH = 'api/nvmeof'; @@ -180,7 +181,23 @@ export class NvmeofService { }); } - removeInitiators(subsystemNQN: string, request: InitiatorRequest) { + updateHostKey(subsystemNQN: string, request: InitiatorRequest) { + return this.http.put( + `${API_PATH}/subsystem/${subsystemNQN}/host/${request.host_nqn}/change_key`, + request, + { + observe: 'response' + } + ); + } + + addNamespaceInitiators(nsid: string, request: NamespaceInitiatorRequest) { + return this.http.post(`${UI_API_PATH}/namespace/${nsid}/host`, request, { + observe: 'response' + }); + } + + removeSubsystemInitiators(subsystemNQN: string, request: InitiatorRequest) { return this.http.delete( `${UI_API_PATH}/subsystem/${subsystemNQN}/host/${request.host_nqn}/${request.gw_group}`, { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/validate.directive.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/validate.directive.ts index 0211103c9a68..60912195e563 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/validate.directive.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/validate.directive.ts @@ -38,7 +38,7 @@ export class ValidateDirective implements OnInit, OnDestroy { const submit$ = this.formGroupDir ? this.formGroupDir.ngSubmit : new Subject(); - merge(this.ngControl.control.statusChanges, submit$) + merge(this.ngControl.control.statusChanges, this.ngControl.control.valueChanges, submit$) .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.updateState(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts index d47d2930d37f..1b7a951855be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts @@ -272,6 +272,26 @@ export class CdValidators { }; } + /** + * Validator for DH-HMAC-CHAP keys that must be Base64 encoded. + * Accepts plain Base64 or DHHC-1:XX:base64: format. + * Skips validation when value is empty (use with required validator if needed). + * @returns {ValidatorFn} Returns error map with `invalidBase64` if validation fails. + */ + static base64(): ValidatorFn { + const plainBase64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + const dhchapFormatRegex = /^DHHC-1:[0-9a-fA-F]{2}:[A-Za-z0-9+/]+:$/; + return (control: AbstractControl): { [key: string]: boolean } | null => { + if (isEmptyInputValue(control.value)) { + return null; + } + const value = control.value; + return plainBase64Regex.test(value) || dhchapFormatRegex.test(value) + ? null + : { invalidBase64: true }; + }; + } + /** * Validate form control if condition is true with validators. * 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 32394528a8bb..1b20b785a563 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 @@ -409,6 +409,9 @@ export class TaskMessageService { 'nvmeof/initiator/add': this.newTaskMessage(this.commonOperations.add, (metadata) => this.nvmeofInitiator(metadata) ), + 'nvmeof/initiator/edit': this.newTaskMessage(this.commonOperations.update, (metadata) => + this.nvmeofInitiator(metadata) + ), 'nvmeof/initiator/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) => this.nvmeofInitiator(metadata) ), diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 3076c0ca8836..1bc614dd682a 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -9635,7 +9635,7 @@ paths: properties: dhchap_key: description: Subsystem DH-HMAC-CHAP key - type: integer + type: string enable_ha: default: true description: Enable high availability @@ -9655,8 +9655,8 @@ paths: type: string serial_number: description: Subsystem serial number - type: integer - traddr: + type: string + server_address: description: NVMeoF gateway address type: string required: @@ -9969,6 +9969,7 @@ paths: summary: Disallow hosts from accessing an NVMeoF subsystem tags: - NVMe-oF Subsystem Host Allowlist +<<<<<<< HEAD /api/nvmeof/subsystem/{nqn}/host/{host_nqn}/change_controller_key: put: parameters: @@ -10025,6 +10026,8 @@ paths: summary: Change host DH-HMAC-CHAP controller key tags: - NVMe-oF Subsystem Host Allowlist +======= +>>>>>>> 77603435ca8 (mgr/dashboard: Fix nvmeof edit host key in subsystem resources page) /api/nvmeof/subsystem/{nqn}/host/{host_nqn}/change_key: put: parameters: @@ -10051,7 +10054,8 @@ paths: gw_group: description: NVMeoF gateway group type: string - traddr: + server_address: + description: NVMeoF gateway address type: string required: - dhchap_key @@ -10059,13 +10063,21 @@ paths: responses: '200': content: + application/json: + schema: + type: object application/vnd.ceph.api.v1.0+json: - type: object + schema: + type: object description: Resource updated. '202': content: + application/json: + schema: + type: object application/vnd.ceph.api.v1.0+json: - type: object + schema: + type: object description: Operation is still executing. Please check the task queue. '400': description: Operation exception. Please check the response body for details. @@ -10081,108 +10093,6 @@ paths: summary: Change host DH-HMAC-CHAP key tags: - NVMe-oF Subsystem Host Allowlist - /api/nvmeof/subsystem/{nqn}/host/{host_nqn}/del_controller_key: - put: - parameters: - - description: NVMeoF subsystem NQN - in: path - name: nqn - required: true - schema: - type: string - - description: NVMeoF host NQN. - in: path - name: host_nqn - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - gw_group: - description: NVMeoF gateway group - type: string - traddr: - type: string - type: object - responses: - '200': - content: - application/vnd.ceph.api.v1.0+json: - type: object - description: Resource updated. - '202': - content: - application/vnd.ceph.api.v1.0+json: - type: object - description: Operation is still executing. Please check the task queue. - '400': - description: Operation exception. Please check the response body for details. - '401': - description: Unauthenticated access. Please login first. - '403': - description: Unauthorized access. Please check your permissions. - '500': - description: Unexpected error. Please check the response body for the stack - trace. - security: - - jwt: [] - summary: Delete host DH-HMAC-CHAP controller key - tags: - - NVMe-oF Subsystem Host Allowlist - /api/nvmeof/subsystem/{nqn}/host/{host_nqn}/del_key: - put: - parameters: - - description: NVMeoF subsystem NQN - in: path - name: nqn - required: true - schema: - type: string - - description: NVMeoF host NQN. - in: path - name: host_nqn - required: true - schema: - type: string - requestBody: - content: - application/json: - schema: - properties: - gw_group: - description: NVMeoF gateway group - type: string - traddr: - type: string - type: object - responses: - '200': - content: - application/vnd.ceph.api.v1.0+json: - type: object - description: Resource updated. - '202': - content: - application/vnd.ceph.api.v1.0+json: - type: object - description: Operation is still executing. Please check the task queue. - '400': - description: Operation exception. Please check the response body for details. - '401': - description: Unauthenticated access. Please login first. - '403': - description: Unauthorized access. Please check your permissions. - '500': - description: Unexpected error. Please check the response body for the stack - trace. - security: - - jwt: [] - summary: Delete host DH-HMAC-CHAP key - tags: - - NVMe-oF Subsystem Host Allowlist /api/nvmeof/subsystem/{nqn}/listener: get: parameters: