Skip to content

Commit b35be54

Browse files
committed
mgr/dashboard: cephfs subvolume list snapshots
Added a tab for displaying the subvolume snapshots - this tab will show an info alert when there are no subvolumes present - if the subvolume is present, then it'll be auto-selected by default Implemented a filter to search the groups and subvolumes by its name. Also added a scrollbar when there are too many items in the nav list Modified the REST APIs to fetch only the names of the resources and fetch the info when an API call is requesting for it. Added unit tests Fixes: https://tracker.ceph.com/issues/63237 Signed-off-by: Nizamudeen A <[email protected]>
1 parent 122c6b2 commit b35be54

22 files changed

+588
-68
lines changed

src/pybind/mgr/dashboard/controllers/cephfs.py

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ def ls_dir(self, fs_id, path=None, depth=1):
676676
@APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume')
677677
class CephFSSubvolume(RESTController):
678678

679-
def get(self, vol_name: str, group_name: str = ""):
679+
def get(self, vol_name: str, group_name: str = "", info=True):
680680
params = {'vol_name': vol_name}
681681
if group_name:
682682
params['group_name'] = group_name
@@ -687,15 +687,17 @@ def get(self, vol_name: str, group_name: str = ""):
687687
f'Failed to list subvolumes for volume {vol_name}: {err}'
688688
)
689689
subvolumes = json.loads(out)
690-
for subvolume in subvolumes:
691-
params['sub_name'] = subvolume['name']
692-
error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
693-
params)
694-
if error_code != 0:
695-
raise DashboardException(
696-
f'Failed to get info for subvolume {subvolume["name"]}: {err}'
697-
)
698-
subvolume['info'] = json.loads(out)
690+
691+
if info:
692+
for subvolume in subvolumes:
693+
params['sub_name'] = subvolume['name']
694+
error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
695+
params)
696+
if error_code != 0:
697+
raise DashboardException(
698+
f'Failed to get info for subvolume {subvolume["name"]}: {err}'
699+
)
700+
subvolume['info'] = json.loads(out)
699701
return subvolumes
700702

701703
@RESTController.Resource('GET')
@@ -752,12 +754,27 @@ def delete(self, vol_name: str, subvol_name: str, group_name: str = "",
752754
component='cephfs')
753755
return f'Subvolume {subvol_name} removed successfully'
754756

757+
@RESTController.Resource('GET')
758+
def exists(self, vol_name: str, group_name=''):
759+
params = {'vol_name': vol_name}
760+
if group_name:
761+
params['group_name'] = group_name
762+
error_code, out, err = mgr.remote(
763+
'volumes', '_cmd_fs_subvolume_exist', None, params)
764+
if error_code != 0:
765+
raise DashboardException(
766+
f'Failed to check if subvolume exists: {err}'
767+
)
768+
if out == 'no subvolume exists':
769+
return False
770+
return True
771+
755772

756773
@APIRouter('/cephfs/subvolume/group', Scope.CEPHFS)
757774
@APIDoc("Cephfs Subvolume Group Management API", "CephfsSubvolumeGroup")
758775
class CephFSSubvolumeGroups(RESTController):
759776

760-
def get(self, vol_name):
777+
def get(self, vol_name, info=True):
761778
if not vol_name:
762779
raise DashboardException(
763780
f'Error listing subvolume groups for {vol_name}')
@@ -767,15 +784,17 @@ def get(self, vol_name):
767784
raise DashboardException(
768785
f'Error listing subvolume groups for {vol_name}')
769786
subvolume_groups = json.loads(out)
770-
for group in subvolume_groups:
771-
error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_info',
772-
None, {'vol_name': vol_name,
773-
'group_name': group['name']})
774-
if error_code != 0:
775-
raise DashboardException(
776-
f'Failed to get info for subvolume group {group["name"]}: {err}'
777-
)
778-
group['info'] = json.loads(out)
787+
788+
if info:
789+
for group in subvolume_groups:
790+
error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_info',
791+
None, {'vol_name': vol_name,
792+
'group_name': group['name']})
793+
if error_code != 0:
794+
raise DashboardException(
795+
f'Failed to get info for subvolume group {group["name"]}: {err}'
796+
)
797+
group['info'] = json.loads(out)
779798
return subvolume_groups
780799

781800
@RESTController.Resource('GET')
@@ -816,3 +835,31 @@ def delete(self, vol_name: str, group_name: str):
816835
f'Failed to delete subvolume group {group_name}: {err}'
817836
)
818837
return f'Subvolume group {group_name} removed successfully'
838+
839+
840+
@APIRouter('/cephfs/subvolume/snapshot', Scope.CEPHFS)
841+
@APIDoc("Cephfs Subvolume Snapshot Management API", "CephfsSubvolumeSnapshot")
842+
class CephFSSubvolumeSnapshots(RESTController):
843+
def get(self, vol_name: str, subvol_name, group_name: str = '', info=True):
844+
params = {'vol_name': vol_name, 'sub_name': subvol_name}
845+
if group_name:
846+
params['group_name'] = group_name
847+
error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_ls', None,
848+
params)
849+
if error_code != 0:
850+
raise DashboardException(
851+
f'Failed to list subvolume snapshots for subvolume {subvol_name}: {err}'
852+
)
853+
snapshots = json.loads(out)
854+
855+
if info:
856+
for snapshot in snapshots:
857+
params['snap_name'] = snapshot['name']
858+
error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_info',
859+
None, params)
860+
if error_code != 0:
861+
raise DashboardException(
862+
f'Failed to get info for subvolume snapshot {snapshot["name"]}: {err}'
863+
)
864+
snapshot['info'] = json.loads(out)
865+
return snapshots

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { CdTableAction } from '~/app/shared/models/cd-table-action';
99
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
1010
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
1111
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
12-
import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model';
1312
import { CephfsSubvolumegroupFormComponent } from '../cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component';
1413
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
1514
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
@@ -18,6 +17,7 @@ import { Permissions } from '~/app/shared/models/permissions';
1817
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
1918
import { FinishedTask } from '~/app/shared/models/finished-task';
2019
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
20+
import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model';
2121

2222
@Component({
2323
selector: 'cd-cephfs-subvolume-group',

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
11
<div class="row">
2-
<div class="col-sm-1">
3-
<h3 i18n>Groups</h3>
4-
<ng-container *ngIf="subVolumeGroups$ | async as subVolumeGroups">
5-
<ul class="nav flex-column nav-pills">
6-
<li class="nav-item">
7-
<a class="nav-link"
8-
[class.active]="!activeGroupName"
9-
(click)="selectSubVolumeGroup()">Default</a>
10-
</li>
11-
<li class="nav-item"
12-
*ngFor="let subVolumeGroup of subVolumeGroups">
13-
<a class="nav-link text-decoration-none text-break"
14-
[class.active]="subVolumeGroup.name === activeGroupName"
15-
(click)="selectSubVolumeGroup(subVolumeGroup.name)">{{subVolumeGroup.name}}</a>
16-
</li>
17-
</ul>
18-
</ng-container>
2+
<div class="col-sm-1"
3+
*ngIf="subVolumeGroups$ | async as subVolumeGroups">
4+
<cd-vertical-navigation title="Groups"
5+
[items]="subvolumeGroupList"
6+
inputIdentifier="group-filter"
7+
(emitActiveItem)="selectSubVolumeGroup($event)"></cd-vertical-navigation>
198
</div>
209
<div class="col-11 vertical-line">
2110
<cd-table [data]="subVolumes$ | async"

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
22
import { Observable, ReplaySubject, of } from 'rxjs';
3-
import { catchError, shareReplay, switchMap } from 'rxjs/operators';
3+
import { catchError, shareReplay, switchMap, tap } from 'rxjs/operators';
44
import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
55
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
66
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
@@ -22,7 +22,7 @@ import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
2222
import { CdForm } from '~/app/shared/forms/cd-form';
2323
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
2424
import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
25-
import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model';
25+
import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model';
2626

2727
@Component({
2828
selector: 'cd-cephfs-subvolume-list',
@@ -67,10 +67,12 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
6767
subject = new ReplaySubject<CephfsSubvolume[]>();
6868
groupsSubject = new ReplaySubject<CephfsSubvolume[]>();
6969

70+
subvolumeGroupList: string[] = [];
71+
7072
activeGroupName: string = '';
7173

7274
constructor(
73-
private cephfsSubVolume: CephfsSubvolumeService,
75+
private cephfsSubVolumeService: CephfsSubvolumeService,
7476
private actionLabels: ActionLabelsI18n,
7577
private modalService: ModalService,
7678
private authStorageService: AuthStorageService,
@@ -150,7 +152,11 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
150152

151153
this.subVolumeGroups$ = this.groupsSubject.pipe(
152154
switchMap(() =>
153-
this.cephfsSubvolumeGroupService.get(this.fsName).pipe(
155+
this.cephfsSubvolumeGroupService.get(this.fsName, false).pipe(
156+
tap((groups) => {
157+
this.subvolumeGroupList = groups.map((group) => group.name);
158+
this.subvolumeGroupList.unshift('');
159+
}),
154160
catchError(() => {
155161
this.context.error();
156162
return of(null);
@@ -203,7 +209,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
203209
this.taskWrapper
204210
.wrapTaskAroundCall({
205211
task: new FinishedTask('cephfs/subvolume/remove', { subVolumeName: this.selectedName }),
206-
call: this.cephfsSubVolume.remove(
212+
call: this.cephfsSubVolumeService.remove(
207213
this.fsName,
208214
this.selectedName,
209215
this.activeGroupName,
@@ -228,7 +234,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
228234
getSubVolumes(subVolumeGroupName = '') {
229235
this.subVolumes$ = this.subject.pipe(
230236
switchMap(() =>
231-
this.cephfsSubVolume.get(this.fsName, subVolumeGroupName).pipe(
237+
this.cephfsSubVolumeService.get(this.fsName, subVolumeGroupName).pipe(
232238
catchError(() => {
233239
this.context.error();
234240
return of(null);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<ng-container *ngIf="isLoading">
2+
<cd-loading-panel>
3+
<span i18n>Loading snapshots...</span>
4+
</cd-loading-panel>
5+
</ng-container>
6+
7+
<div class="row"
8+
*ngIf="isSubVolumesAvailable; else noGroupsTpl">
9+
<div class="col-sm-2">
10+
<cd-vertical-navigation title="Groups"
11+
[items]="subvolumeGroupList"
12+
inputIdentifier="group-filter"
13+
(emitActiveItem)="selectSubVolumeGroup($event)"></cd-vertical-navigation>
14+
</div>
15+
<div class="col-sm-2 vertical-line"
16+
*ngIf="subVolumes$ | async">
17+
<cd-vertical-navigation title="Subvolumes"
18+
[items]="subVolumesList"
19+
(emitActiveItem)="selectSubVolume($event)"
20+
inputIdentifier="subvol-filter"></cd-vertical-navigation>
21+
</div>
22+
<div class="col-8 vertical-line"
23+
*ngIf="isSubVolumesAvailable">
24+
<cd-table [data]="snapshots$ | async"
25+
columnMode="flex"
26+
[columns]="columns"
27+
selectionType="single"
28+
[hasDetails]="false"
29+
(fetchData)="fetchData()"></cd-table>
30+
</div>
31+
</div>
32+
<ng-template #noGroupsTpl>
33+
<cd-alert-panel type="info"
34+
i18n
35+
*ngIf="!isLoading">No subvolumes are present. Please create subvolumes to manage snapshots.</cd-alert-panel>
36+
</ng-template>

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { CephfsSubvolumeSnapshotsListComponent } from './cephfs-subvolume-snapshots-list.component';
4+
import { HttpClientTestingModule } from '@angular/common/http/testing';
5+
import { SharedModule } from '~/app/shared/shared.module';
6+
7+
describe('CephfsSubvolumeSnapshotsListComponent', () => {
8+
let component: CephfsSubvolumeSnapshotsListComponent;
9+
let fixture: ComponentFixture<CephfsSubvolumeSnapshotsListComponent>;
10+
11+
beforeEach(async () => {
12+
await TestBed.configureTestingModule({
13+
declarations: [CephfsSubvolumeSnapshotsListComponent],
14+
imports: [HttpClientTestingModule, SharedModule]
15+
}).compileComponents();
16+
17+
fixture = TestBed.createComponent(CephfsSubvolumeSnapshotsListComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
});
21+
22+
it('should create', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
26+
it('should show loading when the items are loading', () => {
27+
component.isLoading = true;
28+
fixture.detectChanges();
29+
expect(fixture.nativeElement.querySelector('cd-loading-panel')).toBeTruthy();
30+
});
31+
32+
it('should show the alert panel when there are no subvolumes', () => {
33+
component.isLoading = false;
34+
component.subvolumeGroupList = [];
35+
fixture.detectChanges();
36+
expect(fixture.nativeElement.querySelector('cd-alert-panel')).toBeTruthy();
37+
});
38+
});

0 commit comments

Comments
 (0)