]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: CephFS add groups in subvolume tab 53394/head
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Thu, 31 Aug 2023 23:54:28 +0000 (01:54 +0200)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Mon, 11 Sep 2023 15:17:10 +0000 (17:17 +0200)
Adds subvolume groups into the subvolume tabs in order to select the subvolumes from the appropiate group.
Also adds the capabilities to manage the subvolume groups of the subvolume in the different actions, create, edit, remove.

Fixes: https://tracker.ceph.com/issues/62675
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
(cherry picked from commit 041bc0c362bf4109416ecc12bc44aa7496ebb9d1)

src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts
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.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/openapi.yaml

index e879649adcdd34f946d6b538d5c0df5f65109939..09b2bebfc1dfea6044042d82fe7e08d5c5e1447b 100644 (file)
@@ -623,17 +623,21 @@ class CephFsUi(CephFS):
 @APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume')
 class CephFSSubvolume(RESTController):
 
-    def get(self, vol_name: str):
+    def get(self, vol_name: str, group_name: str = ""):
+        params = {'vol_name': vol_name}
+        if group_name:
+            params['group_name'] = group_name
         error_code, out, err = mgr.remote(
-            'volumes', '_cmd_fs_subvolume_ls', None, {'vol_name': vol_name})
+            'volumes', '_cmd_fs_subvolume_ls', None, params)
         if error_code != 0:
             raise DashboardException(
                 f'Failed to list subvolumes for volume {vol_name}: {err}'
             )
         subvolumes = json.loads(out)
         for subvolume in subvolumes:
-            error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, {
-                                              'vol_name': vol_name, 'sub_name': subvolume['name']})
+            params['sub_name'] = subvolume['name']
+            error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
+                                              params)
             if error_code != 0:
                 raise DashboardException(
                     f'Failed to get info for subvolume {subvolume["name"]}: {err}'
@@ -642,9 +646,12 @@ class CephFSSubvolume(RESTController):
         return subvolumes
 
     @RESTController.Resource('GET')
-    def info(self, vol_name: str, subvol_name: str):
-        error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, {
-            'vol_name': vol_name, 'sub_name': subvol_name})
+    def info(self, vol_name: str, subvol_name: str, group_name: str = ""):
+        params = {'vol_name': vol_name, 'sub_name': subvol_name}
+        if group_name:
+            params['group_name'] = group_name
+        error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
+                                          params)
         if error_code != 0:
             raise DashboardException(
                 f'Failed to get info for subvolume {subvol_name}: {err}'
@@ -661,10 +668,14 @@ class CephFSSubvolume(RESTController):
 
         return f'Subvolume {subvol_name} created successfully'
 
-    def set(self, vol_name: str, subvol_name: str, size: str):
+    def set(self, vol_name: str, subvol_name: str, size: str, group_name: str = ""):
+        params = {'vol_name': vol_name, 'sub_name': subvol_name}
         if size:
-            error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_resize', None, {
-                'vol_name': vol_name, 'sub_name': subvol_name, 'new_size': size})
+            params['new_size'] = size
+            if group_name:
+                params['group_name'] = group_name
+            error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_resize', None,
+                                            params)
             if error_code != 0:
                 raise DashboardException(
                     f'Failed to update subvolume {subvol_name}: {err}'
@@ -672,8 +683,11 @@ class CephFSSubvolume(RESTController):
 
         return f'Subvolume {subvol_name} updated successfully'
 
-    def delete(self, vol_name: str, subvol_name: str, retain_snapshots: bool = False):
+    def delete(self, vol_name: str, subvol_name: str, group_name: str = "",
+               retain_snapshots: bool = False):
         params = {'vol_name': vol_name, 'sub_name': subvol_name}
+        if group_name:
+            params['group_name'] = group_name
         retain_snapshots = str_to_bool(retain_snapshots)
         if retain_snapshots:
             params['retain_snapshots'] = 'True'
index 772cc904da4b02ba6aa2724a51180c8466157ebc..ad6f414486d6d5f0d52c34ef093124f2235d89ec 100644 (file)
           </div>
         </div>
 
+          <!--Subvolume Group name -->
+          <div class="form-group row">
+            <label class="cd-col-form-label"
+                   for="subvolumeGroupName"
+                   i18n>Subvolume group
+            </label>
+            <div class="cd-col-form-input">
+              <select class="form-select"
+                      id="subvolumeGroupName"
+                      name="subvolumeGroupName"
+                      formControlName="subvolumeGroupName"
+                      *ngIf="subVolumeGroups$ | async as subvolumeGroups">
+                <option value=""
+                        i18n>Default</option>
+                <option *ngFor="let subvolumegroup of subvolumeGroups"
+                        [value]="subvolumegroup.name">{{ subvolumegroup.name }}</option>
+              </select>
+            </div>
+          </div>
+
         <!-- Size -->
         <div class="form-group row">
           <label class="cd-col-form-label"
index 5ffb29bdce80da76f913a1e4287d3308a01afbd9..b85c268326a5996d0ff020ba5b4f72c324d1db0d 100644 (file)
@@ -9,12 +9,14 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { Pool } from '../../pool/pool';
 import { FormatterService } from '~/app/shared/services/formatter.service';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
-import _ from 'lodash';
 import { CdValidators } from '~/app/shared/forms/cd-validators';
 import { CephfsSubvolumeInfo } from '~/app/shared/models/cephfs-subvolume.model';
 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
 import { OctalToHumanReadablePipe } from '~/app/shared/pipes/octal-to-human-readable.pipe';
 import { CdForm } from '~/app/shared/forms/cd-form';
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model';
+import { Observable } from 'rxjs';
 
 @Component({
   selector: 'cd-cephfs-subvolume-form',
@@ -24,6 +26,7 @@ import { CdForm } from '~/app/shared/forms/cd-form';
 export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
   fsName: string;
   subVolumeName: string;
+  subVolumeGroupName: string;
   pools: Pool[];
   isEdit = false;
 
@@ -32,6 +35,8 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
   action: string;
   resource: string;
 
+  subVolumeGroups$: Observable<CephfsSubvolumeGroup[]>;
+  subVolumeGroups: CephfsSubvolumeGroup[];
   dataPools: Pool[];
 
   columns: CdTableColumn[];
@@ -48,6 +53,7 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
     private actionLabels: ActionLabelsI18n,
     private taskWrapper: TaskWrapperService,
     private cephFsSubvolumeService: CephfsSubvolumeService,
+    private cephFsSubvolumeGroupService: CephfsSubvolumeGroupService,
     private formatter: FormatterService,
     private dimlessBinary: DimlessBinaryPipe,
     private octalToHumanReadable: OctalToHumanReadablePipe
@@ -84,6 +90,7 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
       }
     ];
 
+    this.subVolumeGroups$ = this.cephFsSubvolumeGroupService.get(this.fsName);
     this.dataPools = this.pools.filter((pool) => pool.type === 'data');
     this.createForm();
 
@@ -105,6 +112,7 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
           )
         ]
       }),
+      subvolumeGroupName: new FormControl(this.subVolumeGroupName),
       pool: new FormControl(this.dataPools[0]?.pool, {
         validators: [Validators.required]
       }),
@@ -121,16 +129,18 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
   populateForm() {
     this.action = this.actionLabels.EDIT;
     this.cephFsSubvolumeService
-      .info(this.fsName, this.subVolumeName)
+      .info(this.fsName, this.subVolumeName, this.subVolumeGroupName)
       .subscribe((resp: CephfsSubvolumeInfo) => {
         // Disabled these fields since its not editable
         this.subvolumeForm.get('subvolumeName').disable();
+        this.subvolumeForm.get('subvolumeGroupName').disable();
         this.subvolumeForm.get('pool').disable();
         this.subvolumeForm.get('uid').disable();
         this.subvolumeForm.get('gid').disable();
 
         this.subvolumeForm.get('isolatedNamespace').disable();
         this.subvolumeForm.get('subvolumeName').setValue(this.subVolumeName);
+        this.subvolumeForm.get('subvolumeGroupName').setValue(this.subVolumeGroupName);
         if (resp.bytes_quota !== 'infinite') {
           this.subvolumeForm.get('size').setValue(this.dimlessBinary.transform(resp.bytes_quota));
         }
@@ -145,6 +155,7 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
 
   submit() {
     const subVolumeName = this.subvolumeForm.getValue('subvolumeName');
+    const subVolumeGroupName = this.subvolumeForm.getValue('subvolumeGroupName');
     const pool = this.subvolumeForm.getValue('pool');
     const size = this.formatter.toBytes(this.subvolumeForm.getValue('size')) || 0;
     const uid = this.subvolumeForm.getValue('uid');
@@ -159,7 +170,12 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
           task: new FinishedTask('cephfs/subvolume/' + URLVerbs.EDIT, {
             subVolumeName: subVolumeName
           }),
-          call: this.cephFsSubvolumeService.update(this.fsName, subVolumeName, String(editSize))
+          call: this.cephFsSubvolumeService.update(
+            this.fsName,
+            subVolumeName,
+            String(editSize),
+            subVolumeGroupName
+          )
         })
         .subscribe({
           error: () => {
@@ -178,6 +194,7 @@ export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
           call: this.cephFsSubvolumeService.create(
             this.fsName,
             subVolumeName,
+            subVolumeGroupName,
             pool,
             String(size),
             uid,
index 71160010a73f2953f1d614927a00b919382e9ab7..29731bbbd1b06a9797b9aa9570e6fb4c653d0d24 100644 (file)
@@ -1,22 +1,42 @@
-<ng-container *ngIf="subVolumes$ | async as subVolumes">
-  <cd-table [data]="subVolumes"
-            columnMode="flex"
-            [columns]="columns"
-            selectionType="single"
-            [hasDetails]="false"
-            (fetchData)="fetchData()"
-            (updateSelection)="updateSelection($event)">
+<div class="row">
+  <div class="col-sm-1">
+    <h3 i18n>Groups</h3>
+    <ng-container *ngIf="subVolumeGroups$ | async as subVolumeGroups">
+      <ul class="nav flex-column nav-pills">
+        <li class="nav-item">
+          <a class="nav-link"
+             [class.active]="!activeGroupName"
+             (click)="selectSubVolumeGroup()">Default</a>
+        </li>
+        <li class="nav-item"
+            *ngFor="let subVolumeGroup of subVolumeGroups">
+          <a class="nav-link text-decoration-none text-break"
+             [class.active]="subVolumeGroup.name === activeGroupName"
+             (click)="selectSubVolumeGroup(subVolumeGroup.name)">{{subVolumeGroup.name}}</a>
+        </li>
+      </ul>
+    </ng-container>
+  </div>
+  <div class="col-11 vertical-line">
+    <cd-table [data]="subVolumes$ | async"
+              columnMode="flex"
+              [columns]="columns"
+              selectionType="single"
+              [hasDetails]="false"
+              (fetchData)="fetchData()"
+              (updateSelection)="updateSelection($event)">
 
-    <div class="table-actions btn-toolbar">
-      <cd-table-actions [permission]="permissions.cephfs"
-                        [selection]="selection"
-                        class="btn-group"
-                        id="cephfs-subvolume-actions"
-                        [tableActions]="tableActions">
-      </cd-table-actions>
-    </div>
-  </cd-table>
-</ng-container>
+      <div class="table-actions btn-toolbar">
+        <cd-table-actions [permission]="permissions.cephfs"
+                          [selection]="selection"
+                          class="btn-group"
+                          id="cephfs-subvolume-actions"
+                          [tableActions]="tableActions">
+        </cd-table-actions>
+      </div>
+    </cd-table>
+  </div>
+</div>
 
 <ng-template #quotaUsageTpl
              let-row="row">
index 047acda346af464d44e027b5c5a0247e738a494a..3f679d27b96316a6ad6c106d63a049ee288c7ec8 100644 (file)
@@ -21,6 +21,8 @@ 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';
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model';
 
 @Component({
   selector: 'cd-cephfs-subvolume-list',
@@ -61,14 +63,19 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
   selectedName: string = '';
 
   subVolumes$: Observable<CephfsSubvolume[]>;
+  subVolumeGroups$: Observable<CephfsSubvolumeGroup[]>;
   subject = new ReplaySubject<CephfsSubvolume[]>();
+  groupsSubject = new ReplaySubject<CephfsSubvolume[]>();
+
+  activeGroupName: string = '';
 
   constructor(
     private cephfsSubVolume: CephfsSubvolumeService,
     private actionLabels: ActionLabelsI18n,
     private modalService: ModalService,
     private authStorageService: AuthStorageService,
-    private taskWrapper: TaskWrapperService
+    private taskWrapper: TaskWrapperService,
+    private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService
   ) {
     super();
     this.permissions = this.authStorageService.getPermissions();
@@ -123,15 +130,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
         name: this.actionLabels.CREATE,
         permission: 'create',
         icon: Icons.add,
-        click: () =>
-          this.modalService.show(
-            CephfsSubvolumeFormComponent,
-            {
-              fsName: this.fsName,
-              pools: this.pools
-            },
-            { size: 'lg' }
-          )
+        click: () => this.openModal()
       },
       {
         name: this.actionLabels.EDIT,
@@ -147,16 +146,17 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
       }
     ];
 
-    this.subVolumes$ = this.subject.pipe(
+    this.getSubVolumes();
+
+    this.subVolumeGroups$ = this.groupsSubject.pipe(
       switchMap(() =>
-        this.cephfsSubVolume.get(this.fsName).pipe(
+        this.cephfsSubvolumeGroupService.get(this.fsName).pipe(
           catchError(() => {
             this.context.error();
             return of(null);
           })
         )
-      ),
-      shareReplay(1)
+      )
     );
   }
 
@@ -166,6 +166,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
 
   ngOnChanges() {
     this.subject.next();
+    this.groupsSubject.next();
   }
 
   updateSelection(selection: CdTableSelection) {
@@ -178,6 +179,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
       {
         fsName: this.fsName,
         subVolumeName: this.selection?.first()?.name,
+        subVolumeGroupName: this.activeGroupName,
         pools: this.pools,
         isEdit: edit
       },
@@ -204,6 +206,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
             call: this.cephfsSubVolume.remove(
               this.fsName,
               this.selectedName,
+              this.activeGroupName,
               this.removeForm.getValue('retainSnapshots')
             )
           })
@@ -216,4 +219,23 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
           })
     });
   }
+
+  selectSubVolumeGroup(subVolumeGroupName: string) {
+    this.activeGroupName = subVolumeGroupName;
+    this.getSubVolumes(subVolumeGroupName);
+  }
+
+  getSubVolumes(subVolumeGroupName = '') {
+    this.subVolumes$ = this.subject.pipe(
+      switchMap(() =>
+        this.cephfsSubVolume.get(this.fsName, subVolumeGroupName).pipe(
+          catchError(() => {
+            this.context.error();
+            return of(null);
+          })
+        )
+      ),
+      shareReplay(1)
+    );
+  }
 }
index d612c268433af88f831f2079ae2de05cd6a229f3..e40e9a52f3f3fbc532ce861bc4cafd0be7fd8a89 100644 (file)
@@ -29,14 +29,14 @@ describe('CephfsSubvolumeService', () => {
 
   it('should call get', () => {
     service.get('testFS').subscribe();
-    const req = httpTesting.expectOne('api/cephfs/subvolume/testFS');
+    const req = httpTesting.expectOne('api/cephfs/subvolume/testFS?group_name=');
     expect(req.request.method).toBe('GET');
   });
 
   it('should call remove', () => {
     service.remove('testFS', 'testSubvol').subscribe();
     const req = httpTesting.expectOne(
-      'api/cephfs/subvolume/testFS?subvol_name=testSubvol&retain_snapshots=false'
+      'api/cephfs/subvolume/testFS?subvol_name=testSubvol&group_name=&retain_snapshots=false'
     );
     expect(req.request.method).toBe('DELETE');
   });
index de8a5730cb653e5cd6892117eeab9a88a4c72f83..4c167725007eac16f893a114933e38b29c6a5c2e 100644 (file)
@@ -13,13 +13,18 @@ export class CephfsSubvolumeService {
 
   constructor(private http: HttpClient) {}
 
-  get(fsName: string): Observable<CephfsSubvolume[]> {
-    return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`);
+  get(fsName: string, subVolumeGroupName: string = ''): Observable<CephfsSubvolume[]> {
+    return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`, {
+      params: {
+        group_name: subVolumeGroupName
+      }
+    });
   }
 
   create(
     fsName: string,
     subVolumeName: string,
+    subVolumeGroupName: string,
     poolName: string,
     size: string,
     uid: number,
@@ -32,6 +37,7 @@ export class CephfsSubvolumeService {
       {
         vol_name: fsName,
         subvol_name: subVolumeName,
+        group_name: subVolumeGroupName,
         pool_layout: poolName,
         size: size,
         uid: uid,
@@ -43,18 +49,25 @@ export class CephfsSubvolumeService {
     );
   }
 
-  info(fsName: string, subVolumeName: string) {
+  info(fsName: string, subVolumeName: string, subVolumeGroupName: string = '') {
     return this.http.get(`${this.baseURL}/${fsName}/info`, {
       params: {
-        subvol_name: subVolumeName
+        subvol_name: subVolumeName,
+        group_name: subVolumeGroupName
       }
     });
   }
 
-  remove(fsName: string, subVolumeName: string, retainSnapshots: boolean = false) {
+  remove(
+    fsName: string,
+    subVolumeName: string,
+    subVolumeGroupName: string = '',
+    retainSnapshots: boolean = false
+  ) {
     return this.http.delete(`${this.baseURL}/${fsName}`, {
       params: {
         subvol_name: subVolumeName,
+        group_name: subVolumeGroupName,
         retain_snapshots: retainSnapshots
       },
       observe: 'response'
@@ -73,10 +86,11 @@ export class CephfsSubvolumeService {
     );
   }
 
-  update(fsName: string, subVolumeName: string, size: string) {
+  update(fsName: string, subVolumeName: string, size: string, subVolumeGroupName: string = '') {
     return this.http.put(`${this.baseURL}/${fsName}`, {
       subvol_name: subVolumeName,
-      size: size
+      size: size,
+      group_name: subVolumeGroupName
     });
   }
 }
index 08c8b9686dbb96d29810b5bff665d92da56f40ad..3cb746673041df87e824bf428fba6abe57be2436 100644 (file)
@@ -1967,6 +1967,11 @@ paths:
         required: true
         schema:
           type: string
+      - default: ''
+        in: query
+        name: group_name
+        schema:
+          type: string
       - default: false
         in: query
         name: retain_snapshots
@@ -2003,6 +2008,11 @@ paths:
         required: true
         schema:
           type: string
+      - default: ''
+        in: query
+        name: group_name
+        schema:
+          type: string
       responses:
         '200':
           content:
@@ -2034,6 +2044,9 @@ paths:
           application/json:
             schema:
               properties:
+                group_name:
+                  default: ''
+                  type: string
                 size:
                   type: integer
                 subvol_name:
@@ -2079,6 +2092,11 @@ paths:
         required: true
         schema:
           type: string
+      - default: ''
+        in: query
+        name: group_name
+        schema:
+          type: string
       responses:
         '200':
           content: