Commit f67d38ba authored by Pierre-Elliott Bécue's avatar Pierre-Elliott Bécue

Première version de la refonte du plugin bcfg2

parent 7e5cd649
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Contient les valeurs par défaut du plugin python
de Bcfg2"""
DEFAULT_USER = 'root'
DEFAULT_GROUP = 'root'
DEFAULT_ACLS = 0644
INCLUDES = "../etc/python"
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""SafeEnvironment implementation for use of exec"""
import os
import cStringIO
import PythonDefaults
import PythonFile
class SafeEnvironment(dict):
"""Environnement isolé dans lequel on exécute un script"""
def __init__(self, additionnal=None, parent=None):
# Création de l'environment initial
super(self.__class__, self).__init__({
# Écrit: variable keysep tostring(value)
"defvar": self.defvar,
# La convertion en chaîne de charactère
"tostring": self.tostring,
# Définition des convertions
"conv": {
bool: {
True: "yes",
False: "no",
},
list: lambda l: ", ".join([
str(x)
for x in l
]),
tuple: lambda l: ", ".join([
str(x)
for x in l
]),
},
# Fonction de base pour imprimer quelque chose
"out": self.out,
# Le séparateur pour la forme: variable keysep valeur
"keysep": "=",
# Le charactère de commentaire
"comment_start": "#",
# Du mapping de certaines fonctions
"include": self.include,
# Du mapping de certaines fonctions
"dump": self.dump,
# Infos standard pour le fichier (écrasable localement)
"info": {
'owner': PythonDefaults.DEFAULT_USER,
'group': PythonDefaults.DEFAULT_GROUP,
'mode': PythonDefaults.DEFAULT_ACLS,
}
})
if additionnal is None:
additionnal = {}
super(self.__class__, self).update(additionnal)
# On crée le flux dans lequel le fichier de config sera généré
self.stream = cStringIO.StringIO()
# Le Pythonfile parent est référencé ici
self.parent = parent
# Les trucs inclus
self.included = []
def __setitem__(self, variable, value):
"""Lorsqu'on définit une variable, si elle est listée dans la variable
exports, on l'incorpore dans le fichier produit"""
super(self.__class__, self).__setitem__(variable, value)
def defvar(self, variable, value):
"""Quand on fait un export, on utilise defvar pour incorporer la variable
et sa valeur dans le fichier produit"""
# On écrit mavariable = toto, en appliquant une éventuelle conversion à toto
self.out("%s%s%s\n" % (variable, self['keysep'], self.tostring(value)))
def out(self, string):
"""C'est le print local. Sauf qu'on écrit dans self.stream. C'est pas
indispensable, car l'évaluation du fichier se fait déjà à sys.stdout
pointant vers ledit stream."""
self.stream.write(string)
def tostring(self, value):
"""On convertit un objet python dans un format "string" sympa.
En vrai c'est horrible et il faudrait virer ce genre de kludge."""
convertor = self["conv"].get(type(value))
if convertor:
if type(convertor) == dict:
return convertor[value]
else:
return convertor(value)
else:
return str(value)
def dump(self, incfile):
"""On exécute le fichier python dans l'environnement courant
incfile est le nom du fichier, sans le .py"""
filename = os.path.join(self.parent.parent.include, "%s.py" % (incfile,))
python_file = PythonFile.PythonFile(filename, self.parent.parent)
python_file.run(environment=self)
def include(self, incfile):
"""Pareil qu'au dessus, mais on ne le fait que si ça n'a pas
été fait"""
if incfile in self.included:
return
self.included.append(incfile)
self.dump(incfile)
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""Ce module est prévu pour héberger des factories, stockant toute
instance d'un fichier Python déjà compilé."""
class PythonFileFactory(object):
"""Cette Factory stocke l'ensemble des fichiers Python déjà instanciés.
Elle garantit entre autre leur unicité dans le fonctionnement du plugin"""
#: Stocke la liste des instances avec leur chemin absolu.
files = {}
@classmethod
def get(cls, path):
"""Récupère l'instance si elle existe, ou renvoit None"""
return cls.files.get(path, None)
@classmethod
def record(cls, path, instance):
"""Enregistre l'instance dans la Factory"""
cls.files[path] = instance
@classmethod
def flush_one(cls, path):
"""Vire une instance du dico"""
instance_to_delete = cls.files.pop(path, None)
del instance_to_delete
@classmethod
def flush(cls):
"""Vire toutes les instances du dico"""
for path in cls.files.keys():
cls.flush_one(path)
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""Fournit une couche d'abstraction Python pour les fichiers du même
nom"""
import os
import sys
import re
import marshal
import cStringIO
from Bcfg2.Server.Plugin import Debuggable
from .PythonFactories import PythonFileFactory
import PythonEnv
import PythonTools
__RE_SPECIAL_LINE = re.compile(r"^([ \t]*)(@|%)(.*)$", re.MULTILINE)
__RE_AFFECTATION = re.compile(r"([a-zA-Z_][a-zA-Z_0-9]*)[ \t]*=")
__RE_SPACE_SEP = re.compile(r"([^ \t]*)[ \t]+=?(.*)")
class PythonFile(Debuggable):
"""Classe représentant un fichier Python"""
#: Permet de savoir si l'instance a déjà été initialisée
initialized = False
def __new__(cls, path, parent=None):
"""Si le fichier a déjà été enregistré dans la Factory, on
le retourne, et on évite de réinstancier la classe.
path est le chemin absolu du fichier"""
path = os.path.normpath(path)
file_instance = PythonFileFactory.get(path)
if file_instance is None:
file_instance = super(PythonFile, cls).__new__(cls)
PythonFileFactory.record(path, file_instance)
return file_instance
def __init__(self, path, parent=None):
"""Initialisation, si non déjà faite"""
if self.initialized:
return
super(self.__class__, self).__init__()
#: A string containing the raw data in this file
self.data = None
#: Le chemin complet du fichier
self.path = os.path.normpath(path)
#: Le nom du fichier
self.name = os.path.basename(self.path)
#: Un logger
self.logger = PythonTools.LOGGER
#: Le plugin parent est pointé pour des raisons pratiques
self.parent = parent
#: C'est bon, c'est initialisé
self.initialized = True
def exists(self):
"""Teste l'existence du fichier"""
return os.path.exists(self.path)
def HandleEvent(self, event=None):
""" HandleEvent is called whenever the FAM registers an event.
:param event: The event object
:type event: Bcfg2.Server.FileMonitor.Event
:returns: None
"""
if event and event.code2str() not in ['exists', 'changed', 'created']:
return
try:
self.load()
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to read file %s: %s" % (self.name, err))
except:
err = sys.exc_info()[1]
self.logger.error("Failed to parse file %s: %s" % (self.name, err))
def __repr__(self):
return "%s: %s" % (self.__class__.__name__, self.name)
def load(self, refresh=True):
"""Charge le fichier"""
if self.data is not None and not refresh:
return
try:
directory = os.path.dirname(self.path)
compiled_file = os.path.join(directory, ".%s.COMPILED" % (self.name,))
if os.path.exists(compiled_file) and os.stat(self.path).st_mtime <= os.stat(compiled_file).st_mtime:
self.data = marshal.load(open(compiled_file, 'r'))
else:
self.data = compileSource(open(self.path, 'r').read(), self.path, self.logger)
cfile = open(compiled_file, "w")
marshal.dump(self.data, cfile)
cfile.close()
except Exception as error:
PythonTools.log_traceback(self.path, 'compilation', error, self.logger)
def run(self, additionnal=None, environment=None):
"""Exécute le code"""
if self.data is None:
self.load(True)
if additionnal is None:
additionnal = {}
if environment is None:
environment = PythonEnv.SafeEnvironment(additionnal, self)
# On ne réaffecte stdout que sur le premier élément de la pile
have_to_get_out = False
if sys.stdout == sys.__stdout__:
sys.stdout = environment.stream
have_to_get_out = True
# Lors de l'exécution d'un fichier, on inclut
# toujours common (ie on l'exécute dans l'environnement)
environment.include("common")
try:
exec(self.data, environment)
except Exception:
sys.stderr.write('code: %r\n' % (self.data,))
sys.stdout = sys.__stdout__
raise
if sys.stdout != sys.__stdout__ and have_to_get_out:
sys.stdout = sys.__stdout__
have_to_get_out = False
return environment.stream.getvalue(), environment['info']
#+---------------------------------------------+
#| Tools for compilation |
#+---------------------------------------------+
def compileSource(source, filename="", logger=None):
'''Compile un script'''
# On commence par remplacer les lignes de la forme
# @xxx par out("xxx")
newsource = cStringIO.StringIO()
start = 0
# Parsing de goret : on boucle sur les lignes spéciales,
# c'est-à-dire celles commençant par un @ ou un % précédé
# par d'éventuelles espaces/tabs.
for match in __RE_SPECIAL_LINE.finditer(source):
# On prend tout ce qui ne nous intéresse pas et on l'ajoute.
newsource.write(source[start:match.start()])
# On redéfinit start.
start = match.end()
# On écrit le premier groupe (les espaces et cie)
newsource.write(match.group(1))
# Le linetype est soit @ soit %
linetype = match.group(2)
# @ c'est du print.
if linetype == "@":
# On prend ce qui nous intéresse, et on fait quelques remplacements
# pour éviter les plantages.
line = match.group(3).replace("\\", "\\\\").replace('"', '\\"')
# Si la ligne est un commentaire, on la reproduit en remplaçant éventuellement
# le # par le bon caractère.
if line and line[0] == "#":
newsource.write('out(comment_start + "')
line = line[1:]
# Sinon bah....
else:
newsource.write('out("')
# On écrit ladite ligne
newsource.write(line)
# Et un superbe \n.
newsource.write('\\n")')
# %, affectation.
elif linetype == "%":
# On récupère le reste.
line = match.group(3)
# On fait du matching clef/valeur
match = __RE_AFFECTATION.match(line)
if match:
# Le nom est le premier groupe.
# Et après c'est weird...
varname = match.group(1)
newsource.write(line)
newsource.write("; defvar('")
newsource.write(varname)
newsource.write("', tostring(")
newsource.write(varname)
newsource.write("))\n")
else:
# Pareil, sauf que cette fois, ce qu'on fait a un sens.
match = __RE_SPACE_SEP.match(line)
newsource.write("defvar('")
newsource.write(match.group(1))
# Le tostring est facultatif.
newsource.write("', tostring(")
newsource.write(match.group(2))
newsource.write("))\n")
# On continue.
newsource.write(source[start:])
if logger:
try:
logger.info(newsource.getvalue())
except:
print "Le logger de BCFG2 c'est de la merde, il refuse le non ascii."
print "Voici ce que j'ai essayé de logguer."
print newsource.getvalue()
return compile(newsource.getvalue(), filename, "exec")
This diff is collapsed.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Fournit quelques outils pour le plugin Python"""
import os
import logging
import cStringIO
import traceback
LOGGER = logging.getLogger('Bcfg2.Plugins.Python')
COLOR_CODE = {
'grey': 30,
'red': 31,
'green': 32,
'yellow': 33,
'blue': 34,
'purple': 35,
'cyan': 36,
}
BCFG2_DEBUG = os.getenv("BCFG2_DEBUG")
BCFG2_DEBUG_COLOR = os.getenv("BCFG2_DEBUG_COLOR")
def debug(message, logger, color=None):
"""Stocke dans un logger les messages de debug"""
if not BCFG2_DEBUG:
return
if BCFG2_DEBUG_COLOR and color:
logger.info("\033[1;%dm%s\033[0m" % (COLOR_CODE[color], message))
else:
logger.info(message)
def log_traceback(fname, section, exn, logger):
"""En cas de traceback, on le loggue sans faire planter
le serveur bcfg2"""
logger.error('Python %s error: %s: %s: %s' % (section, fname, str(exn.__class__).split('.', 2)[1], str(exn)))
stream = cStringIO.StringIO()
traceback.print_exc(file=stream)
for line in stream.getvalue().splitlines():
logger.error('Python %s error: -> %s' % (section, line))
class PythonIncludePaths(object):
"""C'est un objet qui stocke les dossier d'inclusion python"""
includes = []
@classmethod
def get(cls, index, default):
"""Retourne includes[index] ou default"""
if len(cls.includes) > index:
return cls.includes[index]
return default
@classmethod
def append(cls, value):
"""Ajoute une valeur à la liste"""
cls.includes.append(value)
@classmethod
def remove(cls, value):
"""Retire une valeur à la liste"""
if value in cls.includes:
cls.includes.remove(value)
@classmethod
def pop(cls, index):
"""Vire un index si existant"""
if len(cls.includes) > index:
return cls.includes.pop(index)
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""Python plugin initializator for
Bcfg2"""
from .PythonPlugin import Python
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment