]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Initiator add shows success but host is not added/displayed in Subsyst... 67713/head
authorSagar Gopale <sagar.gopale@ibm.com>
Mon, 9 Mar 2026 10:42:50 +0000 (16:12 +0530)
committerSagar Gopale <sagar.gopale@ibm.com>
Wed, 11 Mar 2026 08:51:56 +0000 (14:21 +0530)
Fixes: https://tracker.ceph.com/issues/75402
Signed-off-by: Sagar Gopale <sagar.gopale@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts

index a6183a8acf08e15df27ce476f89307d41f735a93..4b2df6992c5a1a90b4f65d788a1aa91f1faa933c 100644 (file)
@@ -3,6 +3,7 @@
   [title]="title"
   [description]="description"
   (submitRequested)="onSubmit($event)"
+  (stepChanged)="onStepChanged()"
   [isSubmitLoading]="isSubmitLoading"
   submitButtonLabel="Add"
   i18n-submitButtonLabel>
       [group]="group"
       [existingHosts]="existingHosts"></cd-nvmeof-subsystem-step-two>
   </cd-tearsheet-step>
+  @if(showAuthStep) {
+  <cd-tearsheet-step>
+    <cd-nvmeof-subsystem-step-three
+      #tearsheetStep
+      [stepTwoValue]="stepTwoValue"
+      [group]="group"></cd-nvmeof-subsystem-step-three>
+  </cd-tearsheet-step>
+  }
 </cd-tearsheet>
index 7d50e1a8c56e9891ddfcf00fb8c398ce32fdece0..4ef4f03929b38ded2b12dd80cbd4a8252941989a 100644 (file)
@@ -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' }]
+      });
+    });
   });
 });
index d1c0d1fcb121f72baf983f396bbb5712ea2a3839..cbb5513c6ecdd0c07a49593fb47d593279630d83 100644 (file)
@@ -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<HostStepType, 'hostType' | 'addedHosts'> &
+  Partial<Pick<AuthStepType, 'hostDchapKeyList'>>;
+
+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<HostStepType>(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
index ab2f0d7bfefcfe95f8b23a53c88bbd495e841055..e4f4bb14fb24e93e72b7a6449e63792a4671b850 100644 (file)
@@ -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();
       });
index 85008935660cc7f2fb1e10291a193b5eaa1c9e20..54487761ac7cf6ffdf06e1664049e9a7057a1880 100644 (file)
           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
           <input cdsPassword
                  cdValidate
           <cds-text-label
             class="cds-mb-3"
             [invalid]="hostKey.isInvalid"
-            [invalidText]="INVALID_TEXTS['required']">
+            [invalidText]="
+              hostDhchapKeyCtrl(i)?.errors?.invalidBase64
+                ? INVALID_TEXTS['invalidBase64']
+                : INVALID_TEXTS['required']
+            ">
             <span
               class="cds-mb-3"
               i18n>DHCHAP Key | {{hostDchapKeyItem.get('host_nqn')?.value }}</span>
index 96316d726189809351ac67e4f763fb235f65b6a0..1811c6c44456fad56d6df56be363dbf1d3e7fb97 100644 (file)
@@ -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();
+      });
     });
   });
 });
index ea83d7d5e4a949f469aed8f6c93d71b669462d5b..dc158ac8b087f1df6202d61860a198db0ec9df63 100644 (file)
@@ -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();
   }