#!/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 *

from invirt import database
from invirt.config import structs as config

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()
			database.clear_cache()
		return [machine.name for machine in database.Machine.select()]
		
	def getacl(self, machine_name):
		"""Build the ACL file for a machine
		"""
		machine = 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):
		"""Build the master conf file, with all machines
		"""
		return '\n'.join("control %s /usr/sbin/sipb-xen-remote-proxy-control"
				 " /etc/remctl/remconffs/acl/%s"
				 % (machine_name, machine_name)
				 for machine_name in self.getMachines())+'\n'
	
	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 = config.authn[0].realm
		
		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] == 'acl':
				st.st_mode = stat.S_IFDIR | 0755
				st.st_nlink = 2
			elif parts[0] == 'conf':
				st.st_mode = stat.S_IFREG | 0444
				st.st_nlink = 1
				st.st_size = len(self.getconf())
			else:
				return -errno.ENOENT
		elif depth == 2:
			if parts[0] != 'acl':
				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.getacl(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')
		elif depth == 1:
			if parts[0] == 'acl':
				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 depth == 0:
			return -errno.EISDIR
		elif parts[0] == 'conf':
			return self.getconf()[offset:offset+length]
		elif parts[0] == 'acl':
			if depth == 1:
				return -errno.EISDIR
			if parts[1] in self.getMachines():
				return self.getacl(parts[1])[offset:offset+length]
		return -errno.ENOENT
	
	def readlink(self, path):
		syslog(LOG_DEBUG, '*** readlink %s' % path)
		return -errno.ENOENT


if __name__ == '__main__':
	database.connect()
	usage="""
$0 [mount_path]
"""
	server = RemConfFS()
	server.flags = 0
	server.main()
