summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugin/helpers.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server/Plugin/helpers.py')
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py866
1 files changed, 447 insertions, 419 deletions
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 55dd255cd..5cfc8998c 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -4,55 +4,52 @@ import os
import re
import sys
import copy
-import time
import glob
import logging
+import genshi
import operator
import lxml.etree
import Bcfg2.Server
import Bcfg2.Options
-import Bcfg2.Statistics
+import Bcfg2.Server.FileMonitor
+from Bcfg2.Logger import Debuggable
from Bcfg2.Compat import CmpMixin, wraps
-from Bcfg2.Server.Plugin.base import Debuggable, Plugin
-from Bcfg2.Server.Plugin.interfaces import Generator
+from Bcfg2.Server.Plugin.base import Plugin
+from Bcfg2.Server.Plugin.interfaces import Generator, TemplateDataProvider
from Bcfg2.Server.Plugin.exceptions import SpecificityError, \
PluginExecutionError, PluginInitError
try:
+ import Bcfg2.Server.Encryption
+ HAS_CRYPTO = True
+except ImportError:
+ HAS_CRYPTO = False
+
+try:
import django # pylint: disable=W0611
HAS_DJANGO = True
except ImportError:
HAS_DJANGO = False
-#: A dict containing default metadata for Path entries from bcfg2.conf
-DEFAULT_FILE_METADATA = Bcfg2.Options.OptionParser(
- dict(configfile=Bcfg2.Options.CFILE,
- owner=Bcfg2.Options.MDATA_OWNER,
- group=Bcfg2.Options.MDATA_GROUP,
- mode=Bcfg2.Options.MDATA_MODE,
- secontext=Bcfg2.Options.MDATA_SECONTEXT,
- important=Bcfg2.Options.MDATA_IMPORTANT,
- paranoid=Bcfg2.Options.MDATA_PARANOID,
- sensitive=Bcfg2.Options.MDATA_SENSITIVE))
-DEFAULT_FILE_METADATA.parse([Bcfg2.Options.CFILE.cmd, Bcfg2.Options.CFILE])
-del DEFAULT_FILE_METADATA['args']
-del DEFAULT_FILE_METADATA['configfile']
-
LOGGER = logging.getLogger(__name__)
-#: a compiled regular expression for parsing info and :info files
-INFO_REGEX = re.compile(r'owner:\s*(?P<owner>\S+)|' +
- r'group:\s*(?P<group>\S+)|' +
- r'mode:\s*(?P<mode>\w+)|' +
- r'secontext:\s*(?P<secontext>\S+)|' +
- r'paranoid:\s*(?P<paranoid>\S+)|' +
- r'sensitive:\s*(?P<sensitive>\S+)|' +
- r'encoding:\s*(?P<encoding>\S+)|' +
- r'important:\s*(?P<important>\S+)|' +
- r'mtime:\s*(?P<mtime>\w+)')
+def removecomment(stream):
+ """ A Genshi filter that removes comments from the stream. This
+ function is a generator.
-def bind_info(entry, metadata, infoxml=None, default=DEFAULT_FILE_METADATA):
+ :param stream: The Genshi stream to remove comments from
+ :type stream: genshi.core.Stream
+ :returns: tuple of ``(kind, data, pos)``, as when iterating
+ through a Genshi stream
+ """
+ for kind, data, pos in stream:
+ if kind is genshi.core.COMMENT:
+ continue
+ yield kind, data, pos
+
+
+def bind_info(entry, metadata, infoxml=None, default=None):
""" Bind the file metadata in the given
:class:`Bcfg2.Server.Plugin.helpers.InfoXML` object to the given
entry.
@@ -69,6 +66,8 @@ def bind_info(entry, metadata, infoxml=None, default=DEFAULT_FILE_METADATA):
:returns: None
:raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginExecutionError`
"""
+ if default is None:
+ default = default_path_metadata()
for attr, val in list(default.items()):
entry.set(attr, val)
if infoxml:
@@ -82,81 +81,126 @@ def bind_info(entry, metadata, infoxml=None, default=DEFAULT_FILE_METADATA):
entry.set(attr, val)
-class track_statistics(object): # pylint: disable=C0103
- """ Decorator that tracks execution time for the given
- :class:`Plugin` method with :mod:`Bcfg2.Statistics` for reporting
- via ``bcfg2-admin perf`` """
+def default_path_metadata():
+ """ Get the default Path entry metadata from the config.
- def __init__(self, name=None):
- """
- :param name: The name under which statistics for this function
- will be tracked. By default, the name will be
- the name of the function concatenated with the
- name of the class the function is a member of.
- :type name: string
- """
- # if this is None, it will be set later during __call_
- self.name = name
+ :returns: dict of metadata attributes and their default values
+ """
+ return dict([(k, getattr(Bcfg2.Options.setup, "default_%s" % k))
+ for k in ['owner', 'group', 'mode', 'secontext', 'important',
+ '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 __call__(self, func):
- if self.name is None:
- self.name = func.__name__
- @wraps(func)
- def inner(obj, *args, **kwargs):
- """ The decorated function """
- name = "%s:%s" % (obj.__class__.__name__, self.name)
+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)
- start = time.time()
- try:
- return func(obj, *args, **kwargs)
- finally:
- Bcfg2.Statistics.stats.add_value(name, time.time() - start)
- return inner
+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.
+ database. The plugin must add an option to flag database use with
+ something like:
+
+ options = Bcfg2.Server.Plugin.Plugins.options + [
+ Bcfg2.Options.BooleanOption(
+ cf=('metadata', 'use_database'), dest="metadata_db",
+ help="Use database capabilities of the Metadata plugin")
+
+ This must be done manually due to various limitations in Python.
.. private-include: _use_db
.. private-include: _must_lock
"""
- #: The option to look up in :attr:`section` to determine whether or
- #: not to use the database capabilities of this plugin. The option
- #: is retrieved with
- #: :py:func:`ConfigParser.SafeConfigParser.getboolean`, and so must
- #: conform to the possible values that function can handle.
- option = "use_database"
-
- def __init__(self, core, datastore):
- Plugin.__init__(self, core, datastore)
- use_db = self.core.setup.cfp.getboolean(self.section,
- self.option,
- default=False)
+ def __init__(self, core):
+ Plugin.__init__(self, core)
+ use_db = getattr(Bcfg2.Options.setup, "%s_db" % self.name.lower(),
+ False)
if use_db and not HAS_DJANGO:
- raise PluginInitError("%s.%s is True but Django not found" %
- (self.section, self.option))
+ raise PluginInitError("%s is configured to use the database but "
+ "Django libraries are not found" % self.name)
elif use_db and not self.core.database_available:
- raise PluginInitError("%s.%s is True but the database is "
- "unavailable due to prior errors" %
- (self.section, self.option))
-
- def _section(self):
- """ The section to look in for :attr:`DatabaseBacked.option`
- """
- return self.name.lower()
- section = property(_section)
+ raise PluginInitError("%s is configured to use the database but "
+ "the database is unavailable due to prior "
+ "errors" % self.name)
@property
def _use_db(self):
""" Whether or not this plugin is configured to use the
database. """
- use_db = self.core.setup.cfp.getboolean(self.section,
- self.option,
- default=False)
+ use_db = getattr(Bcfg2.Options.setup, "%s_db" % self.name.lower(),
+ False)
if use_db and HAS_DJANGO and self.core.database_available:
return True
else:
@@ -167,11 +211,7 @@ class DatabaseBacked(Plugin):
""" Whether or not the backend database must acquire a thread
lock before writing, because it does not allow multiple
threads to write."""
- engine = \
- self.core.setup.cfp.get(Bcfg2.Options.DB_ENGINE.cf[0],
- Bcfg2.Options.DB_ENGINE.cf[1],
- default=Bcfg2.Options.DB_ENGINE.default)
- return engine == 'sqlite3'
+ return self._use_db and Bcfg2.Options.setup.db_engine == 'sqlite3'
@staticmethod
def get_db_lock(func):
@@ -209,13 +249,10 @@ class FileBacked(Debuggable):
principally meant to be used as a part of
:class:`Bcfg2.Server.Plugin.helpers.DirectoryBacked`. """
- def __init__(self, name, fam=None):
+ def __init__(self, name):
"""
:param name: The full path to the file to cache and monitor
:type name: string
- :param fam: The FAM object used to receive notifications of
- changes
- :type fam: Bcfg2.Server.FileMonitor.FileMonitor
"""
Debuggable.__init__(self)
@@ -226,10 +263,10 @@ class FileBacked(Debuggable):
self.name = name
#: The FAM object used to receive notifications of changes
- self.fam = fam
+ self.fam = Bcfg2.Server.FileMonitor.get_fam()
def HandleEvent(self, event=None):
- """ HandleEvent is called whenever the FAM registers an event.
+ """HandleEvent is called whenever the FAM registers an event.
:param event: The event object
:type event: Bcfg2.Server.FileMonitor.Event
@@ -239,13 +276,11 @@ class FileBacked(Debuggable):
return
try:
self.data = open(self.name).read()
- self.Index()
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to read file %s: %s" % (self.name, err))
- except:
- err = sys.exc_info()[1]
- self.logger.error("Failed to parse file %s: %s" % (self.name, err))
+
+ self.Index()
def Index(self):
""" Index() is called by :func:`HandleEvent` every time the
@@ -279,14 +314,11 @@ class DirectoryBacked(Debuggable):
#: :attr:`patterns` or ``ignore``, then a warning will be produced.
ignore = None
- def __init__(self, data, fam):
+ def __init__(self, data):
"""
:param data: The path to the data directory that will be
monitored
:type data: string
- :param fam: The FAM object used to receive notifications of
- changes
- :type fam: Bcfg2.Server.FileMonitor.FileMonitor
.. -----
.. autoattribute:: __child__
@@ -294,7 +326,7 @@ class DirectoryBacked(Debuggable):
Debuggable.__init__(self)
self.data = os.path.normpath(data)
- self.fam = fam
+ self.fam = Bcfg2.Server.FileMonitor.get_fam()
#: self.entries contains information about the files monitored
#: by this object. The keys of the dict are the relative
@@ -366,8 +398,7 @@ class DirectoryBacked(Debuggable):
:returns: None
"""
self.entries[relative] = self.__child__(os.path.join(self.data,
- relative),
- self.fam)
+ relative))
self.entries[relative].HandleEvent(event)
def HandleEvent(self, event): # pylint: disable=R0912
@@ -492,13 +523,10 @@ class XMLFileBacked(FileBacked):
#: to the constructor.
create = None
- def __init__(self, filename, fam=None, should_monitor=False, create=None):
+ def __init__(self, filename, should_monitor=False, create=None):
"""
:param filename: The full path to the file to cache and monitor
:type filename: string
- :param fam: The FAM object used to receive notifications of
- changes
- :type fam: Bcfg2.Server.FileMonitor.FileMonitor
:param should_monitor: Whether or not to monitor this file for
changes. It may be useful to disable
monitoring when, for instance, the file
@@ -518,7 +546,7 @@ class XMLFileBacked(FileBacked):
.. -----
.. autoattribute:: __identifier__
"""
- FileBacked.__init__(self, filename, fam=fam)
+ FileBacked.__init__(self, filename)
#: The raw XML data contained in the file as an
#: :class:`lxml.etree.ElementTree` object, with XIncludes
@@ -542,7 +570,7 @@ class XMLFileBacked(FileBacked):
self.extra_monitors = []
if ((create is not None or self.create not in [None, False]) and
- not os.path.exists(self.name)):
+ not os.path.exists(self.name)):
toptag = create or self.create
self.logger.warning("%s does not exist, creating" % self.name)
if hasattr(toptag, "getroottree"):
@@ -554,7 +582,7 @@ class XMLFileBacked(FileBacked):
#: Whether or not to monitor this file for changes.
self.should_monitor = should_monitor
- if fam and should_monitor:
+ if should_monitor:
self.fam.AddMonitor(filename, self)
def _follow_xincludes(self, fname=None, xdata=None):
@@ -619,18 +647,14 @@ class XMLFileBacked(FileBacked):
Index.__doc__ = FileBacked.Index.__doc__
def add_monitor(self, fpath):
- """ Add a FAM monitor to a file that has been XIncluded. This
- is only done if the constructor got a ``fam`` object,
- regardless of whether ``should_monitor`` is set to True (i.e.,
- whether or not the base file is monitored).
+ """ Add a FAM monitor to a file that has been XIncluded.
:param fpath: The full path to the file to monitor
:type fpath: string
:returns: None
"""
self.extra_monitors.append(fpath)
- if self.fam:
- self.fam.AddMonitor(fpath, self)
+ self.fam.AddMonitor(fpath, self)
def __iter__(self):
return iter(self.entries)
@@ -642,44 +666,171 @@ class XMLFileBacked(FileBacked):
class StructFile(XMLFileBacked):
""" StructFiles are XML files that contain a set of structure file
formatting logic for handling ``<Group>`` and ``<Client>``
- tags. """
+ tags.
+
+ .. -----
+ .. autoattribute:: __identifier__
+ .. automethod:: _include_element
+ """
#: If ``__identifier__`` is not None, then it must be the name of
#: an XML attribute that will be required on the top-level tag of
#: the file being cached
__identifier__ = None
- def _include_element(self, item, metadata):
- """ determine if an XML element matches the metadata """
+ #: Whether or not to enable encryption
+ encryption = True
+
+ #: Callbacks used to determine if children of items with the given
+ #: tags should be included in the return value of
+ #: :func:`Bcfg2.Server.Plugin.helpers.StructFile.Match` and
+ #: :func:`Bcfg2.Server.Plugin.helpers.StructFile.XMLMatch`. Each
+ #: callback is passed the same arguments as
+ #: :func:`Bcfg2.Server.Plugin.helpers.StructFile._include_element`.
+ #: It should return True if children of the element should be
+ #: included in the match, False otherwise. The callback does
+ #: *not* need to consider negation; that will be handled in
+ #: :func:`Bcfg2.Server.Plugin.helpers.StructFile._include_element`
+ _include_tests = \
+ dict(Group=lambda el, md, *args: el.get('name') in md.groups,
+ Client=lambda el, md, *args: el.get('name') == md.hostname)
+
+ def __init__(self, filename, should_monitor=False, create=None):
+ XMLFileBacked.__init__(self, filename, should_monitor=should_monitor,
+ create=create)
+ self.template = None
+
+ def Index(self):
+ XMLFileBacked.Index(self)
+ if (self.name.endswith('.genshi') or
+ ('py' in self.xdata.nsmap and
+ self.xdata.nsmap['py'] == 'http://genshi.edgewall.org/')):
+ try:
+ loader = genshi.template.TemplateLoader()
+ self.template = \
+ loader.load(self.name,
+ cls=genshi.template.MarkupTemplate,
+ encoding=Bcfg2.Options.setup.encoding)
+ except LookupError:
+ err = sys.exc_info()[1]
+ self.logger.error('Genshi lookup error in %s: %s' % (self.name,
+ err))
+ except genshi.template.TemplateError:
+ err = sys.exc_info()[1]
+ self.logger.error('Genshi template error in %s: %s' %
+ (self.name, err))
+ except genshi.input.ParseError:
+ err = sys.exc_info()[1]
+ self.logger.error('Genshi parse error in %s: %s' % (self.name,
+ err))
+
+ if HAS_CRYPTO and self.encryption:
+ for el in self.xdata.xpath("//*[@encrypted]"):
+ try:
+ el.text = self._decrypt(el).encode('ascii',
+ 'xmlcharrefreplace')
+ except UnicodeDecodeError:
+ self.logger.info("%s: Decrypted %s to gibberish, skipping"
+ % (self.name, el.tag))
+ except Bcfg2.Server.Encryption.EVPError:
+ lax_decrypt = self.xdata.get(
+ "lax_decryption",
+ str(Bcfg2.Options.setup.lax_decryption)).lower() == \
+ "true"
+ msg = "Failed to decrypt %s element in %s" % (el.tag,
+ self.name)
+ if lax_decrypt:
+ self.logger.debug(msg)
+ else:
+ raise PluginExecutionError(msg)
+ Index.__doc__ = XMLFileBacked.Index.__doc__
+
+ def _decrypt(self, element):
+ """ Decrypt a single encrypted properties file element """
+ if not element.text or not element.text.strip():
+ return
+ passes = Bcfg2.Options.setup.passphrases
+ try:
+ passphrase = passes[element.get("encrypted")]
+ return Bcfg2.Server.Encryption.ssl_decrypt(element.text,
+ passphrase)
+ except KeyError:
+ raise Bcfg2.Server.Encryption.EVPError("No passphrase named '%s'" %
+ element.get("encrypted"))
+ raise Bcfg2.Server.Encryption.EVPError("Failed to decrypt")
+
+ def _include_element(self, item, metadata, *args):
+ """ Determine if an XML element matches the other arguments.
+
+ The first argument is always the XML element to match, and the
+ second will always be a single
+ :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` object
+ representing the metadata to match against. Subsequent
+ arguments are as given to
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.Match` or
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.XMLMatch`. In
+ the base StructFile implementation, there are no additional
+ arguments; in classes that inherit from StructFile, see the
+ :func:`Match` and :func:`XMLMatch` method signatures."""
if isinstance(item, lxml.etree._Comment): # pylint: disable=W0212
return False
- negate = item.get('negate', 'false').lower() == 'true'
- if item.tag == 'Group':
- return negate == (item.get('name') not in metadata.groups)
- elif item.tag == 'Client':
- return negate == (item.get('name') != metadata.hostname)
+ if item.tag in self._include_tests:
+ negate = item.get('negate', 'false').lower() == 'true'
+ return negate != self._include_tests[item.tag](item, metadata,
+ *args)
else:
return True
- def _match(self, item, metadata):
- """ recursive helper for Match() """
- if self._include_element(item, metadata):
- if item.tag == 'Group' or item.tag == 'Client':
+ def _render(self, metadata):
+ """ Render the template for the given client metadata
+
+ :param metadata: Client metadata to match against.
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :returns: lxml.etree._Element object representing the rendered
+ XML data
+ """
+ stream = self.template.generate(
+ **get_xml_template_data(self, metadata)).filter(removecomment)
+ return lxml.etree.XML(stream.render('xml',
+ strip_whitespace=False).encode(),
+ parser=Bcfg2.Server.XMLParser)
+
+ def _match(self, item, metadata, *args):
+ """ recursive helper for
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.Match` """
+ if self._include_element(item, metadata, *args):
+ if item.tag in self._include_tests.keys():
rv = []
- if self._include_element(item, metadata):
+ if self._include_element(item, metadata, *args):
for child in item.iterchildren():
- rv.extend(self._match(child, metadata))
+ rv.extend(self._match(child, metadata, *args))
return rv
else:
rv = copy.deepcopy(item)
for child in rv.iterchildren():
rv.remove(child)
for child in item.iterchildren():
- rv.extend(self._match(child, metadata))
+ rv.extend(self._match(child, metadata, *args))
return [rv]
else:
return []
+ def _do_match(self, metadata, *args):
+ """ Helper for
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.Match` that lets
+ a subclass of StructFile easily redefine the public Match()
+ interface to accept a different number of arguments. This
+ provides a sane prototype for the Match() function while
+ keeping the internals consistent. """
+ rv = []
+ if self.template is None:
+ entries = self.entries
+ else:
+ entries = self._render(metadata).getchildren()
+ for child in entries:
+ rv.extend(self._match(child, metadata, *args))
+ return rv
+
def Match(self, metadata):
""" Return matching fragments of the data in this file. A tag
is considered to match if all ``<Group>`` and ``<Client>``
@@ -690,22 +841,22 @@ class StructFile(XMLFileBacked):
Match() (and *not* their descendents) should be considered to
match the metadata.
+ Match() returns matching fragments in document order.
+
:param metadata: Client metadata to match against.
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
:returns: list of lxml.etree._Element objects """
- rv = []
- for child in self.entries:
- rv.extend(self._match(child, metadata))
- return rv
+ return self._do_match(metadata)
- def _xml_match(self, item, metadata):
- """ recursive helper for XMLMatch """
- if self._include_element(item, metadata):
- if item.tag == 'Group' or item.tag == 'Client':
+ def _xml_match(self, item, metadata, *args):
+ """ recursive helper for
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.XMLMatch` """
+ if self._include_element(item, metadata, *args):
+ if item.tag in self._include_tests.keys():
for child in item.iterchildren():
item.remove(child)
item.getparent().append(child)
- self._xml_match(child, metadata)
+ self._xml_match(child, metadata, *args)
if item.text:
if item.getparent().text is None:
item.getparent().text = item.text
@@ -714,10 +865,25 @@ class StructFile(XMLFileBacked):
item.getparent().remove(item)
else:
for child in item.iterchildren():
- self._xml_match(child, metadata)
+ self._xml_match(child, metadata, *args)
else:
item.getparent().remove(item)
+ def _do_xmlmatch(self, metadata, *args):
+ """ Helper for
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.XMLMatch` that lets
+ a subclass of StructFile easily redefine the public Match()
+ interface to accept a different number of arguments. This
+ provides a sane prototype for the Match() function while
+ keeping the internals consistent. """
+ if self.template is None:
+ rv = copy.deepcopy(self.xdata)
+ else:
+ rv = self._render(metadata)
+ for child in rv.iterchildren():
+ self._xml_match(child, metadata, *args)
+ return rv
+
def XMLMatch(self, metadata):
""" Return a rebuilt XML document that only contains the
matching portions of the original file. A tag is considered
@@ -727,169 +893,58 @@ class StructFile(XMLFileBacked):
All ``<Group>`` and ``<Client>`` tags will have been stripped
out.
+ The new document produced by XMLMatch() is not necessarily in
+ the same order as the original document.
+
:param metadata: Client metadata to match against.
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
:returns: lxml.etree._Element """
- rv = copy.deepcopy(self.xdata)
- for child in rv.iterchildren():
- self._xml_match(child, metadata)
- return rv
-
+ return self._do_xmlmatch(metadata)
-class INode(object):
- """ INodes provide lists of things available at a particular group
- intersection. INodes are deprecated; new plugins should use
- :class:`Bcfg2.Server.Plugin.helpers.StructFile` instead. """
-
- raw = dict(
- Client="lambda m, e:'%(name)s' == m.hostname and predicate(m, e)",
- Group="lambda m, e:'%(name)s' in m.groups and predicate(m, e)")
- nraw = dict(
- Client="lambda m, e:'%(name)s' != m.hostname and predicate(m, e)",
- Group="lambda m, e:'%(name)s' not in m.groups and predicate(m, e)")
- containers = ['Group', 'Client']
- ignore = []
-
- def __init__(self, data, idict, parent=None):
- self.data = data
- self.contents = {}
- if parent is None:
- self.predicate = lambda m, e: True
- else:
- predicate = parent.predicate
- if data.get('negate', 'false').lower() == 'true':
- psrc = self.nraw
- else:
- psrc = self.raw
- if data.tag in list(psrc.keys()):
- self.predicate = eval(psrc[data.tag] %
- {'name': data.get('name')},
- {'predicate': predicate})
- else:
- raise PluginExecutionError("Unknown tag: %s" % data.tag)
- self.children = []
- self._load_children(data, idict)
- def _load_children(self, data, idict):
- """ load children """
- for item in data.getchildren():
- if item.tag in self.ignore:
- continue
- elif item.tag in self.containers:
- self.children.append(self.__class__(item, idict, self))
- else:
- try:
- self.contents[item.tag][item.get('name')] = \
- dict(item.attrib)
- except KeyError:
- self.contents[item.tag] = \
- {item.get('name'): dict(item.attrib)}
- if item.text:
- self.contents[item.tag][item.get('name')]['__text__'] = \
- item.text
- if item.getchildren():
- self.contents[item.tag][item.get('name')]['__children__'] \
- = item.getchildren()
- try:
- idict[item.tag].append(item.get('name'))
- except KeyError:
- idict[item.tag] = [item.get('name')]
-
- def Match(self, metadata, data, entry=lxml.etree.Element("None")):
- """Return a dictionary of package mappings."""
- if self.predicate(metadata, entry):
- for key in self.contents:
- try:
- data[key].update(self.contents[key])
- except: # pylint: disable=W0702
- data[key] = {}
- data[key].update(self.contents[key])
- for child in self.children:
- child.Match(metadata, data, entry=entry)
-
-
-class InfoNode (INode):
- """ :class:`Bcfg2.Server.Plugin.helpers.INode` implementation that
- includes ``<Path>`` tags, suitable for use with :file:`info.xml`
- files."""
-
- raw = dict(
- Client="lambda m, e: '%(name)s' == m.hostname and predicate(m, e)",
- Group="lambda m, e: '%(name)s' in m.groups and predicate(m, e)",
- Path="lambda m, e: ('%(name)s' == e.get('name') or " +
- "'%(name)s' == e.get('realname')) and " +
- "predicate(m, e)")
- nraw = dict(
- Client="lambda m, e: '%(name)s' != m.hostname and predicate(m, e)",
- Group="lambda m, e: '%(name)s' not in m.groups and predicate(m, e)",
- Path="lambda m, e: '%(name)s' != e.get('name') and " +
- "'%(name)s' != e.get('realname') and " +
- "predicate(m, e)")
- containers = ['Group', 'Client', 'Path']
-
-
-class XMLSrc(XMLFileBacked):
- """ XMLSrc files contain a
- :class:`Bcfg2.Server.Plugin.helpers.INode` hierarchy that returns
- matching entries. XMLSrc objects are deprecated and
- :class:`Bcfg2.Server.Plugin.helpers.StructFile` should be
- preferred where possible."""
- __node__ = INode
- __cacheobj__ = dict
- __priority_required__ = True
-
- def __init__(self, filename, fam=None, should_monitor=False, create=None):
- XMLFileBacked.__init__(self, filename, fam, should_monitor, create)
- self.items = {}
- self.cache = None
- self.pnode = None
- self.priority = -1
+class InfoXML(StructFile):
+ """ InfoXML files contain Group, Client, and Path tags to set the
+ metadata (permissions, owner, etc.) of files. """
+ encryption = False
- def HandleEvent(self, _=None):
- """Read file upon update."""
- self.items = {}
- try:
- xdata = lxml.etree.parse(self.name,
- parser=Bcfg2.Server.XMLParser).getroot()
- except lxml.etree.XMLSyntaxError:
- msg = "Failed to parse file %s: %s" % (self.name,
- sys.exc_info()[1])
- self.logger.error(msg)
- raise PluginExecutionError(msg)
- self.pnode = self.__node__(xdata, self.items)
- self.cache = None
- try:
- self.priority = int(xdata.get('priority'))
- except (ValueError, TypeError):
- if self.__priority_required__:
- msg = "Got bogus priority %s for file %s" % \
- (xdata.get('priority'), self.name)
- self.logger.error(msg)
- raise PluginExecutionError(msg)
+ _include_tests = copy.copy(StructFile._include_tests)
+ _include_tests['Path'] = lambda el, md, entry, *args: \
+ entry.get('realname', entry.get('name')) == el.get("name")
- def Cache(self, metadata):
- """Build a package dict for a given host."""
- if self.cache is None or self.cache[0] != metadata:
- cache = (metadata, self.__cacheobj__())
- if self.pnode is None:
- self.logger.error("Cache method called early for %s; "
- "forcing data load" % self.name)
- self.HandleEvent()
- return
- self.pnode.Match(metadata, cache[1])
- self.cache = cache
+ def Match(self, metadata, entry): # pylint: disable=W0221
+ """ Implementation of
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.Match` that
+ considers Path tags to allow ``info.xml`` files to set
+ different file metadata for different file paths. """
+ return self._do_match(metadata, entry)
- def __str__(self):
- return str(self.items)
+ def XMLMatch(self, metadata, entry): # pylint: disable=W0221
+ """ Implementation of
+ :func:`Bcfg2.Server.Plugin.helpers.StructFile.XMLMatch` that
+ considers Path tags to allow ``info.xml`` files to set
+ different file metadata for different file paths. """
+ return self._do_xmlmatch(metadata, entry)
+ def BindEntry(self, entry, metadata):
+ """ Bind the matching file metadata for this client and entry
+ to the entry.
-class InfoXML(XMLSrc):
- """ InfoXML files contain a
- :class:`Bcfg2.Server.Plugin.helpers.InfoNode` hierarchy that
- returns matching entries, suitable for use with :file:`info.xml`
- files."""
- __node__ = InfoNode
- __priority_required__ = False
+ :param entry: The abstract entry to bind the info to. This
+ will be modified in place
+ :type entry: lxml.etree._Element
+ :param metadata: The client metadata to get info for
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :returns: None
+ """
+ fileinfo = self.Match(metadata, entry)
+ if len(fileinfo) == 0:
+ raise PluginExecutionError("No metadata found in %s for %s" %
+ (self.name, entry.get('name')))
+ elif len(fileinfo) > 1:
+ self.logger.warning("Multiple file metadata found in %s for %s" %
+ (self.name, entry.get('name')))
+ for attr, val in fileinfo[0].attrib.items():
+ entry.set(attr, val)
class XMLDirectoryBacked(DirectoryBacked):
@@ -905,6 +960,25 @@ class XMLDirectoryBacked(DirectoryBacked):
__child__ = XMLFileBacked
+class PriorityStructFile(StructFile):
+ """ A StructFile where each file has a priority, given as a
+ top-level XML attribute. """
+
+ def __init__(self, filename, should_monitor=False):
+ StructFile.__init__(self, filename, should_monitor=should_monitor)
+ self.priority = -1
+ __init__.__doc__ = StructFile.__init__.__doc__
+
+ def Index(self):
+ StructFile.Index(self)
+ try:
+ self.priority = int(self.xdata.get('priority'))
+ except (ValueError, TypeError):
+ raise PluginExecutionError("Got bogus priority %s for file %s" %
+ (self.xdata.get('priority'), self.name))
+ Index.__doc__ = StructFile.Index.__doc__
+
+
class PrioDir(Plugin, Generator, XMLDirectoryBacked):
""" PrioDir handles a directory of XML files where each file has a
set priority.
@@ -915,42 +989,43 @@ class PrioDir(Plugin, Generator, XMLDirectoryBacked):
#: The type of child objects to create for files contained within
#: the directory that is tracked. Default is
- #: :class:`Bcfg2.Server.Plugin.helpers.XMLSrc`
- __child__ = XMLSrc
+ #: :class:`Bcfg2.Server.Plugin.helpers.PriorityStructFile`
+ __child__ = PriorityStructFile
- def __init__(self, core, datastore):
- Plugin.__init__(self, core, datastore)
+ def __init__(self, core):
+ Plugin.__init__(self, core)
Generator.__init__(self)
- XMLDirectoryBacked.__init__(self, self.data, self.core.fam)
+ XMLDirectoryBacked.__init__(self, self.data)
__init__.__doc__ = Plugin.__init__.__doc__
def HandleEvent(self, event):
XMLDirectoryBacked.HandleEvent(self, event)
self.Entries = {}
- for src in list(self.entries.values()):
- for itype, children in list(src.items.items()):
- for child in children:
- try:
- self.Entries[itype][child] = self.BindEntry
- except KeyError:
- self.Entries[itype] = {child: self.BindEntry}
+ for src in self.entries.values():
+ for child in src.xdata.iterchildren():
+ if child.tag in ['Group', 'Client']:
+ continue
+ if child.tag not in self.Entries:
+ self.Entries[child.tag] = dict()
+ self.Entries[child.tag][child.get("name")] = self.BindEntry
HandleEvent.__doc__ = XMLDirectoryBacked.HandleEvent.__doc__
- def _matches(self, entry, metadata, rules): # pylint: disable=W0613
- """ Whether or not a given entry has a matching entry in this
- PrioDir. By default this does strict matching (i.e., the
- entry name is in ``rules.keys()``), but this can be overridden
- to provide regex matching, etc.
+ def _matches(self, entry, metadata, candidate): # pylint: disable=W0613
+ """ Whether or not a given candidate matches the abstract
+ entry given. By default this does strict matching (i.e., the
+ entry name matches the candidate name), but this can be
+ overridden to provide regex matching, etc.
:param entry: The entry to find a match for
:type entry: lxml.etree._Element
:param metadata: The metadata to get attributes for
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :rules: A dict of rules to look in for a matching rule
- :type rules: dict
+ :candidate: A candidate concrete entry to match with
+ :type candidate: lxml.etree._Element
:returns: bool
"""
- return entry.get('name') in rules
+ return (entry.tag == candidate.tag and
+ entry.get('name') == candidate.get('name'))
def BindEntry(self, entry, metadata):
""" Bind the attributes that apply to an entry to it. The
@@ -962,71 +1037,40 @@ class PrioDir(Plugin, Generator, XMLDirectoryBacked):
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
:returns: None
"""
- attrs = self.get_attrs(entry, metadata)
- for key, val in list(attrs.items()):
- entry.attrib[key] = val
-
- def get_attrs(self, entry, metadata):
- """ Get a list of attributes to add to the entry during the
- bind. This is a complex method, in that it both modifies the
- entry, and returns attributes that need to be added to the
- entry. That seems sub-optimal, and should probably be changed
- at some point. Namely:
-
- * The return value includes all XML attributes that need to be
- added to the entry, but it does not add them.
- * If text contents or child tags need to be added to the
- entry, they are added to the entry in place.
-
- :param entry: The entry to add attributes to.
- :type entry: lxml.etree._Element
- :param metadata: The metadata to get attributes for
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :returns: dict of <attr name>:<attr value>
- :raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginExecutionError`
- """
+ matching = []
for src in self.entries.values():
- src.Cache(metadata)
-
- matching = [src for src in list(self.entries.values())
- if (src.cache and
- entry.tag in src.cache[1] and
- self._matches(entry, metadata,
- src.cache[1][entry.tag]))]
+ for candidate in src.XMLMatch(metadata).xpath("//%s" % entry.tag):
+ if self._matches(entry, metadata, candidate):
+ matching.append((src, candidate))
if len(matching) == 0:
raise PluginExecutionError("No matching source for entry when "
- "retrieving attributes for %s(%s)" %
- (entry.tag, entry.attrib.get('name')))
+ "retrieving attributes for %s:%s" %
+ (entry.tag, entry.get('name')))
elif len(matching) == 1:
- index = 0
+ data = matching[0][1]
else:
- prio = [int(src.priority) for src in matching]
- if prio.count(max(prio)) > 1:
- msg = "Found conflicting sources with same priority for " + \
- "%s:%s for %s" % (entry.tag, entry.get("name"),
- metadata.hostname)
+ prio = [int(m[0].priority) for m in matching]
+ priority = max(prio)
+ if prio.count(priority) > 1:
+ msg = "Found conflicting sources with same priority (%s) " \
+ "for %s:%s for %s" % (priority, entry.tag,
+ entry.get("name"), metadata.hostname)
self.logger.error(msg)
- self.logger.error([item.name for item in matching])
- self.logger.error("Priority was %s" % max(prio))
+ self.logger.error([m[0].name for m in matching])
raise PluginExecutionError(msg)
- index = prio.index(max(prio))
- for rname in list(matching[index].cache[1][entry.tag].keys()):
- if self._matches(entry, metadata, [rname]):
- data = matching[index].cache[1][entry.tag][rname]
- break
- else:
- # Fall back on __getitem__. Required if override used
- data = matching[index].cache[1][entry.tag][entry.get('name')]
- if '__text__' in data:
- entry.text = data['__text__']
- if '__children__' in data:
- for item in data['__children__']:
- entry.append(copy.copy(item))
+ for src, candidate in matching:
+ if int(src.priority) == priority:
+ data = candidate
+ break
+
+ entry.text = data.text
+ for item in data.getchildren():
+ entry.append(copy.copy(item))
- return dict([(key, data[key])
- for key in list(data.keys())
- if not key.startswith('__')])
+ for key, val in list(data.attrib.items()):
+ if key not in entry.attrib:
+ entry.attrib[key] = val
class Specificity(CmpMixin):
@@ -1115,11 +1159,11 @@ class Specificity(CmpMixin):
return "".join(rv)
-class SpecificData(object):
+class SpecificData(Debuggable):
""" A file that is specific to certain clients, groups, or all
clients. """
- def __init__(self, name, specific, encoding): # pylint: disable=W0613
+ def __init__(self, name, specific): # pylint: disable=W0613
"""
:param name: The full path to the file
:type name: string
@@ -1128,9 +1172,8 @@ class SpecificData(object):
object describing what clients this file
applies to.
:type specific: Bcfg2.Server.Plugin.helpers.Specificity
- :param encoding: The encoding to use for data in this file
- :type encoding: string
"""
+ Debuggable.__init__(self)
self.name = name
self.specific = specific
self.data = None
@@ -1151,9 +1194,8 @@ class SpecificData(object):
self.data = open(self.name).read()
except UnicodeDecodeError:
self.data = open(self.name, mode='rb').read()
- except: # pylint: disable=W0201
- LOGGER.error("Failed to read file %s: %s" % (self.name,
- sys.exc_info()[1]))
+ except IOError:
+ self.logger.error("Failed to read file %s" % self.name)
class EntrySet(Debuggable):
@@ -1177,7 +1219,7 @@ class EntrySet(Debuggable):
#: considered a plain string and filenames must match exactly.
basename_is_regex = False
- def __init__(self, basename, path, entry_type, encoding):
+ def __init__(self, basename, path, entry_type):
"""
:param basename: The filename or regular expression that files
in this EntrySet must match. See
@@ -1192,12 +1234,10 @@ class EntrySet(Debuggable):
be an object factory or similar callable.
See below for the expected signature.
:type entry_type: callable
- :param encoding: The encoding of all files in this entry set.
- :type encoding: string
The ``entry_type`` callable must have the following signature::
- entry_type(filepath, specificity, encoding)
+ entry_type(filepath, specificity)
Where the parameters are:
@@ -1208,8 +1248,6 @@ class EntrySet(Debuggable):
object describing what clients this file
applies to.
:type specific: Bcfg2.Server.Plugin.helpers.Specificity
- :param encoding: The encoding to use for data in this file
- :type encoding: string
Additionally, the object returned by ``entry_type`` must have
a ``specific`` attribute that is sortable (e.g., a
@@ -1222,9 +1260,8 @@ class EntrySet(Debuggable):
self.path = path
self.entry_type = entry_type
self.entries = {}
- self.metadata = DEFAULT_FILE_METADATA.copy()
+ self.metadata = default_path_metadata()
self.infoxml = None
- self.encoding = encoding
if self.basename_is_regex:
base_pat = basename
@@ -1241,6 +1278,12 @@ class EntrySet(Debuggable):
#: be overridden on a per-entry basis in :func:`entry_init`.
self.specific = re.compile(pattern)
+ def set_debug(self, debug):
+ rv = Debuggable.set_debug(self, debug)
+ for entry in self.entries.values():
+ entry.set_debug(debug)
+ return rv
+
def get_matching(self, metadata):
""" Get a list of all entries that apply to the given client.
This gets all matching entries; for example, there could be an
@@ -1299,7 +1342,7 @@ class EntrySet(Debuggable):
"""
action = event.code2str()
- if event.filename in ['info', 'info.xml', ':info']:
+ if event.filename == 'info.xml':
if action in ['exists', 'created', 'changed']:
self.update_metadata(event)
elif action == 'deleted':
@@ -1358,8 +1401,7 @@ class EntrySet(Debuggable):
self.logger.error("Could not process filename %s; ignoring"
% fpath)
return
- self.entries[event.filename] = entry_type(fpath, spec,
- self.encoding)
+ self.entries[event.filename] = entry_type(fpath, spec)
self.entries[event.filename].handle_event(event)
def specificity_from_filename(self, fname, specific=None):
@@ -1404,8 +1446,8 @@ class EntrySet(Debuggable):
return Specificity(**kwargs)
def update_metadata(self, event):
- """ Process changes to or creation of info, :info, and
- info.xml files for the EntrySet.
+ """ Process changes to or creation of info.xml files for the
+ EntrySet.
:param event: An event that applies to an info handled by this
EntrySet
@@ -1417,24 +1459,9 @@ class EntrySet(Debuggable):
if not self.infoxml:
self.infoxml = InfoXML(fpath)
self.infoxml.HandleEvent(event)
- elif event.filename in [':info', 'info']:
- for line in open(fpath).readlines():
- match = INFO_REGEX.match(line)
- if not match:
- self.logger.warning("Failed to match line in %s: %s" %
- (fpath, line))
- continue
- else:
- mgd = match.groupdict()
- for key, value in list(mgd.items()):
- if value:
- self.metadata[key] = value
- if len(self.metadata['mode']) == 3:
- self.metadata['mode'] = "0%s" % self.metadata['mode']
def reset_metadata(self, event):
- """ Reset metadata to defaults if info. :info, or info.xml are
- removed.
+ """ Reset metadata to defaults if info.xml is removed.
:param event: An event that applies to an info handled by this
EntrySet
@@ -1443,12 +1470,10 @@ class EntrySet(Debuggable):
"""
if event.filename == 'info.xml':
self.infoxml = None
- elif event.filename in [':info', 'info']:
- self.metadata = DEFAULT_FILE_METADATA.copy()
def bind_info_to_entry(self, entry, metadata):
- """ Shortcut to call :func:`bind_info` with the base
- info/info.xml for this EntrySet.
+ """ Bind the metadata for the given client in the base
+ info.xml for this EntrySet to the entry.
:param entry: The abstract entry to bind the info to. This
will be modified in place
@@ -1457,7 +1482,10 @@ class EntrySet(Debuggable):
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
:returns: None
"""
- bind_info(entry, metadata, infoxml=self.infoxml, default=self.metadata)
+ for attr, val in list(self.metadata.items()):
+ entry.set(attr, val)
+ if self.infoxml is not None:
+ self.infoxml.BindEntry(entry, metadata)
def bind_entry(self, entry, metadata):
""" Return the single best fully-bound entry from the set of
@@ -1503,10 +1531,12 @@ class GroupSpool(Plugin, Generator):
#: object.
entry_type = 'Path'
- def __init__(self, core, datastore):
- Plugin.__init__(self, core, datastore)
+ def __init__(self, core):
+ Plugin.__init__(self, core)
Generator.__init__(self)
+ self.fam = Bcfg2.Server.FileMonitor.get_fam()
+
#: See :class:`Bcfg2.Server.Plugins.interfaces.Generator` for
#: details on the Entries attribute.
self.Entries[self.entry_type] = {}
@@ -1518,7 +1548,6 @@ class GroupSpool(Plugin, Generator):
self.entries = {}
self.handles = {}
self.AddDirectoryMonitor('')
- self.encoding = core.setup['encoding']
__init__.__doc__ = Plugin.__init__.__doc__
def add_entry(self, event):
@@ -1542,8 +1571,7 @@ class GroupSpool(Plugin, Generator):
dirpath = self.data + ident
self.entries[ident] = self.es_cls(self.filename_pattern,
dirpath,
- self.es_child_cls,
- self.encoding)
+ self.es_child_cls)
self.Entries[self.entry_type][ident] = \
self.entries[ident].bind_entry
if not os.path.isdir(epath):
@@ -1653,5 +1681,5 @@ class GroupSpool(Plugin, Generator):
if not os.path.isdir(name):
self.logger.error("Failed to open directory %s" % name)
return
- reqid = self.core.fam.AddMonitor(name, self)
+ reqid = self.fam.AddMonitor(name, self)
self.handles[reqid] = relative