This commit adds support for subvolume snap scheduling and translations for snap schedule repeat schedules and retention policies
Fixes: https://tracker.ceph.com/issues/64372
Signed-off-by: Ivo Almeida <ialmeida@redhat.com>
)
return json.loads(output_json)
- def create(self, fs: str, path: str, snap_schedule: str, start: str, retention_policy=None):
+ def create(self, fs: str, path: str, snap_schedule: str, start: str, retention_policy=None,
+ subvol=None, group=None):
error_code, _, err = mgr.remote('snap_schedule',
'snap_schedule_add',
path,
snap_schedule,
start,
- fs)
+ fs,
+ subvol,
+ group)
if retention_policy:
retention_policies = retention_policy.split('|')
return f'Snapshot schedule for path {path} created successfully'
- def set(self, fs: str, path: str, retention_to_add=None, retention_to_remove=None):
+ def set(self, fs: str, path: str, retention_to_add=None, retention_to_remove=None,
+ subvol=None, group=None):
def editRetentionPolicies(method, retention_policy):
if not retention_policy:
return
retention_spec_or_period,
retention_count,
fs,
- None,
- None)
+ subvol,
+ group)
if error_code_retention != 0:
raise DashboardException(
f'Failed to add/remove retention policy for path {path}: {err_retention}'
return f'Retention policies for snapshot schedule on path {path} updated successfully'
@RESTController.Resource('DELETE')
- def delete_snapshot(self, fs: str, path: str, schedule: str, start: str):
+ def delete_snapshot(self, fs: str, path: str, schedule: str, start: str,
+ subvol=None, group=None):
error_code, _, err = mgr.remote('snap_schedule',
'snap_schedule_rm',
path,
schedule,
start,
fs,
- None,
- None)
+ subvol,
+ group)
if error_code != 0:
raise DashboardException(
f'Failed to delete snapshot schedule for path {path}: {err}'
return f'Snapshot schedule for path {path} deleted successfully'
@RESTController.Resource('POST')
- def deactivate(self, fs: str, path: str, schedule: str, start: str):
+ def deactivate(self, fs: str, path: str, schedule: str, start: str, subvol=None, group=None):
error_code, _, err = mgr.remote('snap_schedule',
'snap_schedule_deactivate',
path,
schedule,
start,
fs,
- None,
- None)
+ subvol,
+ group)
if error_code != 0:
raise DashboardException(
f'Failed to deactivate snapshot schedule for path {path}: {err}'
return f'Snapshot schedule for path {path} deactivated successfully'
@RESTController.Resource('POST')
- def activate(self, fs: str, path: str, schedule: str, start: str):
+ def activate(self, fs: str, path: str, schedule: str, start: str, subvol=None, group=None):
error_code, _, err = mgr.remote('snap_schedule',
'snap_schedule_activate',
path,
schedule,
start,
fs,
- None,
- None)
+ subvol,
+ group)
if error_code != 0:
raise DashboardException(
f'Failed to activate snapshot schedule for path {path}: {err}'
i18n>A snapshot schedule for this path already exists.</span>
</div>
</div>
+
<!--Start date -->
<div class="form-group row">
<label class="cd-col-form-label required"
import { Observable, OperatorFunction, of, timer } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
+import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
import { Icons } from '~/app/shared/enum/icons.enum';
const VALIDATON_TIMER = 300;
const DEBOUNCE_TIMER = 300;
+const DEFAULT_SUBVOLUME_GROUP = '_nogroup';
@Component({
selector: 'cd-cephfs-snapshotschedule-form',
retention!: string;
start!: string;
status!: string;
+ subvol!: string;
+ group!: string;
id!: number;
isEdit = false;
icons = Icons;
repeatFrequencies = Object.entries(RepeatFrequency);
retentionFrequencies = Object.entries(RetentionFrequency);
retentionPoliciesToRemove: RetentionPolicy[] = [];
+ isDefaultSubvolumeGroup = false;
+ subvolumeGroup!: string;
+ subvolume!: string;
+ isSubvolume = false;
currentTime!: NgbTimeStruct;
minDate!: NgbDateStruct;
private snapScheduleService: CephfsSnapshotScheduleService,
private taskWrapper: TaskWrapperService,
private cd: ChangeDetectorRef,
- public directoryStore: DirectoryStoreService
+ public directoryStore: DirectoryStoreService,
+ private subvolumeService: CephfsSubvolumeService
) {
super();
this.resource = $localize`Snapshot schedule`;
this.directoryStore.loadDirectories(this.id, '/', 3);
this.createForm();
this.isEdit ? this.populateForm() : this.loadingReady();
+ this.snapScheduleForm.get('directory').valueChanges.subscribe({
+ next: (value: string) => {
+ this.subvolumeGroup = value?.split?.('/')?.[2];
+ this.subvolume = value?.split?.('/')?.[3];
+ this.subvolumeService
+ .exists(
+ this.subvolume,
+ this.fsName,
+ this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
+ )
+ .subscribe({
+ next: (exists: boolean) => {
+ this.isSubvolume = exists;
+ this.isDefaultSubvolumeGroup =
+ exists && this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP;
+ }
+ });
+ }
+ });
}
get retentionPolicies() {
this.snapScheduleForm = new CdFormGroup(
{
directory: new FormControl(undefined, {
+ updateOn: 'blur',
validators: [Validators.required]
}),
startDate: new FormControl(this.minDate, {
const updateObj = {
fs: this.fsName,
path: this.path,
+ subvol: this.subvol,
+ group: this.group,
retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
};
};
const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
- if (retentionPoliciesValues) {
- snapScheduleObj['retention_policy'] = retentionPoliciesValues;
+
+ if (retentionPoliciesValues) snapScheduleObj['retention_policy'] = retentionPoliciesValues;
+
+ if (this.isSubvolume) snapScheduleObj['subvol'] = this.subvolume;
+
+ if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
+ snapScheduleObj['group'] = this.subvolumeGroup;
}
+
this.taskWrapper
.wrapTaskAroundCall({
task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
class="fw-bold"
[ngbTooltip]="fullpathTpl"
triggers="click:blur">
- {{ row.path | path }}
+ {{ row.path?.split?.("@")?.[0] | path }}
</span>
<span
*ngIf="row.active; else inactiveStatusTpl">
<i
[ngClass]="[icons.success, icons.large]"
- ngbTooltip="{{ row.path }} is active"
+ ngbTooltip="{{ row.path?.split?.('@')?.[0] }} is active"
class="text-success"
></i>
</span>
<i
[ngClass]="[icons.warning, icons.large]"
class="text-warning"
- ngbTooltip="{{ row.path }} has been deactivated"
+ ngbTooltip="{{ row.path?.split?.('@')?.[0] }} has been deactivated"
></i>
</ng-template>
data-toggle="tooltip"
[title]="row.path"
class="font-monospace"
- >{{ row.path }}
+ >{{ row.path?.split?.("@")?.[0] }}
<cd-copy-2-clipboard-button
*ngIf="row.path"
- [source]="row.path"
+ [source]="row.path?.split?.('@')?.[0]"
[byId]="false"
[showIconOnly]="true"
>
</ng-template>
</ng-template>
+<ng-template
+ #retentionTpl
+ let-row="row">
+ <ul *ngIf="row.retentionCopy.length; else noDataTpl">
+ <li *ngFor="let ret of row.retentionCopy">{{ ret }}</li>
+ </ul>
+</ng-template>
+
+<ng-template
+ #subvolTpl
+ let-row="row">
+ <span *ngIf="row.subvol; else noDataTpl">
+ {{row.subvol}}
+ </span>
+</ng-template>
+
+
+
+<ng-template #noDataTpl>-</ng-template>
+
<cd-table
[data]="snapshotSchedules$ | async"
*ngIf="snapScheduleModuleStatus$ | async"
+ul {
+ list-style: none;
+ padding: 0;
+}
@ViewChild('pathTpl', { static: true })
pathTpl: any;
+ @ViewChild('retentionTpl', { static: true })
+ retentionTpl: any;
+
+ @ViewChild('subvolTpl', { static: true })
+ subvolTpl: any;
+
@BlockUI()
blockUI: NgBlockUI;
if (!status) {
return of([]);
}
- return this.snapshotScheduleService.getSnapshotScheduleList('/', this.fsName);
+ return this.snapshotScheduleService
+ .getSnapshotScheduleList('/', this.fsName)
+ .pipe(map((list) => list.map((l) => ({ ...l, path: `${l.path}@${l.schedule}` }))));
}),
shareReplay(1)
)
this.columns = [
{ prop: 'path', name: $localize`Path`, flexGrow: 3, cellTemplate: this.pathTpl },
- { prop: 'subvol', name: $localize`Subvolume` },
- { prop: 'schedule', name: $localize`Repeat interval` },
- { prop: 'retention', name: $localize`Retention policy` },
+ { prop: 'subvol', name: $localize`Subvolume`, cellTemplate: this.subvolTpl },
+ { prop: 'scheduleCopy', name: $localize`Repeat interval` },
+ { prop: 'schedule', isHidden: true },
+ { prop: 'retentionCopy', name: $localize`Retention policy`, cellTemplate: this.retentionTpl },
+ { prop: 'retention', isHidden: true },
{ prop: 'created_count', name: $localize`Created Count` },
{ prop: 'pruned_count', name: $localize`Deleted Count` },
{ prop: 'start', name: $localize`Start time`, cellTransformation: CellTemplate.timeAgo },
}
deactivateSnapshotSchedule() {
- const { path, start, fs, schedule } = this.selection.first();
+ const { path, start, fs, schedule, subvol, group } = this.selection.first();
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
itemDescription: $localize`snapshot schedule`,
path,
schedule,
start,
- fs
+ fs,
+ subvol,
+ group
})
})
});
}
activateSnapshotSchedule() {
- const { path, start, fs, schedule } = this.selection.first();
+ const { path, start, fs, schedule, subvol, group } = this.selection.first();
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
itemDescription: $localize`snapshot schedule`,
path,
schedule,
start,
- fs
+ fs,
+ subvol,
+ group
})
})
});
}
deleteSnapshotSchedule() {
- const { path, start, fs, schedule } = this.selection.first();
+ const { path, start, fs, schedule, subvol, group } = this.selection.first();
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
itemDescription: $localize`snapshot schedule`,
path,
schedule,
start,
- fs
+ fs,
+ subvol,
+ group
})
})
});
import { intersection, isEqual, uniqWith } from 'lodash';
import { SnapshotSchedule } from '../models/snapshot-schedule';
import { of } from 'rxjs';
-import { RepeatFrequency } from '../enum/repeat-frequency.enum';
+import {
+ RepeaFrequencyPlural,
+ RepeaFrequencySingular,
+ RepeatFrequency
+} from '../enum/repeat-frequency.enum';
+import { RetentionFrequencyCopy } from '../enum/retention-frequency.enum';
@Injectable({
providedIn: 'root'
);
}
- delete({ fs, path, schedule, start }: Record<string, any>): Observable<any> {
- return this.http.delete(
- `${this.baseURL}/snapshot/schedule/${fs}/${encodeURIComponent(
- path
- )}/delete_snapshot?schedule=${schedule}&start=${encodeURIComponent(start)}`
- );
+ delete({ fs, path, schedule, start, subvol, group }: Record<string, any>): Observable<any> {
+ let deleteUrl = `${this.baseURL}/snapshot/schedule/${fs}/${encodeURIComponent(
+ path
+ )}/delete_snapshot?schedule=${schedule}&start=${encodeURIComponent(start)}`;
+ if (subvol && group) {
+ deleteUrl += `&subvol=${encodeURIComponent(subvol)}&group=${encodeURIComponent(group)}`;
+ }
+ return this.http.delete(deleteUrl);
}
checkScheduleExists(
uniqWith(
snapList.map((snapItem: SnapshotSchedule) => ({
...snapItem,
+ scheduleCopy: this.parseScheduleCopy(snapItem.schedule),
status: snapItem.active ? 'Active' : 'Inactive',
- subvol: snapItem?.subvol || ' - ',
+ subvol: snapItem?.subvol,
+ retentionCopy: this.parseRetentionCopy(snapItem?.retention),
retention: Object.values(snapItem?.retention || [])?.length
? Object.entries(snapItem.retention)
?.map?.(([frequency, interval]) => `${interval}${frequency.toLocaleUpperCase()}`)
)
);
}
+
+ parseScheduleCopy(schedule: string): string {
+ const scheduleArr = schedule.split('');
+ const interval = Number(scheduleArr.filter((x) => !isNaN(Number(x))).join(''));
+ const frequencyUnit = scheduleArr[scheduleArr.length - 1];
+ const frequency =
+ interval > 1 ? RepeaFrequencyPlural[frequencyUnit] : RepeaFrequencySingular[frequencyUnit];
+ return $localize`Every ${interval > 1 ? interval + ' ' : ''}${frequency}`;
+ }
+
+ parseRetentionCopy(retention: string | Record<string, number>): string[] {
+ if (!retention) return ['-'];
+ return Object.entries(retention).map(([frequency, interval]) =>
+ $localize`${interval} ${RetentionFrequencyCopy[frequency]}`.toLocaleLowerCase()
+ );
+ }
}
Daily = 'd',
Weekly = 'w'
}
+
+export enum RepeaFrequencySingular {
+ h = 'hour',
+ d = 'day',
+ w = 'week'
+}
+
+export enum RepeaFrequencyPlural {
+ h = 'hours',
+ d = 'days',
+ w = 'weeks'
+}
Yearly = 'y',
'lastest snapshots' = 'n'
}
+
+export enum RetentionFrequencyCopy {
+ h = 'Hourly',
+ d = 'Daily',
+ w = 'Weekly',
+ m = 'Monthly',
+ y = 'Yearly',
+ n = 'lastest snapshots'
+}
properties:
fs:
type: string
+ group:
+ type: string
path:
type: string
retention_policy:
type: string
start:
type: string
+ subvol:
+ type: string
required:
- fs
- path
application/json:
schema:
properties:
+ group:
+ type: string
retention_to_add:
type: string
retention_to_remove:
type: string
+ subvol:
+ type: string
type: object
responses:
'200':
application/json:
schema:
properties:
+ group:
+ type: string
schedule:
type: string
start:
type: string
+ subvol:
+ type: string
required:
- schedule
- start
application/json:
schema:
properties:
+ group:
+ type: string
schedule:
type: string
start:
type: string
+ subvol:
+ type: string
required:
- schedule
- start
required: true
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: subvol
+ schema:
+ type: string
+ - allowEmptyValue: true
+ in: query
+ name: group
+ schema:
+ type: string
responses:
'202':
content: