]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: subvolume rm with snapshots 53182/head
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Mon, 28 Aug 2023 13:03:06 +0000 (15:03 +0200)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Wed, 30 Aug 2023 06:30:13 +0000 (08:30 +0200)
Fixes: https://tracker.ceph.com/issues/62452
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
12 files changed:
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/cypress/e2e/common/forms-helper.feature.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/filesystems/subvolumes.e2e-spec.feature
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts
src/pybind/mgr/dashboard/openapi.yaml

index ea37930a07c928c148a420b46b45d804d9dcd89c..0475ad7d4afc49d5d24a1585277b3d2e3f3863e9 100644 (file)
@@ -14,7 +14,7 @@ from ..security import Scope
 from ..services.ceph_service import CephService
 from ..services.cephfs import CephFS as CephFS_
 from ..services.exception import handle_cephfs_error
-from ..tools import ViewCache
+from ..tools import ViewCache, str_to_bool
 from . import APIDoc, APIRouter, DeletePermission, Endpoint, EndpointDoc, \
     RESTController, UIRouter, UpdatePermission, allow_empty_body
 
@@ -722,14 +722,17 @@ class CephFSSubvolume(RESTController):
 
         return f'Subvolume {subvol_name} updated successfully'
 
-    def delete(self, vol_name: str, subvol_name: str):
+    def delete(self, vol_name: str, subvol_name: str, retain_snapshots: bool = False):
+        params = {'vol_name': vol_name, 'sub_name': subvol_name}
+        retain_snapshots = str_to_bool(retain_snapshots)
+        if retain_snapshots:
+            params['retain_snapshots'] = 'True'
         error_code, _, err = mgr.remote(
-            'volumes', '_cmd_fs_subvolume_rm', None, {
-                'vol_name': vol_name, 'sub_name': subvol_name})
+            'volumes', '_cmd_fs_subvolume_rm', None, params)
         if error_code != 0:
-            raise RuntimeError(
-                f'Failed to delete subvolume {subvol_name}: {err}'
-            )
+            raise DashboardException(
+                msg=f'Failed to remove subvolume {subvol_name}: {err}',
+                component='cephfs')
         return f'Subvolume {subvol_name} removed successfully'
 
 
index d541df36a0fd08f34d0f8518f04b47ce16faf537..2c14af863a9994fa03c75496394576a9e3d7eed4 100644 (file)
@@ -57,7 +57,7 @@ And('I click on submit button', () => {
  * by ticking the 'Are you sure?' box.
  */
 Then('I check the tick box in modal', () => {
-  cy.get('cd-modal .custom-control-label').click();
+  cy.get('cd-modal input#confirmation').click();
 });
 
 And('I confirm to {string}', (action: string) => {
index 792d1571d925ab01688dd6f583e3fc9f9dd9168f..ae968d4e9c1beb7f9ce104bdb748eff97efd5cc7 100644 (file)
@@ -38,7 +38,7 @@ Feature: CephFS Subvolume management
         When I select a row "test_subvolume" in the expanded row
         And I click on "Remove" button from the table actions in the expanded row
         And I check the tick box in modal
-        And I click on "Remove subvolume" button
+        And I click on "Remove Subvolume" button
         Then I should not see a row with "test_subvolume" in the expanded row
 
     Scenario: Remove CephFS Volume
index 94922c661e27c0161cf93f5debb6ea35772c2ee4..71160010a73f2953f1d614927a00b919382e9ab7 100644 (file)
             *ngIf="row.info.pool_namespace"
             [tooltipText]="row.info.pool_namespace"></cd-label>
 </ng-template>
+
+<ng-template #removeTmpl
+             let-form="form">
+  <ng-container [formGroup]="form">
+    <ng-container formGroupName="child">
+      <cd-alert-panel *ngIf="errorMessage.length > 1"
+                      type="error">
+              {{errorMessage}}
+      </cd-alert-panel>
+      <div class="form-group">
+        <div class="custom-control custom-checkbox">
+          <input type="checkbox"
+                 class="custom-control-input"
+                 name="retainSnapshots"
+                 id="retainSnapshots"
+                 formControlName="retainSnapshots">
+          <label class="custom-control-label"
+                 for="retainSnapshots"
+                 i18n>Retain snapshots <cd-helper>The subvolume can be removed retaining
+                  existing snapshots using this option.
+                   If snapshots are retained, the subvolume is considered empty for all
+                    operations not involving the retained snapshots.</cd-helper></label>
+        </div>
+      </div>
+    </ng-container>
+  </ng-container>
+</ng-template>
index b3e0b526fb1c800261aa93aa38dc7a062168b49b..de6e64956a25a7650adda373d488f9421283ba41 100644 (file)
@@ -5,6 +5,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { SharedModule } from '~/app/shared/shared.module';
 import { ToastrModule } from 'ngx-toastr';
 import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 
 describe('CephfsSubvolumeListComponent', () => {
   let component: CephfsSubvolumeListComponent;
@@ -13,7 +14,8 @@ describe('CephfsSubvolumeListComponent', () => {
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       declarations: [CephfsSubvolumeListComponent],
-      imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule]
+      imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule],
+      providers: [NgbActiveModal]
     }).compileComponents();
   });
 
index c9ff01e2ddd9a9b9a58263487838f91cfb4096de..047acda346af464d44e027b5c5a0247e738a494a 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { Observable, ReplaySubject, of } from 'rxjs';
 import { catchError, shareReplay, switchMap } from 'rxjs/operators';
 import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
@@ -14,16 +14,20 @@ import { ModalService } from '~/app/shared/services/modal.service';
 import { CephfsSubvolumeFormComponent } from '../cephfs-subvolume-form/cephfs-subvolume-form.component';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { Permissions } from '~/app/shared/models/permissions';
-import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { FinishedTask } from '~/app/shared/models/finished-task';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { FormControl } from '@angular/forms';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 
 @Component({
   selector: 'cd-cephfs-subvolume-list',
   templateUrl: './cephfs-subvolume-list.component.html',
   styleUrls: ['./cephfs-subvolume-list.component.scss']
 })
-export class CephfsSubvolumeListComponent implements OnInit, OnChanges {
+export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnChanges {
   @ViewChild('quotaUsageTpl', { static: true })
   quotaUsageTpl: any;
 
@@ -39,6 +43,9 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges {
   @ViewChild('quotaSizeTpl', { static: true })
   quotaSizeTpl: any;
 
+  @ViewChild('removeTmpl', { static: true })
+  removeTmpl: TemplateRef<any>;
+
   @Input() fsName: string;
   @Input() pools: any[];
 
@@ -46,8 +53,12 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges {
   tableActions: CdTableAction[];
   context: CdTableFetchDataContext;
   selection = new CdTableSelection();
+  removeForm: CdFormGroup;
   icons = Icons;
   permissions: Permissions;
+  modalRef: NgbModalRef;
+  errorMessage: string = '';
+  selectedName: string = '';
 
   subVolumes$: Observable<CephfsSubvolume[]>;
   subject = new ReplaySubject<CephfsSubvolume[]>();
@@ -59,6 +70,7 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges {
     private authStorageService: AuthStorageService,
     private taskWrapper: TaskWrapperService
   ) {
+    super();
     this.permissions = this.authStorageService.getPermissions();
   }
 
@@ -174,16 +186,34 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges {
   }
 
   removeSubVolumeModal() {
-    const name = this.selection.first().name;
-    this.modalService.show(CriticalConfirmationModalComponent, {
-      itemDescription: 'subvolume',
-      itemNames: [name],
-      actionDescription: 'remove',
-      submitActionObservable: () =>
-        this.taskWrapper.wrapTaskAroundCall({
-          task: new FinishedTask('cephfs/subvolume/remove', { subVolumeName: name }),
-          call: this.cephfsSubVolume.remove(this.fsName, name)
-        })
+    this.removeForm = new CdFormGroup({
+      retainSnapshots: new FormControl(false)
+    });
+    this.errorMessage = '';
+    this.selectedName = this.selection.first().name;
+    this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+      actionDescription: 'Remove',
+      itemNames: [this.selectedName],
+      itemDescription: 'Subvolume',
+      childFormGroup: this.removeForm,
+      childFormGroupTemplate: this.removeTmpl,
+      submitAction: () =>
+        this.taskWrapper
+          .wrapTaskAroundCall({
+            task: new FinishedTask('cephfs/subvolume/remove', { subVolumeName: this.selectedName }),
+            call: this.cephfsSubVolume.remove(
+              this.fsName,
+              this.selectedName,
+              this.removeForm.getValue('retainSnapshots')
+            )
+          })
+          .subscribe({
+            complete: () => this.modalRef.close(),
+            error: (error) => {
+              this.modalRef.componentInstance.stopLoadingSpinner();
+              this.errorMessage = error.error.detail;
+            }
+          })
     });
   }
 }
index e2d1db3e8f3a6457d8173cf1efcf25148c084796..d612c268433af88f831f2079ae2de05cd6a229f3 100644 (file)
@@ -35,7 +35,9 @@ describe('CephfsSubvolumeService', () => {
 
   it('should call remove', () => {
     service.remove('testFS', 'testSubvol').subscribe();
-    const req = httpTesting.expectOne('api/cephfs/subvolume/testFS?subvol_name=testSubvol');
+    const req = httpTesting.expectOne(
+      'api/cephfs/subvolume/testFS?subvol_name=testSubvol&retain_snapshots=false'
+    );
     expect(req.request.method).toBe('DELETE');
   });
 });
index 67c7bb346a2b112faf700757c9059ad813eb873a..de8a5730cb653e5cd6892117eeab9a88a4c72f83 100644 (file)
@@ -51,10 +51,11 @@ export class CephfsSubvolumeService {
     });
   }
 
-  remove(fsName: string, subVolumeName: string) {
+  remove(fsName: string, subVolumeName: string, retainSnapshots: boolean = false) {
     return this.http.delete(`${this.baseURL}/${fsName}`, {
       params: {
-        subvol_name: subVolumeName
+        subvol_name: subVolumeName,
+        retain_snapshots: retainSnapshots
       },
       observe: 'response'
     });
index 01c0e2ca5ccd4cc06d3d1f28ed602521ca673ab7..cc2eded0e3b8f79d9b0d9d36924c2306eea6c15d 100644 (file)
@@ -43,7 +43,7 @@
       </div>
       <div class="modal-footer">
         <cd-form-button-panel (submitActionEvent)="callSubmitAction()"
-                              (backActionEvent)="callBackAction()"
+                              (backActionEvent)="backAction ? callBackAction() : hideModal()"
                               [form]="deletionForm"
                               [submitText]="(actionDescription | titlecase) + ' ' + itemDescription"></cd-form-button-panel>
       </div>
index 17269efa699a8dd26a4e6dca890da5b88ce61819..01cc1fbc8d92d01995340e976e2661020ce001f6 100644 (file)
   <span data-toggle="tooltip"
         [title]="value"
         class="font-monospace">{{ value | path }}
-    <cd-copy-2-clipboard-button [source]="value"
+    <cd-copy-2-clipboard-button *ngIf="value"
+                                [source]="value"
                                 [byId]="false"
                                 [showIconOnly]="true">
     </cd-copy-2-clipboard-button>
index 1131b3fc7c066d77094473cdecdf22736d5dfcc1..4f75864bdb8e2edd8dbbd9090f3083092811d01c 100644 (file)
@@ -5,6 +5,7 @@ import { Pipe, PipeTransform } from '@angular/core';
 })
 export class PathPipe implements PipeTransform {
   transform(value: unknown): string {
+    if (!value) return '';
     const splittedPath = value.toString().split('/');
 
     if (splittedPath[0] === '') {
index e09aa6c3b1ce965860829ef62f11f76121e3c3aa..85297bae5549971d55f7e460c9e2dc759ccfe8d3 100644 (file)
@@ -1990,6 +1990,11 @@ paths:
         required: true
         schema:
           type: string
+      - default: false
+        in: query
+        name: retain_snapshots
+        schema:
+          type: boolean
       responses:
         '202':
           content: