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