From: Dnyaneshwari Talwekar Date: Wed, 11 Feb 2026 10:55:43 +0000 (+0530) Subject: mgr/dashboard: NFS: Toggle visibility of CephFS snapshots X-Git-Tag: testing/wip-vshankar-testing-20260304.135307~18^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=e4f6620a9f21864e2381b00a3770d69fcff39d82;p=ceph-ci.git mgr/dashboard: NFS: Toggle visibility of CephFS snapshots Signed-off-by: Dnyaneshwari Talwekar Fixes: https://tracker.ceph.com/issues/74875 --- diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 92c3f3911b4..4c150bdc4b7 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -886,6 +886,68 @@ class CephFSSubvolume(RESTController): return False return True + @RESTController.Resource('GET', path='/snapshot-visibility') + def snapshot_visibility( + self, + vol_name: str, + subvol_name: str, + group_name: str = '' + ): + params = { + 'vol_name': vol_name, + 'sub_name': subvol_name + } + + if group_name: + params['group_name'] = group_name + + error_code, out, err = mgr.remote( + 'volumes', + '_cmd_fs_subvolume_snapshot_visibility_get', + None, + params + ) + + if error_code != 0: + raise DashboardException( + f'Failed to get snapshot visibility for subvolume ' + f'{subvol_name}: {err}' + ) + + return out + + @RESTController.Resource('PUT', path='/snapshot-visibility') + def set_snapshot_visibility( + self, + vol_name: str, + subvol_name: str, + value: str, + group_name: str = '' + ): + params = { + 'vol_name': vol_name, + 'sub_name': subvol_name, + 'value': value + } + + if group_name: + params['group_name'] = group_name + + error_code, out, err = mgr.remote( + 'volumes', + '_cmd_fs_subvolume_snapshot_visibility_set', + None, + params + ) + + if error_code != 0: + raise DashboardException( + f'Failed to set snapshot visibility for subvolume ' + f'{subvol_name}: {err}' + ) + + return out + @APIRouter('/cephfs/subvolume/group', Scope.CEPHFS) @APIDoc("Cephfs Subvolume Group Management API", "CephfsSubvolumeGroup") diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html index be9d1ee05b7..c28e1f541d7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html @@ -99,6 +99,28 @@ + + To manage snapshot visibility, first enable + client_respect_subvolume_snapshot_visibility. + + + +
+ + Allow snapshot browsing + + + When enabled, snapshots will be visible inside the subvolume directory under ".snap". + + + +
+
{ @@ -41,8 +43,15 @@ describe('CephfsSubvolumeFormComponent', () => { component.pools = []; component.ngOnInit(); formHelper = new FormHelper(component.subvolumeForm); - createSubVolumeSpy = spyOn(TestBed.inject(CephfsSubvolumeService), 'create').and.stub(); - editSubVolumeSpy = spyOn(TestBed.inject(CephfsSubvolumeService), 'update').and.stub(); + const subvolumeService = TestBed.inject(CephfsSubvolumeService); + createSubVolumeSpy = spyOn(subvolumeService, 'create').and.returnValue(of({ status: 200 })); + editSubVolumeSpy = spyOn(subvolumeService, 'update').and.returnValue(of({ status: 200 })); + spyOn(subvolumeService, 'setSnapshotVisibility').and.returnValue(of({ status: 200 })); + spyOn(subvolumeService, 'info').and.returnValue( + of({ bytes_quota: 'infinite', uid: 0, gid: 0, pool_namespace: false, mode: 755 } as any) + ); + spyOn(subvolumeService, 'getSnapshotVisibility').and.returnValue(of('1')); + spyOn(TestBed.inject(ConfigurationService), 'filter').and.returnValue(of([])); fixture.detectChanges(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts index 37590a73786..813d64e5310 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts @@ -1,6 +1,7 @@ import { Component, Inject, OnInit, Optional } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service'; +import { ConfigurationService } from '~/app/shared/api/configuration.service'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { FinishedTask } from '~/app/shared/models/finished-task'; @@ -9,13 +10,19 @@ import { Pool } from '../../pool/pool'; import { FormatterService } from '~/app/shared/services/formatter.service'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { CdValidators } from '~/app/shared/forms/cd-validators'; -import { CephfsSubvolumeInfo } from '~/app/shared/models/cephfs-subvolume.model'; +import { + CephfsSubvolumeInfo, + SNAPSHOT_VISIBILITY_CONFIG_NAME, + SNAPSHOT_VISIBILITY_CONFIG_SECTION +} from '~/app/shared/models/cephfs-subvolume.model'; +import { ConfigFormModel } from '~/app/shared/components/config-option/config-option.model'; import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; import { OctalToHumanReadablePipe } from '~/app/shared/pipes/octal-to-human-readable.pipe'; import { CdForm } from '~/app/shared/forms/cd-form'; import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service'; import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; +import { catchError, map, switchMap } from 'rxjs/operators'; @Component({ selector: 'cd-cephfs-subvolume-form', @@ -42,11 +49,14 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit { }; scopes: string[] = ['owner', 'group', 'others']; + showSnapshotVisibility = false; + constructor( private actionLabels: ActionLabelsI18n, private taskWrapper: TaskWrapperService, private cephFsSubvolumeService: CephfsSubvolumeService, private cephFsSubvolumeGroupService: CephfsSubvolumeGroupService, + private configurationService: ConfigurationService, private formatter: FormatterService, private dimlessBinary: DimlessBinaryPipe, private octalToHumanReadable: OctalToHumanReadablePipe, @@ -93,7 +103,35 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit { this.dataPools = this.pools.filter((pool) => pool.type === 'data'); this.createForm(); - this.isEdit ? this.populateForm() : this.loadingReady(); + this.loadSnapshotVisibilityConfig().subscribe(() => { + this.isEdit ? this.populateForm() : this.loadingReady(); + }); + } + + private loadSnapshotVisibilityConfig(): Observable { + return this.configurationService.filter([SNAPSHOT_VISIBILITY_CONFIG_NAME]).pipe( + map((configOptions: ConfigFormModel[]) => { + const options = configOptions ?? []; + const option = options.find((opt) => opt.name === SNAPSHOT_VISIBILITY_CONFIG_NAME); + this.showSnapshotVisibility = this.isSnapshotVisibilityEnabled(option); + const visibilityControl = this.subvolumeForm?.get('snapshotVisibility'); + if (!this.showSnapshotVisibility && visibilityControl) { + visibilityControl.disable(); + visibilityControl.setValue(false); + } + }), + catchError(() => { + this.showSnapshotVisibility = false; + return of(undefined); + }) + ); + } + + private isSnapshotVisibilityEnabled(option?: ConfigFormModel) { + const values = option?.value ?? []; + const clientValue = values.find((entry) => entry.section === SNAPSHOT_VISIBILITY_CONFIG_SECTION) + ?.value; + return String(clientValue).toLowerCase() === 'true'; } createForm() { @@ -122,7 +160,8 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit { uid: new FormControl(null), gid: new FormControl(null), mode: new FormControl({}), - isolatedNamespace: new FormControl(false) + isolatedNamespace: new FormControl(false), + snapshotVisibility: new FormControl(true) }); } @@ -149,10 +188,44 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit { this.subvolumeForm.get('isolatedNamespace').setValue(resp.pool_namespace); this.initialMode = this.octalToHumanReadable.transform(resp.mode, true); - this.loadingReady(); + if (!this.showSnapshotVisibility) { + this.subvolumeForm.get('snapshotVisibility').setValue(false); + } + this.loadSnapshotVisibility(); + }); + } + + private loadSnapshotVisibility() { + this.cephFsSubvolumeService + .getSnapshotVisibility(this.fsName, this.subVolumeName, this.subVolumeGroupName) + .subscribe({ + next: (visibilityResp) => { + const visibility = String(visibilityResp) === '1'; + this.subvolumeForm.get('snapshotVisibility').setValue(visibility); + this.loadingReady(); + }, + error: () => { + this.subvolumeForm.get('snapshotVisibility').setValue(true); + this.loadingReady(); + } }); } + private setSnapshotVisibility( + subVolumeName: string, + subVolumeGroupName: string, + visible: boolean + ) { + return this.showSnapshotVisibility + ? this.cephFsSubvolumeService.setSnapshotVisibility( + this.fsName, + subVolumeName, + visible, + subVolumeGroupName + ) + : of({ status: 200 }); + } + submit() { const subVolumeName = this.subvolumeForm.getValue('subvolumeName'); const subVolumeGroupName = this.subvolumeForm.getValue('subvolumeGroupName'); @@ -162,20 +235,24 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit { const gid = this.subvolumeForm.getValue('gid'); const mode = this.formatter.toOctalPermission(this.subvolumeForm.getValue('mode')); const isolatedNamespace = this.subvolumeForm.getValue('isolatedNamespace'); + const snapshotVisibility = this.subvolumeForm.getValue('snapshotVisibility'); if (this.isEdit) { const editSize = size === 0 ? 'infinite' : size; + const visibilityCall = this.setSnapshotVisibility( + subVolumeName, + subVolumeGroupName, + snapshotVisibility + ); + this.taskWrapper .wrapTaskAroundCall({ task: new FinishedTask('cephfs/subvolume/' + URLVerbs.EDIT, { subVolumeName: subVolumeName }), - call: this.cephFsSubvolumeService.update( - this.fsName, - subVolumeName, - String(editSize), - subVolumeGroupName - ) + call: this.cephFsSubvolumeService + .update(this.fsName, subVolumeName, String(editSize), subVolumeGroupName) + .pipe(switchMap(() => visibilityCall)) }) .subscribe({ error: () => { @@ -191,17 +268,23 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit { task: new FinishedTask('cephfs/subvolume/' + URLVerbs.CREATE, { subVolumeName: subVolumeName }), - call: this.cephFsSubvolumeService.create( - this.fsName, - subVolumeName, - subVolumeGroupName, - pool, - String(size), - uid, - gid, - mode, - isolatedNamespace - ) + call: this.cephFsSubvolumeService + .create( + this.fsName, + subVolumeName, + subVolumeGroupName, + pool, + String(size), + uid, + gid, + mode, + isolatedNamespace + ) + .pipe( + switchMap(() => + this.setSnapshotVisibility(subVolumeName, subVolumeGroupName, snapshotVisibility) + ) + ) }) .subscribe({ error: () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts index 6dfa82c4234..fae3e34d416 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts @@ -103,6 +103,28 @@ export class CephfsSubvolumeService { }); } + getSnapshotVisibility(fsName: string, subVolumeName: string, groupName: string = '') { + return this.http.get(`${this.baseURL}/${fsName}/snapshot-visibility`, { + params: { + subvol_name: subVolumeName, + group_name: groupName + } + }); + } + + setSnapshotVisibility( + fsName: string, + subVolumeName: string, + visible: boolean, + groupName: string = '' + ) { + return this.http.put(`${this.baseURL}/${fsName}/snapshot-visibility`, { + subvol_name: subVolumeName, + group_name: groupName, + value: visible.toString() + }); + } + getSnapshots( fsName: string, subVolumeName: string, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts index 25a2a5acc7f..3f225a77189 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts @@ -26,3 +26,7 @@ export interface SubvolumeSnapshotInfo { created_at: string; has_pending_clones: string; } + +export const SNAPSHOT_VISIBILITY_CONFIG_NAME = 'client_respect_subvolume_snapshot_visibility'; + +export const SNAPSHOT_VISIBILITY_CONFIG_SECTION = 'client'; 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 bd4ddc5f85f..e92cee103b2 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 @@ -472,6 +472,10 @@ export class TaskMessageService { 'cephfs/subvolume/edit': this.newTaskMessage(this.commonOperations.update, (metadata) => this.subvolume(metadata) ), + 'cephfs/subvolume/snapshot_visibility/set': this.newTaskMessage( + this.commonOperations.update, + (metadata) => $localize`subvolume snapshot visibility for '${metadata.subVolumeName}'` + ), 'cephfs/subvolume/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) => this.subvolume(metadata) ), diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 5e1fc41fb3a..860c7997039 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -4704,6 +4704,102 @@ paths: - jwt: [] tags: - CephFSSubvolume + /api/cephfs/subvolume/{vol_name}/snapshot-visibility: + get: + parameters: + - in: path + name: vol_name + required: true + schema: + type: string + - in: query + name: subvol_name + required: true + schema: + type: string + - default: '' + in: query + name: group_name + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + application/vnd.ceph.api.v1.0+json: + schema: + 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: [] + tags: + - CephFSSubvolume + put: + parameters: + - in: path + name: vol_name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + group_name: + default: '' + type: string + subvol_name: + type: string + value: + type: string + required: + - subvol_name + - value + type: object + responses: + '200': + content: + application/json: + schema: + type: object + application/vnd.ceph.api.v1.0+json: + schema: + type: object + description: Resource updated. + '202': + content: + application/json: + schema: + type: object + application/vnd.ceph.api.v1.0+json: + schema: + type: object + description: Operation is still executing. Please check the task queue. + '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: [] + tags: + - CephFSSubvolume /api/cephfs/{fs_id}: get: parameters: