]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: create cephfs snapshot clone 55489/head
authorNizamudeen A <nia@redhat.com>
Tue, 23 Jan 2024 16:47:46 +0000 (22:17 +0530)
committerNizamudeen A <nia@redhat.com>
Thu, 8 Feb 2024 05:36:43 +0000 (11:06 +0530)
Fixes: https://tracker.ceph.com/issues/64175
Signed-off-by: Nizamudeen A <nia@redhat.com>
(cherry picked from commit 129f1db734777f3df551965c35f40ee0ab7d467a)

 Conflicts:
src/pybind/mgr/dashboard/controllers/cephfs.py
  - Left the snapshot scheduler controller out

src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/form-modal/form-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-form-modal-field-config.ts
src/pybind/mgr/dashboard/openapi.yaml

index cfa40b2dd7c0be3f2e0ac92119e51b17591e74e3..7dc8b4e9b570aa070540c89461c176904cd68da4 100644 (file)
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+import errno
 import json
 import logging
 import os
@@ -640,11 +641,17 @@ class CephFSSubvolume(RESTController):
                 params['sub_name'] = subvolume['name']
                 error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
                                                   params)
-                if error_code != 0:
+                # just ignore this error for now so the subvolumes page will load.
+                # the ideal solution is to implement a status page where clone status
+                # can be displayed
+                if error_code == -errno.EAGAIN:
+                    pass
+                elif error_code != 0:
                     raise DashboardException(
                         f'Failed to get info for subvolume {subvolume["name"]}: {err}'
                     )
-                subvolume['info'] = json.loads(out)
+                if out:
+                    subvolume['info'] = json.loads(out)
         return subvolumes
 
     @RESTController.Resource('GET')
@@ -804,11 +811,17 @@ class CephFSSubvolumeSnapshots(RESTController):
                 params['snap_name'] = snapshot['name']
                 error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_info',
                                                   None, params)
-                if error_code != 0:
+                # just ignore this error for now so the subvolumes page will load.
+                # the ideal solution is to implement a status page where clone status
+                # can be displayed
+                if error_code == -errno.EAGAIN:
+                    pass
+                elif error_code != 0:
                     raise DashboardException(
                         f'Failed to get info for subvolume snapshot {snapshot["name"]}: {err}'
                     )
-                snapshot['info'] = json.loads(out)
+                if out:
+                    snapshot['info'] = json.loads(out)
         return snapshots
 
     @RESTController.Resource('GET')
@@ -850,3 +863,26 @@ class CephFSSubvolumeSnapshots(RESTController):
                 f'Failed to delete subvolume snapshot {snap_name}: {err}'
             )
         return f'Subvolume snapshot {snap_name} removed successfully'
+
+
+@APIRouter('/cephfs/subvolume/snapshot/clone', Scope.CEPHFS)
+@APIDoc("Cephfs Snapshot Clone Management API", "CephfsSnapshotClone")
+class CephFsSnapshotClone(RESTController):
+    @EndpointDoc("Create a clone of a subvolume snapshot")
+    def create(self, vol_name: str, subvol_name: str, snap_name: str, clone_name: str,
+               group_name='', target_group_name=''):
+        params = {'vol_name': vol_name, 'sub_name': subvol_name, 'snap_name': snap_name,
+                  'target_sub_name': clone_name}
+        if group_name:
+            params['group_name'] = group_name
+
+        if target_group_name:
+            params['target_group_name'] = target_group_name
+
+        error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_clone', None,
+                                        params)
+        if error_code != 0:
+            raise DashboardException(
+                f'Failed to create clone {clone_name}: {err}'
+            )
+        return f'Clone {clone_name} created successfully'
index 4f9cf27db0ffd91143ed69a92ced4390db380663..92c139f8e5dd58cd812384dee8bdc0b4d23bde99 100644 (file)
@@ -245,7 +245,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh
       switchMap(() =>
         this.cephfsSubVolumeService.get(this.fsName, this.activeGroupName).pipe(
           catchError(() => {
-            this.context.error();
+            this.context?.error();
             return of(null);
           })
         )
index 798307a0cf9f44502b142323e4fd48d3fa191b14..3cb5b0bd47b5ef9cb2287146432cb1ff559b9bb4 100644 (file)
@@ -20,6 +20,12 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import moment from 'moment';
+import { Validators } from '@angular/forms';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
 
 @Component({
   selector: 'cd-cephfs-subvolume-snapshots-list',
@@ -59,7 +65,8 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
     private modalService: ModalService,
     private authStorageService: AuthStorageService,
     private cdDatePipe: CdDatePipe,
-    private taskWrapper: TaskWrapperService
+    private taskWrapper: TaskWrapperService,
+    private notificationService: NotificationService
   ) {
     this.permissions = this.authStorageService.getPermissions();
   }
@@ -99,9 +106,17 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
         click: () => this.openModal()
       },
       {
-        name: this.actionLabels.REMOVE,
+        name: this.actionLabels.CLONE,
+        permission: 'create',
+        icon: Icons.clone,
+        disable: () => !this.selection.hasSingleSelection,
+        click: () => this.cloneModal()
+      },
+      {
+        name: this.actionLabels.DELETE,
         permission: 'delete',
         icon: Icons.destroy,
+        disable: () => !this.selection.hasSingleSelection,
         click: () => this.deleteSnapshot()
       }
     ];
@@ -209,7 +224,7 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
     const subVolumeGroupName = this.activeGroupName;
     const fsName = this.fsName;
     this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
-      actionDescription: 'Remove',
+      actionDescription: 'Delete',
       itemNames: [snapshotName],
       itemDescription: 'Snapshot',
       submitAction: () =>
@@ -234,4 +249,63 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
           })
     });
   }
+
+  cloneModal() {
+    const cloneName = `clone_${moment().toISOString(true)}`;
+    const allGroups = Array.from(this.subvolumeGroupList).map((group) => {
+      return { value: group, text: group === '' ? '_nogroup' : group };
+    });
+    this.modalService.show(FormModalComponent, {
+      titleText: $localize`Create clone`,
+      fields: [
+        {
+          type: 'text',
+          name: 'cloneName',
+          value: cloneName,
+          label: $localize`Name`,
+          validators: [Validators.required, Validators.pattern(/^[.A-Za-z0-9_+:-]+$/)],
+          asyncValidators: [
+            CdValidators.unique(
+              this.cephfsSubvolumeService.exists,
+              this.cephfsSubvolumeService,
+              null,
+              null,
+              this.fsName
+            )
+          ],
+          required: true,
+          errors: {
+            pattern: $localize`Allowed characters are letters, numbers, '.', '-', '+', ':' or '_'`
+          }
+        },
+        {
+          type: 'select',
+          name: 'groupName',
+          value: this.activeGroupName,
+          label: $localize`Group Name`,
+          typeConfig: {
+            options: allGroups
+          }
+        }
+      ],
+      submitButtonText: $localize`Create Clone`,
+      onSubmit: (value: any) => {
+        this.cephfsSubvolumeService
+          .createSnapshotClone(
+            this.fsName,
+            this.activeSubVolumeName,
+            this.selection.first().name,
+            value.cloneName,
+            this.activeGroupName,
+            value.groupName
+          )
+          .subscribe(() =>
+            this.notificationService.show(
+              NotificationType.success,
+              $localize`Created Clone "${value.cloneName}" successfully.`
+            )
+          );
+      }
+    });
+  }
 }
index ad0ce248064db29faab549d3d29c1da3afd8115c..6a88fa1d52971824995e209f371df1a0708780fb 100644 (file)
@@ -171,4 +171,26 @@ export class CephfsSubvolumeService {
       observe: 'response'
     });
   }
+
+  createSnapshotClone(
+    fsName: string,
+    subVolumeName: string,
+    snapshotName: string,
+    cloneName: string,
+    groupName = '',
+    targetGroupName = ''
+  ) {
+    return this.http.post(
+      `${this.baseURL}/snapshot/clone`,
+      {
+        vol_name: fsName,
+        subvol_name: subVolumeName,
+        snap_name: snapshotName,
+        clone_name: cloneName,
+        group_name: groupName,
+        target_group_name: targetGroupName
+      },
+      { observe: 'response' }
+    );
+  }
 }
index 59b0d2a8560a13060ba3284905b45b8ec5b6ddd4..1b4af6cd69fc1dae0bf6e681a963d1035ff65b5c 100755 (executable)
@@ -1,5 +1,5 @@
 import { Component, OnInit } from '@angular/core';
-import { UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
+import { AsyncValidatorFn, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
 
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
@@ -47,18 +47,22 @@ export class FormModalComponent implements OnInit {
 
   private createFormControl(field: CdFormModalFieldConfig): UntypedFormControl {
     let validators: ValidatorFn[] = [];
+    let asyncValidators: AsyncValidatorFn[] = [];
     if (_.isBoolean(field.required) && field.required) {
       validators.push(Validators.required);
     }
     if (field.validators) {
       validators = validators.concat(field.validators);
     }
+    if (field.asyncValidators) {
+      asyncValidators = asyncValidators.concat(field.asyncValidators);
+    }
     return new UntypedFormControl(
       _.defaultTo(
         field.type === 'binary' ? this.dimlessBinaryPipe.transform(field.value) : field.value,
         null
       ),
-      { validators }
+      { validators, asyncValidators }
     );
   }
 
index e327be59a27a45722f8b28f85a14604273611994..a899e6daa6902fa1cec6811b1542461e27c07762 100644 (file)
@@ -1,4 +1,4 @@
-import { ValidatorFn } from '@angular/forms';
+import { AsyncValidatorFn, ValidatorFn } from '@angular/forms';
 
 export class CdFormModalFieldConfig {
   // --- Generic field properties ---
@@ -11,6 +11,7 @@ export class CdFormModalFieldConfig {
   value?: any;
   errors?: { [errorName: string]: string };
   validators: ValidatorFn[];
+  asyncValidators?: AsyncValidatorFn[];
 
   // --- Specific field properties ---
   typeConfig?: {
index 45af71681ef46fc7256bb858062b1cf0ea783125..7d7508f5d2b7871e51a3c67b167e3ac85d991ba7 100644 (file)
@@ -2005,6 +2005,59 @@ paths:
       - jwt: []
       tags:
       - CephfsSubvolumeSnapshot
+  /api/cephfs/subvolume/snapshot/clone:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                clone_name:
+                  type: string
+                group_name:
+                  default: ''
+                  type: string
+                snap_name:
+                  type: string
+                subvol_name:
+                  type: string
+                target_group_name:
+                  default: ''
+                  type: string
+                vol_name:
+                  type: string
+              required:
+              - vol_name
+              - subvol_name
+              - snap_name
+              - clone_name
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '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: []
+      summary: Create a clone of a subvolume snapshot
+      tags:
+      - CephfsSnapshotClone
   /api/cephfs/subvolume/snapshot/{vol_name}/{subvol_name}:
     delete:
       parameters:
@@ -12832,6 +12885,8 @@ tags:
   name: CephFSSubvolume
 - description: Cephfs Management API
   name: Cephfs
+- description: Cephfs Snapshot Clone Management API
+  name: CephfsSnapshotClone
 - description: Cephfs Subvolume Group Management API
   name: CephfsSubvolumeGroup
 - description: Cephfs Subvolume Snapshot Management API