</cds-text-label>
</div>
}
+ <div cdsRow
+ class="form-item">
+ <fieldset class="cds--fieldset">
+ <legend class="cds--label"
+ i18n>Listeners</legend>
+ <p class="cds--form__helper-text" i18n>Determine where and how hosts can connect to the subsystem over the network.</p>
+ <cds-radio-group
+ formControlName="listenerMode"
+ orientation="horizontal">
+ <cds-radio
+ [value]="LISTENER_MODE.AUTO_FETCH">
+ <span i18n>Auto-fetch</span>
+ </cds-radio>
+ <cds-radio
+ [value]="LISTENER_MODE.MANUAL">
+ <span i18n>Add manually</span>
+ </cds-radio>
+ </cds-radio-group>
+ </fieldset>
+ </div>
+ @if (listenerMode === LISTENER_MODE.AUTO_FETCH) {
+ <div cdsRow
+ class="form-item">
+ <cds-text-label
+ i18n
+ [invalid]="subnetMaskRef.isInvalid"
+ [invalidText]="subnetMaskInvalidText"
+ helperText="Listeners from this subnet-masks will be use."
+ i18n-helperText>
+ Subnet-mask
+ <input cdsText
+ cdValidate
+ #subnetMaskRef="cdValidate"
+ type="text"
+ placeholder="e.g. 255.0.0.0"
+ i18n-placeholder
+ id="subnetMask"
+ name="subnetMask"
+ formControlName="subnetMask"
+ [invalid]="subnetMaskRef.isInvalid">
+ </cds-text-label>
+ <ng-template #subnetMaskInvalidText>
+ <span i18n>This field is required.</span>
+ </ng-template>
+ </div>
+ }
+ @if (listenerMode === LISTENER_MODE.MANUAL) {
<div cdsRow
class="form-item">
<cds-combo-box i18n
- [invalid]="formGroup.get('listeners').invalid && (formGroup.get('listeners').dirty || formGroup.get('listeners').touched)"
+ cdValidate
+ #listenersRef="cdValidate"
+ [invalid]="listenersRef.isInvalid"
[invalidText]="listenersInvalidText"
[label]="listenersLabel"
[helperText]="listenersHelperText"
</div>
}
<ng-template #listenersLabel>
- <span i18n>Listeners</span>
+ <span i18n>Select listeners</span>
</ng-template>
<ng-template #listenersHelperText>
<span i18n>Select listeners for this subsystem.</span>
<span i18n>This field is required.</span>
</ng-template>
</div>
+ }
</div>
</div>
</form>
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { NvmeofSubsystemsStepOneComponent } from './nvmeof-subsystem-step-1.component';
import { FormHelper } from '~/testing/unit-test-helper';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
-import { ComboBoxModule, GridModule, InputModule } from 'carbon-components-angular';
+import { ComboBoxModule, GridModule, InputModule, RadioModule } from 'carbon-components-angular';
import { of } from 'rxjs';
NgbTypeaheadModule,
InputModule,
GridModule,
- ComboBoxModule
+ ComboBoxModule,
+ RadioModule
],
- providers: [NgbActiveModal]
+ providers: [NgbActiveModal],
+ schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
};
hosts: ListenerItem[] = [];
+ LISTENER_MODE = {
+ AUTO_FETCH: 'auto-fetch',
+ MANUAL: 'manual'
+ };
+ listenerMode: string = this.LISTENER_MODE.AUTO_FETCH;
constructor(
public actionLabels: ActionLabelsI18n,
}
createForm() {
+ const subnetMaskValidators = [
+ CdValidators.composeIf({ listenerMode: this.LISTENER_MODE.AUTO_FETCH }, [Validators.required])
+ ];
+ const listenersValidators = [
+ CdValidators.composeIf({ listenerMode: this.LISTENER_MODE.MANUAL }, [Validators.required])
+ ];
+
if (this.listenersOnly) {
this.formGroup = new CdFormGroup({
- listeners: new UntypedFormControl([])
+ listenerMode: new UntypedFormControl(this.LISTENER_MODE.AUTO_FETCH),
+ subnetMask: new UntypedFormControl('', subnetMaskValidators),
+ listeners: new UntypedFormControl([], listenersValidators)
});
} else {
this.formGroup = new CdFormGroup({
)
]
}),
- listeners: new UntypedFormControl([])
+ listenerMode: new UntypedFormControl(this.LISTENER_MODE.AUTO_FETCH),
+ subnetMask: new UntypedFormControl('', subnetMaskValidators),
+ listeners: new UntypedFormControl([], listenersValidators)
});
}
+
+ this.formGroup.get('listenerMode').valueChanges.subscribe((mode: string) => {
+ this.listenerMode = mode;
+ });
}
removeListener(index: number) {
nvmeofService = TestBed.inject(NvmeofService);
spyOn(nvmeofService, 'createSubsystem').and.returnValue(of({}));
spyOn(nvmeofService, 'addSubsystemInitiators').and.returnValue(of({}));
+ spyOn(nvmeofService, 'createListeners').and.returnValue(of({}));
});
it('should be creating request correctly', () => {
});
});
+ it('should include network_mask in createSubsystem request when listenerMode is auto-fetch and subnetMask is set', () => {
+ const payload: SubsystemPayload = {
+ nqn: 'test-nqn',
+ gw_group: mockGroupName,
+ addedHosts: [],
+ hostType: HOST_TYPE.ALL,
+ subsystemDchapKey: '',
+ listeners: [],
+ authType: AUTHENTICATION.Unidirectional,
+ hostDchapKeyList: [],
+ listenerMode: 'auto-fetch',
+ subnetMask: '192.168.1.0/24'
+ };
+
+ component.group = mockGroupName;
+ component.onSubmit(payload);
+
+ expect(nvmeofService.createSubsystem).toHaveBeenCalledWith({
+ nqn: 'test-nqn',
+ gw_group: mockGroupName,
+ dhchap_key: '',
+ network_mask: ['192.168.1.0/24']
+ });
+ });
+
+ it('should not include network_mask in createSubsystem request when listenerMode is manual', () => {
+ const payload: SubsystemPayload = {
+ nqn: 'test-nqn',
+ gw_group: mockGroupName,
+ addedHosts: [],
+ hostType: HOST_TYPE.ALL,
+ subsystemDchapKey: '',
+ listeners: [],
+ authType: AUTHENTICATION.Unidirectional,
+ hostDchapKeyList: [],
+ listenerMode: 'manual',
+ subnetMask: '192.168.1.0/24'
+ };
+
+ component.group = mockGroupName;
+ component.onSubmit(payload);
+
+ expect(nvmeofService.createSubsystem).toHaveBeenCalledWith({
+ nqn: 'test-nqn',
+ gw_group: mockGroupName,
+ dhchap_key: ''
+ });
+ });
+
+ it('should call createListeners when listenerMode is manual and listeners are provided', () => {
+ const listeners = [{ content: 'host1', addr: '10.0.0.1' }];
+ const payload: SubsystemPayload = {
+ nqn: 'test-nqn',
+ gw_group: mockGroupName,
+ addedHosts: [],
+ hostType: HOST_TYPE.ALL,
+ subsystemDchapKey: '',
+ listeners,
+ authType: AUTHENTICATION.Unidirectional,
+ hostDchapKeyList: [],
+ listenerMode: 'manual'
+ };
+
+ component.group = mockGroupName;
+ component.onSubmit(payload);
+
+ expect(nvmeofService.createListeners).toHaveBeenCalledWith(
+ 'test-nqn.default',
+ mockGroupName,
+ listeners
+ );
+ });
+
+ it('should not call createListeners when listenerMode is auto-fetch', () => {
+ const payload: SubsystemPayload = {
+ nqn: 'test-nqn',
+ gw_group: mockGroupName,
+ addedHosts: [],
+ hostType: HOST_TYPE.ALL,
+ subsystemDchapKey: '',
+ listeners: [{ content: 'host1', addr: '10.0.0.1' }],
+ authType: AUTHENTICATION.Unidirectional,
+ hostDchapKeyList: [],
+ listenerMode: 'auto-fetch',
+ subnetMask: '10.0.0.0/24'
+ };
+
+ component.group = mockGroupName;
+ component.onSubmit(payload);
+
+ expect(nvmeofService.createListeners).not.toHaveBeenCalled();
+ });
+
it('should add initiators with wildcard when hostType is ALL', () => {
const payload: SubsystemPayload = {
nqn: 'test-nqn',
listeners: ListenerItem[];
authType: AUTHENTICATION.Bidirectional | AUTHENTICATION.Unidirectional;
hostDchapKeyList: Array<{ dhchap_key: string; host_nqn: string }>;
+ listenerMode?: string;
+ subnetMask?: string;
};
type StepResult = { step: string; success: boolean; error?: string };
+type CreateSubsystemRequest = {
+ nqn: string;
+ gw_group: string;
+ dhchap_key: string;
+ network_mask?: string[];
+};
+
+type SequentialStep = { step: string; call: () => Observable<unknown> };
+
+const LISTENER_MODE = {
+ AUTO_FETCH: 'auto-fetch',
+ MANUAL: 'manual'
+};
+
const STEP_LABELS = {
DETAILS: 'Subsystem details',
HOSTS: 'Host access control',
hosts: payload.hostType === HOST_TYPE.SPECIFIC ? payload.hostDchapKeyList : [],
gw_group: this.group
};
- this.nvmeofService
- .createSubsystem({
- nqn: payload.nqn,
- gw_group: this.group,
- dhchap_key: payload.subsystemDchapKey
- })
- .subscribe({
- next: () => {
- stepResults.push({ step: this.steps[0].label, success: true });
- const sequentialSteps: { step: string; call: () => Observable<any> }[] = [];
-
- if (payload.listeners && payload.listeners.length > 0) {
- sequentialSteps.push({
- step: $localize`Listeners`,
- call: () =>
- this.nvmeofService.createListeners(
- `${payload.nqn}.${this.group}`,
- this.group,
- payload.listeners
- )
- });
- }
+ // Prepare subsystem creation request
+ const createSubsystemRequest: CreateSubsystemRequest = {
+ nqn: payload.nqn,
+ gw_group: this.group,
+ dhchap_key: payload.subsystemDchapKey
+ };
+
+ if (payload.listenerMode === LISTENER_MODE.AUTO_FETCH && payload.subnetMask) {
+ createSubsystemRequest.network_mask = [payload.subnetMask];
+ }
+
+ this.nvmeofService.createSubsystem(createSubsystemRequest).subscribe({
+ next: () => {
+ stepResults.push({ step: this.steps[0].label, success: true });
+ const sequentialSteps: SequentialStep[] = [];
+
+ if (
+ payload.listenerMode !== LISTENER_MODE.AUTO_FETCH &&
+ payload.listeners &&
+ payload.listeners.length > 0
+ ) {
sequentialSteps.push({
- step: this.steps[1].label,
+ step: $localize`Listeners`,
call: () =>
- this.nvmeofService.addSubsystemInitiators(
+ this.nvmeofService.createListeners(
`${payload.nqn}.${this.group}`,
- initiatorRequest
+ this.group,
+ payload.listeners
)
});
-
- this.runSequentialSteps(sequentialSteps, stepResults).subscribe({
- complete: () => this.showFinalNotification(stepResults)
- });
- },
- error: (err) => {
- err.preventDefault();
- const errorMsg = err?.error?.detail || $localize`Subsystem creation failed`;
- this.notificationService.show(
- NotificationType.error,
- $localize`Subsystem creation failed`,
- errorMsg
- );
- this.isSubmitLoading = false;
- this.router.navigate(['block/nvmeof/subsystems'], {
- queryParams: { group: this.group }
- });
}
- });
+
+ sequentialSteps.push({
+ step: this.steps[1].label,
+ call: () =>
+ this.nvmeofService.addSubsystemInitiators(
+ `${payload.nqn}.${this.group}`,
+ initiatorRequest
+ )
+ });
+
+ this.runSequentialSteps(sequentialSteps, stepResults).subscribe({
+ complete: () => this.showFinalNotification(stepResults)
+ });
+ },
+ error: (err) => {
+ err.preventDefault();
+ const errorMsg = err?.error?.detail || $localize`Subsystem creation failed`;
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Subsystem creation failed`,
+ errorMsg
+ );
+ this.isSubmitLoading = false;
+ this.router.navigate(['block/nvmeof/subsystems'], {
+ queryParams: { group: this.group }
+ });
+ }
+ });
}
- private runSequentialSteps(
- steps: { step: string; call: () => Observable<any> }[],
- stepResults: StepResult[]
- ): Observable<void> {
+ private runSequentialSteps(steps: SequentialStep[], stepResults: StepResult[]): Observable<void> {
return from(steps).pipe(
concatMap((step) =>
step.call().pipe(
return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}?gw_group=${group}`);
}
- createSubsystem(request: { nqn: string; gw_group: string; dhchap_key: string }) {
+ createSubsystem(request: {
+ nqn: string;
+ gw_group: string;
+ dhchap_key: string;
+ network_mask?: string[];
+ }) {
return this.http.post(`${API_PATH}/subsystem`, request, { observe: 'response' });
}
export type DetailsStepType = {
nqn: string;
listeners: Array<string>;
+ listenerMode?: string;
+ subnetMask?: string;
};