From: Sagar Gopale Date: Mon, 9 Mar 2026 10:42:50 +0000 (+0530) Subject: mgr/dashboard: Initiator add shows success but host is not added/displayed in Subsyst... X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=e32e8d251c5506c5a214477d7ba09f2717bf1066;p=ceph.git mgr/dashboard: Initiator add shows success but host is not added/displayed in Subsystem Initiators table Fixes: https://tracker.ceph.com/issues/75402 Signed-off-by: Sagar Gopale --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.html index a6183a8acf0..4b2df6992c5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.html @@ -3,6 +3,7 @@ [title]="title" [description]="description" (submitRequested)="onSubmit($event)" + (stepChanged)="onStepChanged()" [isSubmitLoading]="isSubmitLoading" submitButtonLabel="Add" i18n-submitButtonLabel> @@ -14,4 +15,12 @@ [group]="group" [existingHosts]="existingHosts"> + @if(showAuthStep) { + + + + } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts index 7d50e1a8c56..4ef4f03929b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts @@ -62,6 +62,19 @@ describe('NvmeofInitiatorsFormComponent', () => { expect(component).toBeTruthy(); }); + it('should initialize with two steps (Host access control + Authentication optional)', () => { + expect(component.steps.length).toBe(2); + expect(component.steps[0].label).toBe('Host access control'); + expect(component.steps[1].label).toBe('Authentication (optional)'); + }); + + it('should hide Authentication step when showAuthStep is false', () => { + component.showAuthStep = false; + component.rebuildSteps(); + expect(component.steps.length).toBe(1); + expect(component.steps[0].label).toBe('Host access control'); + }); + describe('should test form', () => { beforeEach(() => { nvmeofService = TestBed.inject(NvmeofService); @@ -86,5 +99,24 @@ describe('NvmeofInitiatorsFormComponent', () => { hosts: [{ dhchap_key: '', host_nqn: 'host1' }] }); }); + + it('should build hosts from addedHosts when hostDchapKeyList is absent', () => { + const subsystemNQN = 'nqn.test'; + component.subsystemNQN = subsystemNQN; + component.group = 'test-group'; + + const payload: any = { + hostType: HOST_TYPE.SPECIFIC, + addedHosts: ['host2'], + gw_group: 'test-group' + }; + + component.onSubmit(payload); + expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith(subsystemNQN, { + allow_all: false, + gw_group: 'test-group', + hosts: [{ dhchap_key: '', host_nqn: 'host2' }] + }); + }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts index d1c0d1fcb12..cbb5513c6ec 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts @@ -1,11 +1,24 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { Step } from 'carbon-components-angular'; import { NvmeofService, SubsystemInitiatorRequest } from '~/app/shared/api/nvmeof.service'; import { FinishedTask } from '~/app/shared/models/finished-task'; -import { HOST_TYPE, NvmeofSubsystemInitiator } from '~/app/shared/models/nvmeof'; +import { + AuthStepType, + HOST_TYPE, + HostStepType, + NvmeofSubsystemInitiator +} from '~/app/shared/models/nvmeof'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { SubsystemPayload } from '../nvmeof-subsystems-form/nvmeof-subsystems-form.component'; +import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.component'; + +type InitiatorsFormPayload = Pick & + Partial>; + +const STEP_LABELS = { + HOSTS: $localize`Host access control`, + AUTH: $localize`Authentication (optional)` +} as const; @Component({ selector: 'cd-nvmeof-initiators-form', @@ -18,13 +31,12 @@ export class NvmeofInitiatorsFormComponent implements OnInit { subsystemNQN!: string; isSubmitLoading = false; existingHosts: string[] = []; + showAuthStep = true; + stepTwoValue: HostStepType = null; - steps: Step[] = [ - { - label: $localize`Host access control`, - invalid: false - } - ]; + @ViewChild(TearsheetComponent) tearsheet!: TearsheetComponent; + + steps: Step[] = []; title = $localize`Add Initiator`; description = $localize`Allow specific hosts to run NVMe/TCP commands to the NVMe subsystem.`; @@ -52,6 +64,38 @@ export class NvmeofInitiatorsFormComponent implements OnInit { } this.fetchExistingHosts(); }); + this.rebuildSteps(); + } + + rebuildSteps() { + const steps: Step[] = [{ label: STEP_LABELS.HOSTS, invalid: false }]; + + if (this.showAuthStep) { + steps.push({ label: STEP_LABELS.AUTH, invalid: false }); + } + + this.steps = steps; + + if (this.tearsheet?.currentStep >= steps.length) { + this.tearsheet.currentStep = steps.length - 1; + } + } + + onStepChanged() { + if (!this.tearsheet) return; + + const hostStep = this.tearsheet.getStepValueByLabel(STEP_LABELS.HOSTS); + + if (hostStep) { + this.stepTwoValue = hostStep; + } + + const nextShowAuth = (hostStep?.hostType ?? HOST_TYPE.SPECIFIC) === HOST_TYPE.SPECIFIC; + + if (nextShowAuth !== this.showAuthStep) { + this.showAuthStep = nextShowAuth; + this.rebuildSteps(); + } } fetchExistingHosts() { @@ -64,13 +108,21 @@ export class NvmeofInitiatorsFormComponent implements OnInit { }); } - onSubmit(payload: SubsystemPayload) { + onSubmit(payload: InitiatorsFormPayload) { this.isSubmitLoading = true; const taskUrl = `nvmeof/initiator/add`; + const hostKeyList = payload.hostDchapKeyList || []; + const addedHosts = payload.addedHosts || []; + const hosts = + payload.hostType === HOST_TYPE.SPECIFIC + ? hostKeyList.length + ? hostKeyList + : addedHosts.map((host_nqn: string) => ({ host_nqn, dhchap_key: '' })) + : []; const request: SubsystemInitiatorRequest = { allow_all: payload.hostType === HOST_TYPE.ALL, - hosts: payload.hostType === HOST_TYPE.SPECIFIC ? payload.hostDchapKeyList : [], + hosts, gw_group: this.group }; this.taskWrapperService 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 ab2f0d7bfef..e4f4bb14fb2 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 @@ -151,7 +151,8 @@ export class NvmeofInitiatorsListComponent implements OnInit { listInitiators() { this.nvmeofService .getInitiators(this.subsystemNQN, this.group) - .subscribe((initiators: NvmeofSubsystemInitiator[]) => { + .subscribe((response: NvmeofSubsystemInitiator[] | { hosts: NvmeofSubsystemInitiator[] }) => { + const initiators = Array.isArray(response) ? response : response?.hosts || []; this.initiators = initiators; this.updateAuthStatus(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html index 85008935660..54487761ac7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html @@ -62,7 +62,11 @@ helperText="A secret key for the subsystem to authenticate itself to hosts." i18n-helperText [invalid]="subDK.isInvalid" - [invalidText]="INVALID_TEXTS['required']"> + [invalidText]=" + formGroup.get('subsystemDchapKey')?.errors?.invalidBase64 + ? INVALID_TEXTS['invalidBase64'] + : INVALID_TEXTS['required'] + "> Subsystem DH-HMAC-CHAP key + [invalidText]=" + hostDhchapKeyCtrl(i)?.errors?.invalidBase64 + ? INVALID_TEXTS['invalidBase64'] + : INVALID_TEXTS['required'] + "> DHCHAP Key | {{hostDchapKeyItem.get('host_nqn')?.value }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts index 96316d72618..1811c6c4445 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts @@ -62,6 +62,34 @@ describe('NvmeofSubsystemsStepThreeComponent', () => { expect(form.get('authType')?.value).toBe(AUTHENTICATION.Unidirectional); expect(form.get('subsystemDchapKey')?.value).toBe(null); }); + + it('should keep host key optional in unidirectional mode', () => { + const hostKeyCtrl = (form.get('hostDchapKeyList') as any).at(0).get('dhchap_key'); + hostKeyCtrl.setValue(''); + hostKeyCtrl.markAsTouched(); + hostKeyCtrl.updateValueAndValidity(); + + expect(hostKeyCtrl.hasError('required')).toBeFalsy(); + }); + + it('should require host key in bidirectional mode', () => { + form.get('authType')?.setValue(AUTHENTICATION.Bidirectional); + const hostKeyCtrl = (form.get('hostDchapKeyList') as any).at(0).get('dhchap_key'); + hostKeyCtrl.setValue(''); + hostKeyCtrl.markAsTouched(); + hostKeyCtrl.updateValueAndValidity(); + + expect(hostKeyCtrl.hasError('required')).toBeTruthy(); + }); + + it('should validate host key base64 format when provided', () => { + const hostKeyCtrl = (form.get('hostDchapKeyList') as any).at(0).get('dhchap_key'); + hostKeyCtrl.setValue('not-valid-key'); + hostKeyCtrl.markAsTouched(); + hostKeyCtrl.updateValueAndValidity(); + + expect(hostKeyCtrl.hasError('invalidBase64')).toBeTruthy(); + }); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts index ea83d7d5e4a..dc158ac8b08 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { FormArray, UntypedFormControl, Validators } from '@angular/forms'; +import { FormArray, UntypedFormControl } from '@angular/forms'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; @@ -26,7 +26,8 @@ export class NvmeofSubsystemsStepThreeComponent implements OnInit, TearsheetStep action: string; pageURL: string; INVALID_TEXTS = { - required: $localize`This field is required` + required: $localize`This field is required`, + invalidBase64: $localize`Invalid key format. Use Base64 or DHHC-1:XX:base64:` }; AUTHENTICATION = AUTHENTICATION; @@ -58,6 +59,7 @@ export class NvmeofSubsystemsStepThreeComponent implements OnInit, TearsheetStep this.formGroup = new CdFormGroup({ authType: new UntypedFormControl(AUTHENTICATION.Unidirectional), subsystemDchapKey: new UntypedFormControl(null, [ + CdValidators.base64(), CdValidators.requiredIf({ authType: AUTHENTICATION.Bidirectional }) @@ -66,17 +68,33 @@ export class NvmeofSubsystemsStepThreeComponent implements OnInit, TearsheetStep }); this.syncHostList(); + this.formGroup.get('authType')?.valueChanges.subscribe(() => { + this.refreshHostKeyValidation(); + }); } private createHostDhchapKeyFormGroup(hostNQN: string = '', key: string | null = null) { return new CdFormGroup({ dhchap_key: new UntypedFormControl(key, { - validators: [Validators.required] + validators: [ + CdValidators.base64(), + CdValidators.custom( + 'required', + (value: string) => + this.formGroup?.get('authType')?.value === AUTHENTICATION.Bidirectional && !value + ) + ] }), host_nqn: new UntypedFormControl(hostNQN) }); } + private refreshHostKeyValidation() { + this.hostDchapKeyList.controls.forEach((control) => { + control.get('dhchap_key')?.updateValueAndValidity({ emitEvent: false }); + }); + } + ngOnInit() { this.createForm(); }