Start a proof-of-concept FUSE interface to old snapshots.
[cumulus.git] / contrib / cumulus-fuse
diff --git a/contrib/cumulus-fuse b/contrib/cumulus-fuse
new file mode 100755 (executable)
index 0000000..8849335
--- /dev/null
@@ -0,0 +1,199 @@
+#!/usr/bin/python
+#
+# FUSE interface to Cumulus, allowing snapshots to be mounted as a virtual
+# filesystem.
+#
+# Copyright (C) 2006-2008  The Regents of the University of California
+# Written by Michael Vrable <mvrable@cs.ucsd.edu>
+#
+# This program can be distributed under the terms of the GNU GPL, either
+# version 2 of the License, or (at your option) any later version.  See the
+# file COPYING.
+
+import itertools, os, stat, errno
+import fuse
+from fuse import Fuse
+import cumulus
+import cumulus.metadata
+
+fuse.fuse_python_api = (0, 2)
+
+# TODO: Figure out FUSE option parsing
+lowlevel = cumulus.LowlevelDataStore('/backups/lbs/turin')
+store = cumulus.ObjectStore(lowlevel)
+
+def _printable(ptr):
+    if ptr is None: return None
+    return tuple(x[1] for x in ptr)
+
+def parse_path(path):
+    """Strip leading slashe from path, and split apart into components."""
+    if not path.startswith('/'):
+        return None
+    if path == '/':
+        return []
+    else:
+        return path[1:].split('/')
+
+def load_metadata(path):
+    if type(path) != type([]):
+        path = parse_path(path)
+
+    if path is None or len(path) < 2:
+        return None
+
+    snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
+    metadata = cumulus.metadata.Metadata(store, snapshot['Root'])
+    ptr = metadata.search(lambda x: cmp(x, path[1:]))
+    item = metadata._read(ptr)
+    if metadata._get_path(item) != path[1:]:
+        return None
+    return cumulus.MetadataItem(item, store)
+
+class MyStat(fuse.Stat):
+    def __init__(self):
+        self.st_mode = 0
+        self.st_ino = 0
+        self.st_dev = 0
+        self.st_nlink = 0
+        self.st_uid = 0
+        self.st_gid = 0
+        self.st_size = 0
+        self.st_atime = 0
+        self.st_mtime = 0
+        self.st_ctime = 0
+
+class CumulusFS(Fuse):
+    def getattr(self, path):
+        st = MyStat()
+        path = parse_path(path)
+
+        if path is None: return -errno.ENOENT
+        if path == []:
+            # Root directory
+            st.st_mode = stat.S_IFDIR | 0755
+            st.st_nlink = 2
+            return st
+
+        snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
+        if len(path) == 1:
+            # Snapshot directory
+            st.st_mode = stat.S_IFDIR | 0755
+            st.st_nlink = 2
+        else:
+            # File contained within a snapshot
+            m = load_metadata(path)
+            if m is None:
+                return -errno.ENOENT
+
+            st.st_nlink = 1
+            st.st_uid = m.items.user[0]
+            st.st_gid = m.items.group[0]
+            st.st_mtime = m.items.mtime
+            st.st_ctime = m.items.ctime
+            st.st_atime = m.items.mtime
+            if m.items.type == 'd':
+                st.st_mode = stat.S_IFDIR | m.items.mode
+                st.st_nlink = 2
+            elif m.items.type == 'l':
+                st.st_mode = stat.S_IFLNK | m.items.mode
+            else:
+                st.st_mode = stat.S_IFREG | m.items.mode
+                st.st_size = m.items.size
+
+        return st
+
+    def _cumulus_readdir(self, metadata, path):
+        # Find pointer to base directory in metadata
+        ptr1 = metadata.search(lambda x: cmp(x, path))
+
+        # Find pointer to end of directory contents
+        def endcmp(p1):
+            def _cmp(p2):
+                if len(p2) > len(p1): p2 = p2[0:len(p1)]
+                if p2 > p1:
+                    return 1
+                else:
+                    return -1
+            return _cmp
+        ptr2 = metadata.search(endcmp(path), ptr1)
+
+        # Scan through looking for top-level files and directories.  Skip over
+        # data for files in subdirectories.
+        while metadata._cmp(ptr1, ptr2) < 0:
+            item = metadata._read(ptr1)
+            m = cumulus.MetadataItem(item, store)
+            if m.items.name == '.':
+                itempath = []
+            else:
+                itempath = m.items.name.split('/')
+            assert itempath[0:len(path)] == path
+
+            if len(itempath) == len(path):
+                ptr1 = metadata._advance(ptr1)
+                continue
+
+            if len(itempath) > len(path) + 1:
+                ptr1 = metadata.search(endcmp(itempath[0:len(path)+1]),
+                                       ptr1, ptr2)
+                continue
+
+            yield itempath[len(path)]
+            ptr1 = metadata._advance(ptr1)
+
+    def readdir(self, path, offset):
+        if path == '/':
+            for r in itertools.chain(('.', '..'), lowlevel.list_snapshots()):
+                yield fuse.Direntry(r)
+        else:
+            path = parse_path(path)
+            if path is None:
+                return
+            snapshot = cumulus.parse_full(store.load_snapshot(path[0]))
+            metadata = cumulus.metadata.Metadata(store, snapshot['Root'])
+            for r in itertools.chain(('.', '..'),
+                                     self._cumulus_readdir(metadata, path[1:])):
+                yield fuse.Direntry(r)
+
+    def readlink(self, path):
+        m = load_metadata(path)
+        if m is None:
+            return -errno.ENOENT
+        else:
+            return m.items.target
+
+    def open(self, path, flags):
+        m = load_metadata(path)
+        if m is None:
+            return -errno.ENOENT
+        accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
+        if (flags & accmode) != os.O_RDONLY:
+            return -errno.EACCES
+
+    def read(self, path, size, offset):
+        m = load_metadata(path)
+        if m is None:
+            return -errno.ENOENT
+
+        return '\0' * size
+
+def main():
+    usage="""
+cumulus-fuse: Mount cumulus snapshots as a filesystem
+
+""" + Fuse.fusage
+    server = CumulusFS(version="%prog " + fuse.__version__,
+                       usage=usage,
+                       dash_s_do='setsingle')
+
+    server.parser.add_option(mountopt="root", metavar="PATH", default='/',
+                             help="read snapshots from PATH [default: %default]")
+
+    server.parse(errex=1)
+    print server.fuse_args
+    print server.fuse_args.assemble()
+    server.main()
+    store.cleanup()
+
+if __name__ == '__main__':
+    main()