Repository: ceph/go-ceph (go)

Issue:
Missing rbd API components: grouping-images
The rbd API implements function(s) below but go-ceph does not:
* rbd_group_image_add
* rbd_group_image_list
* rbd_group_image_list_cleanup
* rbd_group_image_remove
* rbd_group_image_remove_by_id
* rbd_get_group
* rbd_group_info_cleanup

This issue has been filed to track implementing this missing
functionality in the appropriate package within go-ceph.
Please refer to the Ceph C API for details about the function(s)
in question.

NOTE: This issue was generated by a script.
If the content looks wrong or the wording seems a bit odd
please excuse the script and blame @phlogistonjohn



Pull request description:
rbd: add group image functions
Add functions for managing and listing images in rbd groups.

* Add GroupImageAdd implementing rbd_group_image_add
* Add GroupImageRemove implementing rbd_group_image_remove
* Add GroupImageRemoveByID implementing rbd_group_image_remove_by_id
* Add GroupImageList implementing rbd_group_image_list
* Add GetGroup implementing rbd_get_group

Fixes #417 

## 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.go b/rbd/group.go
index 7838458..2566896 100644
--- a/rbd/group.go
+++ b/rbd/group.go
@@ -84,3 +84,175 @@ func GroupList(ioctx *rados.IOContext) ([]string, error) {
 	groups := cutil.SplitBuffer(buf[:ret])
 	return groups, nil
 }
+
+// GroupImageAdd will add the specified image to the named group.
+// An io context must be supplied for both the group and image.
+//
+// Implements:
+//  int rbd_group_image_add(rados_ioctx_t group_p,
+//                          const char *group_name,
+//                          rados_ioctx_t image_p,
+//                          const char *image_name);
+func GroupImageAdd(groupIoctx *rados.IOContext, groupName string,
+	imageIoctx *rados.IOContext, imageName string) error {
+
+	cGroupName := C.CString(groupName)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cImageName := C.CString(imageName)
+	defer C.free(unsafe.Pointer(cImageName))
+
+	ret := C.rbd_group_image_add(
+		cephIoctx(groupIoctx),
+		cGroupName,
+		cephIoctx(imageIoctx),
+		cImageName)
+	return getError(ret)
+}
+
+// GroupImageRemove will remove the specified image from the named group.
+// An io context must be supplied for both the group and image.
+//
+// Implements:
+//  int rbd_group_image_remove(rados_ioctx_t group_p,
+//                             const char *group_name,
+//                             rados_ioctx_t image_p,
+//                             const char *image_name);
+func GroupImageRemove(groupIoctx *rados.IOContext, groupName string,
+	imageIoctx *rados.IOContext, imageName string) error {
+
+	cGroupName := C.CString(groupName)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cImageName := C.CString(imageName)
+	defer C.free(unsafe.Pointer(cImageName))
+
+	ret := C.rbd_group_image_remove(
+		cephIoctx(groupIoctx),
+		cGroupName,
+		cephIoctx(imageIoctx),
+		cImageName)
+	return getError(ret)
+}
+
+// GroupImageRemoveByID will remove the specified image from the named group.
+// An io context must be supplied for both the group and image.
+//
+// Implements:
+//  CEPH_RBD_API int rbd_group_image_remove_by_id(rados_ioctx_t group_p,
+//                                               const char *group_name,
+//                                               rados_ioctx_t image_p,
+//                                               const char *image_id);
+func GroupImageRemoveByID(groupIoctx *rados.IOContext, groupName string,
+	imageIoctx *rados.IOContext, imageID string) error {
+
+	cGroupName := C.CString(groupName)
+	defer C.free(unsafe.Pointer(cGroupName))
+	cid := C.CString(imageID)
+	defer C.free(unsafe.Pointer(cid))
+
+	ret := C.rbd_group_image_remove_by_id(
+		cephIoctx(groupIoctx),
+		cGroupName,
+		cephIoctx(imageIoctx),
+		cid)
+	return getError(ret)
+}
+
+// GroupImageState indicates an image's state in a group.
+type GroupImageState int
+
+const (
+	// GroupImageStateAttached is equivalent to RBD_GROUP_IMAGE_STATE_ATTACHED
+	GroupImageStateAttached = GroupImageState(C.RBD_GROUP_IMAGE_STATE_ATTACHED)
+	// GroupImageStateIncomplete is equivalent to RBD_GROUP_IMAGE_STATE_INCOMPLETE
+	GroupImageStateIncomplete = GroupImageState(C.RBD_GROUP_IMAGE_STATE_INCOMPLETE)
+)
+
+// GroupImageInfo reports on images within a group.
+type GroupImageInfo struct {
+	Name   string
+	PoolID int64
+	State  GroupImageState
+}
+
+// GroupImageList returns a slice of GroupImageInfo types based on the
+// images that are part of the named group.
+//
+// Implements:
+//  int rbd_group_image_list(rados_ioctx_t group_p,
+//                           const char *group_name,
+//                           rbd_group_image_info_t *images,
+//                           size_t group_image_info_size,
+//                           size_t *num_entries);
+func GroupImageList(ioctx *rados.IOContext, name string) ([]GroupImageInfo, error) {
+	cName := C.CString(name)
+	defer C.free(unsafe.Pointer(cName))
+
+	var (
+		cImages []C.rbd_group_image_info_t
+		cSize   C.size_t
+		err     error
+	)
+	retry.WithSizes(1024, 262144, func(size int) retry.Hint {
+		cSize = C.size_t(size)
+		cImages = make([]C.rbd_group_image_info_t, cSize)
+		ret := C.rbd_group_image_list(
+			cephIoctx(ioctx),
+			cName,
+			(*C.rbd_group_image_info_t)(unsafe.Pointer(&cImages[0])),
+			C.sizeof_rbd_group_image_info_t,
+			&cSize)
+		err = getErrorIfNegative(ret)
+		return retry.Size(int(cSize)).If(err == errRange)
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	images := make([]GroupImageInfo, cSize)
+	for i := range images {
+		images[i].Name = C.GoString(cImages[i].name)
+		images[i].PoolID = int64(cImages[i].pool)
+		images[i].State = GroupImageState(cImages[i].state)
+	}
+
+	// free C memory allocated by C.rbd_group_image_list call
+	ret := C.rbd_group_image_list_cleanup(
+		(*C.rbd_group_image_info_t)(unsafe.Pointer(&cImages[0])),
+		C.sizeof_rbd_group_image_info_t,
+		cSize)
+	return images, getError(ret)
+}
+
+// GroupInfo contains the name and pool id of a RBD group.
+type GroupInfo struct {
+	Name   string
+	PoolID int64
+}
+
+// GetGroup returns group info for the group this image is part of.
+//
+// Implements:
+//  int rbd_get_group(rbd_image_t image, rbd_group_info_t *group_info,
+//                    size_t group_info_size);
+func (image *Image) GetGroup() (GroupInfo, error) {
+	if err := image.validate(imageIsOpen); err != nil {
+		return GroupInfo{}, err
+	}
+
+	var cgi C.rbd_group_info_t
+	ret := C.rbd_get_group(
+		image.image,
+		&cgi,
+		C.sizeof_rbd_group_info_t)
+	if err := getErrorIfNegative(ret); err != nil {
+		return GroupInfo{}, err
+	}
+
+	gi := GroupInfo{
+		Name:   C.GoString(cgi.name),
+		PoolID: int64(cgi.pool),
+	}
+	ret = C.rbd_group_info_cleanup(&cgi, C.sizeof_rbd_group_info_t)
+	return gi, getError(ret)
+}


Test patch:
diff --git a/rbd/group_test.go b/rbd/group_test.go
index c4c9f9e..36b68d4 100644
--- a/rbd/group_test.go
+++ b/rbd/group_test.go
@@ -113,3 +113,265 @@ func TestGroupList(t *testing.T) {
 		GroupList(nil)
 	})
 }
+
+func TestGroupImageAdd(t *testing.T) {
+	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()
+
+	name := GetUUID()
+	options := NewRbdImageOptions()
+	assert.NoError(t,
+		options.SetUint64(ImageOptionOrder, uint64(testImageOrder)))
+	err = CreateImage(ioctx, name, testImageSize, options)
+	require.NoError(t, err)
+
+	err = GroupCreate(ioctx, "grone")
+	assert.NoError(t, err)
+
+	err = GroupImageAdd(ioctx, "grone", ioctx, name)
+	assert.NoError(t, err)
+
+	err = GroupImageAdd(ioctx, "badGroup", ioctx, name)
+	assert.Error(t, err)
+
+	assert.Panics(t, func() {
+		GroupImageAdd(nil, "invalid", ioctx, "foobar")
+	})
+	assert.Panics(t, func() {
+		GroupImageAdd(ioctx, "invalid", nil, "foobar")
+	})
+}
+
+func TestGroupImageRemove(t *testing.T) {
+	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()
+
+	name := GetUUID()
+	options := NewRbdImageOptions()
+	assert.NoError(t,
+		options.SetUint64(ImageOptionOrder, uint64(testImageOrder)))
+	err = CreateImage(ioctx, name, testImageSize, options)
+	require.NoError(t, err)
+
+	err = GroupCreate(ioctx, "grone")
+	assert.NoError(t, err)
+
+	err = GroupImageAdd(ioctx, "grone", ioctx, name)
+	assert.NoError(t, err)
+
+	err = GroupImageRemove(ioctx, "grone", ioctx, name)
+	assert.NoError(t, err)
+
+	err = GroupImageRemove(ioctx, "badGroup", ioctx, name)
+	assert.Error(t, err)
+
+	assert.Panics(t, func() {
+		GroupImageRemove(nil, "invalid", ioctx, "foobar")
+	})
+	assert.Panics(t, func() {
+		GroupImageRemove(ioctx, "invalid", nil, "foobar")
+	})
+}
+
+func TestGroupImageRemoveByID(t *testing.T) {
+	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()
+
+	name := GetUUID()
+	options := NewRbdImageOptions()
+	assert.NoError(t,
+		options.SetUint64(ImageOptionOrder, uint64(testImageOrder)))
+	err = CreateImage(ioctx, name, testImageSize, options)
+	require.NoError(t, err)
+
+	err = GroupCreate(ioctx, "grone")
+	assert.NoError(t, err)
+
+	err = GroupImageAdd(ioctx, "grone", ioctx, name)
+	assert.NoError(t, err)
+
+	img, err := OpenImage(ioctx, name, NoSnapshot)
+	assert.NoError(t, err)
+	imageID, err := img.GetId()
+	assert.NoError(t, err)
+	err = img.Close()
+	assert.NoError(t, err)
+
+	err = GroupImageRemoveByID(ioctx, "grone", ioctx, imageID)
+	assert.NoError(t, err)
+
+	err = GroupImageRemoveByID(ioctx, "badGroup", ioctx, imageID)
+	assert.Error(t, err)
+
+	assert.Panics(t, func() {
+		GroupImageRemoveByID(nil, "invalid", ioctx, "foobar")
+	})
+	assert.Panics(t, func() {
+		GroupImageRemoveByID(ioctx, "invalid", nil, "foobar")
+	})
+}
+
+func TestGroupImageList(t *testing.T) {
+	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()
+
+	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 = GroupCreate(ioctx, "grone")
+	assert.NoError(t, err)
+
+	err = GroupImageAdd(ioctx, "grone", ioctx, name1)
+	assert.NoError(t, err)
+
+	gimgs, err := GroupImageList(ioctx, "grone")
+	assert.NoError(t, err)
+	assert.NotNil(t, gimgs)
+	if assert.Len(t, gimgs, 1) {
+		assert.Equal(t, name1, gimgs[0].Name)
+		assert.Equal(t, GroupImageStateAttached, gimgs[0].State)
+	}
+
+	err = GroupImageAdd(ioctx, "grone", ioctx, name2)
+	assert.NoError(t, err)
+
+	gimgs, err = GroupImageList(ioctx, "grone")
+	assert.NoError(t, err)
+	assert.NotNil(t, gimgs)
+	if assert.Len(t, gimgs, 2) {
+		// order is not assured!
+		names := []string{}
+		for _, gimg := range gimgs {
+			names = append(names, gimg.Name)
+		}
+		assert.Contains(t, names, name1)
+		assert.Contains(t, names, name2)
+	}
+
+	err = GroupImageRemove(ioctx, "grone", ioctx, name1)
+	assert.NoError(t, err)
+
+	gimgs, err = GroupImageList(ioctx, "grone")
+	assert.NoError(t, err)
+	assert.NotNil(t, gimgs)
+	if assert.Len(t, gimgs, 1) {
+		assert.Equal(t, name2, gimgs[0].Name)
+		assert.Equal(t, GroupImageStateAttached, gimgs[0].State)
+	}
+
+	assert.Panics(t, func() {
+		GroupImageList(nil, "foo")
+	})
+}
+
+func TestGroupImageGetGroup(t *testing.T) {
+	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()
+
+	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 = GroupCreate(ioctx, "grone")
+	assert.NoError(t, err)
+
+	err = GroupImageAdd(ioctx, "grone", ioctx, name1)
+	assert.NoError(t, err)
+
+	img1, err := OpenImage(ioctx, name1, NoSnapshot)
+	assert.NoError(t, err)
+	defer func() {
+		assert.NoError(t, img1.Close())
+	}()
+
+	gi1, err := img1.GetGroup()
+	assert.NoError(t, err)
+	assert.Equal(t, "grone", gi1.Name)
+
+	img2, err := OpenImage(ioctx, name2, NoSnapshot)
+	assert.NoError(t, err)
+	defer func() {
+		assert.NoError(t, img2.Close())
+	}()
+
+	gi2, err := img2.GetGroup()
+	assert.NoError(t, err)
+	assert.Equal(t, "", gi2.Name)
+
+	t.Run("invalidImage", func(t *testing.T) {
+		x := &Image{}
+		_, err := x.GetGroup()
+		assert.Error(t, err)
+		assert.Panics(t, func() {
+			var x *Image
+			x.GetGroup()
+		})
+	})
+}