d5187c553d8d0777387f975607bf03f6cb772e42
[invirt/packages/invirt-console.git] / files / usr / bin / sipb-xen-consolefs
1 #!/usr/bin/python
2
3 import fuse
4 from fuse import Fuse
5
6 from time import time
7
8 import stat     # for file properties
9 import os         # for filesystem modes (O_RDONLY, etc)
10 import errno   # for error number codes (ENOENT, etc)
11                            # - note: these must be returned as negatives
12
13 from syslog import *
14
15 import sipb_xen_database
16
17 fuse.fuse_python_api = (0, 2)
18
19 realpath = "/home/machines/"
20
21 def 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
30 def getParts(path):
31         """
32         Return the slash-separated parts of a given path as a list
33         """
34         if path == '/':
35                 return ['/']
36         else:
37                 # [1:] because otherwise you get an empty list element from the
38                 # initial '/'
39                 return path[1:].split('/')
40
41 class 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
47                 self.st_uid = 0
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
57 class ConsoleFS(Fuse):
58         """
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
65         """
66         
67         def __init__(self, *args, **kw):
68                 """Initialize the filesystem and set it to allow_other access besides
69                 the user who mounts the filesystem (i.e. root)
70                 """
71                 Fuse.__init__(self, *args, **kw)
72                 self.lasttime = time()
73                 self.allow_other = 1
74                 
75                 openlog('sipb-xen-consolefs ', LOG_PID, LOG_DAEMON)
76                 
77                 syslog(LOG_DEBUG, 'Init complete.')
78         
79         def mirrorPath(self, path):
80                 """Translate a virtual path to its real path counterpart"""
81                 return realpath + "/".join(getParts(path)[1:])
82         
83         def getMachines(self):
84                 """Get the list of VMs in the database, clearing the cache if it's 
85                 older than 15 seconds"""
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):
92                 """Calculate the UID of a machine-account, which is just machine_id+1000
93                 """
94                 return sipb_xen_database.Machine.get_by(name=machine_name).machine_id + 1000
95         
96         def getK5login(self, machine_name):
97                 """Build the ACL for a machine and turn it into a .k5login file
98                 """
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):
104                 """Convert Kerberos v4-style names to v5-style and append a default
105                 realm if none is specified
106                 """
107                 if '@' in user:
108                         (princ, realm) = user.split('@')
109                 else:
110                         princ = user
111                         realm = "ATHENA.MIT.EDU"
112                 
113                 return princ.replace('.', '/') + '@' + realm
114         
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                 
130                 syslog(LOG_DEBUG, "*** getattr: " + path)
131                 
132                 depth = getDepth(path)
133                 parts = getParts(path)
134                 
135                 st = MyStat()
136                 # / is a directory
137                 if path == '/':
138                         st.st_mode = stat.S_IFDIR | 0755
139                         st.st_nlink = 2
140                 # /foo is a directory if foo is a machine - otherwise it doesn't exist
141                 elif depth == 1:
142                         if parts[-1] in self.getMachines():
143                                 st.st_mode = stat.S_IFDIR | 0755
144                                 st.st_nlink = 2
145                                 # Homedirs should be owned by the user whose homedir it is
146                                 st.st_uid = st.st_gid = self.getUid(parts[0])
147                         else:
148                                 return -errno.ENOENT
149                 # Catch the .k5login file, because it's a special case
150                 elif depth == 2 and parts[-1] == '.k5login':
151                         st.st_mode = stat.S_IFREG | 0444
152                         st.st_nlink = 1
153                         st.st_size = len(self.getK5login(parts[0]))
154                         # The .k5login file should be owned by the user whose homedir it is
155                         st.st_uid = st.st_gid = self.getUid(parts[0])
156                 # For anything else, we get the mirror path and call out to the OS
157                 else:
158                         stats = list(os.lstat(self.mirrorPath(path)))
159                         # Shadow the UID and GID from the original homedir
160                         stats[4:6] = [self.getUid(parts[0])] * 2
161                         return tuple(stats)
162                 return st.toTuple()
163         
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
166         def readdir(self, path, offset):
167                 """Return a generator with the listing for a directory
168                 """
169                 syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset))
170                 for (value, zero) in self.getdir(path):
171                         yield fuse.Direntry(value)
172         
173         def getdir(self, path):
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
182                 if path == '/':
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
189                 elif getDepth(path) == 1:
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
193                 else:
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) + ['.', '..'])]
198         
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))
204                 
205                 parts = getParts(path)
206                 
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)
210                 if getDepth(path) < 2:
211                         return -errno.ENOENT
212                 # If we're asking for a real .k5login file, then create it and return
213                 # the snippet requested
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]
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)
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)
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))
240
241 if __name__ == '__main__':
242         sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
243         usage="""
244 ConsoleFS [mount_path]
245 """
246         server = ConsoleFS()
247         server.flags = 0
248         server.main()