Skip to content

Commit c1da4c6

Browse files
authored
Merge pull request ceph#55416 from ivoalmeida/snapshot-schedule-edit
mgr/dashboard: added edit functionality Reviewed-by: Pedro Gonzalez Gomez <[email protected]> Reviewed-by: afreen23 <NOT@FOUND> Reviewed-by: Nizamudeen A <[email protected]>
2 parents f9d771f + a6763cb commit c1da4c6

File tree

6 files changed

+265
-54
lines changed

6 files changed

+265
-54
lines changed

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# -*- coding: utf-8 -*-
2+
# pylint: disable=too-many-lines
23
import errno
34
import json
45
import logging
56
import os
67
from collections import defaultdict
7-
from typing import Any, Dict
8+
from typing import Any, Dict, List
89

910
import cephfs
1011
import cherrypy
@@ -952,7 +953,7 @@ def list(self, fs: str, path: str = '/', recursive: bool = True):
952953
return []
953954

954955
snapshot_schedule_list = out.split('\n')
955-
output: list[Any] = []
956+
output: List[Any] = []
956957

957958
for snap in snapshot_schedule_list:
958959
current_path = snap.strip().split(' ')[0]
@@ -997,3 +998,30 @@ def create(self, fs: str, path: str, snap_schedule: str, start: str, retention_p
997998
)
998999

9991000
return f'Snapshot schedule for path {path} created successfully'
1001+
1002+
def set(self, fs: str, path: str, retention_to_add=None, retention_to_remove=None):
1003+
def editRetentionPolicies(method, retention_policy):
1004+
if not retention_policy:
1005+
return
1006+
1007+
retention_policies = retention_policy.split('|')
1008+
for retention in retention_policies:
1009+
retention_count = retention.split('-')[0]
1010+
retention_spec_or_period = retention.split('-')[1]
1011+
error_code_retention, _, err_retention = mgr.remote('snap_schedule',
1012+
method,
1013+
path,
1014+
retention_spec_or_period,
1015+
retention_count,
1016+
fs,
1017+
None,
1018+
None)
1019+
if error_code_retention != 0:
1020+
raise DashboardException(
1021+
f'Failed to add/remove retention policy for path {path}: {err_retention}'
1022+
)
1023+
1024+
editRetentionPolicies('snap_schedule_retention_rm', retention_to_remove)
1025+
editRetentionPolicies('snap_schedule_retention_add', retention_to_add)
1026+
1027+
return f'Retention policies for snapshot schedule on path {path} updated successfully'

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.ts

Lines changed: 136 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { CdForm } from '~/app/shared/forms/cd-form';
1414
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
1515
import { CdTableColumn } from '~/app/shared/models/cd-table-column';
1616
import { FinishedTask } from '~/app/shared/models/finished-task';
17-
import { RetentionPolicy, SnapshotScheduleFormValue } from '~/app/shared/models/snapshot-schedule';
17+
import {
18+
RetentionPolicy,
19+
SnapshotSchedule,
20+
SnapshotScheduleFormValue
21+
} from '~/app/shared/models/snapshot-schedule';
1822
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
1923

2024
const VALIDATON_TIMER = 300;
@@ -27,11 +31,17 @@ const DEBOUNCE_TIMER = 300;
2731
})
2832
export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
2933
fsName!: string;
34+
path!: string;
35+
schedule!: string;
36+
retention!: string;
37+
start!: string;
38+
status!: string;
3039
id!: number;
3140
isEdit = false;
3241
icons = Icons;
3342
repeatFrequencies = Object.entries(RepeatFrequency);
3443
retentionFrequencies = Object.entries(RetentionFrequency);
44+
retentionPoliciesToRemove: RetentionPolicy[] = [];
3545

3646
currentTime!: NgbTimeStruct;
3747
minDate!: NgbDateStruct;
@@ -71,7 +81,7 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
7181
this.action = this.actionLabels.CREATE;
7282
this.directoryStore.loadDirectories(this.id, '/', 3);
7383
this.createForm();
74-
this.loadingReady();
84+
this.isEdit ? this.populateForm() : this.loadingReady();
7585
}
7686

7787
get retentionPolicies() {
@@ -91,6 +101,50 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
91101
)
92102
);
93103

104+
populateForm() {
105+
this.action = this.actionLabels.EDIT;
106+
this.snapScheduleService.getSnapshotSchedule(this.path, this.fsName, false).subscribe({
107+
next: (response: SnapshotSchedule[]) => {
108+
const first = response.find((x) => x.path === this.path);
109+
this.snapScheduleForm.get('directory').disable();
110+
this.snapScheduleForm.get('directory').setValue(first.path);
111+
this.snapScheduleForm.get('startDate').disable();
112+
this.snapScheduleForm.get('startDate').setValue({
113+
year: new Date(first.start).getUTCFullYear(),
114+
month: new Date(first.start).getUTCMonth() + 1,
115+
day: new Date(first.start).getUTCDate()
116+
});
117+
this.snapScheduleForm.get('startTime').disable();
118+
this.snapScheduleForm.get('startTime').setValue({
119+
hour: new Date(first.start).getUTCHours(),
120+
minute: new Date(first.start).getUTCMinutes(),
121+
second: new Date(first.start).getUTCSeconds()
122+
});
123+
this.snapScheduleForm.get('repeatInterval').disable();
124+
this.snapScheduleForm.get('repeatInterval').setValue(first.schedule.split('')?.[0]);
125+
this.snapScheduleForm.get('repeatFrequency').disable();
126+
this.snapScheduleForm.get('repeatFrequency').setValue(first.schedule.split('')?.[1]);
127+
128+
// retention policies
129+
first.retention &&
130+
Object.entries(first.retention).forEach(([frequency, interval], idx) => {
131+
const freqKey = Object.keys(RetentionFrequency)[
132+
Object.values(RetentionFrequency).indexOf(frequency as any)
133+
];
134+
this.retentionPolicies.push(
135+
new FormGroup({
136+
retentionInterval: new FormControl(interval),
137+
retentionFrequency: new FormControl(RetentionFrequency[freqKey])
138+
})
139+
);
140+
this.retentionPolicies.controls[idx].get('retentionInterval').disable();
141+
this.retentionPolicies.controls[idx].get('retentionFrequency').disable();
142+
});
143+
this.loadingReady();
144+
}
145+
});
146+
}
147+
94148
createForm() {
95149
this.snapScheduleForm = new CdFormGroup(
96150
{
@@ -128,11 +182,19 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
128182
}
129183

130184
removeRetentionPolicy(idx: number) {
185+
if (this.isEdit && this.retentionPolicies.at(idx).disabled) {
186+
const values = this.retentionPolicies.at(idx).value as RetentionPolicy;
187+
this.retentionPoliciesToRemove.push(values);
188+
}
131189
this.retentionPolicies.removeAt(idx);
190+
this.retentionPolicies.controls.forEach((x) =>
191+
x.get('retentionFrequency').updateValueAndValidity()
192+
);
132193
this.cd.detectChanges();
133194
}
134195

135196
parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
197+
if (!date || !time) return null;
136198
return `${date.year}-${date.month}-${date.day}T${time.hour || '00'}:${time.minute || '00'}:${
137199
time.second || '00'
138200
}`;
@@ -156,40 +218,81 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
156218

157219
const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
158220

159-
const snapScheduleObj = {
160-
fs: this.fsName,
161-
path: values.directory,
162-
snap_schedule: this.parseSchedule(values.repeatInterval, values.repeatFrequency),
163-
start: this.parseDatetime(values.startDate, values.startTime)
164-
};
221+
if (this.isEdit) {
222+
const retentionPoliciesToAdd = (this.snapScheduleForm.get(
223+
'retentionPolicies'
224+
) as FormArray).controls
225+
?.filter(
226+
(ctrl) =>
227+
!ctrl.get('retentionInterval').disabled && !ctrl.get('retentionFrequency').disabled
228+
)
229+
.map((ctrl) => ({
230+
retentionInterval: ctrl.get('retentionInterval').value,
231+
retentionFrequency: ctrl.get('retentionFrequency').value
232+
}));
165233

166-
const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
167-
if (retentionPoliciesValues) {
168-
snapScheduleObj['retention_policy'] = retentionPoliciesValues;
169-
}
234+
const updateObj = {
235+
fs: this.fsName,
236+
path: this.path,
237+
retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
238+
retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
239+
};
170240

171-
this.taskWrapper
172-
.wrapTaskAroundCall({
173-
task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
174-
path: snapScheduleObj.path
175-
}),
176-
call: this.snapScheduleService.create(snapScheduleObj)
177-
})
178-
.subscribe({
179-
error: () => {
180-
this.snapScheduleForm.setErrors({ cdSubmitButton: true });
181-
},
182-
complete: () => {
183-
this.activeModal.close();
184-
}
185-
});
241+
this.taskWrapper
242+
.wrapTaskAroundCall({
243+
task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.EDIT, {
244+
path: this.path
245+
}),
246+
call: this.snapScheduleService.update(updateObj)
247+
})
248+
.subscribe({
249+
error: () => {
250+
this.snapScheduleForm.setErrors({ cdSubmitButton: true });
251+
},
252+
complete: () => {
253+
this.activeModal.close();
254+
}
255+
});
256+
} else {
257+
const snapScheduleObj = {
258+
fs: this.fsName,
259+
path: values.directory,
260+
snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
261+
start: this.parseDatetime(values?.startDate, values?.startTime)
262+
};
263+
264+
const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
265+
if (retentionPoliciesValues) {
266+
snapScheduleObj['retention_policy'] = retentionPoliciesValues;
267+
}
268+
this.taskWrapper
269+
.wrapTaskAroundCall({
270+
task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
271+
path: snapScheduleObj.path
272+
}),
273+
call: this.snapScheduleService.create(snapScheduleObj)
274+
})
275+
.subscribe({
276+
error: () => {
277+
this.snapScheduleForm.setErrors({ cdSubmitButton: true });
278+
},
279+
complete: () => {
280+
this.activeModal.close();
281+
}
282+
});
283+
}
186284
}
187285

188286
validateSchedule() {
189287
return (frm: AbstractControl) => {
190288
const directory = frm.get('directory');
191289
const repeatFrequency = frm.get('repeatFrequency');
192290
const repeatInterval = frm.get('repeatInterval');
291+
292+
if (this.isEdit) {
293+
return of(null);
294+
}
295+
193296
return timer(VALIDATON_TIMER).pipe(
194297
switchMap(() =>
195298
this.snapScheduleService
@@ -239,7 +342,12 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
239342
return null;
240343
}
241344
return this.snapScheduleService
242-
.checkRetentionPolicyExists(frm.get('directory').value, this.fsName, retentionList)
345+
.checkRetentionPolicyExists(
346+
frm.get('directory').value,
347+
this.fsName,
348+
retentionList,
349+
this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || []
350+
)
243351
.pipe(
244352
map(({ exists, errorIndex }) => {
245353
if (exists) {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ export class CephfsSnapshotscheduleListComponent
121121
name: this.actionLables.CREATE,
122122
permission: 'create',
123123
icon: Icons.add,
124+
click: () => this.openModal(false)
125+
},
126+
{
127+
name: this.actionLables.EDIT,
128+
permission: 'update',
129+
icon: Icons.edit,
124130
click: () => this.openModal(true)
125131
}
126132
];
@@ -145,6 +151,10 @@ export class CephfsSnapshotscheduleListComponent
145151
fsName: this.fsName,
146152
id: this.id,
147153
path: this.selection?.first()?.path,
154+
schedule: this.selection?.first()?.schedule,
155+
retention: this.selection?.first()?.retention,
156+
start: this.selection?.first()?.start,
157+
status: this.selection?.first()?.status,
148158
isEdit: edit
149159
},
150160
{ size: 'lg' }

src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ export class CephfsSnapshotScheduleService {
1919
return this.http.post(`${this.baseURL}/snapshot/schedule`, data, { observe: 'response' });
2020
}
2121

22+
update(data: Record<string, any>): Observable<any> {
23+
return this.http.put(
24+
`${this.baseURL}/snapshot/schedule/${data.fs}/${encodeURIComponent(data.path)}`,
25+
data,
26+
{ observe: 'response' }
27+
);
28+
}
29+
2230
checkScheduleExists(
2331
path: string,
2432
fs: string,
@@ -41,15 +49,21 @@ export class CephfsSnapshotScheduleService {
4149
checkRetentionPolicyExists(
4250
path: string,
4351
fs: string,
44-
retentionFrequencies: string[]
52+
retentionFrequencies: string[],
53+
retentionFrequenciesRemoved: string[] = []
4554
): Observable<{ exists: boolean; errorIndex: number }> {
46-
return this.getList(path, fs, false).pipe(
55+
return this.getSnapshotSchedule(path, fs, false).pipe(
4756
map((response) => {
4857
let errorIndex = -1;
4958
let exists = false;
5059
const index = response.findIndex((x) => x.path === path);
5160
const result = retentionFrequencies?.length
52-
? intersection(Object.keys(response?.[index]?.retention), retentionFrequencies)
61+
? intersection(
62+
Object.keys(response?.[index]?.retention).filter(
63+
(v) => !retentionFrequenciesRemoved.includes(v)
64+
),
65+
retentionFrequencies
66+
)
5367
: [];
5468
exists = !!result?.length;
5569
result?.forEach((r) => (errorIndex = retentionFrequencies.indexOf(r)));
@@ -62,10 +76,10 @@ export class CephfsSnapshotScheduleService {
6276
);
6377
}
6478

65-
private getList(path: string, fs: string, recursive = true): Observable<SnapshotSchedule[]> {
79+
getSnapshotSchedule(path: string, fs: string, recursive = true): Observable<SnapshotSchedule[]> {
6680
return this.http
6781
.get<SnapshotSchedule[]>(
68-
`${this.baseURL}/snapshot/schedule?path=${path}&fs=${fs}&recursive=${recursive}`
82+
`${this.baseURL}/snapshot/schedule/${fs}?path=${path}&recursive=${recursive}`
6983
)
7084
.pipe(
7185
catchError(() => {
@@ -79,7 +93,7 @@ export class CephfsSnapshotScheduleService {
7993
fs: string,
8094
recursive = true
8195
): Observable<SnapshotSchedule[]> {
82-
return this.getList(path, fs, recursive).pipe(
96+
return this.getSnapshotSchedule(path, fs, recursive).pipe(
8397
map((snapList: SnapshotSchedule[]) =>
8498
uniqWith(
8599
snapList.map((snapItem: SnapshotSchedule) => ({

src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ export class TaskMessageService {
390390
),
391391
'cephfs/snapshot/schedule/create': this.newTaskMessage(this.commonOperations.add, (metadata) =>
392392
this.snapshotSchedule(metadata)
393+
),
394+
'cephfs/snapshot/schedule/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
395+
this.snapshotSchedule(metadata)
393396
)
394397
};
395398

0 commit comments

Comments
 (0)