summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/server/plugins/connectors/templatehelper.txt72
-rw-r--r--examples/TemplateHelper/include.py97
-rw-r--r--src/lib/Server/Plugin.py5
-rw-r--r--src/lib/Server/Plugins/TemplateHelper.py83
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())])