import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service';
import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
import { SharedModule } from '~/app/shared/shared.module';
+import { TextLabelListComponent } from '~/app/shared/components/text-label-list/text-label-list.component';
import { IscsiSettingComponent } from './iscsi-setting/iscsi-setting.component';
import { IscsiTabsComponent } from './iscsi-tabs/iscsi-tabs.component';
import { IscsiTargetDetailsComponent } from './iscsi-target-details/iscsi-target-details.component';
LayoutModule,
ThemeModule,
NvmeofSetupCardsComponent,
- NvmeofGatewayGroupFilterComponent
+ NvmeofGatewayGroupFilterComponent,
+ TextLabelListComponent
+ // ProductiveCardComponent
],
declarations: [
RbdListComponent,
}
}
},
+ {
+ path: `${URLVerbs.EDIT}/:name`,
+ component: NvmeofGroupFormComponent,
+ data: {
+ breadcrumbs: ActionLabels.EDIT,
+ pageHeader: {
+ title: $localize`Edit Gateway Group`,
+ description: $localize`Modify gateway group configuration.`
+ }
+ }
+ },
{
path: `${URLVerbs.VIEW}/:group`,
component: NvmeGatewayViewComponent,
<ng-container *ngIf="gatewayGroup$ | async as gateways">
<!-- Details Card Section -->
- <ng-container *ngIf="selection.hasSelection">
+ <ng-container *ngIf="selection.hasSelection && showDetailsCard">
<cd-details-card
cardTitle="Details"
i18n-cardTitle
[details]="selectedGatewayDetails"
- [showEditButton]="true"
- [editButtonLabel]="actionLabels.EDIT"
- [columns]="4"
- (editClicked)="editSelectedGatewayGroup()">
+ [showEditButton]="false"
+ [columns]="4">
</cd-details-card>
</ng-container>
</ng-template>
<ng-template #customTableItemTemplate
- let-value="data.value">
+ let-value="data.value"
+ let-row="data.row">
<a cdsLink
- [routerLink]="[viewUrl, value | encodeUri]"
- (click)="$event.stopPropagation()">
+ href="#"
+ (click)="onNameClick(row, $event)">
{{ value }}
</a>
</ng-template>
display: block;
margin-bottom: layout.$spacing-05;
}
-
-.table-actions {
- margin-top: layout.$spacing-03;
-}
gatewayCount = 0;
selectedGatewayDetails: DetailItem[] = [];
private lastGroupCount = 0;
+ showDetailsCard = false;
viewUrl = `/${BASE_URL}/view`;
icons = Icons;
canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
};
- const editAction: CdTableAction = {
- permission: 'update',
- icon: Icons.edit,
- routerLink: () => this.urlBuilder.getEdit(this.selection.first()?.name),
- name: this.actionLabels.EDIT,
- canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection
- };
-
const viewAction: CdTableAction = {
permission: 'read',
icon: Icons.eye,
canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
};
- this.tableActions = [createAction, editAction, viewAction, deleteAction];
+ this.tableActions = [createAction, viewAction, deleteAction];
this.gatewayGroup$ = this.subject.pipe(
switchMap(() =>
updateSelection(selection: CdTableSelection): void {
this.selection = selection;
this.selectedGatewayDetails = this.buildGatewayDetails(selection.first());
+ this.showDetailsCard = false;
}
deleteGatewayGroupModal() {
})
.pipe(
tap({
+ next: () => {
+ this.table.data = this.table.data.filter(
+ (row: CephServiceSpec) => row.spec?.group !== selectedGroup.spec.group
+ );
+ },
complete: () => {
this.nvmeofStateService.requestRefresh();
}
});
}
+ onNameClick(row: any, event?: Event): void {
+ event?.preventDefault();
+ event?.stopPropagation();
+ this.selection = new CdTableSelection([row]);
+ this.selectedGatewayDetails = this.buildGatewayDetails(row);
+ this.showDetailsCard = true;
+ }
+
private checkNodesAvailability(): void {
forkJoin([this.nvmeofService.listGatewayGroups(), this.hostService.getAllHosts()]).subscribe(
([groups, hosts]: [GatewayGroup[][], any[]]) => {
this.router.navigate([this.viewUrl, groupName]);
}
- editSelectedGatewayGroup(): void {
- const selectedGroup = this.selection.first();
- if (!selectedGroup) {
- return;
- }
- this.router.navigate([this.urlBuilder.getEdit(selectedGroup.name)]);
- }
-
private buildGatewayDetails(selectedGroup: any): DetailItem[] {
if (!selectedGroup) {
return [];
},
{
label: $localize`mTLS`,
- value: selectedGroup.spec?.enable_mtls ? $localize`Enabled` : $localize`Disabled`,
+ value: selectedGroup.spec?.enable_auth ? $localize`Enabled` : $localize`Disabled`,
type: 'status'
}
];
<div class="content-theme">
@if (mode === 'details' && groupName) {
- <cd-details-card cardTitle="Details" i18n-cardTitle
- [details]="gatewayDetails"
- class="cds-mb-5">
+ <cd-details-card cardTitle="Details" i18n-cardTitle [details]="gatewayDetails" [showEditButton]="false"
+ class="cds-mb-5">
</cd-details-card>
}
},
{
label: $localize`mTLS`,
- value: $localize`Disabled`,
+ value: serviceSpec.spec?.enable_auth ? $localize`Enabled` : $localize`Disabled`,
type: 'status',
- statusIcon: 'error'
+ statusIcon: serviceSpec.spec?.enable_auth ? 'success' : 'error'
}
];
}
</div>
</div>
-
- <!-- CDS Checkbox -->
+ <!-- Pool -->
<div cdsRow
class="form-item">
<div cdsCol>
- <cds-checkbox id="enableCds"
- formControlName="enableEncryption"
- i18n>Enable encryption
- </cds-checkbox>
- </div>
- </div>
-
- <!-- Encryption Configuration (shown when checkbox is checked) -->
- @if (groupForm.controls.enableEncryption.value) {
- <div cdsRow
- class="form-item">
- <div cdsCol>
- <cds-textarea-label
- labelInputID="encryptionConfig"
- helperText="Provide the encryption configuration details."
- cdRequiredField="Encryption configuration"
- i18n
+ <cds-select
+ label="Block pool"
+ helperText="An RBD application-enabled pool in which the gateway configuration can be managed."
+ labelInputID="pool"
+ id="pool"
+ formControlName="pool"
+ cdRequiredField="Block pool"
+ [invalid]="groupForm.controls.pool.invalid && groupForm.controls.pool.dirty"
+ [invalidText]="poolError"
+ i18n-label
i18n-helperText
- [invalid]="encryptionConfigRef.isInvalid">Encryption configuration
- <textarea cdsTextArea
- id="encryptionConfig"
- cdValidate
- #encryptionConfigRef="cdValidate"
- [invalid]="encryptionConfigRef.isInvalid"
- formControlName="encryptionConfig"
- cols="100"
- rows="4">
- </textarea>
- </cds-textarea-label>
- <span
- class="invalid-feedback"
- *ngIf="groupForm.showError('encryptionConfig', formDir, 'required')"
- i18n>This field is required.</span>
+ >
+ <option *ngIf="poolsLoading"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="!poolsLoading && pools.length === 0"
+ [ngValue]="null"
+ i18n>-- No block pools available --</option>
+ <option *ngFor="let pool of pools"
+ [value]="pool.pool_name">{{ pool.pool_name }}</option>
+ </cds-select>
+ <ng-template #poolError>
+ <span class="invalid-feedback"
+ *ngIf="groupForm.showError('pool', formDir, 'required')"
+ i18n>This field is required.</span>
+ </ng-template>
</div>
</div>
- }
- <!-- Target Nodes Selection -->
- <div
+ <!-- Target Nodes Selection -->
+ <div
cdsRow
class="form-item"
>
></cd-nvmeof-gateway-node>
</div>
</div>
+ <div cdsRow class="form-item cds-mb-0">
+ <div cdsCol>
+ <cds-tile [cdsLayer]="1">
+ <!-- Encryption -->
+ <div cdsRow class="form-item">
+ <div cdsCol>
+ <div [cdsStack]="'horizontal'"
+ gap="1"
+ alignment="center">
+ <label class="cds--type-label-01"
+ i18n>Enable encryption</label>
+ <cds-tag type="green"
+ size="sm"
+ i18n>Recommended</cds-tag>
+ </div>
+ <cds-checkbox id="enableEncryption"
+ formControlName="enableEncryption">
+ <span
+ class="cds--type-body-01"
+ i18n>Secures group metadata and unlocks advanced authentication features.</span>
+ </cds-checkbox>
+ </div>
+ </div>
+
+ @if (!groupForm.controls.enableEncryption.value) {
+ <div cdsRow
+ class="cds-mt-2">
+ <div cdsCol>
+ <cd-alert-panel type="warning"
+ spacingClass="mb-0"
+ i18n>
+ <span class="cds--type-heading-01">Encryption is required if you plan to use DH-CHAP or mTLS authentication.</span>
+ </cd-alert-panel>
+ </div>
+ </div>
+ }
+
+ @if (groupForm.controls.enableEncryption.value) {
+ <div cdsRow class="form-item">
+ <div cdsCol>
+ <cds-textarea-label
+ labelInputID="encryptionKey"
+ helperText="Provide the encryption key used for securing the gateway group."
+ i18n
+ i18n-helperText>
+ Encryption key
+ <textarea
+ cdsTextArea
+ id="encryptionKey"
+ formControlName="encryptionKey"
+ rows="4"
+ cols="100">
+ </textarea>
+ </cds-textarea-label>
+ <span
+ class="invalid-feedback"
+ *ngIf="groupForm.showError('encryptionKey', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ }
+ </cds-tile>
+
+ <cds-tile [cdsLayer]="1"
+ class="cds-mt-3">
+ <div cdsRow class="form-item">
+ <div cdsCol>
+ <div [cdsStack]="'horizontal'" gap="1" alignment="center" class="cds-mb-3">
+ <label class="cds--type-label-01" i18n>Enable Mutual TLS (mTLS)</label>
+ </div>
+ <cds-checkbox id="enableMtls" formControlName="enableMtls">
+ <span
+ class="cds--type-body-01"
+ i18n>Use mutual TLS (mTLS) to encrypt control and monitoring commands exchanged with NVMe-oF gateways.</span>
+ </cds-checkbox>
+ </div>
+ </div>
+
+ @if (!groupForm.controls.enableMtls.value) {
+ <div cdsRow class="cds-mt-3">
+ <div cdsCol>
+ <cd-alert-panel type="warning"
+ spacingClass="mb-0"
+ class="cds--type-heading-01"
+ i18n>
+ Disabling mTLS leaves gateway control and monitoring communication unencrypted.
+ </cd-alert-panel>
+ </div>
+ </div>
+ }
+
+ @if (groupForm.controls.enableMtls.value) {
+ <div cdsRow class="form-item">
+ <div cdsCol>
+ <label class="cds--label fw-bold"
+ i18n>Choose Certificate Authority</label>
+ <cds-radio-group
+ formControlName="certificateType"
+ orientation="horizontal"
+ helperText="Select how certificates will be signed for this service. Choose internal to use the cluster's CA, or external to upload certificates signed by your organization."
+ i18n-helperText>
+ <cds-radio
+ value="internal"
+ [checked]="groupForm.controls.certificateType.value === CertificateType.internal"
+ (change)="onCertificateTypeChange(CertificateType.internal)"
+ i18n>
+ Internal
+ </cds-radio>
+ <cds-radio
+ value="external"
+ [checked]="groupForm.controls.certificateType.value === CertificateType.external"
+ (change)="onCertificateTypeChange(CertificateType.external)"
+ i18n>
+ External
+ </cds-radio>
+ </cds-radio-group>
+ </div>
+ </div>
+ }
+ </cds-tile>
+ </div>
+ </div>
+
+ @if (groupForm.controls.enableMtls.value && groupForm.controls.certificateType.value === CertificateType.internal) {
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cd-alert-panel type="info"
+ spacingClass="mb-3"
+ i18n>
+ Certificate will be generated automatically by Cephadm CA for internal certificate type.
+ </cd-alert-panel>
+ <cd-text-label-list formControlName="custom_sans"
+ label="Custom SAN Entries"
+ i18n-label
+ helperText="Optional list of Subject Alternative Names (hostnames, IPs, or DNS names) to include in the auto-generated certificate."
+ i18n-helperText>
+ </cd-text-label-list>
+ </div>
+ </div>
+ }
+
+ @if (groupForm.controls.enableMtls.value && groupForm.controls.certificateType.value === CertificateType.external) {
+ <!-- Root CA Certificate Input -->
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="rootCACert"
+ i18n
+ i18n-helperText
+ helperText="Upload a Root CA certificate file, or paste the Root CA certificate PEM content directly.">
+ Root CA Certificate Input
+ </cds-text-label>
+ <div class="cds-mb-2">
+ <button
+ cdsButton="tertiary"
+ type="button"
+ (click)="rootCAFileInput.click()"
+ i18n>
+ Upload File
+ </button>
+ <input
+ #rootCAFileInput
+ type="file"
+ accept=".pem,.crt,.cer"
+ style="display: none"
+ (change)="onFileUpload($event, 'rootCACert')"
+ />
+ </div>
+ <cds-textarea-label
+ labelInputID="rootCACertText"
+ helperText="Uploaded files will populate the Root CA certificate details automatically. Or paste the PEM content directly in the text area."
+ i18n-helperText>
+ <textarea cdsTextArea
+ id="rootCACertText"
+ formControlName="rootCACert"
+ placeholder="Paste certificate or private key PEM content"
+ i18n-placeholder
+ cols="100"
+ rows="4">
+ </textarea>
+ </cds-textarea-label>
+ <span class="invalid-feedback"
+ *ngIf="groupForm.showError('rootCACert', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Client Certificate Input -->
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="clientCert"
+ i18n
+ i18n-helperText
+ helperText="Upload a client certificate file, or paste the client certificate PEM content directly.">
+ Client Certificate Input
+ </cds-text-label>
+ <div class="cds-mb-2">
+ <button
+ cdsButton="tertiary"
+ type="button"
+ (click)="clientCertFileInput.click()"
+ i18n>
+ Upload File
+ </button>
+ <input
+ #clientCertFileInput
+ type="file"
+ accept=".pem,.crt,.cer"
+ style="display: none"
+ (change)="onFileUpload($event, 'clientCert')"
+ />
+ </div>
+ <cds-textarea-label
+ labelInputID="clientCertText"
+ helperText="Uploaded files will populate the client certificate details automatically. Or paste the PEM content directly in the text area."
+ i18n-helperText>
+ <textarea cdsTextArea
+ id="clientCertText"
+ formControlName="clientCert"
+ placeholder="Paste certificate or private key PEM content"
+ i18n-placeholder
+ cols="100"
+ rows="4">
+ </textarea>
+ </cds-textarea-label>
+ <span class="invalid-feedback"
+ *ngIf="groupForm.showError('clientCert', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Client Key Input -->
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="clientKey"
+ i18n
+ i18n-helperText
+ helperText="Upload a client key file, or paste the client key PEM content directly.">
+ Client Key Input
+ </cds-text-label>
+ <div class="cds-mb-2">
+ <button
+ cdsButton="tertiary"
+ type="button"
+ (click)="clientKeyFileInput.click()"
+ i18n>
+ Upload File
+ </button>
+ <input
+ #clientKeyFileInput
+ type="file"
+ accept=".pem,.key"
+ style="display: none"
+ (change)="onFileUpload($event, 'clientKey')"
+ />
+ </div>
+ <cds-textarea-label
+ labelInputID="clientKeyText"
+ helperText="Uploaded files will populate the client key details automatically. Or paste the PEM content directly in the text area."
+ i18n-helperText>
+ <textarea cdsTextArea
+ id="clientKeyText"
+ formControlName="clientKey"
+ placeholder="Paste certificate or private key PEM content"
+ i18n-placeholder
+ cols="100"
+ rows="4">
+ </textarea>
+ </cds-textarea-label>
+ <span class="invalid-feedback"
+ *ngIf="groupForm.showError('clientKey', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Server Certificate Input -->
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="serverCert"
+ i18n
+ i18n-helperText
+ helperText="Upload a server certificate file, or paste the server certificate PEM content directly.">
+ Server Certificate Input
+ </cds-text-label>
+ <div class="cds-mb-2">
+ <button
+ cdsButton="tertiary"
+ type="button"
+ (click)="serverCertFileInput.click()"
+ i18n>
+ Upload File
+ </button>
+ <input
+ #serverCertFileInput
+ type="file"
+ accept=".pem,.crt,.cer"
+ style="display: none"
+ (change)="onFileUpload($event, 'serverCert')"
+ />
+ </div>
+ <cds-textarea-label
+ labelInputID="serverCertText"
+ helperText="Uploaded files will populate the server certificate details automatically. Or paste the PEM content directly in the text area."
+ i18n-helperText>
+ <textarea cdsTextArea
+ id="serverCertText"
+ formControlName="serverCert"
+ placeholder="Paste certificate or private key PEM content"
+ i18n-placeholder
+ cols="100"
+ rows="4">
+ </textarea>
+ </cds-textarea-label>
+ <span class="invalid-feedback"
+ *ngIf="groupForm.showError('serverCert', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Server Key Input -->
+ <div cdsRow
+ class="form-item">
+ <div cdsCol>
+ <cds-text-label
+ labelInputID="serverKey"
+ i18n
+ i18n-helperText
+ helperText="Upload a server key file, or paste the server key PEM content directly.">
+ Server Key Input
+ </cds-text-label>
+ <div class="cds-mb-2">
+ <button
+ cdsButton="tertiary"
+ type="button"
+ (click)="serverKeyFileInput.click()"
+ i18n>
+ Upload File
+ </button>
+ <input
+ #serverKeyFileInput
+ type="file"
+ accept=".pem,.key"
+ style="display: none"
+ (change)="onFileUpload($event, 'serverKey')"
+ />
+ </div>
+ <cds-textarea-label
+ labelInputID="serverKeyText"
+ helperText="Uploaded files will populate the server key details automatically. Or paste the PEM content directly in the text area."
+ i18n-helperText>
+ <textarea cdsTextArea
+ id="serverKeyText"
+ formControlName="serverKey"
+ placeholder="Paste certificate or private key PEM content"
+ i18n-placeholder
+ cols="100"
+ rows="4">
+ </textarea>
+ </cds-textarea-label>
+ <span class="invalid-feedback"
+ *ngIf="groupForm.showError('serverKey', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ }
- <div cdsRow>
+ <div cdsRow class="cds-mt-5">
<cd-form-button-panel
(submitActionEvent)="onSubmit()"
[form]="groupForm"
[submitText]="(action | titlecase) + ' ' + (resource)"
[disabled]="isCreateDisabled"
- wrappingClass="text-right form-button"
+ wrappingClass="text-right"
>
</cd-form-button-panel>
</div>
expect(form.controls.groupName.value).toBeNull();
expect(form.controls.unmanaged.value).toBe(false);
expect(form.controls.enableEncryption.value).toBe(false);
+ expect(form.controls.certificateType.value).toBe('internal');
});
it('should set action to CREATE on init', () => {
);
});
+ it('should create service with cephadm-signed mTLS when internal selected', () => {
+ component.gatewayNodeComponent = {
+ getSelectedHosts: (): any[] => [{ hostname: 'host1' }],
+ getSelectedHostnames: (): string[] => ['host1']
+ } as any;
+
+ component.groupForm.get('groupName').setValue('mtls-internal');
+ component.groupForm.get('pool').setValue('rbd');
+ component.groupForm.get('enableEncryption').setValue(true);
+ component.groupForm.get('encryptionKey').setValue('test-encryption-key');
+ component.groupForm.get('enableMtls').setValue(true);
+ component.groupForm.get('certificateType').setValue(component.CertificateType.internal);
+ component.groupForm.get('custom_sans').setValue(['gw1.local', '192.168.0.10']);
+
+ component.onSubmit();
+
+ expect(cephServiceService.create).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ service_type: 'nvmeof',
+ service_id: 'rbd.mtls-internal',
+ ssl: true,
+ enable_auth: true,
+ certificate_source: 'cephadm-signed',
+ custom_sans: ['gw1.local', '192.168.0.10']
+ })
+ );
+ });
+
+ it('should create service with inline mTLS when external selected', () => {
+ component.gatewayNodeComponent = {
+ getSelectedHosts: (): any[] => [{ hostname: 'host1' }],
+ getSelectedHostnames: (): string[] => ['host1']
+ } as any;
+
+ component.groupForm.get('groupName').setValue('mtls-external');
+ component.groupForm.get('pool').setValue('rbd');
+ component.groupForm.get('enableEncryption').setValue(true);
+ component.groupForm.get('encryptionKey').setValue('test-encryption-key');
+ component.groupForm.get('enableMtls').setValue(true);
+ component.groupForm.get('certificateType').setValue(component.CertificateType.external);
+ component.groupForm.get('rootCACert').setValue('root');
+ component.groupForm.get('clientCert').setValue('client-cert');
+ component.groupForm.get('clientKey').setValue('client-key');
+ component.groupForm.get('serverCert').setValue('server-cert');
+ component.groupForm.get('serverKey').setValue('server-key');
+
+ component.onSubmit();
+
+ expect(cephServiceService.create).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ service_id: 'rbd.mtls-external',
+ ssl: true,
+ enable_auth: true,
+ certificate_source: 'inline',
+ root_ca_cert: 'root',
+ client_cert: 'client-cert',
+ client_key: 'client-key',
+ server_cert: 'server-cert',
+ server_key: 'server-key'
+ })
+ );
+ });
+
it('should navigate to list view on success', () => {
component.gatewayNodeComponent = {
getSelectedHosts: (): any[] => [{ hostname: 'host1' }],
import { Component, OnInit, ViewChild } from '@angular/core';
import { UntypedFormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
import { CdForm } from '~/app/shared/forms/cd-form';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
import { FinishedTask } from '~/app/shared/models/finished-task';
-import { Router } from '@angular/router';
import { CdValidators } from '~/app/shared/forms/cd-validators';
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+enum CertificateType {
+ internal = 'internal',
+ external = 'external'
+}
+
@Component({
selector: 'cd-nvmeof-group-form',
templateUrl: './nvmeof-group-form.component.html',
group: string;
pageURL: string;
hasAvailableNodes = true;
+ CertificateType = CertificateType;
+ editing = false;
+ gatewayGroupName = '';
+ existingServiceData: any = null;
+ pools: Array<{ pool_name: string }> = [{ pool_name: 'rbd' }];
+ poolsLoading = false;
constructor(
private authStorageService: AuthStorageService,
private taskWrapperService: TaskWrapperService,
private cephServiceService: CephServiceService,
private nvmeofService: NvmeofService,
- private router: Router
+ private router: Router,
+ private route: ActivatedRoute
) {
super();
this.permission = this.authStorageService.getPermissions().nvmeof;
}
ngOnInit() {
- this.action = this.actionLabels.CREATE;
- this.createForm();
+ this.route.params.subscribe((params) => {
+ if (params['name']) {
+ this.editing = true;
+ this.gatewayGroupName = params['name'];
+ this.action = this.actionLabels.EDIT;
+ this.createForm();
+ this.loadGatewayGroupData(params['name']);
+ } else {
+ this.editing = false;
+ this.action = this.actionLabels.CREATE;
+ this.gatewayGroupName = '';
+ this.createForm();
+ }
+ });
}
createForm() {
- this.groupForm = new CdFormGroup({
- groupName: new UntypedFormControl(
- null,
- [
+ const groupNameValidators = this.editing
+ ? [
+ Validators.required,
+ (control) => {
+ const value = control.value;
+ return value && /[^a-zA-Z0-9_-]/.test(value) ? { invalidChars: true } : null;
+ }
+ ]
+ : [
Validators.required,
(control) => {
const value = control.value;
return value && /[^a-zA-Z0-9_-]/.test(value) ? { invalidChars: true } : null;
}
- ],
- [CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)]
- ),
+ ];
+
+ const groupNameAsyncValidators = this.editing
+ ? []
+ : [CdValidators.unique(this.nvmeofService.exists, this.nvmeofService)];
+
+ this.groupForm = new CdFormGroup({
+ groupName: new UntypedFormControl(null, groupNameValidators, groupNameAsyncValidators),
+ pool: new UntypedFormControl('rbd', {
+ validators: [Validators.required]
+ }),
unmanaged: new UntypedFormControl(false),
enableEncryption: new UntypedFormControl(false),
- encryptionConfig: new UntypedFormControl(null)
+ enableMtls: new UntypedFormControl(false),
+ encryptionKey: new UntypedFormControl('', [
+ CdValidators.composeIf(
+ {
+ enableEncryption: true
+ },
+ [Validators.required]
+ )
+ ]),
+ certificateType: new UntypedFormControl(CertificateType.internal),
+ custom_sans: new UntypedFormControl([]),
+ rootCACert: new UntypedFormControl(null, [
+ CdValidators.composeIf(
+ {
+ enableEncryption: true,
+ certificateType: 'external'
+ },
+ [Validators.required]
+ )
+ ]),
+ clientCert: new UntypedFormControl(null, [
+ CdValidators.composeIf(
+ {
+ enableEncryption: true,
+ certificateType: 'external'
+ },
+ [Validators.required]
+ )
+ ]),
+ clientKey: new UntypedFormControl(null, [
+ CdValidators.composeIf(
+ {
+ enableEncryption: true,
+ certificateType: 'external'
+ },
+ [Validators.required]
+ )
+ ]),
+ serverCert: new UntypedFormControl(null, [
+ CdValidators.composeIf(
+ {
+ enableEncryption: true,
+ certificateType: 'external'
+ },
+ [Validators.required]
+ )
+ ]),
+ serverKey: new UntypedFormControl(null, [
+ CdValidators.composeIf(
+ {
+ enableEncryption: true,
+ certificateType: CertificateType.external
+ },
+ [Validators.required]
+ )
+ ])
});
- this.groupForm.get('enableEncryption')?.valueChanges.subscribe((enabled) => {
- const encryptionControl = this.groupForm.get('encryptionConfig');
- if (enabled) {
- encryptionControl?.setValidators([Validators.required]);
- } else {
- encryptionControl?.clearValidators();
+ // Disable group name field in edit mode
+ if (this.editing) {
+ this.groupForm.get('groupName')?.disable();
+ }
+ }
+
+ loadGatewayGroupData(groupName: string) {
+ this.nvmeofService.listGatewayGroups().subscribe(
+ (gatewayGroups: any) => {
+ const groups = gatewayGroups?.[0] ?? [];
+ const group = groups.find((g: any) => g.spec?.group === groupName);
+
+ if (group) {
+ this.existingServiceData = group;
+ const spec = group.spec || {};
+
+ // Prefill form with existing data
+ this.groupForm.patchValue({
+ groupName: groupName,
+ pool: spec.pool || 'rbd',
+ unmanaged: spec.unmanaged || false,
+ enableEncryption: spec.ssl || false,
+ enableMtls: spec.ssl || false,
+ certificateType:
+ spec.certificate_source === 'inline'
+ ? CertificateType.external
+ : CertificateType.internal,
+ encryptionKey: spec.encryption_key || '',
+ custom_sans: spec.custom_sans || []
+ });
+
+ // If external certificates, load them
+ if (spec.certificate_source === 'inline') {
+ this.groupForm.patchValue({
+ rootCACert: spec.root_ca_cert || null,
+ clientCert: spec.client_cert || null,
+ clientKey: spec.client_key || null,
+ serverCert: spec.server_cert || null,
+ serverKey: spec.server_key || null
+ });
+ }
+ }
+ },
+ (_error) => {
+ // Error loading gateway group data
}
- encryptionControl?.updateValueAndValidity();
- });
+ );
}
onHostsLoaded(count: number): void {
return;
}
- const formValues = this.groupForm.value;
+ const formValues = this.groupForm.getRawValue(); // Use getRawValue to get disabled fields too
const selectedHostnames = this.gatewayNodeComponent?.getSelectedHostnames() || [];
if (selectedHostnames.length === 0) {
this.groupForm.setErrors({ cdSubmitButton: true });
return;
}
- let taskUrl = `service/${URLVerbs.CREATE}`;
- const serviceName = `${formValues.groupName}`;
+
+ const groupName = this.editing ? this.gatewayGroupName : formValues.groupName;
+ const pool = formValues.pool;
+ const serviceName = `${pool}.${groupName}`;
+
+ let taskUrl = this.editing ? `service/${URLVerbs.EDIT}` : `service/${URLVerbs.CREATE}`;
const serviceSpec: Record<string, any> = {
service_type: 'nvmeof',
service_id: serviceName,
- group: formValues.groupName,
+ pool: pool,
+ group: groupName,
placement: {
hosts: selectedHostnames
},
unmanaged: formValues.unmanaged
};
- if (formValues.enableEncryption && formValues.encryptionConfig) {
- serviceSpec['encryption_key'] = formValues.encryptionConfig;
+ if (formValues.enableMtls) {
+ serviceSpec['ssl'] = true;
+ serviceSpec['enable_auth'] = true;
+ serviceSpec['certificate_source'] =
+ formValues.certificateType === CertificateType.internal ? 'cephadm-signed' : 'inline';
+
+ if (formValues.certificateType === CertificateType.internal) {
+ if (formValues.custom_sans && formValues.custom_sans.length > 0) {
+ serviceSpec['custom_sans'] = formValues.custom_sans;
+ }
+ } else {
+ serviceSpec['root_ca_cert'] = formValues.rootCACert;
+ serviceSpec['client_cert'] = formValues.clientCert;
+ serviceSpec['client_key'] = formValues.clientKey;
+ serviceSpec['server_cert'] = formValues.serverCert;
+ serviceSpec['server_key'] = formValues.serverKey;
+ }
}
+ const apiCall = this.editing
+ ? this.cephServiceService.update(serviceSpec)
+ : this.cephServiceService.create(serviceSpec);
+
this.taskWrapperService
.wrapTaskAroundCall({
task: new FinishedTask(taskUrl, {
service_name: `nvmeof.${serviceName}`
}),
- call: this.cephServiceService.create(serviceSpec)
+ call: apiCall
})
.subscribe({
complete: () => {
private goToListView() {
this.router.navigateByUrl('/block/nvmeof/gateways');
}
+
+ onFileUpload(event: any, controlName: string) {
+ const file = event.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e: any) => {
+ this.groupForm.get(controlName)?.setValue(e.target.result);
+ };
+ reader.readAsText(file);
+ }
+ }
+
+ onCertificateTypeChange(type: CertificateType) {
+ this.groupForm.get('certificateType')?.setValue(type);
+ if (type === CertificateType.internal) {
+ this.groupForm.patchValue({
+ rootCACert: null,
+ clientCert: null,
+ clientKey: null,
+ serverCert: null,
+ serverKey: null
+ });
+ } else {
+ this.groupForm.patchValue({
+ custom_sans: []
+ });
+ }
+ }
}
import { PageHeaderComponent } from './page-header/page-header.component';
import { SidebarLayoutComponent } from './sidebar-layout/sidebar-layout.component';
import { NumberWithUnitComponent } from './number-with-unit/number-with-unit.component';
-import { ProductiveCardComponent } from './productive-card/productive-card.component';
@NgModule({
imports: [
TagModule,
LinkModule,
LayerModule,
- ThemeModule,
- ProductiveCardComponent
+ ThemeModule
],
declarations: [
SparklineComponent,
}
</ng-template>
- <div [cdsStack]="'vertical'" [gap]="0">
+ <div [cdsStack]="'vertical'"
+ [gap]="0">
@if (details && details.length > 0) {
- <div [cdsStack]="'horizontal'" [gap]="6">
+ <div [cdsStack]="'horizontal'"
+ [gap]="5">
@for (detail of getVisibleDetails(); track detail.label) {
- <div [cdsStack]="'vertical'" [gap]="2">
+ <div [cdsStack]="'vertical'"
+ [gap]="2">
<label class="cds--type-label-01">{{ detail.label }}</label>
<div class="cds--type-body-01">
@switch (detail.type) {
onEditClick(): void {
this.editClicked.emit();
}
-
- @Input()
- columns = 4;
getVisibleDetails(): DetailItem[] {
return (this.details || []).filter((detail) => !detail.hidden);