diff --git a/docs/api-status.json b/docs/api-status.json index 22ef7f9..b4c2df7 100644 --- a/docs/api-status.json +++ b/docs/api-status.json @@ -1956,6 +1956,12 @@ "comment": "GetSnapGroupNamespace returns the SnapGroupNamespace of the snapshot which\nis part of a group. The caller should make sure that the snapshot ID passed\nin this function belongs to a snapshot that was taken as part of a group\nsnapshot.\n\nImplements:\n\n\t\tint rbd_snap_get_group_namespace(rbd_image_t image, uint64_t snap_id,\n\t rbd_snap_group_namespace_t *group_snap,\n\t size_t group_snap_size)\n", "added_in_version": "v0.27.0", "expected_stable_version": "v0.29.0" + }, + { + "name": "CloneImageByID", + "comment": "CloneImageByID creates a clone of the image from a snapshot with the given\nID in the provided io-context with the given name and image options.\n\nImplements:\n\n\tint rbd_clone4(rados_ioctx_t p_ioctx, const char *p_name,\n\t uint64_t p_snap_id, rados_ioctx_t c_ioctx,\n\t const char *c_name, rbd_image_options_t c_opts);\n", + "added_in_version": "$NEXT_RELEASE", + "expected_stable_version": "$NEXT_RELEASE_STABLE" } ] }, @@ -2259,4 +2265,4 @@ } ] } -} \ No newline at end of file +} diff --git a/docs/api-status.md b/docs/api-status.md index 39d64ff..0726f28 100644 --- a/docs/api-status.md +++ b/docs/api-status.md @@ -31,6 +31,7 @@ WriteOp.Exec | $NEXT_RELEASE | $NEXT_RELEASE_STABLE | Name | Added in Version | Expected Stable Version | ---- | ---------------- | ----------------------- | Image.GetSnapGroupNamespace | v0.27.0 | v0.29.0 | +CloneImageByID | $NEXT_RELEASE | $NEXT_RELEASE_STABLE | ### Deprecated APIs diff --git a/rbd/clone_image_by_id.go b/rbd/clone_image_by_id.go new file mode 100644 index 0000000..a73c99b --- /dev/null +++ b/rbd/clone_image_by_id.go @@ -0,0 +1,46 @@ +//go:build !(nautilus || octopus || pacific || quincy || reef) && ceph_preview + +package rbd + +// #cgo LDFLAGS: -lrbd +// #include +// #include +// #include +// #include +import "C" + +import ( + "unsafe" + + "github.com/ceph/go-ceph/rados" +) + +// CloneImageByID creates a clone of the image from a snapshot with the given +// ID in the provided io-context with the given name and image options. +// +// Implements: +// +// int rbd_clone4(rados_ioctx_t p_ioctx, const char *p_name, +// uint64_t p_snap_id, rados_ioctx_t c_ioctx, +// const char *c_name, rbd_image_options_t c_opts); +func CloneImageByID(ioctx *rados.IOContext, parentName string, snapID uint64, + destctx *rados.IOContext, name string, rio *ImageOptions) error { + + if rio == nil { + return rbdError(C.EINVAL) + } + + cParentName := C.CString(parentName) + defer C.free(unsafe.Pointer(cParentName)) + cCloneName := C.CString(name) + defer C.free(unsafe.Pointer(cCloneName)) + + ret := C.rbd_clone4( + cephIoctx(ioctx), + cParentName, + C.uint64_t(snapID), + cephIoctx(destctx), + cCloneName, + C.rbd_image_options_t(rio.options)) + return getError(ret) +} diff --git a/rbd/clone_image_by_id_test.go b/rbd/clone_image_by_id_test.go new file mode 100644 index 0000000..db944ec --- /dev/null +++ b/rbd/clone_image_by_id_test.go @@ -0,0 +1,154 @@ +//go:build !(nautilus || octopus || pacific || quincy || reef) && ceph_preview + +package rbd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCloneImageByID(t *testing.T) { + // tests are done as subtests to avoid creating pools, images, etc + // over and over again. + conn := radosConnect(t) + require.NotNil(t, conn) + defer conn.Shutdown() + + poolname := GetUUID() + err := conn.MakePool(poolname) + require.NoError(t, err) + defer conn.DeletePool(poolname) + + ioctx, err := conn.OpenIOContext(poolname) + require.NoError(t, err) + defer ioctx.Destroy() + + // create a group, some images, and add images to the group + gname := "snapme" + err = GroupCreate(ioctx, gname) + assert.NoError(t, err) + defer func() { + assert.NoError(t, GroupRemove(ioctx, gname)) + }() + + options := NewRbdImageOptions() + assert.NoError(t, + options.SetUint64(ImageOptionOrder, uint64(testImageOrder))) + defer options.Destroy() + + name1 := GetUUID() + err = CreateImage(ioctx, name1, testImageSize, options) + require.NoError(t, err) + defer func() { + assert.NoError(t, RemoveImage(ioctx, name1)) + }() + + name2 := GetUUID() + err = CreateImage(ioctx, name2, testImageSize, options) + require.NoError(t, err) + defer func() { + assert.NoError(t, RemoveImage(ioctx, name2)) + }() + + err = GroupImageAdd(ioctx, gname, ioctx, name1) + assert.NoError(t, err) + defer func() { + assert.NoError(t, GroupImageRemove(ioctx, gname, ioctx, name1)) + }() + + err = GroupImageAdd(ioctx, gname, ioctx, name2) + assert.NoError(t, err) + defer func() { + assert.NoError(t, GroupImageRemove(ioctx, gname, ioctx, name2)) + }() + + t.Run("CloneFromSnapshot", func(t *testing.T) { + cloneName := "child" + optionsClone := NewRbdImageOptions() + defer optionsClone.Destroy() + err := optionsClone.SetUint64(ImageOptionCloneFormat, 2) + assert.NoError(t, err) + + // Get the snapID + img, err := OpenImage(ioctx, name1, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + snapName := "mysnap" + snapshot, err := img.CreateSnapshot(snapName) + assert.NoError(t, err) + defer func() { + assert.NoError(t, snapshot.Remove()) + }() + + snapInfos, err := img.GetSnapshotNames() + assert.NoError(t, err) + require.Equal(t, 1, len(snapInfos)) + + snapID := snapInfos[0].Id + + // Create a clone of the image using the snapshot. + err = CloneImageByID(ioctx, name1, snapID, ioctx, cloneName, optionsClone) + assert.NoError(t, err) + defer func() { assert.NoError(t, RemoveImage(ioctx, cloneName)) }() + + imgNew, err := OpenImage(ioctx, cloneName, NoSnapshot) + defer func() { + assert.NoError(t, imgNew.Close()) + }() + assert.NoError(t, err) + + parentInfo, err := imgNew.GetParent() + assert.NoError(t, err) + assert.Equal(t, parentInfo.Image.ImageName, name1) + assert.Equal(t, parentInfo.Image.PoolName, poolname) + assert.False(t, parentInfo.Image.Trash) + assert.Equal(t, parentInfo.Snap.SnapName, snapName) + assert.Equal(t, parentInfo.Snap.ID, snapID) + }) + + t.Run("CloneFromGroupSnap", func(t *testing.T) { + err := GroupSnapCreate(ioctx, gname, "groupsnap") + assert.NoError(t, err) + + cloneName := "img-clone" + optionsClone := NewRbdImageOptions() + defer optionsClone.Destroy() + err = optionsClone.SetUint64(ImageOptionCloneFormat, 2) + assert.NoError(t, err) + + // Get the snapID of the image + img, err := OpenImageReadOnly(ioctx, name1, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + snapInfos, err := img.GetSnapshotNames() + assert.NoError(t, err) + require.Equal(t, 1, len(snapInfos)) + + snapID := snapInfos[0].Id + + // Create a clone of the image using the snapshot. + err = CloneImageByID(ioctx, name1, snapID, ioctx, cloneName, optionsClone) + assert.NoError(t, err) + defer func() { assert.NoError(t, RemoveImage(ioctx, cloneName)) }() + + imgNew, err := OpenImage(ioctx, cloneName, NoSnapshot) + defer func() { + assert.NoError(t, imgNew.Close()) + }() + assert.NoError(t, err) + + parentInfo, err := imgNew.GetParent() + assert.NoError(t, err) + assert.Equal(t, parentInfo.Image.ImageName, name1) + assert.Equal(t, parentInfo.Snap.ID, snapID) + assert.Equal(t, parentInfo.Image.PoolName, poolname) + assert.False(t, parentInfo.Image.Trash) + + err = GroupSnapRemove(ioctx, gname, "groupsnap") + assert.NoError(t, err) + }) +}