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
15 import sipb_xen_database
17 fuse.fuse_python_api = (0, 2)
21 Return the depth of a given path, zero-based from root ('/')
26 return path.count('/')
30 Return the slash-separated parts of a given path as a list
32 # [1:] to exclude leading empty element
33 split = path.split('/')
40 parts = getParts(path)
41 return parts, len(parts)
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)
59 class RemConfFS(Fuse):
61 RemConfFS creates a filesytem for configuring remctl, like this:
72 The machine list and the acls are drawn from a database.
74 This filesystem only implements the getattr, getdir, read, and readlink
75 calls, because this is a read-only filesystem.
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)
82 Fuse.__init__(self, *args, **kw)
83 self.lasttime = time()
86 openlog('sipb-xen-remconffs ', LOG_PID, LOG_DAEMON)
88 syslog(LOG_DEBUG, 'Init complete.')
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()]
98 def getacl(self, machine_name):
99 """Build the ACL file for a machine
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',
107 def getconf(self, machine_name):
108 """Build the command file for a machine
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))
114 def getfile(self, dir, machine_name):
115 """Build the ACL or command file for a machine
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"
121 def userToPrinc(self, user):
122 """Convert Kerberos v4-style names to v5-style and append a default
123 realm if none is specified
126 (princ, realm) = user.split('@')
129 realm = "ATHENA.MIT.EDU"
131 return princ.replace('.', '/') + '@' + realm
133 def getattr(self, path):
135 - st_mode (protection bits)
136 - st_ino (inode number)
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).
148 syslog(LOG_DEBUG, "*** getattr: " + path)
150 depth = getDepth(path)
151 parts = getParts(path)
155 st.st_mode = stat.S_IFDIR | 0755
158 if parts[0] not in ('acl', 'conf.d'):
160 st.st_mode = stat.S_IFDIR | 0755
163 if parts[0] not in ('acl', 'conf.d'):
165 if parts[1] not in self.getMachines():
167 st.st_mode = stat.S_IFREG | 0444
169 st.st_size = len(self.getfile(parts[0], parts[1]))
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
178 syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset))
179 for (value, zero) in self.getdir(path):
180 yield fuse.Direntry(value)
182 def getdir(self, path):
183 """Return a list of tuples of the form (item, 0) with the contents of
186 Fuse doesn't add '.' or '..' on its own, so we have to
188 syslog(LOG_DEBUG, '*** getdir %s' % path)
190 parts, depth = parse(path)
193 contents = ('acl', 'conf.d')
195 if parts[0] in ('acl', 'conf.d'):
196 contents = self.getMachines()
200 return -errno.ENOTDIR
202 # Format the list the way that Fuse wants it - and don't forget to add
204 return [(i, 0) for i in (list(contents) + ['.', '..'])]
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
210 syslog(LOG_DEBUG, '*** read %s %s %s' % (path, length, offset))
212 parts, depth = parse(path)
214 # If the depth is not 2, then either it's a directory or the file
216 # (realistically this doesn't appear to ever happen)
217 if getDepth(path) != 2:
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]
226 def readlink(self, path):
227 syslog(LOG_DEBUG, '*** readlink %s' % path)
231 if __name__ == '__main__':
232 sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')