]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Fix NFS routing
authorAfreen <afreen23.git@gmail.com>
Wed, 3 Apr 2024 02:15:32 +0000 (07:45 +0530)
committerAfreen Misbah <afreen23.git@gmail.com>
Wed, 10 Jul 2024 07:38:42 +0000 (13:08 +0530)
Fixes https://tracker.ceph.com/issues/65310

The NFS tab in object and File nav uses same route due to which both
gets activated when one of them is clicked.
Hence, this PR separates the routing for Object and File nav.
Object-> NFS: /rgw/nfs
File-> NFS: /cephfs/nfs

Both routes use same NFS List and Form component but under different
routes as mentioned above.

Changes summary
- updated route for File from "/fs" to "/cephfs/<any_other_sub_route>"
  to support both fs and nfs tabs. Since using `/fs` and `/fs/nfs` will
activate both paths and it will be an undesirable user experience.
- `getFsalRouteFromPath` helper function to set the storage backend from
  route.
- removed `stoarge-backend` field from nfs form as now route decides teh
  storage backend
- breadcrumbs redirect to respective navs
- updated e2e tests
- updated unit tests
- changes list page of object-> nfs page to say Bucket instead of Path

Signed-off-by: Afreen <afreen23.git@gmail.com>
(cherry picked from commit 97359d6b1ee8133afcddf1f540b147c26723e24e)

Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html

18 files changed:
src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts
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.scss
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.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/utils.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.ts

index 6f7316f98f59e9a1c8a44c7d98ed88d40b7bb787..970fc8ff509cf17ed89dd3ced4b5a866e82ed937 100644 (file)
@@ -42,7 +42,7 @@ export class UrlsCollection extends PageHelper {
     'rgw daemons': { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' },
 
     // CephFS
-    cephfs: { url: '#/cephfs', id: 'cd-cephfs-list' },
-    'create cephfs': { url: '#/cephfs/create', id: 'cd-cephfs-form' }
+    cephfs: { url: '#/cephfs/fs', id: 'cd-cephfs-list' },
+    'create cephfs': { url: '#/cephfs/fs/create', id: 'cd-cephfs-form' }
   };
 }
index fdd96d7e975ea7da5446da84b88a43429a398096..ff2e7581bb6a155b7403111fb5e572f06f663740 100644 (file)
@@ -19,11 +19,11 @@ describe('nfsExport page', () => {
 
   beforeEach(() => {
     cy.login();
-    nfsExport.navigateTo();
   });
 
   describe('breadcrumb test', () => {
     it('should open and show breadcrumb', () => {
+      nfsExport.navigateTo('rgw_index');
       nfsExport.expectBreadcrumbText('NFS');
     });
   });
@@ -43,23 +43,24 @@ describe('nfsExport page', () => {
       buckets.navigateTo('create');
       buckets.create(bucketName, 'dashboard');
 
-      nfsExport.navigateTo();
+      nfsExport.navigateTo('rgw_index');
       nfsExport.existTableCell(rgwPseudo, false);
-      nfsExport.navigateTo('create');
+      nfsExport.navigateTo('rgw_create');
       nfsExport.create(backends[1], squash, client, rgwPseudo, bucketName);
       nfsExport.existTableCell(rgwPseudo);
     });
 
     // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
     // it('should create a nfs-export with CephFS backend', () => {
-    //   nfsExport.navigateTo();
+    //   nfsExport.navigateTo('cephfs_index');
     //   nfsExport.existTableCell(fsPseudo, false);
-    //   nfsExport.navigateTo('create');
+    //   nfsExport.navigateTo('cephfs_create');
     //   nfsExport.create(backends[0], squash, client, fsPseudo);
     //   nfsExport.existTableCell(fsPseudo);
     // });
 
     it('should show Clients', () => {
+      nfsExport.navigateTo('rgw_index');
       nfsExport.clickTab('cd-nfs-details', rgwPseudo, 'Clients (1)');
       cy.get('cd-nfs-details').within(() => {
         nfsExport.getTableCount('total').should('be.gte', 0);
@@ -67,12 +68,13 @@ describe('nfsExport page', () => {
     });
 
     it('should edit an export', () => {
-      nfsExport.editExport(rgwPseudo, editPseudo);
+      nfsExport.editExport(rgwPseudo, editPseudo, 'rgw_index');
 
       nfsExport.existTableCell(editPseudo);
     });
 
     it('should delete exports and bucket', () => {
+      nfsExport.navigateTo('rgw_index');
       nfsExport.delete(editPseudo);
 
       buckets.navigateTo();
index c700ef0581dd761c89754e1a2c5a660a13c2081b..3639eb9a8ab0860e6c59917bc781b03af5a86b0d 100644 (file)
@@ -3,21 +3,18 @@ import { PageHelper } from '../../../page-helper.po';
 /* tslint:enable*/
 
 const pages = {
-  index: { url: '#/nfs', id: 'cd-nfs-list' },
-  create: { url: '#/nfs/create', id: 'cd-nfs-form' }
+  cephfs_index: { url: '#cephfs/nfs', id: 'cd-nfs-list' },
+  cephfs_create: { url: '#cephfs/nfs/create', id: 'cd-nfs-form' },
+  rgw_index: { url: '#rgw/nfs', id: 'cd-nfs-list' },
+  rgw_create: { url: '#rgw/nfs/create', id: 'cd-nfs-form' }
 };
 
 export class NFSPageHelper extends PageHelper {
   pages = pages;
-
-  @PageHelper.restrictTo(pages.create.url)
   create(backend: string, squash: string, client: object, pseudo: string, rgwPath?: string) {
     this.selectOption('cluster_id', 'testnfs');
-    // select a storage backend
-    this.selectOption('name', backend);
     if (backend === 'CephFS') {
       this.selectOption('fs_name', 'myfs');
-
       cy.get('#security_label').click({ force: true });
     } else {
       cy.get('input[data-testid=rgw_path]').type(rgwPath);
@@ -38,8 +35,8 @@ export class NFSPageHelper extends PageHelper {
     cy.get('cd-submit-button').click();
   }
 
-  editExport(pseudo: string, editPseudo: string) {
-    this.navigateEdit(pseudo);
+  editExport(pseudo: string, editPseudo: string, url: string) {
+    this.navigateEdit(pseudo, true, true, url);
 
     cy.get('input[name=pseudo]').clear().type(editPseudo);
 
index 2a16ff7e1418ea22cb608256c585731aa6c36f86..49144b25fbfc9347e961ff9f657e2637bf0a147f 100644 (file)
@@ -52,9 +52,9 @@ export abstract class PageHelper {
   /**
    * Navigates to the edit page
    */
-  navigateEdit(name: string, select = true, breadcrumb = true) {
+  navigateEdit(name: string, select = true, breadcrumb = true, navigateTo: string = null) {
     if (select) {
-      this.navigateTo();
+      this.navigateTo(navigateTo);
       this.getFirstTableCell(name).click();
     }
     cy.contains('Creating...').should('not.exist');
index 2ba634fa25d0f6715efaefb83b161276c587619d..f861ad183a2ed0d7e946dd1c52ac23bdcf764561 100644 (file)
@@ -352,18 +352,48 @@ const routes: Routes = [
       {
         path: 'cephfs',
         canActivate: [FeatureTogglesGuardService],
-        data: { breadcrumbs: 'File/File Systems' },
         children: [
-          { path: '', component: CephfsListComponent },
           {
-            path: URLVerbs.CREATE,
+            path: 'fs',
+            component: CephfsListComponent,
+            data: { breadcrumbs: 'File/File Systems' }
+          },
+          {
+            path: `fs/${URLVerbs.CREATE}`,
             component: CephfsVolumeFormComponent,
             data: { breadcrumbs: ActionLabels.CREATE }
           },
           {
-            path: `${URLVerbs.EDIT}/:id`,
+            path: `fs/${URLVerbs.EDIT}/:id`,
             component: CephfsVolumeFormComponent,
             data: { breadcrumbs: ActionLabels.EDIT }
+          },
+          {
+            path: 'nfs',
+            canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
+            data: {
+              moduleStatusGuardConfig: {
+                uiApiPath: 'nfs-ganesha',
+                redirectTo: 'error',
+                section: 'nfs-ganesha',
+                section_info: 'NFS GANESHA',
+                header: 'NFS-Ganesha is not configured'
+              },
+              breadcrumbs: 'File/NFS'
+            },
+            children: [
+              { path: '', component: NfsListComponent },
+              {
+                path: URLVerbs.CREATE,
+                component: NfsFormComponent,
+                data: { breadcrumbs: ActionLabels.CREATE }
+              },
+              {
+                path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+                component: NfsFormComponent,
+                data: { breadcrumbs: ActionLabels.EDIT }
+              }
+            ]
           }
         ]
       },
@@ -403,34 +433,6 @@ const routes: Routes = [
             data: { breadcrumbs: ActionLabels.EDIT }
           }
         ]
-      },
-      // NFS
-      {
-        path: 'nfs',
-        canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
-        data: {
-          moduleStatusGuardConfig: {
-            uiApiPath: 'nfs-ganesha',
-            redirectTo: 'error',
-            section: 'nfs-ganesha',
-            section_info: 'NFS GANESHA',
-            header: 'NFS-Ganesha is not configured'
-          },
-          breadcrumbs: 'NFS'
-        },
-        children: [
-          { path: '', component: NfsListComponent },
-          {
-            path: URLVerbs.CREATE,
-            component: NfsFormComponent,
-            data: { breadcrumbs: ActionLabels.CREATE }
-          },
-          {
-            path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
-            component: NfsFormComponent,
-            data: { breadcrumbs: ActionLabels.EDIT }
-          }
-        ]
       }
     ]
   },
index dbbe522fa0a19f6b48549c90924c43d63a889874..0506c4c77341fbe8fc0aa8f978ace62c3da1928f 100644 (file)
@@ -68,7 +68,7 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
     private route: ActivatedRoute
   ) {
     super();
-    this.editing = this.router.url.startsWith(`/cephfs/${URLVerbs.EDIT}`);
+    this.editing = this.router.url.startsWith(`/cephfs/fs/${URLVerbs.EDIT}`);
     this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
     this.resource = $localize`File System`;
     this.hosts = {
@@ -176,7 +176,7 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
             this.form.setErrors({ cdSubmitButton: true });
           },
           complete: () => {
-            this.router.navigate([BASE_URL]);
+            this.router.navigate([`${BASE_URL}/fs`]);
           }
         });
     } else {
@@ -210,7 +210,7 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
             self.form.setErrors({ cdSubmitButton: true });
           },
           complete: () => {
-            this.router.navigate([BASE_URL]);
+            this.router.navigate([`${BASE_URL}/fs`]);
           }
         });
     }
index 2957401d86aae18a966b7f5d23bf0f93454cf708..748eeee0ee4c4dffccbe4ce1d38868856b315984 100644 (file)
@@ -27,7 +27,7 @@ import { map, switchMap } from 'rxjs/operators';
 import { HealthService } from '~/app/shared/api/health.service';
 import { CephfsAuthModalComponent } from '~/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component';
 
-const BASE_URL = 'cephfs';
+const BASE_URL = 'cephfs/fs';
 
 @Component({
   selector: 'cd-cephfs-list',
index f204ac6d8b6b5f8caa32cd9d95a7aacf7984f0ce..cbdc44f3ca8c2c8fd67dbacb5ac6b05cca71b209 100644 (file)
@@ -1,5 +1,9 @@
+export enum SUPPORTED_FSAL {
+  CEPH = 'CEPH',
+  RGW = 'RGW'
+}
 export interface NfsFSAbstractionLayer {
-  value: string;
+  value: SUPPORTED_FSAL;
   descr: string;
   disabled: boolean;
 }
index f54361a5f7d11af851bbdef938e705ad0106761e..ae427886f0e2c33625e1ce2cdaf9f4ba1c3d31fa 100644 (file)
@@ -15,9 +15,6 @@
                  for="cluster_id">
             <span class="required"
                   i18n>Cluster</span>
-            <cd-helper>
-              <p i18n>This is the ID of an NFS Service.</p>
-            </cd-helper>
           </label>
           <div class="cd-col-form-input">
             <select class="form-select"
@@ -36,6 +33,9 @@
               <option *ngFor="let cluster of allClusters"
                       [value]="cluster.cluster_id">{{ cluster.cluster_id }}</option>
             </select>
+            <cd-help-text>
+              <p i18n>This is the ID of an NFS Service.</p>
+            </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('cluster_id', formDir, 'required') || allClusters?.length === 0"
                   i18n>This field is required.
 
         <!-- FSAL -->
         <div formGroupName="fsal">
-          <!-- Name -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="name"
-                   i18n>Storage Backend</label>
-            <div class="cd-col-form-input">
-              <select class="form-select"
-                      formControlName="name"
-                      name="name"
-                      id="name"
-                      (change)="fsalChangeHandler()">
-                <option *ngIf="allFsals === null"
-                        value=""
-                        i18n>Loading...</option>
-                <option *ngIf="allFsals !== null && allFsals.length === 0"
-                        value=""
-                        i18n>-- No data pools available --</option>
-                <option *ngIf="allFsals !== null && allFsals.length > 0"
-                        value=""
-                        i18n>-- Select the storage backend --</option>
-                <option *ngFor="let fsal of allFsals"
-                        [value]="fsal.value"
-                        [disabled]="fsal.disabled">{{ fsal.descr }}</option>
-              </select>
-              <span class="invalid-feedback"
-                    *ngIf="nfsForm.showError('name', formDir, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="fsalAvailabilityError"
-                    i18n>{{ fsalAvailabilityError }}</span>
-            </div>
-          </div>
-
           <!-- CephFS Volume -->
           <div class="form-group row"
-               *ngIf="nfsForm.getValue('name') === 'CEPH'">
+               *ngIf="storageBackend === 'CEPH'">
             <label class="cd-col-form-label required"
                    for="fs_name"
                    i18n>Volume</label>
 
         <!-- Security Label -->
         <div class="form-group row"
-             *ngIf="nfsForm.getValue('name') === 'CEPH'">
+             *ngIf="storageBackend === 'CEPH'">
           <label class="cd-col-form-label"
                  [ngClass]="{'required': nfsForm.getValue('security_label')}"
                  for="security_label"
 
         <!-- Path -->
         <div class="form-group row"
-             *ngIf="nfsForm.getValue('name') === 'CEPH'">
+             *ngIf="storageBackend === 'CEPH'">
           <label class="cd-col-form-label"
                  for="path">
             <span class="required"
                   i18n>CephFS Path</span>
-            <cd-helper>
-              <p i18n>A path in a CephFS file system.</p>
-            </cd-helper>
           </label>
           <div class="cd-col-form-input">
             <input type="text"
                    [ngbTypeahead]="pathDataSource"
                    (selectItem)="pathChangeHandler()"
                    (blur)="pathChangeHandler()">
+            <cd-help-text>
+            <p i18n>A path in a CephFS file system.</p>
+          </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('path', formDir, 'required')"
                   i18n>This field is required.</span>
 
         <!-- Bucket -->
         <div class="form-group row"
-             *ngIf="nfsForm.getValue('name') === 'RGW'">
+             *ngIf="storageBackend === 'RGW'">
           <label class="cd-col-form-label"
                  for="path">
             <span class="required"
                  for="pseudo">
             <span class="required"
                   i18n>Pseudo</span>
-            <cd-helper>
-              <p i18n>The position that this <strong>NFS v4</strong> export occupies
-                in the <strong>Pseudo FS</strong> (it must be unique).</p>
-              <p i18n>By using different Pseudo options, the same Path may be exported multiple times.</p>
-            </cd-helper>
           </label>
           <div class="cd-col-form-input">
             <input type="text"
                    id="pseudo"
                    formControlName="pseudo"
                    minlength="2">
+            <cd-help-text>
+              <span i18n>The position this export occupies in the Pseudo FS. It must be unique.</span><br/>
+              <span i18n>By using different Pseudo options, the same Path may be exported multiple times.</span>
+            </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('pseudo', formDir, 'required')"
                   i18n>This field is required.</span>
               {{ getAccessTypeHelp(nfsForm.getValue('access_type')) }}
             </span>
             <span class="form-text text-warning"
-                  *ngIf="nfsForm.getValue('access_type') === 'RW' && nfsForm.getValue('name') === 'RGW'"
+                  *ngIf="nfsForm.getValue('access_type') === 'RW' && storageBackend === 'RGW'"
                   i18n>The Object Gateway NFS backend has a number of
               limitations which will seriously affect applications writing to
               the share. Please consult the <cd-doc section="rgw-nfs"></cd-doc>
           <label class="cd-col-form-label"
                  for="squash">
             <span i18n>Squash</span>
-            <ng-container *ngTemplateOutlet="squashHelper"></ng-container>
           </label>
           <div class="cd-col-form-input">
             <select class="form-select"
                       [value]="squash">{{ squash }}</option>
 
             </select>
+            <cd-help-text>
+              <span *ngIf="nfsForm.getValue('squash') === 'root_squash'"
+                    i18n>Maps the root user on the NFS client to an anonymous user/group with limited privileges. This prevents a root client user from having total control over the NFS export.</span>
+
+              <span *ngIf="nfsForm.getValue('squash') === 'root_id_squash'"
+                    i18n>Maps the root user on the NFS client to an anonymous user/group with limited privileges, preventing root access but retaining non-root group privileges.</span>
+
+              <span *ngIf="nfsForm.getValue('squash') === 'all_squash'"
+                    i18n>Maps all users on the NFS client to an anonymous user/group with limited privileges, ensuring that no user has special privileges on the NFS export.</span>
+
+              <span *ngIf="nfsForm.getValue('squash') === 'no_root_squash'"
+                    i18n>Allows the root user on the NFS client to retain full root privileges on the NFS server, which may pose security risks.</span>
+
+            </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('squash', formDir,'required')"
                   i18n>This field is required.</span>
         <cd-nfs-form-client [form]="nfsForm"
                             [clients]="clients"
                             #nfsClients>
-          <ng-template #squashHelper>
-            <cd-helper>
-              <ul class="squash-helper">
-                <li>
-                  <span class="squash-helper-item-value">no_root_squash: </span>
-                  <span i18n>No user id squashing is performed.</span>
-                </li>
-                <li>
-                  <span class="squash-helper-item-value">root_id_squash: </span>
-                  <span i18n>uid 0 and gid 0 are squashed to the Anonymous_Uid and Anonymous_Gid gid 0 in alt_groups lists is also squashed.</span>
-                </li>
-                <li>
-                  <span class="squash-helper-item-value">root_squash: </span>
-                  <span i18n>uid 0 and gid of any value are squashed to the Anonymous_Uid and Anonymous_Gid alt_groups lists is discarded.</span>
-                </li>
-                <li>
-                  <span class="squash-helper-item-value">all_squash: </span>
-                  <span i18n>All users are squashed.</span>
-                </li>
-              </ul>
-            </cd-helper>
-          </ng-template>
         </cd-nfs-form-client>
 
+        <!-- Errors -->
+        <cd-alert-panel type="error"
+                        *ngIf="!!storageBackendError">
+          {{storageBackendError}}
+        </cd-alert-panel>
       </div>
-
       <div class="card-footer">
         <cd-form-button-panel (submitActionEvent)="submitAction()"
                               [form]="nfsForm"
+                              [disabled]="!!storageBackendError"
                               [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
                               wrappingClass="text-right"></cd-form-button-panel>
       </div>
index 4d892a120fc64f735d4ba921574256f677c9f8c6..cebcc8877a217ba752a01478da5bfe4d296ed08c 100644 (file)
@@ -1,11 +1,3 @@
 .cd-mb {
   margin-bottom: 10px;
 }
-
-.squash-helper {
-  padding-left: 1rem;
-}
-
-.squash-helper-item-value {
-  font-weight: bold;
-}
index 65267a1579164ac565b023b9db0483c2daf8de6a..e6fc6e8ddff30accad1bc90e40f3397c62707bc3 100644 (file)
@@ -1,7 +1,7 @@
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { ReactiveFormsModule } from '@angular/forms';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
@@ -21,6 +21,7 @@ describe('NfsFormComponent', () => {
   let fixture: ComponentFixture<NfsFormComponent>;
   let httpTesting: HttpTestingController;
   let activatedRoute: ActivatedRouteStub;
+  let router: Router;
 
   configureTestBed(
     {
@@ -45,9 +46,8 @@ describe('NfsFormComponent', () => {
 
   const matchSquash = (backendSquashValue: string, uiSquashValue: string) => {
     component.ngOnInit();
-    httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
-    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.expectOne('api/nfs-ganesha/export/mynfs/1').flush({
       fsal: {
         name: 'RGW'
@@ -69,12 +69,16 @@ describe('NfsFormComponent', () => {
     component = fixture.componentInstance;
     httpTesting = TestBed.inject(HttpTestingController);
     activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+    router = TestBed.inject(Router);
+
+    Object.defineProperty(router, 'url', {
+      get: jasmine.createSpy('url').and.returnValue('/cephfs/nfs')
+    });
     RgwHelper.selectDaemon();
     fixture.detectChanges();
 
-    httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
-    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.verify();
   });
 
@@ -82,15 +86,6 @@ describe('NfsFormComponent', () => {
     expect(component).toBeTruthy();
   });
 
-  it('should process all data', () => {
-    expect(component.allFsals).toEqual([
-      { descr: 'CephFS', value: 'CEPH', disabled: false },
-      { descr: 'Object Gateway', value: 'RGW', disabled: false }
-    ]);
-    expect(component.allFsNames).toEqual([{ id: 1, name: 'a' }]);
-    expect(component.allClusters).toEqual([{ cluster_id: 'mynfs' }]);
-  });
-
   it('should create the form', () => {
     expect(component.nfsForm.value).toEqual({
       access_type: 'RW',
index 0543a9eb7abbdb3d4ce81b1f06e99ad7cb1c66ec..98ef5c85914207b4f676fdd2fd005ce8787cddc9 100644 (file)
@@ -1,4 +1,4 @@
-import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
 import {
   AbstractControl,
   AsyncValidatorFn,
@@ -12,7 +12,7 @@ import _ from 'lodash';
 import { forkJoin, Observable, of } from 'rxjs';
 import { catchError, debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
 
-import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { SUPPORTED_FSAL } from '~/app/ceph/nfs/models/nfs.fsal';
 import { Directory, NfsService } from '~/app/shared/api/nfs.service';
 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
@@ -28,6 +28,7 @@ import { CdHttpErrorResponse } from '~/app/shared/services/api-interceptor.servi
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
+import { getFsalFromRoute, getPathfromFsal } from '../utils';
 
 @Component({
   selector: 'cd-nfs-form',
@@ -50,9 +51,10 @@ export class NfsFormComponent extends CdForm implements OnInit {
   allClusters: { cluster_id: string }[] = null;
   icons = Icons;
 
-  allFsals: any[] = [];
   allFsNames: any[] = null;
-  fsalAvailabilityError: string = null;
+
+  storageBackend: SUPPORTED_FSAL;
+  storageBackendError: string = null;
 
   defaultAccessType = { RGW: 'RO' };
   nfsAccessType: any[] = [];
@@ -87,25 +89,27 @@ export class NfsFormComponent extends CdForm implements OnInit {
     private rgwSiteService: RgwSiteService,
     private formBuilder: CdFormBuilder,
     private taskWrapper: TaskWrapperService,
-    private cdRef: ChangeDetectorRef,
     public actionLabels: ActionLabelsI18n
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().pool;
     this.resource = $localize`NFS export`;
+    this.storageBackend = getFsalFromRoute(this.router.url);
   }
 
   ngOnInit() {
     this.nfsAccessType = this.nfsService.nfsAccessType;
     this.nfsSquash = Object.keys(this.nfsService.nfsSquash);
     this.createForm();
-    const promises: Observable<any>[] = [
-      this.nfsService.listClusters(),
-      this.nfsService.fsals(),
-      this.nfsService.filesystems()
-    ];
+    const promises: Observable<any>[] = [this.nfsService.listClusters()];
 
-    if (this.router.url.startsWith('/nfs/edit')) {
+    if (this.storageBackend === 'RGW') {
+      promises.push(this.rgwSiteService.get('realms'));
+    } else {
+      promises.push(this.nfsService.filesystems());
+    }
+
+    if (this.router.url.startsWith(`/${getPathfromFsal(this.storageBackend)}/nfs/edit`)) {
       this.isEdit = true;
     }
 
@@ -115,7 +119,6 @@ export class NfsFormComponent extends CdForm implements OnInit {
         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.nfsForm.get('cluster_id').disable();
@@ -129,11 +132,9 @@ export class NfsFormComponent extends CdForm implements OnInit {
     forkJoin(promises).subscribe((data: any[]) => {
       this.resolveClusters(data[0]);
       this.resolveFsals(data[1]);
-      this.resolveFilesystems(data[2]);
-      if (data[3]) {
-        this.resolveModel(data[3]);
+      if (data[2]) {
+        this.resolveModel(data[2]);
       }
-
       this.loadingReady();
     });
   }
@@ -144,7 +145,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
         validators: [Validators.required]
       }),
       fsal: new CdFormGroup({
-        name: new UntypedFormControl('', {
+        name: new UntypedFormControl(this.storageBackend, {
           validators: [Validators.required]
         }),
         fs_name: new UntypedFormControl('', {
@@ -155,7 +156,9 @@ export class NfsFormComponent extends CdForm implements OnInit {
           ]
         })
       }),
-      path: new UntypedFormControl('/'),
+      path: new UntypedFormControl('/', {
+        validators: [Validators.required]
+      }),
       protocolNfsv3: new UntypedFormControl(true, {
         validators: [
           CdValidators.requiredIf({ protocolNfsv4: false }, (value: boolean) => {
@@ -247,20 +250,16 @@ export class NfsFormComponent extends CdForm implements OnInit {
   }
 
   resolveFsals(res: string[]) {
-    res.forEach((fsal) => {
-      const fsalItem = this.nfsService.nfsFsal.find((currentFsalItem) => {
-        return fsal === currentFsalItem.value;
-      });
-
-      if (_.isObjectLike(fsalItem)) {
-        this.allFsals.push(fsalItem);
-      }
-    });
-    if (!this.isEdit && this.allFsals.length > 0) {
+    if (this.storageBackend === 'RGW') {
+      this.setPathValidation();
+      this.resolveRealms(res);
+    } else {
+      this.resolveFilesystems(res);
+    }
+    if (!this.isEdit && this.storageBackend === SUPPORTED_FSAL.RGW) {
       this.nfsForm.patchValue({
-        fsal: {
-          name: this.allFsals[0].value
-        }
+        path: '',
+        access_type: this.defaultAccessType[SUPPORTED_FSAL.RGW]
       });
     }
   }
@@ -276,58 +275,26 @@ export class NfsFormComponent extends CdForm implements OnInit {
     }
   }
 
-  fsalChangeHandler() {
-    this.setPathValidation();
-    const fsalValue = this.nfsForm.getValue('name');
-    const checkAvailability =
-      fsalValue === 'RGW'
-        ? this.rgwSiteService.get('realms').pipe(
-            mergeMap((realms: string[]) =>
-              realms.length === 0
-                ? of(true)
-                : this.rgwSiteService.isDefaultRealm().pipe(
-                    mergeMap((isDefaultRealm) => {
-                      if (!isDefaultRealm) {
-                        throw new Error('Selected realm is not the default.');
-                      }
-                      return of(true);
-                    })
-                  )
-            )
-          )
-        : this.nfsService.filesystems();
-
-    checkAvailability.subscribe({
-      next: () => {
-        this.setFsalAvailability(fsalValue, true);
-        if (!this.isEdit) {
-          this.nfsForm.patchValue({
-            path: fsalValue === 'RGW' ? '' : '/',
-            pseudo: this.generatePseudo(),
-            access_type: this.updateAccessType()
-          });
-        }
-
-        this.cdRef.detectChanges();
-      },
-      error: (error) => {
-        this.setFsalAvailability(fsalValue, false, error);
-        this.nfsForm.get('name').setValue('');
-      }
-    });
-  }
-
-  private setFsalAvailability(fsalValue: string, available: boolean, errorMessage: string = '') {
-    this.allFsals = this.allFsals.map((fsalItem: NfsFSAbstractionLayer) => {
-      if (fsalItem.value === fsalValue) {
-        fsalItem.disabled = !available;
-
-        this.fsalAvailabilityError = fsalItem.disabled
-          ? $localize`${fsalItem.descr} backend is not available. ${errorMessage}`
-          : null;
-      }
-      return fsalItem;
-    });
+  resolveRealms(realms: string[]) {
+    if (realms.length !== 0) {
+      this.rgwSiteService
+        .isDefaultRealm()
+        .pipe(
+          mergeMap((isDefaultRealm) => {
+            if (!isDefaultRealm) {
+              throw new Error('Selected realm is not the default.');
+            }
+            return of(true);
+          })
+        )
+        .subscribe({
+          error: (error) => {
+            const fsalDescr = this.nfsService.nfsFsal.find((f) => f.value === this.storageBackend)
+              .descr;
+            this.storageBackendError = $localize`${fsalDescr} backend is not available. ${error}`;
+          }
+        });
+    }
   }
 
   accessTypeChangeHandler() {
@@ -338,8 +305,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
   setPathValidation() {
     const path = this.nfsForm.get('path');
-    path.setValidators([Validators.required]);
-    if (this.nfsForm.getValue('name') === 'RGW') {
+    if (this.storageBackend === SUPPORTED_FSAL.RGW) {
       path.setAsyncValidators([CdValidators.bucketExistence(true, this.rgwBucketService)]);
     } else {
       path.setAsyncValidators([this.pathExistence(true)]);
@@ -410,7 +376,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
     let newPseudo = this.nfsForm.getValue('pseudo');
     if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) {
       newPseudo = undefined;
-      if (this.nfsForm.getValue('fsal') === 'CEPH') {
+      if (this.storageBackend === 'CEPH') {
         newPseudo = '/cephfs';
         if (_.isString(this.nfsForm.getValue('path'))) {
           newPseudo += this.nfsForm.getValue('path');
@@ -420,17 +386,6 @@ export class NfsFormComponent extends CdForm implements OnInit {
     return newPseudo;
   }
 
-  private updateAccessType() {
-    const name = this.nfsForm.getValue('name');
-    let accessType = this.defaultAccessType[name];
-
-    if (!accessType) {
-      accessType = 'RW';
-    }
-
-    return accessType;
-  }
-
   submitAction() {
     let action: Observable<any>;
     const requestModel = this.buildRequest();
@@ -457,7 +412,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
     action.subscribe({
       error: (errorResponse: CdHttpErrorResponse) => this.setFormErrors(errorResponse),
-      complete: () => this.router.navigate(['/nfs'])
+      complete: () => this.router.navigate([`/${getPathfromFsal(this.storageBackend)}/nfs`])
     });
   }
 
index 5e43cdd658cb45d71a598751fc454bceecb61fea..1e82919f4029b25e4bb337b8a6c964903e3f5163 100644 (file)
@@ -17,6 +17,7 @@ import { SharedModule } from '~/app/shared/shared.module';
 import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
 import { NfsDetailsComponent } from '../nfs-details/nfs-details.component';
 import { NfsListComponent } from './nfs-list.component';
+import { SUPPORTED_FSAL } from '../models/nfs.fsal';
 
 describe('NfsListComponent', () => {
   let component: NfsListComponent;
@@ -45,6 +46,7 @@ describe('NfsListComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(NfsListComponent);
     component = fixture.componentInstance;
+    component.fsal = SUPPORTED_FSAL.CEPH;
     summaryService = TestBed.inject(SummaryService);
     nfsService = TestBed.inject(NfsService);
     httpTesting = TestBed.inject(HttpTestingController);
@@ -89,7 +91,9 @@ describe('NfsListComponent', () => {
       const model = {
         export_id: export_id,
         path: 'path_' + export_id,
-        fsal: 'fsal_' + export_id,
+        fsal: {
+          name: 'CEPH'
+        },
         cluster_id: 'cluster_' + export_id
       };
       exports.push(model);
@@ -102,7 +106,9 @@ describe('NfsListComponent', () => {
         case 'nfs/create':
           task.metadata = {
             path: 'path_' + export_id,
-            fsal: 'fsal_' + export_id,
+            fsal: {
+              name: 'CEPH'
+            },
             cluster_id: 'cluster_' + export_id
           };
           break;
index d5d0c2639300c322820a59b3c539c97942828aa2..8be95c6febe7991098e36305279f2be6e77e427c 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
 
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
@@ -22,6 +23,8 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { TaskListService } from '~/app/shared/services/task-list.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { getFsalFromRoute, getPathfromFsal } from '../utils';
+import { SUPPORTED_FSAL } from '../models/nfs.fsal';
 
 @Component({
   selector: 'cd-nfs-list',
@@ -46,6 +49,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
   exports: any[];
   tableActions: CdTableAction[];
   isDefaultCluster = false;
+  fsal: SUPPORTED_FSAL;
 
   modalRef: NgbModalRef;
 
@@ -65,10 +69,13 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
     private nfsService: NfsService,
     private taskListService: TaskListService,
     private taskWrapper: TaskWrapperService,
+    private router: Router,
     public actionLabels: ActionLabelsI18n
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().nfs;
+    this.fsal = getFsalFromRoute(this.router.url);
+    const prefix = getPathfromFsal(this.fsal);
     const getNfsUri = () =>
       this.selection.first() &&
       `${encodeURI(this.selection.first().cluster_id)}/${encodeURI(
@@ -78,7 +85,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
     const createAction: CdTableAction = {
       permission: 'create',
       icon: Icons.add,
-      routerLink: () => '/nfs/create',
+      routerLink: () => `/${prefix}/nfs/create`,
       canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
       name: this.actionLabels.CREATE
     };
@@ -86,7 +93,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
     const editAction: CdTableAction = {
       permission: 'update',
       icon: Icons.edit,
-      routerLink: () => `/nfs/edit/${getNfsUri()}`,
+      routerLink: () => `/${prefix}/nfs/edit/${getNfsUri()}`,
       name: this.actionLabels.EDIT
     };
 
@@ -103,7 +110,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
   ngOnInit() {
     this.columns = [
       {
-        name: $localize`Path`,
+        name: this.fsal === SUPPORTED_FSAL.CEPH ? $localize`Path` : $localize`Bucket`,
         prop: 'path',
         flexGrow: 2,
         cellTransformation: CellTemplate.executing
@@ -150,12 +157,12 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
 
   prepareResponse(resp: any): any[] {
     let result: any[] = [];
-    resp.forEach((nfs: any) => {
+    const filteredresp = resp.filter((nfs: any) => nfs.fsal?.name === this.fsal);
+    filteredresp.forEach((nfs: any) => {
       nfs.id = `${nfs.cluster_id}:${nfs.export_id}`;
       nfs.state = 'LOADING';
       result = result.concat(nfs);
     });
-
     return result;
   }
 
index 4205eb63b26e40f91f68b729a4a181eff9d43124..afd52472c54da7a307bbd884749bf37dd0a7f6c3 100644 (file)
@@ -21,6 +21,7 @@ import { NfsListComponent } from './nfs-list/nfs-list.component';
     NgbTypeaheadModule,
     NgbTooltipModule
   ],
+  exports: [NfsListComponent, NfsFormComponent, NfsDetailsComponent],
   declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent]
 })
 export class NfsModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/utils.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/utils.ts
new file mode 100644 (file)
index 0000000..2cdd7bb
--- /dev/null
@@ -0,0 +1,7 @@
+import { SUPPORTED_FSAL } from './models/nfs.fsal';
+
+export const getFsalFromRoute = (url: string): SUPPORTED_FSAL =>
+  url.startsWith('/rgw/nfs') ? SUPPORTED_FSAL.RGW : SUPPORTED_FSAL.CEPH;
+
+export const getPathfromFsal = (fsal: SUPPORTED_FSAL): string =>
+  fsal === SUPPORTED_FSAL.CEPH ? 'cephfs' : 'rgw';
index 04755928b0a91edd030bc52199f4a87351b205e8..dde99e5cc36b3d162bf5db39352cbb93a50449e3 100644 (file)
@@ -8,6 +8,8 @@ import { NgxPipeFunctionModule } from 'ngx-pipe-function';
 
 import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
 import { CRUDTableComponent } from '~/app/shared/datatable/crud-table/crud-table.component';
+import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service';
+import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
 
 import { SharedModule } from '~/app/shared/shared.module';
 import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
@@ -45,6 +47,8 @@ import { RgwSyncPrimaryZoneComponent } from './rgw-sync-primary-zone/rgw-sync-pr
 import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info/rgw-sync-metadata-info.component';
 import { RgwSyncDataInfoComponent } from './rgw-sync-data-info/rgw-sync-data-info.component';
 import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.component';
+import { NfsListComponent } from '../nfs/nfs-list/nfs-list.component';
+import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component';
 
 @NgModule({
   imports: [
@@ -194,6 +198,33 @@ const routes: Routes = [
     path: 'multisite',
     data: { breadcrumbs: 'Multi-site' },
     children: [{ path: '', component: RgwMultisiteDetailsComponent }]
+  },
+  {
+    path: 'nfs',
+    canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
+    data: {
+      moduleStatusGuardConfig: {
+        uiApiPath: 'nfs-ganesha',
+        redirectTo: 'error',
+        section: 'nfs-ganesha',
+        section_info: 'NFS GANESHA',
+        header: 'NFS-Ganesha is not configured'
+      },
+      breadcrumbs: 'NFS'
+    },
+    children: [
+      { path: '', component: NfsListComponent },
+      {
+        path: URLVerbs.CREATE,
+        component: NfsFormComponent,
+        data: { breadcrumbs: ActionLabels.CREATE }
+      },
+      {
+        path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+        component: NfsFormComponent,
+        data: { breadcrumbs: ActionLabels.EDIT }
+      }
+    ]
   }
 ];
 
index 9b4e4a0a288d06927e53b7e7e9027e37f5e22814..1fcce26e50a4ea9ab21f3024774c7a740394bc00 100644 (file)
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
 
 import { Observable, throwError } from 'rxjs';
 
-import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { NfsFSAbstractionLayer, SUPPORTED_FSAL } from '~/app/ceph/nfs/models/nfs.fsal';
 import { ApiClient } from '~/app/shared/api/api-client';
 
 export interface Directory {
@@ -34,12 +34,12 @@ export class NfsService extends ApiClient {
 
   nfsFsal: NfsFSAbstractionLayer[] = [
     {
-      value: 'CEPH',
+      value: SUPPORTED_FSAL.CEPH,
       descr: $localize`CephFS`,
       disabled: false
     },
     {
-      value: 'RGW',
+      value: SUPPORTED_FSAL.RGW,
       descr: $localize`Object Gateway`,
       disabled: false
     }