Skip to content

Commit f5d1b7d

Browse files
committed
mgr/dashboard: subvolume snapshot creation form
Fixes: https://tracker.ceph.com/issues/63934 Signed-off-by: Nizamudeen A <[email protected]>
1 parent 281de0a commit f5d1b7d

15 files changed

+530
-21
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,33 @@ def get(self, vol_name: str, subvol_name, group_name: str = '', info=True):
864864
snapshot['info'] = json.loads(out)
865865
return snapshots
866866

867+
@RESTController.Resource('GET')
868+
def info(self, vol_name: str, subvol_name: str, snap_name: str, group_name: str = ''):
869+
params = {'vol_name': vol_name, 'sub_name': subvol_name, 'snap_name': snap_name}
870+
if group_name:
871+
params['group_name'] = group_name
872+
error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_info', None,
873+
params)
874+
if error_code != 0:
875+
raise DashboardException(
876+
f'Failed to get info for subvolume snapshot {snap_name}: {err}'
877+
)
878+
return json.loads(out)
879+
880+
def create(self, vol_name: str, subvol_name: str, snap_name: str, group_name=''):
881+
params = {'vol_name': vol_name, 'sub_name': subvol_name, 'snap_name': snap_name}
882+
if group_name:
883+
params['group_name'] = group_name
884+
885+
error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_create', None,
886+
params)
887+
888+
if error_code != 0:
889+
raise DashboardException(
890+
f'Failed to create subvolume snapshot {snap_name}: {err}'
891+
)
892+
return f'Subvolume snapshot {snap_name} created successfully'
893+
867894

868895
@APIRouter('/cephfs/snaphost/schedule', Scope.CEPHFS)
869896
@APIDoc("Cephfs Snapshot Scheduling API", "CephFSSnapshotSchedule")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<cd-modal [modalRef]="activeModal">
2+
<ng-container i18n="form title"
3+
class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
4+
5+
<ng-container class="modal-content"
6+
*cdFormLoading="loading">
7+
<form name="snapshotForm"
8+
#formDir="ngForm"
9+
[formGroup]="snapshotForm"
10+
novalidate>
11+
<div class="modal-body">
12+
<div class="form-group row">
13+
<label class="cd-col-form-label required"
14+
for="snapshotName"
15+
i18n>Name</label>
16+
<div class="cd-col-form-input">
17+
<input class="form-control"
18+
type="text"
19+
placeholder="Snapshot name..."
20+
id="snapshotName"
21+
name="snapshotName"
22+
formControlName="snapshotName"
23+
autofocus>
24+
<span class="invalid-feedback"
25+
*ngIf="snapshotForm.showError('snapshotName', formDir, 'required')"
26+
i18n>This field is required.</span>
27+
<span class="invalid-feedback"
28+
*ngIf="snapshotForm.showError('snapshotName', formDir, 'notUnique')"
29+
i18n>The snapshot already exists.</span>
30+
</div>
31+
</div>
32+
33+
<!-- Volume name -->
34+
<div class="form-group row">
35+
<label class="cd-col-form-label"
36+
for="volumeName"
37+
i18n>Volume name</label>
38+
<div class="cd-col-form-input">
39+
<input class="form-control"
40+
id="volumeName"
41+
name="volumeName"
42+
formControlName="volumeName">
43+
</div>
44+
</div>
45+
46+
<!--Subvolume Group name -->
47+
<div class="form-group row">
48+
<label class="cd-col-form-label"
49+
for="subvolumeGroupName"
50+
i18n>Subvolume group
51+
</label>
52+
<div class="cd-col-form-input">
53+
<select class="form-select"
54+
id="subvolumeGroupName"
55+
name="subvolumeGroupName"
56+
formControlName="subvolumeGroupName"
57+
#selection
58+
(change)="onSelectionChange(selection.value)"
59+
*ngIf="subVolumeGroups">
60+
<ng-container *ngFor="let subvolumegroup of subVolumeGroups">
61+
<option *ngIf="subvolumegroup == ''"
62+
value="">_nogroup</option>
63+
<option [value]="subvolumegroup"
64+
*ngIf="subvolumegroup !== ''">{{ subvolumegroup }}</option>
65+
</ng-container>
66+
</select>
67+
</div>
68+
</div>
69+
70+
<!--Subvolume name -->
71+
<div class="form-group row">
72+
<label class="cd-col-form-label"
73+
for="subVolumeName"
74+
i18n>Subvolume
75+
</label>
76+
<div class="cd-col-form-input">
77+
<select class="form-select"
78+
id="subVolumeName"
79+
name="subVolumeName"
80+
formControlName="subVolumeName"
81+
#selection
82+
(change)="resetValidators(selection.value)"
83+
*ngIf="subVolumes$ | async as subVolumes">
84+
<option *ngFor="let subVolume of subVolumes"
85+
[value]="subVolume.name">{{ subVolume.name }}</option>
86+
</select>
87+
</div>
88+
</div>
89+
</div>
90+
91+
<div class="modal-footer">
92+
<cd-form-button-panel (submitActionEvent)="submit()"
93+
[form]="snapshotForm"
94+
[submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
95+
</div>
96+
</form>
97+
</ng-container>
98+
</cd-modal>

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-form.component';
4+
import { configureTestBed } from '~/testing/unit-test-helper';
5+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
6+
import { SharedModule } from '~/app/shared/shared.module';
7+
import { ToastrModule } from 'ngx-toastr';
8+
import { ReactiveFormsModule } from '@angular/forms';
9+
import { HttpClientTestingModule } from '@angular/common/http/testing';
10+
import { RouterTestingModule } from '@angular/router/testing';
11+
12+
describe('CephfsSubvolumeSnapshotsFormComponent', () => {
13+
let component: CephfsSubvolumeSnapshotsFormComponent;
14+
let fixture: ComponentFixture<CephfsSubvolumeSnapshotsFormComponent>;
15+
16+
configureTestBed({
17+
declarations: [CephfsSubvolumeSnapshotsFormComponent],
18+
providers: [NgbActiveModal],
19+
imports: [
20+
SharedModule,
21+
ToastrModule.forRoot(),
22+
ReactiveFormsModule,
23+
HttpClientTestingModule,
24+
RouterTestingModule
25+
]
26+
});
27+
28+
beforeEach(() => {
29+
fixture = TestBed.createComponent(CephfsSubvolumeSnapshotsFormComponent);
30+
component = fixture.componentInstance;
31+
component.fsName = 'test_volume';
32+
component.subVolumeName = 'test_subvolume';
33+
component.subVolumeGroupName = 'test_subvolume_group';
34+
component.ngOnInit();
35+
fixture.detectChanges();
36+
});
37+
38+
it('should create', () => {
39+
expect(component).toBeTruthy();
40+
});
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { FormControl, Validators } from '@angular/forms';
3+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
4+
import moment from 'moment';
5+
import { Observable } from 'rxjs';
6+
import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
7+
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
8+
import { CdForm } from '~/app/shared/forms/cd-form';
9+
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
10+
import { CdValidators } from '~/app/shared/forms/cd-validators';
11+
import { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model';
12+
import { FinishedTask } from '~/app/shared/models/finished-task';
13+
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
14+
15+
@Component({
16+
selector: 'cd-cephfs-subvolume-snapshots-form',
17+
templateUrl: './cephfs-subvolume-snapshots-form.component.html',
18+
styleUrls: ['./cephfs-subvolume-snapshots-form.component.scss']
19+
})
20+
export class CephfsSubvolumeSnapshotsFormComponent extends CdForm implements OnInit {
21+
fsName: string;
22+
subVolumeName: string;
23+
subVolumeGroupName: string;
24+
subVolumeGroups: string[];
25+
26+
isEdit = false;
27+
28+
snapshotForm: CdFormGroup;
29+
30+
action: string;
31+
resource: string;
32+
33+
subVolumes$: Observable<CephfsSubvolume[]>;
34+
35+
constructor(
36+
public activeModal: NgbActiveModal,
37+
private actionLabels: ActionLabelsI18n,
38+
private taskWrapper: TaskWrapperService,
39+
private cephFsSubvolumeService: CephfsSubvolumeService
40+
) {
41+
super();
42+
this.resource = $localize`snapshot`;
43+
this.action = this.actionLabels.CREATE;
44+
}
45+
46+
ngOnInit(): void {
47+
this.createForm();
48+
49+
this.subVolumes$ = this.cephFsSubvolumeService.get(this.fsName, this.subVolumeGroupName, false);
50+
this.loadingReady();
51+
}
52+
53+
createForm() {
54+
this.snapshotForm = new CdFormGroup({
55+
snapshotName: new FormControl(moment().toISOString(true), {
56+
validators: [Validators.required],
57+
asyncValidators: [
58+
CdValidators.unique(
59+
this.cephFsSubvolumeService.snapshotExists,
60+
this.cephFsSubvolumeService,
61+
null,
62+
null,
63+
this.fsName,
64+
this.subVolumeName,
65+
this.subVolumeGroupName
66+
)
67+
]
68+
}),
69+
volumeName: new FormControl({ value: this.fsName, disabled: true }),
70+
subVolumeName: new FormControl(this.subVolumeName),
71+
subvolumeGroupName: new FormControl(this.subVolumeGroupName)
72+
});
73+
}
74+
75+
onSelectionChange(groupName: string) {
76+
this.subVolumeGroupName = groupName;
77+
this.subVolumes$ = this.cephFsSubvolumeService.get(this.fsName, this.subVolumeGroupName, false);
78+
this.subVolumes$.subscribe((subVolumes) => {
79+
this.subVolumeName = subVolumes[0].name;
80+
this.snapshotForm.get('subVolumeName').setValue(this.subVolumeName);
81+
82+
this.resetValidators();
83+
});
84+
}
85+
86+
resetValidators(subVolumeName?: string) {
87+
this.subVolumeName = subVolumeName;
88+
this.snapshotForm
89+
.get('snapshotName')
90+
.setAsyncValidators(
91+
CdValidators.unique(
92+
this.cephFsSubvolumeService.snapshotExists,
93+
this.cephFsSubvolumeService,
94+
null,
95+
null,
96+
this.fsName,
97+
this.subVolumeName,
98+
this.subVolumeGroupName
99+
)
100+
);
101+
this.snapshotForm.get('snapshotName').updateValueAndValidity();
102+
}
103+
104+
submit() {
105+
const snapshotName = this.snapshotForm.getValue('snapshotName');
106+
const subVolumeName = this.snapshotForm.getValue('subVolumeName');
107+
const subVolumeGroupName = this.snapshotForm.getValue('subvolumeGroupName');
108+
const volumeName = this.snapshotForm.getValue('volumeName');
109+
110+
this.taskWrapper
111+
.wrapTaskAroundCall({
112+
task: new FinishedTask('cephfs/subvolume/snapshot/' + URLVerbs.CREATE, {
113+
snapshotName: snapshotName
114+
}),
115+
call: this.cephFsSubvolumeService.createSnapshot(
116+
volumeName,
117+
snapshotName,
118+
subVolumeName,
119+
subVolumeGroupName
120+
)
121+
})
122+
.subscribe({
123+
error: () => this.snapshotForm.setErrors({ cdSubmitButton: true }),
124+
complete: () => this.activeModal.close()
125+
});
126+
}
127+
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,18 @@
2626
[columns]="columns"
2727
selectionType="single"
2828
[hasDetails]="false"
29-
(fetchData)="fetchData()"></cd-table>
29+
(fetchData)="fetchData()"
30+
(updateSelection)="updateSelection($event)">
31+
32+
<div class="table-actions btn-toolbar">
33+
<cd-table-actions [permission]="permissions.cephfs"
34+
[selection]="selection"
35+
class="btn-group"
36+
id="cephfs-snapshot-actions"
37+
[tableActions]="tableActions">
38+
</cd-table-actions>
39+
</div>
40+
</cd-table>
3041
</div>
3142
</div>
3243
<ng-template #noGroupsTpl>

0 commit comments

Comments
 (0)