| [518] | 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 |                  | 
|---|
| [519] | 107 |         def getconf(self): | 
|---|
 | 108 |                 """Build the master conf file, with all machines | 
|---|
| [518] | 109 |                 """ | 
|---|
| [519] | 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' | 
|---|
| [518] | 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: | 
|---|
| [519] | 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: | 
|---|
| [518] | 160 |                                 return -errno.ENOENT | 
|---|
 | 161 |                 elif depth == 2: | 
|---|
| [519] | 162 |                         if parts[0] != 'acl': | 
|---|
| [518] | 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 | 
|---|
| [519] | 168 |                         st.st_size = len(self.getacl(parts[1])) | 
|---|
| [518] | 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: | 
|---|
| [519] | 192 |                         contents = ('acl', 'conf') | 
|---|
| [518] | 193 |                 elif depth == 1: | 
|---|
| [519] | 194 |                         if parts[0] == 'acl': | 
|---|
| [518] | 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 |                  | 
|---|
| [519] | 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(): | 
|---|
| [518] | 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() | 
|---|