first version of remctl-conf fuse fs
[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, machine_name):
108                 """Build the command file for a machine
109                 """
110                 return ("control %s /usr/sbin/sipb-xen-remote-proxy-control"
111                         " /etc/remctl/sipb-xen-auto/acl/%s\n"
112                         % (machine_name, machine_name))
113                 
114         def getfile(self, dir, machine_name):
115                 """Build the ACL or command file for a machine
116                 """
117                 if dir == 'acl':    return self.getacl(machine_name)
118                 if dir == 'conf.d': return self.getconf(machine_name)
119                 raise "this shouldn't happen"
120         
121         def userToPrinc(self, user):
122                 """Convert Kerberos v4-style names to v5-style and append a default
123                 realm if none is specified
124                 """
125                 if '@' in user:
126                         (princ, realm) = user.split('@')
127                 else:
128                         princ = user
129                         realm = "ATHENA.MIT.EDU"
130                 
131                 return princ.replace('.', '/') + '@' + realm
132         
133         def getattr(self, path):
134                 """
135                 - st_mode (protection bits)
136                 - st_ino (inode number)
137                 - st_dev (device)
138                 - st_nlink (number of hard links)
139                 - st_uid (user ID of owner)
140                 - st_gid (group ID of owner)
141                 - st_size (size of file, in bytes)
142                 - st_atime (time of most recent access)
143                 - st_mtime (time of most recent content modification)
144                 - st_ctime (platform dependent; time of most recent metadata change on Unix,
145                                         or the time of creation on Windows).
146                 """
147                 
148                 syslog(LOG_DEBUG, "*** getattr: " + path)
149                 
150                 depth = getDepth(path)
151                 parts = getParts(path)
152                 
153                 st = MyStat()
154                 if path == '/':
155                         st.st_mode = stat.S_IFDIR | 0755
156                         st.st_nlink = 2
157                 elif depth == 1:
158                         if parts[0] not in ('acl', 'conf.d'):
159                                 return -errno.ENOENT
160                         st.st_mode = stat.S_IFDIR | 0755
161                         st.st_nlink = 2
162                 elif depth == 2:
163                         if parts[0] not in ('acl', 'conf.d'):
164                                 return -errno.ENOENT
165                         if parts[1] not in self.getMachines():
166                                 return -errno.ENOENT
167                         st.st_mode = stat.S_IFREG | 0444
168                         st.st_nlink = 1
169                         st.st_size = len(self.getfile(parts[0], parts[1]))
170
171                 return st.toTuple()
172         
173         # This call isn't actually used in the version of Fuse on console, but we
174         # wanted to leave it implemented to ease the transition in the future
175         def readdir(self, path, offset):
176                 """Return a generator with the listing for a directory
177                 """
178                 syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset))
179                 for (value, zero) in self.getdir(path):
180                         yield fuse.Direntry(value)
181         
182         def getdir(self, path):
183                 """Return a list of tuples of the form (item, 0) with the contents of
184                 the directory path
185                 
186                 Fuse doesn't add '.' or '..' on its own, so we have to
187                 """
188                 syslog(LOG_DEBUG, '*** getdir %s' % path)
189                 
190                 parts, depth = parse(path)
191
192                 if depth == 0:
193                         contents = ('acl', 'conf.d')
194                 elif depth == 1:
195                         if parts[0] in ('acl', 'conf.d'):
196                                 contents = self.getMachines()
197                         else:
198                                 return -errno.ENOENT
199                 else:
200                         return -errno.ENOTDIR
201
202                 # Format the list the way that Fuse wants it - and don't forget to add
203                 # '.' and '..'
204                 return [(i, 0) for i in (list(contents) + ['.', '..'])]
205
206         def read(self, path, length, offset):
207                 """Read length bytes starting at offset of path. In most cases, this
208                 just gets passed on to the OS
209                 """
210                 syslog(LOG_DEBUG, '*** read %s %s %s' % (path, length, offset))
211                 
212                 parts, depth = parse(path)
213                 
214                 # If the depth is not 2, then either it's a directory or the file
215                 # doesn't exist
216                 # (realistically this doesn't appear to ever happen)
217                 if getDepth(path) != 2:
218                         return -errno.ENOENT
219                 elif parts[1] in self.getMachines():
220                         if parts[0] == 'acl':
221                                 return self.getacl(parts[1])[offset:offset+length]
222                         if parts[0] == 'conf.d':
223                                 return self.getconf(parts[1])[offset:offset+length]
224                 return -errno.ENOENT
225         
226         def readlink(self, path):
227                 syslog(LOG_DEBUG, '*** readlink %s' % path)
228                 return -errno.ENOENT
229
230
231 if __name__ == '__main__':
232         sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
233         usage="""
234 $0 [mount_path]
235 """
236         server = RemConfFS()
237         server.flags = 0
238         server.main()