Repository: ceph/go-ceph (go)

Issue:
cephfs: support connecting as a different UID
The libcephfs C api supports connecting as a different uid/gid than the current process (what is uses as a default). This api could be handy for developing additional tests, running the suite outside of the container, and other as-yet-unimagined use cases.

Pull request:
cephfs: add functionality needed to specify mount user creds
Continuing recent work around making tests in cephfs more flexible the following patches fix another lack of idempotence in tests. It also adds new functions wraping libcephfs apis needed to manage UserPerm(s) and set them prior to mounting the fs.

Fixes #205.

## 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/cephfs/cephfs.go b/cephfs/cephfs.go
index 689d9d9..6a8876d 100644
--- a/cephfs/cephfs.go
+++ b/cephfs/cephfs.go
@@ -96,6 +96,14 @@ func (mount *MountInfo) GetConfigOption(option string) (string, error) {
 	return value, nil
 }
 
+// Init the file system client without actually mounting the file system.
+//
+// Implements:
+//  int ceph_init(struct ceph_mount_info *cmount);
+func (mount *MountInfo) Init() error {
+	return getError(C.ceph_init(mount.mount))
+}
+
 // Mount the file system, establishing a connection capable of I/O.
 //
 // Implements:
diff --git a/cephfs/mount_perms_mimic.go b/cephfs/mount_perms_mimic.go
new file mode 100644
index 0000000..604bf65
--- /dev/null
+++ b/cephfs/mount_perms_mimic.go
@@ -0,0 +1,22 @@
+// +build !luminous
+//
+// ceph_mount_perms_set available in mimic & later
+
+package cephfs
+
+/*
+#cgo LDFLAGS: -lcephfs
+#cgo CPPFLAGS: -D_FILE_OFFSET_BITS=64
+#include <cephfs/libcephfs.h>
+*/
+import "C"
+
+// SetMountPerms applies the given UserPerm to the mount object, which it will
+// then use to define the connection's ownership credentials.
+// This function must be called after Init but before Mount.
+//
+// Implements:
+//  int ceph_mount_perms_set(struct ceph_mount_info *cmount, UserPerm *perm);
+func (mount *MountInfo) SetMountPerms(perm *UserPerm) error {
+	return getError(C.ceph_mount_perms_set(mount.mount, perm.userPerm))
+}
diff --git a/cephfs/userperm.go b/cephfs/userperm.go
new file mode 100644
index 0000000..fe816ad
--- /dev/null
+++ b/cephfs/userperm.go
@@ -0,0 +1,72 @@
+package cephfs
+
+/*
+#cgo LDFLAGS: -lcephfs
+#cgo CPPFLAGS: -D_FILE_OFFSET_BITS=64
+#include <cephfs/libcephfs.h>
+*/
+import "C"
+
+import (
+	"runtime"
+	"unsafe"
+)
+
+// UserPerm types may be used to get or change the credentials used by the
+// connection or some operations.
+type UserPerm struct {
+	userPerm *C.UserPerm
+
+	// cache create-time params
+	managed bool // if set, the userPerm was created by go-ceph
+	uid     C.uid_t
+	gid     C.gid_t
+	gidList []C.gid_t
+}
+
+// NewUserPerm creates a UserPerm pointer and the underlying ceph resources.
+//
+// Implements:
+//  UserPerm *ceph_userperm_new(uid_t uid, gid_t gid, int ngids, gid_t *gidlist);
+func NewUserPerm(uid, gid int, gidlist []int) *UserPerm {
+	// the C code does not copy the content of the gid list so we keep the
+	// inputs stashed in the go type. For completeness we stash everything.
+	p := &UserPerm{
+		managed: true,
+		uid:     C.uid_t(uid),
+		gid:     C.gid_t(gid),
+		gidList: make([]C.gid_t, len(gidlist)),
+	}
+	var cgids *C.gid_t
+	if len(p.gidList) > 0 {
+		for i, gid := range gidlist {
+			p.gidList[i] = C.gid_t(gid)
+		}
+		cgids = (*C.gid_t)(unsafe.Pointer(&p.gidList[0]))
+	}
+	p.userPerm = C.ceph_userperm_new(
+		p.uid, p.gid, C.int(len(p.gidList)), cgids)
+	// if the go object is unreachable, we would like to free the c-memory
+	// since this has no other resources than memory associated with it.
+	// This is only valid for UserPerm objects created by new, and thus have
+	// the managed var set.
+	runtime.SetFinalizer(p, destroyUserPerm)
+	return p
+}
+
+// Destroy will explicitly free ceph resources associated with the UserPerm.
+//
+// Implements:
+//  void ceph_userperm_destroy(UserPerm *perm);
+func (p *UserPerm) Destroy() {
+	if p.userPerm == nil || !p.managed {
+		return
+	}
+	C.ceph_userperm_destroy(p.userPerm)
+	p.userPerm = nil
+	p.gidList = nil
+}
+
+func destroyUserPerm(p *UserPerm) {
+	p.Destroy()
+}


Test patch:
diff --git a/cephfs/cephfs_test.go b/cephfs/cephfs_test.go
index 6e82dfc..ac6a1ba 100644
--- a/cephfs/cephfs_test.go
+++ b/cephfs/cephfs_test.go
@@ -203,6 +203,7 @@ func TestChmodDir(t *testing.T) {
 
 	err := mount.MakeDir(dirname, stats_before)
 	assert.NoError(t, err)
+	defer mount.RemoveDir(dirname)
 
 	err = mount.SyncFs()
 	assert.NoError(t, err)
@@ -235,6 +236,7 @@ func TestChown(t *testing.T) {
 
 	err := mount.MakeDir(dirname, 0755)
 	assert.NoError(t, err)
+	defer mount.RemoveDir(dirname)
 
 	err = mount.SyncFs()
 	assert.NoError(t, err)
diff --git a/cephfs/mount_perms_mimic_test.go b/cephfs/mount_perms_mimic_test.go
new file mode 100644
index 0000000..86c98ee
--- /dev/null
+++ b/cephfs/mount_perms_mimic_test.go
@@ -0,0 +1,45 @@
+// +build !luminous
+
+package cephfs
+
+import (
+	"os"
+	"path"
+	"syscall"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSetMountPerms(t *testing.T) {
+	mount, err := CreateMount()
+	require.NoError(t, err)
+	require.NotNil(t, mount)
+	defer func() { assert.NoError(t, mount.Release()) }()
+
+	err = mount.ReadDefaultConfigFile()
+	require.NoError(t, err)
+
+	err = mount.Init()
+	assert.NoError(t, err)
+
+	uperm := NewUserPerm(0, 500, []int{0, 500, 501})
+	err = mount.SetMountPerms(uperm)
+	assert.NoError(t, err)
+
+	err = mount.Mount()
+	assert.NoError(t, err)
+	defer func() { assert.NoError(t, mount.Unmount()) }()
+
+	t.Run("checkStat", func(t *testing.T) {
+		useMount(t)
+		dirname := "/check-mount-perms"
+		err := mount.MakeDir(dirname, 0755)
+		assert.NoError(t, err)
+		defer mount.RemoveDir(dirname)
+		s, err := os.Stat(path.Join(CephMountDir, dirname))
+		require.NoError(t, err)
+		assert.EqualValues(t, s.Sys().(*syscall.Stat_t).Gid, 500)
+	})
+}
diff --git a/cephfs/userperm_test.go b/cephfs/userperm_test.go
new file mode 100644
index 0000000..d006c8d
--- /dev/null
+++ b/cephfs/userperm_test.go
@@ -0,0 +1,43 @@
+package cephfs
+
+import (
+	"runtime"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestUserPerm(t *testing.T) {
+	t.Run("newAndDestroy", func(t *testing.T) {
+		uperm := NewUserPerm(0, 0, nil)
+		assert.NotNil(t, uperm)
+		assert.Equal(t, 0, len(uperm.gidList))
+		assert.True(t, uperm.managed)
+
+		// Destroy should be idempotent in our go library
+		uperm.Destroy()
+		uperm.Destroy()
+	})
+	t.Run("notManagedDestroy", func(t *testing.T) {
+		uperm := &UserPerm{}
+		assert.False(t, uperm.managed)
+		// Calling destroy shouldn't do much but is safe to call (many times)
+		uperm.Destroy()
+		uperm.Destroy()
+	})
+	t.Run("tryForceGc", func(t *testing.T) {
+		func() {
+			uperm := NewUserPerm(0, 0, nil)
+			_ = uperm
+		}()
+		runtime.GC()
+	})
+	t.Run("gidList", func(t *testing.T) {
+		uperm := NewUserPerm(1000, 1000, []int{1028, 1192, 2112})
+		defer uperm.Destroy()
+		assert.Equal(t, 3, len(uperm.gidList))
+		assert.EqualValues(t, 1028, uperm.gidList[0])
+		assert.EqualValues(t, 1192, uperm.gidList[1])
+		assert.EqualValues(t, 2112, uperm.gidList[2])
+	})
+}