from ..tools import str_to_bool
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
DeletePermission, Endpoint, EndpointDoc, Param, ReadPermission, \
- RESTController, UIRouter
+ RESTController, UIRouter, UpdatePermission
logger = logging.getLogger(__name__)
+
NVME_SCHEMA = {
"available": (bool, "Is NVMe/TCP available?"),
"message": (str, "Descriptions")
"max_namespaces": Param(int, "Maximum number of namespaces", True, None),
"no_group_append": Param(int, "Do not append gateway group name to the NQN",
True, False),
- "serial_number": Param(int, "Subsystem serial number", True, None),
- "dhchap_key": Param(int, "Subsystem DH-HMAC-CHAP key", True, None),
+ "serial_number": Param(str, "Subsystem serial number", True, None),
+ "dhchap_key": Param(str, "Subsystem DH-HMAC-CHAP key", True, None),
"gw_group": Param(str, "NVMeoF gateway group", True, None),
"server_address": Param(str, "NVMeoF gateway address", True, None),
},
NVMeoFClient.pb2.create_subsystem_req(
subsystem_nqn=nqn, serial_number=serial_number,
max_namespaces=max_namespaces, enable_ha=enable_ha,
- no_group_append=no_group_append, dhchap_key=dhchap_key
+ no_group_append=no_group_append,
+ dhchap_key=dhchap_key
)
)
server_address=server_address
).stub.change_subsystem_key(
NVMeoFClient.pb2.change_subsystem_key_req(
- subsystem_nqn=nqn, dhchap_key=dhchap_key
+ subsystem_nqn=nqn,
+ dhchap_key=dhchap_key
)
)
gw_group=gw_group,
server_address=server_address
).stub.add_host(
- NVMeoFClient.pb2.add_host_req(subsystem_nqn=nqn, host_nqn=host_nqn,
- dhchap_key=dhchap_key, psk=psk)
+ NVMeoFClient.pb2.add_host_req(
+ subsystem_nqn=nqn, host_nqn=host_nqn,
+ dhchap_key=dhchap_key, psk=psk)
)
@empty_response
NVMeoFClient.pb2.remove_host_req(subsystem_nqn=nqn, host_nqn=host_nqn)
)
+ @Endpoint('PUT', '{host_nqn}/change_key')
+ @UpdatePermission
@empty_response
@NvmeofCLICommand("nvmeof host change_key", model.RequestStatus)
@EndpointDoc(
gw_group=gw_group,
server_address=server_address
).stub.change_host_key(
- NVMeoFClient.pb2.change_host_key_req(subsystem_nqn=nqn,
- host_nqn=host_nqn,
- dhchap_key=dhchap_key)
+ NVMeoFClient.pb2.change_host_key_req(
+ subsystem_nqn=nqn,
+ host_nqn=host_nqn,
+ dhchap_key=dhchap_key)
)
@empty_response
import { NvmeofSubsystemsStepTwoComponent } from './nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component';
import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node/nvmeof-gateway-node.component';
import { NvmeofGroupFormComponent } from './nvmeof-group-form/nvmeof-group-form.component';
+import { NvmeofEditHostKeyModalComponent } from './nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component';
import {
ButtonModule,
NvmeGatewayViewComponent,
NvmeofGatewaySubsystemComponent,
NvmeofGatewayNodeAddModalComponent,
- NvmeSubsystemViewComponent
+ NvmeSubsystemViewComponent,
+ NvmeofEditHostKeyModalComponent
],
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
--- /dev/null
+<cds-modal size="sm"
+ [open]="open"
+ (overlaySelected)="closeModal()">
+ <cds-modal-header (closeSelect)="closeModal()">
+ <h2 cdsModalHeaderLabel
+ i18n>
+ {{'Subsystem'}}
+ </h2>
+
+ <!-- Step 1: Enter DHCHAP key -->
+ <ng-container *ngIf="!showConfirmation">
+ <h2 class="cds--type-heading-03"
+ cdsModalHeaderHeading
+ i18n>Edit Host Key</h2>
+ </ng-container>
+
+ <!-- Step 2: Confirm changes -->
+ <ng-container *ngIf="showConfirmation">
+ <h2 class="cds--type-heading-03"
+ cdsModalHeaderHeading
+ i18n>Confirm changes</h2>
+ </ng-container>
+ </cds-modal-header>
+
+ <section cdsModalContent>
+ <!-- Step 1: Enter DHCHAP key -->
+ <ng-container *ngIf="!showConfirmation">
+ <p class="cds--type-body-01 cds-mb-5"
+ i18n>Update DHCHAP authentication key for the selected host.</p>
+ <form name="editForm"
+ #formDir="ngForm"
+ [formGroup]="editForm"
+ class="form-item "
+ novalidate>
+ <cds-text-label
+ i18n
+ helperText="Enter or update the authentication key for this host."
+ i18n-helperText
+ labelInputID="dhchapKey"
+ [invalid]="dhchapKey.isInvalid"
+ [invalidText]="dhchapKeyError">
+ DHCHAP Key | {{ hostNQN }}
+ <input cdsPassword
+ cdValidate
+ #dhchapKey="cdValidate"
+ [invalid]="dhchapKey.isInvalid"
+ formControlName="dhchapKey"
+ type="password"
+ id="dhchap_key"
+ placeholder="Enter Host DH-HMAC-CHAP key"
+ i18n-placeholder
+ modal-primary-focus
+ autofocus
+ autocomplete="off">
+ </cds-text-label>
+ <ng-template #dhchapKeyError>
+ <span *ngIf="editForm.controls.dhchapKey.hasError('required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ <span *ngIf="editForm.controls.dhchapKey.hasError('invalidBase64')"
+ class="invalid-feedback"
+ i18n>Invalid DH-HMAC-CHAP key. Please enter a valid Base64 encoded key.</span>
+ </ng-template>
+ </form>
+ </ng-container>
+
+ <!-- Step 2: Confirm changes -->
+ <ng-container *ngIf="showConfirmation">
+ <p class="cds--type-body-01"
+ i18n>Modifying or removing the Host key can invalidate existing NVMe sessions and interrupt secure communication with hosts. Ensure new keys are configured on all connected systems before continuing.</p>
+ </ng-container>
+ </section>
+
+ <!-- Step 1: Save button -->
+ <cd-form-button-panel *ngIf="!showConfirmation"
+ (submitActionEvent)="onSave()"
+ (backActionEvent)="closeModal()"
+ [form]="editForm"
+ [submitText]="'Save'"
+ [modalForm]="true"></cd-form-button-panel>
+
+ <!-- Step 2: Confirm button -->
+ <cd-form-button-panel *ngIf="showConfirmation"
+ (submitActionEvent)="onSubmit()"
+ (backActionEvent)="goBack()"
+ [form]="editForm"
+ [submitText]="'Save changes'"
+ [modalForm]="true"></cd-form-button-panel>
+</cds-modal>
--- /dev/null
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { By } from '@angular/platform-browser';
+import { ToastrModule } from 'ngx-toastr';
+import { of, throwError } from 'rxjs';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { NvmeofEditHostKeyModalComponent } from './nvmeof-edit-host-key-modal.component';
+
+describe('NvmeofEditHostKeyModalComponent', () => {
+ let component: NvmeofEditHostKeyModalComponent;
+ let fixture: ComponentFixture<NvmeofEditHostKeyModalComponent>;
+ let nvmeofService: NvmeofService;
+ let taskWrapperService: TaskWrapperService;
+
+ const mockSubsystemNQN = 'nqn.2014-08.org.nvmexpress:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6';
+ const mockHostNQN = 'nqn.2014-08.org.nvmexpress:uuid:12345678-1234-1234-1234-1234567890ab';
+ const mockGroup = 'default';
+
+ const nvmeofServiceSpy = {
+ updateHostKey: jasmine.createSpy('updateHostKey').and.returnValue(of(null))
+ };
+
+ const taskWrapperServiceSpy = {
+ wrapTaskAroundCall: jasmine.createSpy('wrapTaskAroundCall').and.callFake(({ call }) => call)
+ };
+
+ beforeEach(
+ waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [NvmeofEditHostKeyModalComponent],
+ imports: [
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [
+ { provide: NvmeofService, useValue: nvmeofServiceSpy },
+ { provide: TaskWrapperService, useValue: taskWrapperServiceSpy },
+ { provide: 'subsystemNQN', useValue: mockSubsystemNQN },
+ { provide: 'hostNQN', useValue: mockHostNQN },
+ { provide: 'group', useValue: mockGroup },
+ { provide: 'dhchapKey', useValue: '' }
+ ]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NvmeofEditHostKeyModalComponent);
+ component = fixture.componentInstance;
+ nvmeofService = TestBed.inject(NvmeofService);
+ taskWrapperService = TestBed.inject(TaskWrapperService);
+ nvmeofServiceSpy.updateHostKey.calls.reset();
+ taskWrapperServiceSpy.wrapTaskAroundCall.calls.reset();
+ nvmeofServiceSpy.updateHostKey.and.returnValue(of(null));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should initialize the form with empty dhchapKey', () => {
+ expect(component.editForm).toBeDefined();
+ expect(component.editForm.get('dhchapKey').value).toBe('');
+ expect(component.editForm.valid).toBe(false);
+ });
+
+ it('should inject subsystemNQN, hostNQN and group', () => {
+ expect(component.subsystemNQN).toBe(mockSubsystemNQN);
+ expect(component.hostNQN).toBe(mockHostNQN);
+ expect(component.group).toBe(mockGroup);
+ });
+
+ it('should display the host NQN in the form label', () => {
+ const label = fixture.debugElement.query(By.css('cds-text-label'));
+ expect(label.nativeElement.textContent).toContain(mockHostNQN);
+ });
+
+ it('should not submit if form is invalid', () => {
+ component.onSubmit();
+ expect(nvmeofService.updateHostKey).not.toHaveBeenCalled();
+ });
+
+ it('should submit successfully when form is valid', () => {
+ const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=';
+ component.editForm.patchValue({ dhchapKey: mockKey });
+ expect(component.editForm.valid).toBe(true);
+
+ spyOn(component, 'closeModal');
+ component.onSubmit();
+
+ expect(nvmeofService.updateHostKey).toHaveBeenCalledWith(mockSubsystemNQN, {
+ host_nqn: mockHostNQN,
+ dhchap_key: mockKey,
+ gw_group: mockGroup
+ });
+ expect(taskWrapperService.wrapTaskAroundCall).toHaveBeenCalled();
+ expect(component.closeModal).toHaveBeenCalled();
+ });
+
+ it('should call updateHostKey with correct task metadata on submit', () => {
+ const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=';
+ component.editForm.patchValue({ dhchapKey: mockKey });
+ spyOn(component, 'closeModal');
+
+ component.onSubmit();
+
+ const callArgs = taskWrapperServiceSpy.wrapTaskAroundCall.calls.mostRecent().args[0];
+ expect(callArgs.task.name).toBe('nvmeof/initiator/edit');
+ expect(callArgs.task.metadata).toEqual({
+ nqn: mockSubsystemNQN
+ });
+ });
+
+ it('should handle error during submission', () => {
+ const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=';
+ component.editForm.patchValue({ dhchapKey: mockKey });
+ spyOn(component.editForm, 'setErrors');
+
+ nvmeofServiceSpy.updateHostKey.and.returnValue(throwError(() => ({ status: 500 })));
+
+ component.onSubmit();
+
+ expect(nvmeofService.updateHostKey).toHaveBeenCalled();
+ expect(component.editForm.setErrors).toHaveBeenCalledTimes(1);
+ expect(component.editForm.setErrors).toHaveBeenCalledWith({ cdSubmitButton: true });
+ });
+
+ it('should not close modal on error', () => {
+ const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=';
+ component.editForm.patchValue({ dhchapKey: mockKey });
+ spyOn(component, 'closeModal');
+
+ nvmeofServiceSpy.updateHostKey.and.returnValue(throwError(() => ({ status: 400 })));
+
+ component.onSubmit();
+
+ expect(component.closeModal).not.toHaveBeenCalled();
+ });
+
+ it('should be valid with 43-character unpadded Base64 key with DHHC-1: prefix and trailing colon', () => {
+ const prefixedSuffixedKey = 'DHHC-1:00:Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY:';
+ component.editForm.patchValue({ dhchapKey: prefixedSuffixedKey });
+ expect(component.editForm.get('dhchapKey').valid).toBe(true);
+ expect(component.editForm.valid).toBe(true);
+ });
+
+ describe('Save button click', () => {
+ it('should trigger onSubmit and call updateHostKey when Save is clicked with valid form', () => {
+ const mockKey = 'Q2VwaE52bWVvRkNoYXBTeW50aGV0aWNLZXkxMjM0NTY=';
+ component.editForm.patchValue({ dhchapKey: mockKey });
+ fixture.detectChanges();
+
+ spyOn(component, 'closeModal');
+
+ // Step 1: Save (Confirmation)
+ fixture.debugElement
+ .query(By.css('cd-form-button-panel'))
+ .triggerEventHandler('submitActionEvent', null);
+ fixture.detectChanges();
+ expect(component.showConfirmation).toBe(true);
+
+ // Step 2: Save changes (Actual submit)
+ fixture.debugElement
+ .query(By.css('cd-form-button-panel'))
+ .triggerEventHandler('submitActionEvent', null);
+ fixture.detectChanges();
+
+ expect(nvmeofService.updateHostKey).toHaveBeenCalledWith(mockSubsystemNQN, {
+ host_nqn: mockHostNQN,
+ dhchap_key: mockKey,
+ gw_group: mockGroup
+ });
+ expect(taskWrapperService.wrapTaskAroundCall).toHaveBeenCalled();
+ expect(component.closeModal).toHaveBeenCalled();
+ });
+
+ it('should not call updateHostKey when Save is clicked with empty form', () => {
+ fixture.detectChanges();
+
+ const submitPanel = fixture.debugElement.query(By.css('cd-form-button-panel'));
+ submitPanel.triggerEventHandler('submitActionEvent', null);
+ fixture.detectChanges();
+
+ expect(nvmeofService.updateHostKey).not.toHaveBeenCalled();
+ });
+
+ it('should call closeModal when Cancel is clicked', () => {
+ spyOn(component, 'closeModal');
+ fixture.detectChanges();
+
+ const submitPanel = fixture.debugElement.query(By.css('cd-form-button-panel'));
+ submitPanel.triggerEventHandler('backActionEvent', null);
+ fixture.detectChanges();
+
+ expect(component.closeModal).toHaveBeenCalled();
+ });
+ });
+});
--- /dev/null
+import { Component, Inject, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { BaseModal } from 'carbon-components-angular';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-nvmeof-edit-host-key-modal',
+ templateUrl: './nvmeof-edit-host-key-modal.component.html',
+ styleUrls: ['./nvmeof-edit-host-key-modal.component.scss'],
+ standalone: false
+})
+export class NvmeofEditHostKeyModalComponent extends BaseModal implements OnInit {
+ editForm: CdFormGroup;
+ showConfirmation = false;
+
+ constructor(
+ @Inject('subsystemNQN') public subsystemNQN: string,
+ @Inject('hostNQN') public hostNQN: string,
+ @Inject('group') public group: string,
+ @Inject('dhchapKey') public existingDhchapKey: string,
+ private nvmeofService: NvmeofService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ super();
+ }
+
+ ngOnInit() {
+ this.editForm = new CdFormGroup({
+ dhchapKey: new UntypedFormControl(this.existingDhchapKey || '', [
+ Validators.required,
+ CdValidators.base64()
+ ])
+ });
+ }
+
+ onSave() {
+ if (this.editForm.invalid) {
+ return;
+ }
+ this.showConfirmation = true;
+ }
+
+ goBack() {
+ this.showConfirmation = false;
+ }
+
+ onSubmit() {
+ if (this.editForm.invalid) {
+ return;
+ }
+ const dhchapKey = this.editForm.getValue('dhchapKey');
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('nvmeof/initiator/edit', {
+ nqn: this.subsystemNQN
+ }),
+ call: this.nvmeofService.updateHostKey(this.subsystemNQN, {
+ host_nqn: this.hostNQN,
+ dhchap_key: dhchapKey,
+ gw_group: this.group
+ })
+ })
+ .subscribe({
+ error: () => {
+ this.editForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.closeModal();
+ }
+ });
+ }
+}
</div>
</div>
+ <!-- CDS Checkbox -->
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cds-checkbox id="enableCds"
+ formControlName="enableEncryption"
+ i18n>Enable encryption
+ </cds-checkbox>
+ </div>
+ </div>
+
+ <!-- CDS Input (shown when checkbox is checked) -->
+ @if (groupForm.controls.enableEncryption.value) {
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cds-textarea-label
+ labelInputID="cdsInput"
+ helperText="Provide the encryption configuration details."
+ i18n
+ i18n-helperText>Encryption configuration
+ <textarea cdsTextArea
+ id="cdsInput"
+ formControlName="encryptionConfig"
+ cols="100"
+ rows="4">
+ </textarea>
+ </cds-textarea-label>
+ </div>
+ </div>
+ }
+
<!-- Target Nodes Selection -->
<div
cdsRow
import { SharedModule } from '~/app/shared/shared.module';
import { NvmeofGroupFormComponent } from './nvmeof-group-form.component';
-import { GridModule, InputModule, SelectModule } from 'carbon-components-angular';
+import { CheckboxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
import { PoolService } from '~/app/shared/api/pool.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
ReactiveFormsModule,
RouterTestingModule,
SharedModule,
+ CheckboxModule,
GridModule,
InputModule,
SelectModule,
pool: new UntypedFormControl('rbd', {
validators: [Validators.required]
}),
- unmanaged: new UntypedFormControl(false)
+ unmanaged: new UntypedFormControl(false),
+ enableEncryption: new UntypedFormControl(false),
+ encryptionConfig: new UntypedFormControl(null)
});
}
let taskUrl = `service/${URLVerbs.CREATE}`;
const serviceName = `${formValues.pool}.${formValues.groupName}`;
- const serviceSpec = {
+ const serviceSpec: Record<string, any> = {
service_type: 'nvmeof',
service_id: serviceName,
pool: formValues.pool,
unmanaged: formValues.unmanaged
};
+ if (formValues.enableCds && formValues.cdsInput) {
+ serviceSpec['encryption_key'] = formValues.cdsInput;
+ }
+
this.taskWrapperService
.wrapTaskAroundCall({
task: new FinishedTask(taskUrl, {
Allowing all hosts grants access to every initiator on the network. Authentication is not supported in this mode, which may expose the subsystem to unauthorized access.
</p>
</div>
- <span
- class="text-nowrap cds-ml-3 text-muted"
- i18n
- >
- Edit host access
- </span>
+ <a cdsLink
+ class="text-nowrap cds-ml-3 text-muted"
+ (click)="editHostAccess()"
+ i18n>Edit host access</a>
</div>
</cd-alert-panel>
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { NvmeofEditHostKeyModalComponent } from '../nvmeof-edit-host-key-modal/nvmeof-edit-host-key-modal.component';
const BASE_URL = 'block/nvmeof/subsystems';
canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
disable: () => this.hasAllHostsAllowed()
},
+ {
+ name: $localize`Edit host key`,
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.editHostKeyModal(),
+ disable: () => this.selection.selected.length !== 1,
+ canBePrimary: (selection: CdTableSelection) => selection.selected.length === 1
+ },
{
name: this.actionLabels.REMOVE,
permission: 'delete',
}
}
+ editHostKeyModal() {
+ const selected = this.selection.selected[0];
+ if (!selected) return;
+ this.modalService.show(NvmeofEditHostKeyModalComponent, {
+ subsystemNQN: this.subsystemNQN,
+ hostNQN: selected.nqn,
+ group: this.group,
+ dhchapKey: selected.dhchap_key || ''
+ });
+ }
+
getAllowAllHostIndex() {
return this.selection.selected.findIndex((selected) => selected.nqn === '*');
}
host_nqn: payload.hostType === HOST_TYPE.ALL ? '*' : payload.addedHosts.join(','),
gw_group: this.group
};
-
this.nvmeofService
.createSubsystem({
nqn: payload.nqn,
export type InitiatorRequest = NvmeofRequest & {
host_nqn: string;
+ dhchap_key?: string;
};
export type NamespaceInitiatorRequest = InitiatorRequest & {
});
}
+ updateHostKey(subsystemNQN: string, request: InitiatorRequest) {
+ return this.http.put(
+ `${API_PATH}/subsystem/${subsystemNQN}/host/${request.host_nqn}/change_key`,
+ request,
+ {
+ observe: 'response'
+ }
+ );
+ }
+
addNamespaceInitiators(nsid: string, request: NamespaceInitiatorRequest) {
return this.http.post(`${UI_API_PATH}/namespace/${nsid}/host`, request, {
observe: 'response'
const submit$ = this.formGroupDir ? this.formGroupDir.ngSubmit : new Subject();
- merge(this.ngControl.control.statusChanges, submit$)
+ merge(this.ngControl.control.statusChanges, this.ngControl.control.valueChanges, submit$)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.updateState();
};
}
+ /**
+ * Validator for DH-HMAC-CHAP keys that must be Base64 encoded.
+ * Accepts plain Base64 or DHHC-1:XX:base64: format.
+ * Skips validation when value is empty (use with required validator if needed).
+ * @returns {ValidatorFn} Returns error map with `invalidBase64` if validation fails.
+ */
+ static base64(): ValidatorFn {
+ const plainBase64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
+ const dhchapFormatRegex = /^DHHC-1:[0-9a-fA-F]{2}:[A-Za-z0-9+/]+:$/;
+ return (control: AbstractControl): { [key: string]: boolean } | null => {
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ const value = control.value;
+ return plainBase64Regex.test(value) || dhchapFormatRegex.test(value)
+ ? null
+ : { invalidBase64: true };
+ };
+ }
+
/**
* Validate form control if condition is true with validators.
*
'nvmeof/initiator/add': this.newTaskMessage(this.commonOperations.add, (metadata) =>
this.nvmeofInitiator(metadata)
),
+ 'nvmeof/initiator/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.nvmeofInitiator(metadata)
+ ),
'nvmeof/initiator/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
this.nvmeofInitiator(metadata)
),
properties:
dhchap_key:
description: Subsystem DH-HMAC-CHAP key
- type: integer
+ type: string
enable_ha:
default: true
description: Enable high availability
type: string
serial_number:
description: Subsystem serial number
- type: integer
+ type: string
server_address:
description: NVMeoF gateway address
type: string
summary: Disallow hosts from accessing an NVMeoF subsystem
tags:
- NVMe-oF Subsystem Host Allowlist
+ /api/nvmeof/subsystem/{nqn}/host/{host_nqn}/change_key:
+ put:
+ parameters:
+ - description: NVMeoF subsystem NQN
+ in: path
+ name: nqn
+ required: true
+ schema:
+ type: string
+ - description: NVMeoF host NQN
+ in: path
+ name: host_nqn
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ dhchap_key:
+ description: Host DH-HMAC-CHAP key
+ type: string
+ gw_group:
+ description: NVMeoF gateway group
+ type: string
+ server_address:
+ description: NVMeoF gateway address
+ type: string
+ required:
+ - dhchap_key
+ type: object
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ type: object
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/json:
+ schema:
+ type: object
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Change host DH-HMAC-CHAP key
+ tags:
+ - NVMe-oF Subsystem Host Allowlist
/api/nvmeof/subsystem/{nqn}/listener:
get:
parameters: