From 1393a14427452d2b02d5c1b9b5ccd05eb4e23916 Mon Sep 17 00:00:00 2001 From: Dnyaneshwari Date: Sun, 9 Feb 2025 18:02:11 +0530 Subject: [PATCH] mgr/dashboard: RGW - Delete Storage Class Fixes: https://tracker.ceph.com/issues/69880 Signed-off-by: Dnyaneshwari Talwekar --- src/pybind/mgr/dashboard/controllers/rgw.py | 7 +++ .../rgw-storage-class-list.component.html | 7 +++ .../rgw-storage-class-list.component.spec.ts | 7 ++- .../rgw-storage-class-list.component.ts | 46 ++++++++++++++++++- .../api/rgw-storage-class.service.spec.ts | 32 +++++++++++++ .../shared/api/rgw-storage-class.service.ts | 16 +++++++ .../shared/services/task-message.service.ts | 8 ++++ src/pybind/mgr/dashboard/openapi.yaml | 37 +++++++++++++++ .../mgr/dashboard/services/rgw_client.py | 29 ++++++++++++ 9 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.ts diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index d48542a759038..df7d000565842 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -1182,6 +1182,13 @@ class RgwZonegroup(RESTController): result = multisite_instance.get_zonegroup(zonegroup_name) return result + @Endpoint('DELETE', path='storage-class') + @DeletePermission + def remove_storage_class(self, placement_id: str, storage_class: str): + multisite_instance = RgwMultisite() + result = multisite_instance.delete_placement_targets(placement_id, storage_class) + return result + @Endpoint() @ReadPermission def get_all_zonegroups_info(self): 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 3aeebfc3c48d0..a0efeff62f2cf 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,13 @@ (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)" > +
+ + +
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 index 24f472c911cc0..5908ada972942 100644 --- 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 @@ -2,6 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RgwStorageClassListComponent } from './rgw-storage-class-list.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { SharedModule } from '~/app/shared/shared.module'; +import { ToastrModule } from 'ngx-toastr'; +import { RouterTestingModule } from '@angular/router/testing'; describe('RgwStorageClassListComponent', () => { let component: RgwStorageClassListComponent; @@ -9,7 +13,8 @@ describe('RgwStorageClassListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule], + providers: [NgbActiveModal], declarations: [RgwStorageClassListComponent] }).compileComponents(); 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 b1d41077092b7..5ea1dd1b59479 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 @@ -13,6 +13,15 @@ import { Target, ZoneGroupDetails } from '../models/rgw-storage-class.model'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { RgwStorageClassService } from '~/app/shared/api/rgw-storage-class.service'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { Permission } from '~/app/shared/models/permissions'; @Component({ selector: 'cd-rgw-storage-class-list', @@ -22,11 +31,20 @@ import { export class RgwStorageClassListComponent extends ListWithDetails implements OnInit { columns: CdTableColumn[]; selection = new CdTableSelection(); + permission: Permission; tableActions: CdTableAction[]; storageClassList: StorageClass[] = []; - constructor(private rgwZonegroupService: RgwZonegroupService) { + constructor( + private rgwZonegroupService: RgwZonegroupService, + public actionLabels: ActionLabelsI18n, + private cdsModalService: ModalCdsService, + private taskWrapper: TaskWrapperService, + private authStorageService: AuthStorageService, + private rgwStorageClassService: RgwStorageClassService + ) { super(); + this.permission = this.authStorageService.getPermissions().rgw; } ngOnInit() { @@ -57,6 +75,14 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI flexGrow: 2 } ]; + this.tableActions = [ + { + name: this.actionLabels.REMOVE, + permission: 'delete', + icon: Icons.destroy, + click: () => this.removeStorageClassModal() + } + ]; } loadStorageClass(): Promise { @@ -96,6 +122,24 @@ export class RgwStorageClassListComponent extends ListWithDetails implements OnI }; } + removeStorageClassModal() { + const storage_class = this.selection.first().storage_class; + const placement_target = this.selection.first().placement_target; + this.cdsModalService.show(CriticalConfirmationModalComponent, { + itemDescription: $localize`Tiering Storage Class`, + itemNames: [storage_class], + actionDescription: 'remove', + submitActionObservable: () => + this.taskWrapper.wrapTaskAroundCall({ + task: new FinishedTask('rgw/zonegroup/storage-class', { + placement_target: placement_target, + storage_class: storage_class + }), + call: this.rgwStorageClassService.removeStorageClass(placement_target, storage_class) + }) + }); + } + updateSelection(selection: CdTableSelection) { this.selection = selection; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.spec.ts new file mode 100644 index 0000000000000..dc8eef51691c8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.spec.ts @@ -0,0 +1,32 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { RgwStorageClassService } from './rgw-storage-class.service'; +import { configureTestBed } from '~/testing/unit-test-helper'; + +describe('RgwStorageClassService', () => { + let service: RgwStorageClassService; + let httpTesting: HttpTestingController; + + configureTestBed({ + providers: [RgwStorageClassService], + imports: [HttpClientTestingModule] + }); + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RgwStorageClassService); + httpTesting = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call remove', () => { + service.removeStorageClass('default-placement', 'Cloud8ibm').subscribe(); + const req = httpTesting.expectOne( + 'api/rgw/zonegroup/storage-class/default-placement/Cloud8ibm' + ); + expect(req.request.method).toBe('DELETE'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.ts new file mode 100644 index 0000000000000..52d0f7c9326f2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.ts @@ -0,0 +1,16 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +@Injectable({ + providedIn: 'root' +}) +export class RgwStorageClassService { + private url = 'api/rgw/zonegroup'; + + constructor(private http: HttpClient) {} + + removeStorageClass(placement_target: string, storage_class: string) { + return this.http.delete(`${this.url}/storage-class/${placement_target}/${storage_class}`, { + observe: 'response' + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 60c62ec09d07b..8bf5a9bc16cb2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -353,6 +353,10 @@ export class TaskMessageService { }`; } ), + // storage-class + 'rgw/zonegroup/storage-class': this.newTaskMessage(this.commonOperations.remove, (metadata) => + this.rgwStorageClass(metadata) + ), // iSCSI target tasks 'iscsi/target/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.iscsiTarget(metadata) @@ -558,6 +562,10 @@ export class TaskMessageService { return $localize`service '${metadata.service_name}'`; } + rgwStorageClass(metadata: any) { + return $localize`Tiering Storage Class '${metadata.storage_class}'`; + } + crudMessage(metadata: any) { let message = metadata.__message; _.forEach(metadata, (value, key) => { diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 3f4e987655827..794e48572c876 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -13815,6 +13815,43 @@ paths: - jwt: [] tags: - RgwZonegroup + /api/rgw/zonegroup/storage-class/{placement_id}/{storage_class}: + delete: + parameters: + - in: path + name: placement_id + required: true + schema: + type: string + - in: path + name: storage_class + required: true + schema: + type: string + responses: + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '204': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource deleted. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZonegroup /api/rgw/zonegroup/{zonegroup_name}: delete: parameters: diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 9fa249acf444e..19875f656526c 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -1675,6 +1675,15 @@ class RgwMultisite: raise DashboardException(error, http_status_code=500, component='rgw') return out + # If realm list is empty restart RGW daemons else update the period. + def handle_rgw_realm(self): + rgw_realm_list = self.list_realms() + if len(rgw_realm_list['realms']) < 1: + rgw_service_manager = RgwServiceManager() + rgw_service_manager.restart_rgw_daemons_and_set_credentials() + else: + self.update_period() + def add_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]): rgw_add_placement_cmd = ['zonegroup', 'placement', 'add'] for placement_target in placement_targets: @@ -1741,6 +1750,26 @@ class RgwMultisite: raise DashboardException(error, http_status_code=500, component='rgw') self.update_period() + def delete_placement_targets(self, placement_id: str, storage_class: str): + rgw_zonegroup_delete_cmd = ['zonegroup', 'placement', 'rm', + '--placement-id', placement_id, + '--storage-class', storage_class] + + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_delete_cmd) + if exit_code > 0: + raise DashboardException( + e=err, + msg=(f'Unable to delete placement {placement_id} ' + f'with storage-class {storage_class}'), + http_status_code=500, + component='rgw' + ) + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + self.handle_rgw_realm() + # pylint: disable=W0102 def edit_zonegroup(self, realm_name: str, zonegroup_name: str, new_zonegroup_name: str, default: str = '', master: str = '', endpoints: str = '', -- 2.39.5