# -*- mode: python; coding: utf-8 -*-
#
# Python.py
# ---------
#
# Copyright (C) 2007 Jeremie Dimino <jeremie@dimino.org>
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.

''' Plugin pour bcfg2 pour générer des fichiers de conf en prenant la sortie d'un
script python'''

__all__ = ["Python"]

import logging, lxml.etree, posixpath, re, os, sys, binascii
from cStringIO import StringIO
import Bcfg2.Server.Plugin
import traceback

sys.path.append('/usr/scripts/bcfg2')
import pygen

logger = logging.getLogger('Bcfg2.Plugins.Python')

# Helper pour le deboggage
if os.getenv("BCFG2_DEBUG"):
    debug_colored = os.getenv("BCFG2_DEBUG_COLOR")
    color_code = {'grey': 30,
                  'red': 31,
                  'green': 32,
                  'yellow': 33,
                  'blue': 34,
                  'purple': 35,
                  'cyan': 36}
    def debug(msg, color=None):
        if debug_colored and color:
            logger.info("\033[1;%dm%s\033[0m" % (color_code[color], msg))
        else:
            logger.info(msg)
else:
    def debug(msg, color=None):
        pass

# Dico nom de fichier -> code
includes = {}

def log_traceback(fname, section, exn):
    logger.error('Python %s error: %s: %s: %s' % (section, fname, str(exn.__class__).split('.', 2)[1], str(exn)))
    s = StringIO()
    sys.stderr = s
    traceback.print_exc()
    sys.stderr = sys.__stderr__
    for line in s.getvalue().splitlines():
        logger.error('Python %s error: -> %s' % (section, line))

def dump(env, incfile):
    exec(includes[incfile], env)

def include(env, incfile):
    if not incfile in env.included:
        env.included.add(incfile)
        exec(includes[incfile], env)

def load_file(filename, logger):
    '''Charge un script et affiche un message d'erreur en cas d'exception'''
    try:
        return pygen.load(filename, os.path.dirname(filename) + "/." + os.path.basename(filename) + ".COMPILED", logger)
    except Exception, e:
        log_traceback(filename, 'compilation', e)
        return None

class PythonProperties(Bcfg2.Server.Plugin.SingleXMLFileBacked):
    '''Class for Python properties'''
    def Index(self):
        '''Build data into an elementtree object for templating usage'''
        try:
            self.properties = lxml.etree.XML(self.data)
            del self.data
        except lxml.etree.XMLSyntaxError:
            logger.error("Failed to parse properties")

class FakeProperties:
    '''Dummy class used when properties dont exist'''
    def __init__(self):
        self.properties = lxml.etree.Element("Properties")

class Python(Bcfg2.Server.Plugin.Plugin,Bcfg2.Server.Plugin.Generator):
    '''The Python generator implements a templating mechanism for configuration files'''
    name = 'Python'
    __version__ = '1.0'
    __author__ = 'dimino@crans.org'

    def __init__(self, core, datastore):
        Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
        # Les entrées pour bcfg2
        self.Entries['Python'] = {}
        # Correspondance entrée ConfigFile -> code
        self.codes = {}
        # Dico ID de requête GAM -> Dossier surveillé
        self.handles = {}
        # Le dossier qui contient les fichiers à inclure
        self.include = os.path.abspath(self.data + '/../etc/python')

        self.AddDirectoryMonitor(self.data)
        self.AddDirectoryMonitor(self.include)

        try:
            self.properties = PythonProperties('%s/../etc/properties.xml' \
                                               % (self.data), self.core.fam)
        except:
            self.properties = FakeProperties()
            self.logger.info("Failed to read properties file; Python properties disabled")

    def BuildEntry(self, entry, metadata):
        '''Construit le fichier'''
        code = self.codes[entry.get('name')]
        fname = entry.get('realname', entry.get('name'))
        debug("building config file: %s" % fname, 'blue')
        env = pygen.Environment()
        env["metadata"] = metadata
        env["properties"] = self.properties
        env["include"] = lambda incfile: include(env, incfile)
        env["dump"] = lambda incfile: dump(env, incfile)
        env["info"] = { 'owner': 'root',
                        'group': 'root',
                        'perms': 0644 }
        env.included = set([])
        try:
            include(env, "common")
            text = pygen.generate(code, env, logger)
        except Exception, e:
            log_traceback(fname, 'exec', e)
            raise Bcfg2.Server.Plugin.PluginExecutionError
        info = env["info"]
        if info.get('encoding', '') == 'base64':
            text = binascii.b2a_base64(text)
        # lxml n'accepte que de l'ascii ou de l'unicode
        try:
            entry.text = text.decode("UTF-8")
        except:
            # solution de fallback
            entry.text = text.decode("ISO8859-15")
        debug(entry.text)
        entry.attrib['owner'] = info.get('owner', 'root')
        entry.attrib['group'] = info.get('group', 'root')
        entry.attrib['perms'] = oct(info.get('perms', 0644))
        if 'encoding' in info:
            entry.attrib['encoding'] = info['encoding']

    def HandleEvent(self, event):
        '''Traitement des événements de FAM'''
        # On passe les fichiers ennuyeux
        if event.filename[0] == '/' \
                or event.filename.endswith(".COMPILED") \
                or event.filename.endswith("~") \
	        	or event.filename.endswith(".swp") \
                or event.filename.startswith(".#"):
            return

        action = event.code2str()
        debug("event received: action=%s filename=%s requestID=%s" %
              (action, event.filename, event.requestID), 'purple')

        path = self.handles[event.requestID] + "/" + event.filename
        debug("absolute filename: %s" % path, 'yellow')

        if path.startswith(self.include):
            if posixpath.isfile(path):
                # Les fichiers d includes...
                identifier = path[len(self.include)+1:-3]
                if action in ['exists', 'created', 'changed']:
                    debug("adding include file: %s" % identifier, 'green')
                    includes[identifier] = load_file(path, logger)
                elif action == 'deleted':
                    debug("deleting include file: %s" % identifier, 'red')
                    del includes[identifier]
            elif posixpath.isdir(path) and action in ['exists', 'created']:
                self.AddDirectoryMonitor(path)

        elif posixpath.isfile(path):
            # Le nom du dossier est le nom du fichier du fichier de
            # configuration à générer
            identifier = path[len(self.data):]
            if action in ['exists', 'created']:
                debug("adding config file: %s" % identifier, 'green')
                self.codes[identifier] = load_file(path, logger)
                print "Python plugin : creating %s entry due to action %s" % (identifier, action)
                self.Entries['Python'][identifier] = self.BuildEntry
            elif action == 'changed':
                self.codes[identifier] = load_file(path, logger)
            elif action == 'deleted':
                debug("deleting config file: %s" % identifier, 'red')
                del self.codes[identifier]
                del self.Entries['Python'][identifier]

        elif posixpath.isdir(path):
            if action in ['exists', 'created']:
                self.AddDirectoryMonitor(path)

        else:
            logger.info('Ignoring file %s' % path)
        
    def AddDirectoryMonitor(self, path):
        '''Surveille un dossier avec FAM'''
        if path not in self.handles.values():
            if not posixpath.isdir(path):
                print "Python: Failed to open directory %s" % path
                return
            reqid = self.core.fam.AddMonitor(path, self)
            self.handles[reqid] = path
