From b839a7d97a003ab3950bd362680571dcb4addd88 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Mon, 7 Aug 2023 18:26:09 +0530 Subject: [PATCH] mgr/dashboard: provide ability to edit cephfs subvolume Fixes: https://tracker.ceph.com/issues/62347 Signed-off-by: Nizamudeen A (cherry picked from commit 07bc21f34f7068b36b198eff80c10685812178f6) --- .../mgr/dashboard/controllers/cephfs.py | 11 ++ .../cephfs-subvolume-form.component.html | 10 +- .../cephfs-subvolume-form.component.spec.ts | 40 ++++++ .../cephfs-subvolume-form.component.ts | 117 +++++++++++++----- .../cephfs-subvolume-list.component.html | 7 +- .../cephfs-subvolume-list.component.ts | 30 +++-- .../shared/api/cephfs-subvolume.service.ts | 9 +- .../usage-bar/usage-bar.component.html | 2 +- .../usage-bar/usage-bar.component.ts | 2 + .../checked-table-form.component.html | 3 + .../checked-table-form.component.ts | 7 +- .../shared/models/cephfs-subvolume.model.ts | 7 +- .../pipes/octal-to-human-readable.pipe.ts | 18 ++- .../shared/services/task-message.service.ts | 6 + src/pybind/mgr/dashboard/openapi.yaml | 44 +++++++ 15 files changed, 260 insertions(+), 53 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index ba90d7155079d..d0c34f1986bcb 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -658,3 +658,14 @@ class CephFSSubvolumeGroups(RESTController): raise DashboardException( f'Failed to create subvolume group {group_name}: {err}' ) + + def set(self, vol_name: str, subvol_name: str, size: str): + if size: + error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_resize', None, { + 'vol_name': vol_name, 'sub_name': subvol_name, 'new_size': size}) + if error_code != 0: + raise DashboardException( + f'Failed to update subvolume {subvol_name}: {err}' + ) + + return f'Subvolume {subvol_name} updated successfully' 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 4e3bb68014829..336e07cdb3702 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 @@ -2,7 +2,8 @@ {{ action | titlecase }} {{ resource | upperFirst }} - +
Size - The size of the subvolume is specified by setting a quota on it + The size of the subvolume is specified by setting a quota on it. + If left blank or put 0, then quota will be infinite
+ [initialValue]="initialMode" + [scopes]="scopes" + [isDisabled]="isEdit">
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts index 9407290f31688..392e5c54ac757 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts @@ -7,10 +7,15 @@ import { ToastrModule } from 'ngx-toastr'; import { SharedModule } from '~/app/shared/shared.module'; import { RouterTestingModule } from '@angular/router/testing'; import { ReactiveFormsModule } from '@angular/forms'; +import { FormHelper } from '~/testing/unit-test-helper'; +import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service'; describe('CephfsSubvolumeFormComponent', () => { let component: CephfsSubvolumeFormComponent; let fixture: ComponentFixture; + let formHelper: FormHelper; + let createSubVolumeSpy: jasmine.Spy; + let editSubVolumeSpy: jasmine.Spy; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -29,11 +34,46 @@ describe('CephfsSubvolumeFormComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CephfsSubvolumeFormComponent); component = fixture.componentInstance; + component.fsName = 'test_volume'; 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(); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have a form open in modal', () => { + const nativeEl = fixture.debugElement.nativeElement; + expect(nativeEl.querySelector('cd-modal')).not.toBe(null); + }); + + it('should have the volume name prefilled', () => { + component.ngOnInit(); + expect(component.subvolumeForm.get('volumeName').value).toBe('test_volume'); + }); + + it('should submit the form', () => { + formHelper.setValue('subvolumeName', 'test_subvolume'); + formHelper.setValue('size', 10); + component.submit(); + + expect(createSubVolumeSpy).toHaveBeenCalled(); + expect(editSubVolumeSpy).not.toHaveBeenCalled(); + }); + + it('should edit the subvolume', () => { + component.isEdit = true; + component.ngOnInit(); + formHelper.setValue('subvolumeName', 'test_subvolume'); + formHelper.setValue('size', 10); + component.submit(); + + expect(editSubVolumeSpy).toHaveBeenCalled(); + expect(createSubVolumeSpy).not.toHaveBeenCalled(); + }); }); 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 09d52dabe3b74..5ffb29bdce80d 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 @@ -11,15 +11,21 @@ import { FormatterService } from '~/app/shared/services/formatter.service'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import _ from 'lodash'; import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { CephfsSubvolumeInfo } from '~/app/shared/models/cephfs-subvolume.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'; @Component({ selector: 'cd-cephfs-subvolume-form', templateUrl: './cephfs-subvolume-form.component.html', styleUrls: ['./cephfs-subvolume-form.component.scss'] }) -export class CephfsSubvolumeFormComponent implements OnInit { +export class CephfsSubvolumeFormComponent extends CdForm implements OnInit { fsName: string; + subVolumeName: string; pools: Pool[]; + isEdit = false; subvolumeForm: CdFormGroup; @@ -30,6 +36,11 @@ export class CephfsSubvolumeFormComponent implements OnInit { columns: CdTableColumn[]; scopePermissions: Array = []; + initialMode = { + owner: ['read', 'write', 'execute'], + group: ['read', 'execute'], + others: ['read', 'execute'] + }; scopes: string[] = ['owner', 'group', 'others']; constructor( @@ -37,13 +48,16 @@ export class CephfsSubvolumeFormComponent implements OnInit { private actionLabels: ActionLabelsI18n, private taskWrapper: TaskWrapperService, private cephFsSubvolumeService: CephfsSubvolumeService, - private formatter: FormatterService + private formatter: FormatterService, + private dimlessBinary: DimlessBinaryPipe, + private octalToHumanReadable: OctalToHumanReadablePipe ) { - this.action = this.actionLabels.CREATE; + super(); this.resource = $localize`Subvolume`; } ngOnInit(): void { + this.action = this.actionLabels.CREATE; this.columns = [ { prop: 'scope', @@ -72,6 +86,8 @@ export class CephfsSubvolumeFormComponent implements OnInit { this.dataPools = this.pools.filter((pool) => pool.type === 'data'); this.createForm(); + + this.isEdit ? this.populateForm() : this.loadingReady(); } createForm() { @@ -102,37 +118,82 @@ export class CephfsSubvolumeFormComponent implements OnInit { }); } + populateForm() { + this.action = this.actionLabels.EDIT; + this.cephFsSubvolumeService + .info(this.fsName, this.subVolumeName) + .subscribe((resp: CephfsSubvolumeInfo) => { + // Disabled these fields since its not editable + this.subvolumeForm.get('subvolumeName').disable(); + this.subvolumeForm.get('pool').disable(); + this.subvolumeForm.get('uid').disable(); + this.subvolumeForm.get('gid').disable(); + + this.subvolumeForm.get('isolatedNamespace').disable(); + this.subvolumeForm.get('subvolumeName').setValue(this.subVolumeName); + if (resp.bytes_quota !== 'infinite') { + this.subvolumeForm.get('size').setValue(this.dimlessBinary.transform(resp.bytes_quota)); + } + this.subvolumeForm.get('uid').setValue(resp.uid); + this.subvolumeForm.get('gid').setValue(resp.gid); + this.subvolumeForm.get('isolatedNamespace').setValue(resp.pool_namespace); + this.initialMode = this.octalToHumanReadable.transform(resp.mode, true); + + this.loadingReady(); + }); + } + submit() { const subVolumeName = this.subvolumeForm.getValue('subvolumeName'); const pool = this.subvolumeForm.getValue('pool'); - const size = this.formatter.toBytes(this.subvolumeForm.getValue('size')); + const size = this.formatter.toBytes(this.subvolumeForm.getValue('size')) || 0; const uid = this.subvolumeForm.getValue('uid'); const gid = this.subvolumeForm.getValue('gid'); const mode = this.formatter.toOctalPermission(this.subvolumeForm.getValue('mode')); const isolatedNamespace = this.subvolumeForm.getValue('isolatedNamespace'); - this.taskWrapper - .wrapTaskAroundCall({ - task: new FinishedTask('cephfs/subvolume/' + URLVerbs.CREATE, { - subVolumeName: subVolumeName - }), - call: this.cephFsSubvolumeService.create( - this.fsName, - subVolumeName, - pool, - size, - uid, - gid, - mode, - isolatedNamespace - ) - }) - .subscribe({ - error: () => { - this.subvolumeForm.setErrors({ cdSubmitButton: true }); - }, - complete: () => { - this.activeModal.close(); - } - }); + + if (this.isEdit) { + const editSize = size === 0 ? 'infinite' : size; + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('cephfs/subvolume/' + URLVerbs.EDIT, { + subVolumeName: subVolumeName + }), + call: this.cephFsSubvolumeService.update(this.fsName, subVolumeName, String(editSize)) + }) + .subscribe({ + error: () => { + this.subvolumeForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.activeModal.close(); + } + }); + } else { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('cephfs/subvolume/' + URLVerbs.CREATE, { + subVolumeName: subVolumeName + }), + call: this.cephFsSubvolumeService.create( + this.fsName, + subVolumeName, + pool, + String(size), + uid, + gid, + mode, + isolatedNamespace + ) + }) + .subscribe({ + error: () => { + this.subvolumeForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.activeModal.close(); + } + }); + } } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html index 53aa454e4cb6a..94922c661e27c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html @@ -4,7 +4,8 @@ [columns]="columns" selectionType="single" [hasDetails]="false" - (fetchData)="fetchData()"> + (fetchData)="fetchData()" + (updateSelection)="updateSelection($event)">
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 14c0ea724da60..47953b34c89fe 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 @@ -107,15 +107,14 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges { name: this.actionLabels.CREATE, permission: 'create', icon: Icons.add, - click: () => - this.modalService.show( - CephfsSubvolumeFormComponent, - { - fsName: this.fsName, - pools: this.pools - }, - { size: 'lg' } - ) + click: () => this.openModal(), + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + }, + { + name: this.actionLabels.EDIT, + permission: 'update', + icon: Icons.edit, + click: () => this.openModal(true) } ]; @@ -143,4 +142,17 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges { updateSelection(selection: CdTableSelection) { this.selection = selection; } + + openModal(edit = false) { + this.modalService.show( + CephfsSubvolumeFormComponent, + { + fsName: this.fsName, + subVolumeName: this.selection?.first()?.name, + pools: this.pools, + isEdit: edit + }, + { size: 'lg' } + ); + } } 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 bd9a16e0e333b..ca7bf095891c2 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 @@ -21,7 +21,7 @@ export class CephfsSubvolumeService { fsName: string, subVolumeName: string, poolName: string, - size: number, + size: string, uid: number, gid: number, mode: string, @@ -62,4 +62,11 @@ export class CephfsSubvolumeService { }) ); } + + update(fsName: string, subVolumeName: string, size: string) { + return this.http.put(`${this.baseURL}/${fsName}`, { + subvol_name: subVolumeName, + size: size + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html index 10064941f75d6..70020436edecf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html @@ -4,7 +4,7 @@ Used: {{ isBinary ? (used | dimlessBinary) : (used | dimless) }} - + Free: {{ isBinary ? (total - used | dimlessBinary) : (total - used | dimless) }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts index e41ed4c310957..4940c19061ba3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts @@ -28,6 +28,8 @@ export class UsageBarComponent implements OnChanges { customLegend?: string; @Input() customLegendValue?: string; + @Input() + showFreeToolTip = true; usedPercentage: number; freePercentage: number; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html index 7d96239e93d01..dae4985d94316 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/checked-table-form/checked-table-form.component.html @@ -17,6 +17,7 @@ id="scope_{{ row.scope }}" type="checkbox" [checked]="isRowChecked(row.scope)" + [disabled]="isDisabled" (change)="onClickCellCheckbox(row.scope, column.prop, $event)"> @@ -31,6 +32,7 @@