From c4e1b49ff7d5b8f5860f5cc208476ff42159724e Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 29 Oct 2013 16:35:27 -0400 Subject: Plugins: Added TemplateDataProvider plugin interface This lets you provide variables to the top-level namespace of templates in a more seamless way than through a Connector plugin. It's mostly useful for TemplateHelper for now, but may find other uses in the future. --- src/lib/Bcfg2/Server/Plugin/helpers.py | 84 ++++++++++++++++++++-- src/lib/Bcfg2/Server/Plugin/interfaces.py | 37 +++++++--- .../Server/Plugins/Cfg/CfgCheetahGenerator.py | 24 +++++-- .../Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py | 24 +++++-- src/lib/Bcfg2/Server/Plugins/TemplateHelper.py | 57 +++++++++++---- src/lib/Bcfg2/Server/__init__.py | 1 - 6 files changed, 185 insertions(+), 42 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py index 4cd87bd70..1cb5a7b3e 100644 --- a/src/lib/Bcfg2/Server/Plugin/helpers.py +++ b/src/lib/Bcfg2/Server/Plugin/helpers.py @@ -16,7 +16,7 @@ import Bcfg2.Server.FileMonitor from Bcfg2.Logger import Debuggable from Bcfg2.Compat import CmpMixin, wraps from Bcfg2.Server.Plugin.base import Plugin -from Bcfg2.Server.Plugin.interfaces import Generator +from Bcfg2.Server.Plugin.interfaces import Generator, TemplateDataProvider from Bcfg2.Server.Plugin.exceptions import SpecificityError, \ PluginExecutionError @@ -127,6 +127,82 @@ def default_path_metadata(): 'paranoid', 'sensitive']]) +class DefaultTemplateDataProvider(TemplateDataProvider): + """ A base + :class:`Bcfg2.Server.Plugin.interfaces.TemplateDataProvider` that + provides default data for text and XML templates. + + Note that, since Cheetah and Genshi text templates treat the + ``path`` variable differently, this is overridden, by + :class:`Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator.DefaultCheetahDataProvider` + and + :class:`Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator.DefaultGenshiDataProvider`, + respectively. """ + + def get_template_data(self, entry, metadata, template): + return dict(name=entry.get('realname', entry.get('name')), + metadata=metadata, + source_path=template, + repo=Bcfg2.Options.setup.repository) + + def get_xml_template_data(self, _, metadata): + return dict(metadata=metadata, + repo=Bcfg2.Options.setup.repository) + +_sentinel = object() # pylint: disable=C0103 + + +def _get_template_data(func_name, args, default=_sentinel): + """ Generic template data getter for both text and XML templates. + + :param func_name: The name of the function to call on + :class:`Bcfg2.Server.Plugin.interfaces.TemplateDataProvider` + objects to get data for this template type. + Should be one of either ``get_template_data`` + for text templates, or ``get_xml_template_data`` + for XML templates. + :type func_name: string + :param args: The arguments to pass to the data retrieval function + :type args: list + :param default: An object that provides a set of base values. If + this is not provided, an instance of + :class:`Bcfg2.Server.Plugin.helpers.DefaultTemplateDataProvider` + is used. This can be set to None to avoid setting + any base values at all. + :type default: Bcfg2.Server.Plugin.interfaces.TemplateDataProvider + """ + if default is _sentinel: + default = DefaultTemplateDataProvider() + providers = Bcfg2.Server.core.plugins_by_type(TemplateDataProvider) + if default is not None: + providers.insert(0, default) + + rv = dict() + source = dict() + for prov in providers: + pdata = getattr(prov, func_name)(*args) + for key, val in pdata.items(): + if key not in rv: + rv[key] = val + source[key] = prov + else: + LOGGER.warning("Duplicate template variable %s provided by " + "both %s and %s" % (key, prov, source[key])) + return rv + + +def get_template_data(entry, metadata, template, default=_sentinel): + """ Get all template variables for a text (i.e., Cfg) template """ + return _get_template_data("get_template_data", [entry, metadata, template], + default=default) + + +def get_xml_template_data(structfile, metadata, default=_sentinel): + """ Get all template variables for an XML template """ + return _get_template_data("get_xml_template_data", [structfile, metadata], + default=default) + + class DatabaseBacked(Plugin): """ Provides capabilities for a plugin to read and write to a database. The plugin must add an option to flag database use with @@ -741,10 +817,8 @@ class StructFile(XMLFileBacked): XML data """ stream = self.template.generate( - metadata=metadata, - repo=Bcfg2.Options.setup.repository).filter(removecomment) - return lxml.etree.XML(stream.render('xml', - strip_whitespace=False), + **get_xml_template_data(self, metadata)).filter(removecomment) + return lxml.etree.XML(stream.render('xml', strip_whitespace=False), parser=Bcfg2.Server.XMLParser) def _match(self, item, metadata, *args): diff --git a/src/lib/Bcfg2/Server/Plugin/interfaces.py b/src/lib/Bcfg2/Server/Plugin/interfaces.py index d51c199cd..622b69c79 100644 --- a/src/lib/Bcfg2/Server/Plugin/interfaces.py +++ b/src/lib/Bcfg2/Server/Plugin/interfaces.py @@ -12,6 +12,11 @@ from Bcfg2.Server.Plugin.base import Plugin from Bcfg2.Server.Plugin.exceptions import PluginInitError, \ MetadataRuntimeError, MetadataConsistencyError +# Since this file basically just contains abstract interface +# descriptions, just about every function declaration has unused +# arguments. Disable this pylint warning for the whole file. +# pylint: disable=W0613 + class Generator(object): """ Generator plugins contribute to literal client configurations. @@ -27,13 +32,12 @@ class Generator(object): generate the content. The callable will receive two arguments: the abstract entry (as an lxml.etree._Element object), and the client metadata object the entry is being generated for. - #. If the entry is not listed in ``Entries``, the Bcfg2 core calls :func:`HandlesEntry`; if that returns True, then it calls :func:`HandleEntry`. """ - def HandlesEntry(self, entry, metadata): # pylint: disable=W0613 + def HandlesEntry(self, entry, metadata): """ HandlesEntry is the slow path method for routing configuration binding requests. It is called if the ``Entries`` dict does not contain a method for binding the @@ -48,7 +52,7 @@ class Generator(object): """ return False - def HandleEntry(self, entry, metadata): # pylint: disable=W0613 + def HandleEntry(self, entry, metadata): """ HandleEntry is the slow path method for binding configuration binding requests. It is called if the ``Entries`` dict does not contain a method for binding the @@ -138,7 +142,6 @@ class Metadata(object): """ pass - # pylint: disable=W0613 def resolve_client(self, address, cleanup_cache=False): """ Resolve the canonical name of this client. If this method is not implemented, the hostname claimed by the client is @@ -156,7 +159,6 @@ class Metadata(object): :class:`Bcfg2.Server.Plugin.exceptions.MetadataConsistencyError` """ return address[1] - # pylint: enable=W0613 def AuthenticateConnection(self, cert, user, password, address): """ Authenticate the given client. @@ -219,7 +221,7 @@ class Connector(object): """ Connector plugins augment client metadata instances with additional data, additional groups, or both. """ - def get_additional_groups(self, metadata): # pylint: disable=W0613 + def get_additional_groups(self, metadata): """ Return a list of additional groups for the given client. Each group can be either the name of a group (a string), or a :class:`Bcfg2.Server.Plugins.Metadata.MetadataGroup` object @@ -250,7 +252,7 @@ class Connector(object): """ return list() - def get_additional_data(self, metadata): # pylint: disable=W0613 + def get_additional_data(self, metadata): """ Return arbitrary additional data for the given ClientMetadata object. By convention this is usually a dict object, but doesn't need to be. @@ -473,7 +475,7 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread): # Someone who understands these interfaces better needs to write docs # for PullSource and PullTarget class PullSource(object): - def GetExtra(self, client): # pylint: disable=W0613 + def GetExtra(self, client): return [] def GetCurrentEntry(self, client, e_type, e_name): @@ -630,7 +632,7 @@ class ClientACLs(object): """ ClientACLs are used to grant or deny access to different XML-RPC calls based on client IP or metadata. """ - def check_acl_ip(self, address, rmi): # pylint: disable=W0613 + def check_acl_ip(self, address, rmi): """ Check if the given IP address is authorized to make the named XML-RPC call. @@ -643,7 +645,7 @@ class ClientACLs(object): """ return True - def check_acl_metadata(self, metadata, rmi): # pylint: disable=W0613 + def check_acl_metadata(self, metadata, rmi): """ Check if the given client is authorized to make the named XML-RPC call. @@ -654,3 +656,18 @@ class ClientACLs(object): :returns: bool """ return True + + +class TemplateDataProvider(object): + """ TemplateDataProvider plugins provide variables to templates + for use in rendering. """ + + def get_template_data(self, entry, metadata, template): + """ Get a dict of variables that will be supplied to a Cfg + template for rendering """ + return dict() + + def get_xml_template_data(self, structfile, metadata): + """ Get a dict of variables that will be supplied to an XML + template (e.g., a bundle) for rendering """ + return dict() diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py index 476dc1fc6..84309b5dd 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py @@ -3,7 +3,8 @@ :ref:`server-plugins-generators-cfg` files. """ import Bcfg2.Options -from Bcfg2.Server.Plugin import PluginExecutionError +from Bcfg2.Server.Plugin import PluginExecutionError, \ + DefaultTemplateDataProvider, get_template_data from Bcfg2.Server.Plugins.Cfg import CfgGenerator try: @@ -13,6 +14,18 @@ except ImportError: HAS_CHEETAH = False +class DefaultCheetahDataProvider(DefaultTemplateDataProvider): + """ Template data provider for Cheetah templates. Cheetah and + Genshi currently differ over the value of the ``path`` variable, + which is why this is necessary. """ + + def get_template_data(self, entry, metadata, template): + rv = DefaultTemplateDataProvider.get_template_data(self, entry, + metadata, template) + rv['path'] = rv['name'] + return rv + + class CfgCheetahGenerator(CfgGenerator): """ The CfgCheetahGenerator allows you to use the `Cheetah `_ templating system to generate @@ -37,10 +50,9 @@ class CfgCheetahGenerator(CfgGenerator): def get_data(self, entry, metadata): template = Template(self.data.decode(Bcfg2.Options.setup.encoding), compilerSettings=self.settings) - template.metadata = metadata - template.name = entry.get('realname', entry.get('name')) - template.path = entry.get('realname', entry.get('name')) - template.source_path = self.name - template.repo = Bcfg2.Options.setup.repository + for key, val in get_template_data( + entry, metadata, self.name, + default=DefaultCheetahDataProvider()).items(): + setattr(template, key, val) return template.respond() get_data.__doc__ = CfgGenerator.get_data.__doc__ diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py index 7ba8c4491..ef4e6a656 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py @@ -6,7 +6,8 @@ import re import sys import traceback import Bcfg2.Options -from Bcfg2.Server.Plugin import PluginExecutionError, removecomment +from Bcfg2.Server.Plugin import PluginExecutionError, removecomment, \ + DefaultTemplateDataProvider, get_template_data from Bcfg2.Server.Plugins.Cfg import CfgGenerator from genshi.template import TemplateLoader, NewTextTemplate from genshi.template.eval import UndefinedError, Suite @@ -42,6 +43,18 @@ d['a']""" GENSHI_REMOVES_BLANK_LINES = _genshi_removes_blank_lines() +class DefaultGenshiDataProvider(DefaultTemplateDataProvider): + """ Template data provider for Genshi templates. Cheetah and + Genshi currently differ over the value of the ``path`` variable, + which is why this is necessary. """ + + def get_template_data(self, entry, metadata, template): + rv = DefaultTemplateDataProvider.get_template_data(self, entry, + metadata, template) + rv['path'] = template + return rv + + class CfgGenshiGenerator(CfgGenerator): """ The CfgGenshiGenerator allows you to use the `Genshi `_ templating system to generate @@ -81,13 +94,10 @@ class CfgGenshiGenerator(CfgGenerator): raise PluginExecutionError("Failed to load template %s" % self.name) - fname = entry.get('realname', entry.get('name')) stream = self.template.generate( - name=fname, - metadata=metadata, - path=self.name, - source_path=self.name, - repo=Bcfg2.Options.setup.repository).filter(removecomment) + **get_template_data( + entry, metadata, self.name, + default=DefaultGenshiDataProvider())).filter(removecomment) try: try: return stream.render('text', diff --git a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py index 82d8bfae2..05b5086f9 100644 --- a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py +++ b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py @@ -3,11 +3,9 @@ import re import imp import sys -import logging +from Bcfg2.Logger import Debuggable import Bcfg2.Server.Plugin -LOGGER = logging.getLogger(__name__) - MODULE_RE = re.compile(r'(?P(?P[^\/]+)\.py)$') @@ -19,10 +17,11 @@ def safe_module_name(module): return '__TemplateHelper_%s' % module -class HelperModule(object): +class HelperModule(Debuggable): """ Representation of a TemplateHelper module """ def __init__(self, name): + Debuggable.__init__(self) self.name = name #: The name of the module as used by get_additional_data(). @@ -32,6 +31,9 @@ class HelperModule(object): #: The attributes exported by this module self._attrs = [] + #: The attributes added to the template namespace by this module + self.defaults = [] + def HandleEvent(self, event=None): """ HandleEvent is called whenever the FAM registers an event. @@ -47,37 +49,50 @@ class HelperModule(object): self.name) except: # pylint: disable=W0702 err = sys.exc_info()[1] - LOGGER.error("TemplateHelper: Failed to import %s: %s" % - (self.name, err)) + self.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) + self.logger.error("TemplateHelper: %s has no __export__ list" % + self.name) return newattrs = [] 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)) + self.logger.warning( + "TemplateHelper: %s: %s is a reserved keyword, " + "skipping export" % (self.name, sym)) continue try: setattr(self, sym, getattr(module, sym)) newattrs.append(sym) except AttributeError: - LOGGER.warning("TemplateHelper: %s exports %s, but has no " - "such attribute" % (self.name, sym)) + self.logger.warning( + "TemplateHelper: %s exports %s, but has no such attribute" + % (self.name, sym)) + # remove old exports for sym in set(self._attrs) - set(newattrs): delattr(self, sym) self._attrs = newattrs + self.defaults = [] + for sym in getattr(module, "__default__", []): + if sym not in self._attrs: + self.logger.warning( + "TemplateHelper: %s: %s is flagged as a default, " + "but is not exported; skipping") + continue + self.defaults.append(sym) + class TemplateHelper(Bcfg2.Server.Plugin.Plugin, Bcfg2.Server.Plugin.Connector, - Bcfg2.Server.Plugin.DirectoryBacked): + Bcfg2.Server.Plugin.DirectoryBacked, + Bcfg2.Server.Plugin.TemplateDataProvider): """ A plugin to provide helper classes and functions to templates """ __author__ = 'chris.a.st.pierre@gmail.com' ignore = re.compile(r'^(\.#.*|.*~|\..*\.(sw[px])|.*\.py[co])$') @@ -88,7 +103,23 @@ class TemplateHelper(Bcfg2.Server.Plugin.Plugin, Bcfg2.Server.Plugin.Plugin.__init__(self, core) Bcfg2.Server.Plugin.Connector.__init__(self) Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data) + Bcfg2.Server.Plugin.TemplateDataProvider.__init__(self) def get_additional_data(self, _): return dict([(h._module_name, h) # pylint: disable=W0212 for h in self.entries.values()]) + + def get_template_data(self, *_): + rv = dict() + source = dict() + for helper in self.entries.values(): + for key in helper.defaults: + if key not in rv: + rv[key] = getattr(helper, key) + source[key] = helper + else: + self.logger.warning( + "TemplateHelper: Duplicate default variable %s " + "provided by both %s and %s" % + (key, helper.name, source[key].name)) + return rv diff --git a/src/lib/Bcfg2/Server/__init__.py b/src/lib/Bcfg2/Server/__init__.py index 4e46ada09..39ed2ec91 100644 --- a/src/lib/Bcfg2/Server/__init__.py +++ b/src/lib/Bcfg2/Server/__init__.py @@ -1,7 +1,6 @@ """This is the set of modules for Bcfg2.Server.""" import lxml.etree -import Bcfg2.Options XI = 'http://www.w3.org/2001/XInclude' XI_NAMESPACE = '{%s}' % XI -- cgit v1.2.3-1-g7c22