- sipb_xen_database -> invirt.database
[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 from invirt.config import structs as config
16 from invirt import database
17
18 fuse.fuse_python_api = (0, 2)
19
20 realpath = "/home/machines/"
21
22 def 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
31 def getParts(path):
32         """
33         Return the slash-separated parts of a given path as a list
34         """
35         if path == '/':
36                 return ['/']
37         else:
38                 # [1:] because otherwise you get an empty list element from the
39                 # initial '/'
40                 return path[1:].split('/')
41
42 class 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
48                 self.st_uid = 0
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
58 class ConsoleFS(Fuse):
59         """
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
66         """
67         
68         def __init__(self, *args, **kw):
69                 """Initialize the filesystem and set it to allow_other access besides
70                 the user who mounts the filesystem (i.e. root)
71                 """
72                 Fuse.__init__(self, *args, **kw)
73                 self.lasttime = time()
74                 self.allow_other = 1
75                 
76                 openlog('sipb-xen-consolefs ', LOG_PID, LOG_DAEMON)
77                 
78                 syslog(LOG_DEBUG, 'Init complete.')
79         
80         def mirrorPath(self, path):
81                 """Translate a virtual path to its real path counterpart"""
82                 return realpath + "/".join(getParts(path)[1:])
83         
84         def getMachines(self):
85                 """Get the list of VMs in the database, clearing the cache if it's 
86                 older than 15 seconds"""
87                 if time() - self.lasttime > 15:
88                         self.lasttime = time()
89                         database.clear_cache()
90                 return [machine.name for machine in database.Machine.select()]
91         
92         def getUid(self, machine_name):
93                 """Calculate the UID of a machine-account, which is just machine_id+1000
94                 """
95                 return database.Machine.get_by(name=machine_name).machine_id + 1000
96         
97         def getK5login(self, machine_name):
98                 """Build the ACL for a machine and turn it into a .k5login file
99                 """
100                 machine = database.Machine.get_by(name=machine_name)
101                 users = [acl.user for acl in machine.acl]
102                 return "\n".join(map(self.userToPrinc, users) + [''])
103         
104         def userToPrinc(self, user):
105                 """Convert Kerberos v4-style names to v5-style and append a default
106                 realm if none is specified
107                 """
108                 if '@' in user:
109                         (princ, realm) = user.split('@')
110                 else:
111                         princ = user
112                         realm = config.authn[0].realm
113                 
114                 return princ.replace('.', '/') + '@' + realm
115         
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                 
131                 syslog(LOG_DEBUG, "*** getattr: " + path)
132                 
133                 depth = getDepth(path)
134                 parts = getParts(path)
135                 
136                 st = MyStat()
137                 # / is a directory
138                 if path == '/':
139                         st.st_mode = stat.S_IFDIR | 0755
140                         st.st_nlink = 2
141                 # /foo is a directory if foo is a machine - otherwise it doesn't exist
142                 elif depth == 1:
143                         if parts[-1] in self.getMachines():
144                                 st.st_mode = stat.S_IFDIR | 0755
145                                 st.st_nlink = 2
146                                 # Homedirs should be owned by the user whose homedir it is
147                                 st.st_uid = st.st_gid = self.getUid(parts[0])
148                         else:
149                                 return -errno.ENOENT
150                 # Catch the .k5login file, because it's a special case
151                 elif depth == 2 and parts[-1] == '.k5login':
152                         st.st_mode = stat.S_IFREG | 0444
153                         st.st_nlink = 1
154                         st.st_size = len(self.getK5login(parts[0]))
155                         # The .k5login file should be owned by the user whose homedir it is
156                         st.st_uid = st.st_gid = self.getUid(parts[0])
157                 # For anything else, we get the mirror path and call out to the OS
158                 else:
159                         stats = list(os.lstat(self.mirrorPath(path)))
160                         # Shadow the UID and GID from the original homedir
161                         stats[4:6] = [self.getUid(parts[0])] * 2
162                         return tuple(stats)
163                 return st.toTuple()
164         
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
167         def readdir(self, path, offset):
168                 """Return a generator with the listing for a directory
169                 """
170                 syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset))
171                 for (value, zero) in self.getdir(path):
172                         yield fuse.Direntry(value)
173         
174         def getdir(self, path):
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
183                 if path == '/':
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
190                 elif getDepth(path) == 1:
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
194                 else:
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) + ['.', '..'])]
199         
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))
205                 
206                 parts = getParts(path)
207                 
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)
211                 if getDepth(path) < 2:
212                         return -errno.ENOENT
213                 # If we're asking for a real .k5login file, then create it and return
214                 # the snippet requested
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]
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)
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)
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))
241
242 if __name__ == '__main__':
243         database.connect()
244         usage="""
245 ConsoleFS [mount_path]
246 """
247         server = ConsoleFS()
248         server.flags = 0
249         server.main()