From cd394e51c8b351977cc13e01b9f3713a24dce565 Mon Sep 17 00:00:00 2001 From: Dnyaneshwari Date: Thu, 20 Feb 2025 10:58:02 +0530 Subject: [PATCH] mgr/dashboard: SMB - Edit Share. Fixes: https://tracker.ceph.com/issues/70094 Signed-off-by: Dnyaneshwari Talwekar --- src/pybind/mgr/dashboard/controllers/smb.py | 13 + .../frontend/src/app/app-routing.module.ts | 6 +- .../smb-share-form.component.html | 398 +++++++++--------- .../smb-share-form.component.ts | 77 +++- .../smb-share-list.component.ts | 16 +- .../frontend/src/app/ceph/smb/smb.model.ts | 2 + .../src/app/shared/api/smb.service.ts | 4 + .../shared/services/task-message.service.ts | 3 + src/pybind/mgr/dashboard/openapi.yaml | 93 ++++ 9 files changed, 399 insertions(+), 213 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/smb.py b/src/pybind/mgr/dashboard/controllers/smb.py index 9020d4cbd1946..bc9323e9947c2 100644 --- a/src/pybind/mgr/dashboard/controllers/smb.py +++ b/src/pybind/mgr/dashboard/controllers/smb.py @@ -254,6 +254,19 @@ class SMBShare(RESTController): except RuntimeError as e: raise DashboardException(e, component='smb') + @ReadPermission + @EndpointDoc("Get an smb share", + parameters={ + 'cluster_id': (str, 'Unique identifier for the cluster'), + 'share_id': (str, 'Unique identifier for the share') + }, + responses={200: SHARE_SCHEMA}) + def get(self, cluster_id: str, share_id: str) -> Share: + """ + Get an smb share by cluster and share id + """ + return mgr.remote('smb', 'show', [f'{self._resource}.{cluster_id}.{share_id}']) + @raise_on_failure @DeletePermission @EndpointDoc("Remove an smb share", 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 c755dd4f871fe..283d9e8f1a570 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 @@ -477,7 +477,11 @@ const routes: Routes = [ }, { path: `standalone/${URLVerbs.EDIT}/:usersGroupsId`, - component: SmbUsersgroupsFormComponent, + component: SmbUsersgroupsFormComponent + }, + { + path: `share/${URLVerbs.EDIT}/:clusterId/:shareId`, + component: SmbShareFormComponent, data: { breadcrumbs: ActionLabels.EDIT } } ] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html index 3691116984d00..fb0ffee86072a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.html @@ -1,217 +1,213 @@
-
-
- {{ action | titlecase }} {{ resource | upperFirst }} -
+ + +
+ {{ action | titlecase }} {{ resource | upperFirst }} +
- -
- Share Name - - - - +
+ This field is required. - -
+ helperText="Unique share identifier" + i18n-helperText + cdRequiredField="Share Name" + [invalid]="smbShareForm.controls.share_id.invalid && smbShareForm.controls.share_id.dirty" + [invalidText]="shareError" + >Share Name + + + + This field is required. + +
- -
- - - - - - - This field is required. - -
+ +
+ + + + + + + This field is required. + +
-
- - - - - -
+
+ + + + + +
-
- - - - - - -
+ + + + + +
- -
-
- Prefixed Path - - -
-
- Input Path - +
+
+ Prefixed Path + + +
+
+ - - - This field is required. - Path need to start with a '/' and can be followed by a word - + [invalidText]="pathError" + helperText="A relative path in a cephFS file system." + cdRequiredField="Path" + >Input Path + + + + This field is required. + +
-
- -
- Browseable - If selected the share will be included in share listings visible to - clients. - -
+ +
+ Browseable + If selected the share will be included in share listings visible to + clients. + +
- -
- Readonly - If selected no clients are permitted to write to the share. - -
- - + +
+ Readonly + If selected no clients are permitted to write to the share. + +
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts index fce6e75b225e8..8c9ff5ea10895 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-form/smb-share-form.component.ts @@ -11,7 +11,14 @@ import { map } from 'rxjs/operators'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { FinishedTask } from '~/app/shared/models/finished-task'; -import { Filesystem, PROVIDER, SHARE_RESOURCE, ShareRequestModel } from '../smb.model'; +import { + Filesystem, + PROVIDER, + SHARE_RESOURCE, + SHARE_URL, + ShareRequestModel, + SMBShare +} from '../smb.model'; import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; import { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model'; @@ -34,6 +41,9 @@ export class SmbShareFormComponent extends CdForm implements OnInit { allsubvolgrps: CephfsSubvolumeGroup[] = []; allsubvols: CephfsSubvolume[] = []; clusterId: string; + isEdit = false; + share_id: string; + shareResponse: SMBShare; constructor( private formBuilder: CdFormBuilder, @@ -48,16 +58,38 @@ export class SmbShareFormComponent extends CdForm implements OnInit { ) { super(); this.resource = $localize`Share`; + this.isEdit = this.router.url.startsWith(`${SHARE_URL}${URLVerbs.EDIT}`); + this.action = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE; } ngOnInit() { - this.action = this.actionLabels.CREATE; - this.route.params.subscribe((params: { clusterId: string }) => { + this.route.params.subscribe((params: any) => { + this.share_id = params.shareId; this.clusterId = params.clusterId; }); this.nfsService.filesystems().subscribe((data: Filesystem[]) => { this.allFsNames = data; }); this.createForm(); + if (this.isEdit) { + this.smbService.getShare(this.clusterId, this.share_id).subscribe((resp: SMBShare) => { + this.shareResponse = resp; + this.smbShareForm.get('share_id').setValue(this.shareResponse.share_id); + this.smbShareForm.get('share_id').disable(); + this.smbShareForm.get('volume').setValue(this.shareResponse.cephfs.volume); + this.smbShareForm.get('subvolume_group').setValue(this.shareResponse.cephfs.subvolumegroup); + this.smbShareForm.get('subvolume').setValue(this.shareResponse.cephfs.subvolume); + this.smbShareForm.get('inputPath').setValue(this.shareResponse.cephfs.path); + if (this.shareResponse.readonly) { + this.smbShareForm.get('readonly').setValue(this.shareResponse.readonly); + } + if (this.shareResponse.browseable) { + this.smbShareForm.get('browseable').setValue(this.shareResponse.browseable); + } + + this.getSubVolGrp(this.shareResponse.cephfs.volume); + }); + } + this.loadingReady(); } createForm() { @@ -100,6 +132,16 @@ export class SmbShareFormComponent extends CdForm implements OnInit { if (volume) { this.subvolgrpService.get(volume).subscribe((data: CephfsSubvolumeGroup[]) => { this.allsubvolgrps = data; + if (this.isEdit) { + const selectedSubVolGrp = this.shareResponse.cephfs.subvolumegroup; + if (selectedSubVolGrp && volume === this.shareResponse.cephfs.volume) { + const subvolGrp = this.allsubvolgrps.find((group) => group.name === selectedSubVolGrp); + if (subvolGrp) { + this.smbShareForm.get('subvolume_group').setValue(subvolGrp.name); + this.getSubVol(); + } + } + } }); } } @@ -117,6 +159,16 @@ export class SmbShareFormComponent extends CdForm implements OnInit { await this.setSubVolPath(); this.subvolService.get(volume, subvolgrp, false).subscribe((data: CephfsSubvolume[]) => { this.allsubvols = data; + if (this.isEdit) { + const selectedSubVol = this.shareResponse.cephfs.subvolume; + if (selectedSubVol && this.shareResponse.cephfs.subvolumegroup) { + const subvol = this.allsubvols.find((s) => s.name === selectedSubVol); + if (subvol) { + this.smbShareForm.get('subvolume').setValue(subvol.name); + this.setSubVolPath(); + } + } + } }); } } @@ -147,11 +199,12 @@ export class SmbShareFormComponent extends CdForm implements OnInit { buildRequest() { const rawFormValue = _.cloneDeep(this.smbShareForm.value); const correctedPath = rawFormValue.inputPath; + const shareId = this.smbShareForm.get('share_id')?.value; const requestModel: ShareRequestModel = { share_resource: { resource_type: SHARE_RESOURCE, cluster_id: this.clusterId, - share_id: rawFormValue.share_id, + share_id: shareId, cephfs: { volume: rawFormValue.volume, path: correctedPath, @@ -168,21 +221,29 @@ export class SmbShareFormComponent extends CdForm implements OnInit { } submitAction() { - const component = this; + if (this.isEdit) { + this.handleTaskRequest(URLVerbs.EDIT); + } else { + this.handleTaskRequest(URLVerbs.CREATE); + } + } + + handleTaskRequest(urlVerb: string) { const requestModel = this.buildRequest(); const BASE_URL = 'smb/share'; + const component = this; const share_id = this.smbShareForm.get('share_id').value; - const taskUrl = `${BASE_URL}/${URLVerbs.CREATE}`; + this.taskWrapperService .wrapTaskAroundCall({ - task: new FinishedTask(taskUrl, { share_id }), + task: new FinishedTask(`${BASE_URL}/${urlVerb}`, { share_id }), call: this.smbService.createShare(requestModel) }) .subscribe({ complete: () => { this.router.navigate([`cephfs/smb`]); }, - error() { + error: () => { component.smbShareForm.setErrors({ cdSubmitButton: true }); } }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts index 9b9aa86204a1e..e37dd69ff042a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts @@ -6,13 +6,13 @@ import { CdTableAction } from '~/app/shared/models/cd-table-action'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; import { Permission } from '~/app/shared/models/permissions'; -import { SMBShare } from '../smb.model'; +import { SHARE_URL, SMBShare } from '../smb.model'; import { SmbService } from '~/app/shared/api/smb.service'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; -import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; -import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { Icons } from '~/app/shared/enum/icons.enum'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; @@ -96,6 +96,16 @@ export class SmbShareListComponent implements OnInit { routerLink: () => ['/cephfs/smb/share/create', this.clusterId], canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection }, + { + name: this.actionLabels.EDIT, + permission: 'update', + icon: Icons.edit, + routerLink: () => [ + `${SHARE_URL}${URLVerbs.EDIT}`, + this.clusterId, + this.selection.first().name + ] + }, { permission: 'delete', icon: Icons.destroy, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts index 8f1ea56bbd504..58eb555b55a12 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts @@ -141,3 +141,5 @@ export const JOIN_AUTH_RESOURCE = 'ceph.smb.join.auth' as const; export const USERSGROUPS_RESOURCE = 'ceph.smb.usersgroups' as const; export const PROVIDER = 'samba-vfs'; + +export const SHARE_URL = '/cephfs/smb/share/'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts index d51196ced9cb9..960886ef55c80 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts @@ -56,6 +56,10 @@ export class SmbService { return this.http.post(`${this.baseURL}/share`, requestModel); } + getShare(cluster_id: string, share_id: string) { + return this.http.get(`${this.baseURL}/share/${cluster_id}/${share_id}`); + } + deleteShare(clusterId: string, shareId: string): Observable> { return this.http.delete(`${this.baseURL}/share/${clusterId}/${shareId}`, { observe: 'response' diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 281b3f714c981..6fb5a11482185 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -517,6 +517,9 @@ export class TaskMessageService { 'smb/share/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.smbShare(metadata) ), + 'smb/share/edit': this.newTaskMessage(this.commonOperations.update, (metadata) => + this.smbCluster(metadata) + ), 'smb/share/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) => this.smbShare(metadata) ), diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 396efb70a7921..851f7d87cf9dc 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -15912,6 +15912,99 @@ paths: summary: Remove an smb share tags: - SMB + get: + description: "\n Get an smb share by cluster and share id\n " + parameters: + - description: Unique identifier for the cluster + in: path + name: cluster_id + required: true + schema: + type: string + - description: Unique identifier for the share + in: path + name: share_id + required: true + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + schema: + properties: + browseable: + description: Indicates if the share is browseable + type: boolean + cephfs: + description: Configuration for the CephFS share + properties: + path: + description: Path within the CephFS file system + type: string + provider: + description: Provider of the CephFS share, e.g., 'samba-vfs' + type: string + subvolume: + description: Subvolume within the CephFS file system + type: string + subvolumegroup: + description: Subvolume Group in CephFS file system + type: string + volume: + description: Name of the CephFS file system + type: string + required: + - volume + - path + - provider + - subvolumegroup + - subvolume + type: object + cluster_id: + description: Unique identifier for the cluster + type: string + intent: + description: Desired state of the resource, e.g., 'present' or + 'removed' + type: string + name: + description: Name of the share + type: string + readonly: + description: Indicates if the share is read-only + type: boolean + resource_type: + description: ceph.smb.share + type: string + share_id: + description: Unique identifier for the share + type: string + required: + - resource_type + - cluster_id + - share_id + - intent + - name + - readonly + - browseable + - cephfs + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Get an smb share + tags: + - SMB /api/smb/usersgroups: get: description: "\n List all smb usersgroups resources\n\n :return:\ -- 2.39.5