- sipb_xen_database -> invirt.database
[invirt/packages/invirt-console.git] / files / usr / bin / sipb-xen-consolefs
index 7cd3766..3255b15 100755 (executable)
@@ -10,18 +10,14 @@ import os     # for filesystem modes (O_RDONLY, etc)
 import errno   # for error number codes (ENOENT, etc)
                           # - note: these must be returned as negatives
 
 import errno   # for error number codes (ENOENT, etc)
                           # - note: these must be returned as negatives
 
+from syslog import *
+
+from invirt.config import structs as config
+from invirt import database
+
 fuse.fuse_python_api = (0, 2)
 
 fuse.fuse_python_api = (0, 2)
 
-machines = ['moo17', 'remus']
 realpath = "/home/machines/"
 realpath = "/home/machines/"
-uid = 1000
-
-def dirFromList(list):
-       """
-       Return a properly formatted list of items suitable to a directory listing.
-       ['a', 'b', 'c'] => [('a', 0), ('b', 0), ('c', 0)]
-       """
-       return [(x, 0) for x in list]
 
 def getDepth(path):
        """
 
 def getDepth(path):
        """
@@ -39,6 +35,8 @@ def getParts(path):
        if path == '/':
                return ['/']
        else:
        if path == '/':
                return ['/']
        else:
+               # [1:] because otherwise you get an empty list element from the
+               # initial '/'
                return path[1:].split('/')
 
 class MyStat:
                return path[1:].split('/')
 
 class MyStat:
@@ -47,7 +45,7 @@ class MyStat:
                self.st_ino = 0
                self.st_dev = 0
                self.st_nlink = 0
                self.st_ino = 0
                self.st_dev = 0
                self.st_nlink = 0
-               self.st_uid = uid
+               self.st_uid = 0
                self.st_gid = 0
                self.st_size = 0
                self.st_atime = 0
                self.st_gid = 0
                self.st_size = 0
                self.st_atime = 0
@@ -59,15 +57,62 @@ class MyStat:
 
 class ConsoleFS(Fuse):
        """
 
 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):
        """
        
        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)
                Fuse.__init__(self, *args, **kw)
-               print 'Init complete.'
+               self.lasttime = time()
+               self.allow_other = 1
+               
+               openlog('sipb-xen-consolefs ', LOG_PID, LOG_DAEMON)
+               
+               syslog(LOG_DEBUG, 'Init complete.')
        
        def mirrorPath(self, path):
        
        def mirrorPath(self, path):
+               """Translate a virtual path to its real path counterpart"""
                return realpath + "/".join(getParts(path)[1:])
        
                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()
+                       database.clear_cache()
+               return [machine.name for machine in database.Machine.select()]
+       
+       def getUid(self, machine_name):
+               """Calculate the UID of a machine-account, which is just machine_id+1000
+               """
+               return 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 = 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 = config.authn[0].realm
+               
+               return princ.replace('.', '/') + '@' + realm
+       
        def getattr(self, path):
                """
                - st_mode (protection bits)
        def getattr(self, path):
                """
                - st_mode (protection bits)
@@ -83,48 +128,98 @@ class ConsoleFS(Fuse):
                                        or the time of creation on Windows).
                """
                
                                        or the time of creation on Windows).
                """
                
-               print "*** getattr: " + path
+               syslog(LOG_DEBUG, "*** getattr: " + path)
                
                depth = getDepth(path)
                parts = getParts(path)
                
                st = MyStat()
                
                depth = getDepth(path)
                parts = getParts(path)
                
                st = MyStat()
+               # / is a directory
                if path == '/':
                        st.st_mode = stat.S_IFDIR | 0755
                        st.st_nlink = 2
                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:
                elif depth == 1:
-                       if parts[-1] in machines:
+                       if parts[-1] in self.getMachines():
                                st.st_mode = stat.S_IFDIR | 0755
                                st.st_nlink = 2
                                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
                        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
                elif depth == 2 and parts[-1] == '.k5login':
                        st.st_mode = stat.S_IFREG | 0444
                        st.st_nlink = 1
-                       st.st_size = 17
+                       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:
                else:
-                       st = os.lstat(self.mirrorPath(path))
+                       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()
        
                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):
        def readdir(self, path, offset):
-               print '*** readdir', 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 == '/':
                if path == '/':
-                       for r in  ['.', '..']+machines:
-                               yield fuse.Direntry(r)
+                       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:
                elif getDepth(path) == 1:
-                       for r in set(os.listdir(self.mirrorPath(path)) + ['.k5login']):
-                               yield fuse.Direntry(r)
+                       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:
                else:
-                       for r in os.listdir(self.mirrorPath(path)):
-                               yield fuse.Direntry(r)
+                       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 ):
-               print '*** read', path, length, offset
+       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 getDepth(path) < 2:
                        return -errno.ENOENT
-               elif getParts(path)[1:] == ['.k5login']:
-                       pass
+               # 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):
                else:
                        fname = self.mirrorPath(path)
                        if not os.path.isfile(fname):
@@ -133,8 +228,19 @@ class ConsoleFS(Fuse):
                                f = open(fname)
                                f.seek(offset)
                                return f.read(length)
                                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__':
 
 if __name__ == '__main__':
+       database.connect()
        usage="""
 ConsoleFS [mount_path]
 """
        usage="""
 ConsoleFS [mount_path]
 """