From: Dnyaneshwari Date: Thu, 22 May 2025 07:08:25 +0000 (+0530) Subject: mgr/dashboard: Glacier Storage Class - create and list X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=68766699bafc16a16b82f0d72f1f3e074988c5ca;p=ceph.git mgr/dashboard: Glacier Storage Class - create and list Fixes: https://tracker.ceph.com/issues/71897 Signed-off-by: Dnyaneshwari Talwekar --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts index cc7f24fcb888b..15a9a972416f2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts @@ -12,17 +12,6 @@ export interface StorageClass { zonegroup_name?: string; } -export interface StorageClassDetails { - target_path: string; - access_key: string; - secret: string; - multipart_min_part_size: number; - multipart_sync_threshold: number; - host_style: string; - retain_head_object: boolean; - allow_read_through: boolean; -} - export interface TierTarget { key: string; val: { @@ -30,7 +19,10 @@ export interface TierTarget { tier_type: string; retain_head_object: boolean; allow_read_through: boolean; + read_through_restore_days: number; + restore_storage_class: string; s3?: S3Details; + 's3-glacier': S3Glacier; }; } @@ -41,14 +33,21 @@ export interface Target { } export interface StorageClassDetails { + tier_type: string; target_path: string; access_key: string; secret: string; multipart_min_part_size: number; multipart_sync_threshold: number; host_style: string; + allow_read_through: boolean; zonegroup_name?: string; placement_targets?: string; + glacier_restore_days?: number; + glacier_restore_tier_type?: string; + read_through_restore_days?: number; + restore_storage_class?: string; + retain_head_object?: boolean; } export interface ZoneGroup { @@ -71,13 +70,18 @@ export interface S3Details { retain_head_object?: boolean; allow_read_through?: boolean; } +export interface S3Glacier { + glacier_restore_days: number; + glacier_restore_tier_type: string; +} + export interface RequestModel { zone_group: string; placement_targets: PlacementTarget[]; } export interface PlacementTarget { - tags: string[]; + tags?: string[]; placement_id: string; tier_type?: TIER_TYPE; tier_config?: { @@ -90,20 +94,64 @@ export interface PlacementTarget { region: string; multipart_sync_threshold: number; multipart_min_part_size: number; + glacier_restore_days?: number; + glacier_restore_tier_type?: string; + restore_storage_class?: string; + read_through_restore_days?: number; }; storage_class?: string; name?: string; tier_targets?: TierTarget[]; } +export interface StorageClassOption { + value: string; + label: string; +} + +export interface TextLabels { + targetPathText: string; + targetEndpointText: string; + targetRegionText: string; + multipartMinPartText: string; + storageClassText: string; + multipartSyncThresholdText: string; + targetSecretKeyText: string; + targetAccessKeyText: string; + retainHeadObjectText: string; + allowReadThroughText: string; + glacierRestoreDayText: string; + glacierRestoreTiertypeText: string; + tiertypeText: string; + restoreDaysText: string; + readthroughrestoreDaysText: string; + restoreStorageClassText: string; +} + export const TIER_TYPE = { LOCAL: 'local', CLOUD_TIER: 'cloud-s3', GLACIER: 'cloud-s3-glacier' } as const; +export const STORAGE_CLASS_CONSTANTS = { + DEFAULT_GLACIER_RESTORE_DAYS: 1, + DEFAULT_READTHROUGH_RESTORE_DAYS: 1, + DEFAULT_MULTIPART_SYNC_THRESHOLD: 33554432, + DEFAULT_MULTIPART_MIN_PART_SIZE: 33554432, + DEFAULT_STORAGE_CLASS: 'Standard' +} as const; + export const DEFAULT_PLACEMENT = 'default-placement'; +export type TIER_TYPE = typeof TIER_TYPE[keyof typeof TIER_TYPE]; + +export const TIER_TYPE_DISPLAY = { + LOCAL: 'Local', + CLOUD_TIER: 'Cloud S3', + GLACIER: 'Cloud S3 Glacier' +}; + export const ALLOW_READ_THROUGH_TEXT = 'Enables fetching objects from remote cloud S3 if not found locally.'; @@ -138,10 +186,22 @@ export const LOCAL_STORAGE_CLASS_TEXT = $localize`Local storage uses on-premises export const CLOUDS3_STORAGE_CLASS_TEXT = $localize`Cloud S3 storage uses Amazon S3-compatible cloud services for tiering.`; -export type TIER_TYPE = typeof TIER_TYPE[keyof typeof TIER_TYPE]; +export const GLACIER_STORAGE_CLASS_TEXT = $localize`Glacier storage uses Amazon S3 Glacier for low-cost, long-term archival data storage.`; -export const TIER_TYPE_DISPLAY = { - LOCAL: 'Local', - CLOUD_TIER: 'Cloud S3', - GLACIER: 'Cloud S3 Glacier' -}; +export const GLACIER_RESTORE_DAY_TEXT = $localize`Refers to number of days to the object will be restored on glacier/tape endpoint.`; + +export const GLACIER_RESTORE_TIER_TYPE_TEXT = $localize`Restore retrieval type.`; + +export const STANDARD_TIER_TYPE_TEXT = $localize`Standard glacier restore tier type restores data in 3–5 hours.`; + +export const EXPEDITED_TIER_TYPE_TEXT = $localize`Expedited glacier restore tier type restores in 1–5 minutes (faster but costlier).`; + +export const RESTORE_DAYS_TEXT = $localize`Refers to number of days to the object will be restored on glacier/tape endpoint.`; + +export const READTHROUGH_RESTORE_DAYS_TEXT = $localize`The duration for which objects restored via read-through are retained.`; + +export const RESTORE_STORAGE_CLASS_TEXT = $localize`The storage class to which object data is to be restored.`; + +export const ZONEGROUP_TEXT = $localize`A Zone Group is a logical grouping of one or more zones that share the same data + and metadata, allowing for multi-site replication and geographic distribution of + data.`; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html index 13b90e2735f2e..baa283145fdc5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html @@ -7,21 +7,26 @@ data-testid="rgw-storage-details" > - + @if( isTierMatch( + TIER_TYPE_DISPLAY.LOCAL + )){ + Zone Group - A Zone Group is a logical grouping of one or more zones that share the same data - and metadata, allowing for multi-site replication and geographic distribution of - data. + {{ zoneGroupText }} {{ selection?.zonegroup_name }} - + } + @if(isTierMatch( + TIER_TYPE_DISPLAY.LOCAL + )){ + Placement Target @@ -34,19 +39,23 @@ {{ selection?.placement_target }} - + } + @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + Target Path - {{ targetPathText }} + {{ targetPathText }} {{ selection?.target_path }} - + } + @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + Access key @@ -74,7 +83,9 @@ - + } + @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + Secret key @@ -100,7 +111,9 @@ - + } + @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + Host Style @@ -110,54 +123,117 @@ {{ selection?.host_style }} - + } + @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + - Multipart Minimum Part Size + Head Object (Stub File) + + {{ retainHeadObjectText }} + + + {{ selection?.retain_head_object ? 'Enabled' : 'Disabled' }} + + } + @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + + + Allow Read Through - {{ multipartMinPartText }} + {{ allowReadThroughText }} - {{ selection?.multipart_min_part_size }} + {{ selection?.allow_read_through ? 'Enabled' : 'Disabled' }} - + } + @if(isTierMatch(TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER) && (selection?.allow_read_through)) { + - Multipart Sync Threshold + Read through Restore Days - {{ multipartSyncThreholdText }} + {{ readthroughrestoreDaysText }} - {{ selection?.multipart_sync_threshold }} + {{ selection?.read_through_restore_days }} - + } + @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)){ + - Retain Head Object + Glacier Restore Days - Retain object metadata after transition to the cloud (default: false). + {{ glacierRestoreDayText }} - {{ selection?.retain_head_object }} + {{ selection?.glacier_restore_days }} - + } + @if(isTierMatch( TIER_TYPE_DISPLAY.GLACIER)) { + - Allow Read Through + Glacier Restore Tier Type - {{ allowReadThroughText }} + {{ glacierRestoreTiertypeText }} + + + + {{ selection?.glacier_restore_tier_type }} + + } + @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + + + Restore Storage Class + + + {{ restoreStorageClassText }} + + + + {{ selection?.restore_storage_class }} + + } + @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + + + Multipart Minimum Part Size + + + {{ multipartMinPartText }} - {{ selection?.allow_read_through }} + {{ selection?.multipart_min_part_size }} + + } + @if(isTierMatch( TIER_TYPE_DISPLAY.CLOUD_TIER, TIER_TYPE_DISPLAY.GLACIER)){ + + + Multipart Sync Threshold + + + {{ multipartSyncThreholdText }} + + + + {{ selection?.multipart_sync_threshold }} + } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts index 8b9f4bbe64228..6275c62d485c0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts @@ -40,7 +40,8 @@ describe('RgwStorageClassDetailsComponent', () => { multipart_sync_threshold: 200, host_style: 'path', retain_head_object: true, - allow_read_through: true + allow_read_through: true, + tier_type: 'local' }; component.selection = mockSelection; component.ngOnChanges(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts index 3a536f98f1c9d..40049a69b959f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts @@ -10,7 +10,14 @@ import { TARGET_ACCESS_KEY_TEXT, TARGET_PATH_TEXT, TARGET_SECRET_KEY_TEXT, - TIER_TYPE_DISPLAY + TIER_TYPE_DISPLAY, + TIER_TYPE, + GLACIER_RESTORE_DAY_TEXT, + GLACIER_RESTORE_TIER_TYPE_TEXT, + RESTORE_DAYS_TEXT, + READTHROUGH_RESTORE_DAYS_TEXT, + RESTORE_STORAGE_CLASS_TEXT, + ZONEGROUP_TEXT } from '../models/rgw-storage-class.model'; @Component({ selector: 'cd-rgw-storage-class-details', @@ -31,6 +38,13 @@ export class RgwStorageClassDetailsComponent implements OnChanges { targetPathText = TARGET_PATH_TEXT; hostStyleText = HOST_STYLE; TIER_TYPE_DISPLAY = TIER_TYPE_DISPLAY; + TIER_TYPE = TIER_TYPE; + glacierRestoreDayText = GLACIER_RESTORE_DAY_TEXT; + glacierRestoreTiertypeText = GLACIER_RESTORE_TIER_TYPE_TEXT; + restoreDaysText = RESTORE_DAYS_TEXT; + readthroughrestoreDaysText = READTHROUGH_RESTORE_DAYS_TEXT; + restoreStorageClassText = RESTORE_STORAGE_CLASS_TEXT; + zoneGroupText = ZONEGROUP_TEXT; ngOnChanges() { if (this.selection) { @@ -40,12 +54,22 @@ export class RgwStorageClassDetailsComponent implements OnChanges { access_key: this.selection.access_key, secret: this.selection.secret, target_path: this.selection.target_path, + tier_type: this.selection.tier_type, multipart_min_part_size: this.selection.multipart_min_part_size, multipart_sync_threshold: this.selection.multipart_sync_threshold, host_style: this.selection.host_style, retain_head_object: this.selection.retain_head_object, - allow_read_through: this.selection.allow_read_through + allow_read_through: this.selection.allow_read_through, + glacier_restore_days: this.selection.glacier_restore_days, + glacier_restore_tier_type: this.selection.glacier_restore_tier_type, + restore_storage_class: this.selection.restore_storage_class, + read_through_restore_days: this.selection.read_through_restore_days }; } } + + isTierMatch(...types: string[]): boolean { + const tier_type = this.selection.tier_type?.toLowerCase(); + return types.some((type) => type.toLowerCase() === tier_type); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html index b34b48de67b5e..31cf2635c9529 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.html @@ -17,17 +17,18 @@ i18n-label for="storageClassType" formControlName="storageClassType" - [helperText]="storageClassText" + [helperText]="textLabels.storageClassText" id="storageClassType" [invalid]="storageClassForm.showError('storageClassType', formDir, 'required')" [invalidText]="storageError" > - - + -
+ @if( isTierMatch( TIER_TYPE.CLOUD_TIER, TIER_TYPE.GLACIER )){ +
@@ -139,7 +141,7 @@ i18n [invalid]="storageClassForm.showError('region', formDir, 'required')" [invalidText]="regionError" - [helperText]="targetRegionText" + [helperText]="textLabels.targetRegionText" >Target Region Target Endpoint
-
Target Access Key
-
-
Target Path
+
+ Allow Read Through + {{ textLabels?.allowReadThroughText }} + +
+
+ Head Object (Stub File) + {{ textLabels?.retainHeadObjectText }} + +
+
+
+ + + The entered value must be a positive integer. + ReadThrough Restore Days must be positive. + +
+
+ + + + +
+
-
- -
Advanced
-
-
- - + Glacier Configuration +
+
+ - -
-
- Multipart Sync Threshold - - -
-
- Multipart Minimum Part Size - - -
-
-
- Allow Read Through - {{ allowReadThroughText }} - -
-
- Head Object (Stub File) - {{ retainHeadObjectText }} - -
- - -
+ + + + + + This field is required. + +
+
+ + + + The entered value must be a positive integer. + Glacier Restore Days must be positive. + +
+
- + } @if( isTierMatch( TIER_TYPE.CLOUD_TIER, TIER_TYPE.GLACIER )){ +
+
+ +
Advanced
+
+
+ + + +
+
+ Multipart Sync Threshold + + +
+
+ Multipart Minimum Part Size + + +
+
+
+
+
+
+
+ } @if( isTierMatch( TIER_TYPE.CLOUD_TIER, TIER_TYPE.GLACIER )){ + RGW service would be restarted after creating the storage class. + } { let component: RgwStorageClassFormComponent; @@ -74,6 +75,8 @@ describe('RgwStorageClassFormComponent', () => { retain_head_object: true, storage_class: 'CLOUDIBM', allow_read_through: true, + read_through_restore_days: 1, + restore_storage_class: 'test67', s3: { storage_class: 'CLOUDIBM', endpoint: 'https://s3.amazonaws.com', @@ -85,6 +88,10 @@ describe('RgwStorageClassFormComponent', () => { multipart_min_part_size: 87877, multipart_sync_threshold: 987877, host_style: true + }, + 's3-glacier': { + glacier_restore_days: 5, + glacier_restore_tier_type: 'Standard' } } } @@ -118,4 +125,59 @@ describe('RgwStorageClassFormComponent', () => { component.submitAction(); expect(component).toBeTruthy(); }); + + it('should set required validators for CLOUD_TIER fields', () => { + (component as any).updateValidatorsBasedOnStorageClass(TIER_TYPE_DISPLAY.CLOUD_TIER); + const requiredFields = ['region', 'endpoint', 'access_key', 'secret_key', 'target_path']; + requiredFields.forEach((field) => { + const control = component.storageClassForm.get(field); + control.setValue(''); + control.updateValueAndValidity(); + }); + ['glacier_restore_tier_type', 'restore_storage_class'].forEach((field) => { + const control = component.storageClassForm.get(field); + control.setValue(''); + control.updateValueAndValidity(); + expect(component).toBeTruthy(); + }); + }); + + it('should set required validators for GLACIER fields', () => { + (component as any).updateValidatorsBasedOnStorageClass(TIER_TYPE_DISPLAY.GLACIER); + const requiredFields = [ + 'region', + 'endpoint', + 'access_key', + 'secret_key', + 'target_path', + 'glacier_restore_tier_type', + 'restore_storage_class' + ]; + requiredFields.forEach((field) => { + const control = component.storageClassForm.get(field); + control.setValue(''); + control.updateValueAndValidity(); + expect(component).toBeTruthy(); + }); + }); + + it('should clear validators for LOCAL fields', () => { + (component as any).updateValidatorsBasedOnStorageClass(TIER_TYPE_DISPLAY.LOCAL); + + const allFields = [ + 'region', + 'endpoint', + 'access_key', + 'secret_key', + 'target_path', + 'glacier_restore_tier_type', + 'restore_storage_class' + ]; + allFields.forEach((field) => { + const control = component.storageClassForm.get(field); + control.setValue(''); + control.updateValueAndValidity(); + expect(component).toBeTruthy(); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts index 04b0d4a2b03de..2e813005ae59a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms'; +import { AbstractControl, FormControl, Validators } from '@angular/forms'; import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; import { CdForm } from '~/app/shared/forms/cd-form'; import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder'; @@ -28,7 +28,20 @@ import { ZoneGroup, ZoneGroupDetails, CLOUDS3_STORAGE_CLASS_TEXT, - LOCAL_STORAGE_CLASS_TEXT + LOCAL_STORAGE_CLASS_TEXT, + GLACIER_STORAGE_CLASS_TEXT, + GLACIER_RESTORE_DAY_TEXT, + GLACIER_RESTORE_TIER_TYPE_TEXT, + RESTORE_DAYS_TEXT, + READTHROUGH_RESTORE_DAYS_TEXT, + RESTORE_STORAGE_CLASS_TEXT, + TIER_TYPE_DISPLAY, + S3Glacier, + StorageClassOption, + STORAGE_CLASS_CONSTANTS, + STANDARD_TIER_TYPE_TEXT, + EXPEDITED_TIER_TYPE_TEXT, + TextLabels } from '../models/rgw-storage-class.model'; import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { NotificationService } from '~/app/shared/services/notification.service'; @@ -44,27 +57,21 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { action: string; resource: string; editing: boolean; - targetPathText: string; - targetEndpointText: string; - targetRegionText: string; showAdvanced: boolean = false; defaultZoneGroup: string; zonegroupNames: ZoneGroup[]; placementTargets: string[] = []; - multipartMinPartText: string; - storageClassText: string; - multipartSyncThreholdText: string; selectedZoneGroup: string; defaultZonegroup: ZoneGroup; zoneGroupDetails: ZoneGroupDetails; - targetSecretKeyText: string; - targetAccessKeyText: string; - retainHeadObjectText: string; storageClassInfo: StorageClass; tierTargetInfo: TierTarget; - allowReadThroughText: string; + glacierStorageClassDetails: S3Glacier; allowReadThrough: boolean = false; TIER_TYPE = TIER_TYPE; + TIER_TYPE_DISPLAY = TIER_TYPE_DISPLAY; + storageClassOptions: StorageClassOption[]; + textLabels: TextLabels; constructor( public actionLabels: ActionLabelsI18n, @@ -82,18 +89,32 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { } ngOnInit() { - this.multipartMinPartText = MULTIPART_MIN_PART_TEXT; - this.multipartSyncThreholdText = MULTIPART_SYNC_THRESHOLD_TEXT; - this.targetPathText = TARGET_PATH_TEXT; - this.targetRegionText = TARGET_REGION_TEXT; - this.targetEndpointText = TARGET_ENDPOINT_TEXT; - this.targetAccessKeyText = TARGET_ACCESS_KEY_TEXT; - this.targetSecretKeyText = TARGET_SECRET_KEY_TEXT; - this.retainHeadObjectText = RETAIN_HEAD_OBJECT_TEXT; - this.allowReadThroughText = ALLOW_READ_THROUGH_TEXT; - this.storageClassText = LOCAL_STORAGE_CLASS_TEXT; - this.storageClassTypeText(); + this.textLabels = { + targetPathText: TARGET_PATH_TEXT, + targetEndpointText: TARGET_ENDPOINT_TEXT, + targetRegionText: TARGET_REGION_TEXT, + targetAccessKeyText: TARGET_ACCESS_KEY_TEXT, + targetSecretKeyText: TARGET_SECRET_KEY_TEXT, + retainHeadObjectText: RETAIN_HEAD_OBJECT_TEXT, + allowReadThroughText: ALLOW_READ_THROUGH_TEXT, + storageClassText: LOCAL_STORAGE_CLASS_TEXT, + multipartMinPartText: MULTIPART_MIN_PART_TEXT, + multipartSyncThresholdText: MULTIPART_SYNC_THRESHOLD_TEXT, + tiertypeText: STANDARD_TIER_TYPE_TEXT, + glacierRestoreDayText: GLACIER_RESTORE_DAY_TEXT, + glacierRestoreTiertypeText: GLACIER_RESTORE_TIER_TYPE_TEXT, + restoreDaysText: RESTORE_DAYS_TEXT, + readthroughrestoreDaysText: READTHROUGH_RESTORE_DAYS_TEXT, + restoreStorageClassText: RESTORE_STORAGE_CLASS_TEXT + }; + this.storageClassOptions = [ + { value: TIER_TYPE.LOCAL, label: TIER_TYPE_DISPLAY.LOCAL }, + { value: TIER_TYPE.CLOUD_TIER, label: TIER_TYPE_DISPLAY.CLOUD_TIER }, + { value: TIER_TYPE.GLACIER, label: TIER_TYPE_DISPLAY.GLACIER } + ]; this.createForm(); + this.storageClassTypeText(); + this.TierTypeText(); this.loadingReady(); this.loadZoneGroup(); if (this.editing) { @@ -124,44 +145,97 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { retain_head_object: this.tierTargetInfo?.val?.retain_head_object || false, multipart_sync_threshold: response?.multipart_sync_threshold || '', multipart_min_part_size: response?.multipart_min_part_size || '', - allow_read_through: this.tierTargetInfo?.val?.allow_read_through || false + allow_read_through: this.tierTargetInfo?.val?.allow_read_through || false, + restore_storage_class: this.tierTargetInfo?.val?.restore_storage_class, + read_through_restore_days: this.tierTargetInfo?.val?.read_through_restore_days }); + if (this.tierTargetInfo?.val?.tier_type == TIER_TYPE.GLACIER) { + let glacierResponse = this.tierTargetInfo?.val['s3-glacier']; + this.storageClassForm.patchValue({ + glacier_restore_tier_type: glacierResponse.glacier_restore_tier_type, + glacier_restore_days: glacierResponse.glacier_restore_days + }); + } }); } - this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => { - const controlsToUpdate = ['region', 'endpoint', 'access_key', 'secret_key', 'target_path']; - controlsToUpdate.forEach((field) => { - const control = this.storageClassForm.get(field); - if ( - value === TIER_TYPE.CLOUD_TIER && - ['region', 'endpoint', 'access_key', 'secret_key', 'target_path'].includes(field) - ) { - control.setValidators([Validators.required]); - } else { - control.clearValidators(); - } - - control.updateValueAndValidity(); - }); + this.storageClassForm.get('storageClassType').valueChanges.subscribe((value) => { + this.updateValidatorsBasedOnStorageClass(value); }); this.storageClassForm.get('allow_read_through').valueChanges.subscribe((value) => { this.onAllowReadThroughChange(value); }); } + private updateValidatorsBasedOnStorageClass(value: string) { + const controlsToUpdate = [ + 'region', + 'endpoint', + 'access_key', + 'secret_key', + 'target_path', + 'glacier_restore_tier_type', + 'restore_storage_class' + ]; + + controlsToUpdate.forEach((field) => { + const control = this.storageClassForm.get(field); + + if ( + (value === TIER_TYPE.CLOUD_TIER && + ['region', 'endpoint', 'access_key', 'secret_key', 'target_path'].includes(field)) || + (value === TIER_TYPE.GLACIER && + [ + 'glacier_restore_tier_type', + 'restore_storage_class', + 'region', + 'endpoint', + 'access_key', + 'secret_key', + 'target_path' + ].includes(field)) + ) { + control.setValidators([Validators.required]); + } else { + control.clearValidators(); + } + + control.updateValueAndValidity(); + }); + } + storageClassTypeText() { this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => { if (value === TIER_TYPE.LOCAL) { - this.storageClassText = LOCAL_STORAGE_CLASS_TEXT; + this.textLabels.storageClassText = LOCAL_STORAGE_CLASS_TEXT; } else if (value === TIER_TYPE.CLOUD_TIER) { - this.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT; + this.textLabels.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT; + } else if (value === TIER_TYPE.GLACIER) { + this.textLabels.storageClassText = GLACIER_STORAGE_CLASS_TEXT; + } + }); + } + + TierTypeText() { + this.storageClassForm?.get('glacier_restore_tier_type')?.valueChanges.subscribe((value) => { + if (value === STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS) { + this.textLabels.tiertypeText = STANDARD_TIER_TYPE_TEXT; } else { - this.storageClassText = LOCAL_STORAGE_CLASS_TEXT; + this.textLabels.tiertypeText = EXPEDITED_TIER_TYPE_TEXT; } }); } createForm() { + const self = this; + + const lockDaysValidator = CdValidators.custom('lockDays', () => { + if (!self.storageClassForm || !self.storageClassForm.getRawValue()) { + return false; + } + + const lockDays = Number(self.storageClassForm.getValue('read_through_restore_days')); + return !Number.isInteger(lockDays) || lockDays === 0; + }); this.storageClassForm = this.formBuilder.group({ storage_class: new FormControl('', { validators: [Validators.required] @@ -188,8 +262,35 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required]) ]), retain_head_object: new FormControl(true), - multipart_sync_threshold: new FormControl(33554432), - multipart_min_part_size: new FormControl(33554432), + glacier_restore_tier_type: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS, [ + CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER }, [Validators.required]) + ]), + glacier_restore_days: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_GLACIER_RESTORE_DAYS, [ + CdValidators.composeIf({ storageClassType: TIER_TYPE.GLACIER || TIER_TYPE.CLOUD_TIER }, [ + CdValidators.number(false), + lockDaysValidator + ]) + ]), + restore_storage_class: new FormControl(STORAGE_CLASS_CONSTANTS.DEFAULT_STORAGE_CLASS), + read_through_restore_days: new FormControl( + { + value: STORAGE_CLASS_CONSTANTS.DEFAULT_READTHROUGH_RESTORE_DAYS, + disabled: true + }, + CdValidators.composeIf( + (form: AbstractControl) => { + const type = form.get('storageClassType')?.value; + return type === TIER_TYPE.GLACIER || type === TIER_TYPE.CLOUD_TIER; + }, + [CdValidators.number(false), lockDaysValidator] + ) + ), + multipart_sync_threshold: new FormControl( + STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_SYNC_THRESHOLD + ), + multipart_min_part_size: new FormControl( + STORAGE_CLASS_CONSTANTS.DEFAULT_MULTIPART_MIN_PART_SIZE + ), allow_read_through: new FormControl(false), storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required) }); @@ -213,7 +314,6 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { this.defaultZonegroup = this.zonegroupNames.find( (zonegroups: ZoneGroup) => zonegroups.id === data.default_zonegroup ); - this.storageClassForm.get('zonegroup').setValue(this.defaultZonegroup.name); this.onZonegroupChange(); resolve(); @@ -292,14 +392,22 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { onAllowReadThroughChange(checked: boolean): void { this.allowReadThrough = checked; + const readThroughDaysControl = this.storageClassForm.get('read_through_restore_days'); if (this.allowReadThrough) { this.storageClassForm.get('retain_head_object')?.setValue(true); this.storageClassForm.get('retain_head_object')?.disable(); + readThroughDaysControl?.enable(); } else { this.storageClassForm.get('retain_head_object')?.enable(); + readThroughDaysControl?.disable(); } } + isTierMatch(...types: string[]): boolean { + const tierType = this.storageClassForm.getValue('storageClassType'); + return types.includes(tierType); + } + buildRequest() { if (this.storageClassForm.errors) return null; @@ -309,7 +417,6 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { const placementId = this.storageClassForm.get('placement_target').value; const storageClassType = this.storageClassForm.get('storageClassType').value; const retain_head_object = this.storageClassForm.get('retain_head_object').value; - return this.buildPlacementTargets( storageClassType, zoneGroup, @@ -328,44 +435,68 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { retain_head_object: boolean, rawFormValue: any ): RequestModel { - switch (storageClassType) { - case TIER_TYPE.LOCAL: - return { - zone_group: zoneGroup, - placement_targets: [ - { - tags: [], - placement_id: placementId, - storage_class: storageClass + const baseTarget = { + placement_id: placementId, + storage_class: storageClass + }; + + if (storageClassType === TIER_TYPE.LOCAL) { + return { + zone_group: zoneGroup, + placement_targets: [baseTarget] + }; + } + + const tierConfig = { + endpoint: rawFormValue.endpoint, + access_key: rawFormValue.access_key, + secret: rawFormValue.secret_key, + target_path: rawFormValue.target_path, + retain_head_object, + allow_read_through: rawFormValue.allow_read_through, + region: rawFormValue.region, + multipart_sync_threshold: rawFormValue.multipart_sync_threshold, + multipart_min_part_size: rawFormValue.multipart_min_part_size, + restore_storage_class: rawFormValue.restore_storage_class, + ...(rawFormValue.allow_read_through + ? { read_through_restore_days: rawFormValue.read_through_restore_days } + : {}) + }; + + if (storageClassType === TIER_TYPE.CLOUD_TIER) { + return { + zone_group: zoneGroup, + placement_targets: [ + { + ...baseTarget, + tier_type: TIER_TYPE.CLOUD_TIER, + tier_config: { + ...tierConfig } - ] - }; + } + ] + }; + } - case TIER_TYPE.CLOUD_TIER: - return { - zone_group: zoneGroup, - placement_targets: [ - { - tags: [], - placement_id: placementId, - storage_class: storageClass, - tier_type: TIER_TYPE.CLOUD_TIER, - tier_config: { - endpoint: rawFormValue.endpoint, - access_key: rawFormValue.access_key, - secret: rawFormValue.secret_key, - target_path: rawFormValue.target_path, - retain_head_object: retain_head_object, - allow_read_through: rawFormValue.allow_read_through, - region: rawFormValue.region, - multipart_sync_threshold: rawFormValue.multipart_sync_threshold, - multipart_min_part_size: rawFormValue.multipart_min_part_size - } + if (storageClassType === TIER_TYPE.GLACIER) { + return { + zone_group: zoneGroup, + placement_targets: [ + { + ...baseTarget, + tier_type: TIER_TYPE.GLACIER, + tier_config: { + ...tierConfig, + glacier_restore_days: rawFormValue.glacier_restore_days, + glacier_restore_tier_type: rawFormValue.glacier_restore_tier_type } - ] - }; - default: - return null; + } + ] + }; } + return { + zone_group: zoneGroup, + placement_targets: [baseTarget] + }; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts index 714d023431454..f8ddae11909c3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts @@ -124,15 +124,27 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI (data: ZoneGroupDetails) => { this.storageClassList = []; const tierObj = BucketTieringUtils.filterAndMapTierTargets(data); - const tierConfig = tierObj.map((item) => ({ - ...item, - tier_type: - item.tier_type?.toLowerCase() === TIER_TYPE.CLOUD_TIER - ? TIER_TYPE_DISPLAY.CLOUD_TIER - : item.tier_type?.toLowerCase() === TIER_TYPE.LOCAL - ? TIER_TYPE_DISPLAY.LOCAL - : item.tier_type - })); + const tierConfig = tierObj.map((item) => { + let tierTypeDisplay; + + switch (item.tier_type?.toLowerCase()) { + case TIER_TYPE.CLOUD_TIER: + tierTypeDisplay = TIER_TYPE_DISPLAY.CLOUD_TIER; + break; + case TIER_TYPE.LOCAL: + tierTypeDisplay = TIER_TYPE_DISPLAY.LOCAL; + break; + case TIER_TYPE.GLACIER: + tierTypeDisplay = TIER_TYPE_DISPLAY.GLACIER; + break; + default: + tierTypeDisplay = item.tier_type; + } + return { + ...item, + tier_type: tierTypeDisplay + }; + }); this.transformTierData(tierConfig); this.storageClassList.push(...tierConfig); resolve(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/rgw-bucket-tiering.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/rgw-bucket-tiering.ts index 8b8e2076850f2..c075ed1d267b9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/rgw-bucket-tiering.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/utils/rgw-bucket-tiering.ts @@ -32,23 +32,33 @@ export class BucketTieringUtils { private static getTierTargets(tierTarget: TierTarget, zoneGroup: string, targetName: string) { const val = tierTarget.val; - if (val.tier_type === TIER_TYPE.CLOUD_TIER) { - return { - zonegroup_name: zoneGroup, - placement_target: targetName, - storage_class: val.storage_class, - retain_head_object: val.retain_head_object, - allow_read_through: val.allow_read_through, - tier_type: val.tier_type, - ...val.s3 - }; - } else { + const tierType = val.tier_type; + + const commonProps = { + zonegroup_name: zoneGroup, + placement_target: targetName, + storage_class: val.storage_class, + tier_type: tierType + }; + + if (!tierType || tierType === TIER_TYPE.LOCAL) { + return commonProps; + } + const cloudProps = { + ...commonProps, + retain_head_object: val.retain_head_object, + allow_read_through: val.allow_read_through, + restore_storage_class: val.restore_storage_class, + read_through_restore_days: val.read_through_restore_days, + ...val.s3 + }; + + if (tierType === TIER_TYPE.GLACIER) { return { - zonegroup_name: zoneGroup, - placement_target: targetName, - storage_class: val.storage_class, - tier_type: TIER_TYPE.LOCAL + ...cloudProps, + ...val['s3-glacier'] }; } + return cloudProps; } } diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 10cb5a60eba8d..411264c9aee0e 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -2033,7 +2033,7 @@ class RgwMultisite: def add_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]): rgw_add_placement_cmd = ['zonegroup', 'placement', 'add'] STANDARD_STORAGE_CLASS = "STANDARD" - CLOUD_S3_TIER_TYPE = "cloud-s3" + CLOUD_S3_TIER_TYPES = ["cloud-s3", "cloud-s3-glacier"] for placement_target in placement_targets: # pylint: disable=R1702 cmd_add_placement_options = [ @@ -2043,16 +2043,14 @@ class RgwMultisite: storage_class_name = placement_target.get('storage_class', None) tier_type = placement_target.get('tier_type', None) - if ( - placement_target.get('tier_type') == CLOUD_S3_TIER_TYPE - and storage_class_name != STANDARD_STORAGE_CLASS - ): + if tier_type in CLOUD_S3_TIER_TYPES and storage_class_name != STANDARD_STORAGE_CLASS: tier_config = placement_target.get('tier_config', {}) if tier_config: tier_config_items = self.modify_retain_head(tier_config) tier_config_str = ','.join(tier_config_items) cmd_add_placement_options += [ - '--tier-type', 'cloud-s3', '--tier-config', tier_config_str + '--tier-type', tier_type, + '--tier-config', tier_config_str ] if placement_target.get('tags') and storage_class_name != STANDARD_STORAGE_CLASS: @@ -2079,7 +2077,7 @@ class RgwMultisite: ) except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') - if tier_type == CLOUD_S3_TIER_TYPE: + if tier_type in CLOUD_S3_TIER_TYPES: self.ensure_realm_and_sync_period() if storage_classes: @@ -2103,13 +2101,13 @@ class RgwMultisite: ) except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') - if tier_type == CLOUD_S3_TIER_TYPE: + if tier_type in CLOUD_S3_TIER_TYPES: self.ensure_realm_and_sync_period() def modify_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]): rgw_add_placement_cmd = ['zonegroup', 'placement', 'modify'] STANDARD_STORAGE_CLASS = "STANDARD" - CLOUD_S3_TIER_TYPE = "cloud-s3" + CLOUD_S3_TIER_TYPES = ["cloud-s3", "cloud-s3-glacier"] for placement_target in placement_targets: # pylint: disable=R1702,line-too-long # noqa: E501 cmd_add_placement_options = [ @@ -2117,9 +2115,10 @@ class RgwMultisite: '--placement-id', placement_target['placement_id'] ] storage_class_name = placement_target.get('storage_class', None) + tier_type = placement_target.get('tier_type') if ( - placement_target.get('tier_type') == CLOUD_S3_TIER_TYPE + placement_target.get('tier_type') == CLOUD_S3_TIER_TYPES and storage_class_name != STANDARD_STORAGE_CLASS ): tier_config = placement_target.get('tier_config', {}) @@ -2154,7 +2153,8 @@ class RgwMultisite: ) except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') - self.ensure_realm_and_sync_period() + if tier_type in CLOUD_S3_TIER_TYPES: + self.ensure_realm_and_sync_period() if storage_classes: for sc in storage_classes: @@ -2177,7 +2177,8 @@ class RgwMultisite: ) except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') - self.ensure_realm_and_sync_period() + if tier_type in CLOUD_S3_TIER_TYPES: + self.ensure_realm_and_sync_period() def delete_placement_targets(self, placement_id: str, storage_class: str): rgw_zonegroup_delete_cmd = ['zonegroup', 'placement', 'rm',