summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-12-05 09:58:18 -0500
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-12-05 10:00:32 -0500
commit53f8eb67378f6a8054cb107e72b094f070d40c83 (patch)
tree5e3f1d5467d6e663ceca6b1f249bd33ccd7a9326
parent37b65a39545d7c5b64c2403a617a97d1d0f4a012 (diff)
downloadbcfg2-53f8eb67378f6a8054cb107e72b094f070d40c83.tar.gz
bcfg2-53f8eb67378f6a8054cb107e72b094f070d40c83.tar.bz2
bcfg2-53f8eb67378f6a8054cb107e72b094f070d40c83.zip
Tools: new Augeas driver
-rw-r--r--doc/client/tools/augeas.txt72
-rw-r--r--doc/server/plugins/generators/rules.txt14
-rw-r--r--schemas/augeas.xsd220
-rw-r--r--schemas/types.xsd13
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py211
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestAugeas.py233
-rw-r--r--testsuite/common.py53
7 files changed, 798 insertions, 18 deletions
diff --git a/doc/client/tools/augeas.txt b/doc/client/tools/augeas.txt
new file mode 100644
index 000000000..94ed9066f
--- /dev/null
+++ b/doc/client/tools/augeas.txt
@@ -0,0 +1,72 @@
+.. -*- mode: rst -*-
+
+.. _client-tools-augeas:
+
+========
+ Augeas
+========
+
+The Augeas tool provides a way to use `Augeas
+<http://www.augeas.net>`_ to edit files that may not be completely
+managed.
+
+In the simplest case, you simply tell Augeas which path to edit, and
+give it a sequence of commands:
+
+.. code-block:: xml
+
+ <Path type="augeas" name="/etc/hosts" owner="root" group="root"
+ mode="0644">
+ <Set path="01/ipaddr" value="192.168.0.1"/>
+ <Set path="01/canonical" value="pigiron.example.com"/>
+ <Set path="01/alias[1]" value="pigiron"/>
+ <Set path="01/alias[2]" value="piggy"/>
+ </Path>
+
+The commands are run in document order. There's no need to do an
+explicit ``save`` at the end.
+
+Each of these commands will only be run if the path does not already
+have the given setting. That is, the ip address for the first host
+record will only be set to ``192.168.0.1`` if it's not set to that
+value already. Its canonical name will only be set to
+``pigiron.example.com`` if it's not that already; and so on.
+
+The Augeas paths are all relative to ``/files/etc/hosts``.
+
+The Augeas tool understands a subset of ``augtool`` commands. Valid
+tags are: ``Remove``, ``Move``, ``Set``, ``Clear``, ``SetMulti``, and
+``Insert``. Refer to the official Augeas docs or the `Schema`_ below
+for details on the commands.
+
+Editing files outside the default load path
+===========================================
+
+If you're using Augeas to edit files outside of its default load path,
+you must manually specify the lens. For instance:
+
+.. code-block:: xml
+
+ <Path type="augeas" name="/opt/jenkins/home/config.xml" lens="Xml"
+ owner="jenkins" group="jenkins" mode="0640">
+ <Set path="hudson/systemMessage/#text"
+ value="This is a Jenkins server."/>
+ </Path>
+
+Note that there's no need to manually modify the load path by setting
+``/augeas/load/<lens>/incl``, nor do you have to call ``load``
+explicitly.
+
+Schema
+======
+
+.. xml:group:: augeasCommands
+
+
+Performance
+===========
+
+The Augeas tool is quite slow to initialize. For each ``<Path
+type="augeas" ... >`` entry you have, it creates a new Augeas object
+internally, which can take several seconds. It's thus important to
+use this tool sparingly.
diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt
index 9ba70238d..a21dd217f 100644
--- a/doc/server/plugins/generators/rules.txt
+++ b/doc/server/plugins/generators/rules.txt
@@ -136,6 +136,20 @@ Attributes common to all Path tags:
:onlyattrs: name,type
+augeas
+^^^^^^
+
+Run `Augeas <http://www.augeas.net>`_ commands. See
+:ref:`client-tools-augeas` for more details.
+
+.. xml:type:: PathType
+ :nochildren:
+ :noattributegroups:
+ :nodoc:
+ :notext:
+ :onlyattrs: owner,group,mode,secontext,lens
+ :requiredattrs: owner,group,mode
+
device
^^^^^^
diff --git a/schemas/augeas.xsd b/schemas/augeas.xsd
new file mode 100644
index 000000000..0ede106f3
--- /dev/null
+++ b/schemas/augeas.xsd
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:py="http://genshi.edgewall.org/" xml:lang="en">
+
+ <xsd:annotation>
+ <xsd:documentation>
+ Augeas commands
+ </xsd:documentation>
+ </xsd:annotation>
+
+ <xsd:import namespace="http://genshi.edgewall.org/"
+ schemaLocation="genshi.xsd"/>
+
+ <xsd:complexType name="AugeasRemoveCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``rm`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute type="xsd:string" name="path" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ Delete nodes (and all children) matching the given Augeas
+ path expression.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="AugeasMoveCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``mv`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute type="xsd:string" name="source" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ Move the node matching this path expression. ``source``
+ must match exactly one node.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type="xsd:string" name="destination" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ Move the node to this location. ``destination`` must match
+ either zero or one nodes.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="AugeasSetCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``set`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute type="xsd:string" name="path" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ Path to set the value for. If the path does not exist, it
+ and all of its ancestors will be created.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type="xsd:string" name="value" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ Value to set.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="AugeasClearCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``clear`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute type="xsd:string" name="path" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ Path whose value will be set to ``NULL``. If the path does
+ not exist, it and all of its ancestors will be created.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="AugeasSetMultiCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Set multiple node values at once.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute type="xsd:string" name="base" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ The base path.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type="xsd:string" name="sub" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ ``sub`` will be used as an expression relative to each node
+ that matches the :xml:attribute:`AugeasSetMultiCommand:base`
+ expression.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type="xsd:string" name="value" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ The value to set on all nodes that match
+ :xml:attribute:`AugeasSetMultiCommand:sub` relative to each
+ node matching :xml:attribute:`AugeasSetMultiCommand:base`.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:simpleType name="AugeasWhenEnum">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="before"/>
+ <xsd:enumeration value="after"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:complexType name="AugeasInsertCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``ins`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute type="xsd:string" name="path" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ The path to a node that will be the sibling of the new node.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type="xsd:string" name="label" use="required">
+ <xsd:annotation>
+ <xsd:documentation>
+ The label of the new node to be created.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type="AugeasWhenEnum" name="where" default="before">
+ <xsd:annotation>
+ <xsd:documentation>
+ Where to create the node: ``before`` or ``after`` the
+ sibling given in :xml:attribute:`AugeasInsertCommand:path`.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:group name="augeasCommands">
+ <xsd:annotation>
+ <xsd:documentation>
+ All available Augeas commands.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:choice>
+ <xsd:element name="Remove" type="AugeasRemoveCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``rm`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="Move" type="AugeasMoveCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``mv`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="Set" type="AugeasSetCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``set`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="Clear" type="AugeasClearCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``clear`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="SetMulti" type="AugeasSetMultiCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Set multiple node values at once.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name="Insert" type="AugeasInsertCommand">
+ <xsd:annotation>
+ <xsd:documentation>
+ Implementation of the Augeas ``ins`` command.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:group>
+</xsd:schema>
diff --git a/schemas/types.xsd b/schemas/types.xsd
index 03d2e9ddc..ee4f13ea9 100644
--- a/schemas/types.xsd
+++ b/schemas/types.xsd
@@ -9,6 +9,7 @@
</xsd:annotation>
<xsd:include schemaLocation="selinux.xsd"/>
+ <xsd:include schemaLocation="augeas.xsd"/>
<xsd:import namespace="http://genshi.edgewall.org/"
schemaLocation="genshi.xsd"/>
@@ -41,6 +42,7 @@
<xsd:simpleType name='PathTypeEnum'>
<xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='augeas' />
<xsd:enumeration value='device' />
<xsd:enumeration value='directory' />
<xsd:enumeration value='file' />
@@ -133,7 +135,7 @@
<xsd:annotation>
<xsd:documentation>
If the action is always run, or is only run when a bundle
- has been modified.
+ has been modified.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -267,6 +269,7 @@
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
<xsd:element name='ACL' type='ACLType'/>
+ <xsd:group name="augeasCommands"/>
</xsd:choice>
<xsd:attribute type="PathTypeEnum" name="type">
<xsd:annotation>
@@ -394,6 +397,14 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:token" name="lens">
+ <xsd:annotation>
+ <xsd:documentation>
+ The Augeas lens to use when editing files in a non-standard
+ (according to Augeas) location.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py
new file mode 100644
index 000000000..cda9a1e3b
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py
@@ -0,0 +1,211 @@
+""" Augeas driver """
+
+import sys
+import augeas
+import Bcfg2.Client.XML
+from Bcfg2.Client.Tools.POSIX.base import POSIXTool
+
+
+class AugeasCommand(object):
+ def __init__(self, command, augeas, logger):
+ self.augeas = augeas
+ self.command = command
+ self.entry = self.command.getparent()
+ self.logger = logger
+
+ def get_path(self, attr="path"):
+ return "/files/%s/%s" % (self.entry.get("name").strip("/"),
+ self.command.get(attr).lstrip("/"))
+
+ def _exists(self, path):
+ return len(self.augeas.match(path)) > 1
+
+ def _verify_exists(self, path=None):
+ if path is None:
+ path = self.get_path()
+ self.logger.debug("Augeas: Verifying that '%s' exists" % path)
+ return self._exists(path)
+
+ def _verify_not_exists(self, path=None):
+ if path is None:
+ path = self.get_path()
+ self.logger.debug("Augeas: Verifying that '%s' does not exist" % path)
+ return not self._exists(path)
+
+ def _verify_set(self, expected, path=None):
+ if path is None:
+ path = self.get_path()
+ self.logger.debug("Augeas: Verifying '%s' == '%s'" % (path, expected))
+ actual = self.augeas.get(path)
+ if actual == expected:
+ return True
+ else:
+ self.logger.debug("Augeas: '%s' failed verification: '%s' != '%s'"
+ % (path, actual, expected))
+ return False
+
+ def __str__(self):
+ return Bcfg2.Client.XML.tostring(self.command)
+
+
+class Remove(AugeasCommand):
+ def verify(self):
+ return self._verify_not_exists()
+
+ def install(self):
+ self.logger.debug("Augeas: Removing %s" % self.get_path())
+ return self.augeas.remove(self.get_path())
+
+
+class Move(AugeasCommand):
+ def __init__(self, command, augeas, logger):
+ AugeasCommand.__init__(self, command, augeas, logger)
+ self.source = self.get_path("source")
+ self.dest = self.get_path("destination")
+
+ def verify(self):
+ return (self._verify_not_exists(self.source),
+ self._verify_exists(self.dest))
+
+ def install(self):
+ self.logger.debug("Augeas: Moving %s to %s" % (self.source, self.dest))
+ return self.augeas.move(self.source, self.dest)
+
+
+class Set(AugeasCommand):
+ def __init__(self, command, augeas, logger):
+ AugeasCommand.__init__(self, command, augeas, logger)
+ self.value = self.command.get("value")
+
+ def verify(self):
+ return self._verify_set(self.value)
+
+ def install(self):
+ self.logger.debug("Augeas: Setting %s to %s" % (self.get_path(),
+ self.value))
+ return self.augeas.set(self.get_path(), self.value)
+
+
+class Clear(Set):
+ def __init__(self, command, augeas, logger):
+ Set.__init__(self, command, augeas, logger)
+ self.value = None
+
+
+class SetMulti(AugeasCommand):
+ def __init__(self, command, augeas, logger):
+ AugeasCommand.__init__(self, command, augeas, logger)
+ self.sub = self.command.get("sub")
+ self.value = self.command.get("value")
+ self.base = self.get_path("base")
+
+ def verify(self):
+ return all(self._verify_set(self.value,
+ path="%s/%s" % (path, self.sub))
+ for path in self.augeas.match(self.base))
+
+ def install(self):
+ return self.augeas.setm(self.base, self.sub, self.value)
+
+
+class Insert(AugeasCommand):
+ def __init__(self, command, augeas, logger):
+ AugeasCommand.__init__(self, command, augeas, logger)
+ self.label = self.command.get("label")
+ self.where = self.command.get("where", "before")
+ self.before = self.where == "before"
+
+ def verify(self):
+ return self._verify_exists("%s/../%s" % (self.get_path(), self.label))
+
+ def install(self):
+ self.logger.debug("Augeas: Inserting new %s %s %s" %
+ (self.label, self.where, self.get_path()))
+ return self.augeas.insert(self.get_path(), self.label, self.before)
+
+
+class POSIXAugeas(POSIXTool):
+ """ Handle <Path type='augeas'...> entries. See
+ :ref:`client-tools-augeas`. """
+
+ __handles__ = [('Path', 'augeas')]
+ __req__ = {'Path': ['type', 'name', 'setting', 'value']}
+
+ def __init__(self, logger, setup, config):
+ POSIXTool.__init__(self, logger, setup, config)
+ self._augeas = dict()
+
+ def get_augeas(self, entry):
+ if entry.get("name") not in self._augeas:
+ aug = augeas.augeas()
+ if entry.get("lens"):
+ self.logger.debug("Augeas: Adding %s to include path for %s" %
+ (entry.get("name"), entry.get("lens")))
+ incl = "/augeas/load/%s/incl" % entry.get("lens")
+ ilen = len(aug.match(incl))
+ if ilen == 0:
+ self.logger.error("Augeas: Lens %s does not exist" %
+ entry.get("lens"))
+ else:
+ aug.set("%s[%s]" % (incl, ilen + 1), entry.get("name"))
+ aug.load()
+ self._augeas[entry.get("name")] = aug
+ return self._augeas[entry.get("name")]
+
+ def fully_specified(self, entry):
+ return entry.text is not None
+
+ def get_commands(self, entry, unverified=False):
+ rv = []
+ for cmd in entry.iterchildren():
+ if cmd.tag in globals():
+ rv.append(globals()[cmd.tag](cmd, self.get_augeas(entry),
+ self.logger))
+ else:
+ err = "Augeas: Unknown command %s in %s" % (cmd.tag,
+ entry.get("name"))
+ self.logger.error(err)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), err]))
+ return rv
+
+ def verify(self, entry, modlist):
+ rv = True
+ for cmd in self.get_commands(entry):
+ try:
+ if not cmd.verify():
+ err = "Augeas: Command has not been applied to %s: %s" % \
+ (entry.get("name"), cmd)
+ self.logger.debug(err)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''),
+ err]))
+ rv = False
+ cmd.command.set("verified", "false")
+ else:
+ cmd.command.set("verified", "true")
+ except: # pylint: disable=W0702
+ err = "Augeas: Unexpected error verifying %s: %s: %s" % \
+ (entry.get("name"), cmd, sys.exc_info()[1])
+ self.logger.error(err)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), err]))
+ rv = False
+ cmd.command.set("verified", "false")
+ return POSIXTool.verify(self, entry, modlist) and rv
+
+ def install(self, entry):
+ rv = True
+ for cmd in self.get_commands(entry, unverified=True):
+ try:
+ cmd.install()
+ except: # pylint: disable=W0702
+ self.logger.error(
+ "Failure running Augeas command on %s: %s: %s" %
+ (entry.get("name"), cmd, sys.exc_info()[1]))
+ rv = False
+ try:
+ self.get_augeas(entry).save()
+ except: # pylint: disable=W0702
+ self.logger.error(
+ "Failure saving Augeas changes to %s: %s" %
+ (entry.get("name"), sys.exc_info()[1]))
+ rv = False
+ return POSIXTool.install(self, entry) and rv
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestAugeas.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestAugeas.py
new file mode 100644
index 000000000..bfcb8a378
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestAugeas.py
@@ -0,0 +1,233 @@
+# -*- coding: utf-8 -*-
+import os
+import sys
+import copy
+import lxml.etree
+import tempfile
+from mock import Mock, MagicMock, patch
+try:
+ from Bcfg2.Client.Tools.POSIX.Augeas import *
+ HAS_AUGEAS = True
+except ImportError:
+ HAS_AUGEAS = False
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from TestPOSIX.Testbase import TestPOSIXTool
+from common import *
+
+
+test_data = """<Test>
+ <Empty/>
+ <Text>content with spaces</Text>
+ <Attrs foo="foo" bar="bar"/>
+ <Children identical="false">
+ <Foo/>
+ <Bar attr="attr"/>
+ </Children>
+ <Children identical="true">
+ <Thing>one</Thing>
+ <Thing>two</Thing>
+ </Children>
+ <Children multi="true">
+ <Thing>same</Thing>
+ <Thing>same</Thing>
+ <Thing>same</Thing>
+ <Thing>same</Thing>
+ </Children>
+</Test>
+"""
+
+test_xdata = lxml.etree.XML(test_data)
+
+class TestPOSIXAugeas(TestPOSIXTool):
+ test_obj = POSIXAugeas
+
+ applied_commands = dict(
+ insert=lxml.etree.Element(
+ "Insert", label="Thing",
+ path='Test/Children[#attribute/identical = "true"]/Thing'),
+ set=lxml.etree.Element("Set", path="Test/Text/#text",
+ value="content with spaces"),
+ move=lxml.etree.Element(
+ "Move", source="Test/Foo",
+ destination='Test/Children[#attribute/identical = "false"]/Foo'),
+ remove=lxml.etree.Element("Remove", path="Test/Bar"),
+ clear=lxml.etree.Element("Clear", path="Test/Empty/#text"),
+ setm=lxml.etree.Element(
+ "SetMulti", sub="#text", value="same",
+ base='Test/Children[#attribute/multi = "true"]/Thing'))
+
+
+ @skipUnless(HAS_AUGEAS, "Python Augeas libraries not found")
+ def setUp(self):
+ fd, self.tmpfile = tempfile.mkstemp()
+ os.fdopen(fd, 'w').write(test_data)
+
+ def tearDown(self):
+ tmpfile = getattr(self, "tmpfile", None)
+ if tmpfile:
+ os.unlink(tmpfile)
+
+ def test_fully_specified(self):
+ ptool = self.get_obj()
+
+ entry = lxml.etree.Element("Path", name="/test", type="augeas")
+ self.assertFalse(ptool.fully_specified(entry))
+
+ entry.text = "text"
+ self.assertTrue(ptool.fully_specified(entry))
+
+ def test_install(self):
+ # this is tested adequately by the other tests
+ pass
+
+ def test_verify(self):
+ # this is tested adequately by the other tests
+ pass
+
+ @patch("Bcfg2.Client.Tools.POSIX.Augeas.POSIXTool.verify")
+ def _verify(self, commands, mock_verify):
+ ptool = self.get_obj()
+ mock_verify.return_value = True
+
+ entry = lxml.etree.Element("Path", name=self.tmpfile, type="augeas",
+ lens="Xml")
+ entry.extend(commands)
+
+ modlist = []
+ self.assertTrue(ptool.verify(entry, modlist))
+ mock_verify.assert_called_with(ptool, entry, modlist)
+ self.assertXMLEqual(lxml.etree.parse(self.tmpfile).getroot(),
+ test_xdata)
+
+ def test_verify_insert(self):
+ """ Test successfully verifying an Insert command """
+ self._verify([self.applied_commands['insert']])
+
+ def test_verify_set(self):
+ """ Test successfully verifying a Set command """
+ self._verify([self.applied_commands['set']])
+
+ def test_verify_move(self):
+ """ Test successfully verifying a Move command """
+ self._verify([self.applied_commands['move']])
+
+ def test_verify_remove(self):
+ """ Test successfully verifying a Remove command """
+ self._verify([self.applied_commands['remove']])
+
+ def test_verify_clear(self):
+ """ Test successfully verifying a Clear command """
+ self._verify([self.applied_commands['clear']])
+
+ def test_verify_set_multi(self):
+ """ Test successfully verifying a SetMulti command """
+ self._verify([self.applied_commands['setm']])
+
+ def test_verify_all(self):
+ """ Test successfully verifying multiple commands """
+ self._verify(self.applied_commands.values())
+
+ @patch("Bcfg2.Client.Tools.POSIX.Augeas.POSIXTool.install")
+ def _install(self, commands, expected, mock_install):
+ ptool = self.get_obj()
+ mock_install.return_value = True
+
+ entry = lxml.etree.Element("Path", name=self.tmpfile, type="augeas",
+ lens="Xml")
+ entry.extend(commands)
+
+ self.assertTrue(ptool.install(entry))
+ mock_install.assert_called_with(ptool, entry)
+ self.assertXMLEqual(lxml.etree.parse(self.tmpfile).getroot(),
+ expected)
+
+ def test_install_set_existing(self):
+ """ Test setting the value of an existing node """
+ expected = copy.deepcopy(test_xdata)
+ expected.find("Text").text = "Changed content"
+ self._install([lxml.etree.Element("Set", path="Test/Text/#text",
+ value="Changed content")],
+ expected)
+
+ def test_install_set_new(self):
+ """ Test setting the value of an new node """
+ expected = copy.deepcopy(test_xdata)
+ newtext = lxml.etree.SubElement(expected, "NewText")
+ newtext.text = "new content"
+ self._install([lxml.etree.Element("Set", path="Test/NewText/#text",
+ value="new content")],
+ expected)
+
+ def test_install_remove(self):
+ """ Test removing a node """
+ expected = copy.deepcopy(test_xdata)
+ expected.remove(expected.find("Attrs"))
+ self._install(
+ [lxml.etree.Element("Remove",
+ path='Test/*[#attribute/foo = "foo"]')],
+ expected)
+
+ def test_install_move(self):
+ """ Test moving a node """
+ expected = copy.deepcopy(test_xdata)
+ foo = expected.xpath("//Foo")[0]
+ expected.append(foo)
+ self._install(
+ [lxml.etree.Element("Move", source='Test/Children/Foo',
+ destination='Test/Foo')],
+ expected)
+
+ def test_install_clear(self):
+ """ Test clearing a node """
+ # TODO: clearing a node doesn't seem to work with the XML lens
+ #
+ # % augtool -b
+ # augtool> set /augeas/load/Xml/incl[3] "/tmp/test.xml"
+ # augtool> load
+ # augtool> clear '/files/tmp/test.xml/Test/Text/#text'
+ # augtool> save
+ # error: Failed to execute command
+ # saving failed (run 'print /augeas//error' for details)
+ # augtool> print /augeas//error
+ #
+ # The error isn't useful.
+ pass
+
+ def test_install_set_multi(self):
+ """ Test setting multiple nodes at once """
+ expected = copy.deepcopy(test_xdata)
+ for thing in expected.xpath("Children[@identical='true']/Thing"):
+ thing.text = "same"
+ self._install(
+ [lxml.etree.Element(
+ "SetMulti", value="same",
+ base='Test/Children[#attribute/identical = "true"]',
+ sub="Thing/#text")],
+ expected)
+
+ def test_install_insert(self):
+ """ Test inserting a node """
+ expected = copy.deepcopy(test_xdata)
+ children = expected.xpath("Children[@identical='true']")[0]
+ thing = lxml.etree.Element("Thing")
+ thing.text = "three"
+ children.append(thing)
+ self._install(
+ [lxml.etree.Element(
+ "Insert",
+ path='Test/Children[#attribute/identical = "true"]/Thing[2]',
+ label="Thing", where="after"),
+ lxml.etree.Element(
+ "Set",
+ path='Test/Children[#attribute/identical = "true"]/Thing[3]/#text',
+ value="three")],
+ expected)
diff --git a/testsuite/common.py b/testsuite/common.py
index e26d0be61..536b11cbd 100644
--- a/testsuite/common.py
+++ b/testsuite/common.py
@@ -13,6 +13,7 @@ import re
import sys
import codecs
import unittest
+import lxml.etree
from mock import patch, MagicMock, _patch, DEFAULT
from Bcfg2.Compat import wraps
@@ -262,24 +263,43 @@ class Bcfg2TestCase(unittest.TestCase):
"%s is not less than or equal to %s")
def assertXMLEqual(self, el1, el2, msg=None):
- """ Test that the two XML trees given are equal. Both
- elements and all children are expected to have ``name``
- attributes. """
- self.assertEqual(el1.tag, el2.tag, msg=msg)
- self.assertEqual(el1.text, el2.text, msg=msg)
- self.assertItemsEqual(el1.attrib.items(), el2.attrib.items(), msg=msg)
+ """ Test that the two XML trees given are equal. """
+ if msg is None:
+ msg = "XML trees are not equal: %s"
+ else:
+ msg += ": %s"
+ fullmsg = msg + "\nFirst: %s" % lxml.etree.tostring(el1) + \
+ "\nSecond: %s" % lxml.etree.tostring(el2)
+
+ self.assertEqual(el1.tag, el2.tag, msg=fullmsg % "Tags differ")
+ if el1.text is not None and el2.text is not None:
+ self.assertEqual(el1.text.strip(), el2.text.strip(),
+ msg=fullmsg % "Text content differs")
+ else:
+ self.assertEqual(el1.text, el2.text,
+ msg=fullmsg % "Text content differs")
+ self.assertItemsEqual(el1.attrib.items(), el2.attrib.items(),
+ msg=fullmsg % "Attributes differ")
self.assertEqual(len(el1.getchildren()),
- len(el2.getchildren()))
+ len(el2.getchildren()),
+ msg=fullmsg % "Different numbers of children")
+ matched = []
for child1 in el1.getchildren():
- cname = child1.get("name")
- self.assertIsNotNone(cname,
- msg="Element %s has no 'name' attribute" %
- child1.tag)
- children2 = el2.xpath("%s[@name='%s']" % (child1.tag, cname))
- self.assertEqual(len(children2), 1,
- msg="More than one %s element named %s" % \
- (child1.tag, cname))
- self.assertXMLEqual(child1, children2[0], msg=msg)
+ for child2 in el2.xpath(child1.tag):
+ if child2 in matched:
+ continue
+ try:
+ self.assertXMLEqual(child1, child2)
+ matched.append(child2)
+ break
+ except AssertionError:
+ continue
+ else:
+ assert False, \
+ fullmsg % ("Element %s is missing from second" %
+ lxml.etree.tostring(child1))
+ self.assertItemsEqual(el2.getchildren(), matched,
+ msg=fullmsg % "Second has extra element(s)")
class DBModelTestCase(Bcfg2TestCase):
@@ -394,4 +414,3 @@ try:
re_type = re._pattern_type
except AttributeError:
re_type = type(re.compile(""))
-