]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Step three subsystem creation form 67170/head
authorAfreen Misbah <afreen@ibm.com>
Thu, 29 Jan 2026 23:45:53 +0000 (05:15 +0530)
committerAfreen Misbah <afreen@ibm.com>
Thu, 5 Feb 2026 02:39:02 +0000 (08:09 +0530)
- added step3 component
- can add subsystem dhchap key
- adding hosts keys is penidng due to step two PR

Fixes https://tracker.ceph.com/issues/74095

Signed-off-by: Afreen Misbah <afreen@ibm.com>
12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/nvmeof.ts

index f09a5fbf1a1248a338c6725aba2839063306c269..77e8cafc2307fe9c96670c618b6c12e469ed8cf4 100644 (file)
@@ -49,6 +49,7 @@ import { NvmeofInitiatorsListComponent } from './nvmeof-initiators-list/nvmeof-i
 import { NvmeofInitiatorsFormComponent } from './nvmeof-initiators-form/nvmeof-initiators-form.component';
 import { NvmeofGatewayGroupComponent } from './nvmeof-gateway-group/nvmeof-gateway-group.component';
 import { NvmeofSubsystemsStepOneComponent } from './nvmeof-subsystems-form/nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component';
+import { NvmeofSubsystemsStepThreeComponent } from './nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component';
 import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway-node.component';
 import { NvmeofGroupFormComponent } from './nvmeof-group-form /nvmeof-group-form.component';
 
@@ -68,7 +69,8 @@ import {
   UIShellModule,
   TreeviewModule,
   TabsModule,
-  TagModule
+  TagModule,
+  LayerModule
 } from 'carbon-components-angular';
 
 // Icons
@@ -107,7 +109,8 @@ import ProgressBarRound from '@carbon/icons/es/progress-bar--round/32';
     ComboBoxModule,
     TabsModule,
     TagModule,
-    GridModule
+    GridModule,
+    LayerModule
   ],
   declarations: [
     RbdListComponent,
@@ -147,7 +150,8 @@ import ProgressBarRound from '@carbon/icons/es/progress-bar--round/32';
     NvmeofInitiatorsFormComponent,
     NvmeofGatewayNodeComponent,
     NvmeofGroupFormComponent,
-    NvmeofSubsystemsStepOneComponent
+    NvmeofSubsystemsStepOneComponent,
+    NvmeofSubsystemsStepThreeComponent
   ],
   exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
 })
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
new file mode 100644 (file)
index 0000000..9cee05c
--- /dev/null
@@ -0,0 +1,112 @@
+
+<form [formGroup]="formGroup"
+      novalidate>
+  <div cdsGrid
+       [useCssGrid]="true"
+       [narrow]="true"
+       [fullWidth]="true">
+    <div cdsCol
+         [columnNumbers]="{sm: 10, md: 10, lg: 12}">
+      <div cdsRow
+           class="form-heading">
+        <h3 class="cds--type-heading-03"
+            i18n>Authentication</h3>
+        <p
+          class="cds--type-label-02"
+          i18n>Configure authentication to verify the identity of connecting hosts and protect the subsystem from unauthorized access.</p>
+      </div>
+      <div cdsRow
+           class="form-item">
+        <cds-radio-group
+          formControlName="authType"
+          legend="Authentication type"
+          i18n-legend>
+          <cds-radio
+            [value]="AUTHENTICATION.Unidirectional">
+            <div class="auth-radio">
+              <span>Unidirectional</span>
+              <span class="cds--form__helper-text"
+                    i18n>Each host can provide an optional DH-HMAC-CHAP key. The subsystem does not require its own key.</span>
+            </div>
+          </cds-radio>
+          <cds-radio
+            [value]="AUTHENTICATION.Bidirectional">
+            <div class="auth-radio">
+              <div cdsStack="horizontal">
+                <span i18n>Bidirectional</span>
+                <cds-tag
+                  type="blue"
+                  size="sm"
+                  i18n>
+                  Requires keys on both sides
+                </cds-tag>
+              </div>
+              <span class="cds--form__helper-text"
+                    i18n>Both subsystem and hosts must provide DH-HMAC-CHAP keys. All connections will be verified in both directions.</span>
+            </div>
+          </cds-radio>
+        </cds-radio-group>
+      </div>
+      @if(formGroup.get('authType').value === AUTHENTICATION.Bidirectional) {
+      <div cdsRow
+           class="form-item step-3-form-item">
+        <div class="step-3-heading">
+          <h1
+            class="cds--type-heading-compact-01"
+            i18n>Subsystem authentication detail</h1>
+          <p class="cds--type-label-01 text-helper"
+             i18n>Mandatory field.</p>
+        </div>
+        <cds-text-label
+          i18n
+          helperText="A secret key for the subsystem to authenticate itself to hosts."
+          i18n-helperText
+          [invalid]="subDK.isInvalid"
+          [invalidText]="subsystemDchapKeyInvalidTemplate">
+          Subsystem DH-HMAC-CHAP key
+          <input cdsPassword
+                 cdValidate
+                 #subDK="cdValidate"
+                 [invalid]="subDK.isInvalid"
+                 formControlName="subsystemDchapKey"
+                 type="password"
+                 class="step-3-form-item"
+                 placeholder="Enter subsystem DH-HMAC-CHAP key"
+                 i18n-placeholder
+                 autocomplete>
+        </cds-text-label>
+      </div>
+      }
+      <div cdsRow
+           class="form-item step-3-form-item">
+        <div class="step-3-heading">
+          <h1
+            class="cds--type-heading-compact-01"
+            i18n>Host authentication details</h1>
+          <p class="cds--type-label-01 text-helper"
+             i18n>{{formGroup.get('authType').value === AUTHENTICATION.Bidirectional ? 'All fields are required.' : 'Optional fields.'}}</p>
+        </div>
+        @for(hostDchapKeyItem of hostDchapKeyList.controls; track hostDchapKeyItem; let i = $index) {
+        <cds-text-label
+          [formGroupName]="i"
+          i18n>
+          DHCHAP Key | {{hostNQN}}
+          <input cdsPassword
+                 formControlName="key"
+                 type="password"
+                 placeholder="Enter DHCHAP key"
+                 class="step-3-form-item"
+                 i18n-placeholder
+                 autocomplete>
+        </cds-text-label>
+        }
+      </div>
+    </div>
+  </div>
+</form>
+
+<ng-template #subsystemDchapKeyInvalidTemplate>
+@for (err of formGroup.get('subsystemDchapKey').errors | keyvalue; track err.key) {
+<span class="invalid-feedback">{{ INVALID_TEXTS[err.key] }}</span>
+}
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss
new file mode 100644 (file)
index 0000000..1b140bd
--- /dev/null
@@ -0,0 +1,18 @@
+.auth-radio {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.text-helper {
+  color: var(--cds-text-helper);
+}
+
+.step-3-heading {
+  display: flex;
+  justify-content: space-between;
+}
+
+.step-3-form-item {
+  max-inline-size: 23rem;
+}
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
new file mode 100644 (file)
index 0000000..1b004ad
--- /dev/null
@@ -0,0 +1,66 @@
+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 { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SharedModule } from '~/app/shared/shared.module';
+import { NvmeofSubsystemsStepThreeComponent } from './nvmeof-subsystem-step-3.component';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { GridModule, RadioModule, TagModule } from 'carbon-components-angular';
+import { AUTHENTICATION } from '~/app/shared/models/nvmeof';
+
+describe('NvmeofSubsystemsStepThreeComponent', () => {
+  let component: NvmeofSubsystemsStepThreeComponent;
+  let fixture: ComponentFixture<NvmeofSubsystemsStepThreeComponent>;
+  let nvmeofService: NvmeofService;
+  let form: CdFormGroup;
+  const mockGroupName = 'default';
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [NvmeofSubsystemsStepThreeComponent],
+      providers: [NgbActiveModal],
+      imports: [
+        HttpClientTestingModule,
+        NgbTypeaheadModule,
+        ReactiveFormsModule,
+        RouterTestingModule,
+        SharedModule,
+        GridModule,
+        RadioModule,
+        TagModule,
+        ToastrModule.forRoot()
+      ]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(NvmeofSubsystemsStepThreeComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+    form = component.formGroup;
+    component.group = mockGroupName;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  describe('should test form', () => {
+    beforeEach(() => {
+      nvmeofService = TestBed.inject(NvmeofService);
+      spyOn(nvmeofService, 'createSubsystem').and.stub();
+    });
+
+    describe('form initialization', () => {
+      it('should initialize form with default values', () => {
+        expect(form).toBeTruthy();
+        expect(form.get('authType')?.value).toBe(AUTHENTICATION.Unidirectional);
+        expect(form.get('subsystemDchapKey')?.value).toBe(null);
+      });
+    });
+  });
+});
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
new file mode 100644 (file)
index 0000000..a1ecc50
--- /dev/null
@@ -0,0 +1,50 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { FormArray, UntypedFormControl } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { AUTHENTICATION } from '~/app/shared/models/nvmeof';
+import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
+
+@Component({
+  selector: 'cd-nvmeof-subsystem-step-three',
+  templateUrl: './nvmeof-subsystem-step-3.component.html',
+  styleUrls: ['./nvmeof-subsystem-step-3.component.scss'],
+  standalone: false
+})
+export class NvmeofSubsystemsStepThreeComponent implements OnInit, TearsheetStep {
+  @Input() group!: string;
+  formGroup: CdFormGroup;
+  action: string;
+  pageURL: string;
+  INVALID_TEXTS = {
+    required: $localize`This field is required`
+  };
+  AUTHENTICATION = AUTHENTICATION;
+
+  constructor(public actionLabels: ActionLabelsI18n, public activeModal: NgbActiveModal) {}
+
+  ngOnInit() {
+    this.createForm();
+  }
+
+  createForm() {
+    this.formGroup = new CdFormGroup({
+      authType: new UntypedFormControl(AUTHENTICATION.Unidirectional),
+      subsystemDchapKey: new UntypedFormControl(null),
+      hostDchapKeyList: new FormArray([this.createHostDchapKeyItem()])
+    });
+  }
+
+  createHostDchapKeyItem() {
+    return new CdFormGroup({
+      key: new UntypedFormControl(null),
+      hostNQN: new UntypedFormControl('')
+    });
+  }
+
+  get hostDchapKeyList() {
+    return this.formGroup.get('hostDchapKeyList') as FormArray;
+  }
+}
index 04e7e38bbf36982ffd1490dd7050ab225dd08078..59b322d6862492234b1624cf87be417f265d6b47 100644 (file)
      #tearsheetStep
      [group]="group"></cd-nvmeof-subsystem-step-one>
   </cd-tearsheet-step>
+  <cd-tearsheet-step>
+    <ng-template
+     #tearsheetStep></ng-template>
+  </cd-tearsheet-step>
+  <cd-tearsheet-step>
+    <cd-nvmeof-subsystem-step-three
+     #tearsheetStep
+     [group]="group"></cd-nvmeof-subsystem-step-three>
+  </cd-tearsheet-step>
 </cd-tearsheet>
index 79cc234c4d4f9d505a24c867e0fe2958c1871ec2..b82df5cfdee4d669dd4e1236591ef60a22b48d8d 100644 (file)
@@ -14,7 +14,8 @@ import {
 } from './nvmeof-subsystems-form.component';
 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
 import { NvmeofSubsystemsStepOneComponent } from './nvmeof-subsystem-step-1/nvmeof-subsystem-step-1.component';
-import { GridModule, InputModule } from 'carbon-components-angular';
+import { GridModule, InputModule, RadioModule, TagModule } from 'carbon-components-angular';
+import { NvmeofSubsystemsStepThreeComponent } from './nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component';
 
 describe('NvmeofSubsystemsFormComponent', () => {
   let component: NvmeofSubsystemsFormComponent;
@@ -24,13 +25,18 @@ describe('NvmeofSubsystemsFormComponent', () => {
   const mockGroupName = 'default';
   const mockPayload: SubsystemPayload = {
     nqn: '',
-    gw_group: mockGroupName
+    gw_group: mockGroupName,
+    subsystemDchapKey: 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY='
   };
 
   beforeEach(async () => {
     spyOn(Date, 'now').and.returnValue(mockTimestamp);
     await TestBed.configureTestingModule({
-      declarations: [NvmeofSubsystemsFormComponent, NvmeofSubsystemsStepOneComponent],
+      declarations: [
+        NvmeofSubsystemsFormComponent,
+        NvmeofSubsystemsStepOneComponent,
+        NvmeofSubsystemsStepThreeComponent
+      ],
       providers: [NgbActiveModal],
       imports: [
         HttpClientTestingModule,
@@ -40,6 +46,8 @@ describe('NvmeofSubsystemsFormComponent', () => {
         SharedModule,
         InputModule,
         GridModule,
+        RadioModule,
+        TagModule,
         ToastrModule.forRoot()
       ]
     }).compileComponents();
@@ -68,7 +76,8 @@ describe('NvmeofSubsystemsFormComponent', () => {
       expect(nvmeofService.createSubsystem).toHaveBeenCalledWith({
         nqn: expectedNqn,
         gw_group: mockGroupName,
-        enable_ha: true
+        enable_ha: true,
+        dhchap_key: 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY='
       });
     });
   });
index 7f17d7da1c4e5d4f74260be349578c80b0b1a967..a85fb75861177a617f2415ac0642ae2bf70772ff 100644 (file)
@@ -14,6 +14,7 @@ import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.
 export type SubsystemPayload = {
   nqn: string;
   gw_group: string;
+  subsystemDchapKey: string;
 };
 
 @Component({
@@ -34,11 +35,11 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
     },
     {
       label: $localize`Host access control`,
-      complete: false
+      invalid: false
     },
     {
       label: $localize`Authentication`,
-      complete: false
+      invalid: false
     },
     {
       label: $localize`Advanced options`,
@@ -78,7 +79,12 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
         task: new FinishedTask(taskUrl, {
           nqn: payload.nqn
         }),
-        call: this.nvmeofService.createSubsystem({ ...payload, enable_ha: true })
+        call: this.nvmeofService.createSubsystem({
+          nqn: payload.nqn,
+          gw_group: this.group,
+          dhchap_key: payload.subsystemDchapKey,
+          enable_ha: true
+        })
       })
       .subscribe({
         error() {
index d9999feb156a08cec9764b73b258171e33035320..46f38d08038d454a63a86bb258462ccb28da2db4 100755 (executable)
@@ -115,7 +115,8 @@ describe('NvmeofService', () => {
         nqn: mockNQN,
         enable_ha: true,
         initiators: '*',
-        gw_group: mockGroupName
+        gw_group: mockGroupName,
+        dhchap_key: null
       };
       service.createSubsystem(request).subscribe();
       const req = httpTesting.expectOne(`${API_PATH}/subsystem`);
index cac534f427544de41cc3f2c60abda8f32eed5061..d74e7e0fb202f9a73a73bd58f15c6445fbf50fe9 100644 (file)
@@ -91,7 +91,12 @@ export class NvmeofService {
     return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}?gw_group=${group}`);
   }
 
-  createSubsystem(request: { nqn: string; enable_ha: boolean; gw_group: string }) {
+  createSubsystem(request: {
+    nqn: string;
+    enable_ha: boolean;
+    gw_group: string;
+    dhchap_key: string;
+  }) {
     return this.http.post(`${API_PATH}/subsystem`, request, { observe: 'response' });
   }
 
index c2ac38a35ccca324cab64aab2677c0d86898f0a6..5ff65a2afe7a8e89d60d288c031ef900b91ca9d8 100644 (file)
@@ -1,5 +1,4 @@
 import {
-  ChangeDetectorRef,
   Component,
   ContentChildren,
   EventEmitter,
@@ -80,7 +79,6 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
 
   constructor(
     protected formBuilder: FormBuilder,
-    private changeDetectorRef: ChangeDetectorRef,
     private cdsModalService: ModalCdsService,
     private route: ActivatedRoute,
     private location: Location,
@@ -92,6 +90,10 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
     this.hasModalOutlet = this.route.outlet === 'modal';
   }
 
+  private _updateStepInvalid(index: number, invalid: boolean) {
+    this.steps = this.steps.map((step, i) => (i === index ? { ...step, invalid } : step));
+  }
+
   onStepSelect(event: { step: Step; index: number }) {
     this.currentStep = event.index;
   }
@@ -157,19 +159,17 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
   }
 
   ngAfterViewInit() {
-    // Checking Step validity, subscries ot all steps
-    if (!this.stepContents || !this.steps) return;
+    if (!this.stepContents?.length) return;
 
     this.stepContents.forEach((wrapper, index) => {
       const form = wrapper.stepComponent?.formGroup;
       if (!form) return;
 
-      // Initialize invalid flag if missing
-      this.steps[index].invalid = form.invalid;
+      // initial state
+      this._updateStepInvalid(index, form.invalid);
 
       form.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
-        this.steps[index].invalid = form.invalid;
-        this.changeDetectorRef.markForCheck();
+        this._updateStepInvalid(index, form.invalid);
       });
     });
   }
index 43a24a4b5ce2d0b6ec9bcd496323648505c3f4c5..7b3eb8d87defedafd24aa318dc6e1a19ecb1d0c7 100644 (file)
@@ -59,3 +59,8 @@ export interface NvmeofGatewayGroup extends CephServiceSpec {
   subSystemCount: number;
   nodeCount: number;
 }
+
+export enum AUTHENTICATION {
+  Unidirectional = 'unidirectional',
+  Bidirectional = 'bidirectional'
+}