]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: support rgw user level NFS export 58552/head
authorAvan Thakkar <athakkar@redhat.com>
Wed, 3 Jul 2024 04:32:41 +0000 (10:02 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Sat, 27 Jul 2024 07:56:19 +0000 (13:26 +0530)
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
src/pybind/mgr/dashboard/controllers/nfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
src/pybind/mgr/dashboard/tests/test_nfs.py

index 36b88d76b165cb8bab583ecfac1b53409dd45ae4..b417a585c299c823ebd392fdb9a2826e5176a059 100644 (file)
@@ -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(
index ef78407b7e5eef1db9552c83121ca99d38f0893f..13370c82591cc7fff9bcc6176c8e8840de839532 100644 (file)
           </div>
         </div>
 
+        <!-- RGW Export Type -->
+        <div *ngIf="storageBackend === 'RGW' && !isEdit"
+             class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="rgw_export_type"
+                 i18n>Type</label>
+          <div class="col-md-auto custom-checkbox form-check-inline ms-3">
+            <input class="form-check-input"
+                   formControlName="rgw_export_type"
+                   id="bucket_export"
+                   type="radio"
+                   value="bucket"
+                   (change)="setBucket()">
+            <label class="form-check-label"
+                   for="bucket_export"
+                   i18n>Bucket</label>
+          </div>
+          <div class="col-md-auto custom-checkbox form-check-inline">
+            <input class="form-check-input"
+                   formControlName="rgw_export_type"
+                   id="user_export"
+                   type="radio"
+                   value="user"
+                   (change)="setUsers()">
+            <label class="form-check-label"
+                   for="user_export"
+                   i18n>User</label>
+          </div>
+        </div>
+
         <!-- FSAL -->
         <div formGroupName="fsal">
           <!-- CephFS Volume -->
                     i18n>This field is required.</span>
             </div>
           </div>
+
+          <!-- RGW User -->
+          <div class="form-group row"
+               *ngIf="storageBackend === 'RGW' && nfsForm.getValue('rgw_export_type') === 'user'">
+            <label class="cd-col-form-label"
+                   for="user_id">
+              <span class="required"
+                    i18n>User</span>
+            </label>
+            <div class="cd-col-form-input">
+              <select class="form-select"
+                      formControlName="user_id"
+                      name="user_id"
+                      id="user_id"
+                      (change)="pathChangeHandler()">
+                <option *ngIf="allRGWUsers === null"
+                        value=""
+                        i18n>Loading...</option>
+                <option *ngIf="allRGWUsers !== null && allRGWUsers.length === 0"
+                        value=""
+                        i18n>-- No RGW User available --</option>
+                <option *ngIf="allRGWUsers !== null && allRGWUsers.length > 0"
+                        value=""
+                        i18n>-- Select the RGW User --</option>
+                <option *ngFor="let user of allRGWUsers"
+                        [value]="user.user_id">{{ user.user_id }}</option>
+              </select>
+              <span class="invalid-feedback"
+                    *ngIf="nfsForm.showError('user_id', formDir, 'required')"
+                    i18n>This field is required.</span>
+            </div>
+          </div>
         </div>
 
         <!-- Security Label -->
 
         <!-- Bucket -->
         <div class="form-group row"
-             *ngIf="storageBackend === 'RGW'">
+             *ngIf="storageBackend === 'RGW' && nfsForm.getValue('rgw_export_type') === 'bucket'">
           <label class="cd-col-form-label"
                  for="path">
             <span class="required"
                    id="path"
                    data-testid="rgw_path"
                    formControlName="path"
-                   [ngbTypeahead]="bucketDataSource">
+                   [ngbTypeahead]="bucketDataSource"
+                   (selectItem)="pathChangeHandler()"
+                   (blur)="pathChangeHandler()">
 
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('path', formDir, 'required')"
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('path', formDir, 'bucketNameNotAllowed')"
                   i18n>The bucket does not exist or is not in the default realm (if multiple realms are configured).
-                       To continue, <a routerLink="/rgw/bucket/create"
-                                       class="btn-link">create a new bucket</a>.</span>
+                      To continue, <a routerLink="/rgw/bucket/create"
+                                      class="btn-link">create a new bucket</a>.</span>
           </div>
         </div>
 
index 7f88c6486843f586d5339826aa5ff2d01ff3e92d..27e76721335e9d864f0bee0252f807c633e7b27f 100644 (file)
@@ -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',
index 18d91e2cddfdf6a02dc2041511c904b188e3a68d..4b6e1441890213b04a7ed37258668dda0c5e0e33 100644 (file)
@@ -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<any>[] = [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;
 
index 79304265e7ea3a1a317d2fd68f4194dd4b9cf922..21572e893e3e5b0f89dc50322aeef01c7fa49a4d 100644 (file)
   <ng-container *ngIf="value.name==='RGW'"
                 i18n>Object Gateway</ng-container>
 </ng-template>
+
+<ng-template #pathTmpl
+             let-value="value">
+  <span *ngIf="value === ''"
+        i18n
+        i18n-ngbTooltip
+        ngbTooltip="All buckets owned by user">*</span>
+  <span *ngIf="value !== ''">{{ value }}</span>
+</ng-template>
index 8be95c6febe7991098e36305279f2be6e77e427c..ad2a8ef95edbba71f19a858f5bd215a4540b846a 100644 (file)
@@ -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<any>;
   @ViewChild('nfsFsal', { static: true })
   nfsFsal: TemplateRef<any>;
+  @ViewChild('pathTmpl', { static: true })
+  pathTmpl: TemplateRef<any>;
 
   @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`,
index 467d08a4c4e892e48a46a5220cf130321cd37bfe..308eeb07e304fbe051f0ff87e5e0613d7c03bdff 100644 (file)
@@ -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})