summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSol Jerome <sol.jerome@gmail.com>2015-01-03 13:07:14 -0600
committerSol Jerome <sol.jerome@gmail.com>2015-01-03 13:07:14 -0600
commit99f7a6addbad7c7f4bc4e1bcb5238f039e1c5692 (patch)
tree852aa12fe61fb9559f3888fed7b1bf234de6b9e9
parent128efd62c9acf84c54f071043e1ea954da3361dd (diff)
parentd4ae5e04739d9a8e0732dd35ee28c14b0ff96957 (diff)
downloadbcfg2-99f7a6addbad7c7f4bc4e1bcb5238f039e1c5692.tar.gz
bcfg2-99f7a6addbad7c7f4bc4e1bcb5238f039e1c5692.tar.bz2
bcfg2-99f7a6addbad7c7f4bc4e1bcb5238f039e1c5692.zip
Merge branch 'bundle-modification-deps' of https://github.com/AlexanderS/bcfg2
Conflicts: src/lib/Bcfg2/Client/__init__.py
-rw-r--r--doc/server/plugins/structures/bundler/bcfg2.txt2
-rw-r--r--doc/server/plugins/structures/bundler/index.txt25
-rw-r--r--schemas/bundle.xsd29
-rw-r--r--src/lib/Bcfg2/Client/Tools/BundleDeps.py34
-rw-r--r--src/lib/Bcfg2/Client/__init__.py59
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Bundler.py28
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py38
7 files changed, 187 insertions, 28 deletions
diff --git a/doc/server/plugins/structures/bundler/bcfg2.txt b/doc/server/plugins/structures/bundler/bcfg2.txt
index 0fd0a3fdf..6d1159ae0 100644
--- a/doc/server/plugins/structures/bundler/bcfg2.txt
+++ b/doc/server/plugins/structures/bundler/bcfg2.txt
@@ -16,7 +16,7 @@ entries between Bundler and Rules.
.. code-block:: xml
<Bundle>
- <Bundle name="bcfg2-server-base.xml"/>
+ <RequiredBundle name="bcfg2-server-base.xml"/>
<Path name="/etc/pki/tls/private/bcfg2.key"/>
<Path name="/etc/sysconfig/bcfg2-server"/>
diff --git a/doc/server/plugins/structures/bundler/index.txt b/doc/server/plugins/structures/bundler/index.txt
index 31faeaf17..e8f609e09 100644
--- a/doc/server/plugins/structures/bundler/index.txt
+++ b/doc/server/plugins/structures/bundler/index.txt
@@ -151,20 +151,21 @@ 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.:
+Dependencies on other bundles can be specified by adding an
+RequiredBundle tag that adds another bundle by name, e.g.:
.. code-block:: xml
<Bundle>
- <Bundle name="nfs-client"/>
+ <RequiredBundle 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:
+client, *not* to the parent bundle itself. If you want to propagate
+the modification flag from the required bundle, you can add
+``inherit_modification="true"`` to the RequiredBundle tag.
+An example:
``nfs-client.xml``:
@@ -182,7 +183,7 @@ Actions are run in the dependent bundle *only*. An example:
.. code-block:: xml
<Bundle>
- <Bundle name="nfs-client"/>
+ <RequiredBundle name="nfs-client"/>
<Path name="/mnt/home"/>
<Path name="/etc/auto.master"/>
@@ -193,9 +194,13 @@ Actions are run in the dependent bundle *only*. An example:
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.
+``autofs`` service. If you would add ``inherit_modification="true"``
+to the RequiredBundle tag, you would ensure the propagation of the
+modification flag and the ``autofs`` service would be restarted,
+too. But if a new ``/etc/auto.misc`` file was sent out, *only* the
+``autofs`` service would be restarted, but the ``nfslock``,
+``rpcbind``, and ``nfs`` services would not be restarted
+(independent of the ``inherit_modification`` flag).
Altsrc
======
diff --git a/schemas/bundle.xsd b/schemas/bundle.xsd
index aeacd0517..4a11a1d1b 100644
--- a/schemas/bundle.xsd
+++ b/schemas/bundle.xsd
@@ -263,6 +263,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
+ <xsd:element name='RequiredBundle' type='RequiredBundleType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Nesting Bundle tags to specify dependencies to other bundles.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
</xsd:choice>
</xsd:group>
@@ -300,6 +307,28 @@
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
+ <xsd:complexType name='RequiredBundleType'>
+ <xsd:attribute type='xsd:string' name='name' use='required'>
+ <xsd:annotation>
+ <xsd:documentation>
+ The name of the required bundle.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type='xsd:boolean' name='inherit_modification'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Specify how to handle modifications in the required
+ bundle. You can either ignore the modifications (this
+ is the default) or you can inherit the modifications
+ so that Services in the current Bundle are restarted
+ if the required Bundle is modified.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
<xsd:complexType name='BundleType'>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
<xsd:group ref="bundleElements"/>
diff --git a/src/lib/Bcfg2/Client/Tools/BundleDeps.py b/src/lib/Bcfg2/Client/Tools/BundleDeps.py
new file mode 100644
index 000000000..aaa090633
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/BundleDeps.py
@@ -0,0 +1,34 @@
+""" Bundle dependency support """
+
+import Bcfg2.Client.Tools
+
+
+class BundleDeps(Bcfg2.Client.Tools.Tool):
+ """Bundle dependency helper for Bcfg2. It handles Bundle tags inside the
+ bundles that references the required other bundles that should change the
+ modification status if the referenced bundles is modified."""
+
+ name = 'Bundle'
+ __handles__ = [('Bundle', None)]
+ __req__ = {'Bundle': ['name']}
+
+ def InstallBundle(self, _):
+ """Simple no-op because we only need the BundleUpdated hook."""
+ return dict()
+
+ def VerifyBundle(self, entry, _): # pylint: disable=W0613
+ """Simple no-op because we only need the BundleUpdated hook."""
+ return True
+
+ def BundleUpdated(self, entry):
+ """This handles the dependencies on this bundle. It searches all
+ Bundle tags in other bundles that references the current bundle name
+ and marks those tags as modified to trigger the modification hook on
+ the other bundles."""
+
+ bundle_name = entry.get('name')
+ for bundle in self.config.findall('./Bundle/Bundle'):
+ if bundle.get('name') == bundle_name and \
+ bundle not in self.modified:
+ self.modified.append(bundle)
+ return dict()
diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py
index e07eef2fb..359d7ac73 100644
--- a/src/lib/Bcfg2/Client/__init__.py
+++ b/src/lib/Bcfg2/Client/__init__.py
@@ -774,29 +774,29 @@ class Client(object):
if not Bcfg2.Options.setup.interactive:
self.DispatchInstallCalls(clobbered)
- for bundle in self.config.findall('.//Bundle'):
+ all_bundles = self.config.findall('./Bundle')
+ mbundles.extend(self._get_all_modified_bundles(mbundles, all_bundles))
+
+ for bundle in all_bundles:
if (Bcfg2.Options.setup.only_bundles and
bundle.get('name') not in
Bcfg2.Options.setup.only_bundles):
# prune out unspecified bundles when running with -b
continue
if bundle in mbundles:
- self.logger.debug("Bundle %s was modified" %
- bundle.get('name'))
- func = "BundleUpdated"
- else:
- self.logger.debug("Bundle %s was not modified" %
- bundle.get('name'))
- func = "BundleNotUpdated"
+ continue
+
+ self.logger.debug("Bundle %s was not modified" %
+ bundle.get('name'))
for tool in self.tools:
try:
- self.states.update(getattr(tool, func)(bundle))
+ self.states.update(tool.BundleNotUpdated(bundle))
except KeyboardInterrupt:
raise
except: # pylint: disable=W0702
- self.logger.error("%s.%s(%s:%s) call failed:" %
- (tool.name, func, bundle.tag,
- bundle.get("name")), exc_info=1)
+ self.logger.error('%s.BundleNotUpdated(%s:%s) call failed:'
+ % (tool.name, bundle.tag,
+ bundle.get('name')), exc_info=1)
for indep in self.config.findall('.//Independent'):
for tool in self.tools:
@@ -809,6 +809,41 @@ class Client(object):
% (tool.name, indep.tag,
indep.get("name")), exc_info=1)
+ def _get_all_modified_bundles(self, mbundles, all_bundles):
+ """This gets all modified bundles by calling BundleUpdated until no
+ new bundles get added to the modification list."""
+ new_mbundles = mbundles
+ add_mbundles = []
+
+ while new_mbundles:
+ for bundle in self.config.findall('./Bundle'):
+ if (Bcfg2.Options.setup.only_bundles and
+ bundle.get('name') not in
+ Bcfg2.Options.setup.only_bundles):
+ # prune out unspecified bundles when running with -b
+ continue
+ if bundle not in new_mbundles:
+ continue
+
+ self.logger.debug('Bundle %s was modified' %
+ bundle.get('name'))
+ for tool in self.tools:
+ try:
+ self.states.update(tool.BundleUpdated(bundle))
+ except: # pylint: disable=W0702
+ self.logger.error('%s.BundleUpdated(%s:%s) call '
+ 'failed:' % (tool.name, bundle.tag,
+ bundle.get("name")),
+ exc_info=1)
+
+ mods = self.modified
+ new_mbundles = [struct for struct in all_bundles
+ if any(True for mod in mods if mod in struct)
+ and struct not in mbundles + add_mbundles]
+ add_mbundles.extend(new_mbundles)
+
+ return add_mbundles
+
def Remove(self):
"""Remove extra entries."""
for tool in self.tools:
diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py
index 41ee57b6d..6c35ada59 100644
--- a/src/lib/Bcfg2/Server/Plugins/Bundler.py
+++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py
@@ -8,6 +8,7 @@ import fnmatch
import lxml.etree
from Bcfg2.Server.Plugin import StructFile, Plugin, Structure, \
StructureValidator, XMLDirectoryBacked, Generator
+from Bcfg2.version import Bcfg2VersionInfo
from genshi.template import TemplateError
@@ -116,17 +117,36 @@ class Bundler(Plugin,
for el in child.getchildren():
data.append(el)
data.remove(child)
- elif child.get("name"):
+ else:
+ # no children -- wat
+ self.logger.warning("Bundler: Useless empty Bundle tag "
+ "in %s" % self.name)
+ data.remove(child)
+
+ for child in data.findall('RequiredBundle'):
+ if 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"))
+ if child.get('inherit_modification', 'false') == 'true':
+ if metadata.version_info >= \
+ Bcfg2VersionInfo('1.4.0pre2'):
+ lxml.etree.SubElement(data, 'Bundle',
+ name=child.get('name'))
+ else:
+ self.logger.warning(
+ 'Bundler: inherit_modification="true" is '
+ 'only supported for clients starting '
+ '1.4.0pre2')
data.remove(child)
else:
- # neither name or children -- wat
- self.logger.warning("Bundler: Useless empty Bundle tag "
- "in %s" % self.name)
+ # no name -- wat
+ self.logger.warning('Bundler: Missing required name in '
+ 'RequiredBundle tag in %s' %
+ self.name)
data.remove(child)
+
bundleset.append(data)
return bundleset
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py
index cfb379c40..1bf208c3e 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestBundler.py
@@ -3,6 +3,7 @@ import sys
import lxml.etree
from mock import Mock, MagicMock, patch
from Bcfg2.Server.Plugins.Bundler import *
+from Bcfg2.version import Bcfg2VersionInfo
# add all parent testsuite directories to sys.path to allow (most)
# relative imports in python 2.4
@@ -74,11 +75,15 @@ class TestBundler(TestPlugin, TestStructure, TestXMLDirectoryBacked):
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, "RequiredBundle", name="is_dep")
+ lxml.etree.SubElement(has_dep, "RequiredBundle", name="is_mod_dep",
+ inherit_modification="true")
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")
+ lxml.etree.SubElement(expected['has_dep'], "Bundle",
+ name="is_mod_dep")
is_dep = lxml.etree.Element("Bundle")
lxml.etree.SubElement(is_dep, "Package", name="bar")
@@ -94,6 +99,7 @@ class TestBundler(TestPlugin, TestStructure, TestXMLDirectoryBacked):
metadata = Mock()
metadata.bundles = ["error", "xinclude", "has_dep", "indep"]
+ metadata.version_info = Bcfg2VersionInfo('1.4.0')
rv = b.BuildStructures(metadata)
self.assertEqual(len(rv), 4)
@@ -109,3 +115,33 @@ class TestBundler(TestPlugin, TestStructure, TestXMLDirectoryBacked):
b.bundles['error'].XMLMatch.assert_called_with(metadata)
self.assertFalse(b.bundles['skip'].XMLMatch.called)
+
+ def test_BuildStructuresOldClient(self):
+ b = self.get_obj()
+ b.bundles = dict(has_dep=Mock())
+ expected = dict()
+
+ has_dep = lxml.etree.Element("Bundle")
+ lxml.etree.SubElement(has_dep, "RequiredBundle", name="is_dep")
+ lxml.etree.SubElement(has_dep, "RequiredBundle", name="is_mod_dep",
+ inherit_modification="true")
+ 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")
+
+ metadata = Mock()
+ metadata.bundles = ["has_dep"]
+ metadata.version_info = Bcfg2VersionInfo('1.3.0')
+
+ rv = b.BuildStructures(metadata)
+ self.assertEqual(len(rv), len(metadata.bundles))
+ 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)