From: Greg Price Date: Sun, 11 May 2008 00:35:28 +0000 (-0400) Subject: first version of remctl-conf fuse fs X-Git-Tag: sipb-xen-remote-server/0.2~32 X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-remote.git/commitdiff_plain/58921ff93c77f71a30aba863dea5a9a89b403585?hp=c3517663370ab241d02acb939903d569997847a9 first version of remctl-conf fuse fs Heavily borrows from consolefs, obviously. Refactor later. svn path=/trunk/packages/sipb-xen-remote-server/; revision=518 --- diff --git a/files/usr/sbin/sipb-xen-remconffs b/files/usr/sbin/sipb-xen-remconffs new file mode 100755 index 0000000..03099d6 --- /dev/null +++ b/files/usr/sbin/sipb-xen-remconffs @@ -0,0 +1,238 @@ +#!/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) + +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 + """ + # [1:] to exclude leading empty element + split = path.split('/') + if split[-1]: + return split[1:] + else: + return split[1:-1] + +def parse(path): + parts = getParts(path) + return parts, len(parts) + +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 RemConfFS(Fuse): + """ + RemConfFS creates a filesytem for configuring remctl, like this: + / + |-- acl + | |-- machine1 + | ... + | `-- machinen + `-- conf.d + |-- machine1 + ... + `-- machinen + + The machine list and the acls are drawn from a database. + + This filesystem only implements the getattr, getdir, read, and readlink + calls, because 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-remconffs ', LOG_PID, LOG_DAEMON) + + syslog(LOG_DEBUG, 'Init complete.') + + 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 getacl(self, machine_name): + """Build the ACL file for a machine + """ + 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) + + ['include /etc/remctl/acl/web', + '']) + + def getconf(self, machine_name): + """Build the command file for a machine + """ + return ("control %s /usr/sbin/sipb-xen-remote-proxy-control" + " /etc/remctl/sipb-xen-auto/acl/%s\n" + % (machine_name, machine_name)) + + def getfile(self, dir, machine_name): + """Build the ACL or command file for a machine + """ + if dir == 'acl': return self.getacl(machine_name) + if dir == 'conf.d': return self.getconf(machine_name) + raise "this shouldn't happen" + + 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() + if path == '/': + st.st_mode = stat.S_IFDIR | 0755 + st.st_nlink = 2 + elif depth == 1: + if parts[0] not in ('acl', 'conf.d'): + return -errno.ENOENT + st.st_mode = stat.S_IFDIR | 0755 + st.st_nlink = 2 + elif depth == 2: + if parts[0] not in ('acl', 'conf.d'): + return -errno.ENOENT + if parts[1] not in self.getMachines(): + return -errno.ENOENT + st.st_mode = stat.S_IFREG | 0444 + st.st_nlink = 1 + st.st_size = len(self.getfile(parts[0], parts[1])) + + 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) + + parts, depth = parse(path) + + if depth == 0: + contents = ('acl', 'conf.d') + elif depth == 1: + if parts[0] in ('acl', 'conf.d'): + contents = self.getMachines() + else: + return -errno.ENOENT + else: + return -errno.ENOTDIR + + # 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, depth = parse(path) + + # If the depth is not 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 + elif parts[1] in self.getMachines(): + if parts[0] == 'acl': + return self.getacl(parts[1])[offset:offset+length] + if parts[0] == 'conf.d': + return self.getconf(parts[1])[offset:offset+length] + return -errno.ENOENT + + def readlink(self, path): + syslog(LOG_DEBUG, '*** readlink %s' % path) + return -errno.ENOENT + + +if __name__ == '__main__': + sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen') + usage=""" +$0 [mount_path] +""" + server = RemConfFS() + server.flags = 0 + server.main()