summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/server/plugins/grouping/metadata.txt42
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py31
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py53
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py1
4 files changed, 97 insertions, 30 deletions
diff --git a/doc/server/plugins/grouping/metadata.txt b/doc/server/plugins/grouping/metadata.txt
index a6ed37f8e..11b3d5496 100644
--- a/doc/server/plugins/grouping/metadata.txt
+++ b/doc/server/plugins/grouping/metadata.txt
@@ -197,9 +197,9 @@ useful results:
.. code-block:: xml
- <Groups version='3.0' xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="my-groups.xml" />
- <xi:include href="their-groups.xml" />
+ <Groups xmlns:xi="http://www.w3.org/2001/XInclude">
+ <xi:include href="my-groups.xml" />
+ <xi:include href="their-groups.xml" />
</Groups>
Each of the included groups files has the same format. These files are
@@ -207,6 +207,42 @@ properly validated by ``bcfg2-lint``. This mechanism is useful for
composing group definitions from multiple sources, or setting
different permissions in an svn repository.
+You can also optionally include a file that may or may not exist with
+the ``fallback`` tag:
+
+.. code-block:: xml
+
+ <Groups xmlns:xi="http://www.w3.org/2001/XInclude">
+ <xi:include href="my-groups.xml"/>
+ <xi:include href="their-groups.xml"><xi:fallback/></xi:include>
+ </Groups>
+
+In this case, if ``their-groups.xml`` does not exist, no error will be
+raised and everything will work fine. (You can also use ``fallback``
+to include a different file, or explicit content in the case that the
+parent include does not exist.)
+
+Wildcard XInclude
+~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.3.1
+
+Bcfg2 supports an extension to XInclude that allows you to use shell
+globbing in the hrefs. (Stock XInclude doesn't support this, since
+the href is supposed to be a URL.)
+
+For instance:
+
+ <Groups xmlns:xi="http://www.w3.org/2001/XInclude">
+ <xi:include href="groups/*.xml"/>
+ </Groups>
+
+This would include all ``*.xml`` files in the ``groups`` subdirectory.
+
+Note that if a glob finds no files, that is treated the same as if a
+single included file does not exist. You should use the ``fallback``
+tag, described above, if a glob may potentially find no files.
+
Probes
======
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 41c450b4e..c2252f956 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -5,6 +5,7 @@ import re
import sys
import copy
import time
+import glob
import logging
import operator
import lxml.etree
@@ -503,13 +504,14 @@ class XMLFileBacked(FileBacked):
def _follow_xincludes(self, fname=None, xdata=None):
""" follow xincludes, adding included files to self.extras """
+ xinclude = '%sinclude' % Bcfg2.Server.XI_NAMESPACE
+
if xdata is None:
if fname is None:
xdata = self.xdata.getroottree()
else:
xdata = lxml.etree.parse(fname)
- included = [el for el in xdata.findall('//%sinclude' %
- Bcfg2.Server.XI_NAMESPACE)]
+ included = [el for el in xdata.findall('//' + xinclude)]
for el in included:
name = el.get("href")
if name.startswith("/"):
@@ -520,16 +522,23 @@ class XMLFileBacked(FileBacked):
else:
rel = self.name
fpath = os.path.join(os.path.dirname(rel), name)
- if fpath not in self.extras:
- if os.path.exists(fpath):
- self._follow_xincludes(fname=fpath)
- self.add_monitor(fpath)
+
+ # expand globs in xinclude, a bcfg2-specific extension
+ extras = glob.glob(fpath)
+ if not extras:
+ msg = "%s: %s does not exist, skipping" % (self.name, name)
+ if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
+ LOGGER.debug(msg)
else:
- msg = "%s: %s does not exist, skipping" % (self.name, name)
- if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
- LOGGER.debug(msg)
- else:
- LOGGER.warning(msg)
+ LOGGER.warning(msg)
+
+ parent = el.getparent()
+ parent.remove(el)
+ for extra in extras:
+ if extra != self.name and extra not in self.extras:
+ self.add_monitor(extra)
+ lxml.etree.SubElement(parent, xinclude, href=extra)
+ self._follow_xincludes(fname=extra)
def Index(self):
self.xdata = lxml.etree.XML(self.data, base_url=self.name,
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
index 22c53c63e..fb51eb1fe 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
@@ -417,21 +417,22 @@ class TestXMLFileBacked(TestFileBacked):
xfb = self.get_obj(fam=fam, should_monitor=True)
fam.AddMonitor.assert_called_with(self.path, xfb)
- @patch("os.path.exists")
+ @patch("glob.glob")
@patch("lxml.etree.parse")
- def test_follow_xincludes(self, mock_parse, mock_exists):
+ def test_follow_xincludes(self, mock_parse, mock_glob):
xfb = self.get_obj()
xfb.add_monitor = Mock()
+ xfb.add_monitor.side_effect = lambda p: xfb.extras.append(p)
def reset():
xfb.add_monitor.reset_mock()
+ mock_glob.reset_mock()
mock_parse.reset_mock()
- mock_exists.reset_mock()
xfb.extras = []
- mock_exists.return_value = True
xdata = dict()
mock_parse.side_effect = lambda p: xdata[p]
+ mock_glob.side_effect = lambda g: [g]
base = os.path.dirname(self.path)
@@ -465,7 +466,7 @@ class TestXMLFileBacked(TestFileBacked):
xfb.add_monitor.assert_called_with(test2)
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()])
- mock_exists.assert_called_with(test2)
+ mock_glob.assert_called_with(test2)
reset()
xfb._follow_xincludes(fname=self.path, xdata=xdata[self.path])
@@ -473,7 +474,7 @@ class TestXMLFileBacked(TestFileBacked):
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()
if f != self.path])
- mock_exists.assert_called_with(test2)
+ mock_glob.assert_called_with(test2)
# test two-deep level of xinclude, with some files in another
# directory
@@ -501,21 +502,41 @@ class TestXMLFileBacked(TestFileBacked):
reset()
xfb._follow_xincludes(fname=self.path)
- self.assertItemsEqual(xfb.add_monitor.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
+ expected = [call(f) for f in xdata.keys() if f != self.path]
+ self.assertItemsEqual(xfb.add_monitor.call_args_list, expected)
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()])
- self.assertItemsEqual(mock_exists.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
+ self.assertItemsEqual(mock_glob.call_args_list, expected)
reset()
xfb._follow_xincludes(fname=self.path, xdata=xdata[self.path])
- self.assertItemsEqual(xfb.add_monitor.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
- self.assertItemsEqual(mock_parse.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
- self.assertItemsEqual(mock_exists.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
+ expected = [call(f) for f in xdata.keys() if f != self.path]
+ self.assertItemsEqual(xfb.add_monitor.call_args_list, expected)
+ self.assertItemsEqual(mock_parse.call_args_list, expected)
+ self.assertItemsEqual(mock_glob.call_args_list, expected)
+
+ # test wildcard xinclude
+ reset()
+ xdata[self.path] = lxml.etree.Element("Test").getroottree()
+ lxml.etree.SubElement(xdata[self.path].getroot(),
+ Bcfg2.Server.XI_NAMESPACE + "include",
+ href="*.xml")
+
+ def glob_rv(path):
+ if path == os.path.join(base, '*.xml'):
+ return [self.path, test2, test3]
+ else:
+ return [path]
+ mock_glob.side_effect = glob_rv
+
+ xfb._follow_xincludes(xdata=xdata[self.path])
+ expected = [call(f) for f in xdata.keys() if f != self.path]
+ self.assertItemsEqual(xfb.add_monitor.call_args_list, expected)
+ self.assertItemsEqual(mock_parse.call_args_list, expected)
+ self.assertItemsEqual(mock_glob.call_args_list,
+ [call(os.path.join(base, '*.xml')), call(test4),
+ call(test5), call(test6)])
+
@patch("lxml.etree._ElementTree", FakeElementTree)
@patch("Bcfg2.Server.Plugin.helpers.%s._follow_xincludes" %
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
index f627e4465..ab7123fa7 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
@@ -198,6 +198,7 @@ if HAS_DJANGO or can_skip:
class TestXMLMetadataConfig(TestXMLFileBacked):
test_obj = XMLMetadataConfig
+ path = os.path.join(datastore, 'Metadata', 'clients.xml')
def get_obj(self, basefile="clients.xml", core=None, watch_clients=False):
self.metadata = get_metadata_object(core=core,