From 3ec58c0b007d07379b6cd3c2e2123676ef80ba2f Mon Sep 17 00:00:00 2001 From: Dnyaneshwari Date: Fri, 5 Sep 2025 15:47:11 +0530 Subject: [PATCH] mgr/dashboard: Local storage class creation via dashboard doesn't handle creation of pool. Fixes: https://tracker.ceph.com/issues/72569 Signed-off-by: Dnyaneshwari mgr/dashboard: handle creation of new pool Commit includes: 1) Provide link to create a new pool 2) Refactored validation on ACL mapping, removed required validator as default 3) fixed runtime error on console due to ACL length due to which the details section was not opening 4) Used rxjs operators to make API calls and making form ready once all data is available, fixing the form patch issues 5) Refactored some part of code to improve the performance 6) Added zone and pool information in details section for local storage class Fixes: https://tracker.ceph.com/issues/72569 Signed-off-by: Naman Munet (cherry picked from commit 2d0e71c845643a26d4425ddac8ee0ff30153eff2) src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-form/rgw-storage-class-form.component.ts src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts src/pybind/mgr/dashboard/services/rgw_client.py --- src/pybind/mgr/dashboard/controllers/rgw.py | 51 ++ .../rgw/models/rgw-storage-class.model.ts | 46 +- .../rgw-storage-class-details.component.html | 47 +- ...gw-storage-class-details.component.spec.ts | 28 +- .../rgw-storage-class-details.component.ts | 59 ++- .../rgw-storage-class-form.component.html | 126 +++-- .../rgw-storage-class-form.component.ts | 496 +++++++++++------- .../rgw-storage-class-list.component.html | 2 +- .../rgw-storage-class-list.component.ts | 28 +- .../frontend/src/app/ceph/rgw/rgw.module.ts | 6 +- .../app/ceph/rgw/utils/rgw-bucket-tiering.ts | 30 +- .../shared/api/rgw-storage-class.service.ts | 11 +- src/pybind/mgr/dashboard/openapi.yaml | 97 ++++ .../mgr/dashboard/services/rgw_client.py | 43 +- 14 files changed, 759 insertions(+), 311 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index bbbf5b971ad..c9eb6badd30 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -1569,6 +1569,57 @@ class RgwZone(RESTController): result = multisite_instance.get_user_list(zoneName, realmName) return result + @Endpoint('POST', path='storage-class') + @CreatePermission + def create_storage_class(self, zone_name: str, placement_target: str, storage_class: str, + data_pool: str, compression=''): + return self.handle_storage_class(zone_name, placement_target, storage_class, data_pool, + operation='create', compression=compression) + + @Endpoint('PUT', path='storage-class') + @CreatePermission + def edit_storage_class(self, zone_name: str, placement_target: str, storage_class: str, + data_pool: str, compression=''): + return self.handle_storage_class(zone_name, placement_target, storage_class, data_pool, + operation='edit', compression=compression) + + def handle_storage_class(self, zone_name: str, placement_target: str, storage_class: str, + data_pool: str, operation: str, compression=''): + if not (placement_target and storage_class and data_pool): + raise DashboardException( + msg='Failed to get placement target', + http_status_code=404, + component='rgw' + ) + multisite_instance = RgwMultisite() + + try: + if operation == 'create': + multisite_instance.add_storage_class_zone( + zone_name=zone_name, + placement_target=placement_target, + storage_class=storage_class, + data_pool=data_pool, + compression=compression + ) + elif operation == 'edit': + multisite_instance.edit_storage_class_zone( + zone_name=zone_name, + placement_target=placement_target, + storage_class=storage_class, + data_pool=data_pool, + compression=compression + ) + except DashboardException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + + return { + 'placement_target': placement_target, + 'storage_class': storage_class, + 'data_pool': data_pool, + 'status': 'success' + } + @APIRouter('/rgw/topic', Scope.RGW) @APIDoc("RGW Topic Management API", "RGW Topic Management") 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 7a34f945065..370fa78d0b6 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 @@ -10,6 +10,8 @@ export interface StorageClass { endpoint?: string; region?: string; zonegroup_name?: string; + zone_name?: string; + data_pool?: string; } export interface TierTarget { @@ -41,8 +43,9 @@ export interface StorageClassDetails { multipart_sync_threshold: number; host_style: string; allow_read_through: boolean; + storage_class: string; zonegroup_name?: string; - placement_targets?: string; + placement_target?: string; glacier_restore_days?: number; glacier_restore_tier_type?: string; read_through_restore_days?: number; @@ -50,12 +53,43 @@ export interface StorageClassDetails { retain_head_object?: boolean; acls?: ACL[]; acl_mappings?: ACL[]; + zone_name?: string; + data_pool?: string; } export interface ZoneGroup { name: string; id: string; placement_targets?: Target[]; + zones?: string[]; +} + +export interface ZoneRequest { + zone_name: string; + placement_target: string; + storage_class: string; + data_pool: string; +} +export interface StorageClassPool { + data_pool: string; +} + +export interface PlacementPool { + key: string; + val: { + storage_classes: { + [storage_class: string]: StorageClassPool; + }; + }; +} + +export interface Zone { + name: string; + placement_pools: PlacementPool[]; +} + +export interface AllZonesResponse { + zones: Zone[]; } export interface ACL { @@ -102,10 +136,10 @@ export interface RequestModel { } export interface PlacementTarget { - placement_id: string; + placement_id?: string; tags?: string[]; tier_type?: TIER_TYPE; - tier_config_rm: TierConfigRm; + tier_config_rm?: TierConfigRm; tier_config?: { endpoint: string; access_key: string; @@ -126,6 +160,8 @@ export interface PlacementTarget { storage_class?: string; name?: string; tier_targets?: TierTarget[]; + data_pool?: string; + placement_target?: string; } export interface TierConfigRm { @@ -310,3 +346,7 @@ export const AclHelperText: AclMaps = { destination: $localize`The URI identifying the destination group or user.` } }; + +export const POOL = { + PATH: '/pool/create' +}; 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 1a988d3407b..8f794c69222 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 @@ -182,14 +182,57 @@ + } + @if (isTierMatch(TIER_TYPE.LOCAL)) { + + + Zone + + + A zone defines a logical group that consists of one or more Ceph Object Gateway instances. + + + + +@if (loading) { + + + } @else { + {{ localStorageClassDetails?.zone_name }} + } + + + + + Data Pool + + + The data pool contains the objects associated with this storage class. + + + + + @if (loading) { + + + } @else { + {{ localStorageClassDetails?.data_pool }} + } + + } Placement Target - Placement Target defines the destination and rules for moving objects between - storage tiers. + Placement targets control which Pools are associated with a particular bucket. 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 1321a72c035..2d3e5338491 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 @@ -5,13 +5,13 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { SharedModule } from '~/app/shared/shared.module'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { SimpleChange } from '@angular/core'; describe('RgwStorageClassDetailsComponent', () => { let component: RgwStorageClassDetailsComponent; let fixture: ComponentFixture; const mockSelection: StorageClassDetails = { + storage_class: 'TestStorageClass', access_key: 'TestAccessKey', secret: 'TestSecret', target_path: '/test/path', @@ -45,30 +45,4 @@ describe('RgwStorageClassDetailsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - it('should update storageDetails when selection input changes', () => { - const newSelection: StorageClassDetails = { - access_key: 'NewAccessKey', - secret: 'NewSecret', - target_path: '/new/path', - multipart_min_part_size: 500, - multipart_sync_threshold: 1000, - host_style: 'virtual', - retain_head_object: false, - allow_read_through: false, - tier_type: 'archive', - glacier_restore_days: 1, - glacier_restore_tier_type: 'standard', - placement_targets: '', - read_through_restore_days: 7, - restore_storage_class: 'restored', - zonegroup_name: 'zone1' - }; - - component.selection = newSelection; - component.ngOnChanges({ - selection: new SimpleChange(null, newSelection, false) - }); - expect(component.storageDetails).toEqual(newSelection); - }); }); 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 69c8f9dbfe8..5c2e5fddecb 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 @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { ALLOW_READ_THROUGH_TEXT, @@ -19,8 +19,11 @@ import { RESTORE_STORAGE_CLASS_TEXT, ZONEGROUP_TEXT, ACL, - GroupedACLs + GroupedACLs, + AllZonesResponse } from '../models/rgw-storage-class.model'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; +import { BucketTieringUtils } from '../utils/rgw-bucket-tiering'; @Component({ selector: 'cd-rgw-storage-class-details', templateUrl: './rgw-storage-class-details.component.html', @@ -30,7 +33,6 @@ export class RgwStorageClassDetailsComponent implements OnChanges, OnInit { @Input() selection: StorageClassDetails; columns: CdTableColumn[] = []; - storageDetails: StorageClassDetails; allowReadThroughText = ALLOW_READ_THROUGH_TEXT; retainHeadObjectText = RETAIN_HEAD_OBJECT_TEXT; multipartMinPartText = MULTIPART_MIN_PART_TEXT; @@ -48,31 +50,28 @@ export class RgwStorageClassDetailsComponent implements OnChanges, OnInit { restoreStorageClassText = RESTORE_STORAGE_CLASS_TEXT; zoneGroupText = ZONEGROUP_TEXT; groupedACLs: GroupedACLs = {}; + localStorageClassDetails = { zone_name: '', data_pool: '' }; + loading = false; + + private rgwZoneService = inject(RgwZoneService); ngOnChanges(changes: SimpleChanges): void { - if (changes['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, - 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, - 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 - }; + if ( + changes['selection'] && + changes['selection'].currentValue?.tier_type?.toLowerCase() === TIER_TYPE.LOCAL && + changes['selection'].firstChange + ) { + // The idea here is to not call the API if we already have the zone_name and data_pool + // When the details view is expanded and table refreshes data then this API should not be called again + const { zone_name, data_pool } = this.localStorageClassDetails; + if (!zone_name || !data_pool) { + this.getZoneInfo(); + } } } ngOnInit() { - this.groupedACLs = this.groupByType(this.selection.acl_mappings); + this.groupedACLs = this.groupByType(this.selection?.acl_mappings); } isTierMatch(...types: string[]): boolean { @@ -80,6 +79,22 @@ export class RgwStorageClassDetailsComponent implements OnChanges, OnInit { return types.some((type) => type.toLowerCase() === tier_type); } + getZoneInfo() { + this.loading = true; + this.rgwZoneService.getAllZonesInfo().subscribe({ + next: (data: AllZonesResponse) => { + this.localStorageClassDetails = BucketTieringUtils.getZoneInfoHelper( + data.zones, + this.selection + ); + this.loading = false; + }, + error: () => { + this.loading = false; + } + }); + } + groupByType(acls: ACL[]): GroupedACLs { return acls?.reduce((groupAcls: GroupedACLs, item: ACL) => { const type = item.val?.type?.toUpperCase(); 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 aa65fc210d0..821c529a52a 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 @@ -22,8 +22,6 @@ [invalid]="storageClassForm.showError('storageClassType', formDir, 'required')" [invalidText]="storageError" > -