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