Repository: ceph/go-ceph
Pull request #456 (2021-02-01 19:56:47)

rbd: add functions to support rbd group snapshots
Functions to implement rbd group snapshots:

* Add GroupSnapCreate implementing rbd_group_snap_create
* Add GroupSnapRemove implementing rbd_group_snap_remove
* Add GroupSnapRename implementing rbd_group_snap_rename
* Add GroupSnapList implementing rbd_group_snap_list
* Add GroupSnapRollback implementing rbd_group_snap_rollback
* Add GroupSnapRollbackWithProgress implementing rbd_group_snap_rollback_with_progress

Comes with tests.

Fixes: #418 

Based on #447 - please review and merge that first.


## Checklist
- [x] Added tests for features and functional changes
- [x] Public functions and types are documented
- [x] Standard formatting is applied to Go code


Patch:
diff --git a/rbd/group_snap.go b/rbd/group_snap.go
new file mode 100644
index 0000000..fbd3b3f
--- /dev/null
+++ b/rbd/group_snap.go
@@ -0,0 +1,223 @@
+package rbd
+
+/*
+#cgo LDFLAGS: -lrbd
+#include <errno.h>
+#include <stdlib.h>
+#include <rbd/librbd.h>
+
+extern int snapRollbackCallback(uint64_t, uint64_t, uintptr_t);
+
+// inline wrapper to cast uintptr_t to void*
+static inline int wrap_rbd_group_snap_rollback_with_progress(
+		rados_ioctx_t group_p, const char *group_name,
+		const char *snap_name, uintptr_t arg) {
+	return rbd_group_snap_rollback_with_progress(
+		group_p, group_name, snap_name, (librbd_progress_fn_t)snapRollbackCallback, (void*)arg);
+};
+*/
+import "C"
+
+import (
+	"unsafe"
+
+	"github.com/ceph/go-ceph/internal/callbacks"
+	"github.com/ceph/go-ceph/internal/retry"
+	"github.com/ceph/go-ceph/rados"
+)
+
+// GroupSnapCreate will create a group snapshot.
+//
+// Implements:
+//  int rbd_group_snap_create(rados_ioctx_t group_p,
+//                            const char *group_name,
+//                            const char *snap_name);
+func GroupSnapCreate(ioctx *rados.IOContext, group, snap string) error {
+	cGroupName := C.CString(group)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cSnapName := C.CString(snap)
+	defer C.free(unsafe.Pointer(cSnapName))
+
+	ret := C.rbd_group_snap_create(cephIoctx(ioctx), cGroupName, cSnapName)
+	return getError(ret)
+}
+
+// GroupSnapRemove removes an existing group snapshot.
+//
+// Implements:
+//  int rbd_group_snap_remove(rados_ioctx_t group_p,
+//                            const char *group_name,
+//                            const char *snap_name);
+func GroupSnapRemove(ioctx *rados.IOContext, group, snap string) error {
+	cGroupName := C.CString(group)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cSnapName := C.CString(snap)
+	defer C.free(unsafe.Pointer(cSnapName))
+
+	ret := C.rbd_group_snap_remove(cephIoctx(ioctx), cGroupName, cSnapName)
+	return getError(ret)
+}
+
+// GroupSnapRename will rename an existing group snapshot.
+//
+// Implements:
+//  int rbd_group_snap_rename(rados_ioctx_t group_p,
+//                            const char *group_name,
+//                            const char *old_snap_name,
+//                            const char *new_snap_name);
+func GroupSnapRename(ioctx *rados.IOContext, group, src, dest string) error {
+	cGroupName := C.CString(group)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cOldSnapName := C.CString(src)
+	defer C.free(unsafe.Pointer(cOldSnapName))
+	cNewSnapName := C.CString(dest)
+	defer C.free(unsafe.Pointer(cNewSnapName))
+
+	ret := C.rbd_group_snap_rename(
+		cephIoctx(ioctx), cGroupName, cOldSnapName, cNewSnapName)
+	return getError(ret)
+}
+
+// GroupSnapState represents the state of a group snapshot in GroupSnapInfo.
+type GroupSnapState int
+
+const (
+	// GroupSnapStateIncomplete is equivalent to RBD_GROUP_SNAP_STATE_INCOMPLETE.
+	GroupSnapStateIncomplete = GroupSnapState(C.RBD_GROUP_SNAP_STATE_INCOMPLETE)
+	// GroupSnapStateComplete is equivalent to RBD_GROUP_SNAP_STATE_COMPLETE.
+	GroupSnapStateComplete = GroupSnapState(C.RBD_GROUP_SNAP_STATE_COMPLETE)
+)
+
+// GroupSnapInfo values are returned by GroupSnapList, representing the
+// snapshots that are part of an rbd group.
+type GroupSnapInfo struct {
+	Name  string
+	State GroupSnapState
+}
+
+// GroupSnapList returns a slice of snapshots in a group.
+//
+// Implements:
+//  int rbd_group_snap_list(rados_ioctx_t group_p,
+//                          const char *group_name,
+//                          rbd_group_snap_info_t *snaps,
+//                          size_t group_snap_info_size,
+//                          size_t *num_entries);
+func GroupSnapList(ioctx *rados.IOContext, group string) ([]GroupSnapInfo, error) {
+	cGroupName := C.CString(group)
+	defer C.free(unsafe.Pointer(cGroupName))
+
+	var (
+		cSnaps []C.rbd_group_snap_info_t
+		cSize  C.size_t
+		err    error
+	)
+	retry.WithSizes(1024, 262144, func(size int) retry.Hint {
+		cSize = C.size_t(size)
+		cSnaps = make([]C.rbd_group_snap_info_t, cSize)
+		ret := C.rbd_group_snap_list(
+			cephIoctx(ioctx),
+			cGroupName,
+			(*C.rbd_group_snap_info_t)(unsafe.Pointer(&cSnaps[0])),
+			C.sizeof_rbd_group_snap_info_t,
+			&cSize)
+		err = getErrorIfNegative(ret)
+		return retry.Size(int(cSize)).If(err == errRange)
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	snaps := make([]GroupSnapInfo, cSize)
+	for i := range snaps {
+		snaps[i].Name = C.GoString(cSnaps[i].name)
+		snaps[i].State = GroupSnapState(cSnaps[i].state)
+	}
+
+	// free C memory allocated by C.rbd_group_snap_list call
+	ret := C.rbd_group_snap_list_cleanup(
+		(*C.rbd_group_snap_info_t)(unsafe.Pointer(&cSnaps[0])),
+		C.sizeof_rbd_group_snap_info_t,
+		cSize)
+	return snaps, getError(ret)
+}
+
+// GroupSnapRollback will roll back the images in the group to that of the
+// given snapshot.
+//
+// Implements:
+//  int rbd_group_snap_rollback(rados_ioctx_t group_p,
+//                              const char *group_name,
+//                              const char *snap_name);
+func GroupSnapRollback(ioctx *rados.IOContext, group, snap string) error {
+	cGroupName := C.CString(group)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cSnapName := C.CString(snap)
+	defer C.free(unsafe.Pointer(cSnapName))
+
+	ret := C.rbd_group_snap_rollback(cephIoctx(ioctx), cGroupName, cSnapName)
+	return getError(ret)
+}
+
+// GroupSnapRollbackCallback defines the function signature needed for the
+// GroupSnapRollbackWithProgress callback.
+//
+// This callback will be called by GroupSnapRollbackWithProgress when it
+// wishes to report progress rolling back a group snapshot.
+type GroupSnapRollbackCallback func(uint64, uint64, interface{}) int
+
+var groupSnapRollbackCallbacks = callbacks.New()
+
+// GroupSnapRollbackWithProgress will roll back the images in the group
+// to that of given snapshot. The given progress callback will be called
+// to report on the progress of the snapshot rollback.
+//
+// Implements:
+//  int rbd_group_snap_rollback_with_progress(rados_ioctx_t group_p,
+//                                            const char *group_name,
+//                                            const char *snap_name,
+//                                            librbd_progress_fn_t cb,
+//                                            void *cbdata);
+func GroupSnapRollbackWithProgress(
+	ioctx *rados.IOContext, group, snap string,
+	cb GroupSnapRollbackCallback, data interface{}) error {
+	// the provided callback must be a real function
+	if cb == nil {
+		return rbdError(C.EINVAL)
+	}
+
+	cGroupName := C.CString(group)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cSnapName := C.CString(snap)
+	defer C.free(unsafe.Pointer(cSnapName))
+
+	ctx := gsnapRollbackCallbackCtx{
+		callback: cb,
+		data:     data,
+	}
+	cbIndex := groupSnapRollbackCallbacks.Add(ctx)
+	defer diffIterateCallbacks.Remove(cbIndex)
+
+	ret := C.wrap_rbd_group_snap_rollback_with_progress(
+		cephIoctx(ioctx),
+		cGroupName,
+		cSnapName,
+		C.uintptr_t(cbIndex))
+
+	return getError(ret)
+}
+
+type gsnapRollbackCallbackCtx struct {
+	callback GroupSnapRollbackCallback
+	data     interface{}
+}
+
+//export snapRollbackCallback
+func snapRollbackCallback(
+	offset, total C.uint64_t, index uintptr) C.int {
+
+	v := groupSnapRollbackCallbacks.Lookup(index)
+	ctx := v.(gsnapRollbackCallbackCtx)
+	return C.int(ctx.callback(uint64(offset), uint64(total), ctx.data))
+}


Test patch:
diff --git a/rbd/group_snap_test.go b/rbd/group_snap_test.go
new file mode 100644
index 0000000..e1046c6
--- /dev/null
+++ b/rbd/group_snap_test.go
@@ -0,0 +1,245 @@
+package rbd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGroupSnapshots(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)
+
+	options := NewRbdImageOptions()
+	assert.NoError(t,
+		options.SetUint64(ImageOptionOrder, uint64(testImageOrder)))
+
+	name1 := GetUUID()
+	err = CreateImage(ioctx, name1, testImageSize, options)
+	require.NoError(t, err)
+
+	name2 := GetUUID()
+	err = CreateImage(ioctx, name2, testImageSize, options)
+	require.NoError(t, err)
+
+	err = GroupImageAdd(ioctx, gname, ioctx, name1)
+	assert.NoError(t, err)
+	err = GroupImageAdd(ioctx, gname, ioctx, name2)
+	assert.NoError(t, err)
+
+	t.Run("groupSnapCreateRemove", func(t *testing.T) {
+		err := GroupSnapCreate(ioctx, gname, "snap1")
+		assert.NoError(t, err)
+		err = GroupSnapRemove(ioctx, gname, "snap1")
+		assert.NoError(t, err)
+	})
+	t.Run("groupSnapRename", func(t *testing.T) {
+		err := GroupSnapCreate(ioctx, gname, "snap2a")
+		assert.NoError(t, err)
+		err = GroupSnapRename(ioctx, gname, "fred", "wilma")
+		assert.Error(t, err)
+		err = GroupSnapRename(ioctx, gname, "snap2a", "snap2b")
+		assert.NoError(t, err)
+		err = GroupSnapRemove(ioctx, gname, "snap2a")
+		assert.Error(t, err, "remove of old name: expect error")
+		err = GroupSnapRemove(ioctx, gname, "snap2b")
+		assert.NoError(t, err, "remove of current name: expect success")
+	})
+	t.Run("groupSnappList", func(t *testing.T) {
+		err := GroupSnapCreate(ioctx, gname, "snap1")
+		assert.NoError(t, err)
+		err = GroupSnapCreate(ioctx, gname, "snap2")
+		assert.NoError(t, err)
+		err = GroupSnapCreate(ioctx, gname, "snap3")
+		assert.NoError(t, err)
+
+		gsl, err := GroupSnapList(ioctx, gname)
+		assert.NoError(t, err)
+		if assert.Len(t, gsl, 3) {
+			names := []string{}
+			for _, gsi := range gsl {
+				assert.Equal(t, GroupSnapStateComplete, gsi.State)
+				names = append(names, gsi.Name)
+			}
+			assert.Contains(t, names, "snap1")
+			assert.Contains(t, names, "snap2")
+			assert.Contains(t, names, "snap3")
+		}
+
+		err = GroupSnapRemove(ioctx, gname, "snap3")
+		assert.NoError(t, err)
+		err = GroupSnapRemove(ioctx, gname, "snap2")
+		assert.NoError(t, err)
+		err = GroupSnapRename(ioctx, gname, "snap1", "snap1a")
+
+		gsl, err = GroupSnapList(ioctx, gname)
+		assert.NoError(t, err)
+		if assert.Len(t, gsl, 1) {
+			assert.Equal(t, GroupSnapStateComplete, gsl[0].State)
+			assert.Equal(t, "snap1a", gsl[0].Name)
+		}
+
+		err = GroupSnapRemove(ioctx, gname, "snap1a")
+		assert.NoError(t, err)
+
+		gsl, err = GroupSnapList(ioctx, gname)
+		assert.NoError(t, err)
+		assert.Len(t, gsl, 0)
+	})
+	t.Run("groupSnapRollback", func(t *testing.T) {
+		img, err := OpenImage(ioctx, name1, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("HELLO WORLD"), 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+
+		snapname := "snap1"
+		err = GroupSnapCreate(ioctx, gname, snapname)
+		assert.NoError(t, err)
+
+		img, err = OpenImage(ioctx, name1, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("GOODBYE WORLD"), 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+
+		img, err = OpenImage(ioctx, name2, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("2222222222222"), 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+
+		err = GroupSnapRollback(ioctx, gname, snapname)
+		assert.NoError(t, err)
+
+		b := make([]byte, 8)
+		img, err = OpenImage(ioctx, name1, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.ReadAt(b, 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+		assert.Equal(t, []byte("HELLO WO"), b)
+
+		img, err = OpenImage(ioctx, name2, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.ReadAt(b, 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+		assert.Equal(t, []byte("\x00\x00\x00\x00\x00\x00\x00\x00"), b)
+
+		err = GroupSnapRemove(ioctx, gname, snapname)
+		assert.NoError(t, err)
+	})
+	t.Run("groupSnapRollbackWithProgress", func(t *testing.T) {
+		img, err := OpenImage(ioctx, name1, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("SAY CHEESE"), 0)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("AND SMILE_"), 10240)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+
+		snapname := "snap2r"
+		err = GroupSnapCreate(ioctx, gname, snapname)
+		assert.NoError(t, err)
+
+		img, err = OpenImage(ioctx, name1, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("GOODBYE WORLD"), 0)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("_THIS_IS_ALL_"), 10240)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("I_HAVE_TO_SAY"), 11240)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+
+		img, err = OpenImage(ioctx, name2, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.WriteAt([]byte("3333333333333"), 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+
+		cc := 0
+		cb := func(offset, total uint64, v interface{}) int {
+			cc++
+			val := v.(int)
+			assert.Equal(t, 0, val)
+			assert.Equal(t, uint64(1), total)
+			return 0
+		}
+		err = GroupSnapRollbackWithProgress(ioctx, gname, snapname, cb, 0)
+		assert.NoError(t, err)
+		assert.GreaterOrEqual(t, cc, 2)
+
+		b := make([]byte, 8)
+		img, err = OpenImage(ioctx, name1, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.ReadAt(b, 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+		assert.Equal(t, []byte("SAY CHEE"), b)
+
+		img, err = OpenImage(ioctx, name2, NoSnapshot)
+		assert.NoError(t, err)
+		_, err = img.ReadAt(b, 0)
+		assert.NoError(t, err)
+		err = img.Close()
+		assert.NoError(t, err)
+		assert.Equal(t, []byte("\x00\x00\x00\x00\x00\x00\x00\x00"), b)
+
+		err = GroupSnapRemove(ioctx, gname, snapname)
+		assert.NoError(t, err)
+	})
+	t.Run("invalidIOContext", func(t *testing.T) {
+		assert.Panics(t, func() {
+			GroupSnapCreate(nil, gname, "foo")
+		})
+		assert.Panics(t, func() {
+			GroupSnapRemove(nil, gname, "foo")
+		})
+		assert.Panics(t, func() {
+			GroupSnapRename(nil, gname, "foo", "bar")
+		})
+		assert.Panics(t, func() {
+			GroupSnapList(nil, gname)
+		})
+		assert.Panics(t, func() {
+			GroupSnapRollback(nil, gname, "foo")
+		})
+		assert.Panics(t, func() {
+			cb := func(o, t uint64, v interface{}) int { return 0 }
+			GroupSnapRollbackWithProgress(nil, gname, "foo", cb, nil)
+		})
+	})
+	t.Run("invalidCallback", func(t *testing.T) {
+		err := GroupSnapRollbackWithProgress(ioctx, gname, "foo", nil, nil)
+		assert.Error(t, err)
+	})
+}