From c0b2db74b2a99eb0ee4e4a37997d6516d8622855 Mon Sep 17 00:00:00 2001 From: Avan Thakkar Date: Fri, 5 Jul 2024 14:59:13 +0530 Subject: [PATCH] mgr/dashboard: add NFS export route from subvolume/subvolume grp Signed-off-by: Avan Thakkar --- .../mgr/dashboard/controllers/cephfs.py | 11 +- .../frontend/src/app/app-routing.module.ts | 7 +- .../cephfs-subvolume-group.component.ts | 11 ++ .../cephfs-subvolume-list.component.ts | 15 +++ .../ceph/nfs/nfs-form/nfs-form.component.html | 7 +- .../ceph/nfs/nfs-form/nfs-form.component.ts | 105 ++++++++++++++---- .../src/app/shared/constants/app.constants.ts | 3 + .../src/app/shared/enum/icons.enum.ts | 1 + 8 files changed, 136 insertions(+), 24 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 587e7677b0ed6..9f9b7501f44cf 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -871,8 +871,17 @@ class CephFSSubvolumeGroups(RESTController): if error_code != 0: raise DashboardException( f'Failed to get info for subvolume group {group_name}: {err}' + ) - return json.loads(out) + group = json.loads(out) + error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_getpath', None, { + 'vol_name': vol_name, 'group_name': group_name}) + if error_code != 0: + raise DashboardException( + f'Failed to get path for subvolume group {group_name}: {err}' + ) + group['path'] = out + return group def create(self, vol_name: str, group_name: str, **kwargs): error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_create', None, { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 2316896863e47..935be11e98b06 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -407,7 +407,12 @@ const routes: Routes = [ children: [ { path: '', component: NfsListComponent }, { - path: URLVerbs.CREATE, + path: `${URLVerbs.CREATE}/:fs_name/:subvolume_group`, + component: NfsFormComponent, + data: { breadcrumbs: ActionLabels.CREATE } + }, + { + path: `${URLVerbs.CREATE}`, component: NfsFormComponent, data: { breadcrumbs: ActionLabels.CREATE } }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts index a91daf8cb930d..a8b62556f28be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts @@ -18,6 +18,8 @@ import { CriticalConfirmationModalComponent } from '~/app/shared/components/crit import { FinishedTask } from '~/app/shared/models/finished-task'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import _ from 'lodash'; @Component({ selector: 'cd-cephfs-subvolume-group', @@ -54,6 +56,8 @@ export class CephfsSubvolumeGroupComponent implements OnInit, OnChanges { subvolumeGroup$: Observable; subject = new BehaviorSubject([]); + modalRef: NgbModalRef; + constructor( private cephfsSubvolumeGroup: CephfsSubvolumeGroupService, private actionLabels: ActionLabelsI18n, @@ -116,6 +120,13 @@ export class CephfsSubvolumeGroupComponent implements OnInit, OnChanges { icon: Icons.edit, click: () => this.openModal(true) }, + { + name: this.actionLabels.NFS_EXPORT, + permission: 'create', + icon: Icons.nfsExport, + routerLink: () => ['/cephfs/nfs/create', this.fsName, this.selection?.first()?.name], + disable: () => !this.selection.hasSingleSelection + }, { name: this.actionLabels.REMOVE, permission: 'delete', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts index 58d849c901ef5..2d646f968dc54 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts @@ -33,6 +33,9 @@ import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-g import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; import { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-mount-details.component'; import { HealthService } from '~/app/shared/api/health.service'; +import _ from 'lodash'; + +const DEFAULT_SUBVOLUME_GROUP = '_nogroup'; @Component({ selector: 'cd-cephfs-subvolume-list', @@ -159,6 +162,18 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh disable: () => !this.selection?.hasSelection, click: () => this.showAttachInfo() }, + { + name: this.actionLabels.NFS_EXPORT, + permission: 'create', + icon: Icons.nfsExport, + routerLink: () => [ + '/cephfs/nfs/create', + this.fsName, + _.isEmpty(this.activeGroupName) ? DEFAULT_SUBVOLUME_GROUP : this.activeGroupName, + { subvolume: this.selection?.first()?.name } + ], + disable: () => !this.selection?.hasSingleSelection + }, { name: this.actionLabels.REMOVE, permission: 'delete', 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 01350b4def4a8..ef78407b7e5ee 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 @@ -113,7 +113,7 @@
+ *ngIf="storageBackend === 'CEPH' && nfsForm.getValue('fsal').fs_name"> @@ -134,12 +134,13 @@ i18n>-- Select the CephFS subvolume group -- +
+ *ngIf="storageBackend === 'CEPH' && nfsForm.getValue('fsal').fs_name"> @@ -148,7 +149,7 @@ formControlName="subvolume" name="subvolume" id="subvolume" - (change)="getPath()"> + (change)="setSubVolPath()"> 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 f43067f231d91..18d91e2cddfdf 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 @@ -67,7 +67,11 @@ export class NfsFormComponent extends CdForm implements OnInit { allsubvolgrps: any[] = []; allsubvols: any[] = []; - fsPath: string = null; + + selectedFsName: string = ''; + selectedSubvolGroup: string = ''; + selectedSubvol: string = ''; + defaultSubVolGroup = '_nogroup'; pathDataSource = (text$: Observable) => { return text$.pipe( @@ -132,6 +136,14 @@ export class NfsFormComponent extends CdForm implements OnInit { this.nfsForm.get('cluster_id').disable(); } else { this.action = this.actionLabels.CREATE; + this.route.params.subscribe( + (params: { fs_name: string; subvolume_group: string; subvolume?: string }) => { + this.selectedFsName = params.fs_name; + this.selectedSubvolGroup = params.subvolume_group; + if (params.subvolume) this.selectedSubvol = params.subvolume; + } + ); + this.getData(promises); } } @@ -153,40 +165,69 @@ export class NfsFormComponent extends CdForm implements OnInit { this.getSubVolGrp(fs_name); } - getSubVol() { - this.getPath(); + async getSubVol() { const fs_name = this.nfsForm.getValue('fsal').fs_name; const subvolgrp = this.nfsForm.getValue('subvolume_group'); - return this.subvolService.get(fs_name, subvolgrp).subscribe((data: any) => { + await this.setSubVolGrpPath(); + + (subvolgrp === this.defaultSubVolGroup + ? this.subvolService.get(fs_name) + : this.subvolService.get(fs_name, subvolgrp) + ).subscribe((data: any) => { this.allsubvols = data; }); } getSubVolGrp(fs_name: string) { - return this.subvolgrpService.get(fs_name).subscribe((data: any) => { + this.subvolgrpService.get(fs_name).subscribe((data: any) => { this.allsubvolgrps = data; }); } - getFsPath(volList: any[], value: string) { - const match = volList.find((vol) => vol.name === value); - if (match) { - return match.info.path; - } + setSubVolGrpPath(): Promise { + return new Promise((resolve, reject) => { + const subvolGroup = this.nfsForm.getValue('subvolume_group'); + const fs_name = this.nfsForm.getValue('fsal').fs_name; + + if (subvolGroup === this.defaultSubVolGroup) { + this.updatePath('/volumes/' + this.defaultSubVolGroup); + resolve(); + } else { + this.subvolgrpService + .info(fs_name, subvolGroup) + .pipe(map((data) => data['path'])) + .subscribe( + (path) => { + this.updatePath(path); + resolve(); + }, + (error) => reject(error) + ); + } + }); } - getPath() { - const subvol = this.nfsForm.getValue('subvolume'); - if (subvol === '') { + setSubVolPath(): Promise { + return new Promise((resolve, reject) => { + const subvol = this.nfsForm.getValue('subvolume'); const subvolGroup = this.nfsForm.getValue('subvolume_group'); - this.fsPath = this.getFsPath(this.allsubvolgrps, subvolGroup); - } else { - this.fsPath = this.getFsPath(this.allsubvols, subvol); - } - this.nfsForm.patchValue({ - path: this.fsPath + const fs_name = this.nfsForm.getValue('fsal').fs_name; + + this.subvolService + .info(fs_name, subvol, subvolGroup === this.defaultSubVolGroup ? '' : subvolGroup) + .pipe(map((data) => data['path'])) + .subscribe( + (path) => { + this.updatePath(path); + resolve(); + }, + (error) => reject(error) + ); }); + } + updatePath(path: string) { + this.nfsForm.patchValue({ path: path }); this.pathChangeHandler(); } @@ -317,8 +358,34 @@ export class NfsFormComponent extends CdForm implements OnInit { } } + resolveRouteParams() { + if (!_.isEmpty(this.selectedFsName)) { + this.nfsForm.patchValue({ + fsal: { + fs_name: this.selectedFsName + } + }); + this.volumeChangeHandler(); + } + if (!_.isEmpty(this.selectedSubvolGroup)) { + this.nfsForm.patchValue({ + subvolume_group: this.selectedSubvolGroup + }); + this.getSubVol(); + } + if (!_.isEmpty(this.selectedSubvol)) { + this.nfsForm.patchValue({ + subvolume: this.selectedSubvol + }); + this.setSubVolPath(); + } + } + resolveFilesystems(filesystems: any[]) { this.allFsNames = filesystems; + if (!this.isEdit) { + this.resolveRouteParams(); + } } resolveRealms(realms: string[]) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts index 3140f481f4576..96bb439bec320 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts @@ -150,6 +150,7 @@ export class ActionLabelsI18n { AUTHORIZE: string; EXPAND_CLUSTER: string; SETUP_MULTISITE_REPLICATION: string; + NFS_EXPORT: string; constructor() { /* Create a new item */ @@ -239,6 +240,8 @@ export class ActionLabelsI18n { this.DISCONNECT = $localize`Disconnect`; this.RECONNECT = $localize`Reconnect`; this.EXPAND_CLUSTER = $localize`Expand Cluster`; + + this.NFS_EXPORT = $localize`Create NFS Export`; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 8f90a51cf8673..6219c73305163 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -84,6 +84,7 @@ export enum Icons { eye = 'fa fa-eye', // Observability calendar = 'fa fa-calendar', externalUrl = 'fa fa-external-link', // links to external page + nfsExport = 'fa fa-server', // NFS export /* Icons for special effect */ large = 'fa fa-lg', // icon becomes 33% larger -- 2.39.5