summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README3
-rwxr-xr-x[-rw-r--r--]debian/bcfg2.cron.daily14
-rwxr-xr-x[-rw-r--r--]debian/bcfg2.cron.hourly14
-rw-r--r--debian/control2
-rw-r--r--doc/appendix/files/ntp.txt2
-rw-r--r--doc/appendix/guides/nat_howto.txt4
-rw-r--r--doc/development/client-driver.txt2
-rw-r--r--doc/server/configurationentries.txt167
-rw-r--r--doc/server/index.txt1
-rw-r--r--doc/server/info.txt45
-rw-r--r--doc/server/plugins/connectors/properties.txt194
-rw-r--r--doc/server/plugins/connectors/puppetenc.txt123
-rw-r--r--doc/server/plugins/generators/cfg.txt129
-rw-r--r--doc/server/plugins/generators/nagiosgen.txt8
-rw-r--r--doc/server/plugins/generators/packages.txt160
-rw-r--r--doc/server/plugins/generators/rules.txt297
-rw-r--r--doc/server/plugins/generators/semodules.txt66
-rw-r--r--doc/server/plugins/generators/sslca.txt8
-rw-r--r--doc/server/plugins/generators/tgenshi/index.txt2
-rw-r--r--doc/server/plugins/grouping/metadata.txt16
-rw-r--r--doc/server/plugins/misc/trigger.txt4
-rw-r--r--doc/server/plugins/plugin-roles.txt159
-rw-r--r--doc/server/plugins/probes/index.txt2
-rw-r--r--doc/server/selinux.txt97
-rw-r--r--gentoo/bcfg2-1.3.0.ebuild (renamed from gentoo/bcfg2-1.2.2.ebuild)14
-rw-r--r--man/bcfg2-admin.8422
-rw-r--r--man/bcfg2-build-reports.874
-rw-r--r--man/bcfg2-info.8263
-rw-r--r--man/bcfg2-lint.8239
-rw-r--r--man/bcfg2-lint.conf.5259
-rw-r--r--man/bcfg2-ping-sweep.830
-rw-r--r--man/bcfg2-reports.8168
-rw-r--r--man/bcfg2-server.8104
-rw-r--r--man/bcfg2.1345
-rw-r--r--man/bcfg2.conf.5822
-rw-r--r--misc/bcfg2.spec6
-rw-r--r--reports/site_media/bcfg2_base.css16
-rw-r--r--schemas/base.xsd3
-rw-r--r--schemas/bundle.xsd127
-rw-r--r--schemas/clients.xsd1
-rw-r--r--schemas/defaults.xsd27
-rw-r--r--schemas/info.xsd1
-rw-r--r--schemas/packages.xsd7
-rw-r--r--schemas/pathentry.xsd21
-rw-r--r--schemas/rules.xsd113
-rw-r--r--schemas/servicetype.xsd14
-rw-r--r--schemas/types.xsd113
-rwxr-xr-xsetup.py3
-rw-r--r--solaris/Makefile5
-rw-r--r--solaris/gen-prototypes.sh2
-rw-r--r--src/lib/Bcfg2/Bcfg2Py3k.py7
-rw-r--r--src/lib/Bcfg2/Client/Frame.py132
-rw-r--r--src/lib/Bcfg2/Client/Tools/APT.py45
-rw-r--r--src/lib/Bcfg2/Client/Tools/Action.py7
-rw-r--r--src/lib/Bcfg2/Client/Tools/Chkconfig.py46
-rw-r--r--src/lib/Bcfg2/Client/Tools/DebInit.py5
-rw-r--r--src/lib/Bcfg2/Client/Tools/OpenCSW.py33
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX.py811
-rw-r--r--src/lib/Bcfg2/Client/Tools/Portage.py27
-rw-r--r--src/lib/Bcfg2/Client/Tools/RPMng.py122
-rw-r--r--src/lib/Bcfg2/Client/Tools/RcUpdate.py40
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py716
-rw-r--r--src/lib/Bcfg2/Client/Tools/SMF.py5
-rw-r--r--src/lib/Bcfg2/Client/Tools/Systemd.py17
-rw-r--r--src/lib/Bcfg2/Client/Tools/Upstart.py5
-rw-r--r--src/lib/Bcfg2/Client/Tools/YUM24.py21
-rw-r--r--src/lib/Bcfg2/Client/Tools/YUMng.py67
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py58
-rw-r--r--src/lib/Bcfg2/Client/Tools/launchd.py5
-rw-r--r--src/lib/Bcfg2/Component.py14
-rwxr-xr-xsrc/lib/Bcfg2/Encryption.py75
-rw-r--r--src/lib/Bcfg2/Options.py899
-rw-r--r--src/lib/Bcfg2/Proxy.py10
-rw-r--r--src/lib/Bcfg2/SSLServer.py13
-rw-r--r--src/lib/Bcfg2/Server/Admin/Client.py3
-rw-r--r--src/lib/Bcfg2/Server/Admin/Compare.py3
-rw-r--r--src/lib/Bcfg2/Server/Admin/Init.py52
-rw-r--r--src/lib/Bcfg2/Server/Admin/Pull.py8
-rw-r--r--src/lib/Bcfg2/Server/Admin/Reports.py69
-rw-r--r--src/lib/Bcfg2/Server/Admin/Tidy.py7
-rw-r--r--src/lib/Bcfg2/Server/Admin/__init__.py3
-rw-r--r--src/lib/Bcfg2/Server/Core.py138
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor.py315
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Fam.py82
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Gamin.py64
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Inotify.py64
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Pseudo.py27
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/__init__.py141
-rw-r--r--src/lib/Bcfg2/Server/Lint/Bundles.py54
-rw-r--r--src/lib/Bcfg2/Server/Lint/Deltas.py25
-rwxr-xr-xsrc/lib/Bcfg2/Server/Lint/Genshi.py1
-rw-r--r--src/lib/Bcfg2/Server/Lint/GroupPatterns.py35
-rw-r--r--src/lib/Bcfg2/Server/Lint/InfoXML.py6
-rw-r--r--src/lib/Bcfg2/Server/Lint/Pkgmgr.py38
-rw-r--r--src/lib/Bcfg2/Server/Lint/RequiredAttrs.py150
-rw-r--r--src/lib/Bcfg2/Server/Lint/TemplateHelper.py64
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py37
-rw-r--r--src/lib/Bcfg2/Server/Plugin.py262
-rw-r--r--src/lib/Bcfg2/Server/Plugins/BB.py4
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Bundler.py85
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py8
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedCheetahGenerator.py14
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py63
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py26
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py72
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py4
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py47
-rw-r--r--src/lib/Bcfg2/Server/Plugins/DBStats.py39
-rw-r--r--src/lib/Bcfg2/Server/Plugins/FileProbes.py19
-rw-r--r--src/lib/Bcfg2/Server/Plugins/GroupPatterns.py45
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Metadata.py72
-rw-r--r--src/lib/Bcfg2/Server/Plugins/NagiosGen.py25
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Collection.py31
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py21
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Source.py31
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Yum.py33
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/__init__.py26
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Pkgmgr.py54
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Probes.py63
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Properties.py109
-rw-r--r--src/lib/Bcfg2/Server/Plugins/PuppetENC.py117
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SEModules.py46
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SGenshi.py5
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSLCA.py111
-rw-r--r--src/lib/Bcfg2/Server/Plugins/ServiceCompat.py32
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Snapshots.py9
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Statistics.py1
-rw-r--r--src/lib/Bcfg2/Server/Plugins/TemplateHelper.py66
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Trigger.py65
-rw-r--r--src/lib/Bcfg2/Server/Reports/Updater/Changes/1_0_x.py11
-rw-r--r--src/lib/Bcfg2/Server/Reports/Updater/Changes/1_1_x.py59
-rw-r--r--src/lib/Bcfg2/Server/Reports/Updater/Changes/1_2_x.py15
-rw-r--r--src/lib/Bcfg2/Server/Reports/Updater/Changes/1_3_0.py27
-rw-r--r--src/lib/Bcfg2/Server/Reports/Updater/Changes/__init__.py0
-rw-r--r--src/lib/Bcfg2/Server/Reports/Updater/Routines.py279
-rw-r--r--src/lib/Bcfg2/Server/Reports/Updater/__init__.py239
-rwxr-xr-xsrc/lib/Bcfg2/Server/Reports/importscript.py296
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml43
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/models.py139
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/sql/client.sql7
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html5
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/base.html1
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html56
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html18
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html1
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html42
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html30
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html22
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html16
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py130
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py10
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/urls.py9
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/views.py266
-rw-r--r--src/lib/Bcfg2/Server/Reports/settings.py28
-rw-r--r--src/lib/Bcfg2/Server/Reports/updatefix.py281
-rwxr-xr-xsrc/lib/Bcfg2/Server/Reports/utils.py4
-rw-r--r--src/lib/Bcfg2/Server/Snapshots/model.py8
-rw-r--r--src/lib/Bcfg2/Server/__init__.py6
-rw-r--r--src/lib/Bcfg2/version.py115
-rwxr-xr-xsrc/sbin/bcfg2105
-rwxr-xr-xsrc/sbin/bcfg2-admin25
-rwxr-xr-xsrc/sbin/bcfg2-crypt356
-rwxr-xr-xsrc/sbin/bcfg2-info137
-rwxr-xr-xsrc/sbin/bcfg2-lint95
-rwxr-xr-xsrc/sbin/bcfg2-reports576
-rwxr-xr-xsrc/sbin/bcfg2-server33
-rwxr-xr-xsrc/sbin/bcfg2-test21
-rw-r--r--testsuite/Testlib/TestOptions.py25
-rw-r--r--tools/manpagegen/bcfg2-admin.8.ronn180
-rw-r--r--tools/manpagegen/bcfg2-build-reports.8.ronn34
-rw-r--r--tools/manpagegen/bcfg2-crypt.8.ronn108
-rw-r--r--tools/manpagegen/bcfg2-info.8.ronn110
-rw-r--r--tools/manpagegen/bcfg2-lint.8.ronn119
-rw-r--r--tools/manpagegen/bcfg2-lint.conf.5.ronn114
-rw-r--r--tools/manpagegen/bcfg2-ping-sweep.8.ronn16
-rw-r--r--tools/manpagegen/bcfg2-reports.8.ronn82
-rw-r--r--tools/manpagegen/bcfg2-server.8.ronn43
-rw-r--r--tools/manpagegen/bcfg2.1.ronn152
-rw-r--r--tools/manpagegen/bcfg2.conf.5.ronn545
-rw-r--r--tools/manpagegen/generate-manpages.bash17
-rwxr-xr-xtools/selinux_baseline.py51
-rwxr-xr-xtools/upgrade/1.3/service_modes.py50
182 files changed, 10566 insertions, 5480 deletions
diff --git a/README b/README
index 7bc3388ce..1d55387ff 100644
--- a/README
+++ b/README
@@ -46,4 +46,5 @@ Want to help
* Wiki: http://bcfg2.org/wiki/Contribute
-Bcfg2 is licensed under BSD, for more details check COPYING.
+Bcfg2 is licensed under a Simplified (2-clause) BSD license, for more
+details check COPYRIGHT.
diff --git a/debian/bcfg2.cron.daily b/debian/bcfg2.cron.daily
index 92e8ff02c..b28b2062b 100644..100755
--- a/debian/bcfg2.cron.daily
+++ b/debian/bcfg2.cron.daily
@@ -1,3 +1,13 @@
#!/bin/sh
-[ -x /usr/lib/bcfg2/bcfg2-cron ] || exit 0
-/usr/lib/bcfg2/bcfg2-cron --daily > /dev/null 2>&1 || true
+BCFG2CRON=
+if [[ -x /usr/libexec/bcfg2-cron ]]; then
+ BCFG2CRON=/usr/libexec/bcfg2-cron
+elif [[ -x /usr/lib/bcfg2/bcfg2-cron ]]; then
+ BCFG2CRON=/usr/lib/bcfg2/bcfg2-cron
+elif type bcfg2-cron >& /dev/null; then
+ BCFG2CRON=bcfg2-cron
+else
+ echo "No bcfg2-cron command found"
+ exit 1
+fi
+$BCFG2CRON --daily 2>&1 | logger -t bcfg2-cron -p daemon.info
diff --git a/debian/bcfg2.cron.hourly b/debian/bcfg2.cron.hourly
index 1fdb9c30e..300792885 100644..100755
--- a/debian/bcfg2.cron.hourly
+++ b/debian/bcfg2.cron.hourly
@@ -1,3 +1,13 @@
#!/bin/sh
-[ -x /usr/lib/bcfg2/bcfg2-cron ] || exit 0
-/usr/lib/bcfg2/bcfg2-cron --hourly > /dev/null 2>&1 || true
+BCFG2CRON=
+if [[ -x /usr/libexec/bcfg2-cron ]]; then
+ BCFG2CRON=/usr/libexec/bcfg2-cron
+elif [[ -x /usr/lib/bcfg2/bcfg2-cron ]]; then
+ BCFG2CRON=/usr/lib/bcfg2/bcfg2-cron
+elif type bcfg2-cron >& /dev/null; then
+ BCFG2CRON=bcfg2-cron
+else
+ echo "No bcfg2-cron command found"
+ exit 1
+fi
+$BCFG2CRON --hourly 2>&1 | logger -t bcfg2-cron -p daemon.info
diff --git a/debian/control b/debian/control
index 7835334da..ae8b6a2d5 100644
--- a/debian/control
+++ b/debian/control
@@ -21,7 +21,7 @@ Description: Configuration management client
Package: bcfg2-server
Architecture: all
-Depends: ${python:Depends}, ${misc:Depends}, python-lxml (>= 0.9), libxml2-utils (>= 2.6.23), lsb-base (>= 3.1-9), ucf, bcfg2 (= ${binary:Version}), openssl, python-ssl | python2.6 | python3.0 | python3.1 | python3.2, python-gamin
+Depends: ${python:Depends}, ${misc:Depends}, python-lxml (>= 0.9), libxml2-utils (>= 2.6.23), lsb-base (>= 3.1-9), ucf, bcfg2 (= ${binary:Version}), openssl, python-ssl | python2.6 | python3.0 | python3.1 | python3.2, python-pyinotify | python-gamin
XB-Python-Version: >= 2.4
Recommends: graphviz, patch
Suggests: python-cheetah, python-genshi (>= 0.4.4), python-profiler, python-sqlalchemy (>= 0.5.0), python-django, mail-transport-agent, bcfg2-doc (= ${binary:Version})
diff --git a/doc/appendix/files/ntp.txt b/doc/appendix/files/ntp.txt
index ec1fa3094..53b3347c8 100644
--- a/doc/appendix/files/ntp.txt
+++ b/doc/appendix/files/ntp.txt
@@ -26,7 +26,7 @@ a client, a profile group, a list of packages, and a base configuration.
.. code-block:: xml
<Clients version='3.0'>
- <Client profile='fedora' pingable='N' pingtime='0' name='foo.bar.com'/>
+ <Client profile='fedora' name='foo.bar.com'/>
</Clients>
``Metadata/groups.xml``:
diff --git a/doc/appendix/guides/nat_howto.txt b/doc/appendix/guides/nat_howto.txt
index 818d3e644..5bd3f7b13 100644
--- a/doc/appendix/guides/nat_howto.txt
+++ b/doc/appendix/guides/nat_howto.txt
@@ -43,14 +43,14 @@ the Client entry in clients.xml will look something like this:
.. code-block:: xml
- <Client profile="desktop" name="test1" pingable="N"
+ <Client profile="desktop" name="test1"
uuid='9001ec29-1531-4b16-8198-a71bea093d0a' location='floating'/>
Alternatively, the Client entry can be setup like this:
.. code-block:: xml
- <Client profile="desktop" name="test1" pingable="N"
+ <Client profile="desktop" name="test1"
uuid='9001ec29-1531-4b16-8198-a71bea093d0a' address='ip-address-of-NAT'/>
The difference between these definitions is explained in detail in the
diff --git a/doc/development/client-driver.txt b/doc/development/client-driver.txt
index 32bb0aff4..c42d2b964 100644
--- a/doc/development/client-driver.txt
+++ b/doc/development/client-driver.txt
@@ -20,7 +20,7 @@ an existing driver, and the process that was used to create it.
* Otherwise, subclass ``Bcfg2.Client.Tools.Tool`` (from here referenced
as branch [T])
-#. Set ``__name__`` to "RPM"
+#. Set ``name`` to "RPM"
#. Add any required executable programs to ``__execs__``
#. Set ``__handles__`` to a list of (**entry.tag**, **entry.get('type')**)
tuples. This determines which entries the Tool module can be used
diff --git a/doc/server/configurationentries.txt b/doc/server/configurationentries.txt
index 8e669b90a..fb1589926 100644
--- a/doc/server/configurationentries.txt
+++ b/doc/server/configurationentries.txt
@@ -1,138 +1,13 @@
.. -*- mode: rst -*-
-.. NOTE: these are relative links (change when directory structure
-.. changes)
-
-.. _Base: plugins/structures/base
-.. _Bundler: plugins/structures/bundler
-.. _Cfg: plugins/generators/cfg.html
-.. _TGenshi: plugins/generators/tgenshi
-.. _TCheetah: plugins/generators/tcheetah.html
-.. _Rules: plugins/generators/rules.html
-
.. _server-configurationentries:
=====================
Configuration Entries
=====================
-This page describes the names and semantics of each of the configuration
-entries used by Bcfg2.
-
-Non-POSIX entries
-=================
-
-+-------------+---------------------+--------------------------------------------+
-| TagName | Description | Attributes |
-+=============+=====================+============================================+
-| Action | Command | name, command, when, timing, status, build |
-+-------------+---------------------+--------------------------------------------+
-| Package | Software Packages | name, type, version, url |
-+-------------+---------------------+--------------------------------------------+
-| PostInstall | PostInstall command | name |
-+-------------+---------------------+--------------------------------------------+
-| Service | System Services | name, type, status, target |
-+-------------+---------------------+--------------------------------------------+
-
-.. note::
-
- PostInstall entries are deprecated in favor of Action entries. In
- fact, a PostInstall entry is simply a specific type of Action.
- Basically, the following are equivalent:
-
- .. code-block:: xml
-
- <PostInstall name='foo'/>
-
- and
-
- .. code-block:: xml
-
- <Action timing='post' when='modified' name='n' command='foo' status='ignore'/>
-
-POSIX entries
-=============
-
-.. versionadded:: 1.0.0
-
-The unified POSIX Path entries prevent inconsistent configuration
-specifications of multiple entries for a given path. The following table
-describes the various types available for new **Path** entries.
-
-The abstract specification of these entries (i.e. In `Bundler`_)
-will only contain a *name* attribute. The type will be added by the
-plugin that handles the entry in the case of `Cfg`_, `TGenshi`_, or
-`TCheetah`_. If the entry is handled by the `Rules`_ plugin (i.e. it is
-a device, directory, hardlink, symlink, etc), then you will specify both
-the *type* and any other necessary attributes in `Rules`_.
-
-Running ``bcfg2-lint`` will check your configuration specification for
-the presence of any mandatory attributes that are necessary for the
-Path type specified.
-
-.. note:: A tool for converting old POSIX entries is available in the
- Bcfg2 source directory at tools/posixunified.py
-
-+-------------+----------------------+-----------------+--------------------------+
-| Type | Replacement/New | Description | Attributes |
-+=============+======================+=================+==========================+
-| device | New | Create block, | name, owner, group, |
-| | | character, and | dev_type |
-| | | fifo devices | (block, char, fifo), |
-| | | | major/minor |
-| | | | (for block/char devices) |
-+-------------+----------------------+-----------------+--------------------------+
-| directory | Replaces Directory | Directories | name, owner, group, |
-| | entries | | perms, prune |
-+-------------+----------------------+-----------------+--------------------------+
-| file | Replaces ConfigFile | Configuration | name, owner, group, |
-| | entries | File | perms, encoding, empty |
-| | | | |
-| | | | **Note:** see below |
-+-------------+----------------------+-----------------+--------------------------+
-| hardlink | New | Create | name, to |
-| | | hardlinks | |
-+-------------+----------------------+-----------------+--------------------------+
-| symlink | Replaces SymLink | SymLinks | name, to |
-| | entries | | |
-+-------------+----------------------+-----------------+--------------------------+
-| ignore | New | Ignore files | name |
-| | | that cause | |
-| | | package | |
-| | | verification | |
-| | | failures | |
-| | | (currently | |
-| | | applies to only | |
-| | | APT and YUMng) | |
-+-------------+----------------------+-----------------+--------------------------+
-| nonexistent | New | Specify a path | name, recursive |
-| | | that should not | |
-| | | exist | |
-+-------------+----------------------+-----------------+--------------------------+
-| permissions | Replaces Permissions | Permissions of | name, owner, group, |
-| | entries | POSIX entities | perms, recursive |
-| | | | |
-+-------------+----------------------+-----------------+--------------------------+
-| vcs | New | Create version | vcstype (git), |
-| | | control | sourceurl, revision |
-| | | checkout | |
-+-------------+----------------------+-----------------+--------------------------+
-
-Keep in mind that permissions for files handled by Cfg/TGenshi/TCheetah
-are still handled via the traditional :ref:`server-info` mechanisms.
-
-Additional information
-----------------------
-
-This section describes some additional behavior relating to POSIX entry
-attributes.
-
-Recursive permissions
-^^^^^^^^^^^^^^^^^^^^^
-
-As per the request in ticket 871, Path type='permissions' entries allow you to
-set a recursive attribute which allows the owner/group to be set recursively
-for a directory.
+The full semantics of each configuration entry is documented with the
+:ref:`server-plugins-generators-rules` plugin.
.. _boundentries:
@@ -178,13 +53,14 @@ Use Cases
Examples
--------
-* Consider the case of ``/etc/hosts`` on linux and ``/etc/inet/hosts`` on
- solaris. These files contain the same data in the same format,
+* Consider the case of ``/etc/hosts`` on linux and ``/etc/inet/hosts``
+ on solaris. These files contain the same data in the same format,
and should typically be synchronized, however, exist in different
locations. Classically, one would need to create one entry for each
- in `Cfg`_ or `TCheetah`_ and perform manual synchronization. Or,
- you could use symlinks and pray. Altsrc is driven from the bundle
- side. For example:
+ in :ref:`server-plugins-generators-cfg` or
+ :ref:`server-plugins-generators-tcheetah` and perform manual
+ synchronization. Or, you could use symlinks and pray. Altsrc is
+ driven from the bundle side. For example:
.. code-block:: xml
@@ -220,10 +96,12 @@ Examples
named "openssl" with different types.
* Finally, consider the case where there exist complicated, but
- completely independent specifications for the same configuration entry
- but different groups of clients. The following bundle will allow the use
- of two different `TCheetah`_ templates ``/etc/firewall-rules-external``
- and ``/etc/firewall-rules-internal`` for different clients based on
+ completely independent specifications for the same configuration
+ entry but different groups of clients. The following bundle will
+ allow the use of two different
+ :ref:`server-plugins-generators-tcheetah` templates
+ ``/etc/firewall-rules-external`` and
+ ``/etc/firewall-rules-internal`` for different clients based on
their group membership.
.. code-block:: xml
@@ -239,11 +117,13 @@ Examples
</Bundle>
* Consider the case where a variety of files can be constructed by a
- single template (`TCheetah`_ or `TGenshi`_). It would be possible to
- copy this template into the proper location for each file, but that
- requires proper synchronization upon modification and knowing up front
- what the files will all be called. Instead, the following bundle allows
- the use of a single template for all proper config file instances.
+ single template (:ref:`server-plugins-generators-tcheetah` or
+ :ref:`server-plugins-generators-tgenshi-index`). It would be
+ possible to copy this template into the proper location for each
+ file, but that requires proper synchronization upon modification and
+ knowing up front what the files will all be called. Instead, the
+ following bundle allows the use of a single template for all proper
+ config file instances.
.. code-block:: xml
@@ -253,5 +133,6 @@ Examples
<Path name='/etc/sysconfig/network-scripts/ifcfg-eth2' altsrc='/etc/ifcfg-template'/>
</Bundle>
- altsrc can be used as a parameter for any entry type, and can be used
- in any structure, including `Bundler`_ and `Base`_.
+ altsrc can be used as a parameter for any entry type, and can be
+ used in any structure, including
+ :ref:`server-plugins-structures-bundler-index`.
diff --git a/doc/server/index.txt b/doc/server/index.txt
index 9c427a0f4..fb1c95444 100644
--- a/doc/server/index.txt
+++ b/doc/server/index.txt
@@ -28,3 +28,4 @@ clients.
info
snapshots/index
bcfg2-info
+ selinux
diff --git a/doc/server/info.txt b/doc/server/info.txt
index d949aab68..d6bcf67e2 100644
--- a/doc/server/info.txt
+++ b/doc/server/info.txt
@@ -1,8 +1,5 @@
.. -*- mode: rst -*-
-.. NOTE: these are relative links (change when directory structure
-.. changes)
-
.. _server-info:
====
@@ -26,24 +23,29 @@ possible fields in an info file are:
+------------+-------------------+----------------------------------+---------+
| Field | Possible values | Description | Default |
+============+===================+==================================+=========+
-| encoding: | ascii | base64 | Encoding of the file. Use | ascii |
+| encoding | ascii | base64 | Encoding of the file. Use | ascii |
| | | base64 for binary files | |
+------------+-------------------+----------------------------------+---------+
-| group: | Any valid group | Sets group of the file | root |
+| owner | Any valid user | Sets owner of the file | root |
+------------+-------------------+----------------------------------+---------+
-| important: | true | false | Important entries are | false |
-| | | installed first during client | |
-| | | execution | |
+| group | Any valid group | Sets group of the file | root |
+------------+-------------------+----------------------------------+---------+
-| owner: | Any valid user | Sets owner of the file | root |
+| perms | Numeric file mode | Sets the permissions of the file | 0644 |
+| | | 'inherit' | (or inherits from the files on | |
+| | | disk if set to 'inherit') | |
+------------+-------------------+----------------------------------+---------+
-| paranoid: | true | false | Backup file before replacement? | true |
+| secontext | A valid SELinux | Sets the SELinux context of the | default |
+| | context string or | file, or sets to the default | |
+| | '__default__' | context set by policy if set to | |
+| | | '__default__' | |
+------------+-------------------+----------------------------------+---------+
-| perms: | Numeric file mode | Sets the permissions of the file | 0644 |
-| | | 'inherit' | (or inherits from the files on | |
-| | | disk if set to inherit) | |
+| important | true | false | Important entries are | false |
+| | | installed first during client | |
+| | | execution | |
+------------+-------------------+----------------------------------+---------+
-| sensitive: | true | false | The contents of sensitive | false |
+| paranoid | true | false | Backup file before replacement? | true |
++------------+-------------------+----------------------------------+---------+
+| sensitive | true | false | The contents of sensitive | false |
| | | entries aren't included in | |
| | | reports | |
+------------+-------------------+----------------------------------+---------+
@@ -54,15 +56,26 @@ A sample info file for CGI script on a web server might look like::
group: www
perms: 0755
+The equivalent ``info.xml`` file would be:
+
+.. code-block:: xml
+
+ <FileInfo>
+ <Info owner="www" group="www" perms="0755"/>
+ </FileInfo>
+
Back to the fstab example again, our final ``Cfg/etc/fstab/`` directory
might look like::
- :info
+ info.xml
fstab
fstab.G50_server
fstab.G99_fileserver
fstab.H_host.example.com
+See :ref:`server-selinux` for more information on the ``secontext``
+attribute and managing SELinux in general.
+
Important attribute
===================
@@ -76,7 +89,7 @@ specification.
+------------+-------------------+----------------------------------+---------+
| Field | Possible values | Description | Default |
+============+===================+==================================+=========+
-| important: | true | false | Important entries are | root |
+| important | true | false | Important entries are | root |
| | | installed first during client | |
| | | execution | |
+------------+-------------------+----------------------------------+---------+
diff --git a/doc/server/plugins/connectors/properties.txt b/doc/server/plugins/connectors/properties.txt
index 7695e902c..b56c2a488 100644
--- a/doc/server/plugins/connectors/properties.txt
+++ b/doc/server/plugins/connectors/properties.txt
@@ -33,25 +33,95 @@ be checked when running ``bcfg2-lint``. For instance, given::
``dns-config.xml`` will be validated against ``dns-config.xsd``.
+Although Properties files are technically freeform XML, the top-level
+XML tag should be ``<Properties>``.
+
Usage
=====
-Specific property files can be referred to in
-templates as ``metadata.Properties[<filename>]``. The
-``xdata`` attribute is an LXML element object. (Documented
-`here <http://codespeak.net/lxml/tutorial.html#the-element-class>`_)
+Specific property files can be referred to in templates as
+``metadata.Properties[<filename>]``. The ``xdata`` attribute is an
+lxml.etree._Element object. (Documented `here
+<http://codespeak.net/lxml/tutorial.html#the-element-class>`_)
+
+In addition to the ``xdata`` attribute that can be used to access the
+raw data, the following access methods are defined:
-Currently, only one access method is defined for this data, ``Match``.
-``Match`` parses the Group and Client tags in the file and returns a
-list of elements that apply to the client described by a set of
-metadata. (See :ref:`server-plugins-structures-bundler-index` for
-more details on how Group and Client tags are parsed.) For instance::
+* ``Match()`` parses the Group and Client tags in the file and returns
+ a list of elements that apply to the client described by a set of
+ metadata. For instance::
{% python
ntp_servers = [el.text
- for el in metadata.Properties['ntp.xml'].Match(metadata):
+ for el in metadata.Properties['ntp.xml'].Match(metadata)
if el.tag == "Server"]
%}
+* ``XMLMatch()`` parses the Group and Client tags in the file and
+ returns an XML document containing only the data that applies to the
+ client described by a set of metadata. (The Group and Client tags
+ themselves are also removed, leaving only the tags and data
+ contained in them.) For instance::
+
+ {% python
+ ntp_servers = [el.text
+ for el in metadata.Properties['ntp.xml'].XMLMatch(metadata).findall("//Server")]
+ %}
+
+As we formulate more common use cases, we will add them to the
+``PropertyFile`` class as methods. This will simplify templates.
+
+You can also access the XML data that comprises a property file
+directly in one of several ways:
+
+* ``metadata.Properties['prop-file'].xdata`` is an lxml.etree._Element
+ object representing the top-level element in the file.
+* ``metadata.Properties['prop-file'].data`` is the raw contents of the
+ property file as a string.
+* ``metadata.Properties['prop-file'].entries`` is a list of
+ lxml.etree._Element objects representing the direct children of the
+ top-level element. (I.e., everything directly under the
+ ``<Properties>`` tag.)
+
+.. _server-plugins-connectors-properties-automatch:
+
+Automatch
+=========
+
+.. versionadded:: 1.3.0
+
+You can enable ``XMLMatch()`` for all Property files by setting
+``automatch`` to ``true`` in the ``[properties]`` section of
+``bcfg2.conf``. This makes ``metadata.Properties`` values
+lxml.etree._Element objects that contain only matching data. (This
+makes it impossible to do
+:ref:`server-plugins-connectors-properties-write-back` as a
+side-effect.)
+
+In Python terms, setting ``automatch=true`` is the same as doing the
+following at the top of each template::
+
+ {% python
+ for prop in metadata.Properties.values():
+ prop = prop.XMLMatch(metadata)
+ %}
+
+The example above that describes ``XMLMatch()`` would then become
+simply::
+
+ {% python
+ ntp_servers = [el.text
+ for el in metadata.Properties['ntp.xml'].findall("//Server")]
+ %}
+
+You can also enable automatch for individual Property files by setting
+the attribute ``automatch="true"`` in the top-level ``<Property>`` tag.
+
+.. _server-plugins-connectors-properties-write-back:
+
+Writing to Properties files
+===========================
+
+.. versionadded:: 1.2.0
If you need to make persistent changes to properties data, you can use
the ``write`` method of the ``PropertyFile`` class::
@@ -68,20 +138,82 @@ the ``write`` method of the ``PropertyFile`` class::
The ``write`` method checks the data in the object against its schema
before writing it; see `Data Structures`_ for details.
-As we formulate more common use cases, we will add them to the
-``PropertyFile`` class as methods. This will simplify templates.
+Note that use of the ``write`` method can cause race conditions if you
+run more than one Bcfg2 server. If you run more than one Bcfg2
+server, you can disable Properties write-back by setting the following
+in ``bcfg2.conf``::
-You can also access the XML data that comprises a property file
-directly in one of several ways:
+ [properties]
+ writes_enabled = false
-* ``metadata.Properties['prop-file'].xdata`` is an lxml.etree._Element
- object representing the top-level element in the file.
-* ``metadata.Properties['prop-file'].data`` is the raw contents of the
- property file as a string.
-* ``metadata.Properties['prop-file'].entries`` is a list of
- lxml.etree._Element objects representing the direct children of the
- top-level element. (I.e., everything directly under the
- ``<Properties>`` tag.)
+.. _server-plugins-connectors-properties-encrypted:
+
+Encrypted Properties data
+=========================
+
+.. versionadded:: 1.3.0
+
+You can encrypt selected data in Properties files to protect that data
+from other people who need access to the repository. See
+:ref:`server-plugins-generators-cfg-configuring-encryption` for
+details on configuring encryption passphrases. The data is decrypted
+transparently on-the-fly by the server; you never need to decrypt the
+data in your templates.
+
+.. note::
+
+ This feature is *not* intended to secure the files against a
+ malicious attacker who has gained access to your Bcfg2 server, as
+ the encryption passphrases are held in plaintext in
+ ``bcfg2.conf``. This is only intended to make it easier to use a
+ single Bcfg2 repository with multiple admins who should not
+ necessarily have access to each other's sensitive data.
+
+Properties files are encrypted on a per-element basis; that is, rather
+than encrypting the whole file, only the character content of
+individual elements is encrypted. This makes it easier to track
+changes to the file in a VCS, and also lets unprivileged users work
+with the other data in the file. Only character content of an element
+can be encrypted; attribute content and XML elements themselves cannot
+be encrypted.
+
+To encrypt a file, use ``bcfg2-crypt``, e.g.::
+
+ bcfg2-crypt foo.xml
+
+If the top-level tag of a Properties file is not ``<Properties>``,
+then you need to use the ``--properties`` flag to ``bcfg2-crypt``::
+
+ bcfg2-crypt --properties foo.xml
+
+The first time you run ``bcfg2-crypt`` on a Properties file, it will
+encrypt all character data of all elements. Additionally, it will add
+``encrypted="<key name>"`` to each element that has encrypted character
+data. It also adds ``encryption="true"`` to the top-level
+``<Properties>`` tag as a flag to the server that it should try to
+decrypt the data in that file. (If you are using Properties schemas,
+you will need to make sure to add support for these attributes.) On
+subsequent runs, only those elements flagged with ``encrypted="*"``
+are encrypted or decrypted.
+
+To decrypt a Properties file, simply re-run ``bcfg2-crypt``::
+
+ bcfg2-crypt foo.xml
+
+This decrypts the encrypted elements, but it does *not* remove the
+``encrypted`` attribute; this way, you can decrypt a Properties
+file, modify the contents, and then simply re-run ``bcfg2-crypt`` to
+encrypt it again. If you added elements that you also want to be
+encrypted, you can either add the ``encrypted`` attribute to
+them manually, or run::
+
+ bcfg2-crypt --xpath '*' foo.xml
+
+You can also use the ``--xpath`` option to specify more restrictive
+XPath expressions to only encrypt a subset of elements, or to encrypt
+different elements with different passphrases. Alternatively, you can
+manally set the ``encrypted`` attribute on various elements and
+``bcfg2-crypt`` will automatically do the right thing.
Accessing Properties contents from TGenshi
==========================================
@@ -89,3 +221,21 @@ Accessing Properties contents from TGenshi
Access contents of ``Properties/auth.xml``::
${metadata.Properties['auth.xml'].xdata.find('file').find('bcfg2.key').text}
+
+Configuration
+=============
+
+``bcfg2.conf`` contains several miscellaneous configuration options
+for the Properties plugin, which can be set in the ``[properties]``
+section. Any booleans in the config file accept the values "1", "yes",
+"true", and "on" for True, and "0", "no", "false", and "off" for
+False.
+
+It understands the following directives:
+
+* ``automatch``: Enable
+ :ref:`server-plugins-connectors-properties-automatch`. Default is
+ false.
+* ``writes_enabled``: Enable
+ :ref:`server-plugins-connectors-properties-write-back`. Default is
+ true.
diff --git a/doc/server/plugins/connectors/puppetenc.txt b/doc/server/plugins/connectors/puppetenc.txt
new file mode 100644
index 000000000..dc472c546
--- /dev/null
+++ b/doc/server/plugins/connectors/puppetenc.txt
@@ -0,0 +1,123 @@
+.. -*- mode: rst -*-
+
+.. _server-plugins-connectors-puppetenc:
+
+=========
+PuppetENC
+=========
+
+PuppetENC is a connector plugin that adds support for Puppet External
+Node Classifiers
+(`<http://docs.puppetlabs.com/guides/external_nodes.html>`_), or ENCs.
+
+Output Format
+=============
+
+The PuppetENC plugin implements the Puppet 2.6.5+ ENC output format
+with some modifications. The basic output format is described `here
+<http://docs.puppetlabs.com/guides/external_nodes.html#puppet-265-and-higher>`_.
+The following modifications apply:
+
+* ``classes`` are considered to be Bcfg2 groups. (This is basically
+ just a difference in terminology between Puppet and Bcfg2; Bcfg2
+ calls "groups" what Puppet calls "classes.")
+* As an alternative to the Puppet-specific ``classes`` value, you may
+ use ``groups`` if you are writing an ENC from scratch specifically
+ for Bcfg2.
+* Since Bcfg2 does not have the notion of parameterized classes, any
+ class parameters provided will be merged in with the ``parameters``
+ dict.
+* ``parameters`` are presented as connector data. (See Usage
+ below.)
+* The ``environment`` value is not supported. If present, PuppetENC
+ will issue a warning and skip it.
+
+The ``parameters`` from separate ENCs are all merged together,
+including parameters from any parameterized classes. This is a
+shallow merge; in other words, only the top-level keys are
+considered. For instance, assuming you had one ENC that produced::
+
+ parameters:
+ ntp_servers:
+ - 0.pool.ntp.org
+ - ntp1.example.com
+
+And another that produced::
+
+ parameters:
+ ntp_servers:
+ - ntp2.example.com
+
+This would result in connector data that included *either* the first
+value of ``ntp_servers`` *or* the second, but not both; this would
+depend on the order in which the ENCs were run, which is
+non-deterministic and should not be relied upon. However, if you add
+one ENC that produced::
+
+ parameters:
+ ntp_servers:
+ - 0.pool.ntp.org
+ - ntp1.example.com
+
+And another that produced::
+
+ parameters:
+ mail_servers:
+ - mail.example.com
+
+Then the connector data would consist of::
+
+ {"ntp_servers": ["0.pool.ntp.org", "ntp1.example.com"],
+ "mail_servers": ["mail.example.com"]}
+
+Usage
+=====
+
+To use the PuppetENC plugin, first do ``mkdir
+/var/lib/bcfg2/PuppetENC``. Add ``PuppetENC`` to your ``plugins``
+line in ``/etc/bcfg2.conf``. Now you can place any ENCs you wish to
+run in ``/var/lib/bcfg2/PuppetENC``. Note that ENCs are run each time
+client metadata is generated, so if you have a large number of ENCs or
+ENCs that are very time-consuming, they could have a significant
+impact on server performance. In that case, it could be worthwhile to
+write a dedicated Connector plugin.
+
+PuppetENC parameters can be accessed in templates as
+``metadata.PuppetENC``, which is a dict of all parameter data merged
+together. For instance, given the following ENC output::
+
+ ---
+ classes:
+ common:
+ puppet:
+ ntp:
+ ntpserver: 0.pool.ntp.org
+ aptsetup:
+ additional_apt_repos:
+ - deb localrepo.example.com/ubuntu lucid production
+ - deb localrepo.example.com/ubuntu lucid vendor
+ parameters:
+ ntp_servers:
+ - 0.pool.ntp.org
+ - ntp.example.com
+ mail_server: mail.example.com
+ iburst: true
+ environment: production
+
+``metadata.PuppetENC`` would contain::
+
+ 'additional_apt_repos': ['deb localrepo.example.com/ubuntu lucid production',
+ 'deb localrepo.example.com/ubuntu lucid vendor'],
+ 'iburst': True,
+ 'mail_server': 'mail.example.com',
+ 'ntp_servers': ['0.pool.ntp.org', 'ntp.example.com'],
+ 'ntpserver': '0.pool.ntp.org'}
+
+(Note that the duplication of NTP server data doesn't make this an
+especially *good* example; it's just the official Puppet example.)
+
+So, in a template you could do something like::
+
+ {% for repo in metadata.PuppetENC['additional_apt_repos'] %}\
+ ${repo}
+ {% end %}\
diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt
index ba02b929d..6c848fddb 100644
--- a/doc/server/plugins/generators/cfg.txt
+++ b/doc/server/plugins/generators/cfg.txt
@@ -35,24 +35,24 @@ templating -- see below).
Group-Specific Files
====================
-It is often that you want one version of a config file for all of your
-machines except those in a particular group. For example, ``/etc/fstab``
-should look alike on all of your desktop machines, but should be
-different on your file servers. Bcfg2 can handle this case through use
-of group-specific files.
+It is often the case that you want one version of a config file for
+all of your machines except those in a particular group. For example,
+``/etc/fstab`` should look alike on all of your desktop machines, but
+should be different on your file servers. Bcfg2 can handle this case
+through use of group-specific files.
As mentioned above, all Cfg entries live in like-named directories
at the end of their directory tree. In the case of fstab, the file at
``Cfg/etc/fstab/fstab`` will be handed out by default to any client that
asks for a copy of ``/etc/fstab``. Group-specific files are located in
-the same directory and are named with the syntax::
+the same directory and are named with the following syntax::
/path/to/filename/filename.GNN_groupname
-in which **NN** is a priority number where **00** is lowest and
-**99** is highest, and **groupname** is the name of a group defined in
+**NN** is a priority number where **00** is lowest and **99**
+is highest, and **groupname** is the name of a group defined in
``Metadata/groups.xml``. Back to our fstab example, we might have a
-``Cfg/etc/fstab/`` directory that looks like::
+``Cfg/etc/fstab/`` directory that looks like this::
fstab
fstab.G50_server
@@ -139,6 +139,99 @@ using different host-specific or group-specific files. For example:
Cfg/etc/fstab/fstab.H_host.example.com.genshi
Cfg/etc/fstab/fstab.G50_server.cheetah
+Encrypted Files
+===============
+
+.. versionadded:: 1.3.0
+
+Bcfg2 allows you to encrypt files stored in ``Cfg/`` to protect the
+data in them from other people who need access to the repository. See
+also :ref:`server-plugins-connectors-properties-encrypted` for
+information on encrypting elements in Properties files, which is often
+more friendly for tracking changes in a VCS.
+
+.. note::
+
+ This feature is *not* intended to secure the files against a
+ malicious attacker who has gained access to your Bcfg2 server, as
+ the encryption passphrases are held in plaintext in
+ ``bcfg2.conf``. This is only intended to make it easier to use a
+ single Bcfg2 repository with multiple admins who should not
+ necessarily have access to each other's sensitive data.
+
+Encrypting Files
+----------------
+
+An encrypted file should end with ``.crypt``, e.g.::
+
+ Cfg/etc/foo.conf
+ Cfg/etc/foo.conf/foo.conf.crypt
+ Cfg/etc/foo.conf/foo.conf.G10_foo.crypt
+
+Encrypted Genshi or Cheetah templates can have the extensions in
+either order, e.g.::
+
+ Cfg/etc/foo.conf/foo.conf.crypt.genshi
+ Cfg/etc/foo.conf/foo.conf.G10_foo.genshi.crypt
+ Cfg/etc/foo.conf/foo.conf.H_bar.example.com.crypt.cheetah
+
+To encrypt a file, you can use ``bcfg2-crypt``, e.g.::
+
+ bcfg2-crypt foo.conf
+
+Once you are satisfied that the file has been encrypted as you wish,
+you can remove the plaintext version, or you can use the ``--remove``
+flag of ``bcfg2-crypt``.
+
+To decrypt a file, simply run ``bcfg2-crypt`` again::
+
+ bcfg2-crypt foo.conf
+
+See the ``bcfg2-crypt`` man page for more information.
+
+``bcfg2-crypt`` simply performs an AES256 encryption, and is
+more-or-less equivalent to the following commands (encryption and
+decryption, respectively::
+
+ openssl enc -aes-256-cbc -k <passphrase> -in foo.conf -out foo.conf.crypt -a
+ openssl enc -d -aes-256-cbc -k <passphrase> -in foo.conf.crypt -out foo.conf -a
+
+.. _server-plugins-generators-cfg-configuring-encryption:
+
+Configuring Encryption
+----------------------
+
+To configure encryption, add a ``[encryption]`` section to
+``bcfg2.conf`` with any number of name-passphrase pairs. When
+decrypting a file, _all_ passphrases will be tried; the passphrase
+name is currently purely cosmetic, but at some point in the future the
+ability to give Bcfg2 a "hint" about which passphrase to use will be
+added.
+
+For instance::
+
+ [encryption]
+ foo_team=P4ssphr4se
+ bar_team=Pa55phra5e
+
+This would define two separate encryption passphrases, presumably for
+use by two separate teams. The passphrase names are completely
+arbitrary.
+
+Note that this does entail a chicken-and-egg problem. In order for
+the Bcfg2 server to be able to decrypt encrypted files, the
+passphrases must exist in ``bcfg2.conf`` in plaintext; but, if you're
+encrypting data, presumably you don't want to include those plaintext
+passphrases in your Bcfg2 repository, so you'll want to encrypt
+``bcfg2.conf``. The best way to solve this is:
+
+#. On your Bcfg2 server, manually add the ``[encryption]`` section to
+ ``bcfg2.conf`` and restart the Bcfg2 server.
+#. Update ``bcfg2.conf`` in your Bcfg2 repository with the
+ passphrases, and encrypt it.
+
+The first (manual) step breaks the mutual dependency.
+
Deltas
======
@@ -237,13 +330,19 @@ For ``sudoers``, a very simple validator is::
This uses the ``visudo`` command's built-in validation.
-.. note:
+If you wish to disable validation, this can be done with the following
+setting in ``bcfg2.conf``::
+
+ [cfg]
+ validation=no
- Before 1.3 is released, it will be possible to disable validation
- in the configuration, but enable it for ``bcfg2-test``. This is
- recommended for heavily-used servers, since running an external
- command is fairly resource intensive and could quickly exhaust the
- file descriptors of a server.
+If you have a very large number of validators, you may wish to disable
+validation by default to avoid slowing down the generation of
+configurations on the server, and use ``bcfg2-test`` (for instance, as
+a post-commit hook or as part of a code review process) to run
+validation. You can do this by setting ``validation=no`` in
+``bcfg2.conf`` as described above, and then calling ``bcfg2-test``
+with the ``--cfg-validation`` flag.
File permissions
================
diff --git a/doc/server/plugins/generators/nagiosgen.txt b/doc/server/plugins/generators/nagiosgen.txt
index b839c10ca..f722909fc 100644
--- a/doc/server/plugins/generators/nagiosgen.txt
+++ b/doc/server/plugins/generators/nagiosgen.txt
@@ -20,7 +20,7 @@ Create the NagiosGen directory::
Create default host, and group specs in:
-* /var/lib/bcfg2/NagiosGen/default-host.cfg::
+``/var/lib/bcfg2/NagiosGen/default-host.cfg``::
define host{
name default
@@ -44,7 +44,7 @@ Create default host, and group specs in:
retry_interval 1
}
-* /var/lib/bcfg2/NagiosGen/default-group.cfg::
+``/var/lib/bcfg2/NagiosGen/default-group.cfg``::
define service{
name default-service
@@ -73,7 +73,7 @@ Create default host, and group specs in:
Create group configuration files (Named identical to Bcfg2 groups) and
add services, and commands specific to the hostgroup (Bcfg2 group) in
-* /var/lib/bcfg2/NagiosGen/base-group.cfg::
+``/var/lib/bcfg2/NagiosGen/base-group.cfg``::
define hostgroup{
hostgroup_name base
@@ -100,7 +100,7 @@ add services, and commands specific to the hostgroup (Bcfg2 group) in
hostgroup_name base
}
-* /var/lib/bcfg2/NagiosGen/web-server-group.cfg::
+``/var/lib/bcfg2/NagiosGen/web-server-group.cfg``::
define hostgroup{
hostgroup_name web-server
diff --git a/doc/server/plugins/generators/packages.txt b/doc/server/plugins/generators/packages.txt
index 62574be76..fc2e30980 100644
--- a/doc/server/plugins/generators/packages.txt
+++ b/doc/server/plugins/generators/packages.txt
@@ -126,17 +126,19 @@ Disabling dependency resolution
.. versionadded:: 1.1.0
-Dependency resolution can be disabled by adding this to
-``Packages/packages.conf`` in the ``global`` section::
+Dependency resolution can be disabled by adding the following setting
+to ``bcfg2.conf`` in the ``packages`` section::
- [global]
+ [packages]
resolver=0
All metadata processing can be disabled as well::
- [global]
+ [packages]
metadata=0
+This setting implies disabling the resolver.
+
Blacklisting faulty dependencies
--------------------------------
@@ -145,7 +147,9 @@ Packages, please file a bug report so that we can fix the problem in
future releases. In the meantime, you can work around this issue by
blacklisting the offending Package in your Sources. The blacklist
element should immediately follow the Component section of your source
-and should look like the following::
+and should look like the following:
+
+.. code-block:: xml
<Blacklist>unwanted-packagename</Blacklist>
@@ -162,7 +166,9 @@ If you have yum libraries installed, Packages can automatically handle
GPG signing keys for Yum and Pulp repositories. (You do not need to
use the native yum resolver; if yum libraries are available, GPG
signing keys can be handled automatically.) Simply specify the URL to
-the GPG key(s) for a repository in ``sources.xml``::
+the GPG key(s) for a repository in ``sources.xml``:
+
+.. code-block:: xml
<Source type="yum"
rawurl="http://mirror.example.com/centos6-x86_64/RPMS.os">
@@ -176,17 +182,53 @@ With the keys specified thusly, Packages will include the keys in the
generated yum config file, and will ensure that the keys are imported
on the client.
-There is no need to specify ``<GPGKey>`` tags for :ref:``Pulp sources
-<pulp-source-support>``; that data is pulled directly from the Pulp
+There is no need to specify ``<GPGKey>`` tags for :ref:`Pulp sources
+<pulp-source-support>`; that data is pulled directly from the Pulp
REST API.
+Arbitrary Repo Options
+----------------------
+
+.. versionadded:: 1.2.3
+
+You can specify arbitrary options to be added to the repository config
+on the server side, if you are using the native yum libraries, and on
+the client side if you are using the ability of Packages to
+automatically generate your Yum config. To do this, add an
+``<Options>`` tag to a Source; all of its attributes will be added
+verbatim to the repository in the generated config. For instance:
+
+.. code-block:: xml
+
+ <Source type="yum" rawurl="http://mirror.example.com/centos-6-os">
+ <Arch>x86_64</Arch>
+ <Options proxy="http://proxy.example.com"/>
+ </Source>
+
+If you are using native yum libraries and need to set options only on
+the Bcfg2 server, you can set the ``serveronly`` attribute to "true";
+or, if you need to set options only on the client, you can set the
+``clientonly`` attribute to "true". For instance, if your Bcfg2
+server needed to use a proxy to access a repo, and you wanted to
+expire metadata caches very quickly on the client, you could do:
+
+.. code-block:: xml
+
+ <Source type="yum" rawurl="http://mirror.example.com/centos-6-os">
+ <Arch>x86_64</Arch>
+ <Options serveronly="true" proxy="http://proxy.example.com"/>
+ <Options clientonly="true" metadata_expire="0"/>
+ </Source>
+
.. _packages-exampleusage:
Example usage
=============
Create a ``sources.xml`` file in the Packages directory that looks
-something like this::
+something like this:
+
+.. code-block:: xml
<Sources>
<Group name="ubuntu-intrepid">
@@ -228,7 +270,9 @@ something like this::
<Source type="apt" essential="false" ...>
-Yum sources can be similarly specified::
+Yum sources can be similarly specified:
+
+.. code-block:: xml
<Sources>
<Group name="centos-5.2">
@@ -248,8 +292,10 @@ Yum sources can be similarly specified::
For sources with a **URL** attribute, the **Version** attribute is
also necessary.
-:ref:``Pulp sources <pulp-source-support>`` are very simple to specify
-due to the amount of data that can be queried from Pulp itself::
+:ref:`Pulp sources <pulp-source-support>` are very simple to specify
+due to the amount of data that can be queried from Pulp itself:
+
+.. code-block:: xml
<Sources>
<Group name="centos-6-x86_64">
@@ -353,20 +399,13 @@ be validated using ``bcfg2-lint``.
.. note:: The schema requires that elements be specified in the above order.
-Limitations
-===========
-
-Packages does not do traditional caching as other plugins
-do. Modifying sources in the Packages ``sources.xml`` file requires a
-server restart for the time being. You do not have to restart the
-server after changing ``packages.conf`` or after adding new sources to
-``sources.xml``.
-
Package Checking and Verification
=================================
In order to do disable per-package verification Pkgmgr style, you will
-need to use :ref:`BoundEntries <boundentries>`, e.g.::
+need to use :ref:`BoundEntries <boundentries>`, e.g.:
+
+.. code-block:: xml
<BoundPackage name="mem-agent" priority="1" version="auto"
type="yum" verify="false"/>
@@ -380,10 +419,10 @@ Generating Client APT/Yum Configurations
.. versionadded:: 1.2.0
The Packages plugin has native support for generating Yum configs.
-You must set ``yum_config`` in ``Packages/packages.conf`` to the path
-to the yum config file you want to generate::
+You must set ``yum_config`` in ``bcfg2.conf`` to the path to the yum
+config file you want to generate::
- [global]
+ [packages]
yum_config=/etc/yum.repos.d/all.repo
Then add the corresponding Path entry to your Yum bundle.
@@ -414,7 +453,7 @@ resolution and other routines so that the Bcfg2 server can be run on a
host that does not support Yum itself. If you run the Bcfg2 server on
a machine that does have Yum libraries, however, you can enable use of
those native libraries in Bcfg2 by setting ``use_yum_libraries`` to
-``1`` in the ``[yum]`` section of ``Packages/packages.conf``.
+``1`` in the ``[packages:yum]`` section of ``bcfg2.conf``.
Benefits to this include:
@@ -440,23 +479,24 @@ Configuring the Yum Helper
Due to poor memory management by the Yum API, the long-lived
bcfg2-server process uses an external short-lived helper,
``bcfg2-yum-helper``, to do the actual Yum API calls for native yum
-library support. By default, Bcfg2 looks for this helper at
-``/usr/sbin/bcfg2-yum-helper``. If you have installed the helper
-elsewhere, you will need to configure that location with the
-``helper`` option in the ``[yum]`` section, e.g.::
+library support. By default, Bcfg2 looks for this helper in
+``$PATH``, or, failing that, at ``/usr/sbin/bcfg2-yum-helper``. If
+you have installed the helper elsewhere, you will need to configure
+that location with the ``helper`` option in the ``[packages:yum]``
+section, e.g.::
- [yum]
+ [packages:yum]
use_yum_libraries = 1
helper = /usr/local/sbin/bcfg2-yum-helper
Setting Yum Options
-------------------
-In ``Packages/packages.conf``, any options you set in the ``[yum]``
+In ``bcfg2.conf``, any options you set in the ``[packages:yum]``
section other than ``use_yum_libraries`` and ``helper`` will be passed
along verbatim to the configuration of the Yum objects used in the
-Bcfg2 server. The following options are set by default, and should
-not generally be overridden:
+Bcfg2 server. The following options are set by default, and should not
+generally be overridden:
* ``cachedir`` is set to a hashed value unique to each distinct Yum
configuration. Don't set this unless you know what you're doing.
@@ -472,14 +512,18 @@ Package Groups
Yum package groups are supported by the native Yum libraries. To
include a package group, use the ``group`` attribute of the
``Package`` tag. You can use either the short group ID or the long
-group name::
+group name:
+
+.. code-block:: xml
<Package group="SNMP Support"/>
<Package group="system-management-snmp"/>
By default, only those packages considered the "default" packages in a
group will be installed. You can change this behavior using the
-"type" attribute::
+"type" attribute:
+
+.. code-block:: xml
<Package group="development" type="optional"/>
<Package group="Administration Tools" type="mandatory"/>
@@ -506,7 +550,9 @@ Pulp Support
Bcfg2 contains explicit support for repositories managed by Pulp
(http://pulpproject.org/). Due to the amount of data about a
repository that can be retrieved directly from Pulp, the only thing
-necessary to configure a Pulp repo is the repo ID::
+necessary to configure a Pulp repo is the repo ID:
+
+.. code-block:: xml
<Sources>
<Group name="centos-6-x86_64">
@@ -521,8 +567,8 @@ server must have a valid ``/etc/pulp/consumer/consumer.conf`` that is
readable by the user your Bcfg2 server runs as; the Pulp server,
URLs, and so on, are determined from this.
-Secondly, in ``Packages/packages.conf`` you must set the following
-options in the ``[pulp]`` section:
+Secondly, in ``bcfg2.conf`` you must set the following
+options in the ``[packages:pulp]`` section:
* ``username`` and ``password``: The username and password of a Pulp
user that will be used to register new clients and bind them to
@@ -643,26 +689,27 @@ multiple data sources need to be multiplexed.
The APT source in ``src/lib/Server/Plugins/Packages.py`` provides a
relatively simple implementation of a source.
-packages.conf
+Configuration
=============
-``packages.conf`` contains miscellaneous configuration options for the
+``bcfg2.conf`` contains miscellaneous configuration options for the
Packages plugin. Any booleans in the config file accept the values
"1", "yes", "true", and "on" for True, and "0", "no", "false", and
-"off" for False
+"off" for False.
It understands the following directives:
-[global] section
-----------------
+[packages] section
+------------------
* ``resolver``: Enable dependency resolution. Default is ``1``
(true). For historical reasons, this also accepts "enabled" and
"disabled".
-* ``metadata``: Enable metadata processing. Default is ``1``
- (true). For historical reasons, this also accepts "enabled" and
- "disabled".
-* ``yum_config``: The path at which to generate Yum configs. No
+* ``metadata``: Enable metadata processing. Default is ``1``
+ (true). If ``metadata`` is disabled, it's implied that ``resolver``
+ is also disabled. For historical reasons, this also accepts
+ "enabled" and "disabled".
+* ``yum_config``: The path at which to generate Yum configs. No
default.
* ``apt_config``: The path at which to generate APT configs. No
default.
@@ -672,18 +719,21 @@ It understands the following directives:
* ``version``: Set the version attribute used when binding
Packages. Default is ``auto``.
-[yum] section
--------------
+[packages:yum] section
+----------------------
* ``use_yum_libraries``: Whether or not to use the :ref:`native yum
library support <native-yum-libraries>`. Default is ``0`` (false).
+* ``helper``: Path to ``bcfg2-yum-helper``. By default, Bcfg2 looks
+ first in ``$PATH`` and then in ``/usr/sbin/bcfg2-yum-helper`` for
+ the helper.
-All other options in the ``[yum]`` section will be passed along
-verbatim to the Yum configuration if you are using the native Yum
-library support.
+All other options in the ``[packages:yum]`` section will be passed
+along verbatim to the Yum configuration if you are using the native
+Yum library support.
-[pulp] section
---------------
+[packages:pulp] section
+-----------------------
* ``username`` and ``password``: The username and password of a Pulp
user that will be used to register new clients and bind them to
diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt
index c084c5681..f693f6e62 100644
--- a/doc/server/plugins/generators/rules.txt
+++ b/doc/server/plugins/generators/rules.txt
@@ -46,6 +46,10 @@ Group membership may be negated.
Tag Attributes in Rules
=======================
+Running ``bcfg2-lint`` will check your configuration specification for
+the presence of any mandatory attributes that are necessary for the
+entry specified.
+
Rules Tag
---------
@@ -118,7 +122,14 @@ Service Tag
+------------+-------------------------------+---------------------------------------------------------+
| Name | Description | Values |
+============+===============================+=========================================================+
-| mode | Per Service Mode (New in 1.0) | (manual | default | supervised | interactive_only ) |
+| restart | Whether to restart the | ( true | false | interactive ) |
+| | service when the bundle | |
+| | changes (new in 1.3; replaces | |
+| | "mode" attribute) | |
++------------+-------------------------------+---------------------------------------------------------+
+| install | Whether to install the | ( true | false ) |
+| | service (new in 1.3; replaces | |
+| | "mode" attribute) | |
+------------+-------------------------------+---------------------------------------------------------+
| name | Service name or regular | String or regex |
| | expression | |
@@ -139,29 +150,33 @@ Service Tag
| | (Upstart services only) | |
+------------+-------------------------------+---------------------------------------------------------+
-Service mode descriptions
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. versionadded:: 1.0.0
-
-* manual
+Service mode specification
+^^^^^^^^^^^^^^^^^^^^^^^^^^
- * do not start/stop/restart this service
- * service installation is not performed
+.. versionadded:: 1.3.0
-* interactive_only
+In the 1.3.0 release, the "mode" attribute has been replaced by a pair
+of attributes, "restart" and "install," which control how a service is
+handled more granularly than the old "mode" attribute. The old "mode"
+attribute values are equivalent as follows:
- * only attempt to start/stop/restart this service if the client is run interactively
- * service installation is performed
++-----------------------------+------------------------------------------+
+| Mode attribute | Equivalent |
++=============================+==========================================+
+| ``mode="default"`` | ``restart="true" install="true"`` |
++-----------------------------+------------------------------------------+
+| ``mode="interactive_only"`` | ``restart="interactive" install="true"`` |
++-----------------------------+------------------------------------------+
+| ``mode="supervised"`` | ``restart="true" install="true"`` |
++-----------------------------+------------------------------------------+
+| ``mode="manual"`` | ``restart="false" install="false"`` |
++-----------------------------+------------------------------------------+
-* default
+The default is ``restart="true" install="true"``
- * perform appropriate service operations
-
-* supervised
-
- * default and ensure service is running (or stopped) when verification is performed
- * deprecates supervised='true'
+Previously, "supervised" could be used to start a service during the
+verification phase; this is no longer supported. Services that have
+been stopped on a client will be started during the install phase.
Service status descriptions
^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -206,6 +221,12 @@ The Path tag has different values depending on the *type* attribute of
the path specified in your configuration. Below is a set of tables which
describe the attributes available for various Path types.
+Note that ``secontext`` below expects a full context, not just the
+type. For instance, "``system_u:object_r:etc_t:s0``", not just
+``etc_t``. You can also specify "``__default__``", which will restore
+the context of the file to the default set by policy. See
+:ref:`server-selinux` for more information.
+
Attributes common to all Path tags:
+----------+---------------------------------------------------+-----------------+
@@ -218,46 +239,58 @@ Attributes common to all Path tags:
device
^^^^^^
-+----------+---------------------+-------------------+
-| Name | Description | Values |
-+==========+=====================+===================+
-| dev_type | Type of device | (block|char|fifo) |
-+----------+---------------------+-------------------+
-| owner | Device owner | String |
-+----------+---------------------+-------------------+
-| group | Device group | String |
-+----------+---------------------+-------------------+
-| major | Major number (block | integer |
-| | or char devices) | |
-+----------+---------------------+-------------------+
-| minor | Minor number (block | integer |
-| | or char devices) | |
-+----------+---------------------+-------------------+
++-----------+---------------------+-------------------+
+| Name | Description | Values |
++===========+=====================+===================+
+| dev_type | Type of device | (block|char|fifo) |
++-----------+---------------------+-------------------+
+| owner | Device owner | String |
++-----------+---------------------+-------------------+
+| group | Device group | String |
++-----------+---------------------+-------------------+
+| secontext | SELinux context | String |
++-----------+---------------------+-------------------+
+| major | Major number (block | integer |
+| | or char devices) | |
++-----------+---------------------+-------------------+
+| minor | Minor number (block | integer |
+| | or char devices) | |
++-----------+---------------------+-------------------+
directory
^^^^^^^^^
-+-------+------------------------------+------------+
-| Name | Description | Values |
-+=======+==============================+============+
-| perms | Permissions of the directory | String |
-+-------+------------------------------+------------+
-| owner | Owner of the directory | String |
-+-------+------------------------------+------------+
-| group | Group Owner of the directory | String |
-+-------+------------------------------+------------+
-| prune | prune unspecified entries | true|false |
-| | from the Directory | |
-+-------+------------------------------+------------+
++-----------+------------------------------+------------+
+| Name | Description | Values |
++===========+==============================+============+
+| perms | Permissions of the directory | String |
++-----------+------------------------------+------------+
+| owner | Owner of the directory | String |
++-----------+------------------------------+------------+
+| group | Group Owner of the directory | String |
++-----------+------------------------------+------------+
+| secontext | SELinux context | String |
++-----------+------------------------------+------------+
+| prune | prune unspecified entries | true|false |
+| | from the Directory | |
++-----------+------------------------------+------------+
hardlink
^^^^^^^^
-+------+----------------------+--------+
-| Name | Description | Values |
-+======+======================+========+
-| to | File to link to | String |
-+------+----------------------+--------+
++-----------+------------------------------+--------+
+| Name | Description | Values |
++===========+==============================+========+
+| to | File to link to | String |
++-----------+------------------------------+--------+
+| perms | Permissions of the directory | String |
++-----------+------------------------------+--------+
+| owner | Owner of the directory | String |
++-----------+------------------------------+--------+
+| group | Group Owner of the directory | String |
++-----------+------------------------------+--------+
+| secontext | SELinux context | String |
++-----------+------------------------------+--------+
nonexistent
^^^^^^^^^^^
@@ -274,15 +307,17 @@ nonexistent
permissions
^^^^^^^^^^^
-+-------+--------------------------+--------+
-| Name | Description | Values |
-+=======+==========================+========+
-| perms | Permissions of the file. | String |
-+-------+--------------------------+--------+
-| owner | Owner of the file. | String |
-+-------+--------------------------+--------+
-| group | Group of the file. | String |
-+-------+--------------------------+--------+
++-----------+--------------------------+--------+
+| Name | Description | Values |
++===========+==========================+========+
+| perms | Permissions of the file. | String |
++-----------+--------------------------+--------+
+| owner | Owner of the file. | String |
++-----------+--------------------------+--------+
+| group | Group of the file. | String |
++-----------+--------------------------+--------+
+| secontext | SELinux context | String |
++-----------+--------------------------+--------+
symlink
^^^^^^^
@@ -293,6 +328,141 @@ symlink
| to | File to link to | String |
+------+----------------------+--------+
+SELinux Tag
+-----------
+
+The SELinux tag has different values depending on the *type* attribute
+of the SELinux entry specified in your configuration. Below is a set
+of tables which describe the attributes available for various SELinux
+types. The types (except for ``module``) correspond to ``semanage``
+subcommands.
+
+Note that the ``selinuxtype`` attribute takes only an SELinux type,
+not a full context; e.g., "``etc_t``", not
+"``system_u:object_r:etc_t:s0``".
+
+As it can be very tedious to create a baseline of all existing SELinux
+entries, you can use ``selinux_baseline.py`` located in the ``tools/``
+directory to do that for you.
+
+In certain cases, it may be necessary to create multiple SELinux
+entries with the same name. For instance, "root" is both an SELinux
+user and an SELinux login record; or a given fcontext may need two
+different SELinux types depending on whether it's a symlink or a plain
+file. In these (few) cases, it is necessary to create BoundSELinux
+entries directly in Bundler rather than using abstract SELinux entries
+in Bundler and binding them with Rules.
+
+See :ref:`server-selinux` for more information.
+
+boolean
+^^^^^^^
+
++-------+----------------------+---------+----------+
+| Name | Description | Values | Required |
++=======+======================+=========+==========+
+| name | Name of the boolean | String | Yes |
++-------+----------------------+---------+----------+
+| value | Value of the boolean | on|off | Yes |
++-------+----------------------+---------+----------+
+
+port
+^^^^
+
++-------------+------------------------+---------------------------+----------+
+| Name | Description | Values | Required |
++=============+========================+===========================+==========+
+| name | Port number or range | ``<port>/<proto>`` or | Yes |
+| | and protocol (tcp|udp) | ``<start>-<end>/<proto>`` | |
++-------------+------------------------+---------------------------+----------+
+| selinuxtype | SELinux type to apply | String | Yes |
+| | to this port | | |
++-------------+------------------------+---------------------------+----------+
+
+fcontext
+^^^^^^^^
+
++-------------+-------------------------+---------------------+----------+
+| Name | Description | Values | Required |
++=============+=========================+=====================+==========+
+| name | File specification | String | Yes |
++-------------+-------------------------+---------------------+----------+
+| selinuxtype | SELinux type to apply | String | Yes |
+| | to files matching this | | |
+| | specification | | |
++-------------+-------------------------+---------------------+----------+
+| filetype | File type to match. | (regular|directory| | No |
+| | Default: all | symlink|pipe|all| | |
+| | | socket|block|char) | |
++-------------+-------------------------+---------------------+----------+
+
+node
+^^^^
+
++-------------+------------------------------------+------------------+----------+
+| Name | Description | Values | Required |
++=============+====================================+==================+==========+
+| name | IP address and netmask of node. | <addr>/<netmask> | Yes |
+| | Netmask can be numeric (/16) or | | |
+| | dotted-quad (/255.255.0.0) | | |
++-------------+------------------------------------+------------------+----------+
+| selinuxtype | SELinux type to apply to this node | String | Yes |
++-------------+------------------------------------+------------------+----------+
+| proto | Protocol | (ipv4|ipv6) | Yes |
++-------------+------------------------------------+------------------+----------+
+| netmask | Netmask | String | Yes |
++-------------+------------------------------------+------------------+----------+
+
+login
+^^^^^
+
++-------------+-------------------------------+-----------+----------+
+| Name | Description | Values | Required |
++=============+===============================+===========+==========+
+| name | Unix username | String | Yes |
++-------------+-------------------------------+-----------+----------+
+| selinuxuser | SELinux username | String | Yes |
++-------------+-------------------------------+-----------+----------+
+
+user
+^^^^
+
++-------------+-------------------------------+-----------+----------+
+| Name | Description | Values | Required |
++=============+===============================+===========+==========+
+| name | SELinux username | String | Yes |
++-------------+-------------------------------+-----------+----------+
+| roles | Space-separated list of roles | String | No |
++-------------+-------------------------------+-----------+----------+
+| prefix | Home directory context prefix | String | No |
++-------------+-------------------------------+-----------+----------+
+
+interface
+^^^^^^^^^
+
++-------------+-------------------------+-------------+----------+
+| Name | Description | Values | Required |
++=============+=========================+=============+==========+
+| name | Interface name | String | Yes |
++-------------+-------------------------+-------------+----------+
+| selinuxtype | SELinux type to apply | String | Yes |
+| | to this interface | | |
++-------------+-------------------------+-------------+----------+
+
+permissive
+^^^^^^^^^^
+
++-------------+------------------------------------+-------------+----------+
+| Name | Description | Values | Required |
++=============+====================================+=============+==========+
+| name | SELinux type to make permissive | String | Yes |
++-------------+------------------------------------+-------------+----------+
+
+module
+^^^^^^
+
+See :ref:`server-plugins-generators-semodules`
+
Rules Directory
===============
@@ -356,14 +526,11 @@ Using Regular Expressions in Rules
If you wish, you can configure the Rules plugin to support regular
expressions. This entails a small performance and memory usage
-penalty. To do so, create a file, "Rules/rules.conf", and add the
-following text::
+penalty. To do so, add the following setting to ``bcfg2.conf``::
[rules]
regex = yes
-You will have to restart the Bcfg2 server after making that change.
-
With regular expressions enabled, you can use a regex in the ``name``
attribute to match multiple abstract configuration entries.
@@ -372,4 +539,4 @@ name="bcfg2".../>`` will *not* match a Service named ``bcfg2-server``;
you'd have to explicitly specify ``<Service name="bcfg2.*".../>``.
Note that only one Rule can apply to any abstract entry, so you cannot
-specify multiple regexs to match the same rule.
+specify multiple regexes to match the same rule.
diff --git a/doc/server/plugins/generators/semodules.txt b/doc/server/plugins/generators/semodules.txt
new file mode 100644
index 000000000..0d725fc1a
--- /dev/null
+++ b/doc/server/plugins/generators/semodules.txt
@@ -0,0 +1,66 @@
+.. -*- mode: rst -*-
+
+.. _server-plugins-generators-semodules:
+
+=========
+SEModules
+=========
+
+.. versionadded:: 1.3.0
+
+The SEModules plugin handles SELinux module entries. It supports
+group- and host-specific module versions, and enabling/disabling
+modules.
+
+You can use ``selinux_baseline.py`` located in the tools/ directory to
+create a baseline of all of your installed modules.
+
+See :ref:`server-selinux` for more information.
+
+Usage
+=====
+
+To use the SEModules plugin, first do ``mkdir
+/var/lib/bcfg2/SEModules``. Add ``SEModules`` to your ``plugins``
+line in ``/etc/bcfg2.conf`` and restart bcfg2-server.
+
+The SEModules directory contains modules in a layout similar to the
+Cfg plugin: at the top level, SEModules should contain directories
+named after the modules you want to install, and each of those
+directories can contain a global module, plus any number of group- and
+host-specific modules. For instance:
+
+ $ ls -F SEModules
+ foo.pp/ bar.pp/
+ $ ls SEModules/foo.pp/
+ foo.pp
+ foo.pp.G50_server
+ foo.pp.H_baz.example.com
+
+For more information on this directory layout, see
+:ref:`server-plugins-generators-cfg`.
+
+Entries
+=======
+
+SEModules handles ``<SELinux>`` entries with the ``module`` type. For
+instance:
+
+.. code-block:: xml
+
+ <Bundle name="foo">
+ <SELinux type="module" name="foo.pp"/>
+ </Bundle>
+
+The ``.pp`` extension is optional.
+
+.. note::
+
+ If you use a ``BoundSELinux`` tag, you must *not* include the
+ ``.pp`` extension. This is not recommend, though.
+
+You can also install a disabled module:
+
+.. code-block:: xml
+
+ <SELinux type="module" name="foo" disabled="true"/>
diff --git a/doc/server/plugins/generators/sslca.txt b/doc/server/plugins/generators/sslca.txt
index 8e33148cb..d2b051535 100644
--- a/doc/server/plugins/generators/sslca.txt
+++ b/doc/server/plugins/generators/sslca.txt
@@ -33,7 +33,7 @@ must contain full (not relative) paths.
#. Add SSLCA to the **plugins** line in ``/etc/bcfg2.conf`` and restart the
server -- This enabled the SSLCA plugin on the Bcfg2 server.
-#. Add a section to your ``/etc/bcfg2.conf`` called sslca_foo, replacing foo
+#. Add a section to your ``/etc/bcfg2.conf`` called ``sslca_foo``, replacing foo
with the name you wish to give your CA so you can reference it in certificate
definitions.
@@ -51,6 +51,12 @@ must contain full (not relative) paths.
specification. If you're using a self signing CA this would be the CA cert
that you generated.
+#. Optionally, add ``verify_certs = false`` if you don't wish to
+ perform certificate verification on the certs SSLCA generates.
+ Verification includes ``openssl verify`` to verify the CA chain,
+ and ensuring that both the key file and certificate file contain
+ the same key.
+
#. Once all this is done, you should have a section in your ``/etc/bcfg2.conf``
that looks similar to the following::
diff --git a/doc/server/plugins/generators/tgenshi/index.txt b/doc/server/plugins/generators/tgenshi/index.txt
index 21ef8f17f..0d4a7ffb0 100644
--- a/doc/server/plugins/generators/tgenshi/index.txt
+++ b/doc/server/plugins/generators/tgenshi/index.txt
@@ -17,7 +17,7 @@ on the client in the created files.
To begin, you will need to download and install the Genshi templating engine.
-To install on CentOS or RHEL 5, run::
+To install on CentOS or RHEL, run::
sudo yum install python-genshi
diff --git a/doc/server/plugins/grouping/metadata.txt b/doc/server/plugins/grouping/metadata.txt
index 305857578..2c05e9e7e 100644
--- a/doc/server/plugins/grouping/metadata.txt
+++ b/doc/server/plugins/grouping/metadata.txt
@@ -37,11 +37,11 @@ describe one host. A sample file is below:
.. code-block:: xml
<Clients version="3.0">
- <Client profile="backup-server" pingable="Y" pingtime="0" name="backup.example.com"/>
- <Client profile="console-server" pingable="Y" pingtime="0" name="con.example.com"/>
- <Client profile="kerberos-master" pingable="Y" pingtime="0" name="kdc.example.com"/>
- <Client profile="mail-server" pingable="Y" pingtime="0" name="mail.example.com"/>
- <Client name='foo' address='10.0.0.1' pingable='N' pingtime='-1'>
+ <Client profile="backup-server" name="backup.example.com"/>
+ <Client profile="console-server" name="con.example.com"/>
+ <Client profile="kerberos-master" name="kdc.example.com"/>
+ <Client profile="mail-server" name="mail.example.com"/>
+ <Client name='foo' address='10.0.0.1'>
<Alias name='foo-mgmt' address='10.1.0.1'/>
</Client>
</Clients>
@@ -93,12 +93,6 @@ Additionally, the following properties can be specified:
| | can be used instead of the global | |
| | password. | |
+----------+----------------------------------------+----------------+
-| pingable | If the client is pingable (deprecated; | Y|N |
-| | for old reporting system) | |
-+----------+----------------------------------------+----------------+
-| pingtime | Last time the client was pingable | String |
-| | (deprecated; for old reporting system) | |
-+----------+----------------------------------------+----------------+
| secure | Requires the use of the per-client | true|false |
| | password for this client. | |
+----------+----------------------------------------+----------------+
diff --git a/doc/server/plugins/misc/trigger.txt b/doc/server/plugins/misc/trigger.txt
index 7547f2fdd..224b7444b 100644
--- a/doc/server/plugins/misc/trigger.txt
+++ b/doc/server/plugins/misc/trigger.txt
@@ -6,8 +6,8 @@
Trigger
=======
-Trigger is a plugin that calls external scripts (on the server) when
-clients are configured.
+Trigger is a plugin that calls external scripts (on the server) at the
+end of each client run.
Setup
=====
diff --git a/doc/server/plugins/plugin-roles.txt b/doc/server/plugins/plugin-roles.txt
index 2ce7e21ff..58dc36296 100644
--- a/doc/server/plugins/plugin-roles.txt
+++ b/doc/server/plugins/plugin-roles.txt
@@ -6,124 +6,41 @@
Plugin Roles
============
-This documents available plugin roles.
-
-1. list of plugin roles
-
- +---------------+--------------------+--------+
- | Role | Class | Status |
- +===============+====================+========+
- | Metadata | Metadata | done |
- +---------------+--------------------+--------+
- | Connector | Connector | done |
- +---------------+--------------------+--------+
- | Probing | Probing | done |
- +---------------+--------------------+--------+
- | Structure | Structure | done |
- +---------------+--------------------+--------+
- | Structure Val | StructureValidator | done |
- +---------------+--------------------+--------+
- | Generator | Generator | done |
- +---------------+--------------------+--------+
- | Goals Val | GoalValidator | done |
- +---------------+--------------------+--------+
- | Statistics | Statistics | done |
- +---------------+--------------------+--------+
- | Pull Source | PullSource | done |
- +---------------+--------------------+--------+
- | Pull Target | PullTarget | done |
- +---------------+--------------------+--------+
- | Version | Version | done |
- +---------------+--------------------+--------+
- | Decision | Decision | done |
- +---------------+--------------------+--------+
- | Remote | Remote | none |
- +---------------+--------------------+--------+
- | Syncing | Syncing | none |
- +---------------+--------------------+--------+
-
-2. Plugin Capabilities
-
- * Metadata
-
- * Initial metadata construction
- * Connector data accumulation
- * ClientMetadata instance delivery
- * Introspection interface (for bcfg2-info & co)
-
- * Connector
-
- * Provide additional data for ClientMetadata instances
-
- * Probing
-
- * send executable probes to clients and receive data responses
-
- * Structure
-
- * Produce a list of configuration entries that should be included in client configurations
- * Each structure plugin is produces a list of structures
- * Core verifies that each bundle listed has been constructed
-
- * Structure Validation
-
- * Validate a client entry list's internal consistency, modifying if needed
-
- * Generator
- * Goals Validation
-
- * Validate client goals, modifying if needed
-
- * Pull Source
-
- * Plugin can provide entry information about clients
-
- * Pull Target
-
- * Plugin can accept entry data and merge it into the specification
-
- * Version
-
- * Plugin can read revision information from VCS of choice
- * Will provide an interface for producing commits made by the bcfg2-server
-
- * Decision
-
-3. Configuration of plugins
-
- Plugin configuration will be simplified substantially. Now, a single
- list of plugins (including plugins of all capabilities) is specified
- upon startup (either via bcfg2.conf or equivalent). This mechanism
- replaces the current split configuration mechanism where generators,
- structures, and other plugins are listed independently. Instead, all
- plugins included in the startup list will be initialized, and each
- will be enabled in all roles that it supports. This will remove a
- current source of confusion and potential configuration errors,
- wherein a plugin is enabled for an improper set of goals. (ie Cfg
- enabled as a structure, etc) This does remove the possibility of
- partially enabling a plugin for one of its roles without activating it
- across the board, but I think this is a corner case, which will be
- poorly supported by plugin implementers. If needed, this use case can
- be explicitly supported by the plugin author, through use of a config
- file directive.
-
-4. User Visible Changes
-
- Connector data is added to ClientMetadata instances using the name of
- the connector plugin. This means that the dictionary of key/val probe
- pairs included with metadata is now available as metadata.Probes
- (instead of metadata.probes). Once properties are available the same
- way, they will likewise change names to metadata.Properties from their
- current name.
-
- Plugin configuration will change. A single field "plugins" in
- bcfg2.conf will supercede the combination of the "generators" and
- "structures" fields.
-
- Default loading of needed plugins is now explicit; this means that
- Statistics (if used) should be listed in the plugins line of
- bcfg2.conf.
-
-5. Notes
-
- * Need to ensure bundle accumulation occurs with connector groups
+* Metadata
+ * Initial metadata construction
+ * Connector data accumulation
+ * ClientMetadata instance delivery
+ * Introspection interface (for bcfg2-info & co)
+* Connector
+ * Provide additional data for ClientMetadata instances
+* Probing
+ * send executable probes to clients and receive data responses
+* Structure
+ * Produce a list of configuration entries that should be included in
+ client configurations
+ * Each structure plugin is produces a list of structures
+ * Core verifies that each bundle listed has been constructed
+* StructureValidator
+ * Validate a client entry list's internal consistency, modifying if
+ needed
+* Generator
+* GoalValidator
+ * Validate client goals, modifying if needed
+* PullSource
+ * Plugin can provide entry information about clients
+* PullTarget
+ * Plugin can accept entry data and merge it into the specification
+* Version
+ * Plugin can read revision information from VCS of choice
+ * Will provide an interface for producing commits made by the bcfg2-server
+* Decision
+* ClientRunHooks
+ * Provides hooks executed at the start and end of each client run
+
+Configuration of plugins
+========================
+
+A single list of plugins (including plugins of all capabilities) is
+specified upon startup (either via bcfg2.conf or equivalent). All
+plugins included in the startup list are initialized, and each is
+enabled in all roles that it supports.
diff --git a/doc/server/plugins/probes/index.txt b/doc/server/plugins/probes/index.txt
index 95aa2d0ce..cacc42bc1 100644
--- a/doc/server/plugins/probes/index.txt
+++ b/doc/server/plugins/probes/index.txt
@@ -211,7 +211,7 @@ look something like:
<FileProbe name="/etc/blah.conf" update="true"/>
</Group>
<Client name="bar.example.com">
- <FileProbe name="/var/lib/bar.gz" base64="true"/>
+ <FileProbe name="/var/lib/bar.gz" encoding="base64"/>
</Client>
</FileProbes>
diff --git a/doc/server/selinux.txt b/doc/server/selinux.txt
new file mode 100644
index 000000000..0cbf0985e
--- /dev/null
+++ b/doc/server/selinux.txt
@@ -0,0 +1,97 @@
+.. -*- mode: rst -*-
+
+.. _server-selinux:
+
+=======
+SELinux
+=======
+
+.. versionadded:: 1.3.0
+
+Bcfg2 has the ability to handle the majority of SELinux entries with
+the ``SELinux`` entry type, which handles modules (with the
+:ref:`server-plugins-generators-semodules` plugin), file contexts,
+users and user mappings, permissive domains, nodes, and interfaces.
+In addition, ``info.xml`` files and most types of the ``Path`` tag can
+accept an ``secontext`` attribute to set the context of that entry.
+The full semantics of each configuration entry is documented with the
+:ref:`server-plugins-generators-rules` plugin.
+
+.. note:: The ``secontext`` attribute takes a *full* context,
+ e.g., "``system_u:object_r:etc_t:s0``"; the ``selinuxtype``
+ attribute always takes *only* an SELinux type, e.g.,
+ "``etc_t``". ``secontext`` (but not ``selinuxtype``) can
+ also accept the special value "``__default__``", which will
+ restore the context on the Path entry in question to the
+ default supplied by the SELinux policy.
+
+In its current version, the SELinux support in Bcfg2 is not sufficient
+to manage MCS/MLS policies.
+
+Extra Entries
+=============
+
+As it can be very tedious to create a baseline of all existing SELinux
+entries, you can use ``selinux_baseline.py`` located in the ``tools/``
+directory to do that for you.
+
+The actual definition of an "extra" entry actually depends on the
+version of SELinux available; the SELinux APIs have been extremely
+fluid, so many features available in newer versions are not available
+in older versions. Newer SELinux versions (e.g., in recent versions
+of Fedora) can be queried for only entries that have been locally
+modified; on these versions of SELinux, only locally modified entries
+will be considered extra. On older SELinux versions (e.g., on RHEL
+5), however, that functionality is missing, so *all* SELinux entries
+will be considered extra, making ``selinux_baseline.py`` quite
+necessary.
+
+``selinux_baseline.py`` writes a bundle to stdout that contains
+``BoundSELinux`` entries for the appropriate SELinux entities. It
+does this rather than separate Bundle/Rules files because of the
+:ref:`server-selinux-duplicate-entries` problem.
+
+.. _server-selinux-duplicate-entries:
+
+Duplicate Entries
+=================
+
+In certain cases, it may be necessary to create multiple SELinux
+entries with the same name. For instance, "root" is both an SELinux
+user and an SELinux login record, so to manage both, you would have
+the following in Bundler:
+
+.. code-block:: xml
+
+ <SELinux name="root"/>
+ <SELinux name="root"/>
+
+And in Rules:
+
+.. code-block:: xml
+
+ <SELinux type="login" selinuxuser="root" name="root"/>
+ <SELinux type="user" prefix="user" name="root"
+ roles="system_r sysadm_r user_r"/>
+
+But Rules has no way to tell which "root" is which, and you will get
+errors. In these cases, it is necessary to use ``BoundSELinux`` tags
+directly in Bundler. (See :ref:`boundentries` for more details on
+bound entries.) For instance:
+
+.. code-block:: xml
+
+ <BoundSELinux type="login" selinuxuser="root" name="root"/>
+ <BoundSELinux type="user" prefix="user" name="root"
+ roles="system_r sysadm_r user_r"/>
+
+It may also be necessary to use ``BoundSELinux`` tags if a single
+fcontext needs two different SELinux types depending on whether it's a
+symlink or a plain file. For instance:
+
+.. code-block:: xml
+
+ <BoundSELinux type="fcontext" filetype="symlink"
+ name="/etc/localtime" selinuxtype="etc_t"/>
+ <BoundSELinux type="fcontext" filetype="regular"
+ name="/etc/localtime" selinuxtype="locale_t"/>
diff --git a/gentoo/bcfg2-1.2.2.ebuild b/gentoo/bcfg2-1.3.0.ebuild
index c054446fe..68bfa24ea 100644
--- a/gentoo/bcfg2-1.2.2.ebuild
+++ b/gentoo/bcfg2-1.3.0.ebuild
@@ -1,14 +1,15 @@
-# Copyright 1999-2011 Gentoo Foundation
+# Copyright 1999-2012 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
-# $Header: /var/cvsroot/gentoo-x86/app-admin/bcfg2/bcfg2-1.2.0.ebuild,v 1.1 2011/12/28 07:56:20 xmw Exp $
+# $Header: $
+
+EAPI="4"
-EAPI="3"
PYTHON_DEPEND="2:2.6"
SUPPORT_PYTHON_ABIS="1"
# ssl module required.
RESTRICT_PYTHON_ABIS="2.4 2.5 3.*"
-inherit distutils
+inherit distutils eutils
DESCRIPTION="configuration management tool"
HOMEPAGE="http://bcfg2.org"
@@ -17,16 +18,17 @@ SRC_URI="ftp://ftp.mcs.anl.gov/pub/bcfg/${P}.tar.gz"
LICENSE="BSD"
SLOT="0"
KEYWORDS="~amd64 ~x86 ~amd64-linux ~x86-linux ~x64-solaris"
-IUSE="doc genshi server"
+IUSE="doc cheetah genshi server"
DEPEND="dev-python/setuptools
doc? ( dev-python/sphinx )"
RDEPEND="app-portage/gentoolkit
+ cheetah? ( dev-python/cheetah )
genshi? ( dev-python/genshi )
server? (
virtual/fam
dev-python/lxml
- dev-libs/libgamin[python] )"
+ || ( dev-python/pyinotify dev-libs/libgamin[python] ) )"
PYTHON_MODNAME="Bcfg2"
diff --git a/man/bcfg2-admin.8 b/man/bcfg2-admin.8
index 6607344d8..a863151d4 100644
--- a/man/bcfg2-admin.8
+++ b/man/bcfg2-admin.8
@@ -1,214 +1,208 @@
-.TH "bcfg2-admin" 8
-.SH NAME
-bcfg2-admin \- Perform repository administration tasks
-.SH SYNOPSIS
-.B bcfg2-admin
-.I [-C config-file]
-.I <mode>
-.I <mode args>
-.I <mode options>
-.SH DESCRIPTION
-.PP
-.B bcfg2-admin
-Perform Bcfg2 repository administration
-.SH OPTIONS
-.PP
-.B \-C <config-file>
-.RS
-Specify the location of the configuration file (if it is not in
-/etc/bcfg2.conf).
-.RE
-.SH MODES
-.PP
-.B init
-.RS
-Initialize a new repository (interactive).
-.RE
-.B backup
-.RS
-Create an archive of the whole Bcfg2 repository.
-.RE
-.B bundle <action>
-.RS
-Display details about the available bundles.
-.RE
-.B client <action> <client> [attribute=value]
-.RS
-Add, edit, or remove clients entries in metadata.
-.RE
-.B query [g=group] [p=profile] [-f output-file] [-n] [-c]
-.RS
-Search for clients based on group or profile.
-.RE
-.B compare <old> <new>
-.RS
-Compare two client configurations. Can be used to verify consistent
-behavior between releases. Determine differences between files or
-directories.
-.RE
-.B minestruct <client> [-f xml-file] [-g groups]
-.RS
-Build structure entries based on client statistics extra entries.
-.RE
-.B pull <client> <entry-type> <entry-name>
-.RS
-Install configuration information into repo based on client bad
-entries.
-.RE
-.B reports [init|load_stats|purge|scrub|update]
-.RS
-Interact with the dynamic reporting system.
-.RE
-.B snapshots [init|dump|query|reports]
-.RS
-Interact with the Snapshots database.
-.RE
-.B tidy
-.RS
-Remove unused files from repository.
-.RE
-.B viz [-H] [-b] [-k] [-o png-file]
-.RS
-Create a graphviz diagram of client, group and bundle information.
-.RE
-.SH BUNDLE OPTIONS
-.PP
-.B mode
-.RS
-List all available xml bundles 'list-xml' or for all available genshi
-bundles 'list-genshi'. 'show' provides an interactive dialog to get
-details about the available bundles.
-.RE
-.SH CLIENT OPTIONS
-.PP
-.B mode
-.RS
-Add a client 'add', delete a client 'del', or 'list' all client entries.
-.RE
-.B client
-.RS
-Specify the client's name.
-.RE
-.B attribute=value
-.RS
-Set attribute values when adding a new client. Allowed attributes
-are 'profile', 'uuid', 'password', 'location', 'secure', and 'address'.
-.RE
-.SH QUERY OPTIONS
-.PP
-.B g=group
-.RS
-Specify a group to search within.
-.RE
-.B p=profile
-.RS
-Specify a profile to search within.
-.RE
-.B \-f <output-file>
-.RS
-Write the results of the query to a file.
-.RE
-.B \-n
-.RS
-Print the results, one on each line.
-.RE
-.B \-c
-.RS
-Print the results, separated by commas.
-.RE
-.SH COMPARE OPTIONS
-.PP
-.B old
-.RS
-Specify the location of the old configuration file.
-.RE
-.B new
-.RS
-Specify the location of the new configuration file.
-.RE
-.SH MINESTRUCT OPTIONS
-.PP
-.B client
-.RS
-Client whose metadata is to be searched for extra entries.
-.RE
-.B \-g <groups>
-.RS
-Hierarchy of groups in which to place the extra entries in.
-.RE
-.B \-f <xml-output-file>
-.RS
-Specify the xml file in which to write the extra entries.
-.RE
-.SH PULL OPTIONS
-.PP
-.B client
-.RS
-Specify the name of the client to search for.
-.RE
-.B entry-type
-.RS
-Specify the type of the entry to pull.
-.RE
-.B entry-name
-.RS
-Specify the name of the entry to pull.
-.RE
-.SH REPORTS OPTIONS
-.PP
-.B init
-.RS
-Initialize the database.
-.RE
-.B load_stats [-s] [-c] [-03]
-.RS
-Load statistics data.
-.RE
-.B purge [--client [n]] [--days [n]] [--expired]
-.RS
-Purge historic and expired data.
-.RE
-.B scrub
-.RS
-Scrub the database for duplicate reasons and orphaned entries.
-.RE
-.B update
-.RS
-Apply any updates to the reporting database.
-.RE
-.SH SNAPSHOTS OPTIONS
-.PP
-.B init
-.RS
-Initialize the snapshots database.
-.RE
-.B query
-.RS
-Query the snapshots database.
-.RE
-.B dump
-.RS
-Dump some of the contents of the snapshots database.
-.RE
-.B reports [-a] [-b] [-e] [--date=<MM-DD-YYYY>]
-.RS
-Generate reports for clients in the snapshots database.
-.RE
-.SH VIZ OPTIONS
-.PP
-.B \-H
-.RS
-Include hosts in diagram.
-.RE
-.B \-b
-.RS
-Include bundles in diagram.
-.RE
-.B \-o <output file>
-.RS
-Write to outfile file instead of stdout.
-.RE
-.B \-k
-.RS
-Add a shape/color key.
-.RE
+.
+.TH "BCFG2\-ADMIN" "8" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-admin\fR \- Perform repository administration tasks
+.
+.SH "SYNOPSIS"
+\fBbcfg2\-admin\fR [\-C \fIconfigfile\fR] \fImode\fR [\fImode args\fR] [\fImode options\fR]
+.
+.SH "DESCRIPTION"
+\fBbcfg2\-admin\fR is used to perform Bcfg2 repository administration
+.
+.SH "OPTIONS"
+.
+.TP
+\fB\-C\fR \fIconfigfile\fR
+Specify alternate bcfg2\.conf location
+.
+.SH "MODES"
+.
+.TP
+\fBinit\fR
+Initialize a new repository (interactive)\.
+.
+.TP
+\fBbackup\fR
+Create an archive of the entire Bcfg2 repository\.
+.
+.TP
+\fBbundle\fR \fIaction\fR
+Display details about the available bundles (See \fI\fBBUNDLE OPTIONS\fR\fR below)\.
+.
+.TP
+\fBclient\fR \fIaction\fR \fIclient\fR [attribute=value]
+Add, edit, or remove clients entries in metadata (See \fI\fBCLIENT OPTIONS\fR\fR below)\.
+.
+.TP
+\fBquery\fR [g=group] [p=profile] [\-f output\-file] [\-n] [\-c]
+Search for clients based on group or profile (See \fI\fBQUERY OPTIONS\fR\fR below)\.
+.
+.TP
+\fBcompare\fR \fIold\fR \fInew\fR
+Compare two client configurations\. Can be used to verify consistent behavior between releases\. Determine differences between files or directories (See \fI\fBCOMPARE OPTIONS\fR\fR below)\.
+.
+.TP
+\fBminestruct\fR \fIclient\fR [\-f xml\-file] [\-g groups]
+Build structure entries based on client statistics extra entries (See \fI\fBMINESTRUCT OPTIONS\fR\fR below)\.
+.
+.TP
+\fBpull\fR \fIclient\fR \fIentry\-type\fR \fIentry\-name\fR
+Install configuration information into repo based on client bad entries (See \fI\fBPULL OPTIONS\fR\fR below)\.
+.
+.TP
+\fBreports\fR [init|load_stats|purge|scrub|update]
+Interact with the dynamic reporting system (See \fI\fBREPORTS OPTIONS\fR\fR below)\.
+.
+.TP
+\fBsnapshots\fR [init|dump|query|reports]
+Interact with the Snapshots database (See \fI\fBSNAPSHOTS OPTIONS\fR\fR below)\.
+.
+.TP
+\fBtidy\fR
+Remove unused files from repository\.
+.
+.TP
+\fBviz\fR [\-H] [\-b] [\-k] [\-o png\-file]
+Create a graphviz diagram of client, group and bundle information (See \fI\fBVIZ OPTIONS\fR\fR below)\.
+.
+.SS "BUNDLE OPTIONS"
+.
+.TP
+\fBmode\fR
+List all available xml bundles ’list\-xml’ or for all available genshi bundles ’list\-genshi’\. ’show’ provides an interactive dialog to get details about the available bundles\.
+.
+.SS "CLIENT OPTIONS"
+.
+.TP
+\fBmode\fR
+Add a client ’add’, delete a client ’del’, or ’list’ all client entries\.
+.
+.TP
+\fBclient\fR
+Specify the client’s name\.
+.
+.TP
+\fBattribute=value\fR
+Set attribute values when adding a new client\. Allowed attributes are ’profile’, ’uuid’, ’password’, ’location’, ’secure’, and ’address’\.
+.
+.SS "QUERY OPTIONS"
+.
+.TP
+\fBg=group\fR
+Specify a group to search within\.
+.
+.TP
+\fBp=profile\fR
+Specify a profile to search within\.
+.
+.TP
+\fB\-f\fR \fIoutput file\fR
+Write the results of the query to a file\.
+.
+.TP
+\fB\-n\fR
+Print the results, one on each line\.
+.
+.TP
+\fB\-c\fR
+Print the results, separated by commas\.
+.
+.SS "COMPARE OPTIONS"
+.
+.TP
+\fBold\fR
+Specify the location of the old configuration file\.
+.
+.TP
+\fBnew\fR
+Specify the location of the new configuration file\.
+.
+.SS "MINESTRUCT OPTIONS"
+.
+.TP
+\fBclient\fR
+Client whose metadata is to be searched for extra entries\.
+.
+.TP
+\fB\-g\fR \fIgroups\fR
+Hierarchy of groups in which to place the extra entries in\.
+.
+.TP
+\fB\-f\fR \fIxml output file\fR
+Specify the xml file in which to write the extra entries\.
+.
+.SS "PULL OPTIONS"
+.
+.TP
+\fBclient\fR
+Specify the name of the client to search for\.
+.
+.TP
+\fBentry type\fR
+Specify the type of the entry to pull\.
+.
+.TP
+\fBentry name\fR
+Specify the name of the entry to pull\.
+.
+.SS "REPORTS OPTIONS"
+.
+.TP
+\fBinit\fR
+Initialize the database\.
+.
+.TP
+\fBload_stats\fR [\-s] [\-c] [\-03]
+Load statistics data\.
+.
+.TP
+\fBpurge\fR [\-\-client [n]] [\-\-days [n]] [\-\-expired]
+Purge historic and expired data\.
+.
+.TP
+\fBscrub\fR
+Scrub the database for duplicate reasons and orphaned entries\.
+.
+.TP
+\fBupdate\fR
+Apply any updates to the reporting database\.
+.
+.SS "SNAPSHOTS OPTIONS"
+.
+.TP
+\fBinit\fR
+Initialize the snapshots database\.
+.
+.TP
+\fBquery\fR
+Query the snapshots database\.
+.
+.TP
+\fBdump\fR
+Dump some of the contents of the snapshots database\.
+.
+.TP
+\fBreports\fR [\-a] [\-b] [\-e] [\-\-date=\fIMM\-DD\-YYYY\fR]
+Generate reports for clients in the snapshots database\.
+.
+.SS "VIZ OPTIONS"
+.
+.TP
+\fB\-H\fR
+Include hosts in diagram\.
+.
+.TP
+\fB\-b\fR
+Include bundles in diagram\.
+.
+.TP
+\fB\-o\fR \fIoutput file\fR
+Write to outfile file instead of stdout\.
+.
+.TP
+\fB\-k\fR
+Add a shape/color key\.
+.
+.SH "SEE ALSO"
+bcfg2\-info(8), bcfg2\-server(8)
diff --git a/man/bcfg2-build-reports.8 b/man/bcfg2-build-reports.8
index a14dea728..7ece94c32 100644
--- a/man/bcfg2-build-reports.8
+++ b/man/bcfg2-build-reports.8
@@ -1,40 +1,36 @@
-.TH "bcfg2-build-reports" 8
-.SH NAME
-bcfg2-build-reports \- Generate state reports for Bcfg2 clients
-.SH SYNOPSIS
-.B bcfg2-build-reports
-.I [-A] [-c] [-s] [-N]
-.SH DESCRIPTION
-.PP
-.B bcfg2-build-reports
-Build all client state reports. See the Bcfg2 manual for report setup
-information.
-.SH OPTIONS
-.PP
-.B "\-A"
-.RS
-Displays all data.
-.RE
-.B "\-c <configuration file>"
-.RS
-Specify an alternate report configuration path. The default is
-repo/etc/reports-configuration.xml.
-.RE
-.B "\-h"
-.RS
-Produce a help message.
-.RE
-.B "\-s <statistics Path>"
-.RS
-Use an alternative path for the statistics file. The default is
-repo/etc/statistics.xml
-.RE
-.B "\-N"
-.RS
-No pinging.
-.RE
+.
+.TH "BCFG2\-BUILD\-REPORTS" "8" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-build\-reports\fR \- Generate state reports for Bcfg2 clients
+.
+.SH "SYNOPSIS"
+\fBbcfg2\-build\-reports\fR [\fI\-A\fR] [\fI\-c\fR] [\fI\-s\fR] [\fI\-N\fR]
+.
+.SH "DESCRIPTION"
+\fBbcfg2\-build\-reports\fR is used to build all client state reports\. See the Bcfg2 manual for report setup information\.
+.
+.SH "OPTIONS"
+.
+.TP
+\fB\-A\fR
+Displays all data\.
+.
+.TP
+\fB\-c\fR \fIconfiguration file\fR
+Specify an alternate report configuration path\. The default is repo/etc/reports\-configuration\.xml\.
+.
+.TP
+\fB\-h\fR
+Produce a help message\.
+.
+.TP
+\fB\-s\fR \fIstatistics path\fR
+Use an alternative path for the statistics file\. The default is repo/etc/statistics\.xml\.
+.
+.TP
+\fB\-N\fR
+No pinging\.
+.
.SH "SEE ALSO"
-.BR bcfg(1),
-.BR bcfg2-server(8)
-.SH "BUGS"
-None currently known
+bcfg2(1), bcfg2\-server(8)
diff --git a/man/bcfg2-info.8 b/man/bcfg2-info.8
index a644926b5..12b6ae9c6 100644
--- a/man/bcfg2-info.8
+++ b/man/bcfg2-info.8
@@ -1,131 +1,134 @@
-.TH "bcfg2-info" 8
-.SH NAME
-bcfg2-info \- Creates a local version of the bcfg2 server core for
-state observation
-.SH SYNOPSIS
-.B bcfg2-info
-.I [\-C <config file>] [\-E <encoding>] [\-Q <repository path>] [\-h] [\-p] [\-x <password>]
-.I <mode>
-.I <mode args>
-.I <mode options>
-.SH DESCRIPTION
-.PP
-.B bcfg2-info
-Instantiate an instance of the Bcfg2 core for data examination and
-debugging purposes.
-.SH OPTIONS
-.PP
-.B "\-C <config file>"
-.RS
-Specify the location of the configuration file (if it is not in
-/etc/bcfg2.conf).
-.RE
-.B "\-E <encoding>"
-.RS
-Specify the encoding of config files.
-.RE
-.B "\-Q <repository path>
-.RS
-Specify the server repository path.
-.RE
-.B "\-d"
-.RS
-Run in debug mode.
-.RE
-.B "\-h"
-.RS
-Give a bit of help about the command line arguments and
-options. After this bcfg2-info exits.
-.RE
-.B "\-p"
-.RS
-Specify a profile.
-.RE
-.B "\-x <password>"
-.RS
-Set the communication password.
-.RE
-.SH MODES
-.PP
-.B build <hostname> <filename>
-.RS
-Build config for hostname, writing to filename.
-.RE
-.B builddir <hostname> <dirname>
-.RS
-Build config for hostname, writing separate files to dirname.
-.RE
-.B buildall <directory>
-.RS
-Build configs for all clients in directory.
-.RE
-.B buildbundle <filename> <hostname>
-.RS
-Build bundle for hostname (not written to disk). If filename is a bundle
-template, it is rendered.
-.RE
-.B buildfile [--altsrc=<altsrc>] <filename> <hostname>
-.RS
-Build config file for hostname (not written to disk).
-.RE
-.B bundles
-.RS
-Print out group/bundle information.
-.RE
-.B clients
-.RS
-Print out client/profile information.
-.RE
-.B config
-.RS
-Print out the configuration of the Bcfg2 server.
-.RE
-.B debug
-.RS
-Shell out to native python interpreter.
-.RE
-.B event_debug
-.RS
-Display filesystem events as they are processed.
-.RE
-.B groups
-.RS
-List groups
-.RE
-.B help
-.RS
-Print the list of available commands.
-.RE
-.B mappings <type*> <name*>
-.RS
-Print generator mappings for optional type and name.
-.RE
-.B profile <command> <args>
-.RS
-Profile a single bcfg2-info command.
-.RE
-.B quit
-.RS
-Exit bcfg2-info command line.
-.RE
-.B showentries <hostname> <type>
-.RS
-Show abstract configuration entries for a given host.
-.RE
-.B showclient <client1> <client2>
-.RS
-Show metadata for given hosts.
-.RE
-.B update
-.RS
-Process pending file events.
-.RE
-.B version
-.RS
-Print version of this tool.
-.RE
+.
+.TH "BCFG2\-INFO" "8" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-info\fR \- Creates a local version of the Bcfg2 server core for state observation
+.
+.SH "SYNOPSIS"
+\fBbcfg2\-info\fR [\fI\-C configfile\fR] [\-E \fIencoding\fR] [\-Q \fIrepository path\fR] [\-h] [\-p] [\-x \fIpassword\fR] [\fImode\fR] [\fImode args\fR] [\fImode options\fR]
+.
+.SH "DESCRIPTION"
+\fBbcfg2\-info\fR instantiates an instance of the Bcfg2 core for data examination and debugging purposes\.
+.
+.SH "OPTIONS"
+.
+.TP
+\fB\-C\fR \fIconfigfile\fR
+Specify alternate bcfg2\.conf location
+.
+.TP
+\fB\-E\fR \fIencoding\fR
+Specify the encoding of config files\.
+.
+.TP
+\fB\-Q\fR \fIrepository path\fR
+Specify the server repository path\.
+.
+.TP
+\fB\-d\fR
+Run in debug mode\.
+.
+.TP
+\fB\-h\fR
+Give a bit of help about the command line arguments and options\. After this bcfg2\-info exits\.
+.
+.TP
+\fB\-p\fR
+Specify a profile\.
+.
+.TP
+\fB\-x\fR \fIpassword\fR
+Set the communication password\.
+.
+.SH "MODES"
+.
+.TP
+\fBbuild\fR \fIhostname\fR \fIfilename\fR
+Build config for hostname, writing to filename\.
+.
+.TP
+\fBbuildall\fR \fIdirectory\fR
+Build configs for all clients in directory\.
+.
+.TP
+\fBbuildallfile\fR \fIdirectory\fR \fIfilename\fR [\fIhostnames\fR]
+Build config file for all clients in directory\.
+.
+.TP
+\fBbuildbundle\fR \fIfilename\fR \fIhostname\fR
+Build bundle for hostname (not written to disk)\. If filename is a bundle template, it is rendered\.
+.
+.TP
+\fBbuilddir\fR \fIhostname\fR \fIdirname\fR
+Build config for hostname, writing separate files to dirname\.
+.
+.TP
+\fBbuildfile\fR [\-\-altsrc=\fIaltsrc\fR] \fIfilename\fR \fIhostname\fR
+Build config file for hostname (not written to disk)\.
+.
+.TP
+\fBbundles\fR
+Print out group/bundle information\.
+.
+.TP
+\fBclients\fR
+Print out client/profile information\.
+.
+.TP
+\fBconfig\fR
+Print out the configuration of the Bcfg2 server\.
+.
+.TP
+\fBdebug\fR
+Shell out to native python interpreter\.
+.
+.TP
+\fBevent_debug\fR
+Display filesystem events as they are processed\.
+.
+.TP
+\fBgroups\fR
+List groups\.
+.
+.TP
+\fBhelp\fR
+Print the list of available commands\.
+.
+.TP
+\fBmappings\fR [\fIentry type\fR] [\fIentry name\fR]
+Print generator mappings for optional type and name\.
+.
+.TP
+\fBpackageresolve\fR \fIhostname\fR \fIpackage\fR [\fIpackage\fR\.\.\.]
+Resolve the specified set of packages\.
+.
+.TP
+\fBpackagesources\fR \fIhostname\fR
+Show package sources\.
+.
+.TP
+\fBprofile\fR \fIcommand\fR \fIargs\fR
+Profile a single bcfg2\-info command\.
+.
+.TP
+\fBquit\fR
+Exit bcfg2\-info command line\.
+.
+.TP
+\fBshowentries\fR \fIhostname\fR \fItype\fR
+Show abstract configuration entries for a given host\.
+.
+.TP
+\fBshowclient\fR \fIclient1\fR \fIclient2\fR
+Show metadata for given hosts\.
+.
+.TP
+\fBupdate\fR
+Process pending file events\.
+.
+.TP
+\fBversion\fR
+Print version of this tool\.
+.
.SH "SEE ALSO"
-.BR bcfg2(1),
-.BR bcfg2-server(8)
-.SH "BUGS"
-None currently known
+bcfg2(1), bcfg2\-server(8)
diff --git a/man/bcfg2-lint.8 b/man/bcfg2-lint.8
index 25fa30f9e..02d472d22 100644
--- a/man/bcfg2-lint.8
+++ b/man/bcfg2-lint.8
@@ -1,174 +1,99 @@
-.TH "bcfg2-lint" 8
-.SH NAME
-bcfg2-lint \- Check Bcfg2 specification for validity, common mistakes,
-and style
-
-.SH SYNOPSIS
-.B bcfg2-lint
-.I [OPTIONS]
-.I [<plugin> [<plugin>...]]
-
-.SH DESCRIPTION
-.PP
-.B bcfg2-lint
-This script checks the Bcfg2 specification for schema validity, common
-mistakes, and other criteria. It can be quite helpful in finding
-typos or malformed data.
-
-.B bcfg2-lint
-exits with a return value of 2 if errors were found, and 3
-if warnings (but no errors) were found. Any other non-0 exit value
-denotes some failure in the script itself.
-
-.B bcfg2-lint
-is a rewrite of the older
-.B bcfg2-repo-validate
-tool.
-
-.SH OPTIONS
-
+.
+.TH "BCFG2\-LINT" "8" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-lint\fR \- Check Bcfg2 specification for validity, common mistakes, and style
+.
+.SH "SYNOPSIS"
+\fBbcfg2\-lint\fR [\fIoptions\fR] [\fIplugin\fR [\fIplugin\fR\.\.\.]]
+.
+.SH "DESCRIPTION"
+\fBbcfg2\-lint\fR checks the Bcfg2 specification for schema validity, common mistakes, and other criteria\. It can be quite helpful in finding typos or malformed data\.
+.
+.P
+\fBbcfg2\-lint\fR exits with a return value of 2 if errors were found, and 3 if warnings (but no errors) were found\. Any other non\-0 exit value denotes some failure in the script itself\.
+.
+.P
+\fBbcfg2\-lint\fR is a rewrite of the older bcfg2\-repo\-validate tool\.
+.
+.SH "OPTIONS"
+.
.TP
-.BR "-v"
-Be verbose.
-
+\fB\-C\fR \fIconfigfile\fR
+Specify alternate bcfg2\.conf location\.
+.
.TP
-.BR "-C"
-Specify path to bcfg2.conf (default /etc/bcfg2.conf)
-
+\fB\-Q\fR
+Specify the server repository path\.
+.
.TP
-.BR "--lint-config"
-Specify path to bcfg2-lint.conf (default /etc/bcfg2-lint.conf)
-
+\fB\-v\fR
+Be verbose\.
+.
.TP
-.BR "-Q"
-Specify path to Bcfg2 repository (default /var/lib/bcfg2)
-
+\fB\-\-lint\-config\fR
+Specify path to bcfg2\-lint\.conf (default \fB/etc/bcfg2\-lint\.conf\fR)\.
+.
.TP
-.BR "--stdin"
-Rather than operating on all files in the Bcfg2 specification, only
-validate a list of files supplied on stdin. This mode is particularly
-useful in pre-commit hooks.
-
+\fB\-\-stdin\fR
+Rather than operating on all files in the Bcfg2 specification, only validate a list of files supplied on stdin\. This mode is particularly useful in pre\-commit hooks\.
+.
+.IP
This makes a few assumptions:
-
-Metadata files will only be checked if a valid chain of XIncludes can
-be followed all the way from clients.xml or groups.xml. Since there
-are multiple formats of metadata stored in Metadata/ (i.e., clients
-and groups), there is no way to determine which sort of data a file
-contains unless there is a valid chain of XIncludes. It may be useful
-to always specify all metadata files should be checked, even if not
-all of them have changed.
-
-Property files will only be validated if both the property file itself
-and its matching schema are included on stdin.
-
+.
+.IP
+Metadata files will only be checked if a valid chain of XIncludes can be followed all the way from clients\.xml or groups\.xml\. Since there are multiple formats of metadata stored in Metadata/ (i\.e\., clients and groups), there is no way to determine which sort of data a file contains unless there is a valid chain of XIncludes\. It may be useful to always specify all metadata files should be checked, even if not all of them have changed\.
+.
+.IP
+Property files will only be validated if both the property file itself and its matching schema are included on stdin\.
+.
.TP
-.BR "--require-schema"
-Require property files to have matching schema files
-
-.RE
-
+\fBrequire\-schema\fR
+Require property files to have matching schema files\.
+.
.SH "PLUGINS"
-
-See
-.BR bcfg2-lint.conf(5)
-for more information on the configuration of the plugins listed below.
-
+See \fBbcfg2\-lint\.conf\fR(5) for more information on the configuration of the plugins listed below\.
+.
.TP
-.BR Bundles
-Check the specification for several issues with Bundler: bundles
-referenced in metadata but not found in
-.I Bundler/
-; bundles whose
-.I name
-attribute does not match the filename; and Genshi template bundles
-that use the
-.I <Group>
-tag (which is not processed in templated bundles).
-
+\fBBundles\fR
+Check the specification for several issues with Bundler: bundles referenced in metadata but not found in \fBBundler/\fR; bundles whose \fIname\fR attribute does not match the filename; and Genshi template bundles that use the \fI\fIGroup\fR\fR tag (which is not processed in templated bundles)\.
+.
.TP
-.BR Comments
-Check the specification for VCS keywords and any comments that are
-required. By default, this only checks that the
-.I $Id$
-keyword is included and expanded in all files. You may specify VCS
-keywords to check and comments to be required in the config file.
-(For instance, you might require that every file have a "Maintainer"
-comment.)
-
-In XML files, only comments are checked for the keywords and comments
-required.
-
+\fBComments\fR
+Check the specification for VCS keywords and any comments that are required\. By default, this only checks that the \fI$Id$\fR keyword is included and expanded in all files\. You may specify VCS keywords to check and comments to be required in the config file\. (For instance, you might require that every file have a "Maintainer" comment\.)
+.
+.IP
+In XML files, only comments are checked for the keywords and comments required\.
+.
.TP
-.BR Duplicates
-Check for several types of duplicates in the Metadata: duplicate
-groups; duplicate clients; and multiple default groups.
-
+\fBDuplicates\fR
+Check for several types of duplicates in the Metadata: duplicate groups; duplicate clients; and multiple default groups\.
+.
.TP
-.BR InfoXML
-Check that certain attributes are specified in
-.I info.xml
-files. By default, requires that
-.I owner
-,
-.I group
-, and
-.I perms
-are specified. Can also require that an
-.I info.xml
-exists for all Cfg files, and that paranoid mode be enabled for all
-files.
-
+\fBInfoXML\fR
+Check that certain attributes are specified in \fBinfo\.xml\fR files\. By default, requires that \fIowner\fR, \fIgroup\fR, and \fIperms\fR are specified\. Can also require that an \fBinfo\.xml\fR exists for all Cfg files, and that paranoid mode be enabled for all files\.
+.
.TP
-.BR MergeFiles
-Suggest that similar probes and config files be merged into single
-probes or TGenshi templates.
-
+\fBMergeFiles\fR
+Suggest that similar probes and config files be merged into single probes or TGenshi templates\.
+.
.TP
-.BR Pkgmgr
-Check for duplicate packages specified in Pkgmgr.
-
+\fBPkgmgr\fR
+Check for duplicate packages specified in Pkgmgr\.
+.
.TP
-.BR RequiredAttrs
-Check that all
-.I <Path>
-and
-.I <BoundPath>
-tags have the attributes that are required by their type. (E.g., a
-path of type
-.I "symlink"
-must have
-.I name
-and
-.I to
-specified to be valid. This sort of validation is beyond the scope of
-an XML schema.
-
+\fBRequiredAttrs\fR
+Check that all \fIPath\fR and \fIBoundPath\fR tags have the attributes that are required by their type (e\.g\., a path of type symlink must have name and to specified to be valid)\. This sort of validation is beyond the scope of an XML schema\.
+.
.TP
-.BR Validate
-Validate the Bcfg2 specification against the XML schemas.
-
-Property files are freeform XML, but if a
-.I .xsd
-file with a matching filename is provided, then schema validation will
-be performed on property files individually as well. For instance, if
-you have a property file named
-.I ntp.xml
-then by placing a schema for that file in
-.I ntp.xsd
-schema validation will be performed on
-.I ntp.xml
-.
-
-
-.SH "SEE ALSO"
-.BR bcfg2(1),
-.BR bcfg2-server(8),
-.BR bcfg2-lint.conf(5)
-
+\fBValidate\fR
+Validate the Bcfg2 specification against the XML schemas\.
+.
+.IP
+Property files are freeform XML, but if a \fB\.xsd\fR file with a matching filename is provided, then schema validation will be performed on property files individually as well\. For instance, if you have a property file named \fBntp\.xml\fR then by placing a schema for that file in \fBntp\.xsd\fR schema validation will be performed on \fBntp\.xml\fR\.
+.
.SH "BUGS"
-
-bcfg2-lint may not handle some older plugins as well as it handles
-newer ones. For instance, there may be some places where it expects
-all of your configuration files to be handled by Cfg rather than by a
-mix of Cfg and TGenshi or TCheetah.
+\fBbcfg2\-lint\fR may not handle some older plugins as well as it handles newer ones\. For instance, there may be some places where it expects all of your configuration files to be handled by Cfg rather than by a mix of Cfg and TGenshi or TCheetah\.
+.
+.SH "SEE ALSO"
+bcfg2(1), bcfg2\-server(8), bcfg2\-lint\.conf(5)
diff --git a/man/bcfg2-lint.conf.5 b/man/bcfg2-lint.conf.5
index 10a812874..467a717b0 100644
--- a/man/bcfg2-lint.conf.5
+++ b/man/bcfg2-lint.conf.5
@@ -1,174 +1,99 @@
-.TH bcfg2-lint.conf 5
-
-.SH NAME
-bcfg2-lint.conf - configuration parameters for bcfg2-lint
-
-.SH DESCRIPTION
-.TP
-bcfg2-lint.conf includes configuration parameters for
-.I bcfg2-lint
-
-.SH FILE FORMAT
-The file is INI-style and consists of sections and options. A section
-begins with the name of the sections in square brackets and continues
-until the next section begins.
-
-Options are specified in the form 'name = value'.
-
-The file is line-based each newline-terminated line represents either
-a comment, a section name or an option.
-
-Any line beginning with a hash (#) is ignored, as are lines containing
-only whitespace.
-
-The file consists of one
-.I [lint]
-section, up to one
-.I [errors]
-section, and then any number of plugin-specific sections, documented below. (Note that this makes it quite feasible to combine your
-.B bcfg2-lint.conf
-into your
-.B bcfg2.conf(5)
-file, if you so desire.)
-
-.SH GLOBAL OPTIONS
-These options apply to
-.I bcfg2-lint
-generally, and must be in the
-.I [lint]
-section.
-
-.TP
-.BR plugins
-A comma-delimited list of plugins to run. By default, all plugins are
-run. This can be overridden by listing plugins on the command line.
-See
-.B bcfg2-lint(8)
-for a list of the available plugins.
-
-.SH ERROR HANDLING
-Error handling is configured in the
-.I [errors]
-section. Each option should be the name of an error and one of
-.I "error"
-,
-.I "warning"
-, or
-.I "silent"
-, which tells
-.B bcfg2-lint(8)
-how to handle the warning. Error names and their defaults can be
-displayed by running
-.B bcfg2-lint(8)
-with the
-.B --list-errors
-option.
-
-.SH PLUGIN OPTIONS
-
-These options apply only to a single plugin. Each option should be in
-a section named for its plugin; for instance, options for the InfoXML
-plugin would be in a section called
-.I [InfoXML]
-.
-
-If a plugin is not listed below, then it has no configuration.
-
-In many cases, the behavior of a plugin can be configured by modifying
-how errors from it are handled. See
-.B ERROR HANDLING
-, above.
-
+.
+.TH "BCFG2\-LINT\.CONF" "5" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-lint\.conf\fR \- configuration parameters for bcfg2\-lint
+.
+.SH "DESCRIPTION"
+\fBbcfg2\-lint\.conf\fR includes configuration parameters for \fBbcfg2\-lint\fR\.
+.
+.SH "FILE FORMAT"
+The file is INI\-style and consists of sections and options\. A section begins with the name of the sections in square brackets and continues until the next section begins\.
+.
+.P
+Options are specified in the form "name=value"\.
+.
+.P
+The file is line\-based each newline\-terminated line represents either a comment, a section name or an option\.
+.
+.P
+Any line beginning with a hash (#) is ignored, as are lines containing only whitespace\.
+.
+.P
+The file consists of one \fB[lint]\fR section, up to one \fB[errors]\fR section, and then any number of plugin\-specific sections, documented below\. (Note that this makes it quite feasible to combine your \fBbcfg2\-lint\.conf\fR into your \fBbcfg2\.conf\fR(5) file, if you so desire)\.
+.
+.SH "GLOBAL OPTIONS"
+These options apply to \fBbcfg2\-lint\fR generally, and must be in the \fB[lint]\fR section\.
+.
.TP
-.BR Comments
-
-The
-.I Comments
-plugin configuration specifies which VCS keywords and comments are
-required for which file types. The valid types of file are
-.I "global"
-(all file types),
-.I "bundler"
-(non-templated bundle files),
-.I "sgenshi"
-(templated bundle files),
-.I "properties"
-(property files),
-.I "cfg"
-(non-templated Cfg files),
-.I "tgenshi"
-(templated Cfg files),
-.I "infoxml"
-(info.xml files), and
-.I "probe"
-(probe files).
-
-The specific types (i.e., types other than "global") all supplement
-global; they do not override it. The exception is if you specify an
-empty option, e.g.:
-
-.nf
+\fBplugins\fR
+A comma\-delimited list of plugins to run\. By default, all plugins are run\. This can be overridden by listing plugins on the command line\. See \fBbcfg2\-lint\fR(8) for a list of the available plugins\.
+.
+.SH "ERROR HANDLING"
+Error handling is configured in the \fB[errors]\fR section\. Each option should be the name of an error and one of \fIerror\fR, \fIwarning\fR, or \fIsilent\fR, which tells \fBbcfg2\-lint\fR(8) how to handle the warning\. Error names and their defaults can be displayed by running \fBbcfg2\-lint\fR(8) with the \fB\-\-list\-errors\fR option\.
+.
+.SH "PLUGIN OPTIONS"
+These options apply only to a single plugin\. Each option should be in a section named for its plugin; for instance, options for the InfoXML plugin would be in a section called \fB[InfoXML]\fR\.
+.
+.P
+If a plugin is not listed below, then it has no configuration\.
+.
+.P
+In many cases, the behavior of a plugin can be configured by modifying how errors from it are handled\. See \fI\fBERROR HANDLING\fR\fR, above\.
+.
+.SS "Comments"
+The \fBComments\fR plugin configuration specifies which VCS keywords and comments are required for which file types\. The valid types of file are \fIglobal\fR (all file types), \fIbundler\fR (non\-templated bundle files), \fIsgenshi\fR (templated bundle files), \fIproperties\fR (property files), \fIcfg\fR (non\-templated Cfg files), \fItgenshi\fR (templated Cfg files), \fIinfoxml\fR (info\.xml files), and \fIprobe\fR (probe files)\.
+.
+.P
+The specific types (i\.e\., types other than "global") all supplement global; they do not override it\. The exception is if you specify an empty option, e\.g\.:
+.
+.P
cfg_keywords =
-.fi
-
-By default, the
-.I $Id$
-keyword is checked for and nothing else.
-
-Multiple keywords or comments should be comma-delimited.
-
-\(bu
-.B <type>_keywords
-
-Ensure that files of the specified type have the given VCS keyword.
-Do
-.I not
-include the dollar signs. I.e.:
-
-.nf
+.
+.P
+By default, the \fI$Id$\fR keyword is checked for and nothing else\.
+.
+.P
+Multiple keywords or comments should be comma\-delimited\.
+.
+.P
+· \fB<type>_keywords\fR
+.
+.P
+Ensure that files of the specified type have the given VCS keyword\. Do \fInot\fR include the dollar signs\. I\.e\.:
+.
+.P
infoxml_keywords = Revision
-.fi
-
-.I not:
-
-.nf
+.
+.P
+\fInot\fR:
+.
+.P
infoxml_keywords = $Revision$
-.fi
-
-\(bu
-.B <type>_comments
-
-Ensure that files of the specified type have a comment containing the
-given string. In XML files, only comments are checked. In plain text
-files, all lines are checked since comment characters may vary.
-
+.
+.P
+\fB· <type>_comments\fR
+.
+.P
+Ensure that files of the specified type have a comment containing the given string\. In XML files, only comments are checked\. In plain text files, all lines are checked since comment characters may vary\.
+.
+.SS "InfoXML"
+.
.TP
-.BR InfoXML
-
-\(bu
-.B required_attrs
-A comma-delimited list of attributes to require on
-.I <Info>
-tags. Default is "owner,group,perms".
-
+\fBrequired_attrs\fR
+A comma\-delimited list of attributes to require on \fB<Info>\fR tags\. Default is "owner,group,perms"\.
+.
+.SS "MergeFiles"
+.
.TP
-.BR MergeFiles
-
-\(bu
-.B threshold
-The threshold at which MergeFiles will suggest merging config files
-and probes. Default is 75% similar.
-
+\fBthreshold\fR
+The threshold at which MergeFiles will suggest merging config files and probes\. Default is 75% similar\.
+.
+.SS "Validate"
+.
.TP
-.BR Validate
-
-\(bu
-.B schema
-The full path to the XML Schema files. Default is
-"/usr/share/bcfg2/schema". This can be overridden with the
-.I --schema
-command-line option
-
-.SH SEE ALSO
-.BR bcfg2-lint(8)
-
+\fBschema\fR
+The full path to the XML Schema files\. Default is \fB/usr/share/bcfg2/schema\fR\. This can be overridden with the \fI\-\-schema\fR command\-line option
+.
+.SH "SEE ALSO"
+bcfg2\-lint(8)
diff --git a/man/bcfg2-ping-sweep.8 b/man/bcfg2-ping-sweep.8
index 54eaa8e76..709f0eed6 100644
--- a/man/bcfg2-ping-sweep.8
+++ b/man/bcfg2-ping-sweep.8
@@ -1,20 +1,14 @@
-.TH "bcfg2-ping-sweep" 8
-.SH NAME
-bcfg2-ping-sweep \- Update pingable and pingtime attributes in
-clients.xml
-.SH SYNOPSIS
-.B bcfg2-ping-sweep
+.
+.TH "BCFG2\-PING\-SWEEP" "8" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-ping\-sweep\fR \- Update pingable and pingtime attributes in clients\.xml
+.
+.SH "SYNOPSIS"
+\fBbcfg2\-ping\-sweep\fR
+.
.SH "DESCRIPTION"
-.PP
-\fBbcfg2-ping-sweep\fR traverses the list of clients in
-Metadata/clients.xml and updates their pingable/pingtime attributes. The
-pingtime value is set to the last time the client was pinged (not the
-RTT value).
-.SH OPTIONS
-.PP
-.B None
+\fBbcfg2\-ping\-sweep\fR traverses the list of clients in Metadata/clients\.xml and updates their pingable/pingtime attributes\. The pingtime value is set to the last time the client was pinged (not the RTT value)\.
+.
.SH "SEE ALSO"
-.BR bcfg(1),
-.BR bcfg2-server(8)
-.SH "BUGS"
-None currently known
+bcfg2(1), bcfg2\-server(8)
diff --git a/man/bcfg2-reports.8 b/man/bcfg2-reports.8
index 51399e1c9..f1091eb31 100644
--- a/man/bcfg2-reports.8
+++ b/man/bcfg2-reports.8
@@ -1,94 +1,76 @@
-.TH "bcfg2-reports" 8
-.SH NAME
-bcfg2-reports \- Query reporting system for client status
-.SH SYNOPSIS
-.B bcfg2-reports
-.I [-v]
-.SH DESCRIPTION
-.PP
-\fBbcfg2-reports\fR allows you to retrieve data from the database about
-clients, and the states of their current interactions. It also allows
-you to change the expired/unexpired states.
-The utility runs as a standalone application. It does, however, use
-the models from /src/lib/Server/Reports/reports/models.py.
-.SH OPTIONS
-.PP
-.B "\-a"
-.RS
-Shows all hosts, including expired hosts.
-.RE
-.B "\-b NAME"
-.RS
-Single-host mode \- shows bad entries from the current interaction of
-NAME. NAME is the name of the entry.
-.RE
-.B "-c\"
-.RS
-Shows only clean hosts.
-.RE
-.B "\-d"
-.RS
-Shows only dirty hosts.
-.RE
-.B "\-e NAME"
-.RS
-Single host mode \- shows extra entries from the current interaction
-of NAME. NAME is the name of the entry.
-.RE
-.B "\-h"
-.RS
-Shows help and usage info about bcfg2-reports.
-.RE
-.B "\-m NAME"
-.RS
-Single-host mode \- shows modified entries from the current interaction
-of NAME. NAME is the name of the entry.
-.RE
-.B "\-s NAME"
-.RS
-Single host mode \- shows bad, modified, and extra entries from the
-current interaction of NAME. NAME is the name of the entry.
-.RE
-.B "\-x NAME"
-.RS
-Toggles expired/unexpired state of NAME. NAME is the name of the entry.
-.RE
-.B "\-\-badentry=KIND,NAME"
-.RS
-Shows only hosts whose current interaction has bad entries in of KIND
-kind and NAME name; if a single argument ARG1 is given, then KIND,NAME
-pairs will be read from a file of name ARG1. KIND is the type of entry
-(Package, Path, Service, etc). NAME is the name of the entry.
-.RE
-.B "\-\-extraentry=KIND,NAME"
-.RS
-Shows only hosts whose current interaction has extra entries in of KIND
-kind and NAME name; if a single argument ARG1 is given, then KIND,NAME
-pairs will be read from a file of name ARG1. KIND is the type of entry
-(Package, Path, Service, etc). NAME is the name of the entry.
-.RE
-.B "\-\-fields=ARG1,ARG2,..."
-.RS
-Only displays the fields ARG1,ARG2,... (name, time, state, total, good,
-bad)
-.RE
-.B "\-\-modifiedentry=KIND,NAME"
-.RS
-Shows only hosts whose current interaction has modified entries in of
-KIND kind and NAME name; if a single argument ARG1 is given, then
-KIND,NAME pairs will be read from a file of name ARG1. KIND is the type
-of entry (Package, Path, Service, etc). NAME is the name of the entry.
-.RE
-.B "\-\-sort=ARG1,ARG2,..."
-.RS
-Sorts output on ARG1,ARG2,... (name, time, state, total, good, bad)
-.RE
-.B "\-\-stale"
-.RS
-Shows hosts which haven't run in the last 24 hours
-.RE
+.
+.TH "BCFG2\-REPORTS" "8" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-reports\fR \- Query reporting system for client status
+.
+.SH "SYNOPSIS"
+\fBbcfg2\-reports\fR [\-a] [\-b \fINAME\fR] [\-c] [\-d] [\-e \fINAME\fR] [\-h] [\-m \fINAME\fR] [\-s \fINAME\fR] [\-x \fINAME\fR] [\-\-badentry=\fIKIND,NAME\fR] [\-\-extraentry=\fIKIND,NAME\fR] [\-\-fields=<ARG1,ARG2,\.\.\.>] [\-\-modifiedentry=\fIKIND,NAME\fR] [\-\-sort=<ARG1,ARG2,\.\.\.>] [\-\-stale] [\-v]
+.
+.SH "DESCRIPTION"
+\fBbcfg2\-reports\fR allows you to retrieve data from the database about clients, and the states of their current interactions\. It also allows you to change the expired/unexpired states\. The utility runs as a standalone application\. It does, however, use the models from \fB/src/lib/Server/Reports/reports/models\.py\fR\.
+.
+.SH "OPTIONS"
+.
+.TP
+\fB\-a\fR
+Specify alternate bcfg2\.conf location
+.
+.TP
+\fB\-b\fR \fIhostname\fR
+Single host mode \- shows bad entries from the current interaction of \fIhostname\fR\.
+.
+.TP
+\fB\-c\fR
+Shows only clean hosts\.
+.
+.TP
+\fB\-d\fR
+Shows only dirty hosts\.
+.
+.TP
+\fB\-e\fR \fIhostname\fR
+Single host mode \- shows extra entries from the current interaction of \fIhostname\fR\.
+.
+.TP
+\fB\-h\fR
+Shows help and usage info about \fBbcfg2\-reports\fR\.
+.
+.TP
+\fB\-m\fR \fIhostname\fR
+Single host mode \- shows modified entries from the current interaction of \fIhostname\fR\.
+.
+.TP
+\fB\-s\fR \fIhostname\fR
+Single host mode \- shows bad, modified, and extra entries from the current interaction of \fIhostname\fR\.
+.
+.TP
+\fB\-x\fR \fIhostname\fR
+Toggles expired/unexpired state of \fIhostname\fR\.
+.
+.TP
+\fB\-\-badentry=\fR\fIentry type, entry name\fR
+Shows only hosts whose current interaction has bad entries of type \fIentry type\fR and name \fIentry name\fR\. If a single argument ARG1 is given, then \fIentry type\fR,\fIentry name\fR pairs will be read from a file of name ARG1\.
+.
+.TP
+\fB\-\-extraentry=\fR\fIentry type, entry name\fR
+Shows only hosts whose current interaction has extra entries of type \fIentry type\fR and name \fIentry name\fR\. If a single argument ARG1 is given, then \fIentry type\fR,\fIentry name\fR pairs will be read from a file of name ARG1\.
+.
+.TP
+\fB\-\-fields=\fR<ARG1,ARG2,\.\.\.>
+Only displays the fields \fIARG1,ARG2,\.\.\.\fR (name, time, state, total, good, bad)\.
+.
+.TP
+\fB\-\-modifiedentry=\fR\fIentry type, entry name\fR
+Shows only hosts whose current interaction has modified entries of type \fIentry type\fR and name \fIentry name\fR\. If a single argument ARG1 is given, then \fIentry type\fR,\fIentry name\fR pairs will be read from a file of name ARG1\.
+.
+.TP
+\fB\-\-sort=\fR<ARG1,ARG2,\.\.\.>
+Sorts output on ARG1,ARG2,\.\.\. (name, time, state, total, good, bad)\.
+.
+.TP
+\fB\-\-stale\fR
+Shows hosts which haven’t run in the last 24 hours\.
+.
.SH "SEE ALSO"
-.BR bcfg2(1),
-.BR bcfg2-server(8)
-.SH "BUGS"
-None currently known
+bcfg2(1), bcfg2\-server(8)
diff --git a/man/bcfg2-server.8 b/man/bcfg2-server.8
index 2d132ce6d..b1a3a7703 100644
--- a/man/bcfg2-server.8
+++ b/man/bcfg2-server.8
@@ -1,58 +1,48 @@
-.TH "bcfg2-server" 8
-.SH NAME
-bcfg2-server \- Server for client configuration specifications
-.SH SYNOPSIS
-.B bcfg2-server
-.I [-D <pidfile>] [-d] [-v] [-C <Client>]
-.SH DESCRIPTION
-.PP
-.B bcfg2-server
-This daemon serves configurations to clients based on the data in its
-repository.
-.SH OPTIONS
-.PP
-.B \-d
-.RS
-Run bcfg2 in debug mode.
-.RE
-.B \-v
-.RS
-Run bcfg2 in verbose mode.
-.RE
-.B "\-C <ConfigFile Path>"
-.RS
-Use an alternative path for bcfg2.conf. The default is /etc/bcfg2.conf
-.RE
-.B \-D
-.RS
-Daemonize, placing the program pid in the specified pidfile.
-.RE
-.B \-o <LogFile Path>
-.RS
-Writes a log to the specified path.
-.RE
-.B \-E <encoding>
-.RS
-Unicode encoding of config files.
-.RE
-.B \-x <password>
-.RS
-Set server password.
-.RE
-.B \-S <server url>
-.RS
-Set server address.
-.RE
-.B \-Q <repo path>
-.RS
-Set repository path.
-.RE
-.B \-\-ssl\-key=<ssl key>
-.RS
-Set path to SSL key.
-.RE
+.
+.TH "BCFG2\-SERVER" "8" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\-server\fR \- Server for client configuration specifications
+.
+.SH "SYNOPSIS"
+\fBbcfg2\-server\fR [\-d] [\-v] [\-C \fIconfigfile\fR] [\-D \fIpidfile\fR] [\-E \fIencoding\fR] [\-Q \fIrepo path\fR] [\-S \fIserver url\fR] [\-o \fIlogfile\fR] [\-x \fIpassword\fR] [\-\-ssl\-key=\fIssl key\fR]
+.
+.SH "DESCRIPTION"
+\fBbcfg2\-server\fR is the daemon component of Bcfg2 which serves configurations to clients based on the data in its repository\.
+.
+.SH "OPTIONS"
+.
+.TP
+\fB\-C\fR \fIconfigfile\fR
+Specify alternate bcfg2\.conf location\.
+.
+.TP
+\fB\-D\fR \fIpidfile\fR
+Daemonize, placing the program pid in the specified pidfile\.
+.
+.TP
+\fB\-E\fR \fIencoding\fR
+Specify alternate encoding (default is UTF\-8)\.
+.
+.TP
+\fB\-Q\fR \fIrepo path\fR
+Set repository path\.
+.
+.TP
+\fB\-S\fR \fIserver url\fR
+Set server address\.
+.
+.TP
+\fB\-d\fR
+Run \fBbcfg2\-server\fR in debug mode\.
+.
+.TP
+\fB\-v\fR
+Run \fBbcfg2\-server\fR in verbose mode\.
+.
+.TP
+\fB\-\-ssl\-key=\fR\fIssl key\fR
+Set path to SSL key\.
+.
.SH "SEE ALSO"
-.BR bcfg2(1),
-.BR bcfg2-lint(8)
-.SH "BUGS"
-None currently known
+bcfg2(1), bcfg2\-lint(8)
diff --git a/man/bcfg2.1 b/man/bcfg2.1
index 661153a15..d0f0e0062 100644
--- a/man/bcfg2.1
+++ b/man/bcfg2.1
@@ -1,180 +1,167 @@
-.TH "bcfg2" 1
-.SH NAME
-bcfg2 \- reconfigure machine based on settings in Bcfg2
-.SH SYNOPSIS
-.B bcfg2
-.I [\-d] [\-v] [\-p] [\-c cache file] [\-e] [\-f config file] [\-I] [\-q] [\-z] [\-b bundle] [\-r removal mode] [\-\-ca\-cert=file] [\-\-ssl\-cns=list] [\-\-ssl\-cert=file] [\-\-ssl\-key=file]
-.SH DESCRIPTION
-.TP
-.BR bcfg2
-Runs the Bcfg2 configuration process on the current host. This process
-consists of first fetching and executing probes, uploading probe
-results, fetching the client configuration, checking the current
-client state, attempting to install the desired configuration, and
-finally uploading statistics about the Bcfg2 execution and client
-state.
-
-.SH OPTIONS
-.TP
-.BR "\-C <configfile>"
-Specify alternate bcfg2.conf location.
-
-.TP
-.BR "\-D <driver1>,<driver2>"
-Specify a set of Bcfg2 tool drivers. NOTE: only drivers listed will be
-loaded. (IE, if you don't include POSIX, you will be unable to
-verify/install ConfigFiles, etc).
-
-.TP
-.BR "\-E <encoding>"
-Specify the encoding of Cfg files.
-
-.TP
-.BR "\-I"
-Run bcfg2 in interactive mode. The user will be prompted before each
-change.
-
-.TP
-.BR "\-O"
-Omit lock check.
-
-.TP
-.BR "\-P"
-Run bcfg2 in paranoid mode. Diffs will be logged for
-configuration files marked as paranoid by the Bcfg2 server.
-
-.TP
-.BR "\-R <retry count>"
-Specify the number of times that the client will attempt to retry
-network communication.
-
-.TP
-.BR "\-S https://server:port"
-Manually specify the server location (as opposed to using the value in
-bcfg2.conf).
-
-.TP
-.BR "\-b <bundle1>:<bundle2>"
-Run bcfg2 against one or multiple bundles in the configuration.
-
-.TP
-.BR "\-c <cachefile>"
-Cache a copy of the configuration in cachefile.
-
-.TP
-.BR "\-\-ca\-cert=<ca cert>"
-Specifiy the path to the SSL CA certificate.
-
-.TP
-.BR "\-d"
-Run bcfg2 in debug mode.
-
-.TP
-.BR "\-e"
-When in verbose mode, display extra entry information (temporary until
-verbosity rework).
-
-.TP
-.BR "\-f <specification path>"
-Configure from a file rather than querying the server.
-
-.TP
-.BR "\-h"
-Print Usage information.
-
-.TP
-.BR "\-k"
-Run in bulletproof mode. This currently only affects behavior in the
-debian toolset; it calls apt\-get update and clean and
-dpkg \-\-configure \-\-pending.
-
-.TP
-.BR "\-l <whitelist|blacklist|none>"
-Run the client in the server decision list mode (unless "none" is
-specified, which can be done in order to override the decision list mode
-specified in bcfg2.conf). This approach is needed when particular
-changes are deemed "high risk". It gives the ability to centrally
-specify these changes, but only install them on clients when
-administrator supervision is available. Because collaborative
-configuration is one of the remaining hard issues in configuration
-management, these issues typically crop up in environments with several
-administrators and much configuration variety. (This setting will be
-ignored if the -f option is also specified.)
-
-.TP
-.BR "\-n"
-Run bcfg2 in dry\-run mode. No changes will be made to the
-system.
-
-.TP
-.BR "\-o <LogFile Path>"
-Writes a log to the specified path.
-
-.TP
-.BR "\-p <profile>"
-Assert a profile for the current client.
-
-.TP
-.BR "\-q"
-Run bcfg2 in quick mode. Package checksum verification won't be
-performed. This mode relaxes the constraints of correctness, and thus
-should only be used in safe conditions.
-
-.TP
-.BR "\-Q"
-Run bcfg2 in "bundle quick" mode, where only entries in a bundle are
-verified or installed. This runs much faster than -q, but doesn't provide
-statistics to the server at all. In order for this option to work, the
--b option must also be provided. This option is incompatible with -r.
-
-.TP
-.BR "\-r <mode>"
-Cause bcfg2 to remove extra configuration elements it detects. Mode is
-one of all, Services, or Packages. All removes all entries. Likewise,
-Services and Packages remove only the extra configuration elements of
-the respective type.
-
-.TP
-.BR "\-s <service mode>"
-Set bcfg2 interaction level for services. Default behavior is to
-modify all services affected by reconfiguration. build mode attempts
-to stop all services started. disabled suppresses all attempts to
-modify services.
-
-.TP
-.BR "\-\-ssl\-cert=<ssl cert>"
-Specifiy the path to the SSL certificate.
-
-.TP
-.BR "\-\-ssl\-cns=<CommonName1:CommonName2 ...>"
-List of acceptable SSL server Common Names.
-
-.TP
-.BR "\-\-ssl\-key=<ssl key>"
-Specifiy the path to the SSL key.
-
-.TP
-.BR "\-u <user>"
-Attempt to authenticate as 'user'.
-
-.TP
-.BR "\-x <password>"
-Use 'password' for client communication.
-
-.TP
-.BR "\-t <timeout>"
-Set the timeout (in seconds) for client communication. Default is 90
-seconds.
-
-.TP
-.BR "\-v"
-Run bcfg2 in verbose mode.
-
-.TP
-.BR "\-z"
-Only configure independent entries, ignore bundles.
-.RE
+.
+.TH "BCFG2" "1" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\fR \- Bcfg2 client tool
+.
+.SH "SYNOPSIS"
+\fBbcfg2\fR [\fIoptions\fR][\fI\.\.\.\fR]
+.
+.SH "DESCRIPTION"
+\fBbcfg2\fR runs the Bcfg2 configuration process on the current host\. This process consists of the following steps\.
+.
+.IP "\(bu" 4
+Fetch and execute probes
+.
+.IP "\(bu" 4
+Upload probe results
+.
+.IP "\(bu" 4
+Fetch the client configuration
+.
+.IP "\(bu" 4
+Check the current client state
+.
+.IP "\(bu" 4
+Attempt to install the desired configuration
+.
+.IP "\(bu" 4
+Upload statistics about the Bcfg2 execution and client state
+.
+.IP "" 0
+.
+.SH "OPTIONS"
+.
+.TP
+\fB\-C\fR \fIconfigfile\fR
+Specify alternate bcfg2\.conf location
+.
+.TP
+\fB\-D\fR [\fIdriver1\fR,\fIdriver2\fR]
+Specify a set of Bcfg2 tool drivers\.
+.
+.IP
+\fINOTE: only drivers listed will be loaded\. (e\.g\., if you do not include POSIX, you will be unable to verify/install Path entries)\.\fR
+.
+.TP
+\fB\-E\fR \fIencoding\fR
+Specify the encoding of Cfg files\.
+.
+.TP
+\fB\-I\fR
+Run bcfg2 in interactive mode\. The user will be prompted before each change\.
+.
+.TP
+\fB\-O\fR
+Omit lock check
+.
+.TP
+\fB\-P\fR
+Run bcfg2 in paranoid mode\. Diffs will be logged for configuration files marked as paranoid by the Bcfg2 server\.
+.
+.TP
+\fB\-R\fR \fIretry count\fR
+Specify the number of times that the client will attempt to retry network communication\.
+.
+.TP
+\fB\-S\fR \fIhttps://server:port\fR
+Manually specify the server location (as opposed to using the value in bcfg2\.conf)\.
+.
+.TP
+\fB\-b\fR [\fIbundle1:bundle2\fR]
+Run bcfg2 against one or multiple bundles in the configuration\.
+.
+.TP
+\fB\-c\fR \fIcachefile\fR
+Cache a copy of the configuration in cachefile\.
+.
+.TP
+\fB\-\-ca\-cert=\fR\fIca cert\fR
+Specifiy the path to the SSL CA certificate\.
+.
+.TP
+\fB\-d\fR
+Run bcfg2 in debug mode\.
+.
+.TP
+\fB\-e\fR
+When in verbose mode, display extra entry information (temporary until verbosity rework)\.
+.
+.TP
+\fB\-f\fR \fIspecification path\fR
+Configure from a file rather than querying the server\.
+.
+.TP
+\fB\-h\fR
+Print Usage information\.
+.
+.TP
+\fB\-k\fR
+Run in bulletproof mode\. This currently only affects behavior in the debian toolset; it calls apt\-get update and clean and dpkg \-\-configure \-\-pending\.
+.
+.TP
+\fB\-l\fR \fIwhitelist|blacklist|none\fR
+Run the client in the server decision list mode (unless "none" is specified, which can be done in order to override the decision list mode specified in bcfg2\.conf)\. This approach is needed when particular changes are deemed "high risk"\. It gives the ability to centrally specify these changes, but only install them on clients when administrator supervision is available\. Because collaborative configuration is one of the remaining hard issues in configuration management, these issues typically crop up in environments with several administrators and much configuration variety\. (This setting will be ignored if the \-f option is also specified)\.
+.
+.TP
+\fB\-n\fR
+Run bcfg2 in dry\-run mode\. No changes will be made to the system\.
+.
+.TP
+\fB\-o\fR \fIlogfile path\fR
+Writes a log to the specified path\.
+.
+.TP
+\fB\-p\fR \fIprofile\fR
+Assert a profile for the current client\.
+.
+.TP
+\fB\-q\fR
+Run bcfg2 in quick mode\. Package checksum verification won’t be performed\. This mode relaxes the constraints of correctness, and thus should only be used in safe conditions\.
+.
+.TP
+\fB\-Q\fR
+Run bcfg2 in "bundle quick" mode, where only entries in a bundle are verified or installed\. This runs much faster than \-q, but doesn’t provide statistics to the server at all\. In order for this option to work, the \-b option must also be provided\. This option is incompatible with \-r\.
+.
+.TP
+\fB\-r\fR \fImode\fR
+Cause bcfg2 to remove extra configuration elements it detects\. Mode is one of all, Services, or Packages\. All removes all entries\. Likewise, Services and Packages remove only the extra configuration elements of the respective type\.
+.
+.TP
+\fB\-s\fR \fIservice mode\fR
+Set bcfg2 interaction level for services\. Default behavior is to modify all services affected by reconfiguration\. build mode attempts to stop all services started\. disabled suppresses all attempts to modify services
+.
+.TP
+\fB\-\-ssl\-cert=\fR\fIssl cert\fR
+Specifiy the path to the SSL certificate\.
+.
+.TP
+\fB\-\-ssl\-cns=\fR[\fICN1:CN2\fR]
+List of acceptable SSL server Common Names\.
+.
+.TP
+\fB\-\-ssl\-key=\fR\fIssl key\fR
+Specifiy the path to the SSL key\.
+.
+.TP
+\fB\-u\fR \fIuser\fR
+Attempt to authenticate as ’user’\.
+.
+.TP
+\fB\-x\fR \fIpassword\fR
+Use ’password’ for client communication\.
+.
+.TP
+\fB\-t\fR \fItimeout\fR
+Set the timeout (in seconds) for client communication\. Default is 90 seconds\.
+.
+.TP
+\fB\-v\fR
+Run bcfg2 in verbose mode\.
+.
+.TP
+\fB\-z\fR
+Only configure independent entries, ignore bundles\.
+.
.SH "SEE ALSO"
-.BR bcfg2-server(8),
-.BR bcfg2-info(8)
-.SH "BUGS"
+bcfg2\-server(8), bcfg2\-info(8)
diff --git a/man/bcfg2.conf.5 b/man/bcfg2.conf.5
index 684586892..2cb387cad 100644
--- a/man/bcfg2.conf.5
+++ b/man/bcfg2.conf.5
@@ -1,447 +1,395 @@
-.TH bcfg2.conf 5
-
-.SH NAME
-bcfg2.conf - configuration parameters for Bcfg2
-
-.SH DESCRIPTION
-.TP
-bcfg2.conf includes configuration parameters for the Bcfg2 server and
-client.
-
-.SH FILE FORMAT
-The file is INI-style and consists of sections and options. A section
-begins with the name of the sections in square brackets and continues
-until the next section begins.
-
-Options are specified in the form 'name = value'.
-
-The file is line-based each newline-terminated line represents either
-a comment, a section name or an option.
-
-Any line beginning with a hash (#) is ignored, as are lines containing
-only whitespace.
-
-
-.SH SERVER OPTIONS
-These options are only necessary on the Bcfg2 server. They are
-specified in the [server] section of the configuration file.
-
-.TP
-.B repository
-Specifies the path to the Bcfg2 repository containing all of the
-configuration specifications. The repository should be created
-using the 'bcfg2-admin init' command.
-
-.TP
-.B filemonitor
-The file monitor used to watch for changes in the repository.
-Values of 'gamin', 'fam', or 'pseudo' are valid.
-
-.TP
-.B listen_all
-This setting tells the server to listen on all available interfaces. The
-default is to only listen on those interfaces specified by the bcfg2
-setting in the components section of bcfg2.conf.
-
-.TP
-.B plugins
-A comma-delimited list of enabled server plugins. Currently available
-plugins are:
-
-\(bu
-.B Account
-The account plugin manages authentication data, including:
-
- * /etc/passwd
- * /etc/group
- * /etc/security/limits.conf
- * /etc/sudoers
- * /root/.ssh/authorized_keys
-
-\(bu
-.B Actions
-
-Action entries are commands that are executed either before bundle
-installation, after bundle installation or both. If exit status is
-observed, a failing pre-action will cause no modification of the
-enclosing bundle to be performed; all entries included in that bundle
-will not be modified. Failing actions are reported through Bcfg2's
-reporting system, so they can be centrally observed.
-
-\(bu
-.B BB
-The BB plugin maps users to machines and metadata to machines.
-(experimental)
-
-\(bu
-.B Base
-A structure plugin that provides the ability to add lists of unrelated
-entries into client configuration entry inventories. Base works much
-like Bundler in its file format. This structure plugin is good for
-the pile of independent configs needed for most actual systems.
-
-\(bu
-.B Bundler
-Bundler is used to describe groups of inter-dependent configuration
-entries, such as the combination of packages, configuration files,
-and service activations that comprise typical Unix daemons. Bundles
-are used to add groups of configuration entries to the inventory of
-client configurations, as opposed to describing particular versions
-of those
-entries.
-
-\(bu
-.B Bzr
-The Bzr plugin allows you to track changes to your Bcfg2 repository
-using a GNU Bazaar version control backend. Currently, it enables
-you to get revision information out of your repository for reporting
-purposes.
-
-\(bu
-.B Cfg
-The Cfg plugin provides a repository to describe configuration file
-contents for clients. In its simplest form, the Cfg repository is
-just a directory tree modeled off of the directory tree on your client
-machines.
-
-\(bu
-.B Cvs
-The Cvs plugin allows you to track changes to your Bcfg2 repository
-using a Concurrent version control backend. Currently, it enables you
-to get revision information out of your repository for reporting
-purposes. (experimental)
-
-\(bu
-.B Darcs
-The Darcs plugin allows you to track changes to your Bcfg2 repository
-using a Darcs version control backend. Currently, it enables you to
-get revision information out of your repository for reporting purposes.
-(experimental)
-
-\(bu
-.B DBStats
-Direct to database statistics plugin. (0.9.6 and later)
-
-\(bu
-.B Decisions
-The Decisions plugin has support for a centralized set of per-entry
-installation decisions. This approach is needed when particular
-changes are deemed "high risk"; this gives the ability to centrally
-specify these changes, but only install them on clients when
-administrator supervision is available. (0.9.6 and later)
-
-\(bu
-.B Deps
-The Deps plugin allows you to make a series of assertions like
-"Package X requires Package Y (and optionally also Package Z etc.)
-
-\(bu
-.B Editor
-The Editor plugin allows you to partially manage configuration for
-a file. Its use is not recommended and not well documented.
-
-\(bu
-.B Fossil
-The Fossil plugin allows you to track changes to your Bcfg2 repository
-using a Fossil SCM version control backend. Currently, it enables
-you to get revision information out of your repository for reporting
-purposes.
-
-\(bu
-.B Git
-The Git plugin allows you to track changes to your Bcfg2 repository
-using a Git version control backend. Currently, it enables you to
-get revision information out of your repository for reporting purposes.
-
-\(bu
-.B GroupPatterns
-The GroupPatterns plugin is a connector that can assign clients group
-membership based on patterns in client hostnames.
-
-\(bu
-.B Hg
-The Hg plugin allows you to track changes to your Bcfg2 repository
-using a Mercurial version control backend. Currently, it enables you
-to get revision information out of your repository for reporting
-purposes. (experimental)
-
-\(bu
-.B Hostbase
-The Hostbase plugin is an IP management system built on top of Bcfg2.
-
-\(bu
-.B Metadata
-The Metadata plugin is the primary method of specifying Bcfg2 server
-metadata.
-
-\(bu
-.B NagiosGen
-NagiosGen is a Bcfg2 plugin that dynamically generates Nagios
-configuration files based on Bcfg2 data.
-
-\(bu
-.B Ohai
-The Ohai plugin is used to detect information about the client
-operating system. The data is reported back to the server using
-JSON. (experimental)
-
-\(bu
-.B POSIXCompat
-The POSIXCompat plugin provides a compatibility layer which turns
-new-style (1.0) POSIX entries into old-style entries which are
-compatible with previous releases.
-
-\(bu
-.B Packages
-The Packages plugin is an alternative to Pkgmgr for specifying
-package entries for clients. Where Pkgmgr explicitly specifies
-package entry information, Packages delegates control of package
-version information to the underlying package manager, installing
-the latest version available from through those channels.
-
-\(bu
-.B Pkgmgr
-The Pkgmgr plugin resolves the Abstract Configuration Entity
-"Package" to a package specification that the client can use to
-detect, verify and install the specified package.
-
-\(bu
-.B Probes
-The Probes plugin gives you the ability to gather information from a
-client machine before you generate its configuration. This information
-can be used with the various templating systems to generate
-configuration based on the results.
-
-\(bu
-.B Properties
-The Properties plugin is a connector plugin that adds information
-from properties files into client metadata instances. (1.0 and later)
-
-\(bu
-.B Rules
-The Rules plugin resolves Abstract Configuration Entities to literal
-configuration entries suitable for the client drivers to consume.
-
-\(bu
-.B SGenshi (Deprecated)
-See Bundler.
-
-\(bu
-.B Snapshots
-The Snapshots plugin stores various aspects of a client's state when
-the client checks in to the server.
-
-\(bu
-.B SSHbase
-The SSHbase generator plugin manages ssh host keys (both v1 and v2)
-for hosts. It also manages the ssh_known_hosts file. It can integrate
-host keys from other management domains and similarly export its keys.
-
-\(bu
-.B Svn
-The Svn plugin allows you to track changes to your Bcfg2 repository
-using a Subversion backend. Currently, it enables you to get revision
-information out of your repository for reporting purposes.
-
-\(bu
-.B TCheetah
-The TCheetah plugin allows you to use the cheetah templating system
-to create files. It also allows you to include the results of probes
-executed on the client in the created files.
-
-\(bu
-.B TGenshi
-The TGenshi plugin allows you to use the Genshi templating system to
-create files. It also allows you to include the results of probes
-executed on the client in the created files.
-
-\(bu
-.B Trigger
-Trigger is a plugin that calls external scripts when clients are
-configured.
-
-.TP
-.B prefix
-Specifies a prefix if the Bcfg2 installation isn't placed in the
-default location (eg. /usr/local).
-
-.SH MDATA OPTIONS
-These options affect the default metadata settings for Paths with
-type='file'.
-
-.TP
-.B owner
+.
+.TH "BCFG2\.CONF" "5" "June 2012" "" ""
+.
+.SH "NAME"
+\fBbcfg2\.conf\fR \- configuration parameters for Bcfg2
+.
+.SH "DESCRIPTION"
+\fBbcfg2\.conf\fR includes configuration parameters for the Bcfg2 server and client\.
+.
+.SH "FILE FORMAT"
+The file is INI\-style and consists of sections and options\. A section begins with the name of the sections in square brackets and continues until the next section begins\.
+.
+.P
+Options are specified in the form "name=value"\.
+.
+.P
+The file is line\-based each newline\-terminated line represents either a comment, a section name or an option\.
+.
+.P
+Any line beginning with a hash (#) is ignored, as are lines containing only whitespace\.
+.
+.SH "SERVER OPTIONS"
+These options are only necessary on the Bcfg2 server\. They are specified in the \fB[server]\fR section of the configuration file\.
+.
+.TP
+\fBrepository\fR
+Specifies the path to the Bcfg2 repository containing all of the configuration specifications\. The repository should be created using the \fBbcfg2\-admin init\fR command\.
+.
+.TP
+\fBfilemonitor\fR
+The file monitor used to watch for changes in the repository\. The default is the best available monitor\. The following values are valid:
+.
+.IP
+\fBinotify\fR, \fBgamin\fR, \fBfam\fR, \fBpseudo\fR
+.
+.TP
+\fBignore_files\fR
+A comma\-separated list of globs that should be ignored by the file monitor\. Default values are:
+.
+.IP
+\fB*~\fR, \fB*#\fR, \fB\.#*\fR, \fB*\.swp\fR, \fB\.*\.swx\fR, \fBSCCS\fR, \fB\.svn\fR, \fB4913\fR, \fB\.gitignore\fR
+.
+.TP
+\fBlisten_all\fR
+This setting tells the server to listen on all available interfaces\. The default is to only listen on those interfaces specified by the bcfg2 setting in the components section of \fBbcfg2\.conf\fR\.
+.
+.TP
+\fBplugins\fR
+A comma\-delimited list of enabled server plugins\. Currently available plugins are:
+.
+.IP
+\fBAccount\fR, \fBActions\fR, \fBBB\fR, \fBBase\fR, \fBBundler\fR, \fBBzr\fR, \fBCfg\fR, \fBCvs\fR, \fBDarcs\fR, \fBDBStats\fR, \fBDecisions\fR, \fBDeps\fR, \fBEditor\fR, \fBFossil\fR, \fBGit\fR, \fBGroupPatterns\fR, \fBHg\fR, \fBHostbase\fR, \fBMetadata\fR, \fBNagiosGen\fR, \fBOhai\fR, \fBPackages\fR, \fBPkgmgr\fR, \fBProbes\fR, \fBProperties\fR, \fBRules\fR, \fBSGenshi\fR, \fBSnapshots\fR, \fBSSHbase\fR, \fBSvn\fR, \fBSvn2\fR, \fBTCheetah\fR, \fBTGenshi\fR, \fBTrigger\fR
+.
+.IP
+Descriptions of each plugin can be found in their respective sections below\.
+.
+.TP
+\fBprefix\fR
+Specifies a prefix if the Bcfg2 installation isn’t placed in the default location (e\.g\. /usr/local)\.
+.
+.SS "Account Plugin"
+The account plugin manages authentication data, including the following\.
+.
+.IP "\(bu" 4
+\fB/etc/passwd\fR
+.
+.IP "\(bu" 4
+\fB/etc/group\fR
+.
+.IP "\(bu" 4
+\fB/etc/security/limits\.conf\fR
+.
+.IP "\(bu" 4
+\fB/etc/sudoers\fR
+.
+.IP "\(bu" 4
+\fB/root/\.ssh/authorized_keys\fR
+.
+.IP "" 0
+.
+.SS "BB Plugin"
+The BB plugin maps users to machines and metadata to machines\.
+.
+.SS "Base Plugin"
+A structure plugin that provides the ability to add lists of unrelated entries into client configuration entry inventories\. Base works much like Bundler in its file format\. This structure plugin is good for the pile of independent configs needed for most actual systems\.
+.
+.SS "Bundler Plugin"
+Bundler is used to describe groups of inter\-dependent configuration entries, such as the combination of packages, configuration files, and service activations that comprise typical Unix daemons\. Bundles are used to add groups of configuration entries to the inventory of client configurations, as opposed to describing particular versions of those entries\.
+.
+.SS "Bzr Plugin"
+The Bzr plugin allows you to track changes to your Bcfg2 repository using a GNU Bazaar version control backend\. Currently, it enables you to get revision information out of your repository for reporting purposes\.
+.
+.SS "Cfg Plugin"
+The Cfg plugin provides a repository to describe configuration file contents for clients\. In its simplest form, the Cfg repository is just a directory tree modeled off of the directory tree on your client machines\.
+.
+.SS "Cvs Plugin (experimental)"
+The Cvs plugin allows you to track changes to your Bcfg2 repository using a Concurrent version control backend\. Currently, it enables you to get revision information out of your repository for reporting purposes\.
+.
+.SS "Darcs Plugin (experimental)"
+The Darcs plugin allows you to track changes to your Bcfg2 repository using a Darcs version control backend\. Currently, it enables you to get revision information out of your repository for reporting purposes\.
+.
+.SS "DBStats Plugin"
+Direct to database statistics plugin\.
+.
+.SS "Decisions Plugin"
+The Decisions plugin has support for a centralized set of per\-entry installation decisions\. This approach is needed when particular changes are deemed "\fIhigh risk\fR"; this gives the ability to centrally specify these changes, but only install them on clients when administrator supervision is available\.
+.
+.SS "Deps Plugin"
+The Deps plugin allows you to make a series of assertions like "Package X requires Package Y (and optionally also Package Z etc\.)"
+.
+.SS "Editor Plugin"
+The Editor plugin attempts to allow you to partially manage configuration for a file\. Its use is not recommended and not well documented\.
+.
+.SS "Fossil Plugin"
+The Fossil plugin allows you to track changes to your Bcfg2 repository using a Fossil SCM version control backend\. Currently, it enables you to get revision information out of your repository for reporting purposes\.
+.
+.SS "Git Plugin"
+The Git plugin allows you to track changes to your Bcfg2 repository using a Git version control backend\. Currently, it enables you to get revision information out of your repository for reporting purposes\.
+.
+.SS "GroupPatterns Plugin"
+The GroupPatterns plugin is a connector that can assign clients group membership based on patterns in client hostnames\.
+.
+.SS "Hg Plugin (experimental)"
+The Hg plugin allows you to track changes to your Bcfg2 repository using a Mercurial version control backend\. Currently, it enables you to get revision information out of your repository for reporting purposes\.
+.
+.SS "Hostbase Plugin"
+The Hostbase plugin is an IP management system built on top of Bcfg2\.
+.
+.SS "Metadata Plugin"
+The Metadata plugin is the primary method of specifying Bcfg2 server metadata\.
+.
+.SS "NagiosGen Plugin"
+NagiosGen is a Bcfg2 plugin that dynamically generates Nagios configuration files based on Bcfg2 data\.
+.
+.SS "Ohai Plugin (experimental)"
+The Ohai plugin is used to detect information about the client operating system\. The data is reported back to the server using JSON\.
+.
+.SS "Packages Plugin"
+The Packages plugin is an alternative to Pkgmgr for specifying package entries for clients\. Where Pkgmgr explicitly specifies package entry information, Packages delegates control of package version information to the underlying package manager, installing the latest version available from through those channels\.
+.
+.SS "Pkgmgr Plugin"
+The Pkgmgr plugin resolves the Abstract Configuration Entity "Package" to a package specification that the client can use to detect, verify and install the specified package\.
+.
+.SS "Probes Plugin"
+The Probes plugin gives you the ability to gather information from a client machine before you generate its configuration\. This information can be used with the various templating systems to generate configuration based on the results\.
+.
+.SS "Properties Plugin"
+The Properties plugin is a connector plugin that adds information from properties files into client metadata instances\.
+.
+.SS "Rules Plugin"
+The Rules plugin provides literal configuration entries that resolve the abstract configuration entries normally found in the Bundler and Base plugins\. The literal entries in Rules are suitable for consumption by the appropriate client drivers\.
+.
+.SS "Snapshots Plugin"
+The Snapshots plugin stores various aspects of a client’s state when the client checks in to the server\.
+.
+.SS "SSHbase Plugin"
+The SSHbase generator plugin manages ssh host keys (both v1 and v2) for hosts\. It also manages the ssh_known_hosts file\. It can integrate host keys from other management domains and similarly export its keys\.
+.
+.SS "Svn Plugin"
+The Svn plugin allows you to track changes to your Bcfg2 repository using a Subversion backend\. Currently, it enables you to get revision information out of your repository for reporting purposes\.
+.
+.SS "Svn2 Plugin"
+The Svn2 plugin extends on the capabilities in the Svn plugin\. It provides Update and Commit methods which provide hooks for modifying subversion\-backed Bcfg2 repositories\.
+.
+.SS "TCheetah Plugin"
+The TCheetah plugin allows you to use the cheetah templating system to create files\. It also allows you to include the results of probes executed on the client in the created files\.
+.
+.SS "TGenshi Plugin"
+The TGenshi plugin allows you to use the Genshi templating system to create files\. It also allows you to include the results of probes executed on the client in the created files\.
+.
+.SS "Trigger Plugin"
+The Trigger plugin provides a method for calling external scripts when clients are configured\.
+.
+.SH "CLIENT OPTIONS"
+These options only affect client functionality, specified in the \fB[client]\fR section\.
+.
+.TP
+\fBdecision\fR
+Specify the server decision list mode (whitelist or blacklist)\. (This settiing will be ignored if the client is called with the \-f option\.)
+.
+.TP
+\fBdrivers\fR
+Specify tool driver set to use\. This option can be used to explicitly specify the client tool drivers you want to use when the client is run\.
+.
+.TP
+\fBparanoid\fR
+Run the client in paranoid mode\.
+.
+.SH "COMMUNICATION OPTIONS"
+Specified in the \fB[communication]\fR section\. These options define settings used for client\-server communication\.
+.
+.TP
+\fBca\fR
+The path to a file containing the CA certificate\. This file is required on the server, and optional on clients\. However, if the cacert is not present on clients, the server cannot be verified\.
+.
+.TP
+\fBcertificate\fR
+The path to a file containing a PEM formatted certificate which signs the key with the ca certificate\. This setting is required on the server in all cases, and required on clients if using client certificates\.
+.
+.TP
+\fBkey\fR
+Specifies the path to a file containing the SSL Key\. This is required on the server in all cases, and required on clients if using client certificates\.
+.
+.TP
+\fBpassword\fR
+Required on both the server and clients\. On the server, sets the password clients need to use to communicate\. On a client, sets the password to use to connect to the server\.
+.
+.TP
+\fBprotocol\fR
+Communication protocol to use\. Defaults to xmlrpc/ssl\.
+.
+.TP
+\fBretries\fR
+A client\-only option\. Number of times to retry network communication\.
+.
+.TP
+\fBserverCommonNames\fR
+A client\-only option\. A colon\-separated list of Common Names the client will accept in the SSL certificate presented by the server\.
+.
+.TP
+\fBuser\fR
+A client\-only option\. The UUID of the client\.
+.
+.SH "COMPONENT OPTIONS"
+Specified in the \fB[components]\fR section\.
+.
+.TP
+\fBbcfg2\fR
+URL of the server\. On the server this specifies which interface and port the server listens on\. On the client, this specifies where the client will attempt to contact the server\.
+.
+.IP
+e\.g\. \fBbcfg2 = https://10\.3\.1\.6:6789\fR
+.
+.TP
+\fBencoding\fR
+Text encoding of configuration files\. Defaults to UTF\-8\.
+.
+.SH "LOGGING OPTIONS"
+Specified in the \fB[logging]\fR section\. These options control the server logging functionality\.
+.
+.TP
+\fBpath\fR
+Server log file path\.
+.
+.SH "MDATA OPTIONS"
+These options affect the default metadata settings for Paths with type=’file’\.
+.
+.TP
+\fBowner\fR
Global owner for Paths (defaults to root)
-
+.
.TP
-.B group
+\fBgroup\fR
Global group for Paths (defaults to root)
-
+.
.TP
-.B perms
+\fBperms\fR
Global permissions for Paths (defaults to 644)
-
+.
.TP
-.B paranoid
+\fBparanoid\fR
Global paranoid settings for Paths (defaults to false)
-
+.
.TP
-.B sensitive
+\fBsensitive\fR
Global sensitive settings for Paths (defaults to false)
-
-
-.SH CLIENT OPTIONS
-These options only affect client functionality, specified in the
-[client] section.
-
-.TP
-.B decision
-Specify the server decision list mode (whitelist or blacklist). (This
-setting will be ignored if the client is called with the -f option.)
-
-.TP
-.B drivers
-Specify tool driver set to use. This option can be used to explicitly
-specify the client tool drivers you want to use when the client is run.
-
-.TP
-.B paranoid
-Run the client in paranoid mode.
-
-
-.SH STATISTICS OPTIONS
-Server-only, specified in the [statistics] section. These options
-control the statistics collection functionality of the server.
-
-.TP
-.B database_engine
-The database engine used by the statistics module. One of either
-\[oq]postgresql\[cq], \[oq]mysql\[cq], \[oq]sqlite3\[cq], or
-\[oq]ado_mssql\[cq].
-
-.TP
-.B database_name
-The name of the database to use for statistics data. If
-\[oq]database_engine\[cq] is set to \[oq]sqlite3\[cq] this is a file
-path to sqlite file and defaults to $REPOSITORY_DIR/etc/brpt.sqlite
-
-.TP
-.B database_user
-User for database connections. Not used for sqlite3.
-
-.TP
-.B database_password
-Password for database connections. Not used for sqlite3.
-
-.TP
-.B database_host
-Host for database connections. Not used for sqlite3.
-
-.TP
-.B database_port
-Port for database connections. Not used for sqlite3.
-
-.TP
-.B time_zone
-Specify a time zone other than that used on the system. (Note that this
-will cause the bcfg2 server to log messages in this time zone as well).
-
-
-.SH COMMUNICATION OPTIONS
-Specified in the [communication] section. These options define
-settings used for client-server communication.
-
-.TP
-.B ca
-The path to a file containing the CA certificate. This file is
-required on the server, and optional on clients. However, if the
-cacert is not present on clients, the server cannot be verified.
-
-.TP
-.B certificate
-The path to a file containing a PEM formatted certificate which
-signs the key with the ca certificate. This setting is required on
-the server in all cases, and required on clients if using client
-certificates.
-
-.TP
-.B key
-Specifies the path to a file containing the SSL Key. This is required
-on the server in all cases, and required on clients if using client
-certificates.
-
-.TP
-.B password
-Required on both the server and clients. On the server, sets the
-password clients need to use to communicate. On a client, sets the
-password to use to connect to the server.
-
-.TP
-.B protocol
-Communication protocol to use. Defaults to xmlrpc/ssl.
-
-.TP
-.B retries
-A client-only option. Number of times to retry network communication.
-
-.TP
-.B serverCommonNames
-A client-only option. A colon-separated list of Common Names the client
-will accept in the SSL certificate presented by the server.
-
-.TP
-.B user
-A client-only option. The UUID of the client.
-
-.SH PARANOID OPTIONS
-These options allow for finer-grained control of the paranoid mode
-on the Bcfg2 client. They are specified in the [paranoid] section
-of the configuration file.
-
-.TP
-.B path
-Custom path for backups created in paranoid mode. The default is in
-/var/cache/bcfg2.
-
-.TP
-.B max_copies
-Specify a maximum number of copies for the server to keep when running
-in paranoid mode. Only the most recent versions of these copies will
-be kept.
-
-.SH COMPONENT OPTIONS
-Specified in the [components] section.
-
-.TP
-.B bcfg2
-URL of the server. On the server this specifies which interface and
-port the server listens on. On the client, this specifies where the
-client will attempt to contact the server.
-eg: bcfg2 = https://10.3.1.6:6789
-
-.TP
-.B encoding
-Text encoding of configuration files. Defaults to UTF-8.
-
-.SH LOGGING OPTIONS
-Specified in the [logging] section. These options control the server
-logging functionality.
-
-.B path
-Server log file path.
-
-.SH SNAPSHOTS OPTIONS
-Specified in the [snapshots] section. These options control the server
-snapshots functionality.
-
-.B driver
+.
+.SH "PACKAGES OPTIONS"
+The following options are specified in the \fB[packages]\fR section of the configuration file\.
+.
+.TP
+\fBresolver\fR
+Enable dependency resolution\. Default is 1 (true)\.
+.
+.TP
+\fBmetadata\fR
+Enable metadata processing\. Default is 1 (true)\. If metadata is disabled, it’s implied that resolver is also disabled\.
+.
+.TP
+\fByum_config\fR
+The path at which to generate Yum configs\. No default\.
+.
+.TP
+\fBapt_config\fR
+The path at which to generate APT configs\. No default\.
+.
+.TP
+\fBgpg_keypath\fR
+The path on the client where RPM GPG keys will be copied before they are imported on the client\. Default is \fB/etc/pki/rpm\-gpg\fR\.
+.
+.TP
+\fBversion\fR
+Set the version attribute used when binding Packages\. Default is auto\.
+.
+.P
+The following options are specified in the \fB[packages:yum]\fR section of the configuration file\.
+.
+.TP
+\fBuse_yum_libraries\fR
+By default, Bcfg2 uses an internal implementation of Yum’s dependency resolution and other routines so that the Bcfg2 server can be run on a host that does not support Yum itself\. If you run the Bcfg2 server on a machine that does have Yum libraries, however, you can enable use of those native libraries in Bcfg2 by setting this to 1\.
+.
+.TP
+\fBhelper\fR
+Path to bcfg2\-yum\-helper\. By default, Bcfg2 looks first in $PATH and then in \fB/usr/sbin/bcfg2\-yum\-helper\fR for the helper\.
+.
+.P
+All other options in the \fB[packages:yum]\fR section will be passed along verbatim to the Yum configuration if you are using the native Yum library support\.
+.
+.P
+The following options are specified in the \fB[packages:pulp]\fR section of the configuration file\.
+.
+.TP
+\fBusername\fR
+The username of a Pulp user that will be used to register new clients and bind them to repositories\.
+.
+.TP
+\fBpassword\fR
+The password of a Pulp user that will be used to register new clients and bind them to repositories\.
+.
+.SH "PARANOID OPTIONS"
+These options allow for finer\-grained control of the paranoid mode on the Bcfg2 client\. They are specified in the \fB[paranoid]\fR section of the configuration file\.
+.
+.TP
+\fBpath\fR
+Custom path for backups created in paranoid mode\. The default is in \fB/var/cache/bcfg2\fR\.
+.
+.TP
+\fBmax_copies\fR
+Specify a maximum number of copies for the server to keep when running in paranoid mode\. Only the most recent versions of these copies will be kept\.
+.
+.SH "SNAPSHOTS OPTIONS"
+Specified in the \fB[snapshots]\fR section\. These options control the server snapshots functionality\.
+.
+.TP
+\fBdriver\fR
sqlite
-
-.B database
-The name of the database to use for statistics data.
-eg: $REPOSITORY_DIR/etc/bcfg2.sqlite
-
-.SH SEE ALSO
-.BR bcfg2(1),
-.BR bcfg2-server(8)
-
+.
+.TP
+\fBdatabase\fR
+The name of the database to use for statistics data\.
+.
+.IP
+eg: \fB$REPOSITORY_DIR/etc/bcfg2\.sqlite\fR
+.
+.SH "SSLCA OPTIONS"
+These options are necessary to configure the SSLCA plugin and can be found in the \fB[sslca_default]\fR section of the configuration file\.
+.
+.TP
+\fBconfig\fR
+Specifies the location of the openssl configuration file for your CA\.
+.
+.TP
+\fBpassphrase\fR
+Specifies the passphrase for the CA’s private key (if necessary)\. If no passphrase exists, it is assumed that the private key is stored unencrypted\.
+.
+.TP
+\fBchaincert\fR
+Specifies the location of your ssl chaining certificate\. This is used when pre\-existing certifcate hostfiles are found, so that they can be validated and only regenerated if they no longer meet the specification\. If you’re using a self signing CA this would be the CA cert that you generated\.
+.
+.SH "STATISTICS OPTIONS"
+Server\-only, specified in the \fB[statistics]\fR section\. These options control the statistics collection functionality of the server\.
+.
+.TP
+\fBdatabase_engine\fR
+The database engine used by the statistics module\. One of the following:
+.
+.IP
+\fBpostgresql\fR, \fBmysql\fR, \fBsqlite3\fR, \fBado_mssql\fR
+.
+.TP
+\fBdatabase_name\fR
+The name of the database to use for statistics data\. If ‘database_engine’ is set to ‘sqlite3’ this is a file path to sqlite file and defaults to \fB$REPOSITORY_DIR/etc/brpt\.sqlite\fR\.
+.
+.TP
+\fBdatabase_user\fR
+User for database connections\. Not used for sqlite3\.
+.
+.TP
+\fBdatabase_password\fR
+Password for database connections\. Not used for sqlite3\.
+.
+.TP
+\fBdatabase_host\fR
+Host for database connections\. Not used for sqlite3\.
+.
+.TP
+\fBdatabase_port\fR
+Port for database connections\. Not used for sqlite3\.
+.
+.TP
+\fBtime_zone\fR
+Specify a time zone other than that used on the system\. (Note that this will cause the Bcfg2 server to log messages in this time zone as well)\.
+.
+.SH "SEE ALSO"
+bcfg2(1), bcfg2\-server(8)
diff --git a/misc/bcfg2.spec b/misc/bcfg2.spec
index 75c6090a0..832f6b569 100644
--- a/misc/bcfg2.spec
+++ b/misc/bcfg2.spec
@@ -51,7 +51,6 @@ BuildRequires: python-sphinx10
BuildRequires: python-sphinx >= 0.6
%endif
-Requires: python-nose
Requires: python-lxml >= 0.9
%if 0%{?rhel_version}
# the debian init script needs redhat-lsb.
@@ -109,6 +108,7 @@ Requires: gamin-python
%endif
Requires: /usr/sbin/sendmail
Requires: /usr/bin/openssl
+Requires: python-nose
%description server
Bcfg2 helps system administrators produce a consistent, reproducible,
@@ -260,8 +260,8 @@ ln -s %{_initrddir}/bcfg2 %{buildroot}%{_sbindir}/rcbcfg2
ln -s %{_initrddir}/bcfg2-server %{buildroot}%{_sbindir}/rcbcfg2-server
%endif
-mv build/sphinx/html/* %{buildroot}%{_defaultdocdir}/bcfg2-doc-%{version}
-mv build/dtd %{buildroot}%{_defaultdocdir}/bcfg2-doc-%{version}/
+cp -r build/sphinx/html/* %{buildroot}%{_defaultdocdir}/bcfg2-doc-%{version}
+cp -r build/dtd %{buildroot}%{_defaultdocdir}/bcfg2-doc-%{version}/
%{__install} -d %{buildroot}%{apache_conf}/conf.d
%{__install} -m 644 misc/apache/bcfg2.conf %{buildroot}%{apache_conf}/conf.d/wsgi_bcfg2.conf
diff --git a/reports/site_media/bcfg2_base.css b/reports/site_media/bcfg2_base.css
index d74c1b618..ae7e145f1 100644
--- a/reports/site_media/bcfg2_base.css
+++ b/reports/site_media/bcfg2_base.css
@@ -133,6 +133,16 @@ ul.menu-level2 {
color: #10324b;
text-decoration: none;
}
+/*
+ * Convenience for templating..
+ */
+.bad-lineitem {
+ background: #FF7777;
+}
+.bad-lineitem a {
+ color: #10324b;
+ text-decoration: none;
+}
.clean-lineitem {
background: #AAFFBB;
}
@@ -264,3 +274,9 @@ span.nav_bar_current {
border: 1px solid #98DBCC;
padding: 1px;
}
+#threshold_box {
+ border: 1px solid #98DBCC;
+ margin-top: 5px;
+ width: 640px;
+ padding: 5px;
+}
diff --git a/schemas/base.xsd b/schemas/base.xsd
index cca665b38..98682fdb5 100644
--- a/schemas/base.xsd
+++ b/schemas/base.xsd
@@ -10,6 +10,7 @@
<xsd:include schemaLocation="atom.xsd"/>
<xsd:include schemaLocation="pathentry.xsd"/>
<xsd:include schemaLocation="rules.xsd"/>
+ <xsd:include schemaLocation="types.xsd"/>
<xsd:group name='BaseEntries'>
<xsd:choice>
@@ -19,7 +20,7 @@
<xsd:element name='Path' type='PathEntry'/>
<xsd:element name='Service' type='StructureEntry'/>
<xsd:element name='BoundPackage' type='PackageType'/>
- <xsd:element name='BoundPath' type='BoundPathEntry'/>
+ <xsd:element name='BoundPath' type='PathType'/>
<xsd:element name='BoundService' type='ServiceType'/>
</xsd:choice>
</xsd:group>
diff --git a/schemas/bundle.xsd b/schemas/bundle.xsd
index 4e034ee3c..b72b174e2 100644
--- a/schemas/bundle.xsd
+++ b/schemas/bundle.xsd
@@ -16,10 +16,12 @@
<xsd:include schemaLocation="atom.xsd"/>
<xsd:include schemaLocation="pathentry.xsd"/>
<xsd:include schemaLocation="rules.xsd"/>
+ <xsd:include schemaLocation="types.xsd"/>
<xsd:include schemaLocation="services.xsd"/>
- <xsd:complexType name='GroupType'>
- <xsd:choice minOccurs='0' maxOccurs='unbounded'>
+ <xsd:group name="bundleElements">
+ <xsd:choice>
+ <xsd:group ref="py:genshiElements"/>
<xsd:element name='Package' type='PackageStructure'>
<xsd:annotation>
<xsd:documentation>
@@ -56,12 +58,20 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
+ <xsd:element name='SELinux' type='SELinuxStructure'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Abstract implementation of an SELinux entry. The
+ full specification will be included in Rules.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
<xsd:element name='PostInstall' type='StructureEntry'>
<xsd:annotation>
<xsd:documentation>
PostInstall entries are deprecated in favor of Action
- entries. Actions can do everything PostInstall entries can
- do and more.
+ entries. Actions can do everything PostInstall entries can
+ do and more.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
@@ -72,7 +82,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
- <xsd:element name='BoundPath' type='BoundPathEntry'>
+ <xsd:element name='BoundPath' type='PathType'>
<xsd:annotation>
<xsd:documentation>
Fully bound description of a filesystem path to be handled
@@ -94,6 +104,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
+ <xsd:element name='BoundSELinux' type='SELinuxType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of an SELinux entry.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
<xsd:element name='Group' type='GroupType'>
<xsd:annotation>
<xsd:documentation>
@@ -107,7 +124,7 @@
<xsd:annotation>
<xsd:documentation>
Elements within Client tags only apply to the named client
- (or vice-versa; see #element_negate below)
+ (or vice-versa; see #element_negate below)
</xsd:documentation>
</xsd:annotation>
</xsd:element>
@@ -119,7 +136,12 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
- <xsd:group ref="py:genshiElements"/>
+ </xsd:choice>
+ </xsd:group>
+
+ <xsd:complexType name='GroupType'>
+ <xsd:choice minOccurs='0' maxOccurs='unbounded'>
+ <xsd:group ref="bundleElements"/>
</xsd:choice>
<xsd:attribute type='xsd:string' name='name' use='required'>
<xsd:annotation>
@@ -140,96 +162,7 @@
<xsd:complexType name='BundleType'>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='Package' type='PackageStructure'>
- <xsd:annotation>
- <xsd:documentation>
- Abstract implementation of a Package entry. The full
- specification will be generated by a plugin such as
- Packages.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Path' type='PathEntry'>
- <xsd:annotation>
- <xsd:documentation>
- Abstract implementation of a Path entry. The entry will
- either be handled by Cfg, TGenshi, or another
- DirectoryBacked plugin; or handled by Rules, in which case
- the full specification of this entry will be included in
- Rules.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Service' type='StructureEntry'>
- <xsd:annotation>
- <xsd:documentation>
- Abstract implementation of a Service entry. The full
- specification will be included in Rules.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Action' type='StructureEntry'>
- <xsd:annotation>
- <xsd:documentation>
- Abstract implementation of an Action entry. The full
- specification will be included in Rules.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundPackage' type='PackageType'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a software package to be managed.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundPath' type='BoundPathEntry'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a filesystem path to be handled
- by the POSIX driver.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundService' type='ServiceType'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a system service to be managed.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundAction' type='ActionType'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a command to be run.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Group' type='GroupType'>
- <xsd:annotation>
- <xsd:documentation>
- Elements within Group tags only apply to clients that are
- members of that group
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Client' type='GroupType'>
- <xsd:annotation>
- <xsd:documentation>
- Elements within Client tags only apply to the named client
- (or vice-versa; see #element_negate below)
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Bundle' type='BundleType'>
- <xsd:annotation>
- <xsd:documentation>
- Nesting Bundle tags is allowed in order to support
- XInclude within Bundles.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:group ref="py:genshiElements"/>
+ <xsd:group ref="bundleElements"/>
</xsd:choice>
<xsd:attribute type='xsd:string' name='description' />
<xsd:attribute type='xsd:string' name='name'/>
diff --git a/schemas/clients.xsd b/schemas/clients.xsd
index 56f458a45..d50f3626e 100644
--- a/schemas/clients.xsd
+++ b/schemas/clients.xsd
@@ -29,6 +29,7 @@
<xsd:attribute type='xsd:string' name='secure'/>
<xsd:attribute type='xsd:string' name='pingtime' use='optional'/>
<xsd:attribute type='xsd:string' name='address'/>
+ <xsd:attribute type='xsd:string' name='version'/>
</xsd:complexType>
<xsd:complexType name='ClientsType'>
diff --git a/schemas/defaults.xsd b/schemas/defaults.xsd
index c7e2edc7e..17ae84366 100644
--- a/schemas/defaults.xsd
+++ b/schemas/defaults.xsd
@@ -11,33 +11,6 @@
<xsd:include schemaLocation="types.xsd"/>
<xsd:include schemaLocation="pkgtype.xsd"/>
- <xsd:complexType name="ActionType">
- <xsd:attribute type="ActionTimingEnum" name="timing"/>
- <xsd:attribute type="ActionWhenEnum" name="when"/>
- <xsd:attribute type="ActionStatusEnum" name="status"/>
- <xsd:attribute type="xsd:boolean" name="build"/>
- <xsd:attribute type="xsd:string" name="name" use="required"/>
- <xsd:attribute type="xsd:string" name="command"/>
- </xsd:complexType>
-
- <xsd:complexType name="PathType">
- <xsd:attribute type="PathTypeEnum" name="type"/>
- <xsd:attribute type="xsd:string" name="name" use="required"/>
- <xsd:attribute type="xsd:string" name="dev_type"/>
- <xsd:attribute type="xsd:string" name="major"/>
- <xsd:attribute type="xsd:string" name="minor"/>
- <xsd:attribute type="xsd:string" name="mode"/>
- <xsd:attribute type="xsd:string" name="perms"/>
- <xsd:attribute type="xsd:string" name="owner"/>
- <xsd:attribute type="xsd:string" name="group"/>
- <xsd:attribute type="xsd:string" name="recursive"/>
- <xsd:attribute type="xsd:string" name="prune"/>
- <xsd:attribute type="xsd:string" name="to"/>
- <xsd:attribute type="xsd:string" name="vcstype"/>
- <xsd:attribute type="xsd:string" name="revision"/>
- <xsd:attribute type="xsd:string" name="sourceurl"/>
- </xsd:complexType>
-
<xsd:complexType name="DContainerType">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="Service" type="ServiceType"/>
diff --git a/schemas/info.xsd b/schemas/info.xsd
index 37232ab23..c45c40ebc 100644
--- a/schemas/info.xsd
+++ b/schemas/info.xsd
@@ -14,6 +14,7 @@
<xsd:attribute name='important' type='xsd:string'/>
<xsd:attribute name='owner' type='xsd:string'/>
<xsd:attribute name='perms' type='xsd:string'/>
+ <xsd:attribute name='secontext' type='xsd:string'/>
<xsd:attribute name='paranoid' type='xsd:boolean'/>
<xsd:attribute name='sensitive' type='xsd:boolean'/>
</xsd:complexType>
diff --git a/schemas/packages.xsd b/schemas/packages.xsd
index c29a85ecf..c4252194f 100644
--- a/schemas/packages.xsd
+++ b/schemas/packages.xsd
@@ -18,11 +18,18 @@
</xsd:restriction>
</xsd:simpleType>
+ <xsd:complexType name="RepoOptionsType">
+ <xsd:attribute type="xsd:boolean" name="serveronly"/>
+ <xsd:attribute type="xsd:boolean" name="clientonly"/>
+ <xsd:anyAttribute processContents="lax"/>
+ </xsd:complexType>
+
<xsd:complexType name="sourceType">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="Component" type="xsd:string"/>
<xsd:element name="Arch" type="xsd:string"/>
<xsd:element name="GPGKey" type="xsd:string"/>
+ <xsd:element name="Options" type="RepoOptionsType"/>
<xsd:choice>
<xsd:element name="Blacklist" type="xsd:string"/>
<xsd:element name="Whitelist" type="xsd:string"/>
diff --git a/schemas/pathentry.xsd b/schemas/pathentry.xsd
index 080758d0b..e5d2ef6af 100644
--- a/schemas/pathentry.xsd
+++ b/schemas/pathentry.xsd
@@ -16,25 +16,4 @@
<xsd:attribute type='xsd:string' name='altsrc' use='optional'/>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
-
- <xsd:complexType name='BoundPathEntry'>
- <xsd:attribute type='xsd:string' name='name' use='required'/>
- <xsd:attribute type='xsd:string' name='group' use='optional'/>
- <xsd:attribute type='xsd:string' name='important' use='optional'/>
- <xsd:attribute type='xsd:string' name='owner' use='optional'/>
- <xsd:attribute type='xsd:string' name='paranoid' use='optional'/>
- <xsd:attribute type='xsd:string' name='perms' use='optional'/>
- <xsd:attribute type='xsd:string' name='prune' use='optional'/>
- <xsd:attribute type='xsd:string' name='recursive' use='optional'/>
- <xsd:attribute type='xsd:string' name='sensitive' use='optional'/>
- <xsd:attribute type='xsd:string' name='to' use='optional'/>
- <xsd:attribute type='xsd:string' name='type' use='optional'/>
- <!-- device attributes -->
- <xsd:attribute type='xsd:string' name='dev_type' use='optional'/>
- <xsd:attribute type='xsd:string' name='major' use='optional'/>
- <xsd:attribute type='xsd:string' name='minor' use='optional'/>
- <xsd:attribute type='xsd:string' name='mode' use='optional'/>
- <!-- end device attributes -->
- <xsd:attributeGroup ref="py:genshiAttrs"/>
- </xsd:complexType>
</xsd:schema>
diff --git a/schemas/rules.xsd b/schemas/rules.xsd
index 924792b18..2f4f805c0 100644
--- a/schemas/rules.xsd
+++ b/schemas/rules.xsd
@@ -14,66 +14,91 @@
<xsd:import namespace="http://genshi.edgewall.org/"
schemaLocation="genshi.xsd"/>
- <xsd:complexType name='ActionType'>
- <xsd:attribute type='ActionTimingEnum' name='timing'/>
- <xsd:attribute type='ActionWhenEnum' name='when'/>
- <xsd:attribute type='ActionStatusEnum' name='status'/>
- <xsd:attribute type="xsd:boolean" name="build"/>
- <xsd:attribute type='xsd:string' name='name'/>
- <xsd:attribute type='xsd:string' name='command'/>
- <xsd:attributeGroup ref="py:genshiAttrs"/>
- </xsd:complexType>
-
<xsd:complexType name='PostInstallType'>
<xsd:attribute type='xsd:string' name='name' use='required'/>
</xsd:complexType>
- <xsd:complexType name='PathType'>
- <xsd:attribute type='PathTypeEnum' name='type' use='required'/>
- <xsd:attribute type='xsd:string' name='name' use='required'/>
- <xsd:attribute type='xsd:string' name='dev_type'/>
- <xsd:attribute type='xsd:string' name='major'/>
- <xsd:attribute type='xsd:string' name='minor'/>
- <xsd:attribute type='xsd:string' name='mode'/>
- <xsd:attribute type='xsd:string' name='perms'/>
- <xsd:attribute type='xsd:string' name='owner'/>
- <xsd:attribute type='xsd:string' name='group'/>
- <xsd:attribute type='xsd:string' name='recursive'/>
- <xsd:attribute type='xsd:string' name='prune'/>
- <xsd:attribute type='xsd:string' name='to'/>
- <xsd:attribute type='xsd:string' name='vcstype'/>
- <xsd:attribute type='xsd:string' name='revision'/>
- <xsd:attribute type='xsd:string' name='sourceurl'/>
- <xsd:attributeGroup ref="py:genshiAttrs"/>
- </xsd:complexType>
+ <xsd:group name="rulesElements">
+ <xsd:choice>
+ <xsd:group ref="py:genshiElements"/>
+ <xsd:element name='Package' type='PackageType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a software package to be managed.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Path' type='PathType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a filesystem path to be handled
+ by the POSIX driver.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Service' type='ServiceType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a system service to be managed.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Action' type='ActionType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a command to be run.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='SELinux' type='SELinuxType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of an SELinux entry.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='PostInstall' type='PostInstallType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ PostInstall entries are deprecated in favor of Action
+ entries. Actions can do everything PostInstall entries can
+ do and more.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Group' type='RContainerType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Elements within Group tags only apply to clients that are
+ members of that group (or vice-versa; see #element_negate
+ below)
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Client' type='RContainerType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Elements within Client tags only apply to the named client
+ (or vice-versa; see #element_negate below)
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:group>
<xsd:complexType name='RContainerType'>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='Service' type='ServiceType'/>
- <xsd:element name='Package' type='PackageType'/>
- <xsd:element name='Path' type='PathType'/>
- <xsd:element name='Action' type='ActionType'/>
- <xsd:element name='Group' type='RContainerType'/>
- <xsd:element name='Client' type='RContainerType'/>
- <xsd:group ref="py:genshiElements"/>
+ <xsd:group ref="rulesElements"/>
</xsd:choice>
<xsd:attribute name='name' type='xsd:string'/>
<xsd:attribute name='negate' type='xsd:boolean'/>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
-
<xsd:element name='Rules'>
<xsd:complexType>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='Service' type='ServiceType'/>
- <xsd:element name='Package' type='PackageType'/>
- <xsd:element name='Path' type='PathType'/>
- <xsd:element name='Action' type='ActionType'/>
- <xsd:element name='PostInstall' type='PostInstallType'/>
- <xsd:element name='Group' type='RContainerType'/>
- <xsd:element name='Client' type='RContainerType'/>
- <xsd:group ref="py:genshiElements"/>
+ <xsd:group ref="rulesElements"/>
</xsd:choice>
<xsd:attribute name='priority' type='xsd:integer' use='required'/>
<xsd:attributeGroup ref="py:genshiAttrs"/>
diff --git a/schemas/servicetype.xsd b/schemas/servicetype.xsd
index af5bc64a6..7de847c7f 100644
--- a/schemas/servicetype.xsd
+++ b/schemas/servicetype.xsd
@@ -12,6 +12,16 @@
<xsd:import namespace="http://genshi.edgewall.org/"
schemaLocation="genshi.xsd"/>
+ <xsd:simpleType name='RestartEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='true'/>
+ <xsd:enumeration value='false'/>
+ <xsd:enumeration value='1'/>
+ <xsd:enumeration value='0'/>
+ <xsd:enumeration value='interactive'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
<xsd:complexType name="ServiceType">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="User">
@@ -24,13 +34,13 @@
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="status" type="StatusEnum"/>
+ <xsd:attribute name="restart" type="RestartEnum"/>
+ <xsd:attribute name="install" type="xsd:boolean"/>
<xsd:attribute name="type" type="ServiceTypeEnum"/>
<xsd:attribute name="port" type="xsd:string"/>
<xsd:attribute name="protocol" type="xsd:string"/>
- <xsd:attribute name="mode" type="xsd:string"/>
<xsd:attribute name="custom" type="xsd:string"/>
<xsd:attribute name="FMRI" type="xsd:string"/>
- <xsd:attribute name="supervised" type="xsd:string"/>
<xsd:attribute name="sequence" type="xsd:string"/>
<xsd:attribute name="target" type="xsd:string"/>
<xsd:attribute name="parameters" type="xsd:string"/>
diff --git a/schemas/types.xsd b/schemas/types.xsd
index ead377192..a7dae15c9 100644
--- a/schemas/types.xsd
+++ b/schemas/types.xsd
@@ -1,5 +1,6 @@
-<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xml:lang="en">
-
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:py="http://genshi.edgewall.org/" xml:lang="en">
+
<xsd:annotation>
<xsd:documentation>
string enumeration definitions for bcfg2
@@ -7,6 +8,9 @@
</xsd:documentation>
</xsd:annotation>
+ <xsd:import namespace="http://genshi.edgewall.org/"
+ schemaLocation="genshi.xsd"/>
+
<xsd:simpleType name='PackageTypeEnum'>
<xsd:restriction base='xsd:string'>
<xsd:enumeration value='deb' />
@@ -86,4 +90,109 @@
</xsd:restriction>
</xsd:simpleType>
+ <xsd:complexType name='ActionType'>
+ <xsd:attribute type='ActionTimingEnum' name='timing'/>
+ <xsd:attribute type='ActionWhenEnum' name='when'/>
+ <xsd:attribute type='ActionStatusEnum' name='status'/>
+ <xsd:attribute type="xsd:boolean" name="build"/>
+ <xsd:attribute type='xsd:string' name='name'/>
+ <xsd:attribute type='xsd:string' name='command'/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:simpleType name="DeviceTypeEnum">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="block"/>
+ <xsd:enumeration value="char"/>
+ <xsd:enumeration value="fifo"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:complexType name="PathType">
+ <xsd:attribute type="PathTypeEnum" name="type"/>
+ <xsd:attribute type="xsd:string" name="name" use="required"/>
+ <xsd:attribute type="DeviceTypeEnum" name="dev_type"/>
+ <xsd:attribute type="xsd:integer" name="major"/>
+ <xsd:attribute type="xsd:integer" name="minor"/>
+ <xsd:attribute type="xsd:string" name="mode"/>
+ <xsd:attribute type="xsd:string" name="perms"/>
+ <xsd:attribute type="xsd:string" name="owner"/>
+ <xsd:attribute type="xsd:string" name="group"/>
+ <xsd:attribute type="xsd:string" name="secontext"/>
+ <xsd:attribute type="xsd:string" name="recursive"/>
+ <xsd:attribute type="xsd:string" name="prune"/>
+ <xsd:attribute type="xsd:string" name="to"/>
+ <xsd:attribute type="xsd:string" name="vcstype"/>
+ <xsd:attribute type="xsd:string" name="revision"/>
+ <xsd:attribute type="xsd:string" name="sourceurl"/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:simpleType name='SELinuxTypeEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='boolean'/>
+ <xsd:enumeration value='module'/>
+ <xsd:enumeration value='port'/>
+ <xsd:enumeration value='fcontext'/>
+ <xsd:enumeration value='node'/>
+ <xsd:enumeration value='login'/>
+ <xsd:enumeration value='user'/>
+ <xsd:enumeration value='interface'/>
+ <xsd:enumeration value='permissive'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:simpleType name='SELinuxFileTypeEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='all'/>
+ <xsd:enumeration value='regular'/>
+ <xsd:enumeration value='directory'/>
+ <xsd:enumeration value='symlink'/>
+ <xsd:enumeration value='pipe'/>
+ <xsd:enumeration value='socket'/>
+ <xsd:enumeration value='block'/>
+ <xsd:enumeration value='char'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:simpleType name='SELinuxBooleanValueEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='on'/>
+ <xsd:enumeration value='off'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:simpleType name='SELinuxEntryTypeEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='boolean'/>
+ <xsd:enumeration value='module'/>
+ <xsd:enumeration value='port'/>
+ <xsd:enumeration value='fcontext'/>
+ <xsd:enumeration value='node'/>
+ <xsd:enumeration value='login'/>
+ <xsd:enumeration value='user'/>
+ <xsd:enumeration value='interface'/>
+ <xsd:enumeration value='permissive'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:complexType name="SELinuxStructure">
+ <xsd:attribute type='xsd:string' name='name' use='required'/>
+ <xsd:attribute type="xsd:boolean" name="disabled"/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="SELinuxType">
+ <xsd:attribute type="xsd:string" name="name" use="required"/>
+ <xsd:attribute type="SELinuxEntryTypeEnum" name="type" use="required"/>
+ <xsd:attribute type="SELinuxBooleanValueEnum" name="value"/>
+ <xsd:attribute type="xsd:boolean" name="disabled"/>
+ <xsd:attribute type="xsd:string" name="selinuxtype"/>
+ <xsd:attribute type="SELinuxFileTypeEnum" name="filetype"/>
+ <xsd:attribute type="xsd:string" name="proto"/>
+ <xsd:attribute type="xsd:string" name="roles"/>
+ <xsd:attribute type="xsd:string" name="prefix"/>
+ <xsd:attribute type="xsd:string" name="selinuxuser"/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
</xsd:schema>
diff --git a/setup.py b/setup.py
index 13d8dee89..3e5cd1ddc 100755
--- a/setup.py
+++ b/setup.py
@@ -8,6 +8,8 @@ import os
import os.path
import sys
+execfile('src/lib/Bcfg2/version.py')
+
# we only need m2crypto on < python2.6
need_m2crypto = False
version = sys.version_info[:2]
@@ -122,7 +124,6 @@ if need_m2crypto:
setup(cmdclass=cmdclass,
name="Bcfg2",
- version="1.2.2",
description="Bcfg2 Server",
author="Narayan Desai",
author_email="desai@mcs.anl.gov",
diff --git a/solaris/Makefile b/solaris/Makefile
index 77d9019eb..9ca87fc48 100644
--- a/solaris/Makefile
+++ b/solaris/Makefile
@@ -12,8 +12,9 @@ package:
-cd ../ && PYTHONPATH=$(PYTHONPATH):$(PWD)/build/lib/python2.6/site-packages/ $(PYTHON) setup.py install --single-version-externally-managed --record=/dev/null --prefix=$(PWD)/build
#setuptools appears to use a restictive umask
-chmod -R o+r build/
- -cat bin/bcfg2 | sed -e 's!/usr/bin/python!$(PYTHON)!' > bin/bcfg2.new && mv bin/bcfg2.new bin/bcfg2
- -./gen-prototypes.sh
+ -cat build/bin/bcfg2 | sed -e 's!/usr/bin/python!$(PYTHON)!' > build/bin/bcfg2.new && mv build/bin/bcfg2.new build/bin/bcfg2
+ -chmod +x build/bin/bcfg2
+ -sh ./gen-prototypes.sh
-pkgmk -o -a `uname -m` -f prototype.bcfg2 -d $(PWD)/tmp -r $(PWD)/build
-pkgmk -o -a `uname -m` -f prototype.bcfg2-server -d $(PWD)/tmp -r $(PWD)/build
-pkgtrans -o -s $(PWD)/tmp $(PWD)/bcfg2-$(VERS) SCbcfg2
diff --git a/solaris/gen-prototypes.sh b/solaris/gen-prototypes.sh
index ea0b4bb13..64aff9edb 100644
--- a/solaris/gen-prototypes.sh
+++ b/solaris/gen-prototypes.sh
@@ -1,6 +1,6 @@
#!/bin/sh
cd build
-PP="./"`ls -1d lib/*`"/site-packages/"
+PP="./lib/python/site-packages/"
#bcfg2
echo "i pkginfo=./pkginfo.bcfg2" > ../prototype.tmp
diff --git a/src/lib/Bcfg2/Bcfg2Py3k.py b/src/lib/Bcfg2/Bcfg2Py3k.py
index 6af8b3e5c..30feaf8c5 100644
--- a/src/lib/Bcfg2/Bcfg2Py3k.py
+++ b/src/lib/Bcfg2/Bcfg2Py3k.py
@@ -14,6 +14,7 @@ try:
from urllib2 import install_opener
from urllib2 import urlopen
from urllib2 import HTTPError
+ from urllib2 import URLError
except ImportError:
from urllib.parse import urljoin, urlparse
from urllib.request import HTTPBasicAuthHandler
@@ -22,6 +23,7 @@ except ImportError:
from urllib.request import install_opener
from urllib.request import urlopen
from urllib.error import HTTPError
+ from urllib.error import URLError
try:
from cStringIO import StringIO
@@ -75,6 +77,11 @@ def u_str(string, encoding=None):
else:
return unicode(string)
+try:
+ input = raw_input
+except:
+ input = input
+
if sys.hexversion >= 0x03000000:
from io import FileIO as file
else:
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py
index 9ad669ad6..51bc4aec7 100644
--- a/src/lib/Bcfg2/Client/Frame.py
+++ b/src/lib/Bcfg2/Client/Frame.py
@@ -124,33 +124,47 @@ class Frame:
self.logger.info([tool.name for tool in self.tools])
# find entries not handled by any tools
- problems = [entry for struct in config for \
- entry in struct if entry not in self.handled]
+ problems = [entry for struct in config
+ for entry in struct
+ if entry not in self.handled]
if problems:
self.logger.error("The following entries are not handled by any tool:")
- self.logger.error(["%s:%s:%s" % (entry.tag, entry.get('type'), \
- entry.get('name')) for entry in problems])
+ for entry in problems:
+ self.logger.error("%s:%s:%s" % (entry.tag, entry.get('type'),
+ entry.get('name')))
self.logger.error("")
- entries = [(entry.tag, entry.get('name'))
- for struct in config for entry in struct]
+
+ self.find_dups(config)
+
pkgs = [(entry.get('name'), entry.get('origin'))
- for struct in config for entry in struct if entry.tag == 'Package']
- multi = []
- for entry in entries[:]:
- if entries.count(entry) > 1:
- multi.append(entry)
- entries.remove(entry)
- if multi:
- self.logger.debug("The following entries are included multiple times:")
- self.logger.debug(["%s:%s" % entry for entry in multi])
- self.logger.debug("")
+ for struct in config
+ for entry in struct
+ if entry.tag == 'Package']
if pkgs:
self.logger.debug("The following packages are specified in bcfg2:")
self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == None])
self.logger.debug("The following packages are prereqs added by Packages:")
self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == 'Packages'])
+ def find_dups(self, config):
+ entries = dict()
+ for struct in config:
+ for entry in struct:
+ for tool in self.tools:
+ if tool.handlesEntry(entry):
+ pkey = tool.primarykey(entry)
+ if pkey in entries:
+ entries[pkey] += 1
+ else:
+ entries[pkey] = 1
+ multi = [e for e, c in entries.items() if c > 1]
+ if multi:
+ self.logger.debug("The following entries are included multiple times:")
+ for entry in multi:
+ self.logger.debug(entry)
+ self.logger.debug("")
+
def __getattr__(self, name):
if name in ['extra', 'handled', 'modified', '__important__']:
ret = []
@@ -190,14 +204,23 @@ class Frame:
self.whitelist = [x for x in self.whitelist if x not in b_to_rem]
# take care of important entries first
- if not self.dryrun and not self.setup['bundle']:
- for cfile in [cfl for cfl in self.config.findall(".//Path") \
- if cfl.get('name') in self.__important__ and \
- cfl.get('type') == 'file']:
- if cfile not in self.whitelist:
+ if not self.dryrun:
+ for cfile in self.config.findall(".//Path"):
+ if (cfile.get('name') not in self.__important__ or
+ cfile.get('type') != 'file' or
+ cfile not in self.whitelist):
+ continue
+ parent = cfile.getparent()
+ if ((parent.tag == "Bundle" and
+ ((self.setup['bundle'] and
+ parent.get("name") not in self.setup['bundle']) or
+ (self.setup['skipbundle'] and
+ parent.get("name") in self.setup['skipbundle']))) or
+ (parent.tag == "Independent" and
+ (self.setup['bundle'] or self.setup['skipindep']))):
continue
- tl = [t for t in self.tools if t.handlesEntry(cfile) \
- and t.canVerify(cfile)]
+ tl = [t for t in self.tools
+ if t.handlesEntry(cfile) and t.canVerify(cfile)]
if tl:
if self.setup['interactive'] and not \
promptFilter("Install %s: %s? (y/N):", [cfile]):
@@ -262,22 +285,31 @@ class Frame:
return
# Here is where most of the work goes
# first perform bundle filtering
+ all_bundle_names = [b.get('name')
+ for b in self.config.findall('./Bundle')]
+ bundles = self.config.getchildren()
if self.setup['bundle']:
- all_bundle_names = [b.get('name') for b in
- self.config.findall('./Bundle')]
# warn if non-existent bundle given
for bundle in self.setup['bundle']:
if bundle not in all_bundle_names:
self.logger.info("Warning: Bundle %s not found" % bundle)
- bundles = [b for b in self.config.findall('./Bundle')
- if b.get('name') in self.setup['bundle']]
- self.whitelist = [e for e in self.whitelist
- if True in [e in b for b in bundles]]
+ bundles = filter(lambda b: b.get('name') in self.setup['bundle'],
+ bundles)
elif self.setup['indep']:
- bundles = [nb for nb in self.config.getchildren()
- if nb.tag != 'Bundle']
- else:
- bundles = self.config.getchildren()
+ bundles = filter(lambda b: b.tag != 'Bundle', bundles)
+ if self.setup['skipbundle']:
+ # warn if non-existent bundle given
+ for bundle in self.setup['skipbundle']:
+ if bundle not in all_bundle_names:
+ self.logger.info("Warning: Bundle %s not found" % bundle)
+ bundles = filter(lambda b: \
+ b.get('name') not in self.setup['skipbundle'],
+ bundles)
+ if self.setup['skipindep']:
+ bundles = filter(lambda b: b.tag == 'Bundle', bundles)
+
+ self.whitelist = [e for e in self.whitelist
+ if True in [e in b for b in bundles]]
# first process prereq actions
for bundle in bundles[:]:
@@ -381,16 +413,32 @@ class Frame:
def CondDisplayState(self, phase):
"""Conditionally print tracing information."""
self.logger.info('\nPhase: %s' % phase)
- self.logger.info('Correct entries:\t%d' % list(self.states.values()).count(True))
- self.logger.info('Incorrect entries:\t%d' % list(self.states.values()).count(False))
+ self.logger.info('Correct entries:\t%d' %
+ list(self.states.values()).count(True))
+ self.logger.info('Incorrect entries:\t%d' %
+ list(self.states.values()).count(False))
if phase == 'final' and list(self.states.values()).count(False):
- self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) for \
- entry in self.states if not self.states[entry]])
- self.logger.info('Total managed entries:\t%d' % len(list(self.states.values())))
+ for entry in self.states.keys():
+ if not self.states[entry]:
+ etype = entry.get('type')
+ if etype:
+ self.logger.info( "%s:%s:%s" % (entry.tag, etype,
+ entry.get('name')))
+ else:
+ self.logger.info(" %s:%s" % (entry.tag,
+ entry.get('name')))
+ self.logger.info('Total managed entries:\t%d' %
+ len(list(self.states.values())))
self.logger.info('Unmanaged entries:\t%d' % len(self.extra))
if phase == 'final' and self.setup['extra']:
- self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) \
- for entry in self.extra])
+ for entry in self.extra:
+ etype = entry.get('type')
+ if etype:
+ self.logger.info( "%s:%s:%s" % (entry.tag, etype,
+ entry.get('name')))
+ else:
+ self.logger.info(" %s:%s" % (entry.tag,
+ entry.get('name')))
self.logger.info("")
@@ -428,7 +476,8 @@ class Frame:
total=str(len(self.states)),
version='2.0',
revision=self.config.get('revision', '-1'))
- good = len([key for key, val in list(self.states.items()) if val])
+ good_entries = [key for key, val in list(self.states.items()) if val]
+ good = len(good_entries)
stats.set('good', str(good))
if len([key for key, val in list(self.states.items()) if not val]) == 0:
stats.set('state', 'clean')
@@ -437,6 +486,7 @@ class Frame:
# List bad elements of the configuration
for (data, ename) in [(self.modified, 'Modified'), (self.extra, "Extra"), \
+ (good_entries, "Good"),
([entry for entry in self.states if not \
self.states[entry]], "Bad")]:
container = Bcfg2.Client.XML.SubElement(stats, ename)
diff --git a/src/lib/Bcfg2/Client/Tools/APT.py b/src/lib/Bcfg2/Client/Tools/APT.py
index 6b839ffbc..0ad42748c 100644
--- a/src/lib/Bcfg2/Client/Tools/APT.py
+++ b/src/lib/Bcfg2/Client/Tools/APT.py
@@ -6,22 +6,7 @@ warnings.filterwarnings("ignore", "apt API not stable yet",
FutureWarning)
import apt.cache
import os
-
import Bcfg2.Client.Tools
-import Bcfg2.Options
-
-# Options for tool locations
-opts = {'install_path': Bcfg2.Options.CLIENT_APT_TOOLS_INSTALL_PATH,
- 'var_path': Bcfg2.Options.CLIENT_APT_TOOLS_VAR_PATH,
- 'etc_path': Bcfg2.Options.CLIENT_SYSTEM_ETC_PATH}
-setup = Bcfg2.Options.OptionParser(opts)
-setup.parse([])
-install_path = setup['install_path']
-var_path = setup['var_path']
-etc_path = setup['etc_path']
-DEBSUMS = '%s/bin/debsums' % install_path
-APTGET = '%s/bin/apt-get' % install_path
-DPKG = '%s/bin/dpkg' % install_path
class APT(Bcfg2.Client.Tools.Tool):
"""The Debian toolset implements package and service operations and inherits
@@ -29,17 +14,26 @@ class APT(Bcfg2.Client.Tools.Tool):
"""
name = 'APT'
- __execs__ = [DEBSUMS, APTGET, DPKG]
+ __execs__ = []
__handles__ = [('Package', 'deb'), ('Path', 'ignore')]
__req__ = {'Package': ['name', 'version'], 'Path': ['type']}
def __init__(self, logger, setup, config):
Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
+
+ self.install_path = setup.get('apt_install_path', '/usr')
+ self.var_path = setup.get('apt_var_path', '/var')
+ self.etc_path = setup.get('apt_etc_path', '/etc')
+ self.debsums = '%s/bin/debsums' % self.install_path
+ self.aptget = '%s/bin/apt-get' % self.install_path
+ self.dpkg = '%s/bin/dpkg' % self.install_path
+ self.__execs__ = [self.debsums, self.aptget, self.dpkg]
+
path_entries = os.environ['PATH'].split(':')
for reqdir in ['/sbin', '/usr/sbin']:
if reqdir not in path_entries:
os.environ['PATH'] = os.environ['PATH'] + ':' + reqdir
- self.pkgcmd = '%s ' % APTGET + \
+ self.pkgcmd = '%s ' % self.aptget + \
'-o DPkg::Options::=--force-overwrite ' + \
'-o DPkg::Options::=--force-confold ' + \
'-o DPkg::Options::=--force-confmiss ' + \
@@ -53,21 +47,21 @@ class APT(Bcfg2.Client.Tools.Tool):
if entry.tag == 'Path' and \
entry.get('type') == 'ignore']
self.__important__ = self.__important__ + \
- ["%s/cache/debconf/config.dat" % var_path,
- "%s/cache/debconf/templates.dat" % var_path,
+ ["%s/cache/debconf/config.dat" % self.var_path,
+ "%s/cache/debconf/templates.dat" % self.var_path,
'/etc/passwd', '/etc/group',
- '%s/apt/apt.conf' % etc_path,
- '%s/dpkg/dpkg.cfg' % etc_path] + \
+ '%s/apt/apt.conf' % self.etc_path,
+ '%s/dpkg/dpkg.cfg' % self.etc_path] + \
[entry.get('name') for struct in config for entry in struct \
if entry.tag == 'Path' and \
- entry.get('name').startswith('%s/apt/sources.list' % etc_path)]
+ entry.get('name').startswith('%s/apt/sources.list' % self.etc_path)]
self.nonexistent = [entry.get('name') for struct in config for entry in struct \
if entry.tag == 'Path' and entry.get('type') == 'nonexistent']
os.environ["DEBIAN_FRONTEND"] = 'noninteractive'
self.actions = {}
if self.setup['kevlar'] and not self.setup['dryrun']:
- self.cmd.run("%s --force-confold --configure --pending" % DPKG)
- self.cmd.run("%s clean" % APTGET)
+ self.cmd.run("%s --force-confold --configure --pending" % self.dpkg)
+ self.cmd.run("%s clean" % self.aptget)
try:
self.pkg_cache = apt.cache.Cache()
except SystemError:
@@ -95,7 +89,8 @@ class APT(Bcfg2.Client.Tools.Tool):
for (name, version) in extras]
def VerifyDebsums(self, entry, modlist):
- output = self.cmd.run("%s -as %s" % (DEBSUMS, entry.get('name')))[1]
+ output = self.cmd.run("%s -as %s" % (self.debsums,
+ entry.get('name')))[1]
if len(output) == 1 and "no md5sums for" in output[0]:
self.logger.info("Package %s has no md5sums. Cannot verify" % \
entry.get('name'))
diff --git a/src/lib/Bcfg2/Client/Tools/Action.py b/src/lib/Bcfg2/Client/Tools/Action.py
index dc49347e9..e13134e1f 100644
--- a/src/lib/Bcfg2/Client/Tools/Action.py
+++ b/src/lib/Bcfg2/Client/Tools/Action.py
@@ -2,6 +2,7 @@
import Bcfg2.Client.Tools
from Bcfg2.Client.Frame import matches_white_list, passes_black_list
+from Bcfg2.Bcfg2Py3k import input
"""
<Action timing='pre|post|both'
@@ -44,11 +45,7 @@ class Action(Bcfg2.Client.Tools.Tool):
if self.setup['interactive']:
prompt = ('Run Action %s, %s: (y/N): ' %
(entry.get('name'), entry.get('command')))
- # py3k compatibility
- try:
- ans = raw_input(prompt)
- except NameError:
- ans = input(prompt)
+ ans = input(prompt)
if ans not in ['y', 'Y']:
return False
if self.setup['servicemode'] == 'build':
diff --git a/src/lib/Bcfg2/Client/Tools/Chkconfig.py b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
index 12ea5f132..0169b12da 100644
--- a/src/lib/Bcfg2/Client/Tools/Chkconfig.py
+++ b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
@@ -45,30 +45,14 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
except IndexError:
onlevels = []
+ pstatus = self.check_service(entry)
if entry.get('status') == 'on':
- status = (len(onlevels) > 0)
+ status = (len(onlevels) > 0 and pstatus)
command = 'start'
else:
- status = (len(onlevels) == 0)
+ status = (len(onlevels) == 0 and not pstatus)
command = 'stop'
- if entry.get('mode', 'default') == 'supervised':
- # turn on or off the service in supervised mode
- pstatus = self.cmd.run('/sbin/service %s status' % \
- entry.get('name'))[0]
- needs_modification = ((command == 'start' and pstatus) or \
- (command == 'stop' and not pstatus))
- if (not self.setup.get('dryrun') and
- self.setup['servicemode'] != 'disabled' and
- needs_modification):
- self.cmd.run(self.get_svc_command(entry, command))
- # service was modified, so it failed
- pstatus = False
-
- # chkconfig/init.d service
- if entry.get('status') == 'on':
- status = status and not pstatus
-
if not status:
if entry.get('status') == 'on':
entry.set('current_status', 'off')
@@ -78,22 +62,22 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
def InstallService(self, entry):
"""Install Service entry."""
- # don't take any actions for mode='manual'
- if entry.get('mode', 'default') == 'manual':
- self.logger.info("Service %s mode set to manual. Skipping "
- "installation." % (entry.get('name')))
- return False
rcmd = "/sbin/chkconfig %s %s"
self.cmd.run("/sbin/chkconfig --add %s" % (entry.attrib['name']))
self.logger.info("Installing Service %s" % (entry.get('name')))
- pass1 = True
+ rv = True
if entry.get('status') == 'off':
- rc = self.cmd.run(rcmd % (entry.get('name'),
- entry.get('status')) + \
- " --level 0123456")[0]
- pass1 = rc == 0
- rc = self.cmd.run(rcmd % (entry.get('name'), entry.get('status')))[0]
- return pass1 and rc == 0
+ rv &= self.cmd.run(rcmd + " --level 0123456" %
+ (entry.get('name'),
+ entry.get('status')))[0] == 0
+ if entry.get("current_status") == "on":
+ rv &= self.stop_service(entry)
+ else:
+ rv &= self.cmd.run(rcmd % (entry.get('name'),
+ entry.get('status')))[0] == 0
+ if entry.get("current_status") == "off":
+ rv &= self.start_service(entry)
+ return rv
def FindExtra(self):
"""Locate extra chkconfig Services."""
diff --git a/src/lib/Bcfg2/Client/Tools/DebInit.py b/src/lib/Bcfg2/Client/Tools/DebInit.py
index ca6fc439e..7d5af1127 100644
--- a/src/lib/Bcfg2/Client/Tools/DebInit.py
+++ b/src/lib/Bcfg2/Client/Tools/DebInit.py
@@ -76,11 +76,6 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
def InstallService(self, entry):
"""Install Service for entry."""
- # don't take any actions for mode='manual'
- if entry.get('mode', 'default') == 'manual':
- self.logger.info("Service %s mode set to manual. Skipping "
- "installation." % (entry.get('name')))
- return False
self.logger.info("Installing Service %s" % (entry.get('name')))
try:
os.stat('/etc/init.d/%s' % entry.get('name'))
diff --git a/src/lib/Bcfg2/Client/Tools/OpenCSW.py b/src/lib/Bcfg2/Client/Tools/OpenCSW.py
new file mode 100644
index 000000000..6aafe316f
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/OpenCSW.py
@@ -0,0 +1,33 @@
+# This is the bcfg2 support for opencsw packages (pkgutil)
+"""This provides Bcfg2 support for OpenCSW packages."""
+
+import tempfile
+import Bcfg2.Client.Tools.SYSV
+
+
+class OpenCSW(Bcfg2.Client.Tools.SYSV.SYSV):
+ """Support for OpenCSW packages."""
+ pkgtype = 'opencsw'
+ pkgtool = ("/opt/csw/bin/pkgutil -y -i %s", ("%s", ["bname"]))
+ name = 'OpenCSW'
+ __execs__ = ['/opt/csw/bin/pkgutil', "/usr/bin/pkginfo"]
+ __handles__ = [('Package', 'opencsw')]
+ __ireq__ = {'Package': ['name', 'version', 'bname']}
+
+ def __init__(self, logger, setup, config):
+ # dont use the sysv constructor
+ Bcfg2.Client.Tools.PkgTool.__init__(self, logger, setup, config)
+ noaskfile = tempfile.NamedTemporaryFile()
+ self.noaskname = noaskfile.name
+ try:
+ noaskfile.write(Bcfg2.Client.Tools.SYSV.noask)
+ except:
+ pass
+
+ # VerifyPackage comes from Bcfg2.Client.Tools.SYSV
+ # Install comes from Bcfg2.Client.Tools.PkgTool
+ # Extra comes from Bcfg2.Client.Tools.Tool
+ # Remove comes from Bcfg2.Client.Tools.SYSV
+ def FindExtraPackages(self):
+ """Pass through to null FindExtra call."""
+ return []
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX.py b/src/lib/Bcfg2/Client/Tools/POSIX.py
index 0d67dbbab..fb28f4ecb 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX.py
@@ -20,7 +20,13 @@ import Bcfg2.Client.Tools
import Bcfg2.Options
from Bcfg2.Client import XML
-log = logging.getLogger('POSIX')
+log = logging.getLogger(__name__)
+
+try:
+ import selinux
+ has_selinux = True
+except ImportError:
+ has_selinux = False
# map between dev_type attribute and stat constants
device_map = {'block': stat.S_IFBLK,
@@ -28,52 +34,39 @@ device_map = {'block': stat.S_IFBLK,
'fifo': stat.S_IFIFO}
-def calcPerms(initial, perms):
- """This compares ondisk permissions with specified ones."""
- pdisp = [{1:stat.S_ISVTX, 2:stat.S_ISGID, 4:stat.S_ISUID},
- {1:stat.S_IXUSR, 2:stat.S_IWUSR, 4:stat.S_IRUSR},
- {1:stat.S_IXGRP, 2:stat.S_IWGRP, 4:stat.S_IRGRP},
- {1:stat.S_IXOTH, 2:stat.S_IWOTH, 4:stat.S_IROTH}]
- tempperms = initial
- if len(perms) == 3:
- perms = '0%s' % (perms)
- pdigits = [int(perms[digit]) for digit in range(4)]
- for index in range(4):
- for (num, perm) in list(pdisp[index].items()):
- if pdigits[index] & num:
- tempperms |= perm
- return tempperms
-
-
-def normGid(entry):
+def normGid(entry, logger=None):
"""
This takes a group name or gid and
returns the corresponding gid or False.
"""
+ if logger is None:
+ logger = log
try:
try:
return int(entry.get('group'))
except:
return int(grp.getgrnam(entry.get('group'))[2])
except (OSError, KeyError):
- log.error('GID normalization failed for %s. Does group %s exist?'
- % (entry.get('name'), entry.get('group')))
+ logger.error('GID normalization failed for %s. Does group %s exist?' %
+ (entry.get('name'), entry.get('group')))
return False
-def normUid(entry):
+def normUid(entry, logger=None):
"""
This takes a user name or uid and
returns the corresponding uid or False.
"""
+ if logger is None:
+ logger = log
try:
try:
return int(entry.get('owner'))
except:
return int(pwd.getpwnam(entry.get('owner'))[2])
except (OSError, KeyError):
- log.error('UID normalization failed for %s. Does owner %s exist?'
- % (entry.get('name'), entry.get('owner')))
+ logger.error('UID normalization failed for %s. Does owner %s exist?' %
+ (entry.get('name'), entry.get('owner')))
return False
@@ -92,6 +85,115 @@ def isString(strng, encoding):
return False
+def secontextMatches(entry):
+ """ determine if the SELinux context of the file on disk matches
+ the desired context """
+ if not has_selinux:
+ # no selinux libraries
+ return True
+
+ path = entry.get("path")
+ context = entry.get("secontext")
+ if context is None:
+ # no context listed
+ return True
+
+ if context == '__default__':
+ if selinux.getfilecon(entry.get('name'))[1] == \
+ selinux.matchpathcon(entry.get('name'), 0)[1]:
+ return True
+ else:
+ return False
+ elif selinux.getfilecon(entry.get('name'))[1] == context:
+ return True
+ else:
+ return False
+
+
+def setSEContext(entry, path=None, recursive=False):
+ """ set the SELinux context of the file on disk according to the
+ config"""
+ if not has_selinux:
+ return True
+
+ if path is None:
+ path = entry.get("path")
+ context = entry.get("secontext")
+ if context is None:
+ # no context listed
+ return True
+
+ rv = True
+ if context == '__default__':
+ try:
+ selinux.restorecon(path, recursive=recursive)
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s" %
+ (path, err))
+ rv = False
+ else:
+ try:
+ rv &= selinux.lsetfilecon(path, context) == 0
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s" %
+ (path, err))
+ rv = False
+
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for p in dirs + files:
+ try:
+ rv &= selinux.lsetfilecon(p, context) == 0
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s"
+ % (path, err))
+ rv = False
+ return rv
+
+
+def setPerms(entry, path=None):
+ if path is None:
+ path = entry.get("name")
+
+ if (entry.get('perms') == None or
+ entry.get('owner') == None or
+ entry.get('group') == None):
+ self.logger.error('Entry %s not completely specified. '
+ 'Try running bcfg2-lint.' % entry.get('name'))
+ return False
+
+ rv = True
+ # split this into multiple try...except blocks so that even if a
+ # chown fails, the chmod can succeed -- get as close to the
+ # desired state as we can
+ try:
+ os.chown(path, normUid(entry), normGid(entry))
+ except KeyError:
+ logger.error('Failed to change ownership of %s' % path)
+ rv = False
+ os.chown(path, 0, 0)
+ except OSError:
+ logger.error('Failed to change ownership of %s' % path)
+ rv = False
+
+ configPerms = int(entry.get('perms'), 8)
+ if entry.get('dev_type'):
+ configPerms |= device_map[entry.get('dev_type')]
+ try:
+ os.chmod(path, configPerms)
+ except (OSError, KeyError):
+ logger.error('Failed to change permissions mode of %s' % path)
+ rv = False
+
+ if has_selinux:
+ rv &= setSEContext(entry, path=path)
+
+ return rv
+
+
class POSIX(Bcfg2.Client.Tools.Tool):
"""POSIX File support code."""
name = 'POSIX'
@@ -102,7 +204,14 @@ class POSIX(Bcfg2.Client.Tools.Tool):
('Path', 'nonexistent'),
('Path', 'permissions'),
('Path', 'symlink')]
- __req__ = {'Path': ['name', 'type']}
+ __req__ = dict(Path=dict(
+ device=['name', 'dev_type', 'perms', 'owner', 'group'],
+ directory=['name', 'perms', 'owner', 'group'],
+ file=['name', 'perms', 'owner', 'group'],
+ hardlink=['name', 'to'],
+ nonexistent=['name'],
+ permissions=['name', 'perms', 'owner', 'group'],
+ symlink=['name', 'to']))
# grab paranoid options from /etc/bcfg2.conf
opts = {'ppath': Bcfg2.Options.PARANOID_PATH,
@@ -115,13 +224,9 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def canInstall(self, entry):
"""Check if entry is complete for installation."""
if Bcfg2.Client.Tools.Tool.canInstall(self, entry):
- if (entry.tag,
- entry.get('type'),
- entry.text,
- entry.get('empty', 'false')) == ('Path',
- 'file',
- None,
- 'false'):
+ if (entry.get('type') == 'file' and
+ entry.text is None and
+ entry.get('empty', 'false') == 'false'):
return False
return True
else:
@@ -141,69 +246,60 @@ class POSIX(Bcfg2.Client.Tools.Tool):
entry.set('current_group', str(ondisk[stat.ST_GID]))
except (OSError, KeyError):
pass
+
+ if has_selinux:
+ try:
+ entry.set('current_secontext',
+ selinux.getfilecon(entry.get('name'))[1])
+ except (OSError, KeyError):
+ pass
entry.set('perms', str(oct(ondisk[stat.ST_MODE])[-4:]))
def Verifydevice(self, entry, _):
"""Verify device entry."""
- if entry.get('dev_type') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
if entry.get('dev_type') in ['block', 'char']:
# check if major/minor are properly specified
- if entry.get('major') == None or \
- entry.get('minor') == None:
+ if (entry.get('major') == None or
+ entry.get('minor') == None):
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
+ 'Try running bcfg2-lint.' %
+ (entry.get('name')))
return False
+
try:
- # check for file existence
- filestat = os.stat(entry.get('name'))
+ ondisk = os.stat(path)
except OSError:
entry.set('current_exists', 'false')
self.logger.debug("%s %s does not exist" %
- (entry.tag, entry.get('name')))
+ (entry.tag, path))
return False
- try:
- # attempt to verify device properties as specified in config
- dev_type = entry.get('dev_type')
- mode = calcPerms(device_map[dev_type],
- entry.get('mode', '0600'))
- owner = normUid(entry)
- group = normGid(entry)
- if dev_type in ['block', 'char']:
- # check for incompletely specified entries
- if entry.get('major') == None or \
- entry.get('minor') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
- major = int(entry.get('major'))
- minor = int(entry.get('minor'))
- if major == os.major(filestat.st_rdev) and \
- minor == os.minor(filestat.st_rdev) and \
- mode == filestat.st_mode and \
- owner == filestat.st_uid and \
- group == filestat.st_gid:
- return True
- else:
- return False
- elif dev_type == 'fifo' and \
- mode == filestat.st_mode and \
- owner == filestat.st_uid and \
- group == filestat.st_gid:
- return True
- else:
- self.logger.info('Device properties for %s incorrect' % \
- entry.get('name'))
- return False
- except OSError:
- self.logger.debug("%s %s failed to verify" %
- (entry.tag, entry.get('name')))
- return False
+ rv = self._verify_metadata(entry)
+
+ # attempt to verify device properties as specified in config
+ dev_type = entry.get('dev_type')
+ if dev_type in ['block', 'char']:
+ major = int(entry.get('major'))
+ minor = int(entry.get('minor'))
+ if major != os.major(ondisk.st_rdev):
+ entry.set('current_mtime', mtime)
+ msg = ("Major number for device %s is incorrect. "
+ "Current major is %s but should be %s" %
+ (path, os.major(ondisk.st_rdev), major))
+ self.logger.debug(msg)
+ entry.set('qtext', entry.get('qtext') + "\n" + msg)
+ rv = False
+
+ if minor != os.minor(ondisk.st_rdev):
+ entry.set('current_mtime', mtime)
+ msg = ("Minor number for device %s is incorrect. "
+ "Current minor is %s but should be %s" %
+ (path, os.minor(ondisk.st_rdev), minor))
+ self.logger.debug(msg)
+ entry.set('qtext', entry.get('qtext') + "\n" + msg)
+ rv = False
+
+ return rv
def Installdevice(self, entry):
"""Install device entries."""
@@ -214,7 +310,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
os.unlink(entry.get('name'))
exists = False
except OSError:
- self.logger.info('Failed to unlink %s' % \
+ self.logger.info('Failed to unlink %s' %
entry.get('name'))
return False
except OSError:
@@ -223,14 +319,14 @@ class POSIX(Bcfg2.Client.Tools.Tool):
if not exists:
try:
dev_type = entry.get('dev_type')
- mode = calcPerms(device_map[dev_type],
- entry.get('mode', '0600'))
+ mode = device_map[dev_type] | int(entry.get('mode', '0600'), 8)
if dev_type in ['block', 'char']:
# check if major/minor are properly specified
- if entry.get('major') == None or \
- entry.get('minor') == None:
+ if (entry.get('major') == None or
+ entry.get('minor') == None):
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
+ 'Try running bcfg2-lint.' %
+ entry.get('name'))
return False
major = int(entry.get('major'))
minor = int(entry.get('minor'))
@@ -238,15 +334,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
os.mknod(entry.get('name'), mode, device)
else:
os.mknod(entry.get('name'), mode)
- """
- Python uses the OS mknod(2) implementation which modifies the
- mode based on the umask of the running process. Therefore, the
- following chmod(2) call is needed to make sure the permissions
- are set as specified by the user.
- """
- os.chmod(entry.get('name'), mode)
- os.chown(entry.get('name'), normUid(entry), normGid(entry))
- return True
+ return setPerms(entry)
except KeyError:
self.logger.error('Failed to install %s' % entry.get('name'))
except OSError:
@@ -255,47 +343,13 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifydirectory(self, entry, modlist):
"""Verify Path type='directory' entry."""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error("POSIX: Entry %s not completely specified. "
- "Try running bcfg2-lint." % (entry.get('name')))
- return False
- while len(entry.get('perms', '')) < 4:
- entry.set('perms', '0' + entry.get('perms', ''))
- try:
- ondisk = os.stat(entry.get('name'))
- except OSError:
- entry.set('current_exists', 'false')
- self.logger.info("POSIX: %s %s does not exist" %
- (entry.tag, entry.get('name')))
- return False
- try:
- owner = str(ondisk[stat.ST_UID])
- group = str(ondisk[stat.ST_GID])
- except (OSError, KeyError):
- self.logger.info("POSIX: User/Group resolution failed "
- "for path %s" % entry.get('name'))
- owner = 'root'
- group = '0'
- finfo = os.stat(entry.get('name'))
- perms = oct(finfo[stat.ST_MODE])[-4:]
- if entry.get('mtime', '-1') != '-1':
- mtime = str(finfo[stat.ST_MTIME])
- else:
- mtime = '-1'
- pTrue = ((owner == str(normUid(entry))) and
- (group == str(normGid(entry))) and
- (perms == entry.get('perms')) and
- (mtime == entry.get('mtime', '-1')))
-
pruneTrue = True
ex_ents = []
- if entry.get('prune', 'false') == 'true' \
- and (entry.tag == 'Path' and entry.get('type') == 'directory'):
+ if (entry.get('prune', 'false') == 'true'
+ and (entry.tag == 'Path' and entry.get('type') == 'directory')):
# check for any extra entries when prune='true' attribute is set
try:
- entries = ['/'.join([entry.get('name'), ent]) \
+ entries = ['/'.join([entry.get('name'), ent])
for ent in os.listdir(entry.get('name'))]
ex_ents = [e for e in entries if e not in modlist]
if ex_ents:
@@ -307,99 +361,48 @@ class POSIX(Bcfg2.Client.Tools.Tool):
nqtext += "Directory %s contains extra entries: " % \
entry.get('name')
nqtext += ":".join(ex_ents)
- entry.set('qtest', nqtext)
- [entry.append(XML.Element('Prune', path=x)) \
+ entry.set('qtext', nqtext)
+ [entry.append(XML.Element('Prune', path=x))
for x in ex_ents]
except OSError:
ex_ents = []
pruneTrue = True
- if not pTrue:
- if owner != str(normUid(entry)):
- entry.set('current_owner', owner)
- self.logger.debug("%s %s ownership wrong" % \
- (entry.tag, entry.get('name')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s owner wrong. is %s should be %s" % \
- (entry.get('name'), owner, entry.get('owner'))
- entry.set('qtext', nqtext)
- if group != str(normGid(entry)):
- entry.set('current_group', group)
- self.logger.debug("%s %s group wrong" % \
- (entry.tag, entry.get('name')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s group is %s should be %s" % \
- (entry.get('name'), group, entry.get('group'))
- entry.set('qtext', nqtext)
- if perms != entry.get('perms'):
- entry.set('current_perms', perms)
- self.logger.debug("%s %s permissions are %s should be %s" %
- (entry.tag,
- entry.get('name'),
- perms,
- entry.get('perms')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s %s perms are %s should be %s" % \
- (entry.tag,
- entry.get('name'),
- perms,
- entry.get('perms'))
- entry.set('qtext', nqtext)
- if mtime != entry.get('mtime', '-1'):
- entry.set('current_mtime', mtime)
- self.logger.debug("%s %s mtime is %s should be %s" \
- % (entry.tag, entry.get('name'), mtime,
- entry.get('mtime')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s mtime is %s should be %s" % \
- (entry.get('name'), mtime, entry.get('mtime'))
- entry.set('qtext', nqtext)
- if entry.get('type') != 'file':
- nnqtext = entry.get('qtext')
- nnqtext += '\nInstall %s %s: (y/N) ' % (entry.get('type'),
- entry.get('name'))
- entry.set('qtext', nnqtext)
- return pTrue and pruneTrue
+ return pruneTrue and self._verify_metadata(entry)
def Installdirectory(self, entry):
"""Install Path type='directory' entry."""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
- self.logger.info("Installing directory %s" % (entry.get('name')))
+ self.logger.info("Installing directory %s" % entry.get('name'))
try:
fmode = os.lstat(entry.get('name'))
- if not stat.S_ISDIR(fmode[stat.ST_MODE]):
- self.logger.debug("Found a non-directory entry at %s" % \
- (entry.get('name')))
- try:
- os.unlink(entry.get('name'))
- exists = False
- except OSError:
- self.logger.info("Failed to unlink %s" % \
- (entry.get('name')))
- return False
- else:
- self.logger.debug("Found a pre-existing directory at %s" % \
- (entry.get('name')))
- exists = True
except OSError:
# stat failed
exists = False
+ if not stat.S_ISDIR(fmode[stat.ST_MODE]):
+ self.logger.debug("Found a non-directory entry at %s" %
+ entry.get('name'))
+ try:
+ os.unlink(entry.get('name'))
+ exists = False
+ except OSError:
+ self.logger.info("Failed to unlink %s" % entry.get('name'))
+ return False
+ else:
+ self.logger.debug("Found a pre-existing directory at %s" %
+ entry.get('name'))
+ exists = True
+
if not exists:
parent = "/".join(entry.get('name').split('/')[:-1])
if parent:
try:
os.stat(parent)
except:
- self.logger.debug('Creating parent path for directory %s' % (entry.get('name')))
+ self.logger.debug('Creating parent path for directory %s' %
+ entry.get('name'))
for idx in range(len(parent.split('/')[:-1])):
- current = '/'+'/'.join(parent.split('/')[1:2+idx])
+ current = '/' + '/'.join(parent.split('/')[1:2+idx])
try:
sloc = os.stat(current)
except OSError:
@@ -418,10 +421,10 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.mkdir(entry.get('name'))
except OSError:
- self.logger.error('Failed to create directory %s' % \
- (entry.get('name')))
+ self.logger.error('Failed to create directory %s' %
+ entry.get('name'))
return False
- if entry.get('prune', 'false') == 'true' and entry.get("qtest"):
+ if entry.get('prune', 'false') == 'true' and entry.get("qtext"):
for pent in entry.findall('Prune'):
pname = pent.get('path')
ulfailed = False
@@ -442,7 +445,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifyfile(self, entry, _):
"""Verify Path type='file' entry."""
# permissions check + content check
- permissionStatus = self.Verifydirectory(entry, _)
+ permissionStatus = self._verify_metadata(entry)
tbin = False
if entry.text == None and entry.get('empty', 'false') == 'false':
self.logger.error("Cannot verify incomplete Path type='%s' %s" %
@@ -460,7 +463,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
tempdata = tempdata.encode(self.setup['encoding'])
except UnicodeEncodeError:
e = sys.exc_info()[1]
- self.logger.error("Error encoding file %s:\n %s" % \
+ self.logger.error("Error encoding file %s:\n %s" %
(entry.get('name'), e))
different = False
@@ -529,8 +532,6 @@ class POSIX(Bcfg2.Client.Tools.Tool):
else:
prompt.append("Diff took too long to compute, no "
"printable diff")
- prompt.append("Install %s %s: (y/N): " % (entry.tag,
- entry.get('name')))
entry.set("qtext", "\n".join(prompt))
if entry.get('sensitive', 'false').lower() != 'true':
@@ -559,12 +560,6 @@ class POSIX(Bcfg2.Client.Tools.Tool):
binascii.b2a_base64("\n".join(diff)))
elif not tbin and isString(content, self.setup['encoding']):
entry.set('current_bfile', binascii.b2a_base64(content))
- elif permissionStatus == False and self.setup['interactive']:
- prompt = [entry.get('qtext', '')]
- prompt.append("Install %s %s: (y/N): " % (entry.tag,
- entry.get('name')))
- entry.set("qtext", "\n".join(prompt))
-
return permissionStatus and not different
@@ -577,8 +572,8 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.stat(parent)
except:
- self.logger.debug('Creating parent path for config file %s' % \
- (entry.get('name')))
+ self.logger.debug('Creating parent path for config file %s' %
+ entry.get('name'))
current = '/'
for next in parent.split('/')[1:]:
current += next + '/'
@@ -586,23 +581,24 @@ class POSIX(Bcfg2.Client.Tools.Tool):
sloc = os.stat(current)
try:
if not stat.S_ISDIR(sloc[stat.ST_MODE]):
- self.logger.debug('%s is not a directory; recreating' \
- % (current))
+ self.logger.debug('%s is not a directory; recreating'
+ % current)
os.unlink(current)
os.mkdir(current)
except OSError:
return False
except OSError:
try:
- self.logger.debug("Creating non-existent path %s" % current)
+ self.logger.debug("Creating non-existent path %s" %
+ current)
os.mkdir(current)
except OSError:
return False
# If we get here, then the parent directory should exist
- if (entry.get("paranoid", False) in ['true', 'True']) and \
- self.setup.get("paranoid", False) and not \
- (entry.get('current_exists', 'true') == 'false'):
+ if (entry.get("paranoid", 'false').lower() == 'true' and
+ self.setup.get("paranoid", False) and
+ entry.get('current_exists', 'true') != 'false'):
bkupnam = entry.get('name').replace('/', '_')
# current list of backups for this file
try:
@@ -621,7 +617,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.remove("%s/%s" % (self.ppath, oldest))
except:
- self.logger.error("Failed to remove %s/%s" % \
+ self.logger.error("Failed to remove %s/%s" %
(self.ppath, oldest))
return False
try:
@@ -633,8 +629,8 @@ class POSIX(Bcfg2.Client.Tools.Tool):
(entry.get('name'), self.ppath))
except IOError:
e = sys.exc_info()[1]
- self.logger.error("Failed to create backup file for %s" % \
- (entry.get('name')))
+ self.logger.error("Failed to create backup file for %s" %
+ entry.get('name'))
self.logger.error(e)
return False
try:
@@ -650,80 +646,59 @@ class POSIX(Bcfg2.Client.Tools.Tool):
filedata = entry.text
newfile.write(filedata)
newfile.close()
- try:
- os.chown(newfile.name, normUid(entry), normGid(entry))
- except KeyError:
- self.logger.error("Failed to chown %s to %s:%s" %
- (newfile.name, entry.get('owner'),
- entry.get('group')))
- os.chown(newfile.name, 0, 0)
- except OSError:
- err = sys.exc_info()[1]
- self.logger.error("Could not chown %s: %s" % (newfile.name,
- err))
- os.chmod(newfile.name, calcPerms(stat.S_IFREG, entry.get('perms')))
+
+ rv = setPerms(entry, newfile.name)
os.rename(newfile.name, entry.get('name'))
- if entry.get('mtime', '-1') != '-1':
+ if entry.get('mtime'):
try:
os.utime(entry.get('name'), (int(entry.get('mtime')),
int(entry.get('mtime'))))
except:
- self.logger.error("File %s mtime fix failed" \
- % (entry.get('name')))
- return False
- return True
+ logger.error("Failed to set mtime of %s" % path)
+ rv = False
+ return rv
except (OSError, IOError):
err = sys.exc_info()[1]
if err.errno == errno.EACCES:
- self.logger.info("Failed to open %s for writing" % (entry.get('name')))
+ self.logger.info("Failed to open %s for writing" %
+ entry.get('name'))
else:
print(err)
return False
def Verifyhardlink(self, entry, _):
"""Verify HardLink entry."""
- if entry.get('to') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
+ rv = True
+
try:
- if os.path.samefile(entry.get('name'), entry.get('to')):
- return True
- self.logger.debug("Hardlink %s is incorrect" % \
- entry.get('name'))
- entry.set('qtext', "Link %s to %s? [y/N] " % \
- (entry.get('name'),
- entry.get('to')))
- return False
+ if not os.path.samefile(entry.get('name'), entry.get('to')):
+ msg = "Hardlink %s is incorrect." % entry.get('name')
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), msg]))
+ rv = False
except OSError:
entry.set('current_exists', 'false')
- entry.set('qtext', "Link %s to %s? [y/N] " % \
- (entry.get('name'),
- entry.get('to')))
return False
+ rv &= self._verify_secontext(entry)
+ return rv
+
def Installhardlink(self, entry):
"""Install HardLink entry."""
- if entry.get('to') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
- self.logger.info("Installing Hardlink %s" % (entry.get('name')))
+ self.logger.info("Installing Hardlink %s" % entry.get('name'))
if os.path.lexists(entry.get('name')):
try:
fmode = os.lstat(entry.get('name'))[stat.ST_MODE]
if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode):
self.logger.debug("Non-directory entry already exists at "
- "%s. Unlinking entry." % (entry.get('name')))
+ "%s. Unlinking entry." %
+ entry.get('name'))
os.unlink(entry.get('name'))
elif stat.S_ISDIR(fmode):
- self.logger.debug("Directory already exists at %s" % \
- (entry.get('name')))
- self.cmd.run("mv %s/ %s.bak" % \
- (entry.get('name'),
- entry.get('name')))
+ self.logger.debug("Directory already exists at %s" %
+ entry.get('name'))
+ self.cmd.run("mv %s/ %s.bak" % (entry.get('name'),
+ entry.get('name')))
else:
os.unlink(entry.get('name'))
except OSError:
@@ -731,7 +706,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
(entry.get('name')))
try:
os.link(entry.get('to'), entry.get('name'))
- return True
+ return setPerms(entry)
except OSError:
return False
@@ -758,7 +733,10 @@ class POSIX(Bcfg2.Client.Tools.Tool):
self.logger.error('Failed to remove %s: %s' % (ename,
e.strerror))
else:
- if os.path.isdir(ename):
+ if os.path.islink(ename):
+ os.remove(ename)
+ return True
+ elif os.path.isdir(ename):
try:
os.rmdir(ename)
return True
@@ -778,133 +756,63 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifypermissions(self, entry, _):
"""Verify Path type='permissions' entry"""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
- if entry.get('recursive') in ['True', 'true']:
+ rv = self._verify_metadata(entry)
+
+ if entry.get('recursive', 'false').lower() == 'true':
# verify ownership information recursively
- owner = normUid(entry)
- group = normGid(entry)
-
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
- path = os.path.join(root, p)
- pstat = os.stat(path)
- if owner != pstat.st_uid:
- # owner mismatch for path
- entry.set('current_owner', str(pstat.st_uid))
- self.logger.debug("%s %s ownership wrong" % \
- (entry.tag, path))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += ("Owner for path %s is incorrect. "
- "Current owner is %s but should be %s\n" % \
- (path, pstat.st_uid, entry.get('owner')))
- nqtext += ("\nInstall %s %s: (y/N): " %
- (entry.tag, entry.get('name')))
- entry.set('qtext', nqtext)
- return False
- if group != pstat.st_gid:
- # group mismatch for path
- entry.set('current_group', str(pstat.st_gid))
- self.logger.debug("%s %s group wrong" % \
- (entry.tag, path))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += ("Group for path %s is incorrect. "
- "Current group is %s but should be %s\n" % \
- (path, pstat.st_gid, entry.get('group')))
- nqtext += ("\nInstall %s %s: (y/N): " %
- (entry.tag, entry.get('name')))
- entry.set('qtext', nqtext)
- return False
- return self.Verifydirectory(entry, _)
-
- def _diff(self, content1, content2, difffunc, filename=None):
- rv = []
- start = time.time()
- longtime = False
- for diffline in difffunc(content1.split('\n'),
- content2.split('\n')):
- now = time.time()
- rv.append(diffline)
- if now - start > 5 and not longtime:
- if filename:
- self.logger.info("Diff of %s taking a long time" %
- filename)
- else:
- self.logger.info("Diff taking a long time")
- longtime = True
- elif now - start > 30:
- if filename:
- self.logger.error("Diff of %s took too long; giving up" %
- filename)
- else:
- self.logger.error("Diff took too long; giving up")
- return False
+ rv &= self._verify_metadata(entry,
+ path=os.path.join(root, p))
return rv
def Installpermissions(self, entry):
"""Install POSIX permissions"""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
plist = [entry.get('name')]
if entry.get('recursive') in ['True', 'true']:
# verify ownership information recursively
- owner = normUid(entry)
- group = normGid(entry)
-
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
- path = os.path.join(root, p)
- pstat = os.stat(path)
- if owner != pstat.st_uid or group != pstat.st_gid:
- # owner mismatch for path
+ if not self._verify_metadata(entry,
+ path=os.path.join(root, p),
+ checkonly=True):
plist.append(path)
- try:
- for p in plist:
- os.chown(p, normUid(entry), normGid(entry))
- os.chmod(p, calcPerms(stat.S_IFDIR, entry.get('perms')))
- return True
- except (OSError, KeyError):
- self.logger.error('Permission fixup failed for %s' % \
- (entry.get('name')))
- return False
+ rv = True
+ for path in plist:
+ rv &= setPerms(entry, path)
+ return rv
def Verifysymlink(self, entry, _):
"""Verify Path type='symlink' entry."""
if entry.get('to') == None:
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
+ 'Try running bcfg2-lint.' %
(entry.get('name')))
return False
+
+ rv = True
+
try:
sloc = os.readlink(entry.get('name'))
- if sloc == entry.get('to'):
- return True
- self.logger.debug("Symlink %s points to %s, should be %s" % \
- (entry.get('name'), sloc, entry.get('to')))
- entry.set('current_to', sloc)
- entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'),
- entry.get('to')))
- return False
+ if sloc != entry.get('to'):
+ entry.set('current_to', sloc)
+ msg = ("Symlink %s points to %s, should be %s" %
+ (entry.get('name'), sloc, entry.get('to')))
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), msg]))
+ rv = False
except OSError:
entry.set('current_exists', 'false')
- entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'),
- entry.get('to')))
return False
+ rv &= self._verify_secontext(entry)
+ return rv
+
def Installsymlink(self, entry):
"""Install Path type='symlink' entry."""
if entry.get('to') == None:
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
+ 'Try running bcfg2-lint.' % entry.get('name'))
return False
self.logger.info("Installing symlink %s" % (entry.get('name')))
if os.path.lexists(entry.get('name')):
@@ -912,23 +820,22 @@ class POSIX(Bcfg2.Client.Tools.Tool):
fmode = os.lstat(entry.get('name'))[stat.ST_MODE]
if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode):
self.logger.debug("Non-directory entry already exists at "
- "%s. Unlinking entry." % \
- (entry.get('name')))
+ "%s. Unlinking entry." %
+ entry.get('name'))
os.unlink(entry.get('name'))
elif stat.S_ISDIR(fmode):
- self.logger.debug("Directory already exists at %s" %\
- (entry.get('name')))
- self.cmd.run("mv %s/ %s.bak" % \
- (entry.get('name'),
- entry.get('name')))
+ self.logger.debug("Directory already exists at %s" %
+ entry.get('name'))
+ self.cmd.run("mv %s/ %s.bak" % (entry.get('name'),
+ entry.get('name')))
else:
os.unlink(entry.get('name'))
except OSError:
- self.logger.info("Symlink %s cleanup failed" %\
+ self.logger.info("Symlink %s cleanup failed" %
(entry.get('name')))
try:
os.symlink(entry.get('to'), entry.get('name'))
- return True
+ return setSEContext(entry)
except OSError:
return False
@@ -939,5 +846,139 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def VerifyPath(self, entry, _):
"""Dispatch verify to the proper method according to type"""
- ret = getattr(self, 'Verify%s' % entry.get('type'))
- return ret(entry, _)
+ ret = getattr(self, 'Verify%s' % entry.get('type'))(entry, _)
+ if entry.get('qtext') and self.setup['interactive']:
+ entry.set('qtext',
+ '%s\nInstall %s %s: (y/N) ' %
+ (entry.get('qtext'),
+ entry.get('type'), entry.get('name')))
+ return ret
+
+ def _verify_metadata(self, entry, path=None, checkonly=False):
+ """ generic method to verify perms, owner, group, secontext,
+ and mtime """
+
+ # allow setting an alternate path for recursive permissions checking
+ if path is None:
+ path = entry.get('name')
+
+ while len(entry.get('perms', '')) < 4:
+ entry.set('perms', '0' + entry.get('perms', ''))
+
+ try:
+ ondisk = os.stat(path)
+ except OSError:
+ entry.set('current_exists', 'false')
+ self.logger.debug("POSIX: %s %s does not exist" %
+ (entry.tag, path))
+ return False
+
+ try:
+ owner = str(ondisk[stat.ST_UID])
+ group = str(ondisk[stat.ST_GID])
+ except (OSError, KeyError):
+ self.logger.error('POSIX: User/Group resolution failed for path %s'
+ % path)
+ owner = 'root'
+ group = '0'
+
+ perms = oct(ondisk[stat.ST_MODE])[-4:]
+ if entry.get('mtime', '-1') != '-1':
+ mtime = str(ondisk[stat.ST_MTIME])
+ else:
+ mtime = '-1'
+
+ configOwner = str(normUid(entry))
+ configGroup = str(normGid(entry))
+ configPerms = int(entry.get('perms'), 8)
+ if entry.get('dev_type'):
+ configPerms |= device_map[entry.get('dev_type')]
+ if has_selinux:
+ if entry.get("secontext") == "__default__":
+ configContext = selinux.matchpathcon(path, 0)[1]
+ else:
+ configContext = entry.get("secontext")
+
+ errors = []
+ if owner != configOwner:
+ if checkonly:
+ return False
+ entry.set('current_owner', owner)
+ errors.append("POSIX: Owner for path %s is incorrect. "
+ "Current owner is %s but should be %s" %
+ (path, ondisk.st_uid, entry.get('owner')))
+
+ if group != configGroup:
+ if checkonly:
+ return False
+ entry.set('current_group', group)
+ errors.append("POSIX: Group for path %s is incorrect. "
+ "Current group is %s but should be %s" %
+ (path, ondisk.st_gid, entry.get('group')))
+
+ if oct(int(perms, 8)) != oct(configPerms):
+ if checkonly:
+ return False
+ entry.set('current_perms', perms)
+ errors.append("POSIX: Permissions for path %s are incorrect. "
+ "Current permissions are %s but should be %s" %
+ (path, perms, entry.get('perms')))
+
+ if entry.get('mtime') and mtime != entry.get('mtime', '-1'):
+ if checkonly:
+ return False
+ entry.set('current_mtime', mtime)
+ errors.append("POSIX: mtime for path %s is incorrect. "
+ "Current mtime is %s but should be %s" %
+ (path, mtime, entry.get('mtime')))
+
+ seVerifies = self._verify_secontext(entry)
+
+ if errors:
+ for error in errors:
+ self.logger.debug(error)
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return False
+ else:
+ return seVerifies
+
+ def _verify_secontext(self, entry):
+ if not secontextMatches(entry):
+ path = entry.get("name")
+ if entry.get("secontext") == "__default__":
+ configContext = selinux.matchpathcon(path, 0)[1]
+ else:
+ configContext = entry.get("secontext")
+ pcontext = selinux.getfilecon(path)[1]
+ entry.set('current_secontext', pcontext)
+ msg = ("SELinux context for path %s is incorrect. "
+ "Current context is %s but should be %s" %
+ (path, pcontext, configContext))
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get("qtext", ''), msg]))
+ return False
+ return True
+
+ def _diff(self, content1, content2, difffunc, filename=None):
+ rv = []
+ start = time.time()
+ longtime = False
+ for diffline in difffunc(content1.split('\n'),
+ content2.split('\n')):
+ now = time.time()
+ rv.append(diffline)
+ if now - start > 5 and not longtime:
+ if filename:
+ self.logger.info("Diff of %s taking a long time" %
+ filename)
+ else:
+ self.logger.info("Diff taking a long time")
+ longtime = True
+ elif now - start > 30:
+ if filename:
+ self.logger.error("Diff of %s took too long; giving up" %
+ filename)
+ else:
+ self.logger.error("Diff took too long; giving up")
+ return False
+ return rv
diff --git a/src/lib/Bcfg2/Client/Tools/Portage.py b/src/lib/Bcfg2/Client/Tools/Portage.py
index 4516f419d..36d48b8d3 100644
--- a/src/lib/Bcfg2/Client/Tools/Portage.py
+++ b/src/lib/Bcfg2/Client/Tools/Portage.py
@@ -2,8 +2,6 @@
import re
import Bcfg2.Client.Tools
-from Bcfg2.Bcfg2Py3k import ConfigParser
-
class Portage(Bcfg2.Client.Tools.PkgTool):
"""The Gentoo toolset implements package and service operations and
@@ -27,30 +25,11 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
self._ebuild_pattern = re.compile('(ebuild|binary)')
self.cfg = cfg
self.installed = {}
- self._binpkgonly = True
-
- # Used to get options from configuration file
- parser = ConfigParser.ConfigParser()
- parser.read(self.setup.get('setup'))
- for opt in ['binpkgonly']:
- if parser.has_option(self.name, opt):
- setattr(self, ('_%s' % opt),
- self._StrToBoolIfBool(parser.get(self.name, opt)))
-
+ self._binpkgonly = self.setup.get('portage_binpkgonly', False)
if self._binpkgonly:
self.pkgtool = self._binpkgtool
self.RefreshPackages()
- def _StrToBoolIfBool(self, s):
- """Returns a boolean if the string specifies a boolean value.
- Returns a string otherwise"""
- if s.lower() in ('true', 'yes', 't', 'y', '1'):
- return True
- elif s.lower() in ('false', 'no', 'f', 'n', '0'):
- return False
- else:
- return s
-
def RefreshPackages(self):
"""Refresh memory hashes of packages."""
if not self._initialised:
@@ -83,8 +62,8 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
entry.set('current_version', version)
if not self.setup['quick']:
- if ('verify' not in entry.attrib) or \
- self._StrToBoolIfBool(entry.get('verify')):
+ if ('verify' not in entry.attrib or
+ entry.get('verify').lower == 'true'):
# Check the package if:
# - Not running in quick mode
diff --git a/src/lib/Bcfg2/Client/Tools/RPMng.py b/src/lib/Bcfg2/Client/Tools/RPMng.py
index 00dd00d71..d6e5e82c5 100644
--- a/src/lib/Bcfg2/Client/Tools/RPMng.py
+++ b/src/lib/Bcfg2/Client/Tools/RPMng.py
@@ -4,8 +4,6 @@ import os.path
import rpm
import rpmtools
import Bcfg2.Client.Tools
-# Compatibility import
-from Bcfg2.Bcfg2Py3k import ConfigParser
class RPMng(Bcfg2.Client.Tools.PkgTool):
"""Support for RPM packages."""
@@ -44,82 +42,42 @@ class RPMng(Bcfg2.Client.Tools.PkgTool):
self.modlists = {}
self.gpg_keyids = self.getinstalledgpg()
- # Process thee RPMng section from the config file.
- RPMng_CP = ConfigParser.ConfigParser()
- RPMng_CP.read(self.setup.get('setup'))
-
- # installonlypackages
- self.installOnlyPkgs = []
- if RPMng_CP.has_option(self.name, 'installonlypackages'):
- for i in RPMng_CP.get(self.name, 'installonlypackages').split(','):
- self.installOnlyPkgs.append(i.strip())
- if self.installOnlyPkgs == []:
- self.installOnlyPkgs = ['kernel', 'kernel-bigmem', 'kernel-enterprise', 'kernel-smp',
- 'kernel-modules', 'kernel-debug', 'kernel-unsupported',
- 'kernel-source', 'kernel-devel', 'kernel-default',
- 'kernel-largesmp-devel', 'kernel-largesmp', 'kernel-xen',
- 'gpg-pubkey']
+ opt_prefix = self.name.lower()
+ self.installOnlyPkgs = self.setup["%s_installonly" % opt_prefix]
if 'gpg-pubkey' not in self.installOnlyPkgs:
self.installOnlyPkgs.append('gpg-pubkey')
- self.logger.debug('installOnlyPackages = %s' % self.installOnlyPkgs)
-
- # erase_flags
- self.erase_flags = []
- if RPMng_CP.has_option(self.name, 'erase_flags'):
- for i in RPMng_CP.get(self.name, 'erase_flags').split(','):
- self.erase_flags.append(i.strip())
- if self.erase_flags == []:
- self.erase_flags = ['allmatches']
- self.logger.debug('erase_flags = %s' % self.erase_flags)
-
- # pkg_checks
- if RPMng_CP.has_option(self.name, 'pkg_checks'):
- self.pkg_checks = RPMng_CP.get(self.name, 'pkg_checks').lower()
- else:
- self.pkg_checks = 'true'
- self.logger.debug('pkg_checks = %s' % self.pkg_checks)
-
- # pkg_verify
- if RPMng_CP.has_option(self.name, 'pkg_verify'):
- self.pkg_verify = RPMng_CP.get(self.name, 'pkg_verify').lower()
- else:
- self.pkg_verify = 'true'
- self.logger.debug('pkg_verify = %s' % self.pkg_verify)
-
- # installed_action
- if RPMng_CP.has_option(self.name, 'installed_action'):
- self.installed_action = RPMng_CP.get(self.name, 'installed_action').lower()
- else:
- self.installed_action = 'install'
- self.logger.debug('installed_action = %s' % self.installed_action)
-
- # version_fail_action
- if RPMng_CP.has_option(self.name, 'version_fail_action'):
- self.version_fail_action = RPMng_CP.get(self.name, 'version_fail_action').lower()
- else:
- self.version_fail_action = 'upgrade'
- self.logger.debug('version_fail_action = %s' % self.version_fail_action)
-
- # verify_fail_action
- if self.name == "RPMng":
- if RPMng_CP.has_option(self.name, 'verify_fail_action'):
- self.verify_fail_action = RPMng_CP.get(self.name, 'verify_fail_action').lower()
- else:
- self.verify_fail_action = 'reinstall'
- else: # yum can't reinstall packages.
- self.verify_fail_action = 'none'
- self.logger.debug('verify_fail_action = %s' % self.verify_fail_action)
-
- # version_fail_action
- if RPMng_CP.has_option(self.name, 'verify_flags'):
- self.verify_flags = RPMng_CP.get(self.name, 'verify_flags').lower().split(',')
- else:
- self.verify_flags = []
+ self.erase_flags = self.setup['%s_erase_flags' % opt_prefix]
+ self.pkg_checks = self.setup['%s_pkg_checks' % opt_prefix]
+ self.pkg_verify = self.setup['%s_pkg_verify' % opt_prefix]
+ self.installed_action = self.setup['%s_installed_action' % opt_prefix]
+ self.version_fail_action = self.setup['%s_version_fail_action' %
+ opt_prefix]
+ self.verify_fail_action = self.setup['%s_verify_fail_action' %
+ opt_prefix]
+ self.verify_flags = self.setup['%s_verify_flags' % opt_prefix]
if '' in self.verify_flags:
self.verify_flags.remove('')
- self.logger.debug('version_fail_action = %s' % self.version_fail_action)
+
+ self.logger.debug('%s: installOnlyPackages = %s' %
+ (self.name, self.installOnlyPkgs))
+ self.logger.debug('%s: erase_flags = %s' %
+ (self.name, self.erase_flags))
+ self.logger.debug('%s: pkg_checks = %s' %
+ (self.name, self.pkg_checks))
+ self.logger.debug('%s: pkg_verify = %s' %
+ (self.name, self.pkg_verify))
+ self.logger.debug('%s: installed_action = %s' %
+ (self.name, self.installed_action))
+ self.logger.debug('%s: version_fail_action = %s' %
+ (self.name, self.version_fail_action))
+ self.logger.debug('%s: verify_fail_action = %s' %
+ (self.name, self.verify_fail_action))
+ self.logger.debug('%s: verify_flags = %s' %
+ (self.name, self.verify_flags))
+
# Force a re- prelink of all packages if prelink exists.
- # Many, if not most package verifies can be caused by out of date prelinking.
+ # Many, if not most package verifies can be caused by out of
+ # date prelinking.
if os.path.isfile('/usr/sbin/prelink') and not self.setup['dryrun']:
cmdrc, output = self.cmd.run('/usr/sbin/prelink -a -mR')
if cmdrc == 0:
@@ -193,7 +151,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool):
instance = Bcfg2.Client.XML.SubElement(entry, 'Package')
for attrib in list(entry.attrib.keys()):
instance.attrib[attrib] = entry.attrib[attrib]
- if self.pkg_checks == 'true' and entry.get('pkg_checks', 'true') == 'true':
+ if (self.pkg_checks and
+ entry.get('pkg_checks', 'true').lower() == 'true'):
if 'any' in [entry.get('version'), pinned_version]:
version, release = 'any', 'any'
elif entry.get('version') == 'auto':
@@ -215,7 +174,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool):
if entry.get('name') in self.installed:
# There is at least one instance installed.
- if self.pkg_checks == 'true' and entry.get('pkg_checks', 'true') == 'true':
+ if (self.pkg_checks and
+ entry.get('pkg_checks', 'true').lower() == 'true'):
rpmTs = rpm.TransactionSet()
rpmHeader = None
for h in rpmTs.dbMatch(rpm.RPMTAG_NAME, entry.get('name')):
@@ -243,8 +203,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool):
self.logger.debug(" %s" % self.str_evra(inst))
self.instance_status[inst]['installed'] = True
- if self.pkg_verify == 'true' and \
- inst.get('pkg_verify', 'true') == 'true':
+ if (self.pkg_verify and
+ inst.get('pkg_verify', 'true').lower() == 'true'):
flags = inst.get('verify_flags', '').split(',') + self.verify_flags
if pkg.get('gpgkeyid', '')[-8:] not in self.gpg_keyids and \
entry.get('name') != 'gpg-pubkey':
@@ -302,8 +262,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool):
self.logger.debug(" %s" % self.str_evra(inst))
self.instance_status[inst]['installed'] = True
- if self.pkg_verify == 'true' and \
- inst.get('pkg_verify', 'true') == 'true':
+ if (self.pkg_verify and
+ inst.get('pkg_verify', 'true').lower() == 'true'):
flags = inst.get('verify_flags', '').split(',') + self.verify_flags
if pkg.get('gpgkeyid', '')[-8:] not in self.gpg_keyids and \
'nosignature' not in flags:
@@ -824,8 +784,8 @@ class RPMng(Bcfg2.Client.Tools.PkgTool):
return False
# We don't want to do any checks so we don't care what the entry has in it.
- if self.pkg_checks == 'false' or \
- entry.get('pkg_checks', 'true').lower() == 'false':
+ if (not self.pkg_checks or
+ entry.get('pkg_checks', 'true').lower() == 'false'):
return True
instances = entry.findall('Instance')
diff --git a/src/lib/Bcfg2/Client/Tools/RcUpdate.py b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
index 1b9a29478..ddf9c1f2d 100644
--- a/src/lib/Bcfg2/Client/Tools/RcUpdate.py
+++ b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
@@ -23,22 +23,18 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
rc = self.cmd.run(cmd % entry.get('name'))[0]
is_enabled = (rc == 0)
- if entry.get('mode', 'default') == 'supervised':
- # check if init script exists
- try:
- os.stat('/etc/init.d/%s' % entry.get('name'))
- except OSError:
- self.logger.debug('Init script for service %s does not exist' %
- entry.get('name'))
- return False
+ # check if init script exists
+ try:
+ os.stat('/etc/init.d/%s' % entry.get('name'))
+ except OSError:
+ self.logger.debug('Init script for service %s does not exist' %
+ entry.get('name'))
+ return False
- # check if service is enabled
- cmd = '/etc/init.d/%s status | grep started'
- rc = self.cmd.run(cmd % entry.attrib['name'])[0]
- is_running = (rc == 0)
- else:
- # we don't care
- is_running = is_enabled
+ # check if service is enabled
+ cmd = '/etc/init.d/%s status | grep started'
+ rc = self.cmd.run(cmd % entry.attrib['name'])[0]
+ is_running = (rc == 0)
if entry.get('status') == 'on' and not (is_enabled and is_running):
entry.set('current_status', 'off')
@@ -53,19 +49,11 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
def InstallService(self, entry):
"""
Install Service entry
- In supervised mode we also take care it's (not) running.
"""
- # don't take any actions for mode='manual'
- if entry.get('mode', 'default') == 'manual':
- self.logger.info("Service %s mode set to manual. Skipping "
- "installation." % (entry.get('name')))
- return False
self.logger.info('Installing Service %s' % entry.get('name'))
if entry.get('status') == 'on':
- # make sure it's running if in supervised mode
- if entry.get('mode', 'default') == 'supervised' \
- and entry.get('current_status') == 'off':
+ if entry.get('current_status') == 'off':
self.start_service(entry)
# make sure it's enabled
cmd = '/sbin/rc-update add %s default'
@@ -73,9 +61,7 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
return (rc == 0)
elif entry.get('status') == 'off':
- # make sure it's not running if in supervised mode
- if entry.get('mode', 'default') == 'supervised' \
- and entry.get('current_status') == 'on':
+ if entry.get('current_status') == 'on':
self.stop_service(entry)
# make sure it's disabled
cmd = '/sbin/rc-update del %s default'
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py
new file mode 100644
index 000000000..1c0db904b
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -0,0 +1,716 @@
+import os
+import re
+import sys
+import copy
+import glob
+import struct
+import socket
+import selinux
+import seobject
+import Bcfg2.Client.XML
+import Bcfg2.Client.Tools
+import Bcfg2.Client.Tools.POSIX
+
+def pack128(int_val):
+ """ pack a 128-bit integer in big-endian format """
+ max_int = 2 ** (128) - 1
+ max_word_size = 2 ** 32 - 1
+
+ if int_val <= max_word_size:
+ return struct.pack('>L', int_val)
+
+ words = []
+ for i in range(4):
+ word = int_val & max_word_size
+ words.append(int(word))
+ int_val >>= 32
+ words.reverse()
+ return struct.pack('>4I', *words)
+
+def netmask_itoa(netmask, proto="ipv4"):
+ """ convert an integer netmask (e.g., /16) to dotted-quad
+ notation (255.255.0.0) or IPv6 prefix notation (ffff::) """
+ if proto == "ipv4":
+ size = 32
+ family = socket.AF_INET
+ else: # ipv6
+ size = 128
+ family = socket.AF_INET6
+ try:
+ int(netmask)
+ except ValueError:
+ return netmask
+
+ if netmask > size:
+ raise ValueError("Netmask too large: %s" % netmask)
+
+ res = 0L
+ for n in range(netmask):
+ res |= 1 << (size - n - 1)
+ netmask = socket.inet_ntop(family, pack128(res))
+ return netmask
+
+
+class SELinux(Bcfg2.Client.Tools.Tool):
+ """ SELinux boolean and module support """
+ name = 'SELinux'
+ __handles__ = [('SELinux', 'boolean'),
+ ('SELinux', 'port'),
+ ('SELinux', 'fcontext'),
+ ('SELinux', 'node'),
+ ('SELinux', 'login'),
+ ('SELinux', 'user'),
+ ('SELinux', 'interface'),
+ ('SELinux', 'permissive'),
+ ('SELinux', 'module')]
+ __req__ = dict(SELinux=dict(boolean=['name', 'value'],
+ module=['name'],
+ port=['name', 'selinuxtype'],
+ fcontext=['name', 'selinuxtype'],
+ node=['name', 'selinuxtype', 'proto'],
+ login=['name', 'selinuxuser'],
+ user=['name', 'roles', 'prefix'],
+ interface=['name', 'selinuxtype'],
+ permissive=['name']))
+
+ def __init__(self, logger, setup, config):
+ Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
+ self.handlers = {}
+ for handles in self.__handles__:
+ etype = handles[1]
+ self.handlers[etype] = \
+ globals()["SELinux%sHandler" % etype.title()](self, logger,
+ setup, config)
+
+ def BundleUpdated(self, _, states):
+ for handler in self.handlers.values():
+ handler.BundleUpdated(states)
+
+ def FindExtra(self):
+ extra = []
+ for handler in self.handlers.values():
+ extra.extend(handler.FindExtra())
+ return extra
+
+ def canInstall(self, entry):
+ return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and
+ self.handlers[entry.get('type')].canInstall(entry))
+
+ def primarykey(self, entry):
+ """ return a string that should be unique amongst all entries
+ in the specification """
+ return self.handlers[entry.get('type')].primarykey(entry)
+
+ def Install(self, entries, states):
+ # start a transaction
+ sr = seobject.semanageRecords("")
+ if hasattr(sr, "start"):
+ self.logger.debug("Starting SELinux transaction")
+ sr.start()
+ else:
+ self.logger.debug("SELinux transactions not supported; this may "
+ "slow things down considerably")
+ Bcfg2.Client.Tools.Tool.Install(self, entries, states)
+ if hasattr(sr, "finish"):
+ self.logger.debug("Committing SELinux transaction")
+ sr.finish()
+
+ def InstallSELinux(self, entry):
+ """Dispatch install to the proper method according to type"""
+ return self.handlers[entry.get('type')].Install(entry)
+
+ def VerifySELinux(self, entry, _):
+ """Dispatch verify to the proper method according to type"""
+ rv = self.handlers[entry.get('type')].Verify(entry)
+ if entry.get('qtext') and self.setup['interactive']:
+ entry.set('qtext',
+ '%s\nInstall SELinux %s %s: (y/N) ' %
+ (entry.get('qtext'),
+ entry.get('type'),
+ self.handlers[entry.get('type')].tostring(entry)))
+ return rv
+
+ def Remove(self, entries):
+ """Dispatch verify to the proper removal method according to type"""
+ # sort by type
+ types = list()
+ for entry in entries:
+ if entry.get('type') not in types:
+ types.append(entry.get('type'))
+
+ for etype in types:
+ self.handlers[entry.get('type')].Remove([e for e in entries
+ if e.get('type') == etype])
+
+
+class SELinuxEntryHandler(object):
+ etype = None
+ key_format = ("name",)
+ value_format = ()
+ str_format = '%(name)s'
+ custom_re = re.compile(' (?P<name>\S+)$')
+ custom_format = None
+
+ def __init__(self, tool, logger, setup, config):
+ self.tool = tool
+ self.logger = logger
+ self._records = None
+ self._all = None
+ if not self.custom_format:
+ self.custom_format = self.key_format
+
+ @property
+ def records(self):
+ if self._records is None:
+ self._records = getattr(seobject, "%sRecords" % self.etype)("")
+ return self._records
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ self._all = self.records.get_all()
+ return self._all
+
+ @property
+ def custom_records(self):
+ if hasattr(self.records, "customized") and self.custom_re:
+ return dict([(k, self.all_records[k]) for k in self.custom_keys])
+ else:
+ # ValueError is really a pretty dumb exception to raise,
+ # but that's what the seobject customized() method raises
+ # if it's defined but not implemented. yeah, i know, wtf.
+ raise ValueError("custom_records")
+
+ @property
+ def custom_keys(self):
+ keys = []
+ for cmd in self.records.customized():
+ match = self.custom_re.search(cmd)
+ if match:
+ if (len(self.custom_format) == 1 and
+ self.custom_format[0] == "name"):
+ keys.append(match.group("name"))
+ else:
+ keys.append(tuple([match.group(k)
+ for k in self.custom_format]))
+ return keys
+
+ def tostring(self, entry):
+ return self.str_format % entry.attrib
+
+ def keytostring(self, key):
+ return self.str_format % self._key2attrs(key)
+
+ def _key(self, entry):
+ if len(self.key_format) == 1 and self.key_format[0] == "name":
+ return entry.get("name")
+ else:
+ rv = []
+ for key in self.key_format:
+ rv.append(entry.get(key))
+ return tuple(rv)
+
+ def _key2attrs(self, key):
+ if isinstance(key, tuple):
+ rv = dict((self.key_format[i], key[i])
+ for i in range(len(self.key_format))
+ if self.key_format[i])
+ else:
+ rv = dict(name=key)
+ if self.value_format:
+ vals = self.all_records[key]
+ rv.update(dict((self.value_format[i], vals[i])
+ for i in range(len(self.value_format))
+ if self.value_format[i]))
+ return rv
+
+ def key2entry(self, key):
+ attrs = self._key2attrs(key)
+ attrs["type"] = self.etype
+ return Bcfg2.Client.XML.Element("SELinux", **attrs)
+
+ def _args(self, entry, method):
+ if hasattr(self, "_%sargs" % method):
+ return getattr(self, "_%sargs" % method)(entry)
+ elif hasattr(self, "_defaultargs"):
+ # default args
+ return self._defaultargs(entry)
+ else:
+ raise NotImplementedError
+
+ def _deleteargs(self, entry):
+ return (self._key(entry))
+
+ def canInstall(self, entry):
+ return bool(self._key(entry))
+
+ def primarykey(self, entry):
+ return ":".join([entry.tag, entry.get("type"), entry.get("name")])
+
+ def exists(self, entry):
+ if self._key(entry) not in self.all_records:
+ self.logger.debug("SELinux %s %s does not exist" %
+ (self.etype, self.tostring(entry)))
+ return False
+ return True
+
+ def Verify(self, entry):
+ if not self.exists(entry):
+ entry.set('current_exists', 'false')
+ return False
+
+ errors = []
+ current_attrs = self._key2attrs(self._key(entry))
+ desired_attrs = entry.attrib
+ for attr in self.value_format:
+ if not attr:
+ continue
+ if current_attrs[attr] != desired_attrs[attr]:
+ entry.set('current_%s' % attr, current_attrs[attr])
+ errors.append("SELinux %s %s has wrong %s: %s, should be %s" %
+ (self.etype, self.tostring(entry), attr,
+ current_attrs[attr], desired_attrs[attr]))
+
+ if errors:
+ for error in errors:
+ self.logger.debug(error)
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return False
+ else:
+ return True
+
+ def Install(self, entry, method=None):
+ if not method:
+ if self.exists(entry):
+ method = "modify"
+ else:
+ method = "add"
+ self.logger.debug("%s SELinux %s %s" %
+ (method.title(), self.etype, self.tostring(entry)))
+
+ try:
+ getattr(self.records, method)(*self._args(entry, method))
+ self._all = None
+ return True
+ except ValueError:
+ err = sys.exc_info()[1]
+ self.logger.debug("Failed to %s SELinux %s %s: %s" %
+ (method, self.etype, self.tostring(entry), err))
+ return False
+
+ def Remove(self, entries):
+ for entry in entries:
+ try:
+ self.records.delete(*self._args(entry, "delete"))
+ self._all = None
+ except ValueError:
+ err = sys.exc_info()[1]
+ self.logger.info("Failed to remove SELinux %s %s: %s" %
+ (self.etype, self.tostring(entry), err))
+
+ def FindExtra(self):
+ specified = [self._key(e)
+ for e in self.tool.getSupportedEntries()
+ if e.get("type") == self.etype]
+ try:
+ records = self.custom_records
+ except ValueError:
+ records = self.all_records
+ return [self.key2entry(key)
+ for key in records.keys()
+ if key not in specified]
+
+ def BundleUpdated(self, states):
+ pass
+
+
+class SELinuxBooleanHandler(SELinuxEntryHandler):
+ etype = "boolean"
+ value_format = ("value",)
+
+ @property
+ def all_records(self):
+ # older versions of selinux return a single 0/1 value for each
+ # bool, while newer versions return a list of three 0/1 values
+ # representing various states. we don't care about the latter
+ # two values, but it's easier to coerce the older format into
+ # the newer format as far as interoperation with the rest of
+ # SELinuxEntryHandler goes
+ rv = SELinuxEntryHandler.all_records.fget(self)
+ if rv.values()[0] in [0, 1]:
+ for key, val in rv.items():
+ rv[key] = [val, val, val]
+ return rv
+
+ def _key2attrs(self, key):
+ rv = SELinuxEntryHandler._key2attrs(self, key)
+ status = self.all_records[key][0]
+ if status:
+ rv['value'] = "on"
+ else:
+ rv['value'] = "off"
+ return rv
+
+ def _defaultargs(self, entry):
+ # the only values recognized by both new and old versions of
+ # selinux are the strings "0" and "1". old selinux accepts
+ # ints or bools as well, new selinux accepts "on"/"off"
+ if entry.get("value").lower() == "on":
+ value = "1"
+ else:
+ value = "0"
+ return (entry.get("name"), value)
+
+ def canInstall(self, entry):
+ if entry.get("value").lower() not in ["on", "off"]:
+ self.logger.debug("SELinux %s %s has a bad value: %s" %
+ (self.etype, self.tostring(entry),
+ entry.get("value")))
+ return False
+ return (self.exists(entry) and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+
+class SELinuxPortHandler(SELinuxEntryHandler):
+ etype = "port"
+ value_format = ('selinuxtype', None)
+ custom_re = re.compile(r'-p (?P<proto>tcp|udp).*? (?P<start>\d+)(?:-(?P<end>\d+))?$')
+
+ @property
+ def custom_keys(self):
+ keys = []
+ for cmd in self.records.customized():
+ match = self.custom_re.search(cmd)
+ if match:
+ if match.group('end'):
+ keys.append((int(match.group('start')),
+ int(match.group('end')),
+ match.group('proto')))
+ else:
+ keys.append((int(match.group('start')),
+ int(match.group('start')),
+ match.group('proto')))
+ return keys
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # older versions of selinux use (startport, endport) as
+ # they key for the ports.get_all() dict, and (type, proto,
+ # level) as the value; this is obviously broken, so newer
+ # versions use (startport, endport, proto) as the key, and
+ # (type, level) as the value. abstracting around this
+ # sucks.
+ ports = self.records.get_all()
+ if len(ports.keys()[0]) == 3:
+ self._all = ports
+ else:
+ # uglist list comprehension ever?
+ self._all = dict([((k[0], k[1], v[1]), (v[0], v[2]))
+ for k, v in ports.items()])
+ return self._all
+
+ def _key(self, entry):
+ try:
+ (port, proto) = entry.get("name").split("/")
+ except ValueError:
+ self.logger.error("Invalid SELinux node %s: no protocol specified" %
+ entry.get("name"))
+ return
+ if "-" in port:
+ start, end = port.split("-")
+ else:
+ start = port
+ end = port
+ return (int(start), int(end), proto)
+
+ def _key2attrs(self, key):
+ if key[0] == key[1]:
+ port = str(key[0])
+ else:
+ port = "%s-%s" % (key[0], key[1])
+ vals = self.all_records[key]
+ return dict(name="%s/%s" % (port, key[2]), selinuxtype=vals[0])
+
+ def _defaultargs(self, entry):
+ (port, proto) = entry.get("name").split("/")
+ return (port, proto, '', entry.get("selinuxtype"))
+
+ def _deleteargs(self, entry):
+ return tuple(entry.get("name").split("/"))
+
+
+class SELinuxFcontextHandler(SELinuxEntryHandler):
+ etype = "fcontext"
+ key_format = ("name", "filetype")
+ value_format = (None, None, "selinuxtype", None)
+ filetypeargs = dict(all="",
+ regular="--",
+ directory="-d",
+ symlink="-l",
+ pipe="-p",
+ socket="-s",
+ block="-b",
+ char="-c",
+ door="-D")
+ filetypenames = dict(all="all files",
+ regular="regular file",
+ directory="directory",
+ symlink="symbolic link",
+ pipe="named pipe",
+ socket="socket",
+ block="block device",
+ char="character device",
+ door="door")
+ filetypeattrs = dict([v, k] for k, v in filetypenames.iteritems())
+ custom_re = re.compile(r'-f \'(?P<filetype>[a-z ]+)\'.*? \'(?P<name>.*)\'')
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # on older selinux, fcontextRecords.get_all() returns a
+ # list of tuples of (filespec, filetype, seuser, serole,
+ # setype, level); on newer selinux, get_all() returns a
+ # dict of (filespec, filetype) => (seuser, serole, setype,
+ # level).
+ fcontexts = self.records.get_all()
+ if isinstance(fcontexts, dict):
+ self._all = fcontexts
+ else:
+ self._all = dict([(f[0:2], f[2:]) for f in fcontexts])
+ return self._all
+
+ def _key(self, entry):
+ ftype = entry.get("filetype", "all")
+ return (entry.get("name"),
+ self.filetypenames.get(ftype, ftype))
+
+ def _key2attrs(self, key):
+ rv = dict(name=key[0], filetype=self.filetypeattrs[key[1]])
+ vals = self.all_records[key]
+ # in older versions of selinux, an fcontext with no selinux
+ # type is the single value None; in newer versions, it's a
+ # tuple whose 0th (and only) value is None.
+ if vals and vals[0]:
+ rv["selinuxtype"] = vals[2]
+ else:
+ rv["selinuxtype"] = "<<none>>"
+ return rv
+
+ def canInstall(self, entry):
+ return (entry.get("filetype", "all") in self.filetypeargs and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), entry.get("selinuxtype"),
+ self.filetypeargs[entry.get("filetype", "all")],
+ '', '')
+
+ def primarykey(self, entry):
+ return ":".join([entry.tag, entry.get("type"), entry.get("name"),
+ entry.get("filetype", "all")])
+
+
+class SELinuxNodeHandler(SELinuxEntryHandler):
+ etype = "node"
+ value_format = (None, None, "selinuxtype", None)
+ str_format = '%(name)s (%(proto)s)'
+ custom_re = re.compile(r'-M (?P<netmask>\S+).*?-p (?P<proto>ipv\d).*? (?P<addr>\S+)$')
+ custom_format = ('addr', 'netmask', 'proto')
+
+ def _key(self, entry):
+ try:
+ (addr, netmask) = entry.get("name").split("/")
+ except ValueError:
+ self.logger.error("Invalid SELinux node %s: no netmask specified" %
+ entry.get("name"))
+ return
+ netmask = netmask_itoa(netmask, proto=entry.get("proto"))
+ return (addr, netmask, entry.get("proto"))
+
+ def _key2attrs(self, key):
+ vals = self.all_records[key]
+ return dict(name="%s/%s" % (key[0], key[1]), proto=key[2],
+ selinuxtype=vals[2])
+
+ def _defaultargs(self, entry):
+ (addr, netmask) = entry.get("name").split("/")
+ return (addr, netmask, entry.get("proto"), "", entry.get("selinuxtype"))
+
+
+class SELinuxLoginHandler(SELinuxEntryHandler):
+ etype = "login"
+ value_format = ("selinuxuser", None)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), entry.get("selinuxuser"), "")
+
+
+class SELinuxUserHandler(SELinuxEntryHandler):
+ etype = "user"
+ value_format = ("prefix", None, None, "roles")
+
+ def __init__(self, tool, logger, setup, config):
+ SELinuxEntryHandler.__init__(self, tool, logger, setup, config)
+ self.needs_prefix = False
+
+ @property
+ def records(self):
+ if self._records is None:
+ self._records = seobject.seluserRecords()
+ return self._records
+
+ def Install(self, entry):
+ # in older versions of selinux, modify() is broken if you
+ # provide a prefix _at all_, so we try to avoid giving the
+ # prefix. however, in newer versions, prefix is _required_,
+ # so we a) try without a prefix; b) catch TypeError, which
+ # indicates that we had the wrong number of args (ValueError
+ # is thrown by the bug in older versions of selinux); and c)
+ # try with prefix.
+ try:
+ SELinuxEntryHandler.Install(self, entry)
+ except TypeError:
+ self.needs_prefix = True
+ SELinuxEntryHandler.Install(self, entry)
+
+ def _defaultargs(self, entry):
+ # in older versions of selinux, modify() is broken if you
+ # provide a prefix _at all_, so we try to avoid giving the
+ # prefix. see the comment in Install() above for more
+ # details.
+ rv = [entry.get("name"),
+ entry.get("roles", "").replace(" ", ",").split(",")]
+ if self.needs_prefix:
+ rv.extend(['', '', entry.get("prefix")])
+ else:
+ key = self._key(entry)
+ if key in self.all_records:
+ attrs = self._key2attrs(key)
+ if attrs['prefix'] != entry.get("prefix"):
+ rv.extend(['', '', entry.get("prefix")])
+ return tuple(rv)
+
+
+class SELinuxInterfaceHandler(SELinuxEntryHandler):
+ etype = "interface"
+ value_format = (None, None, "selinuxtype", None)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), '', entry.get("selinuxtype"))
+
+
+class SELinuxPermissiveHandler(SELinuxEntryHandler):
+ etype = "permissive"
+
+ @property
+ def records(self):
+ try:
+ return SELinuxEntryHandler.records.fget(self)
+ except AttributeError:
+ self.logger.info("Permissive domains not supported by this version "
+ "of SELinux")
+ self._records = False
+ return self._records
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ if self.records == False:
+ self._all = dict()
+ else:
+ # permissiveRecords.get_all() returns a list, so we just
+ # make it into a dict so that the rest of
+ # SELinuxEntryHandler works
+ self._all = dict([(d, d) for d in self.records.get_all()])
+ return self._all
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"),)
+
+
+class SELinuxModuleHandler(SELinuxEntryHandler):
+ etype = "module"
+ value_format = (None, "disabled")
+
+ def __init__(self, tool, logger, setup, config):
+ SELinuxEntryHandler.__init__(self, tool, logger, setup, config)
+ self.posixtool = Bcfg2.Client.Tools.POSIX.POSIX(logger, setup, config)
+ try:
+ self.setype = selinux.selinux_getpolicytype()[1]
+ except IndexError:
+ self.logger.error("Unable to determine SELinux policy type")
+ self.setype = None
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # we get a list of tuples back; coerce it into a dict
+ self._all = dict([(m[0], (m[1], m[2]))
+ for m in self.records.get_all()])
+ return self._all
+
+ def _key2attrs(self, key):
+ rv = SELinuxEntryHandler._key2attrs(self, key)
+ status = self.all_records[key][1]
+ if status:
+ rv['disabled'] = "false"
+ else:
+ rv['disabled'] = "true"
+ return rv
+
+ def _filepath(self, entry):
+ return os.path.join("/usr/share/selinux", self.setype,
+ "%s.pp" % entry.get("name"))
+
+ def _pathentry(self, entry):
+ pathentry = copy.deepcopy(entry)
+ pathentry.set("name", self._filepath(pathentry))
+ pathentry.set("perms", "0644")
+ pathentry.set("owner", "root")
+ pathentry.set("group", "root")
+ pathentry.set("secontext", "__default__")
+ return pathentry
+
+ def Verify(self, entry):
+ if not entry.get("disabled"):
+ entry.set("disabled", "false")
+ return (SELinuxEntryHandler.Verify(self, entry) and
+ self.posixtool.Verifyfile(self._pathentry(entry), None))
+
+ def canInstall(self, entry):
+ return (entry.text and self.setype and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+ def Install(self, entry):
+ rv = self.posixtool.Installfile(self._pathentry(entry))
+ try:
+ rv = rv and SELinuxEntryHandler.Install(self, entry)
+ except NameError:
+ # some versions of selinux have a bug in seobject that
+ # makes modify() calls fail. add() seems to have the same
+ # effect as modify, but without the bug
+ if self.exists(entry):
+ rv = rv and SELinuxEntryHandler.Install(self, entry,
+ method="add")
+
+ if entry.get("disabled", "false").lower() == "true":
+ method = "disable"
+ else:
+ method = "enable"
+ return rv and SELinuxEntryHandler.Install(self, entry, method=method)
+
+ def _addargs(self, entry):
+ return (self._filepath(entry),)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"),)
+
+ def FindExtra(self):
+ specified = [self._key(e)
+ for e in self.tool.getSupportedEntries()
+ if e.get("type") == self.etype]
+ return [self.key2entry(os.path.basename(f)[:-3])
+ for f in glob.glob(os.path.join("/usr/share/selinux",
+ self.setype, "*.pp"))
+ if f not in specified]
diff --git a/src/lib/Bcfg2/Client/Tools/SMF.py b/src/lib/Bcfg2/Client/Tools/SMF.py
index f824410ad..3e0a9da13 100644
--- a/src/lib/Bcfg2/Client/Tools/SMF.py
+++ b/src/lib/Bcfg2/Client/Tools/SMF.py
@@ -73,11 +73,6 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
def InstallService(self, entry):
"""Install SMF Service entry."""
- # don't take any actions for mode='manual'
- if entry.get('mode', 'default') == 'manual':
- self.logger.info("Service %s mode set to manual. Skipping "
- "installation." % (entry.get('name')))
- return False
self.logger.info("Installing Service %s" % (entry.get('name')))
if entry.get('status') == 'off':
if entry.get("FMRI").startswith('lrc'):
diff --git a/src/lib/Bcfg2/Client/Tools/Systemd.py b/src/lib/Bcfg2/Client/Tools/Systemd.py
index e3f6a4169..a295bc608 100644
--- a/src/lib/Bcfg2/Client/Tools/Systemd.py
+++ b/src/lib/Bcfg2/Client/Tools/Systemd.py
@@ -42,18 +42,11 @@ class Systemd(Bcfg2.Client.Tools.SvcTool):
def InstallService(self, entry):
"""Install Service entry."""
- # don't take any actions for mode = 'manual'
- if entry.get('mode', 'default') == 'manual':
- self.logger.info("Service %s mode set to manual. Skipping "
- "installation." % (entry.get('name')))
- return True
-
if entry.get('status') == 'on':
- pstatus = self.cmd.run(self.get_svc_command(entry, 'enable'))[0]
- pstatus = self.cmd.run(self.get_svc_command(entry, 'start'))[0]
-
+ rv = self.cmd.run(self.get_svc_command(entry, 'enable'))[0] == 0
+ rv &= self.cmd.run(self.get_svc_command(entry, 'start'))[0] == 0
else:
- pstatus = self.cmd.run(self.get_svc_command(entry, 'stop'))[0]
- pstatus = self.cmd.run(self.get_svc_command(entry, 'disable'))[0]
+ rv = self.cmd.run(self.get_svc_command(entry, 'stop'))[0] == 0
+ rv &= self.cmd.run(self.get_svc_command(entry, 'disable'))[0] == 0
- return not pstatus
+ return rv
diff --git a/src/lib/Bcfg2/Client/Tools/Upstart.py b/src/lib/Bcfg2/Client/Tools/Upstart.py
index 7afc8edd7..aa5a921a6 100644
--- a/src/lib/Bcfg2/Client/Tools/Upstart.py
+++ b/src/lib/Bcfg2/Client/Tools/Upstart.py
@@ -69,11 +69,6 @@ class Upstart(Bcfg2.Client.Tools.SvcTool):
def InstallService(self, entry):
"""Install Service for entry."""
- # don't take any actions for mode='manual'
- if entry.get('mode', 'default') == 'manual':
- self.logger.info("Service %s mode set to manual. Skipping "
- "installation." % (entry.get('name')))
- return False
if entry.get('status') == 'on':
pstatus = self.cmd.run(self.get_svc_command(entry, 'start'))[0]
elif entry.get('status') == 'off':
diff --git a/src/lib/Bcfg2/Client/Tools/YUM24.py b/src/lib/Bcfg2/Client/Tools/YUM24.py
index 4e488b9da..2bc821db3 100644
--- a/src/lib/Bcfg2/Client/Tools/YUM24.py
+++ b/src/lib/Bcfg2/Client/Tools/YUM24.py
@@ -6,20 +6,6 @@ import sys
import yum
import Bcfg2.Client.XML
import Bcfg2.Client.Tools.RPMng
-# Compatibility import
-from Bcfg2.Bcfg2Py3k import ConfigParser
-
-YAD = True
-CP = ConfigParser.ConfigParser()
-try:
- if '-C' in sys.argv:
- CP.read([sys.argv[sys.argv.index('-C') + 1]])
- else:
- CP.read(['/etc/bcfg2.conf'])
- if CP.get('YUMng', 'autodep').lower() == 'false':
- YAD = False
-except:
- pass
if not hasattr(Bcfg2.Client.Tools.RPMng, 'RPMng'):
raise ImportError
@@ -79,6 +65,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng):
(entry.get('name').startswith('/etc/yum.d') \
or entry.get('name').startswith('/etc/yum.repos.d')) \
or entry.get('name') == '/etc/yum.conf']
+ self.autodep = setup.get("yum24_autodep")
self.yum_avail = dict()
self.yum_installed = dict()
self.yb = yum.YumBase()
@@ -273,7 +260,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng):
if len(install_pkgs) > 0:
self.logger.info("Attempting to install packages")
- if YAD:
+ if self.autodep:
pkgtool = "/usr/bin/yum -d0 -y install %s"
else:
pkgtool = "/usr/bin/yum -d0 install %s"
@@ -309,7 +296,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng):
if len(upgrade_pkgs) > 0:
self.logger.info("Attempting to upgrade packages")
- if YAD:
+ if self.autodep:
pkgtool = "/usr/bin/yum -d0 -y update %s"
else:
pkgtool = "/usr/bin/yum -d0 update %s"
@@ -359,7 +346,7 @@ class YUM24(Bcfg2.Client.Tools.RPMng.RPMng):
"""
self.logger.debug('Running YUMng.RemovePackages()')
- if YAD:
+ if self.autodep:
pkgtool = "/usr/bin/yum -d0 -y erase %s"
else:
pkgtool = "/usr/bin/yum -d0 erase %s"
diff --git a/src/lib/Bcfg2/Client/Tools/YUMng.py b/src/lib/Bcfg2/Client/Tools/YUMng.py
index 244b66cf4..46fde5abd 100644
--- a/src/lib/Bcfg2/Client/Tools/YUMng.py
+++ b/src/lib/Bcfg2/Client/Tools/YUMng.py
@@ -12,9 +12,6 @@ import yum.misc
import rpmUtils.arch
import Bcfg2.Client.XML
import Bcfg2.Client.Tools
-# Compatibility import
-from Bcfg2.Bcfg2Py3k import ConfigParser
-
def build_yname(pkgname, inst):
"""Build yum appropriate package name."""
@@ -58,20 +55,6 @@ def nevraString(p):
return ret
-class Parser(ConfigParser.ConfigParser):
-
- def get(self, section, option, default):
- """
- Override ConfigParser.get: If the request option is not in the
- config file then return the value of default rather than raise
- an exception. We still raise exceptions on missing sections.
- """
- try:
- return ConfigParser.ConfigParser.get(self, section, option)
- except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
- return default
-
-
class RPMDisplay(yum.rpmtrans.RPMBaseCallback):
"""We subclass the default RPM transaction callback so that we
can control Yum's verbosity and pipe it through the right logger."""
@@ -224,38 +207,24 @@ class YUMng(Bcfg2.Client.Tools.PkgTool):
def _loadConfig(self):
# Process the YUMng section from the config file.
- CP = Parser()
- CP.read(self.setup.get('setup'))
- truth = ['true', 'yes', '1']
-
# These are all boolean flags, either we do stuff or we don't
- self.pkg_checks = CP.get(self.name, "pkg_checks", "true").lower() \
- in truth
- self.pkg_verify = CP.get(self.name, "pkg_verify", "true").lower() \
- in truth
- self.doInstall = CP.get(self.name, "installed_action",
- "install").lower() == "install"
- self.doUpgrade = CP.get(self.name,
- "version_fail_action", "upgrade").lower() == "upgrade"
- self.doReinst = CP.get(self.name, "verify_fail_action",
- "reinstall").lower() == "reinstall"
- self.verifyFlags = CP.get(self.name, "verify_flags",
- "").lower().replace(' ', ',')
+ self.pkg_checks = self.setup["yumng_pkg_checks"]
+ self.pkg_verify = self.setup["yumng_pkg_verify"]
+ self.doInstall = self.setup["yumng_installed_action"] == "install"
+ self.doUpgrade = self.setup["yumng_version_fail_action"] == "upgrade"
+ self.doReinst = self.setup["yumng_verify_fail_action"] == "reinstall"
+ self.verifyFlags = self.setup["yumng_verify_flags"]
self.installOnlyPkgs = self.yb.conf.installonlypkgs
if 'gpg-pubkey' not in self.installOnlyPkgs:
self.installOnlyPkgs.append('gpg-pubkey')
- self.logger.debug("YUMng: Install missing: %s" \
- % self.doInstall)
+ self.logger.debug("YUMng: Install missing: %s" % self.doInstall)
self.logger.debug("YUMng: pkg_checks: %s" % self.pkg_checks)
self.logger.debug("YUMng: pkg_verify: %s" % self.pkg_verify)
- self.logger.debug("YUMng: Upgrade on version fail: %s" \
- % self.doUpgrade)
- self.logger.debug("YUMng: Reinstall on verify fail: %s" \
- % self.doReinst)
- self.logger.debug("YUMng: installOnlyPkgs: %s" \
- % str(self.installOnlyPkgs))
+ self.logger.debug("YUMng: Upgrade on version fail: %s" % self.doUpgrade)
+ self.logger.debug("YUMng: Reinstall on verify fail: %s" % self.doReinst)
+ self.logger.debug("YUMng: installOnlyPkgs: %s" % self.installOnlyPkgs)
self.logger.debug("YUMng: verify_flags: %s" % self.verifyFlags)
def _fixAutoVersion(self, entry):
@@ -473,8 +442,13 @@ class YUMng(Bcfg2.Client.Tools.PkgTool):
stat['verify_fail'] = False
stat['pkg'] = entry
stat['modlist'] = modlist
- verify_flags = inst.get('verify_flags', self.verifyFlags)
- verify_flags = verify_flags.lower().replace(' ', ',').split(',')
+ if inst.get('verify_flags'):
+ # this splits on either space or comma
+ verify_flags = \
+ inst.get('verify_flags').lower().replace(' ',
+ ',').split(',')
+ else:
+ verify_flags = self.verifyFlags
if 'arch' in nevra:
# If arch is specified use it to select the package
@@ -483,6 +457,7 @@ class YUMng(Bcfg2.Client.Tools.PkgTool):
_POs = POs
if len(_POs) == 0:
# Package (name, arch) not installed
+ entry.set('current_exists', 'false')
self.logger.debug(" %s is not installed" % nevraString(nevra))
stat['installed'] = False
package_fail = True
@@ -513,6 +488,12 @@ class YUMng(Bcfg2.Client.Tools.PkgTool):
package_fail = True
stat['version_fail'] = True
# Just chose the first pkg for the error message
+ entry.set('current_version', "%s-%s.%s" % (POs[0].version,
+ POs[0].release,
+ POs[0].arch))
+ entry.set('version', "%s-%s.%s" % (nevra['version'],
+ nevra['release'],
+ nevra['arch']))
self.logger.info(" %s: Wrong version installed. "
"Want %s, but have %s" % (entry.get("name"),
nevraString(nevra),
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index c6cb6e239..e4a0ec220 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -6,6 +6,7 @@ from subprocess import Popen, PIPE
import time
import Bcfg2.Client.XML
+from Bcfg2.Bcfg2Py3k import input
__all__ = [tool.split('.')[0] \
for tool in os.listdir(os.path.dirname(__file__)) \
@@ -136,6 +137,18 @@ class Tool:
"""Default implementation of the information gathering routines."""
pass
+ def missing_attrs(self, entry):
+ required = self.__req__[entry.tag]
+ if isinstance(required, dict):
+ required = ["type"]
+ try:
+ required.extend(self.__req__[entry.tag][entry.get("type")])
+ except KeyError:
+ pass
+
+ return [attr for attr in required
+ if attr not in entry.attrib or not entry.attrib[attr]]
+
def canVerify(self, entry):
"""Test if entry has enough information to be verified."""
if not self.handlesEntry(entry):
@@ -148,8 +161,7 @@ class Tool:
entry.get('failure')))
return False
- missing = [attr for attr in self.__req__[entry.tag] \
- if attr not in entry.attrib]
+ missing = self.missing_attrs(entry)
if missing:
self.logger.error("Incomplete information for entry %s:%s; cannot verify" \
% (entry.tag, entry.get('name')))
@@ -167,6 +179,11 @@ class Tool:
"""Return a list of extra entries."""
return []
+ def primarykey(self, entry):
+ """ return a string that should be unique amongst all entries
+ in the specification """
+ return "%s:%s" % (entry.tag, entry.get("name"))
+
def canInstall(self, entry):
"""Test if entry has enough information to be installed."""
if not self.handlesEntry(entry):
@@ -177,8 +194,7 @@ class Tool:
(entry.tag, entry.get('name')))
return False
- missing = [attr for attr in self.__ireq__[entry.tag] \
- if attr not in entry.attrib or not entry.attrib[attr]]
+ missing = self.missing_attrs(entry)
if missing:
self.logger.error("Incomplete information for entry %s:%s; cannot install" \
% (entry.tag, entry.get('name')))
@@ -305,8 +321,7 @@ class SvcTool(Tool):
return self.cmd.run(self.get_svc_command(service, restart_target))[0]
def check_service(self, service):
- # not supported for this driver
- return 0
+ return self.cmd.run(self.get_svc_command(service, 'status'))[0] == 0
def Remove(self, services):
""" Dummy implementation of service removal method """
@@ -321,13 +336,12 @@ class SvcTool(Tool):
return
for entry in [ent for ent in bundle if self.handlesEntry(ent)]:
- mode = entry.get('mode', 'default')
- if (mode == 'manual' or
- (mode == 'interactive_only' and
+ restart = entry.get("restart", "true")
+ if (restart.lower() == "false" or
+ (restart.lower == "interactive" and
not self.setup['interactive'])):
continue
- # need to handle servicemode = (build|default)
- # need to handle mode = (default|supervised)
+
rc = None
if entry.get('status') == 'on':
if self.setup['servicemode'] == 'build':
@@ -336,11 +350,7 @@ class SvcTool(Tool):
if self.setup['interactive']:
prompt = ('Restart service %s?: (y/N): ' %
entry.get('name'))
- # py3k compatibility
- try:
- ans = raw_input(prompt)
- except NameError:
- ans = input(prompt)
+ ans = input(prompt)
if ans not in ['y', 'Y']:
continue
rc = self.restart_service(entry)
@@ -351,3 +361,19 @@ class SvcTool(Tool):
if rc:
self.logger.error("Failed to manipulate service %s" %
(entry.get('name')))
+
+ def Install(self, entries, states):
+ """Install all entries in sublist."""
+ for entry in entries:
+ if entry.get('install', 'true').lower() == 'false':
+ self.logger.info("Service %s installation is false. Skipping "
+ "installation." % (entry.get('name')))
+ continue
+ try:
+ func = getattr(self, "Install%s" % (entry.tag))
+ states[entry] = func(entry)
+ if states[entry]:
+ self.modified.append(entry)
+ except:
+ self.logger.error("Unexpected failure of install method for entry type %s"
+ % (entry.tag), exc_info=1)
diff --git a/src/lib/Bcfg2/Client/Tools/launchd.py b/src/lib/Bcfg2/Client/Tools/launchd.py
index c022d32ae..6f08559a2 100644
--- a/src/lib/Bcfg2/Client/Tools/launchd.py
+++ b/src/lib/Bcfg2/Client/Tools/launchd.py
@@ -88,11 +88,6 @@ class launchd(Bcfg2.Client.Tools.Tool):
def InstallService(self, entry):
"""Enable or disable launchd item."""
- # don't take any actions for mode='manual'
- if entry.get('mode', 'default') == 'manual':
- self.logger.info("Service %s mode set to manual. Skipping "
- "installation." % (entry.get('name')))
- return False
name = entry.get('name')
if entry.get('status') == 'on':
self.logger.error("Installing service %s" % name)
diff --git a/src/lib/Bcfg2/Component.py b/src/lib/Bcfg2/Component.py
index eb9ea166a..3ee3a14c8 100644
--- a/src/lib/Bcfg2/Component.py
+++ b/src/lib/Bcfg2/Component.py
@@ -6,6 +6,7 @@ import inspect
import logging
import os
import pydoc
+import socket
import sys
import time
import threading
@@ -59,12 +60,14 @@ def run_component(component_cls, listen_all, location, daemon, pidfile_name,
pidfile.close()
component = component_cls(cfile=cfile, **cls_kwargs)
- up = urlparse(location)
- port = tuple(up[1].split(':'))
- port = (port[0], int(port[1]))
+ hostname, port = urlparse(location)[1].split(':')
+ server_address = socket.getaddrinfo(hostname,
+ port,
+ socket.AF_UNSPEC,
+ socket.SOCK_STREAM)[0][4]
try:
server = XMLRPCServer(listen_all,
- port,
+ server_address,
keyfile=keyfile,
certfile=certfile,
register=register,
@@ -213,7 +216,8 @@ class Component (object):
method_func = self._resolve_exposed_method(method)
except NoExposedMethod:
self.logger.error("Unknown method %s" % (method))
- raise xmlrpclib.Fault(7, "Unknown method %s" % method)
+ raise xmlrpclib.Fault(xmlrpclib.METHOD_NOT_FOUND,
+ "Unknown method %s" % method)
except Exception:
e = sys.exc_info()[1]
if getattr(e, "log", True):
diff --git a/src/lib/Bcfg2/Encryption.py b/src/lib/Bcfg2/Encryption.py
new file mode 100755
index 000000000..62b22d7de
--- /dev/null
+++ b/src/lib/Bcfg2/Encryption.py
@@ -0,0 +1,75 @@
+#!/usr/bin/python -Ott
+
+import os
+import base64
+from M2Crypto import Rand
+from M2Crypto.EVP import Cipher, EVPError
+from Bcfg2.Bcfg2Py3k import StringIO
+
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+
+ENCRYPT = 1
+DECRYPT = 0
+ALGORITHM = "aes_256_cbc"
+IV = '\0' * 16
+
+Rand.rand_seed(os.urandom(1024))
+
+def _cipher_filter(cipher, instr):
+ inbuf = StringIO(instr)
+ outbuf = StringIO()
+ while 1:
+ buf = inbuf.read()
+ if not buf:
+ break
+ outbuf.write(cipher.update(buf))
+ outbuf.write(cipher.final())
+ rv = outbuf.getvalue()
+ inbuf.close()
+ outbuf.close()
+ return rv
+
+def str_encrypt(plaintext, key, iv=IV, algorithm=ALGORITHM, salt=None):
+ """ encrypt a string """
+ cipher = Cipher(alg=algorithm, key=key, iv=iv, op=ENCRYPT, salt=salt)
+ return _cipher_filter(cipher, plaintext)
+
+def str_decrypt(crypted, key, iv=IV, algorithm=ALGORITHM):
+ """ decrypt a string """
+ cipher = Cipher(alg=algorithm, key=key, iv=iv, op=DECRYPT)
+ return _cipher_filter(cipher, crypted)
+
+def ssl_decrypt(data, passwd, algorithm=ALGORITHM):
+ """ decrypt openssl-encrypted data """
+ # base64-decode the data if necessary
+ try:
+ data = base64.b64decode(data)
+ except TypeError:
+ # already decoded
+ pass
+
+ salt = data[8:16]
+ hashes = [md5(passwd + salt).digest()]
+ for i in range(1,3):
+ hashes.append(md5(hashes[i-1] + passwd + salt).digest())
+ key = hashes[0] + hashes[1]
+ iv = hashes[2]
+
+ return str_decrypt(data[16:], key=key, iv=iv)
+
+def ssl_encrypt(plaintext, passwd, algorithm=ALGORITHM, salt=None):
+ """ encrypt data in a format that is openssl compatible """
+ if salt is None:
+ salt = Rand.rand_bytes(8)
+
+ hashes = [md5(passwd + salt).digest()]
+ for i in range(1,3):
+ hashes.append(md5(hashes[i-1] + passwd + salt).digest())
+ key = hashes[0] + hashes[1]
+ iv = hashes[2]
+
+ crypted = str_encrypt(plaintext, key=key, salt=salt, iv=iv)
+ return base64.b64encode("Salted__" + salt + crypted) + "\n"
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py
index dfb062341..1fdfa4274 100644
--- a/src/lib/Bcfg2/Options.py
+++ b/src/lib/Bcfg2/Options.py
@@ -1,25 +1,22 @@
"""Option parsing library for utilities."""
-import getopt
import re
import os
import sys
+import copy
import shlex
+import getopt
import Bcfg2.Client.Tools
# Compatibility imports
from Bcfg2.Bcfg2Py3k import ConfigParser
-def bool_cook(x):
- if x:
- return True
- else:
- return False
class OptionFailure(Exception):
pass
-DEFAULT_CONFIG_LOCATION = '/etc/bcfg2.conf' #/etc/bcfg2.conf
-DEFAULT_INSTALL_PREFIX = '/usr' #/usr
+DEFAULT_CONFIG_LOCATION = '/etc/bcfg2.conf'
+DEFAULT_INSTALL_PREFIX = '/usr'
+
class DefaultConfigParser(ConfigParser.ConfigParser):
def get(self, section, option, **kwargs):
@@ -79,26 +76,23 @@ class Option(object):
self.env = env
self.cf = cf
self.boolean = False
- if not odesc and not cook:
+ if not odesc and not cook and isinstance(self.default, bool):
self.boolean = True
self.cook = cook
def buildHelpMessage(self):
- msg = ''
- if self.cmd:
- if not self.long:
- msg = self.cmd.ljust(3)
- else:
- msg = self.cmd
- if self.odesc:
- if self.long:
- msg = "%-28s" % ("%s=%s" % (self.cmd, self.odesc))
- else:
- msg += '%-25s' % (self.odesc)
+ vals = []
+ if not self.cmd:
+ return ''
+ if self.odesc:
+ if self.long:
+ vals.append("%s=%s" % (self.cmd, self.odesc))
else:
- msg += '%-25s' % ('')
- msg += "%s\n" % self.desc
- return msg
+ vals.append("%s %s" % (self.cmd, self.odesc))
+ else:
+ vals.append(self.cmd)
+ vals.append(self.desc)
+ return " %-28s %s\n" % tuple(vals)
def buildGetopt(self):
gstr = ''
@@ -112,7 +106,7 @@ class Option(object):
def buildLongGetopt(self):
if self.odesc:
- return self.cmd[2:]+'='
+ return self.cmd[2:] + '='
else:
return self.cmd[2:]
@@ -143,6 +137,7 @@ class Option(object):
# Default value not cooked
self.value = self.default
+
class OptionSet(dict):
def __init__(self, *args, **kwargs):
dict.__init__(self, *args)
@@ -170,14 +165,15 @@ class OptionSet(dict):
hlist = [] # list of _non-empty_ help messages
for opt in list(self.values()):
hm = opt.buildHelpMessage()
- if hm != '':
+ if hm:
hlist.append(hm)
- return ' '.join(hlist)
+ return ''.join(hlist)
def helpExit(self, msg='', code=1):
if msg:
print(msg)
- print("Usage:\n %s" % self.buildHelpMessage())
+ print("Usage:")
+ print(self.buildHelpMessage())
raise SystemExit(code)
def parse(self, argv, do_getopt=True):
@@ -204,16 +200,19 @@ class OptionSet(dict):
val = option.value
self[key] = val
+
def list_split(c_string):
if c_string:
return re.split("\s*,\s*", c_string)
return []
+
def colon_split(c_string):
if c_string:
return c_string.split(':')
return []
+
def get_bool(s):
# these values copied from ConfigParser.RawConfigParser.getboolean
# with the addition of True and False
@@ -226,195 +225,664 @@ def get_bool(s):
else:
raise ValueError
+"""
+Options:
+
+ Accepts keyword argument list with the following values:
+
+ default: default value for the option
+ cmd: command line switch
+ odesc: option description
+ cf: tuple containing section/option
+ cook: method for parsing option
+ long_arg: (True|False) specifies whether cmd is a long argument
+"""
# General options
-CFILE = Option('Specify configuration file', DEFAULT_CONFIG_LOCATION, cmd='-C',
- odesc='<conffile>')
-LOCKFILE = Option('Specify lockfile',
- "/var/lock/bcfg2.run",
- cf=('components', 'lockfile'),
- odesc='<Path to lockfile>')
-HELP = Option('Print this usage message', False, cmd='-h')
-DEBUG = Option("Enable debugging output", False, cmd='-d')
-VERBOSE = Option("Enable verbose output", False, cmd='-v')
-DAEMON = Option("Daemonize process, storing pid", False,
- cmd='-D', odesc="<pidfile>")
-INSTALL_PREFIX = Option('Installation location', cf=('server', 'prefix'),
- default=DEFAULT_INSTALL_PREFIX, odesc='</path>')
-SENDMAIL_PATH = Option('Path to sendmail', cf=('reports', 'sendmailpath'),
- default='/usr/lib/sendmail')
-INTERACTIVE = Option('Run interactively, prompting the user for each change',
- default=False,
- cmd='-I', )
-ENCODING = Option('Encoding of cfg files',
- default='UTF-8',
- cmd='-E',
- odesc='<encoding>',
- cf=('components', 'encoding'))
-PARANOID_PATH = Option('Specify path for paranoid file backups',
- default='/var/cache/bcfg2', cf=('paranoid', 'path'),
- odesc='<paranoid backup path>')
-PARANOID_MAX_COPIES = Option('Specify the number of paranoid copies you want',
- default=1, cf=('paranoid', 'max_copies'),
- odesc='<max paranoid copies>')
-OMIT_LOCK_CHECK = Option('Omit lock check', default=False, cmd='-O')
-CORE_PROFILE = Option('profile',
- default=False, cmd='-p', )
-FILES_ON_STDIN = Option('Operate on a list of files supplied on stdin',
- cmd='--stdin', default=False, long_arg=True)
-SCHEMA_PATH = Option('Path to XML Schema files', cmd='--schema',
- odesc='<schema path>',
- default="%s/share/bcfg2/schemas" % DEFAULT_INSTALL_PREFIX,
- long_arg=True)
-REQUIRE_SCHEMA = Option("Require property files to have matching schema files",
- cmd="--require-schema", default=False, long_arg=True)
-
-# Metadata options
-MDATA_OWNER = Option('Default Path owner',
- default='root', cf=('mdata', 'owner'),
- odesc='owner permissions')
-MDATA_GROUP = Option('Default Path group',
- default='root', cf=('mdata', 'group'),
- odesc='group permissions')
-MDATA_IMPORTANT = Option('Default Path priority (importance)',
- default='False', cf=('mdata', 'important'),
- odesc='Important entries are installed first')
-MDATA_PERMS = Option('Default Path permissions',
- '644', cf=('mdata', 'perms'),
- odesc='octal permissions')
-MDATA_PARANOID = Option('Default Path paranoid setting',
- 'true', cf=('mdata', 'paranoid'),
- odesc='Path paranoid setting')
-MDATA_SENSITIVE = Option('Default Path sensitive setting',
- 'false', cf=('mdata', 'sensitive'),
- odesc='Path sensitive setting')
+CFILE = \
+ Option('Specify configuration file',
+ default=DEFAULT_CONFIG_LOCATION,
+ cmd='-C',
+ odesc='<conffile>')
+LOCKFILE = \
+ Option('Specify lockfile',
+ default='/var/lock/bcfg2.run',
+ odesc='<Path to lockfile>',
+ cf=('components', 'lockfile'))
+HELP = \
+ Option('Print this usage message',
+ default=False,
+ cmd='-h')
+DEBUG = \
+ Option("Enable debugging output",
+ default=False,
+ cmd='-d')
+VERBOSE = \
+ Option("Enable verbose output",
+ default=False,
+ cmd='-v')
+DAEMON = \
+ Option("Daemonize process, storing pid",
+ default=None,
+ cmd='-D',
+ odesc='<pidfile>')
+INSTALL_PREFIX = \
+ Option('Installation location',
+ default=DEFAULT_INSTALL_PREFIX,
+ odesc='</path>',
+ cf=('server', 'prefix'))
+SENDMAIL_PATH = \
+ Option('Path to sendmail',
+ default='/usr/lib/sendmail',
+ cf=('reports', 'sendmailpath'))
+INTERACTIVE = \
+ Option('Run interactively, prompting the user for each change',
+ default=False,
+ cmd='-I', )
+ENCODING = \
+ Option('Encoding of cfg files',
+ default='UTF-8',
+ cmd='-E',
+ odesc='<encoding>',
+ cf=('components', 'encoding'))
+PARANOID_PATH = \
+ Option('Specify path for paranoid file backups',
+ default='/var/cache/bcfg2',
+ odesc='<paranoid backup path>',
+ cf=('paranoid', 'path'))
+PARANOID_MAX_COPIES = \
+ Option('Specify the number of paranoid copies you want',
+ default=1,
+ odesc='<max paranoid copies>',
+ cf=('paranoid', 'max_copies'))
+OMIT_LOCK_CHECK = \
+ Option('Omit lock check',
+ default=False,
+ cmd='-O')
+CORE_PROFILE = \
+ Option('profile',
+ default=False,
+ cmd='-p')
+SCHEMA_PATH = \
+ Option('Path to XML Schema files',
+ default='%s/share/bcfg2/schemas' % DEFAULT_INSTALL_PREFIX,
+ cmd='--schema',
+ odesc='<schema path>',
+ cf=('lint', 'schema'),
+ long_arg=True)
+INTERPRETER = \
+ Option("Python interpreter to use",
+ default='best',
+ cmd="--interpreter",
+ odesc='<python|bpython|ipython|best>',
+ cf=('bcfg2-info', 'interpreter'),
+ long_arg=True)
+
+# Metadata options (mdata section)
+MDATA_OWNER = \
+ Option('Default Path owner',
+ default='root',
+ odesc='owner permissions',
+ cf=('mdata', 'owner'))
+MDATA_GROUP = \
+ Option('Default Path group',
+ default='root',
+ odesc='group permissions',
+ cf=('mdata', 'group'))
+MDATA_IMPORTANT = \
+ Option('Default Path priority (importance)',
+ default='False',
+ odesc='Important entries are installed first',
+ cf=('mdata', 'important'))
+MDATA_PERMS = \
+ Option('Default Path permissions',
+ default='644',
+ odesc='octal permissions',
+ cf=('mdata', 'perms'))
+MDATA_SECONTEXT = \
+ Option('Default SELinux context',
+ default='__default__',
+ odesc='SELinux context',
+ cf=('mdata', 'secontext'))
+MDATA_PARANOID = \
+ Option('Default Path paranoid setting',
+ default='true',
+ odesc='Path paranoid setting',
+ cf=('mdata', 'paranoid'))
+MDATA_SENSITIVE = \
+ Option('Default Path sensitive setting',
+ default='false',
+ odesc='Path sensitive setting',
+ cf=('mdata', 'sensitive'))
# Server options
-SERVER_REPOSITORY = Option('Server repository path', '/var/lib/bcfg2',
- cf=('server', 'repository'), cmd='-Q',
- odesc='<repository path>')
-SERVER_PLUGINS = Option('Server plugin list', cf=('server', 'plugins'),
- # default server plugins
- default=[
- 'Bundler',
- 'Cfg',
- 'Metadata',
- 'Pkgmgr',
- 'Rules',
- 'SSHbase',
- ],
- cook=list_split)
-SERVER_MCONNECT = Option('Server Metadata Connector list', cook=list_split,
- cf=('server', 'connectors'), default=['Probes'], )
-SERVER_FILEMONITOR = Option('Server file monitor', cf=('server', 'filemonitor'),
- default='default', odesc='File monitoring driver')
-SERVER_LISTEN_ALL = Option('Listen on all interfaces',
- cf=('server', 'listen_all'),
- cmd='--listen-all',
- default=False,
- long_arg=True,
- cook=get_bool,
- odesc='True|False')
-SERVER_LOCATION = Option('Server Location', cf=('components', 'bcfg2'),
- default='https://localhost:6789', cmd='-S',
- odesc='https://server:port')
-SERVER_STATIC = Option('Server runs on static port', cf=('components', 'bcfg2'),
- default=False, cook=bool_cook)
-SERVER_KEY = Option('Path to SSL key', cf=('communication', 'key'),
- default=False, cmd='--ssl-key', odesc='<ssl key>',
- long_arg=True)
-SERVER_CERT = Option('Path to SSL certificate', default='/etc/bcfg2.key',
- cf=('communication', 'certificate'), odesc='<ssl cert>')
-SERVER_CA = Option('Path to SSL CA Cert', default=None,
- cf=('communication', 'ca'), odesc='<ca cert>')
-SERVER_PASSWORD = Option('Communication Password', cmd='-x', odesc='<password>',
- cf=('communication', 'password'), default=False)
-SERVER_PROTOCOL = Option('Server Protocol', cf=('communication', 'procotol'),
- default='xmlrpc/ssl')
+SERVER_REPOSITORY = \
+ Option('Server repository path',
+ default='/var/lib/bcfg2',
+ cmd='-Q',
+ odesc='<repository path>',
+ cf=('server', 'repository'))
+SERVER_PLUGINS = \
+ Option('Server plugin list',
+ # default server plugins
+ default=['Bundler', 'Cfg', 'Metadata', 'Pkgmgr', 'Rules', 'SSHbase'],
+ cf=('server', 'plugins'),
+ cook=list_split)
+SERVER_MCONNECT = \
+ Option('Server Metadata Connector list',
+ default=['Probes'],
+ cf=('server', 'connectors'),
+ cook=list_split)
+SERVER_FILEMONITOR = \
+ Option('Server file monitor',
+ default='default',
+ odesc='File monitoring driver',
+ cf=('server', 'filemonitor'))
+SERVER_FAM_IGNORE = \
+ Option('File globs to ignore',
+ default=['*~', '*#', '.#*', '*.swp', '.*.swx', 'SCCS', '.svn',
+ '4913', '.gitignore',],
+ cf=('server', 'ignore_files'),
+ cook=list_split)
+SERVER_LISTEN_ALL = \
+ Option('Listen on all interfaces',
+ default=False,
+ cmd='--listen-all',
+ cf=('server', 'listen_all'),
+ cook=get_bool,
+ long_arg=True)
+SERVER_LOCATION = \
+ Option('Server Location',
+ default='https://localhost:6789',
+ cmd='-S',
+ odesc='https://server:port',
+ cf=('components', 'bcfg2'))
+SERVER_STATIC = \
+ Option('Server runs on static port',
+ default=False,
+ cf=('components', 'bcfg2'))
+SERVER_KEY = \
+ Option('Path to SSL key',
+ default=None,
+ cmd='--ssl-key',
+ odesc='<ssl key>',
+ cf=('communication', 'key'),
+ long_arg=True)
+SERVER_CERT = \
+ Option('Path to SSL certificate',
+ default='/etc/bcfg2.key',
+ odesc='<ssl cert>',
+ cf=('communication', 'certificate'))
+SERVER_CA = \
+ Option('Path to SSL CA Cert',
+ default=None,
+ odesc='<ca cert>',
+ cf=('communication', 'ca'))
+SERVER_PASSWORD = \
+ Option('Communication Password',
+ default=None,
+ cmd='-x',
+ odesc='<password>',
+ cf=('communication', 'password'))
+SERVER_PROTOCOL = \
+ Option('Server Protocol',
+ default='xmlrpc/ssl',
+ cf=('communication', 'procotol'))
+
# Client options
-CLIENT_KEY = Option('Path to SSL key', cf=('communication', 'key'),
- default=None, cmd="--ssl-key", odesc='<ssl key>',
- long_arg=True)
-CLIENT_CERT = Option('Path to SSL certificate', default=None, cmd="--ssl-cert",
- cf=('communication', 'certificate'), odesc='<ssl cert>',
- long_arg=True)
-CLIENT_CA = Option('Path to SSL CA Cert', default=None, cmd="--ca-cert",
- cf=('communication', 'ca'), odesc='<ca cert>',
- long_arg=True)
-CLIENT_SCNS = Option('List of server commonNames', default=None, cmd="--ssl-cns",
- cf=('communication', 'serverCommonNames'),
- odesc='<commonName1:commonName2>', cook=list_split,
- long_arg=True)
-CLIENT_PROFILE = Option('Assert the given profile for the host',
- default=False, cmd='-p', odesc="<profile>")
-CLIENT_RETRIES = Option('The number of times to retry network communication',
- default='3', cmd='-R', cf=('communication', 'retries'),
- odesc="<retry count>")
-CLIENT_DRYRUN = Option('Do not actually change the system',
- default=False, cmd='-n', )
-CLIENT_EXTRA_DISPLAY = Option('enable extra entry output',
- default=False, cmd='-e', )
-CLIENT_PARANOID = Option('Make automatic backups of config files',
- default=False,
- cmd='-P',
- cook=get_bool,
- cf=('client', 'paranoid'))
-CLIENT_DRIVERS = Option('Specify tool driver set', cmd='-D',
- cf=('client', 'drivers'),
- odesc="<driver1,driver2>", cook=list_split,
- default=Bcfg2.Client.Tools.default)
-CLIENT_CACHE = Option('Store the configuration in a file',
- default=False, cmd='-c', odesc="<cache path>")
-CLIENT_REMOVE = Option('Force removal of additional configuration items',
- default=False, cmd='-r', odesc="<entry type|all>")
-CLIENT_BUNDLE = Option('Only configure the given bundle(s)', default=[],
- cmd='-b', odesc='<bundle:bundle>', cook=colon_split)
-CLIENT_BUNDLEQUICK = Option('only verify/configure the given bundle(s)', default=False,
- cmd='-Q')
-CLIENT_INDEP = Option('Only configure independent entries, ignore bundles', default=False,
- cmd='-z')
-CLIENT_KEVLAR = Option('Run in kevlar (bulletproof) mode', default=False,
- cmd='-k', )
-CLIENT_DLIST = Option('Run client in server decision list mode', default='none',
- cf=('client', 'decision'),
- cmd='-l', odesc='<whitelist|blacklist|none>')
-CLIENT_FILE = Option('Configure from a file rather than querying the server',
- default=False, cmd='-f', odesc='<specification path>')
-CLIENT_QUICK = Option('Disable some checksum verification', default=False,
- cmd='-q', )
-CLIENT_USER = Option('The user to provide for authentication', default='root',
- cmd='-u', cf=('communication', 'user'), odesc='<user>')
-CLIENT_SERVICE_MODE = Option('Set client service mode', default='default',
- cmd='-s', odesc='<default|disabled|build>')
-CLIENT_TIMEOUT = Option('Set the client XML-RPC timeout', default=90,
- cmd='-t', cf=('communication', 'timeout'),
- odesc='<timeout>')
-
-# bcfg2-test options
-TEST_NOSEOPTS = Option('Options to pass to nosetests', default=[],
- cmd='--nose-options', cf=('bcfg2_test', 'nose_options'),
- odesc='<opts>', long_arg=True, cook=shlex.split)
-TEST_IGNORE = Option('Ignore these entries if they fail to build.', default=[],
- cmd='--ignore',
- cf=('bcfg2_test', 'ignore_entries'), long_arg=True,
- odesc='<Type>:<name>,<Type>:<name>', cook=list_split)
-
-# APT client tool options
-CLIENT_APT_TOOLS_INSTALL_PATH = Option('Apt tools install path',
- cf=('APT', 'install_path'),
- default='/usr')
-CLIENT_APT_TOOLS_VAR_PATH = Option('Apt tools var path',
- cf=('APT', 'var_path'), default='/var')
-CLIENT_SYSTEM_ETC_PATH = Option('System etc path', cf=('APT', 'etc_path'),
- default='/etc')
+CLIENT_KEY = \
+ Option('Path to SSL key',
+ default=None,
+ cmd='--ssl-key',
+ odesc='<ssl key>',
+ cf=('communication', 'key'),
+ long_arg=True)
+CLIENT_CERT = \
+ Option('Path to SSL certificate',
+ default=None,
+ cmd='--ssl-cert',
+ odesc='<ssl cert>',
+ cf=('communication', 'certificate'),
+ long_arg=True)
+CLIENT_CA = \
+ Option('Path to SSL CA Cert',
+ default=None,
+ cmd='--ca-cert',
+ odesc='<ca cert>',
+ cf=('communication', 'ca'),
+ long_arg=True)
+CLIENT_SCNS = \
+ Option('List of server commonNames',
+ default=None,
+ cmd='--ssl-cns',
+ odesc='<CN1:CN2>',
+ cf=('communication', 'serverCommonNames'),
+ cook=list_split,
+ long_arg=True)
+CLIENT_PROFILE = \
+ Option('Assert the given profile for the host',
+ default=None,
+ cmd='-p',
+ odesc='<profile>')
+CLIENT_RETRIES = \
+ Option('The number of times to retry network communication',
+ default='3',
+ cmd='-R',
+ odesc='<retry count>',
+ cf=('communication', 'retries'))
+CLIENT_DRYRUN = \
+ Option('Do not actually change the system',
+ default=False,
+ cmd='-n')
+CLIENT_EXTRA_DISPLAY = \
+ Option('enable extra entry output',
+ default=False,
+ cmd='-e')
+CLIENT_PARANOID = \
+ Option('Make automatic backups of config files',
+ default=False,
+ cmd='-P',
+ cf=('client', 'paranoid'),
+ cook=get_bool)
+CLIENT_DRIVERS = \
+ Option('Specify tool driver set',
+ default=Bcfg2.Client.Tools.default,
+ cmd='-D',
+ odesc='<driver1,driver2>',
+ cf=('client', 'drivers'),
+ cook=list_split)
+CLIENT_CACHE = \
+ Option('Store the configuration in a file',
+ default=None,
+ cmd='-c',
+ odesc='<cache path>')
+CLIENT_REMOVE = \
+ Option('Force removal of additional configuration items',
+ default=None,
+ cmd='-r',
+ odesc='<entry type|all>')
+CLIENT_BUNDLE = \
+ Option('Only configure the given bundle(s)',
+ default=[],
+ cmd='-b',
+ odesc='<bundle:bundle>',
+ cook=colon_split)
+CLIENT_SKIPBUNDLE = \
+ Option('Configure everything except the given bundle(s)',
+ default=[],
+ cmd='-B',
+ odesc='<bundle:bundle>',
+ cook=colon_split)
+CLIENT_BUNDLEQUICK = \
+ Option('Only verify/configure the given bundle(s)',
+ default=False,
+ cmd='-Q')
+CLIENT_INDEP = \
+ Option('Only configure independent entries, ignore bundles',
+ default=False,
+ cmd='-z')
+CLIENT_SKIPINDEP = \
+ Option('Do not configure independent entries',
+ default=False,
+ cmd='-Z')
+CLIENT_KEVLAR = \
+ Option('Run in kevlar (bulletproof) mode',
+ default=False,
+ cmd='-k', )
+CLIENT_FILE = \
+ Option('Configure from a file rather than querying the server',
+ default=None,
+ cmd='-f',
+ odesc='<specification path>')
+CLIENT_QUICK = \
+ Option('Disable some checksum verification',
+ default=False,
+ cmd='-q')
+CLIENT_USER = \
+ Option('The user to provide for authentication',
+ default='root',
+ cmd='-u',
+ odesc='<user>',
+ cf=('communication', 'user'))
+CLIENT_SERVICE_MODE = \
+ Option('Set client service mode',
+ default='default',
+ cmd='-s',
+ odesc='<default|disabled|build>')
+CLIENT_TIMEOUT = \
+ Option('Set the client XML-RPC timeout',
+ default=90,
+ cmd='-t',
+ odesc='<timeout>',
+ cf=('communication', 'timeout'))
+CLIENT_DLIST = \
+ Option('Run client in server decision list mode',
+ default='none',
+ cmd='-l',
+ odesc='<whitelist|blacklist|none>',
+ cf=('client', 'decision'))
+CLIENT_DECISION_LIST = \
+ Option('Decision List',
+ default=False,
+ cmd='--decision-list',
+ odesc='<file>',
+ long_arg=True)
+
+# bcfg2-test and bcfg2-lint options
+TEST_NOSEOPTS = \
+ Option('Options to pass to nosetests',
+ default=[],
+ cmd='--nose-options',
+ odesc='<opts>',
+ cf=('bcfg2_test', 'nose_options'),
+ cook=shlex.split,
+ long_arg=True)
+TEST_IGNORE = \
+ Option('Ignore these entries if they fail to build.',
+ default=[],
+ cmd='--ignore',
+ odesc='<Type>:<name>,<Type>:<name>',
+ cf=('bcfg2_test', 'ignore_entries'),
+ cook=list_split,
+ long_arg=True)
+LINT_CONFIG = \
+ Option('Specify bcfg2-lint configuration file',
+ default='/etc/bcfg2-lint.conf',
+ cmd='--lint-config',
+ odesc='<conffile>',
+ long_arg=True)
+LINT_SHOW_ERRORS = \
+ Option('Show error handling',
+ default=False,
+ cmd='--list-errors',
+ long_arg=True)
+LINT_FILES_ON_STDIN = \
+ Option('Operate on a list of files supplied on stdin',
+ default=False,
+ cmd='--stdin',
+ long_arg=True)
+
+# individual client tool options
+CLIENT_APT_TOOLS_INSTALL_PATH = \
+ Option('Apt tools install path',
+ default='/usr',
+ cf=('APT', 'install_path'))
+CLIENT_APT_TOOLS_VAR_PATH = \
+ Option('Apt tools var path',
+ default='/var',
+ cf=('APT', 'var_path'))
+CLIENT_SYSTEM_ETC_PATH = \
+ Option('System etc path',
+ default='/etc',
+ cf=('APT', 'etc_path'))
+CLIENT_PORTAGE_BINPKGONLY = \
+ Option('Portage binary packages only',
+ default=False,
+ cf=('Portage', 'binpkgonly'),
+ cook=get_bool)
+CLIENT_RPMNG_INSTALLONLY = \
+ Option('RPMng install-only packages',
+ default=['kernel', 'kernel-bigmem', 'kernel-enterprise',
+ 'kernel-smp', 'kernel-modules', 'kernel-debug',
+ 'kernel-unsupported', 'kernel-devel', 'kernel-source',
+ 'kernel-default', 'kernel-largesmp-devel',
+ 'kernel-largesmp', 'kernel-xen', 'gpg-pubkey'],
+ cf=('RPMng', 'installonlypackages'),
+ cook=list_split)
+CLIENT_RPMNG_PKG_CHECKS = \
+ Option("Perform RPMng package checks",
+ default=True,
+ cf=('RPMng', 'pkg_checks'),
+ cook=get_bool)
+CLIENT_RPMNG_PKG_VERIFY = \
+ Option("Perform RPMng package verify",
+ default=True,
+ cf=('RPMng', 'pkg_verify'),
+ cook=get_bool)
+CLIENT_RPMNG_INSTALLED_ACTION = \
+ Option("RPMng installed action",
+ default="install",
+ cf=('RPMng', 'installed_action'))
+CLIENT_RPMNG_ERASE_FLAGS = \
+ Option("RPMng erase flags",
+ default=["allmatches"],
+ cf=('RPMng', 'erase_flags'),
+ cook=list_split)
+CLIENT_RPMNG_VERSION_FAIL_ACTION = \
+ Option("RPMng version fail action",
+ default="upgrade",
+ cf=('RPMng', 'version_fail_action'))
+CLIENT_RPMNG_VERIFY_FAIL_ACTION = \
+ Option("RPMng verify fail action",
+ default="reinstall",
+ cf=('RPMng', 'verify_fail_action'))
+CLIENT_RPMNG_VERIFY_FLAGS = \
+ Option("RPMng verify flags",
+ default=[],
+ cf=('RPMng', 'verify_flags'),
+ cook=list_split)
+CLIENT_YUM24_INSTALLONLY = \
+ Option('RPMng install-only packages',
+ default=['kernel', 'kernel-bigmem', 'kernel-enterprise',
+ 'kernel-smp', 'kernel-modules', 'kernel-debug',
+ 'kernel-unsupported', 'kernel-devel', 'kernel-source',
+ 'kernel-default', 'kernel-largesmp-devel',
+ 'kernel-largesmp', 'kernel-xen', 'gpg-pubkey'],
+ cf=('RPMng', 'installonlypackages'),
+ cook=list_split)
+CLIENT_YUM24_PKG_CHECKS = \
+ Option("Perform YUM24 package checks",
+ default=True,
+ cf=('YUM24', 'pkg_checks'),
+ cook=get_bool)
+CLIENT_YUM24_PKG_VERIFY = \
+ Option("Perform YUM24 package verify",
+ default=True,
+ cf=('YUM24', 'pkg_verify'),
+ cook=get_bool)
+CLIENT_YUM24_INSTALLED_ACTION = \
+ Option("YUM24 installed action",
+ default="install",
+ cf=('YUM24', 'installed_action'))
+CLIENT_YUM24_ERASE_FLAGS = \
+ Option("YUM24 erase flags",
+ default=["allmatches"],
+ cf=('YUM24', 'erase_flags'),
+ cook=list_split)
+CLIENT_YUM24_VERSION_FAIL_ACTION = \
+ Option("YUM24 version fail action",
+ cf=('YUM24', 'version_fail_action'),
+ default="upgrade")
+CLIENT_YUM24_VERIFY_FAIL_ACTION = \
+ Option("YUM24 verify fail action",
+ default="reinstall",
+ cf=('YUM24', 'verify_fail_action'))
+CLIENT_YUM24_VERIFY_FLAGS = \
+ Option("YUM24 verify flags",
+ default=[],
+ cf=('YUM24', 'verify_flags'),
+ cook=list_split)
+CLIENT_YUM24_AUTODEP = \
+ Option("YUM24 autodependency processing",
+ default=True,
+ cf=('YUM24', 'autodep'),
+ cook=get_bool)
+CLIENT_YUMNG_PKG_CHECKS = \
+ Option("Perform YUMng package checks",
+ default=True,
+ cf=('YUMng', 'pkg_checks'),
+ cook=get_bool)
+CLIENT_YUMNG_PKG_VERIFY = \
+ Option("Perform YUMng package verify",
+ default=True,
+ cf=('YUMng', 'pkg_verify'),
+ cook=get_bool)
+CLIENT_YUMNG_INSTALLED_ACTION = \
+ Option("YUMng installed action",
+ default="install",
+ cf=('YUMng', 'installed_action'))
+CLIENT_YUMNG_VERSION_FAIL_ACTION = \
+ Option("YUMng version fail action",
+ default="upgrade",
+ cf=('YUMng', 'version_fail_action'))
+CLIENT_YUMNG_VERIFY_FAIL_ACTION = \
+ Option("YUMng verify fail action",
+ default="reinstall",
+ cf=('YUMng', 'verify_fail_action'))
+CLIENT_YUMNG_VERIFY_FLAGS = \
+ Option("YUMng verify flags",
+ default=[],
+ cf=('YUMng', 'verify_flags'),
+ cook=list_split)
# Logging options
-LOGGING_FILE_PATH = Option('Set path of file log', default=None,
- cmd='-o', odesc='<path>', cf=('logging', 'path'))
+LOGGING_FILE_PATH = \
+ Option('Set path of file log',
+ default=None,
+ cmd='-o',
+ odesc='<path>',
+ cf=('logging', 'path'))
# Plugin-specific options
-CFG_VALIDATION = Option('Run validation on Cfg files', default=True,
- cf=('cfg', 'validation'), cmd='--cfg-validation',
- long_arg=True, cook=get_bool)
+CFG_VALIDATION = \
+ Option('Run validation on Cfg files',
+ default=True,
+ cmd='--cfg-validation',
+ cf=('cfg', 'validation'),
+ long_arg=True,
+ cook=get_bool)
+
+# bcfg2-crypt options
+ENCRYPT = \
+ Option('Encrypt the specified file',
+ default=False,
+ cmd='--encrypt',
+ long_arg=True)
+DECRYPT = \
+ Option('Decrypt the specified file',
+ default=False,
+ cmd='--decrypt',
+ long_arg=True)
+CRYPT_PASSPHRASE = \
+ Option('Encryption passphrase (name or passphrase)',
+ default=None,
+ cmd='-p',
+ odesc='<passphrase>')
+CRYPT_XPATH = \
+ Option('XPath expression to select elements to encrypt',
+ default=None,
+ cmd='--xpath',
+ odesc='<xpath>',
+ long_arg=True)
+CRYPT_PROPERTIES = \
+ Option('Encrypt the specified file as a Properties file',
+ default=False,
+ cmd="--properties",
+ long_arg=True)
+CRYPT_CFG = \
+ Option('Encrypt the specified file as a Cfg file',
+ default=False,
+ cmd="--cfg",
+ long_arg=True)
+CRYPT_REMOVE = \
+ Option('Remove the plaintext file after encrypting',
+ default=False,
+ cmd="--remove",
+ long_arg=True)
+
+# Option groups
+CLI_COMMON_OPTIONS = dict(configfile=CFILE,
+ debug=DEBUG,
+ help=HELP,
+ verbose=VERBOSE,
+ encoding=ENCODING,
+ logging=LOGGING_FILE_PATH)
+
+DAEMON_COMMON_OPTIONS = dict(daemon=DAEMON,
+ listen_all=SERVER_LISTEN_ALL)
+
+SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY,
+ plugins=SERVER_PLUGINS,
+ password=SERVER_PASSWORD,
+ filemonitor=SERVER_FILEMONITOR,
+ ignore=SERVER_FAM_IGNORE,
+ location=SERVER_LOCATION,
+ static=SERVER_STATIC,
+ key=SERVER_KEY,
+ cert=SERVER_CERT,
+ ca=SERVER_CA,
+ protocol=SERVER_PROTOCOL)
+
+CRYPT_OPTIONS = dict(encrypt=ENCRYPT,
+ decrypt=DECRYPT,
+ passphrase=CRYPT_PASSPHRASE,
+ xpath=CRYPT_XPATH,
+ properties=CRYPT_PROPERTIES,
+ cfg=CRYPT_CFG,
+ remove=CRYPT_REMOVE)
+
+DRIVER_OPTIONS = \
+ dict(apt_install_path=CLIENT_APT_TOOLS_INSTALL_PATH,
+ apt_var_path=CLIENT_APT_TOOLS_VAR_PATH,
+ apt_etc_path=CLIENT_SYSTEM_ETC_PATH,
+ portage_binpkgonly=CLIENT_PORTAGE_BINPKGONLY,
+ rpmng_installonly=CLIENT_RPMNG_INSTALLONLY,
+ rpmng_pkg_checks=CLIENT_RPMNG_PKG_CHECKS,
+ rpmng_pkg_verify=CLIENT_RPMNG_PKG_VERIFY,
+ rpmng_installed_action=CLIENT_RPMNG_INSTALLED_ACTION,
+ rpmng_erase_flags=CLIENT_RPMNG_ERASE_FLAGS,
+ rpmng_version_fail_action=CLIENT_RPMNG_VERSION_FAIL_ACTION,
+ rpmng_verify_fail_action=CLIENT_RPMNG_VERIFY_FAIL_ACTION,
+ rpmng_verify_flags=CLIENT_RPMNG_VERIFY_FLAGS,
+ yum24_installonly=CLIENT_YUM24_INSTALLONLY,
+ yum24_pkg_checks=CLIENT_YUM24_PKG_CHECKS,
+ yum24_pkg_verify=CLIENT_YUM24_PKG_VERIFY,
+ yum24_installed_action=CLIENT_YUM24_INSTALLED_ACTION,
+ yum24_erase_flags=CLIENT_YUM24_ERASE_FLAGS,
+ yum24_version_fail_action=CLIENT_YUM24_VERSION_FAIL_ACTION,
+ yum24_verify_fail_action=CLIENT_YUM24_VERIFY_FAIL_ACTION,
+ yum24_verify_flags=CLIENT_YUM24_VERIFY_FLAGS,
+ yum24_autodep=CLIENT_YUM24_AUTODEP,
+ yumng_pkg_checks=CLIENT_YUMNG_PKG_CHECKS,
+ yumng_pkg_verify=CLIENT_YUMNG_PKG_VERIFY,
+ yumng_installed_action=CLIENT_YUMNG_INSTALLED_ACTION,
+ yumng_version_fail_action=CLIENT_YUMNG_VERSION_FAIL_ACTION,
+ yumng_verify_fail_action=CLIENT_YUMNG_VERIFY_FAIL_ACTION,
+ yumng_verify_flags=CLIENT_YUMNG_VERIFY_FLAGS)
+
+CLIENT_COMMON_OPTIONS = \
+ dict(extra=CLIENT_EXTRA_DISPLAY,
+ quick=CLIENT_QUICK,
+ lockfile=LOCKFILE,
+ drivers=CLIENT_DRIVERS,
+ dryrun=CLIENT_DRYRUN,
+ paranoid=CLIENT_PARANOID,
+ bundle=CLIENT_BUNDLE,
+ skipbundle=CLIENT_SKIPBUNDLE,
+ bundle_quick=CLIENT_BUNDLEQUICK,
+ indep=CLIENT_INDEP,
+ skipindep=CLIENT_SKIPINDEP,
+ file=CLIENT_FILE,
+ interactive=INTERACTIVE,
+ cache=CLIENT_CACHE,
+ profile=CLIENT_PROFILE,
+ remove=CLIENT_REMOVE,
+ server=SERVER_LOCATION,
+ user=CLIENT_USER,
+ password=SERVER_PASSWORD,
+ retries=CLIENT_RETRIES,
+ kevlar=CLIENT_KEVLAR,
+ omit_lock_check=OMIT_LOCK_CHECK,
+ decision=CLIENT_DLIST,
+ servicemode=CLIENT_SERVICE_MODE,
+ key=CLIENT_KEY,
+ certificate=CLIENT_CERT,
+ ca=CLIENT_CA,
+ serverCN=CLIENT_SCNS,
+ timeout=CLIENT_TIMEOUT,
+ decision_list=CLIENT_DECISION_LIST)
+CLIENT_COMMON_OPTIONS.update(DRIVER_OPTIONS)
+CLIENT_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS)
+
class OptionParser(OptionSet):
"""
@@ -425,10 +893,14 @@ class OptionParser(OptionSet):
self.Bootstrap = OptionSet([('configfile', CFILE)], quiet=True)
self.Bootstrap.parse(sys.argv[1:], do_getopt=False)
OptionSet.__init__(self, args, configfile=self.Bootstrap['configfile'])
- self.optinfo = args
+ self.optinfo = copy.copy(args)
def HandleEvent(self, event):
- if not self['configfile'].endswith(event.filename):
+ if 'configfile' not in self or not isinstance(self['configfile'], str):
+ # we haven't parsed options yet, or CFILE wasn't included
+ # in the options
+ return
+ if event.filename != self['configfile']:
print("Got event for unknown file: %s" % event.filename)
return
if event.code2str() == 'deleted':
@@ -447,3 +919,10 @@ class OptionParser(OptionSet):
self.do_getopt = do_getopt
OptionSet.parse(self, self.argv, do_getopt=self.do_getopt)
+ def add_option(self, name, opt):
+ self[name] = opt
+ self.optinfo[name] = opt
+
+ def update(self, optdict):
+ dict.update(self, optdict)
+ self.optinfo.update(optdict)
diff --git a/src/lib/Bcfg2/Proxy.py b/src/lib/Bcfg2/Proxy.py
index 422d642db..eff9544da 100644
--- a/src/lib/Bcfg2/Proxy.py
+++ b/src/lib/Bcfg2/Proxy.py
@@ -192,7 +192,15 @@ class SSLHTTPConnection(httplib.HTTPConnection):
def _connect_py26ssl(self):
"""Initiates a connection using the ssl module."""
- rawsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ # check for IPv6
+ hostip = socket.getaddrinfo(self.host,
+ self.port,
+ socket.AF_UNSPEC,
+ socket.SOCK_STREAM)[0][4][0]
+ if ':' in hostip:
+ rawsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+ else:
+ rawsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.protocol == 'xmlrpc/ssl':
ssl_protocol_ver = ssl.PROTOCOL_SSLv23
elif self.protocol == 'xmlrpc/tlsv1':
diff --git a/src/lib/Bcfg2/SSLServer.py b/src/lib/Bcfg2/SSLServer.py
index 6aa46ea58..aef44e419 100644
--- a/src/lib/Bcfg2/SSLServer.py
+++ b/src/lib/Bcfg2/SSLServer.py
@@ -45,7 +45,7 @@ class XMLRPCDispatcher (SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
params = (address, ) + params
response = self.instance._dispatch(method, params, self.funcs)
# py3k compatibility
- if type(response) not in [bool, str, list, dict] or response is None:
+ if type(response) not in [bool, str, list, dict]:
response = (response.decode('utf-8'), )
else:
response = (response, )
@@ -98,14 +98,23 @@ class SSLServer (SocketServer.TCPServer, object):
timeout -- timeout for non-blocking request handling
"""
-
+ # check whether or not we should listen on all interfaces
if listen_all:
listen_address = ('', server_address[1])
else:
listen_address = (server_address[0], server_address[1])
+
+ # check for IPv6 address
+ if ':' in server_address[0]:
+ self.address_family = socket.AF_INET6
+
try:
SocketServer.TCPServer.__init__(self, listen_address,
RequestHandlerClass)
+ except socket.gaierror:
+ e = sys.exc_info()[1]
+ self.logger.error("Failed to bind to socket: %s" % e)
+ raise
except socket.error:
self.logger.error("Failed to bind to socket")
raise
diff --git a/src/lib/Bcfg2/Server/Admin/Client.py b/src/lib/Bcfg2/Server/Admin/Client.py
index 4d580c54c..734e9573d 100644
--- a/src/lib/Bcfg2/Server/Admin/Client.py
+++ b/src/lib/Bcfg2/Server/Admin/Client.py
@@ -59,6 +59,3 @@ class Client(Bcfg2.Server.Admin.MetadataCore):
tree.xinclude()
for node in tree.findall("//Client"):
print(node.attrib["name"])
- else:
- print("No command specified")
- raise SystemExit(1)
diff --git a/src/lib/Bcfg2/Server/Admin/Compare.py b/src/lib/Bcfg2/Server/Admin/Compare.py
index 050dd69f8..78b30120a 100644
--- a/src/lib/Bcfg2/Server/Admin/Compare.py
+++ b/src/lib/Bcfg2/Server/Admin/Compare.py
@@ -18,7 +18,8 @@ class Compare(Bcfg2.Server.Admin.Mode):
'important', 'paranoid', 'sensitive',
'dev_type', 'major', 'minor', 'prune',
'encoding', 'empty', 'to', 'recursive',
- 'vcstype', 'sourceurl', 'revision'],
+ 'vcstype', 'sourceurl', 'revision',
+ 'secontext'],
'Package': ['name', 'type', 'version', 'simplefile',
'verify'],
'Service': ['name', 'type', 'status', 'mode',
diff --git a/src/lib/Bcfg2/Server/Admin/Init.py b/src/lib/Bcfg2/Server/Admin/Init.py
index c1f9ed484..8d0c2a4a9 100644
--- a/src/lib/Bcfg2/Server/Admin/Init.py
+++ b/src/lib/Bcfg2/Server/Admin/Init.py
@@ -6,9 +6,11 @@ import stat
import string
import sys
import subprocess
+
import Bcfg2.Server.Admin
import Bcfg2.Server.Plugin
import Bcfg2.Options
+from Bcfg2.Bcfg2Py3k import input
# default config file
config = '''
@@ -61,7 +63,7 @@ groups = '''<Groups version='3.0'>
# Default contents of clients.xml
clients = '''<Clients version="3.0">
- <Client profile="basic" pingable="Y" pingtime="0" name="%s"/>
+ <Client profile="basic" name="%s"/>
</Clients>
'''
@@ -106,14 +108,6 @@ plugin_list = ['Account',
default_plugins = Bcfg2.Options.SERVER_PLUGINS.default
-def get_input(prompt):
- """py3k compatible function to get input"""
- try:
- return raw_input(prompt)
- except NameError:
- return input(prompt)
-
-
def gen_password(length):
"""Generates a random alphanumeric password with length characters."""
chars = string.letters + string.digits
@@ -147,8 +141,8 @@ def create_key(hostname, keypath, certpath, country, state, location):
def create_conf(confpath, confdata, keypath):
# Don't overwrite existing bcfg2.conf file
if os.path.exists(confpath):
- result = get_input("\nWarning: %s already exists. "
- "Overwrite? [y/N]: " % confpath)
+ result = input("\nWarning: %s already exists. "
+ "Overwrite? [y/N]: " % confpath)
if result not in ['Y', 'y']:
print("Leaving %s unchanged" % confpath)
return
@@ -206,8 +200,8 @@ class Init(Bcfg2.Server.Admin.Mode):
def _prompt_hostname(self):
"""Ask for the server hostname."""
- data = get_input("What is the server's hostname [%s]: " %
- socket.getfqdn())
+ data = input("What is the server's hostname [%s]: " %
+ socket.getfqdn())
if data != '':
self.shostname = data
else:
@@ -215,21 +209,21 @@ class Init(Bcfg2.Server.Admin.Mode):
def _prompt_config(self):
"""Ask for the configuration file path."""
- newconfig = get_input("Store Bcfg2 configuration in [%s]: " %
- self.configfile)
+ newconfig = input("Store Bcfg2 configuration in [%s]: " %
+ self.configfile)
if newconfig != '':
self.configfile = os.path.abspath(newconfig)
def _prompt_repopath(self):
"""Ask for the repository path."""
while True:
- newrepo = get_input("Location of Bcfg2 repository [%s]: " %
- self.repopath)
+ newrepo = input("Location of Bcfg2 repository [%s]: " %
+ self.repopath)
if newrepo != '':
self.repopath = os.path.abspath(newrepo)
if os.path.isdir(self.repopath):
- response = get_input("Directory %s exists. Overwrite? [y/N]:" \
- % self.repopath)
+ response = input("Directory %s exists. Overwrite? [y/N]:" \
+ % self.repopath)
if response.lower().strip() == 'y':
break
else:
@@ -245,8 +239,8 @@ class Init(Bcfg2.Server.Admin.Mode):
def _prompt_server(self):
"""Ask for the server name."""
- newserver = get_input("Input the server location [%s]: " %
- self.server_uri)
+ newserver = input("Input the server location [%s]: " %
+ self.server_uri)
if newserver != '':
self.server_uri = newserver
@@ -258,19 +252,19 @@ class Init(Bcfg2.Server.Admin.Mode):
prompt += ': '
while True:
try:
- osidx = int(get_input(prompt))
+ osidx = int(input(prompt))
self.os_sel = os_list[osidx - 1][1]
break
except ValueError:
continue
def _prompt_plugins(self):
- default = get_input("Use default plugins? (%s) [Y/n]: " %
- ''.join(default_plugins)).lower()
+ default = input("Use default plugins? (%s) [Y/n]: " %
+ ''.join(default_plugins)).lower()
if default != 'y' or default != '':
while True:
plugins_are_valid = True
- plug_str = get_input("Specify plugins: ")
+ plug_str = input("Specify plugins: ")
plugins = plug_str.split(',')
for plugin in plugins:
plugin = plugin.strip()
@@ -284,26 +278,26 @@ class Init(Bcfg2.Server.Admin.Mode):
"""Ask for the key details (country, state, and location)."""
print("The following questions affect SSL certificate generation.")
print("If no data is provided, the default values are used.")
- newcountry = get_input("Country name (2 letter code) for certificate: ")
+ newcountry = input("Country name (2 letter code) for certificate: ")
if newcountry != '':
if len(newcountry) == 2:
self.country = newcountry
else:
while len(newcountry) != 2:
- newcountry = get_input("2 letter country code (eg. US): ")
+ newcountry = input("2 letter country code (eg. US): ")
if len(newcountry) == 2:
self.country = newcountry
break
else:
self.country = 'US'
- newstate = get_input("State or Province Name (full name) for certificate: ")
+ newstate = input("State or Province Name (full name) for certificate: ")
if newstate != '':
self.state = newstate
else:
self.state = 'Illinois'
- newlocation = get_input("Locality Name (eg, city) for certificate: ")
+ newlocation = input("Locality Name (eg, city) for certificate: ")
if newlocation != '':
self.location = newlocation
else:
diff --git a/src/lib/Bcfg2/Server/Admin/Pull.py b/src/lib/Bcfg2/Server/Admin/Pull.py
index daf353107..816fd2ca7 100644
--- a/src/lib/Bcfg2/Server/Admin/Pull.py
+++ b/src/lib/Bcfg2/Server/Admin/Pull.py
@@ -2,6 +2,7 @@ import getopt
import sys
import Bcfg2.Server.Admin
+from Bcfg2.Bcfg2Py3k import input
class Pull(Bcfg2.Server.Admin.MetadataCore):
@@ -109,11 +110,8 @@ class Pull(Bcfg2.Server.Admin.MetadataCore):
(choice.group, choice.prio))
else:
print(" => host entry: %s" % (choice.hostname))
- # py3k compatibility
- try:
- ans = raw_input("Use this entry? [yN]: ") in ['y', 'Y']
- except NameError:
- ans = input("Use this entry? [yN]: ") in ['y', 'Y']
+
+ ans = input("Use this entry? [yN]: ") in ['y', 'Y']
if ans:
return choice
return False
diff --git a/src/lib/Bcfg2/Server/Admin/Reports.py b/src/lib/Bcfg2/Server/Admin/Reports.py
index 974cdff9d..17e1e1e4d 100644
--- a/src/lib/Bcfg2/Server/Admin/Reports.py
+++ b/src/lib/Bcfg2/Server/Admin/Reports.py
@@ -26,7 +26,7 @@ import Bcfg2.Server.Reports.settings
# Load django and reports stuff _after_ we know we can load settings
import django.core.management
from Bcfg2.Server.Reports.importscript import load_stats
-from Bcfg2.Server.Reports.updatefix import update_database
+from Bcfg2.Server.Reports.Updater import update_database, UpdaterError
from Bcfg2.Server.Reports.utils import *
project_directory = os.path.dirname(Bcfg2.Server.Reports.settings.__file__)
@@ -41,7 +41,7 @@ from django.db import connection, transaction
from Bcfg2.Server.Reports.reports.models import Client, Interaction, Entries, \
Entries_interactions, Performance, \
- Reason, Ping
+ Reason
def printStats(fn):
@@ -55,7 +55,6 @@ def printStats(fn):
start_i = Interaction.objects.count()
start_ei = Entries_interactions.objects.count()
start_perf = Performance.objects.count()
- start_ping = Ping.objects.count()
fn(self, *data)
@@ -67,8 +66,6 @@ def printStats(fn):
(start_ei - Entries_interactions.objects.count()))
self.log.info("Metrics removed: %s" %
(start_perf - Performance.objects.count()))
- self.log.info("Ping metrics removed: %s" %
- (start_ping - Ping.objects.count()))
return print_stats
@@ -77,16 +74,13 @@ class Reports(Bcfg2.Server.Admin.Mode):
'''Admin interface for dynamic reports'''
__shorthelp__ = "Manage dynamic reports"
__longhelp__ = (__shorthelp__)
- django_commands = ['syncdb', 'sqlall', 'validate']
+ django_commands = ['dbshell', 'shell', 'syncdb', 'sqlall', 'validate']
__usage__ = ("bcfg2-admin reports [command] [options]\n"
- " -v|--verbose Be verbose\n"
- " -q|--quiet Print only errors\n"
"\n"
" Commands:\n"
" init Initialize the database\n"
" load_stats Load statistics data\n"
" -s|--stats Path to statistics.xml file\n"
- " -c|--clients-file Path to clients.xml file\n"
" -O3 Fast mode. Duplicates data!\n"
" purge Purge records\n"
" --client [n] Client to operate on\n"
@@ -95,12 +89,11 @@ class Reports(Bcfg2.Server.Admin.Mode):
" scrub Scrub the database for duplicate reasons and orphaned entries\n"
" update Apply any updates to the reporting database\n"
"\n"
- " Django commands:\n "
- "\n ".join(django_commands))
+ " Django commands:\n " \
+ + "\n ".join(django_commands))
def __init__(self, setup):
Bcfg2.Server.Admin.Mode.__init__(self, setup)
- self.log.setLevel(logging.INFO)
def __call__(self, args):
Bcfg2.Server.Admin.Mode.__call__(self, args)
@@ -108,28 +101,21 @@ class Reports(Bcfg2.Server.Admin.Mode):
print(self.__usage__)
raise SystemExit(0)
- verb = 0
-
- if '-v' in args or '--verbose' in args:
- self.log.setLevel(logging.DEBUG)
- verb = 1
- if '-q' in args or '--quiet' in args:
- self.log.setLevel(logging.WARNING)
-
# FIXME - dry run
if args[0] in self.django_commands:
self.django_command_proxy(args[0])
elif args[0] == 'scrub':
self.scrub()
- elif args[0] == 'init':
- update_database()
- elif args[0] == 'update':
- update_database()
+ elif args[0] in ['init', 'update']:
+ try:
+ update_database()
+ except UpdaterError:
+ print "Update failed"
+ raise SystemExit(-1)
elif args[0] == 'load_stats':
quick = '-O3' in args
stats_file = None
- clients_file = None
i = 1
while i < len(args):
if args[i] == '-s' or args[i] == '--stats':
@@ -137,11 +123,9 @@ class Reports(Bcfg2.Server.Admin.Mode):
if stats_file[0] == '-':
self.errExit("Invalid statistics file: %s" % stats_file)
elif args[i] == '-c' or args[i] == '--clients-file':
- clients_file = args[i + 1]
- if clients_file[0] == '-':
- self.errExit("Invalid clients file: %s" % clients_file)
+ print "DeprecationWarning: %s is no longer used" % args[i]
i = i + 1
- self.load_stats(stats_file, clients_file, verb, quick)
+ self.load_stats(stats_file, self.log.getEffectiveLevel() > logging.WARNING, quick)
elif args[0] == 'purge':
expired = False
client = None
@@ -239,7 +223,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
else:
django.core.management.call_command(command)
- def load_stats(self, stats_file=None, clientspath=None, verb=0, quick=False):
+ def load_stats(self, stats_file=None, verb=0, quick=False):
'''Load statistics data into the database'''
location = ''
@@ -258,27 +242,18 @@ class Reports(Bcfg2.Server.Admin.Mode):
except:
encoding = 'UTF-8'
- if not clientspath:
- try:
- clientspath = "%s/Metadata/clients.xml" % \
- self.cfp.get('server', 'repository')
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- self.errExit("Could not read bcfg2.conf; exiting")
try:
- clientsdata = XML(open(clientspath).read())
- except (IOError, XMLSyntaxError):
- self.errExit("StatReports: Failed to parse %s" % (clientspath))
-
- try:
- load_stats(clientsdata,
- statsdata,
+ load_stats(statsdata,
encoding,
verb,
self.log,
quick=quick,
location=platform.node())
+ except UpdaterError:
+ self.errExit("StatReports: Database updater failed")
except:
- pass
+ self.errExit("failed to import stats: %s"
+ % traceback.format_exc().splitlines()[-1])
@printStats
def purge(self, client=None, maxdate=None, state=None):
@@ -306,12 +281,6 @@ class Reports(Bcfg2.Server.Admin.Mode):
self.log.debug("Filtering by maxdate: %s" % maxdate)
ipurge = ipurge.filter(timestamp__lt=maxdate)
- # Handle ping data as well
- ping = Ping.objects.filter(endtime__lt=maxdate)
- if client:
- ping = ping.filter(client=cobj)
- ping.delete()
-
if state:
filtered = True
if state not in ('dirty', 'clean', 'modified'):
diff --git a/src/lib/Bcfg2/Server/Admin/Tidy.py b/src/lib/Bcfg2/Server/Admin/Tidy.py
index 82319b93e..65aa955b4 100644
--- a/src/lib/Bcfg2/Server/Admin/Tidy.py
+++ b/src/lib/Bcfg2/Server/Admin/Tidy.py
@@ -3,6 +3,7 @@ import re
import socket
import Bcfg2.Server.Admin
+from Bcfg2.Bcfg2Py3k import input
class Tidy(Bcfg2.Server.Admin.Mode):
@@ -22,11 +23,7 @@ class Tidy(Bcfg2.Server.Admin.Mode):
if '-f' in args or '-I' in args:
if '-I' in args:
for name in badfiles[:]:
- # py3k compatibility
- try:
- answer = raw_input("Unlink file %s? [yN] " % name)
- except NameError:
- answer = input("Unlink file %s? [yN] " % name)
+ answer = input("Unlink file %s? [yN] " % name)
if answer not in ['y', 'Y']:
badfiles.remove(name)
for name in badfiles:
diff --git a/src/lib/Bcfg2/Server/Admin/__init__.py b/src/lib/Bcfg2/Server/Admin/__init__.py
index 618fa450e..a7269a289 100644
--- a/src/lib/Bcfg2/Server/Admin/__init__.py
+++ b/src/lib/Bcfg2/Server/Admin/__init__.py
@@ -123,9 +123,8 @@ class MetadataCore(Mode):
setup['password'],
setup['encoding'],
filemonitor=setup['filemonitor'],
+ cfile=setup['configfile'],
setup=setup)
- if setup['event debug']:
- self.bcore.fam.debug = True
except Bcfg2.Server.Core.CoreInitError:
msg = sys.exc_info()[1]
self.errExit("Core load failed: %s" % msg)
diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py
index 8482925b7..6dbab64bd 100644
--- a/src/lib/Bcfg2/Server/Core.py
+++ b/src/lib/Bcfg2/Server/Core.py
@@ -16,6 +16,7 @@ except ImportError:
from Bcfg2.Component import Component, exposed
from Bcfg2.Server.Plugin import PluginInitError, PluginExecutionError
+import Bcfg2.Server
import Bcfg2.Server.FileMonitor
import Bcfg2.Server.Plugins.Metadata
# Compatibility imports
@@ -29,7 +30,7 @@ logger = logging.getLogger('Bcfg2.Server.Core')
def critical_error(operation):
"""Log and err, traceback and return an xmlrpc fault to client."""
logger.error(operation, exc_info=1)
- raise xmlrpclib.Fault(7, "Critical unexpected failure: %s" % (operation))
+ raise xmlrpclib.Fault(xmlrpclib.APPLICATION_ERROR, "Critical unexpected failure: %s" % (operation))
try:
import psyco
@@ -66,17 +67,24 @@ class Core(Component):
filemonitor='default', start_fam_thread=False):
Component.__init__(self)
self.datastore = repo
- if filemonitor not in Bcfg2.Server.FileMonitor.available:
+
+ try:
+ fm = Bcfg2.Server.FileMonitor.available[filemonitor]
+ except KeyError:
logger.error("File monitor driver %s not available; "
"forcing to default" % filemonitor)
- filemonitor = 'default'
+ fm = Bcfg2.Server.FileMonitor.available['default']
+ famargs = dict(ignore=[], debug=False)
+ if 'ignore' in setup:
+ famargs['ignore'] = setup['ignore']
+ if 'debug' in setup:
+ famargs['debug'] = setup['debug']
try:
- self.fam = Bcfg2.Server.FileMonitor.available[filemonitor]()
+ self.fam = fm(**famargs)
except IOError:
- logger.error("Failed to instantiate fam driver %s" % filemonitor,
- exc_info=1)
- raise CoreInitError("failed to instantiate fam driver (used %s)" % \
- filemonitor)
+ msg = "Failed to instantiate fam driver %s" % filemonitor
+ logger.error(msg, exc_info=1)
+ raise CoreInitError(msg)
self.pubspace = {}
self.cfile = cfile
self.cron = {}
@@ -197,9 +205,24 @@ class Core(Component):
"""Shutting down the plugins."""
if not self.terminate.isSet():
self.terminate.set()
+ self.fam.shutdown()
for plugin in list(self.plugins.values()):
plugin.shutdown()
+ def client_run_hook(self, hook, metadata):
+ """Checks the data structure."""
+ for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.ClientRunHooks):
+ try:
+ getattr(plugin, hook)(metadata)
+ except AttributeError:
+ err = sys.exc_info()[1]
+ logger.error("Unknown attribute: %s" % err)
+ raise
+ except:
+ err = sys.exc_info()[1]
+ logger.error("%s: Error invoking hook %s: %s" % (plugin, hook,
+ err))
+
def validate_structures(self, metadata, data):
"""Checks the data structure."""
for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.StructureValidator):
@@ -309,6 +332,8 @@ class Core(Component):
logger.error("Metadata consistency error for client %s" % client)
return lxml.etree.Element("error", type='metadata error')
+ self.client_run_hook("start_client_run", meta)
+
try:
structures = self.GetStructures(meta)
except:
@@ -337,6 +362,8 @@ class Core(Component):
logger.error("error in BindStructure", exc_info=1)
self.validate_goals(meta, config)
+ self.client_run_hook("end_client_run", meta)
+
sort_xml(config, key=lambda e: e.get('name'))
logger.info("Generated config for %s in %.03f seconds" % \
@@ -384,96 +411,110 @@ class Core(Component):
logger.info("Client %s reported state %s" % (client_name,
state.get('state')))
+ self.client_run_hook("end_statistics", meta)
+
+ def resolve_client(self, address, cleanup_cache=False, metadata=True):
+ try:
+ client = self.metadata.resolve_client(address,
+ cleanup_cache=cleanup_cache)
+ if metadata:
+ meta = self.build_metadata(client)
+ else:
+ meta = None
+ except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError:
+ critical_error("Client metadata resolution error for %s; "
+ "check server log" % address[0])
+ except Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError:
+ critical_error('Metadata system runtime failure')
+ return (client, meta)
+
# XMLRPC handlers start here
@exposed
+ def DeclareVersion(self, address, version):
+ """ declare the client version """
+ client, metadata = self.resolve_client(address)
+ try:
+ self.metadata.set_version(client, version)
+ except (Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError,
+ Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError):
+ err = sys.exc_info()[1]
+ critical_error("Unable to set version for %s: %s" %
+ (client, err))
+ return True
+
+ @exposed
def GetProbes(self, address):
"""Fetch probes for a particular client."""
resp = lxml.etree.Element('probes')
+ client, metadata = self.resolve_client(address, cleanup_cache=True)
try:
- name = self.metadata.resolve_client(address, cleanup_cache=True)
- meta = self.build_metadata(name)
-
for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Probing):
- for probe in plugin.GetProbes(meta):
+ for probe in plugin.GetProbes(metadata):
resp.append(probe)
return lxml.etree.tostring(resp, encoding='UTF-8',
xml_declaration=True)
- except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError:
- warning = 'Client metadata resolution error for %s' % address[0]
- self.logger.warning(warning)
- raise xmlrpclib.Fault(6, warning + "; check server log")
- except Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError:
- err_msg = 'Metadata system runtime failure'
- self.logger.error(err_msg)
- raise xmlrpclib.Fault(6, err_msg)
except:
- critical_error("Error determining client probes")
+ critical_error("Error determining probes for %s" % client)
@exposed
def RecvProbeData(self, address, probedata):
"""Receive probe data from clients."""
- try:
- name = self.metadata.resolve_client(address)
- meta = self.build_metadata(name)
- except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError:
- warning = 'Metadata consistency error'
- self.logger.warning(warning)
- raise xmlrpclib.Fault(6, warning)
+ client, metadata = self.resolve_client(address)
# clear dynamic groups
- self.metadata.cgroups[meta.hostname] = []
+ self.metadata.cgroups[metadata.hostname] = []
try:
- xpdata = lxml.etree.XML(probedata.encode('utf-8'))
+ xpdata = lxml.etree.XML(probedata.encode('utf-8'),
+ parser=Bcfg2.Server.XMLParser)
except:
- self.logger.error("Failed to parse probe data from client %s" % \
- (address[0]))
- return False
+ critical_error("Failed to parse probe data from client %s" %
+ client)
sources = []
[sources.append(data.get('source')) for data in xpdata
if data.get('source') not in sources]
for source in sources:
if source not in self.plugins:
- self.logger.warning("Failed to locate plugin %s" % (source))
+ self.logger.warning("Failed to locate plugin %s" % source)
continue
dl = [data for data in xpdata if data.get('source') == source]
try:
- self.plugins[source].ReceiveData(meta, dl)
+ self.plugins[source].ReceiveData(metadata, dl)
except:
- logger.error("Failed to process probe data from client %s" % \
- (address[0]), exc_info=1)
+ critical_error("Failed to process probe data from client %s" %
+ client)
return True
@exposed
def AssertProfile(self, address, profile):
"""Set profile for a client."""
+ client = self.resolve_client(address, metadata=False)[0]
try:
- client = self.metadata.resolve_client(address)
self.metadata.set_profile(client, profile, address)
except (Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError,
Bcfg2.Server.Plugins.Metadata.MetadataRuntimeError):
- warning = 'Metadata consistency error'
- self.logger.warning(warning)
- raise xmlrpclib.Fault(6, warning)
+ err = sys.exc_info()[1]
+ critical_error("Unable to assert profile for %s: %s" %
+ (client, err))
return True
@exposed
def GetConfig(self, address, checksum=False):
"""Build config for a client."""
+ client = self.resolve_client(address)[0]
try:
- client = self.metadata.resolve_client(address)
config = self.BuildConfiguration(client)
return lxml.etree.tostring(config, encoding='UTF-8',
xml_declaration=True)
except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError:
- self.logger.warning("Metadata consistency failure for %s" % (address))
- raise xmlrpclib.Fault(6, "Metadata consistency failure")
+ critical_error("Metadata consistency failure for %s" % client)
@exposed
def RecvStats(self, address, stats):
"""Act on statistics upload."""
- sdata = lxml.etree.XML(stats.encode('utf-8'))
- client = self.metadata.resolve_client(address)
+ client = self.resolve_client(address)[0]
+ sdata = lxml.etree.XML(stats.encode('utf-8'),
+ parser=Bcfg2.Server.XMLParser)
self.process_statistics(client, sdata)
return "<ok/>"
@@ -488,6 +529,5 @@ class Core(Component):
@exposed
def GetDecisionList(self, address, mode):
"""Get the data of the decision list."""
- client = self.metadata.resolve_client(address)
- meta = self.build_metadata(client)
- return self.GetDecisions(meta, mode)
+ client, metadata = self.resolve_client(address)
+ return self.GetDecisions(metadata, mode)
diff --git a/src/lib/Bcfg2/Server/FileMonitor.py b/src/lib/Bcfg2/Server/FileMonitor.py
deleted file mode 100644
index d6b313e6b..000000000
--- a/src/lib/Bcfg2/Server/FileMonitor.py
+++ /dev/null
@@ -1,315 +0,0 @@
-"""Bcfg2.Server.FileMonitor provides the support for monitorung files."""
-
-import logging
-import os
-import stat
-from time import sleep, time
-
-logger = logging.getLogger('Bcfg2.Server.FileMonitor')
-
-
-def ShouldIgnore(event):
- """Test if the event should be suppresed."""
- # FIXME should move event suppression out of the core
- if event.filename.split('/')[-1] == '.svn':
- return True
- if event.filename.endswith('~') or \
- event.filename.startswith('#') or event.filename.startswith('.#'):
- #logger.error("Suppressing event for file %s" % (event.filename))
- return True
- return False
-
-
-class Event(object):
- def __init__(self, request_id, filename, code):
- self.requestID = request_id
- self.filename = filename
- self.action = code
-
- def code2str(self):
- """return static code for event"""
- return self.action
-
-available = {}
-
-
-class FileMonitor(object):
- """File Monitor baseclass."""
- def __init__(self, debug=False):
- object.__init__(self)
- self.debug = debug
- self.handles = dict()
-
- def get_event(self):
- return None
-
- def pending(self):
- return False
-
- def fileno(self):
- return 0
-
- def handle_one_event(self, event):
- if ShouldIgnore(event):
- return
- if event.requestID not in self.handles:
- logger.info("Got event for unexpected id %s, file %s" %
- (event.requestID, event.filename))
- return
- if self.debug:
- logger.info("Dispatching event %s %s to obj %s" \
- % (event.code2str(), event.filename,
- self.handles[event.requestID]))
- try:
- self.handles[event.requestID].HandleEvent(event)
- except:
- logger.error("error in handling of gamin event for %s" % \
- (event.filename), exc_info=1)
-
- def handle_event_set(self, lock=None):
- count = 1
- event = self.get_event()
- start = time()
- if lock:
- lock.acquire()
- try:
- self.handle_one_event(event)
- while self.pending():
- self.handle_one_event(self.get_event())
- count += 1
- except:
- pass
- if lock:
- lock.release()
- end = time()
- logger.info("Handled %d events in %.03fs" % (count, (end - start)))
-
- def handle_events_in_interval(self, interval):
- end = time() + interval
- while time() < end:
- if self.pending():
- self.handle_event_set()
- end = time() + interval
- else:
- sleep(0.5)
-
-
-class FamFam(object):
- """The fam object is a set of callbacks for
- file alteration events (FAM support).
- """
-
- def __init__(self):
- object.__init__(self)
- self.fm = _fam.open()
- self.users = {}
- self.handles = {}
- self.debug = False
-
- def fileno(self):
- """Return fam file handle number."""
- return self.fm.fileno()
-
- def handle_event_set(self, _):
- self.Service()
-
- def handle_events_in_interval(self, interval):
- now = time()
- while (time() - now) < interval:
- if self.Service():
- now = time()
-
- def AddMonitor(self, path, obj):
- """Add a monitor to path, installing a callback to obj.HandleEvent."""
- mode = os.stat(path)[stat.ST_MODE]
- if stat.S_ISDIR(mode):
- handle = self.fm.monitorDirectory(path, None)
- else:
- handle = self.fm.monitorFile(path, None)
- self.handles[handle.requestID()] = handle
- if obj != None:
- self.users[handle.requestID()] = obj
- return handle.requestID()
-
- def Service(self, interval=0.50):
- """Handle all fam work."""
- count = 0
- collapsed = 0
- rawevents = []
- start = time()
- now = time()
- while (time() - now) < interval:
- if self.fm.pending():
- while self.fm.pending():
- count += 1
- rawevents.append(self.fm.nextEvent())
- now = time()
- unique = []
- bookkeeping = []
- for event in rawevents:
- if ShouldIgnore(event):
- continue
- if event.code2str() != 'changed':
- # process all non-change events
- unique.append(event)
- else:
- if (event.filename, event.requestID) not in bookkeeping:
- bookkeeping.append((event.filename, event.requestID))
- unique.append(event)
- else:
- collapsed += 1
- for event in unique:
- if event.requestID in self.users:
- try:
- self.users[event.requestID].HandleEvent(event)
- except:
- logger.error("handling event for file %s" % (event.filename), exc_info=1)
- end = time()
- logger.info("Processed %s fam events in %03.03f seconds. %s coalesced" %
- (count, (end - start), collapsed))
- return count
-
-
-class Fam(FileMonitor):
- """
- The fam object is a set of callbacks for
- file alteration events (FAM support).
- """
-
- def __init__(self, debug=False):
- FileMonitor.__init__(self, debug)
- self.fm = _fam.open()
-
- def fileno(self):
- return self.fm.fileno()
-
- def AddMonitor(self, path, obj):
- """Add a monitor to path, installing a callback to obj.HandleEvent."""
- mode = os.stat(path)[stat.ST_MODE]
- if stat.S_ISDIR(mode):
- handle = self.fm.monitorDirectory(path, None)
- else:
- handle = self.fm.monitorFile(path, None)
- if obj != None:
- self.handles[handle.requestID()] = obj
- return handle.requestID()
-
- def pending(self):
- return self.fm.pending()
-
- def get_event(self):
- return self.fm.nextEvent()
-
-
-class Pseudo(FileMonitor):
- """
- The fam object is a set of callbacks for
- file alteration events (static monitor support).
- """
-
- def __init__(self, debug=False):
- FileMonitor.__init__(self, debug=False)
- self.pending_events = []
-
- def pending(self):
- return len(self.pending_events) != 0
-
- def get_event(self):
- return self.pending_events.pop()
-
- def AddMonitor(self, path, obj):
- """add a monitor to path, installing a callback to obj.HandleEvent"""
- handleID = len(list(self.handles.keys()))
- mode = os.stat(path)[stat.ST_MODE]
- handle = Event(handleID, path, 'exists')
- if stat.S_ISDIR(mode):
- dirList = os.listdir(path)
- self.pending_events.append(handle)
- for includedFile in dirList:
- self.pending_events.append(Event(handleID,
- includedFile,
- 'exists'))
- self.pending_events.append(Event(handleID, path, 'endExist'))
- else:
- self.pending_events.append(Event(handleID, path, 'exists'))
- if obj != None:
- self.handles[handleID] = obj
- return handleID
-
-
-try:
- from gamin import WatchMonitor, GAMCreated, GAMExists, GAMEndExist, \
- GAMChanged, GAMDeleted, GAMMoved
-
- class GaminEvent(Event):
- """
- This class provides an event analogous to
- python-fam events based on gamin sources.
- """
- def __init__(self, request_id, filename, code):
- Event.__init__(self, request_id, filename, code)
- action_map = {GAMCreated: 'created', GAMExists: 'exists',
- GAMChanged: 'changed', GAMDeleted: 'deleted',
- GAMEndExist: 'endExist', GAMMoved: 'moved'}
- if code in action_map:
- self.action = action_map[code]
-
- class Gamin(FileMonitor):
- """
- The fam object is a set of callbacks for
- file alteration events (Gamin support)
- """
- def __init__(self, debug=False):
- FileMonitor.__init__(self, debug)
- self.mon = WatchMonitor()
- self.counter = 0
- self.events = []
-
- def fileno(self):
- return self.mon.get_fd()
-
- def queue(self, path, action, request_id):
- """queue up the event for later handling"""
- self.events.append(GaminEvent(request_id, path, action))
-
- def AddMonitor(self, path, obj):
- """Add a monitor to path, installing a callback to obj.HandleEvent."""
- handle = self.counter
- self.counter += 1
- mode = os.stat(path)[stat.ST_MODE]
-
- # Flush queued gamin events
- while self.mon.event_pending():
- self.mon.handle_one_event()
-
- if stat.S_ISDIR(mode):
- self.mon.watch_directory(path, self.queue, handle)
- else:
- self.mon.watch_file(path, self.queue, handle)
- self.handles[handle] = obj
- return handle
-
- def pending(self):
- return len(self.events) > 0 or self.mon.event_pending()
-
- def get_event(self):
- if self.mon.event_pending():
- self.mon.handle_one_event()
- return self.events.pop(0)
-
- available['gamin'] = Gamin
-except ImportError:
- # fall back to _fam
- pass
-
-try:
- import _fam
- available['fam'] = FamFam
-except ImportError:
- pass
-available['pseudo'] = Pseudo
-
-for fdrv in ['gamin', 'fam', 'pseudo']:
- if fdrv in available:
- available['default'] = available[fdrv]
- break
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Fam.py b/src/lib/Bcfg2/Server/FileMonitor/Fam.py
new file mode 100644
index 000000000..1a00fffa0
--- /dev/null
+++ b/src/lib/Bcfg2/Server/FileMonitor/Fam.py
@@ -0,0 +1,82 @@
+""" Fam provides FAM support for file alteration events """
+
+import os
+import _fam
+import stat
+import logging
+from time import time
+from Bcfg2.Server.FileMonitor import FileMonitor
+
+logger = logging.getLogger(__name__)
+
+class Fam(FileMonitor):
+ __priority__ = 90
+
+ def __init__(self, ignore=None, debug=False):
+ FileMonitor.__init__(self, ignore=ignore, debug=debug)
+ self.fm = _fam.open()
+ self.users = {}
+
+ def fileno(self):
+ """Return fam file handle number."""
+ return self.fm.fileno()
+
+ def handle_event_set(self, _):
+ self.Service()
+
+ def handle_events_in_interval(self, interval):
+ now = time()
+ while (time() - now) < interval:
+ if self.Service():
+ now = time()
+
+ def AddMonitor(self, path, obj):
+ """Add a monitor to path, installing a callback to obj.HandleEvent."""
+ mode = os.stat(path)[stat.ST_MODE]
+ if stat.S_ISDIR(mode):
+ handle = self.fm.monitorDirectory(path, None)
+ else:
+ handle = self.fm.monitorFile(path, None)
+ self.handles[handle.requestID()] = handle
+ if obj != None:
+ self.users[handle.requestID()] = obj
+ return handle.requestID()
+
+ def Service(self, interval=0.50):
+ """Handle all fam work."""
+ count = 0
+ collapsed = 0
+ rawevents = []
+ start = time()
+ now = time()
+ while (time() - now) < interval:
+ if self.fm.pending():
+ while self.fm.pending():
+ count += 1
+ rawevents.append(self.fm.nextEvent())
+ now = time()
+ unique = []
+ bookkeeping = []
+ for event in rawevents:
+ if self.should_ignore(event):
+ continue
+ if event.code2str() != 'changed':
+ # process all non-change events
+ unique.append(event)
+ else:
+ if (event.filename, event.requestID) not in bookkeeping:
+ bookkeeping.append((event.filename, event.requestID))
+ unique.append(event)
+ else:
+ collapsed += 1
+ for event in unique:
+ if event.requestID in self.users:
+ try:
+ self.users[event.requestID].HandleEvent(event)
+ except:
+ logger.error("Handling event for file %s" % event.filename,
+ exc_info=1)
+ end = time()
+ logger.info("Processed %s fam events in %03.03f seconds. %s coalesced" %
+ (count, (end - start), collapsed))
+ return count
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Gamin.py b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py
new file mode 100644
index 000000000..60f80c9c3
--- /dev/null
+++ b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py
@@ -0,0 +1,64 @@
+""" Gamin driver for file alteration events """
+
+import os
+import stat
+import logging
+from gamin import WatchMonitor, GAMCreated, GAMExists, GAMEndExist, \
+ GAMChanged, GAMDeleted
+from Bcfg2.Server.FileMonitor import Event, FileMonitor
+
+logger = logging.getLogger(__name__)
+
+class GaminEvent(Event):
+ """
+ This class provides an event analogous to
+ python-fam events based on gamin sources.
+ """
+ action_map = {GAMCreated: 'created', GAMExists: 'exists',
+ GAMChanged: 'changed', GAMDeleted: 'deleted',
+ GAMEndExist: 'endExist'}
+
+ def __init__(self, request_id, filename, code):
+ Event.__init__(self, request_id, filename, code)
+ if code in self.action_map:
+ self.action = self.action_map[code]
+
+class Gamin(FileMonitor):
+ __priority__ = 10
+
+ def __init__(self, ignore=None, debug=False):
+ FileMonitor.__init__(self, ignore=ignore, debug=debug)
+ self.mon = WatchMonitor()
+ self.counter = 0
+
+ def fileno(self):
+ return self.mon.get_fd()
+
+ def queue(self, path, action, request_id):
+ """queue up the event for later handling"""
+ self.events.append(GaminEvent(request_id, path, action))
+
+ def AddMonitor(self, path, obj):
+ """Add a monitor to path, installing a callback to obj."""
+ handle = self.counter
+ self.counter += 1
+ mode = os.stat(path)[stat.ST_MODE]
+
+ # Flush queued gamin events
+ while self.mon.event_pending():
+ self.mon.handle_one_event()
+
+ if stat.S_ISDIR(mode):
+ self.mon.watch_directory(path, self.queue, handle)
+ else:
+ self.mon.watch_file(path, self.queue, handle)
+ self.handles[handle] = obj
+ return handle
+
+ def pending(self):
+ return FileMonitor.pending(self) or self.mon.event_pending()
+
+ def get_event(self):
+ if self.mon.event_pending():
+ self.mon.handle_one_event()
+ return FileMonitor.get_event(self)
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
new file mode 100644
index 000000000..50c724279
--- /dev/null
+++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
@@ -0,0 +1,64 @@
+""" Inotify driver for file alteration events """
+
+import os
+import stat
+import logging
+import operator
+import pyinotify
+from Bcfg2.Server.FileMonitor import Event
+from Bcfg2.Server.FileMonitor.Pseudo import Pseudo
+
+logger = logging.getLogger(__name__)
+
+class Inotify(Pseudo, pyinotify.ProcessEvent):
+ __priority__ = 1
+ mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
+ action_map = {pyinotify.IN_CREATE: 'created',
+ pyinotify.IN_DELETE: 'deleted',
+ pyinotify.IN_MODIFY: 'changed'}
+
+ def __init__(self, ignore=None, debug=False):
+ Pseudo.__init__(self, ignore=ignore, debug=debug)
+ self.wm = pyinotify.WatchManager()
+ self.notifier = pyinotify.ThreadedNotifier(self.wm, self)
+ self.notifier.start()
+
+ def fileno(self):
+ return self.wm.get_fd()
+
+ def process_default(self, ievent):
+ action = ievent.maskname
+ for amask, aname in self.action_map.items():
+ if ievent.mask & amask:
+ action = aname
+ break
+ # FAM-style file monitors return the full path to the parent
+ # directory that is being watched, relative paths to anything
+ # contained within the directory
+ watch = self.wm.watches[ievent.wd]
+ if watch.path == ievent.pathname:
+ path = ievent.pathname
+ else:
+ # relative path
+ path = os.path.basename(ievent.pathname)
+ evt = Event(ievent.wd, path, action)
+ self.events.append(evt)
+
+ def AddMonitor(self, path, obj):
+ res = self.wm.add_watch(path, self.mask, quiet=False)
+ if not res:
+ # if we didn't get a return, but we also didn't get an
+ # exception, we're already watching this directory, so we
+ # need to find the watch descriptor for it
+ for wd, watch in self.wm.watches.items():
+ if watch.path == path:
+ wd = watch.wd
+ else:
+ wd = res[path]
+
+ # inotify doesn't produce initial 'exists' events, so we
+ # inherit from Pseudo to produce those
+ return Pseudo.AddMonitor(self, path, obj, handleID=wd)
+
+ def shutdown(self):
+ self.notifier.stop()
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
new file mode 100644
index 000000000..baff871d0
--- /dev/null
+++ b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
@@ -0,0 +1,27 @@
+""" Pseudo provides static monitor support for file alteration events """
+
+import os
+import stat
+import logging
+from Bcfg2.Server.FileMonitor import FileMonitor, Event
+
+logger = logging.getLogger(__name__)
+
+class Pseudo(FileMonitor):
+ __priority__ = 99
+
+ def AddMonitor(self, path, obj, handleID=None):
+ """add a monitor to path, installing a callback to obj.HandleEvent"""
+ if handleID is None:
+ handleID = len(list(self.handles.keys()))
+ mode = os.stat(path)[stat.ST_MODE]
+ self.events.append(Event(handleID, path, 'exists'))
+ if stat.S_ISDIR(mode):
+ dirList = os.listdir(path)
+ for includedFile in dirList:
+ self.events.append(Event(handleID, includedFile, 'exists'))
+ self.events.append(Event(handleID, path, 'endExist'))
+
+ if obj != None:
+ self.handles[handleID] = obj
+ return handleID
diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py
new file mode 100644
index 000000000..8bd63e18d
--- /dev/null
+++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py
@@ -0,0 +1,141 @@
+"""Bcfg2.Server.FileMonitor provides the support for monitoring files."""
+
+import os
+import sys
+import fnmatch
+import logging
+import pkgutil
+from time import sleep, time
+
+logger = logging.getLogger(__name__)
+
+class Event(object):
+ def __init__(self, request_id, filename, code):
+ self.requestID = request_id
+ self.filename = filename
+ self.action = code
+
+ def code2str(self):
+ """return static code for event"""
+ return self.action
+
+ def __str__(self):
+ return "%s: %s %s" % (self.__class__.__name__,
+ self.filename, self.action)
+
+ def __repr__(self):
+ return "%s (request ID %s)" % (str(self), self.requestID)
+
+
+class FileMonitor(object):
+ """File Monitor baseclass."""
+ def __init__(self, ignore=None, debug=False):
+ object.__init__(self)
+ self.debug = debug
+ self.handles = dict()
+ self.events = []
+ if ignore is None:
+ ignore = []
+ self.ignore = ignore
+
+ def __str__(self):
+ return "%s: %s" % (__name__, self.__class__.__name__)
+
+ def __repr__(self):
+ return "%s (%s events, fd %s)" % (str(self), len(self.events), self.fileno)
+
+ def should_ignore(self, event):
+ for pattern in self.ignore:
+ if (fnmatch.fnmatch(event.filename, pattern) or
+ fnmatch.fnmatch(os.path.split(event.filename)[-1], pattern)):
+ if self.debug:
+ logger.info("Ignoring %s" % event)
+ return True
+ return False
+
+ def pending(self):
+ return bool(self.events)
+
+ def get_event(self):
+ return self.events.pop(0)
+
+ def fileno(self):
+ return 0
+
+ def handle_one_event(self, event):
+ if self.should_ignore(event):
+ return
+ if event.requestID not in self.handles:
+ logger.info("Got event for unexpected id %s, file %s" %
+ (event.requestID, event.filename))
+ return
+ if self.debug:
+ logger.info("Dispatching event %s %s to obj %s" %
+ (event.code2str(), event.filename,
+ self.handles[event.requestID]))
+ try:
+ self.handles[event.requestID].HandleEvent(event)
+ except:
+ err = sys.exc_info()[1]
+ logger.error("Error in handling of event %s for %s: %s" %
+ (event.code2str(), event.filename, err))
+
+ def handle_event_set(self, lock=None):
+ count = 1
+ event = self.get_event()
+ start = time()
+ if lock:
+ lock.acquire()
+ try:
+ self.handle_one_event(event)
+ while self.pending():
+ self.handle_one_event(self.get_event())
+ count += 1
+ except:
+ pass
+ if lock:
+ lock.release()
+ end = time()
+ logger.info("Handled %d events in %.03fs" % (count, (end - start)))
+
+ def handle_events_in_interval(self, interval):
+ end = time() + interval
+ while time() < end:
+ if self.pending():
+ self.handle_event_set()
+ end = time() + interval
+ else:
+ sleep(0.5)
+
+ def shutdown(self):
+ pass
+
+
+available = dict()
+
+# todo: loading the monitor drivers should be automatic
+from Bcfg2.Server.FileMonitor.Pseudo import Pseudo
+available['pseudo'] = Pseudo
+
+try:
+ from Bcfg2.Server.FileMonitor.Fam import Fam
+ available['fam'] = Fam
+except ImportError:
+ pass
+
+try:
+ from Bcfg2.Server.FileMonitor.Gamin import Gamin
+ available['gamin'] = Gamin
+except ImportError:
+ pass
+
+try:
+ from Bcfg2.Server.FileMonitor.Inotify import Inotify
+ available['inotify'] = Inotify
+except ImportError:
+ pass
+
+for fdrv in sorted(available.keys(), key=lambda k: available[k].__priority__):
+ if fdrv in available:
+ available['default'] = available[fdrv]
+ break
diff --git a/src/lib/Bcfg2/Server/Lint/Bundles.py b/src/lib/Bcfg2/Server/Lint/Bundles.py
deleted file mode 100644
index e6b6307f2..000000000
--- a/src/lib/Bcfg2/Server/Lint/Bundles.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import lxml.etree
-import Bcfg2.Server.Lint
-
-class Bundles(Bcfg2.Server.Lint.ServerPlugin):
- """ Perform various bundle checks """
- def Run(self):
- """ run plugin """
- if 'Bundler' in self.core.plugins:
- self.missing_bundles()
- for bundle in self.core.plugins['Bundler'].entries.values():
- if self.HandlesFile(bundle.name):
- if (not Bcfg2.Server.Plugins.Bundler.have_genshi or
- type(bundle) is not
- Bcfg2.Server.Plugins.SGenshi.SGenshiTemplateFile):
- self.bundle_names(bundle)
-
- @classmethod
- def Errors(cls):
- return {"bundle-not-found":"error",
- "inconsistent-bundle-name":"warning"}
-
- def missing_bundles(self):
- """ find bundles listed in Metadata but not implemented in Bundler """
- if self.files is None:
- # when given a list of files on stdin, this check is
- # useless, so skip it
- groupdata = self.metadata.groups_xml.xdata
- ref_bundles = set([b.get("name")
- for b in groupdata.findall("//Bundle")])
-
- allbundles = self.core.plugins['Bundler'].entries.keys()
- for bundle in ref_bundles:
- xmlbundle = "%s.xml" % bundle
- genshibundle = "%s.genshi" % bundle
- if (xmlbundle not in allbundles and
- genshibundle not in allbundles):
- self.LintError("bundle-not-found",
- "Bundle %s referenced, but does not exist" %
- bundle)
-
- def bundle_names(self, bundle):
- """ verify bundle name attribute matches filename """
- try:
- xdata = lxml.etree.XML(bundle.data)
- except AttributeError:
- # genshi template
- xdata = lxml.etree.parse(bundle.template.filepath).getroot()
-
- fname = bundle.name.split('Bundler/')[1].split('.')[0]
- bname = xdata.get('name')
- if fname != bname:
- self.LintError("inconsistent-bundle-name",
- "Inconsistent bundle name: filename is %s, bundle name is %s" %
- (fname, bname))
diff --git a/src/lib/Bcfg2/Server/Lint/Deltas.py b/src/lib/Bcfg2/Server/Lint/Deltas.py
deleted file mode 100644
index 114f2e348..000000000
--- a/src/lib/Bcfg2/Server/Lint/Deltas.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import Bcfg2.Server.Lint
-from Bcfg2.Server.Plugins.Cfg import CfgFilter
-
-class Deltas(Bcfg2.Server.Lint.ServerPlugin):
- """ Warn about usage of .cat and .diff files """
-
- def Run(self):
- """ run plugin """
- if 'Cfg' in self.core.plugins:
- cfg = self.core.plugins['Cfg']
- for basename, entry in list(cfg.entries.items()):
- self.check_entry(basename, entry)
-
- @classmethod
- def Errors(cls):
- return {"cat-file-used":"warning",
- "diff-file-used":"warning"}
-
- def check_entry(self, basename, entry):
- for fname, processor in entry.entries.items():
- if self.HandlesFile(fname) and isinstance(processor, CfgFilter):
- extension = fname.split(".")[-1]
- self.LintError("%s-file-used" % extension,
- "%s file used on %s: %s" %
- (extension, basename, fname))
diff --git a/src/lib/Bcfg2/Server/Lint/Genshi.py b/src/lib/Bcfg2/Server/Lint/Genshi.py
index b6007161e..74142b446 100755
--- a/src/lib/Bcfg2/Server/Lint/Genshi.py
+++ b/src/lib/Bcfg2/Server/Lint/Genshi.py
@@ -1,3 +1,4 @@
+import sys
import genshi.template
import Bcfg2.Server.Lint
diff --git a/src/lib/Bcfg2/Server/Lint/GroupPatterns.py b/src/lib/Bcfg2/Server/Lint/GroupPatterns.py
deleted file mode 100644
index 431ba4056..000000000
--- a/src/lib/Bcfg2/Server/Lint/GroupPatterns.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import sys
-import Bcfg2.Server.Lint
-from Bcfg2.Server.Plugins.GroupPatterns import PatternMap
-
-class GroupPatterns(Bcfg2.Server.Lint.ServerPlugin):
- """ Check Genshi templates for syntax errors """
-
- def Run(self):
- """ run plugin """
- if 'GroupPatterns' in self.core.plugins:
- cfg = self.core.plugins['GroupPatterns'].config
- for entry in cfg.xdata.xpath('//GroupPattern'):
- groups = [g.text for g in entry.findall('Group')]
- self.check(entry, groups, ptype='NamePattern')
- self.check(entry, groups, ptype='NameRange')
-
- @classmethod
- def Errors(cls):
- return {"pattern-fails-to-initialize":"error"}
-
- def check(self, entry, groups, ptype="NamePattern"):
- if ptype == "NamePattern":
- pmap = lambda p: PatternMap(p, None, groups)
- else:
- pmap = lambda p: PatternMap(None, p, groups)
-
- for el in entry.findall(ptype):
- pat = el.text
- try:
- pmap(pat)
- except:
- err = sys.exc_info()[1]
- self.LintError("pattern-fails-to-initialize",
- "Failed to initialize %s %s for %s: %s" %
- (ptype, pat, entry.get('pattern'), err))
diff --git a/src/lib/Bcfg2/Server/Lint/InfoXML.py b/src/lib/Bcfg2/Server/Lint/InfoXML.py
index db6aeea73..3884c1ed4 100644
--- a/src/lib/Bcfg2/Server/Lint/InfoXML.py
+++ b/src/lib/Bcfg2/Server/Lint/InfoXML.py
@@ -6,8 +6,10 @@ from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
class InfoXML(Bcfg2.Server.Lint.ServerPlugin):
""" ensure that all config files have an info.xml file"""
def Run(self):
- if 'Cfg' in self.core.plugins:
- for filename, entryset in self.core.plugins['Cfg'].entries.items():
+ for plugin in ['Cfg', 'TCheetah', 'TGenshi']:
+ if plugin not in self.core.plugins:
+ continue
+ for filename, entryset in self.core.plugins[plugin].entries.items():
infoxml_fname = os.path.join(entryset.path, "info.xml")
if self.HandlesFile(infoxml_fname):
found = False
diff --git a/src/lib/Bcfg2/Server/Lint/Pkgmgr.py b/src/lib/Bcfg2/Server/Lint/Pkgmgr.py
deleted file mode 100644
index ceb46238a..000000000
--- a/src/lib/Bcfg2/Server/Lint/Pkgmgr.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import glob
-import lxml.etree
-import Bcfg2.Server.Lint
-
-class Pkgmgr(Bcfg2.Server.Lint.ServerlessPlugin):
- """ find duplicate Pkgmgr entries with the same priority """
- def Run(self):
- pset = set()
- for pfile in glob.glob("%s/Pkgmgr/*.xml" % self.config['repo']):
- if self.HandlesFile(pfile):
- xdata = lxml.etree.parse(pfile).getroot()
- # get priority, type, group
- priority = xdata.get('priority')
- ptype = xdata.get('type')
- for pkg in xdata.xpath("//Package"):
- if pkg.getparent().tag == 'Group':
- grp = pkg.getparent().get('name')
- if (type(grp) is not str and
- grp.getparent().tag == 'Group'):
- pgrp = grp.getparent().get('name')
- else:
- pgrp = 'none'
- else:
- grp = 'none'
- pgrp = 'none'
- ptuple = (pkg.get('name'), priority, ptype, grp, pgrp)
- # check if package is already listed with same
- # priority, type, grp
- if ptuple in pset:
- self.LintError("duplicate-package",
- "Duplicate Package %s, priority:%s, type:%s" %
- (pkg.get('name'), priority, ptype))
- else:
- pset.add(ptuple)
-
- @classmethod
- def Errors(cls):
- return {"duplicate-packages":"error"}
diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
index 6f76cf2db..0976ed9dd 100644
--- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
+++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
@@ -1,32 +1,95 @@
-import os.path
+import os
+import re
import lxml.etree
import Bcfg2.Server.Lint
+import Bcfg2.Client.Tools.POSIX
+import Bcfg2.Client.Tools.VCS
from Bcfg2.Server.Plugins.Packages import Apt, Yum
+from Bcfg2.Server.Plugins.Bundler import have_genshi
+if have_genshi:
+ from Bcfg2.Server.Plugins.SGenshi import SGenshiTemplateFile
+
+# format verifying functions
+def is_filename(val):
+ return val.startswith("/") and len(val) > 1
+
+def is_selinux_type(val):
+ return re.match(r'^[a-z_]+_t', val)
+
+def is_selinux_user(val):
+ return re.match(r'^[a-z_]+_u', val)
+
+def is_octal_mode(val):
+ return re.match(r'[0-7]{3,4}', val)
+
+def is_username(val):
+ return re.match(r'^([a-z]\w{0,30}|\d+)$', val)
+
+def is_device_mode(val):
+ try:
+ # checking upper bound seems like a good way to discover some
+ # obscure OS with >8-bit device numbers
+ return int(val) > 0
+ except:
+ return False
class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
""" verify attributes for configuration entries (as defined in
doc/server/configurationentries) """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
- self.required_attrs = {
- 'Path': {
- 'device': ['name', 'owner', 'group', 'dev_type'],
- 'directory': ['name', 'owner', 'group', 'perms'],
- 'file': ['name', 'owner', 'group', 'perms', '__text__'],
- 'hardlink': ['name', 'to'],
- 'symlink': ['name', 'to'],
- 'ignore': ['name'],
- 'nonexistent': ['name'],
- 'permissions': ['name', 'owner', 'group', 'perms'],
- 'vcs': ['vcstype', 'revision', 'sourceurl']},
- 'Service': {
- 'chkconfig': ['name'],
- 'deb': ['name'],
- 'rc-update': ['name'],
- 'smf': ['name', 'FMRI'],
- 'upstart': ['name']},
- 'Action': ['name', 'timing', 'when', 'status', 'command'],
- 'Package': ['name']}
+ self.required_attrs = dict(
+ Path=dict(
+ device=dict(name=is_filename, owner=is_username,
+ group=is_username,
+ dev_type=lambda v: \
+ v in Bcfg2.Client.Tools.POSIX.device_map),
+ directory=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode),
+ file=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode,
+ __text__=None),
+ hardlink=dict(name=is_filename, to=is_filename),
+ symlink=dict(name=is_filename, to=is_filename),
+ ignore=dict(name=is_filename),
+ nonexistent=dict(name=is_filename),
+ permissions=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode),
+ vcs=dict(vcstype=lambda v: (v != 'Path' and
+ hasattr(Bcfg2.Client.Tools.VCS,
+ "Install%s" % v)),
+ revision=None, sourceurl=None)),
+ Service={
+ "chkconfig": dict(name=None),
+ "deb": dict(name=None),
+ "rc-update": dict(name=None),
+ "smf": dict(name=None, FMRI=None),
+ "upstart": dict(name=None)},
+ Action={None: dict(name=None,
+ timing=lambda v: v in ['pre', 'post', 'both'],
+ when=lambda v: v in ['modified', 'always'],
+ status=lambda v: v in ['ignore', 'check'],
+ command=None)},
+ Package={None: dict(name=None)},
+ SELinux=dict(
+ boolean=dict(name=None,
+ value=lambda v: v in ['on', 'off']),
+ module=dict(name=None, __text__=None),
+ port=dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)', v),
+ selinuxtype=is_selinux_type),
+ fcontext=dict(name=None, selinuxtype=is_selinux_type),
+ node=dict(name=lambda v: "/" in v,
+ selinuxtype=is_selinux_type,
+ proto=lambda v: v in ['ipv6', 'ipv4']),
+ login=dict(name=is_username,
+ selinuxuser=is_selinux_user),
+ user=dict(name=is_selinux_user,
+ roles=lambda v: all(is_selinux_user(u)
+ for u in " ".split(v)),
+ prefix=None),
+ interface=dict(name=None, selinuxtype=is_selinux_type),
+ permissive=dict(name=is_selinux_type))
+ )
def Run(self):
self.check_packages()
@@ -42,9 +105,9 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
return {"unknown-entry-type":"error",
"unknown-entry-tag":"error",
"required-attrs-missing":"error",
+ "required-attr-format":"error",
"extra-attrs":"warning"}
-
def check_packages(self):
""" check package sources for Source entries with missing attrs """
if 'Packages' in self.core.plugins:
@@ -85,13 +148,17 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
""" check bundles for BoundPath entries with missing attrs """
if 'Bundler' in self.core.plugins:
for bundle in self.core.plugins['Bundler'].entries.values():
- try:
- xdata = lxml.etree.XML(bundle.data)
- except (lxml.etree.XMLSyntaxError, AttributeError):
- xdata = lxml.etree.parse(bundle.template.filepath).getroot()
+ if (self.HandlesFile(bundle.name) and
+ (not have_genshi or
+ not isinstance(bundle, SGenshiTemplateFile))):
+ try:
+ xdata = lxml.etree.XML(bundle.data)
+ except (lxml.etree.XMLSyntaxError, AttributeError):
+ xdata = \
+ lxml.etree.parse(bundle.template.filepath).getroot()
- for path in xdata.xpath("//*[substring(name(), 1, 5) = 'Bound']"):
- self.check_entry(path, bundle.name)
+ for path in xdata.xpath("//*[substring(name(), 1, 5) = 'Bound']"):
+ self.check_entry(path, bundle.name)
def check_entry(self, entry, filename):
""" generic entry check """
@@ -103,43 +170,52 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
if tag not in self.required_attrs:
self.LintError("unknown-entry-tag",
"Unknown entry tag '%s': %s" %
- (entry.tag, self.RenderXML(entry)))
+ (tag, self.RenderXML(entry)))
if isinstance(self.required_attrs[tag], dict):
etype = entry.get('type')
if etype in self.required_attrs[tag]:
- required_attrs = set(self.required_attrs[tag][etype] +
- ['type'])
+ required_attrs = self.required_attrs[tag][etype]
else:
self.LintError("unknown-entry-type",
"Unknown %s type %s: %s" %
(tag, etype, self.RenderXML(entry)))
return
else:
- required_attrs = set(self.required_attrs[tag])
+ required_attrs = self.required_attrs[tag]
attrs = set(entry.attrib.keys())
if 'dev_type' in required_attrs:
dev_type = entry.get('dev_type')
if dev_type in ['block', 'char']:
# check if major/minor are specified
- required_attrs |= set(['major', 'minor'])
+ required_attrs['major'] = is_device_mode
+ required_attrs['minor'] = is_device_mode
if '__text__' in required_attrs:
- required_attrs.remove('__text__')
+ del required_attrs['__text__']
if (not entry.text and
not entry.get('empty', 'false').lower() == 'true'):
self.LintError("required-attrs-missing",
"Text missing for %s %s in %s: %s" %
- (entry.tag, name, filename,
+ (tag, name, filename,
self.RenderXML(entry)))
- if not attrs.issuperset(required_attrs):
+ if not attrs.issuperset(required_attrs.keys()):
self.LintError("required-attrs-missing",
"The following required attribute(s) are "
"missing for %s %s in %s: %s\n%s" %
- (entry.tag, name, filename,
+ (tag, name, filename,
", ".join([attr
for attr in
- required_attrs.difference(attrs)]),
+ set(required_attrs.keys()).difference(attrs)]),
self.RenderXML(entry)))
+
+ for attr, fmt in required_attrs.items():
+ if fmt and attr in attrs and not fmt(entry.attrib[attr]):
+ self.LintError("required-attr-format",
+ "The %s attribute of %s %s in %s is "
+ "malformed\n%s" %
+ (attr, tag, name, filename,
+ self.RenderXML(entry)))
+
diff --git a/src/lib/Bcfg2/Server/Lint/TemplateHelper.py b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py
deleted file mode 100644
index be270a59c..000000000
--- a/src/lib/Bcfg2/Server/Lint/TemplateHelper.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import sys
-import imp
-import glob
-import Bcfg2.Server.Lint
-from Bcfg2.Server.Plugins.TemplateHelper import HelperModule
-
-class TemplateHelper(Bcfg2.Server.Lint.ServerlessPlugin):
- """ find duplicate Pkgmgr entries with the same priority """
- def __init__(self, *args, **kwargs):
- Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs)
- hm = HelperModule("foo.py", None, None)
- self.reserved_keywords = dir(hm)
-
- def Run(self):
- for helper in glob.glob("%s/TemplateHelper/*.py" % self.config['repo']):
- if not self.HandlesFile(helper):
- continue
-
- match = HelperModule._module_name_re.search(helper)
- if match:
- module_name = match.group(1)
- else:
- module_name = helper
-
- try:
- module = imp.load_source(module_name, helper)
- except:
- err = sys.exc_info()[1]
- self.LintError("templatehelper-import-error",
- "Failed to import %s: %s" %
- (helper, err))
- continue
-
- if not hasattr(module, "__export__"):
- self.LintError("templatehelper-no-export",
- "%s has no __export__ list" % helper)
- continue
- elif not isinstance(module.__export__, list):
- self.LintError("templatehelper-nonlist-export",
- "__export__ is not a list in %s" % helper)
- continue
-
- for sym in module.__export__:
- if not hasattr(module, sym):
- self.LintError("templatehelper-nonexistent-export",
- "%s: exported symbol %s does not exist" %
- (helper, sym))
- elif sym in self.reserved_keywords:
- self.LintError("templatehelper-reserved-export",
- "%s: exported symbol %s is reserved" %
- (helper, sym))
- elif sym.startswith("_"):
- self.LintError("templatehelper-underscore-export",
- "%s: exported symbol %s starts with underscore" %
- (helper, sym))
-
- @classmethod
- def Errors(cls):
- return {"templatehelper-import-error":"error",
- "templatehelper-no-export":"error",
- "templatehelper-nonlist-export":"error",
- "templatehelper-nonexistent-export":"error",
- "templatehelper-reserved-export":"error",
- "templatehelper-underscore-export":"warning"}
diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py
index 05fedc313..aa5cfe62c 100644
--- a/src/lib/Bcfg2/Server/Lint/Validate.py
+++ b/src/lib/Bcfg2/Server/Lint/Validate.py
@@ -46,20 +46,10 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
if filelist:
# avoid loading schemas for empty file lists
schemafile = schemaname % schemadir
- try:
- schema = lxml.etree.XMLSchema(lxml.etree.parse(schemafile))
- except IOError:
- e = sys.exc_info()[1]
- self.LintError("input-output-error", str(e))
- continue
- except lxml.etree.XMLSchemaParseError:
- e = sys.exc_info()[1]
- self.LintError("schema-failed-to-parse",
- "Failed to process schema %s: %s" %
- (schemafile, e))
- continue
- for filename in filelist:
- self.validate(filename, schemafile, schema=schema)
+ schema = self._load_schema(schemafile)
+ if schema:
+ for filename in filelist:
+ self.validate(filename, schemafile, schema=schema)
self.check_properties()
@@ -88,11 +78,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
return True on success, False on failure """
if schema is None:
# if no schema object was provided, instantiate one
- try:
- schema = lxml.etree.XMLSchema(lxml.etree.parse(schemafile))
- except:
- self.LintError("schema-failed-to-parse",
- "Failed to process schema %s" % schemafile)
+ schema = self._load_schema(schemafile)
+ if not schema:
return False
try:
@@ -208,3 +195,15 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
return rv
+ def _load_schema(self, filename):
+ try:
+ return lxml.etree.XMLSchema(lxml.etree.parse(filename))
+ except IOError:
+ e = sys.exc_info()[1]
+ self.LintError("input-output-error", str(e))
+ except lxml.etree.XMLSchemaParseError:
+ e = sys.exc_info()[1]
+ self.LintError("schema-failed-to-parse",
+ "Failed to process schema %s: %s" %
+ (filename, e))
+ return None
diff --git a/src/lib/Bcfg2/Server/Plugin.py b/src/lib/Bcfg2/Server/Plugin.py
index ca37431a2..d035b83d4 100644
--- a/src/lib/Bcfg2/Server/Plugin.py
+++ b/src/lib/Bcfg2/Server/Plugin.py
@@ -9,10 +9,9 @@ import posixpath
import re
import sys
import threading
+import Bcfg2.Server
from Bcfg2.Bcfg2Py3k import ConfigParser
-from lxml.etree import XML, XMLSyntaxError
-
import Bcfg2.Options
# py3k compatibility
@@ -25,11 +24,17 @@ from Bcfg2.Bcfg2Py3k import Queue
from Bcfg2.Bcfg2Py3k import Empty
from Bcfg2.Bcfg2Py3k import Full
+# make encoding available
+encparse = Bcfg2.Options.OptionParser({'encoding': Bcfg2.Options.ENCODING})
+encparse.parse([])
+encoding = encparse['encoding']
+
# grab default metadata info from bcfg2.conf
opts = {'owner': Bcfg2.Options.MDATA_OWNER,
'group': Bcfg2.Options.MDATA_GROUP,
- 'important': Bcfg2.Options.MDATA_IMPORTANT,
'perms': Bcfg2.Options.MDATA_PERMS,
+ 'secontext': Bcfg2.Options.MDATA_SECONTEXT,
+ 'important': Bcfg2.Options.MDATA_IMPORTANT,
'paranoid': Bcfg2.Options.MDATA_PARANOID,
'sensitive': Bcfg2.Options.MDATA_SENSITIVE}
mdata_setup = Bcfg2.Options.OptionParser(opts)
@@ -48,8 +53,22 @@ info_regex = re.compile( \
'owner:(\s)*(?P<owner>\S+)|' +
'paranoid:(\s)*(?P<paranoid>\S+)|' +
'perms:(\s)*(?P<perms>\w+)|' +
+ 'secontext:(\s)*(?P<secontext>\S+)|' +
'sensitive:(\s)*(?P<sensitive>\S+)|')
+def bind_info(entry, metadata, infoxml=None, default=default_file_metadata):
+ for attr, val in list(default.items()):
+ entry.set(attr, val)
+ if infoxml:
+ mdata = dict()
+ infoxml.pnode.Match(metadata, mdata, entry=entry)
+ if 'Info' not in mdata:
+ msg = "Failed to set metadata for file %s" % entry.get('name')
+ logger.error(msg)
+ raise PluginExecutionError(msg)
+ for attr, val in list(mdata['Info'][None].items()):
+ entry.set(attr, val)
+
class PluginInitError(Exception):
"""Error raised in cases of Plugin initialization errors."""
@@ -73,6 +92,10 @@ class Debuggable(object):
def toggle_debug(self):
self.debug_flag = not self.debug_flag
+ self.debug_log("%s: debug_flag = %s" % (self.__class__.__name__,
+ self.debug_flag),
+ flag=True)
+ return self.debug_flag
def debug_log(self, message, flag=None):
if (flag is None and self.debug_flag) or flag:
@@ -203,7 +226,7 @@ class ThreadedStatistics(Statistics,
self.terminate = core.terminate
self.work_queue = Queue(100000)
self.pending_file = "%s/etc/%s.pending" % (datastore, self.__class__.__name__)
- self.daemon = True
+ self.daemon = False
self.start()
def save(self):
@@ -256,7 +279,9 @@ class ThreadedStatistics(Statistics,
if self.terminate.isSet():
return False
- self.work_queue.put_nowait((metadata, lxml.etree.fromstring(pdata)))
+ self.work_queue.put_nowait((metadata,
+ lxml.etree.XML(pdata,
+ parser=Bcfg2.Server.XMLParser)))
except Full:
self.logger.warning("Queue.Full: Failed to load queue data")
break
@@ -275,7 +300,7 @@ class ThreadedStatistics(Statistics,
def run(self):
if not self.load():
return
- while not self.terminate.isSet():
+ while not self.terminate.isSet() and self.work_queue != None:
try:
(xdata, client) = self.work_queue.get(block=True, timeout=2)
except Empty:
@@ -285,7 +310,7 @@ class ThreadedStatistics(Statistics,
self.logger.error("ThreadedStatistics: %s" % e)
continue
self.handle_statistic(xdata, client)
- if not self.work_queue.empty():
+ if self.work_queue != None and not self.work_queue.empty():
self.save()
def process_statistics(self, metadata, data):
@@ -352,6 +377,17 @@ class Version(object):
pass
+class ClientRunHooks(object):
+ """ Provides hooks to interact with client runs """
+ def start_client_run(self, metadata):
+ pass
+
+ def end_client_run(self, metadata):
+ pass
+
+ def end_statistics(self, metadata):
+ pass
+
# the rest of the file contains classes for coherent file caching
class FileBacked(object):
@@ -361,10 +397,11 @@ class FileBacked(object):
This object is meant to be used as a part of DirectoryBacked.
"""
- def __init__(self, name):
+ def __init__(self, name, fam=None):
object.__init__(self)
self.data = ''
self.name = name
+ self.fam = fam
def HandleEvent(self, event=None):
"""Read file upon update."""
@@ -382,10 +419,7 @@ class FileBacked(object):
pass
def __repr__(self):
- return "%s: %s" % (self.__class__.__name__, str(self))
-
- def __str__(self):
- return "%s: %s" % (self.name, self.data)
+ return "%s: %s" % (self.__class__.__name__, self.name)
class DirectoryBacked(object):
@@ -453,7 +487,8 @@ class DirectoryBacked(object):
added.
"""
self.entries[relative] = self.__child__(os.path.join(self.data,
- relative))
+ relative),
+ self.fam)
self.entries[relative].HandleEvent(event)
def HandleEvent(self, event):
@@ -480,8 +515,8 @@ class DirectoryBacked(object):
return
if event.requestID not in self.handles:
- logger.warn("Got %s event with unknown handle (%s) for %s"
- % (action, event.requestID, abspath))
+ logger.warn("Got %s event with unknown handle (%s) for %s" %
+ (action, event.requestID, event.filename))
return
# Calculate the absolute and relative paths this event refers to
@@ -522,21 +557,13 @@ class DirectoryBacked(object):
# didn't know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
- logger.warn("Got %s event for unexpected dir %s" % (action,
- abspath))
+ logger.warn("Got %s event for unexpected dir %s" %
+ (action, abspath))
self.add_directory_monitor(relpath)
else:
- logger.warn("Got unknown dir event %s %s %s" % (event.requestID,
- event.code2str(),
- abspath))
+ logger.warn("Got unknown dir event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
else:
- # Deal with events for non-directories
- if ((event.filename[-1] == '~') or
- (event.filename[:2] == '.#') or
- (event.filename[-4:] == '.swp') or
- (event.filename in ['SCCS', '.svn', '4913']) or
- (not self.patterns.match(event.filename))):
- return
if action in ['exists', 'created']:
self.add_entry(relpath, event)
elif action == 'changed':
@@ -547,13 +574,13 @@ class DirectoryBacked(object):
# know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
- logger.warn("Got %s event for unexpected file %s" % (action,
- abspath))
+ logger.warn("Got %s event for unexpected file %s" %
+ (action,
+ abspath))
self.add_entry(relpath, event)
else:
- logger.warn("Got unknown file event %s %s %s" % (event.requestID,
- event.code2str(),
- abspath))
+ logger.warn("Got unknown file event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
class XMLFileBacked(FileBacked):
@@ -563,40 +590,18 @@ class XMLFileBacked(FileBacked):
"""
__identifier__ = 'name'
- def __init__(self, filename):
- self.label = "dummy"
- self.entries = []
+ def __init__(self, filename, fam=None, should_monitor=False):
FileBacked.__init__(self, filename)
-
- def Index(self):
- """Build local data structures."""
- try:
- self.xdata = XML(self.data)
- except XMLSyntaxError:
- logger.error("Failed to parse %s" % (self.name))
- return
- self.entries = self.xdata.getchildren()
- if self.__identifier__ is not None:
- self.label = self.xdata.attrib[self.__identifier__]
-
- def __iter__(self):
- return iter(self.entries)
-
- def __str__(self):
- return "%s: %s" % (self.name, lxml.etree.tostring(self.xdata))
-
-
-class SingleXMLFileBacked(XMLFileBacked):
- """This object is a coherent cache for an independent XML file."""
- def __init__(self, filename, fam):
- XMLFileBacked.__init__(self, filename)
+ self.label = ""
+ self.entries = []
self.extras = []
self.fam = fam
- self.fam.AddMonitor(filename, self)
+ self.should_monitor = should_monitor
+ if fam and should_monitor:
+ self.fam.AddMonitor(filename, self)
def _follow_xincludes(self, fname=None, xdata=None):
- ''' follow xincludes, adding included files to fam and to
- self.extras '''
+ ''' follow xincludes, adding included files to self.extras '''
if xdata is None:
if fname is None:
xdata = self.xdata.getroottree()
@@ -610,17 +615,14 @@ class SingleXMLFileBacked(XMLFileBacked):
fpath = name
else:
fpath = os.path.join(os.path.dirname(self.name), name)
- self.add_monitor(fpath, name)
self._follow_xincludes(fname=fpath)
-
- def add_monitor(self, fpath, fname):
- self.fam.AddMonitor(fpath, self)
- self.extras.append(fname)
+ self.add_monitor(fpath, name)
def Index(self):
"""Build local data structures."""
try:
- self.xdata = lxml.etree.XML(self.data, base_url=self.name)
+ self.xdata = lxml.etree.XML(self.data, base_url=self.name,
+ parser=Bcfg2.Server.XMLParser)
except lxml.etree.XMLSyntaxError:
err = sys.exc_info()[1]
logger.error("Failed to parse %s: %s" % (self.name, err))
@@ -638,43 +640,63 @@ class SingleXMLFileBacked(XMLFileBacked):
if self.__identifier__ is not None:
self.label = self.xdata.attrib[self.__identifier__]
+ def add_monitor(self, fpath, fname):
+ self.extras.append(fname)
+ if self.fam:
+ self.fam.AddMonitor(fpath, self)
+
+ def __iter__(self):
+ return iter(self.entries)
+
+ def __str__(self):
+ return "%s at %s" % (self.__class__.__name__, self.name)
+
class StructFile(XMLFileBacked):
"""This file contains a set of structure file formatting logic."""
__identifier__ = None
- def __init__(self, name):
- XMLFileBacked.__init__(self, name)
-
- def _match(self, item, metadata):
- """ recursive helper for Match() """
- if isinstance(item, lxml.etree._Comment):
- return []
- elif item.tag == 'Group':
- rv = []
+ def _include_element(self, item, metadata):
+ """ determine if an XML element matches the metadata """
+ if item.tag == 'Group':
if ((item.get('negate', 'false').lower() == 'true' and
item.get('name') not in metadata.groups) or
(item.get('negate', 'false').lower() == 'false' and
item.get('name') in metadata.groups)):
- for child in item.iterchildren():
- rv.extend(self._match(child, metadata))
- return rv
+ return True
+ else:
+ return False
elif item.tag == 'Client':
- rv = []
if ((item.get('negate', 'false').lower() == 'true' and
item.get('name') != metadata.hostname) or
(item.get('negate', 'false').lower() == 'false' and
item.get('name') == metadata.hostname)):
+ return True
+ else:
+ return False
+ elif isinstance(item, lxml.etree._Comment):
+ return False
+ else:
+ return True
+
+ def _match(self, item, metadata):
+ """ recursive helper for Match() """
+ if self._include_element(item, metadata):
+ if item.tag == 'Group' or item.tag == 'Client':
+ rv = []
+ if self._include_element(item, metadata):
+ for child in item.iterchildren():
+ rv.extend(self._match(child, metadata))
+ return rv
+ else:
+ rv = copy.copy(item)
+ for child in rv.iterchildren():
+ rv.remove(child)
for child in item.iterchildren():
rv.extend(self._match(child, metadata))
- return rv
+ return [rv]
else:
- rv = copy.copy(item)
- for child in rv.iterchildren():
- rv.remove(child)
- for child in item.iterchildren():
- rv.extend(self._match(child, metadata))
- return [rv]
+ return []
def Match(self, metadata):
"""Return matching fragments of independent."""
@@ -683,6 +705,29 @@ class StructFile(XMLFileBacked):
rv.extend(self._match(child, metadata))
return rv
+ def _xml_match(self, item, metadata):
+ """ recursive helper for XMLMatch """
+ if self._include_element(item, metadata):
+ if item.tag == 'Group' or item.tag == 'Client':
+ for child in item.iterchildren():
+ item.remove(child)
+ item.getparent().append(child)
+ self._xml_match(child, metadata)
+ item.getparent().remove(item)
+ else:
+ for child in item.iterchildren():
+ self._xml_match(child, metadata)
+ else:
+ item.getparent().remove(item)
+
+ def XMLMatch(self, metadata):
+ """ Return a rebuilt XML document that only contains the
+ matching portions """
+ rv = copy.deepcopy(self.xdata)
+ for child in rv.iterchildren():
+ self._xml_match(child, metadata)
+ return rv
+
class INode:
"""
@@ -761,8 +806,8 @@ class XMLSrc(XMLFileBacked):
__node__ = INode
__cacheobj__ = dict
- def __init__(self, filename, noprio=False):
- XMLFileBacked.__init__(self, filename)
+ def __init__(self, filename, fam=None, should_monitor=False, noprio=False):
+ XMLFileBacked.__init__(self, filename, fam, should_monitor)
self.items = {}
self.cache = None
self.pnode = None
@@ -778,7 +823,7 @@ class XMLSrc(XMLFileBacked):
return
self.items = {}
try:
- xdata = lxml.etree.XML(data)
+ xdata = lxml.etree.XML(data, parser=Bcfg2.Server.XMLParser)
except lxml.etree.XMLSyntaxError:
logger.error("Failed to parse file %s" % (self.name))
return
@@ -814,6 +859,7 @@ class InfoXML(XMLSrc):
class XMLDirectoryBacked(DirectoryBacked):
"""Directorybacked for *.xml."""
patterns = re.compile('.*\.xml')
+ __child__ = XMLFileBacked
class PrioDir(Plugin, Generator, XMLDirectoryBacked):
@@ -880,6 +926,9 @@ class PrioDir(Plugin, Generator, XMLDirectoryBacked):
if self._matches(entry, metadata, [rname]):
data = matching[index].cache[1][entry.tag][rname]
break
+ else:
+ # Fall back on __getitem__. Required if override used
+ data = matching[index].cache[1][entry.tag][entry.get('name')]
if '__text__' in data:
entry.text = data['__text__']
if '__children__' in data:
@@ -970,10 +1019,6 @@ class EntrySet(Debuggable):
pattern += '(G(?P<prio>\d+)_(?P<group>\S+))))?$'
self.specific = re.compile(pattern)
- def debug_log(self, message, flag=None):
- if (flag is None and self.debug_flag) or flag:
- logger.error(message)
-
def sort_by_specific(self, one, other):
return cmp(one.specific, other.specific)
@@ -1104,18 +1149,7 @@ class EntrySet(Debuggable):
return cmp(x.specific.prio, y.specific.prio)
def bind_info_to_entry(self, entry, metadata):
- # first set defaults from global metadata/:info
- for key in self.metadata:
- entry.set(key, self.metadata[key])
- if self.infoxml:
- mdata = {}
- self.infoxml.pnode.Match(metadata, mdata, entry=entry)
- if 'Info' not in mdata:
- logger.error("Failed to set metadata for file %s" % \
- (entry.get('name')))
- raise PluginExecutionError
- [entry.attrib.__setitem__(key, value) \
- for (key, value) in list(mdata['Info'][None].items())]
+ bind_info(entry, metadata, infoxml=self.infoxml, default=self.metadata)
def bind_entry(self, entry, metadata):
"""Return the appropriate interpreted template from the set of available templates."""
@@ -1130,13 +1164,14 @@ class GroupSpool(Plugin, Generator):
filename_pattern = ""
es_child_cls = object
es_cls = EntrySet
+ entry_type = 'Path'
def __init__(self, core, datastore):
Plugin.__init__(self, core, datastore)
Generator.__init__(self)
if self.data[-1] == '/':
self.data = self.data[:-1]
- self.Entries['Path'] = {}
+ self.Entries[self.entry_type] = {}
self.entries = {}
self.handles = {}
self.AddDirectoryMonitor('')
@@ -1153,7 +1188,8 @@ class GroupSpool(Plugin, Generator):
dirpath,
self.es_child_cls,
self.encoding)
- self.Entries['Path'][ident] = self.entries[ident].bind_entry
+ self.Entries[self.entry_type][ident] = \
+ self.entries[ident].bind_entry
if not posixpath.isdir(epath):
# do not pass through directory events
self.entries[ident].handle_event(event)
@@ -1169,6 +1205,12 @@ class GroupSpool(Plugin, Generator):
else:
return self.handles[event.requestID][:-1]
+ def toggle_debug(self):
+ for entry in self.entries.values():
+ if hasattr(entry, "toggle_debug"):
+ entry.toggle_debug()
+ return Plugin.toggle_debug()
+
def HandleEvent(self, event):
"""Unified FAM event handler for GroupSpool."""
action = event.code2str()
@@ -1193,7 +1235,7 @@ class GroupSpool(Plugin, Generator):
if fbase in self.entries:
# a directory was deleted
del self.entries[fbase]
- del self.Entries['Path'][fbase]
+ del self.Entries[self.entry_type][fbase]
elif ident in self.entries:
self.entries[ident].handle_event(event)
elif ident not in self.entries:
@@ -1207,7 +1249,7 @@ class GroupSpool(Plugin, Generator):
name = self.data + relative
if relative not in list(self.handles.values()):
if not posixpath.isdir(name):
- print("Failed to open directory %s" % (name))
+ self.logger.error("Failed to open directory %s" % name)
return
reqid = self.core.fam.AddMonitor(name, self)
self.handles[reqid] = relative
diff --git a/src/lib/Bcfg2/Server/Plugins/BB.py b/src/lib/Bcfg2/Server/Plugins/BB.py
index c015ec47c..bd518ad19 100644
--- a/src/lib/Bcfg2/Server/Plugins/BB.py
+++ b/src/lib/Bcfg2/Server/Plugins/BB.py
@@ -1,4 +1,5 @@
import lxml.etree
+import Bcfg2.Server
import Bcfg2.Server.Plugin
import glob
import os
@@ -16,7 +17,8 @@ class BBfile(Bcfg2.Server.Plugin.XMLFileBacked):
"""Build data into an xml object."""
try:
- self.data = lxml.etree.XML(self.data)
+ self.data = lxml.etree.XML(self.data,
+ parser=Bcfg2.Server.XMLParser)
except lxml.etree.XMLSyntaxError:
Bcfg2.Server.Plugin.logger.error("Failed to parse %s" % self.name)
return
diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py
index ccb99481e..26fe1d822 100644
--- a/src/lib/Bcfg2/Server/Plugins/Bundler.py
+++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py
@@ -6,20 +6,19 @@ import os
import os.path
import re
import sys
-
+import Bcfg2.Server
import Bcfg2.Server.Plugin
+import Bcfg2.Server.Lint
try:
- import genshi.template
import genshi.template.base
- import Bcfg2.Server.Plugins.SGenshi
+ from Bcfg2.Server.Plugins.SGenshi import SGenshiTemplateFile
have_genshi = True
except:
have_genshi = False
class BundleFile(Bcfg2.Server.Plugin.StructFile):
-
def get_xml_value(self, metadata):
bundlename = os.path.splitext(os.path.basename(self.name))[0]
bundle = lxml.etree.Element('Bundle', name=bundlename)
@@ -50,25 +49,20 @@ class Bundler(Bcfg2.Server.Plugin.Plugin,
self.logger.error("Failed to load Bundle repository")
raise Bcfg2.Server.Plugin.PluginInitError
- def template_dispatch(self, name):
- bundle = lxml.etree.parse(name)
+ def template_dispatch(self, name, _):
+ bundle = lxml.etree.parse(name,
+ parser=Bcfg2.Server.XMLParser)
nsmap = bundle.getroot().nsmap
- if name.endswith('.xml'):
- if have_genshi and \
- (nsmap == {'py': 'http://genshi.edgewall.org/'}):
- # allow for genshi bundles with .xml extensions
- spec = Bcfg2.Server.Plugin.Specificity()
- return Bcfg2.Server.Plugins.SGenshi.SGenshiTemplateFile(name,
- spec,
- self.encoding)
- else:
- return BundleFile(name)
- elif name.endswith('.genshi'):
+ if (name.endswith('.genshi') or
+ ('py' in nsmap and
+ nsmap['py'] == 'http://genshi.edgewall.org/')):
if have_genshi:
spec = Bcfg2.Server.Plugin.Specificity()
- return Bcfg2.Server.Plugins.SGenshi.SGenshiTemplateFile(name,
- spec,
- self.encoding)
+ return SGenshiTemplateFile(name, spec, self.encoding)
+ else:
+ raise Bcfg2.Server.Plugin.PluginExecutionError("Genshi not available: %s" % name)
+ else:
+ return BundleFile(name, self.fam)
def BuildStructures(self, metadata):
"""Build all structures for client (metadata)."""
@@ -97,3 +91,54 @@ class Bundler(Bcfg2.Server.Plugin.Plugin,
self.logger.error("Bundler: Unexpected bundler error for %s" %
bundlename, exc_info=1)
return bundleset
+
+
+class BundlerLint(Bcfg2.Server.Lint.ServerPlugin):
+ """ Perform various bundle checks """
+ def Run(self):
+ """ run plugin """
+ self.missing_bundles()
+ for bundle in self.core.plugins['Bundler'].entries.values():
+ if (self.HandlesFile(bundle.name) and
+ (not have_genshi or
+ not isinstance(bundle, SGenshiTemplateFile))):
+ self.bundle_names(bundle)
+
+ @classmethod
+ def Errors(cls):
+ return {"bundle-not-found":"error",
+ "inconsistent-bundle-name":"warning"}
+
+ def missing_bundles(self):
+ """ find bundles listed in Metadata but not implemented in Bundler """
+ if self.files is None:
+ # when given a list of files on stdin, this check is
+ # useless, so skip it
+ groupdata = self.metadata.groups_xml.xdata
+ ref_bundles = set([b.get("name")
+ for b in groupdata.findall("//Bundle")])
+
+ allbundles = self.core.plugins['Bundler'].entries.keys()
+ for bundle in ref_bundles:
+ xmlbundle = "%s.xml" % bundle
+ genshibundle = "%s.genshi" % bundle
+ if (xmlbundle not in allbundles and
+ genshibundle not in allbundles):
+ self.LintError("bundle-not-found",
+ "Bundle %s referenced, but does not exist" %
+ bundle)
+
+ def bundle_names(self, bundle):
+ """ verify bundle name attribute matches filename """
+ try:
+ xdata = lxml.etree.XML(bundle.data)
+ except AttributeError:
+ # genshi template
+ xdata = lxml.etree.parse(bundle.template.filepath).getroot()
+
+ fname = bundle.name.split('Bundler/')[1].split('.')[0]
+ bname = xdata.get('name')
+ if fname != bname:
+ self.LintError("inconsistent-bundle-name",
+ "Inconsistent bundle name: filename is %s, "
+ "bundle name is %s" % (fname, bname))
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py
index 3edd1d8cb..e74b77e83 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py
@@ -6,8 +6,7 @@ from Bcfg2.Server.Plugins.Cfg import CfgGenerator
logger = logging.getLogger(__name__)
try:
- import Cheetah.Template
- import Cheetah.Parser
+ from Cheetah.Template import Template
have_cheetah = True
except ImportError:
have_cheetah = False
@@ -25,9 +24,8 @@ class CfgCheetahGenerator(CfgGenerator):
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
def get_data(self, entry, metadata):
- template = Cheetah.Template.Template(self.data,
- compilerSettings=self.settings)
+ template = Template(self.data, compilerSettings=self.settings)
template.metadata = metadata
template.path = entry.get('realname', entry.get('name'))
- template.source_path = self.path
+ template.source_path = self.name
return template.respond()
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedCheetahGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedCheetahGenerator.py
new file mode 100644
index 000000000..a75329d2a
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedCheetahGenerator.py
@@ -0,0 +1,14 @@
+import logging
+from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator import CfgEncryptedGenerator
+
+logger = logging.getLogger(__name__)
+
+class CfgEncryptedCheetahGenerator(CfgCheetahGenerator, CfgEncryptedGenerator):
+ __extensions__ = ['cheetah.crypt', 'crypt.cheetah']
+
+ def handle_event(self, event):
+ CfgEncryptedGenerator.handle_event(self, event)
+
+ def get_data(self, entry, metadata):
+ return CfgCheetahGenerator.get_data(self, entry, metadata)
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
new file mode 100644
index 000000000..2c926fae7
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
@@ -0,0 +1,63 @@
+import logging
+import Bcfg2.Server.Plugin
+from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP
+try:
+ from Bcfg2.Encryption import ssl_decrypt, EVPError
+ have_crypto = True
+except ImportError:
+ have_crypto = False
+
+logger = logging.getLogger(__name__)
+
+def passphrases():
+ section = "encryption"
+ if SETUP.cfp.has_section(section):
+ return dict([(o, SETUP.cfp.get(section, o))
+ for o in SETUP.cfp.options(section)])
+ else:
+ return dict()
+
+def decrypt(crypted):
+ if not have_crypto:
+ msg = "Cfg: M2Crypto is not available: %s" % entry.get("name")
+ logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+ for passwd in passphrases().values():
+ try:
+ return ssl_decrypt(crypted, passwd)
+ except EVPError:
+ pass
+ raise EVPError("Failed to decrypt")
+
+class CfgEncryptedGenerator(CfgGenerator):
+ __extensions__ = ["crypt"]
+
+ def __init__(self, fname, spec, encoding):
+ CfgGenerator.__init__(self, fname, spec, encoding)
+ if not have_crypto:
+ msg = "Cfg: M2Crypto is not available: %s" % entry.get("name")
+ logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def handle_event(self, event):
+ if event.code2str() == 'deleted':
+ return
+ try:
+ crypted = open(self.name).read()
+ except UnicodeDecodeError:
+ crypted = open(self.name, mode='rb').read()
+ except:
+ logger.error("Failed to read %s" % self.name)
+ return
+ # todo: let the user specify a passphrase by name
+ try:
+ self.data = decrypt(crypted)
+ except EVPError:
+ msg = "Failed to decrypt %s" % self.name
+ logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def get_data(self, entry, metadata):
+ if self.data is None:
+ raise Bcfg2.Server.Plugin.PluginExecutionError("Failed to decrypt %s" % self.name)
+ return CfgGenerator.get_data(self, entry, metadata)
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py
new file mode 100644
index 000000000..6605cca7c
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py
@@ -0,0 +1,26 @@
+import logging
+from Bcfg2.Bcfg2Py3k import StringIO
+from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator import decrypt, \
+ CfgEncryptedGenerator
+
+logger = logging.getLogger(__name__)
+
+try:
+ from genshi.template import TemplateLoader
+except ImportError:
+ # CfgGenshiGenerator will raise errors if genshi doesn't exist
+ TemplateLoader = object
+
+
+class EncryptedTemplateLoader(TemplateLoader):
+ def _instantiate(self, cls, fileobj, filepath, filename, encoding=None):
+ plaintext = StringIO(decrypt(fileobj.read()))
+ return TemplateLoader._instantiate(self, cls, plaintext, filepath,
+ filename, encoding=encoding)
+
+
+class CfgEncryptedGenshiGenerator(CfgGenshiGenerator):
+ __extensions__ = ['genshi.crypt', 'crypt.genshi']
+ __loader_cls__ = EncryptedTemplateLoader
+
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
index 2c0a076d7..277a26f97 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
@@ -1,5 +1,7 @@
+import re
import sys
import logging
+import traceback
import Bcfg2.Server.Plugin
from Bcfg2.Server.Plugins.Cfg import CfgGenerator
@@ -8,8 +10,10 @@ logger = logging.getLogger(__name__)
try:
import genshi.core
from genshi.template import TemplateLoader, NewTextTemplate
+ from genshi.template.eval import UndefinedError
have_genshi = True
except ImportError:
+ TemplateLoader = None
have_genshi = False
# snipped from TGenshi
@@ -23,14 +27,17 @@ def removecomment(stream):
class CfgGenshiGenerator(CfgGenerator):
__extensions__ = ['genshi']
+ __loader_cls__ = TemplateLoader
+ pyerror_re = re.compile('<\w+ u?[\'"](.*?)\s*\.\.\.[\'"]>')
def __init__(self, fname, spec, encoding):
CfgGenerator.__init__(self, fname, spec, encoding)
- self.loader = TemplateLoader()
if not have_genshi:
- msg = "Cfg: Genshi is not available: %s" % entry.get("name")
+ msg = "Cfg: Genshi is not available: %s" % fname
logger.error(msg)
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+ self.loader = self.__loader_cls__()
+ self.template = None
@classmethod
def ignore(cls, event, basename=None):
@@ -44,10 +51,63 @@ class CfgGenshiGenerator(CfgGenerator):
metadata=metadata,
path=self.name).filter(removecomment)
try:
- return stream.render('text', encoding=self.encoding,
- strip_whitespace=False)
- except TypeError:
- return stream.render('text', encoding=self.encoding)
+ try:
+ return stream.render('text', encoding=self.encoding,
+ strip_whitespace=False)
+ except TypeError:
+ return stream.render('text', encoding=self.encoding)
+ except UndefinedError:
+ # a failure in a genshi expression _other_ than %{ python ... %}
+ err = sys.exc_info()[1]
+ stack = traceback.extract_tb(sys.exc_info()[2])
+ for quad in stack:
+ if quad[0] == self.name:
+ logger.error("Cfg: Error rendering %s at %s: %s" %
+ (fname, quad[2], err))
+ break
+ raise
+ except:
+ # a failure in a %{ python ... %} block -- the snippet in
+ # the traceback is just the beginning of the block.
+ err = sys.exc_info()[1]
+ stack = traceback.extract_tb(sys.exc_info()[2])
+ (filename, lineno, func, text) = stack[-1]
+ # this is horrible, and I deeply apologize to whoever gets
+ # to maintain this after I go to the Great Beer Garden in
+ # the Sky. genshi is incredibly opaque about what's being
+ # executed, so the only way I can find to determine which
+ # {% python %} block is being executed -- if there are
+ # multiples -- is to iterate through them and match the
+ # snippet of the first line that's in the traceback with
+ # the first non-empty line of the block.
+ execs = [contents
+ for etype, contents, loc in self.template.stream
+ if etype == self.template.EXEC]
+ contents = None
+ if len(execs) == 1:
+ contents = execs[0]
+ elif len(execs) > 1:
+ match = pyerror_re.match(func)
+ if match:
+ firstline = match.group(0)
+ for pyblock in execs:
+ if pyblock.startswith(firstline):
+ contents = pyblock
+ break
+ # else, no EXEC blocks -- WTF?
+ if contents:
+ # we now have the bogus block, but we need to get the
+ # offending line. To get there, we do (line number
+ # given in the exception) - (firstlineno from the
+ # internal genshi code object of the snippet) + 1 =
+ # (line number of the line with an error within the
+ # block, with all multiple line breaks elided to a
+ # single line break)
+ real_lineno = lineno - contents.code.co_firstlineno
+ src = re.sub(r'\n\n+', '\n', contents.source).splitlines()
+ logger.error("Cfg: Error rendering %s at %s: %s" %
+ (fname, src[real_lineno], err))
+ raise
def handle_event(self, event):
if event.code2str() == 'deleted':
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py
index 54c17c6c5..85c13c1ac 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py
@@ -7,6 +7,10 @@ logger = logging.getLogger(__name__)
class CfgLegacyInfo(CfgInfo):
__basenames__ = ['info', ':info']
+ def __init__(self, path):
+ CfgInfo.__init__(self, path)
+ self.path = path
+
def bind_info_to_entry(self, entry, metadata):
self._set_info(entry, self.metadata)
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index 6c7585993..081a68639 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -11,6 +11,7 @@ import lxml.etree
import Bcfg2.Options
import Bcfg2.Server.Plugin
from Bcfg2.Bcfg2Py3k import u_str
+import Bcfg2.Server.Lint
logger = logging.getLogger(__name__)
@@ -152,7 +153,19 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet):
global PROCESSORS
if PROCESSORS is None:
PROCESSORS = []
- for submodule in pkgutil.walk_packages(path=__path__):
+ if hasattr(pkgutil, 'walk_packages'):
+ submodules = pkgutil.walk_packages(path=__path__)
+ else:
+ #python 2.4
+ import glob
+ submodules = []
+ for path in __path__:
+ for submodule in glob.glob("%s/*.py" % path):
+ mod = '.'.join(submodule.split("/")[-1].split('.')[:-1])
+ if mod != '__init__':
+ submodules.append((None, mod, True))
+
+ for submodule in submodules:
module = getattr(__import__("%s.%s" %
(__name__,
submodule[1])).Server.Plugins.Cfg,
@@ -185,6 +198,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet):
return
elif action == 'changed':
self.entries[event.filename].handle_event(event)
+ return
elif action == 'deleted':
del self.entries[event.filename]
return
@@ -287,6 +301,10 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet):
logger.error("You need to specify base64 encoding for %s." %
entry.get('name'))
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+ except TypeError:
+ # data is already unicode; newer versions of Cheetah
+ # seem to return unicode
+ pass
if data:
entry.text = data
@@ -298,7 +316,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet):
generators = [ent for ent in list(self.entries.values())
if (isinstance(ent, CfgGenerator) and
ent.specific.matches(metadata))]
- if not matching:
+ if not generators:
msg = "No base file found for %s" % entry.get('name')
logger.error(msg)
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
@@ -385,7 +403,7 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool,
SETUP = core.setup
if 'validate' not in SETUP:
- SETUP['validate'] = Bcfg2.Options.CFG_VALIDATION
+ SETUP.add_option('validate', Bcfg2.Options.CFG_VALIDATION)
SETUP.reparse()
def AcceptChoices(self, entry, metadata):
@@ -396,3 +414,26 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool,
return self.entries[new_entry.get('name')].write_update(specific,
new_entry,
log)
+
+class CfgLint(Bcfg2.Server.Lint.ServerPlugin):
+ """ warn about usage of .cat and .diff files """
+
+ def Run(self):
+ for basename, entry in list(self.core.plugins['Cfg'].entries.items()):
+ self.check_entry(basename, entry)
+
+
+ @classmethod
+ def Errors(cls):
+ return {"cat-file-used":"warning",
+ "diff-file-used":"warning"}
+
+ def check_entry(self, basename, entry):
+ cfg = self.core.plugins['Cfg']
+ for basename, entry in list(cfg.entries.items()):
+ for fname, processor in entry.entries.items():
+ if self.HandlesFile(fname) and isinstance(processor, CfgFilter):
+ extension = fname.split(".")[-1]
+ self.LintError("%s-file-used" % extension,
+ "%s file used on %s: %s" %
+ (extension, basename, fname))
diff --git a/src/lib/Bcfg2/Server/Plugins/DBStats.py b/src/lib/Bcfg2/Server/Plugins/DBStats.py
index 999e078b9..ca948aabd 100644
--- a/src/lib/Bcfg2/Server/Plugins/DBStats.py
+++ b/src/lib/Bcfg2/Server/Plugins/DBStats.py
@@ -3,6 +3,7 @@ import difflib
import logging
import lxml.etree
import platform
+import sys
import time
try:
@@ -11,13 +12,14 @@ except ImportError:
pass
import Bcfg2.Server.Plugin
-import Bcfg2.Server.Reports.importscript
+from Bcfg2.Server.Reports.importscript import load_stat
from Bcfg2.Server.Reports.reports.models import Client
import Bcfg2.Server.Reports.settings
-from Bcfg2.Server.Reports.updatefix import update_database
+from Bcfg2.Server.Reports.Updater import update_database, UpdaterError
# for debugging output only
logger = logging.getLogger('Bcfg2.Plugins.DBStats')
+
class DBStats(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.ThreadedStatistics,
Bcfg2.Server.Plugin.PullSource):
@@ -29,9 +31,12 @@ class DBStats(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.PullSource.__init__(self)
self.cpath = "%s/Metadata/clients.xml" % datastore
self.core = core
- logger.debug("Searching for new models to add to the statistics database")
+ logger.debug("Searching for new models to "
+ "add to the statistics database")
try:
update_database()
+ except UpdaterError:
+ raise Bcfg2.Server.Plugin.PluginInitError
except Exception:
inst = sys.exc_info()[1]
logger.debug(str(inst))
@@ -40,32 +45,24 @@ class DBStats(Bcfg2.Server.Plugin.Plugin,
def handle_statistic(self, metadata, data):
newstats = data.find("Statistics")
newstats.set('time', time.asctime(time.localtime()))
- # ick
- data = lxml.etree.tostring(newstats)
- ndx = lxml.etree.XML(data)
- e = lxml.etree.Element('Node', name=metadata.hostname)
- e.append(ndx)
- container = lxml.etree.Element("ConfigStatistics")
- container.append(e)
- # FIXME need to build a metadata interface to expose a list of clients
start = time.time()
for i in [1, 2, 3]:
try:
- Bcfg2.Server.Reports.importscript.load_stats(self.core.metadata.clients_xml.xdata,
- container,
- self.core.encoding,
- 0,
- logger,
- True,
- platform.node())
+ load_stat(metadata,
+ newstats,
+ self.core.encoding,
+ 0,
+ logger,
+ True,
+ platform.node())
logger.info("Imported data for %s in %s seconds" \
% (metadata.hostname, time.time() - start))
return
except MultipleObjectsReturned:
e = sys.exc_info()[1]
- logger.error("DBStats: MultipleObjectsReturned while handling %s: %s" % \
- (metadata.hostname, e))
+ logger.error("DBStats: MultipleObjectsReturned while "
+ "handling %s: %s" % (metadata.hostname, e))
logger.error("DBStats: Data is inconsistent")
break
except:
@@ -100,7 +97,7 @@ class DBStats(Bcfg2.Server.Plugin.Plugin,
if entry.reason.is_sensitive:
raise Bcfg2.Server.Plugin.PluginExecutionError
elif len(entry.reason.unpruned) != 0:
- ret.append('\n'.join(entry.reason.unpruned))
+ ret.append('\n'.join(entry.reason.unpruned))
elif entry.reason.current_diff != '':
if entry.reason.is_binary:
ret.append(binascii.a2b_base64(entry.reason.current_diff))
diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
index 5beec7be0..f95c05d42 100644
--- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py
+++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
@@ -10,6 +10,7 @@ import errno
import binascii
import lxml.etree
import Bcfg2.Options
+import Bcfg2.Server
import Bcfg2.Server.Plugin
probecode = """#!/usr/bin/env python
@@ -36,14 +37,6 @@ data.text = binascii.b2a_base64(open(path).read())
print lxml.etree.tostring(data)
"""
-class FileProbesConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked,
- Bcfg2.Server.Plugin.StructFile):
- """ Config file handler for FileProbes """
- def __init__(self, filename, fam):
- Bcfg2.Server.Plugin.SingleXMLFileBacked.__init__(self, filename, fam)
- Bcfg2.Server.Plugin.StructFile.__init__(self, filename)
-
-
class FileProbes(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.Probing):
""" This module allows you to probe a client for a file, which is then
@@ -59,8 +52,10 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Probing.__init__(self)
- self.config = FileProbesConfig(os.path.join(self.data, 'config.xml'),
- core.fam)
+ self.config = Bcfg2.Server.Plugin.StructFile(os.path.join(self.data,
+ 'config.xml'),
+ fam=core.fam,
+ should_monitor=True)
self.entries = dict()
self.probes = dict()
@@ -102,7 +97,9 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
(data.get('name'), metadata.hostname))
else:
try:
- self.write_data(lxml.etree.XML(data.text), metadata)
+ self.write_data(lxml.etree.XML(data.text,
+ parser=Bcfg2.Server.XMLParser),
+ metadata)
except lxml.etree.XMLSyntaxError:
# if we didn't get XML back from the probe, assume
# it's an error message
diff --git a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
index 58b4d4afb..bea3baee3 100644
--- a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
+++ b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
@@ -1,6 +1,8 @@
import re
+import sys
import logging
import lxml.etree
+import Bcfg2.Server.Lint
import Bcfg2.Server.Plugin
class PackedDigitRange(object):
@@ -71,16 +73,17 @@ class PatternMap(object):
return ret
-class PatternFile(Bcfg2.Server.Plugin.SingleXMLFileBacked):
+class PatternFile(Bcfg2.Server.Plugin.XMLFileBacked):
__identifier__ = None
- def __init__(self, filename, fam):
- Bcfg2.Server.Plugin.SingleXMLFileBacked.__init__(self, filename, fam)
+ def __init__(self, filename, fam=None):
+ Bcfg2.Server.Plugin.XMLFileBacked.__init__(self, filename, fam=fam,
+ should_monitor=True)
self.patterns = []
self.logger = logging.getLogger(self.__class__.__name__)
def Index(self):
- Bcfg2.Server.Plugin.SingleXMLFileBacked.Index(self)
+ Bcfg2.Server.Plugin.XMLFileBacked.Index(self)
self.patterns = []
for entry in self.xdata.xpath('//GroupPattern'):
try:
@@ -117,8 +120,38 @@ class GroupPatterns(Bcfg2.Server.Plugin.Plugin,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Connector.__init__(self)
- self.config = PatternFile(self.data + '/config.xml',
- core.fam)
+ self.config = PatternFile(os.path.join(self.data, 'config.xml'),
+ fam=core.fam)
def get_additional_groups(self, metadata):
return self.config.process_patterns(metadata.hostname)
+
+
+class GroupPatternsLint(Bcfg2.Server.Lint.ServerPlugin):
+ def Run(self):
+ """ run plugin """
+ cfg = self.core.plugins['GroupPatterns'].config
+ for entry in cfg.xdata.xpath('//GroupPattern'):
+ groups = [g.text for g in entry.findall('Group')]
+ self.check(entry, groups, ptype='NamePattern')
+ self.check(entry, groups, ptype='NameRange')
+
+ @classmethod
+ def Errors(cls):
+ return {"pattern-fails-to-initialize":"error"}
+
+ def check(self, entry, groups, ptype="NamePattern"):
+ if ptype == "NamePattern":
+ pmap = lambda p: PatternMap(p, None, groups)
+ else:
+ pmap = lambda p: PatternMap(None, p, groups)
+
+ for el in entry.findall(ptype):
+ pat = el.text
+ try:
+ pmap(pat)
+ except:
+ err = sys.exc_info()[1]
+ self.LintError("pattern-fails-to-initialize",
+ "Failed to initialize %s %s for %s: %s" %
+ (ptype, pat, entry.get('pattern'), err))
diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py
index 970126b80..77e433ab1 100644
--- a/src/lib/Bcfg2/Server/Plugins/Metadata.py
+++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -6,14 +6,13 @@ import copy
import fcntl
import lxml.etree
import os
-import os.path
import socket
import sys
import time
-
+import Bcfg2.Server
import Bcfg2.Server.FileMonitor
import Bcfg2.Server.Plugin
-
+from Bcfg2.version import Bcfg2VersionInfo
def locked(fd):
"""Aquire a lock on a file"""
@@ -36,16 +35,20 @@ class MetadataRuntimeError(Exception):
pass
-class XMLMetadataConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked):
+class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked):
"""Handles xml config files and all XInclude statements"""
def __init__(self, metadata, watch_clients, basefile):
- Bcfg2.Server.Plugin.SingleXMLFileBacked.__init__(self,
- os.path.join(metadata.data,
- basefile),
- metadata.core.fam)
+ # we tell XMLFileBacked _not_ to add a monitor for this
+ # file, because the main Metadata plugin has already added
+ # one. then we immediately set should_monitor to the proper
+ # value, so that XIinclude'd files get properly watched
+ fpath = os.path.join(metadata.data, basefile)
+ Bcfg2.Server.Plugin.XMLFileBacked.__init__(self, fpath,
+ fam=metadata.core.fam)
+ self.should_monitor = watch_clients
self.metadata = metadata
+ self.fam = metadata.core.fam
self.basefile = basefile
- self.should_monitor = watch_clients
self.data = None
self.basedata = None
self.basedir = metadata.data
@@ -65,16 +68,11 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked):
raise MetadataRuntimeError
return self.basedata
- def add_monitor(self, fpath, fname):
- """Add a fam monitor for an included file"""
- if self.should_monitor:
- self.metadata.core.fam.AddMonitor(fpath, self.metadata)
- self.extras.append(fname)
-
def load_xml(self):
"""Load changes from XML"""
try:
- xdata = lxml.etree.parse(os.path.join(self.basedir, self.basefile))
+ xdata = lxml.etree.parse(os.path.join(self.basedir, self.basefile),
+ parser=Bcfg2.Server.XMLParser)
except lxml.etree.XMLSyntaxError:
self.logger.error('Failed to parse %s' % self.basefile)
return
@@ -145,7 +143,8 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked):
for included in self.extras:
try:
xdata = lxml.etree.parse(os.path.join(self.basedir,
- included))
+ included),
+ parser=Bcfg2.Server.XMLParser)
cli = xdata.xpath(xpath)
if len(cli) > 0:
return {'filename': os.path.join(self.basedir,
@@ -172,8 +171,8 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked):
class ClientMetadata(object):
"""This object contains client metadata."""
- def __init__(self, client, profile, groups, bundles,
- aliases, addresses, categories, uuid, password, query):
+ def __init__(self, client, profile, groups, bundles, aliases, addresses,
+ categories, uuid, password, version, query):
self.hostname = client
self.profile = profile
self.bundles = bundles
@@ -184,6 +183,11 @@ class ClientMetadata(object):
self.uuid = uuid
self.password = password
self.connectors = []
+ self.version = version
+ try:
+ self.version_info = Bcfg2VersionInfo(version)
+ except:
+ self.version_info = None
self.query = query
def inGroup(self, group):
@@ -249,6 +253,7 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
self.aliases = {}
self.groups = {}
self.cgroups = {}
+ self.versions = {}
self.public = []
self.private = []
self.profiles = []
@@ -282,7 +287,8 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
def get_groups(self):
'''return groups xml tree'''
- groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"))
+ groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"),
+ parser=Bcfg2.Server.XMLParser)
root = groups_tree.getroot()
return root
@@ -412,6 +418,8 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
self.floating.append(clname)
if 'password' in client.attrib:
self.passwords[clname] = client.get('password')
+ if 'version' in client.attrib:
+ self.versions[clname] = client.get('version')
self.raliases[clname] = set()
for alias in client.findall('Alias'):
@@ -537,6 +545,20 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
self.clients[client] = profile
self.clients_xml.write()
+ def set_version(self, client, version):
+ """Set group parameter for provided client."""
+ self.logger.info("Asserting client %s version to %s" % (client, version))
+ if client in self.clients:
+ self.logger.info("Setting version on client %s to %s" %
+ (client, version))
+ self.update_client(client, dict(version=version))
+ else:
+ msg = "Cannot set version on non-existent client %s" % client
+ self.logger.error(msg)
+ raise MetadataConsistencyError(msg)
+ self.versions[client] = version
+ self.clients_xml.write()
+
def resolve_client(self, addresspair, cleanup_cache=False):
"""Lookup address locally or in DNS to get a hostname."""
if addresspair in self.session_cache:
@@ -575,9 +597,9 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
return self.aliases[cname]
return cname
except socket.herror:
- warning = "address resolution error for %s" % (address)
+ warning = "address resolution error for %s" % address
self.logger.warning(warning)
- raise MetadataConsistencyError
+ raise MetadataConsistencyError(warning)
def get_initial_metadata(self, client):
"""Return the metadata for a given client."""
@@ -599,6 +621,7 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
[bundles, groups, categories] = self.groups[self.default]
aliases = self.raliases.get(client, set())
addresses = self.raddresses.get(client, set())
+ version = self.versions.get(client, None)
newgroups = set(groups)
newbundles = set(bundles)
newcategories = {}
@@ -622,7 +645,7 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
[newgroups.add(g) for g in ngroups if g not in newgroups]
newcategories.update(ncategories)
return ClientMetadata(client, profile, newgroups, newbundles, aliases,
- addresses, newcategories, uuid, password,
+ addresses, newcategories, uuid, password, version,
self.query)
def get_all_group_names(self):
@@ -792,7 +815,8 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
def include_group(group):
return not only_client or group in clientmeta.groups
- groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"))
+ groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"),
+ parser=Bcfg2.Server.XMLParser)
try:
groups_tree.xinclude()
except lxml.etree.XIncludeError:
diff --git a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
index 4dbd57d16..f2b8336e0 100644
--- a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
+++ b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
@@ -7,18 +7,23 @@ import glob
import socket
import logging
import lxml.etree
-
+import Bcfg2.Server
import Bcfg2.Server.Plugin
LOGGER = logging.getLogger('Bcfg2.Plugins.NagiosGen')
line_fmt = '\t%-32s %s'
-class NagiosGenConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked,
- Bcfg2.Server.Plugin.StructFile):
+class NagiosGenConfig(Bcfg2.Server.Plugin.StructFile):
def __init__(self, filename, fam):
- Bcfg2.Server.Plugin.SingleXMLFileBacked.__init__(self, filename, fam)
- Bcfg2.Server.Plugin.StructFile.__init__(self, filename)
+ # create config.xml if missing
+ if not os.path.exists(filename):
+ LOGGER.warning("NagiosGen: %s missing. "
+ "Creating empty one for you." % filename)
+ open(filename, "w").write("<NagiosGen></NagiosGen>")
+
+ Bcfg2.Server.Plugin.StructFile.__init__(self, filename, fam=fam,
+ should_monitor=True)
class NagiosGen(Bcfg2.Server.Plugin.Plugin,
@@ -51,7 +56,12 @@ class NagiosGen(Bcfg2.Server.Plugin.Plugin,
def createhostconfig(self, entry, metadata):
"""Build host specific configuration file."""
- host_address = socket.gethostbyname(metadata.hostname)
+ try:
+ host_address = socket.gethostbyname(metadata.hostname)
+ except socket.gaierror:
+ LOGGER.error("Failed to find IP address for %s" %
+ metadata.hostname)
+ raise Bcfg2.Server.Plugin.PluginExecutionError
host_groups = [grp for grp in metadata.groups
if os.path.isfile('%s/%s-group.cfg' % (self.data, grp))]
host_config = ['define host {',
@@ -84,7 +94,8 @@ class NagiosGen(Bcfg2.Server.Plugin.Plugin,
LOGGER.warn("Parsing deprecated NagiosGen/parents.xml. "
"Update to the new-style config with "
"nagiosgen-convert.py.")
- parents = lxml.etree.parse(pfile)
+ parents = lxml.etree.parse(pfile,
+ parser=Bcfg2.Server.XMLParser)
for el in parents.xpath("//Depend[@name='%s']" % metadata.hostname):
if 'parent' in xtra:
xtra['parent'] += "," + el.get("on")
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py b/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
index 3ea14ce75..ac78ea0fc 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
@@ -52,13 +52,38 @@ class Collection(Bcfg2.Server.Plugin.Debuggable):
@property
def cachekey(self):
- return md5(self.get_config()).hexdigest()
+ return md5(self.sourcelist().encode(Bcfg2.Server.Plugin.encoding)).hexdigest()
def get_config(self):
- self.logger.error("Packages: Cannot generate config for host with "
- "multiple source types (%s)" % self.metadata.hostname)
+ self.logger.error("Packages: Cannot generate config for host %s with "
+ "no sources or multiple source types" %
+ self.metadata.hostname)
return ""
+ def sourcelist(self):
+ srcs = []
+ for source in self.sources:
+ # get_urls() loads url_map as a side-effect
+ source.get_urls()
+ for url_map in source.url_map:
+ reponame = source.get_repo_name(url_map)
+ srcs.append("Name: %s" % reponame)
+ srcs.append(" Type: %s" % source.ptype)
+ if url_map['url']:
+ srcs.append(" URL: %s" % url_map['url'])
+ elif url_map['rawurl']:
+ srcs.append(" RAWURL: %s" % url_map['rawurl'])
+ if source.gpgkeys:
+ srcs.append(" GPG Key(s): %s" % ", ".join(source.gpgkeys))
+ else:
+ srcs.append(" GPG Key(s): None")
+ if len(source.blacklist):
+ srcs.append(" Blacklist: %s" % ", ".join(source.blacklist))
+ if len(source.whitelist):
+ srcs.append(" Whitelist: %s" % ", ".join(source.whitelist))
+ srcs.append("")
+ return "\n".join(srcs)
+
def get_relevant_groups(self):
groups = []
for source in self.sources:
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
index 7796b9e34..3ca96c0a4 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
@@ -4,17 +4,15 @@ import lxml.etree
import Bcfg2.Server.Plugin
from Bcfg2.Server.Plugins.Packages.Source import SourceInitError
-class PackagesSources(Bcfg2.Server.Plugin.SingleXMLFileBacked,
- Bcfg2.Server.Plugin.StructFile,
+class PackagesSources(Bcfg2.Server.Plugin.StructFile,
Bcfg2.Server.Plugin.Debuggable):
__identifier__ = None
def __init__(self, filename, cachepath, fam, packages, setup):
Bcfg2.Server.Plugin.Debuggable.__init__(self)
try:
- Bcfg2.Server.Plugin.SingleXMLFileBacked.__init__(self,
- filename,
- fam)
+ Bcfg2.Server.Plugin.StructFile.__init__(self, filename, fam=fam,
+ should_monitor=True)
except OSError:
err = sys.exc_info()[1]
msg = "Packages: Failed to read configuration file: %s" % err
@@ -22,7 +20,6 @@ class PackagesSources(Bcfg2.Server.Plugin.SingleXMLFileBacked,
msg += " Have you created it?"
self.logger.error(msg)
raise Bcfg2.Server.Plugin.PluginInitError(msg)
- Bcfg2.Server.Plugin.StructFile.__init__(self, filename)
self.cachepath = cachepath
self.setup = setup
if not os.path.exists(self.cachepath):
@@ -42,7 +39,7 @@ class PackagesSources(Bcfg2.Server.Plugin.SingleXMLFileBacked,
source.toggle_debug()
def HandleEvent(self, event=None):
- Bcfg2.Server.Plugin.SingleXMLFileBacked.HandleEvent(self, event=event)
+ Bcfg2.Server.Plugin.XMLFileBacked.HandleEvent(self, event=event)
if event and event.filename != self.name:
for fname in self.extras:
fpath = None
@@ -65,7 +62,7 @@ class PackagesSources(Bcfg2.Server.Plugin.SingleXMLFileBacked,
return sorted(list(self.parsed)) == sorted(self.extras)
def Index(self):
- Bcfg2.Server.Plugin.SingleXMLFileBacked.Index(self)
+ Bcfg2.Server.Plugin.XMLFileBacked.Index(self)
self.entries = []
for xsource in self.xdata.findall('.//Source'):
source = self.source_from_xml(xsource)
@@ -87,7 +84,8 @@ class PackagesSources(Bcfg2.Server.Plugin.SingleXMLFileBacked,
stype.title())
cls = getattr(module, "%sSource" % stype.title())
except (ImportError, AttributeError):
- self.logger.error("Packages: Unknown source type %s" % stype)
+ ex = sys.exc_info()[1]
+ self.logger.error("Packages: Unknown source type %s (%s)" % (stype, ex))
return None
try:
@@ -106,4 +104,7 @@ class PackagesSources(Bcfg2.Server.Plugin.SingleXMLFileBacked,
return "PackagesSources: %s" % repr(self.entries)
def __str__(self):
- return "PackagesSources: %s" % str(self.entries)
+ return "PackagesSources: %s sources" % len(self.entries)
+
+ def __len__(self):
+ return len(self.entries)
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
index edcdcd9f2..332d0c488 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
@@ -1,7 +1,6 @@
import os
import re
import sys
-import base64
import Bcfg2.Server.Plugin
from Bcfg2.Bcfg2Py3k import HTTPError, HTTPBasicAuthHandler, \
HTTPPasswordMgrWithDefaultRealm, install_opener, build_opener, \
@@ -51,7 +50,18 @@ class Source(Bcfg2.Server.Plugin.Debuggable):
for key, tag in [('components', 'Component'), ('arches', 'Arch'),
('blacklist', 'Blacklist'),
('whitelist', 'Whitelist')]:
- self.__dict__[key] = [item.text for item in xsource.findall(tag)]
+ setattr(self, key, [item.text for item in xsource.findall(tag)])
+ self.server_options = dict()
+ self.client_options = dict()
+ opts = xsource.findall("Options")
+ for el in opts:
+ repoopts = dict([(k, v)
+ for k, v in el.attrib.items()
+ if k != "clientonly" and k != "serveronly"])
+ if el.get("clientonly", "false").lower() == "false":
+ self.server_options.update(repoopts)
+ if el.get("serveronly", "false").lower() == "false":
+ self.client_options.update(repoopts)
self.gpgkeys = [el.text for el in xsource.findall("GPGKey")]
@@ -149,12 +159,10 @@ class Source(Bcfg2.Server.Plugin.Debuggable):
if match:
name = match.group(1)
break
- if name is None:
- # couldn't figure out the name from the URL or URL map
- # (which probably means its a screwy URL), so we just
- # generate a random one
- name = base64.b64encode(os.urandom(16))[:-2]
- rname = "%s-%s" % (self.groups[0], name)
+ if name is not None:
+ rname = "%s-%s" % (self.groups[0], name)
+ else:
+ rname = self.groups[0]
# see yum/__init__.py in the yum source, lines 441-449, for
# the source of this regex. yum doesn't like anything but
# string.ascii_letters, string.digits, and [-_.:]. There
@@ -169,6 +177,9 @@ class Source(Bcfg2.Server.Plugin.Debuggable):
else:
return self.__class__.__name__
+ def __repr__(self):
+ return str(self)
+
def get_urls(self):
return []
urls = property(get_urls)
@@ -182,6 +193,10 @@ class Source(Bcfg2.Server.Plugin.Debuggable):
if a in metadata.groups]
vdict = dict()
for agrp in agroups:
+ if agrp not in self.provides:
+ self.logger.warning("%s provides no packages for %s" %
+ (self, agrp))
+ continue
for key, value in list(self.provides[agrp].items()):
if key not in vdict:
vdict[key] = set(value)
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
index 53344e200..87909dc4c 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
@@ -4,14 +4,13 @@ import time
import copy
import glob
import socket
-import random
import logging
import threading
import lxml.etree
from UserDict import DictMixin
from subprocess import Popen, PIPE, STDOUT
import Bcfg2.Server.Plugin
-from Bcfg2.Bcfg2Py3k import StringIO, cPickle, HTTPError, ConfigParser, file
+from Bcfg2.Bcfg2Py3k import StringIO, cPickle, HTTPError, URLError, ConfigParser, file
from Bcfg2.Server.Plugins.Packages.Collection import Collection
from Bcfg2.Server.Plugins.Packages.Source import SourceInitError, Source, \
fetch_url
@@ -105,10 +104,24 @@ class YumCollection(Collection):
if has_pulp and self.has_pulp_sources:
_setup_pulp(self.setup)
+ self._helper = None
+
@property
def helper(self):
- return self.setup.cfp.get("packages:yum", "helper",
- default="/usr/sbin/bcfg2-yum-helper")
+ try:
+ return self.setup.cfp.get("packages:yum", "helper")
+ except:
+ pass
+
+ if not self._helper:
+ # first see if bcfg2-yum-helper is in PATH
+ try:
+ Popen(['bcfg2-yum-helper'],
+ stdin=PIPE, stdout=PIPE, stderr=PIPE).wait()
+ self._helper = 'bcfg2-yum-helper'
+ except OSError:
+ self._helper = "/usr/sbin/bcfg2-yum-helper"
+ return self._helper
@property
def use_yum(self):
@@ -186,6 +199,13 @@ class YumCollection(Collection):
config.set(reponame, "includepkgs",
" ".join(source.whitelist))
+ if raw:
+ opts = source.server_options
+ else:
+ opts = source.client_options
+ for opt, val in opts.items():
+ config.set(reponame, opt, val)
+
if raw:
return config
else:
@@ -546,6 +566,11 @@ class YumSource(Source):
except ValueError:
self.logger.error("Packages: Bad url string %s" % rmdurl)
return []
+ except URLError:
+ err = sys.exc_info()[1]
+ self.logger.error("Packages: Failed to fetch url %s. %s" %
+ (rmdurl, err))
+ return []
except HTTPError:
err = sys.exc_info()[1]
self.logger.error("Packages: Failed to fetch url %s. code=%s" %
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
index d789a6d39..3e46ec67c 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
@@ -15,7 +15,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.StructureValidator,
Bcfg2.Server.Plugin.Generator,
Bcfg2.Server.Plugin.Connector,
- Bcfg2.Server.Plugin.GoalValidator):
+ Bcfg2.Server.Plugin.ClientRunHooks):
name = 'Packages'
conflicts = ['Pkgmgr']
experimental = True
@@ -26,7 +26,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.StructureValidator.__init__(self)
Bcfg2.Server.Plugin.Generator.__init__(self)
Bcfg2.Server.Plugin.Connector.__init__(self)
- Bcfg2.Server.Plugin.Probing.__init__(self)
+ Bcfg2.Server.Plugin.ClientRunHooks.__init__(self)
self.sentinels = set()
self.cachepath = os.path.join(self.data, 'cache')
@@ -40,11 +40,16 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
self.core.setup)
def toggle_debug(self):
- Bcfg2.Server.Plugin.Plugin.toggle_debug(self)
+ rv = Bcfg2.Server.Plugin.Plugin.toggle_debug(self)
self.sources.toggle_debug()
+ return rv
@property
def disableResolver(self):
+ if self.disableMetaData:
+ # disabling metadata without disabling the resolver Breaks
+ # Things
+ return True
try:
return not self.core.setup.cfp.getboolean("packages", "resolver")
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
@@ -87,8 +92,8 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
if entry.tag == 'Package':
collection = self._get_collection(metadata)
entry.set('version', self.core.setup.cfp.get("packages",
- "version",
- default="auto"))
+ "version",
+ default="auto"))
entry.set('type', collection.ptype)
elif entry.tag == 'Path':
if (entry.get("name") == self.core.setup.cfp.get("packages",
@@ -271,10 +276,11 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
collection = self._get_collection(metadata)
return dict(sources=collection.get_additional_data())
- def validate_goals(self, metadata, _):
- """ we abuse the GoalValidator plugin since validate_goals()
- is the very last thing called during a client config run. so
- we use this to clear the collection cache for this client,
- which must persist only the duration of a client run """
+ def end_client_run(self, metadata):
+ """ clear the collection cache for this client, which must
+ persist only the duration of a client run"""
if metadata.hostname in Collection.clients:
del Collection.clients[metadata.hostname]
+
+ def end_statistics(self, metadata):
+ self.end_client_run(metadata)
diff --git a/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py b/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
index e9254cdcc..fcf2045a7 100644
--- a/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
+++ b/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
@@ -1,9 +1,12 @@
'''This module implements a package management scheme for all images'''
-import logging
import re
+import glob
+import logging
+import lxml.etree
import Bcfg2.Server.Plugin
-import lxml
+import Bcfg2.Server.Lint
+
try:
set
except NameError:
@@ -24,12 +27,14 @@ class FuzzyDict(dict):
print("got non-string key %s" % str(key))
return dict.__getitem__(self, key)
- def has_key(self, key):
+ def __contains__(self, key):
if isinstance(key, str):
mdata = self.fuzzy.match(key)
- if self.fuzzy.match(key):
- return dict.has_key(self, mdata.groupdict()['name'])
- return dict.has_key(self, key)
+ if mdata:
+ return dict.__contains__(self, mdata.groupdict()['name'])
+ else:
+ print("got non-string key %s" % str(key))
+ return dict.__contains__(self, key)
def get(self, key, default=None):
try:
@@ -167,3 +172,40 @@ class Pkgmgr(Bcfg2.Server.Plugin.PrioDir):
def HandleEntry(self, entry, metadata):
self.BindEntry(entry, metadata)
+
+
+class PkgmgrLint(Bcfg2.Server.Lint.ServerlessPlugin):
+ """ find duplicate Pkgmgr entries with the same priority """
+ def Run(self):
+ pset = set()
+ for pfile in glob.glob(os.path.join(self.config['repo'], 'Pkgmgr',
+ '*.xml')):
+ if self.HandlesFile(pfile):
+ xdata = lxml.etree.parse(pfile).getroot()
+ # get priority, type, group
+ priority = xdata.get('priority')
+ ptype = xdata.get('type')
+ for pkg in xdata.xpath("//Package"):
+ if pkg.getparent().tag == 'Group':
+ grp = pkg.getparent().get('name')
+ if (type(grp) is not str and
+ grp.getparent().tag == 'Group'):
+ pgrp = grp.getparent().get('name')
+ else:
+ pgrp = 'none'
+ else:
+ grp = 'none'
+ pgrp = 'none'
+ ptuple = (pkg.get('name'), priority, ptype, grp, pgrp)
+ # check if package is already listed with same
+ # priority, type, grp
+ if ptuple in pset:
+ self.LintError("duplicate-package",
+ "Duplicate Package %s, priority:%s, type:%s" %
+ (pkg.get('name'), priority, ptype))
+ else:
+ pset.add(ptuple)
+
+ @classmethod
+ def Errors(cls):
+ return {"duplicate-packages":"error"}
diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py
index af908eee8..8ef6c8737 100644
--- a/src/lib/Bcfg2/Server/Plugins/Probes.py
+++ b/src/lib/Bcfg2/Server/Plugins/Probes.py
@@ -2,6 +2,8 @@ import time
import lxml.etree
import operator
import re
+import os
+import Bcfg2.Server
try:
import json
@@ -39,61 +41,31 @@ class ClientProbeDataSet(dict):
dict.__init__(self, *args, **kwargs)
-class ProbeData(object):
+class ProbeData(str):
""" a ProbeData object emulates a str object, but also has .xdata
and .json properties to provide convenient ways to use ProbeData
objects as XML or JSON data """
+ def __new__(cls, data):
+ return str.__new__(cls, data)
+
def __init__(self, data):
- self.data = data
+ str.__init__(self)
self._xdata = None
self._json = None
self._yaml = None
- def __str__(self):
- return str(self.data)
-
- def __repr__(self):
- return repr(self.data)
-
- def __getattr__(self, name):
- """ make ProbeData act like a str object """
- return getattr(self.data, name)
-
- def __complex__(self):
- return complex(self.data)
-
- def __int__(self):
- return int(self.data)
-
- def __long__(self):
- return long(self.data)
-
- def __float__(self):
- return float(self.data)
-
- def __eq__(self, other):
- return str(self) == str(other)
-
- def __ne__(self, other):
- return str(self) != str(other)
-
- def __gt__(self, other):
- return str(self) > str(other)
-
- def __lt__(self, other):
- return str(self) < str(other)
-
- def __ge__(self, other):
- return self > other or self == other
-
- def __le__(self, other):
- return self < other or self == other
-
+ @property
+ def data(self):
+ """ provide backwards compatibility with broken ProbeData
+ object in bcfg2 1.2.0 thru 1.2.2 """
+ return str(self)
+
@property
def xdata(self):
if self._xdata is None:
try:
- self._xdata = lxml.etree.XML(self.data)
+ self._xdata = lxml.etree.XML(self.data,
+ parser=Bcfg2.Server.XMLParser)
except lxml.etree.XMLSyntaxError:
pass
return self._xdata
@@ -214,13 +186,14 @@ class Probes(Bcfg2.Server.Plugin.Plugin,
pretty_print='true')
try:
datafile = open("%s/%s" % (self.data, 'probed.xml'), 'w')
+ datafile.write(data.decode('utf-8'))
except IOError:
self.logger.error("Failed to write probed.xml")
- datafile.write(data.decode('utf-8'))
def load_data(self):
try:
- data = lxml.etree.parse(self.data + '/probed.xml').getroot()
+ data = lxml.etree.parse(os.path.join(self.data, 'probed.xml'),
+ parser=Bcfg2.Server.XMLParser).getroot()
except:
self.logger.error("Failed to read file probed.xml")
return
diff --git a/src/lib/Bcfg2/Server/Plugins/Properties.py b/src/lib/Bcfg2/Server/Plugins/Properties.py
index 680881858..a879b064f 100644
--- a/src/lib/Bcfg2/Server/Plugins/Properties.py
+++ b/src/lib/Bcfg2/Server/Plugins/Properties.py
@@ -5,26 +5,51 @@ import copy
import logging
import lxml.etree
import Bcfg2.Server.Plugin
+try:
+ from Bcfg2.Encryption import ssl_decrypt, EVPError
+ have_crypto = True
+except ImportError:
+ have_crypto = False
+
+logger = logging.getLogger(__name__)
+
+SETUP = None
+
+def passphrases():
+ section = "encryption"
+ if SETUP.cfp.has_section(section):
+ return dict([(o, SETUP.cfp.get(section, o))
+ for o in SETUP.cfp.options(section)])
+ else:
+ return dict()
-logger = logging.getLogger('Bcfg2.Plugins.Properties')
class PropertyFile(Bcfg2.Server.Plugin.StructFile):
"""Class for properties files."""
def write(self):
""" Write the data in this data structure back to the property
file """
- if self.validate_data():
- try:
- open(self.name,
- "wb").write(lxml.etree.tostring(self.xdata,
- pretty_print=True))
- return True
- except IOError:
- err = sys.exc_info()[1]
- logger.error("Failed to write %s: %s" % (self.name, err))
- return False
- else:
- return False
+ if not SETUP.cfp.getboolean("properties", "writes_enabled",
+ default=True):
+ msg = "Properties files write-back is disabled in the configuration"
+ logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+ try:
+ self.validate_data()
+ except Bcfg2.Server.Plugin.PluginExecutionError:
+ msg = "Cannot write %s: %s" % (self.name, sys.exc_info()[1])
+ logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ try:
+ open(self.name, "wb").write(lxml.etree.tostring(self.xdata,
+ pretty_print=True))
+ return True
+ except IOError:
+ err = sys.exc_info()[1]
+ msg = "Failed to write %s: %s" % (self.name, err)
+ logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
def validate_data(self):
""" ensure that the data in this object validates against the
@@ -34,19 +59,51 @@ class PropertyFile(Bcfg2.Server.Plugin.StructFile):
try:
schema = lxml.etree.XMLSchema(file=schemafile)
except:
- logger.error("Failed to process schema for %s" % self.name)
- return False
+ err = sys.exc_info()[1]
+ raise Bcfg2.Server.Plugin.PluginExecutionError("Failed to process schema for %s: %s" % (self.name, err))
else:
# no schema exists
return True
if not schema.validate(self.xdata):
- logger.error("Data for %s fails to validate; run bcfg2-lint for "
- "more details" % self.name)
- return False
+ raise Bcfg2.Server.Plugin.PluginExecutionError("Data for %s fails to validate; run bcfg2-lint for more details" % self.name)
else:
return True
+ def Index(self):
+ Bcfg2.Server.Plugin.StructFile.Index(self)
+ if self.xdata.get("encryption", "false").lower() != "false":
+ if not have_crypto:
+ msg = "Properties: M2Crypto is not available: %s" % self.name
+ logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+ for el in self.xdata.xpath("*[@encrypted]"):
+ try:
+ el.text = self._decrypt(el)
+ except EVPError:
+ msg = "Failed to decrypt %s element in %s" % (el.tag,
+ self.name)
+ logger.error(msg)
+ raise Bcfg2.Server.PluginExecutionError(msg)
+
+ def _decrypt(self, element):
+ if not element.text.strip():
+ return
+ passes = passphrases()
+ try:
+ passphrase = passes[element.get("encrypted")]
+ try:
+ return ssl_decrypt(element.text, passphrase)
+ except EVPError:
+ # error is raised below
+ pass
+ except KeyError:
+ for passwd in passes.values():
+ try:
+ return ssl_decrypt(element.text, passwd)
+ except EVPError:
+ pass
+ raise EVPError("Failed to decrypt")
class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
__child__ = PropertyFile
@@ -62,6 +119,7 @@ class Properties(Bcfg2.Server.Plugin.Plugin,
name = 'Properties'
def __init__(self, core, datastore):
+ global SETUP
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Connector.__init__(self)
try:
@@ -72,5 +130,16 @@ class Properties(Bcfg2.Server.Plugin.Plugin,
(e.strerror, e.filename))
raise Bcfg2.Server.Plugin.PluginInitError
- def get_additional_data(self, _):
- return copy.copy(self.store.entries)
+ SETUP = core.setup
+
+ def get_additional_data(self, metadata):
+ autowatch = self.core.setup.cfp.getboolean("properties", "automatch",
+ default=False)
+ rv = dict()
+ for fname, pfile in self.store.entries.items():
+ if (autowatch or
+ pfile.xdata.get("automatch", "false").lower() == "true"):
+ rv[fname] = pfile.XMLMatch(metadata)
+ else:
+ rv[fname] = copy.copy(pfile)
+ return rv
diff --git a/src/lib/Bcfg2/Server/Plugins/PuppetENC.py b/src/lib/Bcfg2/Server/Plugins/PuppetENC.py
new file mode 100644
index 000000000..46182e9a2
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/PuppetENC.py
@@ -0,0 +1,117 @@
+import os
+import Bcfg2.Server
+import Bcfg2.Server.Plugin
+from subprocess import Popen, PIPE
+
+try:
+ from syck import load as yaml_load, error as yaml_error
+except ImportError:
+ try:
+ from yaml import load as yaml_load, YAMLError as yaml_error
+ except ImportError:
+ raise ImportError("No yaml library could be found")
+
+class PuppetENCFile(Bcfg2.Server.Plugin.FileBacked):
+ def HandleEvent(self, event=None):
+ return
+
+
+class PuppetENC(Bcfg2.Server.Plugin.Plugin,
+ Bcfg2.Server.Plugin.Connector,
+ Bcfg2.Server.Plugin.ClientRunHooks,
+ Bcfg2.Server.Plugin.DirectoryBacked):
+ """ A plugin to run Puppet external node classifiers
+ (http://docs.puppetlabs.com/guides/external_nodes.html) """
+ name = 'PuppetENC'
+ experimental = True
+ __child__ = PuppetENCFile
+
+ def __init__(self, core, datastore):
+ Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
+ Bcfg2.Server.Plugin.Connector.__init__(self)
+ Bcfg2.Server.Plugin.ClientRunHooks.__init__(self)
+ Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data,
+ self.core.fam)
+ self.cache = dict()
+
+ def _run_encs(self, metadata):
+ cache = dict(groups=[], params=dict())
+ for enc in self.entries.keys():
+ epath = os.path.join(self.data, enc)
+ self.debug_log("PuppetENC: Running ENC %s for %s" %
+ (enc, metadata.hostname))
+ proc = Popen([epath, metadata.hostname], stdin=PIPE, stdout=PIPE,
+ stderr=PIPE)
+ (out, err) = proc.communicate()
+ rv = proc.wait()
+ if rv != 0:
+ msg = "PuppetENC: Error running ENC %s for %s (%s): %s" % \
+ (enc, metadata.hostname, rv)
+ self.logger.error("%s: %s" % (msg, err))
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+ if err:
+ self.debug_log("ENC Error: %s" % err)
+
+ try:
+ yaml = yaml_load(out)
+ self.debug_log("Loaded data from %s for %s: %s" %
+ (enc, metadata.hostname, yaml))
+ except yaml_error:
+ err = sys.exc_info()[1]
+ msg = "Error decoding YAML from %s for %s: %s" % \
+ (enc, metadata.hostname, err)
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ groups = []
+ if "classes" in yaml:
+ # stock Puppet ENC output format
+ groups = yaml['classes']
+ elif "groups" in yaml:
+ # more Bcfg2-ish output format
+ groups = yaml['groups']
+ if groups:
+ if isinstance(groups, list):
+ self.debug_log("ENC %s adding groups to %s: %s" %
+ (enc, metadata.hostname, groups))
+ cache['groups'].extend(groups)
+ else:
+ self.debug_log("ENC %s adding groups to %s: %s" %
+ (enc, metadata.hostname, groups.keys()))
+ for group, params in groups.items():
+ cache['groups'].append(group)
+ if params:
+ cache['params'].update(params)
+ if "parameters" in yaml and yaml['parameters']:
+ cache['params'].update(yaml['parameters'])
+ if "environment" in yaml:
+ self.logger.info("Ignoring unsupported environment section of "
+ "ENC %s for %s" % (enc, metadata.hostname))
+
+ self.cache[metadata.hostname] = cache
+
+ def get_additional_groups(self, metadata):
+ if metadata.hostname not in self.cache:
+ self._run_encs(metadata)
+ return self.cache[metadata.hostname]['groups']
+
+ def get_additional_data(self, metadata):
+ if metadata.hostname not in self.cache:
+ self._run_encs(metadata)
+ return self.cache[metadata.hostname]['params']
+
+ def end_client_run(self, metadata):
+ """ clear the entire cache at the end of each client run. this
+ guarantees that each client will run all ENCs at or near the
+ start of each run; we have to clear the entire cache instead
+ of just the cache for this client because a client that builds
+ templates that use metadata for other clients will populate
+ the cache for those clients, which we don't want. This makes
+ the caching less than stellar, but it does prevent multiple
+ runs of ENCs for a single host a) for groups and data
+ separately; and b) when a single client's metadata is
+ generated multiple times by separate templates """
+ self.cache = dict()
+
+ def end_statistics(self, metadata):
+ self.end_client_run(self, metadata)
diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py
new file mode 100644
index 000000000..2059baf60
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py
@@ -0,0 +1,46 @@
+import os
+import logging
+import binascii
+import posixpath
+
+import Bcfg2.Server.Plugin
+logger = logging.getLogger(__name__)
+
+class SEModuleData(Bcfg2.Server.Plugin.SpecificData):
+ def bind_entry(self, entry, _):
+ entry.set('encoding', 'base64')
+ entry.text = binascii.b2a_base64(self.data)
+
+
+class SEModules(Bcfg2.Server.Plugin.GroupSpool):
+ """ Handle SELinux 'module' entries """
+ name = 'SEModules'
+ __author__ = 'chris.a.st.pierre@gmail.com'
+ es_cls = Bcfg2.Server.Plugin.EntrySet
+ es_child_cls = SEModuleData
+ entry_type = 'SELinux'
+ experimental = True
+
+ def _get_module_name(self, entry):
+ """ GroupSpool stores entries as /foo.pp, but we want people
+ to be able to specify module entries as name='foo' or
+ name='foo.pp', so we put this abstraction in between """
+ if entry.get("name").endswith(".pp"):
+ name = entry.get("name")
+ else:
+ name = entry.get("name") + ".pp"
+ return "/" + name
+
+ def HandlesEntry(self, entry, metadata):
+ if entry.tag in self.Entries and entry.get('type') == 'module':
+ return self._get_module_name(entry) in self.Entries[entry.tag]
+ return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry,
+ metadata)
+
+ def HandleEntry(self, entry, metadata):
+ entry.set("name", self._get_module_name(entry))
+ return self.Entries[entry.tag][name](entry, metadata)
+
+ def add_entry(self, event):
+ self.filename_pattern = os.path.basename(event.filename)
+ Bcfg2.Server.Plugin.GroupSpool.add_entry(self, event)
diff --git a/src/lib/Bcfg2/Server/Plugins/SGenshi.py b/src/lib/Bcfg2/Server/Plugins/SGenshi.py
index 0ba08125e..12c125c62 100644
--- a/src/lib/Bcfg2/Server/Plugins/SGenshi.py
+++ b/src/lib/Bcfg2/Server/Plugins/SGenshi.py
@@ -7,7 +7,7 @@ import logging
import copy
import sys
import os.path
-
+import Bcfg2.Server
import Bcfg2.Server.Plugin
import Bcfg2.Server.Plugins.TGenshi
@@ -28,7 +28,8 @@ class SGenshiTemplateFile(Bcfg2.Server.Plugins.TGenshi.TemplateFile,
try:
stream = self.template.generate(metadata=metadata).filter( \
Bcfg2.Server.Plugins.TGenshi.removecomment)
- data = lxml.etree.XML(stream.render('xml', strip_whitespace=False))
+ data = lxml.etree.XML(stream.render('xml', strip_whitespace=False),
+ parser=Bcfg2.Server.XMLParser)
bundlename = os.path.splitext(os.path.basename(self.name))[0]
bundle = lxml.etree.Element('Bundle', name=bundlename)
for item in self.Match(metadata, data):
diff --git a/src/lib/Bcfg2/Server/Plugins/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
index 0072dc62d..d207c45a2 100644
--- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py
+++ b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
@@ -3,12 +3,15 @@ import Bcfg2.Options
import lxml.etree
import posixpath
import tempfile
-import pipes
import os
from subprocess import Popen, PIPE, STDOUT
# Compatibility import
from Bcfg2.Bcfg2Py3k import ConfigParser
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
"""
@@ -22,6 +25,10 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
cert_specs = {}
CAs = {}
+ def __init__(self, core, datastore):
+ Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore)
+ self.infoxml = dict()
+
def HandleEvent(self, event=None):
"""
Updates which files this plugin handles based upon filesystem events.
@@ -37,19 +44,21 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
else:
ident = self.handles[event.requestID][:-1]
- fname = "".join([ident, '/', event.filename])
+ fname = os.path.join(ident, event.filename)
if event.filename.endswith('.xml'):
if action in ['exists', 'created', 'changed']:
if event.filename.endswith('key.xml'):
- key_spec = dict(list(lxml.etree.parse(epath).find('Key').items()))
+ key_spec = dict(list(lxml.etree.parse(epath,
+ parser=Bcfg2.Server.XMLParser).find('Key').items()))
self.key_specs[ident] = {
'bits': key_spec.get('bits', 2048),
'type': key_spec.get('type', 'rsa')
}
self.Entries['Path'][ident] = self.get_key
elif event.filename.endswith('cert.xml'):
- cert_spec = dict(list(lxml.etree.parse(epath).find('Cert').items()))
+ cert_spec = dict(list(lxml.etree.parse(epath,
+ parser=Bcfg2.Server.XMLParser).find('Cert').items()))
ca = cert_spec.get('ca', 'default')
self.cert_specs[ident] = {
'ca': ca,
@@ -67,6 +76,10 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
cp.read(self.core.cfile)
self.CAs[ca] = dict(cp.items('sslca_' + ca))
self.Entries['Path'][ident] = self.get_cert
+ elif event.filename.endswith("info.xml"):
+ self.infoxml[ident] = Bcfg2.Server.Plugin.InfoXML(epath,
+ noprio=True)
+ self.infoxml[ident].HandleEvent(event)
if action == 'deleted':
if ident in self.Entries['Path']:
del self.Entries['Path'][ident]
@@ -90,28 +103,27 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
either grabs a prexisting key hostfile, or triggers the generation
of a new key if one doesn't exist.
"""
- # set path type and permissions, otherwise bcfg2 won't bind the file
- permdata = {'owner': 'root',
- 'group': 'root',
- 'type': 'file',
- 'perms': '644'}
- [entry.attrib.__setitem__(key, permdata[key]) for key in permdata]
-
# check if we already have a hostfile, or need to generate a new key
# TODO: verify key fits the specs
path = entry.get('name')
- filename = "".join([path, '/', path.rsplit('/', 1)[1],
- '.H_', metadata.hostname])
+ filename = os.path.join(path, "%s.H_%s" % (os.path.basename(path),
+ metadata.hostname))
if filename not in list(self.entries.keys()):
key = self.build_key(filename, entry, metadata)
open(self.data + filename, 'w').write(key)
entry.text = key
- self.entries[filename] = self.__child__("%s%s" % (self.data,
- filename))
+ self.entries[filename] = self.__child__(self.data + filename)
self.entries[filename].HandleEvent()
else:
entry.text = self.entries[filename].data
+ entry.set("type", "file")
+ if path in self.infoxml:
+ Bcfg2.Server.Plugin.bind_info(entry, metadata,
+ infoxml=self.infoxml[path])
+ else:
+ Bcfg2.Server.Plugin.bind_info(entry, metadata)
+
def build_key(self, filename, entry, metadata):
"""
generates a new key according the the specification
@@ -130,56 +142,61 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
either grabs a prexisting cert hostfile, or triggers the generation
of a new cert if one doesn't exist.
"""
- # set path type and permissions, otherwise bcfg2 won't bind the file
- permdata = {'owner': 'root',
- 'group': 'root',
- 'type': 'file',
- 'perms': '644'}
- [entry.attrib.__setitem__(key, permdata[key]) for key in permdata]
-
path = entry.get('name')
- filename = "".join([path, '/', path.rsplit('/', 1)[1],
- '.H_', metadata.hostname])
+ filename = os.path.join(path, "%s.H_%s" % (os.path.basename(path),
+ metadata.hostname))
# first - ensure we have a key to work with
key = self.cert_specs[entry.get('name')].get('key')
- key_filename = "".join([key, '/', key.rsplit('/', 1)[1],
- '.H_', metadata.hostname])
+ key_filename = os.path.join(key, "%s.H_%s" % (os.path.basename(key),
+ metadata.hostname))
if key_filename not in self.entries:
e = lxml.etree.Element('Path')
- e.attrib['name'] = key
+ e.set('name', key)
self.core.Bind(e, metadata)
# check if we have a valid hostfile
- if filename in list(self.entries.keys()) and self.verify_cert(filename,
- key_filename,
- entry):
+ if (filename in list(self.entries.keys()) and
+ self.verify_cert(filename, key_filename, entry)):
entry.text = self.entries[filename].data
else:
cert = self.build_cert(key_filename, entry, metadata)
open(self.data + filename, 'w').write(cert)
- self.entries[filename] = self.__child__("%s%s" % (self.data,
- filename))
+ self.entries[filename] = self.__child__(self.data + filename)
self.entries[filename].HandleEvent()
entry.text = cert
+ entry.set("type", "file")
+ if path in self.infoxml:
+ Bcfg2.Server.Plugin.bind_info(entry, metadata,
+ infoxml=self.infoxml[path])
+ else:
+ Bcfg2.Server.Plugin.bind_info(entry, metadata)
+
def verify_cert(self, filename, key_filename, entry):
- if self.verify_cert_against_ca(filename, entry):
- if self.verify_cert_against_key(filename, key_filename):
- return True
- return False
+ do_verify = self.CAs[self.cert_specs[entry.get('name')]['ca']].get('verify_certs', True)
+ if do_verify:
+ return (self.verify_cert_against_ca(filename, entry) and
+ self.verify_cert_against_key(filename, key_filename))
+ return True
def verify_cert_against_ca(self, filename, entry):
"""
check that a certificate validates against the ca cert,
and that it has not expired.
"""
- chaincert = self.CAs[self.cert_specs[entry.get('name')]['ca']].get('chaincert')
+ chaincert = \
+ self.CAs[self.cert_specs[entry.get('name')]['ca']].get('chaincert')
cert = self.data + filename
- res = Popen(["openssl", "verify", "-CAfile", chaincert, cert],
+ res = Popen(["openssl", "verify", "-untrusted", chaincert, "-purpose",
+ "sslserver", cert],
stdout=PIPE, stderr=STDOUT).stdout.read()
if res == cert + ": OK\n":
+ self.debug_log("SSLCA: %s verified successfully against CA" %
+ entry.get("name"))
return True
+ self.logger.warning("SSLCA: %s failed verification against CA: %s" %
+ (entry.get("name"), res))
return False
def verify_cert_against_key(self, filename, key_filename):
@@ -188,14 +205,20 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
"""
cert = self.data + filename
key = self.data + key_filename
- cmd = ("openssl x509 -noout -modulus -in %s | openssl md5" %
- pipes.quote(cert))
- cert_md5 = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT).stdout.read()
- cmd = ("openssl rsa -noout -modulus -in %s | openssl md5" %
- pipes.quote(key))
- key_md5 = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT).stdout.read()
+ cert_md5 = \
+ md5(Popen(["openssl", "x509", "-noout", "-modulus", "-in", cert],
+ stdout=PIPE,
+ stderr=STDOUT).stdout.read().strip()).hexdigest()
+ key_md5 = \
+ md5(Popen(["openssl", "rsa", "-noout", "-modulus", "-in", key],
+ stdout=PIPE,
+ stderr=STDOUT).stdout.read().strip()).hexdigest()
if cert_md5 == key_md5:
+ self.debug_log("SSLCA: %s verified successfully against key %s" %
+ (filename, key_filename))
return True
+ self.logger.warning("SSLCA: %s failed verification against key %s" %
+ (filename, key_filename))
return False
def build_cert(self, key_filename, entry, metadata):
diff --git a/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
new file mode 100644
index 000000000..aad92b7c7
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
@@ -0,0 +1,32 @@
+import Bcfg2.Server.Plugin
+
+class ServiceCompat(Bcfg2.Server.Plugin.Plugin,
+ Bcfg2.Server.Plugin.StructureValidator):
+ """ Use old-style service modes for older clients """
+ name = 'ServiceCompat'
+ __author__ = 'bcfg-dev@mcs.anl.gov'
+ mode_map = {('true', 'true'): 'default',
+ ('interactive', 'true'): 'interactive_only',
+ ('false', 'false'): 'manual'}
+
+ def validate_structures(self, metadata, structures):
+ """ Apply defaults """
+ if metadata.version_info and metadata.version_info > (1, 3, 0, '', 0):
+ # do not care about a client that is _any_ 1.3.0 release
+ # (including prereleases and RCs)
+ return
+
+ for struct in structures:
+ for entry in struct.xpath("//BoundService|//Service"):
+ mode_key = (entry.get("restart", "true").lower(),
+ entry.get("install", "true").lower())
+ try:
+ mode = self.mode_map[mode_key]
+ except KeyError:
+ self.logger.info("Could not map restart and install "
+ "settings of %s:%s to an old-style "
+ "Service mode for %s; using 'manual'" %
+ (entry.tag, entry.get("name"),
+ metadata.hostname))
+ mode = "manual"
+ entry.set("mode", mode)
diff --git a/src/lib/Bcfg2/Server/Plugins/Snapshots.py b/src/lib/Bcfg2/Server/Plugins/Snapshots.py
index aeb3b9f74..232dbb0c3 100644
--- a/src/lib/Bcfg2/Server/Plugins/Snapshots.py
+++ b/src/lib/Bcfg2/Server/Plugins/Snapshots.py
@@ -14,6 +14,7 @@ import threading
# Compatibility import
from Bcfg2.Bcfg2Py3k import Queue
+from Bcfg2.Bcfg2Py3k import u_str
logger = logging.getLogger('Snapshots')
@@ -28,14 +29,6 @@ datafields = {
}
-# py3k compatibility
-def u_str(string):
- if sys.hexversion >= 0x03000000:
- return string
- else:
- return unicode(string)
-
-
def build_snap_ent(entry):
basefields = []
if entry.tag in ['Package', 'Service']:
diff --git a/src/lib/Bcfg2/Server/Plugins/Statistics.py b/src/lib/Bcfg2/Server/Plugins/Statistics.py
index 265ef95a8..9af7549ff 100644
--- a/src/lib/Bcfg2/Server/Plugins/Statistics.py
+++ b/src/lib/Bcfg2/Server/Plugins/Statistics.py
@@ -7,6 +7,7 @@ import logging
from lxml.etree import XML, SubElement, Element, XMLSyntaxError
import lxml.etree
import os
+import sys
from time import asctime, localtime, time, strptime, mktime
import threading
diff --git a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
index 2c0ee03e0..3712506d6 100644
--- a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
+++ b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
@@ -1,7 +1,9 @@
import re
import imp
import sys
+import glob
import logging
+import Bcfg2.Server.Lint
import Bcfg2.Server.Plugin
logger = logging.getLogger(__name__)
@@ -81,3 +83,67 @@ class TemplateHelper(Bcfg2.Server.Plugin.Plugin,
def get_additional_data(self, metadata):
return dict([(h._module_name, h)
for h in list(self.helpers.entries.values())])
+
+
+class TemplateHelperLint(Bcfg2.Server.Lint.ServerlessPlugin):
+ """ find duplicate Pkgmgr entries with the same priority """
+ def __init__(self, *args, **kwargs):
+ Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs)
+ hm = HelperModule("foo.py", None, None)
+ self.reserved_keywords = dir(hm)
+
+ def Run(self):
+ for helper in glob.glob(os.path.join(self.config['repo'],
+ "TemplateHelper",
+ "*.py")):
+ if not self.HandlesFile(helper):
+ continue
+ self.check_helper(helper)
+
+ def check_helper(self, helper):
+ match = HelperModule._module_name_re.search(helper)
+ if match:
+ module_name = match.group(1)
+ else:
+ module_name = helper
+
+ try:
+ module = imp.load_source(module_name, helper)
+ except:
+ err = sys.exc_info()[1]
+ self.LintError("templatehelper-import-error",
+ "Failed to import %s: %s" %
+ (helper, err))
+ continue
+
+ if not hasattr(module, "__export__"):
+ self.LintError("templatehelper-no-export",
+ "%s has no __export__ list" % helper)
+ continue
+ elif not isinstance(module.__export__, list):
+ self.LintError("templatehelper-nonlist-export",
+ "__export__ is not a list in %s" % helper)
+ continue
+
+ for sym in module.__export__:
+ if not hasattr(module, sym):
+ self.LintError("templatehelper-nonexistent-export",
+ "%s: exported symbol %s does not exist" %
+ (helper, sym))
+ elif sym in self.reserved_keywords:
+ self.LintError("templatehelper-reserved-export",
+ "%s: exported symbol %s is reserved" %
+ (helper, sym))
+ elif sym.startswith("_"):
+ self.LintError("templatehelper-underscore-export",
+ "%s: exported symbol %s starts with underscore" %
+ (helper, sym))
+
+ @classmethod
+ def Errors(cls):
+ return {"templatehelper-import-error":"error",
+ "templatehelper-no-export":"error",
+ "templatehelper-nonlist-export":"error",
+ "templatehelper-nonexistent-export":"error",
+ "templatehelper-reserved-export":"error",
+ "templatehelper-underscore-export":"warning"}
diff --git a/src/lib/Bcfg2/Server/Plugins/Trigger.py b/src/lib/Bcfg2/Server/Plugins/Trigger.py
index b0d21545c..313a1bf03 100644
--- a/src/lib/Bcfg2/Server/Plugins/Trigger.py
+++ b/src/lib/Bcfg2/Server/Plugins/Trigger.py
@@ -1,43 +1,52 @@
import os
+import pipes
import Bcfg2.Server.Plugin
+from subprocess import Popen, PIPE
+class TriggerFile(Bcfg2.Server.Plugin.FileBacked):
+ def HandleEvent(self, event=None):
+ return
-def async_run(prog, args):
- pid = os.fork()
- if pid:
- os.waitpid(pid, 0)
- else:
- dpid = os.fork()
- if not dpid:
- os.system(" ".join([prog] + args))
- os._exit(0)
+ def __str__(self):
+ return "%s: %s" % (self.__class__.__name__, self.name)
class Trigger(Bcfg2.Server.Plugin.Plugin,
- Bcfg2.Server.Plugin.Statistics):
+ Bcfg2.Server.Plugin.ClientRunHooks,
+ Bcfg2.Server.Plugin.DirectoryBacked):
"""Trigger is a plugin that calls external scripts (on the server)."""
name = 'Trigger'
__author__ = 'bcfg-dev@mcs.anl.gov'
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
- Bcfg2.Server.Plugin.Statistics.__init__(self)
- try:
- os.stat(self.data)
- except:
- self.logger.error("Trigger: spool directory %s does not exist; "
- "unloading" % self.data)
- raise Bcfg2.Server.Plugin.PluginInitError
-
- def process_statistics(self, metadata, _):
+ Bcfg2.Server.Plugin.ClientRunHooks.__init__(self)
+ Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data,
+ self.core.fam)
+
+ def async_run(self, args):
+ pid = os.fork()
+ if pid:
+ os.waitpid(pid, 0)
+ else:
+ dpid = os.fork()
+ if not dpid:
+ self.debug_log("Running %s" % " ".join(pipes.quote(a)
+ for a in args))
+ proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+ (out, err) = proc.communicate()
+ rv = proc.wait()
+ if rv != 0:
+ self.logger.error("Trigger: Error running %s (%s): %s" %
+ (args[0], rv, err))
+ elif err:
+ self.debug_log("Trigger: Error: %s" % err)
+ os._exit(0)
+
+
+ def end_client_run(self, metadata):
args = [metadata.hostname, '-p', metadata.profile, '-g',
':'.join([g for g in metadata.groups])]
- for notifier in os.listdir(self.data):
- if ((notifier[-1] == '~') or
- (notifier[:2] == '.#') or
- (notifier[-4:] == '.swp') or
- (notifier in ['SCCS', '.svn', '4913'])):
- continue
- npath = self.data + '/' + notifier
- self.logger.debug("Running %s %s" % (npath, " ".join(args)))
- async_run(npath, args)
+ for notifier in self.entries.keys():
+ npath = os.path.join(self.data, notifier)
+ self.async_run([npath] + args)
diff --git a/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_0_x.py b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_0_x.py
new file mode 100644
index 000000000..54ba07554
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_0_x.py
@@ -0,0 +1,11 @@
+"""
+1_0_x.py
+
+This file should contain updates relevant to the 1.0.x branches ONLY.
+The updates() method must be defined and it should return an Updater object
+"""
+from Bcfg2.Server.Reports.Updater import UnsupportedUpdate
+
+def updates():
+ return UnsupportedUpdate("1.0", 10)
+
diff --git a/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_1_x.py b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_1_x.py
new file mode 100644
index 000000000..26194cb67
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_1_x.py
@@ -0,0 +1,59 @@
+"""
+1_1_x.py
+
+This file should contain updates relevant to the 1.1.x branches ONLY.
+The updates() method must be defined and it should return an Updater object
+"""
+from Bcfg2.Server.Reports.Updater import Updater
+from Bcfg2.Server.Reports.Updater.Routines import updatercallable
+
+from django.db import connection
+import sys
+import Bcfg2.Server.Reports.settings
+from Bcfg2.Server.Reports.reports.models import \
+ TYPE_BAD, TYPE_MODIFIED, TYPE_EXTRA
+
+@updatercallable
+def _interactions_constraint_or_idx():
+ """sqlite doesn't support alter tables.. or constraints"""
+ cursor = connection.cursor()
+ try:
+ cursor.execute('alter table reports_interaction add constraint reports_interaction_20100601 unique (client_id,timestamp)')
+ except:
+ cursor.execute('create unique index reports_interaction_20100601 on reports_interaction (client_id,timestamp)')
+
+
+@updatercallable
+def _populate_interaction_entry_counts():
+ '''Populate up the type totals for the interaction table'''
+ cursor = connection.cursor()
+ count_field = {TYPE_BAD: 'bad_entries',
+ TYPE_MODIFIED: 'modified_entries',
+ TYPE_EXTRA: 'extra_entries'}
+
+ for type in list(count_field.keys()):
+ cursor.execute("select count(type), interaction_id " +
+ "from reports_entries_interactions where type = %s group by interaction_id" % type)
+ updates = []
+ for row in cursor.fetchall():
+ updates.append(row)
+ try:
+ cursor.executemany("update reports_interaction set " + count_field[type] + "=%s where id = %s", updates)
+ except Exception:
+ e = sys.exc_info()[1]
+ print(e)
+ cursor.close()
+
+
+def updates():
+ fixes = Updater("1.1")
+ fixes.override_base_version(12) # Do not do this in new code
+
+ fixes.add('alter table reports_interaction add column bad_entries integer not null default -1;')
+ fixes.add('alter table reports_interaction add column modified_entries integer not null default -1;')
+ fixes.add('alter table reports_interaction add column extra_entries integer not null default -1;')
+ fixes.add(_populate_interaction_entry_counts())
+ fixes.add(_interactions_constraint_or_idx())
+ fixes.add('alter table reports_reason add is_binary bool NOT NULL default False;')
+ return fixes
+
diff --git a/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_2_x.py b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_2_x.py
new file mode 100644
index 000000000..22bd937c2
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_2_x.py
@@ -0,0 +1,15 @@
+"""
+1_2_x.py
+
+This file should contain updates relevant to the 1.2.x branches ONLY.
+The updates() method must be defined and it should return an Updater object
+"""
+from Bcfg2.Server.Reports.Updater import Updater
+from Bcfg2.Server.Reports.Updater.Routines import updatercallable
+
+def updates():
+ fixes = Updater("1.2")
+ fixes.override_base_version(18) # Do not do this in new code
+ fixes.add('alter table reports_reason add is_sensitive bool NOT NULL default False;')
+ return fixes
+
diff --git a/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_3_0.py b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_3_0.py
new file mode 100644
index 000000000..1a2fff1ea
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/Updater/Changes/1_3_0.py
@@ -0,0 +1,27 @@
+"""
+1_3_0.py
+
+This file should contain updates relevant to the 1.3.x branches ONLY.
+The updates() method must be defined and it should return an Updater object
+"""
+from Bcfg2.Server.Reports.Updater import Updater, UpdaterError
+from Bcfg2.Server.Reports.Updater.Routines import AddColumns, \
+ RemoveColumns, RebuildTable, DropTable
+
+from Bcfg2.Server.Reports.reports.models import Reason, Interaction
+
+
+def updates():
+ fixes = Updater("1.3")
+ fixes.add(RemoveColumns(Interaction, 'client_version'))
+ fixes.add(AddColumns(Reason))
+ fixes.add(RebuildTable(Reason, [
+ 'owner', 'current_owner',
+ 'group', 'current_group',
+ 'perms', 'current_perms',
+ 'status', 'current_status',
+ 'to', 'current_to']))
+ fixes.add(DropTable('reports_ping'))
+
+ return fixes
+
diff --git a/src/lib/Bcfg2/Server/Reports/Updater/Changes/__init__.py b/src/lib/Bcfg2/Server/Reports/Updater/Changes/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/Updater/Changes/__init__.py
diff --git a/src/lib/Bcfg2/Server/Reports/Updater/Routines.py b/src/lib/Bcfg2/Server/Reports/Updater/Routines.py
new file mode 100644
index 000000000..edb21c321
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/Updater/Routines.py
@@ -0,0 +1,279 @@
+import logging
+import traceback
+from django.db.models.fields import NOT_PROVIDED
+from django.db import connection, DatabaseError, backend, models
+from django.core.management.color import no_style
+from django.core.management.sql import sql_create
+import django.core.management
+
+import Bcfg2.Server.Reports.settings
+
+logger = logging.getLogger(__name__)
+
+def _quote(value):
+ """
+ Quote a string to use as a table name or column
+ """
+ return backend.DatabaseOperations().quote_name(value)
+
+
+def _rebuild_sqlite_table(model):
+ """Sqlite doesn't support most alter table statments. This streamlines the
+ rebuild process"""
+ try:
+ cursor = connection.cursor()
+ table_name = model._meta.db_table
+
+ # Build create staement from django
+ model._meta.db_table = "%s_temp" % table_name
+ sql, references = connection.creation.sql_create_model(model, no_style())
+ columns = ",".join([_quote(f.column) \
+ for f in model._meta.fields])
+
+ # Create a temp table
+ [cursor.execute(s) for s in sql]
+
+ # Fill the table
+ tbl_name = _quote(table_name)
+ tmp_tbl_name = _quote(model._meta.db_table)
+ # Reset this
+ model._meta.db_table = table_name
+ cursor.execute("insert into %s(%s) select %s from %s;" % (
+ tmp_tbl_name,
+ columns,
+ columns,
+ tbl_name))
+ cursor.execute("drop table %s" % tbl_name)
+
+ # Call syncdb to create the table again
+ django.core.management.call_command("syncdb", interactive=False, verbosity=0)
+ # syncdb closes our cursor
+ cursor = connection.cursor()
+ # Repopulate
+ cursor.execute('insert into %s(%s) select %s from %s;' % (tbl_name,
+ columns,
+ columns,
+ tmp_tbl_name))
+ cursor.execute('DROP TABLE %s;' % tmp_tbl_name)
+ except DatabaseError:
+ logger.error("Failed to rebuild sqlite table %s" % table_name, exc_info=1)
+ raise UpdaterRoutineException
+
+
+class UpdaterRoutineException(Exception):
+ pass
+
+
+class UpdaterRoutine(object):
+ """Base for routines."""
+ def __init__(self):
+ pass
+
+ def __str__(self):
+ return __name__
+
+ def run(self):
+ """Called to execute the action"""
+ raise UpdaterRoutineException
+
+
+
+class AddColumns(UpdaterRoutine):
+ """
+ Routine to add new columns to an existing model
+ """
+ def __init__(self, model):
+ self.model = model
+ self.model_name = model.__name__
+
+ def __str__(self):
+ return "Add new columns for model %s" % self.model_name
+
+ def run(self):
+ try:
+ cursor = connection.cursor()
+ except DatabaseError:
+ logger.error("Failed to connect to the db")
+ raise UpdaterRoutineException
+
+ try:
+ desc = {}
+ for d in connection.introspection.get_table_description(cursor,
+ self.model._meta.db_table):
+ desc[d[0]] = d
+ except DatabaseError:
+ logger.error("Failed to get table description", exc_info=1)
+ raise UpdaterRoutineException
+
+ for field in self.model._meta.fields:
+ if field.column in desc:
+ continue
+ logger.debug("Column %s does not exist yet" % field.column)
+ if field.default == NOT_PROVIDED:
+ logger.error("Cannot add a column with out a default value")
+ raise UpdaterRoutineException
+
+ sql = "ALTER TABLE %s ADD %s %s NOT NULL DEFAULT " % (
+ _quote(self.model._meta.db_table),
+ _quote(field.column), field.db_type(), )
+ db_engine = Bcfg2.Server.Reports.settings.DATABASES['default']['ENGINE']
+ if db_engine == 'django.db.backends.sqlite3':
+ sql += _quote(field.default)
+ sql_values = ()
+ else:
+ sql += '%s'
+ sql_values = (field.default, )
+ try:
+ cursor.execute(sql, sql_values)
+ logger.debug("Added column %s to %s" %
+ (field.column, self.model._meta.db_table))
+ except DatabaseError:
+ logger.error("Unable to add column %s" % field.column)
+ raise UpdaterRoutineException
+
+
+class RebuildTable(UpdaterRoutine):
+ """
+ Rebuild the table for an existing model. Use this if field types have changed.
+ """
+ def __init__(self, model, columns):
+ self.model = model
+ self.model_name = model.__name__
+
+ if type(columns) == str:
+ self.columns = [columns]
+ elif type(columns) in (tuple, list):
+ self.columns = columns
+ else:
+ logger.error("Columns must be a str, tuple, or list")
+ raise UpdaterRoutineException
+
+
+ def __str__(self):
+ return "Rebuild columns for model %s" % self.model_name
+
+ def run(self):
+ try:
+ cursor = connection.cursor()
+ except DatabaseError:
+ logger.error("Failed to connect to the db")
+ raise UpdaterRoutineException
+
+ db_engine = Bcfg2.Server.Reports.settings.DATABASES['default']['ENGINE']
+ if db_engine == 'django.db.backends.sqlite3':
+ """ Sqlite is a special case. Altering columns is not supported. """
+ _rebuild_sqlite_table(self.model)
+ return
+
+ if db_engine == 'django.db.backends.mysql':
+ modify_cmd = 'MODIFY '
+ else:
+ modify_cmd = 'ALTER COLUMN '
+
+ col_strings = []
+ for column in self.columns:
+ col_strings.append("%s %s %s" % ( \
+ modify_cmd,
+ _quote(column),
+ self.model._meta.get_field(column).db_type()
+ ))
+
+ try:
+ cursor.execute('ALTER TABLE %s %s' %
+ (_quote(self.model._meta.db_table), ", ".join(col_strings)))
+ except DatabaseError:
+ logger.debug("Failed modify table %s" % self.model._meta.db_table)
+ raise UpdaterRoutineException
+
+
+
+class RemoveColumns(RebuildTable):
+ """
+ Routine to remove columns from an existing model
+ """
+ def __init__(self, model, columns):
+ super(RemoveColumns, self).__init__(model, columns)
+
+
+ def __str__(self):
+ return "Remove columns from model %s" % self.model_name
+
+ def run(self):
+ try:
+ cursor = connection.cursor()
+ except DatabaseError:
+ logger.error("Failed to connect to the db")
+ raise UpdaterRoutineException
+
+ try:
+ columns = [d[0] for d in connection.introspection.get_table_description(cursor,
+ self.model._meta.db_table)]
+ except DatabaseError:
+ logger.error("Failed to get table description", exc_info=1)
+ raise UpdaterRoutineException
+
+ for column in self.columns:
+ if column not in columns:
+ logger.warning("Cannot drop column %s: does not exist" % column)
+ continue
+
+ logger.debug("Dropping column %s" % column)
+
+ db_engine = Bcfg2.Server.Reports.settings.DATABASES['default']['ENGINE']
+ if db_engine == 'django.db.backends.sqlite3':
+ _rebuild_sqlite_table(self.model)
+ else:
+ sql = "alter table %s drop column %s" % \
+ (_quote(self.model._meta.db_table), _quote(column), )
+ try:
+ cursor.execute(sql)
+ except DatabaseError:
+ logger.debug("Failed to drop column %s from %s" %
+ (column, self.model._meta.db_table))
+ raise UpdaterRoutineException
+
+
+class DropTable(UpdaterRoutine):
+ """
+ Drop a table
+ """
+ def __init__(self, table_name):
+ self.table_name = table_name
+
+ def __str__(self):
+ return "Drop table %s" % self.table_name
+
+ def run(self):
+ try:
+ cursor = connection.cursor()
+ cursor.execute('DROP TABLE %s' % _quote(self.table_name))
+ except DatabaseError:
+ logger.error("Failed to drop table: %s" %
+ traceback.format_exc().splitlines()[-1])
+ raise UpdaterRoutineException
+
+
+class UpdaterCallable(UpdaterRoutine):
+ """Helper for routines. Basically delays execution"""
+ def __init__(self, fn):
+ self.fn = fn
+ self.args = []
+ self.kwargs = {}
+
+ def __call__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ return self
+
+ def __str__(self):
+ return self.fn.__name__
+
+ def run(self):
+ self.fn(*self.args, **self.kwargs)
+
+def updatercallable(fn):
+ """Decorator for UpdaterCallable. Use for any function passed
+ into the fixes list"""
+ return UpdaterCallable(fn)
+
+
diff --git a/src/lib/Bcfg2/Server/Reports/Updater/__init__.py b/src/lib/Bcfg2/Server/Reports/Updater/__init__.py
new file mode 100644
index 000000000..3038e9691
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/Updater/__init__.py
@@ -0,0 +1,239 @@
+from django.db import connection, DatabaseError
+import django.core.management
+import logging
+import pkgutil
+import re
+import sys
+import traceback
+
+from Bcfg2.Server.Reports.reports.models import InternalDatabaseVersion
+from Bcfg2.Server.Reports.Updater.Routines import UpdaterRoutineException, \
+ UpdaterRoutine
+from Bcfg2.Server.Reports.Updater import Changes
+
+logger = logging.getLogger(__name__)
+
+class UpdaterError(Exception):
+ pass
+
+
+class SchemaTooOldError(UpdaterError):
+ pass
+
+
+def _walk_packages(path):
+ """Python 2.4 lacks this routine"""
+ import glob
+ submodules = []
+ for path in __path__:
+ for submodule in glob.glob("%s/*.py" % path):
+ mod = '.'.join(submodule.split("/")[-1].split('.')[:-1])
+ if mod != '__init__':
+ submodules.append((None, mod, False))
+ return submodules
+
+
+def _release_to_version(release):
+ """
+ Build a release base for a version
+
+ Expects a string of the form 00.00
+
+ returns an integer of the form MMmm00
+ """
+ regex = re.compile("^(\d+)\.(\d+)$")
+ m = regex.match(release)
+ if not m:
+ logger.error("Invalid release string: %s" % release)
+ raise TypeError
+ return int("%02d%02d00" % (int(m.group(1)), int(m.group(2))))
+
+
+class Updater(object):
+ """Database updater to standardize updates"""
+
+ def __init__(self, release):
+ self._cursor = None
+ self._release = release
+ try:
+ self._base_version = _release_to_version(release)
+ except:
+ raise UpdaterError
+
+ self._fixes = []
+ self._version = -1
+
+
+ def __cmp__(self, other):
+ return self._base_version - other._base_version
+
+ @property
+ def release(self):
+ return self._release
+
+ @property
+ def version(self):
+ if self._version < 0:
+ try:
+ iv = InternalDatabaseVersion.objects.latest()
+ self._version = iv.version
+ except InternalDatabaseVersion.DoesNotExist:
+ raise UpdaterError
+ return self._version
+
+ @property
+ def cursor(self):
+ if not self._cursor:
+ self._cursor = connection.cursor()
+ return self._cursor
+
+ @property
+ def target_version(self):
+ if(len(self._fixes) == 0):
+ return self._base_version
+ else:
+ return self._base_version + len(self._fixes) - 1
+
+
+ def add(self, update):
+ if type(update) == str or isinstance(update, UpdaterRoutine):
+ self._fixes.append(update)
+ else:
+ raise TypeError
+
+
+ def override_base_version(self, version):
+ """Override our starting point for old releases. New code should
+ not use this method"""
+ self._base_version = int(version)
+
+
+ @staticmethod
+ def get_current_version():
+ """Queries the db for the latest version. Returns 0 for a fresh install"""
+
+ if "call_command" in dir(django.core.management):
+ django.core.management.call_command("syncdb", interactive=False, verbosity=0)
+ else:
+ logger.warning("Unable to call syndb routine")
+ raise UpdaterError
+
+ try:
+ iv = InternalDatabaseVersion.objects.latest()
+ version = iv.version
+ except InternalDatabaseVersion.DoesNotExist:
+ version = 0
+
+ return version
+
+
+ def syncdb(self):
+ """Function to do the syncronisation for the models"""
+
+ self._version = Updater.get_current_version()
+ self._cursor = None
+
+
+ def increment(self):
+ """Increment schema version in the database"""
+ if self._version < self._base_version:
+ self._version = self._base_version
+ else:
+ self._version += 1
+ InternalDatabaseVersion.objects.create(version=self._version)
+
+ def apply(self):
+ """Apply pending schema changes"""
+
+ if self.version >= self.target_version:
+ logger.debug("No updates for release %s" % self._release)
+ return
+
+ logger.debug("Applying updates for release %s" % self._release)
+
+ if self.version < self._base_version:
+ start = 0
+ else:
+ start = self.version - self._base_version + 1
+
+ try:
+ for fix in self._fixes[start:]:
+ if type(fix) == str:
+ self.cursor.execute(fix)
+ elif isinstance(fix, UpdaterRoutine):
+ fix.run()
+ else:
+ logger.error("Invalid schema change at %s" % \
+ self._version + 1)
+ self.increment()
+ logger.debug("Applied schema change number %s: %s" % \
+ (self.version, fix))
+ logger.info("Applied schema changes for release %s" % self._release)
+ except:
+ logger.error("Failed to perform db update %s (%s): %s" % \
+ (self._version + 1, fix, traceback.format_exc().splitlines()[-1]))
+ raise UpdaterError
+
+
+class UnsupportedUpdate(Updater):
+ """Handle an unsupported update"""
+
+ def __init__(self, release, version):
+ super(UnsupportedUpdate, self).__init__(release)
+ self._base_version = version
+
+ def apply(self):
+ """Raise an exception if we're too old"""
+
+ if self.version < self.target_version:
+ logger.error("Upgrade from release %s unsupported" % self._release)
+ raise SchemaTooOldError
+
+
+def update_database():
+ """method to search where we are in the revision
+ of the database models and update them"""
+ try:
+ logger.debug("Verifying database schema")
+
+ updaters = []
+ if hasattr(pkgutil, 'walk_packages'):
+ submodules = pkgutil.walk_packages(path=Changes.__path__)
+ else:
+ #python 2.4
+ submodules = _walk_packages(Changes.__path__)
+ for loader, submodule, ispkg in submodules:
+ if ispkg:
+ continue
+ try:
+ updates = getattr(
+ __import__("%s.%s" % (Changes.__name__, submodule),
+ globals(), locals(), ['*']),
+ "updates")
+ updaters.append(updates())
+ except ImportError:
+ logger.error("Failed to import %s" % submodule)
+ except AttributeError:
+ logger.warning("Module %s does not have an updates function" % submodule)
+ except:
+ logger.error("Failed to build updater for %s" % submodule, exc_info=1)
+ raise UpdaterError
+
+ current_version = Updater.get_current_version()
+ logger.debug("Database version at %s" % current_version)
+
+ if current_version > 0:
+ [u.apply() for u in sorted(updaters)]
+ logger.debug("Database version at %s" % Updater.get_current_version())
+ else:
+ target = updaters[-1].target_version
+ InternalDatabaseVersion.objects.create(version=target)
+ logger.info("A new database was created")
+
+ except UpdaterError:
+ raise
+ except:
+ logger.error("Error while updating the database")
+ for x in traceback.format_exc().splitlines():
+ logger.error(x)
+ raise UpdaterError
diff --git a/src/lib/Bcfg2/Server/Reports/importscript.py b/src/lib/Bcfg2/Server/Reports/importscript.py
index 16df86a9b..14f2bb1f9 100755
--- a/src/lib/Bcfg2/Server/Reports/importscript.py
+++ b/src/lib/Bcfg2/Server/Reports/importscript.py
@@ -7,6 +7,7 @@ new statistics engine
import binascii
import os
import sys
+import traceback
try:
import Bcfg2.Server.Reports.settings
except Exception:
@@ -27,8 +28,9 @@ from lxml.etree import XML, XMLSyntaxError
from getopt import getopt, GetoptError
from datetime import datetime
from time import strptime
-from django.db import connection
-from Bcfg2.Server.Reports.updatefix import update_database
+from django.db import connection, transaction
+from Bcfg2.Server.Plugins.Metadata import ClientMetadata
+from Bcfg2.Server.Reports.Updater import update_database, UpdaterError
import logging
import Bcfg2.Logger
import platform
@@ -86,130 +88,160 @@ def build_reason_kwargs(r_ent, encoding, logger):
is_sensitive=sensitive_file,
unpruned=unpruned_entries)
+def _fetch_reason(elem, kargs, logger):
+ try:
+ rr = None
+ try:
+ rr = Reason.objects.filter(**kargs)[0]
+ except IndexError:
+ rr = Reason(**kargs)
+ rr.save()
+ logger.info("Created reason: %s" % rr.id)
+ except Exception:
+ ex = sys.exc_info()[1]
+ logger.error("Failed to create reason for %s: %s" % (elem.get('name'), ex))
+ rr = Reason(current_exists=elem.get('current_exists',
+ default="True").capitalize() == "True")
+ rr.save()
+ return rr
-def load_stats(cdata, sdata, encoding, vlevel, logger, quick=False, location=''):
- clients = {}
- [clients.__setitem__(c.name, c) \
- for c in Client.objects.all()]
-
- pingability = {}
- [pingability.__setitem__(n.get('name'), n.get('pingable', default='N')) \
- for n in cdata.findall('Client')]
+def load_stats(sdata, encoding, vlevel, logger, quick=False, location=''):
for node in sdata.findall('Node'):
name = node.get('name')
- c_inst, created = Client.objects.get_or_create(name=name)
- if vlevel > 0:
- logger.info("Client %s added to db" % name)
- clients[name] = c_inst
- try:
- pingability[name]
- except KeyError:
- pingability[name] = 'N'
for statistics in node.findall('Statistics'):
- timestamp = datetime(*strptime(statistics.get('time'))[0:6])
- ilist = Interaction.objects.filter(client=c_inst,
- timestamp=timestamp)
- if ilist:
- current_interaction = ilist[0]
- if vlevel > 0:
- logger.info("Interaction for %s at %s with id %s already exists" % \
- (c_inst.id, timestamp, current_interaction.id))
- continue
- else:
- newint = Interaction(client=c_inst,
- timestamp=timestamp,
- state=statistics.get('state',
+ try:
+ load_stat(name, statistics, encoding, vlevel, logger, quick, location)
+ except:
+ logger.error("Failed to create interaction for %s: %s" %
+ (name, traceback.format_exc().splitlines()[-1]))
+
+@transaction.commit_on_success
+def load_stat(cobj, statistics, encoding, vlevel, logger, quick, location):
+ if isinstance(cobj, ClientMetadata):
+ client_name = cobj.hostname
+ else:
+ client_name = cobj
+ client, created = Client.objects.get_or_create(name=client_name)
+ if created and vlevel > 0:
+ logger.info("Client %s added to db" % client_name)
+
+ timestamp = datetime(*strptime(statistics.get('time'))[0:6])
+ ilist = Interaction.objects.filter(client=client,
+ timestamp=timestamp)
+ if ilist:
+ current_interaction = ilist[0]
+ if vlevel > 0:
+ logger.info("Interaction for %s at %s with id %s already exists" % \
+ (client.id, timestamp, current_interaction.id))
+ return
+ else:
+ newint = Interaction(client=client,
+ timestamp=timestamp,
+ state=statistics.get('state',
+ default="unknown"),
+ repo_rev_code=statistics.get('revision',
default="unknown"),
- repo_rev_code=statistics.get('revision',
- default="unknown"),
- goodcount=statistics.get('good',
- default="0"),
- totalcount=statistics.get('total',
- default="0"),
- server=location)
- newint.save()
- current_interaction = newint
- if vlevel > 0:
- logger.info("Interaction for %s at %s with id %s INSERTED in to db" % (c_inst.id,
- timestamp, current_interaction.id))
-
- counter_fields = {TYPE_CHOICES[0]: 0,
- TYPE_CHOICES[1]: 0,
- TYPE_CHOICES[2]: 0}
- pattern = [('Bad/*', TYPE_CHOICES[0]),
- ('Extra/*', TYPE_CHOICES[2]),
- ('Modified/*', TYPE_CHOICES[1])]
- for (xpath, type) in pattern:
- for x in statistics.findall(xpath):
- counter_fields[type] = counter_fields[type] + 1
- kargs = build_reason_kwargs(x, encoding, logger)
-
- try:
- rr = None
- try:
- rr = Reason.objects.filter(**kargs)[0]
- except IndexError:
- rr = Reason(**kargs)
- rr.save()
- if vlevel > 0:
- logger.info("Created reason: %s" % rr.id)
- except Exception:
- ex = sys.exc_info()[1]
- logger.error("Failed to create reason for %s: %s" % (x.get('name'), ex))
- rr = Reason(current_exists=x.get('current_exists',
- default="True").capitalize() == "True")
- rr.save()
-
- entry, created = Entries.objects.get_or_create(\
- name=x.get('name'), kind=x.tag)
-
- Entries_interactions(entry=entry, reason=rr,
- interaction=current_interaction,
- type=type[0]).save()
- if vlevel > 0:
- logger.info("%s interaction created with reason id %s and entry %s" % (xpath, rr.id, entry.id))
-
- # Update interaction counters
- current_interaction.bad_entries = counter_fields[TYPE_CHOICES[0]]
- current_interaction.modified_entries = counter_fields[TYPE_CHOICES[1]]
- current_interaction.extra_entries = counter_fields[TYPE_CHOICES[2]]
- current_interaction.save()
-
- mperfs = []
- for times in statistics.findall('OpStamps'):
- for metric, value in list(times.items()):
- mmatch = []
- if not quick:
- mmatch = Performance.objects.filter(metric=metric, value=value)
-
- if mmatch:
- mperf = mmatch[0]
- else:
- mperf = Performance(metric=metric, value=value)
- mperf.save()
- mperfs.append(mperf)
- current_interaction.performance_items.add(*mperfs)
-
- for key in list(pingability.keys()):
- if key not in clients:
- continue
+ goodcount=statistics.get('good',
+ default="0"),
+ totalcount=statistics.get('total',
+ default="0"),
+ server=location)
+ newint.save()
+ current_interaction = newint
+ if vlevel > 0:
+ logger.info("Interaction for %s at %s with id %s INSERTED in to db" % (client.id,
+ timestamp, current_interaction.id))
+
+ if isinstance(cobj, ClientMetadata):
try:
- pmatch = Ping.objects.filter(client=clients[key]).order_by('-endtime')[0]
- if pmatch.status == pingability[key]:
- pmatch.endtime = datetime.now()
- pmatch.save()
- continue
- except IndexError:
- pass
- Ping(client=clients[key], status=pingability[key],
- starttime=datetime.now(),
- endtime=datetime.now()).save()
+ imeta = InteractionMetadata(interaction=current_interaction)
+ profile, created = Group.objects.get_or_create(name=cobj.profile)
+ imeta.profile = profile
+ imeta.save() # save here for m2m
+
+ #FIXME - this should be more efficient
+ group_set = []
+ for group_name in cobj.groups:
+ group, created = Group.objects.get_or_create(name=group_name)
+ if created:
+ logger.debug("Added group %s" % group)
+ imeta.groups.add(group)
+ for bundle_name in cobj.bundles:
+ bundle, created = Bundle.objects.get_or_create(name=bundle_name)
+ if created:
+ logger.debug("Added bundle %s" % bundle)
+ imeta.bundles.add(bundle)
+ imeta.save()
+ except:
+ logger.error("Failed to save interaction metadata for %s: %s" %
+ (client_name, traceback.format_exc().splitlines()[-1]))
+
+
+ entries_cache = {}
+ [entries_cache.__setitem__((e.kind, e.name), e) \
+ for e in Entries.objects.all()]
+ counter_fields = {TYPE_BAD: 0,
+ TYPE_MODIFIED: 0,
+ TYPE_EXTRA: 0}
+ pattern = [('Bad/*', TYPE_BAD),
+ ('Extra/*', TYPE_EXTRA),
+ ('Modified/*', TYPE_MODIFIED)]
+ for (xpath, type) in pattern:
+ for x in statistics.findall(xpath):
+ counter_fields[type] = counter_fields[type] + 1
+ rr = _fetch_reason(x, build_reason_kwargs(x, encoding, logger), logger)
- if vlevel > 1:
- logger.info("---------------PINGDATA SYNCED---------------------")
+ try:
+ entry = entries_cache[(x.tag, x.get('name'))]
+ except KeyError:
+ entry, created = Entries.objects.get_or_create(\
+ name=x.get('name'), kind=x.tag)
+
+ Entries_interactions(entry=entry, reason=rr,
+ interaction=current_interaction,
+ type=type).save()
+ if vlevel > 0:
+ logger.info("%s interaction created with reason id %s and entry %s" % (xpath, rr.id, entry.id))
+
+ # add good entries
+ good_reason = None
+ for x in statistics.findall('Good/*'):
+ if good_reason == None:
+ # Do this once. Really need to fix Reasons...
+ good_reason = _fetch_reason(x, build_reason_kwargs(x, encoding, logger), logger)
+ try:
+ entry = entries_cache[(x.tag, x.get('name'))]
+ except KeyError:
+ entry, created = Entries.objects.get_or_create(\
+ name=x.get('name'), kind=x.tag)
+ Entries_interactions(entry=entry, reason=good_reason,
+ interaction=current_interaction,
+ type=TYPE_GOOD).save()
+ if vlevel > 0:
+ logger.info("%s interaction created with reason id %s and entry %s" % (xpath, good_reason.id, entry.id))
+
+ # Update interaction counters
+ current_interaction.bad_entries = counter_fields[TYPE_BAD]
+ current_interaction.modified_entries = counter_fields[TYPE_MODIFIED]
+ current_interaction.extra_entries = counter_fields[TYPE_EXTRA]
+ current_interaction.save()
+
+ mperfs = []
+ for times in statistics.findall('OpStamps'):
+ for metric, value in list(times.items()):
+ mmatch = []
+ if not quick:
+ mmatch = Performance.objects.filter(metric=metric, value=value)
+
+ if mmatch:
+ mperf = mmatch[0]
+ else:
+ mperf = Performance(metric=metric, value=value)
+ mperf.save()
+ mperfs.append(mperf)
+ current_interaction.performance_items.add(*mperfs)
- #Clients are consistent
if __name__ == '__main__':
from sys import argv
@@ -231,18 +263,17 @@ if __name__ == '__main__':
except GetoptError:
mesg = sys.exc_info()[1]
# print help information and exit:
- print("%s\nUsage:\nimportscript.py [-h] [-v] [-u] [-d] [-S] [-C bcfg2 config file] [-c clients-file] [-s statistics-file]" % (mesg))
+ print("%s\nUsage:\nimportscript.py [-h] [-v] [-u] [-d] [-S] [-C bcfg2 config file] [-s statistics-file]" % (mesg))
raise SystemExit(2)
for o, a in opts:
if o in ("-h", "--help"):
- print("Usage:\nimportscript.py [-h] [-v] -c <clients-file> -s <statistics-file> \n")
+ print("Usage:\nimportscript.py [-h] [-v] -s <statistics-file> \n")
print("h : help; this message")
print("v : verbose; print messages on record insertion/skip")
print("u : updates; print status messages as items inserted semi-verbose")
print("d : debug; print most SQL used to manipulate database")
print("C : path to bcfg2.conf config file.")
- print("c : clients.xml file")
print("s : statistics.xml file")
print("S : syslog; output to syslog")
raise SystemExit
@@ -256,7 +287,7 @@ if __name__ == '__main__':
if o in ("-d", "--debug"):
verb = 3
if o in ("-c", "--clients"):
- clientspath = a
+ print "DeprecationWarning: %s is no longer used" % o
if o in ("-s", "--stats"):
statpath = a
@@ -267,7 +298,7 @@ if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO)
Bcfg2.Logger.setup_logging('importscript.py',
True,
- syslog)
+ syslog, level=logging.INFO)
cf = ConfigParser.ConfigParser()
cf.read([cpath])
@@ -289,24 +320,13 @@ if __name__ == '__main__':
except:
encoding = 'UTF-8'
- if not clientpath:
- try:
- clientspath = "%s/Metadata/clients.xml" % \
- cf.get('server', 'repository')
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- print("Could not read bcfg2.conf; exiting")
- raise SystemExit(1)
- try:
- clientsdata = XML(open(clientspath).read())
- except (IOError, XMLSyntaxError):
- print("StatReports: Failed to parse %s" % (clientspath))
- raise SystemExit(1)
-
q = '-O3' in sys.argv
# Be sure the database is ready for new schema
- update_database()
- load_stats(clientsdata,
- statsdata,
+ try:
+ update_database()
+ except UpdaterError:
+ raise SystemExit(1)
+ load_stats(statsdata,
encoding,
verb,
logger,
diff --git a/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml b/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml
deleted file mode 100644
index bde236989..000000000
--- a/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version='1.0' encoding='utf-8' ?>
-<django-objects version="1.0">
- <object pk="1" model="reports.internaldatabaseversion">
- <field type="IntegerField" name="version">0</field>
- <field type="DateTimeField" name="updated">2008-08-05 11:03:50</field>
- </object>
- <object pk="2" model="reports.internaldatabaseversion">
- <field type="IntegerField" name="version">1</field>
- <field type="DateTimeField" name="updated">2008-08-05 11:04:10</field>
- </object>
- <object pk="3" model="reports.internaldatabaseversion">
- <field type="IntegerField" name="version">2</field>
- <field type="DateTimeField" name="updated">2008-08-05 13:37:19</field>
- </object>
- <object pk="4" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>3</field>
- <field type='DateTimeField' name='updated'>2008-08-11 08:44:36</field>
- </object>
- <object pk="5" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>10</field>
- <field type='DateTimeField' name='updated'>2008-08-22 11:28:50</field>
- </object>
- <object pk="5" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>11</field>
- <field type='DateTimeField' name='updated'>2009-01-13 12:26:10</field>
- </object>
- <object pk="6" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>16</field>
- <field type='DateTimeField' name='updated'>2010-06-01 12:26:10</field>
- </object>
- <object pk="7" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>17</field>
- <field type='DateTimeField' name='updated'>2010-07-02 00:00:00</field>
- </object>
- <object pk="8" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>18</field>
- <field type='DateTimeField' name='updated'>2011-06-30 00:00:00</field>
- </object>
- <object pk="8" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>19</field>
- <field type='DateTimeField' name='updated'>2012-03-28 00:00:00</field>
- </object>
-</django-objects>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/models.py b/src/lib/Bcfg2/Server/Reports/reports/models.py
index 35f2a4393..4b078eb2c 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/models.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/models.py
@@ -23,16 +23,13 @@ KIND_CHOICES = (
('Path', 'symlink'),
('Service', 'Service'),
)
-PING_CHOICES = (
- #These are possible ping states
- ('Up (Y)', 'Y'),
- ('Down (N)', 'N')
-)
+TYPE_GOOD = 0
TYPE_BAD = 1
TYPE_MODIFIED = 2
TYPE_EXTRA = 3
TYPE_CHOICES = (
+ (TYPE_GOOD, 'Good'),
(TYPE_BAD, 'Bad'),
(TYPE_MODIFIED, 'Modified'),
(TYPE_EXTRA, 'Extra'),
@@ -87,30 +84,9 @@ class Client(models.Model):
pass
-class Ping(models.Model):
- """Represents a ping of a client (sparsely)."""
- client = models.ForeignKey(Client, related_name="pings")
- starttime = models.DateTimeField()
- endtime = models.DateTimeField()
- status = models.CharField(max_length=4, choices=PING_CHOICES) # up/down
-
- class Meta:
- get_latest_by = 'endtime'
-
-
class InteractiveManager(models.Manager):
"""Manages interactions objects."""
- def recent_interactions_dict(self, maxdate=None, active_only=True):
- """
- Return the most recent interactions for clients as of a date.
-
- This method uses aggregated queries to return a ValuesQueryDict object.
- Faster then raw sql since this is executed as a single query.
- """
-
- return list(self.values('client').annotate(max_timestamp=Max('timestamp')).values())
-
def interaction_per_client(self, maxdate=None, active_only=True):
"""
Returns the most recent interactions for clients as of a date
@@ -154,15 +130,15 @@ class InteractiveManager(models.Manager):
cursor.execute(sql)
return [item[0] for item in cursor.fetchall()]
except:
- '''FIXME - really need some error hadling'''
+ '''FIXME - really need some error handling'''
pass
return []
class Interaction(models.Model):
"""Models each reconfiguration operation interaction between client and server."""
- client = models.ForeignKey(Client, related_name="interactions",)
- timestamp = models.DateTimeField() # Timestamp for this record
+ client = models.ForeignKey(Client, related_name="interactions")
+ timestamp = models.DateTimeField(db_index=True) # Timestamp for this record
state = models.CharField(max_length=32) # good/bad/modified/etc
repo_rev_code = models.CharField(max_length=64) # repo revision at time of interaction
goodcount = models.IntegerField() # of good config-items
@@ -270,27 +246,47 @@ class Interaction(models.Model):
class Reason(models.Model):
"""reason why modified or bad entry did not verify, or changed."""
- owner = models.TextField(max_length=128, blank=True)
- current_owner = models.TextField(max_length=128, blank=True)
- group = models.TextField(max_length=128, blank=True)
- current_group = models.TextField(max_length=128, blank=True)
- perms = models.TextField(max_length=4, blank=True) # txt fixes typing issue
- current_perms = models.TextField(max_length=4, blank=True)
- status = models.TextField(max_length=3, blank=True) # on/off/(None)
- current_status = models.TextField(max_length=1, blank=True) # on/off/(None)
- to = models.TextField(max_length=256, blank=True)
- current_to = models.TextField(max_length=256, blank=True)
- version = models.TextField(max_length=128, blank=True)
- current_version = models.TextField(max_length=128, blank=True)
+ owner = models.CharField(max_length=255, blank=True)
+ current_owner = models.CharField(max_length=255, blank=True)
+ group = models.CharField(max_length=255, blank=True)
+ current_group = models.CharField(max_length=255, blank=True)
+ perms = models.CharField(max_length=4, blank=True)
+ current_perms = models.CharField(max_length=4, blank=True)
+ status = models.CharField(max_length=128, blank=True)
+ current_status = models.CharField(max_length=128, blank=True)
+ to = models.CharField(max_length=1024, blank=True)
+ current_to = models.CharField(max_length=1024, blank=True)
+ version = models.CharField(max_length=1024, blank=True)
+ current_version = models.CharField(max_length=1024, blank=True)
current_exists = models.BooleanField() # False means its missing. Default True
- current_diff = models.TextField(max_length=1280, blank=True)
+ current_diff = models.TextField(max_length=1024*1024, blank=True)
is_binary = models.BooleanField(default=False)
is_sensitive = models.BooleanField(default=False)
- unpruned = models.TextField(max_length=1280, blank=True)
+ unpruned = models.TextField(max_length=4096, blank=True, default='')
def _str_(self):
return "Reason"
+ def short_list(self):
+ rv = []
+ if self.current_owner or self.current_group or self.current_perms:
+ rv.append("File permissions")
+ if self.current_status:
+ rv.append("Incorrect status")
+ if self.current_to:
+ rv.append("Incorrect target")
+ if self.current_version or self.version == 'auto':
+ rv.append("Wrong version")
+ if not self.current_exists:
+ rv.append("Missing")
+ if self.current_diff or self.is_sensitive:
+ rv.append("Incorrect data")
+ if self.unpruned:
+ rv.append("Directory has extra files")
+ if len(rv) == 0:
+ rv.append("Exists")
+ return rv
+
@staticmethod
@transaction.commit_on_success
def prune_orphans():
@@ -316,6 +312,9 @@ class Entries(models.Model):
cursor.execute('delete from reports_entries where not exists (select rei.id from reports_entries_interactions rei where rei.entry_id = reports_entries.id)')
transaction.set_dirty()
+ class Meta:
+ unique_together = ("name", "kind")
+
class Entries_interactions(models.Model):
"""Define the relation between the reason, the interaction and the entry."""
@@ -350,3 +349,57 @@ class InternalDatabaseVersion(models.Model):
def __str__(self):
return "version %d updated the %s" % (self.version, self.updated.isoformat())
+
+ class Meta:
+ get_latest_by = "version"
+
+
+class Group(models.Model):
+ """
+ Groups extracted from interactions
+
+ name - The group name
+
+ TODO - Most of this is for future use
+ TODO - set a default group
+ """
+
+ name = models.CharField(max_length=255, unique=True)
+ profile = models.BooleanField(default=False)
+ public = models.BooleanField(default=False)
+ category = models.CharField(max_length=1024, blank=True)
+ comment = models.TextField(blank=True)
+
+ groups = models.ManyToManyField("self", symmetrical=False)
+ bundles = models.ManyToManyField("Bundle")
+
+ def __unicode__(self):
+ return self.name
+
+
+class Bundle(models.Model):
+ """
+ Bundles extracted from interactions
+
+ name - The bundle name
+ """
+
+ name = models.CharField(max_length=255, unique=True)
+
+ def __unicode__(self):
+ return self.name
+
+
+class InteractionMetadata(models.Model):
+ """
+ InteractionMetadata
+
+ Hold extra data associated with the client and interaction
+ """
+
+ interaction = models.OneToOneField(Interaction, primary_key=True, related_name='metadata')
+ profile = models.ForeignKey(Group, related_name="+")
+ groups = models.ManyToManyField(Group)
+ bundles = models.ManyToManyField(Bundle)
+
+
diff --git a/src/lib/Bcfg2/Server/Reports/reports/sql/client.sql b/src/lib/Bcfg2/Server/Reports/reports/sql/client.sql
deleted file mode 100644
index 28e785450..000000000
--- a/src/lib/Bcfg2/Server/Reports/reports/sql/client.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE VIEW reports_current_interactions AS SELECT x.client_id AS client_id, reports_interaction.id AS interaction_id FROM (select client_id, MAX(timestamp) as timer FROM reports_interaction GROUP BY client_id) x, reports_interaction WHERE reports_interaction.client_id = x.client_id AND reports_interaction.timestamp = x.timer;
-
-create index reports_interaction_client_id on reports_interaction (client_id);
-create index reports_client_current_interaction_id on reports_client (current_interaction_id);
-create index reports_performance_interaction_performance_id on reports_performance_interaction (performance_id);
-create index reports_interaction_timestamp on reports_interaction (timestamp);
-create index reports_performance_interation_interaction_id on reports_performance_interaction (interaction_id);
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html b/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
index 842de36f0..9a5ef651c 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
@@ -20,6 +20,9 @@ document.write(getCalendarStyles());
{% if not timestamp %}Rendered at {% now "Y-m-d H:i" %} | {% else %}View as of {{ timestamp|date:"Y-m-d H:i" }} | {% endif %}{% spaceless %}
<a id='cal_link' name='cal_link' href='#' onclick='showCalendar(); return false;'
>[change]</a>
- <form method='post' action='{{ path }}' id='cal_form' name='cal_form'><input id='cal_date' name='cal_date' type='hidden' value=''/></form>
+ <form method='post' action='{{ path }}' id='cal_form' name='cal_form'>
+ <input id='cal_date' name='cal_date' type='hidden' value=''/>
+ <input name='op' type='hidden' value='timeview'/>
+ </form>
{% endspaceless %}
{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/base.html b/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
index f541c0d2b..625177390 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
@@ -62,6 +62,7 @@
<li>Entries Configured</li>
</ul>
<ul class='menu-level2'>
+ <li><a href="{% url reports_common_problems %}">Common problems</a></li>
<li><a href="{% url reports_item_list "bad" %}">Bad</a></li>
<li><a href="{% url reports_item_list "modified" %}">Modified</a></li>
<li><a href="{% url reports_item_list "extra" %}">Extra</a></li>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
index dd4295f21..9b86b609f 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
@@ -50,6 +50,9 @@ span.history_links a {
{% if interaction.server %}
<tr><td>Served by</td><td>{{interaction.server}}</td></tr>
{% endif %}
+ {% if interaction.metadata %}
+ <tr><td>Profile</td><td>{{interaction.metadata.profile}}</td></tr>
+ {% endif %}
{% if interaction.repo_rev_code %}
<tr><td>Revision</td><td>{{interaction.repo_rev_code}}</td></tr>
{% endif %}
@@ -60,58 +63,57 @@ span.history_links a {
{% endif %}
</table>
- {% if interaction.bad_entry_count %}
+ {% if interaction.metadata.groups.count %}
<div class='entry_list'>
- <div class='entry_list_head dirty-lineitem' onclick='javascript:toggleMe("bad_table");'>
- <h3>Bad Entries &#8212; {{ interaction.bad_entry_count }}</h3>
- <div class='entry_expand_tab' id='plusminus_bad_table'>[+]</div>
+ <div class='entry_list_head' onclick='javascript:toggleMe("groups_table");'>
+ <h3>Group membership</h3>
+ <div class='entry_expand_tab' id='plusminus_groups_table'>[+]</div>
</div>
- <table id='bad_table' class='entry_list'>
- {% for e in interaction.bad|sortwell %}
+ <table id='groups_table' class='entry_list' style='display: none'>
+ {% for group in interaction.metadata.groups.all %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='entry_list_type'>{{e.entry.kind}}:</td>
- <td><a href="{% url reports_item "bad",e.id %}">
- {{e.entry.name}}</a></td>
+ <td class='entry_list_type'>{{group}}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
- {% if interaction.modified_entry_count %}
+ {% if interaction.metadata.bundles.count %}
<div class='entry_list'>
- <div class='entry_list_head modified-lineitem' onclick='javascript:toggleMe("modified_table");'>
- <h3>Modified Entries &#8212; {{ interaction.modified_entry_count }}</h3>
- <div class='entry_expand_tab' id='plusminus_modified_table'>[+]</div>
+ <div class='entry_list_head' onclick='javascript:toggleMe("bundles_table");'>
+ <h3>Bundle membership</h3>
+ <div class='entry_expand_tab' id='plusminus_bundless_table'>[+]</div>
</div>
- <table id='modified_table' class='entry_list'>
- {% for e in interaction.modified|sortwell %}
+ <table id='bundles_table' class='entry_list' style='display: none'>
+ {% for bundle in interaction.metadata.bundles.all %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='entry_list_type'>{{e.entry.kind}}:</td>
- <td><a href="{% url reports_item "modified",e.id %}">
- {{e.entry.name}}</a></td>
+ <td class='entry_list_type'>{{bundle}}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
- {% if interaction.extra_entry_count %}
+ {% for type, ei_list in ei_lists %}
+ {% if ei_list %}
<div class='entry_list'>
- <div class='entry_list_head extra-lineitem' onclick='javascript:toggleMe("extra_table");'>
- <h3>Extra Entries &#8212; {{ interaction.extra_entry_count }}</h3>
- <div class='entry_expand_tab' id='plusminus_extra_table'>[+]</div>
+ <div class='entry_list_head {{type}}-lineitem' onclick='javascript:toggleMe("{{type}}_table");'>
+ <h3>{{ type|capfirst }} Entries &#8212; {{ ei_list|length }}</h3>
+ <div class='entry_expand_tab' id='plusminus_{{type}}_table'>[+]</div>
</div>
- <table id='extra_table' class='entry_list'>
- {% for e in interaction.extra|sortwell %}
+ <table id='{{type}}_table' class='entry_list'>
+ {% for ei in ei_list %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='entry_list_type'>{{e.entry.kind}}:</td>
- <td><a href="{% url reports_item "extra",e.id %}">{{e.entry.name}}</a></td>
+ <td class='entry_list_type'>{{ei.entry.kind}}</td>
+ <td><a href="{% url reports_item type ei.id %}">
+ {{ei.entry.name}}</a></td>
</tr>
- {% endfor %}
+ {% endfor %}
</table>
</div>
{% endif %}
+ {% endfor %}
{% if entry_list %}
<div class="entry_list recent_history_wrapper">
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html
index 84ac71d92..9be59e7d2 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html
@@ -6,18 +6,18 @@
{% block content %}
<div class='client_list_box'>
-{% if entry_list %}
{% filter_navigator %}
+{% if entry_list %}
<table cellpadding="3">
<tr id='table_list_header' class='listview'>
- <td class='left_column'>Node</td>
- <td class='right_column' style='width:75px'>State</td>
- <td class='right_column_narrow'>Good</td>
- <td class='right_column_narrow'>Bad</td>
- <td class='right_column_narrow'>Modified</td>
- <td class='right_column_narrow'>Extra</td>
- <td class='right_column'>Last Run</td>
- <td class='right_column_wide'>Server</td>
+ <td class='left_column'>{% sort_link 'client' 'Node' %}</td>
+ <td class='right_column' style='width:75px'>{% sort_link 'state' 'State' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-good' 'Good' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-bad' 'Bad' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-modified' 'Modified' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-extra' 'Extra' %}</td>
+ <td class='right_column'>{% sort_link 'timestamp' 'Last Run' %}</td>
+ <td class='right_column_wide'>{% sort_link 'server' 'Server' %}</td>
</tr>
{% for entry in entry_list %}
<tr class='{% cycle listview,listview_alt %}'>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
index 134e237d6..45ba20b86 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
@@ -9,6 +9,7 @@
{% block pagebanner %}Clients - Grid View{% endblock %}
{% block content %}
+{% filter_navigator %}
{% if inter_list %}
<table class='grid-view' align='center'>
{% for inter in inter_list %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html
new file mode 100644
index 000000000..d6ad303fc
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html
@@ -0,0 +1,42 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Common Problems{% endblock %}
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block pagebanner %}Common configuration problems{% endblock %}
+
+{% block content %}
+ <div id='threshold_box'>
+ <form method='post' action='{{ request.path }}'>
+ <span>Showing items with more then {{ threshold }} entries</span>
+ <input type='text' name='threshold' value='{{ threshold }}' maxlength='5' size='5' />
+ <input type='submit' value='Change' />
+ </form>
+ </div>
+ {% for type_name, type_list in lists %}
+ <div class='entry_list'>
+ <div class='entry_list_head element_list_head' onclick='javascript:toggleMe("table_{{ type_name }}");'>
+ <h3>{{ type_name|capfirst }} entries</h3>
+ <div class='entry_expand_tab' id='plusminus_table_{{ type_name }}'>[&ndash;]</div>
+ </div>
+ {% if type_list %}
+ <table id='table_{{ type_name }}' class='entry_list'>
+ <tr style='text-align: left'><th>Type</th><th>Name</th><th>Count</th><th>Reason</th></tr>
+ {% for entry, reason, interaction in type_list %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td>{{ entry.kind }}</td>
+ <td><a href="{% url reports_entry eid=entry.pk %}">{{ entry.name }}</a></td>
+ <td>{{ interaction|length }}</td>
+ <td><a href="{% url reports_item type=type_name pk=interaction.0 %}">{{ reason.short_list|join:"," }}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>There are currently no inconsistent {{ type_name }} configuration entries.</p>
+ {% endif %}
+ </div>
+ {% endfor %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html
new file mode 100644
index 000000000..5f7579eb9
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html
@@ -0,0 +1,30 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Entry Status{% endblock %}
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block pagebanner %}{{ entry.kind }} entry {{ entry.name }} status{% endblock %}
+
+{% block content %}
+{% filter_navigator %}
+{% if item_data %}
+ <div class='entry_list'>
+ <table class='entry_list'>
+ <tr style='text-align: left' ><th>Name</th><th>Timestamp</th><th>State</th><th>Reason</th></tr>
+ {% for ei, inter, reason in item_data %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a href='{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=inter.client.name, pk=inter.id %}'>{{ inter.client.name }}</a></td>
+ <td style='white-space: nowrap'><a href='{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=inter.client.name, pk=inter.id %}'>{{ inter.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</a></td>
+ <td>{{ ei.get_type_display }}</td>
+ <td style='white-space: nowrap'><a href="{% url reports_item type=ei.get_type_display pk=ei.pk %}">{{ reason.short_list|join:"," }}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+{% else %}
+ <p>There are currently no hosts with this configuration entry.</p>
+{% endif %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html
index 9b1026a08..0a92e7fc0 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html
@@ -9,19 +9,21 @@
{% block pagebanner %}{{mod_or_bad|capfirst}} Element Listing{% endblock %}
{% block content %}
-{% if item_list_dict %}
- {% for kind, entries in item_list_dict.items %}
-
+{% filter_navigator %}
+{% if item_list %}
+ {% for type_name, type_data in item_list %}
<div class='entry_list'>
- <div class='entry_list_head element_list_head' onclick='javascript:toggleMe("table_{{ kind }}");'>
- <h3>{{ kind }} &#8212; {{ entries|length }}</h3>
- <div class='entry_expand_tab' id='plusminus_table_{{ kind }}'>[&ndash;]</div>
+ <div class='entry_list_head element_list_head' onclick='javascript:toggleMe("table_{{ type_name }}");'>
+ <h3>{{ type_name }} &#8212; {{ type_data|length }}</h3>
+ <div class='entry_expand_tab' id='plusminus_table_{{ type_name }}'>[&ndash;]</div>
</div>
-
- <table id='table_{{ kind }}' class='entry_list'>
- {% for e in entries %}
+ <table id='table_{{ type_name }}' class='entry_list'>
+ <tr style='text-align: left' ><th>Name</th><th>Count</th><th>Reason</th></tr>
+ {% for entry, reason, eis in type_data %}
<tr class='{% cycle listview,listview_alt %}'>
- <td><a href="{% url reports_item type=mod_or_bad,pk=e.id %}">{{e.entry.name}}</a></td>
+ <td><a href="{% url reports_entry eid=entry.pk %}">{{entry.name}}</a></td>
+ <td>{{ eis|length }}</td>
+ <td><a href="{% url reports_item type=mod_or_bad,pk=eis.0 %}">{{ reason.short_list|join:"," }}</a></td>
</tr>
{% endfor %}
</table>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html
index 6fbe585ab..759415507 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html
@@ -1,13 +1,25 @@
{% spaceless %}
+<div class="filter_bar">
+<form name='filter_form'>
{% if filters %}
{% for filter, filter_url in filters %}
{% if forloop.first %}
- <div class="filter_bar">Active filters (click to remove):
+ Active filters (click to remove):
{% endif %}
<a href='{{ filter_url }}'>{{ filter|capfirst }}</a>{% if not forloop.last %}, {% endif %}
{% if forloop.last %}
- </div>
+ {% if groups %}|{% endif %}
{% endif %}
{% endfor %}
{% endif %}
+{% if groups %}
+<label for="id_group">Group filter:</label>
+<select id="id_group" name="group" onchange="javascript:url=document.forms['filter_form'].group.value; if(url) { location.href=url }">
+ {% for group, group_url, selected in groups %}
+ <option label="{{group}}" value="{{group_url}}" {% if selected %}selected {% endif %}/>
+ {% endfor %}
+</select>
+{% endif %}
+</form>
+</div>
{% endspaceless %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py b/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
index ac63cda3e..894353bba 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
@@ -1,11 +1,17 @@
import sys
+from copy import copy
from django import template
+from django.conf import settings
from django.core.urlresolvers import resolve, reverse, \
Resolver404, NoReverseMatch
+from django.template.loader import get_template, \
+ get_template_from_string,TemplateDoesNotExist
from django.utils.encoding import smart_unicode, smart_str
+from django.utils.safestring import mark_safe
from datetime import datetime, timedelta
from Bcfg2.Server.Reports.utils import filter_list
+from Bcfg2.Server.Reports.reports.models import Group
register = template.Library()
@@ -115,13 +121,27 @@ def filter_navigator(context):
filters = []
for filter in filter_list:
+ if filter == 'group':
+ continue
if filter in kwargs:
myargs = kwargs.copy()
del myargs[filter]
filters.append((filter,
reverse(view, args=args, kwargs=myargs)))
filters.sort(lambda x, y: cmp(x[0], y[0]))
- return {'filters': filters}
+
+ myargs = kwargs.copy()
+ selected=True
+ if 'group' in myargs:
+ del myargs['group']
+ selected=False
+ groups = [('---', reverse(view, args=args, kwargs=myargs), selected)]
+ for group in Group.objects.values('name'):
+ myargs['group'] = group['name']
+ groups.append((group['name'], reverse(view, args=args, kwargs=myargs),
+ group['name'] == kwargs.get('group', '')))
+
+ return {'filters': filters, 'groups': groups}
except (Resolver404, NoReverseMatch, ValueError, KeyError):
pass
return dict()
@@ -242,19 +262,6 @@ def add_url_filter(parser, token):
return AddUrlFilter(filter_name, filter_value)
-@register.filter
-def sortwell(value):
- """
- Sorts a list(or evaluates queryset to list) of bad, extra, or modified items in the best
- way for presentation
- """
-
- configItems = list(value)
- configItems.sort(lambda x, y: cmp(x.entry.name, y.entry.name))
- configItems.sort(lambda x, y: cmp(x.entry.kind, y.entry.kind))
- return configItems
-
-
class MediaTag(template.Node):
def __init__(self, filter_value):
self.filter_value = filter_value
@@ -311,3 +318,98 @@ def determine_client_state(entry):
else:
thisdirty = "very-dirty-lineitem"
return thisdirty
+
+
+@register.tag(name='qs')
+def do_qs(parser, token):
+ """
+ qs tag
+
+ accepts a name value pair and inserts or replaces it in the query string
+ """
+ try:
+ tag, name, value = token.split_contents()
+ except ValueError:
+ raise TemplateSyntaxError, "%r tag requires exactly two arguments" \
+ % token.contents.split()[0]
+ return QsNode(name, value)
+
+class QsNode(template.Node):
+ def __init__(self, name, value):
+ self.name = template.Variable(name)
+ self.value = template.Variable(value)
+
+ def render(self, context):
+ try:
+ name = self.name.resolve(context)
+ value = self.value.resolve(context)
+ request = context['request']
+ qs = copy(request.GET)
+ qs[name] = value
+ return "?%s" % qs.urlencode()
+ except template.VariableDoesNotExist:
+ return ''
+ except KeyError:
+ if settings.TEMPLATE_DEBUG:
+ raise Exception, "'qs' tag requires context['request']"
+ return ''
+ except:
+ return ''
+
+
+@register.tag
+def sort_link(parser, token):
+ '''
+ Create a sort anchor tag. Reverse it if active.
+
+ {% sort_link sort_key text %}
+ '''
+ try:
+ tag, sort_key, text = token.split_contents()
+ except ValueError:
+ raise TemplateSyntaxError("%r tag requires at least four arguments" \
+ % token.split_contents()[0])
+
+ return SortLinkNode(sort_key, text)
+
+class SortLinkNode(template.Node):
+ __TMPL__ = "{% load bcfg2_tags %}<a href='{% qs 'sort' key %}'>{{ text }}</a>"
+
+ def __init__(self, sort_key, text):
+ self.sort_key = template.Variable(sort_key)
+ self.text = template.Variable(text)
+
+ def render(self, context):
+ try:
+ try:
+ sort = context['request'].GET['sort']
+ except KeyError:
+ #fall back on this
+ sort = context.get('sort', '')
+ sort_key = self.sort_key.resolve(context)
+ text = self.text.resolve(context)
+
+ # add arrows
+ try:
+ sort_base = sort_key.lstrip('-')
+ if sort[0] == '-' and sort[1:] == sort_base:
+ text = text + '&#x25BC;'
+ sort_key = sort_base
+ elif sort_base == sort:
+ text = text + '&#x25B2;'
+ sort_key = '-' + sort_base
+ except IndexError:
+ pass
+
+ context.push()
+ context['key'] = sort_key
+ context['text'] = mark_safe(text)
+ output = get_template_from_string(self.__TMPL__).render(context)
+ context.pop()
+ return output
+ except:
+ if settings.DEBUG:
+ raise
+ raise
+ return ''
+
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py b/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
index 36d4cf693..0d4c6501d 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
@@ -4,6 +4,8 @@ from django.utils.encoding import smart_unicode
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
+from Bcfg2.Bcfg2Py3k import u_str
+
register = template.Library()
try:
@@ -16,14 +18,6 @@ except:
colorize = False
-# py3k compatibility
-def u_str(string):
- if sys.hexversion >= 0x03000000:
- return string
- else:
- return unicode(string)
-
-
@register.filter
def syntaxhilight(value, arg="diff", autoescape=None):
"""
diff --git a/src/lib/Bcfg2/Server/Reports/reports/urls.py b/src/lib/Bcfg2/Server/Reports/reports/urls.py
index 434ce07b7..1cfe725c2 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/urls.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/urls.py
@@ -17,20 +17,23 @@ urlpatterns = patterns('Bcfg2.Server.Reports.reports',
url(r'^client/(?P<hostname>[^/]+)/(?P<pk>\d+)/?$', 'views.client_detail', name='reports_client_detail_pk'),
url(r'^client/(?P<hostname>[^/]+)/?$', 'views.client_detail', name='reports_client_detail'),
url(r'^elements/(?P<type>\w+)/(?P<pk>\d+)/?$', 'views.config_item', name='reports_item'),
+ url(r'^entry/(?P<eid>\w+)/?$', 'views.entry_status', name='reports_entry'),
)
urlpatterns += patterns('Bcfg2.Server.Reports.reports',
*timeviewUrls(
- (r'^grid/?$', 'views.client_index', None, 'reports_grid_view'),
(r'^summary/?$', 'views.display_summary', None, 'reports_summary'),
(r'^timing/?$', 'views.display_timing', None, 'reports_timing'),
- (r'^elements/(?P<type>\w+)/?$', 'views.config_item_list', None, 'reports_item_list'),
+ (r'^common/(?P<threshold>\d+)/?$', 'views.common_problems', None, 'reports_common_problems'),
+ (r'^common/?$', 'views.common_problems', None, 'reports_common_problems'),
))
urlpatterns += patterns('Bcfg2.Server.Reports.reports',
*filteredUrls(*timeviewUrls(
+ (r'^grid/?$', 'views.client_index', None, 'reports_grid_view'),
(r'^detailed/?$',
- 'views.client_detailed_list', None, 'reports_detailed_list')
+ 'views.client_detailed_list', None, 'reports_detailed_list'),
+ (r'^elements/(?P<type>\w+)/?$', 'views.config_item_list', None, 'reports_item_list'),
)))
urlpatterns += patterns('Bcfg2.Server.Reports.reports',
diff --git a/src/lib/Bcfg2/Server/Reports/reports/views.py b/src/lib/Bcfg2/Server/Reports/reports/views.py
index ccd71a60e..e4c38363f 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/views.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/views.py
@@ -13,16 +13,41 @@ from django.http import \
from django.shortcuts import render_to_response, get_object_or_404
from django.core.urlresolvers import \
resolve, reverse, Resolver404, NoReverseMatch
-from django.db import connection
+from django.db import connection, DatabaseError
+from django.db.models import Q
from Bcfg2.Server.Reports.reports.models import *
+__SORT_FIELDS__ = ( 'client', 'state', 'good', 'bad', 'modified', 'extra', \
+ 'timestamp', 'server' )
+
class PaginationError(Exception):
"""This error is raised when pagination cannot be completed."""
pass
+def _in_bulk(model, ids):
+ """
+ Short cut to fetch in bulk and trap database errors. sqlite will raise
+ a "too many SQL variables" exception if this list is too long. Try using
+ django and fetch manually if an error occurs
+
+ returns a dict of this form { id: <model instance> }
+ """
+
+ try:
+ return model.objects.in_bulk(ids)
+ except DatabaseError:
+ pass
+
+ # if objects.in_bulk fails so will obejcts.filter(pk__in=ids)
+ bulk_dict = {}
+ [bulk_dict.__setitem__(i.id, i) \
+ for i in model.objects.all() if i.id in ids]
+ return bulk_dict
+
+
def server_error(request):
"""
500 error handler.
@@ -44,7 +69,7 @@ def timeview(fn):
"""
def _handle_timeview(request, **kwargs):
"""Send any posts back."""
- if request.method == 'POST':
+ if request.method == 'POST' and request.POST.get('op', '') == 'timeview':
cal_date = request.POST['cal_date']
try:
fmt = "%Y/%m/%d"
@@ -84,6 +109,30 @@ def timeview(fn):
return _handle_timeview
+def _handle_filters(query, **kwargs):
+ """
+ Applies standard filters to a query object
+
+ Returns an updated query object
+
+ query - query object to filter
+
+ server -- Filter interactions by server
+ state -- Filter interactions by state
+ group -- Filter interactions by group
+
+ """
+ if 'state' in kwargs and kwargs['state']:
+ query = query.filter(state__exact=kwargs['state'])
+ if 'server' in kwargs and kwargs['server']:
+ query = query.filter(server__exact=kwargs['server'])
+
+ if 'group' in kwargs and kwargs['group']:
+ group = get_object_or_404(Group, name=kwargs['group'])
+ query = query.filter(metadata__groups__id=group.pk)
+ return query
+
+
def config_item(request, pk, type="bad"):
"""
Display a single entry.
@@ -121,47 +170,138 @@ def config_item(request, pk, type="bad"):
@timeview
-def config_item_list(request, type, timestamp=None):
+def config_item_list(request, type, timestamp=None, **kwargs):
"""Render a listing of affected elements"""
mod_or_bad = type.lower()
type = convert_entry_type_to_id(type)
if type < 0:
raise Http404
- current_clients = Interaction.objects.get_interaction_per_client_ids(timestamp)
- item_list_dict = {}
- seen = dict()
- for x in Entries_interactions.objects.filter(interaction__in=current_clients,
- type=type).select_related():
- if (x.entry, x.reason) in seen:
- continue
- seen[(x.entry, x.reason)] = 1
- if item_list_dict.get(x.entry.kind, None):
- item_list_dict[x.entry.kind].append(x)
- else:
- item_list_dict[x.entry.kind] = [x]
+ current_clients = Interaction.objects.interaction_per_client(timestamp)
+ current_clients = [q['id'] for q in _handle_filters(current_clients, **kwargs).values('id')]
+
+ ldata = list(Entries_interactions.objects.filter(
+ interaction__in=current_clients, type=type).values())
+ entry_ids = set([x['entry_id'] for x in ldata])
+ reason_ids = set([x['reason_id'] for x in ldata])
- for kind in item_list_dict:
- item_list_dict[kind].sort(lambda a, b: cmp(a.entry.name, b.entry.name))
+ entries = _in_bulk(Entries, entry_ids)
+ reasons = _in_bulk(Reason, reason_ids)
+
+ kind_list = {}
+ [kind_list.__setitem__(kind, {}) for kind in set([e.kind for e in entries.values()])]
+ for x in ldata:
+ kind = entries[x['entry_id']].kind
+ data_key = (x['entry_id'], x['reason_id'])
+ try:
+ kind_list[kind][data_key].append(x['id'])
+ except KeyError:
+ kind_list[kind][data_key] = [x['id']]
+
+ lists = []
+ for kind in kind_list.keys():
+ lists.append((kind, [(entries[e[0][0]], reasons[e[0][1]], e[1])
+ for e in sorted(kind_list[kind].iteritems(), key=lambda x: entries[x[0][0]].name)]))
return render_to_response('config_items/listing.html',
- {'item_list_dict': item_list_dict,
+ {'item_list': lists,
'mod_or_bad': mod_or_bad,
'timestamp': timestamp},
context_instance=RequestContext(request))
@timeview
-def client_index(request, timestamp=None):
+def entry_status(request, eid, timestamp=None, **kwargs):
+ """Render a listing of affected elements"""
+ entry = get_object_or_404(Entries, pk=eid)
+
+ current_clients = Interaction.objects.interaction_per_client(timestamp)
+ inters = {}
+ [inters.__setitem__(i.id, i) \
+ for i in _handle_filters(current_clients, **kwargs).select_related('client')]
+
+ eis = Entries_interactions.objects.filter(
+ interaction__in=inters.keys(), entry=entry)
+
+ reasons = _in_bulk(Reason, set([x.reason_id for x in eis]))
+
+ item_data = []
+ for ei in eis:
+ item_data.append((ei, inters[ei.interaction_id], reasons[ei.reason_id]))
+
+ return render_to_response('config_items/entry_status.html',
+ {'entry': entry,
+ 'item_data': item_data,
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def common_problems(request, timestamp=None, threshold=None):
+ """Mine config entries"""
+
+ if request.method == 'POST':
+ try:
+ threshold = int(request.POST['threshold'])
+ view, args, kw = resolve(request.META['PATH_INFO'])
+ kw['threshold'] = threshold
+ return HttpResponseRedirect(reverse(view,
+ args=args,
+ kwargs=kw))
+ except:
+ pass
+
+ try:
+ threshold = int(threshold)
+ except:
+ threshold = 10
+
+ c_intr = Interaction.objects.get_interaction_per_client_ids(timestamp)
+ data_list = {}
+ [data_list.__setitem__(t_id, {}) \
+ for t_id, t_label in TYPE_CHOICES if t_id != TYPE_GOOD]
+ ldata = list(Entries_interactions.objects.filter(
+ interaction__in=c_intr).exclude(type=TYPE_GOOD).values())
+
+ entry_ids = set([x['entry_id'] for x in ldata])
+ reason_ids = set([x['reason_id'] for x in ldata])
+ for x in ldata:
+ type = x['type']
+ data_key = (x['entry_id'], x['reason_id'])
+ try:
+ data_list[type][data_key].append(x['id'])
+ except KeyError:
+ data_list[type][data_key] = [x['id']]
+
+ entries = _in_bulk(Entries, entry_ids)
+ reasons = _in_bulk(Reason, reason_ids)
+
+ lists = []
+ for type, type_name in TYPE_CHOICES:
+ if type == TYPE_GOOD:
+ continue
+ lists.append([type_name.lower(), [(entries[e[0][0]], reasons[e[0][1]], e[1])
+ for e in sorted(data_list[type].items(), key=lambda x: len(x[1]), reverse=True)
+ if len(e[1]) > threshold]])
+
+ return render_to_response('config_items/common.html',
+ {'lists': lists,
+ 'timestamp': timestamp,
+ 'threshold': threshold},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def client_index(request, timestamp=None, **kwargs):
"""
Render a grid view of active clients.
Keyword parameters:
- timestamp -- datetime objectto render from
+ timestamp -- datetime object to render from
"""
- list = Interaction.objects.interaction_per_client(timestamp).select_related()\
- .order_by("client__name").all()
+ list = _handle_filters(Interaction.objects.interaction_per_client(timestamp), **kwargs).\
+ select_related().order_by("client__name").all()
return render_to_response('clients/index.html',
{'inter_list': list,
@@ -177,8 +317,29 @@ def client_detailed_list(request, timestamp=None, **kwargs):
"""
+ try:
+ sort = request.GET['sort']
+ if sort[0] == '-':
+ sort_key = sort[1:]
+ else:
+ sort_key = sort
+ if not sort_key in __SORT_FIELDS__:
+ raise ValueError
+
+ if sort_key == "client":
+ kwargs['orderby'] = "%s__name" % sort
+ elif sort_key == "good":
+ kwargs['orderby'] = "%scount" % sort
+ elif sort_key in ["bad", "modified", "extra"]:
+ kwargs['orderby'] = "%s_entries" % sort
+ else:
+ kwargs['orderby'] = sort
+ kwargs['sort'] = sort
+ except (ValueError, KeyError):
+ kwargs['orderby'] = "client__name"
+ kwargs['sort'] = "client"
+
kwargs['interaction_base'] = Interaction.objects.interaction_per_client(timestamp).select_related()
- kwargs['orderby'] = "client__name"
kwargs['page_limit'] = 0
return render_history_view(request, 'clients/detailed-list.html', **kwargs)
@@ -187,13 +348,25 @@ def client_detail(request, hostname=None, pk=None):
context = dict()
client = get_object_or_404(Client, name=hostname)
if(pk == None):
- context['interaction'] = client.current_interaction
- return render_history_view(request, 'clients/detail.html', page_limit=5,
- client=client, context=context)
+ inter = client.current_interaction
+ maxdate = None
else:
- context['interaction'] = client.interactions.get(pk=pk)
- return render_history_view(request, 'clients/detail.html', page_limit=5,
- client=client, maxdate=context['interaction'].timestamp, context=context)
+ inter = client.interactions.get(pk=pk)
+ maxdate = inter.timestamp
+
+ ei = Entries_interactions.objects.filter(interaction=inter).select_related('entry').order_by('entry__kind', 'entry__name')
+ #ei = Entries_interactions.objects.filter(interaction=inter).select_related('entry')
+ #ei = sorted(Entries_interactions.objects.filter(interaction=inter).select_related('entry'),
+ # key=lambda x: (x.entry.kind, x.entry.name))
+ context['ei_lists'] = (
+ ('bad', [x for x in ei if x.type == TYPE_BAD]),
+ ('modified', [x for x in ei if x.type == TYPE_MODIFIED]),
+ ('extra', [x for x in ei if x.type == TYPE_EXTRA])
+ )
+
+ context['interaction']=inter
+ return render_history_view(request, 'clients/detail.html', page_limit=5,
+ client=client, maxdate=maxdate, context=context)
def client_manage(request):
@@ -230,9 +403,9 @@ def display_summary(request, timestamp=None):
"""
Display a summary of the bcfg2 world
"""
- query = Interaction.objects.interaction_per_client(timestamp).select_related()
- node_count = query.count()
- recent_data = query.all()
+ recent_data = Interaction.objects.interaction_per_client(timestamp) \
+ .select_related().all()
+ node_count = len(recent_data)
if not timestamp:
timestamp = datetime.now()
@@ -240,18 +413,11 @@ def display_summary(request, timestamp=None):
bad=[],
modified=[],
extra=[],
- stale=[],
- pings=[])
+ stale=[])
for node in recent_data:
if timestamp - node.timestamp > timedelta(hours=24):
collected_data['stale'].append(node)
# If stale check for uptime
- try:
- if node.client.pings.latest().status == 'N':
- collected_data['pings'].append(node)
- except Ping.DoesNotExist:
- collected_data['pings'].append(node)
- continue
if node.bad_entry_count() > 0:
collected_data['bad'].append(node)
else:
@@ -281,9 +447,6 @@ def display_summary(request, timestamp=None):
if len(collected_data['stale']) > 0:
summary_data.append(get_dict('stale',
'nodes did not run within the last 24 hours.'))
- if len(collected_data['pings']) > 0:
- summary_data.append(get_dict('pings',
- 'are down.'))
return render_to_response('displays/summary.html',
{'summary_data': summary_data, 'node_count': node_count,
@@ -299,7 +462,11 @@ def display_timing(request, timestamp=None):
for inter in inters]
for metric in Performance.objects.filter(interaction__in=list(mdict.keys())).all():
for i in metric.interaction.all():
- mdict[i][metric.metric] = metric.value
+ try:
+ mdict[i][metric.metric] = metric.value
+ except KeyError:
+ #In the unlikely event two interactions share a metric, ignore it
+ pass
return render_to_response('displays/timing.html',
{'metrics': list(mdict.values()),
'timestamp': timestamp},
@@ -324,6 +491,7 @@ def render_history_view(request, template='clients/history.html', **kwargs):
not found
server -- Filter interactions by server
state -- Filter interactions by state
+ group -- Filter interactions by group
entry_max -- Most recent interaction to display
orderby -- Sort results using this field
@@ -345,15 +513,15 @@ def render_history_view(request, template='clients/history.html', **kwargs):
# Either filter by client or limit by clients
iquery = kwargs.get('interaction_base', Interaction.objects)
if client:
- iquery = iquery.filter(client__exact=client).select_related()
+ iquery = iquery.filter(client__exact=client)
+ iquery = iquery.select_related()
if 'orderby' in kwargs and kwargs['orderby']:
iquery = iquery.order_by(kwargs['orderby'])
+ if 'sort' in kwargs:
+ context['sort'] = kwargs['sort']
- if 'state' in kwargs and kwargs['state']:
- iquery = iquery.filter(state__exact=kwargs['state'])
- if 'server' in kwargs and kwargs['server']:
- iquery = iquery.filter(server__exact=kwargs['server'])
+ iquery = _handle_filters(iquery, **kwargs)
if entry_max:
iquery = iquery.filter(timestamp__lte=entry_max)
diff --git a/src/lib/Bcfg2/Server/Reports/settings.py b/src/lib/Bcfg2/Server/Reports/settings.py
index 4d567f1a2..b27348aee 100644
--- a/src/lib/Bcfg2/Server/Reports/settings.py
+++ b/src/lib/Bcfg2/Server/Reports/settings.py
@@ -1,11 +1,25 @@
-import django
+import os
import sys
+import getopt
+import Bcfg2.Options
+
+try:
+ import django
+except ImportError:
+ raise ImportError('Import of Django module failed. Is Django installed?')
+
+cfile_opt=Bcfg2.Options.CFILE
+cfiles=[cfile_opt.default, '/etc/bcfg2-web.conf']
+for i in range(1, len(sys.argv)):
+ if sys.argv[i] == cfile_opt.cmd:
+ cfiles = sys.argv[i+1]
+ break
# Compatibility import
from Bcfg2.Bcfg2Py3k import ConfigParser
# Django settings for bcfg2 reports project.
c = ConfigParser.ConfigParser()
-if len(c.read(['/etc/bcfg2.conf', '/etc/bcfg2-web.conf'])) == 0:
+if len(c.read(cfiles)) == 0:
raise ImportError("Please check that bcfg2.conf or bcfg2-web.conf exists "
"and is readable by your web server.")
@@ -43,6 +57,9 @@ DATABASES = {
}
}
+if db_engine == 'ibm_db_django':
+ DATABASES['default']['ENGINE'] = db_engine
+
if db_engine != 'sqlite3':
DATABASES['default']['USER'] = c.get('statistics', 'database_user')
DATABASES['default']['PASSWORD'] = c.get('statistics', 'database_password')
@@ -117,11 +134,7 @@ AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',
# The NIS group authorized to login to BCFG2's reportinvg system
AUTHORIZED_GROUP = ''
#create login url area:
-try:
- import django.contrib.auth
-except ImportError:
- raise ImportError('Import of Django module failed. Is Django installed?')
-django.contrib.auth.LOGIN_URL = '/login'
+LOGIN_URL = '/login'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
@@ -131,7 +144,6 @@ TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates".
# Always use forward slashes, even on Windows.
'/usr/share/python-support/python-django/django/contrib/admin/templates/',
- 'Bcfg2.Server.Reports.reports'
)
if django.VERSION[0] == 1 and django.VERSION[1] < 2:
diff --git a/src/lib/Bcfg2/Server/Reports/updatefix.py b/src/lib/Bcfg2/Server/Reports/updatefix.py
deleted file mode 100644
index 192b94b61..000000000
--- a/src/lib/Bcfg2/Server/Reports/updatefix.py
+++ /dev/null
@@ -1,281 +0,0 @@
-import Bcfg2.Server.Reports.settings
-
-from django.db import connection, DatabaseError
-import django.core.management
-import logging
-import sys
-import traceback
-from Bcfg2.Server.Reports.reports.models import InternalDatabaseVersion, \
- TYPE_BAD, TYPE_MODIFIED, TYPE_EXTRA
-logger = logging.getLogger('Bcfg2.Server.Reports.UpdateFix')
-
-
-# all update function should go here
-def _merge_database_table_entries():
- cursor = connection.cursor()
- insert_cursor = connection.cursor()
- find_cursor = connection.cursor()
- cursor.execute("""
- Select name, kind from reports_bad
- union
- select name, kind from reports_modified
- union
- select name, kind from reports_extra
- """)
- # this fetch could be better done
- entries_map = {}
- for row in cursor.fetchall():
- insert_cursor.execute("insert into reports_entries (name, kind) \
- values (%s, %s)", (row[0], row[1]))
- entries_map[(row[0], row[1])] = insert_cursor.lastrowid
-
- cursor.execute("""
- Select name, kind, reason_id, interaction_id, 1 from reports_bad
- inner join reports_bad_interactions on reports_bad.id=reports_bad_interactions.bad_id
- union
- Select name, kind, reason_id, interaction_id, 2 from reports_modified
- inner join reports_modified_interactions on reports_modified.id=reports_modified_interactions.modified_id
- union
- Select name, kind, reason_id, interaction_id, 3 from reports_extra
- inner join reports_extra_interactions on reports_extra.id=reports_extra_interactions.extra_id
- """)
- for row in cursor.fetchall():
- key = (row[0], row[1])
- if entries_map.get(key, None):
- entry_id = entries_map[key]
- else:
- find_cursor.execute("Select id from reports_entries where name=%s and kind=%s", key)
- rowe = find_cursor.fetchone()
- entry_id = rowe[0]
- insert_cursor.execute("insert into reports_entries_interactions \
- (entry_id, interaction_id, reason_id, type) values (%s, %s, %s, %s)", (entry_id, row[3], row[2], row[4]))
-
-
-def _interactions_constraint_or_idx():
- """sqlite doesn't support alter tables.. or constraints"""
- cursor = connection.cursor()
- try:
- cursor.execute('alter table reports_interaction add constraint reports_interaction_20100601 unique (client_id,timestamp)')
- except:
- cursor.execute('create unique index reports_interaction_20100601 on reports_interaction (client_id,timestamp)')
-
-
-def _remove_table_column(tbl, col):
- """sqlite doesn't support deleting a column via alter table"""
- cursor = connection.cursor()
- db_engine = Bcfg2.Server.Reports.settings.DATABASES['default']['ENGINE']
- if db_engine == 'django.db.backends.mysql':
- db_name = Bcfg2.Server.Reports.settings.DATABASES['default']['NAME']
- column_exists = cursor.execute('select * from information_schema.columns '
- 'where table_schema="%s" and '
- 'table_name="%s" '
- 'and column_name="%s";' % (db_name, tbl, col))
- if not column_exists:
- # column doesn't exist
- return
- # if column exists from previous database, remove it
- cursor.execute('alter table %s '
- 'drop column %s;' % (tbl, col))
- elif db_engine == 'django.db.backends.sqlite3':
- # check if table exists
- try:
- cursor.execute('select * from sqlite_master where name=%s and type="table";' % tbl)
- except DatabaseError:
- # table doesn't exist
- return
-
- # sqlite wants us to create a new table containing the columns we want
- # and copy into it http://www.sqlite.org/faq.html#q11
- tmptbl_name = "t_backup"
- _tmptbl_create = \
-"""create temporary table "%s" (
- "id" integer NOT NULL PRIMARY KEY,
- "client_id" integer NOT NULL REFERENCES "reports_client" ("id"),
- "timestamp" datetime NOT NULL,
- "state" varchar(32) NOT NULL,
- "repo_rev_code" varchar(64) NOT NULL,
- "goodcount" integer NOT NULL,
- "totalcount" integer NOT NULL,
- "server" varchar(256) NOT NULL,
- "bad_entries" integer NOT NULL,
- "modified_entries" integer NOT NULL,
- "extra_entries" integer NOT NULL,
- UNIQUE ("client_id", "timestamp")
-);""" % tmptbl_name
- _newtbl_create = \
-"""create table "%s" (
- "id" integer NOT NULL PRIMARY KEY,
- "client_id" integer NOT NULL REFERENCES "reports_client" ("id"),
- "timestamp" datetime NOT NULL,
- "state" varchar(32) NOT NULL,
- "repo_rev_code" varchar(64) NOT NULL,
- "goodcount" integer NOT NULL,
- "totalcount" integer NOT NULL,
- "server" varchar(256) NOT NULL,
- "bad_entries" integer NOT NULL,
- "modified_entries" integer NOT NULL,
- "extra_entries" integer NOT NULL,
- UNIQUE ("client_id", "timestamp")
-);""" % tbl
- new_cols = "id,\
- client_id,\
- timestamp,\
- state,\
- repo_rev_code,\
- goodcount,\
- totalcount,\
- server,\
- bad_entries,\
- modified_entries,\
- extra_entries"
-
- delete_col = [_tmptbl_create,
- "insert into %s select %s from %s;" % (tmptbl_name, new_cols, tbl),
- "drop table %s" % tbl,
- _newtbl_create,
- "create index reports_interaction_client_id on %s (client_id);" % tbl,
- "insert into %s select %s from %s;" % (tbl, new_cols,
- tmptbl_name),
- "drop table %s;" % tmptbl_name]
-
- for sql in delete_col:
- cursor.execute(sql)
-
-
-def _populate_interaction_entry_counts():
- '''Populate up the type totals for the interaction table'''
- cursor = connection.cursor()
- count_field = {TYPE_BAD: 'bad_entries',
- TYPE_MODIFIED: 'modified_entries',
- TYPE_EXTRA: 'extra_entries'}
-
- for type in list(count_field.keys()):
- cursor.execute("select count(type), interaction_id " +
- "from reports_entries_interactions where type = %s group by interaction_id" % type)
- updates = []
- for row in cursor.fetchall():
- updates.append(row)
- try:
- cursor.executemany("update reports_interaction set " + count_field[type] + "=%s where id = %s", updates)
- except Exception:
- e = sys.exc_info()[1]
- print(e)
- cursor.close()
-
-
-# be sure to test your upgrade query before reflecting the change in the models
-# the list of function and sql command to do should go here
-_fixes = [_merge_database_table_entries,
- # this will remove unused tables
- "drop table reports_bad;",
- "drop table reports_bad_interactions;",
- "drop table reports_extra;",
- "drop table reports_extra_interactions;",
- "drop table reports_modified;",
- "drop table reports_modified_interactions;",
- "drop table reports_repository;",
- "drop table reports_metadata;",
- "alter table reports_interaction add server varchar(256) not null default 'N/A';",
- # fix revision data type to support $VCS hashes
- "alter table reports_interaction add repo_rev_code varchar(64) default '';",
- # Performance enhancements for large sites
- 'alter table reports_interaction add column bad_entries integer not null default -1;',
- 'alter table reports_interaction add column modified_entries integer not null default -1;',
- 'alter table reports_interaction add column extra_entries integer not null default -1;',
- _populate_interaction_entry_counts,
- _interactions_constraint_or_idx,
- 'alter table reports_reason add is_binary bool NOT NULL default False;',
- 'alter table reports_reason add is_sensitive bool NOT NULL default False;',
- _remove_table_column('reports_interaction', 'client_version'),
- "alter table reports_reason add unpruned varchar(1280) not null default '';",
-]
-
-# this will calculate the last possible version of the database
-lastversion = len(_fixes)
-
-
-def rollupdate(current_version):
- """function responsible to coordinates all the updates
- need current_version as integer
- """
- ret = None
- if current_version < lastversion:
- for i in range(current_version, lastversion):
- try:
- if type(_fixes[i]) == str:
- connection.cursor().execute(_fixes[i])
- else:
- _fixes[i]()
- except:
- logger.error("Failed to perform db update %s" % (_fixes[i]),
- exc_info=1)
- # since the array starts at 0 but version
- # starts at 1 we add 1 to the normal count
- ret = InternalDatabaseVersion.objects.create(version=i + 1)
- return ret
- else:
- return None
-
-
-def dosync():
- """Function to do the syncronisation for the models"""
- # try to detect if it's a fresh new database
- try:
- cursor = connection.cursor()
- # If this table goes missing,
- # don't forget to change it to the new one
- cursor.execute("Select * from reports_client")
- # if we get here with no error then the database has existing tables
- fresh = False
- except:
- logger.debug("there was an error while detecting "
- "the freshness of the database")
- #we should get here if the database is new
- fresh = True
-
- # ensure database connections are closed
- # so that the management can do its job right
- try:
- cursor.close()
- connection.close()
- except:
- # ignore any errors from missing/invalid dbs
- pass
- # Do the syncdb according to the django version
- if "call_command" in dir(django.core.management):
- # this is available since django 1.0 alpha.
- # not yet tested for full functionnality
- django.core.management.call_command("syncdb", interactive=False, verbosity=0)
- if fresh:
- django.core.management.call_command("loaddata", 'initial_version.xml', verbosity=0)
- elif "syncdb" in dir(django.core.management):
- # this exist only for django 0.96.*
- django.core.management.syncdb(interactive=False, verbosity=0)
- if fresh:
- logger.debug("loading the initial_version fixtures")
- django.core.management.load_data(fixture_labels=['initial_version'], verbosity=0)
- else:
- logger.warning("Don't forget to run syncdb")
-
-
-def update_database():
- """method to search where we are in the revision
- of the database models and update them"""
- try:
- logger.debug("Running upgrade of models to the new one")
- dosync()
- know_version = InternalDatabaseVersion.objects.order_by('-version')
- if not know_version:
- logger.debug("No version, creating initial version")
- know_version = InternalDatabaseVersion.objects.create(version=0)
- else:
- know_version = know_version[0]
- logger.debug("Presently at %s" % know_version)
- if know_version.version < lastversion:
- new_version = rollupdate(know_version.version)
- if new_version:
- logger.debug("upgraded to %s" % new_version)
- except:
- logger.error("Error while updating the database")
- for x in traceback.format_exc().splitlines():
- logger.error(x)
diff --git a/src/lib/Bcfg2/Server/Reports/utils.py b/src/lib/Bcfg2/Server/Reports/utils.py
index e0b6ead59..c47763e39 100755
--- a/src/lib/Bcfg2/Server/Reports/utils.py
+++ b/src/lib/Bcfg2/Server/Reports/utils.py
@@ -3,7 +3,7 @@ from django.conf.urls.defaults import *
import re
"""List of filters provided by filteredUrls"""
-filter_list = ('server', 'state')
+filter_list = ('server', 'state', 'group')
class BatchFetch(object):
@@ -97,6 +97,8 @@ def filteredUrls(pattern, view, kwargs=None, name=None):
tail = mtail.group(1)
pattern = pattern[:len(pattern) - len(tail)]
for filter in ('/state/(?P<state>\w+)',
+ '/group/(?P<group>[\w\-\.]+)',
+ '/group/(?P<group>[\w\-\.]+)/(?P<state>[A-Za-z]+)',
'/server/(?P<server>[\w\-\.]+)',
'/server/(?P<server>[\w\-\.]+)/(?P<state>[A-Za-z]+)'):
results += [(pattern + filter + tail, view, kwargs)]
diff --git a/src/lib/Bcfg2/Server/Snapshots/model.py b/src/lib/Bcfg2/Server/Snapshots/model.py
index 5d7973c16..0bbd206da 100644
--- a/src/lib/Bcfg2/Server/Snapshots/model.py
+++ b/src/lib/Bcfg2/Server/Snapshots/model.py
@@ -6,13 +6,7 @@ import sqlalchemy.exceptions
from sqlalchemy.orm import relation, backref
from sqlalchemy.ext.declarative import declarative_base
-
-# py3k compatibility
-def u_str(string):
- if sys.hexversion >= 0x03000000:
- return string
- else:
- return unicode(string)
+from Bcfg2.Bcfg2Py3k import u_str
class Uniquer(object):
diff --git a/src/lib/Bcfg2/Server/__init__.py b/src/lib/Bcfg2/Server/__init__.py
index 96777b0bf..320371284 100644
--- a/src/lib/Bcfg2/Server/__init__.py
+++ b/src/lib/Bcfg2/Server/__init__.py
@@ -1,4 +1,8 @@
"""This is the set of modules for Bcfg2.Server."""
+import lxml.etree
+
__all__ = ["Admin", "Core", "FileMonitor", "Plugin", "Plugins",
- "Hostbase", "Reports", "Snapshots"]
+ "Hostbase", "Reports", "Snapshots", "XMLParser"]
+
+XMLParser = lxml.etree.XMLParser(remove_blank_text=True)
diff --git a/src/lib/Bcfg2/version.py b/src/lib/Bcfg2/version.py
new file mode 100644
index 000000000..ac10dac94
--- /dev/null
+++ b/src/lib/Bcfg2/version.py
@@ -0,0 +1,115 @@
+import re
+
+__version__ = "1.3.0"
+
+class Bcfg2VersionInfo(tuple):
+ v_re = re.compile(r'(\d+)(\w+)(\d+)')
+
+ def __new__(cls, vstr):
+ (major, minor, rest) = vstr.split(".")
+ match = cls.v_re.match(rest)
+ if match:
+ micro, releaselevel, serial = match.groups()
+ else:
+ micro = rest
+ releaselevel = 'final'
+ serial = 0
+ return tuple.__new__(cls, [int(major), int(minor), int(micro),
+ releaselevel, int(serial)])
+
+ def __init__(self, vstr):
+ tuple.__init__(self)
+ self.major, self.minor, self.micro, self.releaselevel, self.serial = \
+ tuple(self)
+
+ def __repr__(self):
+ return "(major=%s, minor=%s, micro=%s, releaselevel=%s, serial=%s)" % \
+ tuple(self)
+
+ def _release_cmp(self, r1, r2):
+ if r1 == r2:
+ return 0
+ elif r1 == "final":
+ return -1
+ elif r2 == "final":
+ return 1
+ elif r1 == "rc":
+ return -1
+ elif r2 == "rc":
+ return 1
+ # should never get to anything past this point
+ elif r1 == "pre":
+ return -1
+ elif r2 == "pre":
+ return 1
+ else:
+ # wtf?
+ return 0
+
+ def __gt__(self, version):
+ if version is None:
+ # older bcfg2 clients didn't report their version, so we
+ # handle this case specially and assume that any reported
+ # version is newer than any indeterminate version
+ return True
+ try:
+ for i in range(3):
+ if self[i] > version[i]:
+ return True
+ elif self[i] < version[i]:
+ return False
+ rel = self._release_cmp(self[3], version[3])
+ if rel < 0:
+ return True
+ elif rel > 0:
+ return False
+ if self[4] > version[4]:
+ return True
+ else:
+ return False
+ except TypeError:
+ return self > Bcfg2VersionInfo(version)
+
+ def __lt__(self, version):
+ if version is None:
+ # older bcfg2 clients didn't report their version, so we
+ # handle this case specially and assume that any reported
+ # version is newer than any indeterminate version
+ return False
+ try:
+ for i in range(3):
+ if self[i] < version[i]:
+ return True
+ elif self[i] > version[i]:
+ return False
+ rel = self._release_cmp(self[3], version[3])
+ if rel > 0:
+ return True
+ elif rel < 0:
+ return False
+ if self[4] < version[4]:
+ return True
+ else:
+ return False
+ except TypeError:
+ return self < Bcfg2VersionInfo(version)
+
+ def __eq__(self, version):
+ if version is None:
+ # older bcfg2 clients didn't report their version, so we
+ # handle this case specially and assume that any reported
+ # version is newer than any indeterminate version
+ return False
+ try:
+ rv = True
+ for i in range(len(self)):
+ rv &= self[i] == version[i]
+ return rv
+ except TypeError:
+ return self == Bcfg2VersionInfo(version)
+
+ def __ge__(self, version):
+ return not self < version
+
+ def __le__(self, version):
+ return not self > version
diff --git a/src/sbin/bcfg2 b/src/sbin/bcfg2
index fb34e627b..4fbbef877 100755
--- a/src/sbin/bcfg2
+++ b/src/sbin/bcfg2
@@ -18,6 +18,8 @@ import Bcfg2.Client.Tools
# Compatibility imports
from Bcfg2.Bcfg2Py3k import xmlrpclib
+from Bcfg2.version import __version__
+
import Bcfg2.Proxy
import Bcfg2.Logger
@@ -27,10 +29,6 @@ def cb_sigint_handler(signum, frame):
"""Exit upon CTRL-C."""
os._exit(1)
-DECISION_LIST = Bcfg2.Options.Option('Decision List', default=False,
- cmd="--decision-list", odesc='<file>',
- long_arg=True)
-
class Client:
"""The main bcfg2 client class"""
@@ -38,46 +36,8 @@ class Client:
def __init__(self):
self.toolset = None
self.config = None
-
- optinfo = {
- # 'optname': (('-a', argdesc, optdesc),
- # env, cfpath, default, boolean)),
- 'verbose': Bcfg2.Options.VERBOSE,
- 'extra': Bcfg2.Options.CLIENT_EXTRA_DISPLAY,
- 'quick': Bcfg2.Options.CLIENT_QUICK,
- 'debug': Bcfg2.Options.DEBUG,
- 'lockfile': Bcfg2.Options.LOCKFILE,
- 'drivers': Bcfg2.Options.CLIENT_DRIVERS,
- 'dryrun': Bcfg2.Options.CLIENT_DRYRUN,
- 'paranoid': Bcfg2.Options.CLIENT_PARANOID,
- 'bundle': Bcfg2.Options.CLIENT_BUNDLE,
- 'bundle-quick': Bcfg2.Options.CLIENT_BUNDLEQUICK,
- 'indep': Bcfg2.Options.CLIENT_INDEP,
- 'file': Bcfg2.Options.CLIENT_FILE,
- 'interactive': Bcfg2.Options.INTERACTIVE,
- 'cache': Bcfg2.Options.CLIENT_CACHE,
- 'profile': Bcfg2.Options.CLIENT_PROFILE,
- 'remove': Bcfg2.Options.CLIENT_REMOVE,
- 'help': Bcfg2.Options.HELP,
- 'setup': Bcfg2.Options.CFILE,
- 'server': Bcfg2.Options.SERVER_LOCATION,
- 'user': Bcfg2.Options.CLIENT_USER,
- 'password': Bcfg2.Options.SERVER_PASSWORD,
- 'retries': Bcfg2.Options.CLIENT_RETRIES,
- 'kevlar': Bcfg2.Options.CLIENT_KEVLAR,
- 'decision-list': DECISION_LIST,
- 'encoding': Bcfg2.Options.ENCODING,
- 'omit-lock-check': Bcfg2.Options.OMIT_LOCK_CHECK,
- 'filelog': Bcfg2.Options.LOGGING_FILE_PATH,
- 'decision': Bcfg2.Options.CLIENT_DLIST,
- 'servicemode': Bcfg2.Options.CLIENT_SERVICE_MODE,
- 'key': Bcfg2.Options.CLIENT_KEY,
- 'certificate': Bcfg2.Options.CLIENT_CERT,
- 'ca': Bcfg2.Options.CLIENT_CA,
- 'serverCN': Bcfg2.Options.CLIENT_SCNS,
- 'timeout': Bcfg2.Options.CLIENT_TIMEOUT,
- }
-
+
+ optinfo = Bcfg2.Options.CLIENT_COMMON_OPTIONS
self.setup = Bcfg2.Options.OptionParser(optinfo)
self.setup.parse(sys.argv[1:])
@@ -93,30 +53,29 @@ class Client:
Bcfg2.Logger.setup_logging('bcfg2',
to_syslog=False,
level=level,
- to_file=self.setup['filelog'])
+ to_file=self.setup['logging'])
self.logger = logging.getLogger('bcfg2')
self.logger.debug(self.setup)
- if self.setup['bundle-quick']:
+ if self.setup['bundle_quick']:
if self.setup['bundle'] == []:
self.logger.error("-Q option requires -b")
raise SystemExit(1)
- elif self.setup['remove'] != False:
+ elif self.setup['remove']:
self.logger.error("-Q option incompatible with -r")
raise SystemExit(1)
if 'drivers' in self.setup and self.setup['drivers'] == 'help':
self.logger.info("The following drivers are available:")
self.logger.info(Bcfg2.Client.Tools.drivers)
raise SystemExit(0)
- if self.setup['remove'] and 'services' in self.setup['remove']:
- self.logger.error("Service removal is nonsensical; removed services will only be disabled")
- if self.setup['remove'] not in [False,
- 'all',
- 'Services',
- 'Packages',
- 'services',
- 'packages']:
- self.logger.error("Got unknown argument %s for -r" % (self.setup['remove']))
- if (self.setup["file"] != False) and (self.setup["cache"] != False):
+ if self.setup['remove'] and 'services' in self.setup['remove'].lower():
+ self.logger.error("Service removal is nonsensical; "
+ "removed services will only be disabled")
+ if (self.setup['remove'] and
+ self.setup['remove'].lower() not in ['all', 'services',
+ 'packages']):
+ self.logger.error("Got unknown argument %s for -r" %
+ self.setup['remove'])
+ if self.setup["file"] and self.setup["cache"]:
print("cannot use -f and -c together")
raise SystemExit(1)
if not self.setup['server'].startswith('https://'):
@@ -195,6 +154,24 @@ class Client:
raise SystemExit(1)
try:
+ probe_data = proxy.DeclareVersion(__version__)
+ except xmlrpclib.Fault:
+ err = sys.exc_info()[1]
+ if (err.faultCode == xmlrpclib.METHOD_NOT_FOUND or
+ (err.faultCode == 7 and
+ err.faultString.startswith("Unknown method"))):
+ self.logger.debug("Server does not support declaring "
+ "client version")
+ else:
+ self.logger.error("Failed to declare version: %s" % err)
+ except (Bcfg2.Proxy.ProxyError,
+ Bcfg2.Proxy.CertificateError,
+ socket.gaierror,
+ socket.error):
+ err = sys.exc_info()[1]
+ self.logger.error("Failed to declare version: %s" % err)
+
+ try:
probe_data = proxy.GetProbes()
except (Bcfg2.Proxy.ProxyError,
Bcfg2.Proxy.CertificateError,
@@ -282,10 +259,12 @@ class Client:
self.fatal_error("Server error: %s" % (self.config.text))
return(1)
- if self.setup['bundle-quick']:
+ if self.setup['bundle_quick']:
newconfig = Bcfg2.Client.XML.XML('<Configuration/>')
- [newconfig.append(bundle) for bundle in self.config.getchildren() if \
- bundle.tag == 'Bundle' and bundle.get('name') in self.setup['bundle']]
+ [newconfig.append(bundle)
+ for bundle in self.config.getchildren()
+ if (bundle.tag == 'Bundle' and
+ bundle.get('name') in self.setup['bundle'])]
self.config = newconfig
self.tools = Bcfg2.Client.Frame.Frame(self.config,
@@ -293,7 +272,7 @@ class Client:
times, self.setup['drivers'],
self.setup['dryrun'])
- if not self.setup['omit-lock-check']:
+ if not self.setup['omit_lock_check']:
#check lock here
try:
lockfile = open(self.setup['lockfile'], 'w')
@@ -309,7 +288,7 @@ class Client:
# execute the said configuration
self.tools.Execute()
- if not self.setup['omit-lock-check']:
+ if not self.setup['omit_lock_check']:
#unlock here
if lockfile:
try:
@@ -318,7 +297,7 @@ class Client:
except OSError:
self.logger.error("Failed to unlock lockfile %s" % lockfile.name)
- if not self.setup['file'] and not self.setup['bundle-quick']:
+ if not self.setup['file'] and not self.setup['bundle_quick']:
# upload statistics
feedback = self.tools.GenerateStats()
diff --git a/src/sbin/bcfg2-admin b/src/sbin/bcfg2-admin
index 007dd0af3..9b28d9bd5 100755
--- a/src/sbin/bcfg2-admin
+++ b/src/sbin/bcfg2-admin
@@ -36,25 +36,21 @@ def create_description():
return description.getvalue()
def main():
- optinfo = {
- 'configfile': Bcfg2.Options.CFILE,
- 'help': Bcfg2.Options.HELP,
- 'verbose': Bcfg2.Options.VERBOSE,
- 'repo': Bcfg2.Options.SERVER_REPOSITORY,
- 'plugins': Bcfg2.Options.SERVER_PLUGINS,
- 'event debug': Bcfg2.Options.DEBUG,
- 'filemonitor': Bcfg2.Options.SERVER_FILEMONITOR,
- 'password': Bcfg2.Options.SERVER_PASSWORD,
- 'encoding': Bcfg2.Options.ENCODING,
- }
+ optinfo = dict()
+ optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
+ optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
# override default help message to include description of all modes
setup.hm = "%s\n%s" % (setup.buildHelpMessage(), create_description())
setup.parse(sys.argv[1:])
- log_args = dict(to_syslog=False, to_console=logging.WARNING)
- if setup['verbose']:
- log_args['to_console'] = logging.DEBUG
+ if setup['debug']:
+ level = logging.DEBUG
+ elif setup['verbose']:
+ level = logging.INFO
+ else:
+ level = logging.WARNING
+ Bcfg2.Logger.setup_logging('bcfg2-admin', to_syslog=False, level=level)
# Provide help if requested or no args were specified
if (not setup['args'] or len(setup['args']) < 1 or
@@ -84,7 +80,6 @@ def main():
mode.bcore.shutdown()
else:
log.error("Unknown mode %s" % setup['args'][0])
- print("Usage:\n %s" % setup.buildHelpMessage())
print(create_description())
raise SystemExit(1)
diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt
new file mode 100755
index 000000000..89dfe3e2a
--- /dev/null
+++ b/src/sbin/bcfg2-crypt
@@ -0,0 +1,356 @@
+#!/usr/bin/env python
+""" helper for encrypting/decrypting Cfg and Properties files """
+
+import os
+import sys
+import logging
+import lxml.etree
+import Bcfg2.Logger
+import Bcfg2.Options
+import Bcfg2.Encryption
+
+LOGGER = None
+
+def get_logger(verbose=0):
+ """ set up logging according to the verbose level given on the
+ command line """
+ global LOGGER
+ if LOGGER is None:
+ LOGGER = logging.getLogger(sys.argv[0])
+ stderr = logging.StreamHandler()
+ if verbose:
+ level = logging.DEBUG
+ else:
+ level = logging.WARNING
+ LOGGER.setLevel(level)
+ LOGGER.addHandler(stderr)
+ syslog = logging.handlers.SysLogHandler("/dev/log")
+ syslog.setFormatter(logging.Formatter("%(name)s: %(message)s"))
+ LOGGER.addHandler(syslog)
+ return LOGGER
+
+
+class Encryptor(object):
+ def __init__(self, setup):
+ self.setup = setup
+ self.logger = get_logger()
+ self.passphrase = None
+ self.pname = None
+
+ def get_encrypted_filename(self, plaintext_filename):
+ return plaintext_filename
+
+ def get_plaintext_filename(self, encrypted_filename):
+ return encrypted_filename
+
+ def chunk(self, data):
+ yield data
+
+ def unchunk(self, data, original):
+ return data[0]
+
+ def set_passphrase(self):
+ if (not self.setup.cfp.has_section("encryption") or
+ self.setup.cfp.options("encryption") == 0):
+ self.logger.error("No passphrases available in %s" %
+ self.setup['configfile'])
+ return False
+
+ if self.passphrase:
+ self.logger.debug("Using previously determined passphrase %s" %
+ self.pname)
+ return True
+
+ if self.setup['passphrase']:
+ self.pname = self.setup['passphrase']
+
+ if self.pname:
+ if self.setup.cfp.has_option("encryption", self.pname):
+ self.passphrase = self.setup.cfp.get("encryption",
+ self.pname)
+ self.logger.debug("Using passphrase %s specified on command "
+ "line" % self.pname)
+ return True
+ else:
+ self.logger.error("Could not find passphrase %s in %s" %
+ (self.pname, self.setup['configfile']))
+ return False
+ else:
+ pnames = self.setup.cfp.options("encryption")
+ if len(pnames) == 1:
+ self.passphrase = self.setup.cfp.get(pnames[0])
+ self.pname = pnames[0]
+ self.logger.info("Using passphrase %s" % pnames[0])
+ return True
+ self.logger.info("No passphrase could be determined")
+ return False
+
+ def encrypt(self, fname):
+ try:
+ plaintext = open(fname).read()
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error reading %s, skipping: %s" % (fname, err))
+ return False
+
+ self.set_passphrase()
+
+ crypted = []
+ for chunk in self.chunk(plaintext):
+ try:
+ passphrase, pname = self.get_passphrase(chunk)
+ except TypeError:
+ return False
+
+ crypted.append(self._encrypt(chunk, passphrase, name=pname))
+
+ new_fname = self.get_encrypted_filename(fname)
+ try:
+ open(new_fname, "wb").write(self.unchunk(crypted, plaintext))
+ self.logger.info("Wrote encrypted data to %s" % new_fname)
+ return True
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error writing encrypted data from %s to %s: %s" %
+ (fname, new_fname, err))
+ return False
+
+ def _encrypt(self, plaintext, passphrase, name=None):
+ return Bcfg2.Encryption.ssl_encrypt(plaintext, passphrase)
+
+ def decrypt(self, fname):
+ try:
+ crypted = open(fname).read()
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error reading %s, skipping: %s" % (fname, err))
+ return False
+
+ self.set_passphrase()
+
+ plaintext = []
+ for chunk in self.chunk(crypted):
+ try:
+ passphrase, pname = self.get_passphrase(chunk)
+ try:
+ plaintext.append(self._decrypt(chunk, passphrase))
+ except Bcfg2.Encryption.EVPError:
+ self.logger.info("Could not decrypt %s with the specified "
+ "passphrase" % fname)
+ return False
+ except:
+ err = sys.exc_info()[1]
+ self.logger.error("Error decrypting %s: %s" % (fname, err))
+ return False
+ except TypeError:
+ pchunk = None
+ for pname in self.setup.cfp.options('encryption'):
+ self.logger.debug("Trying passphrase %s" % pname)
+ passphrase = self.setup.cfp.get('encryption', pname)
+ try:
+ pchunk = self._decrypt(chunk, passphrase)
+ break
+ except Bcfg2.Encryption.EVPError:
+ pass
+ except:
+ err = sys.exc_info()[1]
+ self.logger.error("Error decrypting %s: %s" %
+ (fname, err))
+ if pchunk is not None:
+ plaintext.append(pchunk)
+ else:
+ self.logger.error("Could not decrypt %s with any "
+ "passphrase in %s" %
+ (fname, self.setup['configfile']))
+ return False
+
+ new_fname = self.get_plaintext_filename(fname)
+ try:
+ open(new_fname, "wb").write(self.unchunk(plaintext, crypted))
+ self.logger.info("Wrote decrypted data to %s" % new_fname)
+ return True
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error writing encrypted data from %s to %s: %s" %
+ (fname, new_fname, err))
+ return False
+
+ def get_passphrase(self, chunk):
+ pname = self._get_passphrase(chunk)
+ if not self.pname:
+ if not pname:
+ self.logger.info("No passphrase given on command line or "
+ "found in file")
+ return False
+ elif self.setup.cfp.has_option("encryption", pname):
+ passphrase = self.setup.cfp.get("encryption", pname)
+ else:
+ self.logger.error("Could not find passphrase %s in %s" %
+ (pname, self.setup['configfile']))
+ return False
+ else:
+ pname = self.pname
+ passphrase = self.passphrase
+ if self.pname != pname:
+ self.logger.warning("Passphrase given on command line (%s) "
+ "differs from passphrase embedded in "
+ "file (%s), using command-line option" %
+ (self.pname, pname))
+ return (passphrase, pname)
+
+ def _get_passphrase(self, chunk):
+ return None
+
+ def _decrypt(self, crypted, passphrase):
+ return Bcfg2.Encryption.ssl_decrypt(crypted, passphrase)
+
+
+class CfgEncryptor(Encryptor):
+ def get_encrypted_filename(self, plaintext_filename):
+ return plaintext_filename + ".crypt"
+
+ def get_plaintext_filename(self, encrypted_filename):
+ if encrypted_filename.endswith(".crypt"):
+ return encrypted_filename[:-6]
+ else:
+ return Encryptor.get_plaintext_filename(self, encrypted_filename)
+
+
+class PropertiesEncryptor(Encryptor):
+ def _encrypt(self, plaintext, passphrase, name=None):
+ # plaintext is an lxml.etree._Element
+ if name is None:
+ name = "true"
+ if plaintext.text and plaintext.text.strip():
+ plaintext.text = Bcfg2.Encryption.ssl_encrypt(plaintext.text,
+ passphrase)
+ plaintext.set("encrypted", name)
+ return plaintext
+
+ def chunk(self, data):
+ xdata = lxml.etree.XML(data)
+ if self.setup['xpath']:
+ elements = xdata.xpath(self.setup['xpath'])
+ else:
+ elements = xdata.xpath('//*[@encrypted]')
+ if not elements:
+ elements = list(xdata.getiterator())
+ # this is not a good use of a generator, but we need to
+ # generate the full list of elements in order to ensure that
+ # some exist before we know what to return
+ for elt in elements:
+ yield elt
+
+ def unchunk(self, data, original):
+ # Properties elements are modified in-place, so we don't
+ # actually need to unchunk anything
+ xdata = data[0]
+ # find root element
+ while xdata.getparent() != None:
+ xdata = xdata.getparent()
+ xdata.set("encryption", "true")
+ return lxml.etree.tostring(xdata)
+
+ def _get_passphrase(self, chunk):
+ pname = chunk.get("encrypted") or chunk.get("encryption")
+ if pname and pname.lower() != "true":
+ return pname
+ return None
+
+ def _decrypt(self, crypted, passphrase):
+ # crypted is in lxml.etree._Element
+ if not crypted.text or not crypted.text.strip():
+ self.logger.warning("Skipping empty element %s" % crypted.tag)
+ return crypted
+ rv = Bcfg2.Encryption.ssl_decrypt(crypted.text, passphrase)
+ crypted.text = rv
+ return crypted
+
+
+def main():
+ optinfo = dict()
+ optinfo.update(Bcfg2.Options.CRYPT_OPTIONS)
+ optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
+ setup = Bcfg2.Options.OptionParser(optinfo)
+ setup.hm = " bcfg2-crypt [options] <filename>\nOptions:\n%s" % \
+ setup.buildHelpMessage()
+ setup.parse(sys.argv[1:])
+
+ if not setup['args']:
+ print(setup.hm)
+ raise SystemExit(1)
+ elif setup['encrypt'] and setup['decrypt']:
+ print("You cannot specify both --encrypt) and --decrypt")
+ raise SystemExit(1)
+ elif setup['cfg'] and setup['properties']:
+ print("You cannot specify both --cfg and --properties")
+ raise SystemExit(1)
+ elif setup['cfg'] and setup['properties']:
+ print("Specifying --xpath with --cfg is nonsensical, ignoring --xpath")
+ setup['xpath'] = Bcfg2.Options.CRYPT_XPATH.default
+ elif setup['decrypt'] and setup['remove']:
+ print("--remove cannot be used with --decrypt, ignoring")
+ setup['remove'] = Bcfg2.Options.CRYPT_REMOVE.default
+
+ logger = get_logger(setup['verbose'])
+
+ props_crypt = PropertiesEncryptor(setup)
+ cfg_crypt = CfgEncryptor(setup)
+
+ for fname in setup['args']:
+ if not os.path.exists(fname):
+ logger.error("%s does not exist, skipping" % fname)
+ continue
+
+ # figure out if we need to encrypt this as a Properties file
+ # or as a Cfg file
+ props = False
+ if setup['properties']:
+ props = True
+ elif setup['cfg']:
+ props = False
+ elif fname.endswith(".xml"):
+ try:
+ xroot = lxml.etree.parse(fname).getroot()
+ if xroot.tag == "Properties":
+ props = True
+ else:
+ props = False
+ except IOError:
+ err = sys.exc_info()[1]
+ logger.error("Error reading %s, skipping: %s" % (fname, err))
+ continue
+ except lxml.etree.XMLSyntaxError:
+ props = False
+ else:
+ props = False
+
+ if props:
+ encryptor = props_crypt
+ else:
+ encryptor = cfg_crypt
+
+ if setup['encrypt']:
+ if not encryptor.encrypt(fname):
+ print("Failed to encrypt %s, skipping" % fname)
+ elif setup['decrypt']:
+ if not encryptor.decrypt(fname):
+ print("Failed to decrypt %s, skipping" % fname)
+ else:
+ logger.info("Neither --encrypt nor --decrypt specified, "
+ "determining mode")
+ if not encryptor.decrypt(fname):
+ logger.info("Failed to decrypt %s, trying encryption" % fname)
+ if not encryptor.encrypt(fname):
+ print("Failed to encrypt %s, skipping" % fname)
+
+ if setup['remove'] and encryptor.get_encrypted_filename(fname) != fname:
+ try:
+ os.unlink(fname)
+ except IOError:
+ err = sys.exc_info()[1]
+ logger.error("Error removing %s: %s" % (fname, err))
+ continue
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/src/sbin/bcfg2-info b/src/sbin/bcfg2-info
index 8598a58eb..297b2227d 100755
--- a/src/sbin/bcfg2-info
+++ b/src/sbin/bcfg2-info
@@ -10,6 +10,7 @@ import fnmatch
import logging
import tempfile
import lxml.etree
+import traceback
from code import InteractiveConsole
try:
@@ -26,9 +27,14 @@ import Bcfg2.Logger
import Bcfg2.Options
import Bcfg2.Server.Core
import Bcfg2.Server.Plugins.Metadata
-import Bcfg2.Server.Plugins.SGenshi
import Bcfg2.Server.Plugin
+try:
+ import Bcfg2.Server.Plugins.SGenshi
+ has_genshi = True
+except ImportError:
+ has_genshi = False
+
logger = logging.getLogger('bcfg2-info')
USAGE = """Commands:
build <hostname> <filename> - Build config for hostname, writing to filename
@@ -131,6 +137,38 @@ def displayTrace(trace, num=80, sort=('time', 'calls')):
stats.sort_stats('cumulative', 'calls', 'time')
stats.print_stats(200)
+def load_interpreters():
+ interpreters = dict(python=lambda v: InteractiveConsole(v).interact())
+ best = "python"
+ try:
+ import bpython.cli
+ interpreters["bpython"] = lambda v: bpython.cli.main(args=[], locals_=v)
+ best = "bpython"
+ except ImportError:
+ pass
+
+ try:
+ # whether ipython is actually better than bpython is
+ # up for debate, but this is the behavior that existed
+ # before --interpreter was added, so we call IPython
+ # better
+ import IPython
+ if hasattr(IPython, "Shell"):
+ interpreters["ipython"] = lambda v: \
+ IPython.Shell.IPShell(argv=[], user_ns=v).mainloop
+ best = "ipython"
+ elif hasattr(IPython, "embed"):
+ interpreters["ipython"] = lambda v: IPython.embed(user_ns=v)
+ best = "ipython"
+ else:
+ print("Unknown IPython API version")
+ except ImportError:
+ pass
+
+ interpreters['best'] = interpreters[best]
+ return interpreters
+
+
class infoCore(cmd.Cmd, Bcfg2.Server.Core.Core):
"""Main class for bcfg2-info."""
def __init__(self, repo, plgs, passwd, encoding, event_debug,
@@ -185,24 +223,21 @@ class infoCore(cmd.Cmd, Bcfg2.Server.Core.Core):
spath = opt[1]
elif opt[0] == '-n':
interactive = False
- sh = InteractiveConsole(locals())
if scriptmode:
+ sh = InteractiveConsole(locals())
for command in [c.strip() for c in open(spath).readlines()]:
if command:
sh.push(command)
if interactive:
- print("Dropping to python interpreter; press ^D to resume")
- try:
- import IPython
- if hasattr(IPython, "Shell"):
- shell = IPython.Shell.IPShell(argv=[], user_ns=locals())
- shell.mainloop()
- elif hasattr(IPython, "embed"):
- IPython.embed(user_ns=locals())
- else:
- raise ImportError
- except ImportError:
- sh.interact()
+ interpreters = load_interpreters()
+ if setup['interpreter'] in interpreters:
+ print("Dropping to %s interpreter; press ^D to resume" %
+ setup['interpreter'])
+ interpreters[setup['interpreter']](locals())
+ else:
+ logger.error("Invalid interpreter %s" % setup['interpreter'])
+ logger.error("Valid interpreters are: %s" %
+ ", ".join(interpeters.keys()))
def do_quit(self, _):
"""
@@ -373,7 +408,8 @@ class infoCore(cmd.Cmd, Bcfg2.Server.Core.Core):
print("Could not write to %s: %s" % (outfile, err))
print(data)
except Exception:
- print("Failed to build entry %s for host %s" % (fname, client))
+ print("Failed to build entry %s for host %s: %s" %
+ (fname, client, traceback.format_exc().splitlines()[-1]))
raise
def do_buildbundle(self, args):
@@ -384,8 +420,9 @@ class infoCore(cmd.Cmd, Bcfg2.Server.Core.Core):
metadata = self.build_metadata(client)
if bname in self.plugins['Bundler'].entries:
bundle = self.plugins['Bundler'].entries[bname]
- if isinstance(bundle,
- Bcfg2.Server.Plugins.SGenshi.SGenshiTemplateFile):
+ if (has_genshi and
+ isinstance(bundle,
+ Bcfg2.Server.Plugins.SGenshi.SGenshiTemplateFile)):
stream = bundle.template.generate(metadata=metadata)
print(stream.render("xml"))
else:
@@ -585,36 +622,16 @@ class infoCore(cmd.Cmd, Bcfg2.Server.Core.Core):
print(" %s" % "\n ".join(unknown))
def do_packagesources(self, args):
+ if not args:
+ print("Usage: packagesources <hostname>")
+ return
try:
metadata = self.build_metadata(args)
except Bcfg2.Server.Plugins.Metadata.MetadataConsistencyError:
print("Unable to build metadata for host %s" % args)
return
collection = self.plugins['Packages']._get_collection(metadata)
- for source in collection.sources:
- # get_urls() loads url_map as a side-effect
- source.get_urls()
- for url_map in source.url_map:
- for arch in url_map['arches']:
- # make sure client is in all the proper arch groups
- if arch not in metadata.groups:
- continue
- reponame = source.get_repo_name(url_map)
- print("Name: %s" % reponame)
- print(" Type: %s" % source.ptype)
- if url_map['url'] != '':
- print(" URL: %s" % url_map['url'])
- elif url_map['rawurl'] != '':
- print(" RAWURL: %s" % url_map['rawurl'])
- if source.gpgkeys:
- print(" GPG Key(s): %s" % ", ".join(source.gpgkeys))
- else:
- print(" GPG Key(s): None")
- if len(source.blacklist):
- print(" Blacklist: %s" % ", ".join(source.blacklist))
- if len(source.whitelist):
- print(" Whitelist: %s" % ", ".join(source.whitelist))
- print("")
+ print collection.sourcelist()
def do_profile(self, arg):
"""."""
@@ -636,31 +653,17 @@ class infoCore(cmd.Cmd, Bcfg2.Server.Core.Core):
if __name__ == '__main__':
Bcfg2.Logger.setup_logging('bcfg2-info', to_syslog=False)
- optinfo = {
- 'configfile': Bcfg2.Options.CFILE,
- 'help': Bcfg2.Options.HELP,
- 'event debug': Bcfg2.Options.DEBUG,
- 'profile': Bcfg2.Options.CORE_PROFILE,
- 'encoding': Bcfg2.Options.ENCODING,
- # Server options
- 'repo': Bcfg2.Options.SERVER_REPOSITORY,
- 'plugins': Bcfg2.Options.SERVER_PLUGINS,
- 'password': Bcfg2.Options.SERVER_PASSWORD,
- 'mconnect': Bcfg2.Options.SERVER_MCONNECT,
- 'filemonitor': Bcfg2.Options.SERVER_FILEMONITOR,
- 'location': Bcfg2.Options.SERVER_LOCATION,
- 'static': Bcfg2.Options.SERVER_STATIC,
- 'key': Bcfg2.Options.SERVER_KEY,
- 'cert': Bcfg2.Options.SERVER_CERT,
- 'ca': Bcfg2.Options.SERVER_CA,
- 'password': Bcfg2.Options.SERVER_PASSWORD,
- 'protocol': Bcfg2.Options.SERVER_PROTOCOL,
- # More options
- 'logging': Bcfg2.Options.LOGGING_FILE_PATH
- }
+ optinfo = dict(profile=Bcfg2.Options.CORE_PROFILE,
+ mconnect=Bcfg2.Options.SERVER_MCONNECT,
+ interactive=Bcfg2.Options.INTERACTIVE,
+ interpreter=Bcfg2.Options.INTERPRETER)
+ optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
+ optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
- setup.hm = "Usage:\n %s\n%s" % (setup.buildHelpMessage(),
- USAGE)
+ setup.hm = "\n".join([" bcfg2-info [options] [command <command args>]",
+ "Options:",
+ setup.buildHelpMessage(),
+ USAGE])
setup.parse(sys.argv[1:])
if setup['args'] and setup['args'][0] == 'help':
@@ -670,14 +673,14 @@ if __name__ == '__main__':
prof = profile.Profile()
loop = prof.runcall(infoCore, setup['repo'], setup['plugins'],
setup['password'], setup['encoding'],
- setup['event debug'], setup['filemonitor'],
+ setup['debug'], setup['filemonitor'],
setup)
displayTrace(prof)
else:
if setup['profile']:
print("Profiling functionality not available.")
loop = infoCore(setup['repo'], setup['plugins'], setup['password'],
- setup['encoding'], setup['event debug'],
+ setup['encoding'], setup['debug'],
setup['filemonitor'], setup)
loop.Run(setup['args'])
diff --git a/src/sbin/bcfg2-lint b/src/sbin/bcfg2-lint
index 78b833f02..c06b87d76 100755
--- a/src/sbin/bcfg2-lint
+++ b/src/sbin/bcfg2-lint
@@ -65,43 +65,33 @@ def load_server(setup):
setup['password'], setup['encoding'],
filemonitor=setup['filemonitor'],
setup=setup)
- if setup['event debug']:
- core.fam.debug = True
core.fam.handle_events_in_interval(4)
return core
+def load_plugin(module, obj_name=None):
+ parts = module.split(".")
+ if obj_name is None:
+ obj_name = parts[-1]
+
+ try:
+ mod = __import__(module)
+ except ImportError:
+ err = sys.exc_info()[1]
+ logger.error("Failed to load plugin %s: %s" % (obj_name, err))
+ raise
+
+ for p in parts[1:]:
+ mod = getattr(mod, p)
+ return getattr(mod, obj_name)
+
if __name__ == '__main__':
- optinfo = {
- 'configfile': Bcfg2.Options.CFILE,
- 'help': Bcfg2.Options.HELP,
- 'verbose': Bcfg2.Options.VERBOSE,
- 'event debug': Bcfg2.Options.DEBUG,
- 'encoding': Bcfg2.Options.ENCODING,
- # Server options
- 'repo': Bcfg2.Options.SERVER_REPOSITORY,
- 'plugins': Bcfg2.Options.SERVER_PLUGINS,
- 'mconnect': Bcfg2.Options.SERVER_MCONNECT,
- 'filemonitor': Bcfg2.Options.SERVER_FILEMONITOR,
- 'location': Bcfg2.Options.SERVER_LOCATION,
- 'static': Bcfg2.Options.SERVER_STATIC,
- 'key': Bcfg2.Options.SERVER_KEY,
- 'cert': Bcfg2.Options.SERVER_CERT,
- 'ca': Bcfg2.Options.SERVER_CA,
- 'password': Bcfg2.Options.SERVER_PASSWORD,
- 'protocol': Bcfg2.Options.SERVER_PROTOCOL,
- # More options
- 'logging': Bcfg2.Options.LOGGING_FILE_PATH,
- 'stdin': Bcfg2.Options.FILES_ON_STDIN,
- 'schema': Bcfg2.Options.SCHEMA_PATH,
- 'config': Bcfg2.Options.Option('Specify bcfg2-lint configuration file',
- '/etc/bcfg2-lint.conf',
- cmd='--lint-config',
- odesc='<conffile>',
- long_arg=True),
- 'showerrors': Bcfg2.Options.Option('Show error handling', False,
- cmd='--list-errors',
- long_arg=True),
- }
+ optinfo = dict(config=Bcfg2.Options.LINT_CONFIG,
+ showerrors=Bcfg2.Options.LINT_SHOW_ERRORS,
+ stdin=Bcfg2.Options.LINT_FILES_ON_STDIN,
+ schema=Bcfg2.Options.SCHEMA_PATH,
+ plugins=Bcfg2.Options.SERVER_PLUGINS)
+ optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
+ optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
setup.parse(sys.argv[1:])
@@ -116,36 +106,39 @@ if __name__ == '__main__':
# get list of plugins to run
if setup['args']:
- allplugins = setup['args']
+ plugin_list = setup['args']
elif "bcfg2-repo-validate" in sys.argv[0]:
- allplugins = 'Duplicates,RequiredAttrs,Validate'.split(',')
+ plugin_list = 'Duplicates,RequiredAttrs,Validate'.split(',')
else:
try:
- allplugins = config.get('lint', 'plugins').split(',')
+ plugin_list = config.get('lint', 'plugins').split(',')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
- allplugins = Bcfg2.Server.Lint.__all__
+ plugin_list = Bcfg2.Server.Lint.__all__
if setup['stdin']:
files = [s.strip() for s in sys.stdin.readlines()]
else:
files = None
- # load plugins
- serverplugins = {}
- serverlessplugins = {}
- for plugin_name in allplugins:
+ # load plugins specified in the config first
+ allplugins = dict()
+ for plugin in plugin_list:
try:
- mod = getattr(__import__("Bcfg2.Server.Lint.%s" %
- (plugin_name)).Server.Lint, plugin_name)
+ allplugins[plugin] = load_plugin("Bcfg2.Server.Lint." + plugin)
except ImportError:
- try:
- mod = __import__(plugin_name)
- except Exception:
- err = sys.exc_info()[1]
- logger.error("Failed to load plugin %s: %s" % (plugin_name,
- err))
- raise SystemExit(1)
- plugin = getattr(mod, plugin_name)
+ pass
+
+ # load lint plugins bundled with bcfg2-server plugins
+ for plugin in setup['plugins']:
+ try:
+ allplugins[plugin] = load_plugin("Bcfg2.Server.Plugins." + plugin,
+ obj_name=plugin + "Lint")
+ except (ImportError, AttributeError):
+ pass
+
+ serverplugins = dict()
+ serverlessplugins = dict()
+ for plugin_name, plugin in allplugins.items():
if [c for c in inspect.getmro(plugin)
if c == Bcfg2.Server.Lint.ServerPlugin]:
serverplugins[plugin_name] = plugin
diff --git a/src/sbin/bcfg2-reports b/src/sbin/bcfg2-reports
index 1f101b9a7..cb553c0ba 100755
--- a/src/sbin/bcfg2-reports
+++ b/src/sbin/bcfg2-reports
@@ -3,6 +3,9 @@
import os
import sys
+import datetime
+from optparse import OptionParser, OptionGroup, make_option
+from Bcfg2.Bcfg2Py3k import ConfigParser
try:
import Bcfg2.Server.Reports.settings
@@ -20,376 +23,277 @@ sys.path.pop()
# Set DJANGO_SETTINGS_MODULE appropriately.
os.environ['DJANGO_SETTINGS_MODULE'] = '%s.settings' % project_name
-from Bcfg2.Server.Reports.reports.models import Client
-import getopt
-import datetime
-import fileinput
-
-usage = """Usage: bcfg2-reports [option] ...
-
-Options and arguments (and corresponding environment variables):
--a : shows all hosts, including expired hosts
--b NAME : single-host mode - shows bad entries from the
- current interaction of NAME
--c : shows only clean hosts
--d : shows only dirty hosts
--e NAME : single-host mode - shows extra entries from the
- current interaction of NAME
--h : shows help and usage info about bcfg2-reports
--m NAME : single-host mode - shows modified entries from the
- current interaction of NAME
--s NAME : single-host mode - shows bad, modified, and extra
- entries from the current interaction of NAME
--t NAME : single-host mode - shows total number of managed and
- good entries from the current interaction of NAME
--x NAME : toggles expired/unexpired state of NAME
---badentry=KIND,NAME : shows only hosts whose current interaction has bad
- entries in of KIND kind and NAME name; if a single
- argument ARG1 is given, then KIND,NAME pairs will be
- read from a file of name ARG1
---modifiedentry=KIND,NAME : shows only hosts whose current interaction has
- modified entries in of KIND kind and NAME name; if a
- single argument ARG1 is given, then KIND,NAME pairs
- will be read from a file of name ARG1
---extraentry=KIND,NAME : shows only hosts whose current interaction has extra
- entries in of KIND kind and NAME name; if a single
- argument ARG1 is given, then KIND,NAME pairs will be
- read from a file of name ARG1
---fields=ARG1,ARG2,... : only displays the fields ARG1,ARG2,...
- (name,time,state)
---sort=ARG1,ARG2,... : sorts output on ARG1,ARG2,... (name,time,state)
---stale : shows hosts which haven't run in the last 24 hours
-"""
-
-def timecompare(client1, client2):
- """Compares two clients by their timestamps."""
- return cmp(client1.current_interaction.timestamp, \
- client2.current_interaction.timestamp)
-
-def namecompare(client1, client2):
- """Compares two clients by their names."""
- return cmp(client1.name, client2.name)
-
-def statecompare(client1, client2):
- """Compares two clients by their states."""
- clean1 = client1.current_interaction.isclean()
- clean2 = client2.current_interaction.isclean()
-
- if clean1 and not clean2:
- return -1
- elif clean2 and not clean1:
- return 1
- else:
- return 0
-
-def totalcompare(client1, client2):
- """Compares two clients by their total entry counts."""
- return cmp(client2.current_interaction.totalcount, \
- client1.current_interaction.totalcount)
-
-def goodcompare(client1, client2):
- """Compares two clients by their good entry counts."""
- return cmp(client2.current_interaction.goodcount, \
- client1.current_interaction.goodcount)
+from Bcfg2.Server.Reports.reports.models import (Client, Entries_interactions,
+ Entries, TYPE_CHOICES)
-def badcompare(client1, client2):
- """Compares two clients by their bad entry counts."""
- return cmp(client2.current_interaction.totalcount - \
- client2.current_interaction.goodcount, \
- client1.current_interaction.totalcount - \
- client1.current_interaction.goodcount)
+def hosts_by_entry_type(clients, etype, entryspec):
+ result = []
+ for entry in entryspec:
+ for client in clients:
+ items = getattr(client.current_interaction, etype)()
+ for item in items:
+ if (item.entry.kind == entry[0] and
+ item.entry.name == entry[1]):
+ result.append(client)
+ return result
-def crit_compare(criterion, client1, client2):
- """Compares two clients by the criteria provided in criterion."""
- for crit in criterion:
- comp = 0
- if crit == 'name':
- comp = namecompare(client1, client2)
- elif crit == 'state':
- comp = statecompare(client1, client2)
- elif crit == 'time':
- comp = timecompare(client1, client2)
- elif crit == 'total':
- comp = totalcompare(client1, client2)
- elif crit == 'good':
- comp = goodcompare(client1, client2)
- elif crit == 'bad':
- comp = badcompare(client1, client2)
-
- if comp != 0:
- return comp
-
- return 0
-
-def print_fields(fields, cli, max_name, entrydict):
+def print_fields(fields, client, fmt, extra=None):
"""
- Prints the fields specified in fields of cli, max_name
+ Prints the fields specified in fields of client, max_name
specifies the column width of the name column.
"""
- fmt = ''
- for field in fields:
- if field == 'name':
- fmt += ("%%-%ds " % (max_name))
- else:
- fmt += "%s "
fdata = []
+ if extra is None:
+ extra = dict()
for field in fields:
if field == 'time':
- fdata.append(str(cli.current_interaction.timestamp))
+ fdata.append(str(client.current_interaction.timestamp))
elif field == 'state':
- if cli.current_interaction.isclean():
+ if client.current_interaction.isclean():
fdata.append("clean")
else:
fdata.append("dirty")
elif field == 'total':
- fdata.append("%5d" % cli.current_interaction.totalcount)
+ fdata.append(client.current_interaction.totalcount)
elif field == 'good':
- fdata.append("%5d" % cli.current_interaction.goodcount)
+ fdata.append(client.current_interaction.goodcount)
+ elif field == 'modified':
+ fdata.append(client.current_interaction.modified_entry_count())
+ elif field == 'extra':
+ fdata.append(client.current_interaction.extra_entry_count())
elif field == 'bad':
- fdata.append("%5d" % cli.current_interaction.totalcount \
- - cli.current_interaction.goodcount)
+ fdata.append((client.current_interaction.badcount()))
else:
try:
- fdata.append(getattr(cli, field))
+ fdata.append(getattr(client, field))
except:
- fdata.append("N/A")
+ fdata.append(extra.get(field, "N/A"))
- display = fmt % tuple(fdata)
- if len(entrydict) > 0:
- display += " "
- display += str(entrydict[cli])
- print(display)
+ print(fmt % tuple(fdata))
-def print_entry(item, max_name):
- fmt = ("%%-%ds " % (max_name))
- fdata = item.entry.kind + ":" + item.entry.name
- display = fmt % (fdata)
- print(display)
-
-fields = ""
-sort = ""
-badentry = ""
-modifiedentry = ""
-extraentry = ""
-expire = ""
-singlehost = ""
+def print_entries(interaction, etype):
+ items = getattr(interaction, etype)()
+ for item in items:
+ print("%-70s %s" % (item.entry.kind + ":" + item.entry.name, etype))
-c_list = Client.objects.all()
+def main():
+ parser = OptionParser(usage="%prog [options] <mode> [arg]")
-result = list()
-entrydict = dict()
+ # single host modes
+ multimodes = []
+ singlemodes = []
+ multimodes.append(make_option("-b", "--bad", action="store_true",
+ default=False,
+ help="Show bad entries from HOST"))
+ multimodes.append(make_option("-e", "--extra", action="store_true",
+ default=False,
+ help="Show extra entries from HOST"))
+ multimodes.append(make_option("-m", "--modified", action="store_true",
+ default=False,
+ help="Show modified entries from HOST"))
+ multimodes.append(make_option("-s", "--show", action="store_true",
+ default=False,
+ help="Equivalent to --bad --extra --modified"))
+ singlemodes.append(make_option("-t", "--total", action="store_true",
+ default=False,
+ help="Show total number of managed and good "
+ "entries from HOST"))
+ singlemodes.append(make_option("-x", "--expire", action="store_true",
+ default=False,
+ help="Toggle expired/unexpired state of "
+ "HOST"))
+ hostmodes = \
+ OptionGroup(parser, "Single-Host Modes",
+ "The following mode flags require a single HOST argument")
+ hostmodes.add_options(multimodes)
+ hostmodes.add_options(singlemodes)
+ parser.add_option_group(hostmodes)
-args = sys.argv[1:]
-try:
- opts, pargs = getopt.getopt(args, 'ab:cde:hm:s:t:x:',
- ['stale',
- 'sort=',
- 'fields=',
- 'badentry=',
- 'modifiedentry=',
- 'extraentry='])
-except getopt.GetoptError:
- msg = sys.exc_info()[1]
- print(msg)
- print(usage)
- sys.exit(2)
+ # all host modes
+ allhostmodes = OptionGroup(parser, "Host Selection Modes",
+ "The following mode flags require no arguments")
+ allhostmodes.add_option("-a", "--all", action="store_true", default=False,
+ help="Show all hosts, including expired hosts")
+ allhostmodes.add_option("-c", "--clean", action="store_true", default=False,
+ help="Show only clean hosts")
+ allhostmodes.add_option("-d", "--dirty", action="store_true", default=False,
+ help="Show only dirty hosts")
+ allhostmodes.add_option("--stale", action="store_true", default=False,
+ help="Show hosts that haven't run in the last 24 "
+ "hours")
+ parser.add_option_group(allhostmodes)
+
+ # entry modes
+ entrymodes = \
+ OptionGroup(parser, "Entry Modes",
+ "The following mode flags require either any number of "
+ "TYPE:NAME arguments describing entries, or the --file "
+ "option")
+ entrymodes.add_option("--badentry", action="store_true", default=False,
+ help="Show hosts that have bad entries that match "
+ "the argument")
+ entrymodes.add_option("--modifiedentry", action="store_true", default=False,
+ help="Show hosts that have modified entries that "
+ "match the argument")
+ entrymodes.add_option("--extraentry", action="store_true", default=False,
+ help="Show hosts that have extra entries that match "
+ "the argument")
+ entrymodes.add_option("--entrystatus", action="store_true", default=False,
+ help="Show the status of the named entry on all "
+ "hosts. Only supports a single entry.")
+ parser.add_option_group(entrymodes)
+
+ # entry options
+ entryopts = OptionGroup(parser, "Entry Options",
+ "Options that can be used with entry modes")
+ entryopts.add_option("--fields", metavar="FIELD,FIELD,...",
+ help="Only display the listed fields",
+ default='name,time,state')
+ entryopts.add_option("--file", metavar="FILE",
+ help="Read TYPE:NAME pairs from the specified file "
+ "instead of the command line")
+ parser.add_option_group(entryopts)
-for option in opts:
- if len(option) > 0:
- if option[0] == '--fields':
- fields = option[1]
- if option[0] == '--sort':
- sort = option[1]
- if option[0] == '--badentry':
- badentry = option[1]
- if option[0] == '--modifiedentry':
- modifiedentry = option[1]
- if option[0] == '--extraentry':
- extraentry = option[1]
- if option[0] == '-x':
- expire = option[1]
- if option[0] == '-s' or \
- option[0] == '-t' or \
- option[0] == '-b' or \
- option[0] == '-m' or \
- option[0] == '-e':
- singlehost = option[1]
+ options, args = parser.parse_args()
-if expire != "":
- for c_inst in c_list:
- if expire == c_inst.name:
- if c_inst.expiration == None:
- c_inst.expiration = datetime.datetime.now()
+ # make sure we've specified exactly one mode
+ mode_family = None
+ mode = None
+ for opt in allhostmodes.option_list + entrymodes.option_list + \
+ singlemodes:
+ if getattr(options, opt.dest):
+ if mode is not None:
+ parser.error("Only one mode can be specified; found %s and %s" %
+ (mode.get_opt_string(), opt.get_opt_string()))
+ mode = opt
+ mode_family = parser.get_option_group(opt.get_opt_string())
+
+ # you can specify more than one of --bad, --extra, --modified, --show, so
+ # consider single-host options separately
+ if not mode_family:
+ for opt in multimodes:
+ if getattr(options, opt.dest):
+ mode_family = parser.get_option_group(opt.get_opt_string())
+ break
+
+ if not mode_family:
+ parser.error("You must specify a mode")
+
+ if mode_family == hostmodes:
+ try:
+ cname = args.pop()
+ client = Client.objects.select_related().get(name=cname)
+ except IndexError:
+ parser.error("%s require a single HOST argument" % hostmodes.title)
+ except Client.DoesNotExist:
+ print("No such host: %s" % cname)
+ return 2
+
+ if options.expire:
+ if client.expiration == None:
+ client.expiration = datetime.datetime.now()
print("Host expired.")
else:
- c_inst.expiration = None
+ client.expiration = None
print("Host un-expired.")
- c_inst.save()
+ client.save()
+ elif options.total:
+ managed = client.current_interaction.totalcount
+ good = client.current_interaction.goodcount
+ print("Total managed entries: %d (good: %d)" % (managed, good))
+ elif mode_family == hostmodes:
+ if options.bad or options.show:
+ print_entries(client.current_interaction, "bad")
-elif '-h' in args:
- print(usage)
-elif singlehost != "":
- for c_inst in c_list:
- if singlehost == c_inst.name:
- if '-t' in args:
- managed = c_inst.current_interaction.totalcount
- good = c_inst.current_interaction.goodcount
- print("Total managed entries: %d (good: %d)" % (managed, good))
- baditems = c_inst.current_interaction.bad()
- if len(baditems) > 0 and ('-b' in args or '-s' in args):
- print("Bad Entries:")
- max_name = -1
- for item in baditems:
- if len(item.entry.name) > max_name:
- max_name = len(item.entry.name)
- for item in baditems:
- print_entry(item, max_name)
- modifieditems = c_inst.current_interaction.modified()
- if len(modifieditems) > 0 and ('-m' in args or '-s' in args):
- print "Modified Entries:"
- max_name = -1
- for item in modifieditems:
- if len(item.entry.name) > max_name:
- max_name = len(item.entry.name)
- for item in modifieditems:
- print_entry(item, max_name)
- extraitems = c_inst.current_interaction.extra()
- if len(extraitems) > 0 and ('-e' in args or '-s' in args):
- print("Extra Entries:")
- max_name = -1
- for item in extraitems:
- if len(item.entry.name) > max_name:
- max_name = len(item.entry.name)
- for item in extraitems:
- print_entry(item, max_name)
-
+ if options.modified or options.show:
+ print_entries(client.current_interaction, "modified")
-else:
- if fields == "":
- fields = ['name', 'time', 'state']
+ if options.extra or options.show:
+ print_entries(client.current_interaction, "extra")
else:
- fields = fields.split(',')
-
- if sort != "":
- sort = sort.split(',')
+ clients = Client.objects.exclude(current_interaction__isnull=True)
+ result = list()
+ edata = dict()
+ fields = options.fields.split(',')
- if badentry != "":
- badentry = badentry.split(',')
+ if mode_family == allhostmodes:
+ if args:
+ print("%s do not take any arguments, ignoring" %
+ allhostmodes.title)
- if modifiedentry != "":
- modifiedentry = modifiedentry.split(',')
+ for client in clients:
+ interaction = client.current_interaction
+ if (options.all or
+ (options.stale and interaction.isstale()) or
+ (options.clean and interaction.isclean()) or
+ (options.dirty and not interaction.isclean())):
+ result.append(client)
+ else:
+ # entry query modes
+ if options.file:
+ try:
+ entries = [l.strip().split(":")
+ for l in open(options.file)]
+ except IOError, err:
+ print("Cannot read entries from %s: %s" % (options.file,
+ err))
+ return 2
+ elif args:
+ entries = [a.split(":") for a in args]
+ else:
+ parser.error("%s require either a list of entries on the "
+ "command line or the --file options" %
+ mode_family.title)
+
+ if options.badentry:
+ result = hosts_by_entry_type(clients, "bad", entries)
+ elif options.modifiedentry:
+ result = hosts_by_entry_type(clients, "modified", entries)
+ elif options.extraentry:
+ result = hosts_by_entry_type(clients, "extra", entries)
+ elif options.entrystatus:
+ if 'state' in fields:
+ fields.remove('state')
+ fields.append("entry state")
+ try:
+ entry_obj = Entries.objects.get(
+ kind=entries[0][0],
+ name=entries[0][1])
+ except Entries.DoesNotExist:
+ print("No entry %s found" % ":".join(entries[0]))
+ return 2
- if extraentry != "":
- extraentry = extraentry.split(',')
-
- # stale hosts
- if '--stale' in args:
- for c_inst in c_list:
- if c_inst.current_interaction.isstale():
- result.append(c_inst)
- # clean hosts
- elif '-c' in args:
- for c_inst in c_list:
- if c_inst.current_interaction.isclean():
- result.append(c_inst)
- # dirty hosts
- elif '-d' in args:
- for c_inst in c_list:
- if not c_inst.current_interaction.isclean():
- result.append(c_inst)
+ for client in clients:
+ try:
+ entry = \
+ Entries_interactions.objects.select_related().get(
+ interaction=client.current_interaction,
+ entry=entry_obj)
+ edata[client] = \
+ {"entry state":dict(TYPE_CHOICES)[entry.type],
+ "reason":entry.reason}
+ result.append(client)
+ except Entries_interactions.DoesNotExist:
+ pass
- elif badentry != "":
- if len(badentry) == 1:
- fileread = fileinput.input(badentry[0])
- try:
- for line in fileread:
- badentry = line.strip().split(',')
- for c_inst in c_list:
- baditems = c_inst.current_interaction.bad()
- for item in baditems:
- if item.entry.name == badentry[1] and item.entry.kind == badentry[0]:
- result.append(c_inst)
- if c_inst in entrydict:
- entrydict.get(c_inst).append(badentry[1])
- else:
- entrydict[c_inst] = [badentry[1]]
- break
- except IOError:
- e = sys.exc_info()[1]
- print("Cannot read %s: %s" % (e.filename, e.strerror))
- else:
- for c_inst in c_list:
- baditems = c_inst.current_interaction.bad()
- for item in baditems:
- if item.entry.name == badentry[1] and item.entry.kind == badentry[0]:
- result.append(c_inst)
- break
- elif modifiedentry != "":
- if len(modifiedentry) == 1:
- fileread = fileinput.input(modifiedentry[0])
- try:
- for line in fileread:
- modifiedentry = line.strip().split(',')
- for c_inst in c_list:
- modifieditems = c_inst.current_interaction.modified()
- for item in modifieditems:
- if item.entry.name == modifiedentry[1] and item.entry.kind == modifiedentry[0]:
- result.append(c_inst)
- if c_inst in entrydict:
- entrydict.get(c_inst).append(modifiedentry[1])
- else:
- entrydict[c_inst] = [modifiedentry[1]]
- break
- except IOError:
- e = sys.exc_info()[1]
- print("Cannot read %s: %s" % (e.filename, e.strerror))
- else:
- for c_inst in c_list:
- modifieditems = c_inst.current_interaction.modified()
- for item in modifieditems:
- if item.entry.name == modifiedentry[1] and item.entry.kind == modifiedentry[0]:
- result.append(c_inst)
- break
- elif extraentry != "":
- if len(extraentry) == 1:
- fileread = fileinput.input(extraentry[0])
- try:
- for line in fileread:
- extraentry = line.strip().split(',')
- for c_inst in c_list:
- extraitems = c_inst.current_interaction.extra()
- for item in extraitems:
- if item.entry.name == extraentry[1] and item.entry.kind == extraentry[0]:
- result.append(c_inst)
- if c_inst in entrydict:
- entrydict.get(c_inst).append(extraentry[1])
- else:
- entrydict[c_inst] = [extraentry[1]]
- break
- except IOError:
- e = sys.exc_info()[1]
- print("Cannot read %s: %s" % (e.filename, e.strerror))
- else:
- for c_inst in c_list:
- extraitems = c_inst.current_interaction.extra()
- for item in extraitems:
- if item.entry.name == extraentry[1] and item.entry.kind == extraentry[0]:
- result.append(c_inst)
- break
- else:
- for c_inst in c_list:
- result.append(c_inst)
- max_name = -1
- if 'name' in fields:
- for c_inst in result:
- if len(c_inst.name) > max_name:
- max_name = len(c_inst.name)
+ if 'name' not in fields:
+ fields.insert(0, "name")
+ max_name = max(len(c.name) for c in result)
+ ffmt = []
+ for field in fields:
+ if field == "name":
+ ffmt.append("%%-%ds" % max_name)
+ elif field == "time":
+ ffmt.append("%-19s")
+ else:
+ ffmt.append("%%-%ds" % len(field))
+ fmt = " ".join(ffmt)
+ print(fmt % tuple(f.title() for f in fields))
+ for client in result:
+ if not client.expiration:
+ print_fields(fields, client, fmt,
+ extra=edata.get(client, None))
- if sort != "":
- result.sort(lambda x, y: crit_compare(sort, x, y))
-
- if fields != "":
- for c_inst in result:
- if '-a' in args or c_inst.expiration == None:
- print_fields(fields, c_inst, max_name, entrydict)
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/src/sbin/bcfg2-server b/src/sbin/bcfg2-server
index 757172464..d03edc93e 100755
--- a/src/sbin/bcfg2-server
+++ b/src/sbin/bcfg2-server
@@ -15,30 +15,11 @@ from Bcfg2.Server.Core import CoreInitError
logger = logging.getLogger('bcfg2-server')
if __name__ == '__main__':
- OPTINFO = {
- 'configfile': Bcfg2.Options.CFILE,
- 'daemon' : Bcfg2.Options.DAEMON,
- 'debug' : Bcfg2.Options.DEBUG,
- 'help' : Bcfg2.Options.HELP,
- 'verbose' : Bcfg2.Options.VERBOSE,
- 'to_file' : Bcfg2.Options.LOGGING_FILE_PATH,
- 'repo' : Bcfg2.Options.SERVER_REPOSITORY,
- 'plugins' : Bcfg2.Options.SERVER_PLUGINS,
- 'password' : Bcfg2.Options.SERVER_PASSWORD,
- 'fm' : Bcfg2.Options.SERVER_FILEMONITOR,
- 'key' : Bcfg2.Options.SERVER_KEY,
- 'cert' : Bcfg2.Options.SERVER_CERT,
- 'ca' : Bcfg2.Options.SERVER_CA,
- 'listen_all': Bcfg2.Options.SERVER_LISTEN_ALL,
- 'location' : Bcfg2.Options.SERVER_LOCATION,
- 'passwd' : Bcfg2.Options.SERVER_PASSWORD,
- 'static' : Bcfg2.Options.SERVER_STATIC,
- 'encoding' : Bcfg2.Options.ENCODING,
- 'filelog' : Bcfg2.Options.LOGGING_FILE_PATH,
- 'protocol' : Bcfg2.Options.SERVER_PROTOCOL,
- }
-
- setup = Bcfg2.Options.OptionParser(OPTINFO)
+ optinfo = dict()
+ optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
+ optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
+ optinfo.update(Bcfg2.Options.DAEMON_COMMON_OPTIONS)
+ setup = Bcfg2.Options.OptionParser(optinfo)
setup.parse(sys.argv[1:])
try:
# check whether the specified bcfg2.conf exists
@@ -51,7 +32,7 @@ if __name__ == '__main__':
daemon=setup['daemon'],
pidfile_name=setup['daemon'],
protocol=setup['protocol'],
- to_file=setup['to_file'],
+ to_file=setup['logging'],
cfile=setup['configfile'],
register=False,
cls_kwargs={'repo':setup['repo'],
@@ -59,7 +40,7 @@ if __name__ == '__main__':
'password':setup['password'],
'encoding':setup['encoding'],
'ca':setup['ca'],
- 'filemonitor':setup['fm'],
+ 'filemonitor':setup['filemonitor'],
'start_fam_thread':True,
'setup':setup},
keyfile=setup['key'],
diff --git a/src/sbin/bcfg2-test b/src/sbin/bcfg2-test
index e3cfd27cc..653c24124 100755
--- a/src/sbin/bcfg2-test
+++ b/src/sbin/bcfg2-test
@@ -61,18 +61,11 @@ class ClientTest(TestCase):
id = __str__
def main():
- optinfo = {
- 'configfile': Bcfg2.Options.CFILE,
- 'help': Bcfg2.Options.HELP,
- 'encoding': Bcfg2.Options.ENCODING,
- 'repo': Bcfg2.Options.SERVER_REPOSITORY,
- 'plugins': Bcfg2.Options.SERVER_PLUGINS,
- 'password': Bcfg2.Options.SERVER_PASSWORD,
- 'verbose': Bcfg2.Options.VERBOSE,
- 'noseopts': Bcfg2.Options.TEST_NOSEOPTS,
- 'ignore': Bcfg2.Options.TEST_IGNORE,
- 'validate': Bcfg2.Options.CFG_VALIDATION,
- }
+ optinfo = dict(noseopts=Bcfg2.Options.TEST_NOSEOPTS,
+ test_ignore=Bcfg2.Options.TEST_IGNORE,
+ validate=Bcfg2.Options.CFG_VALIDATION)
+ optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
+ optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
setup.hm = \
"bcfg2-test [options] [client] [client] [...]\nOptions:\n %s" % \
@@ -87,12 +80,12 @@ def main():
setup['plugins'],
setup['password'],
setup['encoding'],
- filemonitor='pseudo',
+ filemonitor=setup['filemonitor'],
setup=setup
)
ignore = dict()
- for entry in setup['ignore']:
+ for entry in setup['test_ignore']:
tag, name = entry.split(":")
try:
ignore[tag].append(name)
diff --git a/testsuite/Testlib/TestOptions.py b/testsuite/Testlib/TestOptions.py
index f5833a54a..066dfa08c 100644
--- a/testsuite/Testlib/TestOptions.py
+++ b/testsuite/Testlib/TestOptions.py
@@ -4,6 +4,7 @@ import unittest
from mock import Mock, patch
import Bcfg2.Options
+
class TestOption(unittest.TestCase):
def test__init(self):
self.assertRaises(Bcfg2.Options.OptionFailure,
@@ -19,18 +20,20 @@ class TestOption(unittest.TestCase):
Bcfg2.Options.Option,
'foo', False, cmd='-foo', long_arg=True)
- @patch('ConfigParser.ConfigParser')
+ @patch('Bcfg2.Options.DefaultConfigParser')
@patch('__builtin__.open')
- def test_getCFP(self, mock_open, mock_cp):
+ def test_get(self, mock_open, mock_cp):
mock_cp.return_value = Mock()
o = Bcfg2.Options.Option('foo', False, cmd='-f')
- self.assertFalse(o._Option__cfp)
- o.getCFP()
+ self.assertFalse(o.cf)
+ c = Bcfg2.Options.DefaultConfigParser()
+ c.get('foo', False, cmd='-f')
mock_cp.assert_any_call()
mock_open.assert_any_call(Bcfg2.Options.DEFAULT_CONFIG_LOCATION)
- self.assertTrue(mock_cp.return_value.readfp.called)
-
- @patch('Bcfg2.Options.Option.cfp')
+ print(mock_cp.return_value.get.called)
+ self.assertTrue(mock_cp.return_value.get.called)
+
+ @patch('Bcfg2.Options.DefaultConfigParser')
def test_parse(self, mock_cfp):
cf = ('communication', 'password')
o = Bcfg2.Options.Option('foo', 'test4', cmd='-F', env='TEST2',
@@ -57,7 +60,7 @@ class TestOption(unittest.TestCase):
def test_cook(self):
# check that default value isn't cooked
- o1 = Bcfg2.Options.Option('foo', 'test4', cook=Bcfg2.Options.bool_cook)
+ o1 = Bcfg2.Options.Option('foo', 'test4', cook=Bcfg2.Options.get_bool)
o1.parse([], [])
assert o1.value == 'test4'
o2 = Bcfg2.Options.Option('foo', False, cmd='-F')
@@ -112,13 +115,13 @@ class TestOptionParser(unittest.TestCase):
('baz', Bcfg2.Options.Option('foo', 'test1', cmd='-H',
odesc='1'))]
oset1 = Bcfg2.Options.OptionParser(opts)
- self.assertEqual(Bcfg2.Options.Option.cfpath,
+ self.assertEqual(oset1.cfile,
Bcfg2.Options.DEFAULT_CONFIG_LOCATION)
sys.argv = ['foo', '-C', '/usr/local/etc/bcfg2.conf']
oset2 = Bcfg2.Options.OptionParser(opts)
- self.assertEqual(Bcfg2.Options.Option.cfpath,
+ self.assertEqual(oset2.cfile,
'/usr/local/etc/bcfg2.conf')
sys.argv = []
oset3 = Bcfg2.Options.OptionParser(opts)
- self.assertEqual(Bcfg2.Options.Option.cfpath,
+ self.assertEqual(oset3.cfile,
Bcfg2.Options.DEFAULT_CONFIG_LOCATION)
diff --git a/tools/manpagegen/bcfg2-admin.8.ronn b/tools/manpagegen/bcfg2-admin.8.ronn
new file mode 100644
index 000000000..d517bdabc
--- /dev/null
+++ b/tools/manpagegen/bcfg2-admin.8.ronn
@@ -0,0 +1,180 @@
+bcfg2-admin(8) -- Perform repository administration tasks
+=========================================================
+
+## SYNOPSIS
+
+`bcfg2-admin` [-C <configfile>] <mode> [<mode args>] [<mode options>]
+
+## DESCRIPTION
+
+`bcfg2-admin` is used to perform Bcfg2 repository administration
+
+## OPTIONS
+
+ * `-C` <configfile>:
+ Specify alternate bcfg2.conf location
+
+## MODES
+
+ * `init`:
+ Initialize a new repository (interactive).
+
+ * `backup`:
+ Create an archive of the entire Bcfg2 repository.
+
+ * `bundle` <action>:
+ Display details about the available bundles (See [`BUNDLE
+ OPTIONS`](###BUNDLE OPTIONS) below).
+
+ * `client` <action> <client> [attribute=value]:
+ Add, edit, or remove clients entries in metadata (See [`CLIENT
+ OPTIONS`](###CLIENT OPTIONS) below).
+
+ * `query` [g=group] [p=profile] [-f output-file] [-n] [-c]:
+ Search for clients based on group or profile (See [`QUERY
+ OPTIONS`](###QUERY OPTIONS) below).
+
+ * `compare` <old> <new>:
+ Compare two client configurations. Can be used to verify consistent
+ behavior between releases. Determine differences between files or
+ directories (See [`COMPARE OPTIONS`](###COMPARE OPTIONS) below).
+
+ * `minestruct` <client> [-f xml-file] [-g groups]:
+ Build structure entries based on client statistics extra entries
+ (See [`MINESTRUCT OPTIONS`](###MINESTRUCT OPTIONS) below).
+
+ * `pull` <client> <entry-type> <entry-name>:
+ Install configuration information into repo based on client bad
+ entries (See [`PULL OPTIONS`](###PULL OPTIONS) below).
+
+ * `reports` [init|load_stats|purge|scrub|update]:
+ Interact with the dynamic reporting system (See [`REPORTS
+ OPTIONS`](###REPORTS OPTIONS) below).
+
+ * `snapshots` [init|dump|query|reports]:
+ Interact with the Snapshots database (See [`SNAPSHOTS
+ OPTIONS`](###SNAPSHOTS OPTIONS) below).
+
+ * `tidy`:
+ Remove unused files from repository.
+
+ * `viz` [-H] [-b] [-k] [-o png-file]:
+ Create a graphviz diagram of client, group and bundle information
+ (See [`VIZ OPTIONS`](###VIZ OPTIONS) below).
+
+### BUNDLE OPTIONS
+
+ * `mode`:
+ List all available xml bundles ’list-xml’ or for all available
+ genshi bundles ’list-genshi’. ’show’ provides an interactive
+ dialog to get details about the available bundles.
+
+### CLIENT OPTIONS
+
+ * `mode`:
+ Add a client ’add’, delete a client ’del’, or ’list’
+ all client entries.
+
+ * `client`:
+ Specify the client’s name.
+
+ * `attribute=value`:
+ Set attribute values when adding a new client. Allowed attributes
+ are ’profile’, ’uuid’, ’password’, ’location’,
+ ’secure’, and ’address’.
+
+### QUERY OPTIONS
+
+ * `g=group`:
+ Specify a group to search within.
+
+ * `p=profile`:
+ Specify a profile to search within.
+
+ * `-f` <output file>:
+ Write the results of the query to a file.
+
+ * `-n`:
+ Print the results, one on each line.
+
+ * `-c`:
+ Print the results, separated by commas.
+
+### COMPARE OPTIONS
+
+ * `old`:
+ Specify the location of the old configuration file.
+
+ * `new`:
+ Specify the location of the new configuration file.
+
+### MINESTRUCT OPTIONS
+
+ * `client`:
+ Client whose metadata is to be searched for extra entries.
+
+ * `-g` <groups>:
+ Hierarchy of groups in which to place the extra entries in.
+
+ * `-f` <xml output file>:
+ Specify the xml file in which to write the extra entries.
+
+### PULL OPTIONS
+
+ * `client`:
+ Specify the name of the client to search for.
+
+ * `entry type`:
+ Specify the type of the entry to pull.
+
+ * `entry name`:
+ Specify the name of the entry to pull.
+
+### REPORTS OPTIONS
+
+ * `init`:
+ Initialize the database.
+
+ * `load_stats` [-s] [-c] [-03]:
+ Load statistics data.
+
+ * `purge` [--client [n]] [--days [n]] [--expired]:
+ Purge historic and expired data.
+
+ * `scrub`:
+ Scrub the database for duplicate reasons and orphaned entries.
+
+ * `update`:
+ Apply any updates to the reporting database.
+
+### SNAPSHOTS OPTIONS
+
+ * `init`:
+ Initialize the snapshots database.
+
+ * `query`:
+ Query the snapshots database.
+
+ * `dump`:
+ Dump some of the contents of the snapshots database.
+
+ * `reports` [-a] [-b] [-e] [--date=<MM-DD-YYYY>]:
+ Generate reports for clients in the snapshots database.
+
+### VIZ OPTIONS
+
+ * `-H`:
+ Include hosts in diagram.
+
+ * `-b`:
+ Include bundles in diagram.
+
+ * `-o` <output file>:
+ Write to outfile file instead of stdout.
+
+ * `-k`:
+ Add a shape/color key.
+
+## SEE ALSO
+
+bcfg2-info(8), bcfg2-server(8)
diff --git a/tools/manpagegen/bcfg2-build-reports.8.ronn b/tools/manpagegen/bcfg2-build-reports.8.ronn
new file mode 100644
index 000000000..43fca5755
--- /dev/null
+++ b/tools/manpagegen/bcfg2-build-reports.8.ronn
@@ -0,0 +1,34 @@
+bcfg2-build-reports(8) -- Generate state reports for Bcfg2 clients
+==================================================================
+
+## SYNOPSIS
+
+`bcfg2-build-reports` [<-A>] [<-c>] [<-s>] [<-N>]
+
+## DESCRIPTION
+
+`bcfg2-build-reports` is used to build all client state reports. See the
+Bcfg2 manual for report setup information.
+
+## OPTIONS
+
+ * `-A`:
+ Displays all data.
+
+ * `-c` <configuration file>:
+ Specify an alternate report configuration path. The default is
+ repo/etc/reports-configuration.xml.
+
+ * `-h`:
+ Produce a help message.
+
+ * `-s` <statistics path>:
+ Use an alternative path for the statistics file. The default is
+ repo/etc/statistics.xml.
+
+ * `-N`:
+ No pinging.
+
+## SEE ALSO
+
+bcfg2(1), bcfg2-server(8)
diff --git a/tools/manpagegen/bcfg2-crypt.8.ronn b/tools/manpagegen/bcfg2-crypt.8.ronn
new file mode 100644
index 000000000..a164d47f1
--- /dev/null
+++ b/tools/manpagegen/bcfg2-crypt.8.ronn
@@ -0,0 +1,108 @@
+bcfg2-crypt(8) -- Bcfg2 encryption and decryption utility
+=========================================================
+
+## SYNOPSIS
+
+`bcfg2-crypt` [<-C configfile>] [--decrypt|--encrypt] [--cfg|--properties] [--remove] [--xpath <xpath>] [-p <passphrase-or-name>] [-v] <filename> [<filename>...]
+
+## DESCRIPTION
+
+`bcfg2-crypt` performs encryption and decryption of Cfg and Properties
+files. It's often sufficient to run `bcfg2-crypt` with only the name
+of the file you wish to encrypt or decrypt; it can usually figure out
+what to do.
+
+## OPTIONS
+
+ * `-C` <configfile>:
+ Specify alternate bcfg2.conf location
+
+ * `--decrypt`, `--encrypt`:
+ Specify which operation you'd like to perform. `bcfg2-crypt` can
+ usually determine which is necessary based on the contents of each
+ file.
+
+ * `--cfg`:
+ Tell `bcfg2-crypt` that an XML file should be encrypted in its
+ entirety rather than element-by-element. This is only necessary
+ if the file is an XML file whose name ends with `.xml` and whose
+ top-level tag is `<Properties>`. See [MODES] below for details.
+
+ * `--properties`:
+ Tell `bcfg2-crypt` to process a file as an XML Properties file,
+ and encrypt the text of each element separately. This is
+ necessary if, for example, you've used a different top-level tag
+ than `<Properties>` in your Properties files. See [MODES] below
+ for details.
+
+ * `--remove`:
+ Remove the plaintext file after it has been encrypted. Only
+ meaningful for Cfg files.
+
+ * `--xpath <xpath>`:
+ Encrypt the character content of all elements that match the
+ specified XPath expression. The default is `*[@encrypted]`
+ or `*`; see [MODES] below for more details. Only meaningful for
+ Properties files.
+
+ * `-p <passphrase>`:
+ Specify the name of a passphrase specified in the `[encryption]`
+ section of `bcfg2.conf`. See [SELECTING PASSPHRASE] below for
+ more details.
+
+ * `-v`:
+ Be verbose.
+
+ * `-h`:
+ Display help and exit.
+
+## MODES
+
+`bcfg2-crypt` can encrypt Cfg files or Properties files; they are
+handled very differently.
+
+ * Cfg:
+ When `bcfg2-crypt` is used on a Cfg file, the entire file is
+ encrypted. This is the default behavior on files that are not
+ XML, or that are XML but whose top-level tag is not
+ `<Properties>`. This can be enforced by use of the `--cfg`
+ option.
+
+ * Properties:
+ When `bcfg2-crypt` is used on a Properties file, it encrypts the
+ character content of elements matching the XPath expression given
+ by `--xpath`. By default the expression is `*[@encrypted]`, which
+ matches all elements with an `encrypted` attribute. If you are
+ encrypting a file and that expression doesn't match any elements,
+ then the default is `*`, which matches everything. When
+ `bcfg2-crypt` encrypts the character content of an element, it
+ also adds the `encrypted` attribute, set to the name of the
+ passphrase used to encrypt that element. When it decrypts an
+ element it does not remove `encrypted`, though; this lets you
+ easily and efficiently run `bcfg2-crypt` against a single
+ Properties file to encrypt and decrypt it without needing to
+ specify a long list of options. See the online Bcfg2 docs on
+ Properties files for more information on how this works.
+
+## SELECTING PASSPHRASE
+
+The passphrase used to encrypt or decrypt a file is discovered in the
+following order:
+
+ * First, the passphrase given on the command line using `-p` is
+ used.
+
+ * Next, if exactly one passphrase is specified in `bcfg2.conf`, it
+ will be used.
+
+ * Next, if operating in Properties mode, `bcfg2-crypt` will attempt
+ to read the name of the passphrase from the encrypted elements.
+
+ * Next, if decrypting, all passphrases will be tried sequentially.
+
+ * If no passphrase has been determined at this point, an error is
+ produced and the file being encrypted or decrypted is skipped.
+
+## SEE ALSO
+
+bcfg2-server(8)
diff --git a/tools/manpagegen/bcfg2-info.8.ronn b/tools/manpagegen/bcfg2-info.8.ronn
new file mode 100644
index 000000000..e19149ca8
--- /dev/null
+++ b/tools/manpagegen/bcfg2-info.8.ronn
@@ -0,0 +1,110 @@
+bcfg2-info(8) -- Creates a local version of the Bcfg2 server core for state observation
+=======================================================================================
+
+## SYNOPSIS
+
+`bcfg2-info` [<-C configfile>] [-E <encoding>] [-Q <repository path>]
+[-h] [-p] [-x <password>] [<mode>] [<mode args>] [<mode options>]
+
+## DESCRIPTION
+
+`bcfg2-info` instantiates an instance of the Bcfg2 core for data
+examination and debugging purposes.
+
+## OPTIONS
+
+ * `-C` <configfile>:
+ Specify alternate bcfg2.conf location
+
+ * `-E` <encoding>:
+ Specify the encoding of config files.
+
+ * `-Q` <repository path>:
+ Specify the server repository path.
+
+ * `-d`:
+ Run in debug mode.
+
+ * `-h`:
+ Give a bit of help about the command line arguments and options.
+ After this bcfg2-info exits.
+
+ * `-p`:
+ Specify a profile.
+
+ * `-x` <password>:
+ Set the communication password.
+
+## MODES
+
+
+ * `build` <hostname> <filename>:
+ Build config for hostname, writing to filename.
+
+ * `buildall` <directory>:
+ Build configs for all clients in directory.
+
+ * `buildallfile` <directory> <filename> [<hostnames>]:
+ Build config file for all clients in directory.
+
+ * `buildbundle` <filename> <hostname>:
+ Build bundle for hostname (not written to disk). If filename is a
+ bundle template, it is rendered.
+
+ * `builddir` <hostname> <dirname>:
+ Build config for hostname, writing separate files to dirname.
+
+ * `buildfile` [--altsrc=<altsrc>] <filename> <hostname>:
+ Build config file for hostname (not written to disk).
+
+ * `bundles`:
+ Print out group/bundle information.
+
+ * `clients`:
+ Print out client/profile information.
+
+ * `config`:
+ Print out the configuration of the Bcfg2 server.
+
+ * `debug`:
+ Shell out to native python interpreter.
+
+ * `event_debug`:
+ Display filesystem events as they are processed.
+
+ * `groups`:
+ List groups.
+
+ * `help`:
+ Print the list of available commands.
+
+ * `mappings` [<entry type>] [<entry name>]:
+ Print generator mappings for optional type and name.
+
+ * `packageresolve` <hostname> <package> [<package>...]:
+ Resolve the specified set of packages.
+
+ * `packagesources` <hostname>:
+ Show package sources.
+
+ * `profile` <command> <args>:
+ Profile a single bcfg2-info command.
+
+ * `quit`:
+ Exit bcfg2-info command line.
+
+ * `showentries` <hostname> <type>:
+ Show abstract configuration entries for a given host.
+
+ * `showclient` <client1> <client2>:
+ Show metadata for given hosts.
+
+ * `update`:
+ Process pending file events.
+
+ * `version`:
+ Print version of this tool.
+
+## SEE ALSO
+
+bcfg2(1), bcfg2-server(8)
diff --git a/tools/manpagegen/bcfg2-lint.8.ronn b/tools/manpagegen/bcfg2-lint.8.ronn
new file mode 100644
index 000000000..e089bf2e7
--- /dev/null
+++ b/tools/manpagegen/bcfg2-lint.8.ronn
@@ -0,0 +1,119 @@
+bcfg2-lint(8) -- Check Bcfg2 specification for validity, common mistakes, and style
+===================================================================================
+
+## SYNOPSIS
+
+`bcfg2-lint` [<options>] [<plugin> [<plugin>...]]
+
+## DESCRIPTION
+
+`bcfg2-lint` checks the Bcfg2 specification for schema validity, common
+mistakes, and other criteria. It can be quite helpful in finding typos
+or malformed data.
+
+`bcfg2-lint` exits with a return value of 2 if errors were found, and 3
+if warnings (but no errors) were found. Any other non-0 exit value
+denotes some failure in the script itself.
+
+`bcfg2-lint` is a rewrite of the older bcfg2-repo-validate tool.
+
+## OPTIONS
+
+ * `-C` <configfile>:
+ Specify alternate bcfg2.conf location.
+
+ * `-Q`:
+ Specify the server repository path.
+
+ * `-v`:
+ Be verbose.
+
+ * `--lint-config`:
+ Specify path to bcfg2-lint.conf (default `/etc/bcfg2-lint.conf`).
+
+ * `--stdin`:
+ Rather than operating on all files in the Bcfg2 specification, only
+ validate a list of files supplied on stdin. This mode is
+ particularly useful in pre-commit hooks.
+
+ This makes a few assumptions:
+
+ Metadata files will only be checked if a valid chain of XIncludes
+ can be followed all the way from clients.xml or groups.xml. Since
+ there are multiple formats of metadata stored in Metadata/ (i.e.,
+ clients and groups), there is no way to determine which sort of
+ data a file contains unless there is a valid chain of XIncludes.
+ It may be useful to always specify all metadata files should be
+ checked, even if not all of them have changed.
+
+ Property files will only be validated if both the property file
+ itself and its matching schema are included on stdin.
+
+ * `require-schema`:
+ Require property files to have matching schema files.
+
+## PLUGINS
+
+See `bcfg2-lint.conf`(5) for more information on the configuration of
+the plugins listed below.
+
+ * `Bundles`:
+ Check the specification for several issues with Bundler: bundles
+ referenced in metadata but not found in `Bundler/`; bundles whose
+ *name* attribute does not match the filename; and Genshi template
+ bundles that use the *<Group>* tag (which is not processed in
+ templated bundles).
+
+ * `Comments`:
+ Check the specification for VCS keywords and any comments that are
+ required. By default, this only checks that the *$Id$* keyword is
+ included and expanded in all files. You may specify VCS keywords to
+ check and comments to be required in the config file. (For instance,
+ you might require that every file have a "Maintainer" comment.)
+
+ In XML files, only comments are checked for the keywords and
+ comments required.
+
+ * `Duplicates`:
+ Check for several types of duplicates in the Metadata: duplicate
+ groups; duplicate clients; and multiple default groups.
+
+ * `InfoXML`:
+ Check that certain attributes are specified in `info.xml` files. By
+ default, requires that *owner*, *group*, and *perms* are specified.
+ Can also require that an `info.xml` exists for all Cfg files, and
+ that paranoid mode be enabled for all files.
+
+ * `MergeFiles`:
+ Suggest that similar probes and config files be merged into single
+ probes or TGenshi templates.
+
+ * `Pkgmgr`:
+ Check for duplicate packages specified in Pkgmgr.
+
+ * `RequiredAttrs`:
+ Check that all *Path* and *BoundPath* tags have the attributes that
+ are required by their type (e.g., a path of type symlink must have
+ name and to specified to be valid). This sort of validation is
+ beyond the scope of an XML schema.
+
+ * `Validate`:
+ Validate the Bcfg2 specification against the XML schemas.
+
+ Property files are freeform XML, but if a `.xsd` file with a
+ matching filename is provided, then schema validation will be
+ performed on property files individually as well. For instance, if
+ you have a property file named `ntp.xml` then by placing a schema
+ for that file in `ntp.xsd` schema validation will be performed on
+ `ntp.xml`.
+
+## BUGS
+
+`bcfg2-lint` may not handle some older plugins as well as it handles
+newer ones. For instance, there may be some places where it expects all
+of your configuration files to be handled by Cfg rather than by a mix of
+Cfg and TGenshi or TCheetah.
+
+## SEE ALSO
+
+bcfg2(1), bcfg2-server(8), bcfg2-lint.conf(5)
diff --git a/tools/manpagegen/bcfg2-lint.conf.5.ronn b/tools/manpagegen/bcfg2-lint.conf.5.ronn
new file mode 100644
index 000000000..657ea6e74
--- /dev/null
+++ b/tools/manpagegen/bcfg2-lint.conf.5.ronn
@@ -0,0 +1,114 @@
+bcfg2-lint.conf(5) -- configuration parameters for bcfg2-lint
+=============================================================
+
+## DESCRIPTION
+
+`bcfg2-lint.conf` includes configuration parameters for `bcfg2-lint`.
+
+## FILE FORMAT
+
+The file is INI-style and consists of sections and options. A section
+begins with the name of the sections in square brackets and continues
+until the next section begins.
+
+Options are specified in the form "name=value".
+
+The file is line-based each newline-terminated line represents either a
+comment, a section name or an option.
+
+Any line beginning with a hash (#) is ignored, as are lines containing
+only whitespace.
+
+The file consists of one `[lint]` section, up to one `[errors]` section,
+and then any number of plugin-specific sections, documented below.
+(Note that this makes it quite feasible to combine your
+`bcfg2-lint.conf` into your `bcfg2.conf`(5) file, if you so desire).
+
+## GLOBAL OPTIONS
+
+These options apply to `bcfg2-lint` generally, and must be in the
+`[lint]` section.
+
+ * `plugins`:
+ A comma-delimited list of plugins to run. By default, all plugins
+ are run. This can be overridden by listing plugins on the command
+ line. See `bcfg2-lint`(8) for a list of the available plugins.
+
+## ERROR HANDLING
+
+Error handling is configured in the `[errors]` section. Each option
+should be the name of an error and one of *error*, *warning*, or
+*silent*, which tells `bcfg2-lint`(8) how to handle the warning. Error
+names and their defaults can be displayed by running `bcfg2-lint`(8)
+with the `--list-errors` option.
+
+## PLUGIN OPTIONS
+
+These options apply only to a single plugin. Each option should be in a
+section named for its plugin; for instance, options for the InfoXML
+plugin would be in a section called `[InfoXML]`.
+
+If a plugin is not listed below, then it has no configuration.
+
+In many cases, the behavior of a plugin can be configured by modifying
+how errors from it are handled. See [`ERROR HANDLING`](### ERROR
+HANDLING), above.
+
+### Comments
+
+The `Comments` plugin configuration specifies which VCS keywords and
+comments are required for which file types. The valid types of file are
+*global* (all file types), *bundler* (non-templated bundle files),
+*sgenshi* (templated bundle files), *properties* (property files), *cfg*
+(non-templated Cfg files), *tgenshi* (templated Cfg files), *infoxml*
+(info.xml files), and *probe* (probe files).
+
+The specific types (i.e., types other than "global") all supplement
+global; they do not override it. The exception is if you specify an
+empty option, e.g.:
+
+ cfg_keywords =
+
+By default, the *$Id$* keyword is checked for and nothing else.
+
+Multiple keywords or comments should be comma-delimited.
+
+· `<type>_keywords`
+
+Ensure that files of the specified type have the given VCS keyword. Do
+*not* include the dollar signs. I.e.:
+
+ infoxml_keywords = Revision
+
+*not*:
+
+ infoxml_keywords = $Revision$
+
+`· <type>_comments`
+
+Ensure that files of the specified type have a comment containing the
+given string. In XML files, only comments are checked. In plain text
+files, all lines are checked since comment characters may vary.
+
+### InfoXML
+
+ * `required_attrs`:
+ A comma-delimited list of attributes to require on `<Info>` tags.
+ Default is "owner,group,perms".
+
+### MergeFiles
+
+ * `threshold`:
+ The threshold at which MergeFiles will suggest merging config files
+ and probes. Default is 75% similar.
+
+### Validate
+
+ * `schema`:
+ The full path to the XML Schema files. Default is
+ `/usr/share/bcfg2/schema`. This can be overridden with the
+ *--schema* command-line option
+
+## SEE ALSO
+
+bcfg2-lint(8)
diff --git a/tools/manpagegen/bcfg2-ping-sweep.8.ronn b/tools/manpagegen/bcfg2-ping-sweep.8.ronn
new file mode 100644
index 000000000..815e0af5b
--- /dev/null
+++ b/tools/manpagegen/bcfg2-ping-sweep.8.ronn
@@ -0,0 +1,16 @@
+bcfg2-ping-sweep(8) -- Update pingable and pingtime attributes in clients.xml
+=============================================================================
+
+## SYNOPSIS
+
+`bcfg2-ping-sweep`
+
+## DESCRIPTION
+
+`bcfg2-ping-sweep` traverses the list of clients in Metadata/clients.xml
+and updates their pingable/pingtime attributes. The pingtime value is
+set to the last time the client was pinged (not the RTT value).
+
+## SEE ALSO
+
+bcfg2(1), bcfg2-server(8)
diff --git a/tools/manpagegen/bcfg2-reports.8.ronn b/tools/manpagegen/bcfg2-reports.8.ronn
new file mode 100644
index 000000000..1cb999dc7
--- /dev/null
+++ b/tools/manpagegen/bcfg2-reports.8.ronn
@@ -0,0 +1,82 @@
+bcfg2-reports(8) -- Query reporting system for client status
+============================================================
+
+## SYNOPSIS
+
+`bcfg2-reports` [-a] [-b <NAME>] [-c] [-d] [-e <NAME>] [-h] [-m <NAME>]
+[-s <NAME>] [-x <NAME>] [--badentry=<KIND,NAME>]
+[--extraentry=<KIND,NAME>] [--fields=<ARG1,ARG2,...>]
+[--modifiedentry=<KIND,NAME>] [--sort=<ARG1,ARG2,...>] [--stale] [-v]
+
+## DESCRIPTION
+
+`bcfg2-reports` allows you to retrieve data from the database about
+clients, and the states of their current interactions. It also allows
+you to change the expired/unexpired states. The utility runs as a
+standalone application. It does, however, use the models from
+`/src/lib/Server/Reports/reports/models.py`.
+
+## OPTIONS
+
+ * `-a`:
+ Specify alternate bcfg2.conf location
+
+ * `-b` <hostname>:
+ Single host mode - shows bad entries from the current interaction of
+ *hostname*.
+
+ * `-c`:
+ Shows only clean hosts.
+
+ * `-d`:
+ Shows only dirty hosts.
+
+ * `-e` <hostname>:
+ Single host mode - shows extra entries from the current interaction
+ of *hostname*.
+
+ * `-h`:
+ Shows help and usage info about `bcfg2-reports`.
+
+ * `-m` <hostname>:
+ Single host mode - shows modified entries from the current
+ interaction of *hostname*.
+
+ * `-s` <hostname>:
+ Single host mode - shows bad, modified, and extra entries from the
+ current interaction of *hostname*.
+
+ * `-x` <hostname>:
+ Toggles expired/unexpired state of *hostname*.
+
+ * `--badentry=`<entry type, entry name>:
+ Shows only hosts whose current interaction has bad entries of type
+ *entry type* and name *entry name*. If a single argument ARG1 is
+ given, then *entry type*,*entry name* pairs will be read from a file
+ of name ARG1.
+
+ * `--extraentry=`<entry type, entry name>:
+ Shows only hosts whose current interaction has extra entries of type
+ *entry type* and name *entry name*. If a single argument ARG1 is
+ given, then *entry type*,*entry name* pairs will be read from a file
+ of name ARG1.
+
+ * `--fields=`<ARG1,ARG2,...>:
+ Only displays the fields *ARG1,ARG2,...* (name, time, state, total,
+ good, bad).
+
+ * `--modifiedentry=`<entry type, entry name>:
+ Shows only hosts whose current interaction has modified entries of
+ type *entry type* and name *entry name*. If a single argument ARG1
+ is given, then *entry type*,*entry name* pairs will be read from a
+ file of name ARG1.
+
+ * `--sort=`<ARG1,ARG2,...>:
+ Sorts output on ARG1,ARG2,... (name, time, state, total, good, bad).
+
+ * `--stale`:
+ Shows hosts which haven’t run in the last 24 hours.
+
+## SEE ALSO
+
+bcfg2(1), bcfg2-server(8)
diff --git a/tools/manpagegen/bcfg2-server.8.ronn b/tools/manpagegen/bcfg2-server.8.ronn
new file mode 100644
index 000000000..c306fa6a4
--- /dev/null
+++ b/tools/manpagegen/bcfg2-server.8.ronn
@@ -0,0 +1,43 @@
+bcfg2-server(8) -- Server for client configuration specifications
+=================================================================
+
+## SYNOPSIS
+
+`bcfg2-server` [-d] [-v] [-C <configfile>] [-D <pidfile>] [-E
+<encoding>] [-Q <repo path>] [-S <server url>] [-o <logfile>] [-x
+<password>] [--ssl-key=<ssl key>]
+
+## DESCRIPTION
+
+`bcfg2-server` is the daemon component of Bcfg2 which serves
+configurations to clients based on the data in its repository.
+
+## OPTIONS
+
+ * `-C` <configfile>:
+ Specify alternate bcfg2.conf location.
+
+ * `-D` <pidfile>:
+ Daemonize, placing the program pid in the specified pidfile.
+
+ * `-E` <encoding>:
+ Specify alternate encoding (default is UTF-8).
+
+ * `-Q` <repo path>:
+ Set repository path.
+
+ * `-S` <server url>:
+ Set server address.
+
+ * `-d`:
+ Run `bcfg2-server` in debug mode.
+
+ * `-v`:
+ Run `bcfg2-server` in verbose mode.
+
+ * `--ssl-key=`<ssl key>:
+ Set path to SSL key.
+
+## SEE ALSO
+
+bcfg2(1), bcfg2-lint(8)
diff --git a/tools/manpagegen/bcfg2.1.ronn b/tools/manpagegen/bcfg2.1.ronn
new file mode 100644
index 000000000..c801c833d
--- /dev/null
+++ b/tools/manpagegen/bcfg2.1.ronn
@@ -0,0 +1,152 @@
+bcfg2(1) -- Bcfg2 client tool
+=============================
+
+## SYNOPSIS
+
+`bcfg2` [_options_][_..._]
+
+## DESCRIPTION
+
+`bcfg2` runs the Bcfg2 configuration process on the current host. This
+process consists of the following steps.
+
+* Fetch and execute probes
+* Upload probe results
+* Fetch the client configuration
+* Check the current client state
+* Attempt to install the desired configuration
+* Upload statistics about the Bcfg2 execution and client state
+
+## OPTIONS
+
+ * `-C` <configfile>:
+ Specify alternate bcfg2.conf location
+
+ * `-D` [<driver1>,<driver2>]:
+ Specify a set of Bcfg2 tool drivers.
+
+ *NOTE: only drivers listed will be loaded. (e.g., if you do not
+ include POSIX, you will be unable to verify/install Path entries).*
+
+ * `-E` <encoding>:
+ Specify the encoding of Cfg files.
+
+ * `-I`:
+ Run bcfg2 in interactive mode. The user will be prompted before
+ each change.
+
+ * `-O`:
+ Omit lock check
+
+ * `-P`:
+ Run bcfg2 in paranoid mode. Diffs will be logged for configuration
+ files marked as paranoid by the Bcfg2 server.
+
+ * `-R` <retry count>:
+ Specify the number of times that the client will attempt to retry
+ network communication.
+
+ * `-S` <https://server:port>:
+ Manually specify the server location (as opposed to using the value
+ in bcfg2.conf).
+
+ * `-b` [_bundle1:bundle2_]:
+ Run bcfg2 against one or multiple bundles in the configuration.
+
+ * `-c` <cachefile>:
+ Cache a copy of the configuration in cachefile.
+
+ * `--ca-cert=`<ca cert>:
+ Specifiy the path to the SSL CA certificate.
+
+ * `-d`:
+ Run bcfg2 in debug mode.
+
+ * `-e`:
+ When in verbose mode, display extra entry information (temporary
+ until verbosity rework).
+
+ * `-f` <specification path>:
+ Configure from a file rather than querying the server.
+
+ * `-h`:
+ Print Usage information.
+
+ * `-k`:
+ Run in bulletproof mode. This currently only affects behavior in
+ the debian toolset; it calls apt-get update and clean and dpkg
+ --configure --pending.
+
+ * `-l` <whitelist|blacklist|none>:
+ Run the client in the server decision list mode (unless "none"
+ is specified, which can be done in order to override the decision
+ list mode specified in bcfg2.conf). This approach is needed when
+ particular changes are deemed "high risk". It gives the ability to
+ centrally specify these changes, but only install them on clients
+ when administrator supervision is available. Because collaborative
+ configuration is one of the remaining hard issues in configuration
+ management, these issues typically crop up in environments with
+ several administrators and much configuration variety. (This setting
+ will be ignored if the -f option is also specified).
+
+ * `-n`:
+ Run bcfg2 in dry-run mode. No changes will be made to the system.
+
+ * `-o` <logfile path>:
+ Writes a log to the specified path.
+
+ * `-p` <profile>:
+ Assert a profile for the current client.
+
+ * `-q`:
+ Run bcfg2 in quick mode. Package checksum verification won’t be
+ performed. This mode relaxes the constraints of correctness, and
+ thus should only be used in safe conditions.
+
+ * `-Q`:
+ Run bcfg2 in "bundle quick" mode, where only entries in a bundle are
+ verified or installed. This runs much faster than -q, but doesn’t
+ provide statistics to the server at all. In order for this option to
+ work, the -b option must also be provided. This option is incompatible
+ with -r.
+
+ * `-r` <mode>:
+ Cause bcfg2 to remove extra configuration elements it detects. Mode is
+ one of all, Services, or Packages. All removes all entries. Likewise,
+ Services and Packages remove only the extra configuration elements
+ of the respective type.
+
+ * `-s` <service mode>:
+ Set bcfg2 interaction level for services. Default behavior is to
+ modify all services affected by reconfiguration. build mode attempts
+ to stop all services started. disabled suppresses all attempts to
+ modify services
+
+ * `--ssl-cert=`<ssl cert>:
+ Specifiy the path to the SSL certificate.
+
+ * `--ssl-cns=`[_CN1:CN2_]:
+ List of acceptable SSL server Common Names.
+
+ * `--ssl-key=`<ssl key>:
+ Specifiy the path to the SSL key.
+
+ * `-u` <user>:
+ Attempt to authenticate as ’user’.
+
+ * `-x` <password>:
+ Use ’password’ for client communication.
+
+ * `-t` <timeout>:
+ Set the timeout (in seconds) for client communication. Default is
+ 90 seconds.
+
+ * `-v`:
+ Run bcfg2 in verbose mode.
+
+ * `-z`:
+ Only configure independent entries, ignore bundles.
+
+## SEE ALSO
+
+bcfg2-server(8), bcfg2-info(8)
diff --git a/tools/manpagegen/bcfg2.conf.5.ronn b/tools/manpagegen/bcfg2.conf.5.ronn
new file mode 100644
index 000000000..e71bdb73a
--- /dev/null
+++ b/tools/manpagegen/bcfg2.conf.5.ronn
@@ -0,0 +1,545 @@
+bcfg2.conf(5) -- configuration parameters for Bcfg2
+===================================================
+
+## DESCRIPTION
+
+`bcfg2.conf` includes configuration parameters for the Bcfg2 server and
+client.
+
+## FILE FORMAT
+
+The file is INI-style and consists of sections and options. A section
+begins with the name of the sections in square brackets and continues
+until the next section begins.
+
+Options are specified in the form ’name = value’.
+
+The file is line-based each newline-terminated line represents either a
+comment, a section name or an option.
+
+Any line beginning with a hash (#) is ignored, as are lines containing
+only whitespace.
+
+## SERVER OPTIONS
+
+These options are only necessary on the Bcfg2 server. They are
+specified in the `[server]` section of the configuration file.
+
+ * `repository`:
+ Specifies the path to the Bcfg2 repository containing all of the
+ configuration specifications. The repository should be created
+ using the `bcfg2-admin init` command.
+
+ * `filemonitor`:
+ The file monitor used to watch for changes in the repository. The
+ default is the best available monitor. The following values are
+ valid:
+
+ `inotify`,
+ `gamin`,
+ `fam`,
+ `pseudo`
+
+ * `ignore_files`:
+ A comma-separated list of globs that should be ignored by the file
+ monitor. Default values are:
+
+ `*~`,
+ `*#`,
+ `.#*`,
+ `*.swp`,
+ `.*.swx`,
+ `SCCS`,
+ `.svn`,
+ `4913`,
+ `.gitignore`
+
+ * `listen_all`:
+ This setting tells the server to listen on all available
+ interfaces. The default is to only listen on those interfaces
+ specified by the bcfg2 setting in the components section of
+ `bcfg2.conf`.
+
+ * `plugins`:
+ A comma-delimited list of enabled server plugins. Currently
+ available plugins are:
+
+ `Account`,
+ `Actions`,
+ `BB`,
+ `Base`,
+ `Bundler`,
+ `Bzr`,
+ `Cfg`,
+ `Cvs`,
+ `Darcs`,
+ `DBStats`,
+ `Decisions`,
+ `Deps`,
+ `Editor`,
+ `Fossil`,
+ `Git`,
+ `GroupPatterns`,
+ `Hg`,
+ `Hostbase`,
+ `Metadata`,
+ `NagiosGen`,
+ `Ohai`,
+ `Packages`,
+ `Pkgmgr`,
+ `Probes`,
+ `Properties`,
+ `Rules`,
+ `SGenshi`,
+ `Snapshots`,
+ `SSHbase`,
+ `Svn`,
+ `Svn2`,
+ `TCheetah`,
+ `TGenshi`,
+ `Trigger`
+
+ Descriptions of each plugin can be found in their respective
+ sections below.
+
+ * `prefix`:
+ Specifies a prefix if the Bcfg2 installation isn’t placed in the
+ default location (e.g. /usr/local).
+
+### Account Plugin
+
+The account plugin manages authentication data, including the following.
+
+ * `/etc/passwd`
+ * `/etc/group`
+ * `/etc/security/limits.conf`
+ * `/etc/sudoers`
+ * `/root/.ssh/authorized_keys`
+
+### BB Plugin
+
+The BB plugin maps users to machines and metadata to machines.
+
+### Base Plugin
+
+A structure plugin that provides the ability to add lists of unrelated
+entries into client configuration entry inventories. Base works much
+like Bundler in its file format. This structure plugin is good for the
+pile of independent configs needed for most actual systems.
+
+### Bundler Plugin
+
+Bundler is used to describe groups of inter-dependent configuration
+entries, such as the combination of packages, configuration files,
+and service activations that comprise typical Unix daemons. Bundles are
+used to add groups of configuration entries to the inventory of client
+configurations, as opposed to describing particular versions of those
+entries.
+
+### Bzr Plugin
+
+The Bzr plugin allows you to track changes to your Bcfg2 repository
+using a GNU Bazaar version control backend. Currently, it enables you to
+get revision information out of your repository for reporting purposes.
+
+### Cfg Plugin
+
+The Cfg plugin provides a repository to describe configuration file
+contents for clients. In its simplest form, the Cfg repository is just a
+directory tree modeled off of the directory tree on your client
+machines.
+
+### Cvs Plugin (experimental)
+
+The Cvs plugin allows you to track changes to your Bcfg2 repository
+using a Concurrent version control backend. Currently, it enables you to
+get revision information out of your repository for reporting purposes.
+
+### Darcs Plugin (experimental)
+
+The Darcs plugin allows you to track changes to your Bcfg2 repository
+using a Darcs version control backend. Currently, it enables you to get
+revision information out of your repository for reporting purposes.
+
+### DBStats Plugin
+
+Direct to database statistics plugin.
+
+### Decisions Plugin
+
+The Decisions plugin has support for a centralized set of per-entry
+installation decisions. This approach is needed when particular changes
+are deemed "*high risk*"; this gives the ability to centrally specify
+these changes, but only install them on clients when administrator
+supervision is available.
+
+### Deps Plugin
+
+The Deps plugin allows you to make a series of assertions like "Package
+X requires Package Y (and optionally also Package Z etc.)"
+
+### Editor Plugin
+
+The Editor plugin attempts to allow you to partially manage
+configuration for a file. Its use is not recommended and not well
+documented.
+
+### Fossil Plugin
+
+The Fossil plugin allows you to track changes to your Bcfg2 repository
+using a Fossil SCM version control backend. Currently, it enables you to
+get revision information out of your repository for reporting purposes.
+
+### Git Plugin
+
+The Git plugin allows you to track changes to your Bcfg2 repository
+using a Git version control backend. Currently, it enables you to get
+revision information out of your repository for reporting purposes.
+
+### GroupPatterns Plugin
+
+The GroupPatterns plugin is a connector that can assign clients group
+membership based on patterns in client hostnames.
+
+### Hg Plugin (experimental)
+
+The Hg plugin allows you to track changes to your Bcfg2 repository using
+a Mercurial version control backend. Currently, it enables you to get
+revision information out of your repository for reporting purposes.
+
+### Hostbase Plugin
+
+The Hostbase plugin is an IP management system built on top of Bcfg2.
+
+### Metadata Plugin
+
+The Metadata plugin is the primary method of specifying Bcfg2 server
+metadata.
+
+### NagiosGen Plugin
+
+NagiosGen is a Bcfg2 plugin that dynamically generates Nagios
+configuration files based on Bcfg2 data.
+
+### Ohai Plugin (experimental)
+
+The Ohai plugin is used to detect information about the client operating
+system. The data is reported back to the server using JSON.
+
+### Packages Plugin
+
+The Packages plugin is an alternative to Pkgmgr for specifying package
+entries for clients. Where Pkgmgr explicitly specifies package entry
+information, Packages delegates control of package version information
+to the underlying package manager, installing the latest version
+available from through those channels.
+
+### Pkgmgr Plugin
+
+The Pkgmgr plugin resolves the Abstract Configuration Entity "Package"
+to a package specification that the client can use to detect, verify and
+install the specified package.
+
+### Probes Plugin
+
+The Probes plugin gives you the ability to gather information from a
+client machine before you generate its configuration. This information
+can be used with the various templating systems to generate
+configuration based on the results.
+
+### Properties Plugin
+
+The Properties plugin is a connector plugin that adds information from
+properties files into client metadata instances.
+
+### Rules Plugin
+
+The Rules plugin provides literal configuration entries that resolve the
+abstract configuration entries normally found in the Bundler and Base
+plugins. The literal entries in Rules are suitable for consumption by
+the appropriate client drivers.
+
+### Snapshots Plugin
+
+The Snapshots plugin stores various aspects of a client’s state when the
+client checks in to the server.
+
+### SSHbase Plugin
+
+The SSHbase generator plugin manages ssh host keys (both v1 and v2) for
+hosts. It also manages the ssh_known_hosts file. It can integrate host
+keys from other management domains and similarly export its keys.
+
+### Svn Plugin
+
+The Svn plugin allows you to track changes to your Bcfg2 repository
+using a Subversion backend. Currently, it enables you to get revision
+information out of your repository for reporting purposes.
+
+### Svn2 Plugin
+
+The Svn2 plugin extends on the capabilities in the Svn plugin. It
+provides Update and Commit methods which provide hooks for modifying
+subversion-backed Bcfg2 repositories.
+
+### TCheetah Plugin
+
+The TCheetah plugin allows you to use the cheetah templating system to
+create files. It also allows you to include the results of probes
+executed on the client in the created files.
+
+### TGenshi Plugin
+
+The TGenshi plugin allows you to use the Genshi templating system to
+create files. It also allows you to include the results of probes
+executed on the client in the created files.
+
+### Trigger Plugin
+
+The Trigger plugin provides a method for calling external scripts when
+clients are configured.
+
+## CLIENT OPTIONS
+
+These options only affect client functionality, specified in the
+`[client]` section.
+
+ * `decision`:
+ Specify the server decision list mode (whitelist or blacklist).
+ (This settiing will be ignored if the client is called with the -f
+ option.)
+
+ * `drivers`:
+ Specify tool driver set to use. This option can be used to
+ explicitly specify the client tool drivers you want to use when the
+ client is run.
+
+ * `paranoid`:
+ Run the client in paranoid mode.
+
+## COMMUNICATION OPTIONS
+
+Specified in the `[communication]` section. These options define
+settings used for client-server communication.
+
+ * `ca`:
+ The path to a file containing the CA certificate. This file is
+ required on the server, and optional on clients. However, if the
+ cacert is not present on clients, the server cannot be verified.
+
+ * `certificate`:
+ The path to a file containing a PEM formatted certificate which
+ signs the key with the ca certificate. This setting is required on
+ the server in all cases, and required on clients if using client
+ certificates.
+
+ * `key`:
+ Specifies the path to a file containing the SSL Key. This is
+ required on the server in all cases, and required on clients if
+ using client certificates.
+
+ * `password`:
+ Required on both the server and clients. On the server, sets the
+ password clients need to use to communicate. On a client, sets the
+ password to use to connect to the server.
+
+ * `protocol`:
+ Communication protocol to use. Defaults to xmlrpc/ssl.
+
+ * `retries`:
+ A client-only option. Number of times to retry network
+ communication.
+
+ * `serverCommonNames`:
+ A client-only option. A colon-separated list of Common Names the
+ client will accept in the SSL certificate presented by the server.
+
+ * `user`:
+ A client-only option. The UUID of the client.
+
+## COMPONENT OPTIONS
+
+Specified in the `[components]` section.
+
+ * `bcfg2`:
+ URL of the server. On the server this specifies which interface and
+ port the server listens on. On the client, this specifies where the
+ client will attempt to contact the server.
+
+ e.g. `bcfg2 = https://10.3.1.6:6789`
+
+ * `encoding`:
+ Text encoding of configuration files. Defaults to UTF-8.
+
+## LOGGING OPTIONS
+
+Specified in the `[logging]` section. These options control the server
+logging functionality.
+
+ * `path`:
+ Server log file path.
+
+## MDATA OPTIONS
+
+These options affect the default metadata settings for Paths with
+type=’file’.
+
+ * `owner`:
+ Global owner for Paths (defaults to root)
+
+ * `group`:
+ Global group for Paths (defaults to root)
+
+ * `perms`:
+ Global permissions for Paths (defaults to 644)
+
+ * `paranoid`:
+ Global paranoid settings for Paths (defaults to false)
+
+ * `sensitive`:
+ Global sensitive settings for Paths (defaults to false)
+
+## PACKAGES OPTIONS
+
+The following options are specified in the `[packages]` section of the
+configuration file.
+
+ * `resolver`:
+ Enable dependency resolution. Default is 1 (true).
+
+ * `metadata`:
+ Enable metadata processing. Default is 1 (true). If metadata is
+ disabled, it’s implied that resolver is also disabled.
+
+ * `yum_config`:
+ The path at which to generate Yum configs. No default.
+
+ * `apt_config`:
+ The path at which to generate APT configs. No default.
+
+ * `gpg_keypath`:
+ The path on the client where RPM GPG keys will be copied before they
+ are imported on the client. Default is `/etc/pki/rpm-gpg`.
+
+ * `version`:
+ Set the version attribute used when binding Packages. Default is
+ auto.
+
+The following options are specified in the `[packages:yum]` section of
+the configuration file.
+
+ * `use_yum_libraries`:
+ By default, Bcfg2 uses an internal implementation of Yum’s
+ dependency resolution and other routines so that the Bcfg2 server
+ can be run on a host that does not support Yum itself. If you run
+ the Bcfg2 server on a machine that does have Yum libraries, however,
+ you can enable use of those native libraries in Bcfg2 by setting
+ this to 1.
+
+ * `helper`:
+ Path to bcfg2-yum-helper. By default, Bcfg2 looks first in $PATH and
+ then in `/usr/sbin/bcfg2-yum-helper` for the helper.
+
+ All other options in the `[packages:yum]` section will be passed along
+ verbatim to the Yum configuration if you are using the native Yum
+ library support.
+
+The following options are specified in the `[packages:pulp]` section of
+the configuration file.
+
+ * `username`:
+ The username of a Pulp user that will be used to register new
+ clients and bind them to repositories.
+
+ * `password`:
+ The password of a Pulp user that will be used to register new
+ clients and bind them to repositories.
+
+## PARANOID OPTIONS
+
+These options allow for finer-grained control of the paranoid mode on
+the Bcfg2 client. They are specified in the `[paranoid]` section of the
+configuration file.
+
+ * `path`:
+ Custom path for backups created in paranoid mode. The default is in
+ `/var/cache/bcfg2`.
+
+ * `max_copies`:
+ Specify a maximum number of copies for the server to keep when
+ running in paranoid mode. Only the most recent versions of these
+ copies will be kept.
+
+## SNAPSHOTS OPTIONS
+
+Specified in the `[snapshots]` section. These options control the server
+snapshots functionality.
+
+ * `driver`:
+ sqlite
+
+ * `database`:
+ The name of the database to use for statistics data.
+
+ eg: `$REPOSITORY_DIR/etc/bcfg2.sqlite`
+
+## SSLCA OPTIONS
+
+These options are necessary to configure the SSLCA plugin and can be
+found in the `[sslca_default]` section of the configuration file.
+
+ * `config`:
+ Specifies the location of the openssl configuration file for your
+ CA.
+
+ * `passphrase`:
+ Specifies the passphrase for the CA’s private key (if necessary).
+ If no passphrase exists, it is assumed that the private key is
+ stored unencrypted.
+
+ * `chaincert`:
+ Specifies the location of your ssl chaining certificate. This is
+ used when pre-existing certifcate hostfiles are found, so that they
+ can be validated and only regenerated if they no longer meet the
+ specification. If you’re using a self signing CA this would be the
+ CA cert that you generated.
+
+## STATISTICS OPTIONS
+
+Server-only, specified in the `[statistics]` section. These options
+control the statistics collection functionality of the server.
+
+ * `database_engine`:
+ The database engine used by the statistics module. One of the
+ following:
+
+ `postgresql`,
+ `mysql`,
+ `sqlite3`,
+ `ado_mssql`
+
+ * `database_name`:
+ The name of the database to use for statistics data. If
+ ‘database_engine’ is set to ‘sqlite3’ this is a file path to sqlite
+ file and defaults to `$REPOSITORY_DIR/etc/brpt.sqlite`.
+
+ * `database_user`:
+ User for database connections. Not used for sqlite3.
+
+ * `database_password`:
+ Password for database connections. Not used for sqlite3.
+
+ * `database_host`:
+ Host for database connections. Not used for sqlite3.
+
+ * `database_port`:
+ Port for database connections. Not used for sqlite3.
+
+ * `time_zone`:
+ Specify a time zone other than that used on the system. (Note that
+ this will cause the Bcfg2 server to log messages in this time zone
+ as well).
+
+## SEE ALSO
+
+bcfg2(1), bcfg2-server(8)
diff --git a/tools/manpagegen/generate-manpages.bash b/tools/manpagegen/generate-manpages.bash
new file mode 100644
index 000000000..62006c953
--- /dev/null
+++ b/tools/manpagegen/generate-manpages.bash
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This makes building our manpages easier and more consistent. More
+# information about the tool used to do this can be found at:
+#
+# https://github.com/rtomayko/ronn
+
+if [ ! -d man -o ! -d tools ]
+then
+ echo "Must be in the top-level bcfg2 source directory"
+ exit 1
+fi
+
+for f in $(ls man)
+do
+ ronn -r --pipe tools/manpagegen/${f}.ronn | grep -iv ronn > man/${f}
+done
diff --git a/tools/selinux_baseline.py b/tools/selinux_baseline.py
new file mode 100755
index 000000000..6ddc390a3
--- /dev/null
+++ b/tools/selinux_baseline.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+import sys
+import logging
+import lxml.etree
+
+import Bcfg2.Logger
+import Bcfg2.Options
+from Bcfg2.Client.Tools.SELinux import *
+
+LOGGER = None
+
+def get_setup():
+ global LOGGER
+ optinfo = Bcfg2.Options.CLIENT_COMMON_OPTIONS
+ setup = Bcfg2.Options.OptionParser(optinfo)
+ setup.parse(sys.argv[1:])
+
+ if setup['args']:
+ print("selinux_baseline.py takes no arguments, only options")
+ print(setup.buildHelpMessage())
+ raise SystemExit(1)
+ level = 30
+ if setup['verbose']:
+ level = 20
+ if setup['debug']:
+ level = 0
+ Bcfg2.Logger.setup_logging('selinux_base',
+ to_syslog=False,
+ level=level,
+ to_file=setup['logging'])
+ LOGGER = logging.getLogger('bcfg2')
+ return setup
+
+def main():
+ setup = get_setup()
+ config = lxml.etree.Element("Configuration")
+ selinux = SELinux(LOGGER, setup, config)
+
+ baseline = lxml.etree.Element("Bundle", name="selinux_baseline")
+ for etype, handler in selinux.handlers.items():
+ baseline.append(lxml.etree.Comment("%s entries" % etype))
+ extra = handler.FindExtra()
+ for entry in extra:
+ entry.tag = "BoundSELinux"
+ baseline.extend(extra)
+
+ print lxml.etree.tostring(baseline, pretty_print=True)
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tools/upgrade/1.3/service_modes.py b/tools/upgrade/1.3/service_modes.py
new file mode 100755
index 000000000..0c458c3a9
--- /dev/null
+++ b/tools/upgrade/1.3/service_modes.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import glob
+import lxml.etree
+import Bcfg2.Options
+
+def main():
+ opts = dict(repo=Bcfg2.Options.SERVER_REPOSITORY)
+ setup = Bcfg2.Options.OptionParser(opts)
+ setup.parse(sys.argv[1:])
+
+ files = []
+ for plugin in ['Bundler', 'Rules', 'Default']:
+ files.extend(glob.glob(os.path.join(setup['repo'], plugin, "*")))
+
+ for bfile in files:
+ bdata = lxml.etree.parse(bfile)
+ changed = False
+ for svc in bdata.xpath("//Service|//BoundService"):
+ if "mode" not in svc.attrib:
+ continue
+ mode = svc.get("mode")
+ del svc.attrib["mode"]
+ if mode not in ["default", "supervised", "interactive_only",
+ "manual"]:
+ print("Unrecognized mode on Service:%s: %s. Assuming default" %
+ (svc.get("name"), mode))
+ mode = "default"
+ if mode == "default" or mode == "supervised":
+ svc.set("restart", "true")
+ svc.set("install", "true")
+ elif mode == "interactive_only":
+ svc.set("restart", "interactive")
+ svc.set("install", "true")
+ elif mode == "manual":
+ svc.set("restart", "false")
+ svc.set("install", "false")
+ changed = True
+ if changed:
+ print("Writing %s" % bfile)
+ try:
+ open(bfile, "w").write(lxml.etree.tostring(bdata))
+ except IOError:
+ err = sys.exc_info()[1]
+ print("Could not write %s: %s" % (bfile, err))
+
+if __name__ == '__main__':
+ sys.exit(main())