# -*- coding: utf-8 -*-
+import errno
import json
import logging
import os
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')
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')
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'
switchMap(() =>
this.cephfsSubVolumeService.get(this.fsName, this.activeGroupName).pipe(
catchError(() => {
- this.context.error();
+ this.context?.error();
return of(null);
})
)
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',
private modalService: ModalService,
private authStorageService: AuthStorageService,
private cdDatePipe: CdDatePipe,
- private taskWrapper: TaskWrapperService
+ private taskWrapper: TaskWrapperService,
+ private notificationService: NotificationService
) {
this.permissions = this.authStorageService.getPermissions();
}
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()
}
];
const subVolumeGroupName = this.activeGroupName;
const fsName = this.fsName;
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
- actionDescription: 'Remove',
+ actionDescription: 'Delete',
itemNames: [snapshotName],
itemDescription: 'Snapshot',
submitAction: () =>
})
});
}
+
+ 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.`
+ )
+ );
+ }
+ });
+ }
}
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' }
+ );
+ }
}
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';
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 }
);
}
-import { ValidatorFn } from '@angular/forms';
+import { AsyncValidatorFn, ValidatorFn } from '@angular/forms';
export class CdFormModalFieldConfig {
// --- Generic field properties ---
value?: any;
errors?: { [errorName: string]: string };
validators: ValidatorFn[];
+ asyncValidators?: AsyncValidatorFn[];
// --- Specific field properties ---
typeConfig?: {
- 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:
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