Repository: ceph/go-ceph (go)

Issue:
Missing rbd API components: function rbd_mirror_image_status_summary
The rbd API implements function(s) below but go-ceph does not:
* rbd_mirror_image_status_summary

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 MirrorImageStatusSummary implementing rbd_mirror_image_status_summary
The MirrorImageStatusSummary returns a map of image mirroring states to
the number of images in those states or an error.
Tests of both the basic conditions and actual mirrored images are added.

Fixes #484 

To the best of my knowledge, this is the core API call for `rbdmirror pool status` from  #398, but that cli command does other stuff too, and so I hesitate to suggest this PR resolves that issue.




## 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/mirror.go b/rbd/mirror.go
index 6de3024..b2bab7e 100644
--- a/rbd/mirror.go
+++ b/rbd/mirror.go
@@ -421,3 +421,49 @@ func (image *Image) CreateMirrorSnapshot() (uint64, error) {
 		&snapID)
 	return uint64(snapID), getError(ret)
 }
+
+// MirrorImageStatusSummary returns a map of images statuses and the count
+// of images with said status.
+//
+// Implements:
+//  int rbd_mirror_image_status_summary(
+//    rados_ioctx_t io_ctx, rbd_mirror_image_status_state_t *states, int *counts,
+//    size_t *maxlen);
+func MirrorImageStatusSummary(
+	ioctx *rados.IOContext) (map[MirrorImageStatusState]uint, error) {
+	// ideally, we already know the size of the arrays - they should be
+	// the size of all the values of the rbd_mirror_image_status_state_t
+	// enum. But the C api doesn't enforce this so we give a little
+	// wiggle room in case the server returns values outside the enum
+	// we know about. This is the only case I can think of that we'd
+	// be able to get -ERANGE.
+	var (
+		cioctx  = cephIoctx(ioctx)
+		err     error
+		cStates []C.rbd_mirror_image_status_state_t
+		cCounts []C.int
+		cSize   C.size_t
+	)
+	retry.WithSizes(16, 1<<16, func(size int) retry.Hint {
+		cSize = C.size_t(size)
+		cStates = make([]C.rbd_mirror_image_status_state_t, cSize)
+		cCounts = make([]C.int, cSize)
+		ret := C.rbd_mirror_image_status_summary(
+			cioctx,
+			(*C.rbd_mirror_image_status_state_t)(&cStates[0]),
+			(*C.int)(&cCounts[0]),
+			&cSize)
+		err = getErrorIfNegative(ret)
+		return retry.Size(int(cSize)).If(err == errRange)
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	m := map[MirrorImageStatusState]uint{}
+	for i := 0; i < int(cSize); i++ {
+		s := MirrorImageStatusState(cStates[i])
+		m[s] = uint(cCounts[i])
+	}
+	return m, nil
+}


Test patch:
diff --git a/rbd/mirror_test.go b/rbd/mirror_test.go
index df04baf..d5a635d 100644
--- a/rbd/mirror_test.go
+++ b/rbd/mirror_test.go
@@ -463,3 +463,127 @@ func TestGetGlobalMirrorStatusMirroredPool(t *testing.T) {
 		}
 	})
 }
+
+func TestMirrorImageStatusSummary(t *testing.T) {
+	t.Run("ioctxNil", func(t *testing.T) {
+		assert.Panics(t, func() {
+			MirrorImageStatusSummary(nil)
+		})
+	})
+	t.Run("emptyPool", func(t *testing.T) {
+		conn := radosConnect(t)
+		poolName := GetUUID()
+		err := conn.MakePool(poolName)
+		require.NoError(t, err)
+		defer func() {
+			assert.NoError(t, conn.DeletePool(poolName))
+			conn.Shutdown()
+		}()
+
+		ioctx, err := conn.OpenIOContext(poolName)
+		assert.NoError(t, err)
+		defer func() {
+			ioctx.Destroy()
+		}()
+
+		ssum, err := MirrorImageStatusSummary(ioctx)
+		assert.NoError(t, err)
+		assert.Len(t, ssum, 0)
+	})
+	t.Run("mirroredPool", testMirrorImageStatusSummaryMirroredPool)
+}
+
+func testMirrorImageStatusSummaryMirroredPool(t *testing.T) {
+	mconfig := mirrorConfig()
+	if mconfig == "" {
+		t.Skip("no mirror config env var set")
+	}
+	conn := radosConnect(t)
+	// this test assumes the rbd pool already exists and is mirrored
+	// this must be set up previously by the CI or manually
+	poolName := "rbd"
+
+	ioctx, err := conn.OpenIOContext(poolName)
+	assert.NoError(t, err)
+	defer func() {
+		ioctx.Destroy()
+	}()
+
+	imgBase := GetUUID()
+	imgName1 := imgBase + "a"
+	imgName2 := imgBase + "b"
+	imgName3 := imgBase + "c"
+	imgName4 := imgBase + "d"
+
+	options := NewRbdImageOptions()
+	assert.NoError(t, options.SetUint64(ImageOptionOrder, uint64(testImageOrder)))
+
+	for _, n := range []string{imgName1, imgName2, imgName3, imgName4} {
+		err = CreateImage(ioctx, n, testImageSize, options)
+		require.NoError(t, err)
+
+		defer func(n string) {
+			err = RemoveImage(ioctx, n)
+			assert.NoError(t, err)
+		}(n)
+	}
+
+	mkMirror := func(n string) {
+		img, err := OpenImage(ioctx, n, NoSnapshot)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, img.Close())
+		}()
+
+		err = img.MirrorEnable(ImageMirrorModeSnapshot)
+		assert.NoError(t, err)
+
+		mid, err := img.CreateMirrorSnapshot()
+		assert.NoError(t, err)
+		assert.NotEqual(t, 0, mid)
+	}
+
+	checkMirror := func(n string) {
+		img, err := OpenImage(ioctx, n, NoSnapshot)
+		assert.NoError(t, err)
+		defer func() {
+			assert.NoError(t, img.Close())
+		}()
+
+		// wait for site statuses to get updated
+		for i := 0; i < 30; i++ {
+			gms, err := img.GetGlobalMirrorStatus()
+			assert.NoError(t, err)
+			if len(gms.SiteStatuses) > 1 {
+				break
+			}
+			time.Sleep(time.Second)
+		}
+	}
+
+	for _, n := range []string{imgName1, imgName3} {
+		mkMirror(n)
+	}
+	for _, n := range []string{imgName1, imgName3} {
+		checkMirror(n)
+	}
+
+	ssum, err := MirrorImageStatusSummary(ioctx)
+	assert.NoError(t, err)
+	if assert.Len(t, ssum, 1) {
+		assert.Contains(t, ssum, MirrorImageStatusStateReplaying)
+		assert.GreaterOrEqual(t, ssum[MirrorImageStatusStateReplaying], uint(2))
+	}
+
+	// immediately going for status right after enabling mirroring and not
+	// waiting for things to settle should give us one unknown status
+	mkMirror(imgName2)
+	ssum, err = MirrorImageStatusSummary(ioctx)
+	assert.NoError(t, err)
+	if assert.Len(t, ssum, 2) {
+		assert.Contains(t, ssum, MirrorImageStatusStateReplaying)
+		assert.GreaterOrEqual(t, ssum[MirrorImageStatusStateReplaying], uint(2))
+		assert.Contains(t, ssum, MirrorImageStatusStateUnknown)
+		assert.GreaterOrEqual(t, ssum[MirrorImageStatusStateUnknown], uint(1))
+	}
+}