Première version de la refonte du plugin bcfg2
This commit is contained in:
parent
7e5cd649eb
commit
f67d38baee
8 changed files with 1441 additions and 0 deletions
294
bcfg2new/Plugins/Python/PythonPlugin.py
Normal file
294
bcfg2new/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)
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue