summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-12-09 09:38:04 -0500
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-12-09 09:38:04 -0500
commit7497f20a4821515fc9c8dadf85d3c4f3b47245eb (patch)
treebe129aa775852ed70bac6be82af719b9bfc7901f
parenteff366a0c3b9ba87f3ee06f90dccdd242579b7b1 (diff)
parentbf2ee31f956447fa42ae85dc69820405eda8c490 (diff)
downloadbcfg2-7497f20a4821515fc9c8dadf85d3c4f3b47245eb.tar.gz
bcfg2-7497f20a4821515fc9c8dadf85d3c4f3b47245eb.tar.bz2
bcfg2-7497f20a4821515fc9c8dadf85d3c4f3b47245eb.zip
Merge branch 'maint'
Conflicts: doc/appendix/guides/fedora.txt misc/bcfg2.spec schemas/types.xsd src/lib/Bcfg2/Encryption.py src/lib/Bcfg2/Options.py src/lib/Bcfg2/Server/Admin/Client.py src/lib/Bcfg2/Server/Core.py src/lib/Bcfg2/Server/Lint/Validate.py src/lib/Bcfg2/Server/Plugin/helpers.py src/lib/Bcfg2/Server/Plugins/Bundler.py src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py src/lib/Bcfg2/Server/Plugins/Probes.py src/sbin/bcfg2-crypt testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedGenerator.py testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py testsuite/common.py testsuite/install.sh
-rw-r--r--.travis.yml1
-rw-r--r--doc/appendix/guides/centos.txt9
-rw-r--r--doc/appendix/guides/ubuntu.txt9
-rw-r--r--doc/client/tools/augeas.txt72
-rw-r--r--doc/installation/prerequisites.txt21
-rw-r--r--doc/server/plugins/generators/rules.txt14
-rw-r--r--doc/server/plugins/probes/index.txt42
-rw-r--r--doc/unsorted/bcfg2.conf-options.txt19
-rw-r--r--doc/unsorted/dynamic_groups.txt27
-rw-r--r--doc/unsorted/howtos.txt2
-rw-r--r--misc/bcfg2.spec3
-rw-r--r--redhat/systemd/bcfg2.service2
-rw-r--r--schemas/augeas.xsd220
-rw-r--r--schemas/types.xsd12
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py284
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/__init__.py7
-rw-r--r--src/lib/Bcfg2/DBSettings.py2
-rw-r--r--src/lib/Bcfg2/Options/Types.py9
-rw-r--r--src/lib/Bcfg2/Reporting/Compat.py6
-rw-r--r--src/lib/Bcfg2/Reporting/urls.py2
-rwxr-xr-xsrc/lib/Bcfg2/Reporting/utils.py1
-rw-r--r--src/lib/Bcfg2/Server/Admin.py65
-rw-r--r--src/lib/Bcfg2/Server/Core.py8
-rwxr-xr-xsrc/lib/Bcfg2/Server/Encryption.py13
-rw-r--r--src/lib/Bcfg2/Server/Lint/TemplateAbuse.py76
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py15
-rw-r--r--src/lib/Bcfg2/Server/Lint/ValidateJSON.py70
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py21
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Metadata.py5
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Probes.py25
-rwxr-xr-xsrc/sbin/bcfg2-crypt8
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestAugeas.py250
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py4
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py4
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedGenerator.py1
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py28
-rwxr-xr-xtestsuite/before_install.sh5
-rw-r--r--testsuite/common.py8
-rw-r--r--testsuite/ext/exception_messages.py30
-rwxr-xr-xtestsuite/install.sh20
-rwxr-xr-xtools/bcfg2-cron2
41 files changed, 1270 insertions, 152 deletions
diff --git a/.travis.yml b/.travis.yml
index 73b8a9594..9ad7dfb19 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,5 @@
language: python
python:
- - "2.5"
- "2.6"
- "2.7"
env:
diff --git a/doc/appendix/guides/centos.txt b/doc/appendix/guides/centos.txt
index 3a35627a8..af097fbac 100644
--- a/doc/appendix/guides/centos.txt
+++ b/doc/appendix/guides/centos.txt
@@ -230,10 +230,11 @@ should look something like this
When editing your xml files by hand, it is useful to occasionally run
`bcfg2-lint` to ensure that your xml validates properly.
-The final thing we need is for the client to have the proper
-arch group membership. For this, we will make use of the
-:ref:`unsorted-dynamic_groups` capabilities of the Probes plugin. Add
-Probes to your plugins line in ``bcfg2.conf`` and create the Probe.::
+The final thing we need is for the client to have the proper arch
+group membership. For this, we will make use of the
+:ref:`server-plugins-probes-dynamic-groups` capabilities of the Probes
+plugin. Add Probes to your plugins line in ``bcfg2.conf`` and create
+the Probe.::
[root@centos ~]# grep plugins /etc/bcfg2.conf
plugins = Bundler,Cfg,...,Probes
diff --git a/doc/appendix/guides/ubuntu.txt b/doc/appendix/guides/ubuntu.txt
index 60f8e3a41..abb894465 100644
--- a/doc/appendix/guides/ubuntu.txt
+++ b/doc/appendix/guides/ubuntu.txt
@@ -253,10 +253,11 @@ that our client is able to obtain these sources.
When editing your xml files by hand, it is useful to occasionally run
``bcfg2-lint -v`` to ensure that your xml validates properly.
-The last thing we need is for the client to have the proper
-arch group membership. For this, we will make use of the
-:ref:`unsorted-dynamic_groups` capabilities of the Probes plugin. Add
-Probes to your plugins line in ``bcfg2.conf`` and create the Probe.
+The last thing we need is for the client to have the proper arch group
+membership. For this, we will make use of the
+:ref:`server-plugins-probes-dynamic-groups` capabilities of the Probes
+plugin. Add Probes to your plugins line in ``bcfg2.conf`` and create
+the Probe.
.. code-block:: sh
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/installation/prerequisites.txt b/doc/installation/prerequisites.txt
index 81ac12632..e3434edd3 100644
--- a/doc/installation/prerequisites.txt
+++ b/doc/installation/prerequisites.txt
@@ -21,7 +21,7 @@ Bcfg2 Client
+----------------------------+------------------------+--------------------------------+
| libxslt (if lxml is used) | Any | libxml2 |
+----------------------------+------------------------+--------------------------------+
-| python | 2.4 and greater [#f1] | |
+| python | 2.4 and greater [#f1]_ | |
+----------------------------+------------------------+--------------------------------+
| lxml or elementtree [#f2]_ | Any | lxml: libxml2, libxslt, python |
+----------------------------+------------------------+--------------------------------+
@@ -54,10 +54,21 @@ Bcfg2 Server
+-------------------------------+----------+--------------------------------+
| python-gamin or pyinotify | Any | gamin or inotify, python |
+-------------------------------+----------+--------------------------------+
-| M2crypto or python-ssl (note | Any | python, openssl |
-| that the ssl module is | | |
-| included in python versions | | |
-| 2.6 and later | | |
+| python-ssl (note | Any | python, backported ssl module |
++-------------------------------+----------+--------------------------------+
+
+Bcfg2 Reporting
+---------------
+
+A webserver capabable of running wsgi applications is required for web
+reporting, such as Apache + mod_wsgi or nginx.
+
++-------------------------------+----------+--------------------------------+
+| Software | Version | Requires |
++===============================+==========+================================+
+| django | 1.2.0+ | |
++-------------------------------+----------+--------------------------------+
+| south | 0.7.0+ | |
+-------------------------------+----------+--------------------------------+
Bcfg2 Reporting
diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt
index 64dbc8597..c5ff699a7 100644
--- a/doc/server/plugins/generators/rules.txt
+++ b/doc/server/plugins/generators/rules.txt
@@ -118,6 +118,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/doc/server/plugins/probes/index.txt b/doc/server/plugins/probes/index.txt
index 306a752b6..2e23c31d5 100644
--- a/doc/server/plugins/probes/index.txt
+++ b/doc/server/plugins/probes/index.txt
@@ -13,6 +13,9 @@ the system disk, you would want to know this information to correctly
generate an `/etc/auto.master` autofs config file for each type. Here
we will look at how to do this.
+Probes also allow dynamic group assignment for clients, see
+:ref:`_server-plugins-probes-dynamic-groups`.
+
First, create a ``Probes`` directory in our toplevel repository
location::
@@ -119,6 +122,45 @@ is to add the ``/etc/auto.master`` to a Bundle:
<Path name='/etc/auto.master'/>
+.. _server-plugins-probes-dynamic-groups:
+
+Dynamic Group Assignment
+========================
+
+The output lines of the probe matching "group:" are used to
+dynamically assign hosts to groups. These dynamic groups need not already
+exist in ``Metadata/groups.xml``. If a dynamic group is defined in
+``Metadata/groups.xml``, clients that include this group will also get
+all included groups and bundles.
+
+Consider the following output of a probe::
+
+ group:debian-wheezy
+ group:amd64
+
+This assigns the client to the groups debian-wheezy and amd64.
+
+To prevent clients from manipulating the probe output and choosing
+unexpected groups (and receiving their potential sensitive files) you
+can use the ``allowed_groups`` option in the ``[probes]`` section of
+``bcfg2.conf`` on the server. This whitespace-separated list of
+anchored regular expressions (must match the complete group name)
+controls dynamic group assignments. Only matching groups are
+allowed. The default allows all groups.
+
+.. versionadded:: 1.3.4
+
+Example:
+
+.. code-block:: ini
+
+ [probes]
+ allowed_groups = debian-(squeeze|wheezy|sid) i386
+
+This allows the groups `debian-squeeze`, `debian-wheezy`, `debian-sid`
+and `i386`. With the probe output from above, this setting would
+disallow the group `amd64`.
+
Handling Probe Output
=====================
diff --git a/doc/unsorted/bcfg2.conf-options.txt b/doc/unsorted/bcfg2.conf-options.txt
deleted file mode 100644
index 57e26cbd2..000000000
--- a/doc/unsorted/bcfg2.conf-options.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-.. -*- mode: rst -*-
-
-.. _unsorted-bcfg2.conf-options:
-
-==========
-bcfg2.conf
-==========
-
-This page documents the various options available in bcfg2.conf. The
-various sections correspond to the sections in the file itself.
-
-components
-==========
-
-logging
--------
-
-Specify an alternate path for the lockfile used by the bcfg2 client.
-Default value is ``/var/lock/bcfg2.run``
diff --git a/doc/unsorted/dynamic_groups.txt b/doc/unsorted/dynamic_groups.txt
deleted file mode 100644
index 11535dc8b..000000000
--- a/doc/unsorted/dynamic_groups.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-.. -*- mode: rst -*-
-
-.. _unsorted-dynamic_groups:
-
-==============
-Dynamic Groups
-==============
-
-Bcfg2 supports the use of dynamic groups. These groups are not included
-in a client's profile group, but instead are derived from the results
-of probes executed on the client. These dynamic groups need not already
-exist in ``Metadata/groups.xml``. If a dynamic group is defined in
-``Metadata/groups.xml``, clients that include this group will also get
-all included groups and bundles.
-
-Setting up dynamic groups
-=========================
-
-In order to define a dynamic group, setup a probe that outputs the text
-based on system properties::
-
- group:groupname
-
-This output is processed by the Bcfg2 server, and results in dynamic
-group membership in groupname for the client. See the :ref:`Probes
-<server-plugins-probes-index>` page for a more thorough description
-of probes.
diff --git a/doc/unsorted/howtos.txt b/doc/unsorted/howtos.txt
index 0c5b482d9..cef64a394 100644
--- a/doc/unsorted/howtos.txt
+++ b/doc/unsorted/howtos.txt
@@ -14,5 +14,5 @@ Here are several howtos that describe different aspects of Bcfg2 deployment
* :ref:`appendix-guides-gentoo` - Issues specific to running Bcfg2 on Gentoo
* :ref:`server-plugins-probes-index` - How to use Probes to gather information from a client machine.
* :ref:`client-tools-actions` - How to use Actions
-* :ref:`unsorted-dynamic_groups` - Using dynamic groups
+* :ref:`server-plugins-probes-dynamic-groups` - Using dynamic groups
* :ref:`client-modes-paranoid` - How to run an update in paranoid mode
diff --git a/misc/bcfg2.spec b/misc/bcfg2.spec
index 276e72478..0ce51b207 100644
--- a/misc/bcfg2.spec
+++ b/misc/bcfg2.spec
@@ -113,7 +113,6 @@ BuildRequires: python-docutils
BuildRequires: systemd-units
%endif
-Requires: python-lxml
%if 0%{?rhel} && 0%{?rhel} < 6
Requires: python-ssl
%endif
@@ -413,8 +412,6 @@ awk '
# Fixup some paths
%{__perl} -pi -e 's@/etc/default@%{_sysconfdir}/sysconfig@g' tools/bcfg2-cron
-%{__perl} -pi -e 's@/usr/lib/bcfg2@%{_libexecdir}@g' debian/bcfg2.cron.daily
-%{__perl} -pi -e 's@/usr/lib/bcfg2@%{_libexecdir}@g' debian/bcfg2.cron.hourly
# Get rid of extraneous shebangs
for f in `find src/lib -name \*.py`
diff --git a/redhat/systemd/bcfg2.service b/redhat/systemd/bcfg2.service
index 572391fd0..6cbad2e5a 100644
--- a/redhat/systemd/bcfg2.service
+++ b/redhat/systemd/bcfg2.service
@@ -7,7 +7,7 @@ Type=forking
StandardOutput=syslog
StandardError=syslog
EnvironmentFile=-/etc/sysconfig/bcfg2
-ExecStart=/usr/sbin/bcfg2 $OPTIONS
+ExecStart=/usr/sbin/bcfg2 $BCFG2_OPTIONS
[Install]
WantedBy=multi-user.target
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 c91a66110..9864730ea 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' />
@@ -260,6 +262,8 @@
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
<xsd:element name='ACL' type='ACLType'/>
+ <xsd:group ref="augeasCommands"/>
+ <xsd:group ref="py:genshiElements"/>
</xsd:choice>
<xsd:attribute type="PathTypeEnum" name="type">
<xsd:annotation>
@@ -395,6 +399,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..4f6953b2a
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py
@@ -0,0 +1,284 @@
+""" Augeas driver """
+
+import sys
+import Bcfg2.Client.XML
+from augeas import Augeas
+from Bcfg2.Client.Tools.POSIX.base import POSIXTool
+
+
+class AugeasCommand(object):
+ """ Base class for all Augeas command objects """
+
+ def __init__(self, command, augeas_obj, logger):
+ self._augeas = augeas_obj
+ self.command = command
+ self.entry = self.command.getparent()
+ self.logger = logger
+
+ def get_path(self, attr="path"):
+ """ Get a fully qualified path from the name of the parent entry and
+ the path given in this command tag.
+
+ @param attr: The attribute to get the relative path from
+ @type attr: string
+ @returns: string - the fully qualified Augeas path
+
+ """
+ return "/files/%s/%s" % (self.entry.get("name").strip("/"),
+ self.command.get(attr).lstrip("/"))
+
+ def _exists(self, path):
+ """ Return True if a path exists in Augeas, False otherwise.
+
+ Note that a False return can mean many things: A file that
+ doesn't exist, a node within the file that doesn't exist, no
+ lens to parse the file, etc. """
+ return len(self._augeas.match(path)) > 1
+
+ def _verify_exists(self, path=None):
+ """ Verify that the given path exists, with friendly debug
+ logging.
+
+ @param path: The path to verify existence of. Defaults to the
+ result of
+ :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`.
+ @type path: string
+ @returns: bool - Whether or not the path exists
+ """
+ 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):
+ """ Verify that the given path does not exist, with friendly
+ debug logging.
+
+ @param path: The path to verify existence of. Defaults to the
+ result of
+ :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`.
+ @type path: string
+ @returns: bool - Whether or not the path does not exist.
+ (I.e., True if it does not exist, False if it does
+ exist.)
+ """
+ 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):
+ """ Verify that the given path is set to the given value, with
+ friendly debug logging.
+
+ @param expected: The expected value of the node.
+ @param path: The path to verify existence of. Defaults to the
+ result of
+ :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`.
+ @type path: string
+ @returns: bool - Whether or not the path matches the expected value.
+
+ """
+ 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)
+
+ def verify(self):
+ """ Verify that the command has been applied. """
+ raise NotImplementedError
+
+ def install(self):
+ """ Run the command. """
+ raise NotImplementedError
+
+
+class Remove(AugeasCommand):
+ """ Augeas ``rm`` command """
+ 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):
+ """ Augeas ``move`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, 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):
+ """ Augeas ``set`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, 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):
+ """ Augeas ``clear`` command """
+ def __init__(self, command, augeas_obj, logger):
+ Set.__init__(self, command, augeas_obj, logger)
+ self.value = None
+
+
+class SetMulti(AugeasCommand):
+ """ Augeas ``setm`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, 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):
+ """ Augeas ``ins`` command """
+ def __init__(self, command, augeas_obj, logger):
+ AugeasCommand.__init__(self, command, augeas_obj, 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, config):
+ POSIXTool.__init__(self, config)
+ self._augeas = dict()
+
+ def get_augeas(self, entry):
+ """ Get an augeas object for the given entry. """
+ if entry.get("name") not in self._augeas:
+ aug = 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):
+ """ Get a list of commands to verify or install.
+
+ @param entry: The entry to get commands from.
+ @type entry: lxml.etree._Element
+ @param unverified: Only get commands that failed verification.
+ @type unverified: bool
+ @returns: list of
+ :class:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand`
+ objects representing the commands.
+ """
+ rv = []
+ for cmd in entry.iterchildren():
+ if unverified and cmd.get("verified", "false") != "false":
+ continue
+ 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/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py
index 13b45a759..c27c7559d 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py
@@ -58,8 +58,11 @@ class POSIX(Bcfg2.Client.Tools.Tool):
mname = submodule[1].rsplit('.', 1)[-1]
if mname == 'base':
continue
- module = getattr(__import__(submodule[1]).Client.Tools.POSIX,
- mname)
+ try:
+ module = getattr(__import__(submodule[1]).Client.Tools.POSIX,
+ mname)
+ except ImportError:
+ continue
hdlr = getattr(module, "POSIX" + mname)
if POSIXTool in hdlr.__mro__:
# figure out what entry type this handler handles
diff --git a/src/lib/Bcfg2/DBSettings.py b/src/lib/Bcfg2/DBSettings.py
index 24835a3e8..cd5183a45 100644
--- a/src/lib/Bcfg2/DBSettings.py
+++ b/src/lib/Bcfg2/DBSettings.py
@@ -148,7 +148,7 @@ class _OptionContainer(object):
cf=('database', 'port'), help='Database port', dest='db_port'),
Bcfg2.Options.Option(
cf=('database', 'schema'), help='Database schema',
- dest='db_schema'),
+ dest='db_schema', default='public'),
Bcfg2.Options.Option(
cf=('database', 'options'), help='Database options',
dest='db_opts', type=Bcfg2.Options.Types.comma_dict,
diff --git a/src/lib/Bcfg2/Options/Types.py b/src/lib/Bcfg2/Options/Types.py
index 2f0fd7d52..d11e54fba 100644
--- a/src/lib/Bcfg2/Options/Types.py
+++ b/src/lib/Bcfg2/Options/Types.py
@@ -50,6 +50,15 @@ def comma_dict(value):
return result
+def anchored_regex_list(value):
+ """ Split an option string on whitespace and compile each element as
+ an anchored regex """
+ try:
+ return [re.compile('^' + x + '$') for x in re.split(r'\s+', value)]
+ except re.error:
+ raise ValueError("Not a list of regexes", value)
+
+
def octal(value):
""" Given an octal string, get an integer representation. """
return int(value, 8)
diff --git a/src/lib/Bcfg2/Reporting/Compat.py b/src/lib/Bcfg2/Reporting/Compat.py
index 57261970d..9113fdb91 100644
--- a/src/lib/Bcfg2/Reporting/Compat.py
+++ b/src/lib/Bcfg2/Reporting/Compat.py
@@ -10,9 +10,7 @@ if VERSION[0] == 1 and VERSION[1] < 6:
try:
# Django < 1.6
- from django.conf.urls import defaults
- django_urls = defaults
+ from django.conf.urls.defaults import url, patterns
except ImportError:
# Django > 1.6
- from django.conf import urls
- django_urls = urls
+ from django.conf.urls import url, patterns
diff --git a/src/lib/Bcfg2/Reporting/urls.py b/src/lib/Bcfg2/Reporting/urls.py
index a9e5690be..3a40cb932 100644
--- a/src/lib/Bcfg2/Reporting/urls.py
+++ b/src/lib/Bcfg2/Reporting/urls.py
@@ -1,4 +1,4 @@
-from Bcfg2.Reporting.Compat.django_urls import *
+from Bcfg2.Reporting.Compat import url, patterns # django compat imports
from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import HttpResponsePermanentRedirect
from Bcfg2.Reporting.utils import filteredUrls, paginatedUrls, timeviewUrls
diff --git a/src/lib/Bcfg2/Reporting/utils.py b/src/lib/Bcfg2/Reporting/utils.py
index d9b8213b1..0d394fcd8 100755
--- a/src/lib/Bcfg2/Reporting/utils.py
+++ b/src/lib/Bcfg2/Reporting/utils.py
@@ -1,5 +1,4 @@
"""Helper functions for reports"""
-from Bcfg2.Reporting.Compat.django_urls import *
import re
"""List of filters provided by filteredUrls"""
diff --git a/src/lib/Bcfg2/Server/Admin.py b/src/lib/Bcfg2/Server/Admin.py
index 207106596..27152b867 100644
--- a/src/lib/Bcfg2/Server/Admin.py
+++ b/src/lib/Bcfg2/Server/Admin.py
@@ -173,15 +173,33 @@ class Backup(AdminCmd):
class Client(_ServerAdminCmd):
- """ Create, delete, or list client entries """
+ """ Create, modify, delete, or list client entries """
+ __plugin_whitelist__ = ["Metadata"]
options = _ServerAdminCmd.options + [
Bcfg2.Options.PositionalArgument(
"mode",
- choices=["add", "del", "list"]),
- Bcfg2.Options.PositionalArgument("hostname", nargs='?')]
-
- __plugin_whitelist__ = ["Metadata"]
+ choices=["add", "del", "delete", "remove", "rm", "up", "update",
+ "list"]),
+ Bcfg2.Options.PositionalArgument("hostname", nargs='?'),
+ Bcfg2.Options.PositionalArgument("attributes", metavar="KEY=VALUE",
+ nargs='*')]
+
+ valid_attribs = ['profile', 'uuid', 'password', 'floating', 'secure',
+ 'address', 'auth']
+
+ def get_attribs(self, setup):
+ """ Get attributes for adding or updating a client from the command
+ line """
+ attr_d = {}
+ for i in setup.attributes:
+ attr, val = i.split('=', 1)
+ if attr not in self.valid_attribs:
+ print("Attribute %s unknown. Valid attributes: %s" %
+ (attr, self.valid_attribs))
+ raise SystemExit(1)
+ attr_d[attr] = val
+ return attr_d
def run(self, setup):
if setup.mode != 'list' and not setup.hostname:
@@ -189,23 +207,32 @@ class Client(_ServerAdminCmd):
elif setup.mode == 'list' and setup.hostname:
self.logger.warning("<hostname> is not honored in list mode")
- if setup.mode == 'add':
- try:
- self.metadata.add_client(setup.hostname)
- except MetadataConsistencyError:
- err = sys.exc_info()[1]
- self.errExit("Error adding client %s: %s" % (setup.hostname,
- err))
- elif setup.mode == 'del':
+ if setup.mode == 'list':
+ for client in self.metadata.list_clients():
+ print(client)
+ else:
+ include_attribs = True
+ if setup.mode == 'add':
+ func = self.metadata.add_client
+ action = "adding"
+ elif setup.mode in ['up', 'update']:
+ func = self.metadata.update_client
+ action = "updating"
+ elif setup.mode in ['del', 'delete', 'rm', 'remove']:
+ func = self.metadata.remove_client
+ include_attribs = False
+ action = "deleting"
+
+ if include_attribs:
+ args = (setup.hostname, self.get_attribs(setup))
+ else:
+ args = (setup.hostname,)
try:
- self.metadata.remove_client(setup.hostname)
+ func(*args)
except MetadataConsistencyError:
err = sys.exc_info()[1]
- self.errExit("Error deleting client %s: %s" % (setup.hostname,
- err))
- elif setup.mode == 'list':
- for client in self.metadata.list_clients():
- print(client)
+ self.errExit("Error %s client %s: %s" % (setup.hostname,
+ action, err))
class Compare(AdminCmd):
diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py
index 398053374..23209448d 100644
--- a/src/lib/Bcfg2/Server/Core.py
+++ b/src/lib/Bcfg2/Server/Core.py
@@ -240,12 +240,12 @@ class Core(object):
verbosity=0)
self._database_available = True
except ImproperlyConfigured:
- err = sys.exc_info()[1]
- self.logger.error("Django configuration problem: %s" % err)
+ self.logger.error("Django configuration problem: %s" %
+ sys.exc_info()[1])
except:
- err = sys.exc_info()[1]
self.logger.error("Updating database %s failed: %s" %
- (Bcfg2.Options.setup.db_name, err))
+ (Bcfg2.Options.setup.db_name,
+ sys.exc_info()[1]))
def __str__(self):
return self.__class__.__name__
diff --git a/src/lib/Bcfg2/Server/Encryption.py b/src/lib/Bcfg2/Server/Encryption.py
index f8b602d90..c96e7ad21 100755
--- a/src/lib/Bcfg2/Server/Encryption.py
+++ b/src/lib/Bcfg2/Server/Encryption.py
@@ -233,6 +233,10 @@ class DecryptError(Exception):
""" Exception raised when decryption fails. """
+class EncryptError(Exception):
+ """ Exception raised when encryption fails. """
+
+
class CryptoTool(object):
""" Generic decryption/encryption interface base object """
@@ -428,8 +432,7 @@ class PropertiesEncryptor(Encryptor, PropertiesCryptoMixin):
try:
pname, passphrase = self._get_element_passphrase(elt)
except PassphraseError:
- self.logger.error(str(sys.exc_info()[1]))
- return False
+ raise EncryptError(str(sys.exc_info()[1]))
self.logger.debug("Encrypting %s" % print_xml(elt))
elt.text = ssl_encrypt(elt.text, passphrase).strip()
elt.set("encrypted", pname)
@@ -640,9 +643,9 @@ class CLI(object):
if data is None:
try:
data = getattr(tool, mode)()
- except DecryptError:
- self.logger.error("Failed to %s %s, skipping" % (mode,
- fname))
+ except (EncryptError, DecryptError):
+ self.logger.error("Failed to %s %s, skipping: %s" %
+ (mode, fname, sys.exc_info()[1]))
continue
if Bcfg2.Options.setup.stdout:
if len(Bcfg2.Options.setup.files) > 1:
diff --git a/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
new file mode 100644
index 000000000..202a1487d
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
@@ -0,0 +1,76 @@
+""" Check for templated scripts or executables. """
+
+import os
+import stat
+import Bcfg2.Server.Lint
+from Bcfg2.Compat import any # pylint: disable=W0622
+from Bcfg2.Server.Plugin import default_path_metadata
+from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
+from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenshiGenerator import \
+ CfgEncryptedGenshiGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedCheetahGenerator import \
+ CfgEncryptedCheetahGenerator
+
+
+class TemplateAbuse(Bcfg2.Server.Lint.ServerPlugin):
+ """ Check for templated scripts or executables. """
+ templates = [CfgGenshiGenerator, CfgCheetahGenerator,
+ CfgEncryptedGenshiGenerator, CfgEncryptedCheetahGenerator]
+ extensions = [".pl", ".py", ".sh", ".rb"]
+
+ def Run(self):
+ if 'Cfg' in self.core.plugins:
+ for entryset in self.core.plugins['Cfg'].entries.values():
+ for entry in entryset.entries.values():
+ if (self.HandlesFile(entry.name) and
+ any(isinstance(entry, t) for t in self.templates)):
+ self.check_template(entryset, entry)
+
+ @classmethod
+ def Errors(cls):
+ return {"templated-script": "warning",
+ "templated-executable": "warning"}
+
+ def check_template(self, entryset, entry):
+ """ Check a template to see if it's a script or an executable. """
+ # first, check for a known script extension
+ ext = os.path.splitext(entryset.path)[1]
+ if ext in self.extensions:
+ self.LintError("templated-script",
+ "Templated script found: %s\n"
+ "File has a known script extension: %s\n"
+ "Template a config file for the script instead" %
+ (entry.name, ext))
+ return
+
+ # next, check for a shebang line
+ firstline = open(entry.name).readline()
+ if firstline.startswith("#!"):
+ self.LintError("templated-script",
+ "Templated script found: %s\n"
+ "File starts with a shebang: %s\n"
+ "Template a config file for the script instead" %
+ (entry.name, firstline))
+ return
+
+ # finally, check for executable permissions in info.xml
+ for entry in entryset.entries.values():
+ if isinstance(entry, CfgInfoXML):
+ for pinfo in entry.infoxml.pnode.data.xpath("//FileInfo"):
+ try:
+ mode = int(
+ pinfo.get("mode",
+ default_path_metadata()['mode']), 8)
+ except ValueError:
+ # LintError will be produced by RequiredAttrs plugin
+ self.logger.warning("Non-octal mode: %s" % mode)
+ continue
+ if mode & stat.S_IXUSR != 0:
+ self.LintError(
+ "templated-executable",
+ "Templated executable found: %s\n"
+ "Template a config file for the executable instead"
+ % entry.name)
+ return
diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py
index e38619355..3ad78ade4 100644
--- a/src/lib/Bcfg2/Server/Lint/Validate.py
+++ b/src/lib/Bcfg2/Server/Lint/Validate.py
@@ -113,9 +113,16 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
:type filename: string
:returns: lxml.etree._ElementTree - the parsed data"""
try:
- return lxml.etree.parse(filename)
- except SyntaxError:
- result = self.cmd.run(["xmllint", filename])
+ xdata = lxml.etree.parse(filename)
+ if self.files is None:
+ xdata.xinclude()
+ return xdata
+ except (lxml.etree.XIncludeError, SyntaxError):
+ cmd = ["xmllint", "--noout"]
+ if self.files is None:
+ cmd.append("--xinclude")
+ cmd.append(filename)
+ result = self.cmd.run(cmd)
self.LintError("xml-failed-to-parse",
"%s fails to parse:\n%s" %
(filename, result.stdout + result.stderr))
@@ -146,6 +153,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
if not schema:
return False
datafile = self.parse(filename)
+ if not datafile:
+ return False
if not schema.validate(datafile):
cmd = ["xmllint"]
if self.files is None:
diff --git a/src/lib/Bcfg2/Server/Lint/ValidateJSON.py b/src/lib/Bcfg2/Server/Lint/ValidateJSON.py
new file mode 100644
index 000000000..04151d764
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/ValidateJSON.py
@@ -0,0 +1,70 @@
+"""Ensure that all JSON files in the Bcfg2 repository are
+valid. Currently, the only plugins that uses JSON are Ohai and
+Properties."""
+
+import os
+import sys
+import glob
+import fnmatch
+import Bcfg2.Server.Lint
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+
+class ValidateJSON(Bcfg2.Server.Lint.ServerlessPlugin):
+ """Ensure that all JSON files in the Bcfg2 repository are
+ valid. Currently, the only plugins that uses JSON are Ohai and
+ Properties. """
+
+ def __init__(self, *args, **kwargs):
+ Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs)
+
+ #: A list of file globs that give the path to JSON files. The
+ #: globs are extended :mod:`fnmatch` globs that also support
+ #: ``**``, which matches any number of any characters,
+ #: including forward slashes.
+ self.globs = ["Properties/*.json", "Ohai/*.json"]
+ self.files = self.get_files()
+
+ def Run(self):
+ for path in self.files:
+ self.logger.debug("Validating JSON in %s" % path)
+ try:
+ json.load(open(path))
+ except ValueError:
+ self.LintError("json-failed-to-parse",
+ "%s does not contain valid JSON: %s" %
+ (path, sys.exc_info()[1]))
+
+ @classmethod
+ def Errors(cls):
+ return {"json-failed-to-parse": "error"}
+
+ def get_files(self):
+ """Return a list of all JSON files to validate, based on
+ :attr:`Bcfg2.Server.Lint.ValidateJSON.ValidateJSON.globs`. """
+ if self.files is not None:
+ listfiles = lambda p: fnmatch.filter(self.files,
+ os.path.join('*', p))
+ else:
+ listfiles = lambda p: glob.glob(
+ os.path.join(Bcfg2.Options.setup.repository, p))
+
+ rv = []
+ for path in self.globs:
+ if '/**/' in path:
+ if self.files is not None:
+ rv.extend(listfiles(path))
+ else: # self.files is None
+ fpath, fname = path.split('/**/')
+ for root, _, files in os.walk(
+ os.path.join(Bcfg2.Options.setup.repository,
+ fpath)):
+ rv.extend([os.path.join(root, f)
+ for f in files if f == fname])
+ else:
+ rv.extend(listfiles(path))
+ return rv
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 1cb5a7b3e..aa8db2bc0 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -18,7 +18,7 @@ from Bcfg2.Compat import CmpMixin, wraps
from Bcfg2.Server.Plugin.base import Plugin
from Bcfg2.Server.Plugin.interfaces import Generator, TemplateDataProvider
from Bcfg2.Server.Plugin.exceptions import SpecificityError, \
- PluginExecutionError
+ PluginExecutionError, PluginInitError
try:
import Bcfg2.Server.Encryption
@@ -219,6 +219,18 @@ class DatabaseBacked(Plugin):
.. private-include: _must_lock
"""
+ def __init__(self, core):
+ Plugin.__init__(self, core)
+ use_db = getattr(Bcfg2.Options.setup, "%s_db" % self.name.lower(),
+ False)
+ if use_db and not HAS_DJANGO:
+ raise PluginInitError("%s is configured to use the database but "
+ "Django libraries are not found" % self.name)
+ elif use_db and not self.core.database_available:
+ raise PluginInitError("%s is configured to use the database but "
+ "the database is unavailable due to prior "
+ "errors" % self.name)
+
@property
def _use_db(self):
""" Whether or not this plugin is configured to use the
@@ -227,11 +239,7 @@ class DatabaseBacked(Plugin):
False)
if use_db and HAS_DJANGO and self.core.database_available:
return True
- elif not use_db:
- return False
else:
- self.logger.error("%s: use_database is true but django not found" %
- self.name)
return False
@property
@@ -818,7 +826,8 @@ class StructFile(XMLFileBacked):
"""
stream = self.template.generate(
**get_xml_template_data(self, metadata)).filter(removecomment)
- return lxml.etree.XML(stream.render('xml', strip_whitespace=False),
+ return lxml.etree.XML(stream.render('xml',
+ strip_whitespace=False).encode(),
parser=Bcfg2.Server.XMLParser)
def _match(self, item, metadata, *args):
diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py
index 78f86f28e..6ff256147 100644
--- a/src/lib/Bcfg2/Server/Plugins/Metadata.py
+++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -674,6 +674,11 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
if attribs is None:
attribs = dict()
if self._use_db:
+ if attribs:
+ msg = "Metadata does not support setting client attributes " +\
+ "with use_database enabled"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
try:
client = MetadataClientModel.objects.get(hostname=client_name)
except MetadataClientModel.DoesNotExist:
diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py
index 9f2375fcd..553c16202 100644
--- a/src/lib/Bcfg2/Server/Plugins/Probes.py
+++ b/src/lib/Bcfg2/Server/Plugins/Probes.py
@@ -10,7 +10,7 @@ import lxml.etree
import Bcfg2.Server
import Bcfg2.Server.Cache
import Bcfg2.Server.Plugin
-from Bcfg2.Compat import unicode # pylint: disable=W0622
+from Bcfg2.Compat import unicode, any # pylint: disable=W0622
import Bcfg2.Server.FileMonitor
from Bcfg2.Logger import Debuggable
from Bcfg2.Server.Statistics import track_statistics
@@ -431,7 +431,13 @@ class Probes(Bcfg2.Server.Plugin.Probing,
options = [
Bcfg2.Options.BooleanOption(
cf=('probes', 'use_database'), dest="probes_db",
- help="Use database capabilities of the Probes plugin")]
+ help="Use database capabilities of the Probes plugin"),
+ Bcfg2.Options.Option(
+ cf=('probes', 'allowed_groups'), dest="probes_allowed_groups",
+ help="Whitespace-separated list of group name regexps to which "
+ "probes can assign a client",
+ default=[re.compile('.*')],
+ type=Bcfg2.Options.Types.anchored_regex_list)]
options_parsed_hook = staticmethod(load_django_models)
def __init__(self, core):
@@ -480,7 +486,13 @@ class Probes(Bcfg2.Server.Plugin.Probing,
for line in dlines[:]:
match = self.groupline_re.match(line)
if match:
- groups.append(match.group("groupname"))
+ newgroup = match.group("groupname")
+ if self._group_allowed(newgroup):
+ groups.append(newgroup)
+ else:
+ self.logger.warning(
+ "Disallowed group assignment %s from %s" %
+ (newgroup, client.hostname))
dlines.remove(line)
return (groups, ProbeData("\n".join(dlines)))
@@ -489,3 +501,10 @@ class Probes(Bcfg2.Server.Plugin.Probing,
def get_additional_data(self, metadata):
return self.probestore.get_data(metadata.hostname)
+
+ def _group_allowed(self, group):
+ """ Determine if the named group can be set as a probe group
+ by checking the regexes listed in the [probes] groups_allowed
+ setting """
+ return any(r.match(group)
+ for r in Bcfg2.Options.setup.probes_allowed_groups)
diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt
deleted file mode 100755
index 26d5eedf1..000000000
--- a/src/sbin/bcfg2-crypt
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python
-""" helper for encrypting/decrypting Cfg and Properties files """
-
-import sys
-from Bcfg2.Server.Encryption import CLI
-
-if __name__ == '__main__':
- sys.exit(CLI().run())
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..74ca617af
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestAugeas.py
@@ -0,0 +1,250 @@
+# -*- 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:
+ POSIXAugeas = None
+ 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):
+ TestPOSIXTool.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",
+ verified="false")],
+ 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",
+ verified="false")],
+ expected)
+
+ def test_install_only_verified(self):
+ """ Test that only unverified commands are installed """
+ 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", verified="false"),
+ lxml.etree.Element("Set", path="Test/Bogus/#text",
+ value="bogus", verified="true")],
+ 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"]',
+ verified="false")],
+ 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',
+ verified="false")],
+ 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", verified="false")],
+ 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", verified="false"),
+ lxml.etree.Element(
+ "Set",
+ path='Test/Children[#attribute/identical = "true"]/Thing[3]/#text',
+ value="three", verified="false")],
+ expected)
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py
index 1bf073f81..0c059b5f3 100644
--- a/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/Test_init.py
@@ -41,10 +41,12 @@ class TestTool(Bcfg2TestCase):
@patch("%s.%s._analyze_config" % (self.test_obj.__module__,
self.test_obj.__name__))
def inner(mock_analyze_config, mock_check_execs):
- t = self.get_obj()
+ self.get_obj()
mock_analyze_config.assert_called_with()
mock_check_execs.assert_called_with()
+ inner()
+
def test__analyze_config(self):
t = self.get_obj()
t.getSupportedEntries = Mock()
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
index 81c4837e1..75bd4ec95 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
@@ -8,6 +8,7 @@ import genshi.core
from Bcfg2.Compat import reduce
from mock import Mock, MagicMock, patch
from Bcfg2.Server.Plugin.helpers import *
+from Bcfg2.Server.Plugin.exceptions import PluginInitError
# add all parent testsuite directories to sys.path to allow (most)
# relative imports in python 2.4
@@ -34,6 +35,7 @@ def tostring(el):
class FakeElementTree(lxml.etree._ElementTree):
xinclude = Mock()
+ parse = Mock
class TestFunctions(Bcfg2TestCase):
@@ -71,7 +73,7 @@ class TestDatabaseBacked(TestPlugin):
self.assertFalse(db._use_db)
setattr(Bcfg2.Options.setup, attr, True)
- self.assertFalse(db._use_db)
+ self.assertRaises(PluginInitError, self.get_obj, core)
class TestPluginDatabaseModel(Bcfg2TestCase):
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedGenerator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedGenerator.py
index 873ebd837..03b9fb0f4 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedGenerator.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgEncryptedGenerator.py
@@ -1,6 +1,7 @@
import os
import sys
import lxml.etree
+import Bcfg2.Server.Plugins.Cfg
from mock import Mock, MagicMock, patch
from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator import *
from Bcfg2.Server.Plugin import PluginExecutionError
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py
index 68313e6fb..c9a982bb3 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py
@@ -1,4 +1,5 @@
import os
+import re
import sys
import shutil
import tempfile
@@ -229,10 +230,12 @@ group: group:with:colons
def setUp(self):
Bcfg2TestCase.setUp(self)
set_setup_default("probes_db")
+ set_setup_default("probes_allowed_groups", [re.compile(".*")])
self.datastore = None
Bcfg2.Server.Cache.expire("Probes")
def tearDown(self):
+ Bcfg2.Server.Cache.expire("Probes")
if self.datastore is not None:
shutil.rmtree(self.datastore)
self.datastore = None
@@ -277,6 +280,31 @@ group: group:with:colons
Bcfg2.Options.setup.probes_db = True
self._perform_tests()
+ def test_allowed_cgroups(self):
+ """ Test option to only allow probes to set certain groups """
+ probes = self.get_obj()
+
+ test_text = """a couple lines
+of freeform text
+"""
+ test_groups = ["group", "group2", "group-with-dashes"]
+ test_probe_data = lxml.etree.Element("Probe", name="test")
+ test_probe_data.text = test_text
+ for group in test_groups:
+ test_probe_data.text += "group:%s\n" % group
+
+ client = Mock()
+ groups, data = probes.ReceiveDataItem(client, test_probe_data)
+ self.assertItemsEqual(groups, test_groups)
+ self.assertEqual(data, test_text)
+
+ old_allowed_groups = Bcfg2.Options.setup.probes_allowed_groups
+ Bcfg2.Options.setup.probes_allowed_groups = [re.compile(r'^group.?$')]
+ groups, data = probes.ReceiveDataItem(client, test_probe_data)
+ self.assertItemsEqual(groups, ['group', 'group2'])
+ self.assertEqual(data, test_text)
+ Bcfg2.Options.setup.probes_allowed_groups = old_allowed_groups
+
def _perform_tests(self):
p = self.get_obj()
diff --git a/testsuite/before_install.sh b/testsuite/before_install.sh
index 5f1a59aaf..2c80036cd 100755
--- a/testsuite/before_install.sh
+++ b/testsuite/before_install.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/bash -ex
# before_install script for Travis-CI
@@ -8,6 +8,7 @@ sudo apt-get update -qq
sudo apt-get install -qq swig libxml2-utils
if [[ "$WITH_OPTIONAL_DEPS" == "yes" ]]; then
if [[ ${PYVER:0:1} == "2" ]]; then
- sudo apt-get install -qq python-selinux python-pylibacl yum
+ sudo apt-get install -y yum libaugeas0 augeas-lenses libacl1-dev \
+ libssl-dev
fi
fi
diff --git a/testsuite/common.py b/testsuite/common.py
index 849c22ef3..5a08f8db5 100644
--- a/testsuite/common.py
+++ b/testsuite/common.py
@@ -129,8 +129,12 @@ class Bcfg2TestCase(TestCase):
"\nSecond: %s" % lxml.etree.tostring(el2)
self.assertEqual(el1.tag, el2.tag, msg=fullmsg % "Tags differ")
- self.assertEqual(el1.text, el2.text,
- msg=fullmsg % "Text content differs")
+ 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()),
diff --git a/testsuite/ext/exception_messages.py b/testsuite/ext/exception_messages.py
index 877ba42a1..cd3d7112c 100644
--- a/testsuite/ext/exception_messages.py
+++ b/testsuite/ext/exception_messages.py
@@ -1,16 +1,30 @@
-from logilab import astng
-from pylint.interfaces import IASTNGChecker
+try:
+ from logilab import astng as ast
+ from pylint.interfaces import IASTNGChecker as IChecker
+ PYLINT = 0 # pylint 0.something
+except ImportError:
+ import astroid as ast
+ from pylint.interfaces import IAstroidChecker as IChecker
+ PYLINT = 1 # pylint 1.something
from pylint.checkers import BaseChecker
from pylint.checkers.utils import safe_infer
+if PYLINT == 0:
+ # this is not quite correct; later versions of pylint 0.* wanted a
+ # three-tuple for messages as well
+ msg = ('Exception raised without arguments',
+ 'Used when an exception is raised without any arguments')
+else:
+ msg = ('Exception raised without arguments',
+ 'exception-without-args',
+ 'Used when an exception is raised without any arguments')
+msgs = {'R9901': msg}
+
class ExceptionMessageChecker(BaseChecker):
- __implements__ = IASTNGChecker
+ __implements__ = IChecker
name = 'Exception Messages'
- msgs = \
- {'R9901': ('Exception raised without arguments',
- 'Used when an exception is raised without any arguments')}
options = (
('exceptions-without-args',
dict(default=('NotImplementedError',),
@@ -23,9 +37,9 @@ class ExceptionMessageChecker(BaseChecker):
def visit_raise(self, node):
if node.exc is None:
return
- if isinstance(node.exc, astng.Name):
+ if isinstance(node.exc, ast.Name):
raised = safe_infer(node.exc)
- if (isinstance(raised, astng.Class) and
+ if (isinstance(raised, ast.Class) and
raised.name not in self.config.exceptions_without_args):
self.add_message('R9901', node=node.exc)
diff --git a/testsuite/install.sh b/testsuite/install.sh
index 8721c8015..50b91a4d2 100755
--- a/testsuite/install.sh
+++ b/testsuite/install.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/bash -ex
# install script for Travis-CI
@@ -11,22 +11,12 @@ if [[ ${PYVER:0:1} == "2" && $PYVER != "2.7" ]]; then
fi
if [[ "$WITH_OPTIONAL_DEPS" == "yes" ]]; then
- pip install --use-mirrors PyYAML pyinotify boto
- if [[ $PYVER == "2.5" ]]; then
- # markdown 2.2+ doesn't work on py2.5
- pip install --use-mirrors simplejson 'markdown<2.2' 'django<1.4.9'
- else
- pip install 'django<1.5'
- fi
+ pip install --use-mirrors PyYAML pyinotify boto pylibacl 'django<1.5'
+ easy_install https://fedorahosted.org/released/python-augeas/python-augeas-0.4.1.tar.gz
if [[ ${PYVER:0:1} == "2" ]]; then
# django supports py3k, but South doesn't, and the django bits
# in bcfg2 require South
- pip install cheetah 'South<0.8' M2Crypto
- fi
-else
- # python < 2.6 requires M2Crypto for SSL communication, not just
- # for encryption support
- if [[ $PYVER == "2.5" || $PYVER == "2.4" ]]; then
- pip install --use-mirrors M2crypto
+ pip install cheetah 'South<0.8'
+ pip install m2crypto
fi
fi
diff --git a/tools/bcfg2-cron b/tools/bcfg2-cron
index fe0e6e90d..9a93c2e44 100755
--- a/tools/bcfg2-cron
+++ b/tools/bcfg2-cron
@@ -1,6 +1,6 @@
#!/bin/sh
#
-# Script to run bcfg2 with cron.
+# Script to run bcfg2 with cron.
#
# This script is designed so that bcfg2-cron can be invoked from both
# /etc/cron.daily and /etc/cron.hourly. This allows the administrators to