first version of remctl-conf fuse fs
authorGreg Price <price@mit.edu>
Sun, 11 May 2008 00:35:28 +0000 (20:35 -0400)
committerGreg Price <price@mit.edu>
Sun, 11 May 2008 00:35:28 +0000 (20:35 -0400)
Heavily borrows from consolefs, obviously.  Refactor later.

svn path=/trunk/packages/sipb-xen-remote-server/; revision=518

files/usr/sbin/sipb-xen-remconffs [new file with mode: 0755]

diff --git a/files/usr/sbin/sipb-xen-remconffs b/files/usr/sbin/sipb-xen-remconffs
new file mode 100755 (executable)
index 0000000..03099d6
--- /dev/null
@@ -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()