return f'Clone {clone_name} created successfully'
-@APIRouter('/cephfs/snaphost/schedule', Scope.CEPHFS)
+@APIRouter('/cephfs/snapshot/schedule', Scope.CEPHFS)
@APIDoc("Cephfs Snapshot Scheduling API", "CephFSSnapshotSchedule")
class CephFSSnapshotSchedule(RESTController):
def list(self, fs: str, path: str = '/', recursive: bool = True):
error_code, out, err = mgr.remote('snap_schedule', 'snap_schedule_list',
- path, recursive, fs, 'plain')
-
+ path, recursive, fs, None, None, 'plain')
if len(out) == 0:
return []
snapshot_schedule_list = out.split('\n')
- output = []
+ output: list[Any] = []
for snap in snapshot_schedule_list:
current_path = snap.strip().split(' ')[0]
error_code, status_out, err = mgr.remote('snap_schedule', 'snap_schedule_get',
- current_path, fs, 'plain')
- output.append(json.loads(status_out))
+ current_path, fs, None, None, 'json')
+ output = output + json.loads(status_out)
output_json = json.dumps(output)
raise DashboardException(
f'Failed to get list of snapshot schedules for path {path}: {err}'
)
-
return json.loads(output_json)
+
+ def create(self, fs: str, path: str, snap_schedule: str, start: str, retention_policy=None):
+ error_code, _, err = mgr.remote('snap_schedule',
+ 'snap_schedule_add',
+ path,
+ snap_schedule,
+ start,
+ fs)
+
+ if retention_policy:
+ retention_policies = retention_policy.split('|')
+ for retention in retention_policies:
+ retention_count = retention.split('-')[0]
+ retention_spec_or_period = retention.split('-')[1]
+ error_code_retention, _, err_retention = mgr.remote('snap_schedule',
+ 'snap_schedule_retention_add',
+ path,
+ retention_spec_or_period,
+ retention_count,
+ fs)
+ if error_code_retention != 0:
+ raise DashboardException(
+ f'Failed to add retention policy for path {path}: {err_retention}'
+ )
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to create snapshot schedule for path {path}: {err}'
+ )
+
+ return f'Snapshot schedule for path {path} created successfully'
--- /dev/null
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+ <ng-container class="modal-content"
+ *cdFormLoading="loading">
+ <form name="snapScheduleForm"
+ #formDir="ngForm"
+ [formGroup]="snapScheduleForm"
+ novalidate>
+ <div class="modal-body">
+ <!-- Directory -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="directory"
+ i18n>Directory
+ </label>
+ <div class="cd-col-form-input">
+ <ng-template #loading>
+ <i [ngClass]="[icons.spinner, icons.spin, 'mt-2', 'me-2']"></i>
+ <span i18n>Loading directories</span>
+ </ng-template>
+ <select class="form-select"
+ id="directory"
+ name="directory"
+ formControlName="directory"
+ *ngIf="directories$ | async as directories; else loading">
+ <option [ngValue]="null"
+ i18n>--Select a directory--</option>
+ <option *ngFor="let dir of directories"
+ [value]="dir.path">{{ dir.path }}</option>
+ </select>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('directory', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('directory', formDir, 'notUnique')"
+ 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"
+ for="startDate"
+ i18n>Start date
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ placeholder="yyyy-mm-dd"
+ name="startDate"
+ id="startDate"
+ formControlName="startDate"
+ [minDate]="minDate"
+ ngbDatepicker
+ #d="ngbDatepicker"
+ (click)="d.open()">
+ <button type="button"
+ class="btn btn-light"
+ (click)="d.toggle()"
+ title="Open">
+ <i [ngClass]="icons.calendar"></i>
+ </button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('startDate', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- Start time -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="startTime"
+ i18n>Start time
+ <cd-helper>The time zone is assumed to be UTC.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <ngb-timepicker [spinners]="false"
+ [seconds]="false"
+ [meridian]="true"
+ formControlName="startTime"
+ id="startTime"
+ name="startTime"></ngb-timepicker>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('startTime', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- Repeat interval -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="repeatInterval"
+ i18n>Schedule
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="number"
+ min="1"
+ id="repeatInterval"
+ name="repeatInterval"
+ formControlName="repeatInterval">
+ <select [ngClass]="['form-select', 'me-5']"
+ id="repeatFrequency"
+ name="repeatFrequency"
+ formControlName="repeatFrequency"
+ *ngIf="repeatFrequencies">
+ <option *ngFor="let freq of repeatFrequencies"
+ [value]="freq[1]"
+ i18n>{{ freq[0] }}</option>
+ </select>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('repeatFrequency', formDir, 'notUnique')"
+ i18n>This schedule already exists for the selected directory.</span>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'min')"
+ i18n>Choose a value greater than 0.</span>
+ </div>
+ </div>
+ <!-- Retention policies -->
+ <ng-container formArrayName="retentionPolicies"
+ *ngFor="let retentionPolicy of retentionPolicies.controls; index as i">
+ <ng-container [formGroupName]="i">
+ <div class="form-group row">
+ <label [ngClass]="{'cd-col-form-label': true, 'visible': i == 0, 'invisible': i > 0}"
+ for="retentionInterval"
+ i18n>Retention policy
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <input class="form-control"
+ type="number"
+ min="1"
+ id="retentionInterval"
+ name="retentionInterval"
+ formControlName="retentionInterval">
+ <select class="form-select"
+ id="retentionFrequency"
+ name="retentionFrequency"
+ formControlName="retentionFrequency"
+ *ngIf="retentionFrequencies">
+ <option *ngFor="let freq of retentionFrequencies"
+ [value]="freq[1]"
+ i18n>{{ freq[0] }}</option>
+ </select>
+ <button class="btn btn-light"
+ type="button"
+ (click)="removeRetentionPolicy(i)">
+ <i [ngClass]="[icons.trash]"></i>
+ </button>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="snapScheduleForm.controls['retentionPolicies'].controls[i].invalid"
+ i18n>This retention policy already exists for the selected directory.</span>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+ <div class="d-flex flex-row align-content-center justify-content-end">
+ <button class="btn btn-light"
+ type="button"
+ (click)="addRetentionPolicy()">
+ <i [ngClass]="[icons.add, 'me-2']"></i>
+ <span i18n>Add retention policy</span>
+ </button>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="snapScheduleForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form.component';
+import {
+ NgbActiveModal,
+ NgbDatepickerModule,
+ NgbTimepickerModule
+} from '@ng-bootstrap/ng-bootstrap';
+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, configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
+
+describe('CephfsSnapshotscheduleFormComponent', () => {
+ let component: CephfsSnapshotscheduleFormComponent;
+ let fixture: ComponentFixture<CephfsSnapshotscheduleFormComponent>;
+ let formHelper: FormHelper;
+ let createSpy: jasmine.Spy;
+
+ configureTestBed({
+ declarations: [CephfsSnapshotscheduleFormComponent],
+ providers: [NgbActiveModal],
+ imports: [
+ SharedModule,
+ ToastrModule.forRoot(),
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ NgbDatepickerModule,
+ NgbTimepickerModule
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsSnapshotscheduleFormComponent);
+ component = fixture.componentInstance;
+ component.fsName = 'test_fs';
+ component.ngOnInit();
+ formHelper = new FormHelper(component.snapScheduleForm);
+ createSpy = spyOn(TestBed.inject(CephfsSnapshotScheduleService), 'create').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 submit the form', () => {
+ const input = {
+ directory: '/test',
+ startDate: {
+ year: 2023,
+ month: 11,
+ day: 14
+ },
+ startTime: {
+ hour: 0,
+ minute: 6,
+ second: 22
+ },
+ repeatInterval: 4,
+ repeatFrequency: 'h'
+ };
+
+ formHelper.setMultipleValues(input);
+ component.snapScheduleForm.get('directory').setValue('/test');
+ component.submit();
+
+ expect(createSpy).toHaveBeenCalled();
+ });
+});
--- /dev/null
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
+import { NgbActiveModal, NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
+import { uniq } from 'lodash';
+import { Observable, timer } from 'rxjs';
+import { map, switchMap } from 'rxjs/operators';
+import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { RepeatFrequency } from '~/app/shared/enum/repeat-frequency.enum';
+import { RetentionFrequency } from '~/app/shared/enum/retention-frequency.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CephfsDir } from '~/app/shared/models/cephfs-directory-models';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { RetentionPolicy, SnapshotScheduleFormValue } from '~/app/shared/models/snapshot-schedule';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+const VALIDATON_TIMER = 300;
+
+@Component({
+ selector: 'cd-cephfs-snapshotschedule-form',
+ templateUrl: './cephfs-snapshotschedule-form.component.html',
+ styleUrls: ['./cephfs-snapshotschedule-form.component.scss']
+})
+export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
+ fsName!: string;
+ id!: number;
+ isEdit = false;
+ icons = Icons;
+ repeatFrequencies = Object.entries(RepeatFrequency);
+ retentionFrequencies = Object.entries(RetentionFrequency);
+
+ currentTime!: NgbTimeStruct;
+ minDate!: NgbDateStruct;
+
+ snapScheduleForm!: CdFormGroup;
+
+ action!: string;
+ resource!: string;
+
+ columns!: CdTableColumn[];
+ directories$!: Observable<CephfsDir[]>;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private actionLabels: ActionLabelsI18n,
+ private cephfsService: CephfsService,
+ private snapScheduleService: CephfsSnapshotScheduleService,
+ private taskWrapper: TaskWrapperService,
+ private cd: ChangeDetectorRef
+ ) {
+ super();
+ this.resource = $localize`Snapshot schedule`;
+
+ const currentDatetime = new Date();
+ this.minDate = {
+ year: currentDatetime.getUTCFullYear(),
+ month: currentDatetime.getUTCMonth() + 1,
+ day: currentDatetime.getUTCDate()
+ };
+ this.currentTime = {
+ hour: currentDatetime.getUTCHours(),
+ minute: currentDatetime.getUTCMinutes(),
+ second: currentDatetime.getUTCSeconds()
+ };
+ }
+
+ ngOnInit(): void {
+ this.action = this.actionLabels.CREATE;
+ this.directories$ = this.cephfsService.lsDir(this.id, '/', 3);
+ this.createForm();
+ this.loadingReady();
+ }
+
+ get retentionPolicies() {
+ return this.snapScheduleForm.get('retentionPolicies') as FormArray;
+ }
+
+ createForm() {
+ this.snapScheduleForm = new CdFormGroup(
+ {
+ directory: new FormControl(undefined, {
+ validators: [Validators.required]
+ }),
+ startDate: new FormControl(this.minDate, {
+ validators: [Validators.required]
+ }),
+ startTime: new FormControl(this.currentTime, {
+ validators: [Validators.required]
+ }),
+ repeatInterval: new FormControl(1, {
+ validators: [Validators.required, Validators.min(1)]
+ }),
+ repeatFrequency: new FormControl(RepeatFrequency.Daily, {
+ validators: [Validators.required]
+ }),
+ retentionPolicies: new FormArray([])
+ },
+ {
+ asyncValidators: [this.validateSchedule(), this.validateRetention()]
+ }
+ );
+ }
+
+ addRetentionPolicy() {
+ this.retentionPolicies.push(
+ new FormGroup({
+ retentionInterval: new FormControl(1),
+ retentionFrequency: new FormControl(RetentionFrequency.Daily)
+ })
+ );
+ this.cd.detectChanges();
+ }
+
+ removeRetentionPolicy(idx: number) {
+ this.retentionPolicies.removeAt(idx);
+ this.cd.detectChanges();
+ }
+
+ parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
+ return `${date.year}-${date.month}-${date.day}T${time.hour || '00'}:${time.minute || '00'}:${
+ time.second || '00'
+ }`;
+ }
+ parseSchedule(interval: number, frequency: string): string {
+ return `${interval}${frequency}`;
+ }
+
+ parseRetentionPolicies(retentionPolicies: RetentionPolicy[]) {
+ return retentionPolicies
+ ?.filter((r) => r?.retentionInterval !== null && r?.retentionFrequency !== null)
+ ?.map?.((r) => `${r.retentionInterval}-${r.retentionFrequency}`)
+ .join('|');
+ }
+
+ submit() {
+ if (this.snapScheduleForm.invalid) {
+ this.snapScheduleForm.setErrors({ cdSubmitButton: true });
+ return;
+ }
+
+ const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
+
+ const snapScheduleObj = {
+ fs: this.fsName,
+ path: values.directory,
+ snap_schedule: this.parseSchedule(values.repeatInterval, values.repeatFrequency),
+ start: this.parseDatetime(values.startDate, values.startTime)
+ };
+
+ const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
+ if (retentionPoliciesValues) {
+ snapScheduleObj['retention_policy'] = retentionPoliciesValues;
+ }
+
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
+ path: snapScheduleObj.path
+ }),
+ call: this.snapScheduleService.create(snapScheduleObj)
+ })
+ .subscribe({
+ error: () => {
+ this.snapScheduleForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+
+ validateSchedule() {
+ return (frm: AbstractControl) => {
+ const directory = frm.get('directory');
+ const repeatFrequency = frm.get('repeatFrequency');
+ const repeatInterval = frm.get('repeatInterval');
+ return timer(VALIDATON_TIMER).pipe(
+ switchMap(() =>
+ this.snapScheduleService
+ .checkScheduleExists(
+ directory?.value,
+ this.fsName,
+ repeatInterval?.value,
+ repeatFrequency?.value
+ )
+ .pipe(
+ map((exists: boolean) => {
+ if (exists) {
+ repeatFrequency?.setErrors({ notUnique: true }, { emitEvent: true });
+ } else {
+ repeatFrequency?.setErrors(null);
+ }
+ return null;
+ })
+ )
+ )
+ );
+ };
+ }
+
+ getFormArrayItem(frm: FormGroup, frmArrayName: string, ctrl: string, idx: number) {
+ return (frm.get(frmArrayName) as FormArray)?.controls?.[idx]?.get?.(ctrl);
+ }
+
+ validateRetention() {
+ return (frm: FormGroup) => {
+ return timer(VALIDATON_TIMER).pipe(
+ switchMap(() => {
+ const retentionList = (frm.get('retentionPolicies') as FormArray).controls?.map(
+ (ctrl) => {
+ return ctrl.get('retentionFrequency').value;
+ }
+ );
+ if (uniq(retentionList)?.length !== retentionList?.length) {
+ this.getFormArrayItem(
+ frm,
+ 'retentionPolicies',
+ 'retentionFrequency',
+ retentionList.length - 1
+ )?.setErrors?.({
+ notUnique: true
+ });
+ return null;
+ }
+ return this.snapScheduleService
+ .checkRetentionPolicyExists(frm.get('directory').value, this.fsName, retentionList)
+ .pipe(
+ map(({ exists, errorIndex }) => {
+ if (exists) {
+ this.getFormArrayItem(
+ frm,
+ 'retentionPolicies',
+ 'retentionFrequency',
+ errorIndex
+ )?.setErrors?.({ notUnique: true });
+ } else {
+ (frm.get('retentionPolicies') as FormArray).controls?.forEach?.((_, i) => {
+ this.getFormArrayItem(
+ frm,
+ 'retentionPolicies',
+ 'retentionFrequency',
+ i
+ )?.setErrors?.(null);
+ });
+ }
+ return null;
+ })
+ );
+ })
+ );
+ };
+ }
+}
import { NotificationService } from '~/app/shared/services/notification.service';
import { BlockUI, NgBlockUI } from 'ng-block-ui';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CephfsSnapshotscheduleFormComponent } from '../cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
@Component({
selector: 'cd-cephfs-snapshotschedule-list',
extends CdForm
implements OnInit, OnChanges, OnDestroy {
@Input() fsName!: string;
+ @Input() id!: number;
@ViewChild('pathTpl', { static: true })
pathTpl: any;
private authStorageService: AuthStorageService,
private modalService: ModalService,
private mgrModuleService: MgrModuleService,
- private notificationService: NotificationService
+ private notificationService: NotificationService,
+ private actionLables: ActionLabelsI18n
) {
super();
this.permissions = this.authStorageService.getPermissions();
{ prop: 'created', name: $localize`Created`, cellTransformation: CellTemplate.timeAgo }
];
- this.tableActions = [];
+ this.tableActions = [
+ {
+ name: this.actionLables.CREATE,
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.openModal(true)
+ }
+ ];
}
ngOnDestroy(): void {
openModal(edit = false) {
this.modalService.show(
- {},
+ CephfsSnapshotscheduleFormComponent,
{
- fsName: 'fs1',
+ fsName: this.fsName,
+ id: this.id,
+ path: this.selection?.first()?.path,
isEdit: edit
},
{ size: 'lg' }
<ng-template ngbNavContent>
<cd-cephfs-snapshotschedule-list
[fsName]="selection.mdsmap.fs_name"
+ [id]="id"
></cd-cephfs-snapshotschedule-list>
</ng-template>
</ng-container>
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TreeModule } from '@circlon/angular-tree-component';
-import { NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import {
+ NgbDatepickerModule,
+ NgbNavModule,
+ NgbTimepickerModule,
+ NgbTooltipModule,
+ NgbTypeaheadModule
+} from '@ng-bootstrap/ng-bootstrap';
import { NgChartsModule } from 'ng2-charts';
import { AppRoutingModule } from '~/app/app-routing.module';
import { CephfsSnapshotscheduleListComponent } from './cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component';
import { DataTableModule } from '../../shared/datatable/datatable.module';
import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component';
+import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
@NgModule({
imports: [
ReactiveFormsModule,
NgbTypeaheadModule,
NgbTooltipModule,
- DataTableModule
+ DataTableModule,
+ NgbDatepickerModule,
+ NgbTimepickerModule
],
declarations: [
CephfsDetailComponent,
CephfsSubvolumegroupFormComponent,
CephfsSubvolumeSnapshotsListComponent,
CephfsSnapshotscheduleListComponent,
+ CephfsSnapshotscheduleFormComponent,
CephfsSubvolumeSnapshotsFormComponent
]
})
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
+import { catchError, map } from 'rxjs/operators';
+import { intersection, isEqual, uniqWith } from 'lodash';
import { SnapshotSchedule } from '../models/snapshot-schedule';
-import { map } from 'rxjs/operators';
+import { of } from 'rxjs';
+import { RepeatFrequency } from '../enum/repeat-frequency.enum';
@Injectable({
providedIn: 'root'
constructor(private http: HttpClient) {}
- getSnapshotScheduleList(
+ create(data: Record<string, any>): Observable<any> {
+ return this.http.post(`${this.baseURL}/snapshot/schedule`, data, { observe: 'response' });
+ }
+
+ checkScheduleExists(
path: string,
fs: string,
- recursive = true
- ): Observable<SnapshotSchedule[]> {
+ interval: number,
+ frequency: RepeatFrequency
+ ): Observable<boolean> {
+ return this.getSnapshotScheduleList(path, fs, false).pipe(
+ map((response) => {
+ const index = response.findIndex(
+ (x) => x.path === path && x.schedule === `${interval}${frequency}`
+ );
+ return index > -1;
+ }),
+ catchError(() => {
+ return of(false);
+ })
+ );
+ }
+
+ checkRetentionPolicyExists(
+ path: string,
+ fs: string,
+ retentionFrequencies: string[]
+ ): Observable<{ exists: boolean; errorIndex: number }> {
+ return this.getList(path, fs, false).pipe(
+ map((response) => {
+ let errorIndex = -1;
+ let exists = false;
+ const index = response.findIndex((x) => x.path === path);
+ const result = retentionFrequencies?.length
+ ? intersection(Object.keys(response?.[index]?.retention), retentionFrequencies)
+ : [];
+ exists = !!result?.length;
+ result?.forEach((r) => (errorIndex = retentionFrequencies.indexOf(r)));
+
+ return { exists, errorIndex };
+ }),
+ catchError(() => {
+ return of({ exists: false, errorIndex: -1 });
+ })
+ );
+ }
+
+ private getList(path: string, fs: string, recursive = true): Observable<SnapshotSchedule[]> {
return this.http
.get<SnapshotSchedule[]>(
- `${this.baseURL}/snaphost/schedule?path=${path}&fs=${fs}&recursive=${recursive}`
+ `${this.baseURL}/snapshot/schedule?path=${path}&fs=${fs}&recursive=${recursive}`
)
.pipe(
- map((snapList: SnapshotSchedule[]) =>
+ catchError(() => {
+ return of([]);
+ })
+ );
+ }
+
+ getSnapshotScheduleList(
+ path: string,
+ fs: string,
+ recursive = true
+ ): Observable<SnapshotSchedule[]> {
+ return this.getList(path, fs, recursive).pipe(
+ map((snapList: SnapshotSchedule[]) =>
+ uniqWith(
snapList.map((snapItem: SnapshotSchedule) => ({
...snapItem,
status: snapItem.active ? 'Active' : 'Inactive',
subvol: snapItem?.subvol || ' - ',
- retention: Object.values(snapItem.retention)?.length
+ retention: Object.values(snapItem?.retention || [])?.length
? Object.entries(snapItem.retention)
?.map?.(([frequency, interval]) => `${interval}${frequency.toLocaleUpperCase()}`)
.join(' ')
: '-'
- }))
+ })),
+ isEqual
)
- );
+ )
+ );
}
}
return this.http.get(`${this.baseURL}`);
}
- lsDir(id: number, path?: string): Observable<CephfsDir[]> {
- let apiPath = `${this.baseUiURL}/${id}/ls_dir?depth=2`;
+ lsDir(id: number, path?: string, depth: number = 2): Observable<CephfsDir[]> {
+ let apiPath = `${this.baseUiURL}/${id}/ls_dir?depth=${depth}`;
if (path) {
apiPath += `&path=${encodeURIComponent(path)}`;
}
navicon = 'fa fa-navicon', // Navigation
areaChart = 'fa fa-area-chart', // Area Chart, dashboard
eye = 'fa fa-eye', // Observability
+ calendar = 'fa fa-calendar',
externalUrl = 'fa fa-external-link', // links to external page
/* Icons for special effect */
--- /dev/null
+export enum RepeatFrequency {
+ Hourly = 'h',
+ Daily = 'd',
+ Weekly = 'w'
+}
--- /dev/null
+export enum RetentionFrequency {
+ Hourly = 'h',
+ Daily = 'd',
+ Weekly = 'w',
+ Monthly = 'm',
+ Yearly = 'y',
+ 'lastest snapshots' = 'n'
+}
+import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
+
export interface SnapshotSchedule {
fs?: string;
subvol?: string;
active: boolean;
status: 'Active' | 'Inactive';
}
+
+export interface SnapshotScheduleFormValue {
+ directory: string;
+ startDate: NgbDateStruct;
+ startTime: NgbTimeStruct;
+ repeatInterval: number;
+ repeatFrequency: string;
+ retentionPolicies: RetentionPolicy[];
+}
+
+export interface RetentionPolicy {
+ retentionInterval: number;
+ retentionFrequency: string;
+}
'cephfs/subvolume/snapshot/delete': this.newTaskMessage(
this.commonOperations.delete,
(metadata) => this.snapshot(metadata)
+ ),
+ 'cephfs/snapshot/schedule/create': this.newTaskMessage(this.commonOperations.add, (metadata) =>
+ this.snapshotSchedule(metadata)
)
};
return $localize`snapshot '${metadata.snapshotName}'`;
}
+ snapshotSchedule(metadata: any) {
+ return $localize`snapshot schedule for path '${metadata?.path}'`;
+ }
crudMessageId(id: string) {
return $localize`${id}`;
}
summary: Rename CephFS Volume
tags:
- Cephfs
- /api/cephfs/snaphost/schedule:
+ /api/cephfs/snapshot/schedule:
get:
parameters:
- in: query
- jwt: []
tags:
- CephFSSnapshotSchedule
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ fs:
+ type: string
+ path:
+ type: string
+ retention_policy:
+ type: string
+ snap_schedule:
+ type: string
+ start:
+ type: string
+ required:
+ - fs
+ - path
+ - snap_schedule
+ - start
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - CephFSSnapshotSchedule
/api/cephfs/subvolume:
post:
parameters: []