11209: Add arv-mount --unmount and --replace flags
authorTom Clegg <tom@curoverse.com>
Thu, 16 Mar 2017 20:49:49 +0000 (16:49 -0400)
committerTom Clegg <tom@curoverse.com>
Thu, 23 Mar 2017 22:09:55 +0000 (18:09 -0400)
services/fuse/arvados_fuse/command.py
services/fuse/arvados_fuse/unmount.py [new file with mode: 0644]

index 66f8a4d39319fb9bdc8f864962bc9c02a0d57ac3..c9a090cbcf470efc1e0828ac4c70ce9e78aa094b 100644 (file)
@@ -90,6 +90,10 @@ class ArgumentParser(argparse.ArgumentParser):
 
         self.add_argument('--crunchstat-interval', type=float, help="Write stats to stderr every N seconds (default disabled)", default=0)
 
+        self.add_argument('--unmount', action='store_true', default=False,
+                          help="Forcefully unmount the specified mountpoint (if it's a fuse mount) and exit")
+        self.add_argument('--replace', action='store_true', default=False,
+                          help="Forcefully unmount any existing fuse mount before mounting")
         self.add_argument('--unmount-timeout',
                           type=float, default=2.0,
                           help="Time to wait for graceful shutdown after --exec program exits and filesystem is unmounted")
@@ -118,6 +122,8 @@ class Mount(object):
             exit(1)
 
     def __enter__(self):
+        if self.args.replace:
+            unmount(self.args.mountpoint, timeout=self.args.unmount_timeout)
         llfuse.init(self.operations, self.args.mountpoint, self._fuse_options())
         if self.listen_for_events and not self.args.disable_event_listening:
             self.operations.listen_for_events()
@@ -139,7 +145,9 @@ class Mount(object):
                                 self.args.unmount_timeout)
 
     def run(self):
-        if self.args.exec_args:
+        if self.args.unmount:
+            unmount(self.args.mountpoint, timeout=self.args.unmount_timeout)
+        elif self.args.exec_args:
             self._run_exec()
         else:
             self._run_standalone()
diff --git a/services/fuse/arvados_fuse/unmount.py b/services/fuse/arvados_fuse/unmount.py
new file mode 100644 (file)
index 0000000..8be549e
--- /dev/null
@@ -0,0 +1,61 @@
+import errno
+import os
+import subprocess
+import time
+
+def unmount(path, timeout=10):
+    """Unmount the fuse mount at path.
+
+    Unmounting is done by writing 1 to the "abort" control file in
+    sysfs to kill the fuse driver process, then executing "fusermount
+    -u -z" to detach the mount point, and repeating these steps until
+    the mount is no longer listed in /proc/self/mountinfo.
+
+    This procedure should enable a non-root user to reliably unmount
+    their own fuse filesystem without risk of deadlock.
+
+    Returns True if unmounting was successful, False if it wasn't a
+    fuse mount at all. Raises an exception if it cannot be unmounted.
+    """
+
+    path = os.path.realpath(path)
+
+    was_mounted = False
+    t0 = time.time()
+    delay = 0
+    while True:
+        if timeout and t0 + timeout < time.time():
+            raise Exception("timed out")
+
+        mounted = False
+        with open('/proc/self/mountinfo') as mi:
+            for m in mi.readlines():
+                mntid, pmntid, dev, root, mnt, extra = m.split(" ", 5)
+                mnttype = extra.split(" - ")[1].split(" ")[0]
+                if not (mnttype == "fuse" or mnttype.startswith("fuse.")):
+                    continue
+                try:
+                    if os.path.realpath(mnt) == path:
+                        was_mounted = True
+                        mounted = True
+                        break
+                except OSError:
+                    continue
+        if not mounted:
+            return was_mounted
+
+        major, minor = dev.split(":")
+        try:
+            with open('/sys/fs/fuse/connections/'+str(minor)+'/abort', 'w') as f:
+                f.write("1")
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise
+        try:
+            subprocess.check_call(["fusermount", "-u", "-z", path])
+        except subprocess.CalledProcessError:
+            pass
+
+        time.sleep(delay)
+        if delay == 0:
+            delay = 1