#!/usr/bin/python import fuse from fuse import Fuse from time import time import stat # for file properties import os # for filesystem modes (O_RDONLY, etc) import errno # for error number codes (ENOENT, etc) # - note: these must be returned as negatives from syslog import * import sipb_xen_database fuse.fuse_python_api = (0, 2) realpath = "/home/machines/" def getDepth(path): """ Return the depth of a given path, zero-based from root ('/') """ if path == '/': return 0 else: return path.count('/') def getParts(path): """ Return the slash-separated parts of a given path as a list """ if path == '/': return ['/'] else: # [1:] because otherwise you get an empty list element from the # initial '/' return path[1:].split('/') class MyStat: 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 def toTuple(self): return (self.st_mode, self.st_ino, self.st_dev, self.st_nlink, self.st_uid, self.st_gid, self.st_size, self.st_atime, self.st_mtime, self.st_ctime) class ConsoleFS(Fuse): """ ConsoleFS creates a series of subdirectories each mirroring the same real directory, except for a single file - the .k5login - which is dynamically generated for each subdirectory This filesystem only implements the getattr, getdir, read, and readlink calls, beacuse this is a read-only filesystem """ def __init__(self, *args, **kw): """Initialize the filesystem and set it to allow_other access besides the user who mounts the filesystem (i.e. root) """ Fuse.__init__(self, *args, **kw) self.lasttime = time() self.allow_other = 1 openlog('sipb-xen-consolefs ', LOG_PID, LOG_DAEMON) syslog(LOG_DEBUG, 'Init complete.') def mirrorPath(self, path): """Translate a virtual path to its real path counterpart""" return realpath + "/".join(getParts(path)[1:]) def getMachines(self): """Get the list of VMs in the database, clearing the cache if it's older than 15 seconds""" if time() - self.lasttime > 15: self.lasttime = time() sipb_xen_database.clear_cache() return [machine.name for machine in sipb_xen_database.Machine.select()] def getUid(self, machine_name): """Calculate the UID of a machine-account, which is just machine_id+1000 """ return sipb_xen_database.Machine.get_by(name=machine_name).machine_id + 1000 def getK5login(self, machine_name): """Build the ACL for a machine and turn it into a .k5login file """ machine = sipb_xen_database.Machine.get_by(name=machine_name) users = [acl.user for acl in machine.acl] return "\n".join(map(self.userToPrinc, users) + ['']) def userToPrinc(self, user): """Convert Kerberos v4-style names to v5-style and append a default realm if none is specified """ if '@' in user: (princ, realm) = user.split('@') else: princ = user realm = "ATHENA.MIT.EDU" return princ.replace('.', '/') + '@' + realm def getattr(self, path): """ - st_mode (protection bits) - st_ino (inode number) - st_dev (device) - st_nlink (number of hard links) - st_uid (user ID of owner) - st_gid (group ID of owner) - st_size (size of file, in bytes) - st_atime (time of most recent access) - st_mtime (time of most recent content modification) - st_ctime (platform dependent; time of most recent metadata change on Unix, or the time of creation on Windows). """ syslog(LOG_DEBUG, "*** getattr: " + path) depth = getDepth(path) parts = getParts(path) st = MyStat() # / is a directory if path == '/': st.st_mode = stat.S_IFDIR | 0755 st.st_nlink = 2 # /foo is a directory if foo is a machine - otherwise it doesn't exist elif depth == 1: if parts[-1] in self.getMachines(): st.st_mode = stat.S_IFDIR | 0755 st.st_nlink = 2 # Homedirs should be owned by the user whose homedir it is st.st_uid = st.st_gid = self.getUid(parts[0]) else: return -errno.ENOENT # Catch the .k5login file, because it's a special case elif depth == 2 and parts[-1] == '.k5login': st.st_mode = stat.S_IFREG | 0444 st.st_nlink = 1 st.st_size = len(self.getK5login(parts[0])) # The .k5login file should be owned by the user whose homedir it is st.st_uid = st.st_gid = self.getUid(parts[0]) # For anything else, we get the mirror path and call out to the OS else: stats = list(os.lstat(self.mirrorPath(path))) # Shadow the UID and GID from the original homedir stats[4:6] = [self.getUid(parts[0])] * 2 return tuple(stats) return st.toTuple() # This call isn't actually used in the version of Fuse on console, but we # wanted to leave it implemented to ease the transition in the future def readdir(self, path, offset): """Return a generator with the listing for a directory """ syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset)) for (value, zero) in self.getdir(path): yield fuse.Direntry(value) def getdir(self, path): """Return a list of tuples of the form (item, 0) with the contents of the directory path Fuse doesn't add '.' or '..' on its own, so we have to """ syslog(LOG_DEBUG, '*** getdir %s' % path) # '/' contains a directory for each machine if path == '/': contents = self.getMachines() # The directory for each machine contains the same files as the realpath # but also the .k5login # # The list is converted to a set so that we can handle the case where # there is already a .k5login in the realpath gracefully elif getDepth(path) == 1: contents = set(os.listdir(self.mirrorPath(path)) + ['.k5login']) # If it's not the root of the homedir, just pass the call onto the OS # for realpath else: contents = os.listdir(self.mirrorPath(path)) # Format the list the way that Fuse wants it - and don't forget to add # '.' and '..' return [(i, 0) for i in (list(contents) + ['.', '..'])] def read(self, path, length, offset): """Read length bytes starting at offset of path. In most cases, this just gets passed on to the OS """ syslog(LOG_DEBUG, '*** read %s %s %s' % (path, length, offset)) parts = getParts(path) # If the depth is less than 2, then either it's a directory or the file # doesn't exist # (realistically this doesn't appear to ever happen) if getDepth(path) < 2: return -errno.ENOENT # If we're asking for a real .k5login file, then create it and return # the snippet requested elif parts[1:] == ['.k5login']: if parts[0] not in self.getMachines(): return -errno.ENOENT else: return self.getK5login(parts[0])[offset:length + offset] # Otherwise, pass the call onto the OS # (note that the file will get closed when this call returns and the # file descriptor goes out of scope) else: fname = self.mirrorPath(path) if not os.path.isfile(fname): return -errno.ENOENT else: f = open(fname) f.seek(offset) return f.read(length) def readlink(self, path): syslog(LOG_DEBUG, '*** readlink %s' % path) # There aren't any symlinks here if getDepth(path) < 2: return -errno.ENOENT # But there might be here else: return os.readlink(self.mirrorPath(path)) if __name__ == '__main__': sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen') usage=""" ConsoleFS [mount_path] """ server = ConsoleFS() server.flags = 0 server.main()