source: trunk/packages/sipb-xen-console/files/usr/bin/sipb-xen-consolefs @ 359

Last change on this file since 359 was 359, checked in by broder, 17 years ago

Added comments and changed to using the syslog module instead of print

  • Property svn:executable set to *
File size: 7.3 KB
RevLine 
[335]1#!/usr/bin/python
2
3import fuse
4from fuse import Fuse
5
6from time import time
7
8import stat     # for file properties
9import os         # for filesystem modes (O_RDONLY, etc)
10import errno   # for error number codes (ENOENT, etc)
11                           # - note: these must be returned as negatives
12
[359]13from syslog import *
14
[344]15import sipb_xen_database
16
[335]17fuse.fuse_python_api = (0, 2)
18
19realpath = "/home/machines/"
20
21def getDepth(path):
22        """
23        Return the depth of a given path, zero-based from root ('/')
24        """
25        if path == '/':
26                return 0
27        else:
28                return path.count('/')
29
30def getParts(path):
31        """
32        Return the slash-separated parts of a given path as a list
33        """
34        if path == '/':
35                return ['/']
36        else:
[359]37                # [1:] because otherwise you get an empty list element from the
38                # initial '/'
[335]39                return path[1:].split('/')
40
41class MyStat:
42        def __init__(self):
43                self.st_mode = 0
44                self.st_ino = 0
45                self.st_dev = 0
46                self.st_nlink = 0
[344]47                self.st_uid = 0
[335]48                self.st_gid = 0
49                self.st_size = 0
50                self.st_atime = 0
51                self.st_mtime = 0
52                self.st_ctime = 0
53       
54        def toTuple(self):
55                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)
56
57class ConsoleFS(Fuse):
58        """
[359]59        ConsoleFS creates a series of subdirectories each mirroring the same real
60        directory, except for a single file - the .k5login - which is dynamically
61        generated for each subdirectory
62       
63        This filesystem only implements the getattr, getdir, read, and readlink
64        calls, beacuse this is a read-only filesystem
[335]65        """
66       
67        def __init__(self, *args, **kw):
[359]68                """Initialize the filesystem and set it to allow_other access besides
69                the user who mounts the filesystem (i.e. root)
70                """
[335]71                Fuse.__init__(self, *args, **kw)
[344]72                self.lasttime = time()
73                self.allow_other = 1
[359]74               
75                openlog('sipb-xen-consolefs ', LOG_PID, LOG_DAEMON)
76               
77                syslog(LOG_DEBUG, 'Init complete.')
[335]78       
79        def mirrorPath(self, path):
[359]80                """Translate a virtual path to its real path counterpart"""
[335]81                return realpath + "/".join(getParts(path)[1:])
82       
[344]83        def getMachines(self):
[359]84                """Get the list of VMs in the database, clearing the cache if it's
85                older than 15 seconds"""
[344]86                if time() - self.lasttime > 15:
87                        self.lasttime = time()
88                        sipb_xen_database.clear_cache()
89                return [machine.name for machine in sipb_xen_database.Machine.select()]
90       
91        def getUid(self, machine_name):
[359]92                """Calculate the UID of a machine-account, which is just machine_id+1000
93                """
[344]94                return sipb_xen_database.Machine.get_by(name=machine_name).machine_id + 1000
95       
96        def getK5login(self, machine_name):
[359]97                """Build the ACL for a machine and turn it into a .k5login file
98                """
[344]99                machine = sipb_xen_database.Machine.get_by(name=machine_name)
100                users = [acl.user for acl in machine.acl]
101                return "\n".join(map(self.userToPrinc, users) + [''])
102       
103        def userToPrinc(self, user):
[359]104                """Convert Kerberos v4-style names to v5-style and append a default
105                realm if none is specified
106                """
[344]107                if '@' in user:
108                        (princ, realm) = user.split('@')
109                else:
110                        princ = user
111                        realm = "ATHENA.MIT.EDU"
112               
[357]113                return princ.replace('.', '/') + '@' + realm
[344]114       
[335]115        def getattr(self, path):
116                """
117                - st_mode (protection bits)
118                - st_ino (inode number)
119                - st_dev (device)
120                - st_nlink (number of hard links)
121                - st_uid (user ID of owner)
122                - st_gid (group ID of owner)
123                - st_size (size of file, in bytes)
124                - st_atime (time of most recent access)
125                - st_mtime (time of most recent content modification)
126                - st_ctime (platform dependent; time of most recent metadata change on Unix,
127                                        or the time of creation on Windows).
128                """
129               
[359]130                syslog(LOG_DEBUG, "*** getattr: " + path)
[335]131               
132                depth = getDepth(path)
133                parts = getParts(path)
134               
135                st = MyStat()
[359]136                # / is a directory
[335]137                if path == '/':
138                        st.st_mode = stat.S_IFDIR | 0755
139                        st.st_nlink = 2
[359]140                # /foo is a directory if foo is a machine - otherwise it doesn't exist
[335]141                elif depth == 1:
[344]142                        if parts[-1] in self.getMachines():
[335]143                                st.st_mode = stat.S_IFDIR | 0755
144                                st.st_nlink = 2
[359]145                                # Homedirs should be owned by the user whose homedir it is
[344]146                                st.st_uid = st.st_gid = self.getUid(parts[0])
[335]147                        else:
148                                return -errno.ENOENT
[359]149                # Catch the .k5login file, because it's a special case
[335]150                elif depth == 2 and parts[-1] == '.k5login':
151                        st.st_mode = stat.S_IFREG | 0444
152                        st.st_nlink = 1
[344]153                        st.st_size = len(self.getK5login(parts[0]))
[359]154                        # The .k5login file should be owned by the user whose homedir it is
[344]155                        st.st_uid = st.st_gid = self.getUid(parts[0])
[359]156                # For anything else, we get the mirror path and call out to the OS
[335]157                else:
[344]158                        stats = list(os.lstat(self.mirrorPath(path)))
[359]159                        # Shadow the UID and GID from the original homedir
[344]160                        stats[4:6] = [self.getUid(parts[0])] * 2
161                        return tuple(stats)
[335]162                return st.toTuple()
163       
[359]164        # This call isn't actually used in the version of Fuse on console, but we
165        # wanted to leave it implemented to ease the transition in the future
[335]166        def readdir(self, path, offset):
[359]167                """Return a generator with the listing for a directory
168                """
169                syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset))
[344]170                for (value, zero) in self.getdir(path):
171                        yield fuse.Direntry(value)
172       
173        def getdir(self, path):
[359]174                """Return a list of tuples of the form (item, 0) with the contents of
175                the directory path
176               
177                Fuse doesn't add '.' or '..' on its own, so we have to
178                """
179                syslog(LOG_DEBUG, '*** getdir %s' % path)
180               
181                # '/' contains a directory for each machine
[335]182                if path == '/':
[359]183                        contents = self.getMachines()
184                # The directory for each machine contains the same files as the realpath
185                # but also the .k5login
186                #
187                # The list is converted to a set so that we can handle the case where
188                # there is already a .k5login in the realpath gracefully
[335]189                elif getDepth(path) == 1:
[359]190                        contents = set(os.listdir(self.mirrorPath(path)) + ['.k5login'])
191                # If it's not the root of the homedir, just pass the call onto the OS
192                # for realpath
[335]193                else:
[359]194                        contents = os.listdir(self.mirrorPath(path))
195                # Format the list the way that Fuse wants it - and don't forget to add
196                # '.' and '..'
197                return [(i, 0) for i in (list(contents) + ['.', '..'])]
[335]198       
[359]199        def read(self, path, length, offset):
200                """Read length bytes starting at offset of path. In most cases, this
201                just gets passed on to the OS
202                """
203                syslog(LOG_DEBUG, '*** read %s %s %s' % (path, length, offset))
[335]204               
[344]205                parts = getParts(path)
206               
[359]207                # If the depth is less than 2, then either it's a directory or the file
208                # doesn't exist
209                # (realistically this doesn't appear to ever happen)
[335]210                if getDepth(path) < 2:
211                        return -errno.ENOENT
[359]212                # If we're asking for a real .k5login file, then create it and return
213                # the snippet requested
[344]214                elif parts[1:] == ['.k5login']:
215                        if parts[0] not in self.getMachines():
216                                return -errno.ENOENT
217                        else:
218                                return self.getK5login(parts[0])[offset:length + offset]
[359]219                # Otherwise, pass the call onto the OS
220                # (note that the file will get closed when this call returns and the
221                # file descriptor goes out of scope)
[335]222                else:
223                        fname = self.mirrorPath(path)
224                        if not os.path.isfile(fname):
225                                return -errno.ENOENT
226                        else:
227                                f = open(fname)
228                                f.seek(offset)
229                                return f.read(length)
[359]230       
231        def readlink(self, path):
232                syslog(LOG_DEBUG, '*** readlink %s' % path)
233               
234                # There aren't any symlinks here
235                if getDepth(path) < 2:
236                        return -errno.ENOENT
237                # But there might be here
238                else:
239                        return os.readlink(self.mirrorPath(path))
[335]240
241if __name__ == '__main__':
[344]242        sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
[335]243        usage="""
244ConsoleFS [mount_path]
245"""
246        server = ConsoleFS()
247        server.flags = 0
248        server.main()
Note: See TracBrowser for help on using the repository browser.