scripts/bcfg2/Plugins/Python/PythonPlugin.py

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)
)