3198: Add enable_write flag to FUSE and --enable-write and --read-only to
authorPeter Amstutz <peter.amstutz@curoverse.com>
Tue, 16 Jun 2015 19:35:31 +0000 (15:35 -0400)
committerPeter Amstutz <peter.amstutz@curoverse.com>
Tue, 16 Jun 2015 20:21:11 +0000 (16:21 -0400)
arv-mount.  Update documentation.  Code cleanup in locators_and_ranges() and
set_state().

doc/user/tutorials/tutorial-keep-mount.html.textile.liquid
sdk/python/arvados/_ranges.py
sdk/python/arvados/arvfile.py
services/fuse/arvados_fuse/__init__.py
services/fuse/bin/arv-mount
services/fuse/tests/test_mount.py

index a41fede744e92dfaab1b43782a81121d57fff94b..c219544d238faf807f6c0f8eed15c41d4e1e3c84 100644 (file)
@@ -14,7 +14,7 @@ h2. Arv-mount
 
 * You can browse, open and read Keep entries as if they are regular files.
 * It is easy for existing tools to access files in Keep.
-* Data is downloaded on demand.  It is not necessary to download an entire file or collection to start processing.
+* Data is streamed on demand.  It is not necessary to download an entire file or collection to start processing.
 
 The default mode permits browsing any collection in Arvados as a subdirectory under the mount directory.  To avoid having to fetch a potentially large list of all collections, collection directories only come into existence when explicitly accessed by their Keep locator. For instance, a collection may be found by its content hash in the @keep/by_id@ directory.
 
@@ -34,3 +34,22 @@ var-GS000016015-ASM.tsv.bz2
 The last line unmounts Keep.  Subdirectories will no longer be accessible.
 
 Within each directory on Keep, there is a @.arvados#collection@ file that does not show up with @ls@. Its contents include, for instance, the @portable_data_hash@, which is the same as the Keep locator.
+
+h3. Modifying files and directories in Keep
+
+By default, all files in the Keep mount are read only.  However, @arv-mount --enable-write@ enables you to perform the following operations using normal Unix command line tools (@touch@, @mv@, @rm@, @mkdir@, @rmdir@) and your own programs using standard POSIX file system APIs:
+
+* Create, update, rename and delete individual files within collections
+* Create and delete subdirectories inside collections
+* Move files and directories within and between collections
+* Create and delete collections within a project (using @mkdir@ and @rmdir@ in a project directory)
+
+Not supported:
+
+* Symlinks, hard links
+* Changing permissions
+* Extended attributes
+
+If multiple clients try to modify the same file in the same collection, this result in a conflict.  In this case, the most recent file wins, and the "loser" will be renamed to a conflict file in the form @name~YYYYMMDD-HHMMSS~conflict~@.
+
+Please note this feature is in beta testing.  In particular, the conflict mechanism is itself currently subject to race condiditions with potential for data loss when a collection is being modified simultaneously by multiple clients.  This issue will be resolved in future development.
index 371d003995fb17c276a0638d95e764f80d4d96ab..83437b2adb9f7817ac0b5ee210cfdb6d50915b90 100644 (file)
@@ -99,9 +99,7 @@ def locators_and_ranges(data_locators, range_start, range_size, limit=None):
 
     # We should always start at the first segment due to the binary
     # search.
-    while i < len(data_locators):
-        if limit and len(resp) > limit:
-            break
+    while i < len(data_locators) and len(resp) != limit:
         dl = data_locators[i]
         block_start = dl.range_start
         block_size = dl.range_size
index 7d6d67654ab0eaeadfb655caf464cf758e41a57b..7cd64aa16e46d415516f1d6179df2a7c3fb6df2f 100644 (file)
@@ -311,28 +311,30 @@ class _BufferBlock(object):
         else:
             raise AssertionError("Buffer block is not writable")
 
+    STATE_TRANSITIONS = frozenset([
+            (WRITABLE, PENDING),
+            (PENDING, COMMITTED),
+            (PENDING, ERROR),
+            (ERROR, PENDING)])
+
     @synchronized
     def set_state(self, nextstate, val=None):
-        if ((self._state == _BufferBlock.WRITABLE and nextstate == _BufferBlock.PENDING) or
-            (self._state == _BufferBlock.PENDING and nextstate == _BufferBlock.COMMITTED) or
-            (self._state == _BufferBlock.PENDING and nextstate == _BufferBlock.ERROR) or
-            (self._state == _BufferBlock.ERROR and nextstate == _BufferBlock.PENDING)):
-            self._state = nextstate
-
-            if self._state == _BufferBlock.PENDING:
-                self.wait_for_commit.clear()
-
-            if self._state == _BufferBlock.COMMITTED:
-                self._locator = val
-                self.buffer_view = None
-                self.buffer_block = None
-                self.wait_for_commit.set()
-
-            if self._state == _BufferBlock.ERROR:
-                self.error = val
-                self.wait_for_commit.set()
-        else:
+        if (self._state, nextstate) not in self.STATE_TRANSITIONS:
             raise StateChangeError("Invalid state change from %s to %s" % (self.state, nextstate), self.state, nextstate)
+        self._state = nextstate
+
+        if self._state == _BufferBlock.PENDING:
+            self.wait_for_commit.clear()
+
+        if self._state == _BufferBlock.COMMITTED:
+            self._locator = val
+            self.buffer_view = None
+            self.buffer_block = None
+            self.wait_for_commit.set()
+
+        if self._state == _BufferBlock.ERROR:
+            self.error = val
+            self.wait_for_commit.set()
 
     @synchronized
     def state(self):
index 91e1907f20ba5f32c002bbe2093e0034a3327f09..a2b91a67187a27122bece6c8c381fcce823cdc19 100644 (file)
@@ -259,7 +259,7 @@ class Operations(llfuse.Operations):
 
     """
 
-    def __init__(self, uid, gid, encoding="utf-8", inode_cache=None, num_retries=4):
+    def __init__(self, uid, gid, encoding="utf-8", inode_cache=None, num_retries=4, enable_write=False):
         super(Operations, self).__init__()
 
         if not inode_cache:
@@ -267,6 +267,7 @@ class Operations(llfuse.Operations):
         self.inodes = Inodes(inode_cache, encoding=encoding)
         self.uid = uid
         self.gid = gid
+        self.enable_write = enable_write
 
         # dict of inode to filehandle
         self._filehandles = {}
@@ -349,7 +350,7 @@ class Operations(llfuse.Operations):
             if isinstance(e, FuseArvadosFile):
                 entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
 
-        if e.writable():
+        if self.enable_write and e.writable():
             entry.st_mode |= stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
 
         entry.st_nlink = 1
@@ -536,6 +537,9 @@ class Operations(llfuse.Operations):
         return st
 
     def _check_writable(self, inode_parent):
+        if not self.enable_write:
+            raise llfuse.FUSEError(errno.EROFS)
+
         if inode_parent in self.inodes:
             p = self.inodes[inode_parent]
         else:
index 6e38728a4da00e631faf12e53aefd62d2c1694e6..34985002340ce1d8b948c4257f12287e8682aa07 100755 (executable)
@@ -50,6 +50,9 @@ with "--".
     parser.add_argument('--file-cache', type=int, help="File data cache size, in bytes (default 256MiB)", default=256*1024*1024)
     parser.add_argument('--directory-cache', type=int, help="Directory data cache size, in bytes (default 128MiB)", default=128*1024*1024)
 
+    parser.add_argument('--read-only', action='store_false', help="Mount will be read only (default)", dest="enable_write", default=False)
+    parser.add_argument('--enable-write', action='store_true', help="Mount will be read-write", dest="enable_write", default=False)
+
     parser.add_argument('--exec', type=str, nargs=argparse.REMAINDER,
                         dest="exec_args", metavar=('command', 'args', '...', '--'),
                         help="""Mount, run a command, then unmount and exit""")
@@ -84,14 +87,17 @@ with "--".
         arvados.logger.setLevel(logging.DEBUG)
         logger.debug("arv-mount debugging enabled")
 
+    logger.warn("enable write is %s", args.enable_write)
+
     try:
         # Create the request handler
         operations = Operations(os.getuid(),
                                 os.getgid(),
                                 encoding=args.encoding,
-                                inode_cache=InodeCache(cap=args.directory_cache))
+                                inode_cache=InodeCache(cap=args.directory_cache),
+                                enable_write=args.enable_write)
         api = ThreadSafeApiCache(apiconfig=arvados.config.settings(),
-                                  keep_params={"block_cache": arvados.keep.KeepBlockCache(args.file_cache)})
+                                 keep_params={"block_cache": arvados.keep.KeepBlockCache(args.file_cache)})
 
         usr = api.users().current().execute(num_retries=args.retries)
         now = time.time()
index 31d4e7a0792ff6f811e1a166923079437b3b8f35..ac5af5ba3a90b9e2968b83d44d1a9e5e42361d3b 100644 (file)
@@ -37,7 +37,7 @@ class MountTestBase(unittest.TestCase):
         self.api = arvados.safeapi.ThreadSafeApiCache(arvados.config.settings())
 
     def make_mount(self, root_class, **root_kwargs):
-        self.operations = fuse.Operations(os.getuid(), os.getgid())
+        self.operations = fuse.Operations(os.getuid(), os.getgid(), enable_write=True)
         self.operations.inodes.add_entry(root_class(
             llfuse.ROOT_INODE, self.operations.inodes, self.api, 0, **root_kwargs))
         llfuse.init(self.operations, self.mounttmp, [])
@@ -47,7 +47,8 @@ class MountTestBase(unittest.TestCase):
         return self.operations.inodes[llfuse.ROOT_INODE]
 
     def tearDown(self):
-        self.pool.close()
+        self.pool.terminate()
+        self.pool.join()
         del self.pool
 
         # llfuse.close is buggy, so use fusermount instead.
@@ -519,7 +520,7 @@ class FuseUpdateFileTest(MountTestBase):
             m.new_collection(collection.api_response(), collection)
         self.assertTrue(m.writable())
 
-        # See note in FuseWriteFileTest
+        # See note in MountTestBase.setUp
         self.pool.apply(fuseUpdateFileTestHelper, (self.mounttmp,))
 
         collection2 = self.api.collections().get(uuid=collection.manifest_locator()).execute()
@@ -802,7 +803,7 @@ class FuseFileConflictTest(MountTestBase):
             with collection2.open("file1.txt", "w") as f:
                 f.write("foo")
 
-        # See comment in FuseWriteFileTest
+        # See note in MountTestBase.setUp
         self.pool.apply(fuseFileConflictTestHelper, (self.mounttmp,))
 
 
@@ -838,7 +839,7 @@ class FuseUnlinkOpenFileTest(MountTestBase):
         with llfuse.lock:
             m.new_collection(collection.api_response(), collection)
 
-        # See comment in FuseWriteFileTest
+        # See note in MountTestBase.setUp
         self.pool.apply(fuseUnlinkOpenFileTest, (self.mounttmp,))
 
         self.assertEqual(collection.manifest_text(), "")
@@ -879,7 +880,7 @@ class FuseMvFileBetweenCollectionsTest(MountTestBase):
 
         m = self.make_mount(fuse.MagicDirectory)
 
-        # See comment in FuseWriteFileTest
+        # See note in MountTestBase.setUp
         self.pool.apply(fuseMvFileBetweenCollectionsTest1, (self.mounttmp,
                                                   collection1.manifest_locator(),
                                                   collection2.manifest_locator()))
@@ -950,7 +951,7 @@ class FuseMvDirBetweenCollectionsTest(MountTestBase):
 
         m = self.make_mount(fuse.MagicDirectory)
 
-        # See comment in FuseWriteFileTest
+        # See note in MountTestBase.setUp
         self.pool.apply(fuseMvDirBetweenCollectionsTest1, (self.mounttmp,
                                                   collection1.manifest_locator(),
                                                   collection2.manifest_locator()))