]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
0ef68f7d529c5edc1e93d7f0aa629fa1cc339154
[ceph-ci.git] /
1 import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2 import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
3 import { NgbActiveModal, NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
4 import { padStart, uniq } from 'lodash';
5 import { Observable, OperatorFunction, of, timer } from 'rxjs';
6 import { catchError, debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
7 import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
8 import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
9 import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
10 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
11 import { Icons } from '~/app/shared/enum/icons.enum';
12 import { RepeatFrequency } from '~/app/shared/enum/repeat-frequency.enum';
13 import { RetentionFrequency } from '~/app/shared/enum/retention-frequency.enum';
14 import { CdForm } from '~/app/shared/forms/cd-form';
15 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
16 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
17 import { FinishedTask } from '~/app/shared/models/finished-task';
18 import {
19   RetentionPolicy,
20   SnapshotSchedule,
21   SnapshotScheduleFormValue
22 } from '~/app/shared/models/snapshot-schedule';
23 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
24
25 const VALIDATON_TIMER = 300;
26 const DEBOUNCE_TIMER = 300;
27 const DEFAULT_SUBVOLUME_GROUP = '_nogroup';
28
29 @Component({
30   selector: 'cd-cephfs-snapshotschedule-form',
31   templateUrl: './cephfs-snapshotschedule-form.component.html',
32   styleUrls: ['./cephfs-snapshotschedule-form.component.scss']
33 })
34 export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
35   fsName!: string;
36   path!: string;
37   schedule!: string;
38   retention!: string;
39   start!: string;
40   status!: string;
41   subvol!: string;
42   group!: string;
43   id!: number;
44   isEdit = false;
45   icons = Icons;
46   repeatFrequencies = Object.entries(RepeatFrequency);
47   retentionFrequencies = Object.entries(RetentionFrequency);
48   retentionPoliciesToRemove: RetentionPolicy[] = [];
49   isDefaultSubvolumeGroup = false;
50   subvolumeGroup!: string;
51   subvolume!: string;
52   isSubvolume = false;
53
54   currentTime!: NgbTimeStruct;
55   minDate!: NgbDateStruct;
56
57   snapScheduleForm!: CdFormGroup;
58
59   action!: string;
60   resource!: string;
61
62   columns!: CdTableColumn[];
63
64   constructor(
65     public activeModal: NgbActiveModal,
66     private actionLabels: ActionLabelsI18n,
67     private snapScheduleService: CephfsSnapshotScheduleService,
68     private taskWrapper: TaskWrapperService,
69     private cd: ChangeDetectorRef,
70     public directoryStore: DirectoryStoreService,
71     private subvolumeService: CephfsSubvolumeService
72   ) {
73     super();
74     this.resource = $localize`Snapshot schedule`;
75
76     const currentDatetime = new Date();
77     this.minDate = {
78       year: currentDatetime.getUTCFullYear(),
79       month: currentDatetime.getUTCMonth() + 1,
80       day: currentDatetime.getUTCDate()
81     };
82     this.currentTime = {
83       hour: currentDatetime.getUTCHours(),
84       minute: currentDatetime.getUTCMinutes(),
85       second: currentDatetime.getUTCSeconds()
86     };
87   }
88
89   ngOnInit(): void {
90     this.action = this.actionLabels.CREATE;
91     this.directoryStore.loadDirectories(this.id, '/', 3);
92     this.createForm();
93     this.isEdit ? this.populateForm() : this.loadingReady();
94     this.snapScheduleForm.get('directory').valueChanges.subscribe({
95       next: (value: string) => {
96         this.subvolumeGroup = value?.split?.('/')?.[2];
97         this.subvolume = value?.split?.('/')?.[3];
98         this.subvolumeService
99           .exists(
100             this.subvolume,
101             this.fsName,
102             this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
103           )
104           .subscribe({
105             next: (exists: boolean) => {
106               this.isSubvolume = exists;
107               this.isDefaultSubvolumeGroup =
108                 exists && this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP;
109             }
110           });
111       }
112     });
113   }
114
115   get retentionPolicies() {
116     return this.snapScheduleForm.get('retentionPolicies') as FormArray;
117   }
118
119   search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
120     input.pipe(
121       debounceTime(DEBOUNCE_TIMER),
122       distinctUntilChanged(),
123       switchMap((term) =>
124         this.directoryStore.search(term, this.id).pipe(
125           catchError(() => {
126             return of([]);
127           })
128         )
129       )
130     );
131
132   populateForm() {
133     this.action = this.actionLabels.EDIT;
134     this.snapScheduleService.getSnapshotSchedule(this.path, this.fsName, false).subscribe({
135       next: (response: SnapshotSchedule[]) => {
136         const first = response.find((x) => x.path === this.path);
137         this.snapScheduleForm.get('directory').disable();
138         this.snapScheduleForm.get('directory').setValue(first.path);
139         this.snapScheduleForm.get('startDate').disable();
140         this.snapScheduleForm.get('startDate').setValue({
141           year: new Date(first.start).getUTCFullYear(),
142           month: new Date(first.start).getUTCMonth() + 1,
143           day: new Date(first.start).getUTCDate()
144         });
145         this.snapScheduleForm.get('startTime').disable();
146         this.snapScheduleForm.get('startTime').setValue({
147           hour: new Date(first.start).getUTCHours(),
148           minute: new Date(first.start).getUTCMinutes(),
149           second: new Date(first.start).getUTCSeconds()
150         });
151         this.snapScheduleForm.get('repeatInterval').disable();
152         this.snapScheduleForm.get('repeatInterval').setValue(first.schedule.split('')?.[0]);
153         this.snapScheduleForm.get('repeatFrequency').disable();
154         this.snapScheduleForm.get('repeatFrequency').setValue(first.schedule.split('')?.[1]);
155
156         // retention policies
157         first.retention &&
158           Object.entries(first.retention).forEach(([frequency, interval], idx) => {
159             const freqKey = Object.keys(RetentionFrequency)[
160               Object.values(RetentionFrequency).indexOf(frequency as any)
161             ];
162             this.retentionPolicies.push(
163               new FormGroup({
164                 retentionInterval: new FormControl(interval),
165                 retentionFrequency: new FormControl(RetentionFrequency[freqKey])
166               })
167             );
168             this.retentionPolicies.controls[idx].get('retentionInterval').disable();
169             this.retentionPolicies.controls[idx].get('retentionFrequency').disable();
170           });
171         this.loadingReady();
172       }
173     });
174   }
175
176   createForm() {
177     this.snapScheduleForm = new CdFormGroup(
178       {
179         directory: new FormControl(undefined, {
180           updateOn: 'blur',
181           validators: [Validators.required]
182         }),
183         startDate: new FormControl(this.minDate, {
184           validators: [Validators.required]
185         }),
186         startTime: new FormControl(this.currentTime, {
187           validators: [Validators.required]
188         }),
189         repeatInterval: new FormControl(1, {
190           validators: [Validators.required, Validators.min(1)]
191         }),
192         repeatFrequency: new FormControl(RepeatFrequency.Daily, {
193           validators: [Validators.required]
194         }),
195         retentionPolicies: new FormArray([])
196       },
197       {
198         asyncValidators: [this.validateSchedule(), this.validateRetention()]
199       }
200     );
201   }
202
203   addRetentionPolicy() {
204     this.retentionPolicies.push(
205       new FormGroup({
206         retentionInterval: new FormControl(1),
207         retentionFrequency: new FormControl(RetentionFrequency.Daily)
208       })
209     );
210     this.cd.detectChanges();
211   }
212
213   removeRetentionPolicy(idx: number) {
214     if (this.isEdit && this.retentionPolicies.at(idx).disabled) {
215       const values = this.retentionPolicies.at(idx).value as RetentionPolicy;
216       this.retentionPoliciesToRemove.push(values);
217     }
218     this.retentionPolicies.removeAt(idx);
219     this.retentionPolicies.controls.forEach((x) =>
220       x.get('retentionFrequency').updateValueAndValidity()
221     );
222     this.cd.detectChanges();
223   }
224
225   convertNumberToString(input: number, length = 2, format = '0'): string {
226     return padStart(input.toString(), length, format);
227   }
228
229   parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
230     if (!date || !time) return null;
231     return `${date.year}-${this.convertNumberToString(date.month)}-${this.convertNumberToString(
232       date.day
233     )}T${this.convertNumberToString(time.hour)}:${this.convertNumberToString(
234       time.minute
235     )}:${this.convertNumberToString(time.second)}`;
236   }
237   parseSchedule(interval: number, frequency: string): string {
238     return `${interval}${frequency}`;
239   }
240
241   parseRetentionPolicies(retentionPolicies: RetentionPolicy[]) {
242     return retentionPolicies
243       ?.filter((r) => r?.retentionInterval !== null && r?.retentionFrequency !== null)
244       ?.map?.((r) => `${r.retentionInterval}-${r.retentionFrequency}`)
245       .join('|');
246   }
247
248   submit() {
249     if (this.snapScheduleForm.invalid) {
250       this.snapScheduleForm.setErrors({ cdSubmitButton: true });
251       return;
252     }
253
254     const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
255
256     if (this.isEdit) {
257       const retentionPoliciesToAdd = (this.snapScheduleForm.get(
258         'retentionPolicies'
259       ) as FormArray).controls
260         ?.filter(
261           (ctrl) =>
262             !ctrl.get('retentionInterval').disabled && !ctrl.get('retentionFrequency').disabled
263         )
264         .map((ctrl) => ({
265           retentionInterval: ctrl.get('retentionInterval').value,
266           retentionFrequency: ctrl.get('retentionFrequency').value
267         }));
268
269       const updateObj = {
270         fs: this.fsName,
271         path: this.path,
272         subvol: this.subvol,
273         group: this.group,
274         retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
275         retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
276       };
277
278       this.taskWrapper
279         .wrapTaskAroundCall({
280           task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.EDIT, {
281             path: this.path
282           }),
283           call: this.snapScheduleService.update(updateObj)
284         })
285         .subscribe({
286           error: () => {
287             this.snapScheduleForm.setErrors({ cdSubmitButton: true });
288           },
289           complete: () => {
290             this.activeModal.close();
291           }
292         });
293     } else {
294       const snapScheduleObj = {
295         fs: this.fsName,
296         path: values.directory,
297         snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
298         start: this.parseDatetime(values?.startDate, values?.startTime)
299       };
300
301       const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
302
303       if (retentionPoliciesValues) snapScheduleObj['retention_policy'] = retentionPoliciesValues;
304
305       if (this.isSubvolume) snapScheduleObj['subvol'] = this.subvolume;
306
307       if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
308         snapScheduleObj['group'] = this.subvolumeGroup;
309       }
310
311       this.taskWrapper
312         .wrapTaskAroundCall({
313           task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
314             path: snapScheduleObj.path
315           }),
316           call: this.snapScheduleService.create(snapScheduleObj)
317         })
318         .subscribe({
319           error: () => {
320             this.snapScheduleForm.setErrors({ cdSubmitButton: true });
321           },
322           complete: () => {
323             this.activeModal.close();
324           }
325         });
326     }
327   }
328
329   validateSchedule() {
330     return (frm: AbstractControl) => {
331       const directory = frm.get('directory');
332       const repeatFrequency = frm.get('repeatFrequency');
333       const repeatInterval = frm.get('repeatInterval');
334
335       if (this.isEdit) {
336         return of(null);
337       }
338
339       return timer(VALIDATON_TIMER).pipe(
340         switchMap(() =>
341           this.snapScheduleService
342             .checkScheduleExists(
343               directory?.value,
344               this.fsName,
345               repeatInterval?.value,
346               repeatFrequency?.value
347             )
348             .pipe(
349               map((exists: boolean) => {
350                 if (exists) {
351                   repeatFrequency?.setErrors({ notUnique: true }, { emitEvent: true });
352                 } else {
353                   repeatFrequency?.setErrors(null);
354                 }
355                 return null;
356               })
357             )
358         )
359       );
360     };
361   }
362
363   getFormArrayItem(frm: FormGroup, frmArrayName: string, ctrl: string, idx: number) {
364     return (frm.get(frmArrayName) as FormArray)?.controls?.[idx]?.get?.(ctrl);
365   }
366
367   validateRetention() {
368     return (frm: FormGroup) => {
369       return timer(VALIDATON_TIMER).pipe(
370         switchMap(() => {
371           const retentionList = (frm.get('retentionPolicies') as FormArray).controls?.map(
372             (ctrl) => {
373               return ctrl.get('retentionFrequency').value;
374             }
375           );
376           if (uniq(retentionList)?.length !== retentionList?.length) {
377             this.getFormArrayItem(
378               frm,
379               'retentionPolicies',
380               'retentionFrequency',
381               retentionList.length - 1
382             )?.setErrors?.({
383               notUnique: true
384             });
385             return null;
386           }
387           return this.snapScheduleService
388             .checkRetentionPolicyExists(
389               frm.get('directory').value,
390               this.fsName,
391               retentionList,
392               this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || []
393             )
394             .pipe(
395               map(({ exists, errorIndex }) => {
396                 if (exists) {
397                   this.getFormArrayItem(
398                     frm,
399                     'retentionPolicies',
400                     'retentionFrequency',
401                     errorIndex
402                   )?.setErrors?.({ notUnique: true });
403                 } else {
404                   (frm.get('retentionPolicies') as FormArray).controls?.forEach?.((_, i) => {
405                     this.getFormArrayItem(
406                       frm,
407                       'retentionPolicies',
408                       'retentionFrequency',
409                       i
410                     )?.setErrors?.(null);
411                   });
412                 }
413                 return null;
414               })
415             );
416         })
417       );
418     };
419   }
420 }