remote-create: keep output in order
[invirt/packages/invirt-remote.git] / files / usr / sbin / sipb-xen-remconffs
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 def getDepth(path):
20         """
21         Return the depth of a given path, zero-based from root ('/')
22         """
23         if path == '/':
24                 return 0
25         else:
26                 return path.count('/')
27
28 def getParts(path):
29         """
30         Return the slash-separated parts of a given path as a list
31         """
32         # [1:] to exclude leading empty element
33         split = path.split('/')
34         if split[-1]:
35                 return split[1:]
36         else:
37                 return split[1:-1]
38
39 def parse(path):
40         parts = getParts(path)
41         return parts, len(parts)
42
43 class MyStat:
44         def __init__(self):
45                 self.st_mode = 0
46                 self.st_ino = 0
47                 self.st_dev = 0
48                 self.st_nlink = 0
49                 self.st_uid = 0
50                 self.st_gid = 0
51                 self.st_size = 0
52                 self.st_atime = 0
53                 self.st_mtime = 0
54                 self.st_ctime = 0
55         
56         def toTuple(self):
57                 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)
58
59 class RemConfFS(Fuse):
60         """
61         RemConfFS creates a filesytem for configuring remctl, like this:
62         /
63         |-- acl
64         |   |-- machine1
65         |   ...
66         |   `-- machinen
67         `-- conf.d
68             |-- machine1
69             ...
70             `-- machinen
71
72         The machine list and the acls are drawn from a database.
73         
74         This filesystem only implements the getattr, getdir, read, and readlink
75         calls, because this is a read-only filesystem.
76         """
77         
78         def __init__(self, *args, **kw):
79                 """Initialize the filesystem and set it to allow_other access besides
80                 the user who mounts the filesystem (i.e. root)
81                 """
82                 Fuse.__init__(self, *args, **kw)
83                 self.lasttime = time()
84                 self.allow_other = 1
85                 
86                 openlog('sipb-xen-remconffs ', LOG_PID, LOG_DAEMON)
87                 
88                 syslog(LOG_DEBUG, 'Init complete.')
89
90         def getMachines(self):
91                 """Get the list of VMs in the database, clearing the cache if it's 
92                 older than 15 seconds"""
93                 if time() - self.lasttime > 15:
94                         self.lasttime = time()
95                         sipb_xen_database.clear_cache()
96                 return [machine.name for machine in sipb_xen_database.Machine.select()]
97                 
98         def getacl(self, machine_name):
99                 """Build the ACL file for a machine
100                 """
101                 machine = sipb_xen_database.Machine.get_by(name=machine_name)
102                 users = [acl.user for acl in machine.acl]
103                 return "\n".join(map(self.userToPrinc, users)
104                                  + ['include /etc/remctl/acl/web',
105                                     ''])
106                 
107         def getconf(self):
108                 """Build the master conf file, with all machines
109                 """
110                 return '\n'.join("control %s /usr/sbin/sipb-xen-remote-proxy-control"
111                                  " /etc/remctl/remconffs/acl/%s"
112                                  % (machine_name, machine_name)
113                                  for machine_name in self.getMachines())+'\n'
114         
115         def userToPrinc(self, user):
116                 """Convert Kerberos v4-style names to v5-style and append a default
117                 realm if none is specified
118                 """
119                 if '@' in user:
120                         (princ, realm) = user.split('@')
121                 else:
122                         princ = user
123                         realm = "ATHENA.MIT.EDU"
124                 
125                 return princ.replace('.', '/') + '@' + realm
126         
127         def getattr(self, path):
128                 """
129                 - st_mode (protection bits)
130                 - st_ino (inode number)
131                 - st_dev (device)
132                 - st_nlink (number of hard links)
133                 - st_uid (user ID of owner)
134                 - st_gid (group ID of owner)
135                 - st_size (size of file, in bytes)
136                 - st_atime (time of most recent access)
137                 - st_mtime (time of most recent content modification)
138                 - st_ctime (platform dependent; time of most recent metadata change on Unix,
139                                         or the time of creation on Windows).
140                 """
141                 
142                 syslog(LOG_DEBUG, "*** getattr: " + path)
143                 
144                 depth = getDepth(path)
145                 parts = getParts(path)
146                 
147                 st = MyStat()
148                 if path == '/':
149                         st.st_mode = stat.S_IFDIR | 0755
150                         st.st_nlink = 2
151                 elif depth == 1:
152                         if parts[0] == 'acl':
153                                 st.st_mode = stat.S_IFDIR | 0755
154                                 st.st_nlink = 2
155                         elif parts[0] == 'conf':
156                                 st.st_mode = stat.S_IFREG | 0444
157                                 st.st_nlink = 1
158                                 st.st_size = len(self.getconf())
159                         else:
160                                 return -errno.ENOENT
161                 elif depth == 2:
162                         if parts[0] != 'acl':
163                                 return -errno.ENOENT
164                         if parts[1] not in self.getMachines():
165                                 return -errno.ENOENT
166                         st.st_mode = stat.S_IFREG | 0444
167                         st.st_nlink = 1
168                         st.st_size = len(self.getacl(parts[1]))
169
170                 return st.toTuple()
171         
172         # This call isn't actually used in the version of Fuse on console, but we
173         # wanted to leave it implemented to ease the transition in the future
174         def readdir(self, path, offset):
175                 """Return a generator with the listing for a directory
176                 """
177                 syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset))
178                 for (value, zero) in self.getdir(path):
179                         yield fuse.Direntry(value)
180         
181         def getdir(self, path):
182                 """Return a list of tuples of the form (item, 0) with the contents of
183                 the directory path
184                 
185                 Fuse doesn't add '.' or '..' on its own, so we have to
186                 """
187                 syslog(LOG_DEBUG, '*** getdir %s' % path)
188                 
189                 parts, depth = parse(path)
190
191                 if depth == 0:
192                         contents = ('acl', 'conf')
193                 elif depth == 1:
194                         if parts[0] == 'acl':
195                                 contents = self.getMachines()
196                         else:
197                                 return -errno.ENOENT
198                 else:
199                         return -errno.ENOTDIR
200
201                 # Format the list the way that Fuse wants it - and don't forget to add
202                 # '.' and '..'
203                 return [(i, 0) for i in (list(contents) + ['.', '..'])]
204
205         def read(self, path, length, offset):
206                 """Read length bytes starting at offset of path. In most cases, this
207                 just gets passed on to the OS
208                 """
209                 syslog(LOG_DEBUG, '*** read %s %s %s' % (path, length, offset))
210                 
211                 parts, depth = parse(path)
212                 
213                 if depth == 0:
214                         return -errno.EISDIR
215                 elif parts[0] == 'conf':
216                         return self.getconf()[offset:offset+length]
217                 elif parts[0] == 'acl':
218                         if depth == 1:
219                                 return -errno.EISDIR
220                         if parts[1] in self.getMachines():
221                                 return self.getacl(parts[1])[offset:offset+length]
222                 return -errno.ENOENT
223         
224         def readlink(self, path):
225                 syslog(LOG_DEBUG, '*** readlink %s' % path)
226                 return -errno.ENOENT
227
228
229 if __name__ == '__main__':
230         sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
231         usage="""
232 $0 [mount_path]
233 """
234         server = RemConfFS()
235         server.flags = 0
236         server.main()