summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml7
-rw-r--r--COPYRIGHT9
-rwxr-xr-xdebian/bcfg2-server.bcfg2-report-collector.init (renamed from debian/bcfg2-report-collector.init)9
-rwxr-xr-xdebian/bcfg2-server.init12
-rwxr-xr-xdebian/bcfg2.cron.daily2
-rwxr-xr-xdebian/bcfg2.cron.hourly2
-rwxr-xr-xdebian/bcfg2.init9
-rw-r--r--debian/control13
-rwxr-xr-xdebian/rules16
-rw-r--r--doc/_templates/indexsidebar.html1
-rw-r--r--doc/appendix/guides/centos.txt18
-rw-r--r--doc/development/compat.txt2
-rw-r--r--doc/development/core.txt5
-rw-r--r--doc/development/documentation.txt27
-rw-r--r--doc/development/lint.txt167
-rw-r--r--doc/development/plugins.txt2
-rw-r--r--doc/exts/xmlschema.py10
-rw-r--r--doc/help/troubleshooting.txt2
-rw-r--r--doc/installation/distributions.txt14
-rw-r--r--doc/reports/dynamic.txt66
-rw-r--r--doc/server/configuration.txt37
-rw-r--r--doc/server/database.txt5
-rw-r--r--doc/server/encryption.txt3
-rw-r--r--doc/server/plugins/connectors/grouplogic.txt122
-rw-r--r--doc/server/plugins/generators/cfg.txt47
-rw-r--r--doc/server/plugins/generators/packages.txt45
-rw-r--r--doc/server/plugins/generators/rules.txt15
-rw-r--r--doc/server/plugins/generators/sslca.txt2
-rw-r--r--doc/server/plugins/generators/tcheetah.txt2
-rw-r--r--doc/server/plugins/grouping/grouppatterns.txt17
-rw-r--r--doc/server/plugins/grouping/metadata.txt19
-rw-r--r--doc/server/selinux.txt10
-rw-r--r--doc/server/snapshots/index.txt5
-rw-r--r--gentoo/bcfg2-1.3.0.ebuild10
-rw-r--r--misc/bcfg2-selinux.spec2
-rw-r--r--misc/bcfg2.spec37
-rwxr-xr-xredhat/scripts/bcfg2-report-collector.init9
-rwxr-xr-xredhat/scripts/bcfg2-server.init3
-rwxr-xr-xredhat/scripts/bcfg2.init4
-rw-r--r--schemas/grouplogic.xsd110
-rw-r--r--schemas/packages.xsd9
-rw-r--r--schemas/selinux.xsd42
-rw-r--r--schemas/servicetype.xsd15
-rw-r--r--schemas/types.xsd60
-rwxr-xr-xsetup.py28
-rw-r--r--solaris-ips/MANIFEST.bcfg2-server.header5
-rw-r--r--solaris-ips/MANIFEST.bcfg2.header6
-rw-r--r--solaris-ips/Makefile20
-rw-r--r--solaris-ips/README22
-rw-r--r--solaris-ips/gen-manifests.sh15
-rw-r--r--solaris-ips/pkginfo.bcfg210
-rw-r--r--solaris-ips/pkginfo.bcfg2-server10
-rw-r--r--src/lib/Bcfg2/Client/Client.py23
-rw-r--r--src/lib/Bcfg2/Client/Frame.py83
-rw-r--r--src/lib/Bcfg2/Client/Tools/APK.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/Action.py20
-rw-r--r--src/lib/Bcfg2/Client/Tools/Chkconfig.py97
-rw-r--r--src/lib/Bcfg2/Client/Tools/DebInit.py105
-rw-r--r--src/lib/Bcfg2/Client/Tools/Encap.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py4
-rw-r--r--src/lib/Bcfg2/Client/Tools/IPS.py6
-rw-r--r--src/lib/Bcfg2/Client/Tools/MacPorts.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/Device.py4
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/Directory.py6
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/File.py16
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX/base.py20
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py6
-rw-r--r--src/lib/Bcfg2/Client/Tools/Pacman.py6
-rw-r--r--src/lib/Bcfg2/Client/Tools/Portage.py14
-rw-r--r--src/lib/Bcfg2/Client/Tools/RcUpdate.py118
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py52
-rw-r--r--src/lib/Bcfg2/Client/Tools/SMF.py23
-rw-r--r--src/lib/Bcfg2/Client/Tools/SYSV.py4
-rw-r--r--src/lib/Bcfg2/Client/Tools/Systemd.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/Upstart.py4
-rw-r--r--src/lib/Bcfg2/Client/Tools/VCS.py112
-rw-r--r--src/lib/Bcfg2/Client/Tools/YUM.py93
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py16
-rw-r--r--src/lib/Bcfg2/Client/__init__.py11
-rw-r--r--src/lib/Bcfg2/Compat.py44
-rwxr-xr-xsrc/lib/Bcfg2/Encryption.py6
-rw-r--r--src/lib/Bcfg2/Logger.py14
-rw-r--r--src/lib/Bcfg2/Options.py46
-rw-r--r--src/lib/Bcfg2/Proxy.py11
-rw-r--r--src/lib/Bcfg2/Reporting/Collector.py6
-rw-r--r--src/lib/Bcfg2/Reporting/Storage/DjangoORM.py3
-rw-r--r--src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py4
-rw-r--r--src/lib/Bcfg2/Reporting/Transport/__init__.py16
-rw-r--r--src/lib/Bcfg2/Reporting/models.py66
-rw-r--r--src/lib/Bcfg2/Reporting/templates/base.html29
-rw-r--r--src/lib/Bcfg2/Reporting/templates/clients/detail.html32
-rw-r--r--src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html6
-rw-r--r--src/lib/Bcfg2/Reporting/templates/clients/index.html8
-rw-r--r--src/lib/Bcfg2/Reporting/templates/clients/manage.html8
-rw-r--r--src/lib/Bcfg2/Reporting/templates/config_items/common.html5
-rw-r--r--src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html10
-rw-r--r--src/lib/Bcfg2/Reporting/templates/config_items/item.html20
-rw-r--r--src/lib/Bcfg2/Reporting/templates/config_items/listing.html8
-rw-r--r--src/lib/Bcfg2/Reporting/templates/displays/summary.html6
-rw-r--r--src/lib/Bcfg2/Reporting/templates/displays/timing.html14
-rw-r--r--src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py36
-rw-r--r--src/lib/Bcfg2/Reporting/templatetags/syntax_coloring.py16
-rw-r--r--src/lib/Bcfg2/Reporting/views.py2
-rw-r--r--src/lib/Bcfg2/SSLServer.py16
-rw-r--r--src/lib/Bcfg2/Server/Admin/Client.py13
-rw-r--r--src/lib/Bcfg2/Server/Admin/Compare.py3
-rw-r--r--src/lib/Bcfg2/Server/Admin/Init.py4
-rw-r--r--src/lib/Bcfg2/Server/Admin/Minestruct.py18
-rw-r--r--src/lib/Bcfg2/Server/Admin/Perf.py11
-rw-r--r--src/lib/Bcfg2/Server/Admin/Pull.py17
-rw-r--r--src/lib/Bcfg2/Server/Admin/Reports.py23
-rw-r--r--src/lib/Bcfg2/Server/Admin/Syncdb.py10
-rw-r--r--src/lib/Bcfg2/Server/Admin/Xcmd.py22
-rw-r--r--src/lib/Bcfg2/Server/Admin/__init__.py32
-rw-r--r--src/lib/Bcfg2/Server/BuiltinCore.py16
-rw-r--r--src/lib/Bcfg2/Server/Core.py255
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Fam.py2
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Inotify.py8
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/Pseudo.py2
-rw-r--r--src/lib/Bcfg2/Server/Lint/Comments.py132
-rwxr-xr-xsrc/lib/Bcfg2/Server/Lint/Genshi.py17
-rw-r--r--src/lib/Bcfg2/Server/Lint/GroupNames.py28
-rw-r--r--src/lib/Bcfg2/Server/Lint/InfoXML.py14
-rw-r--r--src/lib/Bcfg2/Server/Lint/MergeFiles.py2
-rw-r--r--src/lib/Bcfg2/Server/Lint/RequiredAttrs.py53
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py80
-rw-r--r--src/lib/Bcfg2/Server/Lint/__init__.py180
-rw-r--r--src/lib/Bcfg2/Server/MultiprocessingCore.py204
-rw-r--r--src/lib/Bcfg2/Server/Plugin/base.py20
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py187
-rw-r--r--src/lib/Bcfg2/Server/Plugin/interfaces.py19
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Base.py9
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Bundler.py41
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py8
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py128
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cvs.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Darcs.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Decisions.py13
-rw-r--r--src/lib/Bcfg2/Server/Plugins/FileProbes.py16
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Fossil.py10
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Git.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/GroupLogic.py47
-rw-r--r--src/lib/Bcfg2/Server/Plugins/GroupPatterns.py18
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Metadata.py231
-rw-r--r--src/lib/Bcfg2/Server/Plugins/NagiosGen.py24
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Ohai.py31
-rw-r--r--src/lib/Bcfg2/Server/Plugins/POSIXCompat.py4
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Apt.py17
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py14
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Source.py14
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Yum.py75
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/__init__.py39
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Pkgmgr.py16
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Probes.py60
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Properties.py42
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Reporting.py7
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSHbase.py12
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSLCA.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/ServiceCompat.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Svn.py39
-rw-r--r--src/lib/Bcfg2/Server/Plugins/TemplateHelper.py23
-rw-r--r--src/lib/Bcfg2/Server/Plugins/__init__.py33
-rw-r--r--src/lib/Bcfg2/Server/__init__.py5
-rw-r--r--src/lib/Bcfg2/Statistics.py11
-rw-r--r--src/lib/Bcfg2/Utils.py41
-rw-r--r--src/lib/Bcfg2/__init__.py2
-rw-r--r--src/lib/Bcfg2/settings.py6
-rw-r--r--src/lib/Bcfg2/version.py2
-rwxr-xr-xsrc/sbin/bcfg2-admin4
-rwxr-xr-xsrc/sbin/bcfg2-build-reports2
-rwxr-xr-xsrc/sbin/bcfg2-crypt6
-rwxr-xr-xsrc/sbin/bcfg2-info82
-rwxr-xr-xsrc/sbin/bcfg2-lint18
-rwxr-xr-xsrc/sbin/bcfg2-report-collector10
-rwxr-xr-xsrc/sbin/bcfg2-reports3
-rwxr-xr-xsrc/sbin/bcfg2-server33
-rwxr-xr-xsrc/sbin/bcfg2-test17
-rwxr-xr-xsrc/sbin/bcfg2-yum-helper103
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestDirectory.py6
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py8
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Testbase.py2
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py9
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py33
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py62
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py11
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgGenshiGenerator.py1
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py53
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py15
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py20
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py192
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProperties.py28
-rw-r--r--testsuite/Testsrc/Testlib/TestStatistics.py44
-rwxr-xr-xtestsuite/before_install.sh9
-rwxr-xr-xtestsuite/install.sh12
-rw-r--r--testsuite/pylintrc.conf2
-rw-r--r--testsuite/requirements.txt4
-rwxr-xr-xtools/bcfg2-profile-templates.py110
-rwxr-xr-xtools/bcfg2_local.py9
-rwxr-xr-xtools/export.py43
-rwxr-xr-xtools/posixusers_baseline.py4
-rwxr-xr-xtools/selinux_baseline.py5
-rw-r--r--tools/upgrade/1.3/README5
-rwxr-xr-xtools/upgrade/1.3/migrate_dbstats.py19
-rwxr-xr-xtools/upgrade/1.3/migrate_info.py6
-rwxr-xr-xtools/upgrade/1.3/migrate_perms_to_mode.py5
-rwxr-xr-xtools/upgrade/1.3/migrate_probe_groups_to_db.py68
-rwxr-xr-xtools/yum-listpkgs-xml.py2
208 files changed, 4044 insertions, 1877 deletions
diff --git a/.travis.yml b/.travis.yml
index f5aade735..73b8a9594 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,16 +3,9 @@ python:
- "2.5"
- "2.6"
- "2.7"
- - "3.2"
env:
- WITH_OPTIONAL_DEPS=yes
- WITH_OPTIONAL_DEPS=no
-matrix:
- exclude:
- - python: "3.2"
- env: WITH_OPTIONAL_DEPS=yes
- - python: "3.3"
- env: WITH_OPTIONAL_DEPS=yes
before_install:
- testsuite/before_install.sh
install:
diff --git a/COPYRIGHT b/COPYRIGHT
index 571fa6034..379ddaa4b 100644
--- a/COPYRIGHT
+++ b/COPYRIGHT
@@ -1,3 +1,7 @@
+This file contains a list of copyright holders. Anyone who
+contributes more than trivial fixes (typos, etc.) to Bcfg2 should also
+add themselves to this file. See LICENSE for the full license.
+
- Narayan Desai <desai@mcs.anl.gov> has written most of Bcfg2,
including all parts not explicitly mentioned in this file.
@@ -152,3 +156,8 @@
- Zach Lowry <zach@mcs.anl.gov> wrote Solaris support and general
hardening.
+
+- Michael Fenn <fennm@deshawresearch.com> fixed various small bugs
+ related to bcfg2 on CentOS 5
+
+- Alexander Sulfrian <alexander@sulfrian.net> fixed various bugs.
diff --git a/debian/bcfg2-report-collector.init b/debian/bcfg2-server.bcfg2-report-collector.init
index 2d182385a..df7b751cb 100755
--- a/debian/bcfg2-report-collector.init
+++ b/debian/bcfg2-server.bcfg2-report-collector.init
@@ -32,6 +32,7 @@ test -x $DAEMON || exit 5
# Internal variables
BINARY=$(basename $DAEMON)
+RETVAL=0
start () {
echo -n "Starting Configuration Report Collector: "
@@ -85,22 +86,26 @@ status () {
case "$1" in
start)
start
+ RETVAL=$?
;;
stop)
stop
+ RETVAL=$?
;;
status)
status
+ RETVAL=$?
;;
restart|reload|force-reload)
stop
sleep 5
start
+ RETVAL=$?
;;
*)
log_success_msg "Usage: $0 {start|stop|status|reload|restart|force-reload}"
- exit 1
+ RETVAL=1
;;
esac
-exit 0
+exit $RETVAL
diff --git a/debian/bcfg2-server.init b/debian/bcfg2-server.init
index 8de16b9b5..b1c3aba21 100755
--- a/debian/bcfg2-server.init
+++ b/debian/bcfg2-server.init
@@ -17,7 +17,8 @@
### END INIT INFO
# Include lsb functions
-. /lib/lsb/init-functions
+test -f "/lib/lsb/init-functions" && . /lib/lsb/init-functions # debian
+test -f "/etc/init.d/functions" && . /etc/init.d/functions # redhat
# Commonly used stuff
DAEMON=/usr/sbin/bcfg2-server
@@ -41,6 +42,7 @@ test -x $DAEMON || exit 5
# Internal variables
BINARY=$(basename $DAEMON)
+RETVAL=0
start () {
echo -n "Starting Configuration Management Server: "
@@ -91,22 +93,26 @@ status () {
case "$1" in
start)
start
+ RETVAL=$?
;;
stop)
stop
+ RETVAL=$?
;;
status)
status
+ RETVAL=$?
;;
restart|reload|force-reload)
stop
sleep 5
start
+ RETVAL=$?
;;
*)
log_success_msg "Usage: $0 {start|stop|status|reload|restart|force-reload}"
- exit 1
+ RETVAL=1
;;
esac
-exit 0
+exit $RETVAL
diff --git a/debian/bcfg2.cron.daily b/debian/bcfg2.cron.daily
index f2d1efb9f..b872887cb 100755
--- a/debian/bcfg2.cron.daily
+++ b/debian/bcfg2.cron.daily
@@ -10,4 +10,4 @@ else
echo "No bcfg2-cron command found"
exit 1
fi
-$BCFG2CRON --daily 2>&1 | logger -t bcfg2-cron -p daemon.info
+$BCFG2CRON --daily 2>&1 | logger -t bcfg2-cron -p daemon.info -i
diff --git a/debian/bcfg2.cron.hourly b/debian/bcfg2.cron.hourly
index 73aae7606..9f666e083 100755
--- a/debian/bcfg2.cron.hourly
+++ b/debian/bcfg2.cron.hourly
@@ -10,4 +10,4 @@ else
echo "No bcfg2-cron command found"
exit 1
fi
-$BCFG2CRON --hourly 2>&1 | logger -t bcfg2-cron -p daemon.info
+$BCFG2CRON --hourly 2>&1 | logger -t bcfg2-cron -p daemon.info -i
diff --git a/debian/bcfg2.init b/debian/bcfg2.init
index 4f83adbf6..b2e47b346 100755
--- a/debian/bcfg2.init
+++ b/debian/bcfg2.init
@@ -47,6 +47,7 @@ fi
# Internal variables
BINARY=$(basename $BCFG2)
+RETVAL=0
# Include lsb functions
. /lib/lsb/init-functions
@@ -70,17 +71,19 @@ start () {
case "$1" in
start)
start
+ RETVAL=$?
;;
stop|status)
- exit 0
+ RETVAL=0
;;
restart|force-reload)
start
+ RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|status|restart|force-reload}"
- exit 1
+ RETVAL=1
;;
esac
-exit 0
+exit $RETVAL
diff --git a/debian/control b/debian/control
index 6c7278e4e..7b27b27ed 100644
--- a/debian/control
+++ b/debian/control
@@ -4,25 +4,26 @@ Priority: optional
Maintainer: Arto Jantunen <viiru@debian.org>
Uploaders: Sami Haahtinen <ressu@debian.org>
Build-Depends: debhelper (>= 7.0.50~),
- python (>= 2.3.5-7),
+ python (>= 2.6),
python-setuptools,
python-sphinx (>= 1.0.7+dfsg) | python3-sphinx,
python-lxml,
python-daemon,
python-cherrypy,
+ python-gamin,
+ python-genshi,
python-pyinotify,
python-m2crypto,
python-doc,
+ python-mock,
python-mock-doc
Build-Depends-Indep: python-support (>= 0.5.3)
Standards-Version: 3.8.0.0
-XS-Python-Version: >= 2.3
Homepage: http://bcfg2.org/
Package: bcfg2
Architecture: all
-Depends: ${python:Depends}, ${misc:Depends}, debsums, python-apt, ucf, lsb-base (>= 3.1-9), python-m2crypto | python-ssl | python2.6 | python3.0 | python3.1 | python3.2
-XB-Python-Version: >= 2.3
+Depends: ${python:Depends}, ${misc:Depends}, debsums, python-apt, ucf, lsb-base (>= 3.1-9), python (>= 2.6)
Description: Configuration management client
Bcfg2 is a configuration management system that generates configuration sets
for clients bound by client profiles.
@@ -31,8 +32,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-pyinotify | python-gamin, python-daemon
-XB-Python-Version: >= 2.4
+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 (>= 2.6), python-pyinotify | python-gamin, python-daemon
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})
Description: Configuration management server
@@ -45,7 +45,6 @@ Package: bcfg2-web
Architecture: all
Depends: ${python:Depends}, ${misc:Depends}, bcfg2-server (= ${binary:Version}), python-django,
Suggests: python-mysqldb, python-psycopg2, python-sqlite, libapache2-mod-wsgi
-XB-Python-Version: >= 2.4
Description: Configuration management web interface
Bcfg2 is a configuration management system that generates configuration sets
for clients bound by client profiles.
diff --git a/debian/rules b/debian/rules
index fcbf6346c..eaf80a4d7 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,20 +1,20 @@
#!/usr/bin/make -f
-%:
- dh $@ --with python-support,sphinxdoc
+# Lucid does not have dh_python2, but we would like to be able to use
+# this rules file to build on lucid as well.
+WITH_PYTHON2 = $(shell test -f /usr/bin/dh_python2 && echo "--with python2")
+WITH_SPHINXDOC = $(shell test -f /usr/bin/dh_sphinxdoc && echo "--with sphinxdoc")
-override_dh_auto_install:
- # Make the build destination dir consistent between pre-7.3 and 7.3 and
- # later debhelper - see http://bcfg2.org/ticket/791
- dh_auto_install
- test -d debian/tmp/usr/local && mv debian/tmp/usr/local/* debian/tmp/usr || exit 0
- test -d debian/tmp/usr/local && rmdir debian/tmp/usr/local || exit 0
+%:
+ dh $@ ${WITH_PYTHON2} ${WITH_SPHINXDOC}
override_dh_installinit:
# Install bcfg2 initscript without starting it on postinst
dh_installinit --package=bcfg2 --no-start
# Install bcfg2-server initscript without starting it on postinst
dh_installinit --package=bcfg2-server --no-start
+ # Install bcfg2-report-collector initscript without starting it on postinst
+ dh_installinit --package=bcfg2-server --name=bcfg2-report-collector --no-start
override_dh_auto_build:
dh_auto_build
diff --git a/doc/_templates/indexsidebar.html b/doc/_templates/indexsidebar.html
index 39916315d..2133cdcc5 100644
--- a/doc/_templates/indexsidebar.html
+++ b/doc/_templates/indexsidebar.html
@@ -7,5 +7,6 @@
<ul>
<li><a href="http://docs.bcfg2.org/1.1/">Bcfg2 1.1 (stable)</a></li>
<li><a href="http://docs.bcfg2.org/1.2/">Bcfg2 1.2 (stable)</a></li>
+ <li><a href="http://docs.bcfg2.org/1.3/">Bcfg2 1.3 (stable)</a></li>
<li><a href="http://docs.bcfg2.org/dev/">Bcfg2 development documentation</a></li>
</ul>
diff --git a/doc/appendix/guides/centos.txt b/doc/appendix/guides/centos.txt
index 5a2d1bed0..febdf5769 100644
--- a/doc/appendix/guides/centos.txt
+++ b/doc/appendix/guides/centos.txt
@@ -185,21 +185,15 @@ line of ``bcfg2.conf``. Then create Packages layout (as per
<Sources>
<!-- CentOS (5.4) sources -->
- <YUMSource>
- <Group>centos-5.4</Group>
- <RawURL>http://mrepo/centos5-x86_64/RPMS.os</RawURL>
+ <Source type="yum" rawurl="http://mrepo/centos5-x86_64/RPMS.os">
<Arch>x86_64</Arch>
- </YUMSource>
- <YUMSource>
- <Group>centos-5.4</Group>
- <RawURL>http://mrepo/centos5-x86_64/RPMS.updates</RawURL>
+ </Source>
+ <Source type="yum" rawurl="http://mrepo/centos5-x86_64/RPMS.updates">
<Arch>x86_64</Arch>
- </YUMSource>
- <YUMSource>
- <Group>centos-5.4</Group>
- <RawURL>http://mrepo/centos5-x86_64/RPMS.extras</RawURL>
+ </Source>
+ <Source type="yum" rawurl="http://mrepo/centos5-x86_64/RPMS.extras">
<Arch>x86_64</Arch>
- </YUMSource>
+ </Source>
</Sources>
Due to the :ref:`server-plugins-generators-packages-magic-groups`,
diff --git a/doc/development/compat.txt b/doc/development/compat.txt
index 90df45676..f90274ce5 100644
--- a/doc/development/compat.txt
+++ b/doc/development/compat.txt
@@ -100,6 +100,8 @@ behavior (e.g., :func:`input`) do not cause unexpected side-effects.
+---------------------------------+--------------------------------------------------+---------------------------------------------------------+
| long | :func:`long` | :func:`int` |
+---------------------------------+--------------------------------------------------+---------------------------------------------------------+
+| cmp | :func:`cmp` | Not implemented |
++---------------------------------+--------------------------------------------------+---------------------------------------------------------+
Python 2.4 compatibility
------------------------
diff --git a/doc/development/core.txt b/doc/development/core.txt
index 3607533ea..886a5538b 100644
--- a/doc/development/core.txt
+++ b/doc/development/core.txt
@@ -71,6 +71,11 @@ XML-RPC Server
.. automodule:: Bcfg2.SSLServer
+Multiprocessing Core
+--------------------
+
+.. automodule:: Bcfg2.Server.MultiprocessingCore
+
CherryPy Core
-------------
diff --git a/doc/development/documentation.txt b/doc/development/documentation.txt
index 2a3cf46d1..4d8a7c9f8 100644
--- a/doc/development/documentation.txt
+++ b/doc/development/documentation.txt
@@ -8,13 +8,14 @@
There are two parts of documentation in the Bcfg2 project:
-* The wiki
-* The manual
+* The Wiki_
+* The Manual_
The wiki
========
.. _Wiki: http://bcfg2.org
+.. _Manual: http://docs.bcfg2.org
.. _Trac: http://trac.edgewall.org/
.. _OpenID: https://openid.org/
.. _MCS: http://www.mcs.anl.gov/
@@ -31,9 +32,9 @@ The manual
==========
.. _rst: http://en.wikipedia.org/wiki/ReStructuredText
.. _Sphinx: http://sphinx.pocoo.org
-.. _Docutils:
+.. _Docutils: http://docutils.sourceforge.net
-The source for the manual is located in the ``doc/`` directory in the
+The source for the Manual_ is located in the ``doc/`` directory in the
git repository or in the source tarball. All files are written in
rst_ (ReStructuredText) format. Sphinx_ is used to build the
documentation from the restructured text sources.
@@ -49,11 +50,20 @@ Building the Manual
apt-get -t lenny-backports install python-sphinx
- * The needed tools for Fedora based systems are in the `Fedora
+ * The tools for Fedora based systems are in the `Fedora
Package Collection <https://admin.fedoraproject.org/pkgdb>`_;
installation can be done easily with Yum::
yum -y install python-sphinx python-docutils
+
+ * The tools for RHEL6-based systems are in the base distribution; you can install them with Yum::
+
+ yum -y install python-sphinx python-docutils
+
+ * The tools for RHEL5-based systems are in the `Extra Packages for Enterprise Linux(EPEL) <https://fedoraproject.org/wiki/EPEL>`_ repository; if your system is configured for EPEL, you can install them with Yum::
+
+ yum -y install python-sphinx python-docutils
+
* Additionally, to build the PDF version:
@@ -80,14 +90,14 @@ Documentation Style Guide for Bcfg2
===================================
This is a style guide to use when creating documentation for Bcfg2. It
-is meant to be helpful, not a hinderence.
+is meant to be helpful, not a hindrance.
Basics
------
**Bcfg2**
- When referring to project, Bcfg2 is the preferred use of cases.
+ When referring to project, Bcfg2 is the preferred use of case.
**Monospace fonts**
@@ -97,8 +107,7 @@ Basics
**Repository**
When used alone this refers to a Bcfg2 :term:`repository`. When there
- is a chance for confusion, for instance in documents also talking
- about :term:`VCS`, be sure to use the longer Bcfg2 :term:`repository`.
+ is a chance for confusion, for instance in documents that also discuss :term:`VCS`, be sure to use the longer phrase "Bcfg2 :term:`repository`".
Sections
--------
diff --git a/doc/development/lint.txt b/doc/development/lint.txt
new file mode 100644
index 000000000..6a4651f92
--- /dev/null
+++ b/doc/development/lint.txt
@@ -0,0 +1,167 @@
+.. -*- mode: rst -*-
+
+.. _development-lint:
+
+===============================
+ bcfg2-lint Plugin Development
+===============================
+
+``bcfg2-lint``, like most parts of Bcfg2, has a pluggable backend that
+lets you easily write your own plugins to verify various parts of your
+Bcfg2 specification.
+
+Plugins are loaded in one of two ways:
+
+* They may be included in a module of the same name as the plugin
+ class in :mod:`Bcfg2.Server.Lint`, e.g.,
+ :mod:`Bcfg2.Server.Lint.Validate`.
+* They may be included directly in a Bcfg2 server plugin, called
+ "<plugin>Lint", e.g.,
+ :class:`Bcfg2.Server.Plugins.Metadata.MetadataLint`.
+
+Plugin Types
+============
+
+There are two types of ``bcfg2-lint`` plugins:
+
+Serverless plugins
+------------------
+
+Serverless plugins are run before ``bcfg2-lint`` starts up a local
+Bcfg2 server, so the amount of introspection they can do is fairly
+limited. They can directly examine the Bcfg2 specification, of
+course, but they can't examine the entries handled by a given plugin
+or anything that requires a running server.
+
+If a serverless plugin raises a lint error, however, the server will
+not be started and no `Server plugins`_ will be run. This makes them
+useful to check for the sorts of errors that might prevent the Bcfg2
+server from starting properly.
+
+Serverless plugins must subclass
+:class:`Bcfg2.Server.Lint.ServerlessPlugin`.
+
+:mod:`Bcfg2.Server.Lint.Validate` is an example of a serverless
+plugin.
+
+Server plugins
+--------------
+
+Server plugins are run after a local Bcfg2 server has been started,
+and have full access to all of the parsed data and so on. Because of
+this, they tend to be easier to use than `Serverless plugins`_, and
+thus are more common.
+
+Server plugins are only run if all `Serverless plugins`_ run
+successfully (i.e., raise no errors).
+
+Server plugins must subclass :class:`Bcfg2.Server.Lint.ServerPlugin`.
+
+:mod:`Bcfg2.Server.Lint.Genshi` is an example of a server plugin.
+
+Error Handling
+==============
+
+The job of a ``bcfg2-lint`` plugin is to find errors. Each error that
+a plugin may produce must have a name, a short string that briefly
+describes the error and will be used to configure error levels in
+``bcfg2.conf``. It must also have a default reporting level.
+Possible reporting levels are "error", "warning", or "silent". All of
+the errors that may be produced by a plugin must be returned as a dict
+by :func:`Bcfg2.Server.Lint.Plugin.Errors`. For instance, consider
+:func:`Bcfg2.Server.Lint.InfoXML.InfoXML.Errors`:
+
+.. code-block:: python
+
+ @classmethod
+ def Errors(cls):
+ return {"no-infoxml": "warning",
+ "deprecated-info-file": "warning",
+ "paranoid-false": "warning",
+ "required-infoxml-attrs-missing": "error"}
+
+This means that the :class:`Bcfg2.Server.Lint.InfoXML.InfoXML` lint
+plugin can produce five lint errors, although four of them are just
+warnings by default.
+
+The errors returned by each plugin's ``Errors()`` method will be
+passed to :func:`Bcfg2.Server.Lint.ErrorHandler.RegisterErrors`, which
+will use that information and the information in the config file to
+determine how to display (or not display) each error to the end user.
+
+Errors are produced in a plugin with
+:func:`Bcfg2.Server.Lint.Plugin.LintError`, which takes two arguments:
+the name of the error, which must correspond to a key in the dict
+returned by :func:`Bcfg2.Server.Lint.Plugin.Errors`, and a freeform
+string that will be displayed to the end user. Note that the error
+name and its display are thus only tied together when the error is
+produced; that is, a single error (by name) can have two completely
+different outputs.
+
+Basics
+======
+
+.. automodule:: Bcfg2.Server.Lint
+
+Existing ``bcfg2-lint`` Plugins
+===============================
+
+BundlerLint
+-----------
+
+.. autoclass:: Bcfg2.Server.Plugins.Bundler.BundlerLint
+
+Comments
+--------
+
+.. automodule:: Bcfg2.Server.Lint.Comments
+
+Genshi
+------
+
+.. automodule:: Bcfg2.Server.Lint.Genshi
+
+GroupNames
+----------
+
+.. automodule:: Bcfg2.Server.Lint.GroupNames
+
+GroupPatternsLint
+-----------------
+
+.. autoclass:: Bcfg2.Server.Plugins.GroupPatterns.GroupPatternsLint
+
+InfoXML
+-------
+
+.. automodule:: Bcfg2.Server.Lint.InfoXML
+
+MergeFiles
+----------
+
+.. automodule:: Bcfg2.Server.Lint.MergeFiles
+
+MetadataLint
+------------
+
+.. autoclass:: Bcfg2.Server.Plugins.Metadata.MetadataLint
+
+PkgmgrLint
+----------
+
+.. autoclass:: Bcfg2.Server.Plugins.Pkgmgr.PkgmgrLint
+
+RequiredAttrs
+-------------
+
+.. automodule:: Bcfg2.Server.Lint.RequiredAttrs
+
+TemplateHelperLint
+------------------
+
+.. autoclass:: Bcfg2.Server.Plugins.TemplateHelper.TemplateHelperLint
+
+Validate
+--------
+
+.. automodule:: Bcfg2.Server.Lint.Validate
diff --git a/doc/development/plugins.txt b/doc/development/plugins.txt
index 593c2f83e..3f2a888ac 100644
--- a/doc/development/plugins.txt
+++ b/doc/development/plugins.txt
@@ -213,4 +213,4 @@ See Also
--------
* :ref:`development-compat`
-* :ref:`development-utils
+* :ref:`development-utils`
diff --git a/doc/exts/xmlschema.py b/doc/exts/xmlschema.py
index 24cbf2e2d..c26aed81e 100644
--- a/doc/exts/xmlschema.py
+++ b/doc/exts/xmlschema.py
@@ -115,6 +115,7 @@ class _XMLDirective(Directive):
def run(self):
name = self.arguments[0]
env = self.state.document.settings.env
+ reporter = self.state.memo.reporter
ns_name = self.options.get('namespace')
try:
ns_uri = env.xmlschema_namespaces[ns_name]
@@ -129,8 +130,9 @@ class _XMLDirective(Directive):
except KeyError:
pass
else:
- env.app.error("No XML %s %s found" %
- (" or ".join(self.types), name))
+ reporter.error("No XML %s %s found" %
+ (" or ".join(self.types), name))
+ return []
documentor = XMLDocumentor(entity, env, self.state, name=name,
ns_uri=ns_uri,
include=self.process_include(),
@@ -172,6 +174,7 @@ class XMLDocumentor(object):
self.include = include
self.options = options
self.app = self.env.app
+ self.reporter = self.state.memo.reporter
if name is None:
self.ns_uri = ns_uri
@@ -312,7 +315,8 @@ class XMLDocumentor(object):
rv.extend(doc.document_complexType())
return rv
else:
- self.app.error("Unknown element type %s" % fqtype)
+ self.reporter.error("Unknown element type %s" % fqtype)
+ return []
else:
rv = []
typespec = self.entity.xpath("xs:complexType", namespaces=NSMAP)[0]
diff --git a/doc/help/troubleshooting.txt b/doc/help/troubleshooting.txt
index aac831ae0..72fec4c63 100644
--- a/doc/help/troubleshooting.txt
+++ b/doc/help/troubleshooting.txt
@@ -54,7 +54,7 @@ the debug level individually on a given plugin, e.g.::
bcfg2-admin xcmd Probes.toggle_debug
Finally, the File Activity Monitor has its own analogue to these two
-methods, for setting the debug level of the FAM:
+methods, for setting the debug level of the FAM::
bcfg2-admin xcmd Inotify.toggle_debug
bcfg2-admin xcmd Inotify.set_debug false
diff --git a/doc/installation/distributions.txt b/doc/installation/distributions.txt
index 3dcfd7721..9db111682 100644
--- a/doc/installation/distributions.txt
+++ b/doc/installation/distributions.txt
@@ -66,19 +66,7 @@ This way is not recommended on production systems. Only for testing.
Gentoo
======
-Early in July 2008, Bcfg2 was added to the Gentoo portage tree. So far
-it's still keyworded for all architectures, but we are actively working
-to get it marked as stable.
-
-If you don't use portage to install Bcfg2, you'll want to make sure you
-have all the prerequisites installed first. For a server, you'll need:
-
-* ``app-admin/gamin`` or ``app-admin/fam``
-* ``dev-python/lxml``
-
-Clients will need at least:
-
-* ``app-portage/gentoolkit``
+Bcfg2 can be installed via portage.
OS X
====
diff --git a/doc/reports/dynamic.txt b/doc/reports/dynamic.txt
index b3028e9e1..6b8a1f467 100644
--- a/doc/reports/dynamic.txt
+++ b/doc/reports/dynamic.txt
@@ -39,7 +39,7 @@ Prerequisites
* sqlite3
* pysqlite2 (if using python 2.4)
-* `Django <http://www.djangoproject.com>`_ >= 1.2
+* `Django <http://www.djangoproject.com>`_ >= 1.3
* mod-wsgi
.. warning::
@@ -53,40 +53,41 @@ Prerequisites
Install
-------
-Be sure to include the specified fields included in the example
-``bcfg2.conf`` file. These can be specified in either ``/etc/bcfg2.conf``,
-if it is readable by the webserver user, or ``/etc/bcfg2-web.conf``. Any
-database supported by `Django <http://www.djangoproject.com>`_ can be used.
-As of version 1.3, `South <http://south.aeracode.org>`_ is used to control
-schema changes. If your database is not supported by South, any updates
-will need to be applied manually. Sqlite is configured by default.
-Please see the :ref:`reporting-databases` section to configure alternative
-databases.
-.. warning::
+1. Be sure to include the specified fields included in the example
+ ``bcfg2.conf`` file. These can be specified in either
+ ``/etc/bcfg2.conf``, if it is readable by the webserver user,
+ or ``/etc/bcfg2-web.conf``. Any database supported by `Django
+ <http://www.djangoproject.com>`_ can be used. As of version 1.3,
+ `South <http://south.aeracode.org>`_ is used to control schema changes.
+ If your database is not supported by South, any updates will need to
+ be applied manually. Sqlite is configured by default. Please see the
+ :ref:`reporting-databases` section to configure alternative databases.
- If you are using an sqlite database, the directory containing the
- database file will need to be writable by the web server. The reason
- for this is that sqlite will create another file for its journal
- when it tries to update the database file.
+ .. warning::
-.. note::
+ If you are using an sqlite database, the directory containing the
+ database file will need to be writable by the web server. The reason
+ for this is that sqlite will create another file for its journal
+ when it tries to update the database file.
+
+ .. note::
- Distributed environments can share a single remote database for
- reporting.
+ Distributed environments can share a single remote database for
+ reporting.
-After configuring your database be sure to run `bcfg2-admin reports init`
-to create the schema.
+2. After configuring your database be sure to run ``bcfg2-admin reports
+ init`` to create the schema.
-To enable statistics collection in the bcfg2-server, add
-:ref:`server-plugins-statistics-reporting` to the **plugins**
-line in your ``bcfg2.conf`` and restart the bcfg2-server. A report collecting
-daemon should be run to import the collected statistics into the backend.
-Please see the section :ref:`Report Collector <report_collector>` for more
-information.
+3. To enable statistics collection in the bcfg2-server, add
+ :ref:`server-plugins-statistics-reporting` to the **plugins**
+ line in your ``bcfg2.conf`` and restart the bcfg2-server. A report
+ collecting daemon should be run to import the collected statistics
+ into the backend. Please see the section :ref:`Report Collector
+ <report_collector>` for more information.
-Detailed installation instructions can be found :ref:`here
-<appendix-guides-web-reports-install>`.
+ Detailed installation instructions can be found :ref:`here
+ <appendix-guides-web-reports-install>`.
.. _dynamic-http-install:
@@ -175,7 +176,7 @@ Upgrading
.. note::
After the database is upgraded all of the old tables are left
- intact. To remove them any table starting with reports_ can
+ intact. To remove them any table starting with **reports\_** can
be dropped.
4. `(Optional)` Run the :ref:`Report Collector <report_collector>`
@@ -199,11 +200,6 @@ An example using the defaults is listed below::
host =
port =
- [statistics]
- config = /etc/bcfg2-web.conf
- time_zone =
- web_debug = False
-
[reporting]
transport = DirectStore
web_prefix =
@@ -241,6 +237,8 @@ section:
statistics
^^^^^^^^^^
+.. deprecated: 1.3.0
+
* config: The config file to be read for additional reporting
data. This is used to restrict what can be read by the web
server.
diff --git a/doc/server/configuration.txt b/doc/server/configuration.txt
index 2c5879ff0..7892c2612 100644
--- a/doc/server/configuration.txt
+++ b/doc/server/configuration.txt
@@ -20,6 +20,9 @@ Bcfg2, although it has become easier in 1.3.0. The steps to do so are
described in three sections below: Common steps for all versions;
steps for older versions only; and steps for 1.3.0.
+Many of the steps below may have already been performed by your OS
+packages.
+
Common Steps
------------
@@ -131,7 +134,7 @@ is even invoked.
Restart ``bcfg2-server`` and you should see it running as non-root in
``ps`` output::
- % ps -ef | grep '[b]cfg2-server'
+ % ps -ef | grep '[b]cfg2-server'
1000 11581 1 0 07:55 ? 00:00:15 python usr/sbin/bcfg2-server -C /etc/bcfg2.conf -D /var/run/bcfg2-server/bcfg2-server.pid
Steps on Bcfg2 1.3.0
@@ -159,7 +162,7 @@ natively in 1.3. Simply add the following lines to ``bcfg2.conf``::
Restart ``bcfg2-server`` and you should see it running as non-root in
``ps`` output::
- % ps -ef | grep '[b]cfg2-server'
+ % ps -ef | grep '[b]cfg2-server'
1000 11581 1 0 07:55 ? 00:00:15 python usr/sbin/bcfg2-server -C /etc/bcfg2.conf -D /var/run/bcfg2-server/bcfg2-server.pid
.. _server-backends:
@@ -169,10 +172,11 @@ Server Backends
.. versionadded:: 1.3.0
-Bcfg2 supports two different server backends: a builtin server
-based on the Python SimpleXMLRPCServer object, and a server that uses
-CherryPy (http://www.cherrypy.org). Each one has advantages and
-disadvantages.
+Bcfg2 supports three different server backends: a builtin server based
+on the Python SimpleXMLRPCServer object; a server that uses CherryPy
+(http://www.cherrypy.org); and a version of the builtin server that
+uses the Python :mod:`multiprocessing` module. Each one has
+advantages and disadvantages.
The builtin server:
@@ -181,27 +185,36 @@ The builtin server:
* Works on Python 2.4;
* Is slow with larger numbers of clients.
+The multiprocessing server:
+
+* Leverages most of the stability and maturity of the builtin server,
+ but does have some new bits;
+* Introduces concurrent processing to Bcfg2, which may break in
+ various edge cases;
+* Supports certificate authentication;
+* Requires Python 2.6;
+* Is faster with large numbers of concurrent runs.
+
The CherryPy server:
* Is very new and potentially buggy;
* Does not support certificate authentication yet, only password
authentication;
-* Requires CherryPy 3.2, which requires Python 2.5;
+* Requires CherryPy 3.3, which requires Python 2.5;
* Is smarter about daemonization, particularly if you are
:ref:`server-dropping-privs`;
* Is faster with large numbers of clients.
Basically, the builtin server should be used unless you have a
-particular need for performance, and can sacrifice certificate
-authentication.
+particular need for performance. The CherryPy server is purely
+experimental at this point.
To select which backend to use, set the ``backend`` option in the
``[server]`` section of ``/etc/bcfg2.conf``. Options are:
* ``cherrypy``
* ``builtin``
+* ``multiprocessing``
* ``best`` (the default; currently the same as ``builtin``)
-If the certificate authentication issues (a limitation in CherryPy
-itself) can be resolved and the CherryPy server proves to be stable,
-it will likely become the default (and ``best``) in a future release.
+``best`` may change in future releases.
diff --git a/doc/server/database.txt b/doc/server/database.txt
index 87d3e3afe..b0ec7b571 100644
--- a/doc/server/database.txt
+++ b/doc/server/database.txt
@@ -34,9 +34,10 @@ of ``/etc/bcfg2.conf``.
+-------------+------------------------------------------------------------+-------------------------------+
| Option name | Description | Default |
+=============+============================================================+===============================+
-| engine | The full name of the Django database backend to use. See | "django.db.backends.sqlite3" |
+| engine | The name of the Django database backend to use. See | "sqlite3" |
| | https://docs.djangoproject.com/en/dev/ref/settings/#engine | |
-| | for available options | |
+| | for available options (note that django.db.backends is not | |
+| | included in the engine name) | |
+-------------+------------------------------------------------------------+-------------------------------+
| name | The name of the database | "/var/lib/bcfg2/bcfg2.sqlite" |
+-------------+------------------------------------------------------------+-------------------------------+
diff --git a/doc/server/encryption.txt b/doc/server/encryption.txt
index b56487620..e31124d4b 100644
--- a/doc/server/encryption.txt
+++ b/doc/server/encryption.txt
@@ -12,7 +12,8 @@ Bcfg2 supports encrypting some data on the disk, which can help
protect sensitive data from other people who need access to the Bcfg2
repository but are perhaps not authorized to see all data. It
supports multiple passphrases, which can be used to enforce
-separations between teams, environments, etc.
+separations between teams, environments, etc. Use of the encryption
+feature requires M2Crypto 0.18 or newer.
.. note::
diff --git a/doc/server/plugins/connectors/grouplogic.txt b/doc/server/plugins/connectors/grouplogic.txt
new file mode 100644
index 000000000..abf425202
--- /dev/null
+++ b/doc/server/plugins/connectors/grouplogic.txt
@@ -0,0 +1,122 @@
+.. -*- mode: rst -*-
+
+.. _server-plugins-connectors-grouplogic:
+
+==========
+GroupLogic
+==========
+
+.. versionadded:: 1.3.2
+
+GroupLogic is a connector plugin that lets you use an XML Genshi
+template to dynamically set additional groups for clients.
+
+Usage
+=====
+
+To use the GroupLogic plugin, first do ``mkdir
+/var/lib/bcfg2/GroupLogic``. Add ``GroupLogic`` to your ``plugins``
+line in ``/etc/bcfg2.conf``. Next, create
+``/var/lib/bcfg2/GroupLogic/groups.xml``:
+
+.. code-block:: xml
+
+ <GroupLogic xmlns:py="http://genshi.edgewall.org/">
+ </GroupLogic>
+
+``groups.xml`` is structured very similarly to the
+:ref:`server-plugins-grouping-metadata` ``groups.xml``. A Group tag
+that contains no children is a declaration of membership; a Group or
+Client tag that does contain children is a conditional.
+
+Unlike ``Metadata/groups.xml``, GroupLogic supports genshi templating,
+so you can dynamically create groups. ``GroupLogic/groups.xml`` is
+rendered for each client, and the groups set in it are added to the
+client metadata.
+
+.. note::
+
+ Also unlike ``Metadata/groups.xml``, GroupLogic can not be used to
+ associate bundles with clients directly, or to negate groups. But
+ you can use GroupLogic to assign a group that is associated with a
+ bundle in Metadata.
+
+Consider the case where you have four environments -- dev, test,
+staging, and production -- and four components to a web application --
+the frontend, the API, the database server, and the caching proxy. In
+order to make files specific to the component *and* to the
+environment, you need groups to describe each combination:
+webapp-frontend-dev, webapp-frontend-test, and so on. You *could* do
+this in ``Metadata/groups.xml``:
+
+.. code-block:: xml
+
+ <Groups>
+ <Group name="webapp-frontend">
+ <Group name="dev">
+ <Group name="webapp-frontend-dev"/>
+ </Group>
+ <Group name="test">
+ <Group name="webapp-frontend-test"/>
+ </Group>
+ ...
+ </Group>
+ <Group name="webapp-api">
+ ...
+ </Group>
+ ...
+ </Groups>
+
+Creating the sixteen groups this way is incredibly tedious, and this
+is a quite *small* site. GroupLogic can automate this process.
+
+Assume that we've declared the groups thusly in
+``Metadata/groups.xml``:
+
+.. code-block:: xml
+
+ <Groups>
+ <Group name="webapp-frontend" category="webapp-component"/>
+ <Group name="webapp-api" category="webapp-component"/>
+ <Group name="webapp-db" category="webapp-component"/>
+ <Group name="webapp-proxy" category="webapp-component"/>
+ <Group name="dev" category="environment"/>
+ <Group name="test" category="environment"/>
+ <Group name="staging" category="environment"/>
+ <Group name="prod" category="environment"/>
+ </Groups>
+
+One way to automate the creation of the groups would be to simply
+generate the tedious config:
+
+.. code-block:: xml
+
+ <GroupLogic xmlns:py="http://genshi.edgewall.org/">
+ <py:for each="component in metadata.query.all_groups_in_category("webapp-component")>
+ <Group name="${component}">
+ <py:for each="env in metadata.query.all_groups_in_category("environment")>
+ <Group name="${env}">
+ <Group name="${component}-${env}"/>
+ </Group>
+ </py:for>
+ </Group>
+ </py:for>
+ </GroupLogic>
+
+But, since ``GroupLogic/groups.xml`` is rendered for each client
+individually, there's a more elegant way to accomplish the same thing:
+
+.. code-block:: xml
+
+ <GroupLogic xmlns:py="http://genshi.edgewall.org/">
+ <?python
+ component = metadata.group_in_category("webapp-component")
+ env = metadata.group_in_category("environment")
+ ?>
+ <py:if test="component and env">
+ <Group name="${component}-${env}"/>
+ </py:if>
+ </GroupLogic>
+
+This gets only the component and environment for the current client,
+and, if both are set, sets the single appropriate group.
diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt
index f31923866..e3768a3ba 100644
--- a/doc/server/plugins/generators/cfg.txt
+++ b/doc/server/plugins/generators/cfg.txt
@@ -596,6 +596,11 @@ Deltas
cat file functionality. ``bcfg2-lint`` checks for deltas and
warns about them.
+.. warning::
+
+ In Bcfg2 1.3, deltas **do not** work with `SSH key or
+ authorized_keys generation <SSH Keys>`_.
+
Bcfg2 has finer grained control over how to deliver configuration
files to a host. Let's say we have a Group named file-server. Members
of this group need the exact same ``/etc/motd`` as all other hosts except
@@ -632,23 +637,35 @@ server and we have the following configuration files::
motd.G01_web-server
motd.G01_mail-server.cat
motd.G02_file-server.cat
+ motd.H_bar.example.com
motd.H_foo.example.com.cat
-If our machine **isn't** *foo.example.com* then here's what would happen:
-
-Bcfg2 would choose ``motd.G01_web-server`` as the base file. It is
-the most specific base file for this host. Bcfg2 would apply the
-``motd.G01_mail-server.cat`` delta to the ``motd.G01_web-server``
-base file. It is the least specific delta. Bcfg2 would then apply the
-``motd.G02_file-server.cat`` delta to the result of the delta before
-it. If our machine **is** *foo.example.com* then here's what would happen:
-
-Bcfg2 would choose ``motd.G01_web-server`` as the base file. It
-is the most specific base file for this host. Bcfg2 would apply the
-``motd.H_foo.example.com.cat`` delta to the ``motd.G01_web-server`` base
-file. The reason the other deltas aren't applied to *foo.example.com*
-is because a **.H_** delta is more specific than a **.G##_** delta. Bcfg2
-applies all the deltas at the most specific level.
+If our machine isn't *foo.example.com* or *bar.example.com*, but
+is a web server, then Bcfg2 would choose ``motd.G01_web-server`` as
+the base file. It is the most specific base file for this host. Bcfg2
+would apply the ``motd.G01_mail-server.cat`` delta to the
+``motd.G01_web-server`` base file. It is the least specific
+delta. Bcfg2 would then apply the ``motd.G02_file-server.cat`` delta
+to the result of the delta before it.
+
+If our machine is *foo.example.com* and a web server, then Bcfg2 would
+choose ``motd.G01_web-server`` as the base file. It is the most
+specific base file for this host. Bcfg2 would apply the
+``motd.H_foo.example.com.cat`` delta to the ``motd.G01_web-server``
+base file. The reason the other deltas aren't applied to
+*foo.example.com* is because a **.H_** delta is more specific than a
+**.G##_** delta. Bcfg2 applies all the deltas at the most specific
+level.
+
+If our machine is *bar.example.com*, then Bcfg2 would chose
+``motd.H_foo.example.com`` as the base file because it is the most
+specific base file for this host. Regardless of the groups
+*bar.example.com* is a member of, **no cat files** would be applied,
+because only cat files as specific or more specific than the base file
+are applied. (In other words, if a group-specific base file is
+selected, only group- or host-specific cat files can be applied; if a
+host-specific base file is selected, only host-specific cat files can
+be applied.)
.. _server-plugins-generators-cfg-validation:
diff --git a/doc/server/plugins/generators/packages.txt b/doc/server/plugins/generators/packages.txt
index 73145fd6b..cdc4f7282 100644
--- a/doc/server/plugins/generators/packages.txt
+++ b/doc/server/plugins/generators/packages.txt
@@ -252,7 +252,8 @@ something like this:
<Group name="ubuntu-intrepid">
<Source type="apt"
url="http://us.archive.ubuntu.com/ubuntu"
- version="intrepid">
+ version="intrepid"
+ debsrc="true">
<Component>main</Component>
<Component>universe</Component>
<Arch>i386</Arch>
@@ -280,7 +281,7 @@ something like this:
.. warning:: You must regenerate the Packages cache when adding or
removing the recommended attribute (``bcfg2-admin xcmd
- Packages.Refresh``).
+ Packages.Refresh``).
.. [#f1] Bcfg2 will by default add **Essential** packages to the
client specification. You can disable this behavior by
@@ -434,7 +435,7 @@ configs. Simply add entries like these to the appropriate bundles:
.. code-block:: xml
<Path name="/etc/yum.repos.d/bcfg2.repo"/>
- <Path name="/etc/apt/sources.d/bcfg2"/>
+ <Path name="/etc/apt/sources.list.d/bcfg2-packages-generated-sources.list"/>
If you want to change the path to either of those files, you can set
``yum_config`` or ``apt_config`` in ``bcfg2.conf`` to the path to the
@@ -702,25 +703,25 @@ It understands the following directives:
[packages] section
------------------
-+-------------+------------------------------------------------------+----------+-----------------------------+
-| Name | Description | Values | Default |
-+=============+======================================================+==========+=============================+
-| resolver | Enable dependency resolution | Boolean | True |
-+-------------+------------------------------------------------------+----------+-----------------------------+
-| metadata | Enable metadata processing. Disabling ``metadata`` | Boolean | True |
-| | implies disabling ``resolver`` as well. | | |
-+-------------+------------------------------------------------------+----------+-----------------------------+
-| yum_config | The path at which to generate Yum configs. | String | /etc/yum.repos.d/bcfg2.repo |
-+-------------+------------------------------------------------------+----------+-----------------------------+
-| apt_config | The path at which to generate APT configs. | String | /etc/apt/sources.d/bcfg2 |
-+-------------+------------------------------------------------------+----------+-----------------------------+
-| gpg_keypath | The path on the client RPM GPG keys will be copied | String | /etc/pki/rpm-gpg |
-| | to before they are imported on the client. | | |
-+-------------+------------------------------------------------------+----------+-----------------------------+
-| version | Set the version attribute used when binding Packages | any|auto | auto |
-+-------------+------------------------------------------------------+----------+-----------------------------+
-| cache | Path where Packages will store its cache | String | <repo>/Packages/cache |
-+-------------+------------------------------------------------------+----------+-----------------------------+
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
+| Name | Description | Values | Default |
++=============+======================================================+==========+===================================================================+
+| resolver | Enable dependency resolution | Boolean | True |
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
+| metadata | Enable metadata processing. Disabling ``metadata`` | Boolean | True |
+| | implies disabling ``resolver`` as well. | | |
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
+| yum_config | The path at which to generate Yum configs. | String | /etc/yum.repos.d/bcfg2.repo |
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
+| apt_config | The path at which to generate APT configs. | String | /etc/apt/sources.list.d/bcfg2-packages-generated-sources.list |
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
+| gpg_keypath | The path on the client RPM GPG keys will be copied | String | /etc/pki/rpm-gpg |
+| | to before they are imported on the client. | | |
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
+| version | Set the version attribute used when binding Packages | any|auto | auto |
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
+| cache | Path where Packages will store its cache | String | <repo>/Packages/cache |
++-------------+------------------------------------------------------+----------+-------------------------------------------------------------------+
[packages:yum] section
diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt
index 2789411e7..2493be53f 100644
--- a/doc/server/plugins/generators/rules.txt
+++ b/doc/server/plugins/generators/rules.txt
@@ -117,8 +117,13 @@ 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.
+the context of the file to the default set by policy. If a file has
+no default context rule, and you don't wish to set one, you can
+specify ``secontext=''`` (i.e., an empty ``secontext``), in which case
+the client will not try to manage the SELinux context of the file at
+all.
+
+See :ref:`server-selinux` for more information.
Attributes common to all Path tags:
@@ -390,9 +395,9 @@ For example:
<POSIXUser name="daemon" home="/sbin" shell="/sbin/nologin"
gecos="daemon" uid="2" group="daemon">
- <MemberOf>lp</MemberOf>
- <MemberOf>adm</MemberOf>
- <MemberOf>bin</MemberOf>
+ <MemberOf group="lp"/>
+ <MemberOf group="adm"/>
+ <MemberOf group="bin/>
</POSIXUser>
The group specified will automatically be created if it does not
diff --git a/doc/server/plugins/generators/sslca.txt b/doc/server/plugins/generators/sslca.txt
index cab7eb233..7ef358a31 100644
--- a/doc/server/plugins/generators/sslca.txt
+++ b/doc/server/plugins/generators/sslca.txt
@@ -156,7 +156,7 @@ Example
.. code-block:: xml
<CertInfo>
- <SubjectAltName>test.example.com</SubjectAltName>
+ <subjectAltName>test.example.com</subjectAltName>
<Group name="apache">
<Cert key="/etc/pki/tls/private/foo.key" days="730"/>
</Group>
diff --git a/doc/server/plugins/generators/tcheetah.txt b/doc/server/plugins/generators/tcheetah.txt
index ab147ce56..c79a8ced5 100644
--- a/doc/server/plugins/generators/tcheetah.txt
+++ b/doc/server/plugins/generators/tcheetah.txt
@@ -99,7 +99,7 @@ Simple Example
==============
TCheetah works similar to Cfg in that you define all literal information
-about a particular file in a directory rooted at TGenshi/path_to_file.
+about a particular file in a directory rooted at TCheetah/path_to_file.
The actual file contents are placed in a file named `template` in that
directory. Below is a simple example a file ``/foo``.
diff --git a/doc/server/plugins/grouping/grouppatterns.txt b/doc/server/plugins/grouping/grouppatterns.txt
index 39c632f82..44ffa5066 100644
--- a/doc/server/plugins/grouping/grouppatterns.txt
+++ b/doc/server/plugins/grouping/grouppatterns.txt
@@ -8,7 +8,7 @@ GroupPatterns
The GroupPatterns plugin is a connector that can assign clients
group membership pased on patterns in client hostnames. Two basic
-methods are supported:
+methods are supported:
- regular expressions (NamePatterns)
- ranges (NameRange)
@@ -20,7 +20,7 @@ Setup
=====
#. Enable the GroupPatterns plugin
-#. Create the GroupPatterns/config.xml file (similar to the example below).
+#. Create the ``GroupPatterns/config.xml`` file (similar to the example below).
#. Client groups will be augmented based on the specification
Pattern Types
@@ -52,7 +52,7 @@ Examples
</GroupPattern>
<GroupPattern>
<NamePattern>(.*)</NamePattern>
- <Group>group-$1'</Group>
+ <Group>group-$1</Group>
</GroupPattern>
<GroupPattern>
<NameRange>node[[1-32]]</NameRange>
@@ -82,15 +82,14 @@ GroupPatterns configuration:
<GroupPatterns>
<GroupPattern>
- <NamePattern>^x(\w[^\d|\.]+)\d*\..*</NamePattern>
+ <NamePattern>x(\w[^\d\.]+)\d*\.</NamePattern>
<Group>$1-server</Group>
</GroupPattern>
</GroupPatterns>
Regex explanation:
-#. !^x Match any hostname that begins with "x"
-#. (\w[!^\d|\.]+) followed by one or more word characters that are not a decimal digit or "." and save the string to $1
-#. \d* followed by 0 or more decimal digit(s)
-#. \..* followed by a "."
-#. .* followed by 1 or more of anything else.
+#. ``x`` Match any hostname that begins with "x"
+#. ``(\w[!^\d|\.]+)`` followed by one or more word characters that are not a decimal digit or "." and save the string to $1
+#. ``\d*`` followed by 0 or more decimal digit(s)
+#. ``\.`` followed by a literal "."
diff --git a/doc/server/plugins/grouping/metadata.txt b/doc/server/plugins/grouping/metadata.txt
index fe0d2683e..ceac5dc24 100644
--- a/doc/server/plugins/grouping/metadata.txt
+++ b/doc/server/plugins/grouping/metadata.txt
@@ -119,20 +119,19 @@ a simple ``groups.xml`` file:
<Group name='oracle-server'>
<Group name='selinux-enabled' negate='true'/>
</Group>
- <Client name='foo.eample.com'>
+ <Client name='foo.example.com'>
<Group name='oracle-server'/>
<Group name='apache-server'/>
</Client>
</Groups>
-A Group or Client tag that does not contain any child tags is a
-declaration of membership; a Group or Client tag that does contain
-children is a conditional. So the example above does not assign
-either the ``rhel5`` or ``rhel6`` groups to machines in the
-``mail-server`` group, but conditionally assigns the
-``sendmail-server`` or ``postfix-server`` groups depending on the OS
-of the client. (Presumably in this example the OS groups are set by a
-probe.)
+A Group tag that does not contain any child tags is a declaration of
+membership; a Group or Client tag that does contain children is a
+conditional. So the example above does not assign either the
+``rhel5`` or ``rhel6`` groups to machines in the ``mail-server``
+group, but conditionally assigns the ``sendmail-server`` or
+``postfix-server`` groups depending on the OS of the client.
+(Presumably in this example the OS groups are set by a probe.)
Consequently, a client that is RHEL 5 and a member of the
``mail-server`` profile group would also be a member of the
@@ -223,7 +222,7 @@ to include a different file, or explicit content in the case that the
parent include does not exist.)
Wildcard XInclude
-~~~~~~~~~~~~~~~~~
+-----------------
.. versionadded:: 1.3.1
diff --git a/doc/server/selinux.txt b/doc/server/selinux.txt
index 9f54b0d68..79384970a 100644
--- a/doc/server/selinux.txt
+++ b/doc/server/selinux.txt
@@ -142,13 +142,13 @@ necessary.
Duplicate Entries
-----------------
-It may be necessary to use `BoundSELinux` tags if a single fcontext
+It may be necessary to use `BoundSEFcontext` 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"/>
+ <BoundSEFcontext filetype="symlink"
+ name="/etc/localtime" selinuxtype="etc_t"/>
+ <BoundSEFcontext filetype="regular"
+ name="/etc/localtime" selinuxtype="locale_t"/>
diff --git a/doc/server/snapshots/index.txt b/doc/server/snapshots/index.txt
index 13a9fe2c0..a7e5940ed 100644
--- a/doc/server/snapshots/index.txt
+++ b/doc/server/snapshots/index.txt
@@ -8,9 +8,8 @@ Bcfg2 Snapshots
.. versionadded:: 1.0.0
-This page describes the Snapshots plugin. This plugin is meant to replace
-the older :ref:`reports-dynamic`. It stores various aspects of a client's
-state when the client checks into the server.
+This page describes the Snapshots plugin. Snapshots is deprecated, and
+will be removed in a future release.
Before you begin
================
diff --git a/gentoo/bcfg2-1.3.0.ebuild b/gentoo/bcfg2-1.3.0.ebuild
index e600448d9..4d8530b02 100644
--- a/gentoo/bcfg2-1.3.0.ebuild
+++ b/gentoo/bcfg2-1.3.0.ebuild
@@ -1,13 +1,13 @@
-# Copyright 1999-2012 Gentoo Foundation
+# Copyright 1999-2013 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Header: $
-EAPI="4"
+EAPI=4
-PYTHON_DEPEND="2:2.6"
+PYTHON_DEPEND="*:2.6"
SUPPORT_PYTHON_ABIS="1"
# ssl module required.
-RESTRICT_PYTHON_ABIS="2.4 2.5 3.*"
+RESTRICT_PYTHON_ABIS="2.5"
inherit distutils eutils
@@ -15,7 +15,7 @@ DESCRIPTION="configuration management tool"
HOMEPAGE="http://bcfg2.org"
SRC_URI="ftp://ftp.mcs.anl.gov/pub/bcfg/${P}.tar.gz"
-LICENSE="BSD"
+LICENSE="BSD-2"
SLOT="0"
KEYWORDS="~amd64 ~x86 ~amd64-linux ~x86-linux ~x64-solaris"
IUSE="doc cheetah genshi server"
diff --git a/misc/bcfg2-selinux.spec b/misc/bcfg2-selinux.spec
index 4c05f4959..9c5262dfd 100644
--- a/misc/bcfg2-selinux.spec
+++ b/misc/bcfg2-selinux.spec
@@ -3,7 +3,7 @@
%global pythonversion %{py_ver}
%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
%{!?_initrddir: %global _initrddir %{_sysconfdir}/rc.d/init.d}
-%global selinux_policyver %(%{__sed} -e 's,.*selinux-policy-\\([^/]*\\)/.*,\\1,' /usr/share/selinux/devel/policyhelp || echo 0.0.0)
+%global selinux_policyver %(%{__sed} -e 's,.*selinux-policy-\\([^/]*\\)/.*,\\1,' /usr/share/selinux/devel/policyhelp 2>/dev/null || echo 0.0.0)
%global selinux_types %(%{__awk} '/^#[[:space:]]*SELINUXTYPE=/,/^[^#]/ { if ($3 == "-") printf "%s ", $2 }' /etc/selinux/config 2>/dev/null)
%global selinux_variants %([ -z "%{selinux_types}" ] && echo mls strict targeted || echo %{selinux_types})
diff --git a/misc/bcfg2.spec b/misc/bcfg2.spec
index e6b21d76c..b7e6e43f6 100644
--- a/misc/bcfg2.spec
+++ b/misc/bcfg2.spec
@@ -99,12 +99,11 @@ Requires: bcfg2 = %{version}
Requires: python-ssl
%endif
Requires: python-lxml >= 1.2.1
-%if "%{_vendor}" == "redhat"
-Requires: gamin-python
-%endif
%if 0%{?suse_version}
+Requires: python-pyinotify
Requires: python-python-daemon
%else
+Requires: python-inotify
Requires: python-daemon
%endif
Requires: /usr/sbin/sendmail
@@ -150,10 +149,10 @@ Group: System Tools
Requires: bcfg2 = %{version}
Requires: bcfg2-server = %{version}
-# cherrypy 3.2.3 actually doesn't exist yet, but 3.2.2 has bugs that
+# cherrypy 3.3 actually doesn't exist yet, but 3.2 has bugs that
# prevent it from working:
# https://bitbucket.org/cherrypy/cherrypy/issue/1154/assertionerror-in-recv-when-ssl-is-enabled
-Requires: python-cherrypy > 3.2.2
+Requires: python-cherrypy > 3.3
%description server-cherrypy
Bcfg2 helps system administrators produce a consistent, reproducible,
@@ -190,6 +189,27 @@ Group: Documentation/HTML
%else
Group: Documentation
%endif
+%if 0%{?suse_version}
+BuildRequires: python-M2Crypto
+BuildRequires: python-Genshi
+BuildRequires: python-gamin
+BuildRequires: python-pyinotify
+BuildRequires: python-python-daemon
+BuildRequires: python-CherryPy >= 3
+%else
+BuildRequires: m2crypto
+BuildRequires: python-genshi
+BuildRequires: gamin-python
+BuildRequires: python-inotify
+BuildRequires: python-daemon
+%endif
+
+%if "%{_vendor}" == "redhat" && 0%{?rhel} < 6 && 0%{?fedora} == 0
+BuildRequires: python-ssl
+%else
+BuildRequires: python-cherrypy >= 3
+BuildRequires: python-mock
+%endif
%description doc
Bcfg2 helps system administrators produce a consistent, reproducible,
@@ -275,6 +295,8 @@ This package includes the Bcfg2 reports web frontend.
%{?pythonpath: export PYTHONPATH="%{pythonpath}"}
%{__python}%{pythonversion} setup.py build_sphinx
+sed -i "s/apache2/httpd/g" misc/apache/bcfg2.conf
+
%install
rm -rf %{buildroot}
%{__python}%{pythonversion} setup.py install --root=%{buildroot} --record=INSTALLED_FILES --prefix=/usr
@@ -294,7 +316,7 @@ mkdir -p %{buildroot}%{_defaultdocdir}/bcfg2-server-%{version}
%{__mv} %{buildroot}%{_bindir}/bcfg2* %{buildroot}%{_sbindir}
%{__install} -m 755 debian/bcfg2.init %{buildroot}%{_initrddir}/bcfg2
%{__install} -m 755 debian/bcfg2-server.init %{buildroot}%{_initrddir}/bcfg2-server
-%{__install} -m 755 debian/bcfg2-report-collector.init %{buildroot}%{_initrddir}/bcfg2-report-collector
+%{__install} -m 755 debian/bcfg2-server.bcfg2-report-collector.init %{buildroot}%{_initrddir}/bcfg2-report-collector
%{__install} -m 755 debian/bcfg2.default %{buildroot}%{_sysconfdir}/default/bcfg2
%{__install} -m 755 debian/bcfg2-server.default %{buildroot}%{_sysconfdir}/default/bcfg2-server
%{__install} -m 755 debian/bcfg2.cron.daily %{buildroot}%{_sysconfdir}/cron.daily/bcfg2
@@ -311,7 +333,6 @@ cp -r tools/* %{buildroot}%{_defaultdocdir}/bcfg2-server-%{version}
cp -r build/sphinx/html/* %{buildroot}%{_defaultdocdir}/bcfg2-doc-%{version}
%{__install} -d %{buildroot}%{apache_conf}/conf.d
-sed -i "s/apache2/httpd/g" misc/apache/bcfg2.conf
%{__install} -m 644 misc/apache/bcfg2.conf %{buildroot}%{apache_conf}/conf.d/wsgi_bcfg2.conf
%{__mkdir_p} %{buildroot}%{_localstatedir}/cache/%{name}
@@ -334,6 +355,7 @@ touch %{buildroot}%{_sysconfdir}/bcfg2.conf \
%{python_sitelib}/Bcfg2/Logger.py*
%{python_sitelib}/Bcfg2/Options.py*
%{python_sitelib}/Bcfg2/Proxy.py*
+%{python_sitelib}/Bcfg2/Utils.py*
%{python_sitelib}/Bcfg2/version.py*
%{python_sitelib}/Bcfg2/Client
%{_mandir}/man1/bcfg2.1*
@@ -523,4 +545,3 @@ fi
* Fri Sep 15 2006 Narayan Desai <desai@mcs.anl.gov> - 0.8.4-1
- Initial log
-
diff --git a/redhat/scripts/bcfg2-report-collector.init b/redhat/scripts/bcfg2-report-collector.init
index a8e23f080..43e875a6b 100755
--- a/redhat/scripts/bcfg2-report-collector.init
+++ b/redhat/scripts/bcfg2-report-collector.init
@@ -32,6 +32,7 @@ test -x $DAEMON || exit 5
# Internal variables
BINARY=$(basename $DAEMON)
+RETVAL=0
start () {
echo -n "Starting Configuration Report Collector: "
@@ -79,22 +80,26 @@ status () {
case "$1" in
start)
start
+ RETVAL=$?
;;
stop)
stop
+ RETVAL=$?
;;
status)
status
+ RETVAL=$?
;;
restart|reload|force-reload)
stop
sleep 5
start
+ RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|status|reload|restart|force-reload}"
- exit 1
+ RETVAL=1
;;
esac
-exit 0
+exit $RETVAL
diff --git a/redhat/scripts/bcfg2-server.init b/redhat/scripts/bcfg2-server.init
index ffac6ac3d..c4412d1c3 100755
--- a/redhat/scripts/bcfg2-server.init
+++ b/redhat/scripts/bcfg2-server.init
@@ -59,9 +59,11 @@ stop () {
case "$1" in
start)
start
+ RETVAL=$?
;;
stop)
stop
+ RETVAL=$?
;;
status)
status $prog
@@ -71,6 +73,7 @@ case "$1" in
stop
sleep 5
start
+ RETVAL=$?
;;
*)
echo $"Usage: $0 {start|stop|status|restart|reload|force-reload}"
diff --git a/redhat/scripts/bcfg2.init b/redhat/scripts/bcfg2.init
index 5cfdf47bc..9c26434ff 100755
--- a/redhat/scripts/bcfg2.init
+++ b/redhat/scripts/bcfg2.init
@@ -54,12 +54,14 @@ start () {
case "$1" in
start)
start
+ RETVAL=$?
;;
stop|status)
- exit 0
+ RETVAL=0
;;
restart|force-reload)
start
+ RETVAL=$?
;;
*)
echo "Usage: $0 {start|stop|status|restart|force-reload}"
diff --git a/schemas/grouplogic.xsd b/schemas/grouplogic.xsd
new file mode 100644
index 000000000..bf43bceb3
--- /dev/null
+++ b/schemas/grouplogic.xsd
@@ -0,0 +1,110 @@
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:py="http://genshi.edgewall.org/" xml:lang="en">
+
+ <xsd:annotation>
+ <xsd:documentation>
+ GroupLogic schema for bcfg2
+ </xsd:documentation>
+ </xsd:annotation>
+
+ <xsd:import namespace="http://genshi.edgewall.org/"
+ schemaLocation="genshi.xsd"/>
+
+ <xsd:complexType name="GroupLogicDeclarationType">
+ <xsd:annotation>
+ <xsd:documentation>
+ A **GroupLogicDeclarationType** declares a Group to be added
+ to a client.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute type='xsd:string' name='name' use='required'>
+ <xsd:annotation>
+ <xsd:documentation>
+ The group name
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="GroupLogicType">
+ <xsd:annotation>
+ <xsd:documentation>
+ The top-level tag of a GroupLogic configuration file.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:choice minOccurs="1" maxOccurs="unbounded">
+ <xsd:group ref="py:genshiElements"/>
+ <xsd:element name='Group' type='GroupLogicDeclarationType'/>
+ <xsd:element name='Group' type='GroupLogicContainerType'>
+ <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='GroupLogicContainerType'>
+ <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='GroupLogic' type='GroupLogicType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Nesting GroupLogic tags is allowed in order to support
+ XInclude.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ </xsd:choice>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="GroupLogicContainerType">
+ <xsd:annotation>
+ <xsd:documentation>
+ A **GroupLogicContainerType** is a tag used to provide logic.
+ Child entries of a GroupLogicContainerType tag only apply to
+ machines that match the condition specified -- either
+ membership in a group, or a matching client name.
+ :xml:attribute:`GroupLogicContainerType:negate` can be set to
+ negate the sense of the match.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexContent>
+ <xsd:extension base="GroupLogicType">
+ <xsd:attribute type='xsd:string' name='name' use='required'>
+ <xsd:annotation>
+ <xsd:documentation>
+ The group name
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type='xsd:string' name='negate'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Negate the sense of this group or client; i.e., entries
+ within this tag are only used on clients that are not
+ members of the group, or that have hostnames that do not
+ match.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+
+ <xsd:element name='GroupLogic' type='GroupLogicType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ A GroupLogic file is a genshi file that can be used to
+ dynamically add additional groups to a client.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+</xsd:schema>
diff --git a/schemas/packages.xsd b/schemas/packages.xsd
index dbee2f31b..e538bb0e7 100644
--- a/schemas/packages.xsd
+++ b/schemas/packages.xsd
@@ -167,6 +167,15 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:boolean" name="debsrc">
+ <xsd:annotation>
+ <xsd:documentation>
+ Include ``deb-src`` lines in the generated APT
+ configuration. This only applies to sources with
+ :xml:attribute:`SourceType:type` = ``apt``.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attribute type="xsd:string" name="url">
<xsd:annotation>
<xsd:documentation>
diff --git a/schemas/selinux.xsd b/schemas/selinux.xsd
index 760953e34..3651549f5 100644
--- a/schemas/selinux.xsd
+++ b/schemas/selinux.xsd
@@ -80,6 +80,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:token" name="mlsrange">
+ <xsd:annotation>
+ <xsd:documentation>
+ SELinux MLS range to apply to this port
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
@@ -127,6 +134,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:token" name="mlsrange">
+ <xsd:annotation>
+ <xsd:documentation>
+ SELinux MLS range to apply to files matching this specification
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
@@ -157,6 +171,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:token" name="mlsrange">
+ <xsd:annotation>
+ <xsd:documentation>
+ SELinux MLS range to apply to this node
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
@@ -205,6 +226,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:token" name="mlsrange">
+ <xsd:annotation>
+ <xsd:documentation>
+ SELinux MLS range to apply to this user
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
@@ -235,6 +263,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:token" name="mlsrange">
+ <xsd:annotation>
+ <xsd:documentation>
+ SELinux MLS range to apply to this user
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
@@ -258,6 +293,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute type="xsd:token" name="mlsrange">
+ <xsd:annotation>
+ <xsd:documentation>
+ SELinux MLS range to apply to this interface
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
diff --git a/schemas/servicetype.xsd b/schemas/servicetype.xsd
index 4d5ac7c31..4c7e1b803 100644
--- a/schemas/servicetype.xsd
+++ b/schemas/servicetype.xsd
@@ -34,12 +34,21 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute name="bootstatus" type="BootStatusEnum" default="off">
+ <xsd:annotation>
+ <xsd:documentation>
+ Whether the service should start at boot. The default value
+ corresponds to the value of the status attribute.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
<xsd:attribute name="status" type="StatusEnum" default="off">
<xsd:annotation>
<xsd:documentation>
- Whether the service should start at boot. If this is set to
- "ignore", then the boot-time status of the service will not
- be checked.
+ Whether the service should be on or off when the bcfg2 client
+ is run. This attribute may have different behavior depending
+ on the characteristics of the client tool. If set to "ignore",
+ then the status of the service will not be checked.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
diff --git a/schemas/types.xsd b/schemas/types.xsd
index 524b327c5..4e3dfd70f 100644
--- a/schemas/types.xsd
+++ b/schemas/types.xsd
@@ -53,6 +53,13 @@
</xsd:restriction>
</xsd:simpleType>
+ <xsd:simpleType name='BootStatusEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='on'/>
+ <xsd:enumeration value='off'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
<xsd:simpleType name='StatusEnum'>
<xsd:restriction base='xsd:string'>
<xsd:enumeration value='on'/>
@@ -112,14 +119,14 @@
</xsd:documentation>
</xsd:annotation>
- <xsd:attribute type='ActionTimingEnum' name='timing' use='required'>
+ <xsd:attribute type='ActionTimingEnum' name='timing'>
<xsd:annotation>
<xsd:documentation>
When the action is run.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
- <xsd:attribute type='ActionWhenEnum' name='when' use='required'>
+ <xsd:attribute type='ActionWhenEnum' name='when'>
<xsd:annotation>
<xsd:documentation>
If the action is always run, or is only run when a bundle
@@ -129,7 +136,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
- <xsd:attribute type='ActionStatusEnum' name='status' use='required'>
+ <xsd:attribute type='ActionStatusEnum' name='status'>
<xsd:annotation>
<xsd:documentation>
Whether or not to check the return code of the action. If
@@ -155,8 +162,16 @@
<xsd:attribute type='xsd:string' name='command' use='required'>
<xsd:annotation>
<xsd:documentation>
- The command to run. The command is executed within a shell,
- so flow control and other shell-specific things can be used.
+ The command to run.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute type='xsd:boolean' name='shell'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Whether the command string should be executeed within a shell.
+ If enabled flow control and other shell-specific things can
+ be used.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -381,6 +396,27 @@
</xsd:restriction>
</xsd:simpleType>
+ <xsd:complexType name="MemberOfType">
+ <xsd:annotation>
+ <xsd:documentation>
+ Specify additional supplementary groups for the POSIXUser
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:token">
+ <xsd:attribute name="group" type="xsd:token">
+ <xsd:annotation>
+ <xsd:documentation>
+ The name of the supplementary group. This can also be
+ specified as content of the tag, although that is
+ deprecated.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+
<xsd:complexType name="POSIXUserType">
<xsd:annotation>
<xsd:documentation>
@@ -388,13 +424,7 @@
</xsd:documentation>
</xsd:annotation>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='MemberOf' type='xsd:token'>
- <xsd:annotation>
- <xsd:documentation>
- Specify additional supplementary groups for the POSIXUser
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
+ <xsd:element name='MemberOf' type='MemberOfType'/>
</xsd:choice>
<xsd:attribute type="xsd:token" name="name" use="required">
<xsd:annotation>
@@ -422,8 +452,10 @@
<xsd:attribute type="xsd:string" name="gecos">
<xsd:annotation>
<xsd:documentation>
- Human-readable user name or comment. If this is not set,
- the GECOS will be the same as the username.
+ This field is typically used to record general information
+ about the account or its user(s) such as their real name
+ and phone number. If this is not set, the GECOS will be
+ the same as the username.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
diff --git a/setup.py b/setup.py
index 99e1ef025..59b1d65d4 100755
--- a/setup.py
+++ b/setup.py
@@ -4,26 +4,26 @@ from setuptools import setup
from glob import glob
import sys
-vfile = 'src/lib/Bcfg2/version.py'
+version_file = 'src/lib/Bcfg2/version.py'
try:
# python 2
- execfile(vfile)
+ execfile(version_file)
except NameError:
# py3k
- exec(compile(open(vfile).read(), vfile, 'exec'))
+ exec(compile(open(version_file).read(), version_file, 'exec'))
-# we only need m2crypto on < python2.6
-need_m2crypto = False
-version = sys.version_info[:2]
-if version < (2, 6):
- need_m2crypto = True
+inst_reqs = [
+ 'lockfile',
+ 'lxml',
+ 'python-daemon',
+]
-inst_reqs = ['lxml']
-if need_m2crypto:
+# we only need m2crypto on < python2.6
+if sys.version_info[:2] < (2, 6):
inst_reqs.append('M2Crypto')
setup(name="Bcfg2",
- version="1.3.1",
+ version=__version__, # Defined in src/lib/Bcfg2/version.py
description="Bcfg2 Server",
author="Narayan Desai",
author_email="desai@mcs.anl.gov",
@@ -55,9 +55,9 @@ setup(name="Bcfg2",
install_requires=inst_reqs,
tests_require=['mock', 'nose', 'sqlalchemy'],
package_dir={'': 'src/lib', },
- package_data={'Bcfg2.Reporting': [ 'templates/*.html',
- 'templates/*/*.html',
- 'templates/*/*.inc']},
+ package_data={'Bcfg2.Reporting': ['templates/*.html',
+ 'templates/*/*.html',
+ 'templates/*/*.inc']},
scripts=glob('src/sbin/*'),
data_files=[('share/bcfg2/schemas',
glob('schemas/*.xsd')),
diff --git a/solaris-ips/MANIFEST.bcfg2-server.header b/solaris-ips/MANIFEST.bcfg2-server.header
new file mode 100644
index 000000000..efa11181f
--- /dev/null
+++ b/solaris-ips/MANIFEST.bcfg2-server.header
@@ -0,0 +1,5 @@
+license ../../LICENSE license=simplified_bsd
+set name=description value="Configuration management server"
+set name=pkg.summary value="Configuration management server"
+set name=pkg.fmri value="pkg://bcfg2/bcfg2-server@1.3.1"
+
diff --git a/solaris-ips/MANIFEST.bcfg2.header b/solaris-ips/MANIFEST.bcfg2.header
new file mode 100644
index 000000000..8358aafca
--- /dev/null
+++ b/solaris-ips/MANIFEST.bcfg2.header
@@ -0,0 +1,6 @@
+license ../../LICENSE license=simplified_bsd
+set name=description value="Configuration management client"
+set name=pkg.summary value="Configuration management client"
+set name=pkg.fmri value="pkg://bcfg2/bcfg2@1.3.1"
+
+file usr/bin/bcfg2 group=bin mode=0755 owner=root path=usr/bin/bcfg2
diff --git a/solaris-ips/Makefile b/solaris-ips/Makefile
new file mode 100644
index 000000000..343150dc5
--- /dev/null
+++ b/solaris-ips/Makefile
@@ -0,0 +1,20 @@
+#!/usr/bin/gmake
+
+VERS=1.2.4-1
+PYVERSION := $(shell python -c "import sys; print sys.version[0:3]")
+
+default: clean package
+
+package:
+ -mkdir tmp tmp/bcfg2-server tmp/bcfg2
+ -mkdir -p build/lib/$(PYVERSION)/site-packages
+ -cd ../ && PYTHONPATH=$(PYTHONPATH):$(PWD)/build/lib/python2.6/site-packages/ python setup.py install --single-version-externally-managed --record=/dev/null --prefix=$(PWD)/build/usr
+ #setuptools appears to use a restictive umask
+ -chmod -R o+r build/
+ -chmod +x build/usr/bin/bcfg2
+ -sh ./gen-manifests.sh
+
+clean:
+ -rm -rf tmp build
+ -rm -rf MANIFEST.bcfg2
+ -rm -rf MANIFEST.bcfg2-server
diff --git a/solaris-ips/README b/solaris-ips/README
new file mode 100644
index 000000000..24021b992
--- /dev/null
+++ b/solaris-ips/README
@@ -0,0 +1,22 @@
+BUILDING
+--------
+
+Dependancies:
+ gmake
+
+Usage:
+ gmake
+
+
+PUBLISHING
+----------
+
+Modify MANIFEST.bcfg2 and MANIFEST.bcfg2-server to set your publisher name in the fmri, e.g. Change
+ set name=pkg.fmri value="pkg://bcfg2/bcfg2@1.2.4"
+to
+ set name=pkg.fmri value="pkg://example.com/bcfg2@1.2.4"
+
+
+Then run the pkgsend publish, i.e.
+ pkgsend publish -s http://example.com/path/to/repo -d build MANIFEST.bcfg2
+ pkgsend publish -s http://example.com/path/to/repo -d build MANIFEST.bcfg2-server
diff --git a/solaris-ips/gen-manifests.sh b/solaris-ips/gen-manifests.sh
new file mode 100644
index 000000000..3b4cd30df
--- /dev/null
+++ b/solaris-ips/gen-manifests.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/sh
+
+#bcfg2
+cat MANIFEST.bcfg2.header > MANIFEST.bcfg2
+pkgsend generate build | grep man[15] >> MANIFEST.bcfg2
+pkgsend generate build | grep Bcfg2/[^/]*.py$ >> MANIFEST.bcfg2
+pkgsend generate build | grep Bcfg2/Client/.*.py$ >> MANIFEST.bcfg2
+
+#bcfg2-server
+cat MANIFEST.bcfg2-server.header > MANIFEST.bcfg2-server
+pkgsend generate build | grep man[8] >> MANIFEST.bcfg2-server
+pkgsend generate build | grep share/bcfg2 >> MANIFEST.bcfg2-server
+pkgsend generate build | grep bin/bcfg2- >> MANIFEST.bcfg2-server
+pkgsend generate build | grep Bcfg2/Server/.*.py$ >> MANIFEST.bcfg2-server
+
diff --git a/solaris-ips/pkginfo.bcfg2 b/solaris-ips/pkginfo.bcfg2
new file mode 100644
index 000000000..90c628c53
--- /dev/null
+++ b/solaris-ips/pkginfo.bcfg2
@@ -0,0 +1,10 @@
+PKG="SCbcfg2"
+NAME="bcfg2"
+ARCH="sparc"
+VERSION="1.2.4"
+CATEGORY="application"
+VENDOR="Argonne National Labratory"
+EMAIL="bcfg-dev@mcs.anl.gov"
+PSTAMP="Bcfg2 Developers"
+BASEDIR="/opt/csw"
+CLASSES="none"
diff --git a/solaris-ips/pkginfo.bcfg2-server b/solaris-ips/pkginfo.bcfg2-server
new file mode 100644
index 000000000..0e865522c
--- /dev/null
+++ b/solaris-ips/pkginfo.bcfg2-server
@@ -0,0 +1,10 @@
+PKG="SCbcfg2-server"
+NAME="bcfg2-server"
+ARCH="sparc"
+VERSION="1.2.4"
+CATEGORY="application"
+VENDOR="Argonne National Labratory"
+EMAIL="bcfg-dev@mcs.anl.gov"
+PSTAMP="Bcfg2 Developers"
+BASEDIR="/usr"
+CLASSES="none"
diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py
index 88f3bd6ef..1676ee717 100644
--- a/src/lib/Bcfg2/Client/Client.py
+++ b/src/lib/Bcfg2/Client/Client.py
@@ -91,7 +91,10 @@ class Client(object):
try:
script.write("#!%s\n" %
(probe.attrib.get('interpreter', '/bin/sh')))
- script.write(probe.text)
+ if sys.hexversion >= 0x03000000:
+ script.write(probe.text)
+ else:
+ script.write(probe.text.encode('utf-8'))
script.close()
os.chmod(scriptname,
stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH |
@@ -105,6 +108,10 @@ class Client(object):
self._probe_failure(name, "Return value %s" % rv)
self.logger.info("Probe %s has result:" % name)
self.logger.info(rv.stdout)
+ if sys.hexversion >= 0x03000000:
+ ret.text = rv.stdout
+ else:
+ ret.text = rv.stdout.decode('utf-8')
finally:
os.unlink(scriptname)
except SystemExit:
@@ -163,9 +170,10 @@ class Client(object):
if len(probes.findall(".//probe")) > 0:
try:
# upload probe responses
- self.proxy.RecvProbeData(Bcfg2.Client.XML.tostring(
+ self.proxy.RecvProbeData(
+ Bcfg2.Client.XML.tostring(
probedata,
- xml_declaration=False).decode('UTF-8'))
+ xml_declaration=False).decode('utf-8'))
except Bcfg2.Proxy.ProxyError:
err = sys.exc_info()[1]
self.fatal_error("Failed to upload probe data: %s" % err)
@@ -227,7 +235,7 @@ class Client(object):
self.fatal_error("Failed to get decision list: %s" % err)
try:
- rawconfig = self.proxy.GetConfig().encode('UTF-8')
+ rawconfig = self.proxy.GetConfig().encode('utf-8')
except Bcfg2.Proxy.ProxyError:
err = sys.exc_info()[1]
self.fatal_error("Failed to download configuration from "
@@ -245,7 +253,7 @@ class Client(object):
self.logger.info("Starting Bcfg2 client run at %s" % times['start'])
- rawconfig = self.get_config(times=times)
+ rawconfig = self.get_config(times=times).decode('utf-8')
if self.setup['cache']:
try:
@@ -319,9 +327,10 @@ class Client(object):
feedback = self.tools.GenerateStats()
try:
- self.proxy.RecvStats(Bcfg2.Client.XML.tostring(
+ self.proxy.RecvStats(
+ Bcfg2.Client.XML.tostring(
feedback,
- xml_declaration=False).decode('UTF-8'))
+ xml_declaration=False).decode('utf-8'))
except Bcfg2.Proxy.ProxyError:
err = sys.exc_info()[1]
self.logger.error("Failed to upload configuration statistics: "
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py
index bc6bd4d4c..6bef77081 100644
--- a/src/lib/Bcfg2/Client/Frame.py
+++ b/src/lib/Bcfg2/Client/Frame.py
@@ -9,14 +9,6 @@ from Bcfg2.Client import prompt
from Bcfg2.Compat import any, all # pylint: disable=W0622
-def cmpent(ent1, ent2):
- """Sort entries."""
- if ent1.tag != ent2.tag:
- return cmp(ent1.tag, ent2.tag)
- else:
- return cmp(ent1.get('name'), ent2.get('name'))
-
-
def matches_entry(entryspec, entry):
""" Determine if the Decisions-style entry specification matches
the entry. Both are tuples of (tag, name). The entryspec can
@@ -61,8 +53,8 @@ class Frame(object):
self.removal = []
self.logger = logging.getLogger(__name__)
for driver in drivers[:]:
- if driver not in Bcfg2.Client.Tools.drivers and \
- isinstance(driver, str):
+ if (driver not in Bcfg2.Client.Tools.drivers and
+ isinstance(driver, str)):
self.logger.error("Tool driver %s is not available" % driver)
drivers.remove(driver)
@@ -105,8 +97,8 @@ class Frame(object):
self.logger.warning(deprecated)
experimental = [tool.name for tool in self.tools if tool.experimental]
if experimental:
- self.logger.warning("Loaded experimental tool drivers:")
- self.logger.warning(experimental)
+ self.logger.info("Loaded experimental tool drivers:")
+ self.logger.info(experimental)
# find entries not handled by any tools
self.unhandled = [entry for struct in config
@@ -128,7 +120,7 @@ class Frame(object):
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([pkg[0] for pkg in pkgs if pkg[1] is 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'])
@@ -155,7 +147,7 @@ class Frame(object):
def promptFilter(self, msg, entries):
"""Filter a supplied list based on user input."""
ret = []
- entries.sort(cmpent)
+ entries.sort(key=lambda e: e.tag + ":" + e.get('name'))
for entry in entries[:]:
if entry in self.unhandled:
# don't prompt for entries that can't be installed
@@ -187,19 +179,19 @@ class Frame(object):
"""
# Need to process decision stuff early so that dryrun mode
# works with it
- self.whitelist = [entry for entry in self.states \
+ self.whitelist = [entry for entry in self.states
if not self.states[entry]]
if not self.setup['file']:
if self.setup['decision'] == 'whitelist':
dwl = self.setup['decision_list']
- w_to_rem = [e for e in self.whitelist \
+ w_to_rem = [e for e in self.whitelist
if not matches_white_list(e, dwl)]
if w_to_rem:
self.logger.info("In whitelist mode: "
"suppressing installation of:")
self.logger.info(["%s:%s" % (e.tag, e.get('name'))
for e in w_to_rem])
- self.whitelist = [x for x in self.whitelist \
+ self.whitelist = [x for x in self.whitelist
if x not in w_to_rem]
elif self.setup['decision'] == 'blacklist':
b_to_rem = \
@@ -230,7 +222,7 @@ class Frame(object):
cfile not in self.whitelist):
continue
tools = [t for t in self.tools
- if t.handlesEntry(cfile) and t.canVerify(cfile)]
+ if t.handlesEntry(cfile) and t.canVerify(cfile)]
if not tools:
continue
if (self.setup['interactive'] and not
@@ -310,10 +302,10 @@ class Frame(object):
for bundle in self.setup['bundle']:
if bundle not in all_bundle_names:
self.logger.info("Warning: Bundle %s not found" % bundle)
- bundles = filter(lambda b: b.get('name') in self.setup['bundle'],
- bundles)
+ bundles = [b for b in bundles
+ if b.get('name') in self.setup['bundle']]
elif self.setup['indep']:
- bundles = filter(lambda b: b.tag != 'Bundle', bundles)
+ bundles = [b for b in bundles if b.tag != 'Bundle']
if self.setup['skipbundle']:
# warn if non-existent bundle given
if not self.setup['bundle_quick']:
@@ -321,14 +313,13 @@ class Frame(object):
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)
+ bundles = [b for b in bundles
+ if b.get('name') not in self.setup['skipbundle']]
if self.setup['skipindep']:
- bundles = filter(lambda b: b.tag == 'Bundle', bundles)
+ bundles = [b for b in bundles if b.tag == 'Bundle']
self.whitelist = [e for e in self.whitelist
- if True in [e in b for b in bundles]]
+ if any(e in b for b in bundles)]
# first process prereq actions
for bundle in bundles[:]:
@@ -387,8 +378,8 @@ class Frame(object):
"""Install all entries."""
self.DispatchInstallCalls(self.whitelist)
mods = self.modified
- mbundles = [struct for struct in self.config.findall('Bundle') if \
- [mod for mod in mods if mod in struct]]
+ mbundles = [struct for struct in self.config.findall('Bundle')
+ if any(True for mod in mods if mod in struct)]
if self.modified:
# Handle Bundle interdeps
@@ -403,30 +394,35 @@ class Frame(object):
self.logger.error("%s.Inventory() call failed:" %
tool.name,
exc_info=1)
- clobbered = [entry for bundle in mbundles for entry in bundle \
+ clobbered = [entry for bundle in mbundles for entry in bundle
if (not self.states[entry] and
entry not in self.blacklist)]
if clobbered:
self.logger.debug("Found clobbered entries:")
- self.logger.debug(["%s:%s" % (entry.tag, entry.get('name')) \
+ self.logger.debug(["%s:%s" % (entry.tag, entry.get('name'))
for entry in clobbered])
if not self.setup['interactive']:
self.DispatchInstallCalls(clobbered)
for bundle in self.config.findall('.//Bundle'):
- if self.setup['bundle'] and \
- bundle.get('name') not in self.setup['bundle']:
+ if (self.setup['bundle'] and
+ bundle.get('name') not in self.setup['bundle']):
# prune out unspecified bundles when running with -b
continue
+ if bundle in mbundles:
+ self.logger.debug("Bundle %s was modified" %
+ bundle.get('name'))
+ func = "BundleUpdated"
+ else:
+ self.logger.debug("Bundle %s was not modified" %
+ bundle.get('name'))
+ func = "BundleNotUpdated"
for tool in self.tools:
try:
- if bundle in mbundles:
- tool.BundleUpdated(bundle, self.states)
- else:
- tool.BundleNotUpdated(bundle, self.states)
+ getattr(tool, func)(bundle, self.states)
except:
- self.logger.error("%s.BundleNotUpdated() call failed:" %
- tool.name, exc_info=1)
+ self.logger.error("%s.%s() call failed:" %
+ (tool.name, func), exc_info=1)
def Remove(self):
"""Remove extra entries."""
@@ -448,15 +444,16 @@ class Frame(object):
self.logger.info('Incorrect entries: %d' %
list(self.states.values()).count(False))
if phase == 'final' and list(self.states.values()).count(False):
- for entry in self.states.keys():
+ for entry in sorted(self.states.keys(), key=lambda e: e.tag + ":" +
+ e.get('name')):
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("%s:%s" % (entry.tag,
+ entry.get('name')))
self.logger.info('Total managed entries: %d' %
len(list(self.states.values())))
self.logger.info('Unmanaged entries: %d' % len(self.extra))
@@ -468,8 +465,8 @@ class Frame(object):
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("%s:%s" % (entry.tag,
+ entry.get('name')))
if ((list(self.states.values()).count(False) == 0) and not self.extra):
self.logger.info('All entries correct.')
diff --git a/src/lib/Bcfg2/Client/Tools/APK.py b/src/lib/Bcfg2/Client/Tools/APK.py
index 8a02b7d6d..58641ed37 100644
--- a/src/lib/Bcfg2/Client/Tools/APK.py
+++ b/src/lib/Bcfg2/Client/Tools/APK.py
@@ -32,7 +32,7 @@ class APK(Bcfg2.Client.Tools.PkgTool):
"""Verify Package status for entry."""
if not 'version' in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
- (entry.attrib['name']))
+ entry.attrib['name'])
return False
if entry.attrib['name'] in self.installed:
diff --git a/src/lib/Bcfg2/Client/Tools/Action.py b/src/lib/Bcfg2/Client/Tools/Action.py
index 87565d96d..0166e4c00 100644
--- a/src/lib/Bcfg2/Client/Tools/Action.py
+++ b/src/lib/Bcfg2/Client/Tools/Action.py
@@ -20,22 +20,29 @@ class Action(Bcfg2.Client.Tools.Tool):
the whitelist or blacklist """
if self.setup['decision'] == 'whitelist' and \
not matches_white_list(action, self.setup['decision_list']):
- self.logger.info("In whitelist mode: suppressing Action:" + \
+ self.logger.info("In whitelist mode: suppressing Action: %s" %
action.get('name'))
return False
if self.setup['decision'] == 'blacklist' and \
not passes_black_list(action, self.setup['decision_list']):
- self.logger.info("In blacklist mode: suppressing Action:" + \
+ self.logger.info("In blacklist mode: suppressing Action: %s" %
action.get('name'))
return False
return True
def RunAction(self, entry):
"""This method handles command execution and status return."""
+ shell = False
+ shell_string = ''
+ if entry.get('shell', 'false') == 'true':
+ shell = True
+ shell_string = '(in shell) '
+
if not self.setup['dryrun']:
if self.setup['interactive']:
- prompt = ('Run Action %s, %s: (y/N): ' %
- (entry.get('name'), entry.get('command')))
+ prompt = ('Run Action %s%s, %s: (y/N): ' %
+ (shell_string, entry.get('name'),
+ entry.get('command')))
# flush input buffer
while len(select.select([sys.stdin.fileno()], [], [],
0.0)[0]) > 0:
@@ -48,8 +55,9 @@ class Action(Bcfg2.Client.Tools.Tool):
self.logger.debug("Action: Deferring execution of %s due "
"to build mode" % entry.get('command'))
return False
- self.logger.debug("Running Action %s" % (entry.get('name')))
- rv = self.cmd.run(entry.get('command'))
+ self.logger.debug("Running Action %s %s" %
+ (shell_string, entry.get('name')))
+ rv = self.cmd.run(entry.get('command'), shell=shell)
self.logger.debug("Action: %s got return code %s" %
(entry.get('command'), rv.retval))
entry.set('rc', str(rv.retval))
diff --git a/src/lib/Bcfg2/Client/Tools/Chkconfig.py b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
index ec7f462b3..156f76159 100644
--- a/src/lib/Bcfg2/Client/Tools/Chkconfig.py
+++ b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
@@ -19,25 +19,22 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
def get_svc_command(self, service, action):
return "/sbin/service %s %s" % (service.get('name'), action)
- def VerifyService(self, entry, _):
- """Verify Service status for entry."""
- if entry.get('status') == 'ignore':
- return True
-
+ def verify_bootstatus(self, entry, bootstatus):
+ """Verify bootstatus for entry."""
rv = self.cmd.run("/sbin/chkconfig --list %s " % entry.get('name'))
if rv.success:
srvdata = rv.stdout.splitlines()[0].split()
else:
# service not installed
- entry.set('current_status', 'off')
+ entry.set('current_bootstatus', 'service not installed')
return False
if len(srvdata) == 2:
# This is an xinetd service
- if entry.get('status') == srvdata[1]:
+ if bootstatus == srvdata[1]:
return True
else:
- entry.set('current_status', srvdata[1])
+ entry.set('current_bootstatus', srvdata[1])
return False
try:
@@ -46,37 +43,75 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
except IndexError:
onlevels = []
- pstatus = self.check_service(entry)
- if entry.get('status') == 'on':
- status = (len(onlevels) > 0 and pstatus)
+ if bootstatus == 'on':
+ current_bootstatus = (len(onlevels) > 0)
else:
- status = (len(onlevels) == 0 and not pstatus)
+ current_bootstatus = (len(onlevels) == 0)
+ return current_bootstatus
+
+ def VerifyService(self, entry, _):
+ """Verify Service status for entry."""
+ entry.set('target_status', entry.get('status')) # for reporting
+ bootstatus = self.get_bootstatus(entry)
+ if bootstatus is None:
+ return True
+ current_bootstatus = self.verify_bootstatus(entry, bootstatus)
- if not status:
+ if entry.get('status') == 'ignore':
+ # 'ignore' should verify
+ current_svcstatus = True
+ svcstatus = True
+ else:
+ svcstatus = self.check_service(entry)
if entry.get('status') == 'on':
- entry.set('current_status', 'off')
- else:
- entry.set('current_status', 'on')
- return status
+ if svcstatus:
+ current_svcstatus = True
+ else:
+ current_svcstatus = False
+ elif entry.get('status') == 'off':
+ if svcstatus:
+ current_svcstatus = False
+ else:
+ current_svcstatus = True
+
+ if svcstatus:
+ entry.set('current_status', 'on')
+ else:
+ entry.set('current_status', 'off')
+
+ return current_bootstatus and current_svcstatus
def InstallService(self, entry):
"""Install Service entry."""
- rcmd = "/sbin/chkconfig %s %s"
- self.cmd.run("/sbin/chkconfig --add %s" % (entry.attrib['name']))
+ self.cmd.run("/sbin/chkconfig --add %s" % (entry.get('name')))
self.logger.info("Installing Service %s" % (entry.get('name')))
- rv = True
- if entry.get('status') == 'off':
- rv &= self.cmd.run((rcmd + " --level 0123456") %
- (entry.get('name'),
- entry.get('status'))).success
- if entry.get("current_status") == "on":
- rv &= self.stop_service(entry).success
+ bootstatus = entry.get('bootstatus')
+ if bootstatus is not None:
+ if bootstatus == 'on':
+ # make sure service is enabled on boot
+ bootcmd = '/sbin/chkconfig %s %s --level 0123456' % \
+ (entry.get('name'), entry.get('bootstatus'))
+ elif bootstatus == 'off':
+ # make sure service is disabled on boot
+ bootcmd = '/sbin/chkconfig %s %s' % (entry.get('name'),
+ entry.get('bootstatus'))
+ bootcmdrv = self.cmd.run(bootcmd).success
+ if self.setup['servicemode'] == 'disabled':
+ # 'disabled' means we don't attempt to modify running svcs
+ return bootcmdrv
+ buildmode = self.setup['servicemode'] == 'build'
+ if (entry.get('status') == 'on' and not buildmode) and \
+ entry.get('current_status') == 'off':
+ svccmdrv = self.start_service(entry)
+ elif (entry.get('status') == 'off' or buildmode) and \
+ entry.get('current_status') == 'on':
+ svccmdrv = self.stop_service(entry)
+ else:
+ svccmdrv = True # ignore status attribute
+ return bootcmdrv and svccmdrv
else:
- rv &= self.cmd.run(rcmd % (entry.get('name'),
- entry.get('status'))).success
- if entry.get("current_status") == "off":
- rv &= self.start_service(entry).success
- return rv
+ # when bootstatus is 'None', status == 'ignore'
+ return True
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 ca556e98b..761c51db7 100644
--- a/src/lib/Bcfg2/Client/Tools/DebInit.py
+++ b/src/lib/Bcfg2/Client/Tools/DebInit.py
@@ -16,15 +16,13 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
__handles__ = [('Service', 'deb')]
__req__ = {'Service': ['name', 'status']}
svcre = \
- re.compile("/etc/.*/(?P<action>[SK])(?P<sequence>\d+)(?P<name>\S+)")
+ re.compile(r'/etc/.*/(?P<action>[SK])(?P<sequence>\d+)(?P<name>\S+)')
- # implement entry (Verify|Install) ops
- def VerifyService(self, entry, _):
- """Verify Service status for entry."""
-
- if entry.get('status') == 'ignore':
- return True
+ def get_svc_command(self, service, action):
+ return '/usr/sbin/invoke-rc.d %s %s' % (service.get('name'), action)
+ def verify_bootstatus(self, entry, bootstatus):
+ """Verify bootstatus for entry."""
rawfiles = glob.glob("/etc/rc*.d/[SK]*%s" % (entry.get('name')))
files = []
@@ -54,9 +52,9 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
continue
if match.group('name') == entry.get('name'):
files.append(filename)
- if entry.get('status') == 'off':
+ if bootstatus == 'off':
if files:
- entry.set('current_status', 'on')
+ entry.set('current_bootstatus', 'on')
return False
else:
return True
@@ -72,12 +70,47 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
return False
return True
else:
- entry.set('current_status', 'off')
+ entry.set('current_bootstatus', 'off')
return False
+ def VerifyService(self, entry, _):
+ """Verify Service status for entry."""
+ entry.set('target_status', entry.get('status')) # for reporting
+ bootstatus = self.get_bootstatus(entry)
+ if bootstatus is None:
+ return True
+ current_bootstatus = self.verify_bootstatus(entry, bootstatus)
+
+ if entry.get('status') == 'ignore':
+ # 'ignore' should verify
+ current_svcstatus = True
+ svcstatus = True
+ else:
+ svcstatus = self.check_service(entry)
+ if entry.get('status') == 'on':
+ if svcstatus:
+ current_svcstatus = True
+ else:
+ current_svcstatus = False
+ elif entry.get('status') == 'off':
+ if svcstatus:
+ current_svcstatus = False
+ else:
+ current_svcstatus = True
+
+ if svcstatus:
+ entry.set('current_status', 'on')
+ else:
+ entry.set('current_status', 'off')
+
+ return current_bootstatus and current_svcstatus
+
def InstallService(self, entry):
- """Install Service for entry."""
+ """Install Service entry."""
self.logger.info("Installing Service %s" % (entry.get('name')))
+ bootstatus = entry.get('bootstatus')
+
+ # check if init script exists
try:
os.stat('/etc/init.d/%s' % entry.get('name'))
except OSError:
@@ -85,20 +118,41 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
entry.get('name'))
return False
- if entry.get('status') == 'off':
- self.cmd.run("/usr/sbin/invoke-rc.d %s stop" % (entry.get('name')))
- return self.cmd.run("/usr/sbin/update-rc.d -f %s remove" %
- entry.get('name')).success
+ if bootstatus is not None:
+ seqcmdrv = True
+ if bootstatus == 'on':
+ # make sure service is enabled on boot
+ bootcmd = '/usr/sbin/update-rc.d %s defaults' % \
+ entry.get('name')
+ if entry.get('sequence'):
+ seqcmd = '/usr/sbin/update-rc.d -f %s remove' % \
+ entry.get('name')
+ seqcmdrv = self.cmd.run(seqcmd)
+ start_sequence = int(entry.get('sequence'))
+ kill_sequence = 100 - start_sequence
+ bootcmd = '%s %d %d' % (bootcmd, start_sequence,
+ kill_sequence)
+ elif bootstatus == 'off':
+ # make sure service is disabled on boot
+ bootcmd = '/usr/sbin/update-rc.d -f %s remove' % \
+ entry.get('name')
+ bootcmdrv = self.cmd.run(bootcmd)
+ if self.setup['servicemode'] == 'disabled':
+ # 'disabled' means we don't attempt to modify running svcs
+ return bootcmdrv and seqcmdrv
+ buildmode = self.setup['servicemode'] == 'build'
+ if (entry.get('status') == 'on' and not buildmode) and \
+ entry.get('current_status') == 'off':
+ svccmdrv = self.start_service(entry)
+ elif (entry.get('status') == 'off' or buildmode) and \
+ entry.get('current_status') == 'on':
+ svccmdrv = self.stop_service(entry)
+ else:
+ svccmdrv = True # ignore status attribute
+ return bootcmdrv and svccmdrv and seqcmdrv
else:
- command = "/usr/sbin/update-rc.d %s defaults" % (entry.get('name'))
- if entry.get('sequence'):
- if not self.cmd.run("/usr/sbin/update-rc.d -f %s remove" %
- entry.get('name')).success:
- return False
- start_sequence = int(entry.get('sequence'))
- kill_sequence = 100 - start_sequence
- command = "%s %d %d" % (command, start_sequence, kill_sequence)
- return self.cmd.run(command).success
+ # when bootstatus is 'None', status == 'ignore'
+ return True
def FindExtra(self):
"""Find Extra Debian Service entries."""
@@ -116,6 +170,3 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
# Extra service removal is nonsensical
# Extra services need to be reflected in the config
return
-
- def get_svc_command(self, service, action):
- return '/usr/sbin/invoke-rc.d %s %s' % (service.get('name'), action)
diff --git a/src/lib/Bcfg2/Client/Tools/Encap.py b/src/lib/Bcfg2/Client/Tools/Encap.py
index 678e0f00c..270f0a5f2 100644
--- a/src/lib/Bcfg2/Client/Tools/Encap.py
+++ b/src/lib/Bcfg2/Client/Tools/Encap.py
@@ -13,7 +13,7 @@ class Encap(Bcfg2.Client.Tools.PkgTool):
__req__ = {'Package': ['version', 'url']}
pkgtype = 'encap'
pkgtool = ("/usr/local/bin/epkg -l -f -q %s", ("%s", ["url"]))
- splitter = re.compile('.*/(?P<name>[\w-]+)\-(?P<version>[\w\.+-]+)')
+ splitter = re.compile(r'.*/(?P<name>[\w-]+)\-(?P<version>[\w\.+-]+)')
def RefreshPackages(self):
"""Try to find encap packages."""
diff --git a/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py b/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
index 635318805..31925fa3c 100644
--- a/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
+++ b/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
@@ -21,7 +21,7 @@ class FreeBSDPackage(Bcfg2.Client.Tools.PkgTool):
def RefreshPackages(self):
self.installed = {}
packages = self.cmd.run("/usr/sbin/pkg_info -a -E").stdout.splitlines()
- pattern = re.compile('(.*)-(\d.*)')
+ pattern = re.compile(r'(.*)-(\d.*)')
for pkg in packages:
if pattern.match(pkg):
name = pattern.match(pkg).group(1)
@@ -31,7 +31,7 @@ class FreeBSDPackage(Bcfg2.Client.Tools.PkgTool):
def VerifyPackage(self, entry, _):
if not 'version' in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
- (entry.attrib['name']))
+ entry.attrib['name'])
return False
if entry.attrib['name'] in self.installed:
if self.installed[entry.attrib['name']] == entry.attrib['version']:
diff --git a/src/lib/Bcfg2/Client/Tools/IPS.py b/src/lib/Bcfg2/Client/Tools/IPS.py
index dc4d48235..aff276c3a 100644
--- a/src/lib/Bcfg2/Client/Tools/IPS.py
+++ b/src/lib/Bcfg2/Client/Tools/IPS.py
@@ -51,9 +51,9 @@ class IPS(Bcfg2.Client.Tools.PkgTool):
pass
else:
if entry.get('version') != self.installed[pname]:
- self.logger.debug("IPS: Package %s: have %s want %s" \
- % (pname, self.installed[pname],
- entry.get('version')))
+ self.logger.debug("IPS: Package %s: have %s want %s" %
+ (pname, self.installed[pname],
+ entry.get('version')))
return False
# need to implement pkg chksum validation
diff --git a/src/lib/Bcfg2/Client/Tools/MacPorts.py b/src/lib/Bcfg2/Client/Tools/MacPorts.py
index bc3765ec6..bd9d24df3 100644
--- a/src/lib/Bcfg2/Client/Tools/MacPorts.py
+++ b/src/lib/Bcfg2/Client/Tools/MacPorts.py
@@ -38,7 +38,7 @@ class MacPorts(Bcfg2.Client.Tools.PkgTool):
"""Verify Package status for entry."""
if not 'version' in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
- (entry.attrib['name']))
+ entry.attrib['name'])
return False
if entry.attrib['name'] in self.installed:
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Device.py b/src/lib/Bcfg2/Client/Tools/POSIX/Device.py
index d5aaf069d..9b84adad0 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/Device.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/Device.py
@@ -12,8 +12,8 @@ class POSIXDevice(POSIXTool):
def fully_specified(self, entry):
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') is None or
+ entry.get('minor') is None):
return False
return True
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py b/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py
index 9d0fe05e0..675a4461a 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/Directory.py
@@ -36,14 +36,14 @@ class POSIXDirectory(POSIXTool):
self.logger.info("POSIX: " + msg)
entry.set('qtext', entry.get('qtext', '') + '\n' + msg)
for extra in extras:
- Bcfg2.Client.XML.SubElement(entry, 'Prune', path=extra)
+ Bcfg2.Client.XML.SubElement(entry, 'Prune', name=extra)
except OSError:
prune = True
return POSIXTool.verify(self, entry, modlist) and prune
def install(self, entry):
- """Install device entries."""
+ """Install directory entries."""
fmode = self._exists(entry)
if fmode and not stat.S_ISDIR(fmode[stat.ST_MODE]):
@@ -67,7 +67,7 @@ class POSIXDirectory(POSIXTool):
if entry.get('prune', 'false') == 'true':
for pent in entry.findall('Prune'):
- pname = pent.get('path')
+ pname = pent.get('name')
try:
self.logger.debug("POSIX: Removing %s" % pname)
self._remove(pent)
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
index 9b95d2234..9f47fb53a 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
@@ -34,13 +34,11 @@ class POSIXFile(POSIXTool):
def _get_data(self, entry):
""" Get a tuple of (<file data>, <is binary>) for the given entry """
- is_binary = False
- if entry.get('encoding', 'ascii') == 'base64':
- tempdata = b64decode(entry.text)
- is_binary = True
-
- elif entry.get('empty', 'false') == 'true':
+ is_binary = entry.get('encoding', 'ascii') == 'base64'
+ if entry.get('empty', 'false') == 'true' or not entry.text:
tempdata = ''
+ elif is_binary:
+ tempdata = b64decode(entry.text)
else:
tempdata = entry.text
if isinstance(tempdata, unicode) and unicode != str:
@@ -148,8 +146,8 @@ class POSIXFile(POSIXTool):
return POSIXTool.install(self, entry) and rv
- def _get_diffs(self, entry, interactive=False, sensitive=False,
- is_binary=False, content=None):
+ def _get_diffs(self, entry, interactive=False, # pylint: disable=R0912
+ sensitive=False, is_binary=False, content=None):
""" generate the necessary diffs for entry """
if not interactive and sensitive:
return
@@ -165,6 +163,8 @@ class POSIXFile(POSIXTool):
# prompts for -I and the reports
try:
content = open(entry.get('name')).read()
+ except UnicodeDecodeError:
+ content = open(entry.get('name'), encoding='utf-8').read()
except IOError:
self.logger.error("POSIX: Failed to read %s: %s" %
(entry.get("name"), sys.exc_info()[1]))
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
index f46875743..16fe0acb5 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/base.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
@@ -275,7 +275,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
if path is None:
path = entry.get("name")
context = entry.get("secontext")
- if context is None:
+ if not context:
# no context listed
return True
@@ -520,13 +520,19 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
"Current mtime is %s but should be %s" %
(path, mtime, entry.get('mtime')))
- if HAS_SELINUX and entry.get("secontext"):
+ if HAS_SELINUX:
+ wanted_secontext = None
if entry.get("secontext") == "__default__":
- wanted_secontext = \
- selinux.matchpathcon(path, 0)[1].split(":")[2]
+ try:
+ wanted_secontext = \
+ selinux.matchpathcon(path, 0)[1].split(":")[2]
+ except OSError:
+ errors.append("%s has no default SELinux context" %
+ entry.get("name"))
else:
wanted_secontext = entry.get("secontext")
- if attrib['current_secontext'] != wanted_secontext:
+ if (wanted_secontext and
+ attrib['current_secontext'] != wanted_secontext):
errors.append("SELinux context for path %s is incorrect. "
"Current context is %s but should be %s" %
(path, attrib['current_secontext'],
@@ -712,8 +718,8 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
tmpentry.set('mode', oct_mode(newmode))
for acl in tmpentry.findall('ACL'):
acl.set('perms',
- oct_mode(self._norm_acl_perms(acl.get('perms')) | \
- ACL_MAP['x']))
+ oct_mode(self._norm_acl_perms(acl.get('perms')) |
+ ACL_MAP['x']))
for cpath in created:
rv &= self._set_perms(tmpentry, path=cpath)
return rv
diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
index 99ed3c7d9..8226392f9 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
@@ -154,7 +154,8 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
if entry.get("current_exists", "true") == "true":
# verify supplemental groups
actual = [g[0] for g in self.user_supplementary_groups(entry)]
- expected = [e.text for e in entry.findall("MemberOf")]
+ expected = [e.get("group", e.text).strip()
+ for e in entry.findall("MemberOf")]
if set(expected) != set(actual):
entry.set('qtext',
"\n".join([entry.get('qtext', '')] +
@@ -252,7 +253,8 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
if entry.get('uid'):
cmd.extend(['-u', entry.get('uid')])
cmd.extend(['-g', entry.get('group')])
- extras = [e.text for e in entry.findall("MemberOf")]
+ extras = [e.get("group", e.text).strip()
+ for e in entry.findall("MemberOf")]
if extras:
cmd.extend(['-G', ",".join(extras)])
cmd.extend(['-d', entry.get('home')])
diff --git a/src/lib/Bcfg2/Client/Tools/Pacman.py b/src/lib/Bcfg2/Client/Tools/Pacman.py
index 12785afee..a4cfd3315 100644
--- a/src/lib/Bcfg2/Client/Tools/Pacman.py
+++ b/src/lib/Bcfg2/Client/Tools/Pacman.py
@@ -30,12 +30,12 @@ class Pacman(Bcfg2.Client.Tools.PkgTool):
def VerifyPackage(self, entry, _):
'''Verify Package status for entry'''
- self.logger.info("VerifyPackage : %s : %s" % entry.get('name'),
- entry.get('version'))
+ self.logger.info("VerifyPackage: %s : %s" % (entry.get('name'),
+ entry.get('version')))
if not 'version' in entry.attrib:
self.logger.info("Cannot verify unversioned package %s" %
- (entry.attrib['name']))
+ entry.attrib['name'])
return False
if entry.attrib['name'] in self.installed:
diff --git a/src/lib/Bcfg2/Client/Tools/Portage.py b/src/lib/Bcfg2/Client/Tools/Portage.py
index 6b38d7dec..2d8b66ce5 100644
--- a/src/lib/Bcfg2/Client/Tools/Portage.py
+++ b/src/lib/Bcfg2/Client/Tools/Portage.py
@@ -13,8 +13,8 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
__req__ = {'Package': ['name', 'version']}
pkgtype = 'ebuild'
# requires a working PORTAGE_BINHOST in make.conf
- _binpkgtool = ('emerge --getbinpkgonly %s', ('=%s-%s', \
- ['name', 'version']))
+ _binpkgtool = ('emerge --getbinpkgonly %s', ('=%s-%s', ['name',
+ 'version']))
pkgtool = ('emerge %s', ('=%s-%s', ['name', 'version']))
def __init__(self, logger, cfg, setup):
@@ -22,7 +22,7 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
Bcfg2.Client.Tools.PkgTool.__init__(self, logger, cfg, setup)
self._initialised = True
self.__important__ = self.__important__ + ['/etc/make.conf']
- self._pkg_pattern = re.compile('(.*)-(\d.*)')
+ self._pkg_pattern = re.compile(r'(.*)-(\d.*)')
self._ebuild_pattern = re.compile('(ebuild|binary)')
self.cfg = cfg
self.installed = {}
@@ -74,10 +74,10 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
self.logger.debug('Running equery check on %s' %
entry.get('name'))
- for line in self.cmd.run(["/usr/bin/equery", "-N", "check",
- '=%s-%s' %
- (entry.get('name'),
- version)]).stdout.splitlines():
+ for line in self.cmd.run(
+ ["/usr/bin/equery", "-N", "check",
+ '=%s-%s' % (entry.get('name'),
+ entry.get('version'))]).stdout.splitlines():
if '!!!' in line and line.split()[1] not in modlist:
return False
diff --git a/src/lib/Bcfg2/Client/Tools/RcUpdate.py b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
index 552b27842..8e9626521 100644
--- a/src/lib/Bcfg2/Client/Tools/RcUpdate.py
+++ b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
@@ -12,18 +12,47 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
__handles__ = [('Service', 'rc-update')]
__req__ = {'Service': ['name', 'status']}
+ def get_enabled_svcs(self):
+ """
+ Return a list of all enabled services.
+ """
+ return [line.split()[0]
+ for line in self.cmd.run(['/bin/rc-status',
+ '-s']).stdout.splitlines()
+ if 'started' in line]
+
+ def get_default_svcs(self):
+ """Return a list of services in the 'default' runlevel."""
+ return [line.split()[0]
+ for line in self.cmd.run(['/sbin/rc-update',
+ 'show']).stdout.splitlines()
+ if 'default' in line]
+
+ def verify_bootstatus(self, entry, bootstatus):
+ """Verify bootstatus for entry."""
+ # get a list of all started services
+ allsrv = self.get_default_svcs()
+ # set current_bootstatus attribute
+ if entry.get('name') in allsrv:
+ entry.set('current_bootstatus', 'on')
+ else:
+ entry.set('current_bootstatus', 'off')
+ if bootstatus == 'on':
+ return entry.get('name') in allsrv
+ else:
+ return entry.get('name') not in allsrv
+
def VerifyService(self, entry, _):
"""
Verify Service status for entry.
Assumes we run in the "default" runlevel.
"""
- if entry.get('status') == 'ignore':
+ entry.set('target_status', entry.get('status')) # for reporting
+ bootstatus = self.get_bootstatus(entry)
+ if bootstatus is None:
return True
-
- # check if service is enabled
- result = self.cmd.run(["/sbin/rc-update", "show", "default"])
- is_enabled = entry.get("name") in result.stdout
+ current_bootstatus = self.verify_bootstatus(entry, bootstatus)
# check if init script exists
try:
@@ -33,47 +62,62 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
entry.get('name'))
return False
- # check if service is enabled
- result = self.cmd.run(self.get_svc_command(entry, "status"))
- is_running = "started" in result.stdout
-
- if entry.get('status') == 'on' and not (is_enabled and is_running):
- entry.set('current_status', 'off')
- return False
+ if entry.get('status') == 'ignore':
+ # 'ignore' should verify
+ current_svcstatus = True
+ svcstatus = True
+ else:
+ svcstatus = self.check_service(entry)
+ if entry.get('status') == 'on':
+ if svcstatus:
+ current_svcstatus = True
+ else:
+ current_svcstatus = False
+ elif entry.get('status') == 'off':
+ if svcstatus:
+ current_svcstatus = False
+ else:
+ current_svcstatus = True
- elif entry.get('status') == 'off' and (is_enabled or is_running):
+ if svcstatus:
entry.set('current_status', 'on')
- return False
+ else:
+ entry.set('current_status', 'off')
- return True
+ return current_bootstatus and current_svcstatus
def InstallService(self, entry):
- """
- Install Service entry
-
- """
+ """Install Service entry."""
self.logger.info('Installing Service %s' % entry.get('name'))
- if entry.get('status') == 'on':
- if entry.get('current_status') == 'off':
- self.start_service(entry)
- # make sure it's enabled
- cmd = '/sbin/rc-update add %s default'
- return self.cmd.run(cmd % entry.get('name')).success
- elif entry.get('status') == 'off':
- if entry.get('current_status') == 'on':
- self.stop_service(entry)
- # make sure it's disabled
- cmd = '/sbin/rc-update del %s default'
- return self.cmd.run(cmd % entry.get('name')).success
-
- return False
+ bootstatus = entry.get('bootstatus')
+ if bootstatus is not None:
+ if bootstatus == 'on':
+ # make sure service is enabled on boot
+ bootcmd = '/sbin/rc-update add %s default'
+ elif bootstatus == 'off':
+ # make sure service is disabled on boot
+ bootcmd = '/sbin/rc-update del %s default'
+ bootcmdrv = self.cmd.run(bootcmd % entry.get('name')).success
+ if self.setup['servicemode'] == 'disabled':
+ # 'disabled' means we don't attempt to modify running svcs
+ return bootcmdrv
+ buildmode = self.setup['servicemode'] == 'build'
+ if (entry.get('status') == 'on' and not buildmode) and \
+ entry.get('current_status') == 'off':
+ svccmdrv = self.start_service(entry)
+ elif (entry.get('status') == 'off' or buildmode) and \
+ entry.get('current_status') == 'on':
+ svccmdrv = self.stop_service(entry)
+ else:
+ svccmdrv = True # ignore status attribute
+ return bootcmdrv and svccmdrv
+ else:
+ # when bootstatus is 'None', status == 'ignore'
+ return True
def FindExtra(self):
"""Locate extra rc-update services."""
- allsrv = [line.split()[0]
- for line in self.cmd.run(['/bin/rc-status',
- '-s']).stdout.splitlines()
- if 'started' in line]
+ allsrv = self.get_enabled_svcs()
self.logger.debug('Found active services:')
self.logger.debug(allsrv)
specified = [srv.get('name') for srv in self.getSupportedEntries()]
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py
index 451495be2..0b4aba60d 100644
--- a/src/lib/Bcfg2/Client/Tools/SELinux.py
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -12,6 +12,7 @@ import seobject
import Bcfg2.Client.XML
import Bcfg2.Client.Tools
from Bcfg2.Client.Tools.POSIX.File import POSIXFile
+from Bcfg2.Compat import long # pylint: disable=W0622
def pack128(int_val):
@@ -47,7 +48,7 @@ def netmask_itoa(netmask, proto="ipv4"):
if netmask > size:
raise ValueError("Netmask too large: %s" % netmask)
- res = 0L
+ res = long(0)
for i in range(netmask):
res |= 1 << (size - i - 1)
netmask = socket.inet_ntop(family, pack128(res))
@@ -170,7 +171,7 @@ class SELinuxEntryHandler(object):
key_format = ("name",)
value_format = ()
str_format = '%(name)s'
- custom_re = re.compile(' (?P<name>\S+)$')
+ custom_re = re.compile(r' (?P<name>\S+)$')
custom_format = None
def __init__(self, tool, logger, setup, config):
@@ -203,7 +204,16 @@ class SELinuxEntryHandler(object):
type, if the records object supports the customized() method
"""
if hasattr(self.records, "customized") and self.custom_re:
- return dict([(k, self.all_records[k]) for k in self.custom_keys])
+ rv = dict()
+ for key in self.custom_keys:
+ if key in self.all_records:
+ rv[key] = self.all_records[key]
+ else:
+ self.logger.warning("SELinux %s %s customized, but no "
+ "record found. This may indicate an "
+ "error in your SELinux policy." %
+ (self.etype, key))
+ return rv
else:
# ValueError is really a pretty dumb exception to raise,
# but that's what the seobject customized() method raises
@@ -490,7 +500,8 @@ class SELinuxSeportHandler(SELinuxEntryHandler):
def _defaultargs(self, entry):
""" argument list for adding and modifying entries """
(port, proto) = entry.get("name").split("/")
- return (port, proto, '', entry.get("selinuxtype"))
+ return (port, proto, entry.get("mlsrange", ""),
+ entry.get("selinuxtype"))
def _deleteargs(self, entry):
return tuple(entry.get("name").split("/"))
@@ -512,14 +523,14 @@ class SELinuxSefcontextHandler(SELinuxEntryHandler):
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")
+ 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>.*)\'')
@@ -563,7 +574,7 @@ class SELinuxSefcontextHandler(SELinuxEntryHandler):
""" argument list for adding, modifying, and deleting entries """
return (entry.get("name"), entry.get("selinuxtype"),
self.filetypeargs[entry.get("filetype", "all")],
- '', '')
+ entry.get("mlsrange", ""), '')
def primarykey(self, entry):
return ":".join([entry.tag, entry.get("name"),
@@ -598,7 +609,7 @@ class SELinuxSenodeHandler(SELinuxEntryHandler):
def _defaultargs(self, entry):
""" argument list for adding, modifying, and deleting entries """
(addr, netmask) = entry.get("name").split("/")
- return (addr, netmask, entry.get("proto"), "",
+ return (addr, netmask, entry.get("proto"), entry.get("mlsrange", ""),
entry.get("selinuxtype"))
@@ -610,7 +621,8 @@ class SELinuxSeloginHandler(SELinuxEntryHandler):
def _defaultargs(self, entry):
""" argument list for adding, modifying, and deleting entries """
- return (entry.get("name"), entry.get("selinuxuser"), "")
+ return (entry.get("name"), entry.get("selinuxuser"),
+ entry.get("mlsrange", ""))
class SELinuxSeuserHandler(SELinuxEntryHandler):
@@ -650,15 +662,16 @@ class SELinuxSeuserHandler(SELinuxEntryHandler):
# prefix. see the comment in Install() above for more
# details.
rv = [entry.get("name"),
- entry.get("roles", "").replace(" ", ",").split(",")]
+ entry.get("roles", "").replace(" ", ",").split(","),
+ '', entry.get("mlsrange", "")]
if self.needs_prefix:
- rv.extend(['', '', entry.get("prefix")])
+ rv.append(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")])
+ rv.append(entry.get("prefix"))
return tuple(rv)
@@ -670,7 +683,8 @@ class SELinuxSeinterfaceHandler(SELinuxEntryHandler):
def _defaultargs(self, entry):
""" argument list for adding, modifying, and deleting entries """
- return (entry.get("name"), '', entry.get("selinuxtype"))
+ return (entry.get("name"), entry.get("mlsrange", ""),
+ entry.get("selinuxtype"))
class SELinuxSepermissiveHandler(SELinuxEntryHandler):
diff --git a/src/lib/Bcfg2/Client/Tools/SMF.py b/src/lib/Bcfg2/Client/Tools/SMF.py
index 68d8b2965..8b23a4a37 100644
--- a/src/lib/Bcfg2/Client/Tools/SMF.py
+++ b/src/lib/Bcfg2/Client/Tools/SMF.py
@@ -48,12 +48,12 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
gname = "/etc/rc*.d/%s" % filename
files = glob.glob(gname.replace('_', '.'))
if files:
- self.logger.debug("Matched %s with %s" % \
+ self.logger.debug("Matched %s with %s" %
(entry.get("FMRI"), ":".join(files)))
return entry.get('status') == 'on'
else:
- self.logger.debug("No service matching %s" % \
- (entry.get("FMRI")))
+ self.logger.debug("No service matching %s" %
+ entry.get("FMRI"))
return entry.get('status') == 'off'
try:
srvdata = \
@@ -76,13 +76,12 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
if entry.get("FMRI").startswith('lrc'):
try:
loc = entry.get("FMRI")[4:].replace('_', '.')
- self.logger.debug("Renaming file %s to %s" % \
+ self.logger.debug("Renaming file %s to %s" %
(loc, loc.replace('/S', '/DISABLED.S')))
os.rename(loc, loc.replace('/S', '/DISABLED.S'))
return True
except OSError:
- self.logger.error("Failed to rename init script %s" % \
- (loc))
+ self.logger.error("Failed to rename init script %s" % loc)
return False
else:
return self.cmd.run("/usr/sbin/svcadm disable %s" %
@@ -118,12 +117,12 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
def FindExtra(self):
"""Find Extra SMF Services."""
- allsrv = [name for name, version in \
- [srvc.split()
- for srvc in self.cmd.run([
- "/usr/bin/svcs", "-a", "-H",
- "-o", "FMRI,STATE"]).stdout.splitlines()]
- if version != 'disabled']
+ allsrv = []
+ for srvc in self.cmd.run(["/usr/bin/svcs", "-a", "-H",
+ "-o", "FMRI,STATE"]).stdout.splitlines():
+ name, version = srvc.split()
+ if version != 'disabled':
+ allsrv.append(name)
for svc in self.getSupportedEntries():
if svc.get("FMRI") in allsrv:
diff --git a/src/lib/Bcfg2/Client/Tools/SYSV.py b/src/lib/Bcfg2/Client/Tools/SYSV.py
index 38072c52e..aca7d593c 100644
--- a/src/lib/Bcfg2/Client/Tools/SYSV.py
+++ b/src/lib/Bcfg2/Client/Tools/SYSV.py
@@ -41,7 +41,7 @@ class SYSV(Bcfg2.Client.Tools.PkgTool):
self.noaskfile.write(noask)
# flush admin file contents to disk
self.noaskfile.flush()
- self.pkgtool = (self.pkgtool[0] % ("-a %s" % (self.noaskname)), \
+ self.pkgtool = (self.pkgtool[0] % ("-a %s" % (self.noaskname)),
self.pkgtool[1])
except: # pylint: disable=W0702
self.pkgtool = (self.pkgtool[0] % "", self.pkgtool[1])
@@ -66,7 +66,7 @@ class SYSV(Bcfg2.Client.Tools.PkgTool):
desired_version = entry.get('version')
if desired_version == 'any':
desired_version = self.installed.get(entry.get('name'),
- desired_version)
+ desired_version)
if not self.cmd.run(["/usr/bin/pkginfo", "-q", "-v",
desired_version, entry.get('name')]):
diff --git a/src/lib/Bcfg2/Client/Tools/Systemd.py b/src/lib/Bcfg2/Client/Tools/Systemd.py
index 027d91c71..20a172d3d 100644
--- a/src/lib/Bcfg2/Client/Tools/Systemd.py
+++ b/src/lib/Bcfg2/Client/Tools/Systemd.py
@@ -13,6 +13,8 @@ class Systemd(Bcfg2.Client.Tools.SvcTool):
__handles__ = [('Service', 'systemd')]
__req__ = {'Service': ['name', 'status']}
+ conflicts = ['Chkconfig']
+
def get_svc_command(self, service, action):
return "/bin/systemctl %s %s.service" % (action, service.get('name'))
diff --git a/src/lib/Bcfg2/Client/Tools/Upstart.py b/src/lib/Bcfg2/Client/Tools/Upstart.py
index cd1c4a2bc..c96eab69d 100644
--- a/src/lib/Bcfg2/Client/Tools/Upstart.py
+++ b/src/lib/Bcfg2/Client/Tools/Upstart.py
@@ -46,9 +46,9 @@ class Upstart(Bcfg2.Client.Tools.SvcTool):
entry.get('name'))
return False
- match = re.compile("%s( \(.*\))? (start|stop)/(running|waiting)" %
+ match = re.compile(r'%s( \(.*\))? (start|stop)/(running|waiting)' %
entry.get('name')).match(output)
- if match == None:
+ if match is None:
# service does not exist
entry.set('current_status', 'off')
status = False
diff --git a/src/lib/Bcfg2/Client/Tools/VCS.py b/src/lib/Bcfg2/Client/Tools/VCS.py
index 66e76f566..aca5dbbc7 100644
--- a/src/lib/Bcfg2/Client/Tools/VCS.py
+++ b/src/lib/Bcfg2/Client/Tools/VCS.py
@@ -1,14 +1,15 @@
"""VCS support."""
# TODO:
-# * git_write_index
# * add svn support
# * integrate properly with reports
missing = []
+import errno
import os
import shutil
import sys
+import stat
# python-dulwich git imports
try:
@@ -26,6 +27,38 @@ except ImportError:
import Bcfg2.Client.Tools
+def cleanup_mode(mode):
+ """Cleanup a mode value.
+
+ This will return a mode that can be stored in a tree object.
+
+ :param mode: Mode to clean up.
+ """
+ if stat.S_ISLNK(mode):
+ return stat.S_IFLNK
+ elif stat.S_ISDIR(mode):
+ return stat.S_IFDIR
+ elif dulwich.index.S_ISGITLINK(mode):
+ return dulwich.index.S_IFGITLINK
+ ret = stat.S_IFREG | int('644', 8)
+ ret |= (mode & int('111', 8))
+ return ret
+
+
+def index_entry_from_stat(stat_val, hex_sha, flags, mode=None):
+ """Create a new index entry from a stat value.
+
+ :param stat_val: POSIX stat_result instance
+ :param hex_sha: Hex sha of the object
+ :param flags: Index flags
+ """
+ if mode is None:
+ mode = cleanup_mode(stat_val.st_mode)
+ return (stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev,
+ stat_val.st_ino, mode, stat_val.st_uid,
+ stat_val.st_gid, stat_val.st_size, hex_sha, flags)
+
+
class VCS(Bcfg2.Client.Tools.Tool):
"""VCS support."""
__handles__ = [('Path', 'vcs')]
@@ -47,11 +80,24 @@ class VCS(Bcfg2.Client.Tools.Tool):
self.logger.info("Repository %s does not exist" %
entry.get('name'))
return False
- cur_rev = repo.head()
- if cur_rev != entry.get('revision'):
+ try:
+ expected_rev = entry.get('revision')
+ cur_rev = repo.head()
+ except:
+ return False
+
+ try:
+ client, path = dulwich.client.get_transport_and_path(entry.get('sourceurl'))
+ remote_refs = client.fetch_pack(path, (lambda x: None), None, None, None)
+ if expected_rev in remote_refs:
+ expected_rev = remote_refs[expected_rev]
+ except:
+ pass
+
+ if cur_rev != expected_rev:
self.logger.info("At revision %s need to go to revision %s" %
- (cur_rev, entry.get('revision')))
+ (cur_rev.strip(), expected_rev.strip()))
return False
return True
@@ -71,35 +117,47 @@ class VCS(Bcfg2.Client.Tools.Tool):
destname)
return False
- destr = dulwich.repo.Repo.init(destname, mkdir=True)
+ dulwich.file.ensure_dir_exists(destname)
+ destr = dulwich.repo.Repo.init(destname)
cl, host_path = dulwich.client.get_transport_and_path(entry.get('sourceurl'))
remote_refs = cl.fetch(host_path,
destr,
determine_wants=destr.object_store.determine_wants_all,
progress=sys.stdout.write)
- destr.refs['refs/heads/master'] = entry.get('revision')
- dtree = destr[entry.get('revision')].tree
- obj_store = destr.object_store
- for fname, mode, sha in obj_store.iter_tree_contents(dtree):
- fullpath = os.path.join(destname, fname)
- try:
- f = open(os.path.join(destname, fname), 'wb')
- except IOError:
- dir = os.path.split(fullpath)[0]
- os.makedirs(dir)
- f = open(os.path.join(destname, fname), 'wb')
- f.write(destr[sha].data)
- f.close()
- os.chmod(os.path.join(destname, fname), mode)
+
+ if entry.get('revision') in remote_refs:
+ destr.refs['HEAD'] = remote_refs[entry.get('revision')]
+ else:
+ destr.refs['HEAD'] = entry.get('revision')
+
+ dtree = destr['HEAD'].tree
+ index = dulwich.index.Index(destr.index_path())
+ for fname, mode, sha in destr.object_store.iter_tree_contents(dtree):
+ full_path = os.path.join(destname, fname)
+ dulwich.file.ensure_dir_exists(os.path.dirname(full_path))
+
+ if stat.S_ISLNK(mode):
+ src_path = destr[sha].as_raw_string()
+ try:
+ os.symlink(src_path, full_path)
+ except OSError:
+ e = sys.exc_info()[1]
+ if e.errno == errno.EEXIST:
+ os.unlink(full_path)
+ os.symlink(src_path, full_path)
+ else:
+ raise
+ else:
+ file = open(full_path, 'wb')
+ file.write(destr[sha].as_raw_string())
+ file.close()
+ os.chmod(full_path, mode)
+
+ st = os.lstat(full_path)
+ index[fname] = index_entry_from_stat(st, sha, 0)
+
+ index.write()
return True
- # FIXME: figure out how to write the git index properly
- #iname = "%s/.git/index" % entry.get('name')
- #f = open(iname, 'w+')
- #entries = obj_store[sha].iteritems()
- #try:
- # dulwich.index.write_index(f, entries)
- #finally:
- # f.close()
def Verifysvn(self, entry, _):
"""Verify svn repositories"""
diff --git a/src/lib/Bcfg2/Client/Tools/YUM.py b/src/lib/Bcfg2/Client/Tools/YUM.py
index c9fae7fc7..c30c0a13a 100644
--- a/src/lib/Bcfg2/Client/Tools/YUM.py
+++ b/src/lib/Bcfg2/Client/Tools/YUM.py
@@ -131,10 +131,12 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
def __init__(self, logger, setup, config):
self.yumbase = self._loadYumBase(setup=setup, logger=logger)
Bcfg2.Client.Tools.PkgTool.__init__(self, logger, setup, config)
- self.ignores = [entry.get('name') for struct in config \
- for entry in struct \
- if entry.tag == 'Path' and \
- entry.get('type') == 'ignore']
+ self.ignores = []
+ for struct in config:
+ self.ignores.extend([entry.get('name')
+ for entry in struct
+ if (entry.tag == 'Path' and
+ entry.get('type') == 'ignore')])
self.instance_status = {}
self.extra_instances = []
self.modlists = {}
@@ -293,8 +295,8 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
group. """
missing = Bcfg2.Client.Tools.PkgTool.missing_attrs(self, entry)
- if entry.get('name', None) == None and \
- entry.get('group', None) == None:
+ if (entry.get('name', None) is None and
+ entry.get('group', None) is None):
missing += ['name', 'group']
return missing
@@ -422,10 +424,10 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
if entry.get('group'):
self.logger.debug("Verifying packages for group %s" %
- entry.get('group'))
+ entry.get('group'))
else:
self.logger.debug("Verifying package instances for %s" %
- entry.get('name'))
+ entry.get('name'))
self.verify_cache = dict() # Used for checking multilib packages
self.modlists[entry] = modlist
@@ -434,10 +436,10 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
package_fail = False
qtext_versions = []
virt_pkg = False
- pkg_checks = self.pkg_checks and \
- entry.get('pkg_checks', 'true').lower() == 'true'
- pkg_verify = self.pkg_verify and \
- entry.get('pkg_verify', 'true').lower() == 'true'
+ pkg_checks = (self.pkg_checks and
+ entry.get('pkg_checks', 'true').lower() == 'true')
+ pkg_verify = (self.pkg_verify and
+ entry.get('pkg_verify', 'true').lower() == 'true')
yum_group = False
if entry.get('name') == 'gpg-pubkey':
@@ -455,15 +457,13 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
if d]
group_type = entry.get('choose', 'default')
if group_type in ['default', 'optional', 'all']:
- group_packages += [p
- for p, d in
- group.default_packages.items()
- if d]
+ group_packages += [
+ p for p, d in group.default_packages.items()
+ if d]
if group_type in ['optional', 'all']:
- group_packages += [p
- for p, d in
- group.optional_packages.items()
- if d]
+ group_packages += [
+ p for p, d in group.optional_packages.items()
+ if d]
if len(group_packages) == 0:
self.logger.error("No packages found for group %s" %
entry.get("group"))
@@ -489,7 +489,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
else:
all_pkg_objs = \
self.yumbase.rpmdb.searchNevra(name=entry.get('name'))
- if len(all_pkg_objs) == 0 and yum_group != True:
+ if len(all_pkg_objs) == 0 and yum_group is not True:
# Some sort of virtual capability? Try to resolve it
all_pkg_objs = self.yumbase.rpmdb.searchProvides(entry.get('name'))
if len(all_pkg_objs) > 0:
@@ -567,9 +567,9 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
pkg_objs = [po for po in all_pkg_objs]
else:
pkg_objs = [po for po in all_pkg_objs
- if po.checkPrco('provides',
- (nevra["name"], 'EQ',
- tuple(vlist)))]
+ if po.checkPrco('provides',
+ (nevra["name"], 'EQ',
+ tuple(vlist)))]
elif entry.get('name') == 'gpg-pubkey':
if 'version' not in nevra:
self.logger.warning("Skipping verify: gpg-pubkey without "
@@ -622,7 +622,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
if self.setup.get('quick', False):
# Passed -q on the command line
continue
- if not (pkg_verify and \
+ if not (pkg_verify and
inst.get('pkg_verify', 'true').lower() == 'true'):
continue
@@ -648,8 +648,8 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
# Now take out the Yum specific objects / modlists / unproblems
ignores = [ig.get('name') for ig in entry.findall('Ignore')] + \
- [ig.get('name') for ig in inst.findall('Ignore')] + \
- self.ignores
+ [ig.get('name') for ig in inst.findall('Ignore')] + \
+ self.ignores
for fname, probs in list(vrfy_result.items()):
if fname in modlist:
self.logger.debug(" %s in modlist, skipping" % fname)
@@ -737,8 +737,9 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
for pkg in pkg_objs:
self.logger.debug(" Extra Instance Found: %s" % str(pkg))
Bcfg2.Client.XML.SubElement(extra_entry, 'Instance',
- epoch=pkg.epoch, name=pkg.name, version=pkg.version,
- release=pkg.release, arch=pkg.arch)
+ epoch=pkg.epoch, name=pkg.name,
+ version=pkg.version,
+ release=pkg.release, arch=pkg.arch)
if pkg_objs == []:
return None
@@ -782,7 +783,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
ver = yum.misc.keyIdToRPMVer(gpg['keyid'])
rel = yum.misc.keyIdToRPMVer(gpg['timestamp'])
if not (ver == inst.get('version') and rel == inst.get('release')):
- self.logger.info("GPG key file %s does not match gpg-pubkey-%s-%s"\
+ self.logger.info("GPG key file %s does not match gpg-pubkey-%s-%s"
% (key_file, inst.get('version'),
inst.get('release')))
return False
@@ -791,20 +792,21 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
gpg['timestamp']) == 0:
result = tset.pgpImportPubkey(yum.misc.procgpgkey(rawkey))
else:
- self.logger.debug("gpg-pubkey-%s-%s already installed"\
- % (inst.get('version'),
- inst.get('release')))
+ self.logger.debug("gpg-pubkey-%s-%s already installed" %
+ (inst.get('version'), inst.get('release')))
return True
if result != 0:
- self.logger.debug("Unable to install %s-%s" % \
- (self.instance_status[inst].get('pkg').get('name'),
- nevra2string(inst)))
+ self.logger.debug(
+ "Unable to install %s-%s" %
+ (self.instance_status[inst].get('pkg').get('name'),
+ nevra2string(inst)))
return False
else:
- self.logger.debug("Installed %s-%s-%s" % \
- (self.instance_status[inst].get('pkg').get('name'),
- inst.get('version'), inst.get('release')))
+ self.logger.debug(
+ "Installed %s-%s-%s" %
+ (self.instance_status[inst].get('pkg').get('name'),
+ inst.get('version'), inst.get('release')))
return True
def _runYumTransaction(self):
@@ -898,7 +900,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
# Remove extra instances.
# Can not reverify because we don't have a package entry.
if self.extra_instances is not None and len(self.extra_instances) > 0:
- if (self.setup.get('remove') == 'all' or \
+ if (self.setup.get('remove') == 'all' or
self.setup.get('remove') == 'packages'):
self.Remove(self.extra_instances)
else:
@@ -913,7 +915,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
# Figure out which instances of the packages actually need something
# doing to them and place in the appropriate work 'queue'.
for pkg in packages:
- insts = [pinst for pinst in pkg \
+ insts = [pinst for pinst in pkg
if pinst.tag in ['Instance', 'Package']]
if insts:
for inst in insts:
@@ -1006,10 +1008,11 @@ class YUM(Bcfg2.Client.Tools.PkgTool):
if not self.setup['kevlar']:
for pkg_entry in [p for p in packages if self.canVerify(p)]:
- self.logger.debug("Reverifying Failed Package %s" \
- % (pkg_entry.get('name')))
- states[pkg_entry] = self.VerifyPackage(pkg_entry,
- self.modlists.get(pkg_entry, []))
+ self.logger.debug("Reverifying Failed Package %s" %
+ pkg_entry.get('name'))
+ states[pkg_entry] = \
+ self.VerifyPackage(pkg_entry,
+ self.modlists.get(pkg_entry, []))
for entry in [ent for ent in packages if states[ent]]:
self.modified.append(entry)
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index c5a5ee4d6..11fe55bd6 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -519,6 +519,22 @@ class SvcTool(Tool):
"""
return '/etc/init.d/%s %s' % (service.get('name'), action)
+ def get_bootstatus(self, service):
+ """ Return the bootstatus attribute if it exists.
+
+ :param service: The service entry
+ :type service: lxml.etree._Element
+ :returns: string or None - Value of bootstatus if it exists. If
+ bootstatus is unspecified and status is not *ignore*,
+ return value of status. If bootstatus is unspecified
+ and status is *ignore*, return None.
+ """
+ if service.get('bootstatus') is not None:
+ return service.get('bootstatus')
+ elif service.get('status') != 'ignore':
+ return service.get('status')
+ return None
+
def start_service(self, service):
""" Start a service.
diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py
index dd5ae1e83..25603186e 100644
--- a/src/lib/Bcfg2/Client/__init__.py
+++ b/src/lib/Bcfg2/Client/__init__.py
@@ -1,7 +1,5 @@
"""This contains all Bcfg2 Client modules"""
-__all__ = ["Frame", "Tools", "XML", "Client"]
-
import os
import sys
import select
@@ -21,11 +19,8 @@ def prompt(msg):
while len(select.select([sys.stdin.fileno()], [], [], 0.0)[0]) > 0:
os.read(sys.stdin.fileno(), 4096)
try:
- ans = input(msg.encode(sys.stdout.encoding, 'replace'))
+ ans = input(msg)
return ans in ['y', 'Y']
except EOFError:
- # python 2.4.3 on CentOS doesn't like ^C for some reason
- return False
- except:
- print("Error while reading input: %s" % sys.exc_info()[1])
- return False
+ # handle ^C on rhel-based platforms
+ raise SystemExit(1)
diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py
index 44c76303c..049236e03 100644
--- a/src/lib/Bcfg2/Compat.py
+++ b/src/lib/Bcfg2/Compat.py
@@ -10,7 +10,7 @@ Python 2.4 and such-like """
import sys
-# pylint: disable=E0611,W0611,W0622,C0103
+# pylint: disable=E0601,E0602,E0611,W0611,W0622,C0103
try:
from email.Utils import formatdate
@@ -79,21 +79,35 @@ except NameError:
def u_str(string, encoding=None):
""" print to file compatibility """
if sys.hexversion >= 0x03000000:
- if encoding is not None:
- return string.encode(encoding)
- else:
- return string
+ return string
else:
if encoding is not None:
return unicode(string, encoding)
else:
return unicode(string)
+try:
+ from functools import wraps
+except ImportError:
+ def wraps(wrapped): # pylint: disable=W0613
+ """ implementation of functools.wraps() for python 2.4 """
+ return lambda f: f
+
+
# base64 compat
if sys.hexversion >= 0x03000000:
from base64 import b64encode as _b64encode, b64decode as _b64decode
- b64encode = lambda s: _b64encode(s.encode('UTF-8')).decode('UTF-8')
- b64decode = lambda s: _b64decode(s.encode('UTF-8')).decode('UTF-8')
+
+ @wraps(_b64encode)
+ def b64encode(val, **kwargs): # pylint: disable=C0111
+ try:
+ return _b64encode(val, **kwargs)
+ except TypeError:
+ return _b64encode(val.encode('UTF-8'), **kwargs).decode('UTF-8')
+
+ @wraps(_b64decode)
+ def b64decode(val, **kwargs): # pylint: disable=C0111
+ return _b64decode(val.encode('UTF-8'), **kwargs).decode('UTF-8')
else:
from base64 import b64encode, b64decode
@@ -242,14 +256,6 @@ except ImportError:
from md5 import md5
-try:
- from functools import wraps
-except ImportError:
- def wraps(wrapped): # pylint: disable=W0613
- """ implementation of functools.wraps() for python 2.4 """
- return lambda f: f
-
-
def oct_mode(mode):
""" Convert a decimal number describing a POSIX permissions mode
to a string giving the octal mode. In Python 2, this is a synonym
@@ -270,3 +276,11 @@ try:
except NameError:
# longs are just ints in py3k
long = int
+
+
+try:
+ cmp = cmp
+except NameError:
+ def cmp(a, b):
+ """ Py3k implementation of cmp() """
+ return (a > b) - (a < b)
diff --git a/src/lib/Bcfg2/Encryption.py b/src/lib/Bcfg2/Encryption.py
index 2b4ba6237..b4674d72f 100755
--- a/src/lib/Bcfg2/Encryption.py
+++ b/src/lib/Bcfg2/Encryption.py
@@ -27,7 +27,7 @@ ALGORITHM = "aes_256_cbc"
#: Default initialization vector. For best security, you should use a
#: unique IV for each message. :func:`ssl_encrypt` does this in an
#: automated fashion.
-IV = '\0' * 16
+IV = r'\0' * 16
#: The config file section encryption options and passphrases are
#: stored in
@@ -116,9 +116,11 @@ def ssl_decrypt(data, passwd, algorithm=ALGORITHM):
# base64-decode the data
data = b64decode(data)
salt = data[8:16]
+ # pylint: disable=E1101
hashes = [md5(passwd + salt).digest()]
for i in range(1, 3):
hashes.append(md5(hashes[i - 1] + passwd + salt).digest())
+ # pylint: enable=E1101
key = hashes[0] + hashes[1]
iv = hashes[2]
@@ -144,9 +146,11 @@ def ssl_encrypt(plaintext, passwd, algorithm=ALGORITHM, salt=None):
if salt is None:
salt = Rand.rand_bytes(8)
+ # pylint: disable=E1101
hashes = [md5(passwd + salt).digest()]
for i in range(1, 3):
hashes.append(md5(hashes[i - 1] + passwd + salt).digest())
+ # pylint: enable=E1101
key = hashes[0] + hashes[1]
iv = hashes[2]
diff --git a/src/lib/Bcfg2/Logger.py b/src/lib/Bcfg2/Logger.py
index 5bbc9ff96..e537b6148 100644
--- a/src/lib/Bcfg2/Logger.py
+++ b/src/lib/Bcfg2/Logger.py
@@ -144,9 +144,9 @@ def add_console_handler(level=logging.DEBUG):
# tell the handler to use this format
console.setFormatter(TermiosFormatter())
try:
- console.set_name("console")
+ console.set_name("console") # pylint: disable=E1101
except AttributeError:
- console.name = "console"
+ console.name = "console" # pylint: disable=W0201
logging.root.addHandler(console)
@@ -162,9 +162,9 @@ def add_syslog_handler(procname, syslog_facility, level=logging.DEBUG):
('localhost', 514),
syslog_facility)
try:
- syslog.set_name("syslog")
+ syslog.set_name("syslog") # pylint: disable=E1101
except AttributeError:
- syslog.name = "syslog"
+ syslog.name = "syslog" # pylint: disable=W0201
syslog.setLevel(level)
syslog.setFormatter(
logging.Formatter('%(name)s[%(process)d]: %(message)s'))
@@ -179,9 +179,9 @@ def add_file_handler(to_file, level=logging.DEBUG):
"""Add a logging handler that logs to to_file."""
filelog = logging.FileHandler(to_file)
try:
- filelog.set_name("file")
+ filelog.set_name("file") # pylint: disable=E1101
except AttributeError:
- filelog.name = "file"
+ filelog.name = "file" # pylint: disable=W0201
filelog.setLevel(level)
filelog.setFormatter(
logging.Formatter('%(asctime)s %(name)s[%(process)d]: %(message)s'))
@@ -197,7 +197,7 @@ def setup_logging(procname, to_console=True, to_syslog=True,
params = []
if to_console:
- if to_console == True:
+ if to_console is True:
to_console = logging.WARNING
if level == 0:
clvl = to_console
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py
index 7c91ca3cc..243c4ed2a 100644
--- a/src/lib/Bcfg2/Options.py
+++ b/src/lib/Bcfg2/Options.py
@@ -308,14 +308,14 @@ def list_split(c_string):
""" split an option string on commas, optionally surrounded by
whitespace, returning a list """
if c_string:
- return re.split("\s*,\s*", c_string)
+ return re.split(r'\s*,\s*', c_string)
return []
def colon_split(c_string):
""" split an option string on colons, returning a list """
if c_string:
- return c_string.split(':')
+ return c_string.split(r':')
return []
@@ -355,7 +355,7 @@ def get_size(value):
'512m', '2g'), get the absolute number of bytes as an integer """
if value == -1:
return value
- mat = re.match("(\d+)([KkMmGg])?", value)
+ mat = re.match(r'(\d+)([KkMmGg])?', value)
if not mat:
raise ValueError("Not a valid size", value)
rvalue = int(mat.group(1))
@@ -401,7 +401,8 @@ CFILE = \
Option('Specify configuration file',
default=DEFAULT_CONFIG_LOCATION,
cmd='-C',
- odesc='<conffile>')
+ odesc='<conffile>',
+ env="BCFG2_CONFIG")
LOCKFILE = \
Option('Specify lockfile',
default='/var/lock/bcfg2.run',
@@ -534,6 +535,11 @@ SERVER_FAM_IGNORE = \
'SCCS', '.svn', '4913', '.gitignore'],
cf=('server', 'ignore_files'),
cook=list_split)
+SERVER_FAM_BLOCK = \
+ Option('FAM blocks on startup until all events are processed',
+ default=False,
+ cook=get_bool,
+ cf=('server', 'fam_blocking'))
SERVER_LISTEN_ALL = \
Option('Listen on all interfaces',
default=False,
@@ -573,7 +579,7 @@ SERVER_PASSWORD = \
SERVER_PROTOCOL = \
Option('Server Protocol',
default='xmlrpc/ssl',
- cf=('communication', 'procotol'))
+ cf=('communication', 'protocol'))
SERVER_BACKEND = \
Option('Server Backend',
default='best',
@@ -603,6 +609,16 @@ SERVER_AUTHENTICATION = \
default='cert+password',
odesc='{cert|bootstrap|cert+password}',
cf=('communication', 'authentication'))
+SERVER_CHILDREN = \
+ Option('Spawn this number of children for the multiprocessing core. '
+ 'By default spawns children equivalent to the number of processors '
+ 'in the machine.',
+ default=None,
+ cmd='--children',
+ odesc='<children>',
+ cf=('server', 'children'),
+ cook=get_int,
+ long_arg=True)
# database options
DB_ENGINE = \
@@ -1082,6 +1098,15 @@ VERBOSE = \
cmd='-v',
cook=get_bool,
cf=('logging', 'verbose'))
+LOG_PERFORMANCE = \
+ Option("Periodically log performance statistics",
+ default=False,
+ cf=('logging', 'performance'))
+PERFLOG_INTERVAL = \
+ Option("Performance statistics logging interval in seconds",
+ default=300.0,
+ cook=get_timeout,
+ cf=('logging', 'performance_interval'))
# Plugin-specific options
CFG_VALIDATION = \
@@ -1156,6 +1181,7 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY,
password=SERVER_PASSWORD,
filemonitor=SERVER_FILEMONITOR,
ignore=SERVER_FAM_IGNORE,
+ fam_blocking=SERVER_FAM_BLOCK,
location=SERVER_LOCATION,
key=SERVER_KEY,
cert=SERVER_CERT,
@@ -1164,7 +1190,10 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY,
web_configfile=WEB_CFILE,
backend=SERVER_BACKEND,
vcs_root=SERVER_VCS_ROOT,
- authentication=SERVER_AUTHENTICATION)
+ authentication=SERVER_AUTHENTICATION,
+ perflog=LOG_PERFORMANCE,
+ perflog_interval=PERFLOG_INTERVAL,
+ children=SERVER_CHILDREN)
CRYPT_OPTIONS = dict(encrypt=ENCRYPT,
decrypt=DECRYPT,
@@ -1269,6 +1298,11 @@ TEST_COMMON_OPTIONS = dict(noseopts=TEST_NOSEOPTS,
xunit=TEST_XUNIT,
validate=CFG_VALIDATION)
+INFO_COMMON_OPTIONS = dict(ppath=PARANOID_PATH,
+ max_copies=PARANOID_MAX_COPIES)
+INFO_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS)
+INFO_COMMON_OPTIONS.update(SERVER_COMMON_OPTIONS)
+
class OptionParser(OptionSet):
"""
diff --git a/src/lib/Bcfg2/Proxy.py b/src/lib/Bcfg2/Proxy.py
index 62b83d0b4..f6db66a93 100644
--- a/src/lib/Bcfg2/Proxy.py
+++ b/src/lib/Bcfg2/Proxy.py
@@ -24,6 +24,7 @@ from Bcfg2.Compat import httplib, xmlrpclib, urlparse, quote_plus
version = sys.version_info[:2]
has_py26 = version >= (2, 6)
+has_py32 = version >= (3, 2)
__all__ = ["ComponentProxy",
"RetryMethod",
@@ -173,8 +174,12 @@ class SSLHTTPConnection(httplib.HTTPConnection):
"""
if not has_py26:
httplib.HTTPConnection.__init__(self, host, port, strict)
- else:
+ elif not has_py32:
httplib.HTTPConnection.__init__(self, host, port, strict, timeout)
+ else:
+ # the strict parameter is deprecated.
+ # HTTP 0.9-style "Simple Responses" are not supported anymore.
+ httplib.HTTPConnection.__init__(self, host, port, timeout=timeout)
self.key = key
self.cert = cert
self.ca = ca
@@ -309,7 +314,7 @@ class XMLRPCTransport(xmlrpclib.Transport):
errcode = response.status
errmsg = response.reason
headers = response.msg
- except (socket.error, SSL_ERROR):
+ except (socket.error, SSL_ERROR, httplib.BadStatusLine):
err = sys.exc_info()[1]
raise ProxyError(xmlrpclib.ProtocolError(host + handler,
408,
@@ -326,6 +331,7 @@ class XMLRPCTransport(xmlrpclib.Transport):
return self.parse_response(response)
if sys.hexversion < 0x03000000:
+ # pylint: disable=E1101
def send_request(self, host, handler, request_body, debug):
""" send_request() changed significantly in py3k."""
conn = self.make_connection(host)
@@ -334,6 +340,7 @@ class XMLRPCTransport(xmlrpclib.Transport):
self.send_user_agent(conn)
self.send_content(conn, request_body)
return conn
+ # pylint: enable=E1101
def ComponentProxy(url, user=None, password=None, key=None, cert=None, ca=None,
diff --git a/src/lib/Bcfg2/Reporting/Collector.py b/src/lib/Bcfg2/Reporting/Collector.py
index df82248d0..3d224432e 100644
--- a/src/lib/Bcfg2/Reporting/Collector.py
+++ b/src/lib/Bcfg2/Reporting/Collector.py
@@ -125,7 +125,9 @@ class ReportingCollector(object):
# this wil be missing if called from bcfg2-admin
self.terminate.set()
if self.transport:
- self.transport.shutdown()
+ try:
+ self.transport.shutdown()
+ except OSError:
+ pass
if self.storage:
self.storage.shutdown()
-
diff --git a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
index 3b2c0ccfa..aea5e9d4b 100644
--- a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
+++ b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
@@ -197,7 +197,8 @@ class DjangoORM(StorageBase):
def _import_Service(self, entry, state):
return self._import_default(entry, state,
defaults=dict(status='',
- current_status=''),
+ current_status='',
+ target_status=''),
mapping=dict(status='target_status'))
def _import_SEBoolean(self, entry, state):
diff --git a/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py b/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py
index 0a0f032e5..c7d5c512a 100644
--- a/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py
+++ b/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py
@@ -87,7 +87,7 @@ class LocalFilesystem(TransportBase):
# using a tmpfile to hopefully avoid the file monitor from grabbing too
# soon
- saved = open(tmp_file, 'w')
+ saved = open(tmp_file, 'wb')
try:
saved.write(payload)
except IOError:
@@ -123,7 +123,7 @@ class LocalFilesystem(TransportBase):
self.debug_log("Handling event %s" % event.filename)
payload = os.path.join(self.work_path, event.filename)
try:
- payloadfd = open(payload, "r")
+ payloadfd = open(payload, "rb")
interaction = cPickle.load(payloadfd)
payloadfd.close()
os.unlink(payload)
diff --git a/src/lib/Bcfg2/Reporting/Transport/__init__.py b/src/lib/Bcfg2/Reporting/Transport/__init__.py
index 5c51dad1e..73bdd0b3a 100644
--- a/src/lib/Bcfg2/Reporting/Transport/__init__.py
+++ b/src/lib/Bcfg2/Reporting/Transport/__init__.py
@@ -2,11 +2,11 @@
Public transport routines
"""
-import traceback
-
+import sys
from Bcfg2.Reporting.Transport.base import TransportError, \
TransportImportError
+
def load_transport(transport_name, setup):
"""
Try to load the transport. Raise TransportImportError on failure
@@ -18,13 +18,14 @@ def load_transport(transport_name, setup):
try:
mod = __import__(transport_name)
except:
- raise TransportImportError("Unavailable")
+ raise TransportImportError("Error importing transport %s: %s" %
+ (transport_name, sys.exc_info()[1]))
try:
- cls = getattr(mod, transport_name)
- return cls(setup)
+ return getattr(mod, transport_name)(setup)
except:
- raise TransportImportError("Transport unavailable: %s" %
- traceback.format_exc().splitlines()[-1])
+ raise TransportImportError("Error instantiating transport %s: %s" %
+ (transport_name, sys.exc_info()[1]))
+
def load_transport_from_config(setup):
"""Load the transport in the config... eventually"""
@@ -32,4 +33,3 @@ def load_transport_from_config(setup):
return load_transport(setup['reporting_transport'], setup)
except KeyError:
raise TransportImportError('Transport missing in config')
-
diff --git a/src/lib/Bcfg2/Reporting/models.py b/src/lib/Bcfg2/Reporting/models.py
index 4be509f53..598e1c6ec 100644
--- a/src/lib/Bcfg2/Reporting/models.py
+++ b/src/lib/Bcfg2/Reporting/models.py
@@ -3,7 +3,7 @@ import sys
from django.core.exceptions import ImproperlyConfigured
try:
- from django.db import models
+ from django.db import models, backend, connection
except ImproperlyConfigured:
e = sys.exc_info()[1]
print("Reports: unable to import django models: %s" % e)
@@ -26,6 +26,8 @@ TYPE_CHOICES = (
(TYPE_EXTRA, 'Extra'),
)
+_our_backend = None
+
def convert_entry_type_to_id(type_name):
"""Convert a entry type to its entry id"""
@@ -49,6 +51,22 @@ def hash_entry(entry_dict):
return hash(cPickle.dumps(dataset))
+def _quote(value):
+ """
+ Quote a string to use as a table name or column
+
+ Newer versions and various drivers require an argument
+ https://code.djangoproject.com/ticket/13630
+ """
+ global _our_backend
+ if not _our_backend:
+ try:
+ _our_backend = backend.DatabaseOperations(connection)
+ except TypeError:
+ _our_backend = backend.DatabaseOperations()
+ return _our_backend.quote_name(value)
+
+
class Client(models.Model):
"""Object representing every client we have seen stats for."""
creation = models.DateTimeField(auto_now_add=True)
@@ -77,16 +95,20 @@ class InteractionManager(models.Manager):
cursor = connection.cursor()
cfilter = "expiration is null"
- sql = 'select ri.id, x.client_id from (select client_id, MAX(timestamp) ' + \
- 'as timer from Reporting_interaction'
+ sql = 'select ri.id, x.client_id from ' + \
+ '(select client_id, MAX(timestamp) as timer from ' + \
+ _quote('Reporting_interaction')
if maxdate:
if not isinstance(maxdate, datetime):
raise ValueError('Expected a datetime object')
sql = sql + " where timestamp <= '%s' " % maxdate
cfilter = "(expiration is null or expiration > '%s') and creation <= '%s'" % (maxdate, maxdate)
- sql = sql + ' GROUP BY client_id) x, Reporting_interaction ri where ' + \
- 'ri.client_id = x.client_id AND ri.timestamp = x.timer'
- sql = sql + " and x.client_id in (select id from Reporting_client where %s)" % cfilter
+ sql = sql + ' GROUP BY client_id) x, ' + \
+ _quote('Reporting_interaction') + \
+ ' ri where ri.client_id = x.client_id AND' + \
+ ' ri.timestamp = x.timer and x.client_id in' + \
+ ' (select id from %s where %s)' % \
+ (_quote('Reporting_client'), cfilter)
try:
cursor.execute(sql)
return [item[0] for item in cursor.fetchall()]
@@ -95,7 +117,6 @@ class InteractionManager(models.Manager):
pass
return []
-
def recent(self, maxdate=None):
"""
Returns the most recent interactions for clients as of a date
@@ -115,7 +136,7 @@ class Interaction(models.Model):
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
- server = models.CharField(max_length=256) # Name of the server used for the interaction
+ server = models.CharField(max_length=256) # server used for interaction
good_count = models.IntegerField() # of good config-items
total_count = models.IntegerField() # of total config-items
bad_count = models.IntegerField(default=0)
@@ -139,9 +160,11 @@ class Interaction(models.Model):
posixgroups = models.ManyToManyField("POSIXGroupEntry")
failures = models.ManyToManyField("FailureEntry")
- entry_types = ('actions', 'packages', 'paths', 'services', 'sebooleans',
- 'seports', 'sefcontexts', 'senodes', 'selogins', 'seusers',
- 'seinterfaces', 'sepermissives', 'semodules', 'posixusers',
+ entry_types = ('actions', 'failures', 'packages',
+ 'paths', 'services', 'sebooleans',
+ 'seports', 'sefcontexts', 'senodes',
+ 'selogins', 'seusers', 'seinterfaces',
+ 'sepermissives', 'semodules', 'posixusers',
'posixgroups')
# Formerly InteractionMetadata
@@ -209,18 +232,24 @@ class Interaction(models.Model):
def bad(self):
rv = []
for entry in self.entry_types:
+ if entry == 'failures':
+ continue
rv.extend(getattr(self, entry).filter(state=TYPE_BAD))
return rv
def modified(self):
rv = []
for entry in self.entry_types:
+ if entry == 'failures':
+ continue
rv.extend(getattr(self, entry).filter(state=TYPE_MODIFIED))
return rv
def extra(self):
rv = []
for entry in self.entry_types:
+ if entry == 'failures':
+ continue
rv.extend(getattr(self, entry).filter(state=TYPE_EXTRA))
return rv
@@ -232,7 +261,8 @@ class Interaction(models.Model):
class Performance(models.Model):
"""Object representing performance data for any interaction."""
- interaction = models.ForeignKey(Interaction, related_name="performance_items")
+ interaction = models.ForeignKey(Interaction,
+ related_name="performance_items")
metric = models.CharField(max_length=128)
value = models.DecimalField(max_digits=32, decimal_places=16)
@@ -265,11 +295,11 @@ class Group(models.Model):
class Meta:
ordering = ('name',)
-
@staticmethod
def prune_orphans():
'''Prune unused groups'''
- Group.objects.filter(interaction__isnull=True, group__isnull=True).delete()
+ Group.objects.filter(interaction__isnull=True,
+ group__isnull=True).delete()
class Bundle(models.Model):
@@ -287,11 +317,11 @@ class Bundle(models.Model):
class Meta:
ordering = ('name',)
-
@staticmethod
def prune_orphans():
'''Prune unused bundles'''
- Bundle.objects.filter(interaction__isnull=True, group__isnull=True).delete()
+ Bundle.objects.filter(interaction__isnull=True,
+ group__isnull=True).delete()
# new interaction models
@@ -394,7 +424,7 @@ class BaseEntry(models.Model):
def prune_orphans(cls):
'''Remove unused entries'''
# yeat another sqlite hack
- cls_orphans = [x['id'] \
+ cls_orphans = [x['id']
for x in cls.objects.filter(interaction__isnull=True).values("id")]
i = 0
while i < len(cls_orphans):
@@ -669,7 +699,7 @@ class PathEntry(SuccessEntry):
acls = models.ManyToManyField(FileAcl)
detail_type = models.IntegerField(default=0,
- choices=DETAIL_CHOICES)
+ choices=DETAIL_CHOICES)
details = models.TextField(default='')
ENTRY_TYPE = r"Path"
diff --git a/src/lib/Bcfg2/Reporting/templates/base.html b/src/lib/Bcfg2/Reporting/templates/base.html
index c73339911..7f1fcba3b 100644
--- a/src/lib/Bcfg2/Reporting/templates/base.html
+++ b/src/lib/Bcfg2/Reporting/templates/base.html
@@ -1,4 +1,8 @@
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -25,8 +29,9 @@
<div id="header">
<a href="http://bcfg2.org"><img src='{% to_media_url bcfg2_logo.png %}'
- height='115' width='300' alt='Bcfg2' style='float:left; height: 115px' /></a>
- </div>
+ height='115' width='300' alt='Bcfg2'
+ style='float:left; height: 115px' /></a>
+ </div>
<div id="document">
<div id="content"><div id="contentwrapper">
@@ -46,26 +51,26 @@
<li>Overview</li>
</ul>
<ul class='menu-level2'>
- <li><a href="{% url reports_summary %}">Summary</a></li>
- <li><a href="{% url reports_history %}">Recent Interactions</a></li>
- <li><a href="{% url reports_timing %}">Timing</a></li>
+ <li><a href="{% url "reports_summary" %}">Summary</a></li>
+ <li><a href="{% url "reports_history" %}">Recent Interactions</a></li>
+ <li><a href="{% url "reports_timing" %}">Timing</a></li>
</ul>
<ul class='menu-level1'>
<li>Clients</li>
</ul>
<ul class='menu-level2'>
- <li><a href="{% url reports_grid_view %}">Grid View</a></li>
- <li><a href="{% url reports_detailed_list %}">Detailed List</a></li>
- <li><a href="{% url reports_client_manage %}">Manage</a></li>
+ <li><a href="{% url "reports_grid_view" %}">Grid View</a></li>
+ <li><a href="{% url "reports_detailed_list" %}">Detailed List</a></li>
+ <li><a href="{% url "reports_client_manage" %}">Manage</a></li>
</ul>
<ul class='menu-level1'>
<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>
+ <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>
</ul>
{% comment %}
TODO
diff --git a/src/lib/Bcfg2/Reporting/templates/clients/detail.html b/src/lib/Bcfg2/Reporting/templates/clients/detail.html
index 4608ce6f1..e890589a7 100644
--- a/src/lib/Bcfg2/Reporting/templates/clients/detail.html
+++ b/src/lib/Bcfg2/Reporting/templates/clients/detail.html
@@ -1,24 +1,28 @@
{% extends "base.html" %}
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block title %}Bcfg2 - Client {{client.name}}{% endblock %}
{% block extra_header_info %}
<style type="text/css">
.node_data {
- border: 1px solid #98DBCC;
- margin: 10px;
- padding-left: 18px;
+ border: 1px solid #98DBCC;
+ margin: 10px;
+ padding-left: 18px;
}
.node_data td {
- padding: 1px 20px 1px 2px;
+ padding: 1px 20px 1px 2px;
}
span.history_links {
- font-size: 90%;
- margin-left: 50px;
+ font-size: 90%;
+ margin-left: 50px;
}
span.history_links a {
- font-size: 90%;
+ font-size: 90%;
}
</style>
{% endblock %}
@@ -30,12 +34,12 @@ span.history_links a {
{% block content %}
<div class='detail_header'>
<h2>{{client.name}}</h2>
- <a href='{% url reports_client_manage %}#{{ client.name }}'>[manage]</a>
- <span class='history_links'><a href="{% url reports_client_history client.name %}">View History</a> | Jump to&nbsp;
+ <a href='{% url "reports_client_manage" %}#{{ client.name }}'>[manage]</a>
+ <span class='history_links'><a href="{% url "reports_client_history" client.name %}">View History</a> | Jump to&nbsp;
<select id="quick" name="quick" onchange="javascript:pageJump('quick');">
<option value="" selected="selected">--- Time ---</option>
{% for i in client.interactions.all|slice:":25" %}
- <option value="{% url reports_client_detail_pk hostname=client.name, pk=i.id %}">{{i.timestamp|date:"c"}}</option>
+ <option value="{% url "reports_client_detail_pk" hostname=client.name pk=i.id %}">{{i.timestamp|date:"c"}}</option>
{% endfor %}
</select></span>
</div>
@@ -110,7 +114,7 @@ span.history_links a {
{% for entry in entry_list %}
<tr class='{% cycle listview,listview_alt %}'>
<td class='entry_list_type'>{{entry.entry_type}}</td>
- <td><a href="{% url reports_item entry.class_name entry.pk interaction.pk %}">
+ <td><a href="{% url "reports_item" entry.class_name entry.pk interaction.pk %}">
{{entry.name}}</a></td>
</tr>
{% endfor %}
@@ -129,7 +133,7 @@ span.history_links a {
{% for failure in interaction.failures.all %}
<tr class='{% cycle listview,listview_alt %}'>
<td class='entry_list_type'>{{failure.entry_type}}</td>
- <td><a href="{% url reports_item failure.class_name failure.pk interaction.pk %}">
+ <td><a href="{% url "reports_item" failure.class_name failure.pk interaction.pk %}">
{{failure.name}}</a></td>
</tr>
{% endfor %}
@@ -140,11 +144,11 @@ span.history_links a {
{% if entry_list %}
<div class="entry_list recent_history_wrapper">
<div class="entry_list_head" style="border-bottom: 2px solid #98DBCC;">
- <h4 style="display: inline"><a href="{% url reports_client_history client.name %}">Recent Interactions</a></h4>
+ <h4 style="display: inline"><a href="{% url "reports_client_history" client.name %}">Recent Interactions</a></h4>
</div>
<div class='recent_history_box'>
{% include "widgets/interaction_list.inc" %}
- <div style='padding-left: 5px'><a href="{% url reports_client_history client.name %}">more...</a></div>
+ <div style='padding-left: 5px'><a href="{% url "reports_client_history" client.name %}">more...</a></div>
</div>
</div>
{% endif %}
diff --git a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
index fd9a545ce..33c78a5f0 100644
--- a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
+++ b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
@@ -1,5 +1,9 @@
{% extends "base-timeview.html" %}
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block title %}Bcfg2 - Detailed Client Listing{% endblock %}
{% block pagebanner %}Clients - Detailed View{% endblock %}
@@ -21,7 +25,7 @@
</tr>
{% for entry in entry_list %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='left_column'><a href='{% url Bcfg2.Reporting.views.client_detail hostname=entry.client.name, pk=entry.id %}'>{{ entry.client.name }}</a></td>
+ <td class='left_column'><a href='{% url "Bcfg2.Reporting.views.client_detail" hostname=entry.client.name pk=entry.id %}'>{{ entry.client.name }}</a></td>
<td class='right_column' style='width:75px'><a href='{% add_url_filter state=entry.state %}'
class='{{entry|determine_client_state}}'>{{ entry.state }}</a></td>
<td class='right_column_narrow'>{{ entry.good_count }}</td>
diff --git a/src/lib/Bcfg2/Reporting/templates/clients/index.html b/src/lib/Bcfg2/Reporting/templates/clients/index.html
index d9c415c20..eba83670b 100644
--- a/src/lib/Bcfg2/Reporting/templates/clients/index.html
+++ b/src/lib/Bcfg2/Reporting/templates/clients/index.html
@@ -1,5 +1,9 @@
{% extends "base-timeview.html" %}
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block extra_header_info %}
{% endblock%}
@@ -17,9 +21,9 @@
<td class='{{ inter|determine_client_state }}'>
<a href="{% spaceless %}
{% if not timestamp %}
- {% url reports_client_detail inter.client.name %}
+ {% url "reports_client_detail" inter.client.name %}
{% else %}
- {% url reports_client_detail_pk inter.client.name,inter.id %}
+ {% url "reports_client_detail_pk" inter.client.name inter.id %}
{% endif %}
{% endspaceless %}">{{ inter.client.name }}</a>
</td>
diff --git a/src/lib/Bcfg2/Reporting/templates/clients/manage.html b/src/lib/Bcfg2/Reporting/templates/clients/manage.html
index 443ec8ccb..03918aad7 100644
--- a/src/lib/Bcfg2/Reporting/templates/clients/manage.html
+++ b/src/lib/Bcfg2/Reporting/templates/clients/manage.html
@@ -1,4 +1,8 @@
{% extends "base.html" %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block extra_header_info %}
{% endblock%}
@@ -24,10 +28,10 @@
<td><span id="{{ client.name }}"> </span>
<span id="ttag-{{ client.name }}"> </span>
<span id="s-ttag-{{ client.name }}"> </span>
- <a href="{% url reports_client_detail client.name %}">{{ client.name }}</a></td>
+ <a href='{% url "reports_client_detail" client.name %}'>{{ client.name }}</a></td>
<td>{% firstof client.expiration 'Active' %}</td>
<td>
- <form method="post" action="{% url reports_client_manage %}">
+ <form method="post" action='{% url "reports_client_manage" %}'>
<div> {# here for no reason other then to validate #}
<input type="hidden" name="client_name" value="{{ client.name }}" />
<input type="hidden" name="client_action" value="{% if client.expiration %}unexpire{% else %}expire{% endif %}" />
diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/common.html b/src/lib/Bcfg2/Reporting/templates/config_items/common.html
index 57191ec39..91f37d7dc 100644
--- a/src/lib/Bcfg2/Reporting/templates/config_items/common.html
+++ b/src/lib/Bcfg2/Reporting/templates/config_items/common.html
@@ -1,5 +1,6 @@
{% extends "base-timeview.html" %}
{% load bcfg2_tags %}
+{% load url from future %}
{% block title %}Bcfg2 - Common Problems{% endblock %}
@@ -29,9 +30,9 @@
{% for item in type_list %}
<tr class='{% cycle listview,listview_alt %}'>
<td>{{ item.ENTRY_TYPE }}</td>
- <td><a href="{% url reports_entry item.class_name, item.pk %}">{{ item.name }}</a></td>
+ <td><a href='{% url "reports_entry" item.class_name item.pk %}'>{{ item.name }}</a></td>
<td>{{ item.num_entries }}</td>
- <td><a href="{% url reports_item item.ENTRY_TYPE, item.pk %}">{{ item.short_list|join:"," }}</a></td>
+ <td><a href='{% url "reports_item" item.ENTRY_TYPE item.pk %}'>{{ item.short_list|join:"," }}</a></td>
</tr>
{% endfor %}
</table>
diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html b/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html
index e940889ab..e3befb0eb 100644
--- a/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html
+++ b/src/lib/Bcfg2/Reporting/templates/config_items/entry_status.html
@@ -1,5 +1,9 @@
{% extends "base-timeview.html" %}
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block title %}Bcfg2 - Entry Status{% endblock %}
@@ -17,10 +21,10 @@
{% for item, inters in items %}
{% for inter in inters %}
<tr class='{% cycle listview,listview_alt %}'>
- <td><a href='{% url reports_client_detail hostname=inter.client.name %}'>{{inter.client.name}}</a></td>
- <td><a href='{% url reports_client_detail_pk hostname=inter.client.name, pk=inter.pk %}'>{{inter.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe}}</a></td>
+ <td><a href='{% url "reports_client_detail" hostname=inter.client.name %}'>{{inter.client.name}}</a></td>
+ <td><a href='{% url "reports_client_detail_pk" hostname=inter.client.name pk=inter.pk %}'>{{inter.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe}}</a></td>
<td>{{ item.get_state_display }}</td>
- <td style='white-space: nowrap'><a href="{% url reports_item entry_type=item.class_name pk=item.pk %}">({{item.pk}}) {{item.short_list|join:","}}</a></td>
+ <td style='white-space: nowrap'><a href='{% url "reports_item" entry_type=item.class_name pk=item.pk %}'>({{item.pk}}) {{item.short_list|join:","}}</a></td>
</tr>
{% endfor %}
{% endfor %}
diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/item.html b/src/lib/Bcfg2/Reporting/templates/config_items/item.html
index 259414399..b03d48045 100644
--- a/src/lib/Bcfg2/Reporting/templates/config_items/item.html
+++ b/src/lib/Bcfg2/Reporting/templates/config_items/item.html
@@ -1,6 +1,10 @@
{% extends "base.html" %}
{% load split %}
{% load syntax_coloring %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block title %}Bcfg2 - Element Details{% endblock %}
@@ -9,20 +13,20 @@
{% block extra_header_info %}
<style type="text/css">
#table_list_header {
- font-size: 100%;
+ font-size: 100%;
}
table.entry_list {
- width: auto;
+ width: auto;
}
div.information_wrapper {
- margin: 15px;
+ margin: 15px;
}
div.diff_wrapper {
- overflow: auto;
+ overflow: auto;
}
div.entry_list h3 {
- font-size: 90%;
- padding: 5px;
+ font-size: 90%;
+ padding: 5px;
}
</style>
{% endblock%}
@@ -131,9 +135,9 @@ div.entry_list h3 {
{% if associated_list %}
<table class="entry_list" cellpadding="3">
{% for inter in associated_list %}
- <tr><td><a href="{% url reports_client_detail inter.client.name %}"
+ <tr><td><a href='{% url "reports_client_detail" inter.client.name %}'
>{{inter.client.name}}</a></td>
- <td><a href="{% url reports_client_detail_pk hostname=inter.client.name,pk=inter.id %}"
+ <td><a href='{% url "reports_client_detail_pk" hostname=inter.client.name pk=inter.id %}'
>{{inter.timestamp}}</a></td>
</tr>
{% endfor %}
diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/listing.html b/src/lib/Bcfg2/Reporting/templates/config_items/listing.html
index 864392754..0e4812e85 100644
--- a/src/lib/Bcfg2/Reporting/templates/config_items/listing.html
+++ b/src/lib/Bcfg2/Reporting/templates/config_items/listing.html
@@ -1,5 +1,9 @@
{% extends "base-timeview.html" %}
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block title %}Bcfg2 - Element Listing{% endblock %}
@@ -21,9 +25,9 @@
<tr style='text-align: left' ><th>Name</th><th>Count</th><th>Reason</th></tr>
{% for entry in type_data %}
<tr class='{% cycle listview,listview_alt %}'>
- <td><a href="{% url reports_entry entry.class_name entry.pk %}">{{entry.name}}</a></td>
+ <td><a href='{% url "reports_entry" entry.class_name entry.pk %}'>{{entry.name}}</a></td>
<td>{{entry.num_entries}}</td>
- <td><a href="{% url reports_item entry.class_name entry.pk %}">{{entry.short_list|join:","}}</a></td>
+ <td><a href='{% url "reports_item" entry.class_name entry.pk %}'>{{entry.short_list|join:","}}</a></td>
</tr>
{% endfor %}
</table>
diff --git a/src/lib/Bcfg2/Reporting/templates/displays/summary.html b/src/lib/Bcfg2/Reporting/templates/displays/summary.html
index b9847cf96..ffafd52e0 100644
--- a/src/lib/Bcfg2/Reporting/templates/displays/summary.html
+++ b/src/lib/Bcfg2/Reporting/templates/displays/summary.html
@@ -1,5 +1,9 @@
{% extends "base-timeview.html" %}
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block title %}Bcfg2 - Client Summary{% endblock %}
{% block pagebanner %}Clients - Summary{% endblock %}
@@ -30,7 +34,7 @@ hide_tables[{{ forloop.counter0 }}] = "table_{{ summary.name }}";
<table id='table_{{ summary.name }}' class='entry_list'>
{% for node in summary.nodes|sort_interactions_by_name %}
<tr class='{% cycle listview,listview_alt %}'>
- <td><a href="{% url reports_client_detail_pk hostname=node.client.name,pk=node.id %}">{{ node.client.name }}</a></td>
+ <td><a href='{% url "reports_client_detail_pk" hostname=node.client.name pk=node.id %}'>{{ node.client.name }}</a></td>
</tr>
{% endfor %}
</table>
diff --git a/src/lib/Bcfg2/Reporting/templates/displays/timing.html b/src/lib/Bcfg2/Reporting/templates/displays/timing.html
index ff775ded5..8ac5e49bb 100644
--- a/src/lib/Bcfg2/Reporting/templates/displays/timing.html
+++ b/src/lib/Bcfg2/Reporting/templates/displays/timing.html
@@ -1,5 +1,9 @@
{% extends "base-timeview.html" %}
{% load bcfg2_tags %}
+{% comment %}
+This is needed for Django versions less than 1.5
+{% endcomment %}
+{% load url from future %}
{% block title %}Bcfg2 - Performance Metrics{% endblock %}
{% block pagebanner %}Performance Metrics{% endblock %}
@@ -12,7 +16,7 @@
<div class='client_list_box'>
{% if metrics %}
<table cellpadding="3">
- <tr id='table_list_header' class='listview'>
+ <tr id='table_list_header' class='listview'>
<td>Name</td>
<td>Parse</td>
<td>Probe</td>
@@ -21,15 +25,15 @@
<td>Config</td>
<td>Total</td>
</tr>
- {% for metric in metrics|dictsort:"name" %}
+ {% for metric in metrics|dictsort:"name" %}
<tr class='{% cycle listview,listview_alt %}'>
<td><a style='font-size: 100%'
- href="{% url reports_client_detail hostname=metric.name %}">{{ metric.name }}</a></td>
+ href='{% url "reports_client_detail" hostname=metric.name %}'>{{ metric.name }}</a></td>
{% for mitem in metric|build_metric_list %}
<td>{{ mitem }}</td>
{% endfor %}
- </tr>
- {% endfor %}
+ </tr>
+ {% endfor %}
</table>
{% else %}
<p>No metric data available</p>
diff --git a/src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py b/src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py
index f5f2e7528..489682f30 100644
--- a/src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py
+++ b/src/lib/Bcfg2/Reporting/templatetags/bcfg2_tags.py
@@ -5,9 +5,8 @@ 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.template.loader import get_template_from_string
+from django.utils.encoding import smart_str
from django.utils.safestring import mark_safe
from datetime import datetime, timedelta
from Bcfg2.Reporting.utils import filter_list
@@ -133,19 +132,22 @@ def filter_navigator(context):
del myargs[filter]
filters.append((filter,
reverse(view, args=args, kwargs=myargs) + qs))
- filters.sort(lambda x, y: cmp(x[0], y[0]))
+ filters.sort(key=lambda x: x[0])
myargs = kwargs.copy()
- selected=True
+ selected = True
if 'group' in myargs:
del myargs['group']
- selected=False
- groups = [('---', reverse(view, args=args, kwargs=myargs) + qs, selected)]
+ selected = False
+ groups = [('---',
+ reverse(view, args=args, kwargs=myargs) + qs,
+ selected)]
for group in Group.objects.values('name'):
myargs['group'] = group['name']
- groups.append((group['name'], reverse(view, args=args, kwargs=myargs) + qs,
- group['name'] == kwargs.get('group', '')))
-
+ groups.append((group['name'],
+ reverse(view, args=args, kwargs=myargs) + qs,
+ group['name'] == kwargs.get('group', '')))
+
return {'filters': filters, 'groups': groups}
except (Resolver404, NoReverseMatch, ValueError, KeyError):
pass
@@ -205,7 +207,7 @@ def sort_interactions_by_name(value):
Sort an interaction list by client name
"""
inters = list(value)
- inters.sort(lambda a, b: cmp(a.client.name, b.client.name))
+ inters.sort(key=lambda a: a.client.name)
return inters
@@ -223,7 +225,7 @@ class AddUrlFilter(template.Node):
filter_value = self.filter_value.resolve(context, True)
if filter_value:
filter_name = smart_str(self.filter_name)
- filter_value = smart_unicode(filter_value)
+ filter_value = smart_str(filter_value)
kwargs[filter_name] = filter_value
# These two don't make sense
if filter_name == 'server' and 'hostname' in kwargs:
@@ -306,6 +308,7 @@ def to_media_url(parser, token):
return MediaTag(filter_value)
+
@register.filter
def determine_client_state(entry):
"""
@@ -338,10 +341,11 @@ def do_qs(parser, token):
try:
tag, name, value = token.split_contents()
except ValueError:
- raise template.TemplateSyntaxError, "%r tag requires exactly two arguments" \
- % token.contents.split()[0]
+ raise template.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)
@@ -359,7 +363,7 @@ class QsNode(template.Node):
return ''
except KeyError:
if settings.TEMPLATE_DEBUG:
- raise Exception, "'qs' tag requires context['request']"
+ raise Exception("'qs' tag requires context['request']")
return ''
except:
return ''
@@ -380,6 +384,7 @@ def sort_link(parser, token):
return SortLinkNode(sort_key, text)
+
class SortLinkNode(template.Node):
__TMPL__ = "{% load bcfg2_tags %}<a href='{% qs 'sort' key %}'>{{ text }}</a>"
@@ -420,4 +425,3 @@ class SortLinkNode(template.Node):
raise
raise
return ''
-
diff --git a/src/lib/Bcfg2/Reporting/templatetags/syntax_coloring.py b/src/lib/Bcfg2/Reporting/templatetags/syntax_coloring.py
index 2712d6395..22700689f 100644
--- a/src/lib/Bcfg2/Reporting/templatetags/syntax_coloring.py
+++ b/src/lib/Bcfg2/Reporting/templatetags/syntax_coloring.py
@@ -1,11 +1,8 @@
-import sys
from django import template
-from django.utils.encoding import smart_unicode
+from django.utils.encoding import smart_str
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
-from Bcfg2.Compat import u_str
-
register = template.Library()
# pylint: disable=E0611
@@ -33,9 +30,9 @@ def syntaxhilight(value, arg="diff", autoescape=None):
if colorize:
try:
- output = u_str('<style type="text/css">') \
- + smart_unicode(HtmlFormatter().get_style_defs('.highlight')) \
- + u_str('</style>')
+ output = smart_str('<style type="text/css">') \
+ + smart_str(HtmlFormatter().get_style_defs('.highlight')) \
+ + smart_str('</style>')
lexer = get_lexer_by_name(arg)
output += highlight(value, lexer, HtmlFormatter())
@@ -43,6 +40,7 @@ def syntaxhilight(value, arg="diff", autoescape=None):
except:
return value
else:
- return mark_safe(u_str('<div class="note-box">Tip: Install pygments '
- 'for highlighting</div><pre>%s</pre>') % value)
+ return mark_safe(smart_str(
+ '<div class="note-box">Tip: Install pygments '
+ 'for highlighting</div><pre>%s</pre>') % value)
syntaxhilight.needs_autoescape = True
diff --git a/src/lib/Bcfg2/Reporting/views.py b/src/lib/Bcfg2/Reporting/views.py
index 6cba7bf8c..c7c2a503f 100644
--- a/src/lib/Bcfg2/Reporting/views.py
+++ b/src/lib/Bcfg2/Reporting/views.py
@@ -338,6 +338,8 @@ def client_detail(request, hostname=None, pk=None):
for label in etypes.values():
edict[label] = []
for ekind in inter.entry_types:
+ if ekind == 'failures':
+ continue
for ent in getattr(inter, ekind).all():
edict[etypes[ent.state]].append(ent)
context['entry_types'] = edict
diff --git a/src/lib/Bcfg2/SSLServer.py b/src/lib/Bcfg2/SSLServer.py
index a149b676f..316c2f86c 100644
--- a/src/lib/Bcfg2/SSLServer.py
+++ b/src/lib/Bcfg2/SSLServer.py
@@ -52,10 +52,11 @@ class XMLRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
allow_none=self.allow_none,
encoding=self.encoding)
except:
+ err = sys.exc_info()
self.logger.error("Unexpected handler error", exc_info=1)
# report exception back to server
raw_response = xmlrpclib.dumps(
- xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)),
+ xmlrpclib.Fault(1, "%s:%s" % (err[0].__name__, err[1])),
allow_none=self.allow_none, encoding=self.encoding)
return raw_response
@@ -197,8 +198,10 @@ class XMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
try:
username, password = auth_content.split(":")
except TypeError:
+ # pylint: disable=E0602
username, pw = auth_content.split(bytes(":", encoding='utf-8'))
password = pw.decode('utf-8')
+ # pylint: enable=E0602
except ValueError:
username = auth_content
password = ""
@@ -278,7 +281,10 @@ class XMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
raise
except socket.error:
err = sys.exc_info()[1]
- if err[0] == 32:
+ if isinstance(err, socket.timeout):
+ self.logger.warning("Connection timed out for %s" %
+ self.client_address[0])
+ elif err[0] == 32:
self.logger.warning("Connection dropped from %s" %
self.client_address[0])
elif err[0] == 104:
@@ -334,7 +340,7 @@ class XMLRPCServer(SocketServer.ThreadingMixIn, SSLServer,
:param register: Presence should be reported to service-location
:type register: bool
:param allow_none: Allow None values in XML-RPC
- :type allow_non: bool
+ :type allow_none: bool
:param encoding: Encoding to use for XML-RPC
"""
@@ -411,7 +417,9 @@ class XMLRPCServer(SocketServer.ThreadingMixIn, SSLServer,
def serve_forever(self):
"""Serve single requests until (self.serve == False)."""
self.serve = True
- self.task_thread = threading.Thread(target=self._tasks_thread)
+ self.task_thread = \
+ threading.Thread(name="%sThread" % self.__class__.__name__,
+ target=self._tasks_thread)
self.task_thread.start()
self.logger.info("serve_forever() [start]")
signal.signal(signal.SIGINT, self._handle_shutdown_signal)
diff --git a/src/lib/Bcfg2/Server/Admin/Client.py b/src/lib/Bcfg2/Server/Admin/Client.py
index b7916fab9..187ccfd71 100644
--- a/src/lib/Bcfg2/Server/Admin/Client.py
+++ b/src/lib/Bcfg2/Server/Admin/Client.py
@@ -8,6 +8,7 @@ from Bcfg2.Server.Plugin import MetadataConsistencyError
class Client(Bcfg2.Server.Admin.MetadataCore):
""" Create, delete, or list client entries """
__usage__ = "[options] [add|del|list] [attr=val]"
+ __plugin_whitelist__ = ["Metadata"]
def __call__(self, args):
if len(args) == 0:
@@ -17,19 +18,15 @@ class Client(Bcfg2.Server.Admin.MetadataCore):
try:
self.metadata.add_client(args[1])
except MetadataConsistencyError:
- err = sys.exc_info()[1]
- print("Error in adding client: %s" % err)
- raise SystemExit(1)
+ self.errExit("Error in adding client: %s" % sys.exc_info()[1])
elif args[0] in ['delete', 'remove', 'del', 'rm']:
try:
self.metadata.remove_client(args[1])
except MetadataConsistencyError:
- err = sys.exc_info()[1]
- print("Error in deleting client: %s" % err)
- raise SystemExit(1)
+ self.errExit("Error in deleting client: %s" %
+ sys.exc_info()[1])
elif args[0] in ['list', 'ls']:
for client in self.metadata.list_clients():
print(client)
else:
- print("No command specified")
- raise SystemExit(1)
+ self.errExit("No command specified")
diff --git a/src/lib/Bcfg2/Server/Admin/Compare.py b/src/lib/Bcfg2/Server/Admin/Compare.py
index c56dd0a8f..e3648a6d0 100644
--- a/src/lib/Bcfg2/Server/Admin/Compare.py
+++ b/src/lib/Bcfg2/Server/Admin/Compare.py
@@ -145,5 +145,4 @@ class Compare(Bcfg2.Server.Admin.Mode):
(old, new) = args
return self.compareSpecifications(new, old)
except IndexError:
- print(self.__call__.__doc__)
- raise SystemExit(1)
+ self.errExit(self.__call__.__doc__)
diff --git a/src/lib/Bcfg2/Server/Admin/Init.py b/src/lib/Bcfg2/Server/Admin/Init.py
index 4b8d65597..6175d8ed0 100644
--- a/src/lib/Bcfg2/Server/Admin/Init.py
+++ b/src/lib/Bcfg2/Server/Admin/Init.py
@@ -227,8 +227,8 @@ class Init(Bcfg2.Server.Admin.Mode):
def _prompt_password(self):
"""Ask for a password or generate one if none is provided."""
newpassword = getpass.getpass(
- "Input password used for communication verification "
- "(without echoing; leave blank for a random): ").strip()
+ "Input password used for communication verification "
+ "(without echoing; leave blank for a random): ").strip()
if len(newpassword) != 0:
self.data['password'] = newpassword
diff --git a/src/lib/Bcfg2/Server/Admin/Minestruct.py b/src/lib/Bcfg2/Server/Admin/Minestruct.py
index 6d0dab106..37ca74894 100644
--- a/src/lib/Bcfg2/Server/Admin/Minestruct.py
+++ b/src/lib/Bcfg2/Server/Admin/Minestruct.py
@@ -3,6 +3,7 @@ import getopt
import lxml.etree
import sys
import Bcfg2.Server.Admin
+from Bcfg2.Server.Plugin import PullSource
class Minestruct(Bcfg2.Server.Admin.StructureMode):
@@ -19,9 +20,8 @@ class Minestruct(Bcfg2.Server.Admin.StructureMode):
"Please see bcfg2-admin minestruct help for usage.")
try:
(opts, args) = getopt.getopt(args, 'f:g:h')
- except:
- self.log.error(self.__doc__)
- raise SystemExit(1)
+ except getopt.GetoptError:
+ self.errExit(self.__doc__)
client = args[0]
output = sys.stdout
@@ -32,20 +32,18 @@ class Minestruct(Bcfg2.Server.Admin.StructureMode):
try:
output = open(optarg, 'w')
except IOError:
- self.log.error("Failed to open file: %s" % (optarg))
- raise SystemExit(1)
+ self.errExit("Failed to open file: %s" % (optarg))
elif opt == '-g':
groups = optarg.split(':')
try:
extra = set()
- for source in self.bcore.pull_sources:
+ for source in self.bcore.plugins_by_type(PullSource):
for item in source.GetExtra(client):
extra.add(item)
- except:
- self.log.error("Failed to find extra entry info for client %s" %
- client)
- raise SystemExit(1)
+ except: # pylint: disable=W0702
+ self.errExit("Failed to find extra entry info for client %s" %
+ client)
root = lxml.etree.Element("Base")
self.log.info("Found %d extra entries" % (len(extra)))
add_point = root
diff --git a/src/lib/Bcfg2/Server/Admin/Perf.py b/src/lib/Bcfg2/Server/Admin/Perf.py
index 86eb6810d..f6bc22959 100644
--- a/src/lib/Bcfg2/Server/Admin/Perf.py
+++ b/src/lib/Bcfg2/Server/Admin/Perf.py
@@ -18,8 +18,7 @@ class Perf(Bcfg2.Server.Admin.Mode):
'password': Bcfg2.Options.SERVER_PASSWORD,
'server': Bcfg2.Options.SERVER_LOCATION,
'user': Bcfg2.Options.CLIENT_USER,
- 'timeout': Bcfg2.Options.CLIENT_TIMEOUT,
- }
+ 'timeout': Bcfg2.Options.CLIENT_TIMEOUT}
setup = Bcfg2.Options.OptionParser(optinfo)
setup.parse(sys.argv[1:])
proxy = Bcfg2.Proxy.ComponentProxy(setup['server'],
@@ -31,8 +30,8 @@ class Perf(Bcfg2.Server.Admin.Mode):
timeout=setup['timeout'])
data = proxy.get_statistics()
for key in sorted(data.keys()):
- output.append((key, ) +
- tuple(["%.06f" % item
- for item in data[key][:-1]] + \
- [data[key][-1]]))
+ output.append(
+ (key, ) +
+ tuple(["%.06f" % item
+ for item in data[key][:-1]] + [data[key][-1]]))
self.print_table(output)
diff --git a/src/lib/Bcfg2/Server/Admin/Pull.py b/src/lib/Bcfg2/Server/Admin/Pull.py
index 130e85b67..459fcec65 100644
--- a/src/lib/Bcfg2/Server/Admin/Pull.py
+++ b/src/lib/Bcfg2/Server/Admin/Pull.py
@@ -6,6 +6,7 @@ import sys
import getopt
import select
import Bcfg2.Server.Admin
+from Bcfg2.Server.Plugin import PullSource, Generator
from Bcfg2.Compat import input # pylint: disable=W0622
@@ -31,9 +32,8 @@ class Pull(Bcfg2.Server.Admin.MetadataCore):
use_stdin = False
try:
opts, gargs = getopt.getopt(args, 'vfIs')
- except:
- print(self.__doc__)
- raise SystemExit(1)
+ except getopt.GetoptError:
+ self.errExit(self.__doc__)
for opt in opts:
if opt[0] == '-v':
self.log = True
@@ -62,13 +62,14 @@ class Pull(Bcfg2.Server.Admin.MetadataCore):
given client/entry from statistics.
"""
new_entry = {'type': etype, 'name': ename}
- for plugin in self.bcore.pull_sources:
+ pull_sources = self.bcore.plugins_by_type(PullSource)
+ for plugin in pull_sources:
try:
(owner, group, mode, contents) = \
- plugin.GetCurrentEntry(client, etype, ename)
+ plugin.GetCurrentEntry(client, etype, ename)
break
except Bcfg2.Server.Plugin.PluginExecutionError:
- if plugin == self.bcore.pull_sources[-1]:
+ if plugin == pull_sources[-1]:
print("Pull Source failure; could not fetch current state")
raise SystemExit(1)
@@ -121,8 +122,8 @@ class Pull(Bcfg2.Server.Admin.MetadataCore):
meta = self.bcore.build_metadata(client)
# Find appropriate plugin in bcore
- glist = [gen for gen in self.bcore.generators if
- ename in gen.Entries.get(etype, {})]
+ glist = [gen for gen in self.bcore.plugins_by_type(Generator)
+ if ename in gen.Entries.get(etype, {})]
if len(glist) != 1:
self.errExit("Got wrong numbers of matching generators for entry:"
"%s" % ([g.name for g in glist]))
diff --git a/src/lib/Bcfg2/Server/Admin/Reports.py b/src/lib/Bcfg2/Server/Admin/Reports.py
index 6e313e84b..849df8025 100644
--- a/src/lib/Bcfg2/Server/Admin/Reports.py
+++ b/src/lib/Bcfg2/Server/Admin/Reports.py
@@ -79,8 +79,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
def __call__(self, args):
if len(args) == 0 or args[0] == '-h':
- print(self.__usage__)
- raise SystemExit(0)
+ self.errExit(self.__usage__)
# FIXME - dry run
@@ -101,9 +100,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
management.call_command("syncdb", verbosity=vrb)
management.call_command("migrate", verbosity=vrb)
except:
- print("Update failed: %s" %
- traceback.format_exc().splitlines()[-1])
- raise SystemExit(1)
+ self.errExit("Update failed: %s" % sys.exc_info()[1])
elif args[0] == 'purge':
expired = False
client = None
@@ -124,22 +121,20 @@ class Reports(Bcfg2.Server.Admin.Mode):
maxdate = datetime.datetime.now() - \
datetime.timedelta(days=int(args[i + 1]))
except:
- self.log.error("Invalid number of days: %s" %
- args[i + 1])
- raise SystemExit(-1)
+ self.errExit("Invalid number of days: %s" %
+ args[i + 1])
i = i + 1
elif args[i] == '--expired':
expired = True
i = i + 1
if expired:
if state:
- self.log.error("--state is not valid with --expired")
- raise SystemExit(-1)
+ self.errExit("--state is not valid with --expired")
self.purge_expired(maxdate)
else:
self.purge(client, maxdate, state)
else:
- print("Unknown command: %s" % args[0])
+ self.errExit("Unknown command: %s" % args[0])
@transaction.commit_on_success
def scrub(self):
@@ -155,8 +150,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
(start_count - cls.objects.count(), cls.__class__.__name__))
except:
print("Failed to prune %s: %s" %
- (cls.__class__.__name__,
- traceback.format_exc().splitlines()[-1]))
+ (cls.__class__.__name__, sys.exc_info()[1]))
def django_command_proxy(self, command):
'''Call a django command'''
@@ -180,8 +174,7 @@ class Reports(Bcfg2.Server.Admin.Mode):
cobj = Client.objects.get(name=client)
ipurge = ipurge.filter(client=cobj)
except Client.DoesNotExist:
- self.log.error("Client %s not in database" % client)
- raise SystemExit(-1)
+ self.errExit("Client %s not in database" % client)
self.log.debug("Filtering by client: %s" % client)
if maxdate:
diff --git a/src/lib/Bcfg2/Server/Admin/Syncdb.py b/src/lib/Bcfg2/Server/Admin/Syncdb.py
index 4ba840b86..eb417966d 100644
--- a/src/lib/Bcfg2/Server/Admin/Syncdb.py
+++ b/src/lib/Bcfg2/Server/Admin/Syncdb.py
@@ -3,6 +3,7 @@ import Bcfg2.settings
import Bcfg2.Options
import Bcfg2.Server.Admin
import Bcfg2.Server.models
+from django.core.exceptions import ImproperlyConfigured
from django.core.management import setup_environ, call_command
@@ -22,10 +23,7 @@ class Syncdb(Bcfg2.Server.Admin.Mode):
call_command("syncdb", interactive=False, verbosity=0)
self._database_available = True
except ImproperlyConfigured:
- err = sys.exc_info()[1]
- self.log.error("Django configuration problem: %s" % err)
- raise SystemExit(1)
+ self.errExit("Django configuration problem: %s" %
+ sys.exc_info()[1])
except:
- err = sys.exc_info()[1]
- self.log.error("Database update failed: %s" % err)
- raise SystemExit(1)
+ self.errExit("Database update failed: %s" % sys.exc_info()[1])
diff --git a/src/lib/Bcfg2/Server/Admin/Xcmd.py b/src/lib/Bcfg2/Server/Admin/Xcmd.py
index 79eeebc7c..036129a1b 100644
--- a/src/lib/Bcfg2/Server/Admin/Xcmd.py
+++ b/src/lib/Bcfg2/Server/Admin/Xcmd.py
@@ -4,7 +4,6 @@ import sys
import Bcfg2.Options
import Bcfg2.Proxy
import Bcfg2.Server.Admin
-from Bcfg2.Compat import xmlrpclib
class Xcmd(Bcfg2.Server.Admin.Mode):
@@ -19,8 +18,7 @@ class Xcmd(Bcfg2.Server.Admin.Mode):
'key': Bcfg2.Options.SERVER_KEY,
'certificate': Bcfg2.Options.CLIENT_CERT,
'ca': Bcfg2.Options.CLIENT_CA,
- 'timeout': Bcfg2.Options.CLIENT_TIMEOUT,
- }
+ 'timeout': Bcfg2.Options.CLIENT_TIMEOUT}
setup = Bcfg2.Options.OptionParser(optinfo)
setup.parse(args)
Bcfg2.Proxy.RetryMethod.max_retries = 1
@@ -32,27 +30,15 @@ class Xcmd(Bcfg2.Server.Admin.Mode):
ca=setup['ca'],
timeout=setup['timeout'])
if len(setup['args']) == 0:
- print("Usage: xcmd <xmlrpc method> <optional arguments>")
- return
+ self.errExit("Usage: xcmd <xmlrpc method> <optional arguments>")
cmd = setup['args'][0]
args = ()
if len(setup['args']) > 1:
args = tuple(setup['args'][1:])
try:
data = getattr(proxy, cmd)(*args)
- except xmlrpclib.Fault:
- flt = sys.exc_info()[1]
- if flt.faultCode == 7:
- print("Unknown method %s" % cmd)
- return
- elif flt.faultCode == 20:
- return
- else:
- raise
except Bcfg2.Proxy.ProxyError:
- err = sys.exc_info()[1]
- print("Proxy Error: %s" % err)
- return
+ self.errExit("Proxy Error: %s" % sys.exc_info()[1])
- if data != None:
+ if data is not None:
print(data)
diff --git a/src/lib/Bcfg2/Server/Admin/__init__.py b/src/lib/Bcfg2/Server/Admin/__init__.py
index 19175533f..8f12a940e 100644
--- a/src/lib/Bcfg2/Server/Admin/__init__.py
+++ b/src/lib/Bcfg2/Server/Admin/__init__.py
@@ -1,31 +1,14 @@
""" Base classes for admin modes """
-__all__ = [
- 'Backup',
- 'Bundle',
- 'Client',
- 'Compare',
- 'Group',
- 'Init',
- 'Minestruct',
- 'Perf',
- 'Pull',
- 'Query',
- 'Reports',
- 'Snapshots',
- 'Syncdb',
- 'Tidy',
- 'Viz',
- 'Xcmd'
- ]
-
import re
import sys
import logging
import lxml.etree
import Bcfg2.Server.Core
import Bcfg2.Options
-from Bcfg2.Compat import ConfigParser
+from Bcfg2.Compat import ConfigParser, walk_packages
+
+__all__ = [m[1] for m in walk_packages(path=__path__)]
class Mode(object):
@@ -105,15 +88,15 @@ class Mode(object):
# Calculate column widths (longest item in each column
# plus padding on both sides)
cols = list(zip(*rows))
- col_widths = [max([len(str(item)) + 2 * padding for \
- item in col]) for col in cols]
+ col_widths = [max([len(str(item)) + 2 * padding
+ for item in col]) for col in cols]
borderline = vdelim.join([w * hdelim for w in col_widths])
# Print out the table
print(borderline)
for row in rows:
- print(vdelim.join([justify(str(item), width) for \
- (item, width) in zip(row, col_widths)]))
+ print(vdelim.join([justify(str(item), width)
+ for (item, width) in zip(row, col_widths)]))
if hdr:
print(borderline)
hdr = False
@@ -145,6 +128,7 @@ class MetadataCore(Mode):
except Bcfg2.Server.Core.CoreInitError:
msg = sys.exc_info()[1]
self.errExit("Core load failed: %s" % msg)
+ self.bcore.load_plugins()
self.bcore.fam.handle_event_set()
self.metadata = self.bcore.metadata
diff --git a/src/lib/Bcfg2/Server/BuiltinCore.py b/src/lib/Bcfg2/Server/BuiltinCore.py
index 4d7453840..e69a92b64 100644
--- a/src/lib/Bcfg2/Server/BuiltinCore.py
+++ b/src/lib/Bcfg2/Server/BuiltinCore.py
@@ -9,12 +9,12 @@ from Bcfg2.Server.Core import BaseCore, NoExposedMethod
from Bcfg2.Compat import xmlrpclib, urlparse
from Bcfg2.SSLServer import XMLRPCServer
-from lockfile import LockFailed
+from lockfile import LockFailed, LockTimeout
# pylint: disable=E0611
try:
- from daemon.pidfile import PIDLockFile
+ from daemon.pidfile import TimeoutPIDLockFile
except ImportError:
- from daemon.pidlockfile import PIDLockFile
+ from daemon.pidlockfile import TimeoutPIDLockFile
# pylint: enable=E0611
@@ -33,7 +33,8 @@ class Core(BaseCore):
gid=self.setup['daemon_gid'],
umask=int(self.setup['umask'], 8))
if self.setup['daemon']:
- daemon_args['pidfile'] = PIDLockFile(self.setup['daemon'])
+ daemon_args['pidfile'] = TimeoutPIDLockFile(self.setup['daemon'],
+ acquire_timeout=5)
#: The :class:`daemon.DaemonContext` used to drop
#: privileges, write the PID file (with :class:`PidFile`),
#: and daemonize this core.
@@ -89,6 +90,11 @@ class Core(BaseCore):
err = sys.exc_info()[1]
self.logger.error("Failed to daemonize %s: %s" % (self.name, err))
return False
+ except LockTimeout:
+ err = sys.exc_info()[1]
+ self.logger.error("Failed to daemonize %s: Failed to acquire lock "
+ "on %s" % (self.name, self.setup['daemon']))
+ return False
def _run(self):
""" Create :attr:`server` to start the server listening. """
@@ -111,11 +117,11 @@ class Core(BaseCore):
self.logger.error("Server startup failed: %s" % err)
self.context.close()
return False
- self.server.register_instance(self)
return True
def _block(self):
""" Enter the blocking infinite loop. """
+ self.server.register_instance(self)
try:
self.server.serve_forever()
finally:
diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py
index 382f11e50..ecd68e1e4 100644
--- a/src/lib/Bcfg2/Server/Core.py
+++ b/src/lib/Bcfg2/Server/Core.py
@@ -2,14 +2,14 @@
implementations inherit from. """
import os
-import sys
-import time
+import pwd
import atexit
-import select
-import signal
import logging
-import inspect
+import select
+import sys
import threading
+import time
+import inspect
import lxml.etree
import Bcfg2.settings
import Bcfg2.Server
@@ -19,8 +19,9 @@ from Bcfg2.Cache import Cache
import Bcfg2.Statistics
from itertools import chain
from Bcfg2.Compat import xmlrpclib # pylint: disable=W0622
-from Bcfg2.Server.Plugin import PluginInitError, PluginExecutionError, \
- track_statistics
+from Bcfg2.Server.Plugin.exceptions import * # pylint: disable=W0401,W0614
+from Bcfg2.Server.Plugin.interfaces import * # pylint: disable=W0401,W0614
+from Bcfg2.Server.Plugin import track_statistics
try:
import psyco
@@ -96,6 +97,7 @@ class BaseCore(object):
.. automethod:: _block
.. -----
.. automethod:: _file_monitor_thread
+ .. automethod:: _perflog_thread
"""
#: The Bcfg2 repository directory
self.datastore = setup['repo']
@@ -174,6 +176,9 @@ class BaseCore(object):
#: the first one loaded wins.
self.plugin_blacklist = {}
+ #: The Metadata plugin
+ self.metadata = None
+
#: Revision of the Bcfg2 specification. This will be sent to
#: the client in the configuration, and can be set by a
#: :class:`Bcfg2.Server.Plugin.interfaces.Version` plugin.
@@ -235,88 +240,21 @@ class BaseCore(object):
self.logger.error("Failed to set ownership of database "
"at %s: %s" % (db_settings['NAME'], err))
- if '' in setup['plugins']:
- setup['plugins'].remove('')
-
- for plugin in setup['plugins']:
- if not plugin in self.plugins:
- self.init_plugin(plugin)
- # Remove blacklisted plugins
- for plugin, blacklist in list(self.plugin_blacklist.items()):
- if len(blacklist) > 0:
- self.logger.error("The following plugins conflict with %s;"
- "Unloading %s" % (plugin, blacklist))
- for plug in blacklist:
- del self.plugins[plug]
-
- # Log experimental plugins
- expl = [plug for plug in list(self.plugins.values())
- if plug.experimental]
- if expl:
- self.logger.info("Loading experimental plugin(s): %s" %
- (" ".join([x.name for x in expl])))
- self.logger.info("NOTE: Interfaces subject to change")
-
- # Log deprecated plugins
- depr = [plug for plug in list(self.plugins.values())
- if plug.deprecated]
- if depr:
- self.logger.info("Loading deprecated plugin(s): %s" %
- (" ".join([x.name for x in depr])))
-
- # Find the metadata plugin and set self.metadata
- mlist = self.plugins_by_type(Bcfg2.Server.Plugin.Metadata)
- if len(mlist) >= 1:
- #: The Metadata plugin
- self.metadata = mlist[0]
- if len(mlist) > 1:
- self.logger.error("Multiple Metadata plugins loaded; "
- "using %s" % self.metadata)
- else:
- self.logger.error("No Metadata plugin loaded; "
- "failed to instantiate Core")
- raise CoreInitError("No Metadata Plugin")
-
- #: The list of plugins that handle
- #: :class:`Bcfg2.Server.Plugin.interfaces.Statistics`
- self.statistics = self.plugins_by_type(Bcfg2.Server.Plugin.Statistics)
-
- #: The list of plugins that implement the
- #: :class:`Bcfg2.Server.Plugin.interfaces.PullSource`
- #: interface
- self.pull_sources = \
- self.plugins_by_type(Bcfg2.Server.Plugin.PullSource)
-
- #: The list of
- #: :class:`Bcfg2.Server.Plugin.interfaces.Generator` plugins
- self.generators = self.plugins_by_type(Bcfg2.Server.Plugin.Generator)
-
- #: The list of plugins that handle
- #: :class:`Bcfg2.Server.Plugin.interfaces.Structure`
- #: generation
- self.structures = self.plugins_by_type(Bcfg2.Server.Plugin.Structure)
-
- #: The list of plugins that implement the
- #: :class:`Bcfg2.Server.Plugin.interfaces.Connector` interface
- self.connectors = self.plugins_by_type(Bcfg2.Server.Plugin.Connector)
-
#: The CA that signed the server cert
self.ca = setup['ca']
- def hdlr(sig, frame): # pylint: disable=W0613
- """ Handle SIGINT/Ctrl-C by shutting down the core and exiting
- properly. """
- self.shutdown()
- os._exit(1) # pylint: disable=W0212
-
- signal.signal(signal.SIGINT, hdlr)
-
#: The FAM :class:`threading.Thread`,
#: :func:`_file_monitor_thread`
self.fam_thread = \
threading.Thread(name="%sFAMThread" % setup['filemonitor'],
target=self._file_monitor_thread)
+ self.perflog_thread = None
+ if self.setup['perflog']:
+ self.perflog_thread = \
+ threading.Thread(name="PerformanceLoggingThread",
+ target=self._perflog_thread)
+
#: A :func:`threading.Lock` for use by
#: :func:`Bcfg2.Server.FileMonitor.FileMonitor.handle_event_set`
self.lock = threading.Lock()
@@ -325,10 +263,6 @@ class BaseCore(object):
#: metadata
self.metadata_cache = Cache()
- if self.debug_flag:
- # enable debugging on everything else.
- self.plugins[plugin].set_debug(self.debug_flag)
-
def plugins_by_type(self, base_cls):
""" Return a list of loaded plugins that match the passed type.
@@ -349,11 +283,24 @@ class BaseCore(object):
if isinstance(plugin, base_cls)],
key=lambda p: (p.sort_order, p.name))
+ def _perflog_thread(self):
+ """ The thread that periodically logs performance statistics
+ to syslog. """
+ self.logger.debug("Performance logging thread starting")
+ while not self.terminate.isSet():
+ self.terminate.wait(self.setup['perflog_interval'])
+ for name, stats in self.get_statistics(None).items():
+ self.logger.info("Performance statistics: "
+ "%s min=%.06f, max=%.06f, average=%.06f, "
+ "count=%d" % ((name, ) + stats))
+ self.logger.debug("Performance logging thread terminated")
+
def _file_monitor_thread(self):
""" The thread that runs the
:class:`Bcfg2.Server.FileMonitor.FileMonitor`. This also
queries :class:`Bcfg2.Server.Plugin.interfaces.Version`
plugins for the current revision of the Bcfg2 repo. """
+ self.logger.debug("File monitor thread starting")
famfd = self.fam.fileno()
terminate = self.terminate
while not terminate.isSet():
@@ -367,12 +314,13 @@ class BaseCore(object):
except:
continue
self._update_vcs_revision()
+ self.logger.debug("File monitor thread terminated")
@track_statistics()
def _update_vcs_revision(self):
""" Update the revision of the current configuration on-disk
from the VCS plugin """
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Version):
+ for plugin in self.plugins_by_type(Version):
try:
newrev = plugin.get_revision()
if newrev != self.revision:
@@ -384,6 +332,58 @@ class BaseCore(object):
(plugin.name, sys.exc_info()[1]))
self.revision = '-1'
+ def load_plugins(self):
+ """ Load all plugins, setting
+ :attr:`Bcfg2.Server.Core.BaseCore.plugins` and
+ :attr:`Bcfg2.Server.Core.BaseCore.metadata` as side effects.
+ This does not start plugin threads; that is done later, in
+ :func:`Bcfg2.Server.Core.BaseCore.run` """
+ while '' in self.setup['plugins']:
+ self.setup['plugins'].remove('')
+
+ for plugin in self.setup['plugins']:
+ if not plugin in self.plugins:
+ self.init_plugin(plugin)
+
+ # Remove blacklisted plugins
+ for plugin, blacklist in list(self.plugin_blacklist.items()):
+ if len(blacklist) > 0:
+ self.logger.error("The following plugins conflict with %s;"
+ "Unloading %s" % (plugin, blacklist))
+ for plug in blacklist:
+ del self.plugins[plug]
+
+ # Log experimental plugins
+ expl = [plug for plug in list(self.plugins.values())
+ if plug.experimental]
+ if expl:
+ self.logger.info("Loading experimental plugin(s): %s" %
+ (" ".join([x.name for x in expl])))
+ self.logger.info("NOTE: Interfaces subject to change")
+
+ # Log deprecated plugins
+ depr = [plug for plug in list(self.plugins.values())
+ if plug.deprecated]
+ if depr:
+ self.logger.info("Loading deprecated plugin(s): %s" %
+ (" ".join([x.name for x in depr])))
+
+ # Find the metadata plugin and set self.metadata
+ mlist = self.plugins_by_type(Metadata)
+ if len(mlist) >= 1:
+ self.metadata = mlist[0]
+ if len(mlist) > 1:
+ self.logger.error("Multiple Metadata plugins loaded; using %s"
+ % self.metadata)
+ else:
+ self.logger.error("No Metadata plugin loaded; "
+ "failed to instantiate Core")
+ raise CoreInitError("No Metadata Plugin")
+
+ if self.debug_flag:
+ # enable debugging on plugins
+ self.plugins[plugin].set_debug(self.debug_flag)
+
def init_plugin(self, plugin):
""" Import and instantiate a single plugin. The plugin is
stored to :attr:`plugins`.
@@ -397,7 +397,7 @@ class BaseCore(object):
self.logger.debug("Loading plugin %s" % plugin)
try:
mod = getattr(__import__("Bcfg2.Server.Plugins.%s" %
- (plugin)).Server.Plugins, plugin)
+ (plugin)).Server.Plugins, plugin)
except ImportError:
try:
mod = __import__(plugin, globals(), locals(),
@@ -420,6 +420,10 @@ class BaseCore(object):
except PluginInitError:
self.logger.error("Failed to instantiate plugin %s" % plugin,
exc_info=1)
+ except OSError:
+ err = sys.exc_info()[1]
+ self.logger.error("Failed to add a file monitor while "
+ "instantiating plugin %s: %s" % (plugin, err))
except:
self.logger.error("Unexpected instantiation failure for plugin %s"
% plugin, exc_info=1)
@@ -430,8 +434,10 @@ class BaseCore(object):
if not self.terminate.isSet():
self.terminate.set()
self.fam.shutdown()
+ self.logger.debug("FAM shut down")
for plugin in list(self.plugins.values()):
plugin.shutdown()
+ self.logger.debug("All plugins shut down")
@property
def metadata_cache_mode(self):
@@ -464,8 +470,7 @@ class BaseCore(object):
metadata.hostname))
start = time.time()
try:
- for plugin in \
- self.plugins_by_type(Bcfg2.Server.Plugin.ClientRunHooks):
+ for plugin in self.plugins_by_type(ClientRunHooks):
try:
getattr(plugin, hook)(metadata)
except AttributeError:
@@ -496,11 +501,10 @@ class BaseCore(object):
:type data: list of lxml.etree._Element objects
"""
self.logger.debug("Validating structures for %s" % metadata.hostname)
- for plugin in \
- self.plugins_by_type(Bcfg2.Server.Plugin.StructureValidator):
+ for plugin in self.plugins_by_type(StructureValidator):
try:
plugin.validate_structures(metadata, data)
- except Bcfg2.Server.Plugin.ValidationError:
+ except ValidationError:
err = sys.exc_info()[1]
self.logger.error("Plugin %s structure validation failed: %s" %
(plugin.name, err))
@@ -523,10 +527,10 @@ class BaseCore(object):
:type data: list of lxml.etree._Element objects
"""
self.logger.debug("Validating goals for %s" % metadata.hostname)
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.GoalValidator):
+ for plugin in self.plugins_by_type(GoalValidator):
try:
plugin.validate_goals(metadata, data)
- except Bcfg2.Server.Plugin.ValidationError:
+ except ValidationError:
err = sys.exc_info()[1]
self.logger.error("Plugin %s goal validation failed: %s" %
(plugin.name, err.message))
@@ -544,8 +548,9 @@ class BaseCore(object):
:returns: list of :class:`lxml.etree._Element` objects
"""
self.logger.debug("Getting structures for %s" % metadata.hostname)
- structures = list(chain(*[struct.BuildStructures(metadata)
- for struct in self.structures]))
+ structures = list(
+ chain(*[struct.BuildStructures(metadata)
+ for struct in self.plugins_by_type(Structure)]))
sbundles = [b.get('name') for b in structures if b.tag == 'Bundle']
missing = [b for b in metadata.bundles if b not in sbundles]
if missing:
@@ -630,8 +635,9 @@ class BaseCore(object):
self.logger.error("Falling back to %s:%s" %
(entry.tag, entry.get('name')))
- glist = [gen for gen in self.generators if
- entry.get('name') in gen.Entries.get(entry.tag, {})]
+ generators = self.plugins_by_type(Generator)
+ glist = [gen for gen in generators
+ if entry.get('name') in gen.Entries.get(entry.tag, {})]
if len(glist) == 1:
return glist[0].Entries[entry.tag][entry.get('name')](entry,
metadata)
@@ -639,8 +645,8 @@ class BaseCore(object):
generators = ", ".join([gen.name for gen in glist])
self.logger.error("%s %s served by multiple generators: %s" %
(entry.tag, entry.get('name'), generators))
- g2list = [gen for gen in self.generators if
- gen.HandlesEntry(entry, metadata)]
+ g2list = [gen for gen in generators
+ if gen.HandlesEntry(entry, metadata)]
try:
if len(g2list) == 1:
return g2list[0].HandleEntry(entry, metadata)
@@ -667,7 +673,7 @@ class BaseCore(object):
revision=self.revision)
try:
meta = self.build_metadata(client)
- except Bcfg2.Server.Plugin.MetadataConsistencyError:
+ except MetadataConsistencyError:
self.logger.error("Metadata consistency error for client %s" %
client)
return lxml.etree.Element("error", type='metadata error')
@@ -714,7 +720,8 @@ class BaseCore(object):
:type event: Bcfg2.Server.FileMonitor.Event
"""
if event.filename != self.cfile:
- print("Got event for unknown file: %s" % event.filename)
+ self.logger.error("Got event for unknown file: %s" %
+ event.filename)
return
if event.code2str() == 'deleted':
return
@@ -743,6 +750,11 @@ class BaseCore(object):
os.chmod(piddir, 493) # 0775
if not self._daemonize():
return False
+
+ # rewrite $HOME. pulp stores its auth creds in ~/.pulp, so
+ # this is necessary to make that work when privileges are
+ # dropped
+ os.environ['HOME'] = pwd.getpwuid(self.setup['daemon_uid'])[5]
else:
os.umask(int(self.setup['umask'], 8))
@@ -751,17 +763,27 @@ class BaseCore(object):
return False
try:
+ self.load_plugins()
+
self.fam.start()
self.fam_thread.start()
self.fam.AddMonitor(self.cfile, self)
+ if self.perflog_thread is not None:
+ self.perflog_thread.start()
- for plug in self.plugins_by_type(Bcfg2.Server.Plugin.Threaded):
+ for plug in self.plugins_by_type(Threaded):
plug.start_threads()
except:
self.shutdown()
raise
- self.set_debug(None, self.debug_flag)
+ if self.setup['fam_blocking']:
+ time.sleep(1)
+ while self.fam.pending() != 0:
+ time.sleep(1)
+
+ if self.debug_flag:
+ self.set_debug(None, self.debug_flag)
self._block()
def _daemonize(self):
@@ -792,7 +814,7 @@ class BaseCore(object):
"""
self.logger.debug("Getting decision list for %s" % metadata.hostname)
result = []
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Decision):
+ for plugin in self.plugins_by_type(Decision):
try:
result.extend(plugin.GetDecisions(metadata, mode))
except:
@@ -811,7 +833,7 @@ class BaseCore(object):
"""
if not hasattr(self, 'metadata'):
# some threads start before metadata is even loaded
- raise Bcfg2.Server.Plugin.MetadataRuntimeError
+ raise MetadataRuntimeError("Metadata not loaded yet")
if self.metadata_cache_mode == 'initial':
# the Metadata plugin handles loading the cached data if
# we're only caching the initial metadata object
@@ -821,10 +843,11 @@ class BaseCore(object):
if not imd:
self.logger.debug("Building metadata for %s" % client_name)
imd = self.metadata.get_initial_metadata(client_name)
- for conn in self.connectors:
+ connectors = self.plugins_by_type(Connector)
+ for conn in connectors:
grps = conn.get_additional_groups(imd)
self.metadata.merge_additional_groups(imd, grps)
- for conn in self.connectors:
+ for conn in connectors:
data = conn.get_additional_data(imd)
self.metadata.merge_additional_data(imd, conn.name, data)
imd.query.by_name = self.build_metadata
@@ -845,7 +868,7 @@ class BaseCore(object):
meta = self.build_metadata(client_name)
state = statistics.find(".//Statistics")
if state.get('version') >= '2.0':
- for plugin in self.statistics:
+ for plugin in self.plugins_by_type(Statistics):
try:
plugin.process_statistics(meta, statistics)
except:
@@ -887,11 +910,11 @@ class BaseCore(object):
meta = self.build_metadata(client)
else:
meta = None
- except Bcfg2.Server.Plugin.MetadataConsistencyError:
+ except MetadataConsistencyError:
err = sys.exc_info()[1]
self.critical_error("Client metadata resolution error for %s: %s" %
(address[0], err))
- except Bcfg2.Server.Plugin.MetadataRuntimeError:
+ except MetadataRuntimeError:
err = sys.exc_info()[1]
self.critical_error('Metadata system runtime failure for %s: %s' %
(address[0], err))
@@ -985,8 +1008,7 @@ class BaseCore(object):
version))
try:
self.metadata.set_version(client, version)
- except (Bcfg2.Server.Plugin.MetadataConsistencyError,
- Bcfg2.Server.Plugin.MetadataRuntimeError):
+ except (MetadataConsistencyError, MetadataRuntimeError):
err = sys.exc_info()[1]
self.critical_error("Unable to set version for %s: %s" %
(client, err))
@@ -1006,7 +1028,7 @@ class BaseCore(object):
client, metadata = self.resolve_client(address, cleanup_cache=True)
self.logger.debug("Getting probes for %s" % client)
try:
- for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Probing):
+ for plugin in self.plugins_by_type(Probing):
for probe in plugin.GetProbes(metadata):
resp.append(probe)
return lxml.etree.tostring(resp,
@@ -1076,11 +1098,10 @@ class BaseCore(object):
self.logger.debug("%s sets its profile to %s" % (client, profile))
try:
self.metadata.set_profile(client, profile, address)
- except (Bcfg2.Server.Plugin.MetadataConsistencyError,
- Bcfg2.Server.Plugin.MetadataRuntimeError):
+ except (MetadataConsistencyError, MetadataRuntimeError):
err = sys.exc_info()[1]
self.critical_error("Unable to assert profile for %s: %s" %
- (client, err))
+ (client, err))
return True
@exposed
@@ -1099,7 +1120,7 @@ class BaseCore(object):
config = self.BuildConfiguration(client)
return lxml.etree.tostring(config,
xml_declaration=False).decode('UTF-8')
- except Bcfg2.Server.Plugin.MetadataConsistencyError:
+ except MetadataConsistencyError:
self.critical_error("Metadata consistency failure for %s" % client)
@exposed
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Fam.py b/src/lib/Bcfg2/Server/FileMonitor/Fam.py
index 253bb2801..09d41038e 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/Fam.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/Fam.py
@@ -51,7 +51,7 @@ class Fam(FileMonitor):
else:
handle = self.filemonitor.monitorFile(path, None)
self.handles[handle.requestID()] = handle
- if obj != None:
+ if obj is not None:
self.users[handle.requestID()] = obj
return handle.requestID()
AddMonitor.__doc__ = FileMonitor.AddMonitor.__doc__
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
index cdd52dbb9..2cdf27ed8 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py
@@ -3,14 +3,11 @@ support. """
import os
import errno
-import logging
import pyinotify
from Bcfg2.Compat import reduce # pylint: disable=W0622
from Bcfg2.Server.FileMonitor import Event
from Bcfg2.Server.FileMonitor.Pseudo import Pseudo
-LOGGER = logging.getLogger(__name__)
-
class Inotify(Pseudo, pyinotify.ProcessEvent):
""" File monitor backend with `inotify
@@ -123,8 +120,9 @@ class Inotify(Pseudo, pyinotify.ProcessEvent):
try:
watch = self.watchmgr.watches[ievent.wd]
except KeyError:
- LOGGER.error("Error handling event %s for %s: Watch %s not found" %
- (action, ievent.pathname, ievent.wd))
+ self.logger.error("Error handling event %s for %s: "
+ "Watch %s not found" %
+ (action, ievent.pathname, ievent.wd))
return
# FAM-style file monitors return the full path to the parent
# directory that is being watched, relative paths to anything
diff --git a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
index 24cd099d0..b1e1adab7 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py
@@ -24,6 +24,6 @@ class Pseudo(FileMonitor):
self.events.append(Event(handleID, fname, 'exists'))
self.events.append(Event(handleID, path, 'endExist'))
- if obj != None:
+ if obj is not None:
self.handles[handleID] = obj
return handleID
diff --git a/src/lib/Bcfg2/Server/Lint/Comments.py b/src/lib/Bcfg2/Server/Lint/Comments.py
index 8bfb76461..f028e225e 100644
--- a/src/lib/Bcfg2/Server/Lint/Comments.py
+++ b/src/lib/Bcfg2/Server/Lint/Comments.py
@@ -1,8 +1,9 @@
-""" check files for various required comments """
+""" Check files for various required comments. """
import os
import lxml.etree
import Bcfg2.Server.Lint
+from Bcfg2.Server import XI_NAMESPACE
from Bcfg2.Server.Plugins.Cfg.CfgPlaintextGenerator \
import CfgPlaintextGenerator
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
@@ -11,7 +12,10 @@ from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
class Comments(Bcfg2.Server.Lint.ServerPlugin):
- """ check files for various required headers """
+ """ The Comments lint plugin checks files for header comments that
+ give information about the files. For instance, you can require
+ SVN keywords in a comment, or require the name of the maintainer
+ of a Genshi template, and so on. """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
self.config_cache = {}
@@ -27,21 +31,43 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
def Errors(cls):
return {"unexpanded-keywords": "warning",
"keywords-not-found": "warning",
- "comments-not-found": "warning"}
+ "comments-not-found": "warning",
+ "broken-xinclude-chain": "warning"}
def required_keywords(self, rtype):
- """ given a file type, fetch the list of required VCS keywords
- from the bcfg2-lint config """
+ """ Given a file type, fetch the list of required VCS keywords
+ from the bcfg2-lint config. Valid file types are documented
+ in :manpage:`bcfg2-lint.conf(5)`.
+
+ :param rtype: The file type
+ :type rtype: string
+ :returns: list - the required items
+ """
return self.required_items(rtype, "keyword")
def required_comments(self, rtype):
- """ given a file type, fetch the list of required comments
- from the bcfg2-lint config """
+ """ Given a file type, fetch the list of required comments
+ from the bcfg2-lint config. Valid file types are documented
+ in :manpage:`bcfg2-lint.conf(5)`.
+
+ :param rtype: The file type
+ :type rtype: string
+ :returns: list - the required items
+ """
return self.required_items(rtype, "comment")
def required_items(self, rtype, itype):
- """ given a file type and item type (comment or keyword),
- fetch the list of required items from the bcfg2-lint config """
+ """ Given a file type and item type (``comment`` or
+ ``keyword``), fetch the list of required items from the
+ bcfg2-lint config. Valid file types are documented in
+ :manpage:`bcfg2-lint.conf(5)`.
+
+ :param rtype: The file type
+ :type rtype: string
+ :param itype: The item type (``comment`` or ``keyword``)
+ :type itype: string
+ :returns: list - the required items
+ """
if itype not in self.config_cache:
self.config_cache[itype] = {}
@@ -62,7 +88,7 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
return self.config_cache[itype][rtype]
def check_bundles(self):
- """ check bundle files for required headers """
+ """ Check bundle files for required comments. """
if 'Bundler' in self.core.plugins:
for bundle in self.core.plugins['Bundler'].entries.values():
xdata = None
@@ -78,26 +104,54 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
self.check_xml(bundle.name, xdata, rtype)
def check_properties(self):
- """ check properties files for required headers """
+ """ Check Properties files for required comments. """
if 'Properties' in self.core.plugins:
props = self.core.plugins['Properties']
- for propfile, pdata in props.store.entries.items():
+ for propfile, pdata in props.entries.items():
if os.path.splitext(propfile)[1] == ".xml":
self.check_xml(pdata.name, pdata.xdata, 'properties')
+ def has_all_xincludes(self, mfile):
+ """ Return True if :attr:`Bcfg2.Server.Lint.Plugin.files`
+ includes all XIncludes listed in the specified metadata type,
+ false otherwise. In other words, this returns True if
+ bcfg2-lint is dealing with complete metadata.
+
+ :param mfile: The metadata file ("clients.xml" or
+ "groups.xml") to check for XIncludes
+ :type mfile: string
+ :returns: bool
+ """
+ if self.files is None:
+ return True
+ else:
+ path = os.path.join(self.metadata.data, mfile)
+ if path in self.files:
+ xdata = lxml.etree.parse(path)
+ for el in xdata.findall('./%sinclude' % XI_NAMESPACE):
+ if not self.has_all_xincludes(el.get('href')):
+ self.LintError("broken-xinclude-chain",
+ "Broken XInclude chain: could not "
+ "include %s" % path)
+ return False
+
+ return True
+
def check_metadata(self):
- """ check metadata files for required headers """
+ """ Check Metadata files for required comments. """
if self.has_all_xincludes("groups.xml"):
self.check_xml(os.path.join(self.metadata.data, "groups.xml"),
self.metadata.groups_xml.data,
"metadata")
- if self.has_all_xincludes("clients.xml"):
- self.check_xml(os.path.join(self.metadata.data, "clients.xml"),
- self.metadata.clients_xml.data,
- "metadata")
+ if hasattr(self.metadata, "clients_xml"):
+ if self.has_all_xincludes("clients.xml"):
+ self.check_xml(os.path.join(self.metadata.data, "clients.xml"),
+ self.metadata.clients_xml.data,
+ "metadata")
def check_cfg(self):
- """ check Cfg files and info.xml files for required headers """
+ """ Check Cfg files and ``info.xml`` files for required
+ comments. """
if 'Cfg' in self.core.plugins:
for entryset in self.core.plugins['Cfg'].entries.values():
for entry in entryset.entries.values():
@@ -117,29 +171,57 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
self.check_plaintext(entry.name, entry.data, rtype)
def check_probes(self):
- """ check probes for required headers """
+ """ Check Probes for required comments """
if 'Probes' in self.core.plugins:
for probe in self.core.plugins['Probes'].probes.entries.values():
self.check_plaintext(probe.name, probe.data, "probes")
def check_xml(self, filename, xdata, rtype):
- """ check generic XML files for required headers """
+ """ Generic check to check an XML file for required comments.
+
+ :param filename: The filename
+ :type filename: string
+ :param xdata: The file data
+ :type xdata: lxml.etree._Element
+ :param rtype: The type of file. Available types are
+ documented in :manpage:`bcfg2-lint.conf(5)`.
+ :type rtype: string
+ """
self.check_lines(filename,
[str(el)
for el in xdata.getiterator(lxml.etree.Comment)],
rtype)
def check_plaintext(self, filename, data, rtype):
- """ check generic plaintext files for required headers """
+ """ Generic check to check a plain text file for required
+ comments.
+
+ :param filename: The filename
+ :type filename: string
+ :param data: The file data
+ :type data: string
+ :param rtype: The type of file. Available types are
+ documented in :manpage:`bcfg2-lint.conf(5)`.
+ :type rtype: string
+ """
self.check_lines(filename, data.splitlines(), rtype)
def check_lines(self, filename, lines, rtype):
- """ generic header check for a set of lines """
+ """ Generic header check for a set of lines.
+
+ :param filename: The filename
+ :type filename: string
+ :param lines: The data to check
+ :type lines: list of strings
+ :param rtype: The type of file. Available types are
+ documented in :manpage:`bcfg2-lint.conf(5)`.
+ :type rtype: string
+ """
if self.HandlesFile(filename):
# found is trivalent:
- # False == not found
- # None == found but not expanded
- # True == found and expanded
+ # False == keyword not found
+ # None == keyword found but not expanded
+ # True == keyword found and expanded
found = dict((k, False) for k in self.required_keywords(rtype))
for line in lines:
diff --git a/src/lib/Bcfg2/Server/Lint/Genshi.py b/src/lib/Bcfg2/Server/Lint/Genshi.py
index c045c2ca2..1ecb6da42 100755
--- a/src/lib/Bcfg2/Server/Lint/Genshi.py
+++ b/src/lib/Bcfg2/Server/Lint/Genshi.py
@@ -1,4 +1,4 @@
-""" Check Genshi templates for syntax errors """
+""" Check Genshi templates for syntax errors. """
import sys
import Bcfg2.Server.Lint
@@ -9,10 +9,9 @@ from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
class Genshi(Bcfg2.Server.Lint.ServerPlugin):
- """ Check Genshi templates for syntax errors """
+ """ Check Genshi templates for syntax errors. """
def Run(self):
- """ run plugin """
if 'Cfg' in self.core.plugins:
self.check_cfg()
if 'TGenshi' in self.core.plugins:
@@ -25,7 +24,7 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
return {"genshi-syntax-error": "error"}
def check_cfg(self):
- """ Check genshi templates in Cfg for syntax errors """
+ """ Check genshi templates in Cfg for syntax errors. """
for entryset in self.core.plugins['Cfg'].entries.values():
for entry in entryset.entries.values():
if (self.HandlesFile(entry.name) and
@@ -38,9 +37,15 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
err = sys.exc_info()[1]
self.LintError("genshi-syntax-error",
"Genshi syntax error: %s" % err)
+ except:
+ etype, err = sys.exc_info()[:2]
+ self.LintError(
+ "genshi-syntax-error",
+ "Unexpected Genshi error on %s: %s: %s" %
+ (entry.name, etype.__name__, err))
def check_tgenshi(self):
- """ Check templates in TGenshi for syntax errors """
+ """ Check templates in TGenshi for syntax errors. """
loader = TemplateLoader()
for eset in self.core.plugins['TGenshi'].entries.values():
@@ -54,7 +59,7 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
"Genshi syntax error: %s" % err)
def check_bundler(self):
- """ Check templates in Bundler for syntax errors """
+ """ Check templates in Bundler for syntax errors. """
loader = TemplateLoader()
for entry in self.core.plugins['Bundler'].entries.values():
diff --git a/src/lib/Bcfg2/Server/Lint/GroupNames.py b/src/lib/Bcfg2/Server/Lint/GroupNames.py
index 52e42aa7b..b180083d5 100644
--- a/src/lib/Bcfg2/Server/Lint/GroupNames.py
+++ b/src/lib/Bcfg2/Server/Lint/GroupNames.py
@@ -1,4 +1,4 @@
-""" ensure that all named groups are valid group names """
+""" Ensure that all named groups are valid group names. """
import os
import re
@@ -11,8 +11,15 @@ except ImportError:
class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
- """ ensure that all named groups are valid group names """
+ """ Ensure that all named groups are valid group names. """
+
+ #: A string regex that matches only valid group names. Currently,
+ #: a group name is considered valid if it contains only
+ #: non-whitespace characters.
pattern = r'\S+$'
+
+ #: A compiled regex for
+ #: :attr:`Bcfg2.Server.Lint.GroupNames.GroupNames.pattern`
valid = re.compile(r'^' + pattern)
def Run(self):
@@ -31,7 +38,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
return {"invalid-group-name": "error"}
def check_rules(self):
- """ Check groups used in the Rules plugin for validity """
+ """ Check groups used in the Rules plugin for validity. """
for rules in self.core.plugins['Rules'].entries.values():
if not self.HandlesFile(rules.name):
continue
@@ -40,7 +47,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
os.path.join(self.config['repo'], rules.name))
def check_bundles(self):
- """ Check groups used in the Bundler plugin for validity """
+ """ Check groups used in the Bundler plugin for validity. """
for bundle in self.core.plugins['Bundler'].entries.values():
if (self.HandlesFile(bundle.name) and
(not HAS_GENSHI or
@@ -50,7 +57,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
def check_metadata(self):
""" Check groups used or declared in the Metadata plugin for
- validity """
+ validity. """
self.check_entries(self.metadata.groups_xml.xdata.xpath("//Group"),
os.path.join(self.config['repo'],
self.metadata.groups_xml.name))
@@ -68,7 +75,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
def check_cfg(self):
""" Check groups used in group-specific files in the Cfg
- plugin for validity """
+ plugin for validity. """
for root, _, files in os.walk(self.core.plugins['Cfg'].data):
for fname in files:
basename = os.path.basename(root)
@@ -81,7 +88,14 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin):
def check_entries(self, entries, fname):
""" Check a generic list of XML entries for <Group> tags with
- invalid name attributes """
+ invalid name attributes.
+
+ :param entries: A list of XML <Group> tags whose ``name``
+ attributes will be validated.
+ :type entries: list of lxml.etree._Element
+ :param fname: The filename the entry list came from
+ :type fname: string
+ """
for grp in entries:
if not self.valid.search(grp.get("name")):
self.LintError("invalid-group-name",
diff --git a/src/lib/Bcfg2/Server/Lint/InfoXML.py b/src/lib/Bcfg2/Server/Lint/InfoXML.py
index e34f387ff..95657317e 100644
--- a/src/lib/Bcfg2/Server/Lint/InfoXML.py
+++ b/src/lib/Bcfg2/Server/Lint/InfoXML.py
@@ -1,4 +1,4 @@
-""" ensure that all config files have an info.xml file"""
+""" Ensure that all config files have a valid info.xml file. """
import os
import Bcfg2.Options
@@ -8,7 +8,14 @@ from Bcfg2.Server.Plugins.Cfg.CfgLegacyInfo import CfgLegacyInfo
class InfoXML(Bcfg2.Server.Lint.ServerPlugin):
- """ ensure that all config files have an info.xml file"""
+ """ Ensure that all config files have a valid info.xml file. This
+ plugin can check for:
+
+ * Missing ``info.xml`` files;
+ * Use of deprecated ``info``/``:info`` files;
+ * Paranoid mode disabled in an ``info.xml`` file;
+ * Required attributes missing from ``info.xml``
+ """
def Run(self):
if 'Cfg' not in self.core.plugins:
return
@@ -40,11 +47,10 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin):
return {"no-infoxml": "warning",
"deprecated-info-file": "warning",
"paranoid-false": "warning",
- "broken-xinclude-chain": "warning",
"required-infoxml-attrs-missing": "error"}
def check_infoxml(self, fname, xdata):
- """ verify that info.xml contains everything it should """
+ """ Verify that info.xml contains everything it should. """
for info in xdata.getroottree().findall("//Info"):
required = []
if "required_attrs" in self.config:
diff --git a/src/lib/Bcfg2/Server/Lint/MergeFiles.py b/src/lib/Bcfg2/Server/Lint/MergeFiles.py
index 44d02c2ff..2419c3d43 100644
--- a/src/lib/Bcfg2/Server/Lint/MergeFiles.py
+++ b/src/lib/Bcfg2/Server/Lint/MergeFiles.py
@@ -57,7 +57,7 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin):
else:
threshold = 0.75
rv = []
- elist = entries.items()
+ elist = list(entries.items())
while elist:
result = self._find_similar(elist.pop(0), copy.copy(elist),
threshold)
diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
index 2a10da417..e49779a10 100644
--- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
+++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
@@ -1,13 +1,13 @@
-""" verify attributes for configuration entries that cannot be
-verified with an XML schema alone"""
+""" Verify attributes for configuration entries that cannot be
+verified with an XML schema alone. """
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.Client.Tools.POSIX.base import device_map
try:
from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile
HAS_GENSHI = True
@@ -15,7 +15,8 @@ except ImportError:
HAS_GENSHI = False
-# format verifying functions
+# format verifying functions. TODO: These should be moved into XML
+# schemas where possible.
def is_filename(val):
""" Return True if val is a string describing a valid full path
"""
@@ -53,16 +54,16 @@ def is_device_mode(val):
class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
- """ verify attributes for configuration entries that cannot be
- verified with an XML schema alone """
+ """ Verify attributes for configuration entries that cannot be
+ verified with an XML schema alone. """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
self.required_attrs = dict(
Path=dict(
- device=dict(name=is_filename, owner=is_username,
+ device=dict(name=is_filename,
+ owner=is_username,
group=is_username,
- dev_type=lambda v: \
- v in Bcfg2.Client.Tools.POSIX.base.device_map),
+ dev_type=lambda v: v in device_map),
directory=dict(name=is_filename, owner=is_username,
group=is_username, mode=is_octal_mode),
file=dict(name=is_filename, owner=is_username,
@@ -75,7 +76,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
permissions=dict(name=is_filename, owner=is_username,
group=is_username, mode=is_octal_mode),
vcs=dict(vcstype=lambda v: (v != 'Path' and
- hasattr(Bcfg2.Client.Tools.VCS,
+ hasattr(Bcfg2.Client.Tools.VCS.VCS,
"Install%s" % v)),
revision=None, sourceurl=None)),
Service={"__any__": dict(name=None),
@@ -87,21 +88,21 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
command=None)},
ACL=dict(
default=dict(scope=lambda v: v in ['user', 'group'],
- perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}',
+ perms=lambda v: re.match(r'^([0-7]|[rwx\-]{0,3}',
v)),
access=dict(scope=lambda v: v in ['user', 'group'],
- perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}',
+ perms=lambda v: re.match(r'^([0-7]|[rwx\-]{0,3}',
v)),
- mask=dict(perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}',
+ mask=dict(perms=lambda v: re.match(r'^([0-7]|[rwx\-]{0,3}',
v))),
Package={"__any__": dict(name=None)},
SEBoolean={None: dict(name=None,
value=lambda v: v in ['on', 'off'])},
SEModule={None: dict(name=None, __text__=None)},
- SEPort={None:
- dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)',
- v),
- selinuxtype=is_selinux_type)},
+ SEPort={
+ None: dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)',
+ v),
+ selinuxtype=is_selinux_type)},
SEFcontext={None: dict(name=None, selinuxtype=is_selinux_type)},
SENode={None: dict(name=lambda v: "/" in v,
selinuxtype=is_selinux_type,
@@ -115,9 +116,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
SEInterface={None: dict(name=None, selinuxtype=is_selinux_type)},
SEPermissive={None: dict(name=is_selinux_type)},
POSIXGroup={None: dict(name=is_username)},
- POSIXUser={None: dict(name=is_username)},
- MemberOf={None: dict(__text__=is_username)},
- )
+ POSIXUser={None: dict(name=is_username)})
def Run(self):
self.check_packages()
@@ -137,7 +136,8 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
"extra-attrs": "warning"}
def check_packages(self):
- """ check package sources for Source entries with missing attrs """
+ """ Check Packages sources for Source entries with missing
+ attributes. """
if 'Packages' not in self.core.plugins:
return
@@ -177,7 +177,8 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
rules.name))
def check_bundles(self):
- """ check bundles for BoundPath entries with missing attrs """
+ """ Check bundles for BoundPath entries with missing
+ attrs. """
if 'Bundler' not in self.core.plugins:
return
@@ -196,7 +197,13 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
self.check_entry(path, bundle.name)
def check_entry(self, entry, filename):
- """ generic entry check """
+ """ Generic entry check.
+
+ :param entry: The XML entry to check for missing attributes.
+ :type entry: lxml.etree._Element
+ :param filename: The filename the entry came from
+ :type filename: string
+ """
if self.HandlesFile(filename):
name = entry.get('name')
tag = entry.tag
diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py
index 37bc230d1..09f3f3d25 100644
--- a/src/lib/Bcfg2/Server/Lint/Validate.py
+++ b/src/lib/Bcfg2/Server/Lint/Validate.py
@@ -1,4 +1,5 @@
-""" Ensure that the repo validates """
+""" Ensure that all XML files in the Bcfg2 repository validate
+according to their respective schemas. """
import os
import sys
@@ -10,10 +11,19 @@ import Bcfg2.Server.Lint
class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
- """ Ensure that the repo validates """
+ """ Ensure that all XML files in the Bcfg2 repository validate
+ according to their respective schemas. """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs)
+
+ #: A dict of <file glob>: <schema file> that maps files in the
+ #: Bcfg2 specification to their schemas. The globs are
+ #: extended :mod:`fnmatch` globs that also support ``**``,
+ #: which matches any number of any characters, including
+ #: forward slashes. The schema files are relative to the
+ #: schema directory, which can be controlled by the
+ #: ``bcfg2-lint --schema`` option.
self.filesets = \
{"Metadata/groups.xml": "metadata.xsd",
"Metadata/clients.xml": "clients.xsd",
@@ -40,7 +50,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
"NagiosGen/config.xml": "nagiosgen.xsd",
"FileProbes/config.xml": "fileprobes.xsd",
"SSLCA/**/cert.xml": "sslca-cert.xsd",
- "SSLCA/**/key.xml": "sslca-key.xsd"
+ "SSLCA/**/key.xml": "sslca-key.xsd",
+ "GroupLogic/groups.xml": "grouplogic.xsd"
}
self.filelists = {}
@@ -75,7 +86,7 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
"input-output-error": "error"}
def check_properties(self):
- """ check Properties files against their schemas """
+ """ Check Properties files against their schemas. """
for filename in self.filelists['props']:
schemafile = "%s.xsd" % os.path.splitext(filename)[0]
if os.path.exists(schemafile):
@@ -83,17 +94,19 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
else:
self.LintError("properties-schema-not-found",
"No schema found for %s" % filename)
+ # ensure that it at least parses
+ self.parse(filename)
- def validate(self, filename, schemafile, schema=None):
- """validate a file against the given lxml.etree.Schema.
- return True on success, False on failure """
- if schema is None:
- # if no schema object was provided, instantiate one
- schema = self._load_schema(schemafile)
- if not schema:
- return False
+ def parse(self, filename):
+ """ Parse an XML file, raising the appropriate LintErrors if
+ it can't be parsed or read. Return the
+ lxml.etree._ElementTree parsed from the file.
+
+ :param filename: The full path to the file to parse
+ :type filename: string
+ :returns: lxml.etree._ElementTree - the parsed data"""
try:
- datafile = lxml.etree.parse(filename)
+ return lxml.etree.parse(filename)
except SyntaxError:
lint = Popen(["xmllint", filename], stdout=PIPE, stderr=STDOUT)
self.LintError("xml-failed-to-parse",
@@ -106,6 +119,27 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
"Failed to open file %s" % filename)
return False
+ def validate(self, filename, schemafile, schema=None):
+ """ Validate a file against the given schema.
+
+ :param filename: The full path to the file to validate
+ :type filename: string
+ :param schemafile: The full path to the schema file to
+ validate against
+ :type schemafile: string
+ :param schema: The loaded schema to validate against. This
+ can be used to avoid parsing a single schema
+ file for every file that needs to be validate
+ against it.
+ :type schema: lxml.etree.Schema
+ :returns: bool - True if the file validates, false otherwise
+ """
+ if schema is None:
+ # if no schema object was provided, instantiate one
+ schema = self._load_schema(schemafile)
+ if not schema:
+ return False
+ datafile = self.parse(filename)
if not schema.validate(datafile):
cmd = ["xmllint"]
if self.files is None:
@@ -113,6 +147,9 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
cmd.extend(["--noout", "--schema", schemafile, filename])
lint = Popen(cmd, stdout=PIPE, stderr=STDOUT)
output = lint.communicate()[0]
+ # py3k fix
+ if not isinstance(output, str):
+ output = output.decode('utf-8')
if lint.wait():
self.LintError("xml-failed-to-verify",
"%s fails to verify:\n%s" % (filename, output))
@@ -120,7 +157,14 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
return True
def get_filelists(self):
- """ get lists of different kinds of files to validate """
+ """ Get lists of different kinds of files to validate. This
+ doesn't return anything, but it sets
+ :attr:`Bcfg2.Server.Lint.Validate.Validate.filelists` to a
+ dict whose keys are path globs given in
+ :attr:`Bcfg2.Server.Lint.Validate.Validate.filesets` and whose
+ values are lists of the full paths to all files in the Bcfg2
+ repository (or given with ``bcfg2-lint --stdin``) that match
+ the glob."""
if self.files is not None:
listfiles = lambda p: fnmatch.filter(self.files,
os.path.join('*', p))
@@ -147,7 +191,13 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
self.filelists['props'] = listfiles("Properties/*.xml")
def _load_schema(self, filename):
- """ load an XML schema document, returning the Schema object """
+ """ Load an XML schema document, returning the Schema object
+ and raising appropriate lint errors on failure.
+
+ :param filename: The full path to the schema file to load.
+ :type filename: string
+ :returns: lxml.etree.Schema - The loaded schema data
+ """
try:
return lxml.etree.XMLSchema(lxml.etree.parse(filename))
except IOError:
diff --git a/src/lib/Bcfg2/Server/Lint/__init__.py b/src/lib/Bcfg2/Server/Lint/__init__.py
index 11afdd75d..28644263f 100644
--- a/src/lib/Bcfg2/Server/Lint/__init__.py
+++ b/src/lib/Bcfg2/Server/Lint/__init__.py
@@ -9,10 +9,9 @@ import lxml.etree
import fcntl
import termios
import struct
-from Bcfg2.Server import XI_NAMESPACE
from Bcfg2.Compat import walk_packages
-__all__ = [m[1] for m in walk_packages(path=__path__)]
+plugins = [m[1] for m in walk_packages(path=__path__)] # pylint: disable=C0103
def _ioctl_GWINSZ(fd): # pylint: disable=C0103
@@ -45,30 +44,56 @@ def get_termsize():
class Plugin(object):
- """ base class for ServerlessPlugin and ServerPlugin """
+ """ Base class for all bcfg2-lint plugins """
def __init__(self, config, errorhandler=None, files=None):
+ """
+ :param config: A :mod:`Bcfg2.Options` setup dict
+ :type config: dict
+ :param errorhandler: A :class:`Bcfg2.Server.Lint.ErrorHandler`
+ that will be used to handle lint errors.
+ If one is not provided, a new one will be
+ instantiated.
+ :type errorhandler: Bcfg2.Server.Lint.ErrorHandler
+ :param files: A list of files to run bcfg2-lint against. (See
+ the bcfg2-lint ``--stdin`` option.)
+ :type files: list of strings
+ """
+
+ #: The list of files that bcfg2-lint should be run against
self.files = files
+
+ #: The Bcfg2.Options setup dict
self.config = config
+
self.logger = logging.getLogger('bcfg2-lint')
if errorhandler is None:
+ #: The error handler
self.errorhandler = ErrorHandler()
else:
self.errorhandler = errorhandler
self.errorhandler.RegisterErrors(self.Errors())
def Run(self):
- """ run the plugin. must be overloaded by child classes """
- pass
+ """ Run the plugin. Must be overloaded by child classes. """
+ raise NotImplementedError
@classmethod
def Errors(cls):
- """ returns a dict of errors the plugin supplies. must be
- overloaded by child classes """
+ """ Returns a dict of errors the plugin supplies, in a format
+ suitable for passing to
+ :func:`Bcfg2.Server.Lint.ErrorHandler.RegisterErrors`.
+
+ Must be overloaded by child classes.
+
+ :returns: dict
+ """
+ raise NotImplementedError
def HandlesFile(self, fname):
- """ returns true if the given file should be handled by the
- plugin according to the files list, false otherwise """
+ """ Returns True if the given file should be handled by the
+ plugin according to :attr:`Bcfg2.Server.Lint.Plugin.files`,
+ False otherwise. """
return (self.files is None or
fname in self.files or
os.path.join(self.config['repo'], fname) in self.files or
@@ -77,12 +102,27 @@ class Plugin(object):
fname)) in self.files)
def LintError(self, err, msg):
- """ record an error in the lint process """
+ """ Raise an error from the lint process.
+
+ :param err: The name of the error being raised. This name
+ must be a key in the dict returned by
+ :func:`Bcfg2.Server.Lint.Plugin.Errors`.
+ :type err: string
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self.errorhandler.dispatch(err, msg)
def RenderXML(self, element, keep_text=False):
- """render an XML element for error output -- line number
- prefixed, no children"""
+ """ Render an XML element for error output. This prefixes the
+ line number and removes children for nicer display.
+
+ :param element: The element to render
+ :type element: lxml.etree._Element
+ :param keep_text: Do not discard text content from the element
+ for display
+ :type keep_text: boolean
+ """
xml = None
if len(element) or element.text:
el = copy(element)
@@ -100,11 +140,18 @@ class Plugin(object):
return " line %s: %s" % (element.sourceline, xml)
-class ErrorHandler (object):
- """ a class to handle errors for bcfg2-lint plugins """
+class ErrorHandler(object):
+ """ A class to handle errors for bcfg2-lint plugins """
- def __init__(self, config=None):
+ def __init__(self, errors=None):
+ """
+ :param config: An initial dict of errors to register
+ :type config: dict
+ """
+ #: The number of errors passed to this error handler
self.errors = 0
+
+ #: The number of warnings passed to this error handler
self.warnings = 0
self.logger = logging.getLogger('bcfg2-lint')
@@ -114,17 +161,25 @@ class ErrorHandler (object):
twrap = textwrap.TextWrapper(initial_indent=" ",
subsequent_indent=" ",
width=termsize[0])
+ #: A function to wrap text to the width of the terminal
self._wrapper = twrap.wrap
else:
self._wrapper = lambda s: [s]
+ #: A dict of registered errors
self.errortypes = dict()
- if config is not None:
- self.RegisterErrors(dict(config.items()))
+ if errors is not None:
+ self.RegisterErrors(dict(errors.items()))
def RegisterErrors(self, errors):
- """ Register a dict of errors (name: default level) that a
- plugin may raise """
+ """ Register a dict of errors that a plugin may raise. The
+ keys of the dict are short strings that describe each error;
+ the values are the default error handling for that error
+ ("error", "warning", or "silent").
+
+ :param errors: The error dict
+ :type errors: dict
+ """
for err, action in errors.items():
if err not in self.errortypes:
if "warn" in action:
@@ -135,7 +190,16 @@ class ErrorHandler (object):
self.errortypes[err] = self.debug
def dispatch(self, err, msg):
- """ Dispatch an error to the correct handler """
+ """ Dispatch an error to the correct handler.
+
+ :param err: The name of the error being raised. This name
+ must be a key in
+ :attr:`Bcfg2.Server.Lint.ErrorHandler.errortypes`,
+ the dict of registered errors.
+ :type err: string
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
if err in self.errortypes:
self.errortypes[err](msg)
self.logger.debug(" (%s)" % err)
@@ -145,22 +209,34 @@ class ErrorHandler (object):
self.logger.warning("Unknown error %s" % err)
def error(self, msg):
- """ log an error condition """
+ """ Log an error condition.
+
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self.errors += 1
self._log(msg, self.logger.error, prefix="ERROR: ")
def warn(self, msg):
- """ log a warning condition """
+ """ Log a warning condition.
+
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self.warnings += 1
self._log(msg, self.logger.warning, prefix="WARNING: ")
def debug(self, msg):
- """ log a silent/debug condition """
+ """ Log a silent/debug condition.
+
+ :param msg: The freeform message to display to the end user.
+ :type msg: string
+ """
self._log(msg, self.logger.debug)
def _log(self, msg, logfunc, prefix=""):
""" Generic log function that logs a message with the given
- function after wrapping it for the terminal width """
+ function after wrapping it for the terminal width. """
# a message may itself consist of multiple lines. wrap() will
# elide them all into a single paragraph, which we don't want.
# so we split the message into its paragraphs and wrap each
@@ -180,37 +256,37 @@ class ErrorHandler (object):
logfunc(line)
-class ServerlessPlugin (Plugin):
- """ base class for plugins that are run before the server starts
- up (i.e., plugins that check things that may prevent the server
- from starting up) """
+class ServerlessPlugin(Plugin): # pylint: disable=W0223
+ """ Base class for bcfg2-lint plugins that are run before the
+ server starts up (i.e., plugins that check things that may prevent
+ the server from starting up). """
pass
-class ServerPlugin (Plugin):
- """ base class for plugins that check things that require the
- running Bcfg2 server """
- def __init__(self, core, config, **kwargs):
- Plugin.__init__(self, config, **kwargs)
+class ServerPlugin(Plugin): # pylint: disable=W0223
+ """ Base class for bcfg2-lint plugins that check things that
+ require the running Bcfg2 server. """
+
+ def __init__(self, core, config, errorhandler=None, files=None):
+ """
+ :param core: The Bcfg2 server core
+ :type core: Bcfg2.Server.Core.BaseCore
+ :param config: A :mod:`Bcfg2.Options` setup dict
+ :type config: dict
+ :param errorhandler: A :class:`Bcfg2.Server.Lint.ErrorHandler`
+ that will be used to handle lint errors.
+ If one is not provided, a new one will be
+ instantiated.
+ :type errorhandler: Bcfg2.Server.Lint.ErrorHandler
+ :param files: A list of files to run bcfg2-lint against. (See
+ the bcfg2-lint ``--stdin`` option.)
+ :type files: list of strings
+ """
+ Plugin.__init__(self, config, errorhandler=errorhandler, files=files)
+
+ #: The server core
self.core = core
self.logger = self.core.logger
- self.metadata = self.core.metadata
- self.errorhandler.RegisterErrors({"broken-xinclude-chain": "warning"})
- def has_all_xincludes(self, mfile):
- """ return true if self.files includes all XIncludes listed in
- the specified metadata type, false otherwise"""
- if self.files is None:
- return True
- else:
- path = os.path.join(self.metadata.data, mfile)
- if path in self.files:
- xdata = lxml.etree.parse(path)
- for el in xdata.findall('./%sinclude' % XI_NAMESPACE):
- if not self.has_all_xincludes(el.get('href')):
- self.LintError("broken-xinclude-chain",
- "Broken XInclude chain: could not "
- "include %s" % path)
- return False
-
- return True
+ #: The metadata plugin
+ self.metadata = self.core.metadata
diff --git a/src/lib/Bcfg2/Server/MultiprocessingCore.py b/src/lib/Bcfg2/Server/MultiprocessingCore.py
new file mode 100644
index 000000000..81fba7092
--- /dev/null
+++ b/src/lib/Bcfg2/Server/MultiprocessingCore.py
@@ -0,0 +1,204 @@
+""" The multiprocessing server core is a reimplementation of the
+:mod:`Bcfg2.Server.BuiltinCore` that uses the Python
+:mod:`multiprocessing` library to offload work to multiple child
+processes. As such, it requires Python 2.6+.
+"""
+
+import threading
+import lxml.etree
+import multiprocessing
+from Bcfg2.Compat import Queue
+from Bcfg2.Server.Core import BaseCore, exposed
+from Bcfg2.Server.BuiltinCore import Core as BuiltinCore
+
+
+class DualEvent(object):
+ """ DualEvent is a clone of :class:`threading.Event` that
+ internally implements both :class:`threading.Event` and
+ :class:`multiprocessing.Event`. """
+
+ def __init__(self, threading_event=None, multiprocessing_event=None):
+ self._threading_event = threading_event or threading.Event()
+ self._multiproc_event = multiprocessing_event or \
+ multiprocessing.Event()
+ if threading_event or multiprocessing_event:
+ # initialize internal flag to false, regardless of the
+ # state of either object passed in
+ self.clear()
+
+ def is_set(self):
+ """ Return true if and only if the internal flag is true. """
+ return self._threading_event.is_set()
+
+ isSet = is_set
+
+ def set(self):
+ """ Set the internal flag to true. """
+ self._threading_event.set()
+ self._multiproc_event.set()
+
+ def clear(self):
+ """ Reset the internal flag to false. """
+ self._threading_event.clear()
+ self._multiproc_event.clear()
+
+ def wait(self, timeout=None):
+ """ Block until the internal flag is true, or until the
+ optional timeout occurs. """
+ return self._threading_event.wait(timeout=timeout)
+
+
+class ChildCore(BaseCore):
+ """ A child process for :class:`Bcfg2.MultiprocessingCore.Core`.
+ This core builds configurations from a given
+ :class:`multiprocessing.Pipe`. Note that this is a full-fledged
+ server core; the only input it gets from the parent process is the
+ hostnames of clients to render. All other state comes from the
+ FAM. However, this core only is used to render configs; it doesn't
+ handle anything else (authentication, probes, etc.) because those
+ are all much faster. There's no reason that it couldn't handle
+ those, though, if the pipe communication "protocol" were made more
+ robust. """
+
+ #: How long to wait while polling for new clients to build. This
+ #: doesn't affect the speed with which a client is built, but
+ #: setting it too high will result in longer shutdown times, since
+ #: we only check for the termination event from the main process
+ #: every ``poll_wait`` seconds.
+ poll_wait = 5.0
+
+ def __init__(self, setup, pipe, terminate):
+ """
+ :param setup: A Bcfg2 options dict
+ :type setup: Bcfg2.Options.OptionParser
+ :param pipe: The pipe to which client hostnames are added for
+ ChildCore objects to build configurations, and to
+ which client configurations are added after
+ having been built by ChildCore objects.
+ :type pipe: multiprocessing.Pipe
+ :param terminate: An event that flags ChildCore objects to shut
+ themselves down.
+ :type terminate: multiprocessing.Event
+ """
+ BaseCore.__init__(self, setup)
+
+ #: The pipe to which client hostnames are added for ChildCore
+ #: objects to build configurations, and to which client
+ #: configurations are added after having been built by
+ #: ChildCore objects.
+ self.pipe = pipe
+
+ #: The :class:`multiprocessing.Event` that will be monitored
+ #: to determine when this child should shut down.
+ self.terminate = terminate
+
+ def _daemonize(self):
+ return True
+
+ def _run(self):
+ return True
+
+ def _block(self):
+ while not self.terminate.isSet():
+ try:
+ if self.pipe.poll(self.poll_wait):
+ if not self.metadata.use_database:
+ # handle FAM events, in case (for instance) the
+ # client has just been added to clients.xml, or a
+ # profile has just been asserted. but really, you
+ # should be using the metadata database if you're
+ # using this core.
+ self.fam.handle_events_in_interval(0.1)
+ client = self.pipe.recv()
+ self.logger.debug("Building configuration for %s" % client)
+ config = \
+ lxml.etree.tostring(self.BuildConfiguration(client))
+ self.logger.debug("Returning configuration for %s to main "
+ "process" % client)
+ self.pipe.send(config)
+ self.logger.debug("Returned configuration for %s to main "
+ "process" % client)
+ except KeyboardInterrupt:
+ break
+ self.shutdown()
+
+
+class Core(BuiltinCore):
+ """ A multiprocessing core that delegates building the actual
+ client configurations to
+ :class:`Bcfg2.Server.MultiprocessingCore.ChildCore` objects. The
+ parent process doesn't build any children itself; all calls to
+ :func:`GetConfig` are delegated to children. All other calls are
+ handled by the parent process. """
+
+ #: How long to wait for a child process to shut down cleanly
+ #: before it is terminated.
+ shutdown_timeout = 10.0
+
+ def __init__(self, setup):
+ BuiltinCore.__init__(self, setup)
+ if setup['children'] is None:
+ setup['children'] = multiprocessing.cpu_count()
+
+ #: A dict of child name -> one end of the
+ #: :class:`multiprocessing.Pipe` object used to communicate
+ #: with that child. (The child is given the other end of the
+ #: Pipe.)
+ self.pipes = dict()
+
+ #: A queue that keeps track of which children are available to
+ #: render a configuration. A child is popped from the queue
+ #: when it starts to render a config, then it's pushed back on
+ #: when it's done. This lets us use a blocking call to
+ #: :func:`Queue.Queue.get` when waiting for an available
+ #: child.
+ self.available_children = Queue(maxsize=self.setup['children'])
+
+ # sigh. multiprocessing was added in py2.6, which is when the
+ # camelCase methods for threading objects were deprecated in
+ # favor of the Pythonic under_score methods. So
+ # multiprocessing.Event *only* has is_set(), while
+ # threading.Event has *both* isSet() and is_set(). In order
+ # to make the core work with Python 2.4+, and with both
+ # multiprocessing and threading Event objects, we just
+ # monkeypatch self.terminate to have isSet().
+ self.terminate = DualEvent(threading_event=self.terminate)
+
+ def _run(self):
+ for cnum in range(self.setup['children']):
+ name = "Child-%s" % cnum
+ (mainpipe, childpipe) = multiprocessing.Pipe()
+ self.pipes[name] = mainpipe
+ self.logger.debug("Starting child %s" % name)
+ childcore = ChildCore(self.setup, childpipe, self.terminate)
+ child = multiprocessing.Process(target=childcore.run, name=name)
+ child.start()
+ self.logger.debug("Child %s started with PID %s" % (name,
+ child.pid))
+ self.available_children.put(name)
+ return BuiltinCore._run(self)
+
+ def shutdown(self):
+ BuiltinCore.shutdown(self)
+ for child in multiprocessing.active_children():
+ self.logger.debug("Shutting down child %s" % child.name)
+ child.join(self.shutdown_timeout)
+ if child.is_alive():
+ self.logger.error("Waited %s seconds to shut down %s, "
+ "terminating" % (self.shutdown_timeout,
+ child.name))
+ child.terminate()
+ else:
+ self.logger.debug("Child %s shut down" % child.name)
+ self.logger.debug("All children shut down")
+
+ @exposed
+ def GetConfig(self, address):
+ client = self.resolve_client(address)[0]
+ childname = self.available_children.get()
+ self.logger.debug("Building configuration on child %s" % childname)
+ pipe = self.pipes[childname]
+ pipe.send(client)
+ config = pipe.recv()
+ self.available_children.put_nowait(childname)
+ return config
diff --git a/src/lib/Bcfg2/Server/Plugin/base.py b/src/lib/Bcfg2/Server/Plugin/base.py
index f7bc08717..c825a57b5 100644
--- a/src/lib/Bcfg2/Server/Plugin/base.py
+++ b/src/lib/Bcfg2/Server/Plugin/base.py
@@ -87,6 +87,10 @@ class Plugin(Debuggable):
#: alphabetically by their name.
sort_order = 500
+ #: Whether or not to automatically create a data directory for
+ #: this plugin
+ create = True
+
#: List of names of methods to be exposed as XML-RPC functions
__rmi__ = Debuggable.__rmi__
@@ -97,15 +101,21 @@ class Plugin(Debuggable):
:param datastore: The path to the Bcfg2 repository on the
filesystem
:type datastore: string
- :raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError`
+ :raises: :exc:`OSError` if adding a file monitor failed;
+ :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError`
+ on other errors
.. autoattribute:: Bcfg2.Server.Plugin.base.Debuggable.__rmi__
"""
+ Debuggable.__init__(self, name=self.name)
self.Entries = {}
self.core = core
self.data = os.path.join(datastore, self.name)
+ if self.create and not os.path.exists(self.data):
+ self.logger.warning("%s: %s does not exist, creating" %
+ (self.name, self.data))
+ os.makedirs(self.data)
self.running = True
- Debuggable.__init__(self, name=self.name)
@classmethod
def init_repo(cls, repo):
@@ -125,5 +135,11 @@ class Plugin(Debuggable):
self.debug_log("Shutting down %s plugin" % self.name)
self.running = False
+ def set_debug(self, debug):
+ for entry in self.Entries.values():
+ if isinstance(entry, Debuggable):
+ entry.set_debug(debug)
+ return Debuggable.set_debug(self, debug)
+
def __str__(self):
return "%s Plugin" % self.__class__.__name__
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 0b81077a3..81dc1d736 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -25,15 +25,15 @@ except ImportError:
HAS_DJANGO = False
#: A dict containing default metadata for Path entries from bcfg2.conf
-DEFAULT_FILE_METADATA = Bcfg2.Options.OptionParser(dict(
- configfile=Bcfg2.Options.CFILE,
- owner=Bcfg2.Options.MDATA_OWNER,
- group=Bcfg2.Options.MDATA_GROUP,
- mode=Bcfg2.Options.MDATA_MODE,
- secontext=Bcfg2.Options.MDATA_SECONTEXT,
- important=Bcfg2.Options.MDATA_IMPORTANT,
- paranoid=Bcfg2.Options.MDATA_PARANOID,
- sensitive=Bcfg2.Options.MDATA_SENSITIVE))
+DEFAULT_FILE_METADATA = Bcfg2.Options.OptionParser(
+ dict(configfile=Bcfg2.Options.CFILE,
+ owner=Bcfg2.Options.MDATA_OWNER,
+ group=Bcfg2.Options.MDATA_GROUP,
+ mode=Bcfg2.Options.MDATA_MODE,
+ secontext=Bcfg2.Options.MDATA_SECONTEXT,
+ important=Bcfg2.Options.MDATA_IMPORTANT,
+ paranoid=Bcfg2.Options.MDATA_PARANOID,
+ sensitive=Bcfg2.Options.MDATA_SENSITIVE))
DEFAULT_FILE_METADATA.parse([Bcfg2.Options.CFILE.cmd, Bcfg2.Options.CFILE])
del DEFAULT_FILE_METADATA['args']
del DEFAULT_FILE_METADATA['configfile']
@@ -41,15 +41,15 @@ del DEFAULT_FILE_METADATA['configfile']
LOGGER = logging.getLogger(__name__)
#: a compiled regular expression for parsing info and :info files
-INFO_REGEX = re.compile('owner:(\s)*(?P<owner>\S+)|' +
- 'group:(\s)*(?P<group>\S+)|' +
- 'mode:(\s)*(?P<mode>\w+)|' +
- 'secontext:(\s)*(?P<secontext>\S+)|' +
- 'paranoid:(\s)*(?P<paranoid>\S+)|' +
- 'sensitive:(\s)*(?P<sensitive>\S+)|' +
- 'encoding:(\s)*(?P<encoding>\S+)|' +
- 'important:(\s)*(?P<important>\S+)|' +
- 'mtime:(\s)*(?P<mtime>\w+)|')
+INFO_REGEX = re.compile(r'owner:\s*(?P<owner>\S+)|' +
+ r'group:\s*(?P<group>\S+)|' +
+ r'mode:\s*(?P<mode>\w+)|' +
+ r'secontext:\s*(?P<secontext>\S+)|' +
+ r'paranoid:\s*(?P<paranoid>\S+)|' +
+ r'sensitive:\s*(?P<sensitive>\S+)|' +
+ r'encoding:\s*(?P<encoding>\S+)|' +
+ r'important:\s*(?P<important>\S+)|' +
+ r'mtime:\s*(?P<mtime>\w+)')
def bind_info(entry, metadata, infoxml=None, default=DEFAULT_FILE_METADATA):
@@ -193,7 +193,7 @@ class PluginDatabaseModel(object):
app_label = "Server"
-class FileBacked(object):
+class FileBacked(Debuggable):
""" This object caches file data in memory. FileBacked objects are
principally meant to be used as a part of
:class:`Bcfg2.Server.Plugin.helpers.DirectoryBacked`. """
@@ -206,7 +206,7 @@ class FileBacked(object):
changes
:type fam: Bcfg2.Server.FileMonitor.FileMonitor
"""
- object.__init__(self)
+ Debuggable.__init__(self)
#: A string containing the raw data in this file
self.data = ''
@@ -231,10 +231,10 @@ class FileBacked(object):
self.Index()
except IOError:
err = sys.exc_info()[1]
- LOGGER.error("Failed to read file %s: %s" % (self.name, err))
+ self.logger.error("Failed to read file %s: %s" % (self.name, err))
except:
err = sys.exc_info()[1]
- LOGGER.error("Failed to parse file %s: %s" % (self.name, err))
+ self.logger.error("Failed to parse file %s: %s" % (self.name, err))
def Index(self):
""" Index() is called by :func:`HandleEvent` every time the
@@ -246,7 +246,7 @@ class FileBacked(object):
return "%s: %s" % (self.__class__.__name__, self.name)
-class DirectoryBacked(object):
+class DirectoryBacked(Debuggable):
""" DirectoryBacked objects represent a directory that contains
files, represented by objects of the type listed in
:attr:`__child__`, and other directories recursively. It monitors
@@ -280,7 +280,7 @@ class DirectoryBacked(object):
.. -----
.. autoattribute:: __child__
"""
- object.__init__(self)
+ Debuggable.__init__(self)
self.data = os.path.normpath(data)
self.fam = fam
@@ -299,11 +299,29 @@ class DirectoryBacked(object):
self.handles = {}
# Monitor everything in the plugin's directory
+ if not os.path.exists(self.data):
+ self.logger.warning("%s does not exist, creating" % self.data)
+ os.makedirs(self.data)
self.add_directory_monitor('')
+ def set_debug(self, debug):
+ for entry in self.entries.values():
+ if isinstance(entry, Debuggable):
+ entry.set_debug(debug)
+ return Debuggable.set_debug(self, debug)
+
def __getitem__(self, key):
return self.entries[key]
+ def __len__(self):
+ return len(self.entries)
+
+ def __delitem__(self, key):
+ del self.entries[key]
+
+ def __setitem__(self, key, val):
+ self.entries[key] = val
+
def __iter__(self):
return iter(list(self.entries.items()))
@@ -320,7 +338,7 @@ class DirectoryBacked(object):
dirpathname = os.path.join(self.data, relative)
if relative not in self.handles.values():
if not os.path.isdir(dirpathname):
- LOGGER.error("%s is not a directory" % dirpathname)
+ self.logger.error("%s is not a directory" % dirpathname)
return
reqid = self.fam.AddMonitor(dirpathname, self)
self.handles[reqid] = relative
@@ -365,8 +383,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, event.filename))
+ self.logger.warn("Got %s event with unknown handle (%s) for %s" %
+ (action, event.requestID, event.filename))
return
# Clean up path names
@@ -376,7 +394,7 @@ class DirectoryBacked(object):
event.filename = event.filename[len(self.data) + 1:]
if self.ignore and self.ignore.search(event.filename):
- LOGGER.debug("Ignoring event %s" % event.filename)
+ self.logger.debug("Ignoring event %s" % event.filename)
return
# Calculate the absolute and relative paths this event refers to
@@ -411,19 +429,20 @@ class DirectoryBacked(object):
# class doesn't support canceling, so at least let
# the user know that a restart might be a good
# idea.
- LOGGER.warn("Directory properties for %s changed, please "
- " consider restarting the server" % abspath)
+ self.logger.warn("Directory properties for %s changed, "
+ "please consider restarting the server" %
+ abspath)
else:
# Got a "changed" event for a directory that we
# 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))
+ self.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))
+ self.logger.warn("Got unknown dir event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
elif self.patterns.search(event.filename):
if action in ['exists', 'created']:
self.add_entry(relpath, event)
@@ -435,16 +454,15 @@ 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))
+ self.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))
+ self.logger.warn("Got unknown file event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
else:
- LOGGER.warn("Could not process filename %s; ignoring" %
- event.filename)
+ self.logger.warn("Could not process filename %s; ignoring" %
+ event.filename)
class XMLFileBacked(FileBacked):
@@ -459,7 +477,11 @@ class XMLFileBacked(FileBacked):
#: behavior, set ``__identifier__`` to ``None``.
__identifier__ = 'name'
- def __init__(self, filename, fam=None, should_monitor=False):
+ #: If ``create`` is set, then it overrides the ``create`` argument
+ #: to the constructor.
+ create = None
+
+ def __init__(self, filename, fam=None, should_monitor=False, create=None):
"""
:param filename: The full path to the file to cache and monitor
:type filename: string
@@ -474,6 +496,13 @@ class XMLFileBacked(FileBacked):
:class:`Bcfg2.Server.Plugin.helpers.XMLDirectoryBacked`
object).
:type should_monitor: bool
+ :param create: Create the file if it doesn't exist.
+ ``create`` can be either an
+ :class:`lxml.etree._Element` object, which will
+ be used as initial content, or a string, which
+ will be used as the name of the (empty) tag
+ that will be the initial content of the file.
+ :type create: lxml.etree._Element or string
.. -----
.. autoattribute:: __identifier__
@@ -497,6 +526,21 @@ class XMLFileBacked(FileBacked):
#: "Extra" files included in this file by XInclude.
self.extras = []
+ #: Extra FAM monitors set by this object for files included by
+ #: XInclude.
+ self.extra_monitors = []
+
+ if ((create is not None or self.create not in [None, False]) and
+ not os.path.exists(self.name)):
+ toptag = create or self.create
+ self.logger.warning("%s does not exist, creating" % self.name)
+ if hasattr(toptag, "getroottree"):
+ el = toptag
+ else:
+ el = lxml.etree.Element(toptag)
+ el.getroottree().write(self.name, xml_declaration=False,
+ pretty_print=True)
+
#: Whether or not to monitor this file for changes.
self.should_monitor = should_monitor
if fam and should_monitor:
@@ -528,17 +572,19 @@ class XMLFileBacked(FileBacked):
if not extras:
msg = "%s: %s does not exist, skipping" % (self.name, name)
if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
- LOGGER.debug(msg)
+ self.logger.debug(msg)
else:
- LOGGER.warning(msg)
+ self.logger.warning(msg)
parent = el.getparent()
parent.remove(el)
for extra in extras:
if extra != self.name and extra not in self.extras:
- self.add_monitor(extra)
+ self.extras.append(extra)
lxml.etree.SubElement(parent, xinclude, href=extra)
self._follow_xincludes(fname=extra)
+ if extra not in self.extra_monitors:
+ self.add_monitor(extra)
def Index(self):
self.xdata = lxml.etree.XML(self.data, base_url=self.name,
@@ -550,7 +596,8 @@ class XMLFileBacked(FileBacked):
self.xdata.getroottree().xinclude()
except lxml.etree.XIncludeError:
err = sys.exc_info()[1]
- LOGGER.error("XInclude failed on %s: %s" % (self.name, err))
+ self.logger.error("XInclude failed on %s: %s" % (self.name,
+ err))
self.entries = self.xdata.getchildren()
if self.__identifier__ is not None:
@@ -566,7 +613,7 @@ class XMLFileBacked(FileBacked):
:type fpath: string
:returns: None
"""
- self.extras.append(fpath)
+ self.extra_monitors.append(fpath)
if self.fam and self.should_monitor:
self.fam.AddMonitor(fpath, self)
@@ -755,14 +802,14 @@ class InfoNode (INode):
Client="lambda m, e: '%(name)s' == m.hostname and predicate(m, e)",
Group="lambda m, e: '%(name)s' in m.groups and predicate(m, e)",
Path="lambda m, e: ('%(name)s' == e.get('name') or " +
- "'%(name)s' == e.get('realname')) and " +
- "predicate(m, e)")
+ "'%(name)s' == e.get('realname')) and " +
+ "predicate(m, e)")
nraw = dict(
Client="lambda m, e: '%(name)s' != m.hostname and predicate(m, e)",
Group="lambda m, e: '%(name)s' not in m.groups and predicate(m, e)",
Path="lambda m, e: '%(name)s' != e.get('name') and " +
- "'%(name)s' != e.get('realname') and " +
- "predicate(m, e)")
+ "'%(name)s' != e.get('realname') and " +
+ "predicate(m, e)")
containers = ['Group', 'Client', 'Path']
@@ -776,8 +823,8 @@ class XMLSrc(XMLFileBacked):
__cacheobj__ = dict
__priority_required__ = True
- def __init__(self, filename, fam=None, should_monitor=False):
- XMLFileBacked.__init__(self, filename, fam, should_monitor)
+ def __init__(self, filename, fam=None, should_monitor=False, create=None):
+ XMLFileBacked.__init__(self, filename, fam, should_monitor, create)
self.items = {}
self.cache = None
self.pnode = None
@@ -789,7 +836,7 @@ class XMLSrc(XMLFileBacked):
data = open(self.name).read()
except IOError:
msg = "Failed to read file %s: %s" % (self.name, sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.items = {}
try:
@@ -797,7 +844,7 @@ class XMLSrc(XMLFileBacked):
except lxml.etree.XMLSyntaxError:
msg = "Failed to parse file %s: %s" % (self.name,
sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.pnode = self.__node__(xdata, self.items)
self.cache = None
@@ -807,7 +854,7 @@ class XMLSrc(XMLFileBacked):
if self.__priority_required__:
msg = "Got bogus priority %s for file %s" % \
(xdata.get('priority'), self.name)
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
del xdata, data
@@ -817,8 +864,8 @@ class XMLSrc(XMLFileBacked):
if self.cache is None or self.cache[0] != metadata:
cache = (metadata, self.__cacheobj__())
if self.pnode is None:
- LOGGER.error("Cache method called early for %s; "
- "forcing data load" % self.name)
+ self.logger.error("Cache method called early for %s; "
+ "forcing data load" % self.name)
self.HandleEvent()
return
self.pnode.Match(metadata, cache[1])
@@ -842,7 +889,7 @@ class XMLDirectoryBacked(DirectoryBacked):
#: Only track and include files whose names (not paths) match this
#: compiled regex.
- patterns = re.compile('^.*\.xml$')
+ patterns = re.compile(r'^.*\.xml$')
#: The type of child objects to create for files contained within
#: the directory that is tracked. Default is
@@ -1111,7 +1158,7 @@ class EntrySet(Debuggable):
#: file is encountered that does not match the ``basename``
#: argument passed to the constructor or ``ignore``, then a
#: warning will be produced.
- ignore = re.compile("^(\.#.*|.*~|\\..*\\.(sw[px])|.*\\.genshi_include)$")
+ ignore = re.compile(r'^(\.#.*|.*~|\..*\.(sw[px])|.*\.genshi_include)$')
# The ``basename`` argument passed to the constructor will be
#: processed as a string that contains a regular expression (i.e.,
@@ -1174,8 +1221,8 @@ class EntrySet(Debuggable):
base_pat = basename
else:
base_pat = re.escape(basename)
- pattern = '(.*/)?%s(\.((H_(?P<hostname>\S+))|' % base_pat
- pattern += '(G(?P<prio>\d+)_(?P<group>\S+))))?$'
+ pattern = r'(.*/)?' + base_pat + \
+ r'(\.((H_(?P<hostname>\S+))|(G(?P<prio>\d+)_(?P<group>\S+))))?$'
#: ``specific`` is a regular expression that is used to
#: determine the specificity of a file in this entry set. It
@@ -1254,8 +1301,8 @@ class EntrySet(Debuggable):
self.entry_init(event)
else:
if event.filename not in self.entries:
- LOGGER.warning("Got %s event for unknown file %s" %
- (action, event.filename))
+ self.logger.warning("Got %s event for unknown file %s" %
+ (action, event.filename))
if action == 'changed':
# received a bogus changed event; warn, but treat
# it like a created event
@@ -1291,7 +1338,7 @@ class EntrySet(Debuggable):
entry_type = self.entry_type
if event.filename in self.entries:
- LOGGER.warn("Got duplicate add for %s" % event.filename)
+ self.logger.warn("Got duplicate add for %s" % event.filename)
else:
fpath = os.path.join(self.path, event.filename)
try:
@@ -1299,8 +1346,8 @@ class EntrySet(Debuggable):
specific=specific)
except SpecificityError:
if not self.ignore.match(event.filename):
- LOGGER.error("Could not process filename %s; ignoring" %
- fpath)
+ self.logger.error("Could not process filename %s; ignoring"
+ % fpath)
return
self.entries[event.filename] = entry_type(fpath, spec,
self.encoding)
@@ -1365,8 +1412,8 @@ class EntrySet(Debuggable):
for line in open(fpath).readlines():
match = INFO_REGEX.match(line)
if not match:
- LOGGER.warning("Failed to match line in %s: %s" % (fpath,
- line))
+ self.logger.warning("Failed to match line in %s: %s" %
+ (fpath, line))
continue
else:
mgd = match.groupdict()
@@ -1450,8 +1497,6 @@ class GroupSpool(Plugin, Generator):
def __init__(self, core, datastore):
Plugin.__init__(self, core, datastore)
Generator.__init__(self)
- if self.data[-1] == '/':
- self.data = self.data[:-1]
#: See :class:`Bcfg2.Server.Plugins.interfaces.Generator` for
#: details on the Entries attribute.
diff --git a/src/lib/Bcfg2/Server/Plugin/interfaces.py b/src/lib/Bcfg2/Server/Plugin/interfaces.py
index cb996b1ca..222b94fe3 100644
--- a/src/lib/Bcfg2/Server/Plugin/interfaces.py
+++ b/src/lib/Bcfg2/Server/Plugin/interfaces.py
@@ -286,6 +286,8 @@ class Statistics(Plugin):
you should avoid using Statistics and use
:class:`ThreadedStatistics` instead."""
+ create = False
+
def process_statistics(self, client, xdata):
""" Process the given XML statistics data for the specified
client.
@@ -337,12 +339,11 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
pending_data = []
try:
while not self.work_queue.empty():
- (metadata, data) = self.work_queue.get_nowait()
- pending_data.append(
- (metadata.hostname,
- lxml.etree.tostring(
- data,
- xml_declaration=False).decode("UTF-8")))
+ (metadata, xdata) = self.work_queue.get_nowait()
+ data = \
+ lxml.etree.tostring(xdata,
+ xml_declaration=False).decode("UTF-8")
+ pending_data.append((metadata.hostname, data))
except Empty:
pass
@@ -409,7 +410,7 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
def run(self):
if not self._load():
return
- while not self.terminate.isSet() and self.work_queue != None:
+ while not self.terminate.isSet() and self.work_queue is not None:
try:
(client, xdata) = self.work_queue.get(block=True, timeout=2)
except Empty:
@@ -419,7 +420,7 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
self.logger.error("ThreadedStatistics: %s" % err)
continue
self.handle_statistic(client, xdata)
- if self.work_queue != None and not self.work_queue.empty():
+ if self.work_queue is not None and not self.work_queue.empty():
self._save()
def process_statistics(self, metadata, data):
@@ -527,6 +528,8 @@ class GoalValidator(object):
class Version(Plugin):
""" Version plugins interact with various version control systems. """
+ create = False
+
#: The path to the VCS metadata file or directory, relative to the
#: base of the Bcfg2 repository. E.g., for Subversion this would
#: be ".svn"
diff --git a/src/lib/Bcfg2/Server/Plugins/Base.py b/src/lib/Bcfg2/Server/Plugins/Base.py
index d662da60a..a18204d60 100644
--- a/src/lib/Bcfg2/Server/Plugins/Base.py
+++ b/src/lib/Bcfg2/Server/Plugins/Base.py
@@ -20,13 +20,8 @@ class Base(Bcfg2.Server.Plugin.Plugin,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Structure.__init__(self)
- try:
- Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self,
- self.data,
- self.core.fam)
- except OSError:
- self.logger.error("Failed to load Base repository")
- raise Bcfg2.Server.Plugin.PluginInitError
+ Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self, self.data,
+ self.core.fam)
def BuildStructures(self, metadata):
"""Build structures for client described by metadata."""
diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py
index 7030c1574..eef176cca 100644
--- a/src/lib/Bcfg2/Server/Plugins/Bundler.py
+++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py
@@ -13,7 +13,7 @@ import Bcfg2.Server.Lint
try:
import genshi.template.base
- import Bcfg2.Server.Plugins.TGenshi
+ from Bcfg2.Server.Plugins.TGenshi import removecomment, TemplateFile
HAS_GENSHI = True
except ImportError:
HAS_GENSHI = False
@@ -34,14 +34,12 @@ class BundleFile(Bcfg2.Server.Plugin.StructFile):
if HAS_GENSHI:
- class BundleTemplateFile(Bcfg2.Server.Plugins.TGenshi.TemplateFile,
+ class BundleTemplateFile(TemplateFile,
Bcfg2.Server.Plugin.StructFile):
""" Representation of a Genshi-templated bundle XML file """
def __init__(self, name, specific, encoding):
- Bcfg2.Server.Plugins.TGenshi.TemplateFile.__init__(self, name,
- specific,
- encoding)
+ TemplateFile.__init__(self, name, specific, encoding)
Bcfg2.Server.Plugin.StructFile.__init__(self, name)
self.logger = logging.getLogger(name)
@@ -52,9 +50,9 @@ if HAS_GENSHI:
msg = "No parsed template information for %s" % self.name
self.logger.error(msg)
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
- stream = self.template.generate(metadata=metadata,
- repo=SETUP['repo']).filter(
- Bcfg2.Server.Plugins.TGenshi.removecomment)
+ stream = self.template.generate(
+ metadata=metadata,
+ repo=SETUP['repo']).filter(removecomment)
data = lxml.etree.XML(stream.render('xml',
strip_whitespace=False),
parser=Bcfg2.Server.XMLParser)
@@ -85,23 +83,15 @@ class Bundler(Bcfg2.Server.Plugin.Plugin,
""" The bundler creates dependent clauses based on the
bundle/translation scheme from Bcfg1. """
__author__ = 'bcfg-dev@mcs.anl.gov'
- patterns = re.compile('^(?P<name>.*)\.(xml|genshi)$')
+ patterns = re.compile(r'^(?P<name>.*)\.(xml|genshi)$')
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Structure.__init__(self)
self.encoding = core.setup['encoding']
self.__child__ = self.template_dispatch
- try:
- Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self,
- self.data,
- self.core.fam)
- except OSError:
- err = sys.exc_info()[1]
- msg = "Failed to load Bundle repository %s: %s" % (self.data, err)
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
-
+ Bcfg2.Server.Plugin.XMLDirectoryBacked.__init__(self, self.data,
+ self.core.fam)
global SETUP
SETUP = core.setup
@@ -154,10 +144,10 @@ class Bundler(Bcfg2.Server.Plugin.Plugin,
class BundlerLint(Bcfg2.Server.Lint.ServerPlugin):
- """ Perform various bundle checks """
+ """ Perform various :ref:`Bundler
+ <server-plugins-structures-bundler-index>` checks. """
def Run(self):
- """ run plugin """
self.missing_bundles()
for bundle in self.core.plugins['Bundler'].entries.values():
if (self.HandlesFile(bundle.name) and
@@ -171,7 +161,8 @@ class BundlerLint(Bcfg2.Server.Lint.ServerPlugin):
"inconsistent-bundle-name": "warning"}
def missing_bundles(self):
- """ find bundles listed in Metadata but not implemented in Bundler """
+ """ 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
@@ -190,7 +181,11 @@ class BundlerLint(Bcfg2.Server.Lint.ServerPlugin):
bundle)
def bundle_names(self, bundle):
- """ verify bundle name attribute matches filename """
+ """ Verify bundle name attribute matches filename.
+
+ :param bundle: The bundle to verify
+ :type bundle: Bcfg2.Server.Plugins.Bundler.BundleFile
+ """
try:
xdata = lxml.etree.XML(bundle.data)
except AttributeError:
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
index c2e5afbad..83a5c1165 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
@@ -90,7 +90,7 @@ class CfgGenshiGenerator(CfgGenerator):
#: exception in a Genshi template so we can provide a decent error
#: message that actually tells the end user where an error
#: occurred.
- pyerror_re = re.compile('<\w+ u?[\'"](.*?)\s*\.\.\.[\'"]>')
+ pyerror_re = re.compile(r'<\w+ u?[\'"](.*?)\s*\.\.\.[\'"]>')
def __init__(self, fname, spec, encoding):
CfgGenerator.__init__(self, fname, spec, encoding)
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
index aaeb65cd6..c7b62f352 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
@@ -48,9 +48,8 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
if (HAS_CRYPTO and
SETUP.cfp.has_section("sshkeys") and
SETUP.cfp.has_option("sshkeys", "passphrase")):
- return Bcfg2.Encryption.get_passphrases(SETUP)[SETUP.cfp.get(
- "sshkeys",
- "passphrase")]
+ return Bcfg2.Encryption.get_passphrases(SETUP)[
+ SETUP.cfp.get("sshkeys", "passphrase")]
return None
def handle_event(self, event):
@@ -70,7 +69,7 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
the given client metadata, and may be obtained by
doing ``self.XMLMatch(metadata)``
:type spec: lxml.etree._Element
- :returns: None
+ :returns: string - The filename of the private key
"""
if spec is None:
spec = self.XMLMatch(metadata)
@@ -141,7 +140,6 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile):
if spec is None:
spec = self.XMLMatch(metadata)
category = spec.get("category", self.category)
- print("category=%s" % category)
if category is None:
per_host_default = "true"
else:
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index ec3ba222c..154cd5e63 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -35,6 +35,24 @@ SETUP = None
#: facility for passing it otherwise.
CFG = None
+_HANDLERS = []
+
+
+def handlers():
+ """ A list of Cfg handler classes. Loading the handlers must
+ be done at run-time, not at compile-time, or it causes a
+ circular import and Bad Things Happen."""
+ if not _HANDLERS:
+ for submodule in walk_packages(path=__path__, prefix=__name__ + "."):
+ mname = submodule[1].rsplit('.', 1)[-1]
+ module = getattr(__import__(submodule[1]).Server.Plugins.Cfg,
+ mname)
+ hdlr = getattr(module, mname)
+ if issubclass(hdlr, CfgBaseFileMatcher):
+ _HANDLERS.append(hdlr)
+ _HANDLERS.sort(key=operator.attrgetter("__priority__"))
+ return _HANDLERS
+
class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
Bcfg2.Server.Plugin.Debuggable):
@@ -87,7 +105,7 @@ class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
Bcfg2.Server.Plugin.Debuggable.__init__(self)
self.encoding = encoding
__init__.__doc__ = Bcfg2.Server.Plugin.SpecificData.__init__.__doc__ + \
-"""
+ """
.. -----
.. autoattribute:: CfgBaseFileMatcher.__basenames__
.. autoattribute:: CfgBaseFileMatcher.__extensions__
@@ -111,12 +129,12 @@ class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
components = ['^(?P<basename>%s)' % '|'.join(re.escape(b)
for b in basenames)]
if cls.__specific__:
- components.append('(|\\.H_(?P<hostname>\S+?)|' +
- '\.G(?P<prio>\d+)_(?P<group>\S+?))')
+ components.append(r'(|\.H_(?P<hostname>\S+?)|' +
+ r'\.G(?P<prio>\d+)_(?P<group>\S+?))')
if cls.__extensions__:
- components.append('\\.(?P<extension>%s)' %
- '|'.join(cls.__extensions__))
- components.append('$')
+ components.append(r'\.(?P<extension>%s)' %
+ r'|'.join(cls.__extensions__))
+ components.append(r'$')
return re.compile("".join(components))
@classmethod
@@ -459,7 +477,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
entry_type, encoding)
Bcfg2.Server.Plugin.Debuggable.__init__(self)
self.specific = None
- self._handlers = None
__init__.__doc__ = Bcfg2.Server.Plugin.EntrySet.__doc__
def set_debug(self, debug):
@@ -468,24 +485,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
entry.set_debug(debug)
return rv
- @property
- def handlers(self):
- """ A list of Cfg handler classes. Loading the handlers must
- be done at run-time, not at compile-time, or it causes a
- circular import and Bad Things Happen."""
- if self._handlers is None:
- self._handlers = []
- for submodule in walk_packages(path=__path__,
- prefix=__name__ + "."):
- mname = submodule[1].rsplit('.', 1)[-1]
- module = getattr(__import__(submodule[1]).Server.Plugins.Cfg,
- mname)
- hdlr = getattr(module, mname)
- if CfgBaseFileMatcher in hdlr.__mro__:
- self._handlers.append(hdlr)
- self._handlers.sort(key=operator.attrgetter("__priority__"))
- return self._handlers
-
def handle_event(self, event):
""" Dispatch a FAM event to :func:`entry_init` or the
appropriate child handler object.
@@ -502,7 +501,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
# process a bogus changed event like a created
return
- for hdlr in self.handlers:
+ for hdlr in handlers():
if hdlr.handles(event, basename=self.path):
if action == 'changed':
# warn about a bogus 'changed' event, but
@@ -520,7 +519,9 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
return
elif hdlr.ignore(event, basename=self.path):
return
- elif action == 'changed':
+ # we only get here if event.filename in self.entries, so handle
+ # created event like changed
+ elif action == 'changed' or action == 'created':
self.entries[event.filename].handle_event(event)
return
elif action == 'deleted':
@@ -580,10 +581,18 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
def bind_entry(self, entry, metadata):
self.bind_info_to_entry(entry, metadata)
- data = self._generate_data(entry, metadata)
-
- for fltr in self.get_handlers(metadata, CfgFilter):
- data = fltr.modify_data(entry, metadata, data)
+ data, generator = self._generate_data(entry, metadata)
+
+ if generator is not None:
+ # apply no filters if the data was created by a CfgCreator
+ for fltr in self.get_handlers(metadata, CfgFilter):
+ if fltr.specific <= generator.specific:
+ # only apply filters that are as specific or more
+ # specific than the generator used for this entry.
+ # Note that specificity comparison is backwards in
+ # this sense, since it's designed to sort from
+ # most specific to least specific.
+ data = fltr.modify_data(entry, metadata, data)
if SETUP['validate']:
try:
@@ -599,6 +608,8 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
else:
try:
if not isinstance(data, unicode):
+ if not isinstance(data, str):
+ data = data.decode('utf-8')
data = u_str(data, self.encoding)
except UnicodeDecodeError:
msg = "Failed to decode %s: %s" % (entry.get('name'),
@@ -690,7 +701,9 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
:type entry: lxml.etree._Element
:param metadata: The client metadata to generate data for
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :returns: string - the data for the entry
+ :returns: tuple of (string, generator) - the data for the
+ entry and the generator used to generate it (or
+ None, if data was created)
"""
try:
generator = self.best_matching(metadata,
@@ -699,7 +712,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
except PluginExecutionError:
# if no creators or generators exist, _create_data()
# raises an appropriate exception
- return self._create_data(entry, metadata)
+ return (self._create_data(entry, metadata), None)
if entry.get('mode').lower() == 'inherit':
# use on-disk permissions
@@ -709,7 +722,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
entry.set('mode',
oct_mode(stat.S_IMODE(os.stat(fname).st_mode)))
try:
- return generator.get_data(entry, metadata)
+ return (generator.get_data(entry, metadata), generator)
except:
msg = "Cfg: Error rendering %s: %s" % (entry.get("name"),
sys.exc_info()[1])
@@ -758,8 +771,8 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
pass
if not rv or not rv[0].hostname:
- rv.append(Bcfg2.Server.Plugin.Specificity(
- hostname=metadata.hostname))
+ rv.append(
+ Bcfg2.Server.Plugin.Specificity(hostname=metadata.hostname))
return rv
def build_filename(self, specific):
@@ -884,12 +897,15 @@ class CfgLint(Bcfg2.Server.Lint.ServerPlugin):
for basename, entry in list(self.core.plugins['Cfg'].entries.items()):
self.check_delta(basename, entry)
self.check_pubkey(basename, entry)
+ self.check_missing_files()
@classmethod
def Errors(cls):
return {"cat-file-used": "warning",
"diff-file-used": "warning",
- "no-pubkey-xml": "warning"}
+ "no-pubkey-xml": "warning",
+ "unknown-cfg-files": "error",
+ "extra-cfg-files": "error"}
def check_delta(self, basename, entry):
""" check that no .cat or .diff files are in use """
@@ -923,3 +939,41 @@ class CfgLint(Bcfg2.Server.Lint.ServerPlugin):
self.LintError("no-pubkey-xml",
"%s has no corresponding pubkey.xml at %s" %
(basename, pubkey))
+
+ def check_missing_files(self):
+ """ check that all files on the filesystem are known to Cfg """
+ cfg = self.core.plugins['Cfg']
+
+ # first, collect ignore patterns from handlers
+ ignore = []
+ for hdlr in handlers():
+ ignore.extend(hdlr.__ignore__)
+
+ # next, get a list of all non-ignored files on the filesystem
+ all_files = set()
+ for root, _, files in os.walk(cfg.data):
+ all_files.update(os.path.join(root, fname)
+ for fname in files
+ if not any(fname.endswith("." + i)
+ for i in ignore))
+
+ # next, get a list of all files known to Cfg
+ cfg_files = set()
+ for root, eset in cfg.entries.items():
+ cfg_files.update(os.path.join(cfg.data, root.lstrip("/"), fname)
+ for fname in eset.entries.keys())
+
+ # finally, compare the two
+ unknown_files = all_files - cfg_files
+ extra_files = cfg_files - all_files
+ if unknown_files:
+ self.LintError(
+ "unknown-cfg-files",
+ "Files on the filesystem could not be understood by Cfg: %s" %
+ "; ".join(unknown_files))
+ if extra_files:
+ self.LintError(
+ "extra-cfg-files",
+ "Cfg has entries for files that do not exist on the "
+ "filesystem: %s\nThis is probably a bug." %
+ "; ".join(extra_files))
diff --git a/src/lib/Bcfg2/Server/Plugins/Cvs.py b/src/lib/Bcfg2/Server/Plugins/Cvs.py
index ba1559a1a..22cacaa76 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cvs.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cvs.py
@@ -20,9 +20,9 @@ class Cvs(Bcfg2.Server.Plugin.Version):
"""Read cvs revision information for the Bcfg2 repository."""
try:
data = Popen("env LC_ALL=C cvs log",
- shell=True,
- cwd=self.vcs_root,
- stdout=PIPE).stdout.readlines()
+ shell=True,
+ cwd=self.vcs_root,
+ stdout=PIPE).stdout.readlines()
return data[3].strip('\n')
except IndexError:
msg = "Failed to read CVS log"
diff --git a/src/lib/Bcfg2/Server/Plugins/Darcs.py b/src/lib/Bcfg2/Server/Plugins/Darcs.py
index 0033e00f3..b4abafb0e 100644
--- a/src/lib/Bcfg2/Server/Plugins/Darcs.py
+++ b/src/lib/Bcfg2/Server/Plugins/Darcs.py
@@ -20,9 +20,9 @@ class Darcs(Bcfg2.Server.Plugin.Version):
"""Read Darcs changeset information for the Bcfg2 repository."""
try:
data = Popen("env LC_ALL=C darcs changes",
- shell=True,
- cwd=self.vcs_root,
- stdout=PIPE).stdout.readlines()
+ shell=True,
+ cwd=self.vcs_root,
+ stdout=PIPE).stdout.readlines()
revision = data[0].strip('\n')
except:
msg = "Failed to read darcs repository"
diff --git a/src/lib/Bcfg2/Server/Plugins/Decisions.py b/src/lib/Bcfg2/Server/Plugins/Decisions.py
index eae18fdfe..66f299bc9 100644
--- a/src/lib/Bcfg2/Server/Plugins/Decisions.py
+++ b/src/lib/Bcfg2/Server/Plugins/Decisions.py
@@ -2,7 +2,6 @@
blacklist certain entries. """
import os
-import sys
import lxml.etree
import Bcfg2.Server.Plugin
@@ -40,18 +39,10 @@ class Decisions(Bcfg2.Server.Plugin.EntrySet,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Decision.__init__(self)
-
Bcfg2.Server.Plugin.EntrySet.__init__(self, '(white|black)list',
- self.data,
- DecisionFile,
+ self.data, DecisionFile,
core.setup['encoding'])
- try:
- core.fam.AddMonitor(self.data, self)
- except OSError:
- err = sys.exc_info()[1]
- msg = 'Adding filemonitor for %s failed: %s' % (self.data, err)
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
+ core.fam.AddMonitor(self.data, self)
def HandleEvent(self, event):
""" Handle events on Decision files by passing them off to
diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
index 5ec0d7280..8e074118f 100644
--- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py
+++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
@@ -24,7 +24,11 @@ import sys
import pwd
import grp
import Bcfg2.Client.XML
-from Bcfg2.Compat import b64encode, oct_mode
+try:
+ from Bcfg2.Compat import b64encode, oct_mode
+except ImportError:
+ from base64 import b64encode
+ oct_mode = oct
path = "%s"
@@ -67,7 +71,8 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.StructFile(os.path.join(self.data,
'config.xml'),
fam=core.fam,
- should_monitor=True)
+ should_monitor=True,
+ create=self.name)
self.entries = dict()
self.probes = dict()
@@ -225,11 +230,8 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
root = lxml.etree.Element("FileInfo")
root.append(info)
try:
- open(infoxml,
- "w").write(
- lxml.etree.tostring(root,
- xml_declaration=False,
- pretty_print=True).decode('UTF-8'))
+ root.getroottree().write(infoxml, xml_declaration=False,
+ pretty_print=True)
except IOError:
err = sys.exc_info()[1]
self.logger.error("Could not write %s: %s" % (infoxml, err))
diff --git a/src/lib/Bcfg2/Server/Plugins/Fossil.py b/src/lib/Bcfg2/Server/Plugins/Fossil.py
index f6735df12..6165ac651 100644
--- a/src/lib/Bcfg2/Server/Plugins/Fossil.py
+++ b/src/lib/Bcfg2/Server/Plugins/Fossil.py
@@ -20,11 +20,11 @@ class Fossil(Bcfg2.Server.Plugin.Version):
"""Read fossil revision information for the Bcfg2 repository."""
try:
data = Popen("env LC_ALL=C fossil info",
- shell=True,
- cwd=self.vcs_root,
- stdout=PIPE).stdout.readlines()
- revline = [line.split(': ')[1].strip() for line in data if \
- line.split(': ')[0].strip() == 'checkout'][-1]
+ shell=True,
+ cwd=self.vcs_root,
+ stdout=PIPE).stdout.readlines()
+ revline = [line.split(': ')[1].strip() for line in data
+ if line.split(': ')[0].strip() == 'checkout'][-1]
return revline.split(' ')[0]
except IndexError:
msg = "Failed to read fossil info"
diff --git a/src/lib/Bcfg2/Server/Plugins/Git.py b/src/lib/Bcfg2/Server/Plugins/Git.py
index c8362db41..44971aba7 100644
--- a/src/lib/Bcfg2/Server/Plugins/Git.py
+++ b/src/lib/Bcfg2/Server/Plugins/Git.py
@@ -44,7 +44,7 @@ class Git(Version):
else:
cmd = ["git", "--git-dir", self.vcs_path,
"--work-tree", self.vcs_root, "rev-parse", "HEAD"]
- self.debug_log("Git: Running cmd")
+ self.debug_log("Git: Running %s" % cmd)
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
rv, err = proc.communicate()
if proc.wait():
diff --git a/src/lib/Bcfg2/Server/Plugins/GroupLogic.py b/src/lib/Bcfg2/Server/Plugins/GroupLogic.py
new file mode 100644
index 000000000..810b273af
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/GroupLogic.py
@@ -0,0 +1,47 @@
+""" GroupLogic is a connector plugin that lets you use an XML Genshi
+template to dynamically set additional groups for clients. """
+
+import os
+import lxml.etree
+import Bcfg2.Server.Plugin
+try:
+ from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile
+except ImportError:
+ # BundleTemplateFile missing means that genshi is missing. we
+ # import genshi to get the _real_ error
+ import genshi # pylint: disable=W0611
+
+
+class GroupLogicConfig(BundleTemplateFile):
+ """ Representation of the GroupLogic groups.xml file """
+ create = lxml.etree.Element("GroupLogic",
+ nsmap=dict(py="http://genshi.edgewall.org/"))
+
+ def __init__(self, name, fam):
+ BundleTemplateFile.__init__(self, name,
+ Bcfg2.Server.Plugin.Specificity(), None)
+ self.fam = fam
+ self.should_monitor = True
+ self.fam.AddMonitor(self.name, self)
+
+ def _match(self, item, metadata):
+ if item.tag == 'Group' and not len(item.getchildren()):
+ return [item]
+ return BundleTemplateFile._match(self, item, metadata)
+
+
+class GroupLogic(Bcfg2.Server.Plugin.Plugin,
+ Bcfg2.Server.Plugin.Connector):
+ """ GroupLogic is a connector plugin that lets you use an XML
+ Genshi template to dynamically set additional groups for
+ clients. """
+
+ def __init__(self, core, datastore):
+ Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
+ Bcfg2.Server.Plugin.Connector.__init__(self)
+ self.config = GroupLogicConfig(os.path.join(self.data, "groups.xml"),
+ core.fam)
+
+ def get_additional_groups(self, metadata):
+ return [el.get("name")
+ for el in self.config.get_xml_value(metadata).findall("Group")]
diff --git a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
index 5716a134f..09685d972 100644
--- a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
+++ b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
@@ -3,7 +3,6 @@
import os
import re
import sys
-import logging
import Bcfg2.Server.Lint
import Bcfg2.Server.Plugin
from Bcfg2.Utils import PackedDigitRange
@@ -16,16 +15,16 @@ class PatternMap(object):
self.pattern = pattern
self.rangestr = rangestr
self.groups = groups
- if pattern != None:
+ if pattern is not None:
self.re = re.compile(pattern)
self.process = self.process_re
- elif rangestr != None:
+ elif rangestr is not None:
if '\\' in rangestr:
raise Exception("Backslashes are not allowed in NameRanges")
range_finder = r'\[\[[\d\-,]+\]\]'
self.process = self.process_range
- self.re = re.compile('^' + re.sub(range_finder, '(\d+)',
- rangestr))
+ self.re = re.compile(r'^' + re.sub(range_finder, r'(\d+)',
+ rangestr))
dmatcher = re.compile(re.sub(range_finder,
r'\[\[([\d\-,]+)\]\]',
rangestr))
@@ -67,6 +66,7 @@ class PatternMap(object):
class PatternFile(Bcfg2.Server.Plugin.XMLFileBacked):
""" representation of GroupPatterns config.xml """
__identifier__ = None
+ create = 'GroupPatterns'
def __init__(self, filename, core=None):
try:
@@ -77,7 +77,6 @@ class PatternFile(Bcfg2.Server.Plugin.XMLFileBacked):
should_monitor=True)
self.core = core
self.patterns = []
- self.logger = logging.getLogger(self.__class__.__name__)
def Index(self):
Bcfg2.Server.Plugin.XMLFileBacked.Index(self)
@@ -130,7 +129,12 @@ class GroupPatterns(Bcfg2.Server.Plugin.Plugin,
class GroupPatternsLint(Bcfg2.Server.Lint.ServerPlugin):
- """ bcfg2-lint plugin for GroupPatterns """
+ """ ``bcfg2-lint`` plugin to check all given :ref:`GroupPatterns
+ <server-plugins-grouping-grouppatterns>` patterns for validity.
+ This is simply done by trying to create a
+ :class:`Bcfg2.Server.Plugins.GroupPatterns.PatternMap` object for
+ each pattern, and catching exceptions and presenting them as
+ ``bcfg2-lint`` errors."""
def Run(self):
cfg = self.core.plugins['GroupPatterns'].config
diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py
index 8fb3a0998..4ed3dede5 100644
--- a/src/lib/Bcfg2/Server/Plugins/Metadata.py
+++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -40,6 +40,8 @@ if HAS_DJANGO:
""" dict-like object to make it easier to access client bcfg2
versions from the database """
+ create = False
+
def __getitem__(self, key):
try:
return MetadataClientModel.objects.get(hostname=key).version
@@ -75,6 +77,7 @@ if HAS_DJANGO:
yield client.hostname
def keys(self):
+ """ Get keys for the mapping """
return [c.hostname for c in MetadataClientModel.objects.all()]
def __contains__(self, key):
@@ -94,9 +97,11 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked):
# then we immediately set should_monitor to the proper value,
# so that XInclude'd files get properly watched
fpath = os.path.join(metadata.data, basefile)
+ toptag = os.path.splitext(basefile)[0].title()
Bcfg2.Server.Plugin.XMLFileBacked.__init__(self, fpath,
fam=metadata.core.fam,
- should_monitor=False)
+ should_monitor=False,
+ create=toptag)
self.should_monitor = watch_clients
self.metadata = metadata
self.basefile = basefile
@@ -326,6 +331,11 @@ class ClientMetadata(object):
return grp
return ''
+ def __repr__(self):
+ return "%s(%s, profile=%s, groups=%s)" % (self.__class__.__name__,
+ self.hostname,
+ self.profile, self.groups)
+
class MetadataQuery(object):
""" This class provides query methods for the metadata of all
@@ -439,7 +449,7 @@ class MetadataQuery(object):
return [self.by_name(name) for name in self.all_clients()]
-class MetadataGroup(tuple):
+class MetadataGroup(tuple): # pylint: disable=E0012,R0924
""" representation of a metadata group. basically just a named tuple """
# pylint: disable=R0913,W0613
@@ -549,6 +559,12 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
open(os.path.join(repo, cls.name, fname),
"w").write(kwargs[aname])
+ @property
+ def use_database(self):
+ """ Expose self._use_db publicly for use in
+ :class:`Bcfg2.Server.MultiprocessingCore.ChildCore` """
+ return self._use_db
+
def _handle_file(self, fname):
""" set up the necessary magic for handling a metadata file
(clients.xml or groups.xml, e.g.) """
@@ -595,7 +611,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
def _add_xdata(self, config, tag, name, attribs=None, alias=False):
""" Generic method to add XML data (group, client, etc.) """
node = self._search_xdata(tag, name, config.xdata, alias=alias)
- if node != None:
+ if node is not None:
raise Bcfg2.Server.Plugin.MetadataConsistencyError("%s \"%s\" "
"already exists"
% (tag, name))
@@ -655,7 +671,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
def _update_xdata(self, config, tag, name, attribs, alias=False):
""" Generic method to modify XML data (group, client, etc.) """
node = self._search_xdata(tag, name, config.xdata, alias=alias)
- if node == None:
+ if node is None:
self.logger.error("%s \"%s\" does not exist" % (tag, name))
raise Bcfg2.Server.Plugin.MetadataConsistencyError
xdict = config.find_xml_for_xpath('.//%s[@name="%s"]' %
@@ -672,7 +688,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
"""Update a groups attributes."""
if self._use_db:
msg = "Metadata does not support updating groups with " + \
- "use_database enabled"
+ "use_database enabled"
self.logger.error(msg)
raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
else:
@@ -700,7 +716,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
def _remove_xdata(self, config, tag, name):
""" Generic method to remove XML data (group, client, etc.) """
node = self._search_xdata(tag, name, config.xdata)
- if node == None:
+ if node is None:
self.logger.error("%s \"%s\" does not exist" % (tag, name))
raise Bcfg2.Server.Plugin.MetadataConsistencyError
xdict = config.find_xml_for_xpath('.//%s[@name="%s"]' %
@@ -936,16 +952,12 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
if group not in self.groups:
self.debug_log("Client %s set as nonexistent group %s"
% (client, group))
- for gname, ginfo in list(self.groups.items()):
- for group in ginfo.groups:
- if group not in self.groups:
- self.debug_log("Group %s set as nonexistent group %s" %
- (gname, group))
- def set_profile(self, client, profile, addresspair):
+ def set_profile(self, client, profile, # pylint: disable=W0221
+ addresspair, require_public=True):
"""Set group parameter for provided client."""
- self.logger.info("Asserting client %s profile to %s" %
- (client, profile))
+ self.logger.info("Asserting client %s profile to %s" % (client,
+ profile))
if False in list(self.states.values()):
raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not "
"been read yet")
@@ -954,7 +966,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.logger.error(msg)
raise Bcfg2.Server.Plugin.MetadataConsistencyError(msg)
group = self.groups[profile]
- if not group.is_public:
+ if require_public and not group.is_public:
msg = "Cannot set client %s to private group %s" % (client,
profile)
self.logger.error(msg)
@@ -996,19 +1008,18 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.clients_xml.write()
def set_version(self, client, version):
- """Set group parameter for provided client."""
- if client in self.clients:
- if client not in self.versions or version != self.versions[client]:
- self.logger.info("Setting client %s version to %s" %
- (client, version))
- if not self._use_db:
- self.update_client(client, dict(version=version))
- self.clients_xml.write()
- self.versions[client] = version
- else:
- msg = "Cannot set version on non-existent client %s" % client
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.MetadataConsistencyError(msg)
+ """Set version for provided client."""
+ if client not in self.clients:
+ # this creates the client as a side effect
+ self.get_initial_metadata(client)
+
+ if client not in self.versions or version != self.versions[client]:
+ self.logger.info("Setting client %s version to %s" % (client,
+ version))
+ if not self._use_db:
+ self.update_client(client, dict(version=version))
+ self.clients_xml.write()
+ self.versions[client] = version
def resolve_client(self, addresspair, cleanup_cache=False):
"""Lookup address locally or in DNS to get a hostname."""
@@ -1085,7 +1096,6 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not "
"been read yet")
client = client.lower()
-
if client in self.core.metadata_cache:
return self.core.metadata_cache[client]
@@ -1096,6 +1106,29 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
categories = dict()
profile = None
+ def _add_group(grpname):
+ """ Add a group to the set of groups for this client.
+ Handles setting categories and category suppression.
+ Returns the new profile for the client (which might be
+ unchanged). """
+ groups.add(grpname)
+ if grpname in self.groups:
+ group = self.groups[grpname]
+ category = group.category
+ if category:
+ if category in categories:
+ self.logger.warning("%s: Group %s suppressed by "
+ "category %s; %s already a member "
+ "of %s" %
+ (self.name, grpname, category,
+ client, categories[category]))
+ return
+ categories[category] = grpname
+ if not profile and group.is_profile:
+ return grpname
+ else:
+ return profile
+
if client not in self.clients:
pgroup = None
if client in self.clientgroups:
@@ -1104,42 +1137,30 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
pgroup = self.default
if pgroup:
- self.set_profile(client, pgroup, (None, None))
- groups.add(pgroup)
- category = self.groups[pgroup].category
- if category:
- categories[category] = pgroup
- if (pgroup in self.groups and self.groups[pgroup].is_profile):
- profile = pgroup
+ self.set_profile(client, pgroup, (None, None),
+ require_public=False)
+ profile = _add_group(pgroup)
else:
msg = "Cannot add new client %s; no default group set" % client
self.logger.error(msg)
raise Bcfg2.Server.Plugin.MetadataConsistencyError(msg)
- if client in self.clientgroups:
- for cgroup in self.clientgroups[client]:
- if cgroup in groups:
- continue
- if cgroup not in self.groups:
- self.groups[cgroup] = MetadataGroup(cgroup)
- category = self.groups[cgroup].category
- if category and category in categories:
- self.logger.warning("%s: Group %s suppressed by "
- "category %s; %s already a member "
- "of %s" %
- (self.name, cgroup, category,
- client, categories[category]))
- continue
- if category:
- categories[category] = cgroup
- groups.add(cgroup)
- # favor client groups for setting profile
- if not profile and self.groups[cgroup].is_profile:
- profile = cgroup
+ for cgroup in self.clientgroups.get(client, []):
+ if cgroup in groups:
+ continue
+ if cgroup not in self.groups:
+ self.groups[cgroup] = MetadataGroup(cgroup)
+ profile = _add_group(cgroup)
groups, categories = self._merge_groups(client, groups,
categories=categories)
+ if len(groups) == 0 and self.default:
+ # no initial groups; add the default profile
+ profile = _add_group(self.default)
+ groups, categories = self._merge_groups(client, groups,
+ categories=categories)
+
bundles = set()
for group in groups:
try:
@@ -1466,7 +1487,16 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
- """ bcfg2-lint plugin for Metadata """
+ """ ``bcfg2-lint`` plugin for :ref:`Metadata
+ <server-plugins-grouping-metadata>`. This checks for several things:
+
+ * ``<Client>`` tags nested inside other ``<Client>`` tags;
+ * Deprecated options (like ``location="floating"``);
+ * Profiles that don't exist, or that aren't profile groups;
+ * Groups or clients that are defined multiple times;
+ * Multiple default groups or a default group that isn't a profile
+ group.
+ """
def Run(self):
self.nested_clients()
@@ -1475,6 +1505,7 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
self.duplicate_groups()
self.duplicate_default_groups()
self.duplicate_clients()
+ self.default_is_profile()
@classmethod
def Errors(cls):
@@ -1484,11 +1515,15 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
"non-profile-set-as-profile": "error",
"duplicate-group": "error",
"duplicate-client": "error",
- "multiple-default-groups": "error"}
+ "multiple-default-groups": "error",
+ "default-is-not-profile": "error"}
def deprecated_options(self):
- """ check for the location='floating' option, which has been
- deprecated in favor of floating='true' """
+ """ Check for the ``location='floating'`` option, which has
+ been deprecated in favor of ``floating='true'``. """
+ if not hasattr(self.metadata, "clients_xml"):
+ # using metadata database
+ return
clientdata = self.metadata.clients_xml.xdata
for el in clientdata.xpath("//Client"):
loc = el.get("location")
@@ -1503,8 +1538,8 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
(loc, floating, self.RenderXML(el)))
def nested_clients(self):
- """ check for a Client tag inside a Client tag, which doesn't
- make any sense """
+ """ Check for a ``<Client/>`` tag inside a ``<Client/>`` tag,
+ which is either redundant or will never match. """
groupdata = self.metadata.groups_xml.xdata
for el in groupdata.xpath("//Client//Client"):
self.LintError("nested-client-tags",
@@ -1512,8 +1547,11 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
(el.get("name"), self.RenderXML(el)))
def bogus_profiles(self):
- """ check for clients that have profiles that are either not
- flagged as public groups in groups.xml, or don't exist """
+ """ Check for clients that have profiles that are either not
+ flagged as profile groups in ``groups.xml``, or don't exist. """
+ if not hasattr(self.metadata, "clients_xml"):
+ # using metadata database
+ return
for client in self.metadata.clients_xml.xdata.findall('.//Client'):
profile = client.get("profile")
if profile not in self.metadata.groups:
@@ -1528,20 +1566,8 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
(profile, client.get("name"), profile,
self.RenderXML(client)))
- def duplicate_groups(self):
- """ check for groups that are defined twice. We count a group
- tag as a definition if it a) has profile or public set; or b)
- has any children. """
- self.duplicate_entries(
- self.metadata.groups_xml.xdata.xpath("//Groups/Group") + \
- self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"),
- "group",
- include=lambda g: (g.get("profile") or
- g.get("public") or
- g.getchildren()))
-
def duplicate_default_groups(self):
- """ check for multiple default groups """
+ """ Check for multiple default groups. """
defaults = []
for grp in self.metadata.groups_xml.xdata.xpath("//Groups/Group") + \
self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"):
@@ -1553,24 +1579,55 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
"\n".join(defaults))
def duplicate_clients(self):
- """ check for clients that are defined twice. """
+ """ Check for clients that are defined more than once. """
+ if not hasattr(self.metadata, "clients_xml"):
+ # using metadata database
+ return
self.duplicate_entries(
self.metadata.clients_xml.xdata.xpath("//Client"),
"client")
- def duplicate_entries(self, allentries, etype, include=None):
- """ generic duplicate entry finder """
- if include is None:
- include = lambda e: True
+ def duplicate_groups(self):
+ """ Check for groups that are defined more than once. We
+ count a group tag as a definition if it a) has profile or
+ public set; or b) has any children."""
+ allgroups = [
+ g
+ for g in self.metadata.groups_xml.xdata.xpath("//Groups/Group") +
+ self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group")
+ if g.get("profile") or g.get("public") or g.getchildren()]
+ self.duplicate_entries(allgroups, "group")
+
+ def duplicate_entries(self, allentries, etype):
+ """ Generic duplicate entry finder.
+
+ :param allentries: A list of all entries to check for
+ duplicates.
+ :type allentries: list of lxml.etree._Element
+ :param etype: The entry type. This will be used to determine
+ the error name (``duplicate-<etype>``) and for
+ display to the end user.
+ :type etype: string
+ """
entries = dict()
for el in allentries:
- if include(el):
- if el.get("name") in entries:
- entries[el.get("name")].append(self.RenderXML(el))
- else:
- entries[el.get("name")] = [self.RenderXML(el)]
+ if el.get("name") in entries:
+ entries[el.get("name")].append(self.RenderXML(el))
+ else:
+ entries[el.get("name")] = [self.RenderXML(el)]
for ename, els in entries.items():
if len(els) > 1:
self.LintError("duplicate-%s" % etype,
"%s %s is defined multiple times:\n%s" %
(etype.title(), ename, "\n".join(els)))
+
+ def default_is_profile(self):
+ """ Ensure that the default group is a profile group. """
+ if (self.metadata.default and
+ not self.metadata.groups[self.metadata.default].is_profile):
+ xdata = \
+ self.metadata.groups_xml.xdata.xpath("//Group[@name='%s']" %
+ self.metadata.default)[0]
+ self.LintError("default-is-not-profile",
+ "Default group is not a profile group:\n%s" %
+ self.RenderXML(xdata))
diff --git a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
index c39bd4c42..466665382 100644
--- a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
+++ b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py
@@ -5,26 +5,9 @@ import re
import sys
import glob
import socket
-import logging
import Bcfg2.Server
import Bcfg2.Server.Plugin
-LOGGER = logging.getLogger(__name__)
-
-
-class NagiosGenConfig(Bcfg2.Server.Plugin.StructFile):
- """ NagiosGen config file handler """
-
- def __init__(self, filename, fam):
- # 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,
Bcfg2.Server.Plugin.Generator):
@@ -36,8 +19,11 @@ class NagiosGen(Bcfg2.Server.Plugin.Plugin,
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Generator.__init__(self)
- self.config = NagiosGenConfig(os.path.join(self.data, 'config.xml'),
- core.fam)
+ self.config = \
+ Bcfg2.Server.Plugin.StructFile(os.path.join(self.data,
+ 'config.xml'),
+ core.fam, should_monitor=True,
+ create=self.name)
self.Entries = {'Path':
{'/etc/nagiosgen.status': self.createhostconfig,
'/etc/nagios/nagiosgen.cfg': self.createserverconfig}}
diff --git a/src/lib/Bcfg2/Server/Plugins/Ohai.py b/src/lib/Bcfg2/Server/Plugins/Ohai.py
index ebc03197e..1ec3cbd60 100644
--- a/src/lib/Bcfg2/Server/Plugins/Ohai.py
+++ b/src/lib/Bcfg2/Server/Plugins/Ohai.py
@@ -2,8 +2,10 @@
operating system using ohai
(http://wiki.opscode.com/display/chef/Ohai) """
-import lxml.etree
import os
+import sys
+import glob
+import lxml.etree
import Bcfg2.Server.Plugin
try:
@@ -31,22 +33,39 @@ class OhaiCache(object):
self.dirname = dirname
self.cache = dict()
+ def hostpath(self, host):
+ """ Get the path to the file that contains Ohai data for the
+ given host """
+ return os.path.join(self.dirname, "%s.json" % host)
+
def __setitem__(self, item, value):
- if value == None:
+ if value is None:
# simply return if the client returned nothing
return
self.cache[item] = json.loads(value)
- open("%s/%s.json" % (self.dirname, item), 'w').write(value)
+ open(self.hostpath(item), 'w').write(value)
def __getitem__(self, item):
if item not in self.cache:
try:
- data = open("%s/%s.json" % (self.dirname, item)).read()
+ data = open(self.hostpath(item)).read()
except:
raise KeyError(item)
self.cache[item] = json.loads(data)
return self.cache[item]
+ def __delitem__(self, item):
+ if item in self.cache:
+ del self.cache[item]
+ try:
+ os.unlink(self.hostpath(item))
+ except:
+ raise IndexError("Could not unlink %s: %s" % (self.hostpath(item),
+ sys.exc_info()[1]))
+
+ def __len__(self):
+ return len(glob.glob(self.hostpath('*')))
+
def __iter__(self):
data = list(self.cache.keys())
data.extend([x[:-5] for x in os.listdir(self.dirname)])
@@ -69,10 +88,6 @@ class Ohai(Bcfg2.Server.Plugin.Plugin,
self.probe = lxml.etree.Element('probe', name='Ohai', source='Ohai',
interpreter='/bin/sh')
self.probe.text = PROBECODE
- try:
- os.stat(self.data)
- except OSError:
- os.makedirs(self.data)
self.cache = OhaiCache(self.data)
def GetProbes(self, _):
diff --git a/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py b/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py
index 490ee6f20..71128d64c 100644
--- a/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py
+++ b/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py
@@ -9,13 +9,15 @@ class POSIXCompat(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.GoalValidator):
"""POSIXCompat is a goal validator plugin for POSIX entries."""
+ create = False
+
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.GoalValidator.__init__(self)
def validate_goals(self, metadata, goals):
"""Verify that we are generating correct old POSIX entries."""
- if metadata.version_info and metadata.version_info > (1, 3, 0, '', 0):
+ 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
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
index 27f493677..a82a183d8 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
@@ -30,8 +30,8 @@ class AptCollection(Collection):
""" Get an APT configuration file (i.e., ``sources.list``).
:returns: string """
- lines = ["# This config was generated automatically by the Bcfg2 " \
- "Packages plugin", '']
+ lines = ["# This config was generated automatically by the Bcfg2 "
+ "Packages plugin", '']
for source in self:
if source.rawurl:
@@ -40,6 +40,11 @@ class AptCollection(Collection):
else:
lines.append("deb %s %s %s" % (source.url, source.version,
" ".join(source.components)))
+ if source.debsrc:
+ lines.append("deb-src %s %s %s" %
+ (source.url,
+ source.version,
+ " ".join(source.components)))
lines.append("")
return "\n".join(lines)
@@ -93,6 +98,8 @@ class AptSource(Source):
self.logger.error("Packages: Failed to read file %s" % fname)
raise
for line in reader.readlines():
+ if not isinstance(line, str):
+ line = line.decode('utf-8')
words = str(line.strip()).split(':', 1)
if words[0] == 'Package':
pkgname = words[1].strip().rstrip()
@@ -104,8 +111,8 @@ class AptSource(Source):
vindex = 0
for dep in words[1].split(','):
if '|' in dep:
- cdeps = [re.sub('\s+', '',
- re.sub('\(.*\)', '', cdep))
+ cdeps = [re.sub(r'\s+', '',
+ re.sub(r'\(.*\)', '', cdep))
for cdep in dep.split('|')]
dyn_dname = "choice-%s-%s-%s" % (pkgname,
barch,
@@ -114,7 +121,7 @@ class AptSource(Source):
bdeps[barch][pkgname].append(dyn_dname)
bprov[barch][dyn_dname] = set(cdeps)
else:
- raw_dep = re.sub('\(.*\)', '', dep)
+ raw_dep = re.sub(r'\(.*\)', '', dep)
raw_dep = raw_dep.rstrip().strip()
bdeps[barch][pkgname].append(raw_dep)
elif words[0] == 'Provides':
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
index 2735e389a..332f0bbab 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py
@@ -7,6 +7,7 @@ import Bcfg2.Server.Plugin
from Bcfg2.Server.Plugins.Packages.Source import SourceInitError
+# pylint: disable=E0012,R0924
class PackagesSources(Bcfg2.Server.Plugin.StructFile,
Bcfg2.Server.Plugin.Debuggable):
""" PackagesSources handles parsing of the
@@ -16,6 +17,7 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
each ``Source`` tag. """
__identifier__ = None
+ create = "Sources"
def __init__(self, filename, cachepath, fam, packages, setup):
"""
@@ -39,14 +41,8 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
If ``sources.xml`` cannot be read
"""
Bcfg2.Server.Plugin.Debuggable.__init__(self)
- try:
- 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
- self.logger.error(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
+ Bcfg2.Server.Plugin.StructFile.__init__(self, filename, fam=fam,
+ should_monitor=True)
#: The full path to the directory where
#: :class:`Bcfg2.Server.Plugins.Packages.Source.Source` data
@@ -129,7 +125,7 @@ class PackagesSources(Bcfg2.Server.Plugin.StructFile,
""" Create a
:class:`Bcfg2.Server.Plugins.Packages.Source.Source` subclass
object from XML representation of a source in ``sources.xml``.
- ``source_from-xml`` determines the appropriate subclass of
+ ``source_from_xml`` determines the appropriate subclass of
``Source`` to instantiate according to the ``type`` attribute
of the ``Source`` tag.
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
index 985405e65..22073493c 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py
@@ -52,8 +52,8 @@ import re
import sys
import Bcfg2.Server.Plugin
from Bcfg2.Compat import HTTPError, HTTPBasicAuthHandler, \
- HTTPPasswordMgrWithDefaultRealm, install_opener, build_opener, \
- urlopen, cPickle, md5
+ HTTPPasswordMgrWithDefaultRealm, install_opener, build_opener, urlopen, \
+ cPickle, md5
def fetch_url(url):
@@ -65,7 +65,7 @@ def fetch_url(url):
:raises: URLError - Failure fetching URL
:returns: string - the content of the page at the given URL """
if '@' in url:
- mobj = re.match('(\w+://)([^:]+):([^@]+)@(.*)$', url)
+ mobj = re.match(r'(\w+://)([^:]+):([^@]+)@(.*)$', url)
if not mobj:
raise ValueError("Invalid URL")
user = mobj.group(2)
@@ -158,6 +158,10 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
#: this source
self.whitelist = [item.text for item in xsource.findall('Whitelist')]
+ #: Whether or not to include deb-src lines in the generated APT
+ #: configuration
+ self.debsrc = xsource.get('debsrc', 'false') == 'true'
+
#: A dict of repository options that will be included in the
#: configuration generated on the server side (if such is
#: applicable; most backends do not generate any sort of
@@ -315,7 +319,7 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
:raises: OSError - If the saved data cannot be read
:raises: cPickle.UnpicklingError - If the saved data is corrupt """
- data = open(self.cachefile)
+ data = open(self.cachefile, 'rb')
(self.pkgnames, self.deps, self.provides,
self.essentialpkgs) = cPickle.load(data)
@@ -615,7 +619,7 @@ class Source(Bcfg2.Server.Plugin.Debuggable): # pylint: disable=R0902
self.logger.info("Packages: Updating %s" % url)
fname = self.escape_url(url)
try:
- open(fname, 'w').write(fetch_url(url))
+ open(fname, 'wb').write(fetch_url(url))
except ValueError:
self.logger.error("Packages: Bad url string %s" % url)
raise
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
index 6b8ed1f7d..4608bcca5 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py
@@ -66,7 +66,7 @@ from Bcfg2.Compat import StringIO, cPickle, HTTPError, URLError, \
# pylint: enable=W0622
from Bcfg2.Server.Plugins.Packages.Collection import Collection
from Bcfg2.Server.Plugins.Packages.Source import SourceInitError, Source, \
- fetch_url
+ fetch_url
LOGGER = logging.getLogger(__name__)
@@ -281,7 +281,7 @@ class YumCollection(Collection):
#: Define a unique cache file for this collection to use
#: for cached yum metadata
self.cachefile = os.path.join(self.cachepath,
- "cache-%s" % self.cachekey)
+ "cache-%s" % self.cachekey)
if not os.path.exists(self.cachefile):
os.mkdir(self.cachefile)
@@ -422,7 +422,7 @@ class YumCollection(Collection):
config.add_section(reponame)
added = True
except ConfigParser.DuplicateSectionError:
- match = re.search("-(\d+)", reponame)
+ match = re.search(r'-(\d+)', reponame)
if match:
rid = int(match.group(1)) + 1
else:
@@ -675,7 +675,10 @@ class YumCollection(Collection):
gdicts.append(dict(group=group, type=ptype))
if self.use_yum:
- return self.call_helper("get_groups", inputdata=gdicts)
+ try:
+ return self.call_helper("get_groups", inputdata=gdicts)
+ except ValueError:
+ return dict()
else:
pkgs = dict()
for gdict in gdicts:
@@ -838,12 +841,13 @@ class YumCollection(Collection):
return Collection.complete(self, packagelist)
if packagelist:
- result = \
- self.call_helper("complete",
- dict(packages=list(packagelist),
- groups=list(self.get_relevant_groups())))
- if not result:
- # some sort of error, reported by call_helper()
+ try:
+ result = self.call_helper(
+ "complete",
+ dict(packages=list(packagelist),
+ groups=list(self.get_relevant_groups())))
+ except ValueError:
+ # error reported by call_helper()
return set(), packagelist
# json doesn't understand sets or tuples, so we get back a
# lists of lists (packages) and a list of unicode strings
@@ -874,11 +878,16 @@ class YumCollection(Collection):
``bcfg2-yum-helper`` command.
"""
cmd = [self.helper, "-c", self.cfgfile]
- verbose = self.debug_flag or self.setup['verbose']
- if verbose:
+ if self.setup['verbose']:
+ cmd.append("-v")
+ if self.debug_flag:
+ if not self.setup['verbose']:
+ # ensure that running in debug gets -vv, even if
+ # verbose is not enabled
+ cmd.append("-v")
cmd.append("-v")
cmd.append(command)
- self.debug_log("Packages: running %s" % " ".join(cmd), flag=verbose)
+ self.debug_log("Packages: running %s" % " ".join(cmd))
try:
helper = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
except OSError:
@@ -893,19 +902,27 @@ class YumCollection(Collection):
else:
(stdout, stderr) = helper.communicate()
rv = helper.wait()
+ errlines = stderr.splitlines()
if rv:
+ if not errlines:
+ errlines.append("No error output")
self.logger.error("Packages: error running bcfg2-yum-helper "
- "(returned %d): %s" % (rv, stderr))
- else:
+ "(returned %d): %s" % (rv, errlines[0]))
+ for line in errlines[1:]:
+ self.logger.error("Packages: %s" % line)
+ elif errlines:
self.debug_log("Packages: debug info from bcfg2-yum-helper: %s" %
- stderr, flag=verbose)
+ errlines[0])
+ for line in errlines[1:]:
+ self.debug_log("Packages: %s" % line)
+
try:
return json.loads(stdout)
except ValueError:
err = sys.exc_info()[1]
self.logger.error("Packages: error reading bcfg2-yum-helper "
"output: %s" % err)
- return None
+ raise
def setup_data(self, force_update=False):
""" Do any collection-level data setup tasks. This is called
@@ -931,13 +948,21 @@ class YumCollection(Collection):
if force_update:
# we call this twice: one to clean up data from the old
# config, and once to clean up data from the new config
- self.call_helper("clean")
+ try:
+ self.call_helper("clean")
+ except ValueError:
+ # error reported by call_helper
+ pass
os.unlink(self.cfgfile)
self.write_config()
if force_update:
- self.call_helper("clean")
+ try:
+ self.call_helper("clean")
+ except ValueError:
+ # error reported by call_helper
+ pass
class YumSource(Source):
@@ -1120,9 +1145,9 @@ class YumSource(Source):
self.packages['global'] = copy.deepcopy(sdata.pop())
except IndexError:
self.logger.error("Packages: No packages in repo")
+ self.packages['global'] = set()
while sdata:
- self.packages['global'] = \
- self.packages['global'].intersection(sdata.pop())
+ self.packages['global'].update(sdata.pop())
for key in self.packages:
if key == 'global':
@@ -1169,7 +1194,7 @@ class YumSource(Source):
if entry.get('name').startswith('/'):
self.needed_paths.add(entry.get('name'))
pro = pdata.find(RP + 'provides')
- if pro != None:
+ if pro is not None:
for entry in pro.getchildren():
prov = entry.get('name')
if prov not in self.provides[arch]:
@@ -1185,9 +1210,9 @@ class YumSource(Source):
try:
groupid = group.xpath('id')[0].text
self.yumgroups[groupid] = {'mandatory': list(),
- 'default': list(),
- 'optional': list(),
- 'conditional': list()}
+ 'default': list(),
+ 'optional': list(),
+ 'conditional': list()}
except IndexError:
continue
try:
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
index c3eadc6bb..f82b8a392 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py
@@ -9,7 +9,7 @@ import shutil
import lxml.etree
import Bcfg2.Logger
import Bcfg2.Server.Plugin
-from Bcfg2.Compat import ConfigParser, urlopen, HTTPError
+from Bcfg2.Compat import ConfigParser, urlopen, HTTPError, URLError
from Bcfg2.Server.Plugins.Packages.Collection import Collection, \
get_collection_class
from Bcfg2.Server.Plugins.Packages.PackagesSources import PackagesSources
@@ -18,7 +18,8 @@ from Bcfg2.Server.Plugins.Packages.PackagesSources import PackagesSources
YUM_CONFIG_DEFAULT = "/etc/yum.repos.d/bcfg2.repo"
#: The default path for generated apt configs
-APT_CONFIG_DEFAULT = "/etc/apt/sources.d/bcfg2"
+APT_CONFIG_DEFAULT = \
+ "/etc/apt/sources.list.d/bcfg2-packages-generated-sources.list"
class Packages(Bcfg2.Server.Plugin.Plugin,
@@ -184,6 +185,14 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
for (key, value) in list(attrib.items()):
entry.attrib.__setitem__(key, value)
+ def get_config(self, metadata):
+ """ Get yum/apt config, as a string, for the specified client.
+
+ :param metadata: The client to create the config for.
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ """
+ return self.get_collection(metadata).get_config()
+
def HandleEntry(self, entry, metadata):
""" Bind configuration entries. ``HandleEntry`` handles
entries two different ways:
@@ -239,14 +248,14 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
return True
elif entry.tag == 'Path':
# managed entries for yum/apt configs
- if (entry.get("name") == \
- self.core.setup.cfp.get("packages",
- "yum_config",
- default=YUM_CONFIG_DEFAULT) or
- entry.get("name") == \
- self.core.setup.cfp.get("packages",
- "apt_config",
- default=APT_CONFIG_DEFAULT)):
+ if (entry.get("name") ==
+ self.core.setup.cfp.get("packages",
+ "yum_config",
+ default=YUM_CONFIG_DEFAULT) or
+ entry.get("name") ==
+ self.core.setup.cfp.get("packages",
+ "apt_config",
+ default=APT_CONFIG_DEFAULT)):
return True
return False
@@ -450,7 +459,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
try:
open(localfile, 'w').write(urlopen(key).read())
keys.append(key)
- except HTTPError:
+ except (URLError, HTTPError):
err = sys.exc_info()[1]
self.logger.error("Packages: Error downloading %s: %s"
% (key, err))
@@ -518,8 +527,9 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
collection = cclass(metadata, relevant, self.cachepath, self.data,
self.core.fam, debug=self.debug_flag)
ckey = collection.cachekey
- self.clients[metadata.hostname] = ckey
- self.collections[ckey] = collection
+ if cclass != Collection:
+ self.clients[metadata.hostname] = ckey
+ self.collections[ckey] = collection
return collection
def get_additional_data(self, metadata):
@@ -536,7 +546,8 @@ class Packages(Bcfg2.Server.Plugin.Plugin,
:return: dict of lists of ``url_map`` data
"""
collection = self.get_collection(metadata)
- return dict(sources=collection.get_additional_data())
+ return dict(sources=collection.get_additional_data(),
+ get_config=self.get_config)
def end_client_run(self, metadata):
""" Hook to clear the cache for this client in
diff --git a/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py b/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
index 7dac907e1..a1dcb575f 100644
--- a/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
+++ b/src/lib/Bcfg2/Server/Plugins/Pkgmgr.py
@@ -177,7 +177,10 @@ class Pkgmgr(Bcfg2.Server.Plugin.PrioDir):
class PkgmgrLint(Bcfg2.Server.Lint.ServerlessPlugin):
- """ find duplicate Pkgmgr entries with the same priority """
+ """ Find duplicate :ref:`Pkgmgr
+ <server-plugins-generators-pkgmgr>` entries with the same
+ priority. """
+
def Run(self):
pset = set()
for pfile in glob.glob(os.path.join(self.config['repo'], 'Pkgmgr',
@@ -202,12 +205,13 @@ class PkgmgrLint(Bcfg2.Server.Lint.ServerlessPlugin):
# 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))
+ 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"}
+ return {"duplicate-packages": "error"}
diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py
index f106b75a4..0974184b4 100644
--- a/src/lib/Bcfg2/Server/Plugins/Probes.py
+++ b/src/lib/Bcfg2/Server/Plugins/Probes.py
@@ -12,6 +12,7 @@ import Bcfg2.Server.Plugin
try:
from django.db import models
+ from django.core.exceptions import MultipleObjectsReturned
HAS_DJANGO = True
class ProbesDataModel(models.Model,
@@ -58,7 +59,7 @@ class ClientProbeDataSet(dict):
dict.__init__(self, *args, **kwargs)
-class ProbeData(str):
+class ProbeData(str): # pylint: disable=E0012,R0924
""" a ProbeData object emulates a str object, but also has .xdata,
.json, and .yaml properties to provide convenient ways to use
ProbeData objects as XML, JSON, or YAML data """
@@ -111,15 +112,15 @@ class ProbeData(str):
class ProbeSet(Bcfg2.Server.Plugin.EntrySet):
""" Handle universal and group- and host-specific probe files """
- ignore = re.compile("^(\.#.*|.*~|\\..*\\.(tmp|sw[px])|probed\\.xml)$")
+ ignore = re.compile(r'^(\.#.*|.*~|\..*\.(tmp|sw[px])|probed\.xml)$')
probename = \
- re.compile("(.*/)?(?P<basename>\S+?)(\.(?P<mode>(?:G\d\d)|H)_\S+)?$")
- bangline = re.compile('^#!\s*(?P<interpreter>.*)$')
+ re.compile(r'(.*/)?(?P<basename>\S+?)(\.(?P<mode>(?:G\d\d)|H)_\S+)?$')
+ bangline = re.compile(r'^#!\s*(?P<interpreter>.*)$')
basename_is_regex = True
def __init__(self, path, fam, encoding, plugin_name):
self.plugin_name = plugin_name
- Bcfg2.Server.Plugin.EntrySet.__init__(self, '[0-9A-Za-z_\-]+', path,
+ Bcfg2.Server.Plugin.EntrySet.__init__(self, r'[0-9A-Za-z_\-]+', path,
Bcfg2.Server.Plugin.SpecificData,
encoding)
fam.AddMonitor(path, self)
@@ -153,7 +154,20 @@ class ProbeSet(Bcfg2.Server.Plugin.EntrySet):
probe = lxml.etree.Element('probe')
probe.set('name', os.path.basename(name))
probe.set('source', self.plugin_name)
- probe.text = entry.data
+ if (metadata.version_info and
+ metadata.version_info > (1, 3, 1, '', 0)):
+ try:
+ probe.text = entry.data.decode('utf-8')
+ except AttributeError:
+ probe.text = entry.data
+ else:
+ try:
+ probe.text = entry.data
+ except: # pylint: disable=W0702
+ self.logger.error("Client unable to handle unicode "
+ "probes. Skipping %s" %
+ probe.get('name'))
+ continue
match = self.bangline.match(entry.data.split('\n')[0])
if match:
probe.set('interpreter', match.group('interpreter'))
@@ -209,15 +223,15 @@ class Probes(Bcfg2.Server.Plugin.Probing,
lxml.etree.SubElement(top, 'Client', name=client,
timestamp=str(int(probedata.timestamp)))
for probe in sorted(probedata):
- lxml.etree.SubElement(ctag, 'Probe', name=probe,
- value=str(self.probedata[client][probe]))
+ lxml.etree.SubElement(
+ ctag, 'Probe', name=probe,
+ value=self.probedata[client][probe])
for group in sorted(self.cgroups[client]):
lxml.etree.SubElement(ctag, "Group", name=group)
try:
- datafile = open(os.path.join(self.data, 'probed.xml'), 'w')
- datafile.write(lxml.etree.tostring(
- top, xml_declaration=False,
- pretty_print='true').decode('UTF-8'))
+ top.getroottree().write(os.path.join(self.data, 'probed.xml'),
+ xml_declaration=False,
+ pretty_print='true')
except IOError:
err = sys.exc_info()[1]
self.logger.error("Failed to write probed.xml: %s" % err)
@@ -232,21 +246,25 @@ class Probes(Bcfg2.Server.Plugin.Probing,
if pdata.data != data:
pdata.data = data
pdata.save()
+
ProbesDataModel.objects.filter(
hostname=client.hostname).exclude(
- probe__in=self.probedata[client.hostname]).delete()
+ probe__in=self.probedata[client.hostname]).delete()
for group in self.cgroups[client.hostname]:
try:
- ProbesGroupsModel.objects.get(hostname=client.hostname,
- group=group)
- except ProbesGroupsModel.DoesNotExist:
- grp = ProbesGroupsModel(hostname=client.hostname,
- group=group)
- grp.save()
+ ProbesGroupsModel.objects.get_or_create(
+ hostname=client.hostname,
+ group=group)
+ except MultipleObjectsReturned:
+ ProbesGroupsModel.objects.filter(hostname=client.hostname,
+ group=group).delete()
+ ProbesGroupsModel.objects.get_or_create(
+ hostname=client.hostname,
+ group=group)
ProbesGroupsModel.objects.filter(
hostname=client.hostname).exclude(
- group__in=self.cgroups[client.hostname]).delete()
+ group__in=self.cgroups[client.hostname]).delete()
def load_data(self):
""" Load probe data from the appropriate backend (probed.xml
@@ -320,7 +338,7 @@ class Probes(Bcfg2.Server.Plugin.Probing,
def ReceiveDataItem(self, client, data, cgroups, cprobedata):
"""Receive probe results pertaining to client."""
- if data.text == None:
+ if data.text is None:
self.logger.info("Got null response to probe %s from %s" %
(data.get('name'), client.hostname))
cprobedata[data.get('name')] = ProbeData('')
diff --git a/src/lib/Bcfg2/Server/Plugins/Properties.py b/src/lib/Bcfg2/Server/Plugins/Properties.py
index 3ebad40e3..e97f66675 100644
--- a/src/lib/Bcfg2/Server/Plugins/Properties.py
+++ b/src/lib/Bcfg2/Server/Plugins/Properties.py
@@ -266,8 +266,13 @@ class XMLPropertyFile(Bcfg2.Server.Plugin.StructFile, PropertyFile):
return repr(self.xdata)
-class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
- """ A collection of properties files. """
+class Properties(Bcfg2.Server.Plugin.Plugin,
+ Bcfg2.Server.Plugin.Connector,
+ Bcfg2.Server.Plugin.DirectoryBacked):
+ """ The properties plugin maps property files into client metadata
+ instances. """
+
+ #: Extensions that are understood by Properties.
extensions = ["xml"]
if HAS_JSON:
extensions.append("json")
@@ -284,14 +289,18 @@ class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
#: Ignore XML schema (``.xsd``) files
ignore = re.compile(r'.*\.xsd$')
- def __init__(self, data, fam):
- Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, data, fam)
+ def __init__(self, core, datastore):
+ global SETUP # pylint: disable=W0603
+ Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
+ Bcfg2.Server.Plugin.Connector.__init__(self)
+ Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, core.fam)
+ SETUP = core.setup
#: Instead of creating children of this object with a static
#: object, we use :func:`property_dispatcher` to create a
#: child of the appropriate subclass of :class:`PropertyFile`
self.__child__ = self.property_dispatcher
- __init__.__doc__ = Bcfg2.Server.Plugin.DirectoryBacked.__init__.__doc__
+ __init__.__doc__ = Bcfg2.Server.Plugin.Plugin.__init__.__doc__
def property_dispatcher(self, fname, fam):
""" Dispatch an event on a Properties file to the
@@ -314,30 +323,9 @@ class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
raise Bcfg2.Server.Plugin.PluginExecutionError(
"Properties: Unknown extension %s" % fname)
-
-class Properties(Bcfg2.Server.Plugin.Plugin,
- Bcfg2.Server.Plugin.Connector):
- """ The properties plugin maps property files into client metadata
- instances. """
-
- def __init__(self, core, datastore):
- global SETUP # pylint: disable=W0603
- Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
- Bcfg2.Server.Plugin.Connector.__init__(self)
- SETUP = core.setup
- try:
- self.store = PropDirectoryBacked(self.data, core.fam)
- except OSError:
- err = sys.exc_info()[1]
- self.logger.error("Error while creating Properties store: %s" %
- err)
- raise Bcfg2.Server.Plugin.PluginInitError
-
- __init__.__doc__ = Bcfg2.Server.Plugin.Plugin.__init__.__doc__
-
def get_additional_data(self, metadata):
rv = dict()
- for fname, pfile in self.store.entries.items():
+ for fname, pfile in self.entries.items():
rv[fname] = pfile.get_additional_data(metadata)
return rv
get_additional_data.__doc__ = \
diff --git a/src/lib/Bcfg2/Server/Plugins/Reporting.py b/src/lib/Bcfg2/Server/Plugins/Reporting.py
index a6dc2c1ef..3354763d4 100644
--- a/src/lib/Bcfg2/Server/Plugins/Reporting.py
+++ b/src/lib/Bcfg2/Server/Plugins/Reporting.py
@@ -92,10 +92,11 @@ class Reporting(Statistics, Threaded, PullSource, Debuggable):
# try 3 times to store the data
for i in [1, 2, 3]:
try:
- self.transport.store(client.hostname, cdata,
- lxml.etree.tostring(
+ self.transport.store(
+ client.hostname, cdata,
+ lxml.etree.tostring(
stats,
- xml_declaration=False).decode('UTF-8'))
+ xml_declaration=False))
self.debug_log("%s: Queued statistics data for %s" %
(self.__class__.__name__, client.hostname))
return
diff --git a/src/lib/Bcfg2/Server/Plugins/SSHbase.py b/src/lib/Bcfg2/Server/Plugins/SSHbase.py
index c7db67301..d8b3104b7 100644
--- a/src/lib/Bcfg2/Server/Plugins/SSHbase.py
+++ b/src/lib/Bcfg2/Server/Plugins/SSHbase.py
@@ -172,7 +172,7 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin,
for name in names[cmeta.hostname]:
newnames.add(name.split('.')[0])
try:
- newips.add(self.get_ipcache_entry(name)[0])
+ newips.update(self.get_ipcache_entry(name)[0])
except: # pylint: disable=W0702
continue
names[cmeta.hostname].update(newnames)
@@ -201,10 +201,11 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin,
if specific.hostname and specific.hostname in names:
hostnames = names[specific.hostname]
elif specific.group:
- hostnames = list(chain(
+ hostnames = list(
+ chain(
*[names[cmeta.hostname]
- for cmeta in \
- mquery.by_groups([specific.group])]))
+ for cmeta in
+ mquery.by_groups([specific.group])]))
elif specific.all:
# a generic key for all hosts? really?
hostnames = list(chain(*list(names.values())))
@@ -287,7 +288,8 @@ class SSHbase(Bcfg2.Server.Plugin.Plugin,
else:
# need to add entry
try:
- ipaddr = socket.gethostbyname(client)
+ ipaddr = set([info[4][0]
+ for info in socket.getaddrinfo(client, None)])
self.ipcache[client] = (ipaddr, client)
return (ipaddr, client)
except socket.gaierror:
diff --git a/src/lib/Bcfg2/Server/Plugins/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
index 7d00201da..f111ffc60 100644
--- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py
+++ b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
@@ -68,7 +68,7 @@ class SSLCACertSpec(SSLCAXMLSpec):
def get_spec(self, metadata):
rv = SSLCAXMLSpec.get_spec(self, metadata)
rv['subjectaltname'] = [e.text for e in self.Match(metadata)
- if e.tag == "SubjectAltName"]
+ if e.tag == "subjectAltName"]
return rv
diff --git a/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
index 0aea439f9..41e6bf8b5 100644
--- a/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
+++ b/src/lib/Bcfg2/Server/Plugins/ServiceCompat.py
@@ -6,7 +6,9 @@ import Bcfg2.Server.Plugin
class ServiceCompat(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.GoalValidator):
""" Use old-style service modes for older clients """
- name = 'ServiceCompat'
+
+ create = False
+
__author__ = 'bcfg-dev@mcs.anl.gov'
mode_map = {('true', 'true'): 'default',
('interactive', 'true'): 'interactive_only',
@@ -14,7 +16,7 @@ class ServiceCompat(Bcfg2.Server.Plugin.Plugin,
def validate_goals(self, metadata, config):
""" Apply defaults """
- if metadata.version_info and metadata.version_info > (1, 3, 0, '', 0):
+ 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
diff --git a/src/lib/Bcfg2/Server/Plugins/Svn.py b/src/lib/Bcfg2/Server/Plugins/Svn.py
index 51f44c52d..240fd7f89 100644
--- a/src/lib/Bcfg2/Server/Plugins/Svn.py
+++ b/src/lib/Bcfg2/Server/Plugins/Svn.py
@@ -59,9 +59,48 @@ class Svn(Bcfg2.Server.Plugin.Version):
self.client.callback_conflict_resolver = \
self.get_conflict_resolver(choice)
+ try:
+ if self.core.setup.cfp.get(
+ "svn",
+ "always_trust").lower() == "true":
+ self.client.callback_ssl_server_trust_prompt = \
+ self.ssl_server_trust_prompt
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ self.logger.debug("Svn: Using subversion cache for SSL "
+ "certificate trust")
+
+ try:
+ if (self.core.setup.cfp.get("svn", "user") and
+ self.core.setup.cfp.get("svn", "password")):
+ self.client.callback_get_login = \
+ self.get_login
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ self.logger.info("Svn: Using subversion cache for "
+ "password-based authetication")
+
self.logger.debug("Svn: Initialized svn plugin with SVN directory %s" %
self.vcs_path)
+ # pylint: disable=W0613
+ def get_login(self, realm, username, may_save):
+ """ PySvn callback to get credentials for HTTP basic authentication """
+ self.logger.debug("Svn: Logging in with username: %s" %
+ self.core.setup.cfp.get("svn", "user"))
+ return True, \
+ self.core.setup.cfp.get("svn", "user"), \
+ self.core.setup.cfp.get("svn", "password"), \
+ False
+ # pylint: enable=W0613
+
+ def ssl_server_trust_prompt(self, trust_dict):
+ """ PySvn callback to always trust SSL certificates from SVN server """
+ self.logger.debug("Svn: Trusting SSL certificate from %s, "
+ "issued by %s for realm %s" %
+ (trust_dict['hostname'],
+ trust_dict['issuer_dname'],
+ trust_dict['realm']))
+ return True, trust_dict['failures'], False
+
def get_conflict_resolver(self, choice):
""" Get a PySvn conflict resolution callback """
def callback(conflict_description):
diff --git a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
index ea7454e11..db7370f01 100644
--- a/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
+++ b/src/lib/Bcfg2/Server/Plugins/TemplateHelper.py
@@ -82,7 +82,7 @@ class TemplateHelper(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.DirectoryBacked):
""" A plugin to provide helper classes and functions to templates """
__author__ = 'chris.a.st.pierre@gmail.com'
- ignore = re.compile("^(\.#.*|.*~|\\..*\\.(sw[px])|.*\.py[co])$")
+ ignore = re.compile(r'^(\.#.*|.*~|\..*\.(sw[px])|.*\.py[co])$')
patterns = MODULE_RE
__child__ = HelperModule
@@ -97,18 +97,33 @@ class TemplateHelper(Bcfg2.Server.Plugin.Plugin,
class TemplateHelperLint(Bcfg2.Server.Lint.ServerPlugin):
- """ find duplicate Pkgmgr entries with the same priority """
+ """ ``bcfg2-lint`` plugin to ensure that all :ref:`TemplateHelper
+ <server-plugins-connectors-templatehelper>` modules are valid.
+ This can check for:
+
+ * A TemplateHelper module that cannot be imported due to syntax or
+ other compile-time errors;
+ * A TemplateHelper module that does not have an ``__export__``
+ attribute, or whose ``__export__`` is not a list;
+ * Bogus symbols listed in ``__export__``, including symbols that
+ don't exist, that are reserved, or that start with underscores.
+ """
+
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
self.reserved_keywords = dir(HelperModule("foo.py"))
def Run(self):
for helper in self.core.plugins['TemplateHelper'].entries.values():
- if self.HandlesFile(helper):
+ if self.HandlesFile(helper.name):
self.check_helper(helper.name)
def check_helper(self, helper):
- """ check a helper module for export errors """
+ """ Check a single helper module.
+
+ :param helper: The filename of the helper module
+ :type helper: string
+ """
module_name = MODULE_RE.search(helper).group(1)
try:
diff --git a/src/lib/Bcfg2/Server/Plugins/__init__.py b/src/lib/Bcfg2/Server/Plugins/__init__.py
index b33eeba28..ad51cf368 100644
--- a/src/lib/Bcfg2/Server/Plugins/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/__init__.py
@@ -1,32 +1,5 @@
"""Imports for Bcfg2.Server.Plugins."""
-__all__ = [
- 'Account',
- 'Base',
- 'Bundler',
- 'Bzr',
- 'Cfg',
- 'Cvs',
- 'Darcs',
- 'Decisions',
- 'Fossil',
- 'Git',
- 'GroupPatterns',
- 'Hg',
- 'Hostbase',
- 'Metadata',
- 'NagiosGen',
- 'Ohai',
- 'Packages',
- 'Properties',
- 'Probes',
- 'Pkgmgr',
- 'Rules',
- 'SSHbase',
- 'Snapshots',
- 'Statistics',
- 'Svn',
- 'TCheetah',
- 'Trigger',
- 'TGenshi',
- ]
+from Bcfg2.Compat import walk_packages
+
+__all__ = [m[1] for m in walk_packages(path=__path__)]
diff --git a/src/lib/Bcfg2/Server/__init__.py b/src/lib/Bcfg2/Server/__init__.py
index 3eb300a98..0678e4579 100644
--- a/src/lib/Bcfg2/Server/__init__.py
+++ b/src/lib/Bcfg2/Server/__init__.py
@@ -1,10 +1,9 @@
"""This is the set of modules for Bcfg2.Server."""
import lxml.etree
+from Bcfg2.Compat import walk_packages
-__all__ = ["Admin", "Core", "FileMonitor", "Plugin", "Plugins",
- "Hostbase", "Reports", "Snapshots", "XMLParser",
- "XI", "XI_NAMESPACE"]
+__all__ = [m[1] for m in walk_packages(path=__path__)]
XI = 'http://www.w3.org/2001/XInclude'
XI_NAMESPACE = '{%s}' % XI
diff --git a/src/lib/Bcfg2/Statistics.py b/src/lib/Bcfg2/Statistics.py
index a869b03cd..3825941af 100644
--- a/src/lib/Bcfg2/Statistics.py
+++ b/src/lib/Bcfg2/Statistics.py
@@ -28,10 +28,10 @@ class Statistic(object):
:param value: The value to add to this statistic
:type value: int or float
"""
- self.min = min(self.min, value)
- self.max = max(self.max, value)
- self.ave = (((self.ave * (self.count - 1)) + value) / self.count)
+ self.min = min(self.min, float(value))
+ self.max = max(self.max, float(value))
self.count += 1
+ self.ave = (((self.ave * (self.count - 1)) + value) / self.count)
def get_value(self):
""" Get a tuple of all the stats tracked on this named item.
@@ -46,6 +46,11 @@ class Statistic(object):
"""
return (self.name, (self.min, self.max, self.ave, self.count))
+ def __repr__(self):
+ return "%s(%s, (min=%s, avg=%s, max=%s, count=%s))" % (
+ self.__class__.__name__,
+ self.name, self.min, self.ave, self.max, self.count)
+
class Statistics(object):
""" A collection of named :class:`Statistic` objects. """
diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py
index 33da8bd71..9f46582c4 100644
--- a/src/lib/Bcfg2/Utils.py
+++ b/src/lib/Bcfg2/Utils.py
@@ -2,6 +2,7 @@
used by both client and server. Stuff that doesn't fit anywhere
else. """
+import shlex
import fcntl
import logging
import threading
@@ -22,7 +23,7 @@ class ClassName(object):
return owner.__name__
-class PackedDigitRange(object):
+class PackedDigitRange(object): # pylint: disable=E0012,R0924
""" Representation of a set of integer ranges. A range is
described by a comma-delimited string of integers and ranges,
e.g.::
@@ -80,9 +81,6 @@ class PackedDigitRange(object):
def __str__(self):
return "[%s]" % self.str
- def __len__(self):
- return sum(r[1] - r[0] + 1 for r in self.ranges) + len(self.ints)
-
def locked(fd):
""" Acquire a lock on a file.
@@ -108,10 +106,16 @@ class ExecutorResult(object):
def __init__(self, stdout, stderr, retval):
#: The output of the command
- self.stdout = stdout
+ if isinstance(stdout, str):
+ self.stdout = stdout
+ else:
+ self.stdout = stdout.decode('utf-8')
#: The error produced by the command
- self.stderr = stderr
+ if isinstance(stdout, str):
+ self.stderr = stderr
+ else:
+ self.stderr = stderr.decode('utf-8')
#: The return value of the command.
self.retval = retval
@@ -145,6 +149,19 @@ class ExecutorResult(object):
returned a tuple of (return value, stdout split by lines). """
return (self.retval, self.stdout.splitlines())[idx]
+ def __len__(self):
+ """ This provides compatibility with the old Executor, which
+ returned a tuple of (return value, stdout split by lines). """
+ return 2
+
+ def __delitem__(self, _):
+ raise TypeError("'%s' object doesn't support item deletion" %
+ self.__class__.__name__)
+
+ def __setitem__(self, idx, val):
+ raise TypeError("'%s' object does not support item assignment" %
+ self.__class__.__name__)
+
def __nonzero__(self):
return self.__bool__()
@@ -172,7 +189,7 @@ class Executor(object):
:param proc: The process to kill upon timeout.
:type proc: subprocess.Popen
:returns: None """
- if proc.poll() == None:
+ if proc.poll() is None:
try:
proc.kill()
self.logger.warning("Process exceeeded timeout, killing")
@@ -197,8 +214,9 @@ class Executor(object):
:type timeout: float
:returns: :class:`Bcfg2.Utils.ExecutorResult`
"""
- if isinstance(command, basestring):
+ if isinstance(command, str):
cmdstr = command
+ command = shlex.split(cmdstr)
else:
cmdstr = " ".join(command)
self.logger.debug("Running: %s" % cmdstr)
@@ -221,6 +239,13 @@ class Executor(object):
for line in inputdata.splitlines():
self.logger.debug('> %s' % line)
(stdout, stderr) = proc.communicate(input=inputdata)
+
+ # py3k fixes
+ if not isinstance(stdout, str):
+ stdout = stdout.decode('utf-8')
+ if not isinstance(stderr, str):
+ stderr = stderr.decode('utf-8')
+
for line in stdout.splitlines(): # pylint: disable=E1103
self.logger.debug('< %s' % line)
for line in stderr.splitlines(): # pylint: disable=E1103
diff --git a/src/lib/Bcfg2/__init__.py b/src/lib/Bcfg2/__init__.py
index 3fe2a0d75..74a871f2a 100644
--- a/src/lib/Bcfg2/__init__.py
+++ b/src/lib/Bcfg2/__init__.py
@@ -1,3 +1 @@
"""Base modules definition."""
-
-__all__ = ['Server', 'Client', 'Logger', 'Options', 'Proxy', 'Statistics']
diff --git a/src/lib/Bcfg2/settings.py b/src/lib/Bcfg2/settings.py
index 7d405f868..9adfd66bf 100644
--- a/src/lib/Bcfg2/settings.py
+++ b/src/lib/Bcfg2/settings.py
@@ -32,6 +32,8 @@ TIME_ZONE = None
DEBUG = False
TEMPLATE_DEBUG = DEBUG
+ALLOWED_HOSTS = ['*']
+
MEDIA_URL = '/site_media/'
@@ -54,11 +56,11 @@ DEFAULT_CONFIG = _default_config()
def read_config(cfile=DEFAULT_CONFIG, repo=None, quiet=False):
""" read the config file and set django settings based on it """
- # pylint: disable=W0603
+ # pylint: disable=W0602,W0603
global DATABASE_ENGINE, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, \
DATABASE_HOST, DATABASE_PORT, DEBUG, TEMPLATE_DEBUG, TIME_ZONE, \
MEDIA_URL
- # pylint: enable=W0603
+ # pylint: enable=W0602,W0603
if not os.path.exists(cfile) and os.path.exists(DEFAULT_CONFIG):
print("%s does not exist, using %s for database configuration" %
diff --git a/src/lib/Bcfg2/version.py b/src/lib/Bcfg2/version.py
index 6f3ba3e49..12fc584fe 100644
--- a/src/lib/Bcfg2/version.py
+++ b/src/lib/Bcfg2/version.py
@@ -5,7 +5,7 @@ import re
__version__ = "1.3.1"
-class Bcfg2VersionInfo(tuple):
+class Bcfg2VersionInfo(tuple): # pylint: disable=E0012,R0924
""" object to make granular version operations (particularly
comparisons) easier """
diff --git a/src/sbin/bcfg2-admin b/src/sbin/bcfg2-admin
index 31e49c00b..14d188342 100755
--- a/src/sbin/bcfg2-admin
+++ b/src/sbin/bcfg2-admin
@@ -83,7 +83,7 @@ def main():
raise SystemExit(1)
mode = mode_cls(setup)
try:
- mode(setup['args'][1:])
+ return mode(setup['args'][1:])
finally:
mode.shutdown()
else:
@@ -93,6 +93,6 @@ def main():
if __name__ == '__main__':
try:
- main()
+ sys.exit(main())
except KeyboardInterrupt:
raise SystemExit(1)
diff --git a/src/sbin/bcfg2-build-reports b/src/sbin/bcfg2-build-reports
index 27e7c2475..1c9e9ad97 100755
--- a/src/sbin/bcfg2-build-reports
+++ b/src/sbin/bcfg2-build-reports
@@ -13,7 +13,7 @@ import sys
from time import asctime, strptime
from lxml.etree import XML, XSLT, parse, Element, ElementTree, SubElement, tostring, XMLSyntaxError
# Compatibility imports
-from Bcfg2.Compat import ConfigParser
+from Bcfg2.Compat import ConfigParser, cmp
def generatereport(rspec, nrpt):
"""
diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt
index eae316da5..aad89882f 100755
--- a/src/sbin/bcfg2-crypt
+++ b/src/sbin/bcfg2-crypt
@@ -161,8 +161,8 @@ class Encryptor(object):
continue
except TypeError:
pchunk = None
- for pname, passphrase in \
- Bcfg2.Encryption.get_passphrases(self.setup).items():
+ passphrases = Bcfg2.Encryption.get_passphrases(self.setup)
+ for pname, passphrase in passphrases.items():
self.logger.debug("Trying passphrase %s" % pname)
try:
pchunk = self._decrypt(chunk, passphrase)
@@ -341,7 +341,7 @@ class PropertiesEncryptor(Encryptor):
# actually need to unchunk anything
xdata = data[0]
# find root element
- while xdata.getparent() != None:
+ while xdata.getparent() is not None:
xdata = xdata.getparent()
return lxml.etree.tostring(xdata,
xml_declaration=False,
diff --git a/src/sbin/bcfg2-info b/src/sbin/bcfg2-info
index cfcc95be2..133e1ccb3 100755
--- a/src/sbin/bcfg2-info
+++ b/src/sbin/bcfg2-info
@@ -120,7 +120,6 @@ class InfoCore(cmd.Cmd, Bcfg2.Server.Core.BaseCore):
Bcfg2.Server.Core.BaseCore.__init__(self, setup=setup)
self.prompt = '> '
self.cont = True
- self.fam.handle_events_in_interval(4)
def _get_client_list(self, hostglobs):
""" given a host glob, get a list of clients that match it """
@@ -232,10 +231,14 @@ class InfoCore(cmd.Cmd, Bcfg2.Server.Core.BaseCore):
print("Refusing to write files outside of /tmp without -f "
"option")
return
- lxml.etree.ElementTree(self.BuildConfiguration(client)).write(
- ofile,
- encoding='UTF-8', xml_declaration=True,
- pretty_print=True)
+ try:
+ lxml.etree.ElementTree(self.BuildConfiguration(client)).write(
+ ofile,
+ encoding='UTF-8', xml_declaration=True,
+ pretty_print=True)
+ except IOError:
+ err = sys.exc_info()[1]
+ print("Failed to write File %s: %s" % (ofile, err))
else:
print(self._get_usage(self.do_build))
@@ -380,7 +383,7 @@ Bcfg2 client itself.""")
xml_declaration=False).decode('UTF-8')
except Exception:
print("Failed to build entry %s for host %s: %s" %
- (fname, client, traceback.format_exc().splitlines()[-1]))
+ (fname, client, traceback.format_exc().splitlines()[-1]))
raise
try:
if outfile:
@@ -458,9 +461,7 @@ Bcfg2 client itself.""")
def do_clients(self, _):
""" clients - Print out client/profile info """
data = [('Client', 'Profile')]
- clist = self.metadata.clients
- clist.sort()
- for client in clist:
+ for client in sorted(self.metadata.list_clients()):
imd = self.metadata.get_initial_metadata(client)
data.append((client, imd.profile))
print_tabular(data)
@@ -468,19 +469,17 @@ Bcfg2 client itself.""")
def do_config(self, _):
""" config - Print out the current configuration of Bcfg2"""
output = [
- ('Description', 'Value'),
- ('Path Bcfg2 repository', self.setup['repo']),
- ('Plugins', self.setup['plugins']),
- ('Password', self.setup['password']),
- ('Server Metadata Connector', self.setup['mconnect']),
- ('Filemonitor', self.setup['filemonitor']),
- ('Server address', self.setup['location']),
- ('Path to key', self.setup['key']),
- ('Path to SSL certificate', self.setup['cert']),
- ('Path to SSL CA certificate', self.setup['ca']),
- ('Protocol', self.setup['protocol']),
- ('Logging', self.setup['logging'])
- ]
+ ('Description', 'Value'),
+ ('Path Bcfg2 repository', self.setup['repo']),
+ ('Plugins', self.setup['plugins']),
+ ('Password', self.setup['password']),
+ ('Filemonitor', self.setup['filemonitor']),
+ ('Server address', self.setup['location']),
+ ('Path to key', self.setup['key']),
+ ('Path to SSL certificate', self.setup['cert']),
+ ('Path to SSL CA certificate', self.setup['ca']),
+ ('Protocol', self.setup['protocol']),
+ ('Logging', self.setup['logging'])]
print_tabular(output)
def do_probes(self, args):
@@ -607,7 +606,7 @@ Bcfg2 client itself.""")
# Dump all mappings unless type specified
data = [('Plugin', 'Type', 'Name')]
arglen = len(args.split())
- for generator in self.generators:
+ for generator in self.plugins_by_type(Bcfg2.Server.Plugin.Generator):
if arglen == 0:
etypes = list(generator.Entries.keys())
else:
@@ -631,30 +630,34 @@ Bcfg2 client itself.""")
self.fam.debug = True
def do_packageresolve(self, args):
- """ packageresolve <hostname> <package> [<package>...] -
- Resolve the specified set of packages """
+ """ packageresolve <hostname> [<package> [<package>...]] -
+ Resolve packages for the given host, optionally specifying a
+ set of packages """
arglist = args.split(" ")
- if len(arglist) < 2:
+ if len(arglist) < 1:
print(self._get_usage(self.do_packageresolve))
return
- if 'Packages' not in self.plugins:
+ try:
+ pkgs = self.plugins['Packages']
+ except KeyError:
print("Packages plugin not enabled")
return
- self.plugins['Packages'].toggle_debug()
-
- indep = lxml.etree.Element("Independent")
- structures = [lxml.etree.Element("Bundle", name="packages")]
- for arg in arglist[1:]:
- lxml.etree.SubElement(structures[0], "Package", name=arg)
+ pkgs.toggle_debug()
hostname = arglist[0]
metadata = self.build_metadata(hostname)
- # pylint: disable=W0212
- self.plugins['Packages']._build_packages(metadata, indep, structures)
- # pylint: enable=W0212
+ indep = lxml.etree.Element("Independent")
+ if len(arglist) > 1:
+ structures = [lxml.etree.Element("Bundle", name="packages")]
+ for arg in arglist[1:]:
+ lxml.etree.SubElement(structures[0], "Package", name=arg)
+ else:
+ structures = self.GetStructures(metadata)
+ pkgs._build_packages(metadata, indep, # pylint: disable=W0212
+ structures)
print("%d new packages added" % len(indep.getchildren()))
if len(indep.getchildren()):
print(" %s" % "\n ".join(lxml.etree.tostring(p)
@@ -713,6 +716,8 @@ Bcfg2 client itself.""")
def run(self, args): # pylint: disable=W0221
try:
+ self.load_plugins()
+ self.fam.handle_events_in_interval(1)
if args:
self.onecmd(" ".join(args))
else:
@@ -739,7 +744,7 @@ def build_usage():
# shim for python 2.4, __func__ is im_func
funcattr = getattr(attr, "__func__", getattr(attr, "im_func", None))
- if (funcattr != None and
+ if (funcattr is not None and
funcattr.func_name not in cmd_blacklist and
funcattr.func_name.startswith("do_") and
funcattr.func_doc):
@@ -754,8 +759,7 @@ def main():
optinfo = dict(profile=Bcfg2.Options.CORE_PROFILE,
interactive=Bcfg2.Options.INTERACTIVE,
interpreter=Bcfg2.Options.INTERPRETER)
- optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
- optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
+ optinfo.update(Bcfg2.Options.INFO_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
setup.hm = "\n".join([" bcfg2-info [options] [command <command args>]",
"Options:",
diff --git a/src/sbin/bcfg2-lint b/src/sbin/bcfg2-lint
index 430c4c54f..ab3b6450f 100755
--- a/src/sbin/bcfg2-lint
+++ b/src/sbin/bcfg2-lint
@@ -3,6 +3,7 @@
"""This tool examines your Bcfg2 specifications for errors."""
import sys
+import time
import inspect
import logging
import Bcfg2.Logger
@@ -52,22 +53,27 @@ def run_plugin(plugin, plugin_name, setup=None, errorhandler=None,
args.append(setup)
# python 2.5 doesn't support mixing *magic and keyword arguments
- return plugin(*args, **dict(files=files, errorhandler=errorhandler)).Run()
+ start = time.time()
+ rv = plugin(*args, **dict(files=files, errorhandler=errorhandler)).Run()
+ LOGGER.debug(" Ran %s in %0.2f seconds" % (plugin_name,
+ time.time() - start))
+ return rv
def get_errorhandler(setup):
""" get a Bcfg2.Server.Lint.ErrorHandler object """
if setup.cfp.has_section("errors"):
- conf = dict(setup.cfp.items("errors"))
+ errors = dict(setup.cfp.items("errors"))
else:
- conf = None
- return Bcfg2.Server.Lint.ErrorHandler(config=conf)
+ errors = None
+ return Bcfg2.Server.Lint.ErrorHandler(errors=errors)
def load_server(setup):
""" load server """
core = Bcfg2.Server.Core.BaseCore(setup)
- core.fam.handle_events_in_interval(4)
+ core.load_plugins()
+ core.fam.handle_events_in_interval(0.1)
return core
@@ -92,7 +98,7 @@ def load_plugins(setup):
elif setup['lint_plugins']:
plugin_list = setup['lint_plugins']
else:
- plugin_list = Bcfg2.Server.Lint.__all__
+ plugin_list = Bcfg2.Server.Lint.plugins
allplugins = dict()
for plugin in plugin_list:
diff --git a/src/sbin/bcfg2-report-collector b/src/sbin/bcfg2-report-collector
index a0ee2259a..594be13bf 100755
--- a/src/sbin/bcfg2-report-collector
+++ b/src/sbin/bcfg2-report-collector
@@ -12,12 +12,10 @@ from Bcfg2.Reporting.Collector import ReportingCollector, ReportingError
def main():
logger = logging.getLogger('bcfg2-report-collector')
- optinfo = dict(
- daemon=Bcfg2.Options.DAEMON,
- repo=Bcfg2.Options.SERVER_REPOSITORY,
- filemonitor=Bcfg2.Options.SERVER_FILEMONITOR,
- web_configfile=Bcfg2.Options.WEB_CFILE,
- )
+ optinfo = dict(daemon=Bcfg2.Options.DAEMON,
+ repo=Bcfg2.Options.SERVER_REPOSITORY,
+ filemonitor=Bcfg2.Options.SERVER_FILEMONITOR,
+ web_configfile=Bcfg2.Options.WEB_CFILE)
optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
optinfo.update(Bcfg2.Options.REPORTING_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
diff --git a/src/sbin/bcfg2-reports b/src/sbin/bcfg2-reports
index 2c4a918be..bb45e0009 100755
--- a/src/sbin/bcfg2-reports
+++ b/src/sbin/bcfg2-reports
@@ -233,7 +233,8 @@ def main():
try:
entries = [l.strip().split(":")
for l in open(options.file)]
- except IOError, err:
+ except IOError:
+ err = sys.exc_info()[1]
print("Cannot read entries from %s: %s" % (options.file,
err))
return 2
diff --git a/src/sbin/bcfg2-server b/src/sbin/bcfg2-server
index cdca71e74..4c4a71fa7 100755
--- a/src/sbin/bcfg2-server
+++ b/src/sbin/bcfg2-server
@@ -24,21 +24,32 @@ def main():
print("Could not read %s" % setup['configfile'])
sys.exit(1)
- if setup['backend'] not in ['best', 'cherrypy', 'builtin']:
+ # TODO: normalize case of various core modules so we can add a new
+ # core without modifying this script
+ backends = dict(cherrypy='CherryPyCore',
+ builtin='BuiltinCore',
+ best='BuiltinCore',
+ multiprocessing='MultiprocessingCore')
+
+ if setup['backend'] not in backends:
print("Unknown server backend %s, using 'best'" % setup['backend'])
setup['backend'] = 'best'
- if setup['backend'] == 'cherrypy':
- try:
- from Bcfg2.Server.CherryPyCore import Core
- except ImportError:
- err = sys.exc_info()[1]
- print("Unable to import CherryPy server core: %s" % err)
- raise
- elif setup['backend'] == 'builtin' or setup['backend'] == 'best':
- from Bcfg2.Server.BuiltinCore import Core
+
+ coremodule = backends[setup['backend']]
+ try:
+ corecls = getattr(__import__("Bcfg2.Server.%s" % coremodule).Server,
+ coremodule).Core
+ except ImportError:
+ err = sys.exc_info()[1]
+ print("Unable to import %s server core: %s" % (setup['backend'], err))
+ raise
+ except AttributeError:
+ err = sys.exc_info()[1]
+ print("Unable to load %s server core: %s" % (setup['backend'], err))
+ raise
try:
- core = Core(setup)
+ core = corecls(setup)
core.run()
except CoreInitError:
msg = sys.exc_info()[1]
diff --git a/src/sbin/bcfg2-test b/src/sbin/bcfg2-test
index 6eaf0cc33..d7a1894f0 100755
--- a/src/sbin/bcfg2-test
+++ b/src/sbin/bcfg2-test
@@ -5,6 +5,7 @@ without failures"""
import os
import sys
+import signal
import fnmatch
import logging
import Bcfg2.Logger
@@ -155,6 +156,7 @@ class ClientTest(TestCase):
def get_core(setup):
""" Get a server core, with events handled """
core = Bcfg2.Server.Core.BaseCore(setup)
+ core.load_plugins()
core.fam.handle_events_in_interval(0.1)
return core
@@ -189,9 +191,23 @@ def run_child(setup, clients, queue):
core.shutdown()
+def get_sigint_handler(core):
+ """ Get a function that handles SIGINT/Ctrl-C by shutting down the
+ core and exiting properly."""
+
+ def hdlr(sig, frame): # pylint: disable=W0613
+ """ Handle SIGINT/Ctrl-C by shutting down the core and exiting
+ properly. """
+ core.shutdown()
+ os._exit(1) # pylint: disable=W0212
+
+ return hdlr
+
+
def parse_args():
""" Parse command line arguments. """
optinfo = dict(Bcfg2.Options.TEST_COMMON_OPTIONS)
+
optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
@@ -245,6 +261,7 @@ def main():
setup = parse_args()
logger = logging.getLogger(sys.argv[0])
core = get_core(setup)
+ signal.signal(signal.SIGINT, get_sigint_handler(core))
if setup['args']:
clients = setup['args']
diff --git a/src/sbin/bcfg2-yum-helper b/src/sbin/bcfg2-yum-helper
index 7e5c03fd5..4ef531d39 100755
--- a/src/sbin/bcfg2-yum-helper
+++ b/src/sbin/bcfg2-yum-helper
@@ -9,33 +9,13 @@ import os
import sys
import yum
import logging
+import Bcfg2.Logger
from optparse import OptionParser
try:
import json
except ImportError:
import simplejson as json
-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
-
def pkg_to_tuple(package):
""" json doesn't distinguish between tuples and lists, but yum
@@ -76,7 +56,7 @@ class DepSolver(object):
except AttributeError:
self.yumbase._getConfig(cfgfile, debuglevel=verbose)
# pylint: enable=E1121,W0212
- self.logger = get_logger(verbose)
+ self.logger = logging.getLogger(self.__class__.__name__)
self._groups = None
def get_groups(self):
@@ -220,7 +200,17 @@ def main():
parser.add_option("-v", "--verbose", help="Verbosity level",
action="count")
(options, args) = parser.parse_args()
- logger = get_logger(options.verbose)
+
+ if options.verbose:
+ level = logging.DEBUG
+ clevel = logging.DEBUG
+ else:
+ level = logging.WARNING
+ clevel = logging.INFO
+ Bcfg2.Logger.setup_logging('bcfg2-yum-helper', to_syslog=True,
+ to_console=clevel, level=level)
+ logger = logging.getLogger('bcfg2-yum-helper')
+
try:
cmd = args[0]
except IndexError:
@@ -231,29 +221,58 @@ def main():
logger.error("Config file %s not found" % options.config)
return 1
+ # pylint: disable=W0702
+ rv = 0
depsolver = DepSolver(options.config, options.verbose)
if cmd == "clean":
- depsolver.clean_cache()
- print(json.dumps(True))
+ try:
+ depsolver.clean_cache()
+ print(json.dumps(True))
+ except:
+ logger.error("Unexpected error cleaning cache: %s" %
+ sys.exc_info()[1], exc_info=1)
+ print(json.dumps(False))
+ rv = 2
elif cmd == "complete":
- data = json.loads(sys.stdin.read())
- depsolver.groups = data['groups']
- (packages, unknown) = depsolver.complete([pkg_to_tuple(p)
- for p in data['packages']])
- print(json.dumps(dict(packages=list(packages),
- unknown=list(unknown))))
+ try:
+ data = json.loads(sys.stdin.read())
+ except:
+ logger.error("Unexpected error decoding JSON input: %s" %
+ sys.exc_info()[1])
+ rv = 2
+ try:
+ depsolver.groups = data['groups']
+ (packages, unknown) = depsolver.complete(
+ [pkg_to_tuple(p) for p in data['packages']])
+ print(json.dumps(dict(packages=list(packages),
+ unknown=list(unknown))))
+ except:
+ logger.error("Unexpected error completing package set: %s" %
+ sys.exc_info()[1], exc_info=1)
+ print(json.dumps(dict(packages=[], unknown=data['packages'])))
+ rv = 2
elif cmd == "get_groups":
- data = json.loads(sys.stdin.read())
- rv = dict()
- for gdata in data:
- if "type" in gdata:
- packages = depsolver.get_group(gdata['group'],
- ptype=gdata['type'])
- else:
- packages = depsolver.get_group(gdata['group'])
- rv[gdata['group']] = list(packages)
- print(json.dumps(rv))
-
+ try:
+ data = json.loads(sys.stdin.read())
+ rv = dict()
+ for gdata in data:
+ if "type" in gdata:
+ packages = depsolver.get_group(gdata['group'],
+ ptype=gdata['type'])
+ else:
+ packages = depsolver.get_group(gdata['group'])
+ rv[gdata['group']] = list(packages)
+ print(json.dumps(rv))
+ except:
+ logger.error("Unexpected error getting groups: %s" %
+ sys.exc_info()[1], exc_info=1)
+ print(json.dumps(dict()))
+ rv = 2
+ else:
+ logger.error("Unknown command %s" % cmd)
+ print(json.dumps(None))
+ rv = 2
+ return rv
if __name__ == '__main__':
sys.exit(main())
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestDirectory.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestDirectory.py
index d9431dc63..d14696b68 100644
--- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestDirectory.py
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestDirectory.py
@@ -70,7 +70,7 @@ class TestPOSIXDirectory(TestPOSIXTool):
expected = [os.path.join(entry.get("name"), e)
for e in entries
if os.path.join(entry.get("name"), e) not in modlist]
- actual = [e.get("path") for e in entry.findall("Prune")]
+ actual = [e.get("name") for e in entry.findall("Prune")]
self.assertItemsEqual(expected, actual)
mock_verify.reset_mock()
@@ -137,7 +137,7 @@ class TestPOSIXDirectory(TestPOSIXTool):
entry.set("prune", "true")
prune = ["/test/foo/bar/prune1", "/test/foo/bar/prune2"]
for path in prune:
- lxml.etree.SubElement(entry, "Prune", path=path)
+ lxml.etree.SubElement(entry, "Prune", name=path)
reset()
mock_install.return_value = True
@@ -145,6 +145,6 @@ class TestPOSIXDirectory(TestPOSIXTool):
self.assertTrue(ptool.install(entry))
ptool._exists.assert_called_with(entry)
mock_install.assert_called_with(ptool, entry)
- self.assertItemsEqual([c[0][0].get("path")
+ self.assertItemsEqual([c[0][0].get("name")
for c in ptool._remove.call_args_list],
prune)
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py
index 662e0e1b6..8f933e08f 100644
--- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/TestFile.py
@@ -64,10 +64,18 @@ class TestPOSIXFile(TestPOSIXTool):
self.assertEqual(ptool._get_data(entry), ("test", True))
entry = copy.deepcopy(orig_entry)
+ entry.set("encoding", "base64")
+ entry.set("empty", "true")
+ self.assertEqual(ptool._get_data(entry), ("", True))
+
+ entry = copy.deepcopy(orig_entry)
entry.set("empty", "true")
self.assertEqual(ptool._get_data(entry), ("", False))
entry = copy.deepcopy(orig_entry)
+ self.assertEqual(ptool._get_data(entry), ("", False))
+
+ entry = copy.deepcopy(orig_entry)
entry.text = "test"
self.assertEqual(ptool._get_data(entry), ("test", False))
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Testbase.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Testbase.py
index 49e9be2ba..e0406fd92 100644
--- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Testbase.py
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Testbase.py
@@ -897,7 +897,7 @@ class TestPOSIXTool(TestTool):
filedef_rv.__iter__.return_value = iter(file_acls)
defacls = acls
- for akey, perms in acls.items():
+ for akey, perms in list(acls.items()):
defacls[('default', akey[1], akey[2])] = perms
self.assertItemsEqual(ptool._list_file_acls(path), defacls)
mock_isdir.assert_called_with(path)
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py
index 4fcd63a60..9478f7071 100644
--- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py
@@ -227,8 +227,7 @@ class TestPOSIXUsers(TestTool):
users.user_supplementary_groups.assert_called_with(entry)
reset()
- m1 = lxml.etree.SubElement(entry, "MemberOf")
- m1.text = "wheel"
+ m1 = lxml.etree.SubElement(entry, "MemberOf", group="wheel")
m2 = lxml.etree.SubElement(entry, "MemberOf")
m2.text = "users"
self.assertTrue(users.VerifyPOSIXUser(entry, []))
@@ -237,8 +236,7 @@ class TestPOSIXUsers(TestTool):
users.user_supplementary_groups.assert_called_with(entry)
reset()
- m3 = lxml.etree.SubElement(entry, "MemberOf")
- m3.text = "extra"
+ m3 = lxml.etree.SubElement(entry, "MemberOf", group="extra")
self.assertFalse(users.VerifyPOSIXUser(entry, []))
users.populate_user_entry.assert_called_with(entry)
users._verify.assert_called_with(users.populate_user_entry.return_value)
@@ -373,8 +371,7 @@ class TestPOSIXUsers(TestTool):
entry = lxml.etree.Element("POSIXUser", name="test", group="test",
home="/home/test", shell="/bin/zsh",
gecos="Test McTest")
- m1 = lxml.etree.SubElement(entry, "MemberOf")
- m1.text = "wheel"
+ m1 = lxml.etree.SubElement(entry, "MemberOf", group="wheel")
m2 = lxml.etree.SubElement(entry, "MemberOf")
m2.text = "users"
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py
index a1e624824..e26c26d41 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testbase.py
@@ -72,14 +72,33 @@ class TestPlugin(TestDebuggable):
if core is None:
core = Mock()
core.setup = MagicMock()
- return self.test_obj(core, datastore)
+ @patchIf(not isinstance(os.makedirs, Mock), "os.makedirs", Mock())
+ def inner():
+ return self.test_obj(core, datastore)
+ return inner()
- def test__init(self):
- core = Mock()
- core.setup = MagicMock()
- p = self.get_obj(core=core)
- self.assertEqual(p.data, os.path.join(datastore, p.name))
- self.assertEqual(p.core, core)
+ @patch("os.makedirs")
+ @patch("os.path.exists")
+ def test__init(self, mock_exists, mock_makedirs):
+ if self.test_obj.create:
+ core = Mock()
+ core.setup = MagicMock()
+
+ mock_exists.return_value = True
+ p = self.get_obj(core=core)
+ self.assertEqual(p.data, os.path.join(datastore, p.name))
+ self.assertEqual(p.core, core)
+ mock_exists.assert_any_call(p.data)
+ self.assertFalse(mock_makedirs.called)
+
+ mock_exists.reset_mock()
+ mock_makedirs.reset_mock()
+ mock_exists.return_value = False
+ p = self.get_obj(core=core)
+ self.assertEqual(p.data, os.path.join(datastore, p.name))
+ self.assertEqual(p.core, core)
+ mock_exists.assert_any_call(p.data)
+ mock_makedirs.assert_any_call(p.data)
@patch("os.makedirs")
def test_init_repo(self, mock_makedirs):
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
index fb51eb1fe..94866cf39 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
@@ -158,6 +158,7 @@ class TestDirectoryBacked(Bcfg2TestCase):
""" ensure that the child object has the correct interface """
self.assertTrue(hasattr(self.test_obj.__child__, "HandleEvent"))
+ @patch("os.makedirs", Mock())
def get_obj(self, fam=None):
if fam is None:
fam = Mock()
@@ -171,12 +172,26 @@ class TestDirectoryBacked(Bcfg2TestCase):
fam)
return inner()
- def test__init(self):
+ @patch("os.makedirs")
+ @patch("os.path.exists")
+ def test__init(self, mock_exists, mock_makedirs):
@patch("%s.%s.add_directory_monitor" % (self.test_obj.__module__,
self.test_obj.__name__))
def inner(mock_add_monitor):
+ mock_exists.return_value = True
+ db = self.test_obj(datastore, Mock())
+ mock_add_monitor.assert_called_with('')
+ mock_exists.assert_called_with(db.data)
+ self.assertFalse(mock_makedirs.called)
+
+ mock_add_monitor.reset_mock()
+ mock_exists.reset_mock()
+ mock_makedirs.reset_mock()
+ mock_exists.return_value = False
db = self.test_obj(datastore, Mock())
mock_add_monitor.assert_called_with('')
+ mock_exists.assert_called_with(db.data)
+ mock_makedirs.assert_called_with(db.data)
inner()
@@ -398,25 +413,26 @@ class TestXMLFileBacked(TestFileBacked):
def get_obj(self, path=None, fam=None, should_monitor=False):
if path is None:
path = self.path
- return self.test_obj(path, fam=fam, should_monitor=should_monitor)
+
+ @patchIf(not isinstance(os.path.exists, Mock),
+ "os.path.exists", Mock())
+ def inner():
+ return self.test_obj(path, fam=fam, should_monitor=should_monitor)
+ return inner()
def test__init(self):
fam = Mock()
xfb = self.get_obj()
- if self.should_monitor is True:
+ if self.should_monitor:
self.assertIsNotNone(xfb.fam)
+ fam.reset_mock()
+ xfb = self.get_obj(fam=fam, should_monitor=True)
+ fam.AddMonitor.assert_called_with(self.path, xfb)
else:
self.assertIsNone(xfb.fam)
-
- if self.should_monitor is not True:
xfb = self.get_obj(fam=fam)
self.assertFalse(fam.AddMonitor.called)
- if self.should_monitor is not False:
- fam.reset_mock()
- xfb = self.get_obj(fam=fam, should_monitor=True)
- fam.AddMonitor.assert_called_with(self.path, xfb)
-
@patch("glob.glob")
@patch("lxml.etree.parse")
def test_follow_xincludes(self, mock_parse, mock_glob):
@@ -604,7 +620,7 @@ class TestXMLFileBacked(TestFileBacked):
def test_add_monitor(self):
xfb = self.get_obj()
xfb.add_monitor("/test/test2.xml")
- self.assertIn("/test/test2.xml", xfb.extras)
+ self.assertIn("/test/test2.xml", xfb.extra_monitors)
fam = Mock()
if self.should_monitor is not True:
@@ -613,14 +629,14 @@ class TestXMLFileBacked(TestFileBacked):
fam.reset_mock()
xfb.add_monitor("/test/test3.xml")
self.assertFalse(fam.AddMonitor.called)
- self.assertIn("/test/test3.xml", xfb.extras)
+ self.assertIn("/test/test3.xml", xfb.extra_monitors)
if self.should_monitor is not False:
fam.reset_mock()
xfb = self.get_obj(fam=fam, should_monitor=True)
xfb.add_monitor("/test/test4.xml")
fam.AddMonitor.assert_called_with("/test/test4.xml", xfb)
- self.assertIn("/test/test4.xml", xfb.extras)
+ self.assertIn("/test/test4.xml", xfb.extra_monitors)
class TestStructFile(TestXMLFileBacked):
@@ -1186,13 +1202,18 @@ class TestXMLDirectoryBacked(TestDirectoryBacked):
class TestPrioDir(TestPlugin, TestGenerator, TestXMLDirectoryBacked):
test_obj = PrioDir
- @patch("Bcfg2.Server.Plugin.helpers.%s.add_directory_monitor" %
- test_obj.__name__,
- Mock())
def get_obj(self, core=None):
if core is None:
core = Mock()
- return self.test_obj(core, datastore)
+
+ @patch("%s.%s.add_directory_monitor" %
+ (self.test_obj.__module__, self.test_obj.__name__),
+ Mock())
+ @patchIf(not isinstance(os.makedirs, Mock), "os.makedirs", Mock())
+ def inner():
+ return self.test_obj(core, datastore)
+
+ return inner()
def test_HandleEvent(self):
TestXMLDirectoryBacked.test_HandleEvent(self)
@@ -1452,7 +1473,8 @@ class TestEntrySet(TestDebuggable):
bogus)))
for ignore in self.ignore:
- self.assertTrue(eset.ignore.match(ignore))
+ self.assertTrue(eset.ignore.match(ignore),
+ "%s should be ignored but wasn't" % ignore)
self.assertFalse(eset.ignore.match(basename))
self.assertFalse(eset.ignore.match(basename + ".G20_foo"))
@@ -1816,6 +1838,7 @@ class TestGroupSpool(TestPlugin, TestGenerator):
return inner()
def test__init(self):
+ @patchIf(not isinstance(os.makedirs, Mock), "os.makedirs", Mock())
@patch("%s.%s.AddDirectoryMonitor" % (self.test_obj.__module__,
self.test_obj.__name__))
def inner(mock_Add):
@@ -2010,6 +2033,3 @@ class TestGroupSpool(TestPlugin, TestGenerator):
gs.event_id.assert_called_with(event)
self.assertNotIn("/baz/quux", gs.entries)
self.assertNotIn("/baz/quux", gs.Entries[gs.entry_type])
-
-
-
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py
index 35f4e0700..1f5c4790b 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testinterfaces.py
@@ -97,11 +97,6 @@ class TestProbing(Bcfg2TestCase):
class TestStatistics(TestPlugin):
test_obj = Statistics
- def get_obj(self, core=None):
- if core is None:
- core = Mock()
- return self.test_obj(core, datastore)
-
def test_process_statistics(self):
s = self.get_obj()
self.assertRaises(NotImplementedError,
@@ -354,12 +349,6 @@ class TestGoalValidator(Bcfg2TestCase):
class TestVersion(TestPlugin):
test_obj = Version
- def get_obj(self, core=None):
- if core is None:
- core = Mock()
- core.setup = MagicMock()
- return self.test_obj(core, datastore)
-
def test_get_revision(self):
d = self.get_obj()
self.assertRaises(NotImplementedError, d.get_revision)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgGenshiGenerator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgGenshiGenerator.py
index 385f8df77..2e8b7bfa5 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgGenshiGenerator.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgGenshiGenerator.py
@@ -2,6 +2,7 @@ import os
import sys
import lxml.etree
from mock import Mock, MagicMock, patch
+import Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import *
from Bcfg2.Server.Plugin import PluginExecutionError
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
index 2e758774e..ea3549c1b 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
@@ -6,7 +6,7 @@ import Bcfg2.Options
from Bcfg2.Compat import walk_packages
from mock import Mock, MagicMock, patch
from Bcfg2.Server.Plugins.Cfg import *
-from Bcfg2.Server.Plugin import PluginExecutionError
+from Bcfg2.Server.Plugin import PluginExecutionError, Specificity
# add all parent testsuite directories to sys.path to allow (most)
# relative imports in python 2.4
@@ -298,21 +298,20 @@ class TestCfgEntrySet(TestEntrySet):
for submodule in walk_packages(path=Bcfg2.Server.Plugins.Cfg.__path__,
prefix="Bcfg2.Server.Plugins.Cfg."):
expected.append(submodule[1].rsplit('.', 1)[-1])
- eset = self.get_obj()
- self.assertItemsEqual(expected,
- [h.__name__ for h in eset.handlers])
+ self.assertItemsEqual(expected, [h.__name__ for h in handlers()])
- def test_handle_event(self):
+ @patch("Bcfg2.Server.Plugins.Cfg.handlers")
+ def test_handle_event(self, mock_handlers):
eset = self.get_obj()
eset.entry_init = Mock()
- eset._handlers = [Mock(), Mock(), Mock()]
- for hdlr in eset.handlers:
+ mock_handlers.return_value = [Mock(), Mock(), Mock()]
+ for hdlr in mock_handlers.return_value:
hdlr.__name__ = "handler"
eset.entries = dict()
def reset():
eset.entry_init.reset_mock()
- for hdlr in eset.handlers:
+ for hdlr in mock_handlers.return_value:
hdlr.reset_mock()
# test that a bogus deleted event is discarded
@@ -322,7 +321,7 @@ class TestCfgEntrySet(TestEntrySet):
eset.handle_event(evt)
self.assertFalse(eset.entry_init.called)
self.assertItemsEqual(eset.entries, dict())
- for hdlr in eset.handlers:
+ for hdlr in mock_handlers.return_value:
self.assertFalse(hdlr.handles.called)
self.assertFalse(hdlr.ignore.called)
@@ -333,7 +332,7 @@ class TestCfgEntrySet(TestEntrySet):
evt.filename = os.path.join(datastore, "test.txt")
# test with no handler that handles
- for hdlr in eset.handlers:
+ for hdlr in mock_handlers.return_value:
hdlr.handles.return_value = False
hdlr.ignore.return_value = False
@@ -341,16 +340,16 @@ class TestCfgEntrySet(TestEntrySet):
eset.handle_event(evt)
self.assertFalse(eset.entry_init.called)
self.assertItemsEqual(eset.entries, dict())
- for hdlr in eset.handlers:
+ for hdlr in mock_handlers.return_value:
hdlr.handles.assert_called_with(evt, basename=eset.path)
hdlr.ignore.assert_called_with(evt, basename=eset.path)
# test with a handler that handles the entry
reset()
- eset.handlers[-1].handles.return_value = True
+ mock_handlers.return_value[-1].handles.return_value = True
eset.handle_event(evt)
- eset.entry_init.assert_called_with(evt, eset.handlers[-1])
- for hdlr in eset.handlers:
+ eset.entry_init.assert_called_with(evt, mock_handlers.return_value[-1])
+ for hdlr in mock_handlers.return_value:
hdlr.handles.assert_called_with(evt, basename=eset.path)
if not hdlr.return_value:
hdlr.ignore.assert_called_with(evt, basename=eset.path)
@@ -358,14 +357,14 @@ class TestCfgEntrySet(TestEntrySet):
# test with a handler that ignores the entry before one
# that handles it
reset()
- eset.handlers[0].ignore.return_value = True
+ mock_handlers.return_value[0].ignore.return_value = True
eset.handle_event(evt)
self.assertFalse(eset.entry_init.called)
- eset.handlers[0].handles.assert_called_with(evt,
+ mock_handlers.return_value[0].handles.assert_called_with(evt,
basename=eset.path)
- eset.handlers[0].ignore.assert_called_with(evt,
+ mock_handlers.return_value[0].ignore.assert_called_with(evt,
basename=eset.path)
- for hdlr in eset.handlers[1:]:
+ for hdlr in mock_handlers.return_value[1:]:
self.assertFalse(hdlr.handles.called)
self.assertFalse(hdlr.ignore.called)
@@ -377,7 +376,7 @@ class TestCfgEntrySet(TestEntrySet):
eset.entries[evt.filename] = Mock()
eset.handle_event(evt)
self.assertFalse(eset.entry_init.called)
- for hdlr in eset.handlers:
+ for hdlr in mock_handlers.return_value:
self.assertFalse(hdlr.handles.called)
self.assertFalse(hdlr.ignore.called)
eset.entries[evt.filename].handle_event.assert_called_with(evt)
@@ -387,7 +386,7 @@ class TestCfgEntrySet(TestEntrySet):
evt.code2str.return_value = "deleted"
eset.handle_event(evt)
self.assertFalse(eset.entry_init.called)
- for hdlr in eset.handlers:
+ for hdlr in mock_handlers.return_value:
self.assertFalse(hdlr.handles.called)
self.assertFalse(hdlr.ignore.called)
self.assertItemsEqual(eset.entries, dict())
@@ -462,7 +461,7 @@ class TestCfgEntrySet(TestEntrySet):
metadata = Mock()
# test basic entry, no validation, no filters, etc.
- eset._generate_data.return_value = "data"
+ eset._generate_data.return_value = ("data", None)
eset.get_handlers.return_value = []
bound = eset.bind_entry(entry, metadata)
eset.bind_info_to_entry.assert_called_with(entry, metadata)
@@ -475,7 +474,7 @@ class TestCfgEntrySet(TestEntrySet):
# test empty entry
entry = reset()
- eset._generate_data.return_value = ""
+ eset._generate_data.return_value = ("", None)
bound = eset.bind_entry(entry, metadata)
eset.bind_info_to_entry.assert_called_with(entry, metadata)
eset._generate_data.assert_called_with(entry, metadata)
@@ -486,7 +485,9 @@ class TestCfgEntrySet(TestEntrySet):
# test filters
entry = reset()
- eset._generate_data.return_value = "initial data"
+ generator = Mock()
+ generator.specific = Specificity(all=True)
+ eset._generate_data.return_value = ("initial data", generator)
filters = [Mock(), Mock()]
filters[0].modify_data.return_value = "modified data"
filters[1].modify_data.return_value = "final data"
@@ -508,7 +509,7 @@ class TestCfgEntrySet(TestEntrySet):
entry.set("encoding", "base64")
mock_b64encode.return_value = "base64 data"
eset.get_handlers.return_value = []
- eset._generate_data.return_value = "data"
+ eset._generate_data.return_value = ("data", None)
bound = eset.bind_entry(entry, metadata)
eset.bind_info_to_entry.assert_called_with(entry, metadata)
eset._generate_data.assert_called_with(entry, metadata)
@@ -692,7 +693,7 @@ class TestCfgEntrySet(TestEntrySet):
eset._create_data.reset_mock()
# test success
- self.assertEqual(eset._generate_data(entry, metadata),
+ self.assertEqual(eset._generate_data(entry, metadata)[0],
"data")
eset.get_handlers.assert_called_with(metadata, CfgGenerator)
eset.best_matching.assert_called_with(metadata,
@@ -709,7 +710,7 @@ class TestCfgEntrySet(TestEntrySet):
reset()
eset.best_matching.side_effect = PluginExecutionError
self.assertEqual(eset._generate_data(entry, metadata),
- eset._create_data.return_value)
+ (eset._create_data.return_value, None))
eset.get_handlers.assert_called_with(metadata, CfgGenerator)
eset.best_matching.assert_called_with(metadata,
eset.get_handlers.return_value)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py
index a9346156c..c6e6f5ef7 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py
@@ -92,7 +92,12 @@ class TestPatternFile(TestXMLFileBacked):
core.fam = fam
elif not core:
core = Mock()
- return self.test_obj(path, core=core)
+
+ @patchIf(not isinstance(lxml.etree.Element, Mock),
+ "lxml.etree.Element", Mock())
+ def inner():
+ return self.test_obj(path, core=core)
+ return inner()
@patch("Bcfg2.Server.Plugins.GroupPatterns.PatternMap")
def test_Index(self, mock_PatternMap):
@@ -135,6 +140,14 @@ class TestPatternFile(TestXMLFileBacked):
class TestGroupPatterns(TestPlugin, TestConnector):
test_obj = GroupPatterns
+ def get_obj(self, core=None):
+ @patchIf(not isinstance(lxml.etree.Element, Mock),
+ "lxml.etree.Element", Mock())
+ def inner():
+ return TestPlugin.get_obj(self, core=core)
+ return inner()
+
+
def test_get_additional_groups(self):
gp = self.get_obj()
gp.config = Mock()
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
index 69ea45de6..742946c42 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
@@ -94,7 +94,13 @@ def get_metadata_object(core=None, watch_clients=False, use_db=False):
core.setup = MagicMock()
core.metadata_cache = MagicMock()
core.setup.cfp.getboolean = Mock(return_value=use_db)
- return Metadata(core, datastore, watch_clients=watch_clients)
+
+ @patchIf(not isinstance(os.makedirs, Mock), "os.makedirs", Mock())
+ @patchIf(not isinstance(lxml.etree.Element, Mock),
+ "lxml.etree.Element", Mock())
+ def inner():
+ return Metadata(core, datastore, watch_clients=watch_clients)
+ return inner()
class TestMetadataDB(DBModelTestCase):
@@ -203,7 +209,11 @@ class TestXMLMetadataConfig(TestXMLFileBacked):
def get_obj(self, basefile="clients.xml", core=None, watch_clients=False):
self.metadata = get_metadata_object(core=core,
watch_clients=watch_clients)
- return XMLMetadataConfig(self.metadata, watch_clients, basefile)
+ @patchIf(not isinstance(lxml.etree.Element, Mock),
+ "lxml.etree.Element", Mock())
+ def inner():
+ return XMLMetadataConfig(self.metadata, watch_clients, basefile)
+ return inner()
def test__init(self):
xmc = self.get_obj()
@@ -1521,7 +1531,11 @@ class TestMetadata_ClientsXML(TestMetadataBase):
if metadata is None:
metadata = self.get_obj()
metadata.core.fam = Mock()
- metadata.clients_xml = metadata._handle_file("clients.xml")
+ @patchIf(not isinstance(lxml.etree.Element, Mock),
+ "lxml.etree.Element", Mock())
+ def inner():
+ metadata.clients_xml = metadata._handle_file("clients.xml")
+ inner()
metadata = TestMetadata.load_clients_data(self, metadata=metadata,
xdata=xdata)
return TestMetadataBase.load_clients_data(self, metadata=metadata,
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py
index 899fb24a0..0794db62e 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProbes.py
@@ -1,7 +1,9 @@
import os
import sys
+import copy
import time
import lxml.etree
+import Bcfg2.version
import Bcfg2.Server
import Bcfg2.Server.Plugin
from mock import Mock, MagicMock, patch
@@ -25,6 +27,47 @@ test_data = dict(a=1, b=[1, 2, 3], c="test",
d=dict(a=1, b=dict(a=1), c=(1, "2", 3)))
+class FakeElement(lxml.etree._Element):
+ getroottree = Mock()
+
+ def __init__(self, el):
+ self._element = el
+
+ def __getattribute__(self, attr):
+ el = lxml.etree._Element.__getattribute__(self,
+ '__dict__')['_element']
+ if attr == "getroottree":
+ return FakeElement.getroottree
+ elif attr == "_element":
+ return el
+ else:
+ return getattr(el, attr)
+
+
+class StoringElement(object):
+ OriginalElement = copy.copy(lxml.etree.Element)
+
+ def __init__(self):
+ self.element = None
+ self.return_value = None
+
+ def __call__(self, *args, **kwargs):
+ self.element = self.OriginalElement(*args, **kwargs)
+ self.return_value = FakeElement(self.element)
+ return self.return_value
+
+
+class StoringSubElement(object):
+ OriginalSubElement = copy.copy(lxml.etree.SubElement)
+
+ def __call__(self, parent, tag, **kwargs):
+ try:
+ return self.OriginalSubElement(parent._element, tag,
+ **kwargs)
+ except AttributeError:
+ return self.OriginalSubElement(parent, tag, **kwargs)
+
+
class FakeList(list):
pass
@@ -175,6 +218,8 @@ group-specific"""
ps.get_matching.return_value = matching
metadata = Mock()
+ metadata.version_info = \
+ Bcfg2.version.Bcfg2VersionInfo(Bcfg2.version.__version__)
pdata = ps.get_probe_data(metadata)
ps.get_matching.assert_called_with(metadata)
# we can't create a matching operator.attrgetter object, and I
@@ -201,9 +246,7 @@ class TestProbes(TestProbing, TestConnector, TestDatabaseBacked):
test_obj = Probes
def get_obj(self, core=None):
- if core is None:
- core = MagicMock()
- return self.test_obj(core, datastore)
+ return TestDatabaseBacked.get_obj(self, core=core)
def get_test_probedata(self):
test_xdata = lxml.etree.Element("test")
@@ -247,9 +290,10 @@ text
# test__init(), which relies on being able to check the calls
# of load_data(), and thus on load_data() being consistently
# mocked)
- @patch("Bcfg2.Server.Plugins.Probes.Probes.load_data", new=load_data)
+ @patch("%s.%s.load_data" % (self.test_obj.__module__,
+ self.test_obj.__name__), new=load_data)
def inner():
- return Probes(core, datastore)
+ return self.get_obj(core)
return inner()
@@ -289,61 +333,71 @@ text
probes._write_data_db.assert_called_with("test")
self.assertFalse(probes._write_data_xml.called)
- @patch("%s.open" % builtins)
- def test__write_data_xml(self, mock_open):
+ def test__write_data_xml(self):
probes = self.get_probes_object(use_db=False)
probes.probedata = self.get_test_probedata()
probes.cgroups = self.get_test_cgroups()
- probes._write_data_xml(None)
-
- mock_open.assert_called_with(os.path.join(datastore, probes.name,
- "probed.xml"), "w")
- data = lxml.etree.XML(mock_open.return_value.write.call_args[0][0])
- self.assertEqual(len(data.xpath("//Client")), 2)
-
- foodata = data.find("Client[@name='foo.example.com']")
- self.assertIsNotNone(foodata)
- self.assertIsNotNone(foodata.get("timestamp"))
- self.assertEqual(len(foodata.findall("Probe")),
- len(probes.probedata['foo.example.com']))
- self.assertEqual(len(foodata.findall("Group")),
- len(probes.cgroups['foo.example.com']))
- xml = foodata.find("Probe[@name='xml']")
- self.assertIsNotNone(xml)
- self.assertIsNotNone(xml.get("value"))
- xdata = lxml.etree.XML(xml.get("value"))
- self.assertIsNotNone(xdata)
- self.assertIsNotNone(xdata.find("test"))
- self.assertEqual(xdata.find("test").get("foo"), "foo")
- text = foodata.find("Probe[@name='text']")
- self.assertIsNotNone(text)
- self.assertIsNotNone(text.get("value"))
- multiline = foodata.find("Probe[@name='multiline']")
- self.assertIsNotNone(multiline)
- self.assertIsNotNone(multiline.get("value"))
- self.assertGreater(len(multiline.get("value").splitlines()), 1)
-
- bardata = data.find("Client[@name='bar.example.com']")
- self.assertIsNotNone(bardata)
- self.assertIsNotNone(bardata.get("timestamp"))
- self.assertEqual(len(bardata.findall("Probe")),
- len(probes.probedata['bar.example.com']))
- self.assertEqual(len(bardata.findall("Group")),
- len(probes.cgroups['bar.example.com']))
- empty = bardata.find("Probe[@name='empty']")
- self.assertIsNotNone(empty)
- self.assertIsNotNone(empty.get("value"))
- self.assertEqual(empty.get("value"), "")
- if HAS_JSON:
- jdata = bardata.find("Probe[@name='json']")
- self.assertIsNotNone(jdata)
- self.assertIsNotNone(jdata.get("value"))
- self.assertItemsEqual(test_data, json.loads(jdata.get("value")))
- if HAS_YAML:
- ydata = bardata.find("Probe[@name='yaml']")
- self.assertIsNotNone(ydata)
- self.assertIsNotNone(ydata.get("value"))
- self.assertItemsEqual(test_data, yaml.load(ydata.get("value")))
+
+ @patch("lxml.etree.Element")
+ @patch("lxml.etree.SubElement", StoringSubElement())
+ def inner(mock_Element):
+ mock_Element.side_effect = StoringElement()
+ probes._write_data_xml(None)
+
+ top = mock_Element.side_effect.return_value
+ write = top.getroottree.return_value.write
+ self.assertEqual(write.call_args[0][0],
+ os.path.join(datastore, probes.name,
+ "probed.xml"))
+
+ data = top._element
+ foodata = data.find("Client[@name='foo.example.com']")
+ self.assertIsNotNone(foodata)
+ self.assertIsNotNone(foodata.get("timestamp"))
+ self.assertEqual(len(foodata.findall("Probe")),
+ len(probes.probedata['foo.example.com']))
+ self.assertEqual(len(foodata.findall("Group")),
+ len(probes.cgroups['foo.example.com']))
+ xml = foodata.find("Probe[@name='xml']")
+ self.assertIsNotNone(xml)
+ self.assertIsNotNone(xml.get("value"))
+ xdata = lxml.etree.XML(xml.get("value"))
+ self.assertIsNotNone(xdata)
+ self.assertIsNotNone(xdata.find("test"))
+ self.assertEqual(xdata.find("test").get("foo"), "foo")
+ text = foodata.find("Probe[@name='text']")
+ self.assertIsNotNone(text)
+ self.assertIsNotNone(text.get("value"))
+ multiline = foodata.find("Probe[@name='multiline']")
+ self.assertIsNotNone(multiline)
+ self.assertIsNotNone(multiline.get("value"))
+ self.assertGreater(len(multiline.get("value").splitlines()), 1)
+
+ bardata = data.find("Client[@name='bar.example.com']")
+ self.assertIsNotNone(bardata)
+ self.assertIsNotNone(bardata.get("timestamp"))
+ self.assertEqual(len(bardata.findall("Probe")),
+ len(probes.probedata['bar.example.com']))
+ self.assertEqual(len(bardata.findall("Group")),
+ len(probes.cgroups['bar.example.com']))
+ empty = bardata.find("Probe[@name='empty']")
+ self.assertIsNotNone(empty)
+ self.assertIsNotNone(empty.get("value"))
+ self.assertEqual(empty.get("value"), "")
+ if HAS_JSON:
+ jdata = bardata.find("Probe[@name='json']")
+ self.assertIsNotNone(jdata)
+ self.assertIsNotNone(jdata.get("value"))
+ self.assertItemsEqual(test_data,
+ json.loads(jdata.get("value")))
+ if HAS_YAML:
+ ydata = bardata.find("Probe[@name='yaml']")
+ self.assertIsNotNone(ydata)
+ self.assertIsNotNone(ydata.get("value"))
+ self.assertItemsEqual(test_data,
+ yaml.load(ydata.get("value")))
+
+ inner()
@skipUnless(HAS_DJANGO, "Django not found, skipping")
def test__write_data_db(self):
@@ -415,18 +469,24 @@ text
probes._load_data_db.assert_any_call()
self.assertFalse(probes._load_data_xml.called)
- @patch("%s.open" % builtins)
@patch("lxml.etree.parse")
- def test__load_data_xml(self, mock_parse, mock_open):
+ def test__load_data_xml(self, mock_parse):
probes = self.get_probes_object(use_db=False)
- # to get the value for lxml.etree.parse to parse, we call
- # _write_data_xml, mock the open() call, and grab the data
- # that gets "written" to probed.xml
probes.probedata = self.get_test_probedata()
probes.cgroups = self.get_test_cgroups()
- probes._write_data_xml(None)
- xdata = \
- lxml.etree.XML(str(mock_open.return_value.write.call_args[0][0]))
+
+ # to get the value for lxml.etree.parse to parse, we call
+ # _write_data_xml, mock the lxml.etree._ElementTree.write()
+ # call, and grab the data that gets "written" to probed.xml
+ @patch("lxml.etree.Element")
+ @patch("lxml.etree.SubElement", StoringSubElement())
+ def inner(mock_Element):
+ mock_Element.side_effect = StoringElement()
+ probes._write_data_xml(None)
+ top = mock_Element.side_effect.return_value
+ return top._element
+
+ xdata = inner()
mock_parse.return_value = xdata.getroottree()
probes.probedata = dict()
probes.cgroups = dict()
@@ -564,5 +624,3 @@ text
metadata.hostname = "nonexistent"
self.assertEqual(probes.get_additional_data(metadata),
ClientProbeDataSet())
-
-
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProperties.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProperties.py
index 93e2fff51..896f5861e 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProperties.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestProperties.py
@@ -418,8 +418,8 @@ class TestXMLPropertyFile(TestPropertyFile, TestStructFile):
self.assertFalse(mock_copy.called)
-class TestPropDirectoryBacked(TestDirectoryBacked):
- test_obj = PropDirectoryBacked
+class TestProperties(TestPlugin, TestConnector, TestDirectoryBacked):
+ test_obj = Properties
testfiles = ['foo.xml', 'bar.baz.xml']
if HAS_JSON:
testfiles.extend(["foo.json", "foo.xml.json"])
@@ -428,17 +428,13 @@ class TestPropDirectoryBacked(TestDirectoryBacked):
ignore = ['foo.xsd', 'bar.baz.xsd', 'quux.xml.xsd']
badevents = ['bogus.txt']
-
-class TestProperties(TestPlugin, TestConnector):
- test_obj = Properties
-
- def test__init(self):
- TestPlugin.test__init(self)
-
- core = Mock()
- p = self.get_obj(core=core)
- self.assertIsInstance(p.store, PropDirectoryBacked)
- self.assertEqual(Bcfg2.Server.Plugins.Properties.SETUP, core.setup)
+ def get_obj(self, core=None):
+ @patch("%s.%s.add_directory_monitor" % (self.test_obj.__module__,
+ self.test_obj.__name__),
+ Mock())
+ def inner():
+ return TestPlugin.get_obj(self, core=core)
+ return inner()
@patch("copy.copy")
def test_get_additional_data(self, mock_copy):
@@ -446,11 +442,11 @@ class TestProperties(TestPlugin, TestConnector):
p = self.get_obj()
metadata = Mock()
- p.store.entries = {"foo.xml": Mock(),
- "foo.yml": Mock()}
+ p.entries = {"foo.xml": Mock(),
+ "foo.yml": Mock()}
rv = p.get_additional_data(metadata)
expected = dict()
- for name, entry in p.store.entries.items():
+ for name, entry in p.entries.items():
entry.get_additional_data.assert_called_with(metadata)
expected[name] = entry.get_additional_data.return_value
self.assertItemsEqual(rv, expected)
diff --git a/testsuite/Testsrc/Testlib/TestStatistics.py b/testsuite/Testsrc/Testlib/TestStatistics.py
new file mode 100644
index 000000000..496cbac28
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestStatistics.py
@@ -0,0 +1,44 @@
+import os
+import sys
+from mock import Mock, MagicMock, patch
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import *
+
+from Bcfg2.Statistics import *
+
+
+class TestStatistic(Bcfg2TestCase):
+ def test_stat(self):
+ stat = Statistic("test", 1)
+ self.assertEqual(stat.get_value(), ("test", (1.0, 1.0, 1.0, 1)))
+ stat.add_value(10)
+ self.assertEqual(stat.get_value(), ("test", (1.0, 10.0, 5.5, 2)))
+ stat.add_value(100)
+ self.assertEqual(stat.get_value(), ("test", (1.0, 100.0, 37.0, 3)))
+ stat.add_value(12.345)
+ self.assertEqual(stat.get_value(), ("test", (1.0, 100.0, 30.83625, 4)))
+ stat.add_value(0.655)
+ self.assertEqual(stat.get_value(), ("test", (0.655, 100.0, 24.8, 5)))
+
+
+class TestStatistics(Bcfg2TestCase):
+ def test_stats(self):
+ stats = Statistics()
+ self.assertEqual(stats.display(), dict())
+ stats.add_value("test1", 1)
+ self.assertEqual(stats.display(), dict(test1=(1.0, 1.0, 1.0, 1)))
+ stats.add_value("test2", 1.23)
+ self.assertEqual(stats.display(), dict(test1=(1.0, 1.0, 1.0, 1),
+ test2=(1.23, 1.23, 1.23, 1)))
+ stats.add_value("test1", 10)
+ self.assertEqual(stats.display(), dict(test1=(1.0, 10.0, 5.5, 2),
+ test2=(1.23, 1.23, 1.23, 1)))
diff --git a/testsuite/before_install.sh b/testsuite/before_install.sh
index 884971e45..5f1a59aaf 100755
--- a/testsuite/before_install.sh
+++ b/testsuite/before_install.sh
@@ -2,9 +2,12 @@
# before_install script for Travis-CI
+PYVER=$(python -c 'import sys;print(".".join(str(v) for v in sys.version_info[0:2]))')
+
sudo apt-get update -qq
-sudo apt-get install -qq swig pylint libxml2-utils
+sudo apt-get install -qq swig libxml2-utils
if [[ "$WITH_OPTIONAL_DEPS" == "yes" ]]; then
- sudo apt-get install -qq python-selinux python-pylibacl python-pyinotify \
- python-yaml yum
+ if [[ ${PYVER:0:1} == "2" ]]; then
+ sudo apt-get install -qq python-selinux python-pylibacl yum
+ fi
fi
diff --git a/testsuite/install.sh b/testsuite/install.sh
index c1685f831..1ca89f40f 100755
--- a/testsuite/install.sh
+++ b/testsuite/install.sh
@@ -7,12 +7,16 @@ pip install -r testsuite/requirements.txt --use-mirrors
PYVER=$(python -c 'import sys;print(".".join(str(v) for v in sys.version_info[0:2]))')
if [[ "$WITH_OPTIONAL_DEPS" == "yes" ]]; then
+ pip install --use-mirrors genshi PyYAML pyinotify
if [[ $PYVER == "2.5" ]]; then
- # markdown 2.2.0 is broken on py2.5, so until 2.2.1 is released use 2.1
- pip install --use-mirrors 'markdown<2.2'
- pip install --use-mirrors simplejson
+ # markdown 2.2+ doesn't work on py2.5
+ pip install --use-mirrors simplejson 'markdown<2.2'
+ fi
+ if [[ ${PYVER:0:1} == "2" ]]; then
+ # django supports py3k, but South doesn't, and the django bits
+ # in bcfg2 require South
+ pip install cheetah 'django<1.5' 'South<0.8' M2Crypto
fi
- pip install --use-mirrors genshi cheetah 'django<1.4' South M2Crypto
else
# python < 2.6 requires M2Crypto for SSL communication, not just
# for encryption support
diff --git a/testsuite/pylintrc.conf b/testsuite/pylintrc.conf
index 14ccd1d23..653c68426 100644
--- a/testsuite/pylintrc.conf
+++ b/testsuite/pylintrc.conf
@@ -156,7 +156,7 @@ zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
-generated-members=objects,DoesNotExist,isoformat,filter,save,count,get,add,id
+generated-members=objects,DoesNotExist,isoformat,filter,save,count,get,add,id,MultipleObjectsReturned
[MISCELLANEOUS]
diff --git a/testsuite/requirements.txt b/testsuite/requirements.txt
index 8529b247f..2d6dbc557 100644
--- a/testsuite/requirements.txt
+++ b/testsuite/requirements.txt
@@ -2,4 +2,6 @@ lxml
nose
mock
sphinx
-daemon
+pylint
+pep8
+python-daemon
diff --git a/tools/bcfg2-profile-templates.py b/tools/bcfg2-profile-templates.py
index 3cd3786f9..f4069e454 100755
--- a/tools/bcfg2-profile-templates.py
+++ b/tools/bcfg2-profile-templates.py
@@ -1,25 +1,49 @@
#!/usr/bin/python -Ott
+# -*- coding: utf-8 -*-
""" Benchmark template rendering times """
-import os
import sys
import time
+import math
+import signal
import logging
import operator
import Bcfg2.Logger
+import Bcfg2.Options
import Bcfg2.Server.Core
-LOGGER = None
+
+def stdev(nums):
+ mean = float(sum(nums)) / len(nums)
+ return math.sqrt(sum((n - mean)**2 for n in nums) / float(len(nums)))
+
+
+def get_sigint_handler(core):
+ """ Get a function that handles SIGINT/Ctrl-C by shutting down the
+ core and exiting properly."""
+
+ def hdlr(sig, frame): # pylint: disable=W0613
+ """ Handle SIGINT/Ctrl-C by shutting down the core and exiting
+ properly. """
+ core.shutdown()
+ os._exit(1) # pylint: disable=W0212
+
+ return hdlr
def main():
- optinfo = \
- dict(client=Bcfg2.Options.Option("Benchmark templates for one client",
- cmd="--client",
- odesc="<client>",
- long_arg=True,
- default=None),
- )
+ optinfo = dict(
+ client=Bcfg2.Options.Option("Benchmark templates for one client",
+ cmd="--client",
+ odesc="<client>",
+ long_arg=True,
+ default=None),
+ runs=Bcfg2.Options.Option("Number of rendering passes per template",
+ cmd="--runs",
+ odesc="<runs>",
+ long_arg=True,
+ default=5,
+ cook=int))
optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
setup = Bcfg2.Options.OptionParser(optinfo)
@@ -39,13 +63,13 @@ def main():
logger = logging.getLogger(sys.argv[0])
core = Bcfg2.Server.Core.BaseCore(setup)
+ signal.signal(signal.SIGINT, get_sigint_handler(core))
logger.info("Bcfg2 server core loaded")
+ core.load_plugins()
+ logger.debug("Plugins loaded")
core.fam.handle_events_in_interval(0.1)
logger.debug("Repository events processed")
- # how many times to render each template for each client
- runs = 5
-
if setup['args']:
templates = setup['args']
else:
@@ -57,41 +81,57 @@ def main():
clients = [core.build_metadata(setup['client'])]
times = dict()
+ client_count = 0
for metadata in clients:
- for struct in core.GetStructures(metadata):
- logger.info("Rendering templates from structure %s:%s" %
- (struct.tag, struct.get("name")))
- for entry in struct.xpath("//Path"):
- path = entry.get("name")
- logger.info("Rendering %s..." % path)
- times[path] = dict()
- avg = 0.0
- for i in range(runs):
+ client_count += 1
+ logger.info("Rendering templates for client %s (%s/%s)" %
+ (metadata.hostname, client_count, len(clients)))
+ structs = core.GetStructures(metadata)
+ struct_count = 0
+ for struct in structs:
+ struct_count += 1
+ logger.info("Rendering templates from structure %s:%s (%s/%s)" %
+ (struct.tag, struct.get("name"), struct_count,
+ len(structs)))
+ entries = struct.xpath("//Path")
+ entry_count = 0
+ for entry in entries:
+ entry_count += 1
+ if templates and entry.get("name") not in templates:
+ continue
+ logger.info("Rendering Path:%s (%s/%s)..." %
+ (entry.get("name"), entry_count, len(entries)))
+ ptimes = times.setdefault(entry.get("name"), [])
+ for i in range(setup['runs']):
start = time.time()
try:
core.Bind(entry, metadata)
- avg += (time.time() - start) / runs
+ ptimes.append(time.time() - start)
except:
break
- if avg:
- logger.debug(" %s: %.02f sec" % (metadata.hostname, avg))
- times[path][metadata.hostname] = avg
+ if ptimes:
+ avg = sum(ptimes) / len(ptimes)
+ if avg:
+ logger.debug(" %s: %.02f sec" %
+ (metadata.hostname, avg))
# print out per-file results
tmpltimes = []
- for tmpl, clients in times.items():
+ for tmpl, ptimes in times.items():
try:
- avg = sum(clients.values()) / len(clients)
+ mean = float(sum(ptimes)) / len(ptimes)
except ZeroDivisionError:
continue
- if avg > 0.01 or templates:
- tmpltimes.append((tmpl, avg))
- print("%-50s %s" % ("Template", "Average Render Time"))
- for tmpl, avg in reversed(sorted(tmpltimes, key=operator.itemgetter(1))):
- print("%-50s %.02f" % (tmpl, avg))
-
- # TODO: complain about templates that on average were quick but
- # for which some clients were slow
+ ptimes.sort()
+ median = ptimes[len(ptimes) / 2]
+ std = stdev(ptimes)
+ if mean > 0.01 or median > 0.01 or std > 1 or templates:
+ tmpltimes.append((tmpl, mean, median, std))
+ print("%-50s %-9s %-11s %6s" %
+ ("Template", "Mean Time", "Median Time", "σ"))
+ for info in reversed(sorted(tmpltimes, key=operator.itemgetter(1))):
+ print("%-50s %9.02f %11.02f %6.02f" % info)
+ core.shutdown()
if __name__ == "__main__":
diff --git a/tools/bcfg2_local.py b/tools/bcfg2_local.py
index 2b9d39342..8c164e52e 100755
--- a/tools/bcfg2_local.py
+++ b/tools/bcfg2_local.py
@@ -19,7 +19,8 @@ class LocalCore(BaseCore):
setup['logging'] = None
Bcfg2.Server.Core.BaseCore.__init__(self, setup=setup)
setup['syslog'], setup['logging'] = saved
- self.fam.handle_events_in_interval(4)
+ self.load_plugins()
+ self.fam.handle_events_in_interval(0.1)
def _daemonize(self):
return True
@@ -64,6 +65,12 @@ class LocalClient(Client):
def main():
optinfo = Bcfg2.Options.CLIENT_COMMON_OPTIONS
optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
+ if 'bundle_quick' in optinfo:
+ # CLIENT_BUNDLEQUICK option uses -Q, just like the server repo
+ # option. the server repo is more important for this
+ # application.
+ optinfo['bundle_quick'] = Bcfg2.Options.Option('bundlequick',
+ default=False)
setup = Bcfg2.Options.OptionParser(optinfo)
setup.parse(sys.argv[1:])
diff --git a/tools/export.py b/tools/export.py
index 716c831d9..7c3c56db2 100755
--- a/tools/export.py
+++ b/tools/export.py
@@ -136,8 +136,7 @@ E.G. 1.2.0pre1 is a valid version.
tarname = '/tmp/%s-%s.tar.gz' % (pkgname, version)
- newchangelog = \
-"""bcfg2 (%s-0.0) unstable; urgency=low
+ newchangelog = """bcfg2 (%s-0.0) unstable; urgency=low
* New upstream release
@@ -177,7 +176,9 @@ E.G. 1.2.0pre1 is a valid version.
"- New upstream release\n", "\n"]
# write out the new RPM changelog
- specs = ["misc/bcfg2.spec", "misc/bcfg2-selinux.spec", "redhat/bcfg2.spec.in"]
+ specs = ["misc/bcfg2.spec",
+ "misc/bcfg2-selinux.spec",
+ "redhat/bcfg2.spec.in"]
if options.dryrun:
print("*** Add the following to the top of the %%changelog section in %s:\n%s\n"
% (rpmchangelog, " and ".join(specs)))
@@ -227,8 +228,28 @@ E.G. 1.2.0pre1 is a valid version.
'VERSION="%s"\n' % version,
startswith=True,
dryrun=options.dryrun)
- # set new version in setup.py
- find_and_replace('setup.py', 'version=', ' version="%s",\n' % version,
+ # update solaris IPS version
+ find_and_replace('solaris-ips/Makefile', 'VERS=',
+ 'VERS=%s-1\n' % version,
+ startswith=True,
+ dryrun=options.dryrun)
+ find_and_replace('solaris-ips/MANIFEST.bcfg2.header',
+ 'set name=pkg.fmri value="pkg://bcfg2/bcfg2@',
+ 'set name=pkg.fmri value="pkg://bcfg2/bcfg2@%s"' % version,
+ startswith=True,
+ dryrun=options.dryrun)
+ find_and_replace('solaris-ips/MANIFEST.bcfg2-server.header',
+ 'set name=pkg.fmri value="pkg://bcfg2/bcfg2-server@',
+ 'set name=pkg.fmri value="pkg://bcfg2/bcfg2-server@%s"' % version,
+ startswith=True,
+ dryrun=options.dryrun)
+ find_and_replace('solaris-ips/pkginfo.bcfg2', 'VERSION=',
+ 'VERSION="%s"\n' % version,
+ startswith=True,
+ dryrun=options.dryrun)
+ find_and_replace('solaris-ips/pkginfo.bcfg2-server', 'VERSION=',
+ 'VERSION="%s"\n' % version,
+ startswith=True,
dryrun=options.dryrun)
# set new version in Bcfg2/version.py
find_and_replace('src/lib/Bcfg2/version.py',
@@ -252,30 +273,30 @@ E.G. 1.2.0pre1 is a valid version.
else:
find_and_replace('misc/bcfg2.spec', 'Release: ',
'Release: 0.%s.%s\n' %
- (version_info['build'][-1], version_info['build']),
+ (version_info['build'][-1], version_info['build']),
dryrun=options.dryrun)
find_and_replace('misc/bcfg2-selinux.spec', 'Release: ',
'Release: 0.%s.%s\n' %
- (version_info['build'][-1], version_info['build']),
+ (version_info['build'][-1], version_info['build']),
dryrun=options.dryrun)
find_and_replace('misc/bcfg2.spec', '%setup',
'%%setup -q -n %%{name}-%%{version}%s\n' %
- version_info['build'],
+ version_info['build'],
startswith=True,
dryrun=options.dryrun)
find_and_replace('misc/bcfg2-selinux.spec', '%setup',
'%%setup -q -n %%{name}-%%{version}%s\n' %
- version_info['build'],
+ version_info['build'],
startswith=True,
dryrun=options.dryrun)
find_and_replace('misc/bcfg2.spec', 'BuildRoot',
'BuildRoot: %%{_tmppath}/%%{name}-%%{version}%s-%%{release}-root-%%(%%{__id_u} -n)\n' %
- version_info['build'],
+ version_info['build'],
startswith=True,
dryrun=options.dryrun)
find_and_replace('misc/bcfg2-selinux.spec', 'BuildRoot',
'BuildRoot: %%{_tmppath}/%%{name}-%%{version}%s-%%{release}-root-%%(%%{__id_u} -n)\n' %
- version_info['build'],
+ version_info['build'],
startswith=True,
dryrun=options.dryrun)
# fix pre problem noted in
diff --git a/tools/posixusers_baseline.py b/tools/posixusers_baseline.py
index a4abca42d..c45e54f1a 100755
--- a/tools/posixusers_baseline.py
+++ b/tools/posixusers_baseline.py
@@ -61,8 +61,8 @@ def main():
if entry.tag == 'POSIXUser':
entry.set("group", grp.getgrgid(data[3])[0])
for group in users.user_supplementary_groups(entry):
- memberof = lxml.etree.SubElement(entry, "MemberOf")
- memberof.text = group[0]
+ memberof = lxml.etree.SubElement(entry, "MemberOf",
+ group=group[0])
entry.tag = "Bound" + entry.tag
baseline.append(entry)
diff --git a/tools/selinux_baseline.py b/tools/selinux_baseline.py
index b6997bb29..507a16f43 100755
--- a/tools/selinux_baseline.py
+++ b/tools/selinux_baseline.py
@@ -42,7 +42,10 @@ def main():
baseline.append(lxml.etree.Comment("%s entries" % etype))
extra = handler.FindExtra()
for entry in extra:
- entry.tag = "BoundSELinux"
+ if etype != "SEModule":
+ entry.tag = "Bound%s" % etype
+ else:
+ entry.tag = "%s" % etype
baseline.extend(extra)
print(lxml.etree.tostring(baseline, pretty_print=True))
diff --git a/tools/upgrade/1.3/README b/tools/upgrade/1.3/README
index 2831d8f00..1a919f869 100644
--- a/tools/upgrade/1.3/README
+++ b/tools/upgrade/1.3/README
@@ -19,3 +19,8 @@ migrate_dbstats.py
migrate_perms_to_mode.py
- Convert perms attribute to mode (note that if you have info/:info
files, you should run migrate_info.py first)
+
+migrate_probe_groups_to_db.py
+ - Migrate Probe host and group data from XML to DB backend for Metadata
+ and Probe plugins. Does not migrate individual probe return data. Assumes
+ migration to BOTH Metadata and Probe to database backends.
diff --git a/tools/upgrade/1.3/migrate_dbstats.py b/tools/upgrade/1.3/migrate_dbstats.py
index cbd2a6099..07def2ac8 100755
--- a/tools/upgrade/1.3/migrate_dbstats.py
+++ b/tools/upgrade/1.3/migrate_dbstats.py
@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
_our_backend = None
+
def _quote(value):
"""
Quote a string to use as a table name or column
@@ -44,12 +45,12 @@ def _migrate_perms():
fperms = {}
logger.info("Creating FilePerms objects")
- for data in ( ('owner', 'group', 'perms'),
+ for data in (('owner', 'group', 'perms'),
('current_owner', 'current_group', 'current_perms')):
for grp in legacy_models.Reason.objects.values_list(*data).distinct():
if grp in fperms:
continue
- fp = new_models.FilePerms(owner=grp[0], group=grp[1], mode=grp[2])
+ fp = new_models.FilePerms(owner=grp[0], group=grp[1], mode=grp[2])
fp.save()
fperms[grp] = fp
@@ -60,7 +61,7 @@ def _migrate_perms():
def _migrate_transaction(inter, entries, fperms):
"""helper"""
- logger.debug("Migrating interaction %s for %s" %
+ logger.debug("Migrating interaction %s for %s" %
(inter.id, inter.client.name))
newint = new_models.Interaction(id=inter.id,
@@ -107,7 +108,7 @@ def _migrate_transaction(inter, entries, fperms):
elif ent.kind == 'Package':
act_dict['target_version'] = ei.reason.version
act_dict['current_version'] = ei.reason.current_version
- logger.debug("Adding package %s %s" %
+ logger.debug("Adding package %s %s" %
(name, act_dict['target_version']))
updates['packages'].append(new_models.PackageEntry.entry_get_or_create(act_dict))
elif ent.kind == 'Path':
@@ -116,7 +117,7 @@ def _migrate_transaction(inter, entries, fperms):
act_dict['target_perms'] = fperms[(
ei.reason.owner,
- ei.reason.group,
+ ei.reason.group,
ei.reason.perms
)]
@@ -141,7 +142,6 @@ def _migrate_transaction(inter, entries, fperms):
act_dict['detail_type'] = new_models.PathEntry.DETAIL_PRUNED
act_dict['details'] = ei.reason.unpruned
-
if ei.reason.is_sensitive:
act_dict['detail_type'] = new_models.PathEntry.DETAIL_SENSITIVE
elif ei.reason.is_binary:
@@ -164,7 +164,7 @@ def _migrate_transaction(inter, entries, fperms):
for entry_type in updates.keys():
i = 0
while(i < len(updates[entry_type])):
- getattr(newint, entry_type).add(*updates[entry_type][i:i+100])
+ getattr(newint, entry_type).add(*updates[entry_type][i:i + 100])
i += 100
for perf in inter.performance_items.all():
@@ -220,8 +220,8 @@ def _restructure():
# run any migrations from the previous schema
try:
- from Bcfg2.Server.Reports.updatefix import update_database
- update_database()
+ from Bcfg2.Server.Reports.updatefix import update_database
+ update_database()
except:
logger.error("Failed to run legacy schema updates", exc_info=1)
return False
@@ -295,4 +295,3 @@ if __name__ == '__main__':
Reports(setup).__call__(['update'])
_restructure()
-
diff --git a/tools/upgrade/1.3/migrate_info.py b/tools/upgrade/1.3/migrate_info.py
index e72599daf..3ccbf0285 100755
--- a/tools/upgrade/1.3/migrate_info.py
+++ b/tools/upgrade/1.3/migrate_info.py
@@ -1,12 +1,16 @@
#!/usr/bin/env python
import os
+import re
import sys
import lxml.etree
import Bcfg2.Options
from Bcfg2.Server.Plugin import INFO_REGEX
+PERMS_REGEX = re.compile(r'perms:\s*(?P<perms>\w+)')
+
+
def convert(info_file):
info_xml = os.path.join(os.path.dirname(info_file), "info.xml")
if os.path.exists(info_xml):
@@ -16,7 +20,7 @@ def convert(info_file):
fileinfo = lxml.etree.Element("FileInfo")
info = lxml.etree.SubElement(fileinfo, "Info")
for line in open(info_file).readlines():
- match = INFO_REGEX.match(line)
+ match = INFO_REGEX.match(line) or PERMS_REGEX.match(line)
if match:
mgd = match.groupdict()
for key, value in list(mgd.items()):
diff --git a/tools/upgrade/1.3/migrate_perms_to_mode.py b/tools/upgrade/1.3/migrate_perms_to_mode.py
index e061558d3..18abffec2 100755
--- a/tools/upgrade/1.3/migrate_perms_to_mode.py
+++ b/tools/upgrade/1.3/migrate_perms_to_mode.py
@@ -13,6 +13,7 @@ def setmodeattr(elem):
elem.set('mode', elem.get('perms'))
del elem.attrib['perms']
return True
+ return False
def writefile(f, xdata):
@@ -32,7 +33,7 @@ def convertinfo(ifile):
return
found = False
for i in xdata.findall('//Info'):
- found = setmodeattr(i)
+ found |= setmodeattr(i)
if found:
writefile(ifile, xdata)
@@ -47,7 +48,7 @@ def convertstructure(structfile):
return
found = False
for path in xdata.xpath('//BoundPath|//Path'):
- found = setmodeattr(path)
+ found |= setmodeattr(path)
if found:
writefile(structfile, xdata)
diff --git a/tools/upgrade/1.3/migrate_probe_groups_to_db.py b/tools/upgrade/1.3/migrate_probe_groups_to_db.py
new file mode 100755
index 000000000..73339e787
--- /dev/null
+++ b/tools/upgrade/1.3/migrate_probe_groups_to_db.py
@@ -0,0 +1,68 @@
+#!/bin/env python
+""" Migrate Probe host and group data from XML to DB backend for Metadata
+and Probe plugins. Does not migrate individual probe return data. Assumes
+migration to BOTH Metadata and Probe to database backends. """
+
+import os
+os.environ['DJANGO_SETTINGS_MODULE'] = 'Bcfg2.settings'
+
+import lxml.etree
+import sys
+import Bcfg2.Options
+
+from Bcfg2.Server.Plugins.Metadata import MetadataClientModel
+from Bcfg2.Server.Plugins.Probes import ProbesGroupsModel
+
+def migrate(xclient):
+ """ Helper to do the migration given a <Client/> XML element """
+ client_name = xclient.get('name')
+ try:
+ try:
+ client = MetadataClientModel.objects.get(hostname=client_name)
+ except MetadataClientModel.DoesNotExist:
+ client = MetadataClientModel(hostname=client_name)
+ client.save()
+ except:
+ print("Failed to migrate client %s" % (client))
+ return False
+
+ try:
+ cgroups = []
+ for xgroup in xclient.findall('Group'):
+ group_name = xgroup.get('name')
+ cgroups.append(group_name)
+ try:
+ group = ProbesGroupsModel.objects.get(hostname=client_name, group=group_name)
+ except ProbesGroupsModel.DoesNotExist:
+ group = ProbesGroupsModel(hostname=client_name, group=group_name)
+ group.save()
+
+ ProbesGroupsModel.objects.filter(
+ hostname=client.hostname).exclude(
+ group__in=cgroups).delete()
+
+ except:
+ print("Failed to migrate groups")
+ return False
+ return True
+
+def main():
+ """ Main """
+ opts = dict(repo=Bcfg2.Options.SERVER_REPOSITORY)
+ setup = Bcfg2.Options.OptionParser(opts)
+ setup.parse(sys.argv[1:])
+
+ probefile = os.path.join(setup['repo'], 'Probes', "probed.xml")
+
+ try:
+ xdata = lxml.etree.parse(probefile)
+ except lxml.etree.XMLSyntaxError:
+ err = sys.exc_info()[1]
+ print("Could not parse %s, skipping: %s" % (probefile, err))
+
+ for xclient in xdata.findall('Client'):
+ print "Migrating Metadata and Probe groups for %s" % xclient.get('name')
+ migrate(xclient)
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tools/yum-listpkgs-xml.py b/tools/yum-listpkgs-xml.py
index a052e75af..b4c5f6589 100755
--- a/tools/yum-listpkgs-xml.py
+++ b/tools/yum-listpkgs-xml.py
@@ -39,5 +39,5 @@ try:
sys.argv = [sys.argv[0], '-d', '0', 'list']
yummain.main(sys.argv[1:])
except KeyboardInterrupt:
- print("\n\nExiting on user cancel.", file=sys.stderr)
+ sys.stderr.write("\n\nExiting on user cancel.")
sys.exit(1)