describe('should test form', () => {
beforeEach(() => {
nvmeofService = TestBed.inject(NvmeofService);
- spyOn(nvmeofService, 'addInitiators').and.stub();
+ spyOn(nvmeofService, 'addSubsystemInitiators').and.stub();
});
it('should be creating request correctly', () => {
const payload: any = {
hostType: HOST_TYPE.SPECIFIC,
- addedHosts: ['host1']
+ hostDchapKeyList: [{ dhchap_key: '', host_nqn: 'host1' }],
+ gw_group: 'test-group'
};
component.onSubmit(payload);
- expect(nvmeofService.addInitiators).toHaveBeenCalledWith(subsystemNQN, {
- host_nqn: 'host1',
- gw_group: 'test-group'
+ expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith(subsystemNQN, {
+ allow_all: false,
+ gw_group: 'test-group',
+ hosts: [{ dhchap_key: '', host_nqn: 'host1' }]
});
});
});
import { Component, OnInit } from '@angular/core';
import { Step } from 'carbon-components-angular';
-import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service';
+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 { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
this.isSubmitLoading = true;
const taskUrl = `nvmeof/initiator/add`;
- const request: InitiatorRequest = {
- host_nqn: payload.hostType === HOST_TYPE.ALL ? '*' : payload.addedHosts.join(','),
+ const request: SubsystemInitiatorRequest = {
+ allow_all: payload.hostType === HOST_TYPE.ALL,
+ hosts: payload.hostType === HOST_TYPE.SPECIFIC ? payload.hostDchapKeyList : [],
gw_group: this.group
};
this.taskWrapperService
task: new FinishedTask(taskUrl, {
nqn: this.subsystemNQN
}),
- call: this.nvmeofService.addInitiators(this.subsystemNQN, request)
+ call: this.nvmeofService.addSubsystemInitiators(this.subsystemNQN, request)
})
.subscribe({
error: () => {
const mockInitiators = [
{
nqn: '*',
- use_dhchap: ''
+ use_dhchap: false
}
];
}));
it('should update authStatus when initiator has dhchap_key', fakeAsync(() => {
- const initiatorsWithKey = [{ nqn: 'nqn1', use_dhchap: 'key1' }];
+ const initiatorsWithKey = [{ nqn: 'nqn1', use_dhchap: true }];
spyOn(TestBed.inject(NvmeofService), 'getInitiators').and.returnValue(of(initiatorsWithKey));
component.listInitiators();
tick();
expect(component.authStatus).toBe('Unidirectional');
}));
- it('should update authStatus when subsystem has psk', fakeAsync(() => {
- const subsystemWithPsk = { ...mockSubsystem, has_dhchap_key: true };
- component.initiators = [{ nqn: 'nqn1', use_dhchap: 'key1' }];
- spyOn(TestBed.inject(NvmeofService), 'getSubsystem').and.returnValue(of(subsystemWithPsk));
+ it('should update authStatus when subsystem has dhchap_key', fakeAsync(() => {
+ const initiatorsWithKey = [{ nqn: 'nqn1', use_dhchap: true }];
+ component.initiators = initiatorsWithKey;
+ const subsystemWithKey = { ...mockSubsystem, has_dhchap_key: true };
+ spyOn(TestBed.inject(NvmeofService), 'getSubsystem').and.returnValue(of(subsystemWithKey));
component.getSubsystem();
tick();
expect(component.authStatus).toBe('Bi-directional');
helperText="A secret key for the subsystem to authenticate itself to hosts."
i18n-helperText
[invalid]="subDK.isInvalid"
- [invalidText]="subsystemDchapKeyInvalidTemplate">
+ [invalidText]="INVALID_TEXTS['required']">
Subsystem DH-HMAC-CHAP key
<input cdsPassword
cdValidate
@for (hostDchapKeyItem of hostDchapKeyList.controls; track hostDchapKeyItem.get('host_nqn')?.value; let i = $index) {
<div [formGroupName]="i">
<cds-text-label
- class="cds-mb-3">
+ class="cds-mb-3"
+ [invalid]="hostKey.isInvalid"
+ [invalidText]="INVALID_TEXTS['required']">
<span
class="cds-mb-3"
i18n>DHCHAP Key | {{hostDchapKeyItem.get('host_nqn')?.value }}</span>
<input cdsPassword
+ cdValidate
+ #hostKey="cdValidate"
formControlName="dhchap_key"
type="password"
placeholder="Enter DHCHAP key"
class="step-3-form-item"
i18n-placeholder
- autocomplete>
+ autocomplete
+ [invalid]="hostKey.isInvalid">
</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>
import { Component, Input, OnInit } from '@angular/core';
-import { FormArray, UntypedFormControl } from '@angular/forms';
+import { FormArray, UntypedFormControl, Validators } from '@angular/forms';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
-import { AUTHENTICATION, StepTwoType } from '~/app/shared/models/nvmeof';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { AUTHENTICATION, HostStepType } from '~/app/shared/models/nvmeof';
import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
@Component({
})
export class NvmeofSubsystemsStepThreeComponent implements OnInit, TearsheetStep {
@Input() group!: string;
- @Input() set stepTwoValue(value: StepTwoType | null) {
+ @Input() set stepTwoValue(value: HostStepType | null) {
this._addedHosts = value?.addedHosts ?? [];
if (this.formGroup) {
this.syncHostList();
private createForm() {
this.formGroup = new CdFormGroup({
authType: new UntypedFormControl(AUTHENTICATION.Unidirectional),
- subsystemDchapKey: new UntypedFormControl(null),
+ subsystemDchapKey: new UntypedFormControl(null, [
+ CdValidators.requiredIf({
+ authType: AUTHENTICATION.Bidirectional
+ })
+ ]),
hostDchapKeyList: new FormArray([])
});
private createHostDhchapKeyFormGroup(hostNQN: string = '', key: string | null = null) {
return new CdFormGroup({
- dhchap_key: new UntypedFormControl(key),
+ dhchap_key: new UntypedFormControl(key, {
+ validators: [Validators.required]
+ }),
host_nqn: new UntypedFormControl(hostNQN)
});
}
return this.formGroup.get('hostDchapKeyList') as FormArray;
}
+ hostDhchapKeyCtrl(i: number) {
+ return (this.hostDchapKeyList.at(i) as CdFormGroup).get('dhchap_key');
+ }
+
trackByIndex = (i: number) => i;
}
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
-import { AUTHENTICATION, HOST_TYPE } from '~/app/shared/models/nvmeof';
+import { AUTHENTICATION, HOST_TYPE, NO_AUTH } from '~/app/shared/models/nvmeof';
import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
@Component({
}
get authTypeLabel(): string {
+ if (this.authType === AUTHENTICATION.None) return NO_AUTH;
return this.authType === AUTHENTICATION.Bidirectional
? $localize`Bidirectional`
: $localize`Unidirectional`;
[description]="description"
[isSubmitLoading]="isSubmitLoading"
(submitRequested)="onSubmit($event)"
-<<<<<<< HEAD
- (stepChanged)="populateReviewData()"
-=======
- (stepChanged)="onStepChanged($event)"
->>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
- >
+ (stepChanged)="populateReviewData()">
<cd-tearsheet-step>
<cd-nvmeof-subsystem-step-one
#tearsheetStep
[stepTwoValue]="stepTwoValue"
[group]="group"></cd-nvmeof-subsystem-step-three>
</cd-tearsheet-step>
-<<<<<<< HEAD
+ }
<cd-tearsheet-step>
<cd-nvmeof-subsystem-step-four
#tearsheetStep
[subsystemDchapKey]="reviewSubsystemDchapKey"
[hostDchapKeyCount]="reviewHostDchapKeyCount"></cd-nvmeof-subsystem-step-four>
</cd-tearsheet-step>
-=======
- }
->>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
</cd-tearsheet>
beforeEach(() => {
nvmeofService = TestBed.inject(NvmeofService);
spyOn(nvmeofService, 'createSubsystem').and.returnValue(of({}));
- spyOn(nvmeofService, 'addInitiators').and.returnValue(of({}));
+ spyOn(nvmeofService, 'addSubsystemInitiators').and.returnValue(of({}));
});
it('should be creating request correctly', () => {
addedHosts: [],
hostType: HOST_TYPE.ALL,
subsystemDchapKey: 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=',
-<<<<<<< HEAD
- listeners: []
-=======
+ listeners: [],
authType: AUTHENTICATION.Bidirectional,
hostDchapKeyList: []
->>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
};
component.group = mockGroupName;
component.onSubmit(payload);
-<<<<<<< HEAD
- expect(nvmeofService.addInitiators).toHaveBeenCalledWith('test-nqn.default', {
- host_nqn: '*',
-=======
expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith('test-nqn.default', {
allow_all: true,
hosts: [],
->>>>>>> 6d877ea7101 (mgr/dashboard: Allow adding authentication to subsystem flow)
gw_group: mockGroupName
});
});
import { Step } from 'carbon-components-angular';
import { NvmeofService, SubsystemInitiatorRequest } from '~/app/shared/api/nvmeof.service';
import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.component';
-import { HOST_TYPE, ListenerItem, AUTHENTICATION } from '~/app/shared/models/nvmeof';
-import { AUTHENTICATION, HOST_TYPE, StepTwoType } from '~/app/shared/models/nvmeof';
+import {
+ AUTHENTICATION,
+ HOST_TYPE,
+ HostStepType,
+ ListenerItem,
+ AuthStepType,
+ DetailsStepType
+} from '~/app/shared/models/nvmeof';
import { from, Observable, of } from 'rxjs';
import { NotificationService } from '~/app/shared/services/notification.service';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
type StepResult = { step: string; success: boolean; error?: string };
+const STEP_LABELS = {
+ DETAILS: 'Subsystem details',
+ HOSTS: 'Host access control',
+ AUTH: 'Authentication',
+ REVIEW: 'Review'
+} as const;
+
@Component({
selector: 'cd-nvmeof-subsystems-form',
templateUrl: './nvmeof-subsystems-form.component.html',
description: string = $localize`Subsytems define how hosts connect to NVMe namespaces and ensure secure access to storage.`;
isSubmitLoading: boolean = false;
private lastCreatedNqn: string;
- stepTwoValue: StepTwoType = null;
+ stepTwoValue: HostStepType = null;
showAuthStep = true;
@ViewChild(TearsheetComponent) tearsheet!: TearsheetComponent;
this.rebuildSteps();
}
+ private setAuthStepVisibility(nextShowAuth: boolean) {
+ if (this.showAuthStep === nextShowAuth) return;
+ this.showAuthStep = nextShowAuth;
+ this.rebuildSteps();
+ }
+
populateReviewData() {
- if (!this.tearsheet?.stepContents) return;
- const steps = this.tearsheet.stepContents.toArray();
-
- // Step 1: Subsystem details
- const step1Form = steps[0]?.stepComponent?.formGroup;
- if (step1Form) {
- this.reviewNqn = step1Form.get('nqn')?.value || '';
- this.reviewListeners = step1Form.get('listeners')?.value || [];
+ if (!this.tearsheet) return;
+
+ const step1 = this.tearsheet.getStepValueByLabel<DetailsStepType>(STEP_LABELS.DETAILS);
+ const step2 = this.tearsheet.getStepValueByLabel<HostStepType>(STEP_LABELS.HOSTS);
+
+ if (step1) {
+ this.reviewNqn = step1.nqn ?? '';
+ this.reviewListeners = step1.listeners ?? [];
}
- // Step 2: Host access control
- const step2Form = steps[1]?.stepComponent?.formGroup;
- if (step2Form) {
- this.reviewHostType = step2Form.get('hostType')?.value || HOST_TYPE.SPECIFIC;
- this.reviewAddedHosts = step2Form.get('addedHosts')?.value || [];
+ if (step2) {
+ this.reviewHostType = step2.hostType ?? HOST_TYPE.SPECIFIC;
+ this.reviewAddedHosts = step2.addedHosts ?? [];
+ this.stepTwoValue = step2;
}
- // Step 3: Authentication
- const step3Form = steps[2]?.stepComponent?.formGroup;
- if (step3Form) {
- this.reviewAuthType = step3Form.get('authType')?.value || AUTHENTICATION.Unidirectional;
- this.reviewSubsystemDchapKey = step3Form.get('subsystemDchapKey')?.value || '';
- const hostKeys = step3Form.get('hostDchapKeyList')?.value || [];
- this.reviewHostDchapKeyCount = hostKeys.filter((k: any) => k?.key).length;
-
- } }
- onStepChanged(_e: { current: number }) {
- const stepTwo = this.tearsheet?.getStepValue(1);
- this.stepTwoValue = stepTwo;
-
- this.showAuthStep = stepTwo?.hostType !== HOST_TYPE.ALL;
+ const nextShowAuth = (step2?.hostType ?? HOST_TYPE.SPECIFIC) === HOST_TYPE.SPECIFIC;
- this.rebuildSteps();
+ if (nextShowAuth !== this.showAuthStep) {
+ this.setAuthStepVisibility(nextShowAuth);
+ return;
+ }
+
+ const authStep = this.tearsheet.getStepValueByLabel<AuthStepType>(STEP_LABELS.AUTH);
+
+ if (this.showAuthStep && authStep) {
+ this.reviewAuthType = authStep.authType ?? AUTHENTICATION.Unidirectional;
+ this.reviewSubsystemDchapKey = authStep.subsystemDchapKey ?? '';
+ const hostKeyList = authStep.hostDchapKeyList ?? [];
+ this.reviewHostDchapKeyCount = hostKeyList.filter((item) => !!item?.dhchap_key)?.length;
+ } else {
+ this.reviewAuthType = null;
+ this.reviewSubsystemDchapKey = '';
+ this.reviewHostDchapKeyCount = 0;
+ }
}
rebuildSteps() {
const steps: Step[] = [
- { label: 'Subsystem details', invalid: false },
- { label: 'Host access control', invalid: false }
+ { label: STEP_LABELS.DETAILS, invalid: false },
+ { label: STEP_LABELS.HOSTS, invalid: false }
];
if (this.showAuthStep) {
- steps.push({ label: 'Authentication', invalid: false });
+ steps.push({ label: STEP_LABELS.AUTH, invalid: false });
}
- steps.push({ label: 'Review', invalid: false });
+ steps.push({ label: STEP_LABELS.REVIEW, invalid: false });
this.steps = steps;
sequentialSteps.push({
step: this.steps[1].label,
call: () =>
- this.nvmeofService.addInitiators(`${payload.nqn}.${this.group}`, initiatorRequest)
+ this.nvmeofService.addSubsystemInitiators(
+ `${payload.nqn}.${this.group}`,
+ initiatorRequest
+ )
});
this.runSequentialSteps(sequentialSteps, stepResults).subscribe({
return wrapper?.stepComponent?.formGroup?.value ?? null;
}
+ getStepIndexByLabel(label: string): number {
+ return this.steps?.findIndex((s) => s.label === label) ?? -1;
+ }
+
+ getStepValueByLabel<T = any>(label: string): T | null {
+ const idx = this.getStepIndexByLabel(label);
+ if (idx < 0) return null;
+ return this.getStepValue<T>(idx);
+ }
+
currentStep: number = 0;
lastStep: number = null;
isOpen: boolean = true;
}
handleSubmit() {
- if (this.steps[this.currentStep].invalid) return;
+ if (this.steps.some((step) => step?.invalid)) return;
const mergedPayloads = this.getMergedPayload();
export interface NvmeofSubsystemInitiator {
nqn: string;
- use_dhchap?: string;
+ use_dhchap?: boolean;
}
export interface NvmeofListener {
export enum AUTHENTICATION {
Unidirectional = 'unidirectional',
- Bidirectional = 'bidirectional'
+ Bidirectional = 'bidirectional',
+ None = 'none'
}
+export const NO_AUTH = 'No authentication';
+
export const HOST_TYPE = {
ALL: 'all',
SPECIFIC: 'specific'
_initiators: NvmeofSubsystemInitiator[] | { hosts?: NvmeofSubsystemInitiator[] }
): string {
// Import enum value strings to avoid circular dependency
- const NO_AUTH = 'No authentication';
const UNIDIRECTIONAL = 'Unidirectional';
const BIDIRECTIONAL = 'Bi-directional';
+ let auth = NO_AUTH;
+
let hostsList: NvmeofSubsystemInitiator[] = [];
if (_initiators && 'hosts' in _initiators && Array.isArray(_initiators.hosts)) {
hostsList = _initiators.hosts;
hostsList = _initiators as NvmeofSubsystemInitiator[];
}
- let auth = NO_AUTH;
-
const hostHasDhchapKey = hostsList.some((host) => !!host.use_dhchap);
if (hostHasDhchapKey) {
selected: boolean;
};
-export type StepTwoType = {
+export type HostStepType = {
addedHosts: Array<string>;
hostname: string;
hostType: string;
};
+
+export type AuthStepType = {
+ authType: AUTHENTICATION;
+ subsystemDchapKey: string;
+ hostDchapKeyList: Array<{
+ dhchap_key: string;
+ host_nqn: string;
+ }>;
+};
+
+export type DetailsStepType = {
+ nqn: string;
+ listeners: Array<string>;
+};