diff --git a/rbd/metadata.go b/rbd/metadata.go index c48500e..3be2d95 100644 --- a/rbd/metadata.go +++ b/rbd/metadata.go @@ -8,6 +8,7 @@ import "C" import ( "unsafe" + "github.com/ceph/go-ceph/internal/cutil" "github.com/ceph/go-ceph/internal/retry" ) @@ -84,3 +85,65 @@ func (image *Image) RemoveMetadata(key string) error { return nil } + +// ListMetadata returns a map containing all metadata assigned to the RBD image. +// +// Implements: +// int rbd_metadata_list(rbd_image_t image, const char *start, uint64_t max, +// char *keys, size_t *key_len, char *values, size_t *vals_len); +func (image *Image) ListMetadata() (map[string]string, error) { + if err := image.validate(imageIsOpen); err != nil { + return nil, err + } + + var ( + err error + keysbuf []byte + keysSize C.size_t + valsbuf []byte + valsSize C.size_t + ) + retry.WithSizes(4096, 262144, func(size int) retry.Hint { + keysbuf = make([]byte, size) + keysSize = C.size_t(size) + valsbuf = make([]byte, size) + valsSize = C.size_t(size) + // the rbd_metadata_list function can use a start point and a limit. + // we do not use it and prefer our retry helper and just allocating + // buffers large enough to take all the keys and values + ret := C.rbd_metadata_list( + image.image, + (*C.char)(unsafe.Pointer(&empty[0])), // always start at the beginning (no paging) + 0, // fetch all key-value pairs + (*C.char)(unsafe.Pointer(&keysbuf[0])), + &keysSize, + (*C.char)(unsafe.Pointer(&valsbuf[0])), + &valsSize) + + err = getError(ret) + nextSize := valsSize + if keysSize > nextSize { + nextSize = keysSize + } + return retry.Size(int(nextSize)).If(err == errRange) + }) + if err != nil { + return nil, err + } + + m := map[string]string{} + keys := cutil.SplitBuffer(keysbuf[:keysSize]) + vals := cutil.SplitBuffer(valsbuf[:valsSize]) + if len(keys) != len(vals) { + // this should not happen (famous last words) + return nil, errRange + } + for i := range keys { + m[keys[i]] = vals[i] + } + return m, nil +} + +// rather than allocate memory every time that ListMetadata is called, +// define a static byte slice to stand in for the C "empty string" +var empty = []byte{0} diff --git a/rbd/metadata_test.go b/rbd/metadata_test.go index 0949e6e..da5a802 100644 --- a/rbd/metadata_test.go +++ b/rbd/metadata_test.go @@ -39,6 +39,9 @@ func TestImageMetadata(t *testing.T) { value, err = image.GetMetadata(metadataKey) assert.Equal(t, "", value) assert.Equal(t, err, ErrImageNotOpen) + // check ListMetadata for unopened image + _, err = image.ListMetadata() + assert.Equal(t, err, ErrImageNotOpen) image, err = OpenImage(ioctx, name, NoSnapshot) assert.NoError(t, err) @@ -50,6 +53,13 @@ func TestImageMetadata(t *testing.T) { value, err = image.GetMetadata(metadataKey) assert.NoError(t, err) assert.Equal(t, metadataValue, value) + // List metadata + mm, err := image.ListMetadata() + assert.NoError(t, err) + assert.Len(t, mm, 1) + if assert.Contains(t, mm, metadataKey) { + assert.Equal(t, mm[metadataKey], metadataValue) + } // Remove the metadata key err = image.RemoveMetadata(metadataKey) assert.NoError(t, err) @@ -58,6 +68,27 @@ func TestImageMetadata(t *testing.T) { assert.Equal(t, "", value) assert.Error(t, err) + // Set some additional metadata values + m1 := map[string]string{ + metadataKey: metadataValue, + "k2": "himalayas", + "matterhorn": "alps", + "aconcagua": "andes", + } + for k, v := range m1 { + err = image.SetMetadata(k, v) + assert.NoError(t, err) + } + // Get the metadata value + value, err = image.GetMetadata(metadataKey) + assert.NoError(t, err) + assert.Equal(t, metadataValue, value) + // List metadata + mm, err = image.ListMetadata() + assert.NoError(t, err) + assert.Len(t, mm, 4) + assert.Equal(t, m1, mm) + err = image.Close() assert.NoError(t, err) err = image.Remove()