From 717c1c5a084fa48ebf6f6267765daa0ebd76fc83 Mon Sep 17 00:00:00 2001 From: Dnyaneshwari Date: Fri, 17 Jan 2025 15:36:50 +0530 Subject: [PATCH] mgr/dashboard: Storage Class Management Fixes: https://tracker.ceph.com/issues/69606 Signed-off-by: Dnyaneshwari Talwekar --- .../rgw/models/rgw-storage-class.model.ts | 52 ++++++++ .../rgw-storage-class-details.component.html | 114 ++++++++++++++++++ .../rgw-storage-class-details.component.scss | 0 ...gw-storage-class-details.component.spec.ts | 47 ++++++++ .../rgw-storage-class-details.component.ts | 28 +++++ .../rgw-storage-class-list.component.html | 14 +++ .../rgw-storage-class-list.component.scss | 0 .../rgw-storage-class-list.component.spec.ts | 24 ++++ .../rgw-storage-class-list.component.ts | 106 ++++++++++++++++ .../frontend/src/app/ceph/rgw/rgw.module.ts | 20 ++- .../navigation/navigation.component.html | 5 + .../frontend/src/styles/themes/_content.scss | 3 +- 12 files changed, 408 insertions(+), 5 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts 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 new file mode 100644 index 0000000000000..2fd9ede9ec03f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-storage-class.model.ts @@ -0,0 +1,52 @@ +export interface ZoneGroupDetails { + default_zonegroup: string; + zonegroups: ZoneGroup[]; +} + +export interface StorageClass { + storage_class: string; + endpoint: string; + region: string; + placement_target: string; +} + +export interface StorageClassDetails { + target_path: string; + access_key: string; + secret: string; + multipart_min_part_size: number; + multipart_sync_threshold: number; + host_style: string; +} +export interface ZoneGroup { + name: string; + placement_targets: Target[]; +} + +export interface S3Details { + endpoint: string; + access_key: string; + storage_class: string; + target_path: string; + target_storage_class: string; + region: string; + secret: string; + multipart_min_part_size: number; + multipart_sync_threshold: number; + host_style: boolean; +} + +export interface TierTarget { + val: { + storage_class: string; + tier_type: string; + s3: S3Details; + }; +} + +export interface Target { + name: string; + tier_targets: TierTarget[]; +} + +export const CLOUD_TIER = 'cloud-s3'; 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 new file mode 100644 index 0000000000000..1f724fc7a2d1b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.html @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Target Path + + + The target path specifies a prefix to which the source bucket-name/object-name is + appended. + + + {{ selection?.target_path }}
+ Access key + + + Access key is the remote cloud S3 access key used for a specific connection. + + + +
+ + + +
+
+ Secret key + + Secret is the secret key for the remote cloud S3 service. + + +
+ + + +
+
+ Host Style + + The URL format for accessing the remote S3 endpoint: 'Path' for a path-based URL + or 'Virtual' for a domain-based URL. + + {{ selection?.host_style }}
+ Multipart Minimum Part Size + + + Minimum parts size to use when transitioning objects using multipart upload. + + + {{ selection?.multipart_min_part_size }}
+ Multipart Sync Threshold + + + Objects this size or larger will be transitioned to the cloud using multipart + upload. + + + {{ 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.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d 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 new file mode 100644 index 0000000000000..90541d45855c9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details.component'; +import { StorageClassDetails } from '../models/rgw-storage-class.model'; +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'; + +describe('RgwStorageClassDetailsComponent', () => { + let component: RgwStorageClassDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + SharedModule, + HttpClientTestingModule, + RouterTestingModule + ], + declarations: [RgwStorageClassDetailsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwStorageClassDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should update storageDetails when selection input changes', () => { + const mockSelection: StorageClassDetails = { + access_key: 'TestAccessKey', + secret: 'TestSecret', + target_path: '/test/path', + multipart_min_part_size: 100, + multipart_sync_threshold: 200, + host_style: 'path' + }; + component.selection = mockSelection; + component.ngOnChanges(); + expect(component.storageDetails).toEqual(mockSelection); + }); +}); 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 new file mode 100644 index 0000000000000..7494e6deac2c8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-details/rgw-storage-class-details.component.ts @@ -0,0 +1,28 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { StorageClassDetails } from '../models/rgw-storage-class.model'; + +@Component({ + selector: 'cd-rgw-storage-class-details', + templateUrl: './rgw-storage-class-details.component.html', + styleUrls: ['./rgw-storage-class-details.component.scss'] +}) +export class RgwStorageClassDetailsComponent implements OnChanges { + @Input() + selection: StorageClassDetails; + columns: CdTableColumn[] = []; + storageDetails: StorageClassDetails; + + ngOnChanges() { + if (this.selection) { + this.storageDetails = { + access_key: this.selection.access_key, + secret: this.selection.secret, + target_path: this.selection.target_path, + multipart_min_part_size: this.selection.multipart_min_part_size, + multipart_sync_threshold: this.selection.multipart_sync_threshold, + host_style: this.selection.host_style + }; + } + } +} 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 new file mode 100644 index 0000000000000..3aeebfc3c48d0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html @@ -0,0 +1,14 @@ + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.spec.ts new file mode 100644 index 0000000000000..24f472c911cc0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwStorageClassListComponent } from './rgw-storage-class-list.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('RgwStorageClassListComponent', () => { + let component: RgwStorageClassListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [RgwStorageClassListComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwStorageClassListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000000000..b1d41077092b7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts @@ -0,0 +1,106 @@ +import { Component, OnInit } from '@angular/core'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; + +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; +import { + StorageClass, + CLOUD_TIER, + ZoneGroup, + TierTarget, + Target, + ZoneGroupDetails +} from '../models/rgw-storage-class.model'; + +@Component({ + selector: 'cd-rgw-storage-class-list', + templateUrl: './rgw-storage-class-list.component.html', + styleUrls: ['./rgw-storage-class-list.component.scss'] +}) +export class RgwStorageClassListComponent extends ListWithDetails implements OnInit { + columns: CdTableColumn[]; + selection = new CdTableSelection(); + tableActions: CdTableAction[]; + storageClassList: StorageClass[] = []; + + constructor(private rgwZonegroupService: RgwZonegroupService) { + super(); + } + + ngOnInit() { + this.columns = [ + { + name: $localize`Zone Group`, + prop: 'zonegroup_name', + flexGrow: 2 + }, + { + name: $localize`Placement Target`, + prop: 'placement_target', + flexGrow: 2 + }, + { + name: $localize`Storage Class`, + prop: 'storage_class', + flexGrow: 2 + }, + { + name: $localize`Target Region`, + prop: 'region', + flexGrow: 2 + }, + { + name: $localize`Target Endpoint`, + prop: 'endpoint', + flexGrow: 2 + } + ]; + } + + loadStorageClass(): Promise { + return new Promise((resolve, reject) => { + this.rgwZonegroupService.getAllZonegroupsInfo().subscribe( + (data: ZoneGroupDetails) => { + this.storageClassList = []; + + const tierObj = data.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); + }) + ) + ); + this.storageClassList.push(...tierObj); + resolve(); + }, + (error) => { + reject(error); + } + ); + }); + } + + 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, + ...tierTarget.val.s3 + }; + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + setExpandedRow(expandedRow: any) { + super.setExpandedRow(expandedRow); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index f75c86607a9a9..9817ed51dfe73 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -63,6 +63,7 @@ import { RgwMultisiteSyncPolicyDetailsComponent } from './rgw-multisite-sync-pol import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component'; import { RgwMultisiteSyncPipeModalComponent } from './rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component'; import { RgwMultisiteTabsComponent } from './rgw-multisite-tabs/rgw-multisite-tabs.component'; +import { RgwStorageClassListComponent } from './rgw-storage-class-list/rgw-storage-class-list.component'; import { ButtonModule, GridModule, @@ -75,12 +76,14 @@ import { CheckboxModule, TreeviewModule, SelectModule, - NumberModule + NumberModule, + TabsModule } from 'carbon-components-angular'; import { CephSharedModule } from '../shared/ceph-shared.module'; import { RgwUserAccountsComponent } from './rgw-user-accounts/rgw-user-accounts.component'; import { RgwUserAccountsFormComponent } from './rgw-user-accounts-form/rgw-user-accounts-form.component'; import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details/rgw-user-accounts-details.component'; +import { RgwStorageClassDetailsComponent } from './rgw-storage-class-details/rgw-storage-class-details.component'; @NgModule({ imports: [ @@ -110,7 +113,8 @@ import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details/rgw InputModule, CheckboxModule, SelectModule, - NumberModule + NumberModule, + TabsModule ], exports: [ RgwDaemonListComponent, @@ -119,7 +123,8 @@ import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details/rgw RgwBucketListComponent, RgwBucketDetailsComponent, RgwUserListComponent, - RgwUserDetailsComponent + RgwUserDetailsComponent, + RgwStorageClassListComponent ], declarations: [ RgwDaemonListComponent, @@ -164,7 +169,9 @@ import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details/rgw RgwMultisiteTabsComponent, RgwUserAccountsComponent, RgwUserAccountsFormComponent, - RgwUserAccountsDetailsComponent + RgwUserAccountsDetailsComponent, + RgwStorageClassListComponent, + RgwStorageClassDetailsComponent ], providers: [TitleCasePipe] }) @@ -315,6 +322,11 @@ const routes: Routes = [ } ] }, + { + path: 'tiering', + data: { breadcrumbs: 'Tiering' }, + children: [{ path: '', component: RgwStorageClassListComponent }] + }, { path: 'nfs', canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 0bcb5278f917b..a544bbcdb436c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -215,6 +215,11 @@ i18n-title [useRouter]="true" class="tc_submenuitem tc_submenuitem_rgw_users">Users + Tiering