# # FUSE driver for Arvados Keep # import os import sys import llfuse import errno import stat import threading import arvados import pprint import arvados.events import re import apiclient import json import logging from time import time from llfuse import FUSEError class FreshBase(object): '''Base class for maintaining fresh/stale state to determine when to update.''' def __init__(self): self._stale = True self._poll = False self._last_update = time() self._poll_time = 60 # Mark the value as stale def invalidate(self): self._stale = True # Test if the entries dict is stale def stale(self): if self._stale: return True if self._poll: return (self._last_update + self._poll_time) < time() return False def fresh(self): self._stale = False self._last_update = time() class File(FreshBase): '''Base for file objects.''' def __init__(self, parent_inode): super(File, self).__init__() self.inode = None self.parent_inode = parent_inode def size(self): return 0 def readfrom(self, off, size): return '' class StreamReaderFile(File): '''Wraps a StreamFileReader as a file.''' def __init__(self, parent_inode, reader): super(StreamReaderFile, self).__init__(parent_inode) self.reader = reader def size(self): return self.reader.size() def readfrom(self, off, size): return self.reader.readfrom(off, size) def stale(self): return False class ObjectFile(File): '''Wraps a dict as a serialized json object.''' def __init__(self, parent_inode, contents): super(ObjectFile, self).__init__(parent_inode) self.contentsdict = contents self.uuid = self.contentsdict['uuid'] self.contents = json.dumps(self.contentsdict, indent=4, sort_keys=True) def size(self): return len(self.contents) def readfrom(self, off, size): return self.contents[off:(off+size)] class Directory(FreshBase): '''Generic directory object, backed by a dict. Consists of a set of entries with the key representing the filename and the value referencing a File or Directory object. ''' def __init__(self, parent_inode): super(Directory, self).__init__() '''parent_inode is the integer inode number''' self.inode = None if not isinstance(parent_inode, int): raise Exception("parent_inode should be an int") self.parent_inode = parent_inode self._entries = {} # Overriden by subclasses to implement logic to update the entries dict # when the directory is stale def update(self): pass # Only used when computing the size of the disk footprint of the directory # (stub) def size(self): return 0 def checkupdate(self): if self.stale(): try: self.update() except apiclient.errors.HttpError as e: logging.debug(e) def __getitem__(self, item): self.checkupdate() return self._entries[item] def items(self): self.checkupdate() return self._entries.items() def __iter__(self): self.checkupdate() return self._entries.iterkeys() def __contains__(self, k): self.checkupdate() return k in self._entries def merge(self, items, fn, same, new_entry): '''Helper method for updating the contents of the directory. items: array with new directory contents fn: function to take an entry in 'items' and return the desired file or directory name same: function to compare an existing entry with an entry in the items list to determine whether to keep the existing entry. new_entry: function to create a new directory entry from array entry. ''' oldentries = self._entries self._entries = {} for i in items: n = fn(i) if n in oldentries and same(oldentries[n], i): self._entries[n] = oldentries[n] del oldentries[n] else: self._entries[n] = self.inodes.add_entry(new_entry(i)) for n in oldentries: llfuse.invalidate_entry(self.inode, str(n)) self.inodes.del_entry(oldentries[n]) self.fresh() class CollectionDirectory(Directory): '''Represents the root of a directory tree holding a collection.''' def __init__(self, parent_inode, inodes, collection_locator): super(CollectionDirectory, self).__init__(parent_inode) self.inodes = inodes self.collection_locator = collection_locator def same(self, i): return i['uuid'] == self.collection_locator def update(self): try: collection = arvados.CollectionReader(self.collection_locator) for s in collection.all_streams(): cwd = self for part in s.name().split('/'): if part != '' and part != '.': if part not in cwd._entries: cwd._entries[part] = self.inodes.add_entry(Directory(cwd.inode)) cwd = cwd._entries[part] for k, v in s.files().items(): cwd._entries[k] = self.inodes.add_entry(StreamReaderFile(cwd.inode, v)) self.fresh() return True except Exception as detail: logging.debug("arv-mount %s: error: %s" % (self.collection_locator,detail)) return False class MagicDirectory(Directory): '''A special directory that logically contains the set of all extant keep locators. When a file is referenced by lookup(), it is tested to see if it is a valid keep locator to a manifest, and if so, loads the manifest contents as a subdirectory of this directory with the locator as the directory name. Since querying a list of all extant keep locators is impractical, only collections that have already been accessed are visible to readdir(). ''' def __init__(self, parent_inode, inodes): super(MagicDirectory, self).__init__(parent_inode) self.inodes = inodes def __contains__(self, k): if k in self._entries: return True try: e = self.inodes.add_entry(CollectionDirectory(self.inode, self.inodes, k)) if e.update(): self._entries[k] = e return True else: return False except Exception as e: logging.debug('arv-mount exception keep %s', e) return False def __getitem__(self, item): if item in self: return self._entries[item] else: raise KeyError("No collection with id " + item) class TagsDirectory(Directory): '''A special directory that contains as subdirectories all tags visible to the user.''' def __init__(self, parent_inode, inodes, api, poll_time=60): super(TagsDirectory, self).__init__(parent_inode) self.inodes = inodes self.api = api try: arvados.events.subscribe(self.api, [['object_uuid', 'is_a', 'arvados#link']], lambda ev: self.invalidate()) except: self._poll = True self._poll_time = poll_time def invalidate(self): with llfuse.lock: super(TagsDirectory, self).invalidate() for a in self._entries: self._entries[a].invalidate() def update(self): tags = self.api.links().list(filters=[['link_class', '=', 'tag']], select=['name'], distinct = True).execute() if "items" in tags: self.merge(tags['items'], lambda i: i['name'], lambda a, i: a.tag == i, lambda i: TagDirectory(self.inode, self.inodes, self.api, i['name'], poll=self._poll, poll_time=self._poll_time)) class TagDirectory(Directory): '''A special directory that contains as subdirectories all collections visible to the user that are tagged with a particular tag. ''' def __init__(self, parent_inode, inodes, api, tag, poll=False, poll_time=60): super(TagDirectory, self).__init__(parent_inode) self.inodes = inodes self.api = api self.tag = tag self._poll = poll self._poll_time = poll_time def update(self): taggedcollections = self.api.links().list(filters=[['link_class', '=', 'tag'], ['name', '=', self.tag], ['head_uuid', 'is_a', 'arvados#collection']], select=['head_uuid']).execute() self.merge(taggedcollections['items'], lambda i: i['head_uuid'], lambda a, i: a.collection_locator == i['head_uuid'], lambda i: CollectionDirectory(self.inode, self.inodes, i['head_uuid'])) class GroupsDirectory(Directory): '''A special directory that contains as subdirectories all groups visible to the user.''' def __init__(self, parent_inode, inodes, api, poll_time=60): super(GroupsDirectory, self).__init__(parent_inode) self.inodes = inodes self.api = api try: arvados.events.subscribe(self.api, [], lambda ev: self.invalidate()) except: self._poll = True self._poll_time = poll_time def invalidate(self): with llfuse.lock: super(GroupsDirectory, self).invalidate() for a in self._entries: self._entries[a].invalidate() def update(self): groups = self.api.groups().list().execute() self.merge(groups['items'], lambda i: i['uuid'], lambda a, i: a.uuid == i['uuid'], lambda i: GroupDirectory(self.inode, self.inodes, self.api, i, poll=self._poll, poll_time=self._poll_time)) class GroupDirectory(Directory): '''A special directory that contains the contents of a group.''' def __init__(self, parent_inode, inodes, api, uuid, poll=False, poll_time=60): super(GroupDirectory, self).__init__(parent_inode) self.inodes = inodes self.api = api self.uuid = uuid['uuid'] self._poll = poll self._poll_time = poll_time def invalidate(self): with llfuse.lock: super(GroupDirectory, self).invalidate() for a in self._entries: self._entries[a].invalidate() def createDirectory(self, i): if re.match(r'[0-9a-f]{32}\+\d+', i['uuid']): return CollectionDirectory(self.inode, self.inodes, i['uuid']) elif re.match(r'[a-z0-9]{5}-j7d0g-[a-z0-9]{15}', i['uuid']): return GroupDirectory(self.parent_inode, self.inodes, self.api, i, self._poll, self._poll_time) elif re.match(r'[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}', i['uuid']): return ObjectFile(self.parent_inode, i) return None def update(self): contents = self.api.groups().contents(uuid=self.uuid, include_linked=True).execute() links = {} for a in contents['links']: links[a['head_uuid']] = a['name'] def choose_name(i): if i['uuid'] in links: return links[i['uuid']] else: return i['uuid'] def same(a, i): if isinstance(a, CollectionDirectory): return a.collection_locator == i['uuid'] elif isinstance(a, GroupDirectory): return a.uuid == i['uuid'] elif isinstance(a, ObjectFile): return a.uuid == i['uuid'] and not a.stale() return False self.merge(contents['items'], choose_name, same, self.createDirectory) class FileHandle(object): '''Connects a numeric file handle to a File or Directory object that has been opened by the client.''' def __init__(self, fh, entry): self.fh = fh self.entry = entry class Inodes(object): '''Manage the set of inodes. This is the mapping from a numeric id to a concrete File or Directory object''' def __init__(self): self._entries = {} self._counter = llfuse.ROOT_INODE def __getitem__(self, item): return self._entries[item] def __setitem__(self, key, item): self._entries[key] = item def __iter__(self): return self._entries.iterkeys() def items(self): return self._entries.items() def __contains__(self, k): return k in self._entries def add_entry(self, entry): entry.inode = self._counter self._entries[entry.inode] = entry self._counter += 1 return entry def del_entry(self, entry): llfuse.invalidate_inode(entry.inode) del self._entries[entry.inode] class Operations(llfuse.Operations): '''This is the main interface with llfuse. The methods on this object are called by llfuse threads to service FUSE events to query and read from the file system. llfuse has its own global lock which is acquired before calling a request handler, so request handlers do not run concurrently unless the lock is explicitly released with llfuse.lock_released.''' def __init__(self, uid, gid, debug=False): super(Operations, self).__init__() if debug: logging.basicConfig(level=logging.DEBUG) logging.info("arv-mount debug enabled") self.inodes = Inodes() self.uid = uid self.gid = gid # dict of inode to filehandle self._filehandles = {} self._filehandles_counter = 1 # Other threads that need to wait until the fuse driver # is fully initialized should wait() on this event object. self.initlock = threading.Event() def init(self): # Allow threads that are waiting for the driver to be finished # initializing to continue self.initlock.set() def access(self, inode, mode, ctx): return True def getattr(self, inode): if inode not in self.inodes: raise llfuse.FUSEError(errno.ENOENT) e = self.inodes[inode] entry = llfuse.EntryAttributes() entry.st_ino = inode entry.generation = 0 entry.entry_timeout = 300 entry.attr_timeout = 300 entry.st_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH if isinstance(e, Directory): entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IFDIR else: entry.st_mode |= stat.S_IFREG entry.st_nlink = 1 entry.st_uid = self.uid entry.st_gid = self.gid entry.st_rdev = 0 entry.st_size = e.size() entry.st_blksize = 1024 entry.st_blocks = e.size()/1024 if e.size()/1024 != 0: entry.st_blocks += 1 entry.st_atime = 0 entry.st_mtime = 0 entry.st_ctime = 0 return entry def lookup(self, parent_inode, name): logging.debug("arv-mount lookup: parent_inode %i name %s", parent_inode, name) inode = None if name == '.': inode = parent_inode else: if parent_inode in self.inodes: p = self.inodes[parent_inode] if name == '..': inode = p.parent_inode elif name in p: inode = p[name].inode if inode != None: return self.getattr(inode) else: raise llfuse.FUSEError(errno.ENOENT) def open(self, inode, flags): if inode in self.inodes: p = self.inodes[inode] else: raise llfuse.FUSEError(errno.ENOENT) if (flags & os.O_WRONLY) or (flags & os.O_RDWR): raise llfuse.FUSEError(errno.EROFS) if isinstance(p, Directory): raise llfuse.FUSEError(errno.EISDIR) fh = self._filehandles_counter self._filehandles_counter += 1 self._filehandles[fh] = FileHandle(fh, p) return fh def read(self, fh, off, size): logging.debug("arv-mount read %i %i %i", fh, off, size) if fh in self._filehandles: handle = self._filehandles[fh] else: raise llfuse.FUSEError(errno.EBADF) try: with llfuse.lock_released: return handle.entry.readfrom(off, size) except: raise llfuse.FUSEError(errno.EIO) def release(self, fh): if fh in self._filehandles: del self._filehandles[fh] def opendir(self, inode): logging.debug("arv-mount opendir: inode %i", inode) if inode in self.inodes: p = self.inodes[inode] else: raise llfuse.FUSEError(errno.ENOENT) if not isinstance(p, Directory): raise llfuse.FUSEError(errno.ENOTDIR) fh = self._filehandles_counter self._filehandles_counter += 1 if p.parent_inode in self.inodes: parent = self.inodes[p.parent_inode] else: raise llfuse.FUSEError(errno.EIO) self._filehandles[fh] = FileHandle(fh, [('.', p), ('..', parent)] + list(p.items())) return fh def readdir(self, fh, off): logging.debug("arv-mount readdir: fh %i off %i", fh, off) if fh in self._filehandles: handle = self._filehandles[fh] else: raise llfuse.FUSEError(errno.EBADF) logging.debug("arv-mount handle.entry %s", handle.entry) e = off while e < len(handle.entry): if handle.entry[e][1].inode in self.inodes: yield (handle.entry[e][0], self.getattr(handle.entry[e][1].inode), e+1) e += 1 def releasedir(self, fh): del self._filehandles[fh] def statfs(self): st = llfuse.StatvfsData() st.f_bsize = 1024 * 1024 st.f_blocks = 0 st.f_files = 0 st.f_bfree = 0 st.f_bavail = 0 st.f_ffree = 0 st.f_favail = 0 st.f_frsize = 0 return st # The llfuse documentation recommends only overloading functions that # are actually implemented, as the default implementation will raise ENOSYS. # However, there is a bug in the llfuse default implementation of create() # "create() takes exactly 5 positional arguments (6 given)" which will crash # arv-mount. # The workaround is to implement it with the proper number of parameters, # and then everything works out. def create(self, p1, p2, p3, p4, p5): raise llfuse.FUSEError(errno.EROFS)