go-ceph/cephfs/cephfs_test.go
John Mulligan 2ad8361692 cephfs: make the Release function more idempotent to callers
Previously, calling Release more than once for the same MountInfo would
abort due to a double free in the ceph libs. As this is somewhat user
hostile we add some simple state tracking in our wrapper type such that
one can make redundant calls to Release without crashing the
application.

Signed-off-by: John Mulligan <jmulligan@redhat.com>
2020-04-06 16:29:26 -04:00

435 lines
10 KiB
Go

package cephfs
import (
"encoding/json"
"fmt"
"os"
"path"
"syscall"
"testing"
"time"
"github.com/ceph/go-ceph/rados"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
CephMountDir = "/tmp/ceph/mds/mnt/"
requireCephMount = false
testMdsName = "Z"
)
func init() {
mdir := os.Getenv("GO_CEPH_TEST_MOUNT_DIR")
if mdir != "" {
CephMountDir = mdir
}
reqMount := os.Getenv("GO_CEPH_TEST_REQUIRE_MOUNT")
if reqMount == "yes" || reqMount == "true" {
requireCephMount = true
}
mdsName := os.Getenv("GO_CEPH_TEST_MDS_NAME")
if mdsName != "" {
testMdsName = mdsName
}
}
func useMount(t *testing.T) {
fail := func(m string) {
if requireCephMount {
t.Fatalf("cephfs mount required: %s %s", CephMountDir, m)
} else {
t.Skipf("cephfs mount needed: %s %s", CephMountDir, m)
}
}
s, err := os.Stat(CephMountDir)
if err != nil || !s.IsDir() {
fail("missing or not a directory")
}
if us, ok := s.Sys().(*syscall.Stat_t); ok {
ps, err := os.Stat(path.Dir(path.Clean(CephMountDir)))
if err != nil {
fail("missing parent directory (race condition?)")
}
if ps.Sys().(*syscall.Stat_t).Dev == us.Dev {
fail("not a mount point")
}
} else {
fail("not a unix-like file system? how did you even compile this?" +
"no, seriously please contact us or file an issue and let us know!")
}
}
func TestCreateMount(t *testing.T) {
mount, err := CreateMount()
assert.NoError(t, err)
assert.NotNil(t, mount)
assert.NoError(t, mount.Release())
}
func fsConnect(t *testing.T) *MountInfo {
mount, err := CreateMount()
require.NoError(t, err)
require.NotNil(t, mount)
err = mount.ReadDefaultConfigFile()
require.NoError(t, err)
timeout := time.After(time.Second * 5)
ch := make(chan error)
go func(mount *MountInfo) {
ch <- mount.Mount()
}(mount)
select {
case err = <-ch:
case <-timeout:
err = fmt.Errorf("timed out waiting for connect")
}
require.NoError(t, err)
return mount
}
func fsDisconnect(t *testing.T, mount *MountInfo) {
assert.NoError(t, mount.Unmount())
assert.NoError(t, mount.Release())
}
func TestMountRoot(t *testing.T) {
mount := fsConnect(t)
fsDisconnect(t, mount)
}
func TestSyncFs(t *testing.T) {
mount := fsConnect(t)
defer fsDisconnect(t, mount)
err := mount.SyncFs()
assert.NoError(t, err)
}
func TestChangeDir(t *testing.T) {
mount := fsConnect(t)
defer fsDisconnect(t, mount)
dir1 := mount.CurrentDir()
assert.NotNil(t, dir1)
err := mount.MakeDir("/asdf", 0755)
assert.NoError(t, err)
err = mount.ChangeDir("/asdf")
assert.NoError(t, err)
dir2 := mount.CurrentDir()
assert.NotNil(t, dir2)
assert.NotEqual(t, dir1, dir2)
assert.Equal(t, dir1, "/")
assert.Equal(t, dir2, "/asdf")
err = mount.ChangeDir("/")
assert.NoError(t, err)
err = mount.RemoveDir("/asdf")
assert.NoError(t, err)
}
func TestRemoveDir(t *testing.T) {
useMount(t)
dirname := "one"
localPath := path.Join(CephMountDir, dirname)
mount := fsConnect(t)
defer fsDisconnect(t, mount)
err := mount.MakeDir(dirname, 0755)
assert.NoError(t, err)
err = mount.SyncFs()
assert.NoError(t, err)
// os.Stat the actual mounted location to verify Makedir/RemoveDir
_, err = os.Stat(localPath)
assert.NoError(t, err)
err = mount.RemoveDir(dirname)
assert.NoError(t, err)
_, err = os.Stat(localPath)
assert.EqualError(t, err,
fmt.Sprintf("stat %s: no such file or directory", localPath))
}
func TestUnmountMount(t *testing.T) {
t.Run("neverMounted", func(t *testing.T) {
mount, err := CreateMount()
require.NoError(t, err)
require.NotNil(t, mount)
assert.False(t, mount.IsMounted())
assert.NoError(t, mount.Release())
})
t.Run("mountUnmount", func(t *testing.T) {
mount := fsConnect(t)
defer func() { assert.NoError(t, mount.Release()) }()
assert.True(t, mount.IsMounted())
err := mount.Unmount()
assert.NoError(t, err)
assert.False(t, mount.IsMounted())
})
}
func TestReleaseMount(t *testing.T) {
mount, err := CreateMount()
assert.NoError(t, err)
require.NotNil(t, mount)
assert.NoError(t, mount.Release())
// call release again to ensure idempotency of the func
assert.NoError(t, mount.Release())
}
func TestChmodDir(t *testing.T) {
useMount(t)
dirname := "two"
var stats_before uint32 = 0755
var stats_after uint32 = 0700
mount := fsConnect(t)
defer fsDisconnect(t, mount)
err := mount.MakeDir(dirname, stats_before)
assert.NoError(t, err)
err = mount.SyncFs()
assert.NoError(t, err)
// os.Stat the actual mounted location to verify Makedir/RemoveDir
stats, err := os.Stat(path.Join(CephMountDir, dirname))
require.NoError(t, err)
assert.Equal(t, uint32(stats.Mode().Perm()), stats_before)
err = mount.Chmod(dirname, stats_after)
assert.NoError(t, err)
stats, err = os.Stat(path.Join(CephMountDir, dirname))
assert.NoError(t, err)
assert.Equal(t, uint32(stats.Mode().Perm()), stats_after)
}
// Not cross-platform, go's os does not specifiy Sys return type
func TestChown(t *testing.T) {
useMount(t)
dirname := "three"
// dockerfile creates bob user account
var bob uint32 = 1010
var root uint32
mount := fsConnect(t)
defer fsDisconnect(t, mount)
err := mount.MakeDir(dirname, 0755)
assert.NoError(t, err)
err = mount.SyncFs()
assert.NoError(t, err)
// os.Stat the actual mounted location to verify Makedir/RemoveDir
stats, err := os.Stat(path.Join(CephMountDir, dirname))
require.NoError(t, err)
assert.Equal(t, uint32(stats.Sys().(*syscall.Stat_t).Uid), root)
assert.Equal(t, uint32(stats.Sys().(*syscall.Stat_t).Gid), root)
err = mount.Chown(dirname, bob, bob)
assert.NoError(t, err)
stats, err = os.Stat(path.Join(CephMountDir, dirname))
assert.NoError(t, err)
assert.Equal(t, uint32(stats.Sys().(*syscall.Stat_t).Uid), bob)
assert.Equal(t, uint32(stats.Sys().(*syscall.Stat_t).Gid), bob)
}
func TestCephFSError(t *testing.T) {
err := getError(0)
assert.NoError(t, err)
err = getError(-5) // IO error
assert.Error(t, err)
assert.Equal(t, err.Error(), "cephfs: ret=5, Input/output error")
err = getError(345) // no such errno
assert.Error(t, err)
assert.Equal(t, err.Error(), "cephfs: ret=345")
}
func radosConnect(t *testing.T) *rados.Conn {
conn, err := rados.NewConn()
require.NoError(t, err)
err = conn.ReadDefaultConfigFile()
require.NoError(t, err)
timeout := time.After(time.Second * 5)
ch := make(chan error)
go func(conn *rados.Conn) {
ch <- conn.Connect()
}(conn)
select {
case err = <-ch:
case <-timeout:
err = fmt.Errorf("timed out waiting for connect")
}
require.NoError(t, err)
return conn
}
func TestCreateFromRados(t *testing.T) {
conn := radosConnect(t)
mount, err := CreateFromRados(conn)
assert.NoError(t, err)
assert.NotNil(t, mount)
}
func TestCreateMountWithId(t *testing.T) {
mount, err := CreateMountWithId("bobolink")
assert.NoError(t, err)
assert.NotNil(t, mount)
defer func() { assert.NoError(t, mount.Release()) }()
err = mount.ReadDefaultConfigFile()
assert.NoError(t, err)
err = mount.Mount()
assert.NoError(t, err)
defer func() { assert.NoError(t, mount.Unmount()) }()
// verify the custom entity_id is visible in the 'session ls' output
// of mds.
cmd := []byte(`{"prefix": "session ls"}`)
buf, info, err := mount.MdsCommand(
testMdsName,
[][]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)
defer fsDisconnect(t, mount)
cmd := []byte(`{"prefix": "client ls"}`)
buf, info, err := mount.MdsCommand(
testMdsName,
[][]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)
defer fsDisconnect(t, mount)
cmd := []byte("iAMinValId~~~")
buf, info, err := mount.MdsCommand(
testMdsName,
[][]byte{cmd})
assert.Error(t, err)
assert.Equal(t, "", string(buf))
assert.NotEqual(t, "", string(info))
assert.Contains(t, string(info), "unparseable JSON")
}
func TestMountWithRoot(t *testing.T) {
bMount := fsConnect(t)
defer fsDisconnect(t, bMount)
dir1 := "/test-mount-with-root"
err := bMount.MakeDir(dir1, 0755)
assert.NoError(t, err)
defer bMount.RemoveDir(dir1)
sub1 := "/i.was.here"
dir2 := dir1 + sub1
err = bMount.MakeDir(dir2, 0755)
assert.NoError(t, err)
defer bMount.RemoveDir(dir2)
t.Run("withRoot", func(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.MountWithRoot(dir1)
assert.NoError(t, err)
defer func() {
assert.NoError(t, mount.Unmount())
}()
err = mount.ChangeDir(sub1)
assert.NoError(t, err)
})
t.Run("badRoot", func(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.MountWithRoot("/i-yam-what-i-yam")
assert.Error(t, err)
})
}
func TestGetSetConfigOption(t *testing.T) {
// we don't need an active connection for this, just the handle
mount, err := CreateMount()
require.NoError(t, err)
require.NotNil(t, mount)
defer func() { assert.NoError(t, mount.Release()) }()
err = mount.SetConfigOption("__dne__", "value")
assert.Error(t, err)
_, err = mount.GetConfigOption("__dne__")
assert.Error(t, err)
origVal, err := mount.GetConfigOption("log_file")
assert.NoError(t, err)
err = mount.SetConfigOption("log_file", "/dev/null")
assert.NoError(t, err)
currVal, err := mount.GetConfigOption("log_file")
assert.NoError(t, err)
assert.Equal(t, "/dev/null", currVal)
err = mount.SetConfigOption("log_file", origVal)
assert.NoError(t, err)
currVal, err = mount.GetConfigOption("log_file")
assert.NoError(t, err)
assert.Equal(t, origVal, currVal)
}