]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: RGW - Delete Storage Class 61734/head
authorDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Sun, 9 Feb 2025 12:32:11 +0000 (18:02 +0530)
committerDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Thu, 13 Feb 2025 05:08:54 +0000 (10:38 +0530)
Fixes: https://tracker.ceph.com/issues/69880
Signed-off-by: Dnyaneshwari Talwekar <dtalweka@redhat.com>
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-storage-class-list/rgw-storage-class-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-storage-class.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index d48542a7590387555b12441f0c5312e632c8f5e4..df7d0005658426c4ab185613c7e4704aad009935 100755 (executable)
@@ -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):
index 3aeebfc3c48d0d96a7e0b2a22bc46cc18c2f0faf..a0efeff62f2cfb4ae61b7f01a16f3c76c3dbee87 100644 (file)
@@ -8,6 +8,13 @@
   (setExpandedRow)="setExpandedRow($event)"
   (updateSelection)="updateSelection($event)"
 >
+  <div class="table-actions">
+    <cd-table-actions class="btn-group"
+                      [permission]="permission"
+                      [selection]="selection"
+                      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
   <cd-rgw-storage-class-details *cdTableDetail
                                 [selection]="expandedRow">
   </cd-rgw-storage-class-details>
index 24f472c911cc03b6a7b59aaeee3d7e76fcb560f8..5908ada97294206c4a5407ba51fe9ede4ae19d36 100644 (file)
@@ -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();
 
index b1d41077092b76043aea1618a7354f34dc44855b..5ea1dd1b59479c8897f99e2adc1ebc83d17ccc58 100644 (file)
@@ -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<void> {
@@ -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 (file)
index 0000000..dc8eef5
--- /dev/null
@@ -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 (file)
index 0000000..52d0f7c
--- /dev/null
@@ -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'
+    });
+  }
+}
index 60c62ec09d07b9e40e4dbb7cb805a8cef3dc8804..8bf5a9bc16cb22d6c4f9143f3c6a61374a75dd84 100644 (file)
@@ -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) => {
index 3f4e9876558273aa3dbd32775f47db573ee57eef..794e48572c876383f3f8b520e31682c158577c5f 100644 (file)
@@ -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:
index 9fa249acf444e030004ce2bdc4a1e0f52d7cb93c..19875f656526c75658a132d7be92c6908e702396 100755 (executable)
@@ -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 = '',