11209: "--unmount /path/..." unmounts /path and all fuse mounts below it.
authorTom Clegg <tom@curoverse.com>
Fri, 17 Mar 2017 20:32:10 +0000 (16:32 -0400)
committerTom Clegg <tom@curoverse.com>
Thu, 23 Mar 2017 22:09:56 +0000 (18:09 -0400)
services/fuse/arvados_fuse/command.py
services/fuse/arvados_fuse/unmount.py

index c9a090cbcf470efc1e0828ac4c70ce9e78aa094b..883af3652b096a062d16c95120f9f0965a693f7d 100644 (file)
@@ -91,9 +91,9 @@ 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")
+                          help="Forcefully unmount the specified mountpoint (if it's a fuse mount) and exit. Use /path/... to unmount all fuse mounts below /path as well as /path itself.")
         self.add_argument('--replace', action='store_true', default=False,
-                          help="Forcefully unmount any existing fuse mount before mounting")
+                          help="If a fuse mount is already present at mountpoint, forcefully unmount it 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")
@@ -146,7 +146,7 @@ class Mount(object):
 
     def run(self):
         if self.args.unmount:
-            unmount(self.args.mountpoint, timeout=self.args.unmount_timeout)
+            unmount_all(self.args.mountpoint, timeout=self.args.unmount_timeout)
         elif self.args.exec_args:
             self._run_exec()
         else:
index 8be549ef1a925256017e10f8193073a65da7f9f4..ab5ce4f6ab24833cf2a31c0e4072e85f6b85e774 100644 (file)
@@ -1,8 +1,49 @@
+import collections
 import errno
 import os
 import subprocess
 import time
 
+
+MountInfo = collections.namedtuple(
+    'MountInfo', ['is_fuse', 'major', 'minor', 'mnttype', 'path'])
+
+
+def mountinfo():
+    mi = []
+    with open('/proc/self/mountinfo') as f:
+        for m in f.readlines():
+            mntid, pmntid, dev, root, path, extra = m.split(" ", 5)
+            mnttype = extra.split(" - ")[1].split(" ", 1)[0]
+            major, minor = dev.split(":")
+            mi.append(MountInfo(
+                is_fuse=(mnttype == "fuse" or mnttype.startswith("fuse.")),
+                major=major,
+                minor=minor,
+                mnttype=mnttype,
+                path=path,
+            ))
+    return mi
+
+
+def unmount_all(path, timeout=10):
+    if not path.endswith("/..."):
+        return unmount(path, timeout=timeout)
+    root = os.path.realpath(path[:-4])
+
+    paths = []
+    for m in mountinfo():
+        if m.path == root or m.path.startswith(root+"/"):
+            paths.append(m.path)
+            if not m.is_fuse:
+                raise Exception(
+                    "cannot unmount {}: non-fuse mountpoint {}".format(
+                        path, m))
+    for path in sorted(paths, key=len, reverse=True):
+        unmount(path, timeout=timeout)
+    return len(paths) > 0
+
+
 def unmount(path, timeout=10):
     """Unmount the fuse mount at path.
 
@@ -28,14 +69,10 @@ def unmount(path, timeout=10):
             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
+        for m in mountinfo():
+            if m.is_fuse:
                 try:
-                    if os.path.realpath(mnt) == path:
+                    if os.path.realpath(m.path) == path:
                         was_mounted = True
                         mounted = True
                         break
@@ -44,9 +81,9 @@ def unmount(path, timeout=10):
         if not mounted:
             return was_mounted
 
-        major, minor = dev.split(":")
         try:
-            with open('/sys/fs/fuse/connections/'+str(minor)+'/abort', 'w') as f:
+            with open('/sys/fs/fuse/connections/{}/abort'.format(m.minor),
+                      'w') as f:
                 f.write("1")
         except OSError as e:
             if e.errno != errno.ENOENT: