From 6110d7a96a419c471cbb0df3b5fe3162ff3c11b5 Mon Sep 17 00:00:00 2001 From: Afreen Date: Fri, 31 May 2024 13:24:27 +0530 Subject: [PATCH] mgr/dashboard: Configure NVMe/TCP Fixes https://tracker.ceph.com/issues/63686 - creation of Nvme-oF/TCP service - deletion of Nvme-oF/TCP service - edit/update Nvme-oF/TCP service - added unit tests for Nvme-oF/TCP service - changed Id -> Service Name - added prefix of service type in service name (similar to in fs access) - service name and pool are required fields for nvmeof - placement count now takes default value as mentioned in cephadm - slight refactors - prepopulate serviceId for each service type setServiceId() - in case serviceId is same as servcie type then do not add create service name with. format Signed-off-by: Afreen (cherry picked from commit c6cf91766c45cf766c1d5a3851c2e956a80ac9ee) --- .../block/rbd-form/rbd-form.component.html | 2 +- .../service-form/service-form.component.html | 122 +++++--- .../service-form.component.spec.ts | 36 +++ .../service-form/service-form.component.ts | 287 ++++++++++++------ 4 files changed, 304 insertions(+), 143 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html index af6cd3963651a..cb199fe4af4ae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html @@ -75,7 +75,7 @@ i18n>Loading... + i18n>-- No block pools available -- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html index e7278a09868c7..62333d3391b74 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html @@ -15,6 +15,7 @@ (click)="createMultisiteSetup()"> Click here to create a new Realm/Zone Group/Zone +
- +
+ Number of deamons that will be deployed The value must be at least 1. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts index 4f71abcec7a7d..db1e7851c2e4a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts @@ -387,6 +387,33 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA== }); }); + describe('should test service nvmeof', () => { + beforeEach(() => { + formHelper.setValue('service_type', 'nvmeof'); + formHelper.setValue('service_id', 'svc'); + formHelper.setValue('pool', 'xyz'); + }); + + it('should submit nvmeof', () => { + component.onSubmit(); + expect(cephServiceService.create).toHaveBeenCalledWith({ + service_type: 'nvmeof', + service_id: 'svc', + placement: {}, + unmanaged: false, + pool: 'xyz' + }); + }); + + it('should throw error when there is no service id', () => { + formHelper.expectErrorChange('service_id', '', 'required'); + }); + + it('should throw error when there is no pool', () => { + formHelper.expectErrorChange('pool', '', 'required'); + }); + }); + describe('should test service smb', () => { beforeEach(() => { formHelper.setValue('service_type', 'smb'); @@ -608,6 +635,15 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA== expect(serviceType.disabled).toBeTruthy(); expect(serviceId.disabled).toBeTruthy(); }); + + it('should not edit pools for nvmeof service', () => { + component.serviceType = 'nvmeof'; + formHelper.setValue('service_type', 'nvmeof'); + component.ngOnInit(); + fixture.detectChanges(); + const poolId = fixture.debugElement.query(By.css('#pool')).nativeElement; + expect(poolId.disabled).toBeTruthy(); + }); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts index c0f66ed33626f..6142f7457c224 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts @@ -7,12 +7,14 @@ import { NgbActiveModal, NgbModalRef, NgbTypeahead } from '@ng-bootstrap/ng-boot import _ from 'lodash'; import { forkJoin, merge, Observable, Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { Pool } from '~/app/ceph/pool/pool'; import { CreateRgwServiceEntitiesComponent } from '~/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component'; import { RgwRealm, RgwZonegroup, RgwZone } from '~/app/ceph/rgw/models/rgw-multisite'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { HostService } from '~/app/shared/api/host.service'; import { PoolService } from '~/app/shared/api/pool.service'; +import { RbdService } from '~/app/shared/api/rbd.service'; import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; @@ -68,7 +70,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { labels: string[]; labelClick = new Subject(); labelFocus = new Subject(); - pools: Array; + pools: Array; + rbdPools: Array; services: Array = []; pageURL: string; serviceList: CephServiceSpec[]; @@ -94,6 +97,7 @@ export class ServiceFormComponent extends CdForm implements OnInit { private formBuilder: CdFormBuilder, private hostService: HostService, private poolService: PoolService, + private rbdService: RbdService, private router: Router, private taskWrapperService: TaskWrapperService, public timerService: TimerService, @@ -145,6 +149,9 @@ export class ServiceFormComponent extends CdForm implements OnInit { CdValidators.requiredIf({ service_type: 'iscsi' }), + CdValidators.requiredIf({ + service_type: 'nvmeof' + }), CdValidators.requiredIf({ service_type: 'ingress' }), @@ -176,11 +183,15 @@ export class ServiceFormComponent extends CdForm implements OnInit { count: [null, [CdValidators.number(false)]], unmanaged: [false], // iSCSI + // NVMe/TCP pool: [ null, [ CdValidators.requiredIf({ service_type: 'iscsi' + }), + CdValidators.requiredIf({ + service_type: 'nvmeof' }) ] ], @@ -457,8 +468,9 @@ export class ServiceFormComponent extends CdForm implements OnInit { this.hostService.getLabels().subscribe((resp: string[]) => { this.labels = resp; }); - this.poolService.getList().subscribe((resp: Array) => { + this.poolService.getList().subscribe((resp: Pool[]) => { this.pools = resp; + this.rbdPools = this.pools.filter(this.rbdService.isRBDPool); }); if (this.editing) { @@ -495,12 +507,14 @@ export class ServiceFormComponent extends CdForm implements OnInit { this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key); } break; + case 'nvmeof': + this.serviceForm.get('pool').setValue(response[0].spec.pool); + break; case 'rgw': this.serviceForm .get('rgw_frontend_port') .setValue(response[0].spec?.rgw_frontend_port); - this.getServiceIds( - 'rgw', + this.setRgwFields( response[0].spec?.rgw_realm, response[0].spec?.rgw_zonegroup, response[0].spec?.rgw_zone @@ -595,7 +609,7 @@ export class ServiceFormComponent extends CdForm implements OnInit { } } - getDefaultsEntities( + getDefaultsEntitiesForRgw( defaultRealmId: string, defaultZonegroupId: string, defaultZoneId: string @@ -625,100 +639,169 @@ export class ServiceFormComponent extends CdForm implements OnInit { }; } - getServiceIds( - selectedServiceType: string, - realm_name?: string, - zonegroup_name?: string, - zone_name?: string - ) { + getDefaultPlacementCount(serviceType: string) { + /** + * `defaults` from src/pybind/mgr/cephadm/module.py + */ + switch (serviceType) { + case 'mon': + this.serviceForm.get('count').setValue(5); + break; + case 'mgr': + case 'mds': + case 'rgw': + case 'ingress': + case 'rbd-mirror': + this.serviceForm.get('count').setValue(2); + break; + case 'iscsi': + case 'nvmeof': + case 'cephfs-mirror': + case 'nfs': + case 'grafana': + case 'alertmanager': + case 'prometheus': + case 'loki': + case 'container': + case 'snmp-gateway': + case 'elastic-serach': + case 'jaeger-collector': + case 'jaeger-query': + case 'smb': + this.serviceForm.get('count').setValue(1); + break; + } + } + + setRgwFields(realm_name?: string, zonegroup_name?: string, zone_name?: string) { + const observables = [ + this.rgwRealmService.getAllRealmsInfo(), + this.rgwZonegroupService.getAllZonegroupsInfo(), + this.rgwZoneService.getAllZonesInfo() + ]; + this.sub = forkJoin(observables).subscribe( + (multisiteInfo: [object, object, object]) => { + this.multisiteInfo = multisiteInfo; + this.realmList = + this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms') + ? this.multisiteInfo[0]['realms'] + : []; + this.zonegroupList = + this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups') + ? this.multisiteInfo[1]['zonegroups'] + : []; + this.zoneList = + this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones') + ? this.multisiteInfo[2]['zones'] + : []; + this.realmNames = this.realmList.map((realm) => { + return realm['name']; + }); + this.zonegroupNames = this.zonegroupList.map((zonegroup) => { + return zonegroup['name']; + }); + this.zoneNames = this.zoneList.map((zone) => { + return zone['name']; + }); + this.defaultRealmId = multisiteInfo[0]['default_realm']; + this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup']; + this.defaultZoneId = multisiteInfo[2]['default_zone']; + this.defaultsInfo = this.getDefaultsEntitiesForRgw( + this.defaultRealmId, + this.defaultZonegroupId, + this.defaultZoneId + ); + if (!this.editing) { + this.serviceForm.get('realm_name').setValue(this.defaultsInfo['defaultRealmName']); + this.serviceForm + .get('zonegroup_name') + .setValue(this.defaultsInfo['defaultZonegroupName']); + this.serviceForm.get('zone_name').setValue(this.defaultsInfo['defaultZoneName']); + } else { + if (realm_name && !this.realmNames.includes(realm_name)) { + const realm = new RgwRealm(); + realm.name = realm_name; + this.realmList.push(realm); + } + if (zonegroup_name && !this.zonegroupNames.includes(zonegroup_name)) { + const zonegroup = new RgwZonegroup(); + zonegroup.name = zonegroup_name; + this.zonegroupList.push(zonegroup); + } + if (zone_name && !this.zoneNames.includes(zone_name)) { + const zone = new RgwZone(); + zone.name = zone_name; + this.zoneList.push(zone); + } + if (zonegroup_name === undefined && zone_name === undefined) { + zonegroup_name = 'default'; + zone_name = 'default'; + } + this.serviceForm.get('realm_name').setValue(realm_name); + this.serviceForm.get('zonegroup_name').setValue(zonegroup_name); + this.serviceForm.get('zone_name').setValue(zone_name); + } + if (this.realmList.length === 0) { + this.showRealmCreationForm = true; + } else { + this.showRealmCreationForm = false; + } + }, + (_error) => { + const defaultZone = new RgwZone(); + defaultZone.name = 'default'; + const defaultZonegroup = new RgwZonegroup(); + defaultZonegroup.name = 'default'; + this.zoneList.push(defaultZone); + this.zonegroupList.push(defaultZonegroup); + } + ); + } + + setNvmeofServiceId(): void { + const defaultRbdPool: string = this.rbdPools.find((p: Pool) => p.pool_name === 'rbd') + ?.pool_name; + if (defaultRbdPool) { + this.serviceForm.get('pool').setValue(defaultRbdPool); + this.serviceForm.get('service_id').setValue(defaultRbdPool); + } + } + + requiresServiceId(serviceType: string) { + return ['mds', 'rgw', 'nfs', 'iscsi', 'nvmeof', 'smb', 'ingress'].includes(serviceType); + } + + setServiceId(serviceId: string): void { + const requiresServiceId: boolean = this.requiresServiceId(serviceId); + if (requiresServiceId && serviceId === 'nvmeof') { + this.setNvmeofServiceId(); + } else if (requiresServiceId) { + this.serviceForm.get('service_id').setValue(null); + } else { + this.serviceForm.get('service_id').setValue(serviceId); + } + } + + onServiceTypeChange(selectedServiceType: string) { + this.setServiceId(selectedServiceType); + this.serviceIds = this.serviceList ?.filter((service) => service['service_type'] === selectedServiceType) .map((service) => service['service_id']); + this.getDefaultPlacementCount(selectedServiceType); + if (selectedServiceType === 'rgw') { - const observables = [ - this.rgwRealmService.getAllRealmsInfo(), - this.rgwZonegroupService.getAllZonegroupsInfo(), - this.rgwZoneService.getAllZonesInfo() - ]; - this.sub = forkJoin(observables).subscribe( - (multisiteInfo: [object, object, object]) => { - this.multisiteInfo = multisiteInfo; - this.realmList = - this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms') - ? this.multisiteInfo[0]['realms'] - : []; - this.zonegroupList = - this.multisiteInfo[1] !== undefined && - this.multisiteInfo[1].hasOwnProperty('zonegroups') - ? this.multisiteInfo[1]['zonegroups'] - : []; - this.zoneList = - this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones') - ? this.multisiteInfo[2]['zones'] - : []; - this.realmNames = this.realmList.map((realm) => { - return realm['name']; - }); - this.zonegroupNames = this.zonegroupList.map((zonegroup) => { - return zonegroup['name']; - }); - this.zoneNames = this.zoneList.map((zone) => { - return zone['name']; - }); - this.defaultRealmId = multisiteInfo[0]['default_realm']; - this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup']; - this.defaultZoneId = multisiteInfo[2]['default_zone']; - this.defaultsInfo = this.getDefaultsEntities( - this.defaultRealmId, - this.defaultZonegroupId, - this.defaultZoneId - ); - if (!this.editing) { - this.serviceForm.get('realm_name').setValue(this.defaultsInfo['defaultRealmName']); - this.serviceForm - .get('zonegroup_name') - .setValue(this.defaultsInfo['defaultZonegroupName']); - this.serviceForm.get('zone_name').setValue(this.defaultsInfo['defaultZoneName']); - } else { - if (realm_name && !this.realmNames.includes(realm_name)) { - const realm = new RgwRealm(); - realm.name = realm_name; - this.realmList.push(realm); - } - if (zonegroup_name && !this.zonegroupNames.includes(zonegroup_name)) { - const zonegroup = new RgwZonegroup(); - zonegroup.name = zonegroup_name; - this.zonegroupList.push(zonegroup); - } - if (zone_name && !this.zoneNames.includes(zone_name)) { - const zone = new RgwZone(); - zone.name = zone_name; - this.zoneList.push(zone); - } - if (zonegroup_name === undefined && zone_name === undefined) { - zonegroup_name = 'default'; - zone_name = 'default'; - } - this.serviceForm.get('realm_name').setValue(realm_name); - this.serviceForm.get('zonegroup_name').setValue(zonegroup_name); - this.serviceForm.get('zone_name').setValue(zone_name); - } - if (this.realmList.length === 0) { - this.showRealmCreationForm = true; - } else { - this.showRealmCreationForm = false; - } - }, - (_error) => { - const defaultZone = new RgwZone(); - defaultZone.name = 'default'; - const defaultZonegroup = new RgwZonegroup(); - defaultZonegroup.name = 'default'; - this.zoneList.push(defaultZone); - this.zonegroupList.push(defaultZonegroup); - } - ); + this.setRgwFields(); + } + } + + onBlockPoolChange() { + const selectedBlockPool = this.serviceForm.get('pool').value; + if (selectedBlockPool) { + this.serviceForm.get('service_id').setValue(selectedBlockPool); + } else { + this.serviceForm.get('service_id').setValue(null); } } @@ -730,6 +813,10 @@ export class ServiceFormComponent extends CdForm implements OnInit { switch (serviceType) { case 'ingress': this.serviceForm.get('backend_service').disable(); + break; + case 'nvmeof': + this.serviceForm.get('pool').disable(); + break; } } @@ -780,19 +867,16 @@ export class ServiceFormComponent extends CdForm implements OnInit { placement: {}, unmanaged: values['unmanaged'] }; - let svcId: string; if (serviceType === 'rgw') { serviceSpec['rgw_realm'] = values['realm_name'] ? values['realm_name'] : null; serviceSpec['rgw_zonegroup'] = values['zonegroup_name'] !== 'default' ? values['zonegroup_name'] : null; serviceSpec['rgw_zone'] = values['zone_name'] !== 'default' ? values['zone_name'] : null; - svcId = values['service_id']; - } else { - svcId = values['service_id']; } - const serviceId: string = svcId; + + const serviceId: string = values['service_id']; let serviceName: string = serviceType; - if (_.isString(serviceId) && !_.isEmpty(serviceId)) { + if (_.isString(serviceId) && !_.isEmpty(serviceId) && serviceId !== serviceType) { serviceName = `${serviceType}.${serviceId}`; serviceSpec['service_id'] = serviceId; } @@ -814,6 +898,7 @@ export class ServiceFormComponent extends CdForm implements OnInit { } break; + case 'nvmeof': case 'iscsi': serviceSpec['pool'] = values['pool']; break; @@ -947,7 +1032,7 @@ export class ServiceFormComponent extends CdForm implements OnInit { size: 'lg' }); this.bsModalRef.componentInstance.submitAction.subscribe(() => { - this.getServiceIds('rgw'); + this.setRgwFields(); }); } } -- 2.39.5