scripts/bcfg2/plugins/Python.py
Jeremie Dimino 16248737ba Plugin Python pour bcfg2
Ajout d'un plugin pour bcfg2 qui éxécute un script en python pour
générer une entrée de type ConfigFile.
Ça peut s'avérer assez pratique notamment pour la conf de monit, munin
et co.

darcs-hash:20071215223223-af139-778a3830aabcfb7d2fb1af84850973d7db437f63.gz
2007-12-15 23:32:23 +01:00

234 lines
8.9 KiB
Python

# -*- 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'''
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