diff options
Diffstat (limited to 'src')
48 files changed, 2118 insertions, 423 deletions
diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py index e2521c0f8..66c1ce430 100644 --- a/src/lib/Bcfg2/Client/Client.py +++ b/src/lib/Bcfg2/Client/Client.py @@ -105,6 +105,7 @@ class Client(object): self._probe_failure(name, "Return value %s" % rv) self.logger.info("Probe %s has result:" % name) self.logger.info(rv.stdout) + ret.text = rv.stdout finally: os.unlink(scriptname) except SystemExit: diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py index 82512130c..6ef686c10 100644 --- a/src/lib/Bcfg2/Client/Frame.py +++ b/src/lib/Bcfg2/Client/Frame.py @@ -481,7 +481,8 @@ class Frame(object): len(list(self.states.values()))) self.logger.info('Unmanaged entries: %d' % len(self.extra)) if phase == 'final' and self.setup['extra']: - for entry in self.extra: + for entry in sorted(self.extra, key=lambda e: e.tag + ":" + + e.get('name')): etype = entry.get('type') if etype: self.logger.info("%s:%s:%s" % (entry.tag, etype, diff --git a/src/lib/Bcfg2/Client/Proxy.py b/src/lib/Bcfg2/Client/Proxy.py index 1276c3ce9..f8817bb27 100644 --- a/src/lib/Bcfg2/Client/Proxy.py +++ b/src/lib/Bcfg2/Client/Proxy.py @@ -1,6 +1,6 @@ -import logging import re import socket +import logging # The ssl module is provided by either Python 2.6 or a separate ssl # package that works on older versions of Python (see @@ -20,7 +20,7 @@ import sys import time # Compatibility imports -from Bcfg2.Compat import httplib, xmlrpclib, urlparse +from Bcfg2.Compat import httplib, xmlrpclib, urlparse, quote_plus version = sys.version_info[:2] has_py26 = version >= (2, 6) @@ -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): @@ -350,7 +353,8 @@ def ComponentProxy(url, user=None, password=None, key=None, cert=None, ca=None, if user and password: method, path = urlparse(url)[:2] - newurl = "%s://%s:%s@%s" % (method, user, password, path) + newurl = "%s://%s:%s@%s" % (method, quote_plus(user, ''), + quote_plus(password, ''), path) else: newurl = url ssl_trans = XMLRPCTransport(key, cert, ca, diff --git a/src/lib/Bcfg2/Client/Tools/APT.py b/src/lib/Bcfg2/Client/Tools/APT.py index cc2f657d0..f449557aa 100644 --- a/src/lib/Bcfg2/Client/Tools/APT.py +++ b/src/lib/Bcfg2/Client/Tools/APT.py @@ -228,8 +228,13 @@ class APT(Bcfg2.Client.Tools.Tool): continue if pkg.get('version') in ['auto', 'any']: if self._newapi: - ipkgs.append("%s=%s" % (pkg.get('name'), - self.pkg_cache[pkg.get('name')].candidate.version)) + try: + ipkgs.append("%s=%s" % (pkg.get('name'), + self.pkg_cache[pkg.get('name')].candidate.version)) + except AttributeError: + self.logger.error("Failed to find %s in apt package cache" % + pkg.get('name')) + continue else: ipkgs.append("%s=%s" % (pkg.get('name'), self.pkg_cache[pkg.get('name')].candidateVersion)) diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py index b867fa3d8..f46875743 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/base.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py @@ -687,7 +687,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): if path is None: path = entry.get("name") cur = path - while cur != '/': + while cur and cur != '/': if not os.path.exists(cur): created.append(cur) cur = os.path.dirname(cur) diff --git a/src/lib/Bcfg2/Client/Tools/Portage.py b/src/lib/Bcfg2/Client/Tools/Portage.py index aa1254b46..32afa8cbf 100644 --- a/src/lib/Bcfg2/Client/Tools/Portage.py +++ b/src/lib/Bcfg2/Client/Tools/Portage.py @@ -36,7 +36,8 @@ class Portage(Bcfg2.Client.Tools.PkgTool): return self.logger.info('Getting list of installed packages') self.installed = {} - for pkg in self.cmd.run("equery -q list '*'").stdout.splitlines(): + 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) diff --git a/src/lib/Bcfg2/Client/Tools/RcUpdate.py b/src/lib/Bcfg2/Client/Tools/RcUpdate.py index 2e58f2564..552b27842 100644 --- a/src/lib/Bcfg2/Client/Tools/RcUpdate.py +++ b/src/lib/Bcfg2/Client/Tools/RcUpdate.py @@ -22,8 +22,8 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool): return True # check if service is enabled - cmd = '/sbin/rc-update show default | grep %s' - is_enabled = self.cmd.run(cmd % entry.get('name')).success + result = self.cmd.run(["/sbin/rc-update", "show", "default"]) + is_enabled = entry.get("name") in result.stdout # check if init script exists try: @@ -34,8 +34,8 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool): return False # check if service is enabled - cmd = '/etc/init.d/%s status | grep started' - is_running = self.cmd.run(cmd % entry.attrib['name']).success + result = self.cmd.run(self.get_svc_command(entry, "status")) + is_running = "started" in result.stdout if entry.get('status') == 'on' and not (is_enabled and is_running): entry.set('current_status', 'off') @@ -70,9 +70,9 @@ class RcUpdate(Bcfg2.Client.Tools.SvcTool): def FindExtra(self): """Locate extra rc-update services.""" - cmd = '/bin/rc-status -s' allsrv = [line.split()[0] - for line in self.cmd.run(cmd).stdout.splitlines() + for line in self.cmd.run(['/bin/rc-status', + '-s']).stdout.splitlines() if 'started' in line] self.logger.debug('Found active services:') self.logger.debug(allsrv) diff --git a/src/lib/Bcfg2/Client/Tools/VCS.py b/src/lib/Bcfg2/Client/Tools/VCS.py index fa3d22e1d..1ab867215 100644 --- a/src/lib/Bcfg2/Client/Tools/VCS.py +++ b/src/lib/Bcfg2/Client/Tools/VCS.py @@ -120,8 +120,9 @@ class VCS(Bcfg2.Client.Tools.Tool): def Installsvn(self, entry): """Checkout contents from a svn repository""" # pylint: disable=E1101 + client = pysvn.Client() try: - client = pysvn.Client.update(entry.get('name'), recurse=True) + client.update(entry.get('name'), recurse=True) except pysvn.ClientError: self.logger.error("Failed to update repository", exc_info=1) return False diff --git a/src/lib/Bcfg2/Client/Tools/YUM.py b/src/lib/Bcfg2/Client/Tools/YUM.py index 648d30d15..57ca06e77 100644 --- a/src/lib/Bcfg2/Client/Tools/YUM.py +++ b/src/lib/Bcfg2/Client/Tools/YUM.py @@ -125,7 +125,7 @@ class YUM(Bcfg2.Client.Tools.PkgTool): ('Package', 'rpm'), ('Path', 'ignore')] - __req__ = {'Package': ['name'], + __req__ = {'Package': ['type'], 'Path': ['type']} conflicts = ['RPM'] @@ -290,6 +290,17 @@ class YUM(Bcfg2.Client.Tools.PkgTool): return self.yumbase.rpmdb.returnGPGPubkeyPackages() return self.yumbase.rpmdb.searchNevra(name='gpg-pubkey') + def missing_attrs(self, entry): + """ Implementing from superclass to check for existence of either + name or group attribute for Package entry in the case of a YUM + group. """ + missing = Bcfg2.Client.Tools.PkgTool.missing_attrs(self, entry) + + if entry.get('name', None) == None and \ + entry.get('group', None) == None: + missing += ['name', 'group'] + return missing + def _verifyHelper(self, pkg_obj): """ _verifyHelper primarly deals with a yum bug where the pkg_obj.verify() method does not properly take into count multilib @@ -412,8 +423,12 @@ class YUM(Bcfg2.Client.Tools.PkgTool): if entry.get('version', False) == 'auto': self._fixAutoVersion(entry) - self.logger.debug("Verifying package instances for %s" % - entry.get('name')) + if entry.get('group'): + self.logger.debug("Verifying packages for group %s" % + entry.get('group')) + else: + self.logger.debug("Verifying package instances for %s" % + entry.get('name')) self.verify_cache = dict() # Used for checking multilib packages self.modlists[entry] = modlist @@ -426,14 +441,58 @@ class YUM(Bcfg2.Client.Tools.PkgTool): entry.get('pkg_checks', 'true').lower() == 'true' pkg_verify = self.pkg_verify and \ entry.get('pkg_verify', 'true').lower() == 'true' + yum_group = False if entry.get('name') == 'gpg-pubkey': all_pkg_objs = self._getGPGKeysAsPackages() pkg_verify = False # No files here to verify + elif entry.get('group'): + entry.set('name', 'group:%s' % entry.get('group')) + yum_group = True + all_pkg_objs = [] + instances = [] + if self.yumbase.comps.has_group(entry.get('group')): + group = self.yumbase.comps.return_group(entry.get('group')) + group_packages = [p + for p, d in group.mandatory_packages.items() + if d] + group_type = entry.get('choose', 'default') + if group_type in ['default', 'optional', 'all']: + group_packages += [p + for p, d in + group.default_packages.items() + if d] + if group_type in ['optional', 'all']: + group_packages += [p + for p, d in + group.optional_packages.items() + if d] + if len(group_packages) == 0: + self.logger.error("No packages found for group %s" % + entry.get("group")) + for pkg in group_packages: + # create package instances for each package in yum group + instance = Bcfg2.Client.XML.SubElement(entry, 'Package') + instance.attrib['name'] = pkg + instance.attrib['type'] = 'yum' + try: + newest = \ + self.yumbase.pkgSack.returnNewestByName(pkg)[0] + instance.attrib['version'] = newest['version'] + instance.attrib['epoch'] = newest['epoch'] + instance.attrib['release'] = newest['release'] + except: # pylint: disable=W0702 + self.logger.info("Error finding newest package " + "for %s" % + pkg) + instance.attrib['version'] = 'any' + instances.append(instance) + else: + self.logger.error("Group not found: %s" % entry.get("group")) else: all_pkg_objs = \ self.yumbase.rpmdb.searchNevra(name=entry.get('name')) - if len(all_pkg_objs) == 0: + if len(all_pkg_objs) == 0 and yum_group != True: # Some sort of virtual capability? Try to resolve it all_pkg_objs = self.yumbase.rpmdb.searchProvides(entry.get('name')) if len(all_pkg_objs) > 0: @@ -444,7 +503,13 @@ class YUM(Bcfg2.Client.Tools.PkgTool): self.logger.info(" %s" % pkg) for inst in instances: - nevra = build_yname(entry.get('name'), inst) + if yum_group: + # the entry is not the name of the package + nevra = build_yname(inst.get('name'), inst) + all_pkg_objs = \ + self.yumbase.rpmdb.searchNevra(name=inst.get('name')) + else: + nevra = build_yname(entry.get('name'), inst) if nevra in pkg_cache: continue # Ignore duplicate instances else: @@ -458,7 +523,10 @@ class YUM(Bcfg2.Client.Tools.PkgTool): stat['version_fail'] = False stat['verify'] = {} stat['verify_fail'] = False - stat['pkg'] = entry + if yum_group: + stat['pkg'] = inst + else: + stat['pkg'] = entry stat['modlist'] = modlist if inst.get('verify_flags'): # this splits on either space or comma @@ -627,7 +695,9 @@ class YUM(Bcfg2.Client.Tools.PkgTool): else: install_only = False - if virt_pkg or (install_only and not self.setup['kevlar']): + if virt_pkg or \ + (install_only and not self.setup['kevlar']) or \ + yum_group: # virtual capability supplied, we are probably dealing # with multiple packages of different names. This check # doesn't make a lot of since in this case. diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py index 39a664df1..473d2bf09 100644 --- a/src/lib/Bcfg2/Client/Tools/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -130,7 +130,6 @@ class Tool(object): raise ToolInstantiationError("%s: %s not executable" % (self.name, filename)) - def BundleUpdated(self, bundle): # pylint: disable=W0613 """ Callback that is invoked when a bundle has been updated. @@ -516,8 +515,8 @@ class SvcTool(Tool): :param service: The service entry to modify :type service: lxml.etree._Element - :returns: tuple - The return value from - :class:`Bcfg2.Client.Tools.Executor.run` + :returns: Bcfg2.Utils.ExecutorResult - The return value from + :class:`Bcfg2.Utils.Executor.run` """ self.logger.debug('Starting service %s' % service.get('name')) return self.cmd.run(self.get_svc_command(service, 'start')) @@ -527,8 +526,8 @@ class SvcTool(Tool): :param service: The service entry to modify :type service: lxml.etree._Element - :returns: tuple - The return value from - :class:`Bcfg2.Client.Tools.Executor.run` + :returns: Bcfg2.Utils.ExecutorResult - The return value from + :class:`Bcfg2.Utils.Executor.run` """ self.logger.debug('Stopping service %s' % service.get('name')) return self.cmd.run(self.get_svc_command(service, 'stop')) @@ -538,8 +537,8 @@ class SvcTool(Tool): :param service: The service entry to modify :type service: lxml.etree._Element - :returns: tuple - The return value from - :class:`Bcfg2.Client.Tools.Executor.run` + :returns: Bcfg2.Utils.ExecutorResult - The return value from + :class:`Bcfg2.Utils.Executor.run` """ self.logger.debug('Restarting service %s' % service.get('name')) restart_target = service.get('target', 'restart') @@ -553,7 +552,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')) + return bool(self.cmd.run(self.get_svc_command(service, 'status'))) def Remove(self, services): if self.setup['servicemode'] != 'disabled': diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py index beb534791..44c76303c 100644 --- a/src/lib/Bcfg2/Compat.py +++ b/src/lib/Bcfg2/Compat.py @@ -19,12 +19,13 @@ except ImportError: # urllib imports try: + from urllib import quote_plus from urlparse import urljoin, urlparse from urllib2 import HTTPBasicAuthHandler, \ HTTPPasswordMgrWithDefaultRealm, build_opener, install_opener, \ urlopen, HTTPError, URLError except ImportError: - from urllib.parse import urljoin, urlparse + from urllib.parse import urljoin, urlparse, quote_plus from urllib.request import HTTPBasicAuthHandler, \ HTTPPasswordMgrWithDefaultRealm, build_opener, install_opener, urlopen from urllib.error import HTTPError, URLError @@ -51,7 +52,8 @@ except ImportError: # xmlrpc imports try: - import xmlrpclib, SimpleXMLRPCServer + import xmlrpclib + import SimpleXMLRPCServer except ImportError: import xmlrpc.client as xmlrpclib import xmlrpc.server as SimpleXMLRPCServer @@ -73,6 +75,7 @@ try: except NameError: unicode = str + def u_str(string, encoding=None): """ print to file compatibility """ if sys.hexversion >= 0x03000000: diff --git a/src/lib/Bcfg2/Logger.py b/src/lib/Bcfg2/Logger.py index c2eac1e60..5bbc9ff96 100644 --- a/src/lib/Bcfg2/Logger.py +++ b/src/lib/Bcfg2/Logger.py @@ -105,7 +105,11 @@ class FragmentingSysLogHandler(logging.handlers.SysLogHandler): (self.encodePriority(self.facility, newrec.levelname.lower()), self.format(newrec)) try: - self.socket.send(msg.encode('ascii')) + try: + encoded = msg.encode('utf-8') + except UnicodeDecodeError: + encoded = msg + self.socket.send(encoded) except socket.error: for i in range(10): # pylint: disable=W0612 try: @@ -139,6 +143,10 @@ def add_console_handler(level=logging.DEBUG): console.setLevel(level) # tell the handler to use this format console.setFormatter(TermiosFormatter()) + try: + console.set_name("console") + except AttributeError: + console.name = "console" logging.root.addHandler(console) @@ -153,6 +161,10 @@ def add_syslog_handler(procname, syslog_facility, level=logging.DEBUG): syslog = FragmentingSysLogHandler(procname, ('localhost', 514), syslog_facility) + try: + syslog.set_name("syslog") + except AttributeError: + syslog.name = "syslog" syslog.setLevel(level) syslog.setFormatter( logging.Formatter('%(name)s[%(process)d]: %(message)s')) @@ -166,6 +178,10 @@ 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) + try: + filelog.set_name("file") + except AttributeError: + filelog.name = "file" filelog.setLevel(level) filelog.setFormatter( logging.Formatter('%(asctime)s %(name)s[%(process)d]: %(message)s')) diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index 74c488b45..e5aeccf4d 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -528,6 +528,11 @@ SERVER_FAM_IGNORE = \ 'SCCS', '.svn', '4913', '.gitignore'], cf=('server', 'ignore_files'), cook=list_split) +SERVER_FAM_BLOCK = \ + Option('FAM blocks on startup until all events are processed', + default=False, + cook=get_bool, + cf=('server', 'fam_blocking')) SERVER_LISTEN_ALL = \ Option('Listen on all interfaces', default=False, @@ -630,17 +635,19 @@ WEB_CFILE = \ default="/etc/bcfg2-web.conf", cmd='-W', odesc='<conffile>', - cf=('statistics', 'config'),) + cf=('reporting', 'config'), + deprecated_cf=('statistics', 'web_prefix'),) DJANGO_TIME_ZONE = \ Option('Django timezone', default=None, - cf=('statistics', 'time_zone'),) + cf=('reporting', 'time_zone'), + deprecated_cf=('statistics', 'web_prefix'),) DJANGO_DEBUG = \ Option('Django debug', default=None, - cf=('statistics', 'web_debug'), + cf=('reporting', 'web_debug'), + deprecated_cf=('statistics', 'web_prefix'), cook=get_bool,) -# Django options DJANGO_WEB_PREFIX = \ Option('Web prefix', default=None, @@ -824,7 +831,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='<opts>', @@ -839,6 +846,21 @@ TEST_IGNORE = \ cf=('bcfg2_test', 'ignore_entries'), cook=list_split, long_arg=True) +TEST_CHILDREN = \ + Option('Spawn this number of children for bcfg2-test (python 2.6+)', + default=0, + cmd='--children', + odesc='<children>', + cf=('bcfg2_test', 'children'), + cook=int, + long_arg=True) +TEST_XUNIT = \ + Option('Output an XUnit result file with --children', + default=None, + cmd='--xunit', + odesc='<xunit file>', + cf=('bcfg2_test', 'xunit'), + long_arg=True) LINT_CONFIG = \ Option('Specify bcfg2-lint configuration file', default='/etc/bcfg2-lint.conf', @@ -1019,7 +1041,7 @@ CRYPT_STDOUT = \ cmd='--stdout', long_arg=True) CRYPT_PASSPHRASE = \ - Option('Encryption passphrase (name or passphrase)', + Option('Encryption passphrase name', default=None, cmd='-p', odesc='<passphrase>') @@ -1066,6 +1088,7 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY, password=SERVER_PASSWORD, filemonitor=SERVER_FILEMONITOR, ignore=SERVER_FAM_IGNORE, + fam_blocking=SERVER_FAM_BLOCK, location=SERVER_LOCATION, key=SERVER_KEY, cert=SERVER_CERT, @@ -1172,6 +1195,16 @@ 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) + +INFO_COMMON_OPTIONS = dict(ppath=PARANOID_PATH, + max_copies=PARANOID_MAX_COPIES) +INFO_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS) +INFO_COMMON_OPTIONS.update(SERVER_COMMON_OPTIONS) class OptionParser(OptionSet): """ OptionParser bootstraps option parsing, getting the value of diff --git a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py index bca4a9c1e..3b2c0ccfa 100644 --- a/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py +++ b/src/lib/Bcfg2/Reporting/Storage/DjangoORM.py @@ -16,6 +16,7 @@ from Bcfg2.Reporting.Storage.base import StorageBase, StorageError from Bcfg2.Server.Plugin.exceptions import PluginExecutionError from django.core import management from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned +from django.db.models import FieldDoesNotExist from django.core.cache import cache from django.db import transaction @@ -30,6 +31,230 @@ class DjangoORM(StorageBase): super(DjangoORM, self).__init__(setup) self.size_limit = setup.get('reporting_file_limit') + def _import_default(self, entry, state, entrytype=None, defaults=None, + mapping=None, boolean=None, xforms=None): + """ Default entry importer. Maps the entry (in state + ``state``) to an appropriate *Entry object; by default, this + is determined by the entry tag, e.g., from an Action entry an + ActionEntry object is created. This can be overridden with + ``entrytype``, which should be the class to instantiate for + this entry. + + ``defaults`` is an optional mapping of <attribute + name>:<value> that will be used to set the default values for + various attributes. + + ``mapping`` is a mapping of <field name>:<attribute name> that + can be used to map fields that are named differently on the + XML entry and in the database model. + + ``boolean`` is a list of attribute names that should be + treated as booleans. + + ``xforms`` is a dict of <attribute name>:<function>, where the + given function will be applied to the value of the named + attribute before trying to store it in the database. + """ + if entrytype is None: + entrytype = globals()["%sEntry" % entry.tag] + if defaults is None: + defaults = dict() + if mapping is None: + mapping = dict() + if boolean is None: + boolean = [] + if xforms is None: + xforms = dict() + mapping['exists'] = 'current_exists' + defaults['current_exists'] = 'true' + boolean.append("current_exists") + + def boolean_xform(val): + try: + return val.lower() == "true" + except AttributeError: + return False + + for attr in boolean + ["current_exists"]: + xforms[attr] = boolean_xform + act_dict = dict(state=state) + for fieldname in entrytype._meta.get_all_field_names(): + if fieldname in ['id', 'hash_key', 'state']: + continue + try: + field = entrytype._meta.get_field(fieldname) + except FieldDoesNotExist: + continue + attrname = mapping.get(fieldname, fieldname) + val = entry.get(fieldname, defaults.get(attrname)) + act_dict[fieldname] = xforms.get(attrname, lambda v: v)(val) + self.logger.debug("Adding %s:%s" % (entry.tag, entry.get("name"))) + return entrytype.entry_get_or_create(act_dict) + + def _import_Action(self, entry, state): + return self._import_default(entry, state, + defaults=dict(status='check', rc=-1), + mapping=dict(output="rc")) + + def _import_Package(self, entry, state): + name = entry.get('name') + exists = entry.get('current_exists', default="true").lower() == "true" + act_dict = dict(name=name, state=state, exists=exists, + target_version=entry.get('version', default=''), + current_version=entry.get('current_version', + default='')) + + # extra entries are a bit different. They can have Instance + # objects + if not act_dict['target_version']: + for instance in entry.findall("Instance"): + # FIXME - this probably only works for rpms + release = instance.get('release', '') + arch = instance.get('arch', '') + act_dict['current_version'] = instance.get('version') + if release: + act_dict['current_version'] += "-" + release + if arch: + act_dict['current_version'] += "." + arch + self.logger.debug("Adding package %s %s" % + (name, act_dict['current_version'])) + return PackageEntry.entry_get_or_create(act_dict) + else: + self.logger.debug("Adding package %s %s" % + (name, act_dict['target_version'])) + + # not implemented yet + act_dict['verification_details'] = \ + entry.get('verification_details', '') + return PackageEntry.entry_get_or_create(act_dict) + + def _import_Path(self, entry, state): + name = entry.get('name') + exists = entry.get('current_exists', default="true").lower() == "true" + path_type = entry.get("type").lower() + act_dict = dict(name=name, state=state, exists=exists, + path_type=path_type) + + target_dict = dict( + owner=entry.get('owner', default="root"), + group=entry.get('group', default="root"), + mode=entry.get('mode', default=entry.get('perms', + default="")) + ) + fperm, created = FilePerms.objects.get_or_create(**target_dict) + act_dict['target_perms'] = fperm + + current_dict = dict( + owner=entry.get('current_owner', default=""), + group=entry.get('current_group', default=""), + mode=entry.get('current_mode', + default=entry.get('current_perms', default="")) + ) + fperm, created = FilePerms.objects.get_or_create(**current_dict) + act_dict['current_perms'] = fperm + + if path_type in ('symlink', 'hardlink'): + act_dict['target_path'] = entry.get('to', default="") + act_dict['current_path'] = entry.get('current_to', default="") + self.logger.debug("Adding link %s" % name) + return LinkEntry.entry_get_or_create(act_dict) + elif path_type == 'device': + # TODO devices + self.logger.warn("device path types are not supported yet") + return + + # TODO - vcs output + act_dict['detail_type'] = PathEntry.DETAIL_UNUSED + if path_type == 'directory' and entry.get('prune', 'false') == 'true': + unpruned_elist = [e.get('path') for e in entry.findall('Prune')] + if unpruned_elist: + act_dict['detail_type'] = PathEntry.DETAIL_PRUNED + act_dict['details'] = "\n".join(unpruned_elist) + elif entry.get('sensitive', 'false').lower() == 'true': + act_dict['detail_type'] = PathEntry.DETAIL_SENSITIVE + else: + cdata = None + if entry.get('current_bfile', None): + act_dict['detail_type'] = PathEntry.DETAIL_BINARY + cdata = entry.get('current_bfile') + elif entry.get('current_bdiff', None): + act_dict['detail_type'] = PathEntry.DETAIL_DIFF + cdata = b64decode(entry.get('current_bdiff')) + elif entry.get('current_diff', None): + act_dict['detail_type'] = PathEntry.DETAIL_DIFF + cdata = entry.get('current_bdiff') + if cdata: + if len(cdata) > self.size_limit: + act_dict['detail_type'] = PathEntry.DETAIL_SIZE_LIMIT + act_dict['details'] = md5(cdata).hexdigest() + else: + act_dict['details'] = cdata + self.logger.debug("Adding path %s" % name) + return PathEntry.entry_get_or_create(act_dict) + # TODO - secontext + # TODO - acls + + def _import_Service(self, entry, state): + return self._import_default(entry, state, + defaults=dict(status='', + current_status=''), + mapping=dict(status='target_status')) + + def _import_SEBoolean(self, entry, state): + return self._import_default( + entry, state, + xforms=dict(value=lambda v: v.lower() == "on")) + + def _import_SEFcontext(self, entry, state): + return self._import_default(entry, state, + defaults=dict(filetype='all')) + + def _import_SEInterface(self, entry, state): + return self._import_default(entry, state) + + def _import_SEPort(self, entry, state): + return self._import_default(entry, state) + + def _import_SENode(self, entry, state): + return self._import_default(entry, state) + + def _import_SELogin(self, entry, state): + return self._import_default(entry, state) + + def _import_SEUser(self, entry, state): + return self._import_default(entry, state) + + def _import_SEPermissive(self, entry, state): + return self._import_default(entry, state) + + def _import_SEModule(self, entry, state): + return self._import_default(entry, state, + defaults=dict(disabled='false'), + boolean=['disabled', 'current_disabled']) + + def _import_POSIXUser(self, entry, state): + defaults = dict(group=entry.get("name"), + gecos=entry.get("name"), + shell='/bin/bash', + uid=entry.get("current_uid")) + if entry.get('name') == 'root': + defaults['home'] = '/root' + else: + defaults['home'] = '/home/%s' % entry.get('name') + + # TODO: supplementary group membership + return self._import_default(entry, state, defaults=defaults) + + def _import_POSIXGroup(self, entry, state): + return self._import_default( + entry, state, + defaults=dict(gid=entry.get("current_gid"))) + + def _import_unknown(self, entry, _): + self.logger.error("Unknown type %s not handled by reporting yet" % + entry.tag) + return None + @transaction.commit_on_success def _import_interaction(self, interaction): """Real import function""" @@ -46,13 +271,15 @@ class DjangoORM(StorageBase): cache.set(hostname, client) timestamp = datetime(*strptime(stats.get('time'))[0:6]) - if len(Interaction.objects.filter(client=client, timestamp=timestamp)) > 0: + if len(Interaction.objects.filter(client=client, + timestamp=timestamp)) > 0: self.logger.warn("Interaction for %s at %s already exists" % (hostname, timestamp)) return if 'profile' in metadata: - profile, created = Group.objects.get_or_create(name=metadata['profile']) + profile, created = \ + Group.objects.get_or_create(name=metadata['profile']) else: profile = None inter = Interaction(client=client, @@ -65,10 +292,10 @@ class DjangoORM(StorageBase): server=server, profile=profile) inter.save() - self.logger.debug("Interaction for %s at %s with INSERTED in to db" % + self.logger.debug("Interaction for %s at %s with INSERTED in to db" % (client.id, timestamp)) - #FIXME - this should be more efficient + # FIXME - this should be more efficient for group_name in metadata['groups']: group = cache.get("GROUP_" + group_name) if not group: @@ -76,12 +303,13 @@ class DjangoORM(StorageBase): if created: self.logger.debug("Added group %s" % group) cache.set("GROUP_" + group_name, group) - + inter.groups.add(group) - for bundle_name in metadata['bundles']: + for bundle_name in metadata.get('bundles', []): bundle = cache.get("BUNDLE_" + bundle_name) if not bundle: - bundle, created = Bundle.objects.get_or_create(name=bundle_name) + bundle, created = \ + Bundle.objects.get_or_create(name=bundle_name) if created: self.logger.debug("Added bundle %s" % bundle) cache.set("BUNDLE_" + bundle_name, bundle) @@ -94,130 +322,26 @@ class DjangoORM(StorageBase): pattern = [('Bad/*', TYPE_BAD), ('Extra/*', TYPE_EXTRA), ('Modified/*', TYPE_MODIFIED)] - updates = dict(failures=[], paths=[], packages=[], actions=[], services=[]) + updates = dict([(etype, []) for etype in Interaction.entry_types]) for (xpath, state) in pattern: for entry in stats.findall(xpath): counter_fields[state] = counter_fields[state] + 1 - entry_type = entry.tag - name = entry.get('name') - exists = entry.get('current_exists', default="true").lower() == "true" - # handle server failures differently failure = entry.get('failure', '') if failure: - act_dict = dict(name=name, entry_type=entry_type, - message=failure) + act_dict = dict(name=entry.get("name"), + entry_type=entry.tag, + message=failure) newact = FailureEntry.entry_get_or_create(act_dict) updates['failures'].append(newact) continue - act_dict = dict(name=name, state=state, exists=exists) - - if entry_type == 'Action': - act_dict['status'] = entry.get('status', default="check") - act_dict['output'] = entry.get('rc', default=-1) - self.logger.debug("Adding action %s" % name) - updates['actions'].append(ActionEntry.entry_get_or_create(act_dict)) - elif entry_type == 'Package': - act_dict['target_version'] = entry.get('version', default='') - act_dict['current_version'] = entry.get('current_version', default='') - - # extra entries are a bit different. They can have Instance objects - if not act_dict['target_version']: - for instance in entry.findall("Instance"): - #TODO - this probably only works for rpms - release = instance.get('release', '') - arch = instance.get('arch', '') - act_dict['current_version'] = instance.get('version') - if release: - act_dict['current_version'] += "-" + release - if arch: - act_dict['current_version'] += "." + arch - self.logger.debug("Adding package %s %s" % (name, act_dict['current_version'])) - updates['packages'].append(PackageEntry.entry_get_or_create(act_dict)) - else: - - self.logger.debug("Adding package %s %s" % (name, act_dict['target_version'])) - - # not implemented yet - act_dict['verification_details'] = entry.get('verification_details', '') - updates['packages'].append(PackageEntry.entry_get_or_create(act_dict)) - - elif entry_type == 'Path': - path_type = entry.get("type").lower() - act_dict['path_type'] = path_type - - target_dict = dict( - owner=entry.get('owner', default="root"), - group=entry.get('group', default="root"), - mode=entry.get('mode', default=entry.get('perms', default="")) - ) - fperm, created = FilePerms.objects.get_or_create(**target_dict) - act_dict['target_perms'] = fperm - - current_dict = dict( - owner=entry.get('current_owner', default=""), - group=entry.get('current_group', default=""), - mode=entry.get('current_mode', - default=entry.get('current_perms', default="")) - ) - fperm, created = FilePerms.objects.get_or_create(**current_dict) - act_dict['current_perms'] = fperm - - if path_type in ('symlink', 'hardlink'): - act_dict['target_path'] = entry.get('to', default="") - act_dict['current_path'] = entry.get('current_to', default="") - self.logger.debug("Adding link %s" % name) - updates['paths'].append(LinkEntry.entry_get_or_create(act_dict)) - continue - elif path_type == 'device': - #TODO devices - self.logger.warn("device path types are not supported yet") - continue - - # TODO - vcs output - act_dict['detail_type'] = PathEntry.DETAIL_UNUSED - if path_type == 'directory' and entry.get('prune', 'false') == 'true': - unpruned_elist = [e.get('path') for e in entry.findall('Prune')] - if unpruned_elist: - act_dict['detail_type'] = PathEntry.DETAIL_PRUNED - act_dict['details'] = "\n".join(unpruned_elist) - elif entry.get('sensitive', 'false').lower() == 'true': - act_dict['detail_type'] = PathEntry.DETAIL_SENSITIVE - else: - cdata = None - if entry.get('current_bfile', None): - act_dict['detail_type'] = PathEntry.DETAIL_BINARY - cdata = entry.get('current_bfile') - elif entry.get('current_bdiff', None): - act_dict['detail_type'] = PathEntry.DETAIL_DIFF - cdata = b64decode(entry.get('current_bdiff')) - elif entry.get('current_diff', None): - act_dict['detail_type'] = PathEntry.DETAIL_DIFF - cdata = entry.get('current_bdiff') - if cdata: - if len(cdata) > self.size_limit: - act_dict['detail_type'] = PathEntry.DETAIL_SIZE_LIMIT - act_dict['details'] = md5(cdata).hexdigest() - else: - act_dict['details'] = cdata - self.logger.debug("Adding path %s" % name) - updates['paths'].append(PathEntry.entry_get_or_create(act_dict)) - - - #TODO - secontext - #TODO - acls - - elif entry_type == 'Service': - act_dict['target_status'] = entry.get('status', default='') - act_dict['current_status'] = entry.get('current_status', default='') - self.logger.debug("Adding service %s" % name) - updates['services'].append(ServiceEntry.entry_get_or_create(act_dict)) - elif entry_type == 'SELinux': - self.logger.info("SELinux not implemented yet") - else: - self.logger.error("Unknown type %s not handled by reporting yet" % entry_type) + updatetype = entry.tag.lower() + "s" + update = getattr(self, "_import_%s" % entry.tag, + self._import_unknown)(entry, state) + if update is not None: + updates[updatetype].append(update) inter.bad_count = counter_fields[TYPE_BAD] inter.modified_count = counter_fields[TYPE_MODIFIED] @@ -227,15 +351,16 @@ class DjangoORM(StorageBase): # batch this for sqlite i = 0 while(i < len(updates[entry_type])): - getattr(inter, entry_type).add(*updates[entry_type][i:i+100]) + getattr(inter, entry_type).add(*updates[entry_type][i:i + 100]) i += 100 # performance metrics for times in stats.findall('OpStamps'): for metric, value in list(times.items()): - Performance(interaction=inter, metric=metric, value=value).save() + Performance(interaction=inter, + metric=metric, + value=value).save() - def import_interaction(self, interaction): """Import the data into the backend""" @@ -245,7 +370,6 @@ class DjangoORM(StorageBase): self.logger.error("Failed to import interaction: %s" % traceback.format_exc().splitlines()[-1]) - def validate(self): """Validate backend storage. Should be called once when loaded""" diff --git a/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py b/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py index 30ea39263..0a0f032e5 100644 --- a/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py +++ b/src/lib/Bcfg2/Reporting/Transport/LocalFilesystem.py @@ -36,7 +36,8 @@ class LocalFilesystem(TransportBase): def set_debug(self, debug): rv = TransportBase.set_debug(self, debug) - self.fmon.set_debug(debug) + if self.fmon is not None: + self.fmon.set_debug(debug) return rv def start_monitor(self, collector): @@ -48,7 +49,8 @@ class LocalFilesystem(TransportBase): self.logger.error("File monitor driver %s not available; " "forcing to default" % setup['filemonitor']) fmon = Bcfg2.Server.FileMonitor.available['default'] - + if self.debug_flag: + self.fmon.set_debug(self.debug_flag) try: self.fmon = fmon(debug=self.debug_flag) self.logger.info("Using the %s file monitor" % diff --git a/src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py b/src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py new file mode 100644 index 000000000..d5f5d801a --- /dev/null +++ b/src/lib/Bcfg2/Reporting/migrations/0005_add_selinux_entry_support.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'SELoginEntry' + db.create_table('Reporting_seloginentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('selinuxuser', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('current_selinuxuser', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + )) + db.send_create_signal('Reporting', ['SELoginEntry']) + + # Adding model 'SEUserEntry' + db.create_table('Reporting_seuserentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('roles', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('current_roles', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + ('prefix', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('current_prefix', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + )) + db.send_create_signal('Reporting', ['SEUserEntry']) + + # Adding model 'SEBooleanEntry' + db.create_table('Reporting_sebooleanentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('value', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal('Reporting', ['SEBooleanEntry']) + + # Adding model 'SENodeEntry' + db.create_table('Reporting_senodeentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + ('proto', self.gf('django.db.models.fields.CharField')(max_length=4)), + )) + db.send_create_signal('Reporting', ['SENodeEntry']) + + # Adding model 'SEFcontextEntry' + db.create_table('Reporting_sefcontextentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + ('filetype', self.gf('django.db.models.fields.CharField')(max_length=16)), + )) + db.send_create_signal('Reporting', ['SEFcontextEntry']) + + # Adding model 'SEInterfaceEntry' + db.create_table('Reporting_seinterfaceentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + )) + db.send_create_signal('Reporting', ['SEInterfaceEntry']) + + # Adding model 'SEPermissiveEntry' + db.create_table('Reporting_sepermissiveentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal('Reporting', ['SEPermissiveEntry']) + + # Adding model 'SEModuleEntry' + db.create_table('Reporting_semoduleentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('disabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('current_disabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('Reporting', ['SEModuleEntry']) + + # Adding model 'SEPortEntry' + db.create_table('Reporting_seportentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('current_selinuxtype', self.gf('django.db.models.fields.CharField')(max_length=128, null=True)), + )) + db.send_create_signal('Reporting', ['SEPortEntry']) + + # Adding M2M table for field sebooleans on 'Interaction' + db.create_table('Reporting_interaction_sebooleans', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('sebooleanentry', models.ForeignKey(orm['Reporting.sebooleanentry'], null=False)) + )) + db.create_unique('Reporting_interaction_sebooleans', ['interaction_id', 'sebooleanentry_id']) + + # Adding M2M table for field seports on 'Interaction' + db.create_table('Reporting_interaction_seports', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('seportentry', models.ForeignKey(orm['Reporting.seportentry'], null=False)) + )) + db.create_unique('Reporting_interaction_seports', ['interaction_id', 'seportentry_id']) + + # Adding M2M table for field sefcontexts on 'Interaction' + db.create_table('Reporting_interaction_sefcontexts', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('sefcontextentry', models.ForeignKey(orm['Reporting.sefcontextentry'], null=False)) + )) + db.create_unique('Reporting_interaction_sefcontexts', ['interaction_id', 'sefcontextentry_id']) + + # Adding M2M table for field senodes on 'Interaction' + db.create_table('Reporting_interaction_senodes', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('senodeentry', models.ForeignKey(orm['Reporting.senodeentry'], null=False)) + )) + db.create_unique('Reporting_interaction_senodes', ['interaction_id', 'senodeentry_id']) + + # Adding M2M table for field selogins on 'Interaction' + db.create_table('Reporting_interaction_selogins', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('seloginentry', models.ForeignKey(orm['Reporting.seloginentry'], null=False)) + )) + db.create_unique('Reporting_interaction_selogins', ['interaction_id', 'seloginentry_id']) + + # Adding M2M table for field seusers on 'Interaction' + db.create_table('Reporting_interaction_seusers', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('seuserentry', models.ForeignKey(orm['Reporting.seuserentry'], null=False)) + )) + db.create_unique('Reporting_interaction_seusers', ['interaction_id', 'seuserentry_id']) + + # Adding M2M table for field seinterfaces on 'Interaction' + db.create_table('Reporting_interaction_seinterfaces', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('seinterfaceentry', models.ForeignKey(orm['Reporting.seinterfaceentry'], null=False)) + )) + db.create_unique('Reporting_interaction_seinterfaces', ['interaction_id', 'seinterfaceentry_id']) + + # Adding M2M table for field sepermissives on 'Interaction' + db.create_table('Reporting_interaction_sepermissives', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('sepermissiveentry', models.ForeignKey(orm['Reporting.sepermissiveentry'], null=False)) + )) + db.create_unique('Reporting_interaction_sepermissives', ['interaction_id', 'sepermissiveentry_id']) + + # Adding M2M table for field semodules on 'Interaction' + db.create_table('Reporting_interaction_semodules', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('semoduleentry', models.ForeignKey(orm['Reporting.semoduleentry'], null=False)) + )) + db.create_unique('Reporting_interaction_semodules', ['interaction_id', 'semoduleentry_id']) + + + def backwards(self, orm): + # Deleting model 'SELoginEntry' + db.delete_table('Reporting_seloginentry') + + # Deleting model 'SEUserEntry' + db.delete_table('Reporting_seuserentry') + + # Deleting model 'SEBooleanEntry' + db.delete_table('Reporting_sebooleanentry') + + # Deleting model 'SENodeEntry' + db.delete_table('Reporting_senodeentry') + + # Deleting model 'SEFcontextEntry' + db.delete_table('Reporting_sefcontextentry') + + # Deleting model 'SEInterfaceEntry' + db.delete_table('Reporting_seinterfaceentry') + + # Deleting model 'SEPermissiveEntry' + db.delete_table('Reporting_sepermissiveentry') + + # Deleting model 'SEModuleEntry' + db.delete_table('Reporting_semoduleentry') + + # Deleting model 'SEPortEntry' + db.delete_table('Reporting_seportentry') + + # Removing M2M table for field sebooleans on 'Interaction' + db.delete_table('Reporting_interaction_sebooleans') + + # Removing M2M table for field seports on 'Interaction' + db.delete_table('Reporting_interaction_seports') + + # Removing M2M table for field sefcontexts on 'Interaction' + db.delete_table('Reporting_interaction_sefcontexts') + + # Removing M2M table for field senodes on 'Interaction' + db.delete_table('Reporting_interaction_senodes') + + # Removing M2M table for field selogins on 'Interaction' + db.delete_table('Reporting_interaction_selogins') + + # Removing M2M table for field seusers on 'Interaction' + db.delete_table('Reporting_interaction_seusers') + + # Removing M2M table for field seinterfaces on 'Interaction' + db.delete_table('Reporting_interaction_seinterfaces') + + # Removing M2M table for field sepermissives on 'Interaction' + db.delete_table('Reporting_interaction_sepermissives') + + # Removing M2M table for field semodules on 'Interaction' + db.delete_table('Reporting_interaction_semodules') + + + models = { + 'Reporting.actionentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ActionEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'output': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'check'", 'max_length': '128'}) + }, + 'Reporting.bundle': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Bundle'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'Reporting.client': { + 'Meta': {'object_name': 'Client'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'current_interaction': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'parent_client'", 'null': 'True', 'to': "orm['Reporting.Interaction']"}), + 'expiration': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'Reporting.deviceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'DeviceEntry', '_ormbases': ['Reporting.PathEntry']}, + 'current_major': ('django.db.models.fields.IntegerField', [], {}), + 'current_minor': ('django.db.models.fields.IntegerField', [], {}), + 'device_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}), + 'target_major': ('django.db.models.fields.IntegerField', [], {}), + 'target_minor': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.failureentry': { + 'Meta': {'object_name': 'FailureEntry'}, + 'entry_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'Reporting.fileacl': { + 'Meta': {'object_name': 'FileAcl'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'Reporting.fileperms': { + 'Meta': {'unique_together': "(('owner', 'group', 'mode'),)", 'object_name': 'FilePerms'}, + 'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'Reporting.group': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Group'}, + 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'profile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'Reporting.interaction': { + 'Meta': {'ordering': "['-timestamp']", 'unique_together': "(('client', 'timestamp'),)", 'object_name': 'Interaction'}, + 'actions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ActionEntry']", 'symmetrical': 'False'}), + 'bad_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}), + 'client': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'interactions'", 'to': "orm['Reporting.Client']"}), + 'extra_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'failures': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FailureEntry']", 'symmetrical': 'False'}), + 'good_count': ('django.db.models.fields.IntegerField', [], {}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PackageEntry']", 'symmetrical': 'False'}), + 'paths': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PathEntry']", 'symmetrical': 'False'}), + 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['Reporting.Group']"}), + 'repo_rev_code': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'sebooleans': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEBooleanEntry']", 'symmetrical': 'False'}), + 'sefcontexts': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEFcontextEntry']", 'symmetrical': 'False'}), + 'seinterfaces': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEInterfaceEntry']", 'symmetrical': 'False'}), + 'selogins': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SELoginEntry']", 'symmetrical': 'False'}), + 'semodules': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEModuleEntry']", 'symmetrical': 'False'}), + 'senodes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SENodeEntry']", 'symmetrical': 'False'}), + 'sepermissives': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPermissiveEntry']", 'symmetrical': 'False'}), + 'seports': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPortEntry']", 'symmetrical': 'False'}), + 'server': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'services': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ServiceEntry']", 'symmetrical': 'False'}), + 'seusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEUserEntry']", 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'total_count': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.linkentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'LinkEntry', '_ormbases': ['Reporting.PathEntry']}, + 'current_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}), + 'target_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}) + }, + 'Reporting.packageentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PackageEntry'}, + 'current_version': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'verification_details': ('django.db.models.fields.TextField', [], {'default': "''"}) + }, + 'Reporting.pathentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PathEntry'}, + 'acls': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FileAcl']", 'symmetrical': 'False'}), + 'current_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}), + 'detail_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'details': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'path_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}) + }, + 'Reporting.performance': { + 'Meta': {'object_name': 'Performance'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interaction': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'performance_items'", 'to': "orm['Reporting.Interaction']"}), + 'metric': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'value': ('django.db.models.fields.DecimalField', [], {'max_digits': '32', 'decimal_places': '16'}) + }, + 'Reporting.sebooleanentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEBooleanEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'value': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'Reporting.sefcontextentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEFcontextEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seinterfaceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEInterfaceEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seloginentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SELoginEntry'}, + 'current_selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.semoduleentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEModuleEntry'}, + 'current_disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.senodeentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SENodeEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'proto': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.sepermissiveentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPermissiveEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seportentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPortEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.serviceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ServiceEntry'}, + 'current_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}) + }, + 'Reporting.seuserentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEUserEntry'}, + 'current_prefix': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'current_roles': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'prefix': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'roles': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['Reporting']
\ No newline at end of file diff --git a/src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py b/src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py new file mode 100644 index 000000000..d86e663d5 --- /dev/null +++ b/src/lib/Bcfg2/Reporting/migrations/0006_add_user_group_entry_support.py @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'POSIXGroupEntry' + db.create_table('Reporting_posixgroupentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('gid', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('current_gid', self.gf('django.db.models.fields.IntegerField')(null=True)), + )) + db.send_create_signal('Reporting', ['POSIXGroupEntry']) + + # Adding model 'POSIXUserEntry' + db.create_table('Reporting_posixuserentry', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128, db_index=True)), + ('hash_key', self.gf('django.db.models.fields.BigIntegerField')(db_index=True)), + ('state', self.gf('django.db.models.fields.IntegerField')()), + ('exists', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('uid', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('current_uid', self.gf('django.db.models.fields.IntegerField')(null=True)), + ('group', self.gf('django.db.models.fields.CharField')(max_length=64)), + ('current_group', self.gf('django.db.models.fields.CharField')(max_length=64, null=True)), + ('gecos', self.gf('django.db.models.fields.CharField')(max_length=1024)), + ('current_gecos', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)), + ('home', self.gf('django.db.models.fields.CharField')(max_length=1024)), + ('current_home', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)), + ('shell', self.gf('django.db.models.fields.CharField')(default='/bin/bash', max_length=1024)), + ('current_shell', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)), + )) + db.send_create_signal('Reporting', ['POSIXUserEntry']) + + # Adding M2M table for field posixusers on 'Interaction' + db.create_table('Reporting_interaction_posixusers', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('posixuserentry', models.ForeignKey(orm['Reporting.posixuserentry'], null=False)) + )) + db.create_unique('Reporting_interaction_posixusers', ['interaction_id', 'posixuserentry_id']) + + # Adding M2M table for field posixgroups on 'Interaction' + db.create_table('Reporting_interaction_posixgroups', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('interaction', models.ForeignKey(orm['Reporting.interaction'], null=False)), + ('posixgroupentry', models.ForeignKey(orm['Reporting.posixgroupentry'], null=False)) + )) + db.create_unique('Reporting_interaction_posixgroups', ['interaction_id', 'posixgroupentry_id']) + + + def backwards(self, orm): + # Deleting model 'POSIXGroupEntry' + db.delete_table('Reporting_posixgroupentry') + + # Deleting model 'POSIXUserEntry' + db.delete_table('Reporting_posixuserentry') + + # Removing M2M table for field posixusers on 'Interaction' + db.delete_table('Reporting_interaction_posixusers') + + # Removing M2M table for field posixgroups on 'Interaction' + db.delete_table('Reporting_interaction_posixgroups') + + + models = { + 'Reporting.actionentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ActionEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'output': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'check'", 'max_length': '128'}) + }, + 'Reporting.bundle': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Bundle'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'Reporting.client': { + 'Meta': {'object_name': 'Client'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'current_interaction': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'parent_client'", 'null': 'True', 'to': "orm['Reporting.Interaction']"}), + 'expiration': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'Reporting.deviceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'DeviceEntry', '_ormbases': ['Reporting.PathEntry']}, + 'current_major': ('django.db.models.fields.IntegerField', [], {}), + 'current_minor': ('django.db.models.fields.IntegerField', [], {}), + 'device_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}), + 'target_major': ('django.db.models.fields.IntegerField', [], {}), + 'target_minor': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.failureentry': { + 'Meta': {'object_name': 'FailureEntry'}, + 'entry_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'Reporting.fileacl': { + 'Meta': {'object_name': 'FileAcl'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}) + }, + 'Reporting.fileperms': { + 'Meta': {'unique_together': "(('owner', 'group', 'mode'),)", 'object_name': 'FilePerms'}, + 'group': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'owner': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'Reporting.group': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Group'}, + 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'profile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'Reporting.interaction': { + 'Meta': {'ordering': "['-timestamp']", 'unique_together': "(('client', 'timestamp'),)", 'object_name': 'Interaction'}, + 'actions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ActionEntry']", 'symmetrical': 'False'}), + 'bad_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'bundles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Bundle']", 'symmetrical': 'False'}), + 'client': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'interactions'", 'to': "orm['Reporting.Client']"}), + 'extra_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'failures': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FailureEntry']", 'symmetrical': 'False'}), + 'good_count': ('django.db.models.fields.IntegerField', [], {}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PackageEntry']", 'symmetrical': 'False'}), + 'paths': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.PathEntry']", 'symmetrical': 'False'}), + 'posixgroups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.POSIXGroupEntry']", 'symmetrical': 'False'}), + 'posixusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.POSIXUserEntry']", 'symmetrical': 'False'}), + 'profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['Reporting.Group']"}), + 'repo_rev_code': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'sebooleans': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEBooleanEntry']", 'symmetrical': 'False'}), + 'sefcontexts': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEFcontextEntry']", 'symmetrical': 'False'}), + 'seinterfaces': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEInterfaceEntry']", 'symmetrical': 'False'}), + 'selogins': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SELoginEntry']", 'symmetrical': 'False'}), + 'semodules': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEModuleEntry']", 'symmetrical': 'False'}), + 'senodes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SENodeEntry']", 'symmetrical': 'False'}), + 'sepermissives': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPermissiveEntry']", 'symmetrical': 'False'}), + 'seports': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEPortEntry']", 'symmetrical': 'False'}), + 'server': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'services': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.ServiceEntry']", 'symmetrical': 'False'}), + 'seusers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.SEUserEntry']", 'symmetrical': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'total_count': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.linkentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'LinkEntry', '_ormbases': ['Reporting.PathEntry']}, + 'current_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'pathentry_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['Reporting.PathEntry']", 'unique': 'True', 'primary_key': 'True'}), + 'target_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}) + }, + 'Reporting.packageentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PackageEntry'}, + 'current_version': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'verification_details': ('django.db.models.fields.TextField', [], {'default': "''"}) + }, + 'Reporting.pathentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'PathEntry'}, + 'acls': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['Reporting.FileAcl']", 'symmetrical': 'False'}), + 'current_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}), + 'detail_type': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'details': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'path_type': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_perms': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['Reporting.FilePerms']"}) + }, + 'Reporting.performance': { + 'Meta': {'object_name': 'Performance'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interaction': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'performance_items'", 'to': "orm['Reporting.Interaction']"}), + 'metric': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'value': ('django.db.models.fields.DecimalField', [], {'max_digits': '32', 'decimal_places': '16'}) + }, + 'Reporting.posixgroupentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'POSIXGroupEntry'}, + 'current_gid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'gid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.posixuserentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'POSIXUserEntry'}, + 'current_gecos': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'current_group': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'current_home': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'current_shell': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}), + 'current_uid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'gecos': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'group': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'home': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'shell': ('django.db.models.fields.CharField', [], {'default': "'/bin/bash'", 'max_length': '1024'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'uid': ('django.db.models.fields.IntegerField', [], {'null': 'True'}) + }, + 'Reporting.sebooleanentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEBooleanEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'value': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'Reporting.sefcontextentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEFcontextEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seinterfaceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEInterfaceEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seloginentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SELoginEntry'}, + 'current_selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxuser': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.semoduleentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEModuleEntry'}, + 'current_disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'disabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.senodeentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SENodeEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'proto': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.sepermissiveentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPermissiveEntry'}, + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.seportentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEPortEntry'}, + 'current_selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'selinuxtype': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'Reporting.serviceentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'ServiceEntry'}, + 'current_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {}), + 'target_status': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}) + }, + 'Reporting.seuserentry': { + 'Meta': {'ordering': "('state', 'name')", 'object_name': 'SEUserEntry'}, + 'current_prefix': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'current_roles': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}), + 'exists': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'hash_key': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'prefix': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'roles': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'state': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['Reporting']
\ No newline at end of file diff --git a/src/lib/Bcfg2/Reporting/models.py b/src/lib/Bcfg2/Reporting/models.py index ab2dc8418..4be509f53 100644 --- a/src/lib/Bcfg2/Reporting/models.py +++ b/src/lib/Bcfg2/Reporting/models.py @@ -11,21 +11,9 @@ except ImproperlyConfigured: from django.core.cache import cache from datetime import datetime, timedelta +from Bcfg2.Compat import cPickle + -try: - import cPickle as pickle -except: - import pickle - -KIND_CHOICES = ( - #These are the kinds of config elements - ('Package', 'Package'), - ('Path', 'directory'), - ('Path', 'file'), - ('Path', 'permissions'), - ('Path', 'symlink'), - ('Service', 'Service'), -) TYPE_GOOD = 0 TYPE_BAD = 1 TYPE_MODIFIED = 2 @@ -57,8 +45,8 @@ def hash_entry(entry_dict): for key in sorted(entry_dict.keys()): if key in ('id', 'hash_key') or key.startswith('_'): continue - dataset.append( (key, entry_dict[key]) ) - return hash(pickle.dumps(dataset)) + dataset.append((key, entry_dict[key])) + return hash(cPickle.dumps(dataset)) class Client(models.Model): @@ -121,7 +109,8 @@ class InteractionManager(models.Manager): class Interaction(models.Model): - """Models each reconfiguration operation interaction between client and server.""" + """ Models each reconfiguration operation interaction between + client and server. """ client = models.ForeignKey(Client, related_name="interactions") timestamp = models.DateTimeField(db_index=True) # Timestamp for this record state = models.CharField(max_length=32) # good/bad/modified/etc @@ -137,8 +126,24 @@ class Interaction(models.Model): packages = models.ManyToManyField("PackageEntry") paths = models.ManyToManyField("PathEntry") services = models.ManyToManyField("ServiceEntry") + sebooleans = models.ManyToManyField("SEBooleanEntry") + seports = models.ManyToManyField("SEPortEntry") + sefcontexts = models.ManyToManyField("SEFcontextEntry") + senodes = models.ManyToManyField("SENodeEntry") + selogins = models.ManyToManyField("SELoginEntry") + seusers = models.ManyToManyField("SEUserEntry") + seinterfaces = models.ManyToManyField("SEInterfaceEntry") + sepermissives = models.ManyToManyField("SEPermissiveEntry") + semodules = models.ManyToManyField("SEModuleEntry") + posixusers = models.ManyToManyField("POSIXUserEntry") + posixgroups = models.ManyToManyField("POSIXGroupEntry") failures = models.ManyToManyField("FailureEntry") + entry_types = ('actions', 'packages', 'paths', 'services', 'sebooleans', + 'seports', 'sefcontexts', 'senodes', 'selogins', 'seusers', + 'seinterfaces', 'sepermissives', 'semodules', 'posixusers', + 'posixgroups') + # Formerly InteractionMetadata profile = models.ForeignKey("Group", related_name="+", null=True) groups = models.ManyToManyField("Group") @@ -157,7 +162,8 @@ class Interaction(models.Model): def percentbad(self): if not self.total_count == 0: - return ((self.total_count - self.good_count) / (float(self.total_count))) * 100 + return ((self.total_count - self.good_count) / + (float(self.total_count))) * 100 else: return 0 @@ -189,7 +195,8 @@ class Interaction(models.Model): self.client.save() # save again post update def delete(self): - '''Override the default delete. Allows us to remove Performance items''' + '''Override the default delete. Allows us to remove + Performance items ''' pitems = list(self.performance_items.all()) super(Interaction, self).delete() for perf in pitems: @@ -201,19 +208,19 @@ class Interaction(models.Model): def bad(self): rv = [] - for entry in ('actions', 'packages', 'paths', 'services'): + for entry in self.entry_types: rv.extend(getattr(self, entry).filter(state=TYPE_BAD)) return rv def modified(self): rv = [] - for entry in ('actions', 'packages', 'paths', 'services'): + for entry in self.entry_types: rv.extend(getattr(self, entry).filter(state=TYPE_MODIFIED)) return rv def extra(self): rv = [] - for entry in ('actions', 'packages', 'paths', 'services'): + for entry in self.entry_types: rv.extend(getattr(self, entry).filter(state=TYPE_EXTRA)) return rv @@ -325,7 +332,6 @@ class BaseEntry(models.Model): self.hash_key = hash_entry(self.__dict__) super(BaseEntry, self).save(*args, **kwargs) - def class_name(self): return self.__class__.__name__ @@ -333,7 +339,6 @@ class BaseEntry(models.Model): """todo""" return [] - @classmethod def entry_from_name(cls, name): try: @@ -344,28 +349,26 @@ class BaseEntry(models.Model): except KeyError: raise ValueError("Invalid type %s" % name) - @classmethod def entry_from_type(cls, etype): - for entry_cls in (ActionEntry, PackageEntry, PathEntry, ServiceEntry): + for entry_cls in ENTRY_CLASSES: if etype == entry_cls.ENTRY_TYPE: return entry_cls else: raise ValueError("Invalid type %s" % etype) - @classmethod def entry_get_or_create(cls, act_dict): """Helper to quickly lookup an object""" cls_name = cls().__class__.__name__ act_hash = hash_entry(act_dict) - + # TODO - get form cache and validate act_key = "%s_%s" % (cls_name, act_hash) newact = cache.get(act_key) if newact: return newact - + acts = cls.objects.filter(hash_key=act_hash) if len(acts) > 0: for act in acts: @@ -375,20 +378,18 @@ class BaseEntry(models.Model): #match found newact = act break - + # worst case, its new if not newact: newact = cls(**act_dict) newact.save(hash_key=act_hash) - + cache.set(act_key, newact, 60 * 60) return newact - def is_failure(self): return isinstance(self, FailureEntry) - @classmethod def prune_orphans(cls): '''Remove unused entries''' @@ -397,7 +398,7 @@ class BaseEntry(models.Model): for x in cls.objects.filter(interaction__isnull=True).values("id")] i = 0 while i < len(cls_orphans): - cls.objects.filter(id__in=cls_orphans[i:i+100]).delete() + cls.objects.filter(id__in=cls_orphans[i:i + 100]).delete() i += 100 @@ -439,13 +440,161 @@ class FailureEntry(BaseEntry): class ActionEntry(SuccessEntry): - """ The new model for package information """ + """ Action entry """ status = models.CharField(max_length=128, default="check") output = models.IntegerField(default=0) ENTRY_TYPE = r"Action" +class SEBooleanEntry(SuccessEntry): + """ SELinux boolean """ + value = models.BooleanField(default=True) + + ENTRY_TYPE = r"SEBoolean" + + +class SEPortEntry(SuccessEntry): + """ SELinux port """ + selinuxtype = models.CharField(max_length=128) + current_selinuxtype = models.CharField(max_length=128, null=True) + + ENTRY_TYPE = r"SEPort" + + def selinuxtype_problem(self): + """Check for an selinux type problem.""" + if not self.current_selinuxtype: + return True + return self.selinuxtype != self.current_selinuxtype + + def short_list(self): + """Return a list of problems""" + rv = super(SEPortEntry, self).short_list() + if self.selinuxtype_problem(): + rv.append("Wrong SELinux type") + return rv + + +class SEFcontextEntry(SuccessEntry): + """ SELinux file context """ + selinuxtype = models.CharField(max_length=128) + current_selinuxtype = models.CharField(max_length=128, null=True) + filetype = models.CharField(max_length=16) + + ENTRY_TYPE = r"SEFcontext" + + def selinuxtype_problem(self): + """Check for an selinux type problem.""" + if not self.current_selinuxtype: + return True + return self.selinuxtype != self.current_selinuxtype + + def short_list(self): + """Return a list of problems""" + rv = super(SEFcontextEntry, self).short_list() + if self.selinuxtype_problem(): + rv.append("Wrong SELinux type") + return rv + + +class SENodeEntry(SuccessEntry): + """ SELinux node """ + selinuxtype = models.CharField(max_length=128) + current_selinuxtype = models.CharField(max_length=128, null=True) + proto = models.CharField(max_length=4) + + ENTRY_TYPE = r"SENode" + + def selinuxtype_problem(self): + """Check for an selinux type problem.""" + if not self.current_selinuxtype: + return True + return self.selinuxtype != self.current_selinuxtype + + def short_list(self): + """Return a list of problems""" + rv = super(SENodeEntry, self).short_list() + if self.selinuxtype_problem(): + rv.append("Wrong SELinux type") + return rv + + +class SELoginEntry(SuccessEntry): + """ SELinux login """ + selinuxuser = models.CharField(max_length=128) + current_selinuxuser = models.CharField(max_length=128, null=True) + + ENTRY_TYPE = r"SELogin" + + +class SEUserEntry(SuccessEntry): + """ SELinux user """ + roles = models.CharField(max_length=128) + current_roles = models.CharField(max_length=128, null=True) + prefix = models.CharField(max_length=128) + current_prefix = models.CharField(max_length=128, null=True) + + ENTRY_TYPE = r"SEUser" + + +class SEInterfaceEntry(SuccessEntry): + """ SELinux interface """ + selinuxtype = models.CharField(max_length=128) + current_selinuxtype = models.CharField(max_length=128, null=True) + + ENTRY_TYPE = r"SEInterface" + + def selinuxtype_problem(self): + """Check for an selinux type problem.""" + if not self.current_selinuxtype: + return True + return self.selinuxtype != self.current_selinuxtype + + def short_list(self): + """Return a list of problems""" + rv = super(SEInterfaceEntry, self).short_list() + if self.selinuxtype_problem(): + rv.append("Wrong SELinux type") + return rv + + +class SEPermissiveEntry(SuccessEntry): + """ SELinux permissive domain """ + ENTRY_TYPE = r"SEPermissive" + + +class SEModuleEntry(SuccessEntry): + """ SELinux module """ + disabled = models.BooleanField(default=False) + current_disabled = models.BooleanField(default=False) + + ENTRY_TYPE = r"SEModule" + + +class POSIXUserEntry(SuccessEntry): + """ POSIX user """ + uid = models.IntegerField(null=True) + current_uid = models.IntegerField(null=True) + group = models.CharField(max_length=64) + current_group = models.CharField(max_length=64, null=True) + gecos = models.CharField(max_length=1024) + current_gecos = models.CharField(max_length=1024, null=True) + home = models.CharField(max_length=1024) + current_home = models.CharField(max_length=1024, null=True) + shell = models.CharField(max_length=1024, default='/bin/bash') + current_shell = models.CharField(max_length=1024, null=True) + + ENTRY_TYPE = r"POSIXUser" + + +class POSIXGroupEntry(SuccessEntry): + """ POSIX group """ + gid = models.IntegerField(null=True) + current_gid = models.IntegerField(null=True) + + ENTRY_TYPE = r"POSIXGroup" + + class PackageEntry(SuccessEntry): """ The new model for package information """ @@ -455,7 +604,7 @@ class PackageEntry(SuccessEntry): verification_details = models.TextField(default="") ENTRY_TYPE = r"Package" - #TODO - prune + # TODO - prune def version_problem(self): """Check for a version problem.""" @@ -612,3 +761,7 @@ class ServiceEntry(SuccessEntry): return rv +ENTRY_TYPES = (ActionEntry, PackageEntry, PathEntry, ServiceEntry, + SEBooleanEntry, SEPortEntry, SEFcontextEntry, SENodeEntry, + SELoginEntry, SEUserEntry, SEInterfaceEntry, SEPermissiveEntry, + SEModuleEntry) diff --git a/src/lib/Bcfg2/Reporting/templates/base.html b/src/lib/Bcfg2/Reporting/templates/base.html index 533dcc79e..c73339911 100644 --- a/src/lib/Bcfg2/Reporting/templates/base.html +++ b/src/lib/Bcfg2/Reporting/templates/base.html @@ -88,7 +88,7 @@ <div style='clear:both'></div> </div><!-- document --> <div id="footer"> - <span>Bcfg2 Version 1.3.0rc2</span> + <span>Bcfg2 Version 1.3.1</span> </div> <div id="calendar_div" style='position:absolute; visibility:hidden; background-color:white; layer-background-color:white;'></div> diff --git a/src/lib/Bcfg2/Reporting/templates/config_items/item.html b/src/lib/Bcfg2/Reporting/templates/config_items/item.html index 737760252..259414399 100644 --- a/src/lib/Bcfg2/Reporting/templates/config_items/item.html +++ b/src/lib/Bcfg2/Reporting/templates/config_items/item.html @@ -45,36 +45,47 @@ div.entry_list h3 { {% endif %} {# Really need a better test here #} -{% if item.mdoe_problem or item.status_problem or item.linkentry.link_problem or item.version_problem %} +{% if item.mode_problem or item.status_problem or item.linkentry.link_problem or item.version_problem %} <table class='entry_list'> <tr id='table_list_header'> <td style='text-align: right;'>Problem Type</td><td>Expected</td><td style='border-bottom: 1px solid #98DBCC;'>Found</td></tr> {% if item.mode_problem %} {% if item.current_perms.owner %} - <tr><td style='text-align: right'><b>Owner</b></td><td>{{item.target_perms.owner}}</td> + <tr><td style='text-align: right'><b>Owner</b></td> + <td>{{item.target_perms.owner}}</td> <td>{{item.current_perms.owner}}</td></tr> {% endif %} {% if item.current_perms.group %} - <tr><td style='text-align: right'><b>Group</b></td><td>{{item.target_perms.group}}</td> + <tr><td style='text-align: right'><b>Group</b></td> + <td>{{item.target_perms.group}}</td> <td>{{item.current_perms.group}}</td></tr> {% endif %} {% if item.current_perms.mode%} - <tr><td style='text-align: right'><b>Mode</b></td><td>{{item.target_perms.mode}}</td> + <tr><td style='text-align: right'><b>Permissions</b> + </td><td>{{item.target_perms.mode}}</td> <td>{{item.current_perms.mode}}</td></tr> {% endif %} {% endif %} {% if item.status_problem %} - <tr><td style='text-align: right'><b>Status</b></td><td>{{item.target_status}}</td> - <td>{{item.current_status}}</td></tr> + <tr><td style='text-align: right'><b>Status</b></td> + <td>{{item.target_status}}</td> + <td>{{item.current_status}}</td></tr> {% endif %} {% if item.linkentry.link_problem %} - <tr><td style='text-align: right'><b>{{item.get_path_type_display}}</b></td><td>{{item.linkentry.target_path}}</td> - <td>{{item.linkentry.current_path}}</td></tr> + <tr><td style='text-align: right'><b>{{item.get_path_type_display}}</b></td> + <td>{{item.linkentry.target_path}}</td> + <td>{{item.linkentry.current_path}}</td></tr> {% endif %} {% if item.version_problem %} - <tr><td style='text-align: right'><b>Package Version</b></td><td>{{item.target_version|cut:"("|cut:")"}}</td> + <tr><td style='text-align: right'><b>Package Version</b></td> + <td>{{item.target_version|cut:"("|cut:")"}}</td> <td>{{item.current_version|cut:"("|cut:")"}}</td></tr> {% endif %} + {% if item.selinuxtype_problem %} + <tr><td style='text-align: right'><b>SELinux Type</b></td> + <td>{{item.selinuxtype}}</td> + <td>{{item.current_selinuxtype}}</td></tr> + {% endif %} </table> {% endif %} @@ -92,7 +103,7 @@ div.entry_list h3 { {{ item.details|syntaxhilight }} </div> {% else %} - {{ item.details }} + {{ item.details }} {% endif %} </div> {% endif %} diff --git a/src/lib/Bcfg2/Reporting/templates/widgets/filter_bar.html b/src/lib/Bcfg2/Reporting/templates/widgets/filter_bar.html index 759415507..bb4f650d1 100644 --- a/src/lib/Bcfg2/Reporting/templates/widgets/filter_bar.html +++ b/src/lib/Bcfg2/Reporting/templates/widgets/filter_bar.html @@ -16,7 +16,7 @@ <label for="id_group">Group filter:</label> <select id="id_group" name="group" onchange="javascript:url=document.forms['filter_form'].group.value; if(url) { location.href=url }"> {% for group, group_url, selected in groups %} - <option label="{{group}}" value="{{group_url}}" {% if selected %}selected {% endif %}/> + <option value="{{group_url}}" {% if selected %}selected {% endif %}>{{group}}</option> {% endfor %} </select> {% endif %} diff --git a/src/lib/Bcfg2/Reporting/views.py b/src/lib/Bcfg2/Reporting/views.py index 0341a18af..6cba7bf8c 100644 --- a/src/lib/Bcfg2/Reporting/views.py +++ b/src/lib/Bcfg2/Reporting/views.py @@ -161,7 +161,7 @@ def config_item(request, pk, entry_type, interaction=None): ts_end = ts_start + timedelta(days=1) associated_list = item.interaction_set.select_related('client').filter(\ timestamp__gte=ts_start, timestamp__lt=ts_end) - + if item.is_failure(): template = 'config_items/item-failure.html' else: @@ -184,7 +184,7 @@ def config_item_list(request, item_state, timestamp=None, **kwargs): current_clients = [q['id'] for q in _handle_filters(current_clients, **kwargs).values('id')] lists = [] - for etype in ActionEntry, PackageEntry, PathEntry, ServiceEntry: + for etype in ENTRY_TYPES: ldata = etype.objects.filter(state=state, interaction__in=current_clients)\ .annotate(num_entries=Count('id')).select_related('linkentry', 'target_perms', 'current_perms') if len(ldata) > 0: @@ -218,7 +218,7 @@ def entry_status(request, entry_type, pk, timestamp=None, **kwargs): if it.pk not in seen: items.append((it, it.interaction_set.filter(pk__in=current_clients).order_by('client__name').select_related('client'))) seen.append(it.pk) - + return render_to_response('config_items/entry_status.html', {'entry': item, 'items': items, @@ -254,8 +254,8 @@ def common_problems(request, timestamp=None, threshold=None, group=None): else: current_clients = Interaction.objects.recent_ids(timestamp) lists = [] - for etype in ActionEntry, PackageEntry, PathEntry, ServiceEntry: - ldata = etype.objects.exclude(state=TYPE_GOOD).filter( + for etype in ENTRY_TYPES: + ldata = etype.objects.exclude(state=TYPE_GOOD).filter( interaction__in=current_clients).annotate(num_entries=Count('id')).filter(num_entries__gte=threshold)\ .order_by('-num_entries', 'name') if len(ldata) > 0: @@ -315,7 +315,8 @@ def client_detailed_list(request, timestamp=None, **kwargs): kwargs['orderby'] = "client__name" kwargs['sort'] = "client" - kwargs['interaction_base'] = Interaction.objects.recent(timestamp).select_related() + kwargs['interaction_base'] = \ + Interaction.objects.recent(timestamp).select_related() kwargs['page_limit'] = 0 return render_history_view(request, 'clients/detailed-list.html', **kwargs) @@ -330,16 +331,18 @@ def client_detail(request, hostname=None, pk=None): inter = client.interactions.get(pk=pk) maxdate = inter.timestamp - etypes = { TYPE_BAD: 'bad', TYPE_MODIFIED: 'modified', TYPE_EXTRA: 'extra' } + etypes = {TYPE_BAD: 'bad', + TYPE_MODIFIED: 'modified', + TYPE_EXTRA: 'extra'} edict = dict() for label in etypes.values(): edict[label] = [] - for ekind in ('actions', 'packages', 'paths', 'services'): + for ekind in inter.entry_types: for ent in getattr(inter, ekind).all(): edict[etypes[ent.state]].append(ent) context['entry_types'] = edict - context['interaction']=inter + context['interaction'] = inter return render_history_view(request, 'clients/detail.html', page_limit=5, client=client, maxdate=maxdate, context=context) @@ -356,7 +359,8 @@ def client_manage(request): client.expiration = datetime.now() client.save() message = "Expiration for %s set to %s." % \ - (client_name, client.expiration.strftime("%Y-%m-%d %H:%M:%S")) + (client_name, + client.expiration.strftime("%Y-%m-%d %H:%M:%S")) elif client_action == 'unexpire': client.expiration = None client.save() diff --git a/src/lib/Bcfg2/Server/Admin/Init.py b/src/lib/Bcfg2/Server/Admin/Init.py index 724da124b..884405786 100644 --- a/src/lib/Bcfg2/Server/Admin/Init.py +++ b/src/lib/Bcfg2/Server/Admin/Init.py @@ -12,6 +12,7 @@ from Bcfg2.Utils import Executor import Bcfg2.Server.Admin import Bcfg2.Server.Plugin import Bcfg2.Options +import Bcfg2.Server.Plugins.Metadata from Bcfg2.Compat import input # pylint: disable=W0622 # default config file @@ -171,8 +172,6 @@ class Init(Bcfg2.Server.Admin.Mode): self.data['certpath'] = os.path.join(basepath, 'bcfg2.crt') def __call__(self, args): - Bcfg2.Server.Admin.Mode.__call__(self, args) - # Parse options setup = Bcfg2.Options.get_option_parser() setup.add_options(dict(configfile=Bcfg2.Options.CFILE, @@ -218,7 +217,7 @@ class Init(Bcfg2.Server.Admin.Mode): """Ask for the repository path.""" while True: newrepo = safe_input("Location of Bcfg2 repository [%s]: " % - self.data['repopath']) + self.data['repopath']) if newrepo != '': self.data['repopath'] = os.path.abspath(newrepo) if os.path.isdir(self.data['repopath']): @@ -296,7 +295,7 @@ class Init(Bcfg2.Server.Admin.Mode): "created [%s]: " % self.data['keypath']) if keypath: self.data['keypath'] = keypath - certpath = safe_input("Path where Bcfg2 server cert will be created" + certpath = safe_input("Path where Bcfg2 server cert will be created " "[%s]: " % self.data['certpath']) if certpath: self.data['certpath'] = certpath @@ -324,6 +323,16 @@ class Init(Bcfg2.Server.Admin.Mode): def init_repo(self): """Setup a new repo and create the content of the configuration file.""" + # Create the repository + path = os.path.join(self.data['repopath'], 'etc') + try: + os.makedirs(path) + self._init_plugins() + print("Repository created successfuly in %s" % + self.data['repopath']) + except OSError: + print("Failed to create %s." % path) + confdata = CONFIG % (self.data['repopath'], ','.join(self.plugins), self.data['sendmail'], @@ -339,13 +348,3 @@ class Init(Bcfg2.Server.Admin.Mode): create_key(self.data['shostname'], self.data['keypath'], self.data['certpath'], self.data['country'], self.data['state'], self.data['location']) - - # Create the repository - path = os.path.join(self.data['repopath'], 'etc') - try: - os.makedirs(path) - self._init_plugins() - print("Repository created successfuly in %s" % - self.data['repopath']) - except OSError: - print("Failed to create %s." % path) diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 07f9e0588..c69e8b055 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -4,8 +4,9 @@ implementations inherit from. """ import os import sys import time -import select import atexit +import select +import signal import logging import inspect import threading @@ -119,13 +120,36 @@ 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 self.setup['debug']: + self.set_core_debug(None, setup['debug']) + if 'ignore' not in self.setup: self.setup.add_option('ignore', SERVER_FAM_IGNORE) self.setup.reparse() + famargs = dict(filemonitor=self.setup['filemonitor'], debug=self.setup['debug'], ignore=self.setup['ignore']) - if self.setup['filemonitor'] not in Bcfg2.Server.FileMonitor.available: + try: + filemonitor = \ + Bcfg2.Server.FileMonitor.available[setup['filemonitor']] + except KeyError: self.logger.error("File monitor driver %s not available; " "forcing to default" % self.setup['filemonitor']) famargs['filemonitor'] = 'default' @@ -281,6 +305,14 @@ class BaseCore(object): #: The CA that signed the server cert self.ca = self.setup['ca'] + def hdlr(sig, frame): # pylint: disable=W0613 + """ Handle SIGINT/Ctrl-C by shutting down the core and exiting + properly. """ + self.shutdown() + os._exit(1) # pylint: disable=W0212 + + signal.signal(signal.SIGINT, hdlr) + #: The FAM :class:`threading.Thread`, #: :func:`_file_monitor_thread` self.fam_thread = \ @@ -295,6 +327,10 @@ 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. @@ -392,6 +428,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() @@ -427,6 +464,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 \ @@ -460,6 +499,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: @@ -486,6 +526,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) @@ -506,6 +547,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'] @@ -528,6 +570,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) @@ -544,6 +587,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:] @@ -619,6 +665,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) @@ -718,6 +765,12 @@ class BaseCore(object): self.shutdown() raise + if self.setup['fam_blocking']: + time.sleep(1) + while self.fam.pending() != 0: + time.sleep(1) + + self.set_debug(None, self.debug_flag) self._block() def _daemonize(self): @@ -746,6 +799,7 @@ class BaseCore(object): :type mode: string :returns: list of Decision tuples ``(<entry tag>, <entry name>)`` """ + self.logger.debug("Getting decision list for %s" % metadata.hostname) result = [] for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.Decision): try: @@ -816,6 +870,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) @@ -837,6 +892,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': @@ -907,10 +963,12 @@ class BaseCore(object): def _get_rmi(self): """ Get a list of RMI calls exposed by plugins """ rmi = dict() - if self.plugins: - for pname, pinst in list(self.plugins.items()): - for mname in pinst.__rmi__: - rmi["%s.%s" % (pname, mname)] = getattr(pinst, mname) + for pname, pinst in list(self.plugins.items()): + for mname in pinst.__rmi__: + rmi["%s.%s" % (pname, mname)] = getattr(pinst, mname) + famname = self.fam.__class__.__name__ + for mname in self.fam.__rmi__: + rmi["%s.%s" % (famname, mname)] = getattr(self.fam, mname) return rmi def _resolve_exposed_method(self, method_name): @@ -964,6 +1022,7 @@ class BaseCore(object): return func.__doc__ @exposed + @track_statistics() def DeclareVersion(self, address, version): """ Declare the client version. @@ -974,7 +1033,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, @@ -996,6 +1057,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): @@ -1017,6 +1079,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 @@ -1063,6 +1126,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, @@ -1171,22 +1235,35 @@ 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_fam_debug(self, _): + def toggle_core_debug(self, address): + """ Toggle debug status of the server core + + :param address: Client (address, hostname) pair + :type address: tuple + :returns: bool - The new debug state of the FAM + """ + return self.set_core_debug(address, not self.debug_flag) + + @exposed + def toggle_fam_debug(self, address): """ Toggle debug status of the FAM :returns: bool - The new debug state of the FAM """ - return self.fam.toggle_debug() + self.logger.warning("Deprecated method set_fam_debug called by %s" % + address[0]) + return "This method is deprecated and will be removed in a future " + \ + "release\n%s" % self.fam.toggle_debug() @exposed 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) @@ -1197,10 +1274,33 @@ 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.fam.set_debug(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.name, levels['default']) + self.logger.debug("Setting %s log handler to %s" % + (handler.name, logging.getLevelName(level))) + handler.setLevel(level) + return self.debug_flag @exposed - def set_fam_debug(self, _, debug): + def set_fam_debug(self, address, debug): """ Explicitly set debug status of the FAM :param debug: The new debug status of the FAM. This can @@ -1212,4 +1312,7 @@ class BaseCore(object): """ if debug not in [True, False]: debug = debug.lower() == "true" - return self.fam.set_debug(debug) + self.logger.warning("Deprecated method set_fam_debug called by %s" % + address[0]) + return "This method is deprecated and will be removed in a future " + \ + "release\n%s" % self.fam.set_debug(debug) diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py index 178a47b1a..cdd52dbb9 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py @@ -2,6 +2,7 @@ support. """ import os +import errno import logging import pyinotify from Bcfg2.Compat import reduce # pylint: disable=W0622 @@ -15,6 +16,8 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): """ File monitor backend with `inotify <http://inotify.aiken.cz/>`_ support. """ + __rmi__ = Pseudo.__rmi__ + ["list_watches", "list_paths"] + #: Inotify is the best FAM backend, so it gets a very high #: priority __priority__ = 99 @@ -182,6 +185,9 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): try: watchdir = self.watches_by_path[watch_path] except KeyError: + if not os.path.exists(watch_path): + raise OSError(errno.ENOENT, + "No such file or directory: '%s'" % path) watchdir = self.watchmgr.add_watch(watch_path, self.mask, quiet=False)[watch_path] self.watches_by_path[watch_path] = watchdir @@ -211,3 +217,20 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): if self.notifier: self.notifier.stop() shutdown.__doc__ = Pseudo.shutdown.__doc__ + + def list_watches(self): + """ XML-RPC that returns a list of current inotify watches for + debugging purposes. """ + return list(self.watches_by_path.keys()) + + def list_paths(self): + """ XML-RPC that returns a list of paths that are handled for + debugging purposes. Because inotify doesn't like watching + files, but prefers to watch directories, this will be + different from + :func:`Bcfg2.Server.FileMonitor.Inotify.Inotify.ListWatches`. For + instance, if a plugin adds a monitor to + ``/var/lib/bcfg2/Plugin/foo.xml``, :func:`ListPaths` will + return ``/var/lib/bcfg2/Plugin/foo.xml``, while + :func:`ListWatches` will return ``/var/lib/bcfg2/Plugin``. """ + return list(self.handles.keys()) diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py index d77f21b93..522ddb705 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/__init__.py +++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py @@ -116,6 +116,9 @@ class FileMonitor(Debuggable): #: should have higher priorities. __priority__ = -1 + #: List of names of methods to be exposed as XML-RPC functions + __rmi__ = Debuggable.__rmi__ + ["list_event_handlers"] + def __init__(self, ignore=None, debug=False): """ :param ignore: A list of filename globs describing events that @@ -288,6 +291,8 @@ class FileMonitor(Debuggable): def shutdown(self): """ Handle any tasks required to shut down the monitor. """ + self.debug_log("Shutting down %s file monitor" % + self.__class__.__name__) self.started = False def AddMonitor(self, path, obj, handleID=None): @@ -310,6 +315,15 @@ class FileMonitor(Debuggable): """ raise NotImplementedError + def list_event_handlers(self): + """ XML-RPC that returns + :attr:`Bcfg2.Server.FileMonitor.FileMonitor.handles` for + debugging purposes. """ + rv = dict() + for watch, handler in self.handles.items(): + rv[watch] = getattr(handler, "name", handler.__class__.__name__) + return rv + #: A module-level FAM object that all plugins, etc., can use. This #: should not be used directly, but retrieved via :func:`get_fam`. diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py index 60525d5a1..497e8fac6 100644 --- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py +++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py @@ -37,7 +37,7 @@ def is_octal_mode(val): def is_username(val): """ Return True if val is a string giving either a positive integer uid, or a valid Unix username """ - return re.match(r'^([a-z]\w{0,30}|\d+)$', val) + return re.match(r'^([A-z][-_A-z0-9]{0,30}|\d+)$', val) def is_device_mode(val): diff --git a/src/lib/Bcfg2/Server/Plugin/base.py b/src/lib/Bcfg2/Server/Plugin/base.py index 25a687874..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 @@ -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): diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py index 187c594fd..ded7dd8dc 100644 --- a/src/lib/Bcfg2/Server/Plugin/helpers.py +++ b/src/lib/Bcfg2/Server/Plugin/helpers.py @@ -478,6 +478,7 @@ class XMLFileBacked(FileBacked): def Index(self): self.xdata = lxml.etree.XML(self.data, base_url=self.name, parser=Bcfg2.Server.XMLParser) + self.extras = [] self._follow_xincludes() if self.extras: try: diff --git a/src/lib/Bcfg2/Server/Plugin/interfaces.py b/src/lib/Bcfg2/Server/Plugin/interfaces.py index 3ef29775d..11a61ff9c 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/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py index 5f10879be..b3781e299 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py @@ -77,6 +77,10 @@ class CfgGenshiGenerator(CfgGenerator): __init__.__doc__ = CfgGenerator.__init__.__doc__ def get_data(self, entry, metadata): + if self.template is None: + raise PluginExecutionError("Failed to load template %s" % + self.name) + fname = entry.get('realname', entry.get('name')) stream = self.template.generate( name=fname, 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/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index b053e65d3..7f8db7b6d 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -138,7 +138,7 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): self.logger.error('Failed to parse %s' % self.basefile) return self.extras = [] - self.basedata = copy.copy(xdata) + self.basedata = copy.deepcopy(xdata) self._follow_xincludes(xdata=xdata) if self.extras: try: @@ -263,31 +263,63 @@ class ClientMetadata(object): # pylint: disable=R0913 def __init__(self, client, profile, groups, bundles, aliases, addresses, categories, uuid, password, version, query): + #: The client hostname (as a string) self.hostname = client + + #: The client profile (as a string) self.profile = profile + + #: The set of all bundles this client gets self.bundles = bundles + + #: A list of all client aliases self.aliases = aliases + + #: A list of all addresses this client is known by self.addresses = addresses + + #: A list of groups this client is a member of self.groups = groups + + #: A dict of categories of this client's groups. Keys are + #: category names, values are corresponding group names. self.categories = categories + + #: The UUID identifier for this client self.uuid = uuid + + #: The Bcfg2 password for this client self.password = password + + #: Connector plugins known to this client self.connectors = [] + + #: The version of the Bcfg2 client this client is running, as + #: a string self.version = version try: + #: The version of the Bcfg2 client this client is running, + #: as a :class:`Bcfg2.version.Bcfg2VersionInfo` object. self.version_info = Bcfg2VersionInfo(version) except (ValueError, AttributeError): self.version_info = None + + #: A :class:`Bcfg2.Server.Plugins.Metadata.MetadataQuery` + #: object for this client. self.query = query # pylint: enable=R0913 def inGroup(self, group): - """Test to see if client is a member of group.""" + """Test to see if client is a member of group. + + :returns: bool """ return group in self.groups def group_in_category(self, category): - """ return the group in the given category that the client is - a member of, or the empty string """ + """ Return the group in the given category that the client is + a member of, or an empty string. + + :returns: string """ for grp in self.query.all_groups_in_category(category): if grp in self.groups: return grp @@ -295,17 +327,59 @@ class ClientMetadata(object): class MetadataQuery(object): - """ object supplied to client metadata to allow client metadata - objects to query metadata without being able to modify it """ + """ This class provides query methods for the metadata of all + clients known to the Bcfg2 server, without being able to modify + that data. + + Note that ``*by_groups()`` and ``*by_profiles()`` behave + differently; for a client to be included in the return value of a + ``*by_groups()`` method, it must be a member of *all* groups + listed in the argument; for a client to be included in the return + value of a ``*by_profiles()`` method, it must have *any* group + listed as its profile group. """ def __init__(self, by_name, get_clients, by_groups, by_profiles, all_groups, all_groups_in_category): - # resolver is set later + #: Get :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` + #: object for the given hostname. + #: + #: :returns: Bcfg2.Server.Plugins.Metadata.ClientMetadata self.by_name = by_name + + #: Get a list of hostnames of clients that are in all given + #: groups. + #: + #: :param groups: The groups to check clients for membership in + #: :type groups: list + #: + #: :returns: list of strings self.names_by_groups = self._warn_string(by_groups) + + #: Get a list of hostnames of clients whose profile matches + #: any given profile group. + #: + #: :param profiles: The profiles to check clients for + #: membership in. + #: :type profiles: list + #: :returns: list of strings self.names_by_profiles = self._warn_string(by_profiles) + + #: Get all known client hostnames. + #: + #: :returns: list of strings self.all_clients = get_clients + + #: Get all known group names. + #: + #: :returns: list of strings self.all_groups = all_groups + + #: Get the names of all groups in the given category. + #: + #: :param category: The category to query for groups that + #: belong to it. + #: :type category: string + #: :returns: list of strings self.all_groups_in_category = all_groups_in_category def _warn_string(self, func): @@ -326,22 +400,41 @@ class MetadataQuery(object): return inner def by_groups(self, groups): - """ get a list of ClientMetadata objects that are in all given - groups """ + """ Get a list of + :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` objects + that are in all given groups. + + :param groups: The groups to check clients for membership in. + :type groups: list + :returns: list of Bcfg2.Server.Plugins.Metadata.ClientMetadata + objects + """ # don't need to decorate this with _warn_string because # names_by_groups is decorated return [self.by_name(name) for name in self.names_by_groups(groups)] def by_profiles(self, profiles): - """ get a list of ClientMetadata objects that are in any of - the given profiles """ + """ Get a list of + :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` objects + that have any of the given groups as their profile. + + :param profiles: The profiles to check clients for membership + in. + :type profiles: list + :returns: list of Bcfg2.Server.Plugins.Metadata.ClientMetadata + objects + """ # don't need to decorate this with _warn_string because # names_by_profiles is decorated return [self.by_name(name) for name in self.names_by_profiles(profiles)] def all(self): - """ get a list of all ClientMetadata objects """ + """ Get a list of all + :class:`Bcfg2.Server.Plugins.Metadata.ClientMetadata` objects. + + :returns: list of Bcfg2.Server.Plugins.Metadata.ClientMetadata + """ return [self.by_name(name) for name in self.all_clients()] @@ -1255,7 +1348,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): diff --git a/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py b/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py index 0dd42c9cb..490ee6f20 100644 --- a/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py +++ b/src/lib/Bcfg2/Server/Plugins/POSIXCompat.py @@ -15,6 +15,11 @@ class POSIXCompat(Bcfg2.Server.Plugin.Plugin, def validate_goals(self, metadata, goals): """Verify that we are generating correct old POSIX entries.""" + if metadata.version_info and metadata.version_info > (1, 3, 0, '', 0): + # do not care about a client that is _any_ 1.3.0 release + # (including prereleases and RCs) + return + for goal in goals: for entry in goal.getchildren(): if entry.tag == 'Path' and 'mode' in entry.keys(): diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py index 3799b1723..4535fb76d 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py @@ -313,9 +313,7 @@ class YumCollection(Collection): @property def __package_groups__(self): - """ YumCollections support package groups only if - :attr:`use_yum` is True """ - return self.use_yum + return True @property def helper(self): @@ -663,11 +661,6 @@ class YumCollection(Collection): In this implementation the packages may be strings or tuples. See :ref:`yum-pkg-objects` for more information. """ - if not self.use_yum: - self.logger.warning("Packages: Package groups are not supported " - "by Bcfg2's internal Yum dependency generator") - return dict() - if not grouplist: return dict() @@ -679,7 +672,15 @@ class YumCollection(Collection): ptype = "default" gdicts.append(dict(group=group, type=ptype)) - return self.call_helper("get_groups", inputdata=gdicts) + if self.use_yum: + return self.call_helper("get_groups", inputdata=gdicts) + else: + pkgs = dict() + for gdict in gdicts: + pkgs[gdict['group']] = Collection.get_group(self, + gdict['group'], + gdict['type']) + return pkgs def _element_to_pkg(self, el, name): """ Convert a Package or Instance element to a package tuple """ @@ -975,6 +976,7 @@ class YumSource(Source): for x in ['global'] + self.arches]) self.needed_paths = set() self.file_to_arch = dict() + self.yumgroups = dict() __init__.__doc__ = Source.__init__.__doc__ @property @@ -992,7 +994,8 @@ class YumSource(Source): if not self.use_yum: cache = open(self.cachefile, 'wb') cPickle.dump((self.packages, self.deps, self.provides, - self.filemap, self.url_map), cache, 2) + self.filemap, self.url_map, + self.yumgroups), cache, 2) cache.close() def load_state(self): @@ -1002,7 +1005,7 @@ class YumSource(Source): if not self.use_yum: data = open(self.cachefile) (self.packages, self.deps, self.provides, - self.filemap, self.url_map) = cPickle.load(data) + self.filemap, self.url_map, self.yumgroups) = cPickle.load(data) @property def urls(self): @@ -1057,7 +1060,7 @@ class YumSource(Source): urls = [] for elt in xdata.findall(RPO + 'data'): - if elt.get('type') in ['filelists', 'primary']: + if elt.get('type') in ['filelists', 'primary', 'group']: floc = elt.find(RPO + 'location') fullurl = url + floc.get('href') urls.append(fullurl) @@ -1074,11 +1077,14 @@ class YumSource(Source): # we have to read primary.xml first, and filelists.xml afterwards; primaries = list() filelists = list() + groups = list() for fname in self.files: if fname.endswith('primary.xml.gz'): primaries.append(fname) elif fname.endswith('filelists.xml.gz'): filelists.append(fname) + elif fname.find('comps'): + groups.append(fname) for fname in primaries: farch = self.file_to_arch[fname] @@ -1088,6 +1094,9 @@ class YumSource(Source): farch = self.file_to_arch[fname] fdata = lxml.etree.parse(fname).getroot() self.parse_filelist(fdata, farch) + for fname in groups: + fdata = lxml.etree.parse(fname).getroot() + self.parse_group(fdata) # merge data sdata = list(self.packages.values()) @@ -1151,6 +1160,35 @@ class YumSource(Source): self.provides[arch][prov] = list() self.provides[arch][prov].append(pkgname) + @Bcfg2.Server.Plugin.track_statistics() + def parse_group(self, data): + """ parse comps.xml.gz data """ + for group in data.getchildren(): + if not group.tag.endswith('group'): + continue + try: + groupid = group.xpath('id')[0].text + self.yumgroups[groupid] = {'mandatory': list(), + 'default': list(), + 'optional': list(), + 'conditional': list()} + except IndexError: + continue + try: + packagelist = group.xpath('packagelist')[0] + except IndexError: + continue + for pkgreq in packagelist.getchildren(): + pkgtype = pkgreq.get('type', None) + if pkgtype == 'mandatory': + self.yumgroups[groupid]['mandatory'].append(pkgreq.text) + elif pkgtype == 'default': + self.yumgroups[groupid]['default'].append(pkgreq.text) + elif pkgtype == 'optional': + self.yumgroups[groupid]['optional'].append(pkgreq.text) + elif pkgtype == 'conditional': + self.yumgroups[groupid]['conditional'].append(pkgreq.text) + def is_package(self, metadata, package): arch = [a for a in self.arches if a in metadata.groups] if not arch: @@ -1230,3 +1268,26 @@ class YumSource(Source): return self.pulp_id else: return Source.get_repo_name(self, url_map) + + def get_group(self, metadata, group, ptype=None): # pylint: disable=W0613 + """ Get the list of packages of the given type in a package + group. + + :param group: The name of the group to query + :type group: string + :param ptype: The type of packages to get, for backends that + support multiple package types in package groups + (e.g., "recommended," "optional," etc.) + :type ptype: string + :returns: list of strings - package names + """ + try: + yumgroup = self.yumgroups[group] + except KeyError: + return [] + packages = yumgroup['conditional'] + yumgroup['mandatory'] + if ptype in ['default', 'optional', 'all']: + packages += yumgroup['default'] + if ptype in ['optional', 'all']: + packages += yumgroup['optional'] + return packages diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py index 5d3fbae2e..2175cf0aa 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py @@ -5,6 +5,7 @@ determine the completeness of the client configuration. """ import os import sys import glob +import copy import shutil import lxml.etree import Bcfg2.Logger @@ -296,20 +297,22 @@ class Packages(Bcfg2.Server.Plugin.Plugin, """ if self.disableResolver: # Config requests no resolver + for struct in structures: + for pkg in struct.xpath('//Package | //BoundPackage'): + if pkg.get("group"): + if pkg.get("type"): + pkg.set("choose", pkg.get("type")) return if collection is None: collection = self.get_collection(metadata) - # base is the set of initial packages -- explicitly - # given in the specification, from expanded package groups, - # and essential to the distribution - base = set() + initial = set() to_remove = [] groups = [] for struct in structures: for pkg in struct.xpath('//Package | //BoundPackage'): if pkg.get("name"): - base.update(collection.packages_from_entry(pkg)) + initial.update(collection.packages_from_entry(pkg)) elif pkg.get("group"): groups.append((pkg.get("group"), pkg.get("type"))) @@ -321,6 +324,11 @@ class Packages(Bcfg2.Server.Plugin.Plugin, pkg, xml_declaration=False).decode('UTF-8')) + # base is the set of initial packages explicitly given in the + # specification, packages from expanded package groups, and + # packages essential to the distribution + base = set(initial) + # remove package groups for el in to_remove: el.getparent().remove(el) @@ -336,7 +344,7 @@ class Packages(Bcfg2.Server.Plugin.Plugin, if unknown: self.logger.info("Packages: Got %d unknown entries" % len(unknown)) self.logger.info("Packages: %s" % list(unknown)) - newpkgs = collection.get_new_packages(base, packages) + newpkgs = collection.get_new_packages(initial, packages) self.debug_log("Packages: %d base, %d complete, %d new" % (len(base), len(packages), len(newpkgs))) newpkgs.sort() @@ -514,7 +522,8 @@ class Packages(Bcfg2.Server.Plugin.Plugin, :return: dict of lists of ``url_map`` data """ collection = self.get_collection(metadata) - return dict(sources=collection.get_additional_data()) + return dict(sources=collection.get_additional_data(), + allsources=copy.deepcopy(self.sources)) def end_client_run(self, metadata): """ Hook to clear the cache for this client in diff --git a/src/lib/Bcfg2/Server/Plugins/Reporting.py b/src/lib/Bcfg2/Server/Plugins/Reporting.py index d072f1a33..a6dc2c1ef 100644 --- a/src/lib/Bcfg2/Server/Plugins/Reporting.py +++ b/src/lib/Bcfg2/Server/Plugins/Reporting.py @@ -65,10 +65,13 @@ class Reporting(Statistics, Threaded, PullSource, Debuggable): (self.name, traceback.format_exc().splitlines()[-1]) self.logger.error(msg) raise PluginInitError(msg) + if self.debug_flag: + self.transport.set_debug(self.debug_flag) def set_debug(self, debug): rv = Debuggable.set_debug(self, debug) - self.transport.set_debug(debug) + if self.transport is not None: + self.transport.set_debug(debug) return rv def process_statistics(self, client, xdata): diff --git a/src/lib/Bcfg2/Server/SSLServer.py b/src/lib/Bcfg2/Server/SSLServer.py index 0d1246d85..28450aa1a 100644 --- a/src/lib/Bcfg2/Server/SSLServer.py +++ b/src/lib/Bcfg2/Server/SSLServer.py @@ -412,12 +412,9 @@ class XMLRPCServer(SocketServer.ThreadingMixIn, SSLServer, name = instance.name except AttributeError: name = "unknown" - if hasattr(instance, 'plugins'): - for pname, pinst in list(instance.plugins.items()): - for mname in pinst.__rmi__: - xmname = "%s.%s" % (pname, mname) - fn = getattr(pinst, mname) - self.register_function(fn, name=xmname) + if hasattr(instance, '_get_rmi'): + for fname, func in instance._get_rmi().items(): + self.register_function(func, name=fname) self.logger.info("serving %s at %s" % (name, self.url)) def serve_forever(self): diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py index 29c27257a..dd76f04d3 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, timeout=None, **kwargs): """ Run a command, given as a list, optionally giving it the @@ -213,9 +209,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: diff --git a/src/lib/Bcfg2/settings.py b/src/lib/Bcfg2/settings.py index d819ee534..87f2a0df0 100644 --- a/src/lib/Bcfg2/settings.py +++ b/src/lib/Bcfg2/settings.py @@ -32,7 +32,7 @@ TIME_ZONE = None DEBUG = False TEMPLATE_DEBUG = DEBUG -MEDIA_URL = '/site_media' +MEDIA_URL = '/site_media/' def _default_config(): @@ -86,14 +86,6 @@ def read_config(cfile=DEFAULT_CONFIG, repo=None): HOST=setup['db_host'], PORT=setup['db_port']) - if HAS_DJANGO and django.VERSION[0] == 1 and django.VERSION[1] < 2: - DATABASE_ENGINE = setup['db_engine'] - DATABASE_NAME = DATABASES['default']['NAME'] - DATABASE_USER = DATABASES['default']['USER'] - DATABASE_PASSWORD = DATABASES['default']['PASSWORD'] - DATABASE_HOST = DATABASES['default']['HOST'] - DATABASE_PORT = DATABASES['default']['PORT'] - # dropping the version check. This was added in 1.1.2 TIME_ZONE = setup['time_zone'] @@ -106,7 +98,7 @@ def read_config(cfile=DEFAULT_CONFIG, repo=None): if setup['web_prefix']: MEDIA_URL = setup['web_prefix'].rstrip('/') + MEDIA_URL else: - MEDIA_URL = '/site_media' + MEDIA_URL = '/site_media/' # initialize settings from /etc/bcfg2-web.conf or /etc/bcfg2.conf, or # set up basic defaults. this lets manage.py work in all cases @@ -144,7 +136,7 @@ MEDIA_ROOT = '' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. -ADMIN_MEDIA_PREFIX = '/media/' +STATIC_URL = '/media/' #TODO - make this unique # Make this unique, and don't share it with anybody. @@ -159,16 +151,10 @@ else: } } -if HAS_DJANGO and django.VERSION[0] == 1 and django.VERSION[1] < 2: - TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', - ) -else: - TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ) +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +) #TODO - review these. auth and sessions aren't really used MIDDLEWARE_CLASSES = ( @@ -194,20 +180,10 @@ TEMPLATE_DIRS = ( '/usr/share/python-support/python-django/django/contrib/admin/templates/', ) -# TODO - sanitize this -if HAS_DJANGO and django.VERSION[0] == 1 and django.VERSION[1] < 2: - TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.auth', - 'django.core.context_processors.debug', - 'django.core.context_processors.i18n', - 'django.core.context_processors.media', - 'django.core.context_processors.request' - ) -else: - TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.contrib.auth.context_processors.auth', - 'django.core.context_processors.debug', - 'django.core.context_processors.i18n', - 'django.core.context_processors.media', - 'django.core.context_processors.request' - ) +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.request' +) diff --git a/src/lib/Bcfg2/version.py b/src/lib/Bcfg2/version.py index 8223d7543..6f3ba3e49 100644 --- a/src/lib/Bcfg2/version.py +++ b/src/lib/Bcfg2/version.py @@ -2,7 +2,7 @@ import re -__version__ = "1.3.0rc2" +__version__ = "1.3.1" class Bcfg2VersionInfo(tuple): diff --git a/src/sbin/bcfg2-admin b/src/sbin/bcfg2-admin index 3c63cd3f5..3bce7fdab 100755 --- a/src/sbin/bcfg2-admin +++ b/src/sbin/bcfg2-admin @@ -10,6 +10,7 @@ import Bcfg2.Options import Bcfg2.Server.Admin from Bcfg2.Compat import StringIO + def mode_import(modename): """Load Bcfg2.Server.Admin.<mode>.""" modname = modename.capitalize() diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt index 810406567..f7deba90c 100755 --- a/src/sbin/bcfg2-crypt +++ b/src/sbin/bcfg2-crypt @@ -258,7 +258,7 @@ class Encryptor(object): (self.pname, pname)) return (passphrase, pname) - def _get_passphrase(self, chunk): # pylint: disable=W0613 + def _get_passphrase(self, chunk): # pylint: disable=W0613 """ get the passphrase for a chunk of a file """ return None diff --git a/src/sbin/bcfg2-info b/src/sbin/bcfg2-info index 287d0a161..ad35bbeeb 100755 --- a/src/sbin/bcfg2-info +++ b/src/sbin/bcfg2-info @@ -473,7 +473,7 @@ Bcfg2 client itself.""") ('Password', self.setup['password']), ('Server Metadata Connector', self.setup['mconnect']), ('Filemonitor', self.setup['filemonitor']), - ('Server address', self.setup['location']), + ('Server address', self.setup['location']), ('Path to key', self.setup['key']), ('Path to SSL certificate', self.setup['cert']), ('Path to SSL CA certificate', self.setup['ca']), @@ -640,21 +640,24 @@ Bcfg2 client itself.""") if 'Packages' not in self.plugins: print("Packages plugin not enabled") return + self.plugins['Packages'].toggle_debug() + + indep = lxml.etree.Element("Independent") + structures = [lxml.etree.Element("Bundle", name="packages")] + for arg in arglist[1:]: + lxml.etree.SubElement(structures[0], "Package", name=arg) + hostname = arglist[0] - initial = arglist[1:] metadata = self.build_metadata(hostname) - self.plugins['Packages'].toggle_debug() - collection = self.plugins['Packages'].get_collection(metadata) - packages, unknown = collection.complete(initial) - newpkgs = list(packages.difference(initial)) - print("%d initial packages" % len(initial)) - print(" %s" % "\n ".join(initial)) - print("%d new packages added" % len(newpkgs)) - if newpkgs: - print(" %s" % "\n ".join(newpkgs)) - print("%d unknown packages" % len(unknown)) - if unknown: - print(" %s" % "\n ".join(unknown)) + + # pylint: disable=W0212 + self.plugins['Packages']._build_packages(metadata, indep, structures) + # pylint: enable=W0212 + + print("%d new packages added" % len(indep.getchildren())) + if len(indep.getchildren()): + print(" %s" % "\n ".join(lxml.etree.tostring(p) + for p in indep.getchildren())) def do_packagesources(self, args): """ packagesources <hostname> - Show package sources """ @@ -726,17 +729,19 @@ Bcfg2 client itself.""") pass - def build_usage(): """ build usage message """ cmd_blacklist = ["do_loop", "do_EOF"] usage = dict() for attrname in dir(InfoCore): attr = getattr(InfoCore, attrname) - if (hasattr(attr, "__func__") and - attr.__func__.func_name not in cmd_blacklist and - attr.__func__.func_name.startswith("do_") and - attr.__func__.func_doc): + + # shim for python 2.4, __func__ is im_func + funcattr = getattr(attr, "__func__", getattr(attr, "im_func", None)) + if (funcattr != None and + funcattr.func_name not in cmd_blacklist and + funcattr.func_name.startswith("do_") and + funcattr.func_doc): usage[attr.__name__] = re.sub(r'\s+', ' ', attr.__doc__) return "Commands:\n" + "\n".join(usage[k] for k in sorted(usage.keys())) @@ -748,9 +753,8 @@ def main(): optinfo = dict(profile=Bcfg2.Options.CORE_PROFILE, interactive=Bcfg2.Options.INTERACTIVE, interpreter=Bcfg2.Options.INTERPRETER) - optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) - optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS) - setup = Bcfg2.Options.load_option_parser(optinfo) + optinfo.update(Bcfg2.Options.INFO_COMMON_OPTIONS) + setup = Bcfg2.Options.OptionParser(optinfo) setup.hm = "\n".join([" bcfg2-info [options] [command <command args>]", "Options:", setup.buildHelpMessage(), diff --git a/src/sbin/bcfg2-reports b/src/sbin/bcfg2-reports index 9f2ff96c2..2c4a918be 100755 --- a/src/sbin/bcfg2-reports +++ b/src/sbin/bcfg2-reports @@ -10,7 +10,7 @@ from Bcfg2.Compat import ConfigParser try: import Bcfg2.settings except ConfigParser.NoSectionError: - print("Your bcfg2.conf is currently missing the statistics section which " + print("Your bcfg2.conf is currently missing the [database] section which " "is necessary for the reporting interface. Please see bcfg2.conf(5) " "for more details.") sys.exit(1) @@ -121,7 +121,7 @@ def main(): help="Show hosts that haven't run in the last 24 " "hours") parser.add_option_group(allhostmodes) - + # entry modes entrymodes = \ OptionGroup(parser, "Entry Modes", @@ -166,7 +166,7 @@ def main(): (mode.get_opt_string(), opt.get_opt_string())) mode = opt mode_family = parser.get_option_group(opt.get_opt_string()) - + # you can specify more than one of --bad, --extra, --modified, --show, so # consider single-host options separately if not mode_family: @@ -174,7 +174,7 @@ def main(): if getattr(options, opt.dest): mode_family = parser.get_option_group(opt.get_opt_string()) break - + if not mode_family: parser.error("You must specify a mode") @@ -243,7 +243,7 @@ def main(): parser.error("%s require either a list of entries on the " "command line or the --file options" % mode_family.title) - + if options.badentry: result = hosts_by_entry_type(clients, "bad", entries) elif options.modifiedentry: @@ -263,7 +263,7 @@ def main(): # todo batch fetch this. sqlite could break for client in clients: - ents = entry_cls.objects.filter(name=entries[0][1], + ents = entry_cls.objects.filter(name=entries[0][1], interaction=client.current_interaction) if len(ents) == 0: continue diff --git a/src/sbin/bcfg2-server b/src/sbin/bcfg2-server index 8e96d65f2..33ee327fc 100755 --- a/src/sbin/bcfg2-server +++ b/src/sbin/bcfg2-server @@ -11,6 +11,7 @@ from Bcfg2.Server.Core import CoreInitError LOGGER = logging.getLogger('bcfg2-server') + def main(): optinfo = dict() optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) diff --git a/src/sbin/bcfg2-test b/src/sbin/bcfg2-test index 75079ec3f..510bb898b 100755 --- a/src/sbin/bcfg2-test +++ b/src/sbin/bcfg2-test @@ -5,29 +5,87 @@ without failures""" import os import sys -import signal import fnmatch import logging import Bcfg2.Logger import Bcfg2.Server.Core +from math import ceil from nose.core import TestProgram from nose.suite import LazySuite from unittest import TestCase +try: + from multiprocessing import Process, Queue, active_children + HAS_MULTIPROC = True +except ImportError: + HAS_MULTIPROC = False + active_children = lambda: [] # pylint: disable=C0103 + + +class CapturingLogger(object): + """ Fake logger that captures logging output so that errors are + only displayed for clients that fail tests """ + def __init__(self, *args, **kwargs): # pylint: disable=W0613 + self.output = [] + + def error(self, msg): + """ discard error messages """ + self.output.append(msg) + + def warning(self, msg): + """ discard error messages """ + self.output.append(msg) + + def info(self, msg): + """ discard error messages """ + self.output.append(msg) + + def debug(self, msg): + """ discard error messages """ + self.output.append(msg) + + def reset_output(self): + """ Reset the captured output """ + self.output = [] + + +class ClientTestFromQueue(TestCase): + """ A test case that tests a value that has been enqueued by a + child test process. ``client`` is the name of the client that has + been tested; ``result`` is the result from the :class:`ClientTest` + test. ``None`` indicates a successful test; a string value + indicates a failed test; and an exception indicates an error while + running the test. """ + __test__ = False # Do not collect + + def __init__(self, client, result): + TestCase.__init__(self) + self.client = client + self.result = result + + def shortDescription(self): + return "Building configuration for %s" % self.client + + def runTest(self): + """ parse the result from this test """ + if isinstance(self.result, Exception): + raise self.result + assert self.result is None, self.result + class ClientTest(TestCase): - """ - A test case representing the build of all of the configuration for + """ A test case representing the build of all of the configuration for a single host. Checks that none of the build config entities has had a failure when it is building. Optionally ignores some config files that we know will cause errors (because they are private - files we don't have access to, for instance) - """ + files we don't have access to, for instance) """ __test__ = False # Do not collect + divider = "-" * 70 - def __init__(self, bcfg2_core, client, ignore=None): + def __init__(self, core, client, ignore=None): TestCase.__init__(self) - self.bcfg2_core = bcfg2_core + self.core = core + self.core.logger = CapturingLogger() self.client = client if ignore is None: self.ignore = dict() @@ -52,13 +110,34 @@ class ClientTest(TestCase): def runTest(self): """ run this individual test """ - config = self.bcfg2_core.BuildConfiguration(self.client) + config = self.core.BuildConfiguration(self.client) + output = self.core.logger.output[:] + if output: + output.append(self.divider) + self.core.logger.reset_output() + # check for empty client configuration assert len(config.findall("Bundle")) > 0, \ - "%s has no content" % self.client + "\n".join(output + ["%s has no content" % self.client]) + + # check for missing bundles + metadata = self.core.build_metadata(self.client) + sbundles = [el.get('name') for el in config.findall("Bundle")] + missing = [b for b in metadata.bundles if b not in sbundles] + assert len(missing) == 0, \ + "\n".join(output + ["Configuration is missing bundle(s): %s" % + ':'.join(missing)]) + + # check for unknown packages + unknown_pkgs = [el.get("name") + for el in config.xpath('//Package[@type="unknown"]') + if not self.ignore_entry(el.tag, el.get("name"))] + assert len(unknown_pkgs) == 0, \ + "Configuration contains unknown packages: %s" % \ + ", ".join(unknown_pkgs) failures = [] - msg = ["Failures:"] + msg = output + ["Failures:"] for failure in config.xpath('//*[@failure]'): if not self.ignore_entry(failure.tag, failure.get('name')): failures.append(failure) @@ -73,23 +152,46 @@ class ClientTest(TestCase): id = __str__ -def get_sigint_handler(core): - """ Get a function that handles SIGINT/Ctrl-C by shutting down the - core and exiting properly.""" +def get_core(setup): + """ Get a server core, with events handled """ + core = Bcfg2.Server.Core.BaseCore(setup) + core.fam.handle_events_in_interval(0.1) + return core - def hdlr(sig, frame): # pylint: disable=W0613 - """ Handle SIGINT/Ctrl-C by shutting down the core and exiting - properly. """ - core.shutdown() - os._exit(1) # pylint: disable=W0212 - return hdlr +def get_ignore(setup): + """ Given an options dict, get a dict of entry tags and names to + ignore errors from """ + ignore = dict() + for entry in setup['test_ignore']: + tag, name = entry.split(":") + try: + ignore[tag].append(name) + except KeyError: + ignore[tag] = [name] + return ignore -def main(): - optinfo = dict(noseopts=Bcfg2.Options.TEST_NOSEOPTS, - test_ignore=Bcfg2.Options.TEST_IGNORE, - validate=Bcfg2.Options.CFG_VALIDATION) +def run_child(setup, clients, queue): + """ Run tests for the given clients in a child process, returning + results via the given Queue """ + core = get_core(setup) + ignore = get_ignore(setup) + for client in clients: + try: + ClientTest(core, client, ignore).runTest() + queue.put((client, None)) + except AssertionError: + queue.put((client, str(sys.exc_info()[1]))) + except: + queue.put((client, sys.exc_info()[1])) + + core.shutdown() + + +def parse_args(): + """ Parse command line arguments. """ + optinfo = dict(Bcfg2.Options.TEST_COMMON_OPTIONS) optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS) setup = Bcfg2.Options.load_option_parser(optinfo) @@ -109,37 +211,88 @@ def main(): to_syslog=False, to_file=setup['logging'], level=level) + logger = logging.getLogger(sys.argv[0]) if (setup['debug'] or setup['verbose']) and "-v" not in setup['noseopts']: setup['noseopts'].append("-v") - core = Bcfg2.Server.Core.BaseCore() - signal.signal(signal.SIGINT, get_sigint_handler(core)) + if setup['children'] and not HAS_MULTIPROC: + logger.warning("Python multiprocessing library not found, running " + "with no children") + setup['children'] = 0 - ignore = dict() - for entry in setup['test_ignore']: - tag, name = entry.split(":") - try: - ignore[tag].append(name) - except KeyError: - ignore[tag] = [name] + if (setup['children'] and ('--with-xunit' in setup['noseopts'] or + '--xunit-file' in setup['noseopts'])): + logger.warning("Use the --xunit option to bcfg2-test instead of the " + "--with-xunit or --xunit-file options to nosetest") + xunitfile = None + if '--with-xunit' in setup['noseopts']: + setup['noseopts'].remove('--with-xunit') + xunitfile = "nosetests.xml" + if '--xunit-file' in setup['noseopts']: + idx = setup['noseopts'].index('--xunit-file') + try: + setup['noseopts'].pop(idx) # remove --xunit-file + # remove the argument to it + xunitfile = setup['noseopts'].pop(idx) + except IndexError: + pass + if xunitfile and not setup['xunit']: + setup['xunit'] = xunitfile + return setup - core.fam.handle_events_in_interval(0.1) + +def main(): + setup = parse_args() + logger = logging.getLogger(sys.argv[0]) + core = get_core(setup) if setup['args']: clients = setup['args'] else: clients = core.metadata.clients - def run_tests(): - """ Run the test suite """ - for client in clients: - yield ClientTest(core, client, ignore) + ignore = get_ignore(setup) - TestProgram(argv=sys.argv[0:1] + setup['noseopts'], - suite=LazySuite(run_tests)) + if setup['children']: + if setup['children'] > len(clients): + logger.info("Refusing to spawn more children than clients to test," + " setting children=%s" % len(clients)) + setup['children'] = len(clients) + perchild = int(ceil(len(clients) / float(setup['children'] + 1))) + queue = Queue() + for child in range(setup['children']): + start = child * perchild + end = (child + 1) * perchild + child = Process(target=run_child, + args=(setup, clients[start:end], queue)) + child.start() + + def generate_tests(): + """ Read test results for the clients """ + start = setup['children'] * perchild + for client in clients[start:]: + yield ClientTest(core, client, ignore) + + for i in range(start): # pylint: disable=W0612 + yield ClientTestFromQueue(*queue.get()) + else: + def generate_tests(): + """ Run tests for the clients """ + for client in clients: + yield ClientTest(core, client, ignore) + + TestProgram(argv=sys.argv[:1] + core.setup['noseopts'], + suite=LazySuite(generate_tests), exit=False) + + # block until all children have completed -- should be + # immediate since we've already gotten all the results we + # expect + for child in active_children(): + child.join() core.shutdown() os._exit(0) # pylint: disable=W0212 + if __name__ == "__main__": sys.exit(main()) diff --git a/src/sbin/bcfg2-yum-helper b/src/sbin/bcfg2-yum-helper index ba6f30406..7e5c03fd5 100755 --- a/src/sbin/bcfg2-yum-helper +++ b/src/sbin/bcfg2-yum-helper @@ -129,7 +129,7 @@ class DepSolver(object): err = sys.exc_info()[1] self.logger.warning(err) return [] - + if ptype == "default": return [p for p, d in list(group.default_packages.items()) @@ -254,6 +254,6 @@ def main(): rv[gdata['group']] = list(packages) print(json.dumps(rv)) - + if __name__ == '__main__': sys.exit(main()) |