diff options
-rw-r--r-- | doc/server/plugins/connectors/templatehelper.txt | 72 | ||||
-rw-r--r-- | examples/TemplateHelper/include.py | 97 | ||||
-rw-r--r-- | src/lib/Server/Plugin.py | 5 | ||||
-rw-r--r-- | src/lib/Server/Plugins/TemplateHelper.py | 83 |
4 files changed, 255 insertions, 2 deletions
diff --git a/doc/server/plugins/connectors/templatehelper.txt b/doc/server/plugins/connectors/templatehelper.txt new file mode 100644 index 000000000..67219109f --- /dev/null +++ b/doc/server/plugins/connectors/templatehelper.txt @@ -0,0 +1,72 @@ +.. -*- mode: rst -*- + +.. _server-plugins-connectors-templatehelper: + +============== +TemplateHelper +============== + +The TemplateHelper plugin is a connector plugin that adds Python +classes and methods to client metadata instances for use in +templates. This allows you to easily reuse code that is common +amongst multiple templates and add convenience methods. + +Using TemplateHelper +==================== + +First, ``mkdir /var/lib/bcfg2/TemplateHelper`` and add +**TemplateHelper** to your ``plugins`` line in ``/etc/bcfg2.conf``. +Restart ``bcfg2-server``. + +Now, any ``.py`` file placed in ``/var/lib/bcfg2/TemplateHelper/`` +will be read and added to matching client metadata objects. See +:ref:`Writing Helpers` below for more information on how to write +TemplateHelper scripts. + +TemplateHelper supports group- and host-specific helpers, so you could +create, e.g., ``foo.py.G80_test`` to create a helper that only applied +to machines in the group ``test``. + +Writing Helpers +=============== + +A helper module is just a Python module with three special conditions: + +* The filename must end with ``.py`` (before any specificity + strings, e.g., ``.G80_foo`` or ``.H_blah.example.com`` +* The module must have an attribute, ``__export__``, that lists all of + the classes, functions, variables, or other symbols you wish to + export from the module. +* ``data``, ``handle_event``, ``name``, and ``specific`` are reserved + names. You should not include symbols with a reserved name in + ``__export__``. Additionally, including symbols that start with an + underscore or double underscore is bad form, and may also produce + errors. + +See ``examples/TemplateHelper`` for examples of helper modules. + +Usage +===== + +Specific helpers can be referred to in +templates as ``metadata.TemplateHelper[<modulename>]``. That accesses +a HelperModule object will has, as attributes, all symbols listed in +``__export__``. For example, consider this helper module:: + + __export__ = ["hello"] + + def hello(metadata): + return "Hello, %s!" % metadata.hostname + +To use this in a TGenshi template, we could do:: + + ${metadata.TemplateHelper['hello'].hello(metadata)} + +The template would produce:: + + Hello, foo.example.com! + +Note that the client metadata object is not passed to a helper module +in any magical way; if you want to access the client metadata object +in a helper function or class, you must pass the object to the +function manually. diff --git a/examples/TemplateHelper/include.py b/examples/TemplateHelper/include.py new file mode 100644 index 000000000..5fba75558 --- /dev/null +++ b/examples/TemplateHelper/include.py @@ -0,0 +1,97 @@ +"""IncludeHelper makes it easier to include group- and host-specific files in a template. + +Synopsis: + + {% python + import os + include = metadata.TemplateHelper['include'].IncludeHelper + custom = include(metadata, path).files(os.path.basename(name)) + %}\ + {% for file in custom %}\ + + ########## Start ${include.specificity(file)} ########## + {% include ${file} %} + ########## End ${include.specificity(file)} ########## + {% end %}\ + +This would let you include files with the same base name; e.g. in a +template for ''foo.conf'', the include files would be called +''foo.conf.G_<group>.genshi_include''. If a template needs to include +different files in different places, you can do that like so: + + inc = metadata.TemplateHelper['include'].IncludeHelper(metadata, path) + custom_bar = inc.files("bar") + custom_baz = inc.files("baz") + +This would result in two different sets of custom files being used, +one drawn from ''bar.conf.G_<group>.genshi_include'' and the other +from ''baz.conf.G_<group>.genshi_include''. + +==== Methods ==== + + +=== files === + +Usage: + + + +""" + +import os +import re +import Bcfg2.Options + +__export__ = ["IncludeHelper"] + +class IncludeHelper (object): + def __init__(self, metadata, path): + """ Constructor. + + The template path can be found in the ''path'' variable that is set for all Genshi templates.""" + self.metadata = metadata + self.path = path + + def _get_basedir(self): + setup = Bcfg2.Options.OptionParser({'repo': + Bcfg2.Options.SERVER_REPOSITORY}) + setup.parse('--') + return os.path.join(setup['repo'], os.path.dirname(self.path)) + + def files(self, fname): + """ Return a list of files to include for this host. Files + are found in the template directory based on the following + patterns: + + * ''<prefix>.H_<hostname>.genshi_include'': Host-specific files + * ''<prefix>.G_<group>.genshi_include'': Group-specific files + + Note that there is no numeric priority on the group-specific + files. All matching files are returned by + ''IncludeHelper.files()''. """ + files = [] + hostfile = os.path.join(self._get_basedir(), + "%s.H_%s.genshi_include" % + (fname, self.metadata.hostname)) + if os.path.isfile(hostfile): + files.append(hostfile) + + for group in self.metadata.groups: + filename = os.path.join(self._get_basedir(), + "%s.G_%s.genshi_include" % (fname, group)) + if os.path.isfile(filename): + files.append(filename) + + return sorted(files) + + @staticmethod + def specificity(fname): + """ Get a string describing the specificity of the given file """ + match = re.search(r'(G|H)_(.*)\.genshi_include', fname) + if match: + if match.group(1) == "G": + stype = "group" + else: + stype = "host" + return "%s-specific configs for %s" % (stype, match.group(2)) + return "Unknown specificity" diff --git a/src/lib/Server/Plugin.py b/src/lib/Server/Plugin.py index 06de18f29..c48859a44 100644 --- a/src/lib/Server/Plugin.py +++ b/src/lib/Server/Plugin.py @@ -935,7 +935,7 @@ class SpecificData(object): logger.error("Failed to read file %s" % self.name) -class EntrySet: +class EntrySet(object): """Entry sets deal with the host- and group-specific entries.""" ignore = re.compile("^(\.#.*|.*~|\\..*\\.(sw[px])|.*\\.genshi_include)$") @@ -1011,7 +1011,8 @@ class EntrySet: spec = self.specificity_from_filename(event.filename) except SpecificityError: if not self.ignore.match(event.filename): - logger.error("Could not process filename %s; ignoring" % fpath) + logger.error("Could not process filename %s; ignoring" % + fpath) return self.entries[event.filename] = self.entry_type(fpath, spec, self.encoding) diff --git a/src/lib/Server/Plugins/TemplateHelper.py b/src/lib/Server/Plugins/TemplateHelper.py new file mode 100644 index 000000000..42eafed56 --- /dev/null +++ b/src/lib/Server/Plugins/TemplateHelper.py @@ -0,0 +1,83 @@ +import re +import imp +import sys +import logging +import Bcfg2.Server.Plugin + +logger = logging.getLogger(__name__) + +class HelperModule(Bcfg2.Server.Plugin.SpecificData): + _module_name_re = re.compile(r'([^/]+?)\.py') + + def __init__(self, name, specific, encoding): + Bcfg2.Server.Plugin.SpecificData.__init__(self, name, specific, + encoding) + match = self._module_name_re.search(self.name) + if match: + self._module_name = match.group(1) + else: + self._module_name = name + self._attrs = [] + + def handle_event(self, event): + Bcfg2.Server.Plugin.SpecificData.handle_event(self, event) + try: + module = imp.load_source(self._module_name, self.name) + except: + err = sys.exc_info()[1] + logger.error("TemplateHelper: Failed to import %s: %s" % + (self.name, err)) + return + + if not hasattr(module, "__export__"): + logger.error("TemplateHelper: %s has no __export__ list" % + self.name) + return + + for sym in module.__export__: + if sym not in self._attrs and hasattr(self, sym): + logger.warning("TemplateHelper: %s: %s is a reserved keyword, " + "skipping export" % (self.name, sym)) + setattr(self, sym, getattr(module, sym)) + # remove old exports + for sym in set(self._attrs) - set(module.__export__): + delattr(self, sym) + + self._attrs = module.__export__ + + +class HelperSet(Bcfg2.Server.Plugin.EntrySet): + ignore = re.compile("^(\.#.*|.*~|\\..*\\.(sw[px])|.*\.py[co])$") + + def __init__(self, path, fam, encoding, plugin_name): + fpattern = '[0-9A-Za-z_\-]+\.py' + self.plugin_name = plugin_name + Bcfg2.Server.Plugin.EntrySet.__init__(self, fpattern, path, + HelperModule, encoding) + fam.AddMonitor(path, self) + + def HandleEvent(self, event): + if (event.filename != self.path and + not self.ignore.match(event.filename)): + return self.handle_event(event) + + +class TemplateHelper(Bcfg2.Server.Plugin.Plugin, + Bcfg2.Server.Plugin.Connector): + """ A plugin to provide helper classes and functions to templates """ + name = 'TemplateHelper' + __author__ = 'chris.a.st.pierre@gmail.com' + + def __init__(self, core, datastore): + Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) + Bcfg2.Server.Plugin.Connector.__init__(self) + + try: + self.helpers = HelperSet(self.data, core.fam, core.encoding, + self.name) + except: + raise Bcfg2.Server.Plugin.PluginInitError + + def get_additional_data(self, metadata): + return dict([(h._module_name, h) + for h in list(self.helpers.entries.values())]) |