294 lines
12 KiB
Python
294 lines
12 KiB
Python
#!/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)
|
|
)
|