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.py616
1 files changed, 344 insertions, 272 deletions
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 0b81077a3..ded7dd8dc 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -4,14 +4,14 @@ import os
import re
import sys
import copy
-import time
import glob
+import genshi
import logging
import operator
import lxml.etree
import Bcfg2.Server
import Bcfg2.Options
-import Bcfg2.Statistics
+import Bcfg2.Server.FileMonitor
from Bcfg2.Compat import CmpMixin, wraps
from Bcfg2.Server.Plugin.base import Debuggable, Plugin
from Bcfg2.Server.Plugin.interfaces import Generator
@@ -19,101 +19,46 @@ from Bcfg2.Server.Plugin.exceptions import SpecificityError, \
PluginExecutionError
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('owner:(\s)*(?P<owner>\S+)|' +
- 'group:(\s)*(?P<group>\S+)|' +
- 'mode:(\s)*(?P<mode>\w+)|' +
- 'secontext:(\s)*(?P<secontext>\S+)|' +
- 'paranoid:(\s)*(?P<paranoid>\S+)|' +
- 'sensitive:(\s)*(?P<sensitive>\S+)|' +
- 'encoding:(\s)*(?P<encoding>\S+)|' +
- 'important:(\s)*(?P<important>\S+)|' +
- 'mtime:(\s)*(?P<mtime>\w+)|')
-
-
-def bind_info(entry, metadata, infoxml=None, default=DEFAULT_FILE_METADATA):
- """ Bind the file metadata in the given
- :class:`Bcfg2.Server.Plugin.helpers.InfoXML` object to the given
- entry.
-
- :param entry: The abstract entry to bind the info to
- :type entry: lxml.etree._Element
- :param metadata: The client metadata to get info for
- :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :param infoxml: The info.xml file to pull file metadata from
- :type infoxml: Bcfg2.Server.Plugin.helpers.InfoXML
- :param default: Default metadata to supply when the info.xml file
- does not include a particular attribute
- :type default: dict
- :returns: None
- :raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginExecutionError`
- """
- for attr, val in list(default.items()):
- entry.set(attr, val)
- if infoxml:
- mdata = dict()
- infoxml.pnode.Match(metadata, mdata, entry=entry)
- if 'Info' not in mdata:
- msg = "Failed to set metadata for file %s" % entry.get('name')
- LOGGER.error(msg)
- raise PluginExecutionError(msg)
- for attr, val in list(mdata['Info'][None].items()):
- 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 __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
+def removecomment(stream):
+ """ A Genshi filter that removes comments from the stream. This
+ function is a generator.
- def __call__(self, func):
- if self.name is None:
- self.name = func.__name__
+ :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
- @wraps(func)
- def inner(obj, *args, **kwargs):
- """ The decorated function """
- name = "%s:%s" % (obj.__class__.__name__, self.name)
- start = time.time()
- try:
- return func(obj, *args, **kwargs)
- finally:
- Bcfg2.Statistics.stats.add_value(name, time.time() - start)
+def default_path_metadata():
+ """ Get the default Path entry metadata from the config.
- return inner
+ :returns: dict of metadata attributes and their default values
+ """
+ attrs = Bcfg2.Options.PATH_METADATA_OPTIONS.keys()
+ setup = Bcfg2.Options.get_option_parser()
+ if not set(attrs).issubset(setup.keys()):
+ setup.add_options(Bcfg2.Options.PATH_METADATA_OPTIONS)
+ setup.reparse(argv=[Bcfg2.Options.CFILE.cmd, Bcfg2.Options.CFILE])
+ return dict([(k, setup[k]) for k in attrs])
class DatabaseBacked(Plugin):
@@ -193,20 +138,17 @@ class PluginDatabaseModel(object):
app_label = "Server"
-class FileBacked(object):
+class FileBacked(Debuggable):
""" This object caches file data in memory. FileBacked objects are
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
"""
- object.__init__(self)
+ Debuggable.__init__(self)
#: A string containing the raw data in this file
self.data = ''
@@ -215,7 +157,7 @@ class FileBacked(object):
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.
@@ -231,10 +173,10 @@ class FileBacked(object):
self.Index()
except IOError:
err = sys.exc_info()[1]
- LOGGER.error("Failed to read file %s: %s" % (self.name, err))
+ self.logger.error("Failed to read file %s: %s" % (self.name, err))
except:
err = sys.exc_info()[1]
- LOGGER.error("Failed to parse file %s: %s" % (self.name, err))
+ self.logger.error("Failed to parse file %s: %s" % (self.name, err))
def Index(self):
""" Index() is called by :func:`HandleEvent` every time the
@@ -268,14 +210,11 @@ class DirectoryBacked(object):
#: :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__
@@ -283,7 +222,7 @@ class DirectoryBacked(object):
object.__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
@@ -337,8 +276,7 @@ class DirectoryBacked(object):
: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
@@ -459,13 +397,10 @@ class XMLFileBacked(FileBacked):
#: behavior, set ``__identifier__`` to ``None``.
__identifier__ = 'name'
- def __init__(self, filename, fam=None, should_monitor=False):
+ def __init__(self, filename, should_monitor=False):
"""
: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
@@ -478,7 +413,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
@@ -499,7 +434,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):
@@ -528,9 +463,9 @@ class XMLFileBacked(FileBacked):
if not extras:
msg = "%s: %s does not exist, skipping" % (self.name, name)
if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
- LOGGER.debug(msg)
+ self.logger.debug(msg)
else:
- LOGGER.warning(msg)
+ self.logger.warning(msg)
parent = el.getparent()
parent.remove(el)
@@ -550,7 +485,8 @@ class XMLFileBacked(FileBacked):
self.xdata.getroottree().xinclude()
except lxml.etree.XIncludeError:
err = sys.exc_info()[1]
- LOGGER.error("XInclude failed on %s: %s" % (self.name, err))
+ self.logger.error("XInclude failed on %s: %s" % (self.name,
+ err))
self.entries = self.xdata.getchildren()
if self.__identifier__ is not None:
@@ -567,7 +503,7 @@ class XMLFileBacked(FileBacked):
:returns: None
"""
self.extras.append(fpath)
- if self.fam and self.should_monitor:
+ if self.should_monitor:
self.fam.AddMonitor(fpath, self)
def __iter__(self):
@@ -580,44 +516,174 @@ 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 """
+ #: 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):
+ XMLFileBacked.__init__(self, filename, should_monitor=should_monitor)
+ self.setup = Bcfg2.Options.get_option_parser()
+ self.encoding = self.setup['encoding']
+ 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=self.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:
+ strict = self.xdata.get(
+ "decrypt",
+ self.setup.cfp.get(Bcfg2.Server.Encryption.CFG_SECTION,
+ "decrypt", default="strict")) == "strict"
+ 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:
+ msg = "Failed to decrypt %s element in %s" % (el.tag,
+ self.name)
+ if strict:
+ raise PluginExecutionError(msg)
+ else:
+ self.logger.warning(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.Server.Encryption.get_passphrases()
+ try:
+ passphrase = passes[element.get("encrypted")]
+ try:
+ return Bcfg2.Server.Encryption.ssl_decrypt(element.text,
+ passphrase)
+ except Bcfg2.Server.Encryption.EVPError:
+ # error is raised below
+ pass
+ except KeyError:
+ # bruteforce_decrypt raises an EVPError with a sensible
+ # error message, so we just let it propagate up the stack
+ return Bcfg2.Server.Encryption.bruteforce_decrypt(element.text)
+ 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(
+ metadata=metadata,
+ repo=self.setup['repo']).filter(removecomment)
+ return lxml.etree.XML(stream.render('xml',
+ strip_whitespace=False),
+ 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>``
@@ -628,22 +694,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
@@ -652,10 +718,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
@@ -665,13 +746,13 @@ 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):
@@ -746,26 +827,6 @@ class INode(object):
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
@@ -776,8 +837,8 @@ class XMLSrc(XMLFileBacked):
__cacheobj__ = dict
__priority_required__ = True
- def __init__(self, filename, fam=None, should_monitor=False):
- XMLFileBacked.__init__(self, filename, fam, should_monitor)
+ def __init__(self, filename, should_monitor=False):
+ XMLFileBacked.__init__(self, filename, should_monitor)
self.items = {}
self.cache = None
self.pnode = None
@@ -789,7 +850,7 @@ class XMLSrc(XMLFileBacked):
data = open(self.name).read()
except IOError:
msg = "Failed to read file %s: %s" % (self.name, sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.items = {}
try:
@@ -797,7 +858,7 @@ class XMLSrc(XMLFileBacked):
except lxml.etree.XMLSyntaxError:
msg = "Failed to parse file %s: %s" % (self.name,
sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.pnode = self.__node__(xdata, self.items)
self.cache = None
@@ -807,7 +868,7 @@ class XMLSrc(XMLFileBacked):
if self.__priority_required__:
msg = "Got bogus priority %s for file %s" % \
(xdata.get('priority'), self.name)
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
del xdata, data
@@ -817,8 +878,8 @@ class XMLSrc(XMLFileBacked):
if self.cache is None or self.cache[0] != metadata:
cache = (metadata, self.__cacheobj__())
if self.pnode is None:
- LOGGER.error("Cache method called early for %s; "
- "forcing data load" % self.name)
+ self.logger.error("Cache method called early for %s; "
+ "forcing data load" % self.name)
self.HandleEvent()
return
self.pnode.Match(metadata, cache[1])
@@ -828,13 +889,48 @@ class XMLSrc(XMLFileBacked):
return str(self.items)
-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
+class InfoXML(StructFile):
+ """ InfoXML files contain Group, Client, and Path tags to set the
+ metadata (permissions, owner, etc.) of files. """
+
+ _include_tests = StructFile._include_tests
+ _include_tests['Path'] = lambda el, md, entry, *args: \
+ entry.get("name") == el.get("name")
+
+ 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 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.
+
+ :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):
@@ -850,6 +946,24 @@ 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):
+ 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.
@@ -860,13 +974,13 @@ 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)
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):
@@ -881,21 +995,22 @@ class PrioDir(Plugin, Generator, XMLDirectoryBacked):
self.Entries[itype] = {child: 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
@@ -907,71 +1022,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
- return dict([(key, data[key])
- for key in list(data.keys())
- if not key.startswith('__')])
+ entry.text = data.text
+ for item in data.getchildren():
+ entry.append(copy.copy(item))
+
+ for key, val in list(data.attrib.items()):
+ if key not in entry.attrib:
+ entry.attrib[key] = val
class Specificity(CmpMixin):
@@ -1166,7 +1250,7 @@ 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
@@ -1243,7 +1327,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':
@@ -1254,8 +1338,8 @@ class EntrySet(Debuggable):
self.entry_init(event)
else:
if event.filename not in self.entries:
- LOGGER.warning("Got %s event for unknown file %s" %
- (action, event.filename))
+ self.logger.warning("Got %s event for unknown file %s" %
+ (action, event.filename))
if action == 'changed':
# received a bogus changed event; warn, but treat
# it like a created event
@@ -1291,7 +1375,7 @@ class EntrySet(Debuggable):
entry_type = self.entry_type
if event.filename in self.entries:
- LOGGER.warn("Got duplicate add for %s" % event.filename)
+ self.logger.warn("Got duplicate add for %s" % event.filename)
else:
fpath = os.path.join(self.path, event.filename)
try:
@@ -1299,8 +1383,8 @@ class EntrySet(Debuggable):
specific=specific)
except SpecificityError:
if not self.ignore.match(event.filename):
- LOGGER.error("Could not process filename %s; ignoring" %
- fpath)
+ self.logger.error("Could not process filename %s; ignoring"
+ % fpath)
return
self.entries[event.filename] = entry_type(fpath, spec,
self.encoding)
@@ -1348,8 +1432,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
@@ -1361,24 +1445,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:
- 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
@@ -1387,12 +1456,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
@@ -1401,7 +1468,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
@@ -1453,6 +1523,8 @@ class GroupSpool(Plugin, Generator):
if self.data[-1] == '/':
self.data = self.data[:-1]
+ 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] = {}
@@ -1599,5 +1671,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