# -*- mode: python; coding: utf-8 -*- # # Python.py # --------- # # Copyright (C) 2007 Jeremie Dimino # # 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