209 lines
7.8 KiB
Python
209 lines
7.8 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'''
|
|
|
|
__all__ = ["Python"]
|
|
|
|
import logging, lxml.etree, posixpath, re, os, sys
|
|
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):
|
|
'''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")
|
|
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):
|
|
'''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'] = {}
|
|
# 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")
|
|
entry.text = pygen.generate(code, env).decode("UTF-8")
|
|
debug(entry.text)
|
|
except Exception, e:
|
|
log_traceback(fname, 'exec', e)
|
|
raise Bcfg2.Server.Plugin.PluginExecutionError
|
|
entry.attrib['owner'] = env["info"]['owner']
|
|
entry.attrib['group'] = env["info"]['group']
|
|
entry.attrib['perms'] = oct(env["info"]['perms'])
|
|
|
|
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.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)
|
|
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)
|
|
self.Entries['ConfigFile'][identifier] = self.BuildEntry
|
|
elif action == 'changed':
|
|
self.codes[identifier] = load_file(path)
|
|
elif action == 'deleted':
|
|
debug("deleting config file: %s" % identifier, 'red')
|
|
del self.codes[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
|