]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: Implement backword support for Create existing nvme-oF gateway groups...
authorpujashahu <pshahu@redhat.com>
Wed, 24 Jun 2026 08:08:54 +0000 (13:38 +0530)
committerpujashahu <pshahu@redhat.com>
Fri, 3 Jul 2026 08:11:19 +0000 (13:41 +0530)
Fixes : https://tracker.ceph.com/issues/77632

Signed-off-by: pujaoshahu <pshahu@redhat.com>
(cherry picked from commit 26b7e9e4849f8f0f1d044cb23cc94a5ce910061d)

12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-group/nvmeof-gateway-group.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/details-card/details-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/details-card/details-card.component.ts

index 368a7de95b71de598041a50992f43d9b8d3bf511..7dbdfbbd518ee8c04ff226a938914a4525adf29b 100644 (file)
@@ -10,6 +10,7 @@ import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
 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';
@@ -147,7 +148,9 @@ import { NvmeofGatewayGroupFilterComponent } from './nvmeof-gateway-group-filter
     LayoutModule,
     ThemeModule,
     NvmeofSetupCardsComponent,
-    NvmeofGatewayGroupFilterComponent
+    NvmeofGatewayGroupFilterComponent,
+    TextLabelListComponent
+    // ProductiveCardComponent
   ],
   declarations: [
     RbdListComponent,
@@ -378,6 +381,17 @@ const routes: Routes = [
               }
             }
           },
+          {
+            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,
index 5084e98a14b72bf9faa13c884405c9c3f1161350..435410a6acaac020f5d2d1d6a66c2816d5e488f2 100644 (file)
@@ -1,14 +1,12 @@
 <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>
index b89d8bc9a51437ae2459645f03caaa19e93ca6f5..9b00fff75333bb709e0139f6ef3acba7390c3bf2 100644 (file)
@@ -76,6 +76,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
   gatewayCount = 0;
   selectedGatewayDetails: DetailItem[] = [];
   private lastGroupCount = 0;
+  showDetailsCard = false;
 
   viewUrl = `/${BASE_URL}/view`;
   icons = Icons;
@@ -129,14 +130,6 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
       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,
@@ -153,7 +146,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
       canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection
     };
 
-    this.tableActions = [createAction, editAction, viewAction, deleteAction];
+    this.tableActions = [createAction, viewAction, deleteAction];
 
     this.gatewayGroup$ = this.subject.pipe(
       switchMap(() =>
@@ -217,6 +210,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
   updateSelection(selection: CdTableSelection): void {
     this.selection = selection;
     this.selectedGatewayDetails = this.buildGatewayDetails(selection.first());
+    this.showDetailsCard = false;
   }
 
   deleteGatewayGroupModal() {
@@ -275,6 +269,11 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
           })
           .pipe(
             tap({
+              next: () => {
+                this.table.data = this.table.data.filter(
+                  (row: CephServiceSpec) => row.spec?.group !== selectedGroup.spec.group
+                );
+              },
               complete: () => {
                 this.nvmeofStateService.requestRefresh();
               }
@@ -292,6 +291,14 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
     });
   }
 
+  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[]]) => {
@@ -335,14 +342,6 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
     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 [];
@@ -368,7 +367,7 @@ export class NvmeofGatewayGroupComponent implements OnInit, OnDestroy {
       },
       {
         label: $localize`mTLS`,
-        value: selectedGroup.spec?.enable_mtls ? $localize`Enabled` : $localize`Disabled`,
+        value: selectedGroup.spec?.enable_auth ? $localize`Enabled` : $localize`Disabled`,
         type: 'status'
       }
     ];
index 7e420bfe86ee08aba48d183348fe7a88fcf7d13d..0a97671fa3b1d685b415f32dfbb99a90c317b487 100644 (file)
@@ -1,8 +1,7 @@
 <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>
   }
 
index b0595304e7445be9ec9204466b5ec7ef242179fd..98e7190faf206a92733d3974096a600fc1218a42 100644 (file)
@@ -380,9 +380,9 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy {
       },
       {
         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'
       }
     ];
   }
index bdac48c78b206e5d96d11e6cb003471828ae43e5..8cd803d79aa277719cee3ea7a1ff6c0fc349ced8 100644 (file)
 
         </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>
index 5b60872cd1ba7837af58ab1e2ae599b9239a5dd9..640d7a7d0bc1ef405d43ed26eb92f552ea58f898 100644 (file)
@@ -65,6 +65,7 @@ describe('NvmeofGroupFormComponent', () => {
     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', () => {
@@ -169,6 +170,69 @@ describe('NvmeofGroupFormComponent', () => {
       );
     });
 
+    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' }],
index f55ac8f735785a2f89561aaa16469f676a7d1c37..f27cbec917dcb5cf7b806d3f78efc25a32837f69 100644 (file)
@@ -1,5 +1,6 @@
 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';
@@ -10,10 +11,14 @@ import { NvmeofGatewayNodeComponent } from '../nvmeof-gateway-node/nvmeof-gatewa
 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',
@@ -30,6 +35,12 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
   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,
@@ -37,7 +48,8 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
     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;
@@ -45,37 +57,155 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
   }
 
   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 {
@@ -119,35 +249,59 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
       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: () => {
@@ -162,4 +316,32 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
   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: []
+      });
+    }
+  }
 }
index 294dd0ad5f28545c76c3a61118dd71bc6bfa5e65..91a5cbced6af39472fa65225a32a4474bde4eef8 100644 (file)
@@ -138,7 +138,6 @@ import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.componen
 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: [
@@ -187,8 +186,7 @@ import { ProductiveCardComponent } from './productive-card/productive-card.compo
     TagModule,
     LinkModule,
     LayerModule,
-    ThemeModule,
-    ProductiveCardComponent
+    ThemeModule
   ],
   declarations: [
     SparklineComponent,
index d60b1f3b3454ea2ab286df4149c7f672334ec874..b1595fbcdf2d9da26e69fed05ae54b8b28115efc 100644 (file)
     }
   </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) {
index 7cfed7235e0a16d3c462f5be5df9e72f88b09239..9b2fad7afb85d2366ae532ebfb97e105a5527e80 100644 (file)
@@ -43,9 +43,6 @@ export class DetailsCardComponent {
   onEditClick(): void {
     this.editClicked.emit();
   }
-  
-  @Input()
-  columns = 4;
 
   getVisibleDetails(): DetailItem[] {
     return (this.details || []).filter((detail) => !detail.hidden);