diff options
-rw-r--r-- | doc/server/plugins/structures/bundler/index.txt | 49 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/Bundler.py | 55 | ||||
-rw-r--r-- | testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py | 48 |
3 files changed, 136 insertions, 16 deletions
diff --git a/doc/server/plugins/structures/bundler/index.txt b/doc/server/plugins/structures/bundler/index.txt index 1b88627a5..8f767ba03 100644 --- a/doc/server/plugins/structures/bundler/index.txt +++ b/doc/server/plugins/structures/bundler/index.txt @@ -133,6 +133,55 @@ entries in the bundle. See :ref:`bcfg2-info <server-bcfg2-info>` for more details. +Dependencies +============ + +Dependencies on other bundles can be specified by adding an empty +bundle tag that adds another bundle by name, e.g.: + +.. code-block:: xml + + <Bundle> + <Bundle name="nfs-client"/> + ... + </Bundle> + +The dependent bundle is added to the list of bundles sent to the +client, *not* to the parent bundle itself. In other words, if an +entry in the dependent bundle changes, Services are restarted and +Actions are run in the dependent bundle *only*. An example: + +``nfs-client.xml``: + +.. code-block:: xml + + <Bundle> + <Package name="nfs-utils"/> + <Service name="nfslock"/> + <Service name="rpcbind"/> + <Service name="nfs"/> + </Bundle> + +``automount.xml``: + +.. code-block:: xml + + <Bundle> + <Bundle name="nfs-client"/> + + <Path name="/mnt/home"/> + <Path name="/etc/auto.master"/> + <Path name="/etc/auto.misc"/> + <Service name="autofs"/> + <Package name="automount"/> + </Bundle> + +If a new ``nfs-utils`` package was installed, the ``nfslock``, +``rpcbind``, and ``nfs`` services would be restarted, but *not* the +``autofs`` service. Similarly, if a new ``/etc/auto.misc`` file was +sent out, the ``autofs`` service would be restarted, but the +``nfslock``, ``rpcbind``, and ``nfs`` services would not be restarted. + Altsrc ====== diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py index 5eeb542ee..5a6767755 100644 --- a/src/lib/Bcfg2/Server/Plugins/Bundler.py +++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py @@ -3,6 +3,7 @@ import os import re import sys +import copy import Bcfg2.Server import Bcfg2.Server.Plugin import Bcfg2.Server.Lint @@ -17,16 +18,17 @@ class BundleFile(Bcfg2.Server.Plugin.StructFile): Bcfg2.Server.Plugin.StructFile.__init__(self, filename, should_monitor=should_monitor) if self.name.endswith(".genshi"): - self.logger.warning("Bundler: Bundle filenames ending with " + self.logger.warning("Bundler: %s: Bundle filenames ending with " ".genshi are deprecated; add the Genshi XML " - "namespace to a .xml bundle instead") + "namespace to a .xml bundle instead" % + self.name) __init__.__doc__ = Bcfg2.Server.Plugin.StructFile.__init__.__doc__ def Index(self): Bcfg2.Server.Plugin.StructFile.Index(self) if self.xdata.get("name"): - self.logger.warning("Bundler: Explicitly specifying bundle names " - "is deprecated") + self.logger.warning("Bundler: %s: Explicitly specifying bundle " + "names is deprecated" % self.name) Index.__doc__ = Bcfg2.Server.Plugin.StructFile.Index.__doc__ @property @@ -55,30 +57,65 @@ class Bundler(Bcfg2.Server.Plugin.Plugin, def HandleEvent(self, event): Bcfg2.Server.Plugin.XMLDirectoryBacked.HandleEvent(self, event) - self.bundles = dict() - for bundle in self.entries.values(): - self.bundles[bundle.bundle_name] = bundle + self.bundles = dict([(b.bundle_name, b) + for b in self.entries.values()]) HandleEvent.__doc__ = \ Bcfg2.Server.Plugin.XMLDirectoryBacked.HandleEvent.__doc__ def BuildStructures(self, metadata): bundleset = [] - for bundlename in metadata.bundles: + bundles = copy.copy(metadata.bundles) + bundles_added = set(bundles) + while bundles: + bundlename = bundles.pop() try: bundle = self.bundles[bundlename] except KeyError: self.logger.error("Bundler: Bundle %s does not exist" % bundlename) continue + try: - bundleset.append(bundle.XMLMatch(metadata)) + data = bundle.XMLMatch(metadata) except TemplateError: err = sys.exc_info()[1] self.logger.error("Bundler: Failed to render templated bundle " "%s: %s" % (bundlename, err)) + continue except: self.logger.error("Bundler: Unexpected bundler error for %s" % bundlename, exc_info=1) + continue + + data.set("name", bundlename) + + for child in data.findall("Bundle"): + if child.getchildren(): + # XInclude'd bundle -- "flatten" it so there + # aren't extra Bundle tags, since other bits in + # Bcfg2 only handle the direct children of the + # top-level Bundle tag + if data.get("name"): + self.logger.warning("Bundler: In file XIncluded from " + "%s: Explicitly specifying " + "bundle names is deprecated" % + self.name) + for el in child.getchildren(): + data.append(el) + data.remove(child) + elif child.get("name"): + # dependent bundle -- add it to the list of + # bundles for this client + if child.get("name") not in bundles_added: + bundles.append(child.get("name")) + bundles_added.add(child.get("name")) + data.remove(child) + else: + # neither name or children -- wat + self.logger.warning("Bundler: Useless empty Bundle tag " + "in %s" % self.name) + data.remove(child) + bundleset.append(data) return bundleset BuildStructures.__doc__ = \ Bcfg2.Server.Plugin.Structure.BuildStructures.__doc__ diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py index f64d66d11..f5250ed85 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py @@ -60,12 +60,46 @@ class TestBundler(TestPlugin, TestStructure, TestXMLDirectoryBacked): def test_BuildStructures(self): b = self.get_obj() - b.bundles = dict(foo=Mock(), bar=Mock(), baz=Mock()) + b.bundles = dict(error=Mock(), skip=Mock(), xinclude=Mock(), + has_dep=Mock(), is_dep=Mock()) + expected = dict() + + b.bundles['error'].XMLMatch.side_effect = TemplateError(None) + + xinclude = lxml.etree.Element("Bundle") + lxml.etree.SubElement(lxml.etree.SubElement(xinclude, "Bundle"), + "Path", name="/test") + b.bundles['xinclude'].XMLMatch.return_value = xinclude + expected['xinclude'] = lxml.etree.Element("Bundle", name="xinclude") + lxml.etree.SubElement(expected['xinclude'], "Path", name="/test") + + has_dep = lxml.etree.Element("Bundle") + lxml.etree.SubElement(has_dep, "Bundle", name="is_dep") + lxml.etree.SubElement(has_dep, "Package", name="foo") + b.bundles['has_dep'].XMLMatch.return_value = has_dep + expected['has_dep'] = lxml.etree.Element("Bundle", name="has_dep") + lxml.etree.SubElement(expected['has_dep'], "Package", name="foo") + + is_dep = lxml.etree.Element("Bundle") + lxml.etree.SubElement(is_dep, "Package", name="bar") + b.bundles['is_dep'].XMLMatch.return_value = is_dep + expected['is_dep'] = lxml.etree.Element("Bundle", name="is_dep") + lxml.etree.SubElement(expected['is_dep'], "Package", name="bar") + metadata = Mock() - metadata.bundles = ["foo", "baz"] + metadata.bundles = ["error", "xinclude", "has_dep"] + + rv = b.BuildStructures(metadata) + self.assertEqual(len(rv), 3) + for bundle in rv: + name = bundle.get("name") + self.assertIsNotNone(name, + "Bundle %s was not built" % name) + self.assertIn(name, expected, + "Unexpected bundle %s was built" % name) + self.assertXMLEqual(bundle, expected[name], + "Bundle %s was not built correctly" % name) + b.bundles[name].XMLMatch.assert_called_with(metadata) - self.assertItemsEqual(b.BuildStructures(metadata), - [b.bundles[n].XMLMatch.return_value - for n in metadata.bundles]) - for bname in metadata.bundles: - b.bundles[bname].XMLMatch.assert_called_with(metadata) + b.bundles['error'].XMLMatch.assert_called_with(metadata) + self.assertFalse(b.bundles['skip'].XMLMatch.called) |