#!/usr/bin/env python # -*- coding: utf-8 -*- # # PythonPlugin.py # --------- # # Copyright © 2015 Pierre-Elliott Bécue """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) )