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

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