summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-03-21 14:35:04 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-03-21 14:35:04 -0400
commit38f99b402d3118e2c26ff42d72ddf9b666adac13 (patch)
tree705d18bed66337192816325facb1733280291ff5
parenta3f2a731bbef5d9c2033eed23432aa7792d47989 (diff)
parent71d7285c405bd639f1f9f2642ea8fb567b97caec (diff)
downloadbcfg2-38f99b402d3118e2c26ff42d72ddf9b666adac13.tar.gz
bcfg2-38f99b402d3118e2c26ff42d72ddf9b666adac13.tar.bz2
bcfg2-38f99b402d3118e2c26ff42d72ddf9b666adac13.zip
Merge branch '1.3.2' into maint
Conflicts: src/lib/Bcfg2/Client/Tools/__init__.py src/sbin/bcfg2-test testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py testsuite/Testsrc/test_code_checks.py
-rw-r--r--doc/appendix/guides/authentication.txt33
-rw-r--r--doc/development/client-driver.txt9
-rw-r--r--doc/development/plugins.txt8
-rw-r--r--doc/development/utils.txt15
-rw-r--r--doc/server/plugins/grouping/metadata.txt46
-rw-r--r--src/lib/Bcfg2/Client/Client.py26
-rw-r--r--src/lib/Bcfg2/Client/Tools/APK.py7
-rw-r--r--src/lib/Bcfg2/Client/Tools/APT.py16
-rw-r--r--src/lib/Bcfg2/Client/Tools/Action.py11
-rw-r--r--src/lib/Bcfg2/Client/Tools/Chkconfig.py34
-rw-r--r--src/lib/Bcfg2/Client/Tools/DebInit.py59
-rw-r--r--src/lib/Bcfg2/Client/Tools/Encap.py11
-rw-r--r--src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py2
-rw-r--r--src/lib/Bcfg2/Client/Tools/MacPorts.py5
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py120
-rw-r--r--src/lib/Bcfg2/Client/Tools/Pacman.py5
-rw-r--r--src/lib/Bcfg2/Client/Tools/Portage.py16
-rw-r--r--src/lib/Bcfg2/Client/Tools/RPM.py67
-rw-r--r--src/lib/Bcfg2/Client/Tools/RcUpdate.py24
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py32
-rw-r--r--src/lib/Bcfg2/Client/Tools/SMF.py73
-rw-r--r--src/lib/Bcfg2/Client/Tools/SYSV.py58
-rw-r--r--src/lib/Bcfg2/Client/Tools/Systemd.py31
-rw-r--r--src/lib/Bcfg2/Client/Tools/Upstart.py10
-rw-r--r--src/lib/Bcfg2/Client/Tools/YUM24.py33
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py74
-rw-r--r--src/lib/Bcfg2/Client/Tools/launchd.py110
-rw-r--r--src/lib/Bcfg2/Compat.py4
-rw-r--r--src/lib/Bcfg2/Logger.py3
-rw-r--r--src/lib/Bcfg2/Options.py61
-rw-r--r--src/lib/Bcfg2/Proxy.py3
-rw-r--r--src/lib/Bcfg2/Reporting/Storage/DjangoORM.py372
-rw-r--r--src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py485
-rw-r--r--src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py340
-rw-r--r--src/lib/Bcfg2/Reporting/models.py225
-rw-r--r--src/lib/Bcfg2/Reporting/templates/config_items/item.html31
-rw-r--r--src/lib/Bcfg2/Reporting/views.py24
-rw-r--r--src/lib/Bcfg2/Server/Core.py93
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/__init__.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugin/base.py18
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py31
-rw-r--r--src/lib/Bcfg2/Server/Plugin/interfaces.py1
-rw-r--r--src/lib/Bcfg2/Server/Plugins/DBStats.py5
-rw-r--r--src/lib/Bcfg2/Server/Plugins/GroupPatterns.py23
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Metadata.py59
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Packages/Apt.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Reporting.py5
-rw-r--r--src/lib/Bcfg2/Utils.py225
-rwxr-xr-xsrc/sbin/bcfg2-admin1
-rwxr-xr-xsrc/sbin/bcfg2-crypt2
-rwxr-xr-xsrc/sbin/bcfg2-info3
-rwxr-xr-xsrc/sbin/bcfg2-server3
-rwxr-xr-xsrc/sbin/bcfg2-test213
-rwxr-xr-xsrc/sbin/bcfg2-yum-helper4
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py122
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py53
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py21
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py22
-rw-r--r--testsuite/Testsrc/Testlib/TestUtils.py48
-rw-r--r--testsuite/Testsrc/test_code_checks.py260
60 files changed, 2612 insertions, 1087 deletions
diff --git a/doc/appendix/guides/authentication.txt b/doc/appendix/guides/authentication.txt
index 3fd0e1e2d..b8ec82590 100644
--- a/doc/appendix/guides/authentication.txt
+++ b/doc/appendix/guides/authentication.txt
@@ -132,13 +132,26 @@ controlled through the use of the auth attribute in
Allowed values are:
- +---------------+------------------------------------------+
- | **Auth Type** | **Meaning** |
- +===============+==========================================+
- | cert | Certificates must be used |
- +---------------+------------------------------------------+
- | cert+password | Certificate or password may be used |
- +---------------+------------------------------------------+
- | bootstrap | Password can be used for one client run, |
- | | after that certificate is required |
- +---------------+------------------------------------------+
++-------------------+------------------------------------------+
+| Auth Type | Meaning |
++===================+==========================================+
+| ``cert`` | Certificates must be used |
++-------------------+------------------------------------------+
+| ``cert+password`` | Certificate or password may be used. If |
+| | a certificate is used, the password must |
+| | also be used. |
++-------------------+------------------------------------------+
+| ``bootstrap`` | Password can be used for one client run, |
+| | after that only certificate is allowed |
++-------------------+------------------------------------------+
+
+``cert+password`` is the default. This can be changed by setting the
+``authentication`` parameter in the ``[communcation]`` section of
+``bcfg2.conf``. For instance, to set ``bootstrap`` mode as the global
+default, you would add the following to ``bcfg2.conf``::
+
+ [communication]
+ authentication = bootstrap
+
+``bootstrap`` mode is currently incompatible with the
+:ref:`server-plugins-grouping-metadata-clients-database`.
diff --git a/doc/development/client-driver.txt b/doc/development/client-driver.txt
index 29216acd5..5977f2a91 100644
--- a/doc/development/client-driver.txt
+++ b/doc/development/client-driver.txt
@@ -65,6 +65,11 @@ Base Classes
Helper Classes
--------------
-.. autoclass:: Bcfg2.Client.Tools.ClassName
-.. autoclass:: Bcfg2.Client.Tools.Executor
.. autoclass:: Bcfg2.Client.Tools.ToolInstantiationError
+
+See Also
+--------
+
+* :ref:`development-compat`
+* :ref:`development-utils`
+
diff --git a/doc/development/plugins.txt b/doc/development/plugins.txt
index 91a4e6868..593c2f83e 100644
--- a/doc/development/plugins.txt
+++ b/doc/development/plugins.txt
@@ -175,7 +175,7 @@ decorate functions that you would like to track execution times for:
.. code-block:: python
from Bcfg2.Server.Plugin import track_statistics
-
+
@track_statistics()
def do_something(self, ...):
...
@@ -208,3 +208,9 @@ Plugin Exceptions
-----------------
.. automodule:: Bcfg2.Server.Plugin.exceptions
+
+See Also
+--------
+
+* :ref:`development-compat`
+* :ref:`development-utils
diff --git a/doc/development/utils.txt b/doc/development/utils.txt
new file mode 100644
index 000000000..a4c158bf0
--- /dev/null
+++ b/doc/development/utils.txt
@@ -0,0 +1,15 @@
+.. -*- mode: rst -*-
+
+.. _development-utils:
+
+================
+Common Utilities
+================
+
+Some helper functions, classes, etc., are useful to both the client
+and server. Some of these are used to maintain
+:ref:`development-compat`, and should go in ``Bcfg2.Compat``. Those
+that aren't strictly for Python compatibility go in ``Bcfg2.Utils``,
+which is documented below.
+
+.. automodule:: Bcfg2.Utils
diff --git a/doc/server/plugins/grouping/metadata.txt b/doc/server/plugins/grouping/metadata.txt
index a6ed37f8e..f4c5cbcb3 100644
--- a/doc/server/plugins/grouping/metadata.txt
+++ b/doc/server/plugins/grouping/metadata.txt
@@ -32,7 +32,7 @@ clients.xml
===========
The ``clients.xml`` file contains the mappings of Profile Groups
-to clients. The file is just a series of *<Client />* tags, each of which
+to clients. The file is just a series of ``<Client />`` tags, each of which
describe one host. A sample file is below:
.. code-block:: xml
@@ -43,7 +43,7 @@ describe one host. A sample file is below:
<Client profile="kerberos-master" name="kdc.example.com"/>
<Client profile="mail-server" name="mail.example.com"/>
<Client name='foo' address='10.0.0.1'>
- <Alias name='foo-mgmt' address='10.1.0.1'/>
+ <Alias name='foo-mgmt' address='10.1.0.1'/>
</Client>
</Clients>
@@ -197,9 +197,9 @@ useful results:
.. code-block:: xml
- <Groups version='3.0' xmlns:xi="http://www.w3.org/2001/XInclude">
- <xi:include href="my-groups.xml" />
- <xi:include href="their-groups.xml" />
+ <Groups xmlns:xi="http://www.w3.org/2001/XInclude">
+ <xi:include href="my-groups.xml" />
+ <xi:include href="their-groups.xml" />
</Groups>
Each of the included groups files has the same format. These files are
@@ -207,6 +207,42 @@ properly validated by ``bcfg2-lint``. This mechanism is useful for
composing group definitions from multiple sources, or setting
different permissions in an svn repository.
+You can also optionally include a file that may or may not exist with
+the ``fallback`` tag:
+
+.. code-block:: xml
+
+ <Groups xmlns:xi="http://www.w3.org/2001/XInclude">
+ <xi:include href="my-groups.xml"/>
+ <xi:include href="their-groups.xml"><xi:fallback/></xi:include>
+ </Groups>
+
+In this case, if ``their-groups.xml`` does not exist, no error will be
+raised and everything will work fine. (You can also use ``fallback``
+to include a different file, or explicit content in the case that the
+parent include does not exist.)
+
+Wildcard XInclude
+~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.3.1
+
+Bcfg2 supports an extension to XInclude that allows you to use shell
+globbing in the hrefs. (Stock XInclude doesn't support this, since
+the href is supposed to be a URL.)
+
+For instance:
+
+ <Groups xmlns:xi="http://www.w3.org/2001/XInclude">
+ <xi:include href="groups/*.xml"/>
+ </Groups>
+
+This would include all ``*.xml`` files in the ``groups`` subdirectory.
+
+Note that if a glob finds no files, that is treated the same as if a
+single included file does not exist. You should use the ``fallback``
+tag, described above, if a glob may potentially find no files.
+
Probes
======
diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py
index 45e0b64e6..88f3bd6ef 100644
--- a/src/lib/Bcfg2/Client/Client.py
+++ b/src/lib/Bcfg2/Client/Client.py
@@ -14,9 +14,9 @@ import Bcfg2.Options
import Bcfg2.Client.XML
import Bcfg2.Client.Frame
import Bcfg2.Client.Tools
+from Bcfg2.Utils import locked, Executor
from Bcfg2.Compat import xmlrpclib
from Bcfg2.version import __version__
-from subprocess import Popen, PIPE
class Client(object):
@@ -41,6 +41,9 @@ class Client(object):
to_file=self.setup['logging'])
self.logger = logging.getLogger('bcfg2')
self.logger.debug(self.setup)
+
+ self.cmd = Executor(self.setup['command_timeout'])
+
if self.setup['bundle_quick']:
if not self.setup['bundle'] and not self.setup['skipbundle']:
self.logger.error("-Q option requires -b or -B")
@@ -94,16 +97,14 @@ class Client(object):
stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH |
stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH |
stat.S_IWUSR) # 0755
- proc = Popen(scriptname, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- ret.text, err = proc.communicate()
- rv = proc.wait()
- if err:
+ rv = self.cmd.run(scriptname, timeout=self.setup['timeout'])
+ if rv.stderr:
self.logger.warning("Probe %s has error output: %s" %
- (name, err))
- if rv:
+ (name, rv.stderr))
+ if not rv.success:
self._probe_failure(name, "Return value %s" % rv)
self.logger.info("Probe %s has result:" % name)
- self.logger.info(ret.text)
+ self.logger.info(rv.stdout)
finally:
os.unlink(scriptname)
except SystemExit:
@@ -288,11 +289,7 @@ class Client(object):
#check lock here
try:
lockfile = open(self.setup['lockfile'], 'w')
- try:
- fcntl.lockf(lockfile.fileno(),
- fcntl.LOCK_EX | fcntl.LOCK_NB)
- except IOError:
- # otherwise exit and give a warning to the user
+ if locked(lockfile.fileno()):
self.fatal_error("Another instance of Bcfg2 is running. "
"If you want to bypass the check, run "
"with the %s option" %
@@ -301,7 +298,8 @@ class Client(object):
raise
except:
lockfile = None
- self.logger.error("Failed to open lockfile")
+ self.logger.error("Failed to open lockfile %s: %s" %
+ (self.setup['lockfile'], sys.exc_info()[1]))
# execute the configuration
self.tools.Execute()
diff --git a/src/lib/Bcfg2/Client/Tools/APK.py b/src/lib/Bcfg2/Client/Tools/APK.py
index f23fbb119..8a02b7d6d 100644
--- a/src/lib/Bcfg2/Client/Tools/APK.py
+++ b/src/lib/Bcfg2/Client/Tools/APK.py
@@ -19,8 +19,8 @@ class APK(Bcfg2.Client.Tools.PkgTool):
def RefreshPackages(self):
"""Refresh memory hashes of packages."""
- names = self.cmd.run("/sbin/apk info")[1]
- nameversions = self.cmd.run("/sbin/apk info -v")[1]
+ names = self.cmd.run("/sbin/apk info").stdout.splitlines()
+ nameversions = self.cmd.run("/sbin/apk info -v").stdout.splitlines()
for pkg in zip(names, nameversions):
pkgname = pkg[0]
version = pkg[1][len(pkgname) + 1:]
@@ -56,7 +56,6 @@ class APK(Bcfg2.Client.Tools.PkgTool):
"""Remove extra packages."""
names = [pkg.get('name') for pkg in packages]
self.logger.info("Removing packages: %s" % " ".join(names))
- self.cmd.run("/sbin/apk del %s" % \
- " ".join(names))
+ self.cmd.run("/sbin/apk del %s" % " ".join(names))
self.RefreshPackages()
self.extra = self.FindExtra()
diff --git a/src/lib/Bcfg2/Client/Tools/APT.py b/src/lib/Bcfg2/Client/Tools/APT.py
index 879d2720a..0cdefa613 100644
--- a/src/lib/Bcfg2/Client/Tools/APT.py
+++ b/src/lib/Bcfg2/Client/Tools/APT.py
@@ -59,7 +59,8 @@ class APT(Bcfg2.Client.Tools.Tool):
os.environ["DEBIAN_FRONTEND"] = 'noninteractive'
self.actions = {}
if self.setup['kevlar'] and not self.setup['dryrun']:
- self.cmd.run("%s --force-confold --configure --pending" % self.dpkg)
+ self.cmd.run("%s --force-confold --configure --pending" %
+ self.dpkg)
self.cmd.run("%s clean" % self.aptget)
try:
self.pkg_cache = apt.cache.Cache()
@@ -88,13 +89,15 @@ class APT(Bcfg2.Client.Tools.Tool):
for (name, version) in extras]
def VerifyDebsums(self, entry, modlist):
- output = self.cmd.run("%s -as %s" % (self.debsums,
- entry.get('name')))[1]
+ output = \
+ self.cmd.run("%s -as %s" %
+ (self.debsums, entry.get('name'))).stdout.splitlines()
if len(output) == 1 and "no md5sums for" in output[0]:
self.logger.info("Package %s has no md5sums. Cannot verify" % \
entry.get('name'))
- entry.set('qtext', "Reinstall Package %s-%s to setup md5sums? (y/N) " \
- % (entry.get('name'), entry.get('version')))
+ entry.set('qtext',
+ "Reinstall Package %s-%s to setup md5sums? (y/N) " %
+ (entry.get('name'), entry.get('version')))
return False
files = []
for item in output:
@@ -250,8 +253,7 @@ class APT(Bcfg2.Client.Tools.Tool):
self.logger.error(bad_pkgs)
if not ipkgs:
return
- rc = self.cmd.run(self.pkgcmd % (" ".join(ipkgs)))[0]
- if rc:
+ if not self.cmd.run(self.pkgcmd % (" ".join(ipkgs))):
self.logger.error("APT command failed")
self.pkg_cache = apt.cache.Cache()
self.extra = self.FindExtra()
diff --git a/src/lib/Bcfg2/Client/Tools/Action.py b/src/lib/Bcfg2/Client/Tools/Action.py
index b1a897c81..87565d96d 100644
--- a/src/lib/Bcfg2/Client/Tools/Action.py
+++ b/src/lib/Bcfg2/Client/Tools/Action.py
@@ -49,14 +49,11 @@ class Action(Bcfg2.Client.Tools.Tool):
"to build mode" % entry.get('command'))
return False
self.logger.debug("Running Action %s" % (entry.get('name')))
- rv = self.cmd.run(entry.get('command'))[0]
+ rv = self.cmd.run(entry.get('command'))
self.logger.debug("Action: %s got return code %s" %
- (entry.get('command'), rv))
- entry.set('rc', str(rv))
- if entry.get('status', 'check') == 'ignore':
- return True
- else:
- return rv == 0
+ (entry.get('command'), rv.retval))
+ entry.set('rc', str(rv.retval))
+ return entry.get('status', 'check') == 'ignore' or rv.success
else:
self.logger.debug("In dryrun mode: not running action: %s" %
(entry.get('name')))
diff --git a/src/lib/Bcfg2/Client/Tools/Chkconfig.py b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
index e1ad35861..ec7f462b3 100644
--- a/src/lib/Bcfg2/Client/Tools/Chkconfig.py
+++ b/src/lib/Bcfg2/Client/Tools/Chkconfig.py
@@ -24,16 +24,14 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
if entry.get('status') == 'ignore':
return True
- try:
- cmd = "/sbin/chkconfig --list %s " % (entry.get('name'))
- raw = self.cmd.run(cmd)[1]
- patterns = ["error reading information", "unknown service"]
- srvdata = [line.split() for line in raw for pattern in patterns \
- if pattern not in line][0]
- except IndexError:
- # Ocurrs when no lines are returned (service not installed)
+ 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')
return False
+
if len(srvdata) == 2:
# This is an xinetd service
if entry.get('status') == srvdata[1]:
@@ -43,7 +41,7 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
return False
try:
- onlevels = [level.split(':')[0] for level in srvdata[1:] \
+ onlevels = [level.split(':')[0] for level in srvdata[1:]
if level.split(':')[1] == 'on']
except IndexError:
onlevels = []
@@ -70,25 +68,25 @@ class Chkconfig(Bcfg2.Client.Tools.SvcTool):
if entry.get('status') == 'off':
rv &= self.cmd.run((rcmd + " --level 0123456") %
(entry.get('name'),
- entry.get('status')))[0] == 0
+ entry.get('status'))).success
if entry.get("current_status") == "on":
- rv &= self.stop_service(entry)
+ rv &= self.stop_service(entry).success
else:
rv &= self.cmd.run(rcmd % (entry.get('name'),
- entry.get('status')))[0] == 0
+ entry.get('status'))).success
if entry.get("current_status") == "off":
- rv &= (self.start_service(entry) == 0)
+ rv &= self.start_service(entry).success
return rv
def FindExtra(self):
"""Locate extra chkconfig Services."""
allsrv = [line.split()[0]
- for line in self.cmd.run("/sbin/chkconfig "
- "--list 2>/dev/null|grep :on")[1]]
+ for line in self.cmd.run("/sbin/chkconfig",
+ "--list").stdout.splitlines()
+ if ":on" in line]
self.logger.debug('Found active services:')
self.logger.debug(allsrv)
specified = [srv.get('name') for srv in self.getSupportedEntries()]
- return [Bcfg2.Client.XML.Element('Service',
- type='chkconfig',
- name=name) \
+ return [Bcfg2.Client.XML.Element('Service', type='chkconfig',
+ name=name)
for name in allsrv if name not in specified]
diff --git a/src/lib/Bcfg2/Client/Tools/DebInit.py b/src/lib/Bcfg2/Client/Tools/DebInit.py
index 7d5af1127..ca556e98b 100644
--- a/src/lib/Bcfg2/Client/Tools/DebInit.py
+++ b/src/lib/Bcfg2/Client/Tools/DebInit.py
@@ -15,7 +15,8 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
__execs__ = ['/usr/sbin/update-rc.d', '/usr/sbin/invoke-rc.d']
__handles__ = [('Service', 'deb')]
__req__ = {'Service': ['name', 'status']}
- svcre = re.compile("/etc/.*/(?P<action>[SK])(?P<sequence>\d+)(?P<name>\S+)")
+ svcre = \
+ re.compile("/etc/.*/(?P<action>[SK])(?P<sequence>\d+)(?P<name>\S+)")
# implement entry (Verify|Install) ops
def VerifyService(self, entry, _):
@@ -28,7 +29,7 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
files = []
try:
- deb_version = open('/etc/debian_version', 'r').read().split('/', 1)[0]
+ deb_version = open('/etc/debian_version').read().split('/', 1)[0]
except IOError:
deb_version = 'unknown'
@@ -59,20 +60,20 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
return False
else:
return True
+ elif files:
+ if start_sequence:
+ for filename in files:
+ match = self.svcre.match(filename)
+ file_sequence = int(match.group('sequence'))
+ if ((match.group('action') == 'S' and
+ file_sequence != start_sequence) or
+ (match.group('action') == 'K' and
+ file_sequence != kill_sequence)):
+ return False
+ return True
else:
- if files:
- if start_sequence:
- for filename in files:
- match = self.svcre.match(filename)
- file_sequence = int(match.group('sequence'))
- if match.group('action') == 'S' and file_sequence != start_sequence:
- return False
- if match.group('action') == 'K' and file_sequence != kill_sequence:
- return False
- return True
- else:
- entry.set('current_status', 'off')
- return False
+ entry.set('current_status', 'off')
+ return False
def InstallService(self, entry):
"""Install Service for entry."""
@@ -80,35 +81,35 @@ class DebInit(Bcfg2.Client.Tools.SvcTool):
try:
os.stat('/etc/init.d/%s' % entry.get('name'))
except OSError:
- self.logger.debug("Init script for service %s does not exist" % entry.get('name'))
+ self.logger.debug("Init script for service %s does not exist" %
+ entry.get('name'))
return False
if entry.get('status') == 'off':
self.cmd.run("/usr/sbin/invoke-rc.d %s stop" % (entry.get('name')))
- cmdrc = self.cmd.run("/usr/sbin/update-rc.d -f %s remove" % entry.get('name'))[0]
+ return self.cmd.run("/usr/sbin/update-rc.d -f %s remove" %
+ entry.get('name')).success
else:
command = "/usr/sbin/update-rc.d %s defaults" % (entry.get('name'))
if entry.get('sequence'):
- cmdrc = self.cmd.run("/usr/sbin/update-rc.d -f %s remove" % entry.get('name'))[0]
- if cmdrc != 0:
+ 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)
- cmdrc = self.cmd.run(command)[0]
- return cmdrc == 0
+ return self.cmd.run(command).success
def FindExtra(self):
"""Find Extra Debian Service entries."""
specified = [entry.get('name') for entry in self.getSupportedEntries()]
- extra = []
- for name in [self.svcre.match(fname).group('name') for fname in
- glob.glob("/etc/rc[12345].d/S*") \
- if self.svcre.match(fname).group('name') not in specified]:
- if name not in extra:
- extra.append(name)
- return [Bcfg2.Client.XML.Element('Service', name=name, type='deb') for name \
- in extra]
+ extra = set()
+ for fname in glob.glob("/etc/rc[12345].d/S*"):
+ name = self.svcre.match(fname).group('name')
+ if name not in specified:
+ extra.add(name)
+ return [Bcfg2.Client.XML.Element('Service', name=name, type='deb')
+ for name in list(extra)]
def Remove(self, _):
"""Remove extra service entries."""
diff --git a/src/lib/Bcfg2/Client/Tools/Encap.py b/src/lib/Bcfg2/Client/Tools/Encap.py
index ca6fc7653..678e0f00c 100644
--- a/src/lib/Bcfg2/Client/Tools/Encap.py
+++ b/src/lib/Bcfg2/Client/Tools/Encap.py
@@ -33,14 +33,13 @@ class Encap(Bcfg2.Client.Tools.PkgTool):
self.logger.info("Insufficient information of Package %s; "
"cannot Verify" % entry.get('name'))
return False
- cmdrc = self.cmd.run("/usr/local/bin/epkg -q -S -k %s-%s >/dev/null" %
- (entry.get('name'), entry.get('version')))[0]
- if cmdrc != 0:
+ success = self.cmd.run("/usr/local/bin/epkg -q -S -k %s-%s" %
+ (entry.get('name'),
+ entry.get('version'))).success
+ if not success:
self.logger.debug("Package %s version incorrect" %
entry.get('name'))
- else:
- return True
- return False
+ return success
def Remove(self, packages):
"""Deal with extra configuration detected."""
diff --git a/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py b/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
index ded84bef4..635318805 100644
--- a/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
+++ b/src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py
@@ -20,7 +20,7 @@ class FreeBSDPackage(Bcfg2.Client.Tools.PkgTool):
def RefreshPackages(self):
self.installed = {}
- packages = self.cmd.run("/usr/sbin/pkg_info -a -E")[1]
+ packages = self.cmd.run("/usr/sbin/pkg_info -a -E").stdout.splitlines()
pattern = re.compile('(.*)-(\d.*)')
for pkg in packages:
if pattern.match(pkg):
diff --git a/src/lib/Bcfg2/Client/Tools/MacPorts.py b/src/lib/Bcfg2/Client/Tools/MacPorts.py
index be441135e..bc3765ec6 100644
--- a/src/lib/Bcfg2/Client/Tools/MacPorts.py
+++ b/src/lib/Bcfg2/Client/Tools/MacPorts.py
@@ -19,7 +19,8 @@ class MacPorts(Bcfg2.Client.Tools.PkgTool):
def RefreshPackages(self):
"""Refresh memory hashes of packages."""
- pkgcache = self.cmd.run("/opt/local/bin/port installed")[1]
+ pkgcache = self.cmd.run(["/opt/local/bin/port",
+ "installed"]).stdout.splitlines()
self.installed = {}
for pkg in pkgcache:
if pkg.startswith("Warning:"):
@@ -65,7 +66,7 @@ class MacPorts(Bcfg2.Client.Tools.PkgTool):
"""Remove extra packages."""
names = [pkg.get('name') for pkg in packages]
self.logger.info("Removing packages: %s" % " ".join(names))
- self.cmd.run("/opt/local/bin/port uninstall %s" % \
+ self.cmd.run("/opt/local/bin/port uninstall %s" %
" ".join(names))
self.RefreshPackages()
self.extra = self.FindExtra()
diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
index 7c8a4d578..99ed3c7d9 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
@@ -1,95 +1,11 @@
""" A tool to handle creating users and groups with useradd/mod/del
and groupadd/mod/del """
-import sys
import pwd
import grp
-import subprocess
import Bcfg2.Client.XML
import Bcfg2.Client.Tools
-from Bcfg2.Compat import any # pylint: disable=W0622
-
-
-class IDRangeSet(object):
- """ Representation of a set of integer ranges. Used to describe
- which UID/GID ranges are managed or unmanaged. """
-
- def __init__(self, *ranges):
- self.ranges = []
- self.ints = []
- self.str = ",".join(str(r) for r in ranges)
- for item in ranges:
- item = str(item).strip()
- if item.endswith("-"):
- self.ranges.append((int(item[:-1]), None))
- elif '-' in str(item):
- self.ranges.append(tuple(int(x) for x in item.split('-')))
- else:
- self.ints.append(int(item))
-
- def __contains__(self, other):
- other = int(other)
- if other in self.ints:
- return True
- return any((end is None and other >= start) or
- (end is not None and other >= start and other <= end)
- for start, end in self.ranges)
-
- def __repr__(self):
- return "%s:%s" % (self.__class__.__name__, str(self))
-
- def __str__(self):
- return "[%s]" % self.str
-
- def __len__(self):
- return len(self.ranges) + len(self.ints)
-
-
-class ExecutionError(Exception):
- """ Raised when running an external command fails """
-
- def __init__(self, msg, retval=None):
- Exception.__init__(self, msg)
- self.retval = retval
-
- def __str__(self):
- return "%s (rv: %s)" % (Exception.__str__(self),
- self.retval)
-
-
-class Executor(object):
- """ A better version of Bcfg2.Client.Tool.Executor, which captures
- stderr, raises exceptions on error, and doesn't use the shell to
- execute by default """
-
- def __init__(self, logger):
- self.logger = logger
- self.stdout = None
- self.stderr = None
- self.retval = None
-
- def run(self, command, inputdata=None, shell=False):
- """ Run a command, given as a list, optionally giving it the
- specified input data """
- self.logger.debug("Running: %s" % " ".join(command))
- proc = subprocess.Popen(command, shell=shell, bufsize=16384,
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE, close_fds=True)
- if inputdata:
- for line in inputdata.splitlines():
- self.logger.debug('> %s' % line)
- (self.stdout, self.stderr) = proc.communicate(inputdata)
- else:
- (self.stdout, self.stderr) = proc.communicate()
- for line in self.stdout.splitlines(): # pylint: disable=E1103
- self.logger.debug('< %s' % line)
- self.retval = proc.wait()
- if self.retval == 0:
- for line in self.stderr.splitlines(): # pylint: disable=E1103
- self.logger.warning(line)
- return True
- else:
- raise ExecutionError(self.stderr, self.retval)
+from Bcfg2.Utils import PackedDigitRange
class POSIXUsers(Bcfg2.Client.Tools.Tool):
@@ -118,22 +34,21 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
self.set_defaults = dict(POSIXUser=self.populate_user_entry,
POSIXGroup=lambda g: g)
- self.cmd = Executor(logger)
self._existing = None
self._whitelist = dict(POSIXUser=None, POSIXGroup=None)
self._blacklist = dict(POSIXUser=None, POSIXGroup=None)
if self.setup['posix_uid_whitelist']:
self._whitelist['POSIXUser'] = \
- IDRangeSet(*self.setup['posix_uid_whitelist'])
+ PackedDigitRange(*self.setup['posix_uid_whitelist'])
else:
self._blacklist['POSIXUser'] = \
- IDRangeSet(*self.setup['posix_uid_blacklist'])
+ PackedDigitRange(*self.setup['posix_uid_blacklist'])
if self.setup['posix_gid_whitelist']:
self._whitelist['POSIXGroup'] = \
- IDRangeSet(*self.setup['posix_gid_whitelist'])
+ PackedDigitRange(*self.setup['posix_gid_whitelist'])
else:
self._blacklist['POSIXGroup'] = \
- IDRangeSet(*self.setup['posix_gid_blacklist'])
+ PackedDigitRange(*self.setup['posix_gid_blacklist'])
@property
def existing(self):
@@ -309,16 +224,14 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
action = "add"
else:
action = "mod"
- try:
- self.cmd.run(self._get_cmd(action,
- self.set_defaults[entry.tag](entry)))
+ rv = self.cmd.run(self._get_cmd(action,
+ self.set_defaults[entry.tag](entry)))
+ if rv.success:
self.modified.append(entry)
- return True
- except ExecutionError:
+ else:
self.logger.error("POSIXUsers: Error creating %s %s: %s" %
- (entry.tag, entry.get("name"),
- sys.exc_info()[1]))
- return False
+ (entry.tag, entry.get("name"), rv.error))
+ return rv.success
def _get_cmd(self, action, entry):
""" Get a command to perform the appropriate action (add, mod,
@@ -373,11 +286,8 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool):
def _remove(self, entry):
""" Remove an entry """
- try:
- self.cmd.run(self._get_cmd("del", entry))
- return True
- except ExecutionError:
+ rv = self.cmd.run(self._get_cmd("del", entry))
+ if not rv.success:
self.logger.error("POSIXUsers: Error deleting %s %s: %s" %
- (entry.tag, entry.get("name"),
- sys.exc_info()[1]))
- return False
+ (entry.tag, entry.get("name"), rv.error))
+ return rv.success
diff --git a/src/lib/Bcfg2/Client/Tools/Pacman.py b/src/lib/Bcfg2/Client/Tools/Pacman.py
index 9c14a3de6..12785afee 100644
--- a/src/lib/Bcfg2/Client/Tools/Pacman.py
+++ b/src/lib/Bcfg2/Client/Tools/Pacman.py
@@ -20,9 +20,8 @@ class Pacman(Bcfg2.Client.Tools.PkgTool):
def RefreshPackages(self):
'''Refresh memory hashes of packages'''
- pkgcache = self.cmd.run("/usr/bin/pacman -Q")[1]
self.installed = {}
- for pkg in pkgcache:
+ for pkg in self.cmd.run("/usr/bin/pacman -Q").stdout.splitlines():
pkgname = pkg.split(' ')[0].strip()
version = pkg.split(' ')[1].strip()
#self.logger.info(" pkgname: %s, version: %s" % (pkgname, version))
@@ -62,7 +61,7 @@ class Pacman(Bcfg2.Client.Tools.PkgTool):
'''Remove extra packages'''
names = [pkg.get('name') for pkg in packages]
self.logger.info("Removing packages: %s" % " ".join(names))
- self.cmd.run("%s --noconfirm --noprogressbar -R %s" % \
+ self.cmd.run("%s --noconfirm --noprogressbar -R %s" %
(self.pkgtool, " ".join(names)))
self.RefreshPackages()
self.extra = self.FindExtra()
diff --git a/src/lib/Bcfg2/Client/Tools/Portage.py b/src/lib/Bcfg2/Client/Tools/Portage.py
index 9381f44e9..6cbcff2e0 100644
--- a/src/lib/Bcfg2/Client/Tools/Portage.py
+++ b/src/lib/Bcfg2/Client/Tools/Portage.py
@@ -3,6 +3,7 @@
import re
import Bcfg2.Client.Tools
+
class Portage(Bcfg2.Client.Tools.PkgTool):
"""The Gentoo toolset implements package and service operations and
inherits the rest from Toolset.Toolset."""
@@ -35,9 +36,8 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
if not self._initialised:
return
self.logger.info('Getting list of installed packages')
- cache = self.cmd.run("equery -q list '*'")[1]
self.installed = {}
- for pkg in cache:
+ for pkg in self.cmd.run("equery -q list '*'").stdout.splitlines():
if self._pkg_pattern.match(pkg):
name = self._pkg_pattern.match(pkg).group(1)
version = self._pkg_pattern.match(pkg).group(2)
@@ -73,12 +73,12 @@ class Portage(Bcfg2.Client.Tools.PkgTool):
self.logger.debug('Running equery check on %s' %
entry.get('name'))
- output = self.cmd.run("/usr/bin/equery -N check '=%s-%s' "
- "2>&1 | grep '!!!' | awk '{print $2}'"
- % ((entry.get('name'), version)))[1]
- if [filename for filename in output \
- if filename not in modlist]:
- return False
+ for line in self.cmd.run(["/usr/bin/equery", "-N", "check",
+ '=%s-%s' %
+ (entry.get('name'),
+ version)]).stdout.splitlines():
+ if '!!!' in line and line.split()[1] not in modlist:
+ return False
# By now the package must be in one of the following states:
# - Not require checking
diff --git a/src/lib/Bcfg2/Client/Tools/RPM.py b/src/lib/Bcfg2/Client/Tools/RPM.py
index 3d93149ff..a4dd2b730 100644
--- a/src/lib/Bcfg2/Client/Tools/RPM.py
+++ b/src/lib/Bcfg2/Client/Tools/RPM.py
@@ -80,13 +80,12 @@ class RPM(Bcfg2.Client.Tools.PkgTool):
# Many, if not most package verifies can be caused by out of
# date prelinking.
if os.path.isfile('/usr/sbin/prelink') and not self.setup['dryrun']:
- cmdrc, output = self.cmd.run('/usr/sbin/prelink -a -mR')
- if cmdrc == 0:
+ rv = self.cmd.run('/usr/sbin/prelink -a -mR')
+ if rv.success:
self.logger.debug('Pre-emptive prelink succeeded')
else:
# FIXME : this is dumb - what if the output is huge?
- self.logger.error('Pre-emptive prelink failed: %s' % output)
-
+ self.logger.error('Pre-emptive prelink failed: %s' % rv.error)
def RefreshPackages(self):
"""
@@ -593,29 +592,26 @@ class RPM(Bcfg2.Client.Tools.PkgTool):
# Fix installOnlyPackages
if len(install_only_pkgs) > 0:
self.logger.info("Attempting to install 'install only packages'")
- install_args = " ".join([os.path.join(self.instance_status[inst].get('pkg').get('uri'), \
- inst.get('simplefile')) \
- for inst in install_only_pkgs])
- self.logger.debug("rpm --install --quiet --oldpackage %s" % install_args)
- cmdrc, output = self.cmd.run("rpm --install --quiet --oldpackage --replacepkgs %s" % \
- install_args)
- if cmdrc == 0:
+ install_args = \
+ " ".join(os.path.join(self.instance_status[inst].get('pkg').get('uri'),
+ inst.get('simplefile'))
+ for inst in install_only_pkgs)
+ if self.cmd.run("rpm --install --quiet --oldpackage --replacepkgs "
+ "%s" % install_args):
# The rpm command succeeded. All packages installed.
self.logger.info("Single Pass for InstallOnlyPkgs Succeded")
self.RefreshPackages()
-
else:
# The rpm command failed. No packages installed.
# Try installing instances individually.
self.logger.error("Single Pass for InstallOnlyPackages Failed")
installed_instances = []
for inst in install_only_pkgs:
- install_args = os.path.join(self.instance_status[inst].get('pkg').get('uri'), \
- inst.get('simplefile'))
- self.logger.debug("rpm --install --quiet --oldpackage %s" % install_args)
- cmdrc, output = self.cmd.run("rpm --install --quiet --oldpackage --replacepkgs %s" % \
- install_args)
- if cmdrc == 0:
+ install_args = \
+ os.path.join(self.instance_status[inst].get('pkg').get('uri'),
+ inst.get('simplefile'))
+ if self.cmd.run("rpm --install --quiet --oldpackage "
+ "--replacepkgs %s" % install_args):
installed_instances.append(inst)
else:
self.logger.debug("InstallOnlyPackage %s %s would not install." % \
@@ -632,15 +628,15 @@ class RPM(Bcfg2.Client.Tools.PkgTool):
self.logger.info("Installing GPG keys.")
key_arg = os.path.join(self.instance_status[inst].get('pkg').get('uri'), \
inst.get('simplefile'))
- cmdrc, output = self.cmd.run("rpm --import %s" % key_arg)
- if cmdrc != 0:
- self.logger.debug("Unable to install %s-%s" % \
- (self.instance_status[inst].get('pkg').get('name'), \
- self.str_evra(inst)))
+ if not self.cmd.run("rpm --import %s" % key_arg):
+ self.logger.debug("Unable to install %s-%s" %
+ (self.instance_status[inst].get('pkg').get('name'),
+ self.str_evra(inst)))
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')))
self.RefreshPackages()
self.gpg_keyids = self.getinstalledgpg()
pkg = self.instance_status[gpg_keys[0]].get('pkg')
@@ -652,13 +648,12 @@ class RPM(Bcfg2.Client.Tools.PkgTool):
upgrade_args = " ".join([os.path.join(self.instance_status[inst].get('pkg').get('uri'), \
inst.get('simplefile')) \
for inst in upgrade_pkgs])
- cmdrc, output = self.cmd.run("rpm --upgrade --quiet --oldpackage --replacepkgs %s" % \
- upgrade_args)
- if cmdrc == 0:
+ if self.cmd.run("rpm --upgrade --quiet --oldpackage --replacepkgs "
+ "%s" % upgrade_args):
# The rpm command succeeded. All packages upgraded.
self.logger.info("Single Pass for Upgraded Packages Succeded")
- upgrade_pkg_set = set([self.instance_status[inst].get('pkg') \
- for inst in upgrade_pkgs])
+ upgrade_pkg_set = set([self.instance_status[inst].get('pkg')
+ for inst in upgrade_pkgs])
self.RefreshPackages()
else:
# The rpm command failed. No packages upgraded.
@@ -670,13 +665,13 @@ class RPM(Bcfg2.Client.Tools.PkgTool):
inst.get('simplefile'))
#self.logger.debug("rpm --upgrade --quiet --oldpackage --replacepkgs %s" % \
# upgrade_args)
- cmdrc, output = self.cmd.run("rpm --upgrade --quiet --oldpackage --replacepkgs %s" % upgrade_args)
- if cmdrc == 0:
+ if self.cmd.run("rpm --upgrade --quiet --oldpackage "
+ "--replacepkgs %s" % upgrade_args):
upgraded_instances.append(inst)
else:
- self.logger.debug("Package %s %s would not upgrade." % \
- (self.instance_status[inst].get('pkg').get('name'), \
- self.str_evra(inst)))
+ self.logger.debug("Package %s %s would not upgrade." %
+ (self.instance_status[inst].get('pkg').get('name'),
+ self.str_evra(inst)))
upgrade_pkg_set = set([self.instance_status[inst].get('pkg') \
for inst in upgrade_pkgs])
diff --git a/src/lib/Bcfg2/Client/Tools/RcUpdate.py b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
index d5cef6e34..2e58f2564 100644
--- a/src/lib/Bcfg2/Client/Tools/RcUpdate.py
+++ b/src/lib/Bcfg2/Client/Tools/RcUpdate.py
@@ -23,8 +23,7 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
# check if service is enabled
cmd = '/sbin/rc-update show default | grep %s'
- rv = self.cmd.run(cmd % entry.get('name'))[0]
- is_enabled = (rv == 0)
+ is_enabled = self.cmd.run(cmd % entry.get('name')).success
# check if init script exists
try:
@@ -36,8 +35,7 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
# check if service is enabled
cmd = '/etc/init.d/%s status | grep started'
- rv = self.cmd.run(cmd % entry.attrib['name'])[0]
- is_running = (rv == 0)
+ is_running = self.cmd.run(cmd % entry.attrib['name']).success
if entry.get('status') == 'on' and not (is_enabled and is_running):
entry.set('current_status', 'off')
@@ -60,27 +58,25 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool):
self.start_service(entry)
# make sure it's enabled
cmd = '/sbin/rc-update add %s default'
- rv = self.cmd.run(cmd % entry.get('name'))[0]
- return (rv == 0)
-
+ 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'
- rv = self.cmd.run(cmd % entry.get('name'))[0]
- return (rv == 0)
+ return self.cmd.run(cmd % entry.get('name')).success
return False
def FindExtra(self):
"""Locate extra rc-update services."""
- cmd = '/bin/rc-status -s | grep started'
- allsrv = [line.split()[0] for line in self.cmd.run(cmd)[1]]
+ cmd = '/bin/rc-status -s'
+ allsrv = [line.split()[0]
+ for line in self.cmd.run(cmd).stdout.splitlines()
+ if 'started' in line]
self.logger.debug('Found active services:')
self.logger.debug(allsrv)
specified = [srv.get('name') for srv in self.getSupportedEntries()]
- return [Bcfg2.Client.XML.Element('Service',
- type='rc-update',
- name=name) \
+ return [Bcfg2.Client.XML.Element('Service', type='rc-update',
+ name=name)
for name in allsrv if name not in specified]
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py
index f2b868316..451495be2 100644
--- a/src/lib/Bcfg2/Client/Tools/SELinux.py
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -12,7 +12,6 @@ import seobject
import Bcfg2.Client.XML
import Bcfg2.Client.Tools
from Bcfg2.Client.Tools.POSIX.File import POSIXFile
-from subprocess import Popen, PIPE
def pack128(int_val):
@@ -734,9 +733,7 @@ class SELinuxSemoduleHandler(SELinuxEntryHandler):
self._all = dict()
self.logger.debug("SELinux: Getting modules from semodule")
try:
- proc = Popen(['semodule', '-l'], stdout=PIPE, stderr=PIPE)
- out = proc.communicate()[0]
- rv = proc.wait()
+ rv = self.tool.cmd.run(['semodule', '-l'])
except OSError:
# semanage failed; probably not in $PATH. try to
# get the list of modules from the filesystem
@@ -745,13 +742,9 @@ class SELinuxSemoduleHandler(SELinuxEntryHandler):
err)
self._all.update(self._all_records_from_filesystem())
else:
- if rv:
- self.logger.error("SELinux: Failed to run semodule: %s"
- % err)
- self._all.update(self._all_records_from_filesystem())
- else:
+ if rv.success:
# ran semodule successfully
- for line in out.splitlines():
+ for line in rv.stdout.splitlines():
mod, version = line.split()
self._all[mod] = (version, 1)
@@ -759,6 +752,10 @@ class SELinuxSemoduleHandler(SELinuxEntryHandler):
for mod in self._all_records_from_filesystem().keys():
if mod not in self._all:
self._all[mod] = ('', 0)
+ else:
+ self.logger.error("SELinux: Failed to run semodule: %s"
+ % rv.error)
+ self._all.update(self._all_records_from_filesystem())
return self._all
def _all_records_from_filesystem(self):
@@ -870,26 +867,23 @@ class SELinuxSemoduleHandler(SELinuxEntryHandler):
self.logger.debug("Install SELinux module %s with semodule -i %s" %
(entry.get('name'), self._filepath(entry)))
try:
- proc = Popen(['semodule', '-i', self._filepath(entry)],
- stdout=PIPE, stderr=PIPE)
- err = proc.communicate()[1]
- rv = proc.wait()
+ rv = self.tool.cmd.run(['semodule', '-i', self._filepath(entry)])
except OSError:
err = sys.exc_info()[1]
self.logger.error("Failed to install SELinux module %s with "
"semodule: %s" % (entry.get("name"), err))
return False
- if rv:
- self.logger.error("Failed to install SELinux module %s with "
- "semodule: %s" % (entry.get("name"), err))
- return False
- else:
+ if rv.success:
if entry.get("disabled", "false").lower() == "true":
self.logger.warning("SELinux: Cannot disable modules with "
"semodule")
return False
else:
return True
+ else:
+ self.logger.error("Failed to install SELinux module %s with "
+ "semodule: %s" % (entry.get("name"), rv.error))
+ return False
def _addargs(self, entry):
""" argument list for adding entries """
diff --git a/src/lib/Bcfg2/Client/Tools/SMF.py b/src/lib/Bcfg2/Client/Tools/SMF.py
index 4409b40f3..68d8b2965 100644
--- a/src/lib/Bcfg2/Client/Tools/SMF.py
+++ b/src/lib/Bcfg2/Client/Tools/SMF.py
@@ -26,21 +26,20 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
def GetFMRI(self, entry):
"""Perform FMRI resolution for service."""
if not 'FMRI' in entry.attrib:
- name = self.cmd.run("/usr/bin/svcs -H -o FMRI %s 2>/dev/null" % \
- entry.get('name'))[1]
- if name:
- entry.set('FMRI', name[0])
- return True
+ rv = self.cmd.run(["/usr/bin/svcs", "-H", "-o", "FMRI",
+ entry.get('name')])
+ if rv.success:
+ entry.set('FMRI', rv.stdout.splitlines()[0])
else:
- self.logger.info('Failed to locate FMRI for service %s' % \
+ self.logger.info('Failed to locate FMRI for service %s' %
entry.get('name'))
- return False
+ return rv.success
return True
def VerifyService(self, entry, _):
"""Verify SMF Service entry."""
if not self.GetFMRI(entry):
- self.logger.error("smf service %s doesn't have FMRI set" % \
+ self.logger.error("smf service %s doesn't have FMRI set" %
entry.get('name'))
return False
if entry.get('FMRI').startswith('lrc'):
@@ -57,8 +56,9 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
(entry.get("FMRI")))
return entry.get('status') == 'off'
try:
- srvdata = self.cmd.run("/usr/bin/svcs -H -o STA %s" % \
- entry.get('FMRI'))[1][0].split()
+ srvdata = \
+ self.cmd.run("/usr/bin/svcs -H -o STA %s" %
+ entry.get('FMRI')).stdout.splitlines()[0].split()
except IndexError:
# Occurs when no lines are returned (service not installed)
return False
@@ -85,31 +85,30 @@ class SMF(Bcfg2.Client.Tools.SvcTool):
(loc))
return False
else:
- cmdrc = self.cmd.run("/usr/sbin/svcadm disable %s" % \
- (entry.get('FMRI')))[0]
+ return self.cmd.run("/usr/sbin/svcadm disable %s" %
+ entry.get('FMRI')).success
+ elif entry.get('FMRI').startswith('lrc'):
+ loc = entry.get("FMRI")[4:].replace('_', '.')
+ try:
+ os.stat(loc.replace('/S', '/Disabled.'))
+ self.logger.debug("Renaming file %s to %s" %
+ (loc.replace('/S', '/DISABLED.S'), loc))
+ os.rename(loc.replace('/S', '/DISABLED.S'), loc)
+ return True
+ except OSError:
+ self.logger.debug("Failed to rename %s to %s" %
+ (loc.replace('/S', '/DISABLED.S'), loc))
+ return False
else:
- if entry.get('FMRI').startswith('lrc'):
- loc = entry.get("FMRI")[4:].replace('_', '.')
- try:
- os.stat(loc.replace('/S', '/Disabled.'))
- self.logger.debug("Renaming file %s to %s" % \
- (loc.replace('/S', '/DISABLED.S'), loc))
- os.rename(loc.replace('/S', '/DISABLED.S'), loc)
- cmdrc = 0
- except OSError:
- self.logger.debug("Failed to rename %s to %s" % \
- (loc.replace('/S', '/DISABLED.S'), loc))
- cmdrc = 1
+ srvdata = \
+ self.cmd.run("/usr/bin/svcs -H -o STA %s" %
+ entry.get('FMRI'))[1].splitlines()[0].split()
+ if srvdata[0] == 'MNT':
+ cmdarg = 'clear'
else:
- srvdata = self.cmd.run("/usr/bin/svcs -H -o STA %s" %
- entry.get('FMRI'))[1][0].split()
- if srvdata[0] == 'MNT':
- cmdarg = 'clear'
- else:
- cmdarg = 'enable'
- cmdrc = self.cmd.run("/usr/sbin/svcadm %s -r %s" % \
- (cmdarg, entry.get('FMRI')))[0]
- return cmdrc == 0
+ cmdarg = 'enable'
+ return self.cmd.run("/usr/sbin/svcadm %s -r %s" %
+ (cmdarg, entry.get('FMRI'))).success
def Remove(self, svcs):
"""Remove Extra SMF entries."""
@@ -120,12 +119,14 @@ 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")[1]]
+ [srvc.split()
+ for srvc in self.cmd.run([
+ "/usr/bin/svcs", "-a", "-H",
+ "-o", "FMRI,STATE"]).stdout.splitlines()]
if version != 'disabled']
for svc in self.getSupportedEntries():
if svc.get("FMRI") in allsrv:
allsrv.remove(svc.get('FMRI'))
- return [Bcfg2.Client.XML.Element("Service", type='smf', name=name) \
+ return [Bcfg2.Client.XML.Element("Service", type='smf', name=name)
for name in allsrv]
diff --git a/src/lib/Bcfg2/Client/Tools/SYSV.py b/src/lib/Bcfg2/Client/Tools/SYSV.py
index 9b84a14cc..38072c52e 100644
--- a/src/lib/Bcfg2/Client/Tools/SYSV.py
+++ b/src/lib/Bcfg2/Client/Tools/SYSV.py
@@ -1,11 +1,11 @@
"""This provides bcfg2 support for Solaris SYSV packages."""
import tempfile
-
+from Bcfg2.Compat import any # pylint: disable=W0622
import Bcfg2.Client.Tools
import Bcfg2.Client.XML
-
+# pylint: disable=C0103
noask = '''
mail=
instance=overwrite
@@ -19,6 +19,7 @@ conflict=nocheck
action=nocheck
basedir=default
'''
+# pylint: enable=C0103
class SYSV(Bcfg2.Client.Tools.PkgTool):
@@ -42,14 +43,14 @@ class SYSV(Bcfg2.Client.Tools.PkgTool):
self.noaskfile.flush()
self.pkgtool = (self.pkgtool[0] % ("-a %s" % (self.noaskname)), \
self.pkgtool[1])
- except:
- self.pkgtool = (self.pkgtool[0] % (""), self.pkgtool[1])
+ except: # pylint: disable=W0702
+ self.pkgtool = (self.pkgtool[0] % "", self.pkgtool[1])
def RefreshPackages(self):
"""Refresh memory hashes of packages."""
self.installed = {}
# Build list of packages
- lines = self.cmd.run("/usr/bin/pkginfo -x")[1]
+ lines = self.cmd.run("/usr/bin/pkginfo -x").stdout.splitlines()
while lines:
# Splitting on whitespace means that packages with spaces in
# their version numbers don't work right. Found this with
@@ -62,35 +63,36 @@ class SYSV(Bcfg2.Client.Tools.PkgTool):
def VerifyPackage(self, entry, modlist):
"""Verify Package status for entry."""
- if not entry.get('version'):
- self.logger.info("Insufficient information of Package %s; cannot Verify" % entry.get('name'))
- return False
-
- desiredVersion = entry.get('version')
- if desiredVersion == 'any':
- desiredVersion = self.installed.get(entry.get('name'), desiredVersion)
-
- cmdrc = self.cmd.run("/usr/bin/pkginfo -q -v \"%s\" %s" % \
- (desiredVersion, entry.get('name')))[0]
+ desired_version = entry.get('version')
+ if desired_version == 'any':
+ desired_version = self.installed.get(entry.get('name'),
+ desired_version)
- if cmdrc != 0:
+ if not self.cmd.run(["/usr/bin/pkginfo", "-q", "-v",
+ desired_version, entry.get('name')]):
if entry.get('name') in self.installed:
- self.logger.debug("Package %s version incorrect: have %s want %s" \
- % (entry.get('name'), self.installed[entry.get('name')],
- desiredVersion))
+ self.logger.debug("Package %s version incorrect: "
+ "have %s want %s" %
+ (entry.get('name'),
+ self.installed[entry.get('name')],
+ desired_version))
else:
- self.logger.debug("Package %s not installed" % (entry.get("name")))
+ self.logger.debug("Package %s not installed" %
+ entry.get("name"))
else:
- if self.setup['quick'] or entry.attrib.get('verify', 'true') == 'false':
+ if (self.setup['quick'] or
+ entry.attrib.get('verify', 'true') == 'false'):
return True
- (vstat, odata) = self.cmd.run("/usr/sbin/pkgchk -n %s" % (entry.get('name')))
- if vstat == 0:
+ rv = self.cmd.run("/usr/sbin/pkgchk -n %s" % entry.get('name'))
+ if rv.success:
return True
else:
- output = [line for line in odata if line[:5] == 'ERROR']
- if len([name for name in output if name.split()[-1] not in modlist]):
- self.logger.debug("Package %s content verification failed" % \
- (entry.get('name')))
+ output = [line for line in rv.stdout.splitlines()
+ if line[:5] == 'ERROR']
+ if any(name for name in output
+ if name.split()[-1] not in modlist):
+ self.logger.debug("Package %s content verification failed"
+ % entry.get('name'))
else:
return True
return False
@@ -99,7 +101,7 @@ class SYSV(Bcfg2.Client.Tools.PkgTool):
"""Remove specified Sysv packages."""
names = [pkg.get('name') for pkg in packages]
self.logger.info("Removing packages: %s" % (names))
- self.cmd.run("/usr/sbin/pkgrm -a %s -n %s" % \
+ self.cmd.run("/usr/sbin/pkgrm -a %s -n %s" %
(self.noaskname, names))
self.RefreshPackages()
self.extra = self.FindExtra()
diff --git a/src/lib/Bcfg2/Client/Tools/Systemd.py b/src/lib/Bcfg2/Client/Tools/Systemd.py
index 43eca2eac..027d91c71 100644
--- a/src/lib/Bcfg2/Client/Tools/Systemd.py
+++ b/src/lib/Bcfg2/Client/Tools/Systemd.py
@@ -5,6 +5,7 @@
import Bcfg2.Client.Tools
import Bcfg2.Client.XML
+
class Systemd(Bcfg2.Client.Tools.SvcTool):
"""Systemd support for Bcfg2."""
name = 'Systemd'
@@ -21,35 +22,25 @@ class Systemd(Bcfg2.Client.Tools.SvcTool):
return True
cmd = "/bin/systemctl status %s.service " % (entry.get('name'))
- raw = ''.join(self.cmd.run(cmd)[1])
+ rv = self.cmd.run(cmd)
- if raw.find('Loaded: error') >= 0:
+ if 'Loaded: error' in rv.stdout:
entry.set('current_status', 'off')
- status = False
-
- elif raw.find('Active: active') >= 0:
+ return False
+ elif 'Active: active' in rv.stdout:
entry.set('current_status', 'on')
- if entry.get('status') == 'off':
- status = False
- else:
- status = True
-
+ return entry.get('status') == 'on'
else:
entry.set('current_status', 'off')
- if entry.get('status') == 'on':
- status = False
- else:
- status = True
-
- return status
+ return entry.get('status') == 'off'
def InstallService(self, entry):
"""Install Service entry."""
if entry.get('status') == 'on':
- rv = self.cmd.run(self.get_svc_command(entry, 'enable'))[0] == 0
- rv &= self.cmd.run(self.get_svc_command(entry, 'start'))[0] == 0
+ rv = self.cmd.run(self.get_svc_command(entry, 'enable')).success
+ rv &= self.cmd.run(self.get_svc_command(entry, 'start')).success
else:
- rv = self.cmd.run(self.get_svc_command(entry, 'stop'))[0] == 0
- rv &= self.cmd.run(self.get_svc_command(entry, 'disable'))[0] == 0
+ rv = self.cmd.run(self.get_svc_command(entry, 'stop')).success
+ rv &= self.cmd.run(self.get_svc_command(entry, 'disable')).success
return rv
diff --git a/src/lib/Bcfg2/Client/Tools/Upstart.py b/src/lib/Bcfg2/Client/Tools/Upstart.py
index 02ed52544..cd1c4a2bc 100644
--- a/src/lib/Bcfg2/Client/Tools/Upstart.py
+++ b/src/lib/Bcfg2/Client/Tools/Upstart.py
@@ -39,7 +39,8 @@ class Upstart(Bcfg2.Client.Tools.SvcTool):
try:
output = self.cmd.run('/usr/sbin/service %s status %s' %
- (entry.get('name'), params))[1][0]
+ (entry.get('name'),
+ params)).stdout.splitlines()[0]
except IndexError:
self.logger.error("Service %s not an Upstart service" %
entry.get('name'))
@@ -71,11 +72,10 @@ class Upstart(Bcfg2.Client.Tools.SvcTool):
def InstallService(self, entry):
"""Install Service for entry."""
if entry.get('status') == 'on':
- pstatus = self.cmd.run(self.get_svc_command(entry, 'start'))[0]
+ cmd = "start"
elif entry.get('status') == 'off':
- pstatus = self.cmd.run(self.get_svc_command(entry, 'stop'))[0]
- # pstatus is true if command failed
- return not pstatus
+ cmd = "stop"
+ return self.cmd.run(self.get_svc_command(entry, cmd)).success
def FindExtra(self):
"""Locate extra Upstart services."""
diff --git a/src/lib/Bcfg2/Client/Tools/YUM24.py b/src/lib/Bcfg2/Client/Tools/YUM24.py
index cd25ecf37..d78127ddd 100644
--- a/src/lib/Bcfg2/Client/Tools/YUM24.py
+++ b/src/lib/Bcfg2/Client/Tools/YUM24.py
@@ -237,8 +237,7 @@ class YUM24(RPM):
continue
key_arg = os.path.join(self.instance_status[inst].get('pkg').get('uri'), \
inst.get('simplefile'))
- cmdrc, output = self.cmd.run("rpm --import %s" % key_arg)
- if cmdrc != 0:
+ if self.cmd.run("rpm --import %s" % key_arg).success:
self.logger.debug("Unable to install %s-%s" % \
(self.instance_status[inst].get('pkg').get('name'), \
self.str_evra(inst)))
@@ -265,8 +264,7 @@ class YUM24(RPM):
pkg_arg = self.instance_status[inst].get('pkg').get('name')
install_args.append(build_yname(pkg_arg, inst))
- cmdrc, output = self.cmd.run(pkgtool % " ".join(install_args))
- if cmdrc == 0:
+ if self.cmd.run(pkgtool % " ".join(install_args)).success:
# The yum command succeeded. All packages installed.
self.logger.info("Single Pass for Install Succeeded")
self.RefreshPackages()
@@ -278,12 +276,11 @@ class YUM24(RPM):
for inst in install_pkgs:
pkg_arg = build_yname(self.instance_status[inst].get('pkg').get('name'), inst)
- cmdrc, output = self.cmd.run(pkgtool % pkg_arg)
- if cmdrc == 0:
+ if self.cmd.run(pkgtool % pkg_arg).success:
installed_instances.append(inst)
else:
- self.logger.debug("%s %s would not install." % \
- (self.instance_status[inst].get('pkg').get('name'), \
+ self.logger.debug("%s %s would not install." %
+ (self.instance_status[inst].get('pkg').get('name'),
self.str_evra(inst)))
self.RefreshPackages()
@@ -301,8 +298,7 @@ class YUM24(RPM):
pkg_arg = build_yname(self.instance_status[inst].get('pkg').get('name'), inst)
upgrade_args.append(pkg_arg)
- cmdrc, output = self.cmd.run(pkgtool % " ".join(upgrade_args))
- if cmdrc == 0:
+ if self.cmd.run(pkgtool % " ".join(upgrade_args)).success:
# The yum command succeeded. All packages installed.
self.logger.info("Single Pass for Install Succeeded")
self.RefreshPackages()
@@ -313,8 +309,7 @@ class YUM24(RPM):
installed_instances = []
for inst in upgrade_pkgs:
pkg_arg = build_yname(self.instance_status[inst].get('pkg').get('name'), inst)
- cmdrc, output = self.cmd.run(pkgtool % pkg_arg)
- if cmdrc == 0:
+ if self.cmd.run(pkgtool % pkg_arg).success:
installed_instances.append(inst)
else:
self.logger.debug("%s %s would not install." % \
@@ -365,14 +360,14 @@ class YUM24(RPM):
% (pkgspec.get('name'), self.str_evra(pkgspec)))
self.logger.info(" This package will be deleted in a future version of the YUM24 driver.")
- cmdrc, output = self.cmd.run(pkgtool % " ".join(erase_args))
- if cmdrc == 0:
+ rv = self.cmd.run(pkgtool % " ".join(erase_args))
+ if rv.success:
self.modified += packages
for pkg in erase_args:
self.logger.info("Deleted %s" % (pkg))
else:
self.logger.info("Bulk erase failed with errors:")
- self.logger.debug("Erase results = %s" % output)
+ self.logger.debug("Erase results: %s" % rv.error)
self.logger.info("Attempting individual erase for each package.")
for pkg in packages:
pkg_modified = False
@@ -390,13 +385,13 @@ class YUM24(RPM):
self.logger.info(" This package will be deleted in a future version of the YUM24 driver.")
continue
- cmdrc, output = self.cmd.run(self.pkgtool % pkg_arg)
- if cmdrc == 0:
+ rv = self.cmd.run(self.pkgtool % pkg_arg)
+ if rv.success:
pkg_modified = True
self.logger.info("Deleted %s" % pkg_arg)
else:
- self.logger.error("unable to delete %s" % pkg_arg)
- self.logger.debug("Failure = %s" % output)
+ self.logger.error("Unable to delete %s" % pkg_arg)
+ self.logger.debug("Failure: %s" % rv.error)
if pkg_modified == True:
self.modified.append(pkg)
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index 08dc09294..a4a68ea3b 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -3,9 +3,9 @@
import os
import sys
import stat
-from subprocess import Popen, PIPE
import Bcfg2.Client
import Bcfg2.Client.XML
+from Bcfg2.Utils import Executor, ClassName
from Bcfg2.Compat import walk_packages # pylint: disable=W0622
__all__ = [m[1] for m in walk_packages(path=__path__)]
@@ -25,48 +25,6 @@ class ToolInstantiationError(Exception):
pass
-class Executor:
- """ This class runs shell commands. """
-
- def __init__(self, logger):
- """
- :param logger: The logger to use to produce debug logging
- :type logger: logging.Logger
- """
- self.logger = logger
-
- def run(self, command):
- """ Run a command inside a shell.
-
- :param command: The command to run, given as a list as to
- :class:`subprocess.Popen`. Since the command
- will be run within a shell it is particularly
- important to pass it as a list.
- :type command: list
- :returns: tuple of return value (integer) and output (list of
- lines)
- """
- self.logger.debug("Running: %s" % command)
- proc = Popen(command, shell=True, bufsize=16384,
- stdin=PIPE, stdout=PIPE, close_fds=True)
- output = proc.communicate()[0].splitlines()
- for line in output:
- self.logger.debug('< %s' % line)
- return (proc.wait(), output)
-
-
-class ClassName(object):
- """ This very simple descriptor class exists only to get the name
- of the owner class. This is used because, for historical reasons,
- we expect every tool to have a ``name`` attribute that is in
- almost all cases the same as the ``__class__.__name__`` attribute
- of the plugin object. This makes that more dynamic so that each
- plugin isn't repeating its own name."""
-
- def __get__(self, inst, owner):
- return owner.__name__
-
-
class Tool(object):
""" The base tool class. All tools subclass this.
@@ -141,9 +99,9 @@ class Tool(object):
#: The XML configuration for this client
self.config = config
- #: An :class:`Bcfg2.Client.Tools.Executor` object for
+ #: An :class:`Bcfg2.Utils.Executor` object for
#: running external commands.
- self.cmd = Executor(logger)
+ self.cmd = Executor(timeout=self.setup['command_timeout'])
#: A list of entries that have been modified by this tool
self.modified = []
@@ -492,9 +450,7 @@ class PkgTool(Tool):
pkgcmd = self._get_package_command(packages)
self.logger.debug("Running command: %s" % pkgcmd)
-
- cmdrc = self.cmd.run(pkgcmd)[0]
- if cmdrc == 0:
+ if self.cmd.run(pkgcmd):
self.logger.info("Single Pass Succeded")
# set all package states to true and flush workqueues
pkgnames = [pkg.get('name') for pkg in packages]
@@ -519,7 +475,7 @@ class PkgTool(Tool):
else:
self.logger.info("Installing pkg %s version %s" %
(pkg.get('name'), pkg.get('version')))
- if self.cmd.run(self._get_package_command([pkg]))[0] == 0:
+ if self.cmd.run(self._get_package_command([pkg])):
states[pkg] = True
else:
self.logger.error("Failed to install package %s" %
@@ -573,7 +529,7 @@ class SvcTool(Tool):
:class:`Bcfg2.Client.Tools.Executor.run`
"""
self.logger.debug('Starting service %s' % service.get('name'))
- return self.cmd.run(self.get_svc_command(service, 'start'))[0]
+ return self.cmd.run(self.get_svc_command(service, 'start'))
def stop_service(self, service):
""" Stop a service.
@@ -584,7 +540,7 @@ class SvcTool(Tool):
:class:`Bcfg2.Client.Tools.Executor.run`
"""
self.logger.debug('Stopping service %s' % service.get('name'))
- return self.cmd.run(self.get_svc_command(service, 'stop'))[0]
+ return self.cmd.run(self.get_svc_command(service, 'stop'))
def restart_service(self, service):
""" Restart a service.
@@ -596,7 +552,7 @@ class SvcTool(Tool):
"""
self.logger.debug('Restarting service %s' % service.get('name'))
restart_target = service.get('target', 'restart')
- return self.cmd.run(self.get_svc_command(service, restart_target))[0]
+ return self.cmd.run(self.get_svc_command(service, restart_target))
def check_service(self, service):
""" Check the status a service.
@@ -606,7 +562,7 @@ class SvcTool(Tool):
:returns: bool - True if the status command returned 0, False
otherwise
"""
- return self.cmd.run(self.get_svc_command(service, 'status'))[0] == 0
+ return self.cmd.run(self.get_svc_command(service, 'status'))
def Remove(self, services):
if self.setup['servicemode'] != 'disabled':
@@ -628,21 +584,21 @@ class SvcTool(Tool):
(restart == "interactive" and not self.setup['interactive'])):
continue
- rv = None
+ success = False
if entry.get('status') == 'on':
if self.setup['servicemode'] == 'build':
- rv = self.stop_service(entry)
+ success = self.stop_service(entry)
elif entry.get('name') not in self.restarted:
if self.setup['interactive']:
if not Bcfg2.Client.prompt('Restart service %s? (y/N) '
% entry.get('name')):
continue
- rv = self.restart_service(entry)
- if not rv:
+ success = self.restart_service(entry)
+ if success:
self.restarted.append(entry.get('name'))
else:
- rv = self.stop_service(entry)
- if rv:
+ success = self.stop_service(entry)
+ if not success:
self.logger.error("Failed to manipulate service %s" %
(entry.get('name')))
BundleUpdated.__doc__ = Tool.BundleUpdated.__doc__
diff --git a/src/lib/Bcfg2/Client/Tools/launchd.py b/src/lib/Bcfg2/Client/Tools/launchd.py
index 0a587da2e..b0661b26b 100644
--- a/src/lib/Bcfg2/Client/Tools/launchd.py
+++ b/src/lib/Bcfg2/Client/Tools/launchd.py
@@ -1,61 +1,58 @@
"""launchd support for Bcfg2."""
import os
-
import Bcfg2.Client.Tools
-class launchd(Bcfg2.Client.Tools.Tool):
- """Support for Mac OS X launchd services."""
+class launchd(Bcfg2.Client.Tools.Tool): # pylint: disable=C0103
+ """Support for Mac OS X launchd services. Currently requires the
+ path to the plist to load/unload, and Name is acually a
+ reverse-fqdn (or the label)."""
__handles__ = [('Service', 'launchd')]
__execs__ = ['/bin/launchctl', '/usr/bin/defaults']
- name = 'launchd'
__req__ = {'Service': ['name', 'status']}
- '''
- Currently requires the path to the plist to load/unload,
- and Name is acually a reverse-fqdn (or the label).
- '''
-
def __init__(self, logger, setup, config):
Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
- '''Locate plist file that provides given reverse-fqdn name
- /Library/LaunchAgents Per-user agents provided by the administrator.
- /Library/LaunchDaemons System wide daemons provided by the administrator.
- /System/Library/LaunchAgents Mac OS X Per-user agents.
- /System/Library/LaunchDaemons Mac OS X System wide daemons.'''
- plistLocations = ["/Library/LaunchDaemons",
- "/System/Library/LaunchDaemons"]
- self.plistMapping = {}
- for directory in plistLocations:
+ # Locate plist file that provides given reverse-fqdn name:
+ #
+ # * ``/Library/LaunchAgents``: Per-user agents provided by the
+ # administrator.
+ # * ``/Library/LaunchDaemons``: System-wide daemons provided
+ # by the administrator.
+ # * ``/System/Library/LaunchAgents``: Mac OS X per-user
+ # agents.
+ # * ``/System/Library/LaunchDaemons``: Mac OS X system-wide
+ # daemons.
+ plist_locations = ["/Library/LaunchDaemons",
+ "/System/Library/LaunchDaemons"]
+ self.plist_mapping = {}
+ for directory in plist_locations:
for daemon in os.listdir(directory):
- try:
- if daemon.endswith(".plist"):
- d = daemon[:-6]
- else:
- d = daemon
- label = self.cmd.run('defaults read %s/%s Label' %
- (directory, d))[1][0]
- self.plistMapping[label] = "%s/%s" % (directory, daemon)
- except KeyError:
- self.logger.warning("Could not get label from %s/%s" %
- (directory, daemon))
+ if daemon.endswith(".plist"):
+ daemon = daemon[:-6]
+ dpath = os.path.join(directory, daemon)
+ rv = self.cmd.run(['defaults', 'read', dpath, 'Label'])
+ if rv.success:
+ label = rv.stdout.splitlines()[0]
+ self.plist_mapping[label] = dpath
+ else:
+ self.logger.warning("Could not get label from %s" % dpath)
def FindPlist(self, entry):
- return self.plistMapping.get(entry.get('name'), None)
+ """ Find the location of the plist file for the given entry """
+ return self.plist_mapping.get(entry.get('name'), None)
def os_version(self):
- version = ""
- try:
- vers = self.cmd.run('sw_vers')[1]
- except:
- return version
-
- for line in vers:
- if line.startswith("ProductVersion"):
- version = line.split()[-1]
- return version
+ """ Determine the OS version """
+ rv = self.cmd.run('sw_vers')
+ if rv:
+ for line in rv.stdout.splitlines():
+ if line.startswith("ProductVersion"):
+ return line.split()[-1]
+ else:
+ return ''
def VerifyService(self, entry, _):
"""Verify launchd service entry."""
@@ -63,7 +60,7 @@ class launchd(Bcfg2.Client.Tools.Tool):
return True
try:
- services = self.cmd.run("/bin/launchctl list")[1]
+ services = self.cmd.run("/bin/launchctl list").stdout.splitlines()
except IndexError:
# happens when no services are running (should be never)
services = []
@@ -93,15 +90,13 @@ class launchd(Bcfg2.Client.Tools.Tool):
name = entry.get('name')
if entry.get('status') == 'on':
self.logger.error("Installing service %s" % name)
- cmdrc = self.cmd.run("/bin/launchctl load -w %s" %
- self.FindPlist(entry))
- cmdrc = self.cmd.run("/bin/launchctl start %s" % name)
+ self.cmd.run("/bin/launchctl load -w %s" % self.FindPlist(entry))
+ return self.cmd.run("/bin/launchctl start %s" % name).success
else:
self.logger.error("Uninstalling service %s" % name)
- cmdrc = self.cmd.run("/bin/launchctl stop %s" % name)
- cmdrc = self.cmd.run("/bin/launchctl unload -w %s" %
- self.FindPlist(entry))
- return cmdrc[0] == 0
+ self.cmd.run("/bin/launchctl stop %s" % name)
+ return self.cmd.run("/bin/launchctl unload -w %s" %
+ self.FindPlist(entry)).success
def Remove(self, svcs):
"""Remove Extra launchd entries."""
@@ -110,23 +105,24 @@ class launchd(Bcfg2.Client.Tools.Tool):
def FindExtra(self):
"""Find Extra launchd services."""
try:
- allsrv = self.cmd.run("/bin/launchctl list")[1]
+ allsrv = self.cmd.run("/bin/launchctl list").stdout.splitlines()
except IndexError:
allsrv = []
- [allsrv.remove(svc) for svc in [entry.get("name") for entry
- in self.getSupportedEntries()] if svc in allsrv]
- return [Bcfg2.Client.XML.Element("Service",
- type='launchd',
- name=name,
- status='on') for name in allsrv]
+ for entry in self.getSupportedEntries():
+ svc = entry.get("name")
+ if svc in allsrv:
+ allsrv.remove(svc)
+ return [Bcfg2.Client.XML.Element("Service", type='launchd', name=name,
+ status='on')
+ for name in allsrv]
def BundleUpdated(self, bundle, states):
"""Reload launchd plist."""
for entry in [entry for entry in bundle if self.handlesEntry(entry)]:
if not self.canInstall(entry):
- self.logger.error("Insufficient information to restart service %s" %
- (entry.get('name')))
+ self.logger.error("Insufficient information to restart "
+ "service %s" % entry.get('name'))
else:
name = entry.get('name')
if entry.get('status') == 'on' and self.FindPlist(entry):
diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py
index 57f96274f..44c76303c 100644
--- a/src/lib/Bcfg2/Compat.py
+++ b/src/lib/Bcfg2/Compat.py
@@ -52,7 +52,8 @@ except ImportError:
# xmlrpc imports
try:
- import xmlrpclib, SimpleXMLRPCServer
+ import xmlrpclib
+ import SimpleXMLRPCServer
except ImportError:
import xmlrpc.client as xmlrpclib
import xmlrpc.server as SimpleXMLRPCServer
@@ -74,6 +75,7 @@ try:
except NameError:
unicode = str
+
def u_str(string, encoding=None):
""" print to file compatibility """
if sys.hexversion >= 0x03000000:
diff --git a/src/lib/Bcfg2/Logger.py b/src/lib/Bcfg2/Logger.py
index 89fa9fb79..8f7bb14f8 100644
--- a/src/lib/Bcfg2/Logger.py
+++ b/src/lib/Bcfg2/Logger.py
@@ -143,6 +143,7 @@ def add_console_handler(level=logging.DEBUG):
console.setLevel(level)
# tell the handler to use this format
console.setFormatter(TermiosFormatter())
+ console.set_name("console")
logging.root.addHandler(console)
@@ -157,6 +158,7 @@ def add_syslog_handler(procname, syslog_facility, level=logging.DEBUG):
syslog = FragmentingSysLogHandler(procname,
('localhost', 514),
syslog_facility)
+ syslog.set_name("syslog")
syslog.setLevel(level)
syslog.setFormatter(
logging.Formatter('%(name)s[%(process)d]: %(message)s'))
@@ -170,6 +172,7 @@ def add_syslog_handler(procname, syslog_facility, level=logging.DEBUG):
def add_file_handler(to_file, level=logging.DEBUG):
"""Add a logging handler that logs to to_file."""
filelog = logging.FileHandler(to_file)
+ filelog.set_name("file")
filelog.setLevel(level)
filelog.setFormatter(
logging.Formatter('%(asctime)s %(name)s[%(process)d]: %(message)s'))
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py
index 5f079f0b4..7c91ca3cc 100644
--- a/src/lib/Bcfg2/Options.py
+++ b/src/lib/Bcfg2/Options.py
@@ -334,6 +334,22 @@ def get_bool(val):
raise ValueError("Not a boolean value", val)
+def get_int(val):
+ """ given a string value of an integer configuration option,
+ return an actual int """
+ return int(val)
+
+
+def get_timeout(val):
+ """ convert the timeout value into a float or None """
+ if val is None:
+ return val
+ timeout = float(val) # pass ValueError up the stack
+ if timeout <= 0:
+ return None
+ return timeout
+
+
def get_size(value):
""" Given a number of bytes in a human-readable format (e.g.,
'512m', '2g'), get the absolute number of bytes as an integer """
@@ -582,6 +598,11 @@ SERVER_UMASK = \
default='0077',
odesc='<Server umask>',
cf=('server', 'umask'))
+SERVER_AUTHENTICATION = \
+ Option('Default client authentication method',
+ default='cert+password',
+ odesc='{cert|bootstrap|cert+password}',
+ cf=('communication', 'authentication'))
# database options
DB_ENGINE = \
@@ -805,10 +826,20 @@ CLIENT_EXIT_ON_PROBE_FAILURE = \
long_arg=True,
cf=('client', 'exit_on_probe_failure'),
cook=get_bool)
+CLIENT_PROBE_TIMEOUT = \
+ Option("Timeout when running client probes",
+ default=None,
+ cf=('client', 'probe_timeout'),
+ cook=get_timeout)
+CLIENT_COMMAND_TIMEOUT = \
+ Option("Timeout when client runs other external commands (not probes)",
+ default=None,
+ cf=('client', 'command_timeout'),
+ cook=get_timeout)
# bcfg2-test and bcfg2-lint options
TEST_NOSEOPTS = \
- Option('Options to pass to nosetests',
+ Option('Options to pass to nosetests. Only honored with --children 0',
default=[],
cmd='--nose-options',
odesc='<opts>',
@@ -823,6 +854,21 @@ TEST_IGNORE = \
cf=('bcfg2_test', 'ignore_entries'),
cook=list_split,
long_arg=True)
+TEST_CHILDREN = \
+ Option('Spawn this number of children for bcfg2-test (python 2.6+)',
+ default=0,
+ cmd='--children',
+ odesc='<children>',
+ cf=('bcfg2_test', 'children'),
+ cook=get_int,
+ long_arg=True)
+TEST_XUNIT = \
+ Option('Output an XUnit result file with --children',
+ default=None,
+ cmd='--xunit',
+ odesc='<xunit file>',
+ cf=('bcfg2_test', 'xunit'),
+ long_arg=True)
LINT_CONFIG = \
Option('Specify bcfg2-lint configuration file',
default='/etc/bcfg2-lint.conf',
@@ -1117,7 +1163,8 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY,
protocol=SERVER_PROTOCOL,
web_configfile=WEB_CFILE,
backend=SERVER_BACKEND,
- vcs_root=SERVER_VCS_ROOT)
+ vcs_root=SERVER_VCS_ROOT,
+ authentication=SERVER_AUTHENTICATION)
CRYPT_OPTIONS = dict(encrypt=ENCRYPT,
decrypt=DECRYPT,
@@ -1195,7 +1242,9 @@ CLIENT_COMMON_OPTIONS = \
serverCN=CLIENT_SCNS,
timeout=CLIENT_TIMEOUT,
decision_list=CLIENT_DECISION_LIST,
- probe_exit=CLIENT_EXIT_ON_PROBE_FAILURE)
+ probe_exit=CLIENT_EXIT_ON_PROBE_FAILURE,
+ probe_timeout=CLIENT_PROBE_TIMEOUT,
+ command_timeout=CLIENT_COMMAND_TIMEOUT)
CLIENT_COMMON_OPTIONS.update(DRIVER_OPTIONS)
CLIENT_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS)
@@ -1214,6 +1263,12 @@ DATABASE_COMMON_OPTIONS = dict(web_configfile=WEB_CFILE,
REPORTING_COMMON_OPTIONS = dict(reporting_file_limit=REPORTING_FILE_LIMIT,
reporting_transport=REPORTING_TRANSPORT)
+TEST_COMMON_OPTIONS = dict(noseopts=TEST_NOSEOPTS,
+ test_ignore=TEST_IGNORE,
+ children=TEST_CHILDREN,
+ xunit=TEST_XUNIT,
+ validate=CFG_VALIDATION)
+
class OptionParser(OptionSet):
"""
diff --git a/src/lib/Bcfg2/Proxy.py b/src/lib/Bcfg2/Proxy.py
index 224028b6a..62b83d0b4 100644
--- a/src/lib/Bcfg2/Proxy.py
+++ b/src/lib/Bcfg2/Proxy.py
@@ -51,13 +51,16 @@ class ProxyError(Exception):
msg = str(err)
Exception.__init__(self, msg)
+
class CertificateError(Exception):
def __init__(self, commonName):
self.commonName = commonName
+
def __str__(self):
return ("Got unallowed commonName %s from server"
% self.commonName)
+
_orig_Method = xmlrpclib._Method
class RetryMethod(xmlrpclib._Method):
diff --git a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
index bca4a9c1e..3b2c0ccfa 100644
--- a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
+++ b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py
@@ -16,6 +16,7 @@ from Bcfg2.Reporting.Storage.base import StorageBase, StorageError
from Bcfg2.Server.Plugin.exceptions import PluginExecutionError
from django.core import management
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
+from django.db.models import FieldDoesNotExist
from django.core.cache import cache
from django.db import transaction
@@ -30,6 +31,230 @@ class DjangoORM(StorageBase):
super(DjangoORM, self).__init__(setup)
self.size_limit = setup.get('reporting_file_limit')
+ def _import_default(self, entry, state, entrytype=None, defaults=None,
+ mapping=None, boolean=None, xforms=None):
+ """ Default entry importer. Maps the entry (in state
+ ``state``) to an appropriate *Entry object; by default, this
+ is determined by the entry tag, e.g., from an Action entry an
+ ActionEntry object is created. This can be overridden with
+ ``entrytype``, which should be the class to instantiate for
+ this entry.
+
+ ``defaults`` is an optional mapping of <attribute
+ name>:<value> that will be used to set the default values for
+ various attributes.
+
+ ``mapping`` is a mapping of <field name>:<attribute name> that
+ can be used to map fields that are named differently on the
+ XML entry and in the database model.
+
+ ``boolean`` is a list of attribute names that should be
+ treated as booleans.
+
+ ``xforms`` is a dict of <attribute name>:<function>, where the
+ given function will be applied to the value of the named
+ attribute before trying to store it in the database.
+ """
+ if entrytype is None:
+ entrytype = globals()["%sEntry" % entry.tag]
+ if defaults is None:
+ defaults = dict()
+ if mapping is None:
+ mapping = dict()
+ if boolean is None:
+ boolean = []
+ if xforms is None:
+ xforms = dict()
+ mapping['exists'] = 'current_exists'
+ defaults['current_exists'] = 'true'
+ boolean.append("current_exists")
+
+ def boolean_xform(val):
+ try:
+ return val.lower() == "true"
+ except AttributeError:
+ return False
+
+ for attr in boolean + ["current_exists"]:
+ xforms[attr] = boolean_xform
+ act_dict = dict(state=state)
+ for fieldname in entrytype._meta.get_all_field_names():
+ if fieldname in ['id', 'hash_key', 'state']:
+ continue
+ try:
+ field = entrytype._meta.get_field(fieldname)
+ except FieldDoesNotExist:
+ continue
+ attrname = mapping.get(fieldname, fieldname)
+ val = entry.get(fieldname, defaults.get(attrname))
+ act_dict[fieldname] = xforms.get(attrname, lambda v: v)(val)
+ self.logger.debug("Adding %s:%s" % (entry.tag, entry.get("name")))
+ return entrytype.entry_get_or_create(act_dict)
+
+ def _import_Action(self, entry, state):
+ return self._import_default(entry, state,
+ defaults=dict(status='check', rc=-1),
+ mapping=dict(output="rc"))
+
+ def _import_Package(self, entry, state):
+ name = entry.get('name')
+ exists = entry.get('current_exists', default="true").lower() == "true"
+ act_dict = dict(name=name, state=state, exists=exists,
+ target_version=entry.get('version', default=''),
+ current_version=entry.get('current_version',
+ default=''))
+
+ # extra entries are a bit different. They can have Instance
+ # objects
+ if not act_dict['target_version']:
+ for instance in entry.findall("Instance"):
+ # FIXME - this probably only works for rpms
+ release = instance.get('release', '')
+ arch = instance.get('arch', '')
+ act_dict['current_version'] = instance.get('version')
+ if release:
+ act_dict['current_version'] += "-" + release
+ if arch:
+ act_dict['current_version'] += "." + arch
+ self.logger.debug("Adding package %s %s" %
+ (name, act_dict['current_version']))
+ return PackageEntry.entry_get_or_create(act_dict)
+ else:
+ self.logger.debug("Adding package %s %s" %
+ (name, act_dict['target_version']))
+
+ # not implemented yet
+ act_dict['verification_details'] = \
+ entry.get('verification_details', '')
+ return PackageEntry.entry_get_or_create(act_dict)
+
+ def _import_Path(self, entry, state):
+ name = entry.get('name')
+ exists = entry.get('current_exists', default="true").lower() == "true"
+ path_type = entry.get("type").lower()
+ act_dict = dict(name=name, state=state, exists=exists,
+ path_type=path_type)
+
+ target_dict = dict(
+ owner=entry.get('owner', default="root"),
+ group=entry.get('group', default="root"),
+ mode=entry.get('mode', default=entry.get('perms',
+ default=""))
+ )
+ fperm, created = FilePerms.objects.get_or_create(**target_dict)
+ act_dict['target_perms'] = fperm
+
+ current_dict = dict(
+ owner=entry.get('current_owner', default=""),
+ group=entry.get('current_group', default=""),
+ mode=entry.get('current_mode',
+ default=entry.get('current_perms', default=""))
+ )
+ fperm, created = FilePerms.objects.get_or_create(**current_dict)
+ act_dict['current_perms'] = fperm
+
+ if path_type in ('symlink', 'hardlink'):
+ act_dict['target_path'] = entry.get('to', default="")
+ act_dict['current_path'] = entry.get('current_to', default="")
+ self.logger.debug("Adding link %s" % name)
+ return LinkEntry.entry_get_or_create(act_dict)
+ elif path_type == 'device':
+ # TODO devices
+ self.logger.warn("device path types are not supported yet")
+ return
+
+ # TODO - vcs output
+ act_dict['detail_type'] = PathEntry.DETAIL_UNUSED
+ if path_type == 'directory' and entry.get('prune', 'false') == 'true':
+ unpruned_elist = [e.get('path') for e in entry.findall('Prune')]
+ if unpruned_elist:
+ act_dict['detail_type'] = PathEntry.DETAIL_PRUNED
+ act_dict['details'] = "\n".join(unpruned_elist)
+ elif entry.get('sensitive', 'false').lower() == 'true':
+ act_dict['detail_type'] = PathEntry.DETAIL_SENSITIVE
+ else:
+ cdata = None
+ if entry.get('current_bfile', None):
+ act_dict['detail_type'] = PathEntry.DETAIL_BINARY
+ cdata = entry.get('current_bfile')
+ elif entry.get('current_bdiff', None):
+ act_dict['detail_type'] = PathEntry.DETAIL_DIFF
+ cdata = b64decode(entry.get('current_bdiff'))
+ elif entry.get('current_diff', None):
+ act_dict['detail_type'] = PathEntry.DETAIL_DIFF
+ cdata = entry.get('current_bdiff')
+ if cdata:
+ if len(cdata) > self.size_limit:
+ act_dict['detail_type'] = PathEntry.DETAIL_SIZE_LIMIT
+ act_dict['details'] = md5(cdata).hexdigest()
+ else:
+ act_dict['details'] = cdata
+ self.logger.debug("Adding path %s" % name)
+ return PathEntry.entry_get_or_create(act_dict)
+ # TODO - secontext
+ # TODO - acls
+
+ def _import_Service(self, entry, state):
+ return self._import_default(entry, state,
+ defaults=dict(status='',
+ current_status=''),
+ mapping=dict(status='target_status'))
+
+ def _import_SEBoolean(self, entry, state):
+ return self._import_default(
+ entry, state,
+ xforms=dict(value=lambda v: v.lower() == "on"))
+
+ def _import_SEFcontext(self, entry, state):
+ return self._import_default(entry, state,
+ defaults=dict(filetype='all'))
+
+ def _import_SEInterface(self, entry, state):
+ return self._import_default(entry, state)
+
+ def _import_SEPort(self, entry, state):
+ return self._import_default(entry, state)
+
+ def _import_SENode(self, entry, state):
+ return self._import_default(entry, state)
+
+ def _import_SELogin(self, entry, state):
+ return self._import_default(entry, state)
+
+ def _import_SEUser(self, entry, state):
+ return self._import_default(entry, state)
+
+ def _import_SEPermissive(self, entry, state):
+ return self._import_default(entry, state)
+
+ def _import_SEModule(self, entry, state):
+ return self._import_default(entry, state,
+ defaults=dict(disabled='false'),
+ boolean=['disabled', 'current_disabled'])
+
+ def _import_POSIXUser(self, entry, state):
+ defaults = dict(group=entry.get("name"),
+ gecos=entry.get("name"),
+ shell='/bin/bash',
+ uid=entry.get("current_uid"))
+ if entry.get('name') == 'root':
+ defaults['home'] = '/root'
+ else:
+ defaults['home'] = '/home/%s' % entry.get('name')
+
+ # TODO: supplementary group membership
+ return self._import_default(entry, state, defaults=defaults)
+
+ def _import_POSIXGroup(self, entry, state):
+ return self._import_default(
+ entry, state,
+ defaults=dict(gid=entry.get("current_gid")))
+
+ def _import_unknown(self, entry, _):
+ self.logger.error("Unknown type %s not handled by reporting yet" %
+ entry.tag)
+ return None
+
@transaction.commit_on_success
def _import_interaction(self, interaction):
"""Real import function"""
@@ -46,13 +271,15 @@ class DjangoORM(StorageBase):
cache.set(hostname, client)
timestamp = datetime(*strptime(stats.get('time'))[0:6])
- if len(Interaction.objects.filter(client=client, timestamp=timestamp)) > 0:
+ if len(Interaction.objects.filter(client=client,
+ timestamp=timestamp)) > 0:
self.logger.warn("Interaction for %s at %s already exists" %
(hostname, timestamp))
return
if 'profile' in metadata:
- profile, created = Group.objects.get_or_create(name=metadata['profile'])
+ profile, created = \
+ Group.objects.get_or_create(name=metadata['profile'])
else:
profile = None
inter = Interaction(client=client,
@@ -65,10 +292,10 @@ class DjangoORM(StorageBase):
server=server,
profile=profile)
inter.save()
- self.logger.debug("Interaction for %s at %s with INSERTED in to db" %
+ self.logger.debug("Interaction for %s at %s with INSERTED in to db" %
(client.id, timestamp))
- #FIXME - this should be more efficient
+ # FIXME - this should be more efficient
for group_name in metadata['groups']:
group = cache.get("GROUP_" + group_name)
if not group:
@@ -76,12 +303,13 @@ class DjangoORM(StorageBase):
if created:
self.logger.debug("Added group %s" % group)
cache.set("GROUP_" + group_name, group)
-
+
inter.groups.add(group)
- for bundle_name in metadata['bundles']:
+ for bundle_name in metadata.get('bundles', []):
bundle = cache.get("BUNDLE_" + bundle_name)
if not bundle:
- bundle, created = Bundle.objects.get_or_create(name=bundle_name)
+ bundle, created = \
+ Bundle.objects.get_or_create(name=bundle_name)
if created:
self.logger.debug("Added bundle %s" % bundle)
cache.set("BUNDLE_" + bundle_name, bundle)
@@ -94,130 +322,26 @@ class DjangoORM(StorageBase):
pattern = [('Bad/*', TYPE_BAD),
('Extra/*', TYPE_EXTRA),
('Modified/*', TYPE_MODIFIED)]
- updates = dict(failures=[], paths=[], packages=[], actions=[], services=[])
+ updates = dict([(etype, []) for etype in Interaction.entry_types])
for (xpath, state) in pattern:
for entry in stats.findall(xpath):
counter_fields[state] = counter_fields[state] + 1
- entry_type = entry.tag
- name = entry.get('name')
- exists = entry.get('current_exists', default="true").lower() == "true"
-
# handle server failures differently
failure = entry.get('failure', '')
if failure:
- act_dict = dict(name=name, entry_type=entry_type,
- message=failure)
+ act_dict = dict(name=entry.get("name"),
+ entry_type=entry.tag,
+ message=failure)
newact = FailureEntry.entry_get_or_create(act_dict)
updates['failures'].append(newact)
continue
- act_dict = dict(name=name, state=state, exists=exists)
-
- if entry_type == 'Action':
- act_dict['status'] = entry.get('status', default="check")
- act_dict['output'] = entry.get('rc', default=-1)
- self.logger.debug("Adding action %s" % name)
- updates['actions'].append(ActionEntry.entry_get_or_create(act_dict))
- elif entry_type == 'Package':
- act_dict['target_version'] = entry.get('version', default='')
- act_dict['current_version'] = entry.get('current_version', default='')
-
- # extra entries are a bit different. They can have Instance objects
- if not act_dict['target_version']:
- for instance in entry.findall("Instance"):
- #TODO - this probably only works for rpms
- release = instance.get('release', '')
- arch = instance.get('arch', '')
- act_dict['current_version'] = instance.get('version')
- if release:
- act_dict['current_version'] += "-" + release
- if arch:
- act_dict['current_version'] += "." + arch
- self.logger.debug("Adding package %s %s" % (name, act_dict['current_version']))
- updates['packages'].append(PackageEntry.entry_get_or_create(act_dict))
- else:
-
- self.logger.debug("Adding package %s %s" % (name, act_dict['target_version']))
-
- # not implemented yet
- act_dict['verification_details'] = entry.get('verification_details', '')
- updates['packages'].append(PackageEntry.entry_get_or_create(act_dict))
-
- elif entry_type == 'Path':
- path_type = entry.get("type").lower()
- act_dict['path_type'] = path_type
-
- target_dict = dict(
- owner=entry.get('owner', default="root"),
- group=entry.get('group', default="root"),
- mode=entry.get('mode', default=entry.get('perms', default=""))
- )
- fperm, created = FilePerms.objects.get_or_create(**target_dict)
- act_dict['target_perms'] = fperm
-
- current_dict = dict(
- owner=entry.get('current_owner', default=""),
- group=entry.get('current_group', default=""),
- mode=entry.get('current_mode',
- default=entry.get('current_perms', default=""))
- )
- fperm, created = FilePerms.objects.get_or_create(**current_dict)
- act_dict['current_perms'] = fperm
-
- if path_type in ('symlink', 'hardlink'):
- act_dict['target_path'] = entry.get('to', default="")
- act_dict['current_path'] = entry.get('current_to', default="")
- self.logger.debug("Adding link %s" % name)
- updates['paths'].append(LinkEntry.entry_get_or_create(act_dict))
- continue
- elif path_type == 'device':
- #TODO devices
- self.logger.warn("device path types are not supported yet")
- continue
-
- # TODO - vcs output
- act_dict['detail_type'] = PathEntry.DETAIL_UNUSED
- if path_type == 'directory' and entry.get('prune', 'false') == 'true':
- unpruned_elist = [e.get('path') for e in entry.findall('Prune')]
- if unpruned_elist:
- act_dict['detail_type'] = PathEntry.DETAIL_PRUNED
- act_dict['details'] = "\n".join(unpruned_elist)
- elif entry.get('sensitive', 'false').lower() == 'true':
- act_dict['detail_type'] = PathEntry.DETAIL_SENSITIVE
- else:
- cdata = None
- if entry.get('current_bfile', None):
- act_dict['detail_type'] = PathEntry.DETAIL_BINARY
- cdata = entry.get('current_bfile')
- elif entry.get('current_bdiff', None):
- act_dict['detail_type'] = PathEntry.DETAIL_DIFF
- cdata = b64decode(entry.get('current_bdiff'))
- elif entry.get('current_diff', None):
- act_dict['detail_type'] = PathEntry.DETAIL_DIFF
- cdata = entry.get('current_bdiff')
- if cdata:
- if len(cdata) > self.size_limit:
- act_dict['detail_type'] = PathEntry.DETAIL_SIZE_LIMIT
- act_dict['details'] = md5(cdata).hexdigest()
- else:
- act_dict['details'] = cdata
- self.logger.debug("Adding path %s" % name)
- updates['paths'].append(PathEntry.entry_get_or_create(act_dict))
-
-
- #TODO - secontext
- #TODO - acls
-
- elif entry_type == 'Service':
- act_dict['target_status'] = entry.get('status', default='')
- act_dict['current_status'] = entry.get('current_status', default='')
- self.logger.debug("Adding service %s" % name)
- updates['services'].append(ServiceEntry.entry_get_or_create(act_dict))
- elif entry_type == 'SELinux':
- self.logger.info("SELinux not implemented yet")
- else:
- self.logger.error("Unknown type %s not handled by reporting yet" % entry_type)
+ updatetype = entry.tag.lower() + "s"
+ update = getattr(self, "_import_%s" % entry.tag,
+ self._import_unknown)(entry, state)
+ if update is not None:
+ updates[updatetype].append(update)
inter.bad_count = counter_fields[TYPE_BAD]
inter.modified_count = counter_fields[TYPE_MODIFIED]
@@ -227,15 +351,16 @@ class DjangoORM(StorageBase):
# batch this for sqlite
i = 0
while(i < len(updates[entry_type])):
- getattr(inter, entry_type).add(*updates[entry_type][i:i+100])
+ getattr(inter, entry_type).add(*updates[entry_type][i:i + 100])
i += 100
# performance metrics
for times in stats.findall('OpStamps'):
for metric, value in list(times.items()):
- Performance(interaction=inter, metric=metric, value=value).save()
+ Performance(interaction=inter,
+ metric=metric,
+ value=value).save()
-
def import_interaction(self, interaction):
"""Import the data into the backend"""
@@ -245,7 +370,6 @@ class DjangoORM(StorageBase):
self.logger.error("Failed to import interaction: %s" %
traceback.format_exc().splitlines()[-1])
-
def validate(self):
"""Validate backend storage. Should be called once when loaded"""
diff --git a/src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py b/src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py
new file mode 100644
index 000000000..d5f5d801a
--- /dev/null
+++ b/src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py
@@ -0,0 +1,485 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'SELoginEntry'
+ db.create_table('Reporting_seloginentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('selinuxuser', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('current_selinuxuser', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
+ ))
+ db.send_create_signal('Reporting', ['SELoginEntry'])
+
+ # Adding model 'SEUserEntry'
+ db.create_table('Reporting_seuserentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('roles', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('current_roles', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
+ ('prefix', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('current_prefix', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
+ ))
+ db.send_create_signal('Reporting', ['SEUserEntry'])
+
+ # Adding model 'SEBooleanEntry'
+ db.create_table('Reporting_sebooleanentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('value', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ))
+ db.send_create_signal('Reporting', ['SEBooleanEntry'])
+
+ # Adding model 'SENodeEntry'
+ db.create_table('Reporting_senodeentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
+ ('proto', self.gf('django.db.models.fields.CharField')(max_length=4)),
+ ))
+ db.send_create_signal('Reporting', ['SENodeEntry'])
+
+ # Adding model 'SEFcontextEntry'
+ db.create_table('Reporting_sefcontextentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
+ ('filetype', self.gf('django.db.models.fields.CharField')(max_length=16)),
+ ))
+ db.send_create_signal('Reporting', ['SEFcontextEntry'])
+
+ # Adding model 'SEInterfaceEntry'
+ db.create_table('Reporting_seinterfaceentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
+ ))
+ db.send_create_signal('Reporting', ['SEInterfaceEntry'])
+
+ # Adding model 'SEPermissiveEntry'
+ db.create_table('Reporting_sepermissiveentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ))
+ db.send_create_signal('Reporting', ['SEPermissiveEntry'])
+
+ # Adding model 'SEModuleEntry'
+ db.create_table('Reporting_semoduleentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('disabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('current_disabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ))
+ db.send_create_signal('Reporting', ['SEModuleEntry'])
+
+ # Adding model 'SEPortEntry'
+ db.create_table('Reporting_seportentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)),
+ ))
+ db.send_create_signal('Reporting', ['SEPortEntry'])
+
+ # Adding M2M table for field sebooleans on 'Interaction'
+ db.create_table('Reporting_interaction_sebooleans', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('sebooleanentry', models.ForeignKey(orm['Reporting.sebooleanentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_sebooleans', ['interaction_id', 'sebooleanentry_id'])
+
+ # Adding M2M table for field seports on 'Interaction'
+ db.create_table('Reporting_interaction_seports', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('seportentry', models.ForeignKey(orm['Reporting.seportentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_seports', ['interaction_id', 'seportentry_id'])
+
+ # Adding M2M table for field sefcontexts on 'Interaction'
+ db.create_table('Reporting_interaction_sefcontexts', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('sefcontextentry', models.ForeignKey(orm['Reporting.sefcontextentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_sefcontexts', ['interaction_id', 'sefcontextentry_id'])
+
+ # Adding M2M table for field senodes on 'Interaction'
+ db.create_table('Reporting_interaction_senodes', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('senodeentry', models.ForeignKey(orm['Reporting.senodeentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_senodes', ['interaction_id', 'senodeentry_id'])
+
+ # Adding M2M table for field selogins on 'Interaction'
+ db.create_table('Reporting_interaction_selogins', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('seloginentry', models.ForeignKey(orm['Reporting.seloginentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_selogins', ['interaction_id', 'seloginentry_id'])
+
+ # Adding M2M table for field seusers on 'Interaction'
+ db.create_table('Reporting_interaction_seusers', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('seuserentry', models.ForeignKey(orm['Reporting.seuserentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_seusers', ['interaction_id', 'seuserentry_id'])
+
+ # Adding M2M table for field seinterfaces on 'Interaction'
+ db.create_table('Reporting_interaction_seinterfaces', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('seinterfaceentry', models.ForeignKey(orm['Reporting.seinterfaceentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_seinterfaces', ['interaction_id', 'seinterfaceentry_id'])
+
+ # Adding M2M table for field sepermissives on 'Interaction'
+ db.create_table('Reporting_interaction_sepermissives', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('sepermissiveentry', models.ForeignKey(orm['Reporting.sepermissiveentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_sepermissives', ['interaction_id', 'sepermissiveentry_id'])
+
+ # Adding M2M table for field semodules on 'Interaction'
+ db.create_table('Reporting_interaction_semodules', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('semoduleentry', models.ForeignKey(orm['Reporting.semoduleentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_semodules', ['interaction_id', 'semoduleentry_id'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'SELoginEntry'
+ db.delete_table('Reporting_seloginentry')
+
+ # Deleting model 'SEUserEntry'
+ db.delete_table('Reporting_seuserentry')
+
+ # Deleting model 'SEBooleanEntry'
+ db.delete_table('Reporting_sebooleanentry')
+
+ # Deleting model 'SENodeEntry'
+ db.delete_table('Reporting_senodeentry')
+
+ # Deleting model 'SEFcontextEntry'
+ db.delete_table('Reporting_sefcontextentry')
+
+ # Deleting model 'SEInterfaceEntry'
+ db.delete_table('Reporting_seinterfaceentry')
+
+ # Deleting model 'SEPermissiveEntry'
+ db.delete_table('Reporting_sepermissiveentry')
+
+ # Deleting model 'SEModuleEntry'
+ db.delete_table('Reporting_semoduleentry')
+
+ # Deleting model 'SEPortEntry'
+ db.delete_table('Reporting_seportentry')
+
+ # Removing M2M table for field sebooleans on 'Interaction'
+ db.delete_table('Reporting_interaction_sebooleans')
+
+ # Removing M2M table for field seports on 'Interaction'
+ db.delete_table('Reporting_interaction_seports')
+
+ # Removing M2M table for field sefcontexts on 'Interaction'
+ db.delete_table('Reporting_interaction_sefcontexts')
+
+ # Removing M2M table for field senodes on 'Interaction'
+ db.delete_table('Reporting_interaction_senodes')
+
+ # Removing M2M table for field selogins on 'Interaction'
+ db.delete_table('Reporting_interaction_selogins')
+
+ # Removing M2M table for field seusers on 'Interaction'
+ db.delete_table('Reporting_interaction_seusers')
+
+ # Removing M2M table for field seinterfaces on 'Interaction'
+ db.delete_table('Reporting_interaction_seinterfaces')
+
+ # Removing M2M table for field sepermissives on 'Interaction'
+ db.delete_table('Reporting_interaction_sepermissives')
+
+ # Removing M2M table for field semodules on 'Interaction'
+ db.delete_table('Reporting_interaction_semodules')
+
+
+ models = {
+ 'Reporting.actionentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ActionEntry'},
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'output': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'check'", 'max_length': '128'})
+ },
+ 'Reporting.bundle': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Bundle'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'Reporting.client': {
+ 'Meta': {'object_name': 'Client'},
+ 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'current_interaction': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'parent_client'", 'null': 'True', 'to': "orm['Reporting.Interaction']"}),
+ 'expiration': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'Reporting.deviceentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'DeviceEntry', '_ormbases': ['Reporting.PathEntry']},
+ 'current_major': ('django.db.models.fields.IntegerField', [], {}),
+ 'current_minor': ('django.db.models.fields.IntegerField', [], {}),
+ 'device_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}),
+ 'target_major': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_minor': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.failureentry': {
+ 'Meta': {'object_name': 'FailureEntry'},
+ 'entry_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'message': ('django.db.models.fields.TextField', [], {}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'})
+ },
+ 'Reporting.fileacl': {
+ 'Meta': {'object_name': 'FileAcl'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'})
+ },
+ 'Reporting.fileperms': {
+ 'Meta': {'unique_together': "(('owner', 'group', 'mode'),)", 'object_name': 'FilePerms'},
+ 'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mode': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'Reporting.group': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Group'},
+ 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}),
+ 'category': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'profile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'Reporting.interaction': {
+ 'Meta': {'ordering': "['-timestamp']", 'unique_together': "(('client', 'timestamp'),)", 'object_name': 'Interaction'},
+ 'actions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ActionEntry']", 'symmetrical': 'False'}),
+ 'bad_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}),
+ 'client': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'interactions'", 'to': "orm['Reporting.Client']"}),
+ 'extra_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'failures': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FailureEntry']", 'symmetrical': 'False'}),
+ 'good_count': ('django.db.models.fields.IntegerField', [], {}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PackageEntry']", 'symmetrical': 'False'}),
+ 'paths': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PathEntry']", 'symmetrical': 'False'}),
+ 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['Reporting.Group']"}),
+ 'repo_rev_code': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'sebooleans': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEBooleanEntry']", 'symmetrical': 'False'}),
+ 'sefcontexts': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEFcontextEntry']", 'symmetrical': 'False'}),
+ 'seinterfaces': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEInterfaceEntry']", 'symmetrical': 'False'}),
+ 'selogins': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SELoginEntry']", 'symmetrical': 'False'}),
+ 'semodules': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEModuleEntry']", 'symmetrical': 'False'}),
+ 'senodes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SENodeEntry']", 'symmetrical': 'False'}),
+ 'sepermissives': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPermissiveEntry']", 'symmetrical': 'False'}),
+ 'seports': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPortEntry']", 'symmetrical': 'False'}),
+ 'server': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+ 'services': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ServiceEntry']", 'symmetrical': 'False'}),
+ 'seusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEUserEntry']", 'symmetrical': 'False'}),
+ 'state': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'total_count': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.linkentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'LinkEntry', '_ormbases': ['Reporting.PathEntry']},
+ 'current_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+ 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}),
+ 'target_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'})
+ },
+ 'Reporting.packageentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PackageEntry'},
+ 'current_version': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
+ 'verification_details': ('django.db.models.fields.TextField', [], {'default': "''"})
+ },
+ 'Reporting.pathentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PathEntry'},
+ 'acls': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FileAcl']", 'symmetrical': 'False'}),
+ 'current_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}),
+ 'detail_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'details': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'path_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"})
+ },
+ 'Reporting.performance': {
+ 'Meta': {'object_name': 'Performance'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interaction': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'performance_items'", 'to': "orm['Reporting.Interaction']"}),
+ 'metric': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'value': ('django.db.models.fields.DecimalField', [], {'max_digits': '32', 'decimal_places': '16'})
+ },
+ 'Reporting.sebooleanentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEBooleanEntry'},
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'value': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
+ },
+ 'Reporting.sefcontextentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEFcontextEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.seinterfaceentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEInterfaceEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.seloginentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SELoginEntry'},
+ 'current_selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.semoduleentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEModuleEntry'},
+ 'current_disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.senodeentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SENodeEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'proto': ('django.db.models.fields.CharField', [], {'max_length': '4'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.sepermissiveentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPermissiveEntry'},
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.seportentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPortEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.serviceentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ServiceEntry'},
+ 'current_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'})
+ },
+ 'Reporting.seuserentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEUserEntry'},
+ 'current_prefix': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'current_roles': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'prefix': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'roles': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ }
+ }
+
+ complete_apps = ['Reporting'] \ No newline at end of file
diff --git a/src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py b/src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py
new file mode 100644
index 000000000..d86e663d5
--- /dev/null
+++ b/src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py
@@ -0,0 +1,340 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'POSIXGroupEntry'
+ db.create_table('Reporting_posixgroupentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('gid', self.gf('django.db.models.fields.IntegerField')(null=True)),
+ ('current_gid', self.gf('django.db.models.fields.IntegerField')(null=True)),
+ ))
+ db.send_create_signal('Reporting', ['POSIXGroupEntry'])
+
+ # Adding model 'POSIXUserEntry'
+ db.create_table('Reporting_posixuserentry', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)),
+ ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)),
+ ('state', self.gf('django.db.models.fields.IntegerField')()),
+ ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('uid', self.gf('django.db.models.fields.IntegerField')(null=True)),
+ ('current_uid', self.gf('django.db.models.fields.IntegerField')(null=True)),
+ ('group', self.gf('django.db.models.fields.CharField')(max_length=64)),
+ ('current_group', self.gf('django.db.models.fields.CharField')(max_length=64, null=True)),
+ ('gecos', self.gf('django.db.models.fields.CharField')(max_length=1024)),
+ ('current_gecos', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)),
+ ('home', self.gf('django.db.models.fields.CharField')(max_length=1024)),
+ ('current_home', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)),
+ ('shell', self.gf('django.db.models.fields.CharField')(default='/bin/bash', max_length=1024)),
+ ('current_shell', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)),
+ ))
+ db.send_create_signal('Reporting', ['POSIXUserEntry'])
+
+ # Adding M2M table for field posixusers on 'Interaction'
+ db.create_table('Reporting_interaction_posixusers', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('posixuserentry', models.ForeignKey(orm['Reporting.posixuserentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_posixusers', ['interaction_id', 'posixuserentry_id'])
+
+ # Adding M2M table for field posixgroups on 'Interaction'
+ db.create_table('Reporting_interaction_posixgroups', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)),
+ ('posixgroupentry', models.ForeignKey(orm['Reporting.posixgroupentry'], null=False))
+ ))
+ db.create_unique('Reporting_interaction_posixgroups', ['interaction_id', 'posixgroupentry_id'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'POSIXGroupEntry'
+ db.delete_table('Reporting_posixgroupentry')
+
+ # Deleting model 'POSIXUserEntry'
+ db.delete_table('Reporting_posixuserentry')
+
+ # Removing M2M table for field posixusers on 'Interaction'
+ db.delete_table('Reporting_interaction_posixusers')
+
+ # Removing M2M table for field posixgroups on 'Interaction'
+ db.delete_table('Reporting_interaction_posixgroups')
+
+
+ models = {
+ 'Reporting.actionentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ActionEntry'},
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'output': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'check'", 'max_length': '128'})
+ },
+ 'Reporting.bundle': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Bundle'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'Reporting.client': {
+ 'Meta': {'object_name': 'Client'},
+ 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'current_interaction': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'parent_client'", 'null': 'True', 'to': "orm['Reporting.Interaction']"}),
+ 'expiration': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'Reporting.deviceentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'DeviceEntry', '_ormbases': ['Reporting.PathEntry']},
+ 'current_major': ('django.db.models.fields.IntegerField', [], {}),
+ 'current_minor': ('django.db.models.fields.IntegerField', [], {}),
+ 'device_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}),
+ 'target_major': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_minor': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.failureentry': {
+ 'Meta': {'object_name': 'FailureEntry'},
+ 'entry_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'message': ('django.db.models.fields.TextField', [], {}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'})
+ },
+ 'Reporting.fileacl': {
+ 'Meta': {'object_name': 'FileAcl'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'})
+ },
+ 'Reporting.fileperms': {
+ 'Meta': {'unique_together': "(('owner', 'group', 'mode'),)", 'object_name': 'FilePerms'},
+ 'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mode': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'Reporting.group': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Group'},
+ 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}),
+ 'category': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'profile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'Reporting.interaction': {
+ 'Meta': {'ordering': "['-timestamp']", 'unique_together': "(('client', 'timestamp'),)", 'object_name': 'Interaction'},
+ 'actions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ActionEntry']", 'symmetrical': 'False'}),
+ 'bad_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}),
+ 'client': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'interactions'", 'to': "orm['Reporting.Client']"}),
+ 'extra_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'failures': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FailureEntry']", 'symmetrical': 'False'}),
+ 'good_count': ('django.db.models.fields.IntegerField', [], {}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PackageEntry']", 'symmetrical': 'False'}),
+ 'paths': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PathEntry']", 'symmetrical': 'False'}),
+ 'posixgroups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.POSIXGroupEntry']", 'symmetrical': 'False'}),
+ 'posixusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.POSIXUserEntry']", 'symmetrical': 'False'}),
+ 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['Reporting.Group']"}),
+ 'repo_rev_code': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'sebooleans': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEBooleanEntry']", 'symmetrical': 'False'}),
+ 'sefcontexts': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEFcontextEntry']", 'symmetrical': 'False'}),
+ 'seinterfaces': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEInterfaceEntry']", 'symmetrical': 'False'}),
+ 'selogins': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SELoginEntry']", 'symmetrical': 'False'}),
+ 'semodules': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEModuleEntry']", 'symmetrical': 'False'}),
+ 'senodes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SENodeEntry']", 'symmetrical': 'False'}),
+ 'sepermissives': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPermissiveEntry']", 'symmetrical': 'False'}),
+ 'seports': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPortEntry']", 'symmetrical': 'False'}),
+ 'server': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+ 'services': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ServiceEntry']", 'symmetrical': 'False'}),
+ 'seusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEUserEntry']", 'symmetrical': 'False'}),
+ 'state': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'total_count': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.linkentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'LinkEntry', '_ormbases': ['Reporting.PathEntry']},
+ 'current_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+ 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}),
+ 'target_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'})
+ },
+ 'Reporting.packageentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PackageEntry'},
+ 'current_version': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
+ 'verification_details': ('django.db.models.fields.TextField', [], {'default': "''"})
+ },
+ 'Reporting.pathentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PathEntry'},
+ 'acls': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FileAcl']", 'symmetrical': 'False'}),
+ 'current_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}),
+ 'detail_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'details': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'path_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"})
+ },
+ 'Reporting.performance': {
+ 'Meta': {'object_name': 'Performance'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interaction': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'performance_items'", 'to': "orm['Reporting.Interaction']"}),
+ 'metric': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'value': ('django.db.models.fields.DecimalField', [], {'max_digits': '32', 'decimal_places': '16'})
+ },
+ 'Reporting.posixgroupentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'POSIXGroupEntry'},
+ 'current_gid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'gid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.posixuserentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'POSIXUserEntry'},
+ 'current_gecos': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+ 'current_group': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+ 'current_home': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+ 'current_shell': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+ 'current_uid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'gecos': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'group': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'home': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'shell': ('django.db.models.fields.CharField', [], {'default': "'/bin/bash'", 'max_length': '1024'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'uid': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
+ },
+ 'Reporting.sebooleanentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEBooleanEntry'},
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'value': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
+ },
+ 'Reporting.sefcontextentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEFcontextEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.seinterfaceentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEInterfaceEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.seloginentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SELoginEntry'},
+ 'current_selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.semoduleentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEModuleEntry'},
+ 'current_disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.senodeentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SENodeEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'proto': ('django.db.models.fields.CharField', [], {'max_length': '4'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.sepermissiveentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPermissiveEntry'},
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.seportentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPortEntry'},
+ 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'Reporting.serviceentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ServiceEntry'},
+ 'current_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {}),
+ 'target_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'})
+ },
+ 'Reporting.seuserentry': {
+ 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEUserEntry'},
+ 'current_prefix': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'current_roles': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'prefix': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'roles': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {})
+ }
+ }
+
+ complete_apps = ['Reporting'] \ No newline at end of file
diff --git a/src/lib/Bcfg2/Reporting/models.py b/src/lib/Bcfg2/Reporting/models.py
index ab2dc8418..4be509f53 100644
--- a/src/lib/Bcfg2/Reporting/models.py
+++ b/src/lib/Bcfg2/Reporting/models.py
@@ -11,21 +11,9 @@ except ImproperlyConfigured:
from django.core.cache import cache
from datetime import datetime, timedelta
+from Bcfg2.Compat import cPickle
+
-try:
- import cPickle as pickle
-except:
- import pickle
-
-KIND_CHOICES = (
- #These are the kinds of config elements
- ('Package', 'Package'),
- ('Path', 'directory'),
- ('Path', 'file'),
- ('Path', 'permissions'),
- ('Path', 'symlink'),
- ('Service', 'Service'),
-)
TYPE_GOOD = 0
TYPE_BAD = 1
TYPE_MODIFIED = 2
@@ -57,8 +45,8 @@ def hash_entry(entry_dict):
for key in sorted(entry_dict.keys()):
if key in ('id', 'hash_key') or key.startswith('_'):
continue
- dataset.append( (key, entry_dict[key]) )
- return hash(pickle.dumps(dataset))
+ dataset.append((key, entry_dict[key]))
+ return hash(cPickle.dumps(dataset))
class Client(models.Model):
@@ -121,7 +109,8 @@ class InteractionManager(models.Manager):
class Interaction(models.Model):
- """Models each reconfiguration operation interaction between client and server."""
+ """ Models each reconfiguration operation interaction between
+ client and server. """
client = models.ForeignKey(Client, related_name="interactions")
timestamp = models.DateTimeField(db_index=True) # Timestamp for this record
state = models.CharField(max_length=32) # good/bad/modified/etc
@@ -137,8 +126,24 @@ class Interaction(models.Model):
packages = models.ManyToManyField("PackageEntry")
paths = models.ManyToManyField("PathEntry")
services = models.ManyToManyField("ServiceEntry")
+ sebooleans = models.ManyToManyField("SEBooleanEntry")
+ seports = models.ManyToManyField("SEPortEntry")
+ sefcontexts = models.ManyToManyField("SEFcontextEntry")
+ senodes = models.ManyToManyField("SENodeEntry")
+ selogins = models.ManyToManyField("SELoginEntry")
+ seusers = models.ManyToManyField("SEUserEntry")
+ seinterfaces = models.ManyToManyField("SEInterfaceEntry")
+ sepermissives = models.ManyToManyField("SEPermissiveEntry")
+ semodules = models.ManyToManyField("SEModuleEntry")
+ posixusers = models.ManyToManyField("POSIXUserEntry")
+ posixgroups = models.ManyToManyField("POSIXGroupEntry")
failures = models.ManyToManyField("FailureEntry")
+ entry_types = ('actions', 'packages', 'paths', 'services', 'sebooleans',
+ 'seports', 'sefcontexts', 'senodes', 'selogins', 'seusers',
+ 'seinterfaces', 'sepermissives', 'semodules', 'posixusers',
+ 'posixgroups')
+
# Formerly InteractionMetadata
profile = models.ForeignKey("Group", related_name="+", null=True)
groups = models.ManyToManyField("Group")
@@ -157,7 +162,8 @@ class Interaction(models.Model):
def percentbad(self):
if not self.total_count == 0:
- return ((self.total_count - self.good_count) / (float(self.total_count))) * 100
+ return ((self.total_count - self.good_count) /
+ (float(self.total_count))) * 100
else:
return 0
@@ -189,7 +195,8 @@ class Interaction(models.Model):
self.client.save() # save again post update
def delete(self):
- '''Override the default delete. Allows us to remove Performance items'''
+ '''Override the default delete. Allows us to remove
+ Performance items '''
pitems = list(self.performance_items.all())
super(Interaction, self).delete()
for perf in pitems:
@@ -201,19 +208,19 @@ class Interaction(models.Model):
def bad(self):
rv = []
- for entry in ('actions', 'packages', 'paths', 'services'):
+ for entry in self.entry_types:
rv.extend(getattr(self, entry).filter(state=TYPE_BAD))
return rv
def modified(self):
rv = []
- for entry in ('actions', 'packages', 'paths', 'services'):
+ for entry in self.entry_types:
rv.extend(getattr(self, entry).filter(state=TYPE_MODIFIED))
return rv
def extra(self):
rv = []
- for entry in ('actions', 'packages', 'paths', 'services'):
+ for entry in self.entry_types:
rv.extend(getattr(self, entry).filter(state=TYPE_EXTRA))
return rv
@@ -325,7 +332,6 @@ class BaseEntry(models.Model):
self.hash_key = hash_entry(self.__dict__)
super(BaseEntry, self).save(*args, **kwargs)
-
def class_name(self):
return self.__class__.__name__
@@ -333,7 +339,6 @@ class BaseEntry(models.Model):
"""todo"""
return []
-
@classmethod
def entry_from_name(cls, name):
try:
@@ -344,28 +349,26 @@ class BaseEntry(models.Model):
except KeyError:
raise ValueError("Invalid type %s" % name)
-
@classmethod
def entry_from_type(cls, etype):
- for entry_cls in (ActionEntry, PackageEntry, PathEntry, ServiceEntry):
+ for entry_cls in ENTRY_CLASSES:
if etype == entry_cls.ENTRY_TYPE:
return entry_cls
else:
raise ValueError("Invalid type %s" % etype)
-
@classmethod
def entry_get_or_create(cls, act_dict):
"""Helper to quickly lookup an object"""
cls_name = cls().__class__.__name__
act_hash = hash_entry(act_dict)
-
+
# TODO - get form cache and validate
act_key = "%s_%s" % (cls_name, act_hash)
newact = cache.get(act_key)
if newact:
return newact
-
+
acts = cls.objects.filter(hash_key=act_hash)
if len(acts) > 0:
for act in acts:
@@ -375,20 +378,18 @@ class BaseEntry(models.Model):
#match found
newact = act
break
-
+
# worst case, its new
if not newact:
newact = cls(**act_dict)
newact.save(hash_key=act_hash)
-
+
cache.set(act_key, newact, 60 * 60)
return newact
-
def is_failure(self):
return isinstance(self, FailureEntry)
-
@classmethod
def prune_orphans(cls):
'''Remove unused entries'''
@@ -397,7 +398,7 @@ class BaseEntry(models.Model):
for x in cls.objects.filter(interaction__isnull=True).values("id")]
i = 0
while i < len(cls_orphans):
- cls.objects.filter(id__in=cls_orphans[i:i+100]).delete()
+ cls.objects.filter(id__in=cls_orphans[i:i + 100]).delete()
i += 100
@@ -439,13 +440,161 @@ class FailureEntry(BaseEntry):
class ActionEntry(SuccessEntry):
- """ The new model for package information """
+ """ Action entry """
status = models.CharField(max_length=128, default="check")
output = models.IntegerField(default=0)
ENTRY_TYPE = r"Action"
+class SEBooleanEntry(SuccessEntry):
+ """ SELinux boolean """
+ value = models.BooleanField(default=True)
+
+ ENTRY_TYPE = r"SEBoolean"
+
+
+class SEPortEntry(SuccessEntry):
+ """ SELinux port """
+ selinuxtype = models.CharField(max_length=128)
+ current_selinuxtype = models.CharField(max_length=128, null=True)
+
+ ENTRY_TYPE = r"SEPort"
+
+ def selinuxtype_problem(self):
+ """Check for an selinux type problem."""
+ if not self.current_selinuxtype:
+ return True
+ return self.selinuxtype != self.current_selinuxtype
+
+ def short_list(self):
+ """Return a list of problems"""
+ rv = super(SEPortEntry, self).short_list()
+ if self.selinuxtype_problem():
+ rv.append("Wrong SELinux type")
+ return rv
+
+
+class SEFcontextEntry(SuccessEntry):
+ """ SELinux file context """
+ selinuxtype = models.CharField(max_length=128)
+ current_selinuxtype = models.CharField(max_length=128, null=True)
+ filetype = models.CharField(max_length=16)
+
+ ENTRY_TYPE = r"SEFcontext"
+
+ def selinuxtype_problem(self):
+ """Check for an selinux type problem."""
+ if not self.current_selinuxtype:
+ return True
+ return self.selinuxtype != self.current_selinuxtype
+
+ def short_list(self):
+ """Return a list of problems"""
+ rv = super(SEFcontextEntry, self).short_list()
+ if self.selinuxtype_problem():
+ rv.append("Wrong SELinux type")
+ return rv
+
+
+class SENodeEntry(SuccessEntry):
+ """ SELinux node """
+ selinuxtype = models.CharField(max_length=128)
+ current_selinuxtype = models.CharField(max_length=128, null=True)
+ proto = models.CharField(max_length=4)
+
+ ENTRY_TYPE = r"SENode"
+
+ def selinuxtype_problem(self):
+ """Check for an selinux type problem."""
+ if not self.current_selinuxtype:
+ return True
+ return self.selinuxtype != self.current_selinuxtype
+
+ def short_list(self):
+ """Return a list of problems"""
+ rv = super(SENodeEntry, self).short_list()
+ if self.selinuxtype_problem():
+ rv.append("Wrong SELinux type")
+ return rv
+
+
+class SELoginEntry(SuccessEntry):
+ """ SELinux login """
+ selinuxuser = models.CharField(max_length=128)
+ current_selinuxuser = models.CharField(max_length=128, null=True)
+
+ ENTRY_TYPE = r"SELogin"
+
+
+class SEUserEntry(SuccessEntry):
+ """ SELinux user """
+ roles = models.CharField(max_length=128)
+ current_roles = models.CharField(max_length=128, null=True)
+ prefix = models.CharField(max_length=128)
+ current_prefix = models.CharField(max_length=128, null=True)
+
+ ENTRY_TYPE = r"SEUser"
+
+
+class SEInterfaceEntry(SuccessEntry):
+ """ SELinux interface """
+ selinuxtype = models.CharField(max_length=128)
+ current_selinuxtype = models.CharField(max_length=128, null=True)
+
+ ENTRY_TYPE = r"SEInterface"
+
+ def selinuxtype_problem(self):
+ """Check for an selinux type problem."""
+ if not self.current_selinuxtype:
+ return True
+ return self.selinuxtype != self.current_selinuxtype
+
+ def short_list(self):
+ """Return a list of problems"""
+ rv = super(SEInterfaceEntry, self).short_list()
+ if self.selinuxtype_problem():
+ rv.append("Wrong SELinux type")
+ return rv
+
+
+class SEPermissiveEntry(SuccessEntry):
+ """ SELinux permissive domain """
+ ENTRY_TYPE = r"SEPermissive"
+
+
+class SEModuleEntry(SuccessEntry):
+ """ SELinux module """
+ disabled = models.BooleanField(default=False)
+ current_disabled = models.BooleanField(default=False)
+
+ ENTRY_TYPE = r"SEModule"
+
+
+class POSIXUserEntry(SuccessEntry):
+ """ POSIX user """
+ uid = models.IntegerField(null=True)
+ current_uid = models.IntegerField(null=True)
+ group = models.CharField(max_length=64)
+ current_group = models.CharField(max_length=64, null=True)
+ gecos = models.CharField(max_length=1024)
+ current_gecos = models.CharField(max_length=1024, null=True)
+ home = models.CharField(max_length=1024)
+ current_home = models.CharField(max_length=1024, null=True)
+ shell = models.CharField(max_length=1024, default='/bin/bash')
+ current_shell = models.CharField(max_length=1024, null=True)
+
+ ENTRY_TYPE = r"POSIXUser"
+
+
+class POSIXGroupEntry(SuccessEntry):
+ """ POSIX group """
+ gid = models.IntegerField(null=True)
+ current_gid = models.IntegerField(null=True)
+
+ ENTRY_TYPE = r"POSIXGroup"
+
+
class PackageEntry(SuccessEntry):
""" The new model for package information """
@@ -455,7 +604,7 @@ class PackageEntry(SuccessEntry):
verification_details = models.TextField(default="")
ENTRY_TYPE = r"Package"
- #TODO - prune
+ # TODO - prune
def version_problem(self):
"""Check for a version problem."""
@@ -612,3 +761,7 @@ class ServiceEntry(SuccessEntry):
return rv
+ENTRY_TYPES = (ActionEntry, PackageEntry, PathEntry, ServiceEntry,
+ SEBooleanEntry, SEPortEntry, SEFcontextEntry, SENodeEntry,
+ SELoginEntry, SEUserEntry, SEInterfaceEntry, SEPermissiveEntry,
+ SEModuleEntry)
diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/item.html b/src/lib/Bcfg2/Reporting/templates/config_items/item.html
index 737760252..259414399 100644
--- a/src/lib/Bcfg2/Reporting/templates/config_items/item.html
+++ b/src/lib/Bcfg2/Reporting/templates/config_items/item.html
@@ -45,36 +45,47 @@ div.entry_list h3 {
{% endif %}
{# Really need a better test here #}
-{% if item.mdoe_problem or item.status_problem or item.linkentry.link_problem or item.version_problem %}
+{% if item.mode_problem or item.status_problem or item.linkentry.link_problem or item.version_problem %}
<table class='entry_list'>
<tr id='table_list_header'>
<td style='text-align: right;'>Problem Type</td><td>Expected</td><td style='border-bottom: 1px solid #98DBCC;'>Found</td></tr>
{% if item.mode_problem %}
{% if item.current_perms.owner %}
- <tr><td style='text-align: right'><b>Owner</b></td><td>{{item.target_perms.owner}}</td>
+ <tr><td style='text-align: right'><b>Owner</b></td>
+ <td>{{item.target_perms.owner}}</td>
<td>{{item.current_perms.owner}}</td></tr>
{% endif %}
{% if item.current_perms.group %}
- <tr><td style='text-align: right'><b>Group</b></td><td>{{item.target_perms.group}}</td>
+ <tr><td style='text-align: right'><b>Group</b></td>
+ <td>{{item.target_perms.group}}</td>
<td>{{item.current_perms.group}}</td></tr>
{% endif %}
{% if item.current_perms.mode%}
- <tr><td style='text-align: right'><b>Mode</b></td><td>{{item.target_perms.mode}}</td>
+ <tr><td style='text-align: right'><b>Permissions</b>
+ </td><td>{{item.target_perms.mode}}</td>
<td>{{item.current_perms.mode}}</td></tr>
{% endif %}
{% endif %}
{% if item.status_problem %}
- <tr><td style='text-align: right'><b>Status</b></td><td>{{item.target_status}}</td>
- <td>{{item.current_status}}</td></tr>
+ <tr><td style='text-align: right'><b>Status</b></td>
+ <td>{{item.target_status}}</td>
+ <td>{{item.current_status}}</td></tr>
{% endif %}
{% if item.linkentry.link_problem %}
- <tr><td style='text-align: right'><b>{{item.get_path_type_display}}</b></td><td>{{item.linkentry.target_path}}</td>
- <td>{{item.linkentry.current_path}}</td></tr>
+ <tr><td style='text-align: right'><b>{{item.get_path_type_display}}</b></td>
+ <td>{{item.linkentry.target_path}}</td>
+ <td>{{item.linkentry.current_path}}</td></tr>
{% endif %}
{% if item.version_problem %}
- <tr><td style='text-align: right'><b>Package Version</b></td><td>{{item.target_version|cut:"("|cut:")"}}</td>
+ <tr><td style='text-align: right'><b>Package Version</b></td>
+ <td>{{item.target_version|cut:"("|cut:")"}}</td>
<td>{{item.current_version|cut:"("|cut:")"}}</td></tr>
{% endif %}
+ {% if item.selinuxtype_problem %}
+ <tr><td style='text-align: right'><b>SELinux Type</b></td>
+ <td>{{item.selinuxtype}}</td>
+ <td>{{item.current_selinuxtype}}</td></tr>
+ {% endif %}
</table>
{% endif %}
@@ -92,7 +103,7 @@ div.entry_list h3 {
{{ item.details|syntaxhilight }}
</div>
{% else %}
- {{ item.details }}
+ {{ item.details }}
{% endif %}
</div>
{% endif %}
diff --git a/src/lib/Bcfg2/Reporting/views.py b/src/lib/Bcfg2/Reporting/views.py
index 0341a18af..6cba7bf8c 100644
--- a/src/lib/Bcfg2/Reporting/views.py
+++ b/src/lib/Bcfg2/Reporting/views.py
@@ -161,7 +161,7 @@ def config_item(request, pk, entry_type, interaction=None):
ts_end = ts_start + timedelta(days=1)
associated_list = item.interaction_set.select_related('client').filter(\
timestamp__gte=ts_start, timestamp__lt=ts_end)
-
+
if item.is_failure():
template = 'config_items/item-failure.html'
else:
@@ -184,7 +184,7 @@ def config_item_list(request, item_state, timestamp=None, **kwargs):
current_clients = [q['id'] for q in _handle_filters(current_clients, **kwargs).values('id')]
lists = []
- for etype in ActionEntry, PackageEntry, PathEntry, ServiceEntry:
+ for etype in ENTRY_TYPES:
ldata = etype.objects.filter(state=state, interaction__in=current_clients)\
.annotate(num_entries=Count('id')).select_related('linkentry', 'target_perms', 'current_perms')
if len(ldata) > 0:
@@ -218,7 +218,7 @@ def entry_status(request, entry_type, pk, timestamp=None, **kwargs):
if it.pk not in seen:
items.append((it, it.interaction_set.filter(pk__in=current_clients).order_by('client__name').select_related('client')))
seen.append(it.pk)
-
+
return render_to_response('config_items/entry_status.html',
{'entry': item,
'items': items,
@@ -254,8 +254,8 @@ def common_problems(request, timestamp=None, threshold=None, group=None):
else:
current_clients = Interaction.objects.recent_ids(timestamp)
lists = []
- for etype in ActionEntry, PackageEntry, PathEntry, ServiceEntry:
- ldata = etype.objects.exclude(state=TYPE_GOOD).filter(
+ for etype in ENTRY_TYPES:
+ ldata = etype.objects.exclude(state=TYPE_GOOD).filter(
interaction__in=current_clients).annotate(num_entries=Count('id')).filter(num_entries__gte=threshold)\
.order_by('-num_entries', 'name')
if len(ldata) > 0:
@@ -315,7 +315,8 @@ def client_detailed_list(request, timestamp=None, **kwargs):
kwargs['orderby'] = "client__name"
kwargs['sort'] = "client"
- kwargs['interaction_base'] = Interaction.objects.recent(timestamp).select_related()
+ kwargs['interaction_base'] = \
+ Interaction.objects.recent(timestamp).select_related()
kwargs['page_limit'] = 0
return render_history_view(request, 'clients/detailed-list.html', **kwargs)
@@ -330,16 +331,18 @@ def client_detail(request, hostname=None, pk=None):
inter = client.interactions.get(pk=pk)
maxdate = inter.timestamp
- etypes = { TYPE_BAD: 'bad', TYPE_MODIFIED: 'modified', TYPE_EXTRA: 'extra' }
+ etypes = {TYPE_BAD: 'bad',
+ TYPE_MODIFIED: 'modified',
+ TYPE_EXTRA: 'extra'}
edict = dict()
for label in etypes.values():
edict[label] = []
- for ekind in ('actions', 'packages', 'paths', 'services'):
+ for ekind in inter.entry_types:
for ent in getattr(inter, ekind).all():
edict[etypes[ent.state]].append(ent)
context['entry_types'] = edict
- context['interaction']=inter
+ context['interaction'] = inter
return render_history_view(request, 'clients/detail.html', page_limit=5,
client=client, maxdate=maxdate, context=context)
@@ -356,7 +359,8 @@ def client_manage(request):
client.expiration = datetime.now()
client.save()
message = "Expiration for %s set to %s." % \
- (client_name, client.expiration.strftime("%Y-%m-%d %H:%M:%S"))
+ (client_name,
+ client.expiration.strftime("%Y-%m-%d %H:%M:%S"))
elif client_action == 'unexpire':
client.expiration = None
client.save()
diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py
index ee1006d1a..0ded7ac26 100644
--- a/src/lib/Bcfg2/Server/Core.py
+++ b/src/lib/Bcfg2/Server/Core.py
@@ -100,9 +100,7 @@ class BaseCore(object):
#: The Bcfg2 repository directory
self.datastore = setup['repo']
- if setup['debug']:
- level = logging.DEBUG
- elif setup['verbose']:
+ if setup['verbose']:
level = logging.INFO
else:
level = logging.WARNING
@@ -120,6 +118,25 @@ class BaseCore(object):
#: A :class:`logging.Logger` object for use by the core
self.logger = logging.getLogger('bcfg2-server')
+ #: Log levels for the various logging handlers with debug True
+ #: and False. Each loglevel dict is a dict of ``logger name
+ #: => log level``; the logger names are set in
+ #: :mod:`Bcfg2.Logger`. The logger name ``default`` is
+ #: special, and will be used for any log handlers whose name
+ #: does not appear elsewhere in the dict. At a minimum,
+ #: ``default`` must be provided.
+ self._loglevels = {True: dict(default=logging.DEBUG),
+ False: dict(console=logging.INFO,
+ default=level)}
+
+ #: Used to keep track of the current debug state of the core.
+ self.debug_flag = False
+
+ # enable debugging on the core now. debugging is enabled on
+ # everything else later
+ if setup['debug']:
+ self.set_core_debug(None, setup['debug'])
+
try:
filemonitor = \
Bcfg2.Server.FileMonitor.available[setup['filemonitor']]
@@ -308,6 +325,11 @@ 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.
@@ -405,6 +427,7 @@ class BaseCore(object):
def shutdown(self):
""" Perform plugin and FAM shutdown tasks. """
+ self.logger.debug("Shutting down core...")
if not self.terminate.isSet():
self.terminate.set()
self.fam.shutdown()
@@ -438,6 +461,8 @@ class BaseCore(object):
hook.
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
"""
+ self.logger.debug("Running %s hooks for %s" % (hook,
+ metadata.hostname))
start = time.time()
try:
for plugin in \
@@ -471,6 +496,7 @@ class BaseCore(object):
client
: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):
try:
@@ -497,6 +523,7 @@ class BaseCore(object):
client
: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):
try:
plugin.validate_goals(metadata, data)
@@ -517,6 +544,7 @@ class BaseCore(object):
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
: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]))
sbundles = [b.get('name') for b in structures if b.tag == 'Bundle']
@@ -539,6 +567,7 @@ class BaseCore(object):
structures to. Modified in-place.
:type config: lxml.etree._Element
"""
+ self.logger.debug("Binding structures for %s" % metadata.hostname)
for astruct in structures:
try:
self.BindStructure(astruct, metadata)
@@ -555,6 +584,9 @@ class BaseCore(object):
:param metadata: Client metadata to bind structure for
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
"""
+ self.logger.debug("Binding structure %s for %s" %
+ (structure.get("name", "unknown"),
+ metadata.hostname))
for entry in structure.getchildren():
if entry.tag.startswith("Bound"):
entry.tag = entry.tag[5:]
@@ -630,6 +662,7 @@ class BaseCore(object):
:type client: string
:returns: :class:`lxml.etree._Element` - A complete Bcfg2
configuration document """
+ self.logger.debug("Building configuration for %s" % client)
start = time.time()
config = lxml.etree.Element("Configuration", version='2.0',
revision=self.revision)
@@ -729,6 +762,7 @@ class BaseCore(object):
self.shutdown()
raise
+ self.set_debug(None, self.debug_flag)
self._block()
def _daemonize(self):
@@ -757,6 +791,7 @@ class BaseCore(object):
:type mode: string
:returns: list of Decision tuples ``(<entry tag>, <entry name>)``
"""
+ self.logger.debug("Getting decision list for %s" % metadata.hostname)
result = []
for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Decision):
try:
@@ -785,6 +820,7 @@ class BaseCore(object):
else:
imd = self.metadata_cache.get(client_name, None)
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:
grps = conn.get_additional_groups(imd)
@@ -806,6 +842,7 @@ class BaseCore(object):
:param statistics: The statistics document to process
:type statistics: lxml.etree._Element
"""
+ self.logger.debug("Processing statistics for %s" % client_name)
meta = self.build_metadata(client_name)
state = statistics.find(".//Statistics")
if state.get('version') >= '2.0':
@@ -933,6 +970,7 @@ class BaseCore(object):
return func.__doc__
@exposed
+ @track_statistics()
def DeclareVersion(self, address, version):
""" Declare the client version.
@@ -943,7 +981,9 @@ class BaseCore(object):
:returns: bool - True on success
:raises: :exc:`xmlrpclib.Fault`
"""
- client = self.resolve_client(address)[0]
+ client = self.resolve_client(address, metadata=False)[0]
+ self.logger.debug("%s is running Bcfg2 client version %s" % (client,
+ version))
try:
self.metadata.set_version(client, version)
except (Bcfg2.Server.Plugin.MetadataConsistencyError,
@@ -965,6 +1005,7 @@ class BaseCore(object):
"""
resp = lxml.etree.Element('probes')
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 probe in plugin.GetProbes(metadata):
@@ -986,6 +1027,7 @@ class BaseCore(object):
:raises: :exc:`xmlrpclib.Fault`
"""
client, metadata = self.resolve_client(address)
+ self.logger.debug("Receiving probe data from %s" % client)
if self.metadata_cache_mode == 'cautious':
# clear the metadata cache right after building the
# metadata object; that way the cache is cleared for any
@@ -1032,6 +1074,7 @@ class BaseCore(object):
:raises: :exc:`xmlrpclib.Fault`
"""
client = self.resolve_client(address, metadata=False)[0]
+ self.logger.debug("%s sets its profile to %s" % (client, profile))
try:
self.metadata.set_profile(client, profile, address)
except (Bcfg2.Server.Plugin.MetadataConsistencyError,
@@ -1132,9 +1175,17 @@ class BaseCore(object):
:type address: tuple
:returns: bool - The new debug state of the FAM
"""
- for plugin in self.plugins.values():
- plugin.toggle_debug()
- return self.toggle_fam_debug(address)
+ return self.set_debug(address, not self.debug_flag)
+
+ @exposed
+ def toggle_core_debug(self, address):
+ """ Toggle debug status of the server core
+
+ :param address: Client (address, hostname) pair
+ :type address: tuple
+ :returns: bool - The new debug state of the FAM
+ """
+ return self.set_core_debug(address, not self.debug_flag)
@exposed
def toggle_fam_debug(self, address):
@@ -1151,6 +1202,8 @@ class BaseCore(object):
def set_debug(self, address, debug):
""" Explicitly set debug status of the FAM and all plugins
+ :param address: Client (address, hostname) pair
+ :type address: tuple
:param debug: The new debug status. This can either be a
boolean, or a string describing the state (e.g.,
"true" or "false"; case-insensitive)
@@ -1161,7 +1214,31 @@ class BaseCore(object):
debug = debug.lower() == "true"
for plugin in self.plugins.values():
plugin.set_debug(debug)
- return self.set_fam_debug(address, debug)
+ rv = self.set_core_debug(address, debug)
+ return self.set_fam_debug(address, debug) and rv
+
+ @exposed
+ def set_core_debug(self, _, debug):
+ """ Explicity set debug status of the server core
+
+ :param debug: The new debug status. This can either be a
+ boolean, or a string describing the state (e.g.,
+ "true" or "false"; case-insensitive)
+ :type debug: bool or string
+ :returns: bool - The new debug state of the FAM
+ """
+ if debug not in [True, False]:
+ debug = debug.lower() == "true"
+ self.debug_flag = debug
+ self.logger.info("Core: debug = %s" % debug)
+ levels = self._loglevels[self.debug_flag]
+ for handler in logging.root.handlers:
+ level = levels.get(handler.get_name(), levels['default'])
+ self.logger.debug("Setting %s log handler to %s" %
+ (handler.get_name(),
+ logging.getLevelName(level)))
+ handler.setLevel(level)
+ return self.debug_flag
@exposed
def set_fam_debug(self, address, debug):
diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py
index 58144958e..e430e3160 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/__init__.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py
@@ -291,6 +291,8 @@ class FileMonitor(Debuggable):
def shutdown(self):
""" Handle any tasks required to shut down the monitor. """
+ self.debug_log("Shutting down %s file monitor" %
+ self.__class__.__name__)
self.started = False
def AddMonitor(self, path, obj, handleID=None):
diff --git a/src/lib/Bcfg2/Server/Plugin/base.py b/src/lib/Bcfg2/Server/Plugin/base.py
index e74909ee9..f7bc08717 100644
--- a/src/lib/Bcfg2/Server/Plugin/base.py
+++ b/src/lib/Bcfg2/Server/Plugin/base.py
@@ -2,6 +2,7 @@
import os
import logging
+from Bcfg2.Utils import ClassName
class Debuggable(object):
@@ -33,8 +34,8 @@ class Debuggable(object):
:returns: bool - The new value of the debug flag
"""
self.debug_flag = debug
- self.debug_log("%s: debug_flag = %s" % (self.__class__.__name__,
- self.debug_flag),
+ self.debug_log("%s: debug = %s" % (self.__class__.__name__,
+ self.debug_flag),
flag=True)
return debug
@@ -59,18 +60,6 @@ class Debuggable(object):
self.logger.error(message)
-class ClassName(object):
- """ This very simple descriptor class exists only to get the name
- of the owner class. This is used because, for historical reasons,
- we expect every plugin to have a ``name`` attribute that is in
- almost all cases the same as the ``__class__.__name__`` attribute
- of the plugin object. This makes that more dynamic so that each
- plugin isn't repeating its own name. """
-
- def __get__(self, inst, owner):
- return owner.__name__
-
-
class Plugin(Debuggable):
""" The base class for all Bcfg2 Server plugins. """
@@ -133,6 +122,7 @@ class Plugin(Debuggable):
""" Perform shutdown tasks for the plugin
:returns: None """
+ self.debug_log("Shutting down %s plugin" % self.name)
self.running = False
def __str__(self):
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 41c450b4e..c2252f956 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -5,6 +5,7 @@ import re
import sys
import copy
import time
+import glob
import logging
import operator
import lxml.etree
@@ -503,13 +504,14 @@ class XMLFileBacked(FileBacked):
def _follow_xincludes(self, fname=None, xdata=None):
""" follow xincludes, adding included files to self.extras """
+ xinclude = '%sinclude' % Bcfg2.Server.XI_NAMESPACE
+
if xdata is None:
if fname is None:
xdata = self.xdata.getroottree()
else:
xdata = lxml.etree.parse(fname)
- included = [el for el in xdata.findall('//%sinclude' %
- Bcfg2.Server.XI_NAMESPACE)]
+ included = [el for el in xdata.findall('//' + xinclude)]
for el in included:
name = el.get("href")
if name.startswith("/"):
@@ -520,16 +522,23 @@ class XMLFileBacked(FileBacked):
else:
rel = self.name
fpath = os.path.join(os.path.dirname(rel), name)
- if fpath not in self.extras:
- if os.path.exists(fpath):
- self._follow_xincludes(fname=fpath)
- self.add_monitor(fpath)
+
+ # expand globs in xinclude, a bcfg2-specific extension
+ extras = glob.glob(fpath)
+ if not extras:
+ msg = "%s: %s does not exist, skipping" % (self.name, name)
+ if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
+ LOGGER.debug(msg)
else:
- msg = "%s: %s does not exist, skipping" % (self.name, name)
- if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
- LOGGER.debug(msg)
- else:
- LOGGER.warning(msg)
+ 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)
+ lxml.etree.SubElement(parent, xinclude, href=extra)
+ self._follow_xincludes(fname=extra)
def Index(self):
self.xdata = lxml.etree.XML(self.data, base_url=self.name,
diff --git a/src/lib/Bcfg2/Server/Plugin/interfaces.py b/src/lib/Bcfg2/Server/Plugin/interfaces.py
index f42ada773..cb996b1ca 100644
--- a/src/lib/Bcfg2/Server/Plugin/interfaces.py
+++ b/src/lib/Bcfg2/Server/Plugin/interfaces.py
@@ -313,6 +313,7 @@ class Threaded(object):
"""
raise NotImplementedError
+
class ThreadedStatistics(Statistics, Threaded, threading.Thread):
""" ThreadedStatistics plugins process client statistics in a
separate thread. """
diff --git a/src/lib/Bcfg2/Server/Plugins/DBStats.py b/src/lib/Bcfg2/Server/Plugins/DBStats.py
index e0794f019..e6ef50fa1 100644
--- a/src/lib/Bcfg2/Server/Plugins/DBStats.py
+++ b/src/lib/Bcfg2/Server/Plugins/DBStats.py
@@ -9,7 +9,6 @@ class DBStats(Bcfg2.Server.Plugin.Plugin):
def __init__(self, core, datastore):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
self.logger.error("DBStats has been replaced with Reporting")
- self.logger.error("DBStats: Be sure to migrate your data "\
- "before running the report collector")
+ self.logger.error("DBStats: Be sure to migrate your data "
+ "before running the report collector")
raise Bcfg2.Server.Plugin.PluginInitError
-
diff --git a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
index 1b12e590a..5716a134f 100644
--- a/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
+++ b/src/lib/Bcfg2/Server/Plugins/GroupPatterns.py
@@ -6,28 +6,7 @@ import sys
import logging
import Bcfg2.Server.Lint
import Bcfg2.Server.Plugin
-from Bcfg2.Compat import any # pylint: disable=W0622
-
-
-class PackedDigitRange(object):
- """ Helper object for NameRange entries """
-
- def __init__(self, digit_range):
- self.sparse = list()
- self.ranges = list()
- for item in digit_range.split(','):
- if '-' in item:
- self.ranges.append(tuple([int(x) for x in item.split('-')]))
- else:
- self.sparse.append(int(item))
-
- def includes(self, other):
- """ return True if other is included in this range """
- iother = int(other)
- if iother in self.sparse:
- return True
- return any(iother in range(start, end + 1)
- for start, end in self.ranges)
+from Bcfg2.Utils import PackedDigitRange
class PatternMap(object):
diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py
index fe8dd86a6..a81139b5d 100644
--- a/src/lib/Bcfg2/Server/Plugins/Metadata.py
+++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -15,6 +15,7 @@ import Bcfg2.Server
import Bcfg2.Server.Lint
import Bcfg2.Server.Plugin
import Bcfg2.Server.FileMonitor
+from Bcfg2.Utils import locked
from Bcfg2.Compat import MutableMapping, all, wraps # pylint: disable=W0622
from Bcfg2.version import Bcfg2VersionInfo
@@ -27,15 +28,6 @@ except ImportError:
LOGGER = logging.getLogger(__name__)
-def locked(fd):
- """ Acquire a lock on a file """
- try:
- fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
- except IOError:
- return True
- return False
-
-
if HAS_DJANGO:
class MetadataClientModel(models.Model,
Bcfg2.Server.Plugin.PluginDatabaseModel):
@@ -195,7 +187,7 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked):
newcontents = lxml.etree.tostring(dataroot, xml_declaration=False,
pretty_print=True).decode('UTF-8')
- while locked(fd) == True:
+ while locked(fd):
pass
try:
datafile.write(newcontents)
@@ -392,7 +384,7 @@ class MetadataGroup(tuple):
class Metadata(Bcfg2.Server.Plugin.Metadata,
- Bcfg2.Server.Plugin.Statistics,
+ Bcfg2.Server.Plugin.ClientRunHooks,
Bcfg2.Server.Plugin.DatabaseBacked):
"""This class contains data for bcfg2 server metadata."""
__author__ = 'bcfg-dev@mcs.anl.gov'
@@ -400,7 +392,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
def __init__(self, core, datastore, watch_clients=True):
Bcfg2.Server.Plugin.Metadata.__init__(self)
- Bcfg2.Server.Plugin.Statistics.__init__(self, core, datastore)
+ Bcfg2.Server.Plugin.ClientRunHooks.__init__(self)
Bcfg2.Server.Plugin.DatabaseBacked.__init__(self, core, datastore)
self.watch_clients = watch_clients
self.states = dict()
@@ -685,8 +677,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.raddresses[clname] = set()
self.raddresses[clname].add(caddr)
if 'auth' in client.attrib:
- self.auth[client.get('name')] = client.get('auth',
- 'cert+password')
+ self.auth[client.get('name')] = client.get('auth')
if 'uuid' in client.attrib:
self.uuid[client.get('uuid')] = clname
if client.get('secure', 'false').lower() == 'true':
@@ -1200,7 +1191,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
# look at cert.cN
client = certinfo['commonName']
self.debug_log("Got cN %s; using as client name" % client)
- auth_type = self.auth.get(client, 'cert+password')
+ auth_type = self.auth.get(client,
+ self.core.setup['authentication'])
elif user == 'root':
id_method = 'address'
try:
@@ -1223,12 +1215,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
self.debug_log("Authenticating client %s" % client)
# next we validate the address
- if id_method == 'uuid':
- addr_is_valid = True
- else:
- addr_is_valid = self.validate_client_address(client, address)
-
- if not addr_is_valid:
+ if (id_method != 'uuid' and
+ not self.validate_client_address(client, address)):
return False
if id_method == 'cert' and auth_type != 'cert+password':
@@ -1238,23 +1226,19 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
# we are done if cert+password not required
return True
- if client not in self.passwords:
- if client in self.secure:
- self.logger.error("Client %s in secure mode but has no "
- "password" % address[0])
- return False
- if password != self.password:
- self.logger.error("Client %s used incorrect global password" %
- address[0])
- return False
+ if client not in self.passwords and client in self.secure:
+ self.logger.error("Client %s in secure mode but has no password" %
+ address[0])
+ return False
+
if client not in self.secure:
if client in self.passwords:
plist = [self.password, self.passwords[client]]
else:
plist = [self.password]
if password not in plist:
- self.logger.error("Client %s failed to use either allowed "
- "password" % address[0])
+ self.logger.error("Client %s failed to use an allowed password"
+ % address[0])
return False
else:
# client in secure mode and has a client password
@@ -1268,12 +1252,11 @@ class Metadata(Bcfg2.Server.Plugin.Metadata,
return True
# pylint: enable=R0911,R0912
- def process_statistics(self, meta, _):
- """ Hook into statistics interface to toggle clients in
- bootstrap mode """
- client = meta.hostname
- if client in self.auth and self.auth[client] == 'bootstrap':
- self.update_client(client, dict(auth='cert'))
+ def end_statistics(self, metadata):
+ """ Hook to toggle clients in bootstrap mode """
+ if self.auth.get(metadata.hostname,
+ self.core.setup['authentication']) == 'bootstrap':
+ self.update_client(metadata.hostname, dict(auth='cert'))
def viz(self, hosts, bundles, key, only_client, colors):
"""Admin mode viz support."""
diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
index ec0d8e828..27f493677 100644
--- a/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
+++ b/src/lib/Bcfg2/Server/Plugins/Packages/Apt.py
@@ -35,7 +35,7 @@ class AptCollection(Collection):
for source in self:
if source.rawurl:
- self.logger.info("Packages: Skipping rawurl %s" %
+ self.logger.info("Packages: Skipping rawurl %s" %
source.rawurl)
else:
lines.append("deb %s %s %s" % (source.url, source.version,
diff --git a/src/lib/Bcfg2/Server/Plugins/Reporting.py b/src/lib/Bcfg2/Server/Plugins/Reporting.py
index d072f1a33..a6dc2c1ef 100644
--- a/src/lib/Bcfg2/Server/Plugins/Reporting.py
+++ b/src/lib/Bcfg2/Server/Plugins/Reporting.py
@@ -65,10 +65,13 @@ class Reporting(Statistics, Threaded, PullSource, Debuggable):
(self.name, traceback.format_exc().splitlines()[-1])
self.logger.error(msg)
raise PluginInitError(msg)
+ if self.debug_flag:
+ self.transport.set_debug(self.debug_flag)
def set_debug(self, debug):
rv = Debuggable.set_debug(self, debug)
- self.transport.set_debug(debug)
+ if self.transport is not None:
+ self.transport.set_debug(debug)
return rv
def process_statistics(self, client, xdata):
diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py
new file mode 100644
index 000000000..7d7d26d5d
--- /dev/null
+++ b/src/lib/Bcfg2/Utils.py
@@ -0,0 +1,225 @@
+""" Miscellaneous useful utility functions, classes, etc., that are
+used by both client and server. Stuff that doesn't fit anywhere
+else. """
+
+import fcntl
+import logging
+import threading
+import subprocess
+from Bcfg2.Compat import any # pylint: disable=W0622
+
+
+class ClassName(object):
+ """ This very simple descriptor class exists only to get the name
+ of the owner class. This is used because, for historical reasons,
+ we expect every server plugin and every client tool to have a
+ ``name`` attribute that is in almost all cases the same as the
+ ``__class__.__name__`` attribute of the plugin object. This makes
+ that more dynamic so that each plugin and tool isn't repeating its own
+ name."""
+
+ def __get__(self, inst, owner):
+ return owner.__name__
+
+
+class PackedDigitRange(object):
+ """ Representation of a set of integer ranges. A range is
+ described by a comma-delimited string of integers and ranges,
+ e.g.::
+
+ 1,10-12,15-20
+
+ Ranges are inclusive on both bounds, and may include 0. Negative
+ numbers are not supported."""
+
+ def __init__(self, *ranges):
+ """ May be instantiated in one of two ways::
+
+ PackedDigitRange(<comma-delimited list of ranges>)
+
+ Or::
+
+ PackedDigitRange(<int_or_range>[, <int_or_range>[, ...]])
+
+ E.g., both of the following are valid::
+
+ PackedDigitRange("1-5,7, 10-12")
+ PackedDigitRange("1-5", 7, "10-12")
+ """
+ self.ranges = []
+ self.ints = []
+ self.str = ",".join(str(r) for r in ranges)
+ if len(ranges) == 1 and "," in ranges[0]:
+ ranges = ranges[0].split(",")
+ for item in ranges:
+ item = str(item).strip()
+ if item.endswith("-"):
+ self.ranges.append((int(item[:-1]), None))
+ elif '-' in str(item):
+ self.ranges.append(tuple(int(x) for x in item.split('-')))
+ else:
+ self.ints.append(int(item))
+
+ def includes(self, other):
+ """ Return True if ``other`` is included in this range.
+ Functionally equivalent to ``other in range``, which should be
+ used instead. """
+ return other in self
+
+ def __contains__(self, other):
+ other = int(other)
+ if other in self.ints:
+ return True
+ return any((end is None and other >= start) or
+ (end is not None and other >= start and other <= end)
+ for start, end in self.ranges)
+
+ def __repr__(self):
+ return "%s:%s" % (self.__class__.__name__, str(self))
+
+ 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.
+
+ :param fd: The file descriptor to lock
+ :type fd: int
+ :returns: bool - True if the file is already locked, False
+ otherwise """
+ try:
+ fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except IOError:
+ return True
+ return False
+
+
+class ExecutorResult(object):
+ """ Returned as the result of a call to
+ :func:`Bcfg2.Utils.Executor.run`. The result can be accessed via
+ the instance variables, documented below, as a boolean (which is
+ equivalent to :attr:`Bcfg2.Utils.ExecutorResult.success`), or as a
+ tuple, which, for backwards compatibility, is equivalent to
+ ``(result.retval, result.stdout.splitlines())``."""
+
+ def __init__(self, stdout, stderr, retval):
+ #: The output of the command
+ self.stdout = stdout
+
+ #: The error produced by the command
+ self.stderr = stderr
+
+ #: The return value of the command.
+ self.retval = retval
+
+ #: Whether or not the command was successful. If the
+ #: ExecutorResult is used as a boolean, ``success`` is
+ #: returned.
+ self.success = retval == 0
+
+ #: A friendly error message
+ self.error = None
+ if self.retval:
+ if self.stderr:
+ self.error = "%s (rv: %s)" % (self.stderr, self.retval)
+ elif self.stdout:
+ self.error = "%s (rv: %s)" % (self.stdout, self.retval)
+ else:
+ self.error = "No output or error; return value %s" % \
+ self.retval
+
+ def __repr__(self):
+ if self.error:
+ return "Errored command result: %s" % self.error
+ elif self.stdout:
+ return "Successful command result: %s" % self.stdout
+ else:
+ return "Successful command result: No output"
+
+ def __getitem__(self, idx):
+ """ This provides compatibility with the old Executor, which
+ returned a tuple of (return value, stdout split by lines). """
+ return (self.retval, self.stdout.splitlines())[idx]
+
+ def __nonzero__(self):
+ return self.__bool__()
+
+ def __bool__(self):
+ return self.success
+
+
+class Executor(object):
+ """ A convenient way to run external commands with
+ :class:`subprocess.Popen` """
+
+ def __init__(self, timeout=None):
+ """
+ :param timeout: Set a default timeout for all commands run by
+ this Executor object
+ :type timeout: float
+ """
+ self.logger = logging.getLogger(self.__class__.__name__)
+ self.timeout = timeout
+
+ def _timeout(self, proc):
+ """ A function suitable for passing to
+ :class:`threading.Timer` that kills the given process.
+
+ :param proc: The process to kill upon timeout.
+ :type proc: subprocess.Popen
+ :returns: None """
+ if proc.poll() == None:
+ try:
+ proc.kill()
+ self.logger.warning("Process exceeeded timeout, killing")
+ except OSError:
+ pass
+
+ def run(self, command, inputdata=None, shell=False, timeout=None):
+ """ Run a command, given as a list, optionally giving it the
+ specified input data.
+
+ :param command: The command to run, as a list (preferred) or
+ as a string. See :class:`subprocess.Popen` for
+ details.
+ :type command: list or string
+ :param inputdata: Data to pass to the command on stdin
+ :type inputdata: string
+ :param shell: Run the given command in a shell (not recommended)
+ :type shell: bool
+ :param timeout: Kill the command if it runs longer than this
+ many seconds. Set to 0 or -1 to explicitly
+ override a default timeout.
+ :type timeout: float
+ :returns: :class:`Bcfg2.Utils.ExecutorResult`
+ """
+ if isinstance(command, str):
+ cmdstr = command
+ else:
+ cmdstr = " ".join(command)
+ self.logger.debug("Running: %s" % cmdstr)
+ proc = subprocess.Popen(command, shell=shell, bufsize=16384,
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, close_fds=True)
+ if timeout is None:
+ timeout = self.timeout
+ if timeout is not None:
+ timer = threading.Timer(float(timeout), self._timeout, [proc])
+ timer.start()
+ try:
+ if inputdata:
+ for line in inputdata.splitlines():
+ self.logger.debug('> %s' % line)
+ (stdout, stderr) = proc.communicate(input=inputdata)
+ for line in stdout.splitlines(): # pylint: disable=E1103
+ self.logger.debug('< %s' % line)
+ for line in stderr.splitlines(): # pylint: disable=E1103
+ self.logger.info(line)
+ return ExecutorResult(stdout, stderr, proc.wait())
+ finally:
+ if timeout is not None:
+ timer.cancel()
diff --git a/src/sbin/bcfg2-admin b/src/sbin/bcfg2-admin
index fa4307702..31e49c00b 100755
--- a/src/sbin/bcfg2-admin
+++ b/src/sbin/bcfg2-admin
@@ -10,6 +10,7 @@ import Bcfg2.Options
import Bcfg2.Server.Admin
from Bcfg2.Compat import StringIO
+
def mode_import(modename):
"""Load Bcfg2.Server.Admin.<mode>."""
modname = modename.capitalize()
diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt
index fde6af582..eae316da5 100755
--- a/src/sbin/bcfg2-crypt
+++ b/src/sbin/bcfg2-crypt
@@ -261,7 +261,7 @@ class Encryptor(object):
(self.pname, pname))
return (passphrase, pname)
- def _get_passphrase(self, chunk): # pylint: disable=W0613
+ def _get_passphrase(self, chunk): # pylint: disable=W0613
""" get the passphrase for a chunk of a file """
return None
diff --git a/src/sbin/bcfg2-info b/src/sbin/bcfg2-info
index fa8c89b46..311784606 100755
--- a/src/sbin/bcfg2-info
+++ b/src/sbin/bcfg2-info
@@ -474,7 +474,7 @@ Bcfg2 client itself.""")
('Password', self.setup['password']),
('Server Metadata Connector', self.setup['mconnect']),
('Filemonitor', self.setup['filemonitor']),
- ('Server address', self.setup['location']),
+ ('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']),
@@ -727,7 +727,6 @@ Bcfg2 client itself.""")
pass
-
def build_usage():
""" build usage message """
cmd_blacklist = ["do_loop", "do_EOF"]
diff --git a/src/sbin/bcfg2-server b/src/sbin/bcfg2-server
index 8322edeaa..cdca71e74 100755
--- a/src/sbin/bcfg2-server
+++ b/src/sbin/bcfg2-server
@@ -11,6 +11,7 @@ from Bcfg2.Server.Core import CoreInitError
LOGGER = logging.getLogger('bcfg2-server')
+
def main():
optinfo = dict()
optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
@@ -22,7 +23,7 @@ def main():
if not os.path.exists(setup['configfile']):
print("Could not read %s" % setup['configfile'])
sys.exit(1)
-
+
if setup['backend'] not in ['best', 'cherrypy', 'builtin']:
print("Unknown server backend %s, using 'best'" % setup['backend'])
setup['backend'] = 'best'
diff --git a/src/sbin/bcfg2-test b/src/sbin/bcfg2-test
index f62155850..6eaf0cc33 100755
--- a/src/sbin/bcfg2-test
+++ b/src/sbin/bcfg2-test
@@ -9,24 +9,83 @@ import fnmatch
import logging
import Bcfg2.Logger
import Bcfg2.Server.Core
+from math import ceil
from nose.core import TestProgram
from nose.suite import LazySuite
from unittest import TestCase
+try:
+ from multiprocessing import Process, Queue, active_children
+ HAS_MULTIPROC = True
+except ImportError:
+ HAS_MULTIPROC = False
+ active_children = lambda: [] # pylint: disable=C0103
+
+
+class CapturingLogger(object):
+ """ Fake logger that captures logging output so that errors are
+ only displayed for clients that fail tests """
+ def __init__(self, *args, **kwargs): # pylint: disable=W0613
+ self.output = []
+
+ def error(self, msg):
+ """ discard error messages """
+ self.output.append(msg)
+
+ def warning(self, msg):
+ """ discard error messages """
+ self.output.append(msg)
+
+ def info(self, msg):
+ """ discard error messages """
+ self.output.append(msg)
+
+ def debug(self, msg):
+ """ discard error messages """
+ self.output.append(msg)
+
+ def reset_output(self):
+ """ Reset the captured output """
+ self.output = []
+
+
+class ClientTestFromQueue(TestCase):
+ """ A test case that tests a value that has been enqueued by a
+ child test process. ``client`` is the name of the client that has
+ been tested; ``result`` is the result from the :class:`ClientTest`
+ test. ``None`` indicates a successful test; a string value
+ indicates a failed test; and an exception indicates an error while
+ running the test. """
+ __test__ = False # Do not collect
+
+ def __init__(self, client, result):
+ TestCase.__init__(self)
+ self.client = client
+ self.result = result
+
+ def shortDescription(self):
+ return "Building configuration for %s" % self.client
+
+ def runTest(self):
+ """ parse the result from this test """
+ if isinstance(self.result, Exception):
+ raise self.result
+ assert self.result is None, self.result
+
class ClientTest(TestCase):
- """
- A test case representing the build of all of the configuration for
+ """ A test case representing the build of all of the configuration for
a single host. Checks that none of the build config entities has
had a failure when it is building. Optionally ignores some config
files that we know will cause errors (because they are private
- files we don't have access to, for instance)
- """
+ files we don't have access to, for instance) """
__test__ = False # Do not collect
+ divider = "-" * 70
- def __init__(self, bcfg2_core, client, ignore=None):
+ def __init__(self, core, client, ignore=None):
TestCase.__init__(self)
- self.bcfg2_core = bcfg2_core
+ self.core = core
+ self.core.logger = CapturingLogger()
self.client = client
if ignore is None:
self.ignore = dict()
@@ -51,17 +110,23 @@ class ClientTest(TestCase):
def runTest(self):
""" run this individual test """
- config = self.bcfg2_core.BuildConfiguration(self.client)
+ config = self.core.BuildConfiguration(self.client)
+ output = self.core.logger.output[:]
+ if output:
+ output.append(self.divider)
+ self.core.logger.reset_output()
+ # check for empty client configuration
assert len(config.findall("Bundle")) > 0, \
- "%s has no content" % self.client
+ "\n".join(output + ["%s has no content" % self.client])
# check for missing bundles
- metadata = self.bcfg2_core.build_metadata(self.client)
+ metadata = self.core.build_metadata(self.client)
sbundles = [el.get('name') for el in config.findall("Bundle")]
missing = [b for b in metadata.bundles if b not in sbundles]
assert len(missing) == 0, \
- "Configuration is missing bundle(s): %s" % ':'.join(missing)
+ "\n".join(output + ["Configuration is missing bundle(s): %s" %
+ ':'.join(missing)])
# check for unknown packages
unknown_pkgs = [el.get("name")
@@ -72,7 +137,7 @@ class ClientTest(TestCase):
", ".join(unknown_pkgs)
failures = []
- msg = ["Failures:"]
+ msg = output + ["Failures:"]
for failure in config.xpath('//*[@failure]'):
if not self.ignore_entry(failure.tag, failure.get('name')):
failures.append(failure)
@@ -87,10 +152,46 @@ class ClientTest(TestCase):
id = __str__
-def main():
- optinfo = dict(noseopts=Bcfg2.Options.TEST_NOSEOPTS,
- test_ignore=Bcfg2.Options.TEST_IGNORE,
- validate=Bcfg2.Options.CFG_VALIDATION)
+def get_core(setup):
+ """ Get a server core, with events handled """
+ core = Bcfg2.Server.Core.BaseCore(setup)
+ core.fam.handle_events_in_interval(0.1)
+ return core
+
+
+def get_ignore(setup):
+ """ Given an options dict, get a dict of entry tags and names to
+ ignore errors from """
+ ignore = dict()
+ for entry in setup['test_ignore']:
+ tag, name = entry.split(":")
+ try:
+ ignore[tag].append(name)
+ except KeyError:
+ ignore[tag] = [name]
+ return ignore
+
+
+def run_child(setup, clients, queue):
+ """ Run tests for the given clients in a child process, returning
+ results via the given Queue """
+ core = get_core(setup)
+ ignore = get_ignore(setup)
+ for client in clients:
+ try:
+ ClientTest(core, client, ignore).runTest()
+ queue.put((client, None))
+ except AssertionError:
+ queue.put((client, str(sys.exc_info()[1])))
+ except:
+ queue.put((client, sys.exc_info()[1]))
+
+ core.shutdown()
+
+
+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)
@@ -110,36 +211,88 @@ def main():
to_syslog=False,
to_file=setup['logging'],
level=level)
+ logger = logging.getLogger(sys.argv[0])
if (setup['debug'] or setup['verbose']) and "-v" not in setup['noseopts']:
setup['noseopts'].append("-v")
- core = Bcfg2.Server.Core.BaseCore(setup)
+ if setup['children'] and not HAS_MULTIPROC:
+ logger.warning("Python multiprocessing library not found, running "
+ "with no children")
+ setup['children'] = 0
- ignore = dict()
- for entry in setup['test_ignore']:
- tag, name = entry.split(":")
- try:
- ignore[tag].append(name)
- except KeyError:
- ignore[tag] = [name]
+ if (setup['children'] and ('--with-xunit' in setup['noseopts'] or
+ '--xunit-file' in setup['noseopts'])):
+ logger.warning("Use the --xunit option to bcfg2-test instead of the "
+ "--with-xunit or --xunit-file options to nosetest")
+ xunitfile = None
+ if '--with-xunit' in setup['noseopts']:
+ setup['noseopts'].remove('--with-xunit')
+ xunitfile = "nosetests.xml"
+ if '--xunit-file' in setup['noseopts']:
+ idx = setup['noseopts'].index('--xunit-file')
+ try:
+ setup['noseopts'].pop(idx) # remove --xunit-file
+ # remove the argument to it
+ xunitfile = setup['noseopts'].pop(idx)
+ except IndexError:
+ pass
+ if xunitfile and not setup['xunit']:
+ setup['xunit'] = xunitfile
+ return setup
- core.fam.handle_events_in_interval(0.1)
+
+def main():
+ setup = parse_args()
+ logger = logging.getLogger(sys.argv[0])
+ core = get_core(setup)
if setup['args']:
clients = setup['args']
else:
clients = core.metadata.clients
- def run_tests():
- """ Run the test suite """
- for client in clients:
- yield ClientTest(core, client, ignore)
+ ignore = get_ignore(setup)
- TestProgram(argv=sys.argv[0:1] + setup['noseopts'],
- suite=LazySuite(run_tests))
+ if setup['children']:
+ if setup['children'] > len(clients):
+ logger.info("Refusing to spawn more children than clients to test,"
+ " setting children=%s" % len(clients))
+ setup['children'] = len(clients)
+ perchild = int(ceil(len(clients) / float(setup['children'] + 1)))
+ queue = Queue()
+ for child in range(setup['children']):
+ start = child * perchild
+ end = (child + 1) * perchild
+ child = Process(target=run_child,
+ args=(setup, clients[start:end], queue))
+ child.start()
+
+ def generate_tests():
+ """ Read test results for the clients """
+ start = setup['children'] * perchild
+ for client in clients[start:]:
+ yield ClientTest(core, client, ignore)
+
+ for i in range(start): # pylint: disable=W0612
+ yield ClientTestFromQueue(*queue.get())
+ else:
+ def generate_tests():
+ """ Run tests for the clients """
+ for client in clients:
+ yield ClientTest(core, client, ignore)
+
+ TestProgram(argv=sys.argv[:1] + core.setup['noseopts'],
+ suite=LazySuite(generate_tests), exit=False)
+
+ # block until all children have completed -- should be
+ # immediate since we've already gotten all the results we
+ # expect
+ for child in active_children():
+ child.join()
core.shutdown()
os._exit(0) # pylint: disable=W0212
+
if __name__ == "__main__":
sys.exit(main())
diff --git a/src/sbin/bcfg2-yum-helper b/src/sbin/bcfg2-yum-helper
index ba6f30406..7e5c03fd5 100755
--- a/src/sbin/bcfg2-yum-helper
+++ b/src/sbin/bcfg2-yum-helper
@@ -129,7 +129,7 @@ class DepSolver(object):
err = sys.exc_info()[1]
self.logger.warning(err)
return []
-
+
if ptype == "default":
return [p
for p, d in list(group.default_packages.items())
@@ -254,6 +254,6 @@ def main():
rv[gdata['group']] = list(packages)
print(json.dumps(rv))
-
+
if __name__ == '__main__':
sys.exit(main())
diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py
index b42dc57d8..4fcd63a60 100644
--- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py
+++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py
@@ -6,6 +6,7 @@ import subprocess
from mock import Mock, MagicMock, patch
import Bcfg2.Client.Tools
from Bcfg2.Client.Tools.POSIXUsers import *
+from Bcfg2.Utils import PackedDigitRange
# add all parent testsuite directories to sys.path to allow (most)
# relative imports in python 2.4
@@ -20,105 +21,6 @@ from common import *
from TestTools.Test_init import TestTool
-class TestIDRangeSet(Bcfg2TestCase):
- def test_ranges(self):
- # test cases. tuples of (ranges, included numbers, excluded
- # numbers)
- # tuples of (range description, numbers that are included,
- # numebrs that are excluded)
- tests = [(["0-3"], ["0", 1, "2", 3], [4]),
- (["1"], [1], [0, "2"]),
- (["10-11"], [10, 11], [0, 1]),
- (["9-9"], [9], [8, 10]),
- (["0-100"], [0, 10, 99, 100], []),
- (["1", "3", "5"], [1, 3, 5], [0, 2, 4, 6]),
- (["1-5", "7"], [1, 3, 5, 7], [0, 6, 8]),
- (["1-5", 7, "9-11"], [1, 3, 5, 7, 9, 11], [0, 6, 8, 12]),
- (["852-855", "321-497", 763], [852, 855, 321, 400, 497, 763],
- [851, 320, 766, 999]),
- (["0-"], [0, 1, 100, 100000], []),
- ([1, "5-10", "1000-"], [1, 5, 10, 1000, 10000000],
- [4, 11, 999])]
- for ranges, inc, exc in tests:
- rng = IDRangeSet(*ranges)
- for test in inc:
- self.assertIn(test, rng)
- for test in exc:
- self.assertNotIn(test, rng)
-
-
-class TestExecutor(Bcfg2TestCase):
- test_obj = Executor
-
- def get_obj(self, logger=None):
- if not logger:
- def print_msg(msg):
- print(msg)
- logger = Mock()
- logger.error = Mock(side_effect=print_msg)
- logger.warning = Mock(side_effect=print_msg)
- logger.info = Mock(side_effect=print_msg)
- logger.debug = Mock(side_effect=print_msg)
- return self.test_obj(logger)
-
- @patch("subprocess.Popen")
- def test_run(self, mock_Popen):
- exc = self.get_obj()
- cmd = ["/bin/test", "-a", "foo"]
- proc = Mock()
- proc.wait = Mock()
- proc.wait.return_value = 0
- proc.communicate = Mock()
- proc.communicate.return_value = (MagicMock(), MagicMock())
- mock_Popen.return_value = proc
-
- self.assertTrue(exc.run(cmd))
- args = mock_Popen.call_args
- self.assertEqual(args[0][0], cmd)
- self.assertEqual(args[1]['shell'], False)
- self.assertEqual(args[1]['stdin'], subprocess.PIPE)
- self.assertEqual(args[1]['stdout'], subprocess.PIPE)
- self.assertEqual(args[1]['stderr'], subprocess.PIPE)
- proc.communicate.assert_called_with()
- proc.wait.assert_called_with()
- self.assertEqual(proc.communicate.return_value,
- (exc.stdout, exc.stderr))
- self.assertEqual(proc.wait.return_value,
- exc.retval)
-
- mock_Popen.reset_mock()
- inputdata = "foo\n\nbar"
- self.assertTrue(exc.run(cmd, inputdata=inputdata, shell=True))
- args = mock_Popen.call_args
- self.assertEqual(args[0][0], cmd)
- self.assertEqual(args[1]['shell'], True)
- self.assertEqual(args[1]['stdin'], subprocess.PIPE)
- self.assertEqual(args[1]['stdout'], subprocess.PIPE)
- self.assertEqual(args[1]['stderr'], subprocess.PIPE)
- proc.communicate.assert_called_with(inputdata)
- proc.wait.assert_called_with()
- self.assertEqual(proc.communicate.return_value,
- (exc.stdout, exc.stderr))
- self.assertEqual(proc.wait.return_value,
- exc.retval)
-
- mock_Popen.reset_mock()
- proc.wait.return_value = 1
- self.assertRaises(ExecutionError, exc.run, cmd)
- args = mock_Popen.call_args
- self.assertEqual(args[0][0], cmd)
- self.assertEqual(args[1]['shell'], False)
- self.assertEqual(args[1]['stdin'], subprocess.PIPE)
- self.assertEqual(args[1]['stdout'], subprocess.PIPE)
- self.assertEqual(args[1]['stderr'], subprocess.PIPE)
- proc.communicate.assert_called_with()
- proc.wait.assert_called_with()
- self.assertEqual(proc.communicate.return_value,
- (exc.stdout, exc.stderr))
- self.assertEqual(proc.wait.return_value,
- exc.retval)
-
-
class TestPOSIXUsers(TestTool):
test_obj = POSIXUsers
@@ -166,19 +68,19 @@ class TestPOSIXUsers(TestTool):
def test__in_managed_range(self):
users = self.get_obj()
- users._whitelist = dict(POSIXGroup=IDRangeSet("1-10"))
- users._blacklist = dict(POSIXGroup=IDRangeSet("8-100"))
+ users._whitelist = dict(POSIXGroup=PackedDigitRange("1-10"))
+ users._blacklist = dict(POSIXGroup=PackedDigitRange("8-100"))
self.assertTrue(users._in_managed_range("POSIXGroup", "9"))
users._whitelist = dict(POSIXGroup=None)
- users._blacklist = dict(POSIXGroup=IDRangeSet("8-100"))
+ users._blacklist = dict(POSIXGroup=PackedDigitRange("8-100"))
self.assertFalse(users._in_managed_range("POSIXGroup", "9"))
users._whitelist = dict(POSIXGroup=None)
- users._blacklist = dict(POSIXGroup=IDRangeSet("100-"))
+ users._blacklist = dict(POSIXGroup=PackedDigitRange("100-"))
self.assertTrue(users._in_managed_range("POSIXGroup", "9"))
- users._whitelist = dict(POSIXGroup=IDRangeSet("1-10"))
+ users._whitelist = dict(POSIXGroup=PackedDigitRange("1-10"))
users._blacklist = dict(POSIXGroup=None)
self.assertFalse(users._in_managed_range("POSIXGroup", "25"))
@@ -434,6 +336,10 @@ class TestPOSIXUsers(TestTool):
setter.reset_mock()
users.modified = []
+ cmd_rv = Mock()
+ cmd_rv.success = True
+ users.cmd.run.return_value = cmd_rv
+
reset()
entry = lxml.etree.Element("POSIXUser", name="test2")
self.assertTrue(users._install(entry))
@@ -453,7 +359,7 @@ class TestPOSIXUsers(TestTool):
self.assertIn(entry, users.modified)
reset()
- users.cmd.run.side_effect = ExecutionError(None)
+ cmd_rv.success = False
self.assertFalse(users._install(entry))
users.set_defaults[entry.tag].assert_called_with(entry)
users._get_cmd.assert_called_with("mod",
@@ -551,19 +457,21 @@ class TestPOSIXUsers(TestTool):
users = self.get_obj()
users._get_cmd = Mock()
users.cmd = Mock()
+ cmd_rv = Mock()
+ cmd_rv.success = True
+ users.cmd.run.return_value = cmd_rv
def reset():
users._get_cmd.reset_mock()
users.cmd.reset_mock()
-
entry = lxml.etree.Element("POSIXUser", name="test2")
self.assertTrue(users._remove(entry))
users._get_cmd.assert_called_with("del", entry)
users.cmd.run.assert_called_with(users._get_cmd.return_value)
reset()
- users.cmd.run.side_effect = ExecutionError(None)
+ cmd_rv.success = False
self.assertFalse(users._remove(entry))
users._get_cmd.assert_called_with("del", entry)
users.cmd.run.assert_called_with(users._get_cmd.return_value)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
index 22c53c63e..fb51eb1fe 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
@@ -417,21 +417,22 @@ class TestXMLFileBacked(TestFileBacked):
xfb = self.get_obj(fam=fam, should_monitor=True)
fam.AddMonitor.assert_called_with(self.path, xfb)
- @patch("os.path.exists")
+ @patch("glob.glob")
@patch("lxml.etree.parse")
- def test_follow_xincludes(self, mock_parse, mock_exists):
+ def test_follow_xincludes(self, mock_parse, mock_glob):
xfb = self.get_obj()
xfb.add_monitor = Mock()
+ xfb.add_monitor.side_effect = lambda p: xfb.extras.append(p)
def reset():
xfb.add_monitor.reset_mock()
+ mock_glob.reset_mock()
mock_parse.reset_mock()
- mock_exists.reset_mock()
xfb.extras = []
- mock_exists.return_value = True
xdata = dict()
mock_parse.side_effect = lambda p: xdata[p]
+ mock_glob.side_effect = lambda g: [g]
base = os.path.dirname(self.path)
@@ -465,7 +466,7 @@ class TestXMLFileBacked(TestFileBacked):
xfb.add_monitor.assert_called_with(test2)
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()])
- mock_exists.assert_called_with(test2)
+ mock_glob.assert_called_with(test2)
reset()
xfb._follow_xincludes(fname=self.path, xdata=xdata[self.path])
@@ -473,7 +474,7 @@ class TestXMLFileBacked(TestFileBacked):
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()
if f != self.path])
- mock_exists.assert_called_with(test2)
+ mock_glob.assert_called_with(test2)
# test two-deep level of xinclude, with some files in another
# directory
@@ -501,21 +502,41 @@ class TestXMLFileBacked(TestFileBacked):
reset()
xfb._follow_xincludes(fname=self.path)
- self.assertItemsEqual(xfb.add_monitor.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
+ expected = [call(f) for f in xdata.keys() if f != self.path]
+ self.assertItemsEqual(xfb.add_monitor.call_args_list, expected)
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()])
- self.assertItemsEqual(mock_exists.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
+ self.assertItemsEqual(mock_glob.call_args_list, expected)
reset()
xfb._follow_xincludes(fname=self.path, xdata=xdata[self.path])
- self.assertItemsEqual(xfb.add_monitor.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
- self.assertItemsEqual(mock_parse.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
- self.assertItemsEqual(mock_exists.call_args_list,
- [call(f) for f in xdata.keys() if f != self.path])
+ expected = [call(f) for f in xdata.keys() if f != self.path]
+ self.assertItemsEqual(xfb.add_monitor.call_args_list, expected)
+ self.assertItemsEqual(mock_parse.call_args_list, expected)
+ self.assertItemsEqual(mock_glob.call_args_list, expected)
+
+ # test wildcard xinclude
+ reset()
+ xdata[self.path] = lxml.etree.Element("Test").getroottree()
+ lxml.etree.SubElement(xdata[self.path].getroot(),
+ Bcfg2.Server.XI_NAMESPACE + "include",
+ href="*.xml")
+
+ def glob_rv(path):
+ if path == os.path.join(base, '*.xml'):
+ return [self.path, test2, test3]
+ else:
+ return [path]
+ mock_glob.side_effect = glob_rv
+
+ xfb._follow_xincludes(xdata=xdata[self.path])
+ expected = [call(f) for f in xdata.keys() if f != self.path]
+ self.assertItemsEqual(xfb.add_monitor.call_args_list, expected)
+ self.assertItemsEqual(mock_parse.call_args_list, expected)
+ self.assertItemsEqual(mock_glob.call_args_list,
+ [call(os.path.join(base, '*.xml')), call(test4),
+ call(test5), call(test6)])
+
@patch("lxml.etree._ElementTree", FakeElementTree)
@patch("Bcfg2.Server.Plugin.helpers.%s._follow_xincludes" %
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py
index a7a6b3d6e..a9346156c 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestGroupPatterns.py
@@ -18,27 +18,6 @@ from common import *
from TestPlugin import TestXMLFileBacked, TestPlugin, TestConnector
-class TestPackedDigitRange(Bcfg2TestCase):
- def test_includes(self):
- # tuples of (range description, numbers that are included,
- # numebrs that are excluded)
- tests = [("1-3", [1, "2", 3], [4]),
- ("1", [1], [0, "2"]),
- ("10-11", [10, 11], [0, 1]),
- ("9-9", [9], [8, 10]),
- ("0-100", [0, 10, 99, 100], []),
- ("1,3,5", [1, 3, 5], [0, 2, 4, 6]),
- ("1-5,7", [1, 3, 5, 7], [0, 6, 8]),
- ("1-5,7,9-11", [1, 3, 5, 7, 9, 11], [0, 6, 8, 12]),
- ("852-855,321-497,763", [852, 855, 321, 400, 497, 763], [])]
- for rng, inc, exc in tests:
- r = PackedDigitRange(rng)
- for test in inc:
- self.assertTrue(r.includes(test))
- for test in exc:
- self.assertFalse(r.includes(test))
-
-
class TestPatternMap(Bcfg2TestCase):
def test_ranges(self):
""" test processing NameRange patterns """
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
index f627e4465..69ea45de6 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestMetadata.py
@@ -20,7 +20,7 @@ while path != "/":
path = os.path.dirname(path)
from common import *
from TestPlugin import TestXMLFileBacked, TestMetadata as _TestMetadata, \
- TestStatistics, TestDatabaseBacked
+ TestClientRunHooks, TestDatabaseBacked
def get_clients_test_tree():
@@ -198,6 +198,7 @@ if HAS_DJANGO or can_skip:
class TestXMLMetadataConfig(TestXMLFileBacked):
test_obj = XMLMetadataConfig
+ path = os.path.join(datastore, 'Metadata', 'clients.xml')
def get_obj(self, basefile="clients.xml", core=None, watch_clients=False):
self.metadata = get_metadata_object(core=core,
@@ -326,7 +327,7 @@ class TestXMLMetadataConfig(TestXMLFileBacked):
"clients.xml"),
"<test/>")
- @patch('Bcfg2.Server.Plugins.Metadata.locked', Mock(return_value=False))
+ @patch('Bcfg2.Utils.locked', Mock(return_value=False))
@patch('fcntl.lockf', Mock())
@patch('os.open')
@patch('os.fdopen')
@@ -462,7 +463,7 @@ class TestClientMetadata(Bcfg2TestCase):
self.assertFalse(cm.inGroup("group3"))
-class TestMetadata(_TestMetadata, TestStatistics, TestDatabaseBacked):
+class TestMetadata(_TestMetadata, TestClientRunHooks, TestDatabaseBacked):
test_obj = Metadata
use_db = False
@@ -494,7 +495,7 @@ class TestMetadata(_TestMetadata, TestStatistics, TestDatabaseBacked):
metadata = self.get_obj(core=core)
self.assertIsInstance(metadata, Bcfg2.Server.Plugin.Plugin)
self.assertIsInstance(metadata, Bcfg2.Server.Plugin.Metadata)
- self.assertIsInstance(metadata, Bcfg2.Server.Plugin.Statistics)
+ self.assertIsInstance(metadata, Bcfg2.Server.Plugin.ClientRunHooks)
self.assertIsInstance(metadata.clients_xml, XMLMetadataConfig)
self.assertIsInstance(metadata.groups_xml, XMLMetadataConfig)
self.assertIsInstance(metadata.query, MetadataQuery)
@@ -1216,17 +1217,16 @@ class TestMetadata(_TestMetadata, TestStatistics, TestDatabaseBacked):
@patch("Bcfg2.Server.Plugins.Metadata.XMLMetadataConfig.load_xml", Mock())
@patch("Bcfg2.Server.Plugins.Metadata.Metadata.update_client")
- def test_process_statistics(self, mock_update_client):
+ def test_end_statistics(self, mock_update_client):
metadata = self.load_clients_data(metadata=self.load_groups_data())
md = Mock()
md.hostname = "client6"
- metadata.process_statistics(md, None)
- mock_update_client.assert_called_with(md.hostname,
- dict(auth='cert'))
+ metadata.end_statistics(md)
+ mock_update_client.assert_called_with(md.hostname, dict(auth='cert'))
mock_update_client.reset_mock()
md.hostname = "client5"
- metadata.process_statistics(md, None)
+ metadata.end_statistics(md)
self.assertFalse(mock_update_client.called)
def test_viz(self):
@@ -1503,6 +1503,10 @@ class TestMetadata_NoClientsXML(TestMetadataBase):
def test_handle_clients_xml_event(self):
pass
+ def test_end_statistics(self):
+ # bootstrap mode, which is what is being tested here, doesn't
+ # work without clients.xml
+ pass
class TestMetadata_ClientsXML(TestMetadataBase):
""" test Metadata with a clients.xml. """
diff --git a/testsuite/Testsrc/Testlib/TestUtils.py b/testsuite/Testsrc/Testlib/TestUtils.py
new file mode 100644
index 000000000..349d6cd40
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestUtils.py
@@ -0,0 +1,48 @@
+import os
+import sys
+import copy
+import lxml.etree
+import subprocess
+from mock import Mock, MagicMock, patch
+from Bcfg2.Utils import *
+
+# 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 *
+
+
+class TestPackedDigitRange(Bcfg2TestCase):
+ def test_ranges(self):
+ # test cases. tuples of (ranges, included numbers, excluded
+ # numbers)
+ # tuples of (range description, numbers that are included,
+ # numebrs that are excluded)
+ tests = [(["0-3"], ["0", 1, "2", 3], [4]),
+ (["1"], [1], [0, "2"]),
+ (["10-11"], [10, 11], [0, 1]),
+ (["9-9"], [9], [8, 10]),
+ (["0-100"], [0, 10, 99, 100], []),
+ (["1", "3", "5"], [1, 3, 5], [0, 2, 4, 6]),
+ (["1-5", "7"], [1, 3, 5, 7], [0, 6, 8]),
+ (["1-5", 7, "9-11"], [1, 3, 5, 7, 9, 11], [0, 6, 8, 12]),
+ (["1-5, 7,9-11 "], [1, 3, 5, 7, 9, 11], [0, 6, 8, 12]),
+ (["852-855", "321-497", 763], [852, 855, 321, 400, 497, 763],
+ [851, 320, 766, 999]),
+ (["0-"], [0, 1, 100, 100000], []),
+ ([1, "5-10", "1000-"], [1, 5, 10, 1000, 10000000],
+ [4, 11, 999])]
+ for ranges, inc, exc in tests:
+ rng = PackedDigitRange(*ranges)
+ for test in inc:
+ self.assertIn(test, rng)
+ self.assertTrue(rng.includes(test))
+ for test in exc:
+ self.assertNotIn(test, rng)
+ self.assertFalse(rng.includes(test))
diff --git a/testsuite/Testsrc/test_code_checks.py b/testsuite/Testsrc/test_code_checks.py
index 3036397b6..85aea29d6 100644
--- a/testsuite/Testsrc/test_code_checks.py
+++ b/testsuite/Testsrc/test_code_checks.py
@@ -25,14 +25,6 @@ srcpath = os.path.abspath(os.path.join(testdir, "..", "src"))
# path to pylint rc file
rcfile = os.path.join(testdir, "pylintrc.conf")
-# test for pylint existence
-try:
- Popen(['pylint'], stdout=PIPE, stderr=STDOUT).wait()
- HAS_PYLINT = True
-except OSError:
- HAS_PYLINT = False
-
-
# perform checks on the listed files only if the module listed in the
# keys can be imported
contingent_checks = {
@@ -57,18 +49,14 @@ contingent_checks = {
# perform only error checking on the listed files
error_checks = {
- "sbin": ["bcfg2-build-reports", "bcfg2-reports"],
+ "sbin": ["bcfg2-build-reports"],
"lib/Bcfg2": ["Proxy.py", "SSLServer.py", "Reporting"],
"lib/Bcfg2/Server": ["Reports", "SchemaUpdater"],
"lib/Bcfg2/Server/Admin": ["Compare.py",
"Snapshots.py"],
- "lib/Bcfg2/Client/Tools": ["launchd.py",
- "OpenCSW.py",
+ "lib/Bcfg2/Client/Tools": ["OpenCSW.py",
"Blast.py",
- "SYSV.py",
"FreeBSDInit.py",
- "DebInit.py",
- "RcUpdate.py",
"VCS.py",
"YUM24.py"],
"lib/Bcfg2/Server/Plugins": ["Deps.py",
@@ -140,13 +128,8 @@ def blacklist_filter(filelist, blacklist):
return rv
-class TestPylint(Bcfg2TestCase):
- pylint_cmd = ["pylint", "--rcfile", rcfile, "--init-hook",
- "import sys;sys.path.append('%s')" %
- os.path.join(srcpath, "lib")]
-
- # regex to find errors and fatal errors
- error_re = re.compile(r':\d+:\s+\[[EF]\d{4}')
+class CodeTestCase(Bcfg2TestCase):
+ __test__ = False
# build the blacklists
blacklist = expand_path_dict(no_checks)
@@ -158,6 +141,34 @@ class TestPylint(Bcfg2TestCase):
full_blacklist = expand_path_dict(error_checks) + contingent_blacklist + \
blacklist
+ command = [None]
+
+ has_command = None
+
+ # extra arguments when running tests on sbin/*
+ sbin_args = []
+
+ # extra arguments when running tests on lib/*
+ lib_args = []
+
+ # extra arguments for full tests
+ full_args = []
+
+ # extra arguments for error tests
+ error_args = []
+
+ def has_exec(self):
+ if self.has_command is None:
+ try:
+ proc = Popen(self.command,
+ stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ proc.communicate(input="\n")
+ proc.wait()
+ self.has_command = True
+ except OSError:
+ self.has_command = False
+ return self.has_command
+
def get_env(self):
if ('PYTHONPATH' not in os.environ or
testdir not in os.environ['PYTHONPATH'].split(":")):
@@ -168,110 +179,149 @@ class TestPylint(Bcfg2TestCase):
else:
return os.environ
+ def _test_full(self, files, extra_args=None):
+ """ test select files for all problems """
+ if not len(files):
+ return
+ if extra_args is None:
+ extra_args = []
+ cmd = self.command + self.full_args + extra_args + \
+ [os.path.join(srcpath, f) for f in files]
+ proc = Popen(cmd, stdout=PIPE, stderr=STDOUT, env=self.get_env())
+ print(proc.communicate()[0])
+ self.assertEqual(proc.wait(), 0)
+
+ def _test_errors(self, files, extra_args=None):
+ """ test select files for errors """
+ if not len(files):
+ return
+ if extra_args is None:
+ extra_args = []
+ cmd = self.command + self.error_args + extra_args + \
+ [os.path.join(srcpath, f) for f in files]
+ proc = Popen(cmd, stdout=PIPE, stderr=STDOUT, env=self.get_env())
+ print(proc.communicate()[0])
+ self.assertEqual(proc.wait(), 0)
+
@skipIf(not os.path.exists(srcpath), "%s does not exist" % srcpath)
@skipIf(not os.path.exists(rcfile), "%s does not exist" % rcfile)
- @skipUnless(HAS_PYLINT, "pylint not found, skipping")
def test_lib_full(self):
- full_list = []
- for root, _, files in os.walk(os.path.join(srcpath, "lib")):
- full_list.extend(blacklist_filter([os.path.join(root, f)
- for f in files
- if f.endswith(".py")],
- self.full_blacklist))
- self._pylint_full(full_list)
+ @skipUnless(self.has_exec(),
+ "%s not found, skipping" % self.command[0])
+ def inner():
+ full_list = []
+ for root, _, files in os.walk(os.path.join(srcpath, "lib")):
+ full_list.extend(blacklist_filter([os.path.join(root, f)
+ for f in files
+ if f.endswith(".py")],
+ self.full_blacklist))
+ self._test_full(full_list, extra_args=self.lib_args)
+
+ inner()
@skipIf(not os.path.exists(srcpath), "%s does not exist" % srcpath)
@skipIf(not os.path.exists(rcfile), "%s does not exist" % rcfile)
- @skipUnless(HAS_PYLINT, "pylint not found, skipping")
def test_contingent_full(self):
- blacklist = set(expand_path_dict(error_checks) + self.blacklist)
- for (mods, filedict) in contingent_checks.items():
- if "django" in mods:
- # there's some issue with running pylint on modules
- # that use django in Travis CI (but not elsewhere), so
- # skip these for now
- continue
- try:
- for mod in mods:
- __import__(mod)
- except ImportError:
- continue
- self._pylint_full(blacklist_filter(expand_path_dict(filedict),
- blacklist))
+ @skipUnless(self.has_exec(),
+ "%s not found, skipping" % self.command[0])
+ def inner():
+ filelist = []
+ blacklist = set(expand_path_dict(error_checks) + self.blacklist)
+ for (mods, filedict) in contingent_checks.items():
+ try:
+ for mod in mods:
+ __import__(mod)
+ except ImportError:
+ continue
+ filelist.extend(expand_path_dict(filedict))
+ self._test_full(blacklist_filter(filelist, blacklist),
+ extra_args=self.lib_args)
+
+ inner()
@skipIf(not os.path.exists(srcpath), "%s does not exist" % srcpath)
@skipIf(not os.path.exists(rcfile), "%s does not exist" % rcfile)
- @skipUnless(HAS_PYLINT, "pylint not found, skipping")
- def test_sbin_full(self):
- all_sbin = [os.path.join(srcpath, "sbin", f)
- for f in glob.glob(os.path.join(srcpath, "sbin", "*"))]
- sbin_list = blacklist_filter([f for f in all_sbin
- if not os.path.islink(f)],
- self.full_blacklist)
- self._pylint_full(sbin_list,
- extra_args=["--module-rgx",
- "[a-z_-][a-z0-9_-]*$"])
-
- def _pylint_full(self, paths, extra_args=None):
- """ test select files for all pylint errors """
- if extra_args is None:
- extra_args = []
- args = self.pylint_cmd + extra_args + \
- [os.path.join(srcpath, p) for p in paths]
- pylint = Popen(args, stdout=PIPE, stderr=STDOUT, env=self.get_env())
- print(pylint.communicate()[0])
- self.assertEqual(pylint.wait(), 0)
+ def test_sbin(self):
+ @skipUnless(self.has_exec(),
+ "%s not found, skipping" % self.command[0])
+ def inner():
+ all_sbin = [os.path.join(srcpath, "sbin", f)
+ for f in glob.glob(os.path.join(srcpath, "sbin", "*"))]
+ full_list = blacklist_filter([f for f in all_sbin
+ if not os.path.islink(f)],
+ self.full_blacklist)
+ self._test_full(full_list, extra_args=self.sbin_args)
+
+ errors_list = blacklist_filter([f for f in all_sbin
+ if not os.path.islink(f)],
+ self.contingent_blacklist)
+ self._test_errors(errors_list, extra_args=self.sbin_args)
+
+ inner()
@skipIf(not os.path.exists(srcpath), "%s does not exist" % srcpath)
@skipIf(not os.path.exists(rcfile), "%s does not exist" % rcfile)
- @skipUnless(HAS_PYLINT, "pylint not found, skipping")
- def test_sbin_errors(self):
- all_sbin = [os.path.join(srcpath, "sbin", f)
- for f in glob.glob(os.path.join(srcpath, "sbin", "*"))]
- sbin_list = blacklist_filter([f for f in all_sbin
- if not os.path.islink(f)],
- self.contingent_blacklist)
- self._pylint_errors(sbin_list,
- extra_args=["--module-rgx",
- "[a-z_-][a-z0-9_-]*$"])
+ def test_contingent_errors(self):
+ @skipUnless(self.has_exec(),
+ "%s not found, skipping" % self.command[0])
+ def inner():
+ filelist = []
+ whitelist = expand_path_dict(error_checks)
+ for (mods, filedict) in contingent_checks.items():
+ try:
+ for mod in mods:
+ __import__(mod)
+ except ImportError:
+ continue
+ filelist.extend(expand_path_dict(filedict))
+ flist = blacklist_filter(whitelist_filter(filelist, whitelist),
+ self.blacklist)
+ self._test_errors(flist, extra_args=self.lib_args)
+
+ inner()
@skipIf(not os.path.exists(srcpath), "%s does not exist" % srcpath)
@skipIf(not os.path.exists(rcfile), "%s does not exist" % rcfile)
- @skipUnless(HAS_PYLINT, "pylint not found, skipping")
- def test_contingent_errors(self):
- whitelist = expand_path_dict(error_checks)
- for (mods, filedict) in contingent_checks.items():
+ def test_lib_errors(self):
+ @skipUnless(self.has_exec(),
+ "%s not found, skipping" % self.command[0])
+ def inner():
+ filelist = blacklist_filter(expand_path_dict(error_checks),
+ self.contingent_blacklist)
+ return self._test_errors(filelist, extra_args=self.lib_args)
+
+ inner()
+
+
+class TestPylint(CodeTestCase):
+ __test__ = True
+ command = ["pylint", "--rcfile", rcfile, "--init-hook",
+ "import sys;sys.path.append('%s')" %
+ os.path.join(srcpath, "lib")]
+
+ sbin_args = ["--module-rgx", "[a-z_-][a-z0-9_-]*$"]
+ error_args = ["-f", "parseable", "-d", "R0801,E1103"]
+
+ # regex to find errors and fatal errors
+ error_re = re.compile(r':\d+:\s+\[[EF]\d{4}')
+
+ def __init__(self, *args, **kwargs):
+ CodeTestCase.__init__(self, *args, **kwargs)
+ for mods, filedict in contingent_checks.items():
if "django" in mods:
# there's some issue with running pylint on modules
# that use django in Travis CI (but not elsewhere), so
# skip these for now
- continue
- try:
- for mod in mods:
- __import__(mod)
- except ImportError:
- continue
- flist = \
- blacklist_filter(whitelist_filter(expand_path_dict(filedict),
- whitelist),
- self.blacklist)
- self._pylint_errors(flist)
-
- @skipIf(not os.path.exists(srcpath), "%s does not exist" % srcpath)
- @skipIf(not os.path.exists(rcfile), "%s does not exist" % rcfile)
- @skipUnless(HAS_PYLINT, "pylint not found, skipping")
- def test_lib_errors(self):
- filelist = blacklist_filter(expand_path_dict(error_checks),
- self.contingent_blacklist)
- return self._pylint_errors(filelist)
+ self.blacklist += expand_path_dict(filedict)
- def _pylint_errors(self, paths, extra_args=None):
+ def _test_errors(self, files, extra_args=None):
""" test all files for fatals and errors """
+ if not len(files):
+ return
if extra_args is None:
extra_args = []
- args = self.pylint_cmd + extra_args + \
- ["-f", "parseable", "-d", "R0801,E1103"] + \
- [os.path.join(srcpath, p) for p in paths]
+ args = self.command + self.error_args + extra_args + \
+ [os.path.join(srcpath, p) for p in files]
pylint = Popen(args, stdout=PIPE, stderr=STDOUT, env=self.get_env())
output = pylint.communicate()[0]
rv = pylint.wait()
@@ -282,3 +332,11 @@ class TestPylint(Bcfg2TestCase):
# pylint returns a bitmask, where 1 means fatal errors
# were encountered and 2 means errors were encountered.
self.assertEqual(rv & 3, 0)
+
+
+class TestPEP8(CodeTestCase):
+ __test__ = True
+ command = ["pep8", "--ignore=E125,E501"]
+
+ def _test_errors(self, files, extra_args=None):
+ pass