summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-02-08 10:29:03 -0500
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-02-08 10:29:03 -0500
commit0dab7e284017e4559019ac1e7b861ab7ccdadf5c (patch)
tree40cc3e3e92b23761dd164ba3b0cdb555486132df
parent041196dc73d55d73c8b165168312eb5e0a018704 (diff)
downloadbcfg2-0dab7e284017e4559019ac1e7b861ab7ccdadf5c.tar.gz
bcfg2-0dab7e284017e4559019ac1e7b861ab7ccdadf5c.tar.bz2
bcfg2-0dab7e284017e4559019ac1e7b861ab7ccdadf5c.zip
Bundler: improved XInclude support, added inter-bundle dependencies
-rw-r--r--doc/server/plugins/structures/bundler/index.txt49
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Bundler.py55
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py48
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)