From 87665b7aa06271f9fbf9229abf1f5ed0e3063d1a Mon Sep 17 00:00:00 2001 From: Afreen Misbah Date: Fri, 30 Jan 2026 05:15:53 +0530 Subject: [PATCH] mgr/dashboard: Step three subsystem creation form - 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 --- .../src/app/ceph/block/block.module.ts | 10 +- .../nvmeof-subsystem-step-3.component.html | 112 ++++++++++++++++++ .../nvmeof-subsystem-step-3.component.scss | 18 +++ .../nvmeof-subsystem-step-3.component.spec.ts | 66 +++++++++++ .../nvmeof-subsystem-step-3.component.ts | 50 ++++++++ .../nvmeof-subsystems-form.component.html | 9 ++ .../nvmeof-subsystems-form.component.spec.ts | 17 ++- .../nvmeof-subsystems-form.component.ts | 12 +- .../src/app/shared/api/nvmeof.service.spec.ts | 3 +- .../src/app/shared/api/nvmeof.service.ts | 7 +- .../tearsheet/tearsheet.component.ts | 16 +-- .../frontend/src/app/shared/models/nvmeof.ts | 5 + 12 files changed, 305 insertions(+), 20 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.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 f09a5fbf1a1..77e8cafc230 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 @@ -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 index 00000000000..9cee05cd5c4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.html @@ -0,0 +1,112 @@ + +
+
+
+
+

Authentication

+

Configure authentication to verify the identity of connecting hosts and protect the subsystem from unauthorized access.

+
+
+ + +
+ Unidirectional + Each host can provide an optional DH-HMAC-CHAP key. The subsystem does not require its own key. +
+
+ +
+
+ Bidirectional + + Requires keys on both sides + +
+ Both subsystem and hosts must provide DH-HMAC-CHAP keys. All connections will be verified in both directions. +
+
+
+
+ @if(formGroup.get('authType').value === AUTHENTICATION.Bidirectional) { +
+
+

Subsystem authentication detail

+

Mandatory field.

+
+ + Subsystem DH-HMAC-CHAP key + + +
+ } +
+
+

Host authentication details

+

{{formGroup.get('authType').value === AUTHENTICATION.Bidirectional ? 'All fields are required.' : 'Optional fields.'}}

+
+ @for(hostDchapKeyItem of hostDchapKeyList.controls; track hostDchapKeyItem; let i = $index) { + + DHCHAP Key | {{hostNQN}} + + + } +
+
+
+
+ + +@for (err of formGroup.get('subsystemDchapKey').errors | keyvalue; track err.key) { +{{ INVALID_TEXTS[err.key] }} +} + 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 index 00000000000..1b140bda80a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.scss @@ -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 index 00000000000..1b004adeada --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.spec.ts @@ -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; + 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 index 00000000000..a1ecc509441 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-3/nvmeof-subsystem-step-3.component.ts @@ -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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html index 04e7e38bbf3..59b322d6862 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.html @@ -10,4 +10,13 @@ #tearsheetStep [group]="group"> + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts index 79cc234c4d4..b82df5cfdee 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts @@ -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=' }); }); }); 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 7f17d7da1c4..a85fb758611 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 @@ -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() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts index d9999feb156..46f38d08038 100755 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts @@ -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`); 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 cac534f4275..d74e7e0fb20 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 @@ -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' }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts index c2ac38a35cc..5ff65a2afe7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts @@ -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); }); }); } 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 43a24a4b5ce..7b3eb8d87de 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 @@ -59,3 +59,8 @@ export interface NvmeofGatewayGroup extends CephServiceSpec { subSystemCount: number; nodeCount: number; } + +export enum AUTHENTICATION { + Unidirectional = 'unidirectional', + Bidirectional = 'bidirectional' +} -- 2.47.3