#!/usr/bin/python
# 
'''
svnengine : a tool to help creating and defining management class for
integrating cfengine with subversion
'''

# std imports
import os, sys
import string
import shutil
import time
from pwd import getpwuid, getpwnam
from grp import getgrgid, getgrnam
from stat import ST_MODE, ST_UID, ST_GID

# special imports
from inisettings import IniSettings
from svn_cmd import SvnCommand, CommandError
import pysvn

def normalize_path(m_path):
	# expand user's home directory
	if m_path[0] is '~':
	    m_path = os.path.expanduser(m_path)
	# get abs path if it not already
	if not os.path.isabs(m_path):
	    m_path = os.path.abspath(m_path)
	m_path = os.path.normpath(m_path)
	return m_path

class SvnEngineConfig(IniSettings):

    def __init__(self, filename):
	IniSettings.__init__(self, filename)
	self.default_working_directory = os.path.expanduser('~/.svnengine/repos')
	self.setDefault('global', 'repos_path', self.default_working_directory)
	self.setDefault('global', 'current_class', '')
	self.save()

    def getWorkingDirectory(self):
	return self.settings['global']['repos_path']

    def getDefaultWorkingDirectory(self):
	return self.default_working_directory

    def setWorkingDirectory(self, wd):
	wd = normalize_path(wd)
	# save the value
	self.set('global', 'repos_path', wd)
	self.save()

    def getCurrentClass(self):
	return self.settings['global']['current_class']

    def setCurrentClass(self, m_class):
	self.set('global', 'current_class', m_class)	

class SvnEngineCommand(SvnCommand):

    cmd_list = ['checkout','commit','update','status','propset','add','apply','setclass','delclass','listclasses','useclass','getclass','newclass','collect','cfengine','help']

    def __init__(self, config):
	self.config = config
	self.m_class_prop = 'svnengine:isclass'
	SvnCommand.__init__(self,'svnengine')

    def cmd_checkout(self, args):
	recurse = args.getBooleanOption( '--non-recursive', False )
	if recurse is False:
	    raise CommandError( '--non-recursive must not be set' )
	positional_args = args.getPositionalArgs( 1, 2 )
	basename = None
	if len(positional_args) == 2:
	    # Set custom working directory if specified
	    basename = positional_args[1]
	
	if not basename:
	    if positional_args[0][-1] == "/":
		positional_args[0] = positional_args[0][:-1]
	    basename = string.split(positional_args[0],"/")[-1]

	self.revision_update_complete = None
	co_dir = os.path.join(self.config.getDefaultWorkingDirectory(), basename)
	self.config.setWorkingDirectory( co_dir )
	self.client.checkout( positional_args[0], co_dir, recurse=recurse )
	self.printNotifyMessages()
	if self.revision_update_complete is not None:
	    print 'Checked out revision',self.revision_update_complete.number
	else:
	    print 'Checked out unknown revision - checkout failed?'

    # FIXME: This shortcut doesn't seems to work as expected
    cmd_co = cmd_checkout

    def cmd_commit(self, args):
	msg = args.getOptionalValue( '--message', '' )
	if msg == '':
	     raise CommandError('--message is mandatory')

	recurse = args.getBooleanOption( '--non-recursive', False )
	if recurse is False:
	    raise CommandError('--non-recursive must not be set')

	rev = self.client.checkin( self.config.getWorkingDirectory(), msg, recurse=recurse )
	self.printNotifyMessages()
	if rev is None:
	    print 'Nothing to commit'
	elif rev.number > 0:
	    print 'Revision',rev.number
	else:
	    print 'Commit failed'

    def cmd_update(self, args):
	recurse = args.getBooleanOption( '--non-recursive', False )
	if recurse is False:
	    raise CommandError('--non-recursive must not be set')
	rev_list = self.client.update( self.config.getWorkingDirectory(), recurse=recurse )
	self.printNotifyMessages()
	if type(rev_list) == type([]) and len(rev_list) != 1:
	    print 'rev_list = %r' % [rev.number for rev in rev_list]

	if self.revision_update_complete is not None:
	    print 'Updated to revision',self.revision_update_complete.number
	else:
	    print 'Updated to unknown revision - update failed?'

    def cmd_status(self, args):
	recurse = args.getBooleanOption( '--non-recursive', False )
	if recurse is False:
	    raise CommandError('--non-recursive must not be set')
	verbose = args.getBooleanOption( '--verbose', True )
	ignore = args.getBooleanOption( '--no-ignore', False )
	update = args.getBooleanOption( '--show-updates', True )
	all_files = self.client.status( self.config.getWorkingDirectory(), recurse=recurse, get_all=verbose, ignore=ignore, update=update )
	self._cmd_status_print( all_files, verbose, update, ignore )

    def cmd_useclass(self, args):
	positional_args = args.getPositionalArgs( 1, 1 )
	wanted_class = positional_args[0]
	if wanted_class in self.getAvailableClasses():
	    self.config.setCurrentClass(wanted_class)
	    self.config.save()
	    print 'Now using the class "%s"' % wanted_class
	else:
	    raise CommandError('The class "%s" does not exists' % wanted_class)

    def cmd_listclasses(self, args):
	all_classes = self.getAvailableClasses()
	if len(all_classes) == 0:
	    print 'No classes availables'
	else:
	    print 'Available classes : '
	    for c in all_classes:
		print c

    def getAvailableClasses(self):
	wd = self.config.getWorkingDirectory()
	dirs = os.listdir(wd)
	self.debug(str(dirs))
	revision = pysvn.Revision( pysvn.opt_revision_kind.working )
	classes = []
	# remove hiden directory
	for d in dirs:
	    if d[0] is not '.':
		# get only directories that have the property m_class_prop set
		m_path =  os.path.join(wd, d)
		prop_dic = self.client.propget( self.m_class_prop, m_path, revision=revision, recurse=False )
		prop_val = prop_dic.get(m_path)
		if prop_val:
			prop_val = prop_val.strip().lower()
		if prop_val == 'on' or prop_val == '*' or prop_val == 'yes':
		    classes.append(d)
	self.debug('available classes : %s' % classes)
	return classes

    def cmd_newclass(self, args):
	wd = self.config.getWorkingDirectory()
	revision = pysvn.Revision( pysvn.opt_revision_kind.working )
	positional_args = args.getPositionalArgs( 1, 1 )
	new_class = positional_args[0]
	if not self.is_validate_class_name(new_class):
	    raise CommandError('The class name "%s" does contain illegal characters.' % new_class)
	dirs = os.listdir(wd)
	if new_class in dirs:
	    print 'The directory for the class "%s" already exists' % new_class
	else:
	    m_path = os.path.join(wd,new_class)
	    self.client.mkdir(m_path, 'adding new class')
	    self.client.propset(self.m_class_prop, 'yes', m_path, revision=revision, recurse=False)
	    print 'New class "%s" has been created. You may commit now.' % new_class 

    def cmd_add(self, args):
	wd = self.config.getWorkingDirectory()
	# Get the current class, we will operate onto it. 
	m_class = self.config.getCurrentClass()
	if m_class not in self.getAvailableClasses():
	    raise CommandError('The class "%s" does not exists. Please select a valid class with the command "useclass"' % m_class)

	positional_args = args.getPositionalArgs( 1, 1 )

	# normalize the path
	m_path = normalize_path(positional_args[0])

	# Verify that we are trying to add a file
	if not os.path.isfile(m_path):
	     raise CommandError('The path "%s" is not a file.' % m_path)
	
	# Verify that the file does not exists already in the class
	if os.path.isfile( os.path.join( wd, m_class, m_path[1:] ) ):
	    raise CommandError('The file "%s" already exists in class %s' % (m_path, m_class) )

	# Create directory tree
	dirs = os.path.split( m_path[1:] )[0]
	dirs_path = os.path.join( wd, m_class, dirs )
	if not os.path.isdir( dirs_path ):
	    os.makedirs( dirs_path )

	# Copy the file in the working directory
	complete_path = os.path.join( wd, m_class, m_path[1:] )
	shutil.copy2( m_path, complete_path )

	# Schedule the add of the file
	to_add = string.split( m_path[1:], os.path.sep )
	self.debug( 'to_add = %s' % to_add )
	for i in range( 1, len(to_add) + 1 ):
	    path_to_add = os.path.join( wd, m_class, string.join(to_add[0:i], os.path.sep) )
	    self.debug( path_to_add )
	    # FIXME: should add only if the file or directory is not already under revision
	    try:
		self.client.add( path_to_add, recurse=True, force=False )
	    except:
		pass

	# Add properties to the added file
        self.set_svn_properties( path_to_add, self.get_system_file_properties(m_path) )
        print 'Properties has been set on the file.'
	print 'The file "%s" has been added to the repository' % m_path

    def get_system_file_properties(self, m_path):
	file_props  = os.stat(m_path)
        file_group  = getgrgid(file_props[ST_GID])[0]
	file_owner  = getpwuid(file_props[ST_UID])[0]
	file_dest   = m_path
        file_backup = True
        file_mode   = oct(os.stat(m_path)[ST_MODE] & 0777)
        file_type   = 'sum'
        properties  = {	'group':file_group,
    			'owner':file_owner,
    			'dest':file_dest,
    			'backup':file_backup,
    			'mode':file_mode,
    			'type':file_type
    		    }
	return properties

    def get_file_list_in_class(self, m_class):
	wd = self.config.getWorkingDirectory()
	base_class_dir = os.path.join(wd, m_class)
	file_list = []
	for root, dirs, files in os.walk( base_class_dir ):
	    if '.svn' not in string.split(root,os.path.sep):
		for file in files:
		    file_list.append( os.path.join(root,file) )
	return file_list

    def cmd_collect(self, args):
	"""Collect changements from managed files on the filesystem. Notify the user about modifications"""
	# Get the current class, we will operate onto it. 
	wd = self.config.getWorkingDirectory()
	m_class = self.config.getCurrentClass()
	if m_class not in self.getAvailableClasses():
	    raise CommandError('The class "%s" does not exists. Please select a valid class with the command "useclass"' % m_class)
	# For each directory and files from the current class
	base_class_dir = os.path.join(wd, m_class)
	m_files = self.get_file_list_in_class( m_class )	
	self.debug( 'Files to process : %s ' % m_files )
	for m_file in m_files:
	    # Remove common prefix to get the corresponding file path on the system
	    s_file = string.split( m_file, base_class_dir )[1]
	    if os.path.isfile(s_file):
		try:
		    self.debug("Copy %s to %s " % ( s_file, m_file ))
		    shutil.copy2( s_file,  m_file )
		except:
		    print "Error while copying the file %s. " % m_file

		# Get the file attributes and assign properties accordingly
		self.set_svn_properties( m_file, self.get_system_file_properties(s_file) )

	    else:
		# If the file doesn't exists on the system then inform the user
		print "Warning : Can't collect file %s because it's not a file." % ( m_file )

	# Do a diff to see what has changed
        revision1 = pysvn.Revision( pysvn.opt_revision_kind.base )
	revision2 = pysvn.Revision( pysvn.opt_revision_kind.working )

        if os.environ.has_key('TEMP'):
            tmpdir = os.environ['TEMP']
        elif os.environ.has_key('TMPDIR'):
            tmpdir = os.environ['TMPDIR']
        elif os.environ.has_key('TMP'):
            tmpdir = os.environ['TMP']
        elif os.path.exists( '/usr/tmp' ):
            tmpdir = '/usr/tmp'
        elif os.path.exists( '/tmp' ):
            tmpdir = '/tmp'
        else:
            print 'No tmp dir!'
            return

        tmpdir = os.path.join( tmpdir, 'svn_tmp' )
        self.debug( 'cmd_diff %r, %r, %r, %r, %r' % (tmpdir, base_class_dir, True, revision1, revision2) )
        diff_text = self.client.diff( tmpdir, base_class_dir, recurse=True, revision1=revision1, revision2=revision2 )
        print diff_text
	

    def set_svn_properties(self, m_path, m_prop):
	"""
	Set properties from a dictionnary on a path under revision control
	"""
	rev = pysvn.Revision( pysvn.opt_revision_kind.working )
	for prop,value in m_prop.items():
	    self.debug('On %s setting %s=%s' % ( m_path, prop, value ) )
	    self.client.propset( prop, str(value), m_path, revision=rev, recurse=False )

    def get_svn_properties(self, m_path):
	"""
	Get all properties names and values defined on a path under revision control
	"""
	rev = pysvn.Revision( pysvn.opt_revision_kind.working )
	prop_list = self.client.proplist( m_path, revision=rev, recurse=False )
	if len(prop_list) == 1:
	    return prop_list[0][1]
	else:
	    return {}

    def cmd_apply(self, args):
	# Get the list of classes to apply
	# perform applyClass on each
	wd = self.config.getWorkingDirectory()
	m_class = self.getAvailableClasses()
	positional_args = args.getPositionalArgs( 1 )
	for cl in positional_args:
	    if cl not in m_class:
	    	raise CommandError( 'The class "%s" does not exists' % cl )
	for cl in positional_args:
	    self.debug( 'Apply the class %s' % cl )
	    self.applyClass(cl)
	    
    def applyClass(self, m_class):
	# Get the list of files to manage
	wd = self.config.getWorkingDirectory()
	base_class_dir = os.path.join(wd, m_class)
	file_list = self.get_file_list_in_class( m_class )
	for m_file in file_list:
	    self.debug( 'About to process the file : %s ' % m_file )
	    # Get the dest property of the file
	    props = self.get_svn_properties( m_file )
	    self.debug('Properties of the file %s : %s' % ( m_file, str(props) ) )
	    # Verify properties
	    mandatory_properties = ['dest','owner','group', 'mode', 'backup']
	    for mp in mandatory_properties:
		if not props.has_key(mp):
		    print 'Warning: the file %s does not have the property %s ' % ( m_file, mp )
		    props[mp] = None

	    # Perform actions
	    s_file = string.split( m_file, base_class_dir )[1] 
	    if props['backup']:
		back_file = s_file + '-' + str(int(time.time()))
		self.debug('cp %s %s' % ( s_file, back_file ) )
		try:
		    shutil.copy2( s_file, back_file )
		except:
		    print "Error while backup the file %s" % s_file

	    if props['dest']:
		self.debug('cp %s %s' % ( m_file, props['dest'] ) )
		try:
		    shutil.copy2( m_file, props['dest'] )
		except:
		    print "Error while copying the file %s" % m_file

	    if props['owner'] and props['group']:
		self.debug('chown %s:%s %s' % ( props['owner'], props['group'], props['dest'] ) )
		uid = getpwnam(props['owner'])[2]
		gid = getgrnam(props['group'])[2]
		try:
		    os.chown( props['dest'], uid, gid )
		except:
		    print "Error while chown the file %s" % props['dest']

	    if props['mode']:
		self.debug('chmod %s %s' % ( props['mode'], props['dest'] ) )
		try:
		    os.chmod( props['dest'], string.atoi('0700',8) )
		except:
		    print "Error while chmod the file %s" % props['dest']
	    
    def is_validate_class_name(self, class_name):
	valid = True
	s =  string.letters + string.digits
	for char in class_name:
	    if char not in s:
		valid = False
	return valid

    def cmd_cfengine(self, args):
	"""Generate a CFengine class file with the class definition from svnengine"""
	# Get the class to generate
	m_class = self.getAvailableClasses()
	positional_args = args.getPositionalArgs( 1 )
	for cl in positional_args:
	    if cl not in m_class:
	    	raise CommandError( 'The class "%s" does not exists' % cl )
	c = []
	for cl in positional_args:
	    self.debug( 'About to generate the class %s' % cl )
	    c += self.generate_cfengine_class(cl)
	for line in c:
	    sys.stdout.writelines(line + '\n')

    def generate_cfengine_class(self, m_class):
	# Get the list of files from the current class
	wd = self.config.getWorkingDirectory()
	base_class_dir = os.path.join(wd, m_class)
	file_list = self.get_file_list_in_class( m_class )
	file_source_name = "fileSource%s" % m_class
	s = ["# This Cfengine class has been auto generated by svnengine on %s. Do not edit by hand." % time.ctime()]
	s.append('control:')
	s.append('\tany::' )
	s.append('\tactionsequence  = ( copy processes shellcommands )' )
	s.append('\t%s = ( $(MasterFiles)/%s )' % (file_source_name, m_class) )
	
	s.append('copy:')
	s.append('any::')
	for m_file in file_list:
	    self.debug( 'About to process the file : %s ' % m_file )
	    # Get the dest property of the file
	    props = self.get_svn_properties( m_file )
	    self.debug('Properties of the file %s : %s' % ( m_file, str(props) ) )
	    # Verify properties
	    s_file = string.split( m_file, base_class_dir )[1]
	    supported_properties = {'dest':s_file,'owner':os.getenv('USER'),'group':os.getenv('USER'), 'mode':'0644', 'backup':'true'}
	    for mp, default in supported_properties.items():
		if not props.has_key(mp):
		    print 'Warning: the file %s does not have the property %s, using default value %s ' % ( m_file, mp, default )
		    props[mp] = default

	    s.append('\t$(%s)%s' % (file_source_name, s_file) )
	    for prop,val in props.items():
		# Prevent to add unknow properties to the list
		if prop in supported_properties.keys():
		    if prop == "backup":
			val = val.lower()
		    s.append('\t\t%s=%s' % (prop,val) )
	    s.append('\t\tserver=$(PolicyServer)')
	return s

#    def cmd_propset(self, args):
#	recurse = args.getBooleanOption( '--recursive', True )
#	revision = args.getOptionalRevision( '--revision', 'working' )
#	verbose = args.getBooleanOption( '--verbose', True )
	

def usage(e=None):
    if e:
	print e
    print ""
    print "Enter 'svnengine help' for help"
    sys.exit(1)

def help():
    print "usage : svn <sous-commande> [options] [parametres]"
    for cmd in cmd_list:
	    print cmd
    print

def main():
    if len(sys.argv) < 2:
	usage()

    program     = os.path.split(sys.argv[0])[1]
    command	= sys.argv[1]
    debug       = 0

    if command not in SvnEngineCommand.cmd_list:
	usage("Error : command %s is invalid." % (command))

    # Find configuration file
    # local config file should have precedence on
    # global settings
    default_config_file = os.path.join(os.getenv("HOME"),".svnengine/svnengine.conf")
    config_files_alternatives = ["./svnengine.conf", default_config_file]
    config_file = None
    for file in config_files_alternatives:
	if os.path.isfile(file):
	    config_file = file

    if not config_file:
	# Create default configuration file in the user's home
	config_file = default_config_file 
        print "Creating default config file :  ", config_file
	config_dir = os.path.join(os.getenv("HOME"),".svnengine")
	try:
	    os.makedirs(config_dir)
	except:
	    print "Unable to create configuration directory"

	default_config = """
[global]

[cfengine_default_props]
group=auto
dest=auto
backup=true
mode=auto
owner=auto
type=sum

"""
	file = open(config_file,"w+")
	file.writelines(default_config)
	file.close()

    try:
        config = SvnEngineConfig(config_file)
    except:
	usage("Error when accessing the configuration file")

    engine = SvnEngineCommand(config)
    engine.dispatch(sys.argv[1:]) 
    
if __name__=="__main__":
    main()

