#!/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.config import structs as config
from invirt import database

fuse.fuse_python_api = (0, 2)

realpath = "/home/machines/"

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
	"""
	if path == '/':
		return ['/']
	else:
		# [1:] because otherwise you get an empty list element from the
		# initial '/'
		return path[1:].split('/')

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 ConsoleFS(Fuse):
	"""
	ConsoleFS creates a series of subdirectories each mirroring the same real
	directory, except for a single file - the .k5login - which is dynamically
	generated for each subdirectory
	
	This filesystem only implements the getattr, getdir, read, and readlink
	calls, beacuse 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-consolefs ', LOG_PID, LOG_DAEMON)
		
		syslog(LOG_DEBUG, 'Init complete.')
	
	def mirrorPath(self, path):
		"""Translate a virtual path to its real path counterpart"""
		return realpath + "/".join(getParts(path)[1:])
	
	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 getUid(self, machine_name):
		"""Calculate the UID of a machine-account, which is just machine_id+1000
		"""
		return database.Machine.get_by(name=machine_name).machine_id + 1000
	
	def getK5login(self, machine_name):
		"""Build the ACL for a machine and turn it into a .k5login file
		"""
		machine = database.Machine.get_by(name=machine_name)
		users = [acl.user for acl in machine.acl]
		return "\n".join(map(self.userToPrinc, users) + [''])
	
	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()
		# / is a directory
		if path == '/':
			st.st_mode = stat.S_IFDIR | 0755
			st.st_nlink = 2
		# /foo is a directory if foo is a machine - otherwise it doesn't exist
		elif depth == 1:
			if parts[-1] in self.getMachines():
				st.st_mode = stat.S_IFDIR | 0755
				st.st_nlink = 2
				# Homedirs should be owned by the user whose homedir it is
				st.st_uid = st.st_gid = self.getUid(parts[0])
			else:
				return -errno.ENOENT
		# Catch the .k5login file, because it's a special case
		elif depth == 2 and parts[-1] == '.k5login':
			st.st_mode = stat.S_IFREG | 0444
			st.st_nlink = 1
			st.st_size = len(self.getK5login(parts[0]))
			# The .k5login file should be owned by the user whose homedir it is
			st.st_uid = st.st_gid = self.getUid(parts[0])
		# For anything else, we get the mirror path and call out to the OS
		else:
			stats = list(os.lstat(self.mirrorPath(path)))
			# Shadow the UID and GID from the original homedir
			stats[4:6] = [self.getUid(parts[0])] * 2
			return tuple(stats)
		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)
		
		# '/' contains a directory for each machine
		if path == '/':
			contents = self.getMachines()
		# The directory for each machine contains the same files as the realpath
		# but also the .k5login
		#
		# The list is converted to a set so that we can handle the case where 
		# there is already a .k5login in the realpath gracefully
		elif getDepth(path) == 1:
			contents = set(os.listdir(self.mirrorPath(path)) + ['.k5login'])
		# If it's not the root of the homedir, just pass the call onto the OS
		# for realpath
		else:
			contents = os.listdir(self.mirrorPath(path))
		# 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 = getParts(path)
		
		# If the depth is less than 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
		# If we're asking for a real .k5login file, then create it and return
		# the snippet requested
		elif parts[1:] == ['.k5login']:
			if parts[0] not in self.getMachines():
				return -errno.ENOENT
			else:
				return self.getK5login(parts[0])[offset:length + offset]
		# Otherwise, pass the call onto the OS
		# (note that the file will get closed when this call returns and the
		# file descriptor goes out of scope)
		else:
			fname = self.mirrorPath(path)
			if not os.path.isfile(fname):
				return -errno.ENOENT
			else:
				f = open(fname)
				f.seek(offset)
				return f.read(length)
	
	def readlink(self, path):
		syslog(LOG_DEBUG, '*** readlink %s' % path)
		
		# There aren't any symlinks here
		if getDepth(path) < 2:
			return -errno.ENOENT
		# But there might be here
		else:
			return os.readlink(self.mirrorPath(path))

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