]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Enable cephadm-signed TLS for NVMeOf
authorAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 15 Jun 2026 11:12:15 +0000 (16:42 +0530)
committerpujashahu <pshahu@redhat.com>
Fri, 3 Jul 2026 08:02:08 +0000 (13:32 +0530)
fixes: https://tracker.ceph.com/issues/77406
Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
(cherry picked from commit 9623f6847cecf7f2e79f461420fce4821fd1b159)

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts

index 0e7ecb82f24d6f6874f3226a4bc262c557d18d98..1fc6559d020accd6c9eae235f862f3a8a0ced3fa 100644 (file)
       }
 
       <!-- Certificate Management UI -->
-      @if (serviceForm.controls.ssl.value && ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'mgmt-gateway', 'nvmeof', 'nfs'].includes(serviceForm.controls.service_type.value)) {
-      <div>
-        <!-- Current Certificate Section - Only shown in Edit mode when certificate exists -->
-        @if (editing && currentCertificate?.has_certificate) {
-        <div class="form-item">
-          <label class="cds--label fw-bold"
-                 i18n>Current Certificate</label>
-          <div class="row">
-            <div class="col-6">
-              <label class="cds--label"
-                     i18n>Certificate</label>
-              <div>{{ currentCertificate.cert_name }}</div>
-            </div>
-            <div class="col-6">
-              <label class="cds--label"
-                     i18n>Valid Until</label>
-              <div>{{ currentCertificate.expiry_date | cdDate }} • {{ currentCertificate.days_to_expiration }} <span i18n>days left</span></div>
-            </div>
-          </div>
-          <div class="row mt-3">
-            <div class="col-6">
-              <label class="cds--label"
-                     i18n>Status</label>
-              <div class="align-items-center">
-                <cd-icon [type]="statusIconMap[currentCertificate.status] || statusIconMap['default']"></cd-icon>
-                @switch (currentCertificate.status) {
-                  @case ('valid') { <span i18n>Valid</span> }
-                  @case ('expiring') { <span i18n>Expiring soon</span> }
-                  @case ('expired') { <span i18n>Expired</span> }
-                  @case ('not_configured') { <span i18n>Not configured</span> }
-                  @case ('invalid') { <span i18n>Invalid</span> }
-                  @default { {{ currentCertificate.status }} }
-                }
-              </div>
-            </div>
-            <div class="col-6">
-              <label class="cds--label"
-                     i18n>Issuer</label>
-              <div>
-                @if (currentCertificate.signed_by === 'cephadm') {
-                <span i18n>Internal (Cephadm CA)</span>
-                } @else {
-                {{ currentCertificate.issuer || 'External' }}
-                }
-              </div>
-            </div>
-          </div>
-        </div>
-        }
-
-        <!-- Certificate Authority Selection -->
-        <div class="form-item">
-          <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.">
-            <cds-radio [value]="CertificateType.internal"
-                       (change)="onCertificateTypeChange(CertificateType.internal)"
-                       i18n>
-              Internal
-            </cds-radio>
-            <cds-radio [value]="CertificateType.external"
-                       (change)="onCertificateTypeChange(CertificateType.external)"
-                       i18n>
-              External
-            </cds-radio>
-          </cds-radio-group>
-        </div>
-
-        @if (showCertSourceChangeWarning) {
-          <cd-alert-panel type="warning"
-                          spacingClass="mb-3"
-                          i18n>
-            Changing the certificate source will redeploy the service daemons to apply the new certificate configuration.
-          </cd-alert-panel>
-        }
-
-        @if (serviceForm.controls.certificateType.value === CertificateType.internal) {
-        <cd-alert-panel type="info"
-                        spacingClass="mb-3"
-                        i18n>
-          Certificate will be generated automatically by Cephadm CA for internal certificate type.
-        </cd-alert-panel>
-        <div class="form-item">
-          <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>
-        @if (serviceForm.controls.service_type.value === 'rgw'
-             && serviceForm.controls.virtual_host_enabled.value) {
-        <div class="form-item">
-          <cds-checkbox i18n-label
-                        formControlName="wildcard_enabled">
-            Include wildcard certificate for bucket subdomains
-            <cd-help-text i18n>
-              Add wildcard certificates (*.domain) to allow SSL for all bucket subdomains. Required for virtual-host style with SSL.
-            </cd-help-text>
-          </cds-checkbox>
-        </div>
-        }
-        }
-      </div>
+      @if (serviceForm.controls.ssl.value && ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'mgmt-gateway', 'nfs'].includes(serviceForm.controls.service_type.value)) {
+      <ng-container *ngTemplateOutlet="certificateAuthorityManagement"></ng-container>
       }
 
       <!-- ssl_cert - Only show when SSL is enabled AND certificate type is external -->
         </fieldset>
       </div>
       @if (serviceForm.controls.enable_mtls.value) {
+      <ng-container *ngTemplateOutlet="certificateAuthorityManagement"></ng-container>
+
+      @if (serviceForm.controls.certificateType.value === CertificateType.external) {
       <!-- root_ca_cert -->
       <div class="form-item">
         <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'root_ca_cert', title: 'Root CA Certificate Input', helperText: 'Uploaded files will populate the Root CA certificate details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a Root CA certificate file, or paste the Root CA certificate PEM content directly.', invalidTemplate: invalidRootCaCertError, isRequired: true }"></ng-container>
       </div>
       }
       }
+      }
     </form>
   </section>
   <cd-form-button-panel (submitActionEvent)="onSubmit()"
   </span>
 </ng-template>
 
+<ng-template #certificateAuthorityManagement>
+  <div [formGroup]="serviceForm">
+  <!-- Current Certificate Section - Only shown in Edit mode when certificate exists -->
+  @if (editing && currentCertificate?.has_certificate) {
+  <div class="form-item">
+    <label class="cds--label cds--type-heading-compact-01"
+           i18n>Current Certificate</label>
+    <div class="row">
+      <div class="col-6">
+        <label class="cds--label"
+               i18n>Certificate</label>
+        <div>{{ currentCertificate.cert_name }}</div>
+      </div>
+      <div class="col-6">
+        <label class="cds--label"
+               i18n>Valid Until</label>
+        <div>{{ currentCertificate.expiry_date | cdDate }} • {{ currentCertificate.days_to_expiration }} <span i18n>days left</span></div>
+      </div>
+    </div>
+    <div class="row mt-3">
+      <div class="col-6">
+        <label class="cds--label"
+               i18n>Status</label>
+        <div class="align-items-center">
+          <cd-icon [type]="statusIconMap[currentCertificate.status] || statusIconMap['default']"></cd-icon>
+          @switch (currentCertificate.status) {
+            @case ('valid') { <span i18n>Valid</span> }
+            @case ('expiring') { <span i18n>Expiring soon</span> }
+            @case ('expired') { <span i18n>Expired</span> }
+            @case ('not_configured') { <span i18n>Not configured</span> }
+            @case ('invalid') { <span i18n>Invalid</span> }
+            @default { {{ currentCertificate.status }} }
+          }
+        </div>
+      </div>
+      <div class="col-6">
+        <label class="cds--label"
+               i18n>Issuer</label>
+        <div>
+          @if (currentCertificate.signed_by === 'cephadm') {
+          <span i18n>Internal (Cephadm CA)</span>
+          } @else {
+          {{ currentCertificate.issuer || 'External' }}
+          }
+        </div>
+      </div>
+    </div>
+  </div>
+  }
+
+  <!-- Certificate Authority Selection -->
+  <div class="form-item">
+    <label class="cds--label cds--type-heading-compact-01"
+           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]="CertificateType.internal"
+                 (change)="onCertificateTypeChange(CertificateType.internal)"
+                 i18n>
+        Internal
+      </cds-radio>
+      <cds-radio [value]="CertificateType.external"
+                 (change)="onCertificateTypeChange(CertificateType.external)"
+                 i18n>
+        External
+      </cds-radio>
+    </cds-radio-group>
+  </div>
+
+  @if (showCertSourceChangeWarning) {
+    <cd-alert-panel type="warning"
+                    spacingClass="mb-3"
+                    i18n>
+      Changing the certificate source will redeploy the service daemons to apply the new certificate configuration.
+    </cd-alert-panel>
+  }
+
+  @if (serviceForm.controls.certificateType.value === CertificateType.internal) {
+  <cd-alert-panel type="info"
+                  spacingClass="mb-3"
+                  i18n>
+    Certificate will be generated automatically by Cephadm CA for internal certificate type.
+  </cd-alert-panel>
+  <div class="form-item">
+    <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>
+  @if (serviceForm.controls.service_type.value === 'rgw'
+       && serviceForm.controls.virtual_host_enabled.value) {
+  <div class="form-item">
+    <cds-checkbox i18n-label
+                  formControlName="wildcard_enabled">
+      Include wildcard certificate for bucket subdomains
+      <cd-help-text i18n>
+        Add wildcard certificates (*.domain) to allow SSL for all bucket subdomains. Required for virtual-host style with SSL.
+      </cd-help-text>
+    </cds-checkbox>
+  </div>
+  }
+  }
+  </div>
+</ng-template>
+
 <ng-template #fileUploaderTextarea
              let-controlName="controlName"
              let-title="title"
index a1c2590e20f06bd10a6c27eedc56df0f461abc9d..b4fbab508ea2c5c3331d5921e5f7d577dcb35c18 100644 (file)
@@ -558,6 +558,22 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
         expect(server_key).toBeNull();
       });
 
+      it('should not show certs and keys field with internal mTLS', () => {
+        formHelper.setValue('enable_mtls', true);
+        formHelper.setValue('certificateType', 'internal');
+        fixture.detectChanges();
+        const root_ca_cert = fixture.debugElement.query(By.css('#root_ca_cert'));
+        const client_cert = fixture.debugElement.query(By.css('#client_cert'));
+        const client_key = fixture.debugElement.query(By.css('#client_key'));
+        const server_cert = fixture.debugElement.query(By.css('#server_cert'));
+        const server_key = fixture.debugElement.query(By.css('#server_key'));
+        expect(root_ca_cert).toBeNull();
+        expect(client_cert).toBeNull();
+        expect(client_key).toBeNull();
+        expect(server_cert).toBeNull();
+        expect(server_key).toBeNull();
+      });
+
       it('should submit nvmeof without mTLS', () => {
         component.onSubmit();
         expect(cephServiceService.create).toHaveBeenCalledWith({
@@ -572,6 +588,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
 
       it('should submit nvmeof with mTLS', () => {
         formHelper.setValue('enable_mtls', true);
+        formHelper.setValue('certificateType', 'external');
         formHelper.setValue('root_ca_cert', 'root_ca_cert');
         formHelper.setValue('client_cert', 'client_cert');
         formHelper.setValue('client_key', 'client_key');
@@ -585,6 +602,8 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
           unmanaged: false,
           group: 'default',
           enable_auth: true,
+          ssl: true,
+          certificate_source: 'inline',
           root_ca_cert: 'root_ca_cert',
           client_cert: 'client_cert',
           client_key: 'client_key',
@@ -592,6 +611,23 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA==
           server_key: 'server_key'
         });
       });
+
+      it('should submit nvmeof with internal mTLS', () => {
+        formHelper.setValue('enable_mtls', true);
+        formHelper.setValue('certificateType', 'internal');
+        component.onSubmit();
+        expect(cephServiceService.create).toHaveBeenCalledWith({
+          service_type: 'nvmeof',
+          service_id: 'rbd.default',
+          placement: {},
+          unmanaged: false,
+          pool: 'rbd',
+          group: 'default',
+          enable_auth: true,
+          ssl: true,
+          certificate_source: 'cephadm-signed'
+        });
+      });
     });
 
     describe('should test service smb', () => {
index 86a074fef8c4b9b4de4b8a86409c4a625e841795..6c10ff5a2ce0e60eaec64e81309f41ef07cbf1db 100644 (file)
@@ -238,7 +238,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           CdValidators.composeIf(
             {
               service_type: 'nvmeof',
-              enable_mtls: true
+              enable_mtls: true,
+              certificateType: CertificateType.external
             },
             [Validators.required]
           )
@@ -250,7 +251,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           CdValidators.composeIf(
             {
               service_type: 'nvmeof',
-              enable_mtls: true
+              enable_mtls: true,
+              certificateType: CertificateType.external
             },
             [Validators.required]
           )
@@ -262,7 +264,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           CdValidators.composeIf(
             {
               service_type: 'nvmeof',
-              enable_mtls: true
+              enable_mtls: true,
+              certificateType: CertificateType.external
             },
             [Validators.required]
           )
@@ -274,7 +277,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           CdValidators.composeIf(
             {
               service_type: 'nvmeof',
-              enable_mtls: true
+              enable_mtls: true,
+              certificateType: CertificateType.external
             },
             [Validators.required]
           )
@@ -286,7 +290,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           CdValidators.composeIf(
             {
               service_type: 'nvmeof',
-              enable_mtls: true
+              enable_mtls: true,
+              certificateType: CertificateType.external
             },
             [Validators.required]
           )
@@ -781,11 +786,19 @@ export class ServiceFormComponent extends CdForm implements OnInit {
             case 'nvmeof':
               this.serviceForm.get('group').setValue(response[0].spec.group);
               this.serviceForm.get('enable_mtls').setValue(response[0].spec?.enable_auth);
-              this.serviceForm.get('root_ca_cert').setValue(response[0].spec?.root_ca_cert);
-              this.serviceForm.get('client_cert').setValue(response[0].spec?.client_cert);
-              this.serviceForm.get('client_key').setValue(response[0].spec?.client_key);
-              this.serviceForm.get('server_cert').setValue(response[0].spec?.server_cert);
-              this.serviceForm.get('server_key').setValue(response[0].spec?.server_key);
+              if (response[0].spec?.enable_auth) {
+                if (response[0].spec?.certificate_source !== 'cephadm-signed') {
+                  this.serviceForm.get('certificateType').setValue(CertificateType.external);
+                }
+                if (response[0].spec?.['custom_sans']) {
+                  this.serviceForm.get('custom_sans').setValue(response[0].spec['custom_sans']);
+                }
+                this.serviceForm.get('root_ca_cert').setValue(response[0].spec?.root_ca_cert);
+                this.serviceForm.get('client_cert').setValue(response[0].spec?.client_cert);
+                this.serviceForm.get('client_key').setValue(response[0].spec?.client_key);
+                this.serviceForm.get('server_cert').setValue(response[0].spec?.server_cert);
+                this.serviceForm.get('server_key').setValue(response[0].spec?.server_key);
+              }
               break;
             case 'rgw':
               this.serviceForm
@@ -1329,11 +1342,22 @@ export class ServiceFormComponent extends CdForm implements OnInit {
         serviceSpec['group'] = values['group'];
         serviceSpec['enable_auth'] = values['enable_mtls'];
         if (values['enable_mtls']) {
-          serviceSpec['root_ca_cert'] = values['root_ca_cert'];
-          serviceSpec['client_cert'] = values['client_cert'];
-          serviceSpec['client_key'] = values['client_key'];
-          serviceSpec['server_cert'] = values['server_cert'];
-          serviceSpec['server_key'] = values['server_key'];
+          serviceSpec['ssl'] = true;
+          serviceSpec['certificate_source'] =
+            values['certificateType'] === CertificateType.internal ? 'cephadm-signed' : 'inline';
+          if (
+            values['certificateType'] === CertificateType.internal &&
+            values['custom_sans']?.length > 0
+          ) {
+            serviceSpec['custom_sans'] = values['custom_sans'];
+          }
+          if (values['certificateType'] === CertificateType.external) {
+            serviceSpec['root_ca_cert'] = values['root_ca_cert'];
+            serviceSpec['client_cert'] = values['client_cert'];
+            serviceSpec['client_key'] = values['client_key'];
+            serviceSpec['server_cert'] = values['server_cert'];
+            serviceSpec['server_key'] = values['server_key'];
+          }
         }
         break;
       case 'iscsi':
@@ -1583,7 +1607,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       return isExternalCert;
     }
 
-    const sslCertServices = ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'nvmeof', 'nfs'];
+    const sslCertServices = ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'nfs'];
     return isSslEnabled && isExternalCert && sslCertServices.includes(serviceType);
   }
 
@@ -1593,7 +1617,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       this.serviceForm.controls.certificateType?.value === CertificateType.external;
     const isSslEnabled = this.serviceForm.controls.ssl?.value;
 
-    const sslKeyServices = ['iscsi', 'grafana', 'oauth2-proxy', 'nvmeof', 'nfs', 'mgmt-gateway'];
+    const sslKeyServices = ['iscsi', 'grafana', 'oauth2-proxy', 'nfs', 'mgmt-gateway'];
     return isSslEnabled && isExternalCert && sslKeyServices.includes(serviceType);
   }