On remet bcfg2_reports et bcfg2-graph, et on renomme le nouveau dossier
This commit is contained in:
parent
1df9d480af
commit
08007c623e
10 changed files with 0 additions and 0 deletions
10
bcfg2/Plugins/Python/PythonDefaults.py
Normal file
10
bcfg2/Plugins/Python/PythonDefaults.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
#!/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"
|
113
bcfg2/Plugins/Python/PythonEnv.py
Normal file
113
bcfg2/Plugins/Python/PythonEnv.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
#!/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,
|
||||
"_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" % (variable, self['keysep'], self.tostring(value)))
|
||||
|
||||
def out(self, string=""):
|
||||
"""C'est le print local. Sauf qu'on écrit dans self.stream"""
|
||||
self._out("%s\n" % (string,))
|
||||
|
||||
def _out(self, string=""):
|
||||
"""C'est le print local sans retour à la ligne."""
|
||||
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)
|
33
bcfg2/Plugins/Python/PythonFactories.py
Normal file
33
bcfg2/Plugins/Python/PythonFactories.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
#!/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)
|
221
bcfg2/Plugins/Python/PythonFile.py
Normal file
221
bcfg2/Plugins/Python/PythonFile.py
Normal file
|
@ -0,0 +1,221 @@
|
|||
#!/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)
|
||||
|
||||
# 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,))
|
||||
raise
|
||||
|
||||
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('")')
|
||||
|
||||
# %, 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")
|
294
bcfg2/Plugins/Python/PythonPlugin.py
Normal file
294
bcfg2/Plugins/Python/PythonPlugin.py
Normal file
|
@ -0,0 +1,294 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# PythonPlugin.py
|
||||
# ---------
|
||||
#
|
||||
# Copyright © 2015 Pierre-Elliott Bécue <becue@crans.org>
|
||||
"""Plugin servant à gérer des fichiers python, dont la sortie sera
|
||||
la configuration d'un client."""
|
||||
|
||||
#: N'exporte que la classe Python
|
||||
__all__ = [
|
||||
"Python",
|
||||
]
|
||||
|
||||
import os
|
||||
import re
|
||||
import binascii
|
||||
|
||||
from Bcfg2.Server.Plugin import Plugin, Generator, PluginExecutionError, track_statistics
|
||||
from Bcfg2.Server.Plugin.base import Debuggable
|
||||
|
||||
import PythonTools
|
||||
import PythonDefaults
|
||||
import PythonFile
|
||||
|
||||
class Python(Plugin, Generator, Debuggable):
|
||||
"""Générateur offrant des fonctionnalités de templating pour les fichiers python"""
|
||||
|
||||
name = 'Python'
|
||||
|
||||
#: Les DirectoryBacked ont des fonctions de monitoring
|
||||
#: intégrées. Quand des changements arrivent sur les dossiers,
|
||||
#: c'est la merde, il est préférable de relancer Bcfg2, car
|
||||
#: FileMonitor ne sait pas démonitorer/remonitorer.
|
||||
#: En revanche, pour les fichiers, il appelle __child__ comme
|
||||
#: "générateur" pour les trucs à surveiller. Quand un fichier
|
||||
#: est créé/modifié, sa méthode HandleEvent est appelée.
|
||||
__child__ = PythonFile.PythonFile
|
||||
|
||||
#: Ce module gère plein de choses.
|
||||
patterns = re.compile(r'.*')
|
||||
|
||||
#: Ignore ces chemins spécifiques
|
||||
ignore = re.compile(r'.*\.(COMPILED|pyc)')
|
||||
|
||||
__version__ = '2.0'
|
||||
|
||||
__author__ = 'becue@crans.org'
|
||||
|
||||
def __init__(self, core, datastore):
|
||||
"""Pour initialiser le plugin"""
|
||||
|
||||
#: Initialise un certain nombre de choses en background
|
||||
Plugin.__init__(self, core, datastore)
|
||||
Debuggable.__init__(self)
|
||||
|
||||
#: self.entries contains information about the files monitored
|
||||
#: by this object. The keys of the dict are the relative
|
||||
#: paths to the files. The values are the objects (of type
|
||||
#: :attr:`__child__`) that handle their contents.
|
||||
self.entries = {}
|
||||
|
||||
#: self.handles contains information about the directories
|
||||
#: monitored by this object. The keys of the dict are the
|
||||
#: values returned by the initial fam.AddMonitor() call (which
|
||||
#: appear to be integers). The values are the relative paths of
|
||||
#: the directories.
|
||||
self.handles = {}
|
||||
|
||||
#: FileMonitor
|
||||
self.fam = self.core.fam
|
||||
|
||||
#: Monitor everything in the plugin's directory
|
||||
if not os.path.exists(self.data):
|
||||
self.logger.warning("%s does not exist, creating" % (self.data,))
|
||||
os.makedirs(self.data)
|
||||
self.add_directory_monitor('')
|
||||
|
||||
#: Dossier des includes
|
||||
self.include = os.path.abspath(os.path.join(self.data, PythonDefaults.INCLUDES))
|
||||
|
||||
#: Quand on initialise un DirectoryBacked, on a déjà un monitoring de
|
||||
#: self.data, donc on a besoin que des includes
|
||||
self.add_directory_monitor(PythonDefaults.INCLUDES)
|
||||
|
||||
@track_statistics()
|
||||
def HandlesEntry(self, entry, metadata):
|
||||
"""Vérifie si l'entrée est gérée par le plugin"""
|
||||
relpath = entry.get('name')[1:]
|
||||
if relpath in self.entries:
|
||||
return True
|
||||
return False
|
||||
|
||||
@track_statistics()
|
||||
def HandleEntry(self, entry, metadata):
|
||||
"""Construit le fichier demandé"""
|
||||
# On récupère le code qui va bien.
|
||||
relpath = entry.get('name')[1:]
|
||||
python_file = self.entries[relpath]
|
||||
|
||||
# Et le nom de fichier.
|
||||
fname = entry.get('realname', entry.get('name'))
|
||||
|
||||
# Si on est en débug, on loggue ce qu'on fait.
|
||||
PythonTools.debug("Building config file: %s" % (fname,), PythonTools.LOGGER, 'blue')
|
||||
|
||||
# On crée un environnement autonome pour exécuter le fichier.
|
||||
additionnal = {
|
||||
'metadata': metadata,
|
||||
}
|
||||
|
||||
try:
|
||||
text, info = python_file.run(additionnal)
|
||||
except Exception as error:
|
||||
PythonTools.log_traceback(fname, 'exec', error, PythonTools.LOGGER)
|
||||
raise PluginExecutionError
|
||||
|
||||
# On récupère les infos
|
||||
if info.get('encoding', '') == 'base64':
|
||||
text = binascii.b2a_base64(text)
|
||||
|
||||
# lxml n'accepte que de l'ascii ou de l'unicode
|
||||
# donc faut décoder.
|
||||
try:
|
||||
entry.text = text.decode("UTF-8")
|
||||
except:
|
||||
# solution de fallback
|
||||
entry.text = text.decode("ISO-8859-15")
|
||||
|
||||
# En cas de débug, on stocke les données
|
||||
PythonTools.debug(entry.text, PythonTools.LOGGER)
|
||||
|
||||
# On récupère les permissions depuis le dico "info".
|
||||
# En théorie, les valeurs par défaut ne devraient pas être utilisées
|
||||
# Car elles sont déjà affectées dans Pygen
|
||||
entry.attrib['owner'] = info.get('owner', PythonDefaults.DEFAULT_USER)
|
||||
entry.attrib['group'] = info.get('group', PythonDefaults.DEFAULT_GROUP)
|
||||
entry.attrib['mode'] = oct(info.get('mode', PythonDefaults.DEFAULT_ACLS))
|
||||
|
||||
if 'encoding' in info:
|
||||
entry.attrib['encoding'] = info['encoding']
|
||||
|
||||
def add_directory_monitor(self, relative):
|
||||
""" Add a new directory to the FAM for monitoring.
|
||||
|
||||
:param relative: Path name to monitor. This must be relative
|
||||
to the plugin's directory. An empty string
|
||||
value ("") will cause the plugin directory
|
||||
itself to be monitored.
|
||||
:type relative: string
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
#: On normalise pour éviter des problèmes quand le FileMonitor
|
||||
#: voit des changements par la suite.
|
||||
#: Les chemins sont absolus pour la même raison.
|
||||
dirpathname = os.path.normpath(os.path.join(self.data, relative))
|
||||
if relative not in self.handles.values():
|
||||
if not os.path.isdir(dirpathname):
|
||||
self.logger.error("%s is not a directory" % (dirpathname,))
|
||||
return
|
||||
#: reqid est un chemin absolu sans trailing slash
|
||||
reqid = self.fam.AddMonitor(dirpathname, self)
|
||||
self.handles[reqid] = relative
|
||||
|
||||
def add_entry(self, relative, event):
|
||||
""" Add a new file to our tracked entries, and to our FAM for
|
||||
monitoring.
|
||||
|
||||
:param relative: Path name to monitor. This must be relative
|
||||
to the plugin's directory.
|
||||
:type relative: string:
|
||||
:param event: FAM event that caused this entry to be added.
|
||||
:type event: Bcfg2.Server.FileMonitor.Event
|
||||
:returns: None
|
||||
"""
|
||||
#: Les entrées sont en relatif depuis le dossier de config
|
||||
self.entries[relative] = self.__child__(
|
||||
os.path.join(self.data, relative),
|
||||
self
|
||||
)
|
||||
self.entries[relative].HandleEvent(event)
|
||||
|
||||
def HandleEvent(self, event):
|
||||
""" Handle FAM events.
|
||||
|
||||
This method is invoked by the FAM when it detects a change to
|
||||
a filesystem object we have requsted to be monitored.
|
||||
|
||||
This method manages the lifecycle of events related to the
|
||||
monitored objects, adding them to our list of entries and
|
||||
creating objects of type :attr:`__child__` that actually do
|
||||
the domain-specific processing. When appropriate, it
|
||||
propogates events those objects by invoking their HandleEvent
|
||||
method in turn.
|
||||
|
||||
:param event: FAM event that caused this entry to be added.
|
||||
:type event: Bcfg2.Server.FileMonitor.Event
|
||||
:returns: None
|
||||
"""
|
||||
action = event.code2str()
|
||||
|
||||
# Exclude events for actions we don't care about
|
||||
if action == 'endExist':
|
||||
return
|
||||
|
||||
if event.requestID not in self.handles:
|
||||
self.logger.warn(
|
||||
"Got %s event with unknown handle (%s) for %s" % (action, event.requestID, event.filename)
|
||||
)
|
||||
return
|
||||
|
||||
# Clean up path names
|
||||
event.filename = os.path.normpath(event.filename)
|
||||
if event.filename.startswith(self.data) or os.path.normpath(event.requestID) == event.filename:
|
||||
# the first event we get is on the data directory itself
|
||||
event.filename = event.filename[len(os.path.normpath(event.requestID)) + 1:]
|
||||
|
||||
if self.ignore and self.ignore.search(event.filename):
|
||||
self.logger.debug("Ignoring event %s" % (event.filename,))
|
||||
return
|
||||
|
||||
# Calculate the absolute and relative paths this event refers to
|
||||
abspath = os.path.join(self.data, self.handles[event.requestID],
|
||||
event.filename)
|
||||
relpath = os.path.join(self.handles[event.requestID],
|
||||
event.filename).lstrip('/')
|
||||
|
||||
if action == 'deleted':
|
||||
for key in list(self.entries.keys()):
|
||||
if key.startswith(relpath):
|
||||
del self.entries[key]
|
||||
# We remove values from self.entries, but not
|
||||
# self.handles, because the FileMonitor doesn't stop
|
||||
# watching a directory just because it gets deleted. If it
|
||||
# is recreated, we will start getting notifications for it
|
||||
# again without having to add a new monitor.
|
||||
elif os.path.isdir(abspath):
|
||||
# Deal with events for directories
|
||||
if action in ['exists', 'created']:
|
||||
self.add_directory_monitor(relpath)
|
||||
elif action == 'changed':
|
||||
if relpath in self.entries:
|
||||
# Ownerships, permissions or timestamps changed on
|
||||
# the directory. None of these should affect the
|
||||
# contents of the files, though it could change
|
||||
# our ability to access them.
|
||||
#
|
||||
# It seems like the right thing to do is to cancel
|
||||
# monitoring the directory and then begin
|
||||
# monitoring it again. But the current FileMonitor
|
||||
# class doesn't support canceling, so at least let
|
||||
# the user know that a restart might be a good
|
||||
# idea.
|
||||
self.logger.warn(
|
||||
"Directory properties for %s changed, please consider restarting the server" % (abspath)
|
||||
)
|
||||
else:
|
||||
# Got a "changed" event for a directory that we
|
||||
# didn't know about. Go ahead and treat it like a
|
||||
# "created" event, but log a warning, because this
|
||||
# is unexpected.
|
||||
self.logger.warn(
|
||||
"Got %s event for unexpected dir %s" % (action, abspath)
|
||||
)
|
||||
self.add_directory_monitor(relpath)
|
||||
else:
|
||||
self.logger.warn(
|
||||
"Got unknown dir event %s %s %s" % (event.requestID, event.code2str(), abspath)
|
||||
)
|
||||
elif self.patterns.search(event.filename):
|
||||
if action in ['exists', 'created']:
|
||||
self.add_entry(relpath, event)
|
||||
elif action == 'changed':
|
||||
if relpath in self.entries:
|
||||
self.entries[relpath].HandleEvent(event)
|
||||
else:
|
||||
# Got a "changed" event for a file that we didn't
|
||||
# know about. Go ahead and treat it like a
|
||||
# "created" event, but log a warning, because this
|
||||
# is unexpected.
|
||||
self.logger.warn(
|
||||
"Got %s event for unexpected file %s" % (action, abspath)
|
||||
)
|
||||
self.add_entry(relpath, event)
|
||||
else:
|
||||
self.logger.warn(
|
||||
"Got unknown file event %s %s %s" % (event.requestID, event.code2str(), abspath)
|
||||
)
|
||||
else:
|
||||
self.logger.warn(
|
||||
"Could not process filename %s; ignoring" % (event.filename)
|
||||
)
|
73
bcfg2/Plugins/Python/PythonTools.py
Normal file
73
bcfg2/Plugins/Python/PythonTools.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
#!/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)
|
||||
|
6
bcfg2/Plugins/Python/__init__.py
Normal file
6
bcfg2/Plugins/Python/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env python2.7
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Python plugin initializator for
|
||||
Bcfg2"""
|
||||
|
||||
from .PythonPlugin import Python
|
Loading…
Add table
Add a link
Reference in a new issue