From f5a581b9806eec82440ed4fed3e16b24a4676cfe Mon Sep 17 00:00:00 2001 From: Avan Thakkar Date: Wed, 3 Jul 2024 10:02:41 +0530 Subject: [PATCH] mgr/dashboard: support rgw user level NFS export Signed-off-by: Avan Thakkar --- src/pybind/mgr/dashboard/controllers/nfs.py | 5 + .../ceph/nfs/nfs-form/nfs-form.component.html | 72 ++++++++- .../nfs/nfs-form/nfs-form.component.spec.ts | 3 +- .../ceph/nfs/nfs-form/nfs-form.component.ts | 140 +++++++++++++----- .../ceph/nfs/nfs-list/nfs-list.component.html | 9 ++ .../ceph/nfs/nfs-list/nfs-list.component.ts | 25 +++- src/pybind/mgr/dashboard/tests/test_nfs.py | 12 +- 7 files changed, 224 insertions(+), 42 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/nfs.py b/src/pybind/mgr/dashboard/controllers/nfs.py index 36b88d76b165c..b417a585c299c 100644 --- a/src/pybind/mgr/dashboard/controllers/nfs.py +++ b/src/pybind/mgr/dashboard/controllers/nfs.py @@ -191,7 +191,12 @@ class NFSGaneshaExports(RESTController): 'clients': clients } + existing_export = mgr.remote('nfs', 'export_get', cluster_id, export_id) export_mgr = mgr.remote('nfs', 'fetch_nfs_export_obj') + if existing_export and raw_ex: + ss_export_fsal = existing_export.get('fsal', {}) + for key, value in ss_export_fsal.items(): + raw_ex['fsal'][key] = value applied_exports = export_mgr.apply_export(cluster_id, json.dumps(raw_ex)) if not applied_exports.has_error: return self._get_schema_export( diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html index ef78407b7e5ee..13370c82591cc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html @@ -44,6 +44,36 @@ + +
+ +
+ + +
+
+ + +
+
+
@@ -75,6 +105,38 @@ i18n>This field is required.
+ + +
+ +
+ + This field is required. +
+
@@ -201,7 +263,7 @@
+ *ngIf="storageBackend === 'RGW' && nfsForm.getValue('rgw_export_type') === 'bucket'">
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts index 7f88c6486843f..27e76721335e9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts @@ -91,11 +91,12 @@ describe('NfsFormComponent', () => { access_type: 'RW', clients: [], cluster_id: 'mynfs', - fsal: { fs_name: '', name: 'CEPH' }, + fsal: { fs_name: '', name: 'CEPH', user_id: '' }, path: '/', protocolNfsv4: true, protocolNfsv3: true, pseudo: '', + rgw_export_type: null, sec_label_xattr: 'security.selinux', security_label: false, squash: 'no_root_squash', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts index 18d91e2cddfdf..4b6e144189021 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts @@ -31,6 +31,8 @@ import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.compo import { getFsalFromRoute, getPathfromFsal } from '../utils'; import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service'; import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service'; +import { RgwUserService } from '~/app/shared/api/rgw-user.service'; +import { RgwExportType } from '../nfs-list/nfs-list.component'; @Component({ selector: 'cd-nfs-form', @@ -55,6 +57,8 @@ export class NfsFormComponent extends CdForm implements OnInit { allFsNames: any[] = null; + allRGWUsers: any[] = null; + storageBackend: SUPPORTED_FSAL; storageBackendError: string = null; @@ -98,6 +102,7 @@ export class NfsFormComponent extends CdForm implements OnInit { private route: ActivatedRoute, private router: Router, private rgwBucketService: RgwBucketService, + private rgwUserService: RgwUserService, private rgwSiteService: RgwSiteService, private formBuilder: CdFormBuilder, private taskWrapper: TaskWrapperService, @@ -115,7 +120,7 @@ export class NfsFormComponent extends CdForm implements OnInit { this.createForm(); const promises: Observable[] = [this.nfsService.listClusters()]; - if (this.storageBackend === 'RGW') { + if (this.storageBackend === SUPPORTED_FSAL.RGW) { promises.push(this.rgwSiteService.get('realms')); } else { promises.push(this.nfsService.filesystems()); @@ -127,13 +132,25 @@ export class NfsFormComponent extends CdForm implements OnInit { if (this.isEdit) { this.action = this.actionLabels.EDIT; - this.route.params.subscribe((params: { cluster_id: string; export_id: string }) => { - this.cluster_id = decodeURIComponent(params.cluster_id); - this.export_id = decodeURIComponent(params.export_id); - promises.push(this.nfsService.get(this.cluster_id, this.export_id)); - this.getData(promises); - }); + this.route.params.subscribe( + (params: { cluster_id: string; export_id: string; rgw_export_type?: string }) => { + this.cluster_id = decodeURIComponent(params.cluster_id); + this.export_id = decodeURIComponent(params.export_id); + if (params.rgw_export_type) { + this.nfsForm.get('rgw_export_type').setValue(params.rgw_export_type); + if (params.rgw_export_type === RgwExportType.BUCKET) { + this.setBucket(); + } else { + this.setUsers(); + } + } + promises.push(this.nfsService.get(this.cluster_id, this.export_id)); + this.getData(promises); + } + ); this.nfsForm.get('cluster_id').disable(); + this.nfsForm.get('path').disable(); + this.nfsForm.get('fsal.user_id').disable(); } else { this.action = this.actionLabels.CREATE; this.route.params.subscribe( @@ -144,6 +161,10 @@ export class NfsFormComponent extends CdForm implements OnInit { } ); + if (this.storageBackend === SUPPORTED_FSAL.RGW) { + this.nfsForm.get('rgw_export_type').setValue('bucket'); + this.setBucket(); + } this.getData(promises); } } @@ -233,23 +254,10 @@ export class NfsFormComponent extends CdForm implements OnInit { createForm() { this.nfsForm = new CdFormGroup({ + // Common fields cluster_id: new UntypedFormControl('', { validators: [Validators.required] }), - fsal: new CdFormGroup({ - name: new UntypedFormControl(this.storageBackend, { - validators: [Validators.required] - }), - fs_name: new UntypedFormControl('', { - validators: [ - CdValidators.requiredIf({ - name: 'CEPH' - }) - ] - }) - }), - subvolume_group: new UntypedFormControl(''), - subvolume: new UntypedFormControl(''), path: new UntypedFormControl('/', { validators: [Validators.required] }), @@ -291,9 +299,44 @@ export class NfsFormComponent extends CdForm implements OnInit { }), clients: this.formBuilder.array([]), security_label: new UntypedFormControl(false), + + // FSAL fields (common for RGW and CephFS) + fsal: new CdFormGroup({ + name: new UntypedFormControl(this.storageBackend, { + validators: [Validators.required] + }), + // RGW-specific field + user_id: new UntypedFormControl('', { + validators: [ + CdValidators.requiredIf({ + name: 'RGW' + }) + ] + }), + // CephFS-specific field + fs_name: new UntypedFormControl('', { + validators: [ + CdValidators.requiredIf({ + name: 'CEPH' + }) + ] + }) + }), + + // CephFS-specific fields + subvolume_group: new UntypedFormControl(''), + subvolume: new UntypedFormControl(''), sec_label_xattr: new UntypedFormControl( 'security.selinux', CdValidators.requiredIf({ security_label: true, 'fsal.name': 'CEPH' }) + ), + + // RGW-specific fields + rgw_export_type: new UntypedFormControl( + null, + CdValidators.requiredIf({ + 'fsal.name': 'RGW' + }) ) }); } @@ -344,8 +387,7 @@ export class NfsFormComponent extends CdForm implements OnInit { } resolveFsals(res: string[]) { - if (this.storageBackend === 'RGW') { - this.setPathValidation(); + if (this.storageBackend === SUPPORTED_FSAL.RGW) { this.resolveRealms(res); } else { this.resolveFilesystems(res); @@ -410,6 +452,23 @@ export class NfsFormComponent extends CdForm implements OnInit { } } + setUsers() { + this.nfsForm.get('fsal.user_id').enable(); + this.nfsForm.get('path').setValue(''); + this.nfsForm.get('path').disable(); + this.rgwUserService.list().subscribe((users: any) => { + this.allRGWUsers = users; + }); + } + + setBucket() { + this.nfsForm.get('path').enable(); + this.nfsForm.get('fsal.user_id').setValue(''); + this.nfsForm.get('fsal.user_id').disable(); + + this.setPathValidation(); + } + accessTypeChangeHandler() { const name = this.nfsForm.getValue('name'); const accessType = this.nfsForm.getValue('access_type'); @@ -486,16 +545,20 @@ export class NfsFormComponent extends CdForm implements OnInit { } private generatePseudo() { - let newPseudo = this.nfsForm.getValue('pseudo'); - if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) { - newPseudo = undefined; - if (this.storageBackend === 'CEPH') { - newPseudo = '/cephfs'; - if (_.isString(this.nfsForm.getValue('path'))) { - newPseudo += this.nfsForm.getValue('path'); - } + const pseudoControl = this.nfsForm.get('pseudo'); + let newPseudo = pseudoControl?.dirty && this.nfsForm.getValue('pseudo'); + + if (!newPseudo) { + const path = this.nfsForm.getValue('path'); + newPseudo = `/${getPathfromFsal(this.storageBackend)}`; + + if (_.isString(path) && !_.isEmpty(path)) { + newPseudo += '/' + path; + } else if (!_.isEmpty(this.nfsForm.getValue('fsal').user_id)) { + newPseudo += '/' + this.nfsForm.getValue('fsal').user_id; } } + return newPseudo; } @@ -546,12 +609,23 @@ export class NfsFormComponent extends CdForm implements OnInit { if (this.isEdit) { requestModel.export_id = _.parseInt(this.export_id); + if (requestModel.fsal.name === SUPPORTED_FSAL.RGW) { + requestModel.fsal.user_id = this.nfsForm.getValue('fsal').user_id; + requestModel.path = this.nfsForm.getValue('path'); + } } - if (requestModel.fsal.name === 'RGW') { + if (requestModel.fsal.name === SUPPORTED_FSAL.RGW) { delete requestModel.fsal.fs_name; + if (requestModel.rgw_export_type === 'bucket') { + delete requestModel.fsal.user_id; + } else { + requestModel.path = ''; + } + } else { + delete requestModel.fsal.user_id; } - + delete requestModel.rgw_export_type; delete requestModel.subvolume; delete requestModel.subvolume_group; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html index 79304265e7ea3..21572e893e3e5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html @@ -28,3 +28,12 @@ Object Gateway + + + * + {{ value }} + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts index 8be95c6febe79..ad2a8ef95edbb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts @@ -26,6 +26,11 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { getFsalFromRoute, getPathfromFsal } from '../utils'; import { SUPPORTED_FSAL } from '../models/nfs.fsal'; +export enum RgwExportType { + BUCKET = 'bucket', + USER = 'user' +} + @Component({ selector: 'cd-nfs-list', templateUrl: './nfs-list.component.html', @@ -37,6 +42,8 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr nfsState: TemplateRef; @ViewChild('nfsFsal', { static: true }) nfsFsal: TemplateRef; + @ViewChild('pathTmpl', { static: true }) + pathTmpl: TemplateRef; @ViewChild('table', { static: true }) table: TableComponent; @@ -93,7 +100,15 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr const editAction: CdTableAction = { permission: 'update', icon: Icons.edit, - routerLink: () => `/${prefix}/nfs/edit/${getNfsUri()}`, + routerLink: () => [ + `/${prefix}/nfs/edit/${getNfsUri()}`, + { + rgw_export_type: + this.fsal === SUPPORTED_FSAL.RGW && !_.isEmpty(this.selection?.first()?.path) + ? RgwExportType.BUCKET + : RgwExportType.USER + } + ], name: this.actionLabels.EDIT }; @@ -109,11 +124,17 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr ngOnInit() { this.columns = [ + { + name: $localize`User`, + prop: 'fsal.user_id', + flexGrow: 2, + cellTransformation: CellTemplate.executing + }, { name: this.fsal === SUPPORTED_FSAL.CEPH ? $localize`Path` : $localize`Bucket`, prop: 'path', flexGrow: 2, - cellTransformation: CellTemplate.executing + cellTemplate: this.pathTmpl }, { name: $localize`Pseudo`, diff --git a/src/pybind/mgr/dashboard/tests/test_nfs.py b/src/pybind/mgr/dashboard/tests/test_nfs.py index 467d08a4c4e89..308eeb07e304f 100644 --- a/src/pybind/mgr/dashboard/tests/test_nfs.py +++ b/src/pybind/mgr/dashboard/tests/test_nfs.py @@ -112,12 +112,17 @@ class NFSGaneshaExportsTest(ControllerTestCase): def test_set_export(self): export_mgr = Mock() + existing_export = deepcopy(self._nfs_module_export) updated_nfs_export = deepcopy(self._nfs_module_export) applied_nfs_export = deepcopy(self._applied_export) + + existing_export['fsal']['user_id'] = 'dashboard' + + mgr.remote = Mock(side_effect=[existing_export, export_mgr]) + updated_nfs_export['pseudo'] = 'updated-pseudo' export_mgr.get_export_by_pseudo.return_value = updated_nfs_export export_mgr.apply_export.return_value = applied_nfs_export - mgr.remote.return_value = export_mgr updated_export_body = deepcopy(self._expected_export) updated_export_body['pseudo'] = updated_nfs_export['pseudo'] @@ -235,7 +240,10 @@ class NFSGaneshaUiControllerTest(ControllerTestCase): self.assertStatus(200) self.assertJsonBody({'paths': []}) - def test_status_available(self): + @patch('dashboard.controllers.nfs.mgr.remote') + def test_status_available(self, mock_remote): + mock_remote.return_value = ['cluster1', 'cluster2'] + self._get('/ui-api/nfs-ganesha/status') self.assertStatus(200) self.assertJsonBody({'available': True, 'message': None}) -- 2.39.5