[ARVADOS] created: 1.1.3-387-g6b784b9

Git user git at public.curoverse.com
Fri Apr 13 16:42:12 EDT 2018


        at  6b784b924cdea0ac49bd0d5535ba299d4258e7fb (commit)


commit 6b784b924cdea0ac49bd0d5535ba299d4258e7fb
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Apr 13 16:40:56 2018 -0400

    12308: Add "arvados-client mount" command via cgofuse.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index b89c8d9..2f4c491 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -75,6 +75,7 @@ lib/cli
 lib/cmd
 lib/crunchstat
 lib/dispatchcloud
+lib/mount
 services/api
 services/arv-git-httpd
 services/crunchstat
@@ -900,6 +901,7 @@ gostuff=(
     lib/cmd
     lib/crunchstat
     lib/dispatchcloud
+    lib/mount
     sdk/go/arvados
     sdk/go/arvadosclient
     sdk/go/blockdigest
diff --git a/cmd/arvados-client/Makefile b/cmd/arvados-client/Makefile
new file mode 100644
index 0000000..33fbc40
--- /dev/null
+++ b/cmd/arvados-client/Makefile
@@ -0,0 +1,11 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+all:
+	go get .
+	docker build --tag=cgofuse --build-arg=http_proxy="$(http_proxy)" --build-arg=https_proxy="$(https_proxy)" "$(GOPATH)"/src/github.com/curoverse/cgofuse
+	go get github.com/karalabe/xgo
+	xgo --image=cgofuse --targets=linux/amd64,linux/386,darwin/amd64,darwin/386,windows/amd64,windows/386 .
+	install arvados-* "$(GOPATH)"/bin/
+	rm --interactive=never arvados-*
diff --git a/cmd/arvados-client/cmd.go b/cmd/arvados-client/cmd.go
index b616b54..c9068fb 100644
--- a/cmd/arvados-client/cmd.go
+++ b/cmd/arvados-client/cmd.go
@@ -13,6 +13,7 @@ import (
 
 	"git.curoverse.com/arvados.git/lib/cli"
 	"git.curoverse.com/arvados.git/lib/cmd"
+	"git.curoverse.com/arvados.git/lib/mount"
 )
 
 var (
@@ -58,6 +59,8 @@ var (
 		"user":                     cli.APICall,
 		"virtual_machine":          cli.APICall,
 		"workflow":                 cli.APICall,
+
+		"mount": mount.Command,
 	})
 )
 
diff --git a/lib/mount/command.go b/lib/mount/command.go
new file mode 100644
index 0000000..4acaeae
--- /dev/null
+++ b/lib/mount/command.go
@@ -0,0 +1,76 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package mount
+
+import (
+	"flag"
+	"io"
+	"log"
+	"os"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
+	"git.curoverse.com/arvados.git/sdk/go/keepclient"
+	"github.com/curoverse/cgofuse/fuse"
+)
+
+var Command = &cmd{}
+
+type cmd struct {
+	// ready, if non-nil, will be closed when the mount is
+	// initialized.  If ready is non-nil, it RunCommand() should
+	// not be called more than once, or when ready is already
+	// closed.
+	ready chan struct{}
+	// It is safe to call Unmount ounly after ready has been
+	// closed.
+	Unmount func() (ok bool)
+}
+
+// RunCommand implements the subcommand "mount <path> [fuse options]".
+//
+// The "-d" fuse option (and perhaps other features) ignores the
+// stderr argument and prints to os.Stderr instead.
+func (c *cmd) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	logger := log.New(stderr, prog+" ", 0)
+	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+	ro := flags.Bool("ro", false, "read-only")
+	experimental := flags.Bool("experimental", false, "acknowledge this is an experimental command, and should not be used in production (required)")
+	err := flags.Parse(args)
+	if err != nil {
+		logger.Print(err)
+		return 2
+	}
+	if !*experimental {
+		logger.Printf("error: experimental command %q used without --experimental flag", prog)
+		return 2
+	}
+
+	client := arvados.NewClientFromEnv()
+	ac, err := arvadosclient.New(client)
+	if err != nil {
+		logger.Print(err)
+		return 1
+	}
+	kc, err := keepclient.MakeKeepClient(ac)
+	if err != nil {
+		logger.Print(err)
+		return 1
+	}
+	host := fuse.NewFileSystemHost(&keepFS{
+		Client:     client,
+		KeepClient: kc,
+		ReadOnly:   *ro,
+		Uid:        os.Getuid(),
+		Gid:        os.Getgid(),
+		ready:      c.ready,
+	})
+	c.Unmount = host.Unmount
+	ok := host.Mount("", flags.Args())
+	if !ok {
+		return 1
+	}
+	return 0
+}
diff --git a/lib/mount/command_test.go b/lib/mount/command_test.go
new file mode 100644
index 0000000..9cd4139
--- /dev/null
+++ b/lib/mount/command_test.go
@@ -0,0 +1,59 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package mount
+
+import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"time"
+
+	check "gopkg.in/check.v1"
+)
+
+var _ = check.Suite(&CmdSuite{})
+
+type CmdSuite struct {
+	mnt string
+}
+
+func (s *CmdSuite) SetUpTest(c *check.C) {
+	tmpdir, err := ioutil.TempDir("", "")
+	c.Assert(err, check.IsNil)
+	s.mnt = tmpdir
+}
+
+func (s *CmdSuite) TearDownTest(c *check.C) {
+	c.Check(os.RemoveAll(s.mnt), check.IsNil)
+}
+
+func (s *CmdSuite) TestMount(c *check.C) {
+	exited := make(chan int)
+	stdin := bytes.NewBufferString("stdin")
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
+	mountCmd := cmd{ready: make(chan struct{})}
+	ready := false
+	go func() {
+		exited <- mountCmd.RunCommand("test mount", []string{"--experimental", s.mnt}, stdin, stdout, stderr)
+	}()
+	go func() {
+		<-mountCmd.ready
+		ready = true
+		ok := mountCmd.Unmount()
+		c.Check(ok, check.Equals, true)
+	}()
+	select {
+	case <-time.After(5 * time.Second):
+		c.Fatal("timed out")
+	case errCode, ok := <-exited:
+		c.Check(ok, check.Equals, true)
+		c.Check(errCode, check.Equals, 0)
+	}
+	c.Check(ready, check.Equals, true)
+	c.Check(stdout.String(), check.Equals, "")
+	// stdin should not have been read
+	c.Check(stdin.String(), check.Equals, "stdin")
+}
diff --git a/lib/mount/fs.go b/lib/mount/fs.go
new file mode 100644
index 0000000..68ad0b4
--- /dev/null
+++ b/lib/mount/fs.go
@@ -0,0 +1,375 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package mount
+
+import (
+	"io"
+	"log"
+	"os"
+	"runtime/debug"
+	"sync"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/keepclient"
+	"github.com/curoverse/cgofuse/fuse"
+)
+
+// sharedFile wraps arvados.File with a sync.Mutex, so fuse can safely
+// use a single filehandle concurrently on behalf of multiple
+// threads/processes.
+type sharedFile struct {
+	arvados.File
+	sync.Mutex
+}
+
+// keepFS implements cgofuse's FileSystemInterface.
+type keepFS struct {
+	fuse.FileSystemBase
+	Client     *arvados.Client
+	KeepClient *keepclient.KeepClient
+	ReadOnly   bool
+	Uid        int
+	Gid        int
+
+	root   arvados.CustomFileSystem
+	open   map[uint64]*sharedFile
+	lastFH uint64
+	sync.Mutex
+
+	// If non-nil, this channel will be closed by Init() to notify
+	// other goroutines that the mount is ready.
+	ready chan struct{}
+}
+
+var (
+	invalidFH = ^uint64(0)
+)
+
+// newFH wraps f in a sharedFile, adds it to fs's lookup table using a
+// new handle number, and returns the handle number.
+func (fs *keepFS) newFH(f arvados.File) uint64 {
+	fs.Lock()
+	defer fs.Unlock()
+	if fs.open == nil {
+		fs.open = make(map[uint64]*sharedFile)
+	}
+	fs.lastFH++
+	fh := fs.lastFH
+	fs.open[fh] = &sharedFile{File: f}
+	return fh
+}
+
+func (fs *keepFS) lookupFH(fh uint64) *sharedFile {
+	fs.Lock()
+	defer fs.Unlock()
+	return fs.open[fh]
+}
+
+func (fs *keepFS) Init() {
+	defer fs.debugPanics()
+	fs.root = fs.Client.SiteFileSystem(fs.KeepClient)
+	fs.root.MountProject("home", "")
+	if fs.ready != nil {
+		close(fs.ready)
+	}
+}
+
+func (fs *keepFS) Create(path string, flags int, mode uint32) (errc int, fh uint64) {
+	defer fs.debugPanics()
+	if fs.ReadOnly {
+		return -fuse.EROFS, invalidFH
+	}
+	f, err := fs.root.OpenFile(path, flags|os.O_CREATE, os.FileMode(mode))
+	if err == os.ErrExist {
+		return -fuse.EEXIST, invalidFH
+	} else if err != nil {
+		return -fuse.EINVAL, invalidFH
+	}
+	return 0, fs.newFH(f)
+}
+
+func (fs *keepFS) Open(path string, flags int) (errc int, fh uint64) {
+	defer fs.debugPanics()
+	if fs.ReadOnly && flags&(os.O_RDWR|os.O_WRONLY|os.O_CREATE) != 0 {
+		return -fuse.EROFS, invalidFH
+	}
+	f, err := fs.root.OpenFile(path, flags, 0)
+	if err != nil {
+		return -fuse.ENOENT, invalidFH
+	} else if fi, err := f.Stat(); err != nil {
+		return -fuse.EIO, invalidFH
+	} else if fi.IsDir() {
+		f.Close()
+		return -fuse.EISDIR, invalidFH
+	}
+	return 0, fs.newFH(f)
+}
+
+func (fs *keepFS) Utimens(path string, tmsp []fuse.Timespec) int {
+	defer fs.debugPanics()
+	if fs.ReadOnly {
+		return -fuse.EROFS
+	}
+	f, err := fs.root.OpenFile(path, 0, 0)
+	if err != nil {
+		return fs.errCode(err)
+	}
+	f.Close()
+	return 0
+}
+
+func (fs *keepFS) errCode(err error) int {
+	if os.IsNotExist(err) {
+		return -fuse.ENOENT
+	}
+	switch err {
+	case os.ErrExist:
+		return -fuse.EEXIST
+	case arvados.ErrInvalidArgument:
+		return -fuse.EINVAL
+	case arvados.ErrInvalidOperation:
+		return -fuse.ENOSYS
+	case arvados.ErrDirectoryNotEmpty:
+		return -fuse.ENOTEMPTY
+	case nil:
+		return 0
+	default:
+		return -fuse.EIO
+	}
+}
+
+func (fs *keepFS) Mkdir(path string, mode uint32) int {
+	defer fs.debugPanics()
+	if fs.ReadOnly {
+		return -fuse.EROFS
+	}
+	f, err := fs.root.OpenFile(path, os.O_CREATE|os.O_EXCL, os.FileMode(mode)|os.ModeDir)
+	if err != nil {
+		return fs.errCode(err)
+	}
+	f.Close()
+	return 0
+}
+
+func (fs *keepFS) Opendir(path string) (errc int, fh uint64) {
+	defer fs.debugPanics()
+	f, err := fs.root.OpenFile(path, 0, 0)
+	if err != nil {
+		return fs.errCode(err), invalidFH
+	} else if fi, err := f.Stat(); err != nil {
+		return fs.errCode(err), invalidFH
+	} else if !fi.IsDir() {
+		f.Close()
+		return -fuse.ENOTDIR, invalidFH
+	}
+	return 0, fs.newFH(f)
+}
+
+func (fs *keepFS) Releasedir(path string, fh uint64) (errc int) {
+	defer fs.debugPanics()
+	return fs.Release(path, fh)
+}
+
+func (fs *keepFS) Rmdir(path string) int {
+	defer fs.debugPanics()
+	return fs.errCode(fs.root.Remove(path))
+}
+
+func (fs *keepFS) Release(path string, fh uint64) (errc int) {
+	defer fs.debugPanics()
+	fs.Lock()
+	defer fs.Unlock()
+	defer delete(fs.open, fh)
+	if f := fs.open[fh]; f != nil {
+		err := f.Close()
+		if err != nil {
+			return -fuse.EIO
+		}
+	}
+	return 0
+}
+
+func (fs *keepFS) Rename(oldname, newname string) (errc int) {
+	defer fs.debugPanics()
+	if fs.ReadOnly {
+		return -fuse.EROFS
+	}
+	return fs.errCode(fs.root.Rename(oldname, newname))
+}
+
+func (fs *keepFS) Unlink(path string) (errc int) {
+	defer fs.debugPanics()
+	if fs.ReadOnly {
+		return -fuse.EROFS
+	}
+	return fs.errCode(fs.root.Remove(path))
+}
+
+func (fs *keepFS) Truncate(path string, size int64, fh uint64) (errc int) {
+	defer fs.debugPanics()
+	if fs.ReadOnly {
+		return -fuse.EROFS
+	}
+
+	// Sometimes fh is a valid filehandle and we don't need to
+	// waste a name lookup.
+	if f := fs.lookupFH(fh); f != nil {
+		return fs.errCode(f.Truncate(size))
+	}
+
+	// Other times, fh is invalid and we need to lookup path.
+	f, err := fs.root.OpenFile(path, os.O_RDWR, 0)
+	if err != nil {
+		return fs.errCode(err)
+	}
+	defer f.Close()
+	return fs.errCode(f.Truncate(size))
+}
+
+func (fs *keepFS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) {
+	defer fs.debugPanics()
+	var fi os.FileInfo
+	var err error
+	if f := fs.lookupFH(fh); f != nil {
+		// Valid filehandle -- ignore path.
+		fi, err = f.Stat()
+	} else {
+		// Invalid filehandle -- lookup path.
+		fi, err = fs.root.Stat(path)
+	}
+	if err != nil {
+		return fs.errCode(err)
+	}
+	fs.fillStat(stat, fi)
+	return 0
+}
+
+func (fs *keepFS) Chmod(path string, mode uint32) (errc int) {
+	if fs.ReadOnly {
+		return -fuse.EROFS
+	}
+	if fi, err := fs.root.Stat(path); err != nil {
+		return fs.errCode(err)
+	} else if mode & ^uint32(fuse.S_IFREG|fuse.S_IFDIR|0777) != 0 || (fi.Mode()&os.ModeDir != 0) != (mode&fuse.S_IFDIR != 0) {
+		return -fuse.ENOSYS
+	} else {
+		return 0
+	}
+}
+
+func (fs *keepFS) fillStat(stat *fuse.Stat_t, fi os.FileInfo) {
+	defer fs.debugPanics()
+	var m uint32
+	if fi.IsDir() {
+		m = m | fuse.S_IFDIR
+	} else {
+		m = m | fuse.S_IFREG
+	}
+	m = m | uint32(fi.Mode()&os.ModePerm)
+	stat.Mode = m
+	stat.Nlink = 1
+	stat.Size = fi.Size()
+	t := fuse.NewTimespec(fi.ModTime())
+	stat.Mtim = t
+	stat.Ctim = t
+	stat.Atim = t
+	stat.Birthtim = t
+	stat.Blksize = 1024
+	stat.Blocks = (stat.Size + stat.Blksize - 1) / stat.Blksize
+	if fs.Uid > 0 && int64(fs.Uid) < 1<<31 {
+		stat.Uid = uint32(fs.Uid)
+	}
+	if fs.Gid > 0 && int64(fs.Gid) < 1<<31 {
+		stat.Gid = uint32(fs.Gid)
+	}
+}
+
+func (fs *keepFS) Write(path string, buf []byte, ofst int64, fh uint64) (n int) {
+	defer fs.debugPanics()
+	if fs.ReadOnly {
+		return -fuse.EROFS
+	}
+	f := fs.lookupFH(fh)
+	if f == nil {
+		return -fuse.EBADF
+	}
+	f.Lock()
+	defer f.Unlock()
+	if _, err := f.Seek(ofst, io.SeekStart); err != nil {
+		return fs.errCode(err)
+	}
+	n, err := f.Write(buf)
+	if err != nil {
+		log.Printf("error writing %q: %s", path, err)
+		return fs.errCode(err)
+	}
+	return n
+}
+
+func (fs *keepFS) Read(path string, buf []byte, ofst int64, fh uint64) (n int) {
+	defer fs.debugPanics()
+	f := fs.lookupFH(fh)
+	if f == nil {
+		return -fuse.EBADF
+	}
+	f.Lock()
+	defer f.Unlock()
+	if _, err := f.Seek(ofst, io.SeekStart); err != nil {
+		return fs.errCode(err)
+	}
+	n, err := f.Read(buf)
+	if err != nil && err != io.EOF {
+		log.Printf("error reading %q: %s", path, err)
+		return fs.errCode(err)
+	}
+	return n
+}
+
+func (fs *keepFS) Readdir(path string,
+	fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
+	ofst int64,
+	fh uint64) (errc int) {
+	defer fs.debugPanics()
+	f := fs.lookupFH(fh)
+	if f == nil {
+		return -fuse.EBADF
+	}
+	fill(".", nil, 0)
+	fill("..", nil, 0)
+	var stat fuse.Stat_t
+	fis, err := f.Readdir(-1)
+	if err != nil {
+		return fs.errCode(err)
+	}
+	for _, fi := range fis {
+		fs.fillStat(&stat, fi)
+		fill(fi.Name(), &stat, 0)
+	}
+	return 0
+}
+
+func (fs *keepFS) Fsync(path string, datasync bool, fh uint64) int {
+	defer fs.debugPanics()
+	f := fs.lookupFH(fh)
+	if f == nil {
+		return -fuse.EBADF
+	}
+	return fs.errCode(f.Sync())
+}
+
+func (fs *keepFS) Fsyncdir(path string, datasync bool, fh uint64) int {
+	return fs.Fsync(path, datasync, fh)
+}
+
+// debugPanics (when deferred by keepFS handlers) prints an error and
+// stack trace on stderr when a handler crashes. (Without this,
+// cgofuse recovers from panics silently and returns EIO.)
+func (fs *keepFS) debugPanics() {
+	if err := recover(); err != nil {
+		log.Printf("(%T) %v", err, err)
+		debug.PrintStack()
+		panic(err)
+	}
+}
diff --git a/lib/mount/fs_test.go b/lib/mount/fs_test.go
new file mode 100644
index 0000000..1e63b76
--- /dev/null
+++ b/lib/mount/fs_test.go
@@ -0,0 +1,49 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package mount
+
+import (
+	"testing"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
+	"git.curoverse.com/arvados.git/sdk/go/keepclient"
+	"github.com/curoverse/cgofuse/fuse"
+	check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+	check.TestingT(t)
+}
+
+var _ = check.Suite(&FSSuite{})
+
+type FSSuite struct{}
+
+func (*FSSuite) TestFuseInterface(c *check.C) {
+	var _ fuse.FileSystemInterface = &keepFS{}
+}
+
+func (*FSSuite) TestOpendir(c *check.C) {
+	client := arvados.NewClientFromEnv()
+	ac, err := arvadosclient.New(client)
+	c.Assert(err, check.IsNil)
+	kc, err := keepclient.MakeKeepClient(ac)
+	c.Assert(err, check.IsNil)
+
+	var fs fuse.FileSystemInterface = &keepFS{
+		Client:     client,
+		KeepClient: kc,
+	}
+	fs.Init()
+	errc, fh := fs.Opendir("/by_id")
+	c.Check(errc, check.Equals, 0)
+	c.Check(fh, check.Not(check.Equals), uint64(0))
+	c.Check(fh, check.Not(check.Equals), invalidFH)
+	errc, fh = fs.Opendir("/bogus")
+	c.Check(errc, check.Equals, -fuse.ENOENT)
+	c.Check(fh, check.Equals, invalidFH)
+}

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list