#!/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 # # 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/corinth') 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('/') lookup_cache = {} def load_metadata(path): if type(path) != type([]): path = parse_path(path) if path is None or len(path) < 2: return None path = tuple(path) if path in lookup_cache: return lookup_cache[path] 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:]: lookup_cache[path] = None return None lookup_cache[path] = cumulus.MetadataItem(item, store) return lookup_cache[path] 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, snapshot, 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 lookup_cache[(snapshot,) + tuple(itempath)] = m 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[0], 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 buf = '' for b in m.data(): if size == 0: break # Skip over this data block if we can, based on remaining data # offset and block size. (bseg, boff, bcsum, bslice) = store.parse_ref(b) if bslice is not None: bsize = bslice[1] if offset >= bsize: offset -= bsize continue # Otherwise, load the data block and read any data out of it we # can. data = store.get(b) if offset >= len(data): offset -= len(data) continue if offset > 0: data = data[offset:] offset = 0 if size < len(data): data = data[0:size] buf += data size -= len(data) return buf 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()