# -*- 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''' import logging, lxml.etree, posixpath, re, os, sys from cStringIO import StringIO import Bcfg2.Server.Plugin 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 # Les fichiers qui sont surveillés par le plugin dans les dossier /etc/../machin.cf/ _monitored_files = ["info.xml", "gen.py"] # Dico nom de fichier -> code includes = {} def include(env, incfile): exec(includes[incfile], env) def load_file(filename): '''Charge un script et affiche un message d'erreur en cas d'exception''' try: return pygen.load(filename) except Exception, e: logger.error('Python compilation error: %s: %s' % (str(e.__class__).split('.', 2)[1], str(e))) return None class PythonFile: '''Template file creates Python template structures for the loaded file''' def __init__(self, name, properties): self.name = name self.info = None self.code = None self.properties = properties def HandleEvent(self, event): '''Handle all fs events for this template''' if event.filename == 'gen.py': self.code = load_file(self.name + '/' + event.filename) elif event.filename == 'info.xml': if not self.info: self.info = Bcfg2.Server.Plugin.XMLSrc(os.path.join(self.name[1:], event.filename), True) self.info.HandleEvent(event) else: logger.info('Ignoring event for %s' % event.filename) def Created(self, path, event): '''Traitement des créations de fichier''' if event.filename == "gen.py": self.code = load_file(path) else: self.info = Bcfg2.Server.Plugin.XMLSrc(path, True) self.info.HandleEvent(event) def Changed(self, path, event): '''Traitement des modifications de fichier''' if event.filename == "gen.py": self.code = load_file(path) else: self.info.HandleEvent(event) def Deleted(self, path, event): '''Traitement des suppressions de fichier''' if event.filename == "gen.py": self.code = None else: self.info = None return self.code or self.info def BuildFile(self, entry, metadata): '''Build literal file information''' fname = entry.get('realname', entry.get('name')) try: env = pygen.Environment() env["metadata"] = metadata env["properties"] = self.properties env["include"] = lambda incfile: include(env, incfile) include(env, "common") entry.text = pygen.generate(self.code, env) except Exception, e: logger.error('Python exec error: %s: %s' % (str(e.__class__).split('.', 2)[1], str(e))) raise Bcfg2.Server.Plugin.PluginExecutionError if self.info: mdata = {} self.info.pnode.Match(metadata, mdata) mdata = mdata['Info'][None] [entry.attrib.__setitem__(key, value) for (key, value) in mdata.iteritems()] 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): '''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['ConfigFile'] = {} # Nos entrées pour nous (dico de PythonFile) self.entries = {} # 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): '''Dispatch fetch calls to the correct object''' self.entries[entry.get('name')].BuildFile(entry, metadata) def HandleEvent(self, event): '''Traitement des événements de FAM''' action = event.code2str() debug("event received: action=%s filename=%s requestID=%s" % (action, event.filename, event.requestID), 'purple') if event.filename[0] == '/' or event.filename.endswith(".pyc"): # Rien d'intéressant return hdle_path = self.handles[event.requestID] path = hdle_path + "/" + event.filename debug("absolute filename: %s" % path, 'yellow') if path.startswith(self.include): if posixpath.isfile(path) and path.endswith(".py"): # Les fichiers d'includes... if action in ['exists', 'created', 'changed']: debug("adding include file: %s" % event.filename[:-3], 'green') includes[event.filename[:-3]] = load_file(path) elif action == 'deleted': debug("deleting include file: %s" % event.filename[:-3], 'red') del includes[event.filename[:-3]] elif posixpath.isfile(path) and event.filename in _monitored_files: # Le nom du dossier est le nom du fichier du fichier de # configuration à générer identifier = hdle_path[len(self.data):] if action in ['exists', 'created']: if not self.entries.has_key(identifier): debug("adding config file: %s" % identifier, 'green') self.entries[identifier] = PythonFile(identifier, self.properties) self.Entries['ConfigFile'][identifier] = self.BuildEntry self.entries[identifier].Created(path, event) elif action == 'changed': self.entries[identifier].Changed(path, event) elif action == 'deleted': if self.entries[identifier].Deleted(path, event): debug("deleting config file: %s" % identifier, 'red') del self.entries[identifier] del self.Entries['ConfigFile'][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" % (name) return reqid = self.core.fam.AddMonitor(path, self) self.handles[reqid] = path