From 675057f4204acd454fd6f0b0064fcdba925f1f4a Mon Sep 17 00:00:00 2001 From: Dnyaneshwari Date: Thu, 22 May 2025 12:38:25 +0530 Subject: [PATCH] mgr/dashboard: Local Storage Class - create and list Fixes: https://tracker.ceph.com/issues/71460 Signed-off-by: Dnyaneshwari Talwekar --- .../rgw/models/rgw-storage-class.model.ts | 43 +- .../rgw-storage-class-details.component.html | 98 ++-- .../rgw-storage-class-details.component.ts | 7 +- .../rgw-storage-class-form.component.html | 491 +++++++++--------- .../rgw-storage-class-form.component.spec.ts | 32 +- .../rgw-storage-class-form.component.ts | 193 ++++--- .../rgw-storage-class-list.component.html | 2 + .../rgw-storage-class-list.component.ts | 40 +- .../app/ceph/rgw/utils/rgw-bucket-tiering.ts | 57 +- .../mgr/dashboard/services/rgw_client.py | 7 +- 10 files changed, 577 insertions(+), 393 deletions(-) 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 3f5523dcb96df..cc7f24fcb888b 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 @@ -5,10 +5,10 @@ export interface ZoneGroupDetails { } export interface StorageClass { - storage_class: string; - endpoint: string; - region: string; placement_target: string; + storage_class?: string; + endpoint?: string; + region?: string; zonegroup_name?: string; } @@ -24,18 +24,31 @@ export interface StorageClassDetails { } export interface TierTarget { + key: string; val: { storage_class: string; tier_type: string; retain_head_object: boolean; allow_read_through: boolean; - s3: S3Details; + s3?: S3Details; }; } export interface Target { name: string; tier_targets: TierTarget[]; + storage_classes?: string[]; +} + +export interface StorageClassDetails { + target_path: string; + access_key: string; + secret: string; + multipart_min_part_size: number; + multipart_sync_threshold: number; + host_style: string; + zonegroup_name?: string; + placement_targets?: string; } export interface ZoneGroup { @@ -66,8 +79,8 @@ export interface RequestModel { export interface PlacementTarget { tags: string[]; placement_id: string; - tier_type: typeof CLOUD_TIER; - tier_config: { + tier_type?: TIER_TYPE; + tier_config?: { endpoint: string; access_key: string; secret: string; @@ -83,7 +96,11 @@ export interface PlacementTarget { tier_targets?: TierTarget[]; } -export const CLOUD_TIER = 'cloud-s3'; +export const TIER_TYPE = { + LOCAL: 'local', + CLOUD_TIER: 'cloud-s3', + GLACIER: 'cloud-s3-glacier' +} as const; export const DEFAULT_PLACEMENT = 'default-placement'; @@ -116,3 +133,15 @@ export const RETAIN_HEAD_OBJECT_TEXT = 'Retain object metadata after transition export const HOST_STYLE = `The URL format for accessing the remote S3 endpoint: - 'Path': Use for a path-based URL - 'Virtual': Use for a domain-based URL`; + +export const LOCAL_STORAGE_CLASS_TEXT = $localize`Local storage uses on-premises or directly attached devices for data storage.`; + +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 TIER_TYPE_DISPLAY = { + LOCAL: 'Local', + CLOUD_TIER: 'Cloud S3', + GLACIER: 'Cloud S3 Glacier' +}; 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 998867d3b6f29..13b90e2735f2e 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,22 +7,51 @@ data-testid="rgw-storage-details" > - - + + + 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. + + + + {{ selection?.zonegroup_name }} + + + + Placement Target + + + Placement Target defines the destination and rules for moving objects between + storage tiers. + + + + {{ selection?.placement_target }} + + + Target Path - + {{ targetPathText }} {{ selection?.target_path }} - - + + Access key - + {{ targetAccessKeyText }} @@ -45,11 +74,12 @@ - - + + Secret key - {{ targetSecretKeyText }} + {{ targetSecretKeyText }} @@ -70,59 +100,63 @@ - - + + Host Style - {{ hostStyleText }} + {{ hostStyleText }} {{ selection?.host_style }} - - + + Multipart Minimum Part Size - + {{ multipartMinPartText }} {{ selection?.multipart_min_part_size }} - - + + Multipart Sync Threshold - - {{ multipartSyncThreholdText }} + + {{ multipartSyncThreholdText }} {{ selection?.multipart_sync_threshold }} - - - Allow Read Through + + + Retain Head Object - - {{ allowReadThroughText }} + + Retain object metadata after transition to the cloud (default: false). - {{ selection?.allow_read_through }} + {{ selection?.retain_head_object }} - - - Head Object (Stub File) + + + Allow Read Through - - {{ retainHeadObjectText }} + + {{ allowReadThroughText }} - {{ selection?.retain_head_object }} + {{ selection?.allow_read_through }} 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 14b01a140428e..3a536f98f1c9d 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 @@ -9,9 +9,9 @@ import { StorageClassDetails, TARGET_ACCESS_KEY_TEXT, TARGET_PATH_TEXT, - TARGET_SECRET_KEY_TEXT + TARGET_SECRET_KEY_TEXT, + TIER_TYPE_DISPLAY } from '../models/rgw-storage-class.model'; - @Component({ selector: 'cd-rgw-storage-class-details', templateUrl: './rgw-storage-class-details.component.html', @@ -30,10 +30,13 @@ export class RgwStorageClassDetailsComponent implements OnChanges { targetSecretKeyText = TARGET_SECRET_KEY_TEXT; targetPathText = TARGET_PATH_TEXT; hostStyleText = HOST_STYLE; + TIER_TYPE_DISPLAY = TIER_TYPE_DISPLAY; ngOnChanges() { if (this.selection) { this.storageDetails = { + zonegroup_name: this.selection.zonegroup_name, + placement_targets: this.selection.placement_targets, access_key: this.selection.access_key, secret: this.selection.secret, target_path: this.selection.target_path, 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 ce50c68d11632..b34b48de67b5e 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 @@ -11,6 +11,33 @@ +
+ + + + + + + This field is required. + +
@@ -20,16 +47,16 @@ i18n-label formControlName="zonegroup" id="zonegroup" - [invalid]=" - storageClassForm.showError('zonegroup', formDir, 'required') - " + [invalid]="storageClassForm.showError('zonegroup', formDir, 'required')" (change)="onZonegroupChange()" [invalidText]="zonegroupError" > - @@ -49,19 +76,19 @@ i18n-label formControlName="placement_target" id="placement_target" - [invalid]=" - storageClassForm.showError('placement_target', formDir, 'required') - " + [invalid]="storageClassForm.showError('placement_target', formDir, 'required')" [invalidText]="placementError" > - - + + Storage Class Name + [invalidText]="storageClassError" + >Name - +
-
-
- - Target Region - - - - +
+
+ + This field is required. - + [invalid]="storageClassForm.showError('region', formDir, 'required')" + [invalidText]="regionError" + [helperText]="targetRegionText" + >Target Region + + + + This field is required. + +
+
+ + Target Endpoint + + + + This field is required. + +
-
- - Target Endpoint - - - - +
+
+ This field is required. - + >Target Access Key + + + + + This field is required. + +
-
- -
-
- Target Access Key - - - - - +
+
+ This field is required. - + >Target Secret Key + + + + + This field is required. + +
-
- -
-
- +
+ Target Secret Key + [invalid]="storageClassForm.showError('target_path', formDir, 'required')" + [invalidText]="targetError" + [helperText]="targetPathText" + >Target Path - - - + + This field is required.
- - -
- Target Path - - - - This field is required. +
+ +
Advanced
-
-
- Allow Read Through - {{ allowReadThroughText }} - -
-
- Head Object (Stub File) - {{ retainHeadObjectText }} - -
- -
- - - -
-
- + + + +
+
+ Multipart Sync Threshold + + +
+
+ Multipart Minimum Part Size + + +
+
+
+ Multipart Sync Threshold - - + (change)="onAllowReadThroughChange($event)" + >Allow Read Through + {{ allowReadThroughText }} +
-
- + Multipart Minimum Part Size - - + >Head Object (Stub File) + {{ retainHeadObjectText }} +
-
- - - -
Advanced
-
-
- + + + +
+ RGW service would be restarted after creating the storage class. { name: 'default-placement', tier_targets: [ { + key: 'test', val: { - storage_class: 'CLOUDIBM', tier_type: 'cloud-s3', retain_head_object: true, + storage_class: 'CLOUDIBM', allow_read_through: true, s3: { + storage_class: 'CLOUDIBM', endpoint: 'https://s3.amazonaws.com', access_key: 'ACCESSKEY', - storage_class: 'STANDARD', target_path: '/path/to/storage', target_storage_class: 'STANDARD', region: 'useastr1', @@ -88,31 +89,6 @@ describe('RgwStorageClassFormComponent', () => { } } ] - }, - { - name: 'placement1', - tier_targets: [ - { - val: { - storage_class: 'CloudIBM', - tier_type: 'cloud-s3', - retain_head_object: true, - allow_read_through: true, - s3: { - endpoint: 'https://s3.amazonaws.com', - access_key: 'ACCESSKEY', - storage_class: 'GLACIER', - target_path: '/pathStorage', - target_storage_class: 'CloudIBM', - region: 'useast1', - secret: 'SECRETKEY', - multipart_min_part_size: 187988787, - multipart_sync_threshold: 878787878, - host_style: false - } - } - } - ] } ] } @@ -120,7 +96,7 @@ describe('RgwStorageClassFormComponent', () => { }; component.storageClassForm.get('zonegroup').setValue('zonegroup1'); component.onZonegroupChange(); - expect(component.placementTargets).toEqual(['default-placement', 'placement1']); + expect(component.placementTargets).toEqual(['default-placement']); expect(component.storageClassForm.get('placement_target').value).toBe('default-placement'); }); 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 b2b8862109fc4..04b0d4a2b03de 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 @@ -10,7 +10,6 @@ import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.servi import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; import { ALLOW_READ_THROUGH_TEXT, - CLOUD_TIER, DEFAULT_PLACEMENT, MULTIPART_MIN_PART_TEXT, MULTIPART_SYNC_THRESHOLD_TEXT, @@ -25,11 +24,15 @@ import { TARGET_REGION_TEXT, TARGET_SECRET_KEY_TEXT, TierTarget, + TIER_TYPE, ZoneGroup, - ZoneGroupDetails + ZoneGroupDetails, + CLOUDS3_STORAGE_CLASS_TEXT, + LOCAL_STORAGE_CLASS_TEXT } from '../models/rgw-storage-class.model'; import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { NotificationService } from '~/app/shared/services/notification.service'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; @Component({ selector: 'cd-rgw-storage-class-form', @@ -49,6 +52,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { zonegroupNames: ZoneGroup[]; placementTargets: string[] = []; multipartMinPartText: string; + storageClassText: string; multipartSyncThreholdText: string; selectedZoneGroup: string; defaultZonegroup: ZoneGroup; @@ -60,6 +64,7 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { tierTargetInfo: TierTarget; allowReadThroughText: string; allowReadThrough: boolean = false; + TIER_TYPE = TIER_TYPE; constructor( public actionLabels: ActionLabelsI18n, @@ -86,6 +91,8 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { 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.createForm(); this.loadingReady(); this.loadZoneGroup(); @@ -100,39 +107,60 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { placementTargetInfo, this.storageClassInfo.storage_class ); - let response = this.tierTargetInfo.val.s3; + let response = this.tierTargetInfo?.val?.s3; this.storageClassForm.get('zonegroup').disable(); this.storageClassForm.get('placement_target').disable(); this.storageClassForm.get('storage_class').disable(); - this.storageClassForm.get('zonegroup').setValue(this.storageClassInfo.zonegroup_name); - this.storageClassForm.get('region').setValue(response.region); - this.storageClassForm - .get('placement_target') - .setValue(this.storageClassInfo.placement_target); - this.storageClassForm.get('endpoint').setValue(response.endpoint); - this.storageClassForm.get('storage_class').setValue(this.storageClassInfo.storage_class); - this.storageClassForm.get('access_key').setValue(response.access_key); - this.storageClassForm.get('secret_key').setValue(response.secret); - this.storageClassForm.get('target_path').setValue(response.target_path); - this.storageClassForm - .get('retain_head_object') - .setValue(this.tierTargetInfo?.val?.retain_head_object || false); - this.storageClassForm - .get('multipart_sync_threshold') - .setValue(response.multipart_sync_threshold || ''); - this.storageClassForm - .get('multipart_min_part_size') - .setValue(response.multipart_min_part_size || ''); - this.storageClassForm - .get('allow_read_through') - .setValue(this.tierTargetInfo?.val?.allow_read_through || false); + this.storageClassForm.patchValue({ + zonegroup: this.storageClassInfo?.zonegroup_name, + region: response?.region, + placement_target: this.storageClassInfo?.placement_target, + storageClassType: this.tierTargetInfo?.val?.tier_type ?? TIER_TYPE.LOCAL, + endpoint: response?.endpoint, + storage_class: this.storageClassInfo?.storage_class, + access_key: response?.access_key, + secret_key: response?.secret, + target_path: response?.target_path, + 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 + }); }); } + 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('allow_read_through').valueChanges.subscribe((value) => { this.onAllowReadThroughChange(value); }); } + storageClassTypeText() { + this.storageClassForm?.get('storageClassType')?.valueChanges.subscribe((value) => { + if (value === TIER_TYPE.LOCAL) { + this.storageClassText = LOCAL_STORAGE_CLASS_TEXT; + } else if (value === TIER_TYPE.CLOUD_TIER) { + this.storageClassText = CLOUDS3_STORAGE_CLASS_TEXT; + } else { + this.storageClassText = LOCAL_STORAGE_CLASS_TEXT; + } + }); + } + createForm() { this.storageClassForm = this.formBuilder.group({ storage_class: new FormControl('', { @@ -141,24 +169,29 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { zonegroup: new FormControl(this.selectedZoneGroup, { validators: [Validators.required] }), - region: new FormControl('', { - validators: [Validators.required] - }), + region: new FormControl('', [ + CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required]) + ]), placement_target: new FormControl('', { validators: [Validators.required] }), - endpoint: new FormControl(null, { - validators: [Validators.required] - }), - access_key: new FormControl(null, Validators.required), - secret_key: new FormControl(null, Validators.required), - target_path: new FormControl('', { - validators: [Validators.required] - }), + endpoint: new FormControl(null, [ + CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required]) + ]), + access_key: new FormControl(null, [ + CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required]) + ]), + secret_key: new FormControl(null, [ + CdValidators.composeIf({ storageClassType: TIER_TYPE.CLOUD_TIER }, [Validators.required]) + ]), + target_path: new FormControl('', [ + 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), - allow_read_through: new FormControl(false) + allow_read_through: new FormControl(false), + storageClassType: new FormControl(TIER_TYPE.LOCAL, Validators.required) }); } @@ -268,33 +301,71 @@ export class RgwStorageClassFormComponent extends CdForm implements OnInit { } buildRequest() { + if (this.storageClassForm.errors) return null; + const rawFormValue = _.cloneDeep(this.storageClassForm.value); const zoneGroup = this.storageClassForm.get('zonegroup').value; const storageClass = this.storageClassForm.get('storage_class').value; const placementId = this.storageClassForm.get('placement_target').value; - const headObject = this.storageClassForm.get('retain_head_object').value; - const requestModel: RequestModel = { - zone_group: zoneGroup, - placement_targets: [ - { - tags: [], - placement_id: placementId, - storage_class: storageClass, - 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: headObject, - 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 - } - } - ] - }; - return requestModel; + const storageClassType = this.storageClassForm.get('storageClassType').value; + const retain_head_object = this.storageClassForm.get('retain_head_object').value; + + return this.buildPlacementTargets( + storageClassType, + zoneGroup, + placementId, + storageClass, + retain_head_object, + rawFormValue + ); + } + + private buildPlacementTargets( + storageClassType: string, + zoneGroup: string, + placementId: string, + storageClass: string, + 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 + } + ] + }; + + 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 + } + } + ] + }; + default: + return null; + } } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html index cdea55a32e560..696be083a660c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html @@ -8,6 +8,8 @@ columnMode="flex" [columns]="columns" (fetchData)="loadStorageClass()" + identifier="uniqueId" + [forceIdentifier]="true" selectionType="single" [hasDetails]="true" (setExpandedRow)="setExpandedRow($event)" 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 e36b8acae1960..714d023431454 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 @@ -4,7 +4,12 @@ import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; -import { StorageClass, ZoneGroupDetails } from '../models/rgw-storage-class.model'; +import { + StorageClass, + TIER_TYPE, + TIER_TYPE_DISPLAY, + ZoneGroupDetails +} from '../models/rgw-storage-class.model'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { Icons } from '~/app/shared/enum/icons.enum'; @@ -17,7 +22,6 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; import { Permission } from '~/app/shared/models/permissions'; import { BucketTieringUtils } from '../utils/rgw-bucket-tiering'; - import { Router } from '@angular/router'; const BASE_URL = 'rgw/tiering'; @@ -50,11 +54,21 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI ngOnInit() { this.columns = [ + { + prop: 'uniqueId', + isInvisible: true, + isHidden: true + }, { name: $localize`Storage Class`, prop: 'storage_class', flexGrow: 2 }, + { + name: $localize`Type`, + prop: 'tier_type', + flexGrow: 2 + }, { name: $localize`Zone Group`, prop: 'zonegroup_name', @@ -110,7 +124,17 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI (data: ZoneGroupDetails) => { this.storageClassList = []; const tierObj = BucketTieringUtils.filterAndMapTierTargets(data); - this.storageClassList.push(...tierObj); + 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 + })); + this.transformTierData(tierConfig); + this.storageClassList.push(...tierConfig); resolve(); }, (error) => { @@ -120,6 +144,16 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI }); } + transformTierData(tierConfig: any[]) { + tierConfig.forEach((item, index) => { + const zone_group = item?.zone_group; + const storageClass = item?.storage_class; + const uniqueId = `${zone_group}-${storageClass}-${index}`; + item.uniqueId = uniqueId; + }); + return tierConfig; + } + removeStorageClassModal() { const storage_class = this.selection.first().storage_class; const placement_target = this.selection.first().placement_target; 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 9cb19b680dbce..8b8e2076850f2 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 @@ -1,7 +1,7 @@ import { - CLOUD_TIER, Target, TierTarget, + TIER_TYPE, ZoneGroup, ZoneGroupDetails } from '../models/rgw-storage-class.model'; @@ -9,27 +9,46 @@ import { export class BucketTieringUtils { static filterAndMapTierTargets(zonegroupData: ZoneGroupDetails) { return zonegroupData.zonegroups.flatMap((zoneGroup: ZoneGroup) => - zoneGroup.placement_targets - .filter((target: Target) => target.tier_targets) - .flatMap((target: Target) => - target.tier_targets - .filter((tierTarget: TierTarget) => tierTarget.val.tier_type === CLOUD_TIER) - .map((tierTarget: TierTarget) => { - return this.getTierTargets(tierTarget, zoneGroup.name, target.name); - }) - ) + zoneGroup.placement_targets.flatMap((target: Target) => { + const storage_class = new Set( + (target.tier_targets || []).map((tier_target: TierTarget) => tier_target.key) + ); + const tierTargetDetails = (target.tier_targets || []).map((tierTarget: TierTarget) => + this.getTierTargets(tierTarget, zoneGroup.name, target.name) + ); + const localStorageClasses = (target.storage_classes || []) + .filter((storageClass) => storageClass !== 'STANDARD' && !storage_class.has(storageClass)) + .map((storageClass) => ({ + zonegroup_name: zoneGroup.name, + placement_target: target.name, + storage_class: storageClass, + tier_type: TIER_TYPE.LOCAL + })); + + return [...tierTargetDetails, ...localStorageClasses]; + }) ); } private static getTierTargets(tierTarget: TierTarget, zoneGroup: string, targetName: string) { - if (tierTarget.val.tier_type !== CLOUD_TIER) return null; - return { - zonegroup_name: zoneGroup, - placement_target: targetName, - storage_class: tierTarget.val.storage_class, - retain_head_object: tierTarget.val.retain_head_object, - allow_read_through: tierTarget.val.allow_read_through, - ...tierTarget.val.s3 - }; + 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 { + return { + zonegroup_name: zoneGroup, + placement_target: targetName, + storage_class: val.storage_class, + tier_type: TIER_TYPE.LOCAL + }; + } } } diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 081c89ba23ae0..0cbc0f9f8a543 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -2030,6 +2030,7 @@ class RgwMultisite: '--placement-id', placement_target['placement_id'] ] 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 @@ -2067,7 +2068,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 == CLOUD_S3_TIER_TYPE: + self.ensure_realm_and_sync_period() if storage_classes: for sc in storage_classes: @@ -2090,7 +2092,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 == CLOUD_S3_TIER_TYPE: + 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'] -- 2.39.5