From: Greg Price <price@mit.edu>
Date: Sun, 11 May 2008 00:35:28 +0000 (-0400)
Subject: first version of remctl-conf fuse fs
X-Git-Tag: sipb-xen-remote-server/0.2~32
X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-remote.git/commitdiff_plain/58921ff93c77f71a30aba863dea5a9a89b403585

first version of remctl-conf fuse fs

Heavily borrows from consolefs, obviously.  Refactor later.

svn path=/trunk/packages/sipb-xen-remote-server/; revision=518
---

diff --git a/files/usr/sbin/sipb-xen-remconffs b/files/usr/sbin/sipb-xen-remconffs
new file mode 100755
index 0000000..03099d6
--- /dev/null
+++ b/files/usr/sbin/sipb-xen-remconffs
@@ -0,0 +1,238 @@
+#!/usr/bin/python
+
+import fuse
+from fuse import Fuse
+
+from time import time
+
+import stat	# for file properties
+import os	  # for filesystem modes (O_RDONLY, etc)
+import errno   # for error number codes (ENOENT, etc)
+			   # - note: these must be returned as negatives
+
+from syslog import *
+
+import sipb_xen_database
+
+fuse.fuse_python_api = (0, 2)
+
+def getDepth(path):
+	"""
+	Return the depth of a given path, zero-based from root ('/')
+	"""
+	if path == '/':
+		return 0
+	else:
+		return path.count('/')
+
+def getParts(path):
+	"""
+	Return the slash-separated parts of a given path as a list
+	"""
+	# [1:] to exclude leading empty element
+	split = path.split('/')
+	if split[-1]:
+		return split[1:]
+	else:
+		return split[1:-1]
+
+def parse(path):
+	parts = getParts(path)
+	return parts, len(parts)
+
+class MyStat:
+	def __init__(self):
+		self.st_mode = 0
+		self.st_ino = 0
+		self.st_dev = 0
+		self.st_nlink = 0
+		self.st_uid = 0
+		self.st_gid = 0
+		self.st_size = 0
+		self.st_atime = 0
+		self.st_mtime = 0
+		self.st_ctime = 0
+	
+	def toTuple(self):
+		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)
+
+class RemConfFS(Fuse):
+	"""
+	RemConfFS creates a filesytem for configuring remctl, like this:
+	/
+	|-- acl
+	|   |-- machine1
+	|   ...
+	|   `-- machinen
+	`-- conf.d
+	    |-- machine1
+	    ...
+	    `-- machinen
+
+	The machine list and the acls are drawn from a database.
+	
+	This filesystem only implements the getattr, getdir, read, and readlink
+	calls, because this is a read-only filesystem.
+	"""
+	
+	def __init__(self, *args, **kw):
+		"""Initialize the filesystem and set it to allow_other access besides
+		the user who mounts the filesystem (i.e. root)
+		"""
+		Fuse.__init__(self, *args, **kw)
+		self.lasttime = time()
+		self.allow_other = 1
+		
+		openlog('sipb-xen-remconffs ', LOG_PID, LOG_DAEMON)
+		
+		syslog(LOG_DEBUG, 'Init complete.')
+
+	def getMachines(self):
+		"""Get the list of VMs in the database, clearing the cache if it's 
+		older than 15 seconds"""
+		if time() - self.lasttime > 15:
+			self.lasttime = time()
+			sipb_xen_database.clear_cache()
+		return [machine.name for machine in sipb_xen_database.Machine.select()]
+		
+	def getacl(self, machine_name):
+		"""Build the ACL file for a machine
+		"""
+		machine = sipb_xen_database.Machine.get_by(name=machine_name)
+		users = [acl.user for acl in machine.acl]
+		return "\n".join(map(self.userToPrinc, users)
+				 + ['include /etc/remctl/acl/web',
+				    ''])
+		
+	def getconf(self, machine_name):
+		"""Build the command file for a machine
+		"""
+		return ("control %s /usr/sbin/sipb-xen-remote-proxy-control"
+			" /etc/remctl/sipb-xen-auto/acl/%s\n"
+			% (machine_name, machine_name))
+		
+	def getfile(self, dir, machine_name):
+		"""Build the ACL or command file for a machine
+		"""
+		if dir == 'acl':    return self.getacl(machine_name)
+		if dir == 'conf.d': return self.getconf(machine_name)
+		raise "this shouldn't happen"
+	
+	def userToPrinc(self, user):
+		"""Convert Kerberos v4-style names to v5-style and append a default
+		realm if none is specified
+		"""
+		if '@' in user:
+			(princ, realm) = user.split('@')
+		else:
+			princ = user
+			realm = "ATHENA.MIT.EDU"
+		
+		return princ.replace('.', '/') + '@' + realm
+	
+	def getattr(self, path):
+		"""
+		- st_mode (protection bits)
+		- st_ino (inode number)
+		- st_dev (device)
+		- st_nlink (number of hard links)
+		- st_uid (user ID of owner)
+		- st_gid (group ID of owner)
+		- st_size (size of file, in bytes)
+		- st_atime (time of most recent access)
+		- st_mtime (time of most recent content modification)
+		- st_ctime (platform dependent; time of most recent metadata change on Unix,
+					or the time of creation on Windows).
+		"""
+		
+		syslog(LOG_DEBUG, "*** getattr: " + path)
+		
+		depth = getDepth(path)
+		parts = getParts(path)
+		
+		st = MyStat()
+		if path == '/':
+			st.st_mode = stat.S_IFDIR | 0755
+			st.st_nlink = 2
+		elif depth == 1:
+			if parts[0] not in ('acl', 'conf.d'):
+				return -errno.ENOENT
+			st.st_mode = stat.S_IFDIR | 0755
+			st.st_nlink = 2
+		elif depth == 2:
+			if parts[0] not in ('acl', 'conf.d'):
+				return -errno.ENOENT
+			if parts[1] not in self.getMachines():
+				return -errno.ENOENT
+			st.st_mode = stat.S_IFREG | 0444
+			st.st_nlink = 1
+			st.st_size = len(self.getfile(parts[0], parts[1]))
+
+		return st.toTuple()
+	
+	# This call isn't actually used in the version of Fuse on console, but we
+	# wanted to leave it implemented to ease the transition in the future
+	def readdir(self, path, offset):
+		"""Return a generator with the listing for a directory
+		"""
+		syslog(LOG_DEBUG, '*** readdir %s %s' % (path, offset))
+		for (value, zero) in self.getdir(path):
+			yield fuse.Direntry(value)
+	
+	def getdir(self, path):
+		"""Return a list of tuples of the form (item, 0) with the contents of
+		the directory path
+		
+		Fuse doesn't add '.' or '..' on its own, so we have to
+		"""
+		syslog(LOG_DEBUG, '*** getdir %s' % path)
+		
+		parts, depth = parse(path)
+
+		if depth == 0:
+			contents = ('acl', 'conf.d')
+		elif depth == 1:
+			if parts[0] in ('acl', 'conf.d'):
+				contents = self.getMachines()
+			else:
+				return -errno.ENOENT
+		else:
+			return -errno.ENOTDIR
+
+		# Format the list the way that Fuse wants it - and don't forget to add
+		# '.' and '..'
+		return [(i, 0) for i in (list(contents) + ['.', '..'])]
+
+	def read(self, path, length, offset):
+		"""Read length bytes starting at offset of path. In most cases, this
+		just gets passed on to the OS
+		"""
+		syslog(LOG_DEBUG, '*** read %s %s %s' % (path, length, offset))
+		
+		parts, depth = parse(path)
+		
+		# If the depth is not 2, then either it's a directory or the file
+		# doesn't exist
+		# (realistically this doesn't appear to ever happen)
+		if getDepth(path) != 2:
+			return -errno.ENOENT
+		elif parts[1] in self.getMachines():
+			if parts[0] == 'acl':
+				return self.getacl(parts[1])[offset:offset+length]
+			if parts[0] == 'conf.d':
+				return self.getconf(parts[1])[offset:offset+length]
+		return -errno.ENOENT
+	
+	def readlink(self, path):
+		syslog(LOG_DEBUG, '*** readlink %s' % path)
+		return -errno.ENOENT
+
+
+if __name__ == '__main__':
+	sipb_xen_database.connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
+	usage="""
+$0 [mount_path]
+"""
+	server = RemConfFS()
+	server.flags = 0
+	server.main()