From cfa4ce0a6fe82ed8578fe4668998012ad3833e05 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 5 Feb 2013 11:36:49 -0500 Subject: added support for wildcard XInclude in XMLFileBacked --- src/lib/Bcfg2/Server/Plugin/helpers.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) (limited to 'src/lib') 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, -- cgit v1.2.3-1-g7c22 From 9d0e6991fc23c073efc0db6bf10e1081f6725e55 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 5 Feb 2013 12:13:20 -0500 Subject: abstracted similar digit range classes in POSIXUsers/GroupPatterns into Bcfg2.Utils --- src/lib/Bcfg2/Client/Tools/POSIXUsers.py | 45 ++---------------- src/lib/Bcfg2/Server/Plugins/GroupPatterns.py | 23 +-------- src/lib/Bcfg2/Utils.py | 67 +++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 src/lib/Bcfg2/Utils.py (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py index 7c8a4d578..849785e4a 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py +++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py @@ -5,44 +5,9 @@ import sys import pwd import grp import subprocess +from Bcfg2.Utils import PackedDigitRange 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): @@ -124,16 +89,16 @@ class POSIXUsers(Bcfg2.Client.Tools.Tool): 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): 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/Utils.py b/src/lib/Bcfg2/Utils.py new file mode 100644 index 000000000..ba17e1a63 --- /dev/null +++ b/src/lib/Bcfg2/Utils.py @@ -0,0 +1,67 @@ +""" Miscellaneous useful utility functions, classes, etc., that are +used by both client and server. Stuff that doesn't fit anywhere +else. """ + +from Bcfg2.Compat import any # pylint: disable=W0622 + + +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() + + Or:: + + PackedDigitRange([, [, ...]]) + + 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) -- cgit v1.2.3-1-g7c22 From ff0048a3a26c9076c8052a8c7be171e4364f0d09 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 7 Feb 2013 10:00:37 -0500 Subject: moved common file locking code into Bcfg2.Utils --- src/lib/Bcfg2/Client/Client.py | 10 ++++------ src/lib/Bcfg2/Server/Plugins/Metadata.py | 12 ++---------- src/lib/Bcfg2/Utils.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 16 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py index 45e0b64e6..b0d877a19 100644 --- a/src/lib/Bcfg2/Client/Client.py +++ b/src/lib/Bcfg2/Client/Client.py @@ -14,6 +14,7 @@ import Bcfg2.Options import Bcfg2.Client.XML import Bcfg2.Client.Frame import Bcfg2.Client.Tools +from Bcfg2.Utils import locked from Bcfg2.Compat import xmlrpclib from Bcfg2.version import __version__ from subprocess import Popen, PIPE @@ -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/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index ef0c152fd..df98e6ea8 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) diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py index ba17e1a63..247e4f16b 100644 --- a/src/lib/Bcfg2/Utils.py +++ b/src/lib/Bcfg2/Utils.py @@ -2,6 +2,7 @@ used by both client and server. Stuff that doesn't fit anywhere else. """ +import fcntl from Bcfg2.Compat import any # pylint: disable=W0622 @@ -65,3 +66,12 @@ class PackedDigitRange(object): 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 """ + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError: + return True + return False -- cgit v1.2.3-1-g7c22 From b8ebd636ac3a5e2919b3e62041019f26acb1e2c0 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 7 Feb 2013 10:01:16 -0500 Subject: Metadata: allowed setting global default authentication type --- src/lib/Bcfg2/Options.py | 8 +++++++- src/lib/Bcfg2/Server/Plugins/Metadata.py | 32 ++++++++++++-------------------- 2 files changed, 19 insertions(+), 21 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index 07d089f05..be3a4c7b3 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -582,6 +582,11 @@ SERVER_UMASK = \ default='0077', odesc='', 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 = \ @@ -1115,7 +1120,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, diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index df98e6ea8..bd02739d5 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -677,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': @@ -1192,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: @@ -1215,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': @@ -1230,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 -- cgit v1.2.3-1-g7c22 From fb723b0164318459e337911cf243ba497d8f6683 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 7 Feb 2013 10:14:31 -0500 Subject: Metadata: process default client bootstrap mode properly --- src/lib/Bcfg2/Server/Plugins/Metadata.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index bd02739d5..383e8e1dc 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -384,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' @@ -392,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() @@ -1252,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.""" -- cgit v1.2.3-1-g7c22 From 43984bc5ebc59bd8c5890ed6ba3de162e6698dcc Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 12 Feb 2013 16:02:24 -0500 Subject: better Executor class for client tools --- src/lib/Bcfg2/Client/Tools/APK.py | 7 +- src/lib/Bcfg2/Client/Tools/APT.py | 16 ++-- src/lib/Bcfg2/Client/Tools/Action.py | 11 +-- src/lib/Bcfg2/Client/Tools/Chkconfig.py | 34 ++++--- src/lib/Bcfg2/Client/Tools/DebInit.py | 59 ++++++------ src/lib/Bcfg2/Client/Tools/Encap.py | 11 +-- src/lib/Bcfg2/Client/Tools/FreeBSDPackage.py | 2 +- src/lib/Bcfg2/Client/Tools/MacPorts.py | 5 +- src/lib/Bcfg2/Client/Tools/POSIXUsers.py | 77 +++------------ src/lib/Bcfg2/Client/Tools/Pacman.py | 5 +- src/lib/Bcfg2/Client/Tools/Portage.py | 16 ++-- src/lib/Bcfg2/Client/Tools/RPM.py | 67 ++++++------- src/lib/Bcfg2/Client/Tools/RcUpdate.py | 24 ++--- src/lib/Bcfg2/Client/Tools/SMF.py | 73 +++++++------- src/lib/Bcfg2/Client/Tools/SYSV.py | 58 +++++------ src/lib/Bcfg2/Client/Tools/Systemd.py | 31 +++--- src/lib/Bcfg2/Client/Tools/Upstart.py | 10 +- src/lib/Bcfg2/Client/Tools/YUM24.py | 33 +++---- src/lib/Bcfg2/Client/Tools/__init__.py | 138 ++++++++++++++++++--------- src/lib/Bcfg2/Client/Tools/launchd.py | 110 ++++++++++----------- 20 files changed, 378 insertions(+), 409 deletions(-) (limited to 'src/lib') 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[SK])(?P\d+)(?P\S+)") + svcre = \ + re.compile("/etc/.*/(?P[SK])(?P\d+)(?P\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 849785e4a..99ed3c7d9 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py +++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py @@ -1,60 +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 -from Bcfg2.Utils import PackedDigitRange import Bcfg2.Client.XML import Bcfg2.Client.Tools - - -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): @@ -83,7 +34,6 @@ 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) @@ -274,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, @@ -338,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/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 c0dd60c1e..7014f334f 100644 --- a/src/lib/Bcfg2/Client/Tools/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -4,7 +4,8 @@ import os import sys import stat import select -from subprocess import Popen, PIPE +import logging +import subprocess import Bcfg2.Client.XML from Bcfg2.Compat import input, walk_packages # pylint: disable=W0622 @@ -25,34 +26,89 @@ class ToolInstantiationError(Exception): pass -class Executor: - """ This class runs shell commands. """ +class ExecutorResult(object): + """ Returned as the result of a call to + :func:`Bcfg2.Client.Tools.Executor.run`. The result can be + accessed via the instance variables, documented below, as a + boolean (which is equivalent to + :attr:`Bcfg2.Client.Tools.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 __init__(self, logger): - """ - :param logger: The logger to use to produce debug logging - :type logger: logging.Logger - """ - self.logger = logger + 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 run(self, command): - """ Run a command inside a shell. + def __nonzero__(self): + return self.__bool__() - :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: + def __bool__(self): + return self.success + + +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): + self.logger = logging.getLogger(self.__class__.__name__) + + def run(self, command, inputdata=None, shell=False): + """ Run a command, given as a list, optionally giving it the + specified input data """ + 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 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) - return (proc.wait(), output) + for line in stderr.splitlines(): # pylint: disable=E1103 + self.logger.info(line) + return ExecutorResult(stdout, stderr, proc.wait()) class ClassName(object): @@ -143,7 +199,7 @@ class Tool(object): #: An :class:`Bcfg2.Client.Tools.Executor` object for #: running external commands. - self.cmd = Executor(logger) + self.cmd = Executor() #: A list of entries that have been modified by this tool self.modified = [] @@ -469,10 +525,7 @@ class PkgTool(Tool): pkgargs = " ".join([self.pkgtool[1][0] % datum for datum in data]) self.logger.debug("Installing packages: %s" % pkgargs) - self.logger.debug("Running command: %s" % (self.pkgtool[0] % pkgargs)) - - cmdrc = self.cmd.run(self.pkgtool[0] % pkgargs)[0] - if cmdrc == 0: + if self.cmd.run(self.pkgtool[0] % pkgargs): self.logger.info("Single Pass Succeded") # set all package states to true and flush workqueues pkgnames = [pkg.get('name') for pkg in packages] @@ -497,12 +550,11 @@ class PkgTool(Tool): else: self.logger.info("Installing pkg %s version %s" % (pkg.get('name'), pkg.get('version'))) - cmdrc = self.cmd.run( + if self.cmd.run( self.pkgtool[0] % (self.pkgtool[1][0] % tuple([pkg.get(field) - for field in self.pkgtool[1][1]]))) - if cmdrc[0] == 0: + for field in self.pkgtool[1][1]]))): states[pkg] = True else: self.logger.error("Failed to install package %s" % @@ -557,7 +609,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. @@ -568,7 +620,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. @@ -580,7 +632,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. @@ -590,7 +642,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': @@ -610,10 +662,10 @@ class SvcTool(Tool): 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']: prompt = ('Restart service %s?: (y/N): ' % @@ -625,12 +677,12 @@ class SvcTool(Tool): ans = input(prompt) if ans not in ['y', 'Y']: 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): -- cgit v1.2.3-1-g7c22 From b7995e7b2cb8e527ab8bb8bd3f01c8fb2fce736a Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 13 Feb 2013 16:08:08 -0500 Subject: extended usage of Executor class, added client-side timeout options --- src/lib/Bcfg2/Client/Client.py | 18 ++-- src/lib/Bcfg2/Client/Tools/SELinux.py | 32 +++---- src/lib/Bcfg2/Client/Tools/__init__.py | 104 +--------------------- src/lib/Bcfg2/Options.py | 24 ++++- src/lib/Bcfg2/Server/Plugin/base.py | 13 +-- src/lib/Bcfg2/Utils.py | 156 ++++++++++++++++++++++++++++++++- 6 files changed, 204 insertions(+), 143 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py index b0d877a19..88f3bd6ef 100644 --- a/src/lib/Bcfg2/Client/Client.py +++ b/src/lib/Bcfg2/Client/Client.py @@ -14,10 +14,9 @@ import Bcfg2.Options import Bcfg2.Client.XML import Bcfg2.Client.Frame import Bcfg2.Client.Tools -from Bcfg2.Utils import locked +from Bcfg2.Utils import locked, Executor from Bcfg2.Compat import xmlrpclib from Bcfg2.version import __version__ -from subprocess import Popen, PIPE class Client(object): @@ -42,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") @@ -95,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: diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py index 414ca1f93..08d943251 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/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py index 7014f334f..cd86a2a4b 100644 --- a/src/lib/Bcfg2/Client/Tools/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -4,9 +4,8 @@ import os import sys import stat import select -import logging -import subprocess import Bcfg2.Client.XML +from Bcfg2.Utils import Executor, ClassName from Bcfg2.Compat import input, walk_packages # pylint: disable=W0622 __all__ = [m[1] for m in walk_packages(path=__path__)] @@ -26,103 +25,6 @@ class ToolInstantiationError(Exception): pass -class ExecutorResult(object): - """ Returned as the result of a call to - :func:`Bcfg2.Client.Tools.Executor.run`. The result can be - accessed via the instance variables, documented below, as a - boolean (which is equivalent to - :attr:`Bcfg2.Client.Tools.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 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): - self.logger = logging.getLogger(self.__class__.__name__) - - def run(self, command, inputdata=None, shell=False): - """ Run a command, given as a list, optionally giving it the - specified input data """ - 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 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()) - - -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. @@ -197,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() + self.cmd = Executor(timeout=self.setup['command_timeout']) #: A list of entries that have been modified by this tool self.modified = [] diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index be3a4c7b3..6796bfe08 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -334,6 +334,16 @@ def get_bool(val): raise ValueError("Not a boolean value", 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 """ @@ -808,6 +818,16 @@ 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 = \ @@ -1199,7 +1219,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) diff --git a/src/lib/Bcfg2/Server/Plugin/base.py b/src/lib/Bcfg2/Server/Plugin/base.py index e74909ee9..25a687874 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): @@ -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. """ diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py index 247e4f16b..3b1559528 100644 --- a/src/lib/Bcfg2/Utils.py +++ b/src/lib/Bcfg2/Utils.py @@ -3,9 +3,25 @@ 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, @@ -69,9 +85,147 @@ class PackedDigitRange(object): def locked(fd): - """ Acquire a lock on a file """ + """ 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_callback(self, proc): + """ Get a callback (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: function """ + def _timeout(): + """ Callback that kills ``proc`` """ + if proc.poll() == None: + try: + proc.kill() + self.logger.warning("Process exceeeded timeout, killing") + except OSError: + pass + + return _timeout + + 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_callback(proc), + [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() -- cgit v1.2.3-1-g7c22 From 6fcce024d45c0f6312b17c19140272f192f18841 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 25 Feb 2013 13:50:48 -0500 Subject: bcfg2-test: added option to spawn multiple child processes to run tests --- src/lib/Bcfg2/Options.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index 6796bfe08..8e7540c22 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -334,6 +334,12 @@ 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: @@ -831,7 +837,7 @@ CLIENT_COMMAND_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='', @@ -846,6 +852,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='', + 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='', + cf=('bcfg2_test', 'xunit'), + long_arg=True) LINT_CONFIG = \ Option('Specify bcfg2-lint configuration file', default='/etc/bcfg2-lint.conf', @@ -1240,6 +1261,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): """ -- cgit v1.2.3-1-g7c22 From d624e7eba92731c99909d74a1c20d502a6695837 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 8 Mar 2013 13:34:06 -0500 Subject: Metadata: fixed typo --- src/lib/Bcfg2/Server/Plugins/Metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index 383e8e1dc..09ecfaf82 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -1255,7 +1255,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, def end_statistics(self, metadata): """ Hook to toggle clients in bootstrap mode """ if self.auth.get(metadata.hostname, - self.core.setup('authentication')) == 'bootstrap': + self.core.setup['authentication']) == 'bootstrap': self.update_client(metadata.hostname, dict(auth='cert')) def viz(self, hosts, bundles, key, only_client, colors): -- cgit v1.2.3-1-g7c22 From a5b5c1926d437a8132115bd608d2fd8141982f70 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 8 Mar 2013 13:34:33 -0500 Subject: Reporting: added support for various SELinux entries --- src/lib/Bcfg2/Reporting/Storage/DjangoORM.py | 354 +++++++++------ .../migrations/0005_add_selinux_entry_support.py | 485 +++++++++++++++++++++ src/lib/Bcfg2/Reporting/models.py | 198 +++++++-- .../Reporting/templates/config_items/item.html | 31 +- src/lib/Bcfg2/Reporting/views.py | 24 +- 5 files changed, 912 insertions(+), 180 deletions(-) create mode 100644 src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py index bca4a9c1e..8826d6991 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,212 @@ 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 : that will be used to set the default values for + various attributes. + + ``mapping`` is a mapping of : 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 :, 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_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 +253,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 +274,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 +285,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 +304,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 +333,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 +352,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/models.py b/src/lib/Bcfg2/Reporting/models.py index ab2dc8418..a04203c73 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,21 @@ 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") failures = models.ManyToManyField("FailureEntry") + entry_types = ('actions', 'packages', 'paths', 'services', 'sebooleans', + 'seports', 'sefcontexts', 'senodes', 'selogins', 'seusers', + 'seinterfaces', 'sepermissives', 'semodules') + # Formerly InteractionMetadata profile = models.ForeignKey("Group", related_name="+", null=True) groups = models.ManyToManyField("Group") @@ -157,7 +159,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 +192,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 +205,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 +329,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 +336,6 @@ class BaseEntry(models.Model): """todo""" return [] - @classmethod def entry_from_name(cls, name): try: @@ -344,28 +346,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 +375,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 +395,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 +437,137 @@ 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 PackageEntry(SuccessEntry): """ The new model for package information """ @@ -455,7 +577,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 +734,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 %} {% if item.mode_problem %} {% if item.current_perms.owner %} - + + {% endif %} {% if item.current_perms.group %} - + + {% endif %} {% if item.current_perms.mode%} - + {% endif %} {% endif %} {% if item.status_problem %} - - + + + {% endif %} {% if item.linkentry.link_problem %} - - + + + {% endif %} {% if item.version_problem %} - + + {% endif %} + {% if item.selinuxtype_problem %} + + + + {% endif %}
Problem TypeExpectedFound
Owner{{item.target_perms.owner}}
Owner{{item.target_perms.owner}} {{item.current_perms.owner}}
Group{{item.target_perms.group}}
Group{{item.target_perms.group}} {{item.current_perms.group}}
Mode{{item.target_perms.mode}}
Permissions + {{item.target_perms.mode}} {{item.current_perms.mode}}
Status{{item.target_status}}{{item.current_status}}
Status{{item.target_status}}{{item.current_status}}
{{item.get_path_type_display}}{{item.linkentry.target_path}}{{item.linkentry.current_path}}
{{item.get_path_type_display}}{{item.linkentry.target_path}}{{item.linkentry.current_path}}
Package Version{{item.target_version|cut:"("|cut:")"}}
Package Version{{item.target_version|cut:"("|cut:")"}} {{item.current_version|cut:"("|cut:")"}}
SELinux Type{{item.selinuxtype}}{{item.current_selinuxtype}}
{% endif %} @@ -92,7 +103,7 @@ div.entry_list h3 { {{ item.details|syntaxhilight }} {% else %} - {{ item.details }} + {{ item.details }} {% endif %} {% 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() -- cgit v1.2.3-1-g7c22 From aaa2d3230082eb283c34e308250da1381d181365 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 8 Mar 2013 14:09:44 -0500 Subject: Reporting: added support for POSIX user/group entries --- src/lib/Bcfg2/Reporting/Storage/DjangoORM.py | 18 ++ .../0006_add_user_group_entry_support.py | 340 +++++++++++++++++++++ src/lib/Bcfg2/Reporting/models.py | 29 +- 3 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py index 8826d6991..3b2c0ccfa 100644 --- a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py +++ b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py @@ -232,6 +232,24 @@ class DjangoORM(StorageBase): 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) 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 a04203c73..4be509f53 100644 --- a/src/lib/Bcfg2/Reporting/models.py +++ b/src/lib/Bcfg2/Reporting/models.py @@ -135,11 +135,14 @@ class Interaction(models.Model): 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') + 'seinterfaces', 'sepermissives', 'semodules', 'posixusers', + 'posixgroups') # Formerly InteractionMetadata profile = models.ForeignKey("Group", related_name="+", null=True) @@ -568,6 +571,30 @@ class SEModuleEntry(SuccessEntry): 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 """ -- cgit v1.2.3-1-g7c22 From 439e72b36f66855cfb66de111147a4b92e80da19 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 11 Mar 2013 08:01:39 -0400 Subject: added PEP-8 style checks to test suite --- src/lib/Bcfg2/Compat.py | 4 +++- src/lib/Bcfg2/Proxy.py | 3 +++ src/lib/Bcfg2/Server/Plugin/interfaces.py | 1 + src/lib/Bcfg2/Server/Plugins/DBStats.py | 5 ++--- src/lib/Bcfg2/Server/Plugins/Packages/Apt.py | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py index b0f0ef5cf..4bcc76e8f 100644 --- a/src/lib/Bcfg2/Compat.py +++ b/src/lib/Bcfg2/Compat.py @@ -51,7 +51,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 @@ -73,6 +74,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/Proxy.py b/src/lib/Bcfg2/Proxy.py index 3b406c78e..3aefed5d1 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/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/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, -- cgit v1.2.3-1-g7c22 From d401641268aca4d93fd63abebe480fc8fee8ec24 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 15 Mar 2013 14:11:54 -0400 Subject: Executor: better timeout implementation --- src/lib/Bcfg2/Utils.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py index 3b1559528..7d7d26d5d 100644 --- a/src/lib/Bcfg2/Utils.py +++ b/src/lib/Bcfg2/Utils.py @@ -165,23 +165,19 @@ class Executor(object): self.logger = logging.getLogger(self.__class__.__name__) self.timeout = timeout - def _timeout_callback(self, proc): - """ Get a callback (suitable for passing to - :class:`threading.Timer`) that kills the given process. + 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: function """ - def _timeout(): - """ Callback that kills ``proc`` """ - if proc.poll() == None: - try: - proc.kill() - self.logger.warning("Process exceeeded timeout, killing") - except OSError: - pass - - return _timeout + :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 @@ -212,9 +208,7 @@ class Executor(object): if timeout is None: timeout = self.timeout if timeout is not None: - timer = threading.Timer(float(timeout), - self._timeout_callback(proc), - [proc]) + timer = threading.Timer(float(timeout), self._timeout, [proc]) timer.start() try: if inputdata: -- cgit v1.2.3-1-g7c22 From 182f95cc48029617825dabede3fc812c36995146 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 19 Mar 2013 15:04:29 -0400 Subject: Core: added more debug logging for core methods --- src/lib/Bcfg2/Server/Core.py | 22 +++++++++++++++++++++- src/lib/Bcfg2/Server/FileMonitor/__init__.py | 2 ++ src/lib/Bcfg2/Server/Plugin/base.py | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 782aafbf1..72c0953b8 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -396,6 +396,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() @@ -429,6 +430,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 \ @@ -462,6 +465,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: @@ -488,6 +492,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) @@ -508,6 +513,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'] @@ -530,6 +536,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) @@ -546,6 +553,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:] @@ -621,6 +631,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) @@ -748,6 +759,7 @@ class BaseCore(object): :type mode: string :returns: list of Decision tuples ``(, )`` """ + self.logger.debug("Getting decision list for %s" % metadata.hostname) result = [] for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Decision): try: @@ -776,6 +788,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) @@ -797,6 +810,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': @@ -922,6 +936,7 @@ class BaseCore(object): return func.__doc__ @exposed + @track_statistics() def DeclareVersion(self, address, version): """ Declare the client version. @@ -932,7 +947,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, @@ -954,6 +971,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): @@ -975,6 +993,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 @@ -1021,6 +1040,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, diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py index 42ad4c041..54d35e38d 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/__init__.py +++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py @@ -288,6 +288,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 25a687874..30645e445 100644 --- a/src/lib/Bcfg2/Server/Plugin/base.py +++ b/src/lib/Bcfg2/Server/Plugin/base.py @@ -122,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): -- cgit v1.2.3-1-g7c22 From 71d7285c405bd639f1f9f2642ea8fb567b97caec Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 19 Mar 2013 15:05:51 -0400 Subject: Debug improvements: * Added (set|toggle)_core_debug RMI methods to set debugging in the core * Enable plugin debugging when run with -d * Allow enabling debugging on Reporting plugin before threads have started --- src/lib/Bcfg2/Logger.py | 3 ++ src/lib/Bcfg2/Server/Core.py | 71 ++++++++++++++++++++++++++++--- src/lib/Bcfg2/Server/Plugin/base.py | 4 +- src/lib/Bcfg2/Server/Plugins/Reporting.py | 5 ++- 4 files changed, 73 insertions(+), 10 deletions(-) (limited to 'src/lib') diff --git a/src/lib/Bcfg2/Logger.py b/src/lib/Bcfg2/Logger.py index c2eac1e60..618d0f2cd 100644 --- a/src/lib/Bcfg2/Logger.py +++ b/src/lib/Bcfg2/Logger.py @@ -139,6 +139,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) @@ -153,6 +154,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')) @@ -166,6 +168,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/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 72c0953b8..9be71e2e2 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -99,9 +99,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 @@ -119,6 +117,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']] @@ -299,6 +316,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. @@ -731,6 +753,7 @@ class BaseCore(object): self.shutdown() raise + self.set_debug(None, self.debug_flag) self._block() def _daemonize(self): @@ -1141,9 +1164,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, _): @@ -1157,6 +1188,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) @@ -1167,7 +1200,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, _, debug): diff --git a/src/lib/Bcfg2/Server/Plugin/base.py b/src/lib/Bcfg2/Server/Plugin/base.py index 30645e445..f7bc08717 100644 --- a/src/lib/Bcfg2/Server/Plugin/base.py +++ b/src/lib/Bcfg2/Server/Plugin/base.py @@ -34,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 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): -- cgit v1.2.3-1-g7c22