Repository: ceph/go-ceph
Pull request #164 (2020-01-28 18:32:19)

cephfs: wrap ceph mds command
Add a wrapper for ceph_mds_command allows the client to make
queries against the mds server.
Use the mds command function to check for the id
we passed to CreateMountWithId appears in the session list.

## Checklist
- [x] Added tests for features and functional changes
- [x] Public functions and types are documented
- [x] Standard formatting is applied to Go code


nixpanic: Changes are acceptable, but I wonder what kind of commands can be sent to an MDS?
phlogistonjohn: > Changes are acceptable, but I wonder what kind of commands can be sent to an MDS?

It turns out there are a lot of things you can do with it but largely around debugging/information. In the test suite you can see we use 'session ls' and 'client ls'. 
Recently I found out you can get the "help" by doing `ceph daemon mds.Z get_command_descriptions | jq . `  Where mds.Z is one of names for "your" MDS. This pattern applies to other stuff like the mon command too.

Patch:
diff --git a/cephfs/cephfs.go b/cephfs/cephfs.go
index ef12050..a83ec1e 100644
--- a/cephfs/cephfs.go
+++ b/cephfs/cephfs.go
@@ -160,3 +160,76 @@ func (mount *MountInfo) IsMounted() bool {
 	ret := C.ceph_is_mounted(mount.mount)
 	return ret == 1
 }
+
+// MdsCommand sends commands to the specified MDS.
+func (mount *MountInfo) MdsCommand(mdsSpec string, args [][]byte) ([]byte, string, error) {
+	return mount.mdsCommand(mdsSpec, args, nil)
+}
+
+// MdsCommandWithInputBuffer sends commands to the specified MDS, with an input
+// buffer.
+func (mount *MountInfo) MdsCommandWithInputBuffer(mdsSpec string, args [][]byte, inputBuffer []byte) ([]byte, string, error) {
+	return mount.mdsCommand(mdsSpec, args, inputBuffer)
+}
+
+// mdsCommand supports sending formatted commands to MDS.
+//
+// Implements:
+//  int ceph_mds_command(struct ceph_mount_info *cmount,
+//      const char *mds_spec,
+//      const char **cmd,
+//      size_t cmdlen,
+//      const char *inbuf, size_t inbuflen,
+//      char **outbuf, size_t *outbuflen,
+//      char **outs, size_t *outslen);
+func (mount *MountInfo) mdsCommand(mdsSpec string, args [][]byte, inputBuffer []byte) (buffer []byte, info string, err error) {
+	spec := C.CString(mdsSpec)
+	defer C.free(unsafe.Pointer(spec))
+
+	argc := len(args)
+	argv := make([]*C.char, argc)
+
+	for i, arg := range args {
+		argv[i] = C.CString(string(arg))
+	}
+	// free all array elements in a single defer
+	defer func() {
+		for i := range argv {
+			C.free(unsafe.Pointer(argv[i]))
+		}
+	}()
+
+	var (
+		outs, outbuf       *C.char
+		outslen, outbuflen C.size_t
+	)
+	inbuf := C.CString(string(inputBuffer))
+	inbufLen := len(inputBuffer)
+	defer C.free(unsafe.Pointer(inbuf))
+
+	ret := C.ceph_mds_command(
+		mount.mount,        // cephfs mount ref
+		spec,               // mds spec
+		&argv[0],           // cmd array
+		C.size_t(argc),     // cmd array length
+		inbuf,              // bulk input
+		C.size_t(inbufLen), // length inbuf
+		&outbuf,            // buffer
+		&outbuflen,         // buffer length
+		&outs,              // status string
+		&outslen)
+
+	if outslen > 0 {
+		info = C.GoStringN(outs, C.int(outslen))
+		C.free(unsafe.Pointer(outs))
+	}
+	if outbuflen > 0 {
+		buffer = C.GoBytes(unsafe.Pointer(outbuf), C.int(outbuflen))
+		C.free(unsafe.Pointer(outbuf))
+	}
+	if ret != 0 {
+		return nil, info, getError(ret)
+	}
+
+	return buffer, info, nil
+}


Test patch:
diff --git a/cephfs/cephfs_test.go b/cephfs/cephfs_test.go
index 506102f..445d5c2 100644
--- a/cephfs/cephfs_test.go
+++ b/cephfs/cephfs_test.go
@@ -1,6 +1,7 @@
 package cephfs
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
 	"syscall"
@@ -223,10 +224,7 @@ func TestCreateFromRados(t *testing.T) {
 }
 
 func TestCreateMountWithId(t *testing.T) {
-	// TODO: the entity_id is visible w/in the 'session ls' output
-	// of mds. Consider running the equivalent api call and checking
-	// that the string is set.
-	mount, err := CreateMountWithId("bob")
+	mount, err := CreateMountWithId("bobolink")
 	assert.NoError(t, err)
 	assert.NotNil(t, mount)
 
@@ -235,4 +233,46 @@ func TestCreateMountWithId(t *testing.T) {
 
 	err = mount.Mount()
 	assert.NoError(t, err)
+
+	// verify the custom entity_id is visible in the 'session ls' output
+	// of mds.
+	cmd := []byte(`{"prefix": "session ls"}`)
+	buf, info, err := mount.MdsCommand(
+		"Z", // TODO: fix hard-coded name mds (from ci container script)
+		[][]byte{cmd})
+	assert.NoError(t, err)
+	assert.NotEqual(t, "", string(buf))
+	assert.Equal(t, "", string(info))
+	assert.Contains(t, string(buf), `"bobolink"`)
+}
+
+func TestMdsCommand(t *testing.T) {
+	mount := fsConnect(t)
+
+	cmd := []byte(`{"prefix": "client ls"}`)
+	buf, info, err := mount.MdsCommand(
+		"Z", // TODO: fix hard-coded name mds (from ci container script)
+		[][]byte{cmd})
+	assert.NoError(t, err)
+	assert.NotEqual(t, "", string(buf))
+	assert.Equal(t, "", string(info))
+	assert.Contains(t, string(buf), "ceph_version")
+	// response should also be valid json
+	var j []interface{}
+	err = json.Unmarshal(buf, &j)
+	assert.NoError(t, err)
+	assert.GreaterOrEqual(t, len(j), 1)
+}
+
+func TestMdsCommandError(t *testing.T) {
+	mount := fsConnect(t)
+
+	cmd := []byte("iAMinValId~~~")
+	buf, info, err := mount.MdsCommand(
+		"Z", // TODO: fix hard-coded name mds (from ci container script)
+		[][]byte{cmd})
+	assert.Error(t, err)
+	assert.Equal(t, "", string(buf))
+	assert.NotEqual(t, "", string(info))
+	assert.Contains(t, string(info), "unparseable JSON")
 }