]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
2e813005ae59af122cd96edc9a58c29d4b067ed1
[ceph.git] /
1 import { Component, OnInit } from '@angular/core';
2 import { AbstractControl, FormControl, Validators } from '@angular/forms';
3 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
4 import { CdForm } from '~/app/shared/forms/cd-form';
5 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
6 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
7 import _ from 'lodash';
8 import { ActivatedRoute, Router } from '@angular/router';
9 import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.service';
10 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
11 import {
12   ALLOW_READ_THROUGH_TEXT,
13   DEFAULT_PLACEMENT,
14   MULTIPART_MIN_PART_TEXT,
15   MULTIPART_SYNC_THRESHOLD_TEXT,
16   PlacementTarget,
17   RequestModel,
18   RETAIN_HEAD_OBJECT_TEXT,
19   StorageClass,
20   Target,
21   TARGET_ACCESS_KEY_TEXT,
22   TARGET_ENDPOINT_TEXT,
23   TARGET_PATH_TEXT,
24   TARGET_REGION_TEXT,
25   TARGET_SECRET_KEY_TEXT,
26   TierTarget,
27   TIER_TYPE,
28   ZoneGroup,
29   ZoneGroupDetails,
30   CLOUDS3_STORAGE_CLASS_TEXT,
31   LOCAL_STORAGE_CLASS_TEXT,
32   GLACIER_STORAGE_CLASS_TEXT,
33   GLACIER_RESTORE_DAY_TEXT,
34   GLACIER_RESTORE_TIER_TYPE_TEXT,
35   RESTORE_DAYS_TEXT,
36   READTHROUGH_RESTORE_DAYS_TEXT,
37   RESTORE_STORAGE_CLASS_TEXT,
38   TIER_TYPE_DISPLAY,
39   S3Glacier,
40   StorageClassOption,
41   STORAGE_CLASS_CONSTANTS,
42   STANDARD_TIER_TYPE_TEXT,
43   EXPEDITED_TIER_TYPE_TEXT,
44   TextLabels
45 } from '../models/rgw-storage-class.model';
46 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
47 import { NotificationService } from '~/app/shared/services/notification.service';
48 import { CdValidators } from '~/app/shared/forms/cd-validators';
49
50 @Component({
51   selector: 'cd-rgw-storage-class-form',
52   templateUrl: './rgw-storage-class-form.component.html',
53   styleUrls: ['./rgw-storage-class-form.component.scss']
54 })
55 export class RgwStorageClassFormComponent extends CdForm implements OnInit {
56   storageClassForm: CdFormGroup;
57   action: string;
58   resource: string;
59   editing: boolean;
60   showAdvanced: boolean = false;
61   defaultZoneGroup: string;
62   zonegroupNames: ZoneGroup[];
63   placementTargets: string[] = [];
64   selectedZoneGroup: string;
65   defaultZonegroup: ZoneGroup;
66   zoneGroupDetails: ZoneGroupDetails;
67   storageClassInfo: StorageClass;
68   tierTargetInfo: TierTarget;
69   glacierStorageClassDetails: S3Glacier;
70   allowReadThrough: boolean = false;
71   TIER_TYPE = TIER_TYPE;
72   TIER_TYPE_DISPLAY = TIER_TYPE_DISPLAY;
73   storageClassOptions: StorageClassOption[];
74   textLabels: TextLabels;
75
76   constructor(
77     public actionLabels: ActionLabelsI18n,
78     private formBuilder: CdFormBuilder,
79     private notificationService: NotificationService,
80     private rgwStorageService: RgwStorageClassService,
81     private rgwZoneGroupService: RgwZonegroupService,
82     private router: Router,
83     private route: ActivatedRoute
84   ) {
85     super();
86     this.resource = $localize`Tiering Storage Class`;
87     this.editing = this.router.url.startsWith(`/rgw/tiering/${URLVerbs.EDIT}`);
88     this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
89   }
90
91   ngOnInit() {
92     this.textLabels = {
93       targetPathText: TARGET_PATH_TEXT,
94       targetEndpointText: TARGET_ENDPOINT_TEXT,
95       targetRegionText: TARGET_REGION_TEXT,
96       targetAccessKeyText: TARGET_ACCESS_KEY_TEXT,
97       targetSecretKeyText: TARGET_SECRET_KEY_TEXT,
98       retainHeadObjectText: RETAIN_HEAD_OBJECT_TEXT,
99       allowReadThroughText: ALLOW_READ_THROUGH_TEXT,
100       storageClassText: LOCAL_STORAGE_CLASS_TEXT,
101       multipartMinPartText: MULTIPART_MIN_PART_TEXT,
102       multipartSyncThresholdText: MULTIPART_SYNC_THRESHOLD_TEXT,
103       tiertypeText: STANDARD_TIER_TYPE_TEXT,
104       glacierRestoreDayText: GLACIER_RESTORE_DAY_TEXT,
105       glacierRestoreTiertypeText: GLACIER_RESTORE_TIER_TYPE_TEXT,
106       restoreDaysText: RESTORE_DAYS_TEXT,
107       readthroughrestoreDaysText: READTHROUGH_RESTORE_DAYS_TEXT,
108       restoreStorageClassText: RESTORE_STORAGE_CLASS_TEXT
109     };
110     this.storageClassOptions = [
111       { value: TIER_TYPE.LOCAL, label: TIER_TYPE_DISPLAY.LOCAL },
112       { value: TIER_TYPE.CLOUD_TIER, label: TIER_TYPE_DISPLAY.CLOUD_TIER },
113       { value: TIER_TYPE.GLACIER, label: TIER_TYPE_DISPLAY.GLACIER }
114     ];
115     this.createForm();
116     this.storageClassTypeText();
117     this.TierTypeText();
118     this.loadingReady();
119     this.loadZoneGroup();
120     if (this.editing) {
121       this.route.params.subscribe((params: StorageClass) => {
122         this.storageClassInfo = params;
123       });
124       this.rgwStorageService
125         .getPlacement_target(this.storageClassInfo.placement_target)
126         .subscribe((placementTargetInfo: PlacementTarget) => {
127           this.tierTargetInfo = this.getTierTargetByStorageClass(
128             placementTargetInfo,
129             this.storageClassInfo.storage_class
130           );
131           let response = this.tierTargetInfo?.val?.s3;
132           this.storageClassForm.get('zonegroup').disable();
133           this.storageClassForm.get('placement_target').disable();
134           this.storageClassForm.get('storage_class').disable();
135           this.storageClassForm.patchValue({
136             zonegroup: this.storageClassInfo?.zonegroup_name,
137             region: response?.region,
138             placement_target: this.storageClassInfo?.placement_target,
139             storageClassType: this.tierTargetInfo?.val?.tier_type ?? TIER_TYPE.LOCAL,
140             endpoint: response?.endpoint,
141             storage_class: this.storageClassInfo?.storage_class,
142             access_key: response?.access_key,
143             secret_key: response?.secret,
144             target_path: response?.target_path,
145             retain_head_object: this.tierTargetInfo?.val?.retain_head_object || false,
146             multipart_sync_threshold: response?.multipart_sync_threshold || '',
147             multipart_min_part_size: response?.multipart_min_part_size || '',
148             allow_read_through: this.tierTargetInfo?.val?.allow_read_through || false,
149             restore_storage_class: this.tierTargetInfo?.val?.restore_storage_class,
150             read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days
151           });
152           if (this.tierTargetInfo?.val?.tier_type == TIER_TYPE.GLACIER) {
153             let glacierResponse = this.tierTargetInfo?.val['s3-glacier'];
154             this.storageClassForm.patchValue({
155               glacier_restore_tier_type: glacierResponse.glacier_restore_tier_type,
156               glacier_restore_days: glacierResponse.glacier_restore_days
157             });
158           }
159         });
160     }
161     this.storageClassForm.get('storageClassType').valueChanges.subscribe((value) => {
162       this.updateValidatorsBasedOnStorageClass(value);
163     });
164     this.storageClassForm.get('allow_read_through').valueChanges.subscribe((value) => {
165       this.onAllowReadThroughChange(value);
166     });
167   }
168
169   private updateValidatorsBasedOnStorageClass(value: string) {
170     const controlsToUpdate = [
171       'region',
172       'endpoint',
173       'access_key',
174       'secret_key',
175       'target_path',
176       'glacier_restore_tier_type',
177       'restore_storage_class'
178     ];
179
180     controlsToUpdate.forEach((field) => {
181       const control = this.storageClassForm.get(field);
182
183       if (
184         (value === TIER_TYPE.CLOUD_TIER &&
185           ['region', 'endpoint', 'access_key', 'secret_key', 'target_path'].includes(field)) ||
186         (value === TIER_TYPE.GLACIER &&
187           [
188             'glacier_restore_tier_type',
189             'restore_storage_class',
190             'region',
191             'endpoint',
192             'access_key',
193             'secret_key',
194             'target_path'
195           ].includes(field))
196       ) {
197         control.setValidators([Validators.required]);
198       } else {
199         control.clearValidators();
200       }
201
202       control.updateValueAndValidity();
203     });
204   }
205
206   storageClassTypeText() {
207     this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => {
208       if (value === TIER_TYPE.LOCAL) {
209         this.textLabels.storageClassText = LOCAL_STORAGE_CLASS_TEXT;
210       } else if (value === TIER_TYPE.CLOUD_TIER) {
211         this.textLabels.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT;
212       } else if (value === TIER_TYPE.GLACIER) {
213         this.textLabels.storageClassText = GLACIER_STORAGE_CLASS_TEXT;
214       }
215     });
216   }
217
218   TierTypeText() {
219     this.storageClassForm?.get('glacier_restore_tier_type')?.valueChanges.subscribe((value) => {
220       if (value === STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS) {
221         this.textLabels.tiertypeText = STANDARD_TIER_TYPE_TEXT;
222       } else {
223         this.textLabels.tiertypeText = EXPEDITED_TIER_TYPE_TEXT;
224       }
225     });
226   }
227
228   createForm() {
229     const self = this;
230
231     const lockDaysValidator = CdValidators.custom('lockDays', () => {
232       if (!self.storageClassForm || !self.storageClassForm.getRawValue()) {
233         return false;
234       }
235
236       const lockDays = Number(self.storageClassForm.getValue('read_through_restore_days'));
237       return !Number.isInteger(lockDays) || lockDays === 0;
238     });
239     this.storageClassForm = this.formBuilder.group({
240       storage_class: new FormControl('', {
241         validators: [Validators.required]
242       }),
243       zonegroup: new FormControl(this.selectedZoneGroup, {
244         validators: [Validators.required]
245       }),
246       region: new FormControl('', [
247         CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
248       ]),
249       placement_target: new FormControl('', {
250         validators: [Validators.required]
251       }),
252       endpoint: new FormControl(null, [
253         CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
254       ]),
255       access_key: new FormControl(null, [
256         CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
257       ]),
258       secret_key: new FormControl(null, [
259         CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
260       ]),
261       target_path: new FormControl('', [
262         CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required])
263       ]),
264       retain_head_object: new FormControl(true),
265       glacier_restore_tier_type: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS, [
266         CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER }, [Validators.required])
267       ]),
268       glacier_restore_days: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_GLACIER_RESTORE_DAYS, [
269         CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER || TIER_TYPE.CLOUD_TIER }, [
270           CdValidators.number(false),
271           lockDaysValidator
272         ])
273       ]),
274       restore_storage_class: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS),
275       read_through_restore_days: new FormControl(
276         {
277           value: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS,
278           disabled: true
279         },
280         CdValidators.composeIf(
281           (form: AbstractControl) => {
282             const type = form.get('storageClassType')?.value;
283             return type === TIER_TYPE.GLACIER || type === TIER_TYPE.CLOUD_TIER;
284           },
285           [CdValidators.number(false), lockDaysValidator]
286         )
287       ),
288       multipart_sync_threshold: new FormControl(
289         STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD
290       ),
291       multipart_min_part_size: new FormControl(
292         STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE
293       ),
294       allow_read_through: new FormControl(false),
295       storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required)
296     });
297   }
298
299   loadZoneGroup(): Promise<void> {
300     return new Promise((resolve, reject) => {
301       this.rgwZoneGroupService.getAllZonegroupsInfo().subscribe(
302         (data: ZoneGroupDetails) => {
303           this.zoneGroupDetails = data;
304           this.zonegroupNames = [];
305           this.placementTargets = [];
306           if (data.zonegroups && data.zonegroups.length > 0) {
307             this.zonegroupNames = data.zonegroups.map((zoneGroup: ZoneGroup) => {
308               return {
309                 id: zoneGroup.id,
310                 name: zoneGroup.name
311               };
312             });
313           }
314           this.defaultZonegroup = this.zonegroupNames.find(
315             (zonegroups: ZoneGroup) => zonegroups.id === data.default_zonegroup
316           );
317           this.storageClassForm.get('zonegroup').setValue(this.defaultZonegroup.name);
318           this.onZonegroupChange();
319           resolve();
320         },
321         (error) => reject(error)
322       );
323     });
324   }
325
326   onZonegroupChange() {
327     const zoneGroupControl = this.storageClassForm.get('zonegroup').value;
328     const selectedZoneGroup = this.zoneGroupDetails.zonegroups.find(
329       (zonegroup) => zonegroup.name === zoneGroupControl
330     );
331     const defaultPlacementTarget = selectedZoneGroup.placement_targets.find(
332       (target: Target) => target.name === DEFAULT_PLACEMENT
333     );
334     if (selectedZoneGroup) {
335       const placementTargetNames = selectedZoneGroup.placement_targets.map(
336         (target: Target) => target.name
337       );
338       this.placementTargets = placementTargetNames;
339     }
340     if (defaultPlacementTarget && !this.editing) {
341       this.storageClassForm.get('placement_target').setValue(defaultPlacementTarget.name);
342     } else {
343       this.storageClassForm
344         .get('placement_target')
345         .setValue(this.storageClassInfo.placement_target);
346     }
347   }
348
349   submitAction() {
350     const component = this;
351     const requestModel = this.buildRequest();
352     const storageclassName = this.storageClassForm.get('storage_class').value;
353     if (this.editing) {
354       this.rgwStorageService.editStorageClass(requestModel).subscribe(
355         () => {
356           this.notificationService.show(
357             NotificationType.success,
358             $localize`Updated Storage Class '${storageclassName}'`
359           );
360           this.goToListView();
361         },
362         () => {
363           component.storageClassForm.setErrors({ cdSubmitButton: true });
364         }
365       );
366     } else {
367       this.rgwStorageService.createStorageClass(requestModel).subscribe(
368         () => {
369           this.notificationService.show(
370             NotificationType.success,
371             $localize`Created Storage Class '${storageclassName}'`
372           );
373           this.goToListView();
374         },
375         () => {
376           component.storageClassForm.setErrors({ cdSubmitButton: true });
377         }
378       );
379     }
380   }
381
382   goToListView() {
383     this.router.navigate([`rgw/tiering`]);
384   }
385
386   getTierTargetByStorageClass(placementTargetInfo: PlacementTarget, storageClass: string) {
387     const tierTarget = placementTargetInfo.tier_targets.find(
388       (target: TierTarget) => target.val.storage_class === storageClass
389     );
390     return tierTarget;
391   }
392
393   onAllowReadThroughChange(checked: boolean): void {
394     this.allowReadThrough = checked;
395     const readThroughDaysControl = this.storageClassForm.get('read_through_restore_days');
396     if (this.allowReadThrough) {
397       this.storageClassForm.get('retain_head_object')?.setValue(true);
398       this.storageClassForm.get('retain_head_object')?.disable();
399       readThroughDaysControl?.enable();
400     } else {
401       this.storageClassForm.get('retain_head_object')?.enable();
402       readThroughDaysControl?.disable();
403     }
404   }
405
406   isTierMatch(...types: string[]): boolean {
407     const tierType = this.storageClassForm.getValue('storageClassType');
408     return types.includes(tierType);
409   }
410
411   buildRequest() {
412     if (this.storageClassForm.errors) return null;
413
414     const rawFormValue = _.cloneDeep(this.storageClassForm.value);
415     const zoneGroup = this.storageClassForm.get('zonegroup').value;
416     const storageClass = this.storageClassForm.get('storage_class').value;
417     const placementId = this.storageClassForm.get('placement_target').value;
418     const storageClassType = this.storageClassForm.get('storageClassType').value;
419     const retain_head_object = this.storageClassForm.get('retain_head_object').value;
420     return this.buildPlacementTargets(
421       storageClassType,
422       zoneGroup,
423       placementId,
424       storageClass,
425       retain_head_object,
426       rawFormValue
427     );
428   }
429
430   private buildPlacementTargets(
431     storageClassType: string,
432     zoneGroup: string,
433     placementId: string,
434     storageClass: string,
435     retain_head_object: boolean,
436     rawFormValue: any
437   ): RequestModel {
438     const baseTarget = {
439       placement_id: placementId,
440       storage_class: storageClass
441     };
442
443     if (storageClassType === TIER_TYPE.LOCAL) {
444       return {
445         zone_group: zoneGroup,
446         placement_targets: [baseTarget]
447       };
448     }
449
450     const tierConfig = {
451       endpoint: rawFormValue.endpoint,
452       access_key: rawFormValue.access_key,
453       secret: rawFormValue.secret_key,
454       target_path: rawFormValue.target_path,
455       retain_head_object,
456       allow_read_through: rawFormValue.allow_read_through,
457       region: rawFormValue.region,
458       multipart_sync_threshold: rawFormValue.multipart_sync_threshold,
459       multipart_min_part_size: rawFormValue.multipart_min_part_size,
460       restore_storage_class: rawFormValue.restore_storage_class,
461       ...(rawFormValue.allow_read_through
462         ? { read_through_restore_days: rawFormValue.read_through_restore_days }
463         : {})
464     };
465
466     if (storageClassType === TIER_TYPE.CLOUD_TIER) {
467       return {
468         zone_group: zoneGroup,
469         placement_targets: [
470           {
471             ...baseTarget,
472             tier_type: TIER_TYPE.CLOUD_TIER,
473             tier_config: {
474               ...tierConfig
475             }
476           }
477         ]
478       };
479     }
480
481     if (storageClassType === TIER_TYPE.GLACIER) {
482       return {
483         zone_group: zoneGroup,
484         placement_targets: [
485           {
486             ...baseTarget,
487             tier_type: TIER_TYPE.GLACIER,
488             tier_config: {
489               ...tierConfig,
490               glacier_restore_days: rawFormValue.glacier_restore_days,
491               glacier_restore_tier_type: rawFormValue.glacier_restore_tier_type
492             }
493           }
494         ]
495       };
496     }
497     return {
498       zone_group: zoneGroup,
499       placement_targets: [baseTarget]
500     };
501   }
502 }