--- /dev/null
+#!/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()