From 3544a4ad5dfdcf29ebe096eee45f3c9949d1de4d Mon Sep 17 00:00:00 2001 From: pujashahu Date: Fri, 31 Jul 2026 02:03:52 +0530 Subject: [PATCH] mgr/dashboard: Current security configuration should be visible in edit page of nvme gateway service Fixes: https://tracker.ceph.com/issues/78889 Signed-off-by: pujaoshahu --- .../src/app/ceph/block/block.module.ts | 4 +- .../nvmeof-gateway-node.component.ts | 12 +- .../nvmeof-group-form.component.html | 306 ++++++------------ .../nvmeof-group-form.component.spec.ts | 165 +++++++++- .../nvmeof-group-form.component.ts | 204 ++++++++---- .../certificate-authority-form.component.html | 237 ++++++-------- .../text-label-list.component.html | 7 +- 7 files changed, 537 insertions(+), 398 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index 07c3068df46..0633f9200da 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -11,6 +11,7 @@ import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggle 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 { CertificateAuthorityFormComponent } from '~/app/shared/components/certificate-authority-form/certificate-authority-form.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'; @@ -152,7 +153,8 @@ import { NvmeofEditAuthenticationComponent } from './nvmeof-edit-authentication/ ThemeModule, NvmeofSetupCardsComponent, NvmeofGatewayGroupFilterComponent, - TextLabelListComponent + TextLabelListComponent, + CertificateAuthorityFormComponent ], declarations: [ RbdListComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts index 01e4f48c88e..06a1b4c5571 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts @@ -91,6 +91,7 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges orchStatus: OrchestratorStatus | undefined; private destroy$ = new Subject(); private sub: Subscription | undefined; + private pendingHostReload = false; constructor( private authStorageService: AuthStorageService, @@ -303,9 +304,12 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges this.tableContext = context || this.tableContext || new CdTableFetchDataContext(() => undefined); if (this.isLoadingHosts) { + // Edit preselection may arrive while the initial fetch is in-flight + this.pendingHostReload = true; return; } this.isLoadingHosts = true; + this.pendingHostReload = false; if (this.sub) { this.sub.unsubscribe(); @@ -323,6 +327,10 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges .pipe( finalize(() => { this.isLoadingHosts = false; + if (this.pendingHostReload) { + this.pendingHostReload = false; + this.getHosts(this.tableContext); + } }) ) .subscribe({ @@ -452,9 +460,9 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges }, { label: $localize`Encryption`, - value: serviceSpec.spec?.enable_auth ? $localize`Enabled` : $localize`Disabled`, + value: serviceSpec.spec?.encryption_key ? $localize`Enabled` : $localize`Disabled`, type: 'status', - statusIcon: serviceSpec.spec?.enable_auth ? 'success' : 'error' + statusIcon: serviceSpec.spec?.encryption_key ? 'success' : 'error' }, { label: $localize`mTLS`, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html index 5f488596d45..610ca6092da 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.html @@ -87,225 +87,125 @@ cdsCol class="cds-pt-3 cds-pb-3" > - + +
- - -
-
-
- - Recommended -
- - Secures group metadata and unlocks advanced authentication features. - -
-
- - @if (!groupForm.controls.enableEncryption.value) { -
-
- - Encryption is required if you plan to use DH-CHAP or mTLS - authentication. - -
-
- } - - @if (groupForm.controls.enableEncryption.value) { -
-
- - Encryption key - - - This field is required. -
-
- } -
- - -
Enable encryption -
-
- -
- - Use mutual TLS (mTLS) to encrypt control and monitoring commands exchanged with - NVMe-oF gateways. - -
-
+ Recommended +
+ + Secures group metadata and unlocks advanced authentication features. + - @if (!groupForm.controls.enableMtls.value) { -
+ Encryption is required if you plan to use DH-CHAP or mTLS authentication. -
- - Disabling mTLS leaves gateway control and monitoring communication unencrypted. - -
-
- } + + } - @if (groupForm.controls.enableMtls.value) { -
+ + + }
- @if ( - groupForm.controls.enableMtls.value && - groupForm.controls.certificateType.value === CertificateType.internal - ) { -
-
+ +
+
+ + + Use mutual TLS (mTLS) to encrypt control and monitoring commands exchanged with + NVMe-oF gateways. + + + @if (!groupForm.controls.enableMtls.value) { - Certificate will be generated automatically by Cephadm CA for internal certificate - type. + Disabling mTLS leaves gateway control and monitoring communication unencrypted. - - -
+ + }
- } +
@if ( groupForm.controls.enableMtls.value && @@ -351,9 +251,9 @@ cdsTextArea id="rootCACertText" formControlName="rootCACert" + class="w-100" placeholder="Paste certificate or private key PEM content" i18n-placeholder - cols="100" rows="4" > @@ -407,9 +307,9 @@ cdsTextArea id="clientCertText" formControlName="clientCert" + class="w-100" placeholder="Paste certificate or private key PEM content" i18n-placeholder - cols="100" rows="4" > @@ -463,9 +363,9 @@ cdsTextArea id="clientKeyText" formControlName="clientKey" + class="w-100" placeholder="Paste certificate or private key PEM content" i18n-placeholder - cols="100" rows="4" > @@ -519,9 +419,9 @@ cdsTextArea id="serverCertText" formControlName="serverCert" + class="w-100" placeholder="Paste certificate or private key PEM content" i18n-placeholder - cols="100" rows="4" > @@ -575,9 +475,9 @@ cdsTextArea id="serverKeyText" formControlName="serverKey" + class="w-100" placeholder="Paste certificate or private key PEM content" i18n-placeholder - cols="100" rows="4" > diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.spec.ts index 4e6b297418c..5ac4136cf4f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.spec.ts @@ -2,15 +2,17 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { of } from 'rxjs'; import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; +import { PaginateObservable } from '~/app/shared/api/paginate.model'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { SharedModule } from '~/app/shared/shared.module'; +import { CertificateType } from '~/app/shared/models/service.interface'; import { NvmeofGroupFormComponent } from './nvmeof-group-form.component'; import { CheckboxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular'; @@ -32,7 +34,13 @@ describe('NvmeofGroupFormComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [NvmeofGroupFormComponent], - providers: [NgbActiveModal], + providers: [ + NgbActiveModal, + { + provide: ActivatedRoute, + useValue: { params: of({}) } + } + ], imports: [ HttpClientTestingModule, NgbTypeaheadModule, @@ -132,7 +140,7 @@ describe('NvmeofGroupFormComponent', () => { expect(cephServiceService.create).toHaveBeenCalledWith({ service_type: 'nvmeof', - service_id: 'nvmeof.default', + service_id: 'default', group: 'default', placement: { hosts: ['host1', 'host2'] @@ -196,7 +204,7 @@ describe('NvmeofGroupFormComponent', () => { expect(cephServiceService.create).toHaveBeenCalledWith( jasmine.objectContaining({ service_type: 'nvmeof', - service_id: 'nvmeof.mtls-internal', + service_id: 'mtls-internal', ssl: true, enable_auth: true, certificate_source: 'cephadm-signed', @@ -226,7 +234,7 @@ describe('NvmeofGroupFormComponent', () => { expect(cephServiceService.create).toHaveBeenCalledWith( jasmine.objectContaining({ - service_id: 'nvmeof.mtls-external', + service_id: 'mtls-external', ssl: true, enable_auth: true, certificate_source: 'inline', @@ -251,4 +259,151 @@ describe('NvmeofGroupFormComponent', () => { expect(router.navigateByUrl).toHaveBeenCalledWith('/block/nvmeof/gateways'); }); }); + + describe('edit mode', () => { + const mockCertificate = { + cert_name: 'nvmeof.Test1', + scope: 'service', + requires_certificate: true, + status: 'valid', + days_to_expiration: 90, + signed_by: 'cephadm', + has_certificate: true, + certificate_source: 'cephadm-signed', + expiry_date: '2026-12-01T00:00:00Z', + issuer: 'Cephadm', + common_name: 'nvmeof.Test1' + }; + + const mockService = { + service_name: 'nvmeof.Test1', + service_type: 'nvmeof', + service_id: 'Test1', + unmanaged: false, + certificate: mockCertificate, + placement: { hosts: ['ceph-node-01', 'ceph-node-02'] }, + spec: { + group: 'Test1', + encryption_key: 'existing-encryption-key', + enable_auth: true, + ssl: true, + certificate_source: 'cephadm-signed', + custom_sans: ['gw1.local'] + } + }; + + beforeEach(() => { + spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[mockService]])); + spyOn(cephServiceService, 'list').and.returnValue( + new PaginateObservable(of([mockService])) + ); + + component.editing = true; + component.gatewayGroupName = 'Test1'; + component.action = 'Edit'; + component.createForm(); + component.loadGatewayGroupData('Test1'); + form = component.groupForm; + }); + + it('should resolve group by spec.group and preselect hosts', () => { + expect(nvmeofService.listGatewayGroups).toHaveBeenCalled(); + expect(cephServiceService.list).toHaveBeenCalled(); + expect(component.preSelectedHostnames).toEqual(['ceph-node-01', 'ceph-node-02']); + expect(form.controls.groupName.value).toBe('Test1'); + }); + + it('should prepopulate encryption checkbox and key when encryption_key is present', () => { + expect(form.controls.enableEncryption.value).toBe(true); + expect(form.controls.encryptionKey.value).toBe('existing-encryption-key'); + }); + + it('should prepopulate mTLS from enable_auth and load current certificate', () => { + expect(form.controls.enableMtls.value).toBe(true); + expect(form.controls.certificateType.value).toBe(CertificateType.internal); + expect(form.controls.custom_sans.value).toEqual(['gw1.local']); + expect(component.currentCertificate).toEqual(mockCertificate); + expect(component.currentSpecCertificateSource).toBe('cephadm-signed'); + }); + + it('should prepopulate external cert fields when certificate_source is inline', () => { + const inlineService = { + ...mockService, + certificate: { + ...mockCertificate, + signed_by: 'external', + certificate_source: 'inline' + }, + spec: { + ...mockService.spec, + certificate_source: 'inline', + root_ca_cert: 'root-pem', + client_cert: 'client-pem', + client_key: 'client-key-pem', + server_cert: 'server-pem', + server_key: 'server-key-pem' + } + }; + (nvmeofService.listGatewayGroups as jasmine.Spy).and.returnValue(of([[inlineService]])); + (cephServiceService.list as jasmine.Spy).and.returnValue( + new PaginateObservable(of([inlineService])) + ); + + component.createForm(); + component.loadGatewayGroupData('Test1'); + + expect(component.groupForm.controls.certificateType.value).toBe(CertificateType.external); + expect(component.groupForm.controls.rootCACert.value).toBe('root-pem'); + expect(component.groupForm.controls.clientCert.value).toBe('client-pem'); + expect(component.groupForm.controls.clientKey.value).toBe('client-key-pem'); + expect(component.groupForm.controls.serverCert.value).toBe('server-pem'); + expect(component.groupForm.controls.serverKey.value).toBe('server-key-pem'); + }); + + it('should show cert source change warning when switching type on edit', () => { + component.onCertificateTypeChange(CertificateType.external); + expect(component.showCertSourceChangeWarning).toBe(true); + + component.onCertificateTypeChange(CertificateType.internal); + expect(component.showCertSourceChangeWarning).toBe(false); + }); + + it('should keep form valid when encryption key is cleared or encryption is unchecked', () => { + form.controls.encryptionKey.setValue(''); + expect(form.valid).toBe(true); + expect(component.isCreateDisabled).toBe(false); + + form.controls.enableEncryption.setValue(false); + expect(form.controls.encryptionKey.value).toBeNull(); + expect(form.valid).toBe(true); + expect(component.isCreateDisabled).toBe(false); + }); + + it('should update service on submit in edit mode using existing service_id', () => { + spyOn(cephServiceService, 'update').and.returnValue(of({})); + spyOn(taskWrapperService, 'wrapTaskAroundCall').and.callFake(({ call }) => call); + spyOn(router, 'navigateByUrl'); + + component.gatewayNodeComponent = { + getSelectedHosts: (): any[] => [{ hostname: 'ceph-node-01' }], + getSelectedHostnames: (): string[] => ['ceph-node-01'] + } as any; + + component.onSubmit(); + + expect(cephServiceService.update).toHaveBeenCalledWith( + jasmine.objectContaining({ + service_type: 'nvmeof', + service_id: 'Test1', + group: 'Test1', + placement: { hosts: ['ceph-node-01'] }, + encryption_key: 'existing-encryption-key', + ssl: true, + enable_auth: true, + certificate_source: 'cephadm-signed' + }) + ); + expect(router.navigateByUrl).toHaveBeenCalledWith('/block/nvmeof/gateways'); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts index b03d8028139..58dd042bc8f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-group-form/nvmeof-group-form.component.ts @@ -1,6 +1,10 @@ +import { HttpParams } from '@angular/common/http'; import { Component, OnInit, ViewChild } from '@angular/core'; import { UntypedFormControl, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { of } from 'rxjs'; +import { catchError, switchMap } from 'rxjs/operators'; + 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'; @@ -13,7 +17,11 @@ import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { CdValidators } from '~/app/shared/forms/cd-validators'; import { NvmeofService } from '~/app/shared/api/nvmeof.service'; -import { CertificateType } from '~/app/shared/models/service.interface'; +import { + CephServiceCertificate, + CephServiceSpec, + CertificateType +} from '~/app/shared/models/service.interface'; @Component({ selector: 'cd-nvmeof-group-form', @@ -34,7 +42,11 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { hasAvailableNodes = true; editing = false; gatewayGroupName = ''; - existingServiceData: any = null; + existingServiceData: CephServiceSpec | null = null; + preSelectedHostnames: string[] = []; + currentCertificate: CephServiceCertificate = null; + currentSpecCertificateSource = ''; + showCertSourceChangeWarning = false; constructor( private authStorageService: AuthStorageService, @@ -97,11 +109,18 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { }); this.groupForm.get('enableEncryption')?.valueChanges.subscribe((enabled) => { - if (!enabled) { - return; - } const encryptionConfigControl = this.groupForm.get('encryptionConfig'); const encryptionKeyControl = this.groupForm.get('encryptionKey'); + + if (!enabled) { + // Encryption is optional — clearing the toggle must not leave the form invalid + encryptionKeyControl?.setValue(null, { emitEvent: false }); + encryptionConfigControl?.setValue(null, { emitEvent: false }); + encryptionKeyControl?.setErrors(null); + encryptionConfigControl?.setErrors(null); + return; + } + if (!encryptionKeyControl?.value && encryptionConfigControl?.value) { encryptionKeyControl.setValue(encryptionConfigControl.value, { emitEvent: false }); } @@ -112,43 +131,90 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { } 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 || {}; - - this.groupForm.patchValue({ - groupName: groupName, - 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 (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 - }); + // Resolve by spec.group — service_name may be nvmeof. or nvmeof.. + this.nvmeofService + .listGatewayGroups() + .pipe( + switchMap((gatewayGroups: CephServiceSpec[][]) => { + const groups = gatewayGroups?.[0] ?? []; + const group = groups.find((g: CephServiceSpec) => g.spec?.group === groupName); + if (!group) { + return of(null); } + + const serviceName = group.service_name; + if (!serviceName) { + return of(group); + } + + // Enrich with certificate metadata (same as service edit form) + return this.cephServiceService + .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), serviceName) + .observable.pipe( + catchError(() => of(group)), + switchMap((response: CephServiceSpec[] | CephServiceSpec) => { + if (Array.isArray(response) && response[0]) { + return of({ + ...group, + ...response[0], + // Prefer placement/hosts from gateway-group listing + placement: group.placement || response[0].placement, + // Prefer enriched service spec (certs / encryption_key) + spec: { ...group.spec, ...response[0].spec } + } as CephServiceSpec); + } + return of(group); + }) + ); + }) + ) + .subscribe((group: CephServiceSpec | null) => { + if (!group) { + return; } - }, - (_error) => { - // Error loading gateway group data - } - ); + this.populateFormFromService(group, groupName); + }); + } + + private populateFormFromService(group: CephServiceSpec, groupName: string) { + this.existingServiceData = group; + const spec = group.spec || ({} as any); + const encryptionKey = spec.encryption_key || ''; + const enableMtls = !!spec.enable_auth; + + this.preSelectedHostnames = group.placement?.hosts || []; + + if (group.certificate) { + this.currentCertificate = group.certificate; + } + if (spec.certificate_source) { + this.currentSpecCertificateSource = spec.certificate_source; + } + + this.groupForm.patchValue({ + groupName: groupName, + unmanaged: group.unmanaged || false, + enableEncryption: !!encryptionKey, + encryptionKey: encryptionKey, + encryptionConfig: encryptionKey, + enableMtls: enableMtls, + certificateType: + enableMtls && spec.certificate_source !== 'cephadm-signed' + ? CertificateType.external + : CertificateType.internal, + custom_sans: spec.custom_sans || [] + }); + + // Cert PEM fields are optional on edit; prepopulate when present. + if (enableMtls) { + 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 + }); + } } onHostsLoaded(count: number): void { @@ -156,7 +222,7 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { } get isCreateDisabled(): boolean { - if (!this.hasAvailableNodes) { + if (!this.hasAvailableNodes && !(this.editing && this.preSelectedHostnames.length > 0)) { return true; } if (!this.groupForm) { @@ -172,16 +238,22 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { if (errors && errors.cdSubmitButton) { return true; } - if (this.gatewayNodeComponent) { - const selected = this.gatewayNodeComponent.getSelectedHostnames?.() || []; - if (selected.length === 0) { - return true; - } + if (this.getSelectedOrPreselectedHosts().length === 0) { + return true; } return false; } + private getSelectedOrPreselectedHosts(): string[] { + const selected = this.gatewayNodeComponent?.getSelectedHostnames?.() || []; + if (selected.length > 0) { + return selected; + } + // Edit: fall back to loaded placement while table selection syncs + return this.editing ? this.preSelectedHostnames : []; + } + onSubmit() { if (this.groupForm.invalid) { return; @@ -193,19 +265,24 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { } const formValues = this.groupForm.getRawValue(); - const selectedHostnames = this.gatewayNodeComponent?.getSelectedHostnames() || []; + const selectedHostnames = this.getSelectedOrPreselectedHosts(); if (selectedHostnames.length === 0) { this.groupForm.setErrors({ cdSubmitButton: true }); return; } const groupName = this.editing ? this.gatewayGroupName : formValues.groupName; - const serviceName = `nvmeof.${groupName}`; + // Match service-form: service_id is the group name (not nvmeof.) + const serviceId = this.editing ? this.existingServiceData?.service_id || groupName : groupName; + const serviceName = + this.editing && this.existingServiceData?.service_name + ? this.existingServiceData.service_name + : `nvmeof.${serviceId}`; const taskUrl = this.editing ? `service/${URLVerbs.EDIT}` : `service/${URLVerbs.CREATE}`; const serviceSpec: Record = { service_type: 'nvmeof', - service_id: serviceName, + service_id: serviceId, group: groupName, placement: { hosts: selectedHostnames @@ -234,11 +311,21 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { } if (formValues.certificateType === CertificateType.external) { - 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; + if (formValues.rootCACert) { + serviceSpec['root_ca_cert'] = formValues.rootCACert; + } + if (formValues.clientCert) { + serviceSpec['client_cert'] = formValues.clientCert; + } + if (formValues.clientKey) { + serviceSpec['client_key'] = formValues.clientKey; + } + if (formValues.serverCert) { + serviceSpec['server_cert'] = formValues.serverCert; + } + if (formValues.serverKey) { + serviceSpec['server_key'] = formValues.serverKey; + } } } @@ -282,6 +369,15 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit { onCertificateTypeChange(type: CertificateType): void { this.groupForm.get('certificateType')?.setValue(type); + + if (this.editing && this.currentCertificate?.has_certificate) { + const originalSource = + this.currentSpecCertificateSource === 'cephadm-signed' + ? CertificateType.internal + : CertificateType.external; + this.showCertSourceChangeWarning = type !== originalSource; + } + if (type === CertificateType.internal) { this.groupForm.patchValue({ rootCACert: null, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/certificate-authority-form/certificate-authority-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/certificate-authority-form/certificate-authority-form.component.html index 81578747d32..a95438d6795 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/certificate-authority-form/certificate-authority-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/certificate-authority-form/certificate-authority-form.component.html @@ -1,141 +1,118 @@ -
+
@if (editing && currentCertificate?.has_certificate) { -
+
Current Certificate
-
Certificate + {{ currentCertificate.cert_name }} - Certificate - {{ currentCertificate.cert_name }} -
-
Valid Until - Valid Until - - {{ currentCertificate.expiry_date | cdDate }} • - {{ currentCertificate.days_to_expiration }} - days left - -
+ + {{ currentCertificate.expiry_date | cdDate }} • + {{ currentCertificate.days_to_expiration }} + days left +
-
-
-
Status - Status -
- - @switch (currentCertificate.status) { - @case ('valid') { - Valid - } - @case ('expiring') { - Expiring soon - } - @case ('expired') { - Expired - } - @case ('not_configured') { - Not configured - } - @case ('invalid') { - Invalid - } - @default { - {{ currentCertificate.status }} - } +
+ + @switch (currentCertificate.status) { + @case ('valid') { + Valid } -
+ @case ('expiring') { + Expiring soon + } + @case ('expired') { + Expired + } + @case ('not_configured') { + Not configured + } + @case ('invalid') { + Invalid + } + @default { + {{ currentCertificate.status }} + } + }
-
Issuer - Issuer - - @if (currentCertificate.signed_by === 'cephadm') { - Internal (Cephadm CA) - } @else { - {{ currentCertificate.issuer || 'External' }} - } - -
+ + @if (currentCertificate.signed_by === 'cephadm') { + Internal (Cephadm CA) + } @else { + {{ currentCertificate.issuer || 'External' }} + } +
} -
+
Changing the certificate source will redeploy the service daemons to apply the new certificate @@ -178,37 +155,33 @@ @if (formGroup.controls.certificateType.value === CertificateType.internal) { Certificate will be generated automatically by Cephadm CA for internal certificate type. -
- - -
+ + @if ( formGroup.controls.service_type?.value === 'rgw' && formGroup.controls.virtual_host_enabled?.value ) { -
- - Include wildcard certificate for bucket subdomains - - Add wildcard certificates (*.domain) to allow SSL for all bucket subdomains. Required - for virtual-host style with SSL. - - -
+ + Include wildcard certificate for bucket subdomains + + Add wildcard certificates (*.domain) to allow SSL for all bucket subdomains. Required for + virtual-host style with SSL. + + } }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/text-label-list/text-label-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/text-label-list/text-label-list.component.html index 37c2da3cf85..f4d0ed89366 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/text-label-list/text-label-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/text-label-list/text-label-list.component.html @@ -4,6 +4,12 @@ i18n-helperText i18n > + @if ($index === 0) { + {{ label }} + } @else { + + ​ + }
}
- {{ $index === 0 ? label : '' }} }