#!/usr/bin/env python """ Gather information about a system and report it using plugins supplied for application-specific information """ ## sosreport.py ## gather information about a system and report it ## Copyright (C) 2006 Steve Conklin ### This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # pylint: disable-msg = W0611 # pylint: disable-msg = W0702 import sys import os #import curses from optparse import OptionParser, Option import sos.policyredhat from sos.helpers import * from snack import * from threading import Thread, activeCount, enumerate import signal import logging __breakHits__ = 0 # Use this to track how many times we enter the exit routine ## Set up routines to be linked to signals for termination handling def exittermhandler(signum, frame): doExitCode() def doExitCode(): global __breakHits__ __breakHits__ += 1 if ( ( activeCount() > 1 ) and ( __breakHits__ == 1 ) ): print "SIGTERM received, multiple threads detected, waiting for all threads to exit" for thread in enumerate(): thread.wait() print "All threads ended, cleaning up." if ( ( activeCount() > 1 ) and ( __breakHits__ > 1 ) ): print "Multiple SIGTERMs, multiple threads, attempting to signal threads to die immediately" ## FIXME: Add thread-kill code (see FIXME below) print "Threads dead, cleaning up." if ( ( activeCount() == 1 ) and ( __breakHits__ > 2 ) ): print "Multiple SIGTERMs, single thread, exiting without cleaning up." sys.exit(3) # FIXME: Add code here to clean up /tmp print "Calling sys.exit from doExitCode()" sys.exit("Abnormal exit") # Handle any sort of exit signal cleanly # Currently, we intercept only sig 15 (TERM) signal.signal(signal.SIGTERM, exittermhandler) ## FIXME: Need to figure out how to IPC with child threads in case of ## multiple SIGTERMs. ## FIXME: Need to figure out how to handle SIGKILL - we can't intercept it. if os.getuid() != 0: print 'You must run sosreport as root!' sys.exit(1) # for debugging __raisePlugins__ = 1 class SosOption (Option): """Allow to specify comma delimited list of plugins""" ACTIONS = Option.ACTIONS + ("extend",) STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",) TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",) ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",) def take_action(self, action, dest, opt, value, values, parser): if action == "extend": lvalue = value.split(",") values.ensure_value(dest, []).extend(lvalue) else: Option.take_action(self, action, dest, opt, value, values, parser) __cmdParser__ = OptionParser(option_class=SosOption) __cmdParser__.add_option("-a", "--alloptions", action="store_true", \ dest="usealloptions", default=False, \ help="Use all options for loaded plugins") __cmdParser__.add_option("-f", "--fastoptions", action="store_true", \ dest="fastoptions", default=False, \ help="Use only fast options for loaded plugins") __cmdParser__.add_option("-g", "--gatheronly", action="store_true", \ dest="gatheronly", default=False, \ help="Gather information locally but don't package or submit") __cmdParser__.add_option("-l", "--list-plugins", action="store_true", \ dest="listPlugins", default=False, \ help="list existing plugins") __cmdParser__.add_option("-n", "--noplugin", action="extend", \ dest="noplugins",\ help="list of plugins _not_ to load") __cmdParser__.add_option("-o", "--onlyplugin", action="extend", \ dest="onlyplugins",\ help="list of plugins to load") __cmdParser__.add_option("-v", "--verbose", action="count", \ dest="verbosity", \ help="How obnoxious we're being about telling the user what we're doing.") __cmdParser__.add_option("-b", "--no-progressbar", action="store_false", \ dest="progressbar", default=True, \ help="Do not display a progress bar.") __cmdParser__.add_option("-m", "--multithreaded", action="store_true", \ dest="multithread", \ help="Use the multithreaded information gathering mode to speed up the report (experimental)") (__cmdLineOpts__, __cmdLineArgs__)=__cmdParser__.parse_args() def get_curse_options(alloptions): """ use curses to enable the user to select some options """ # alloptions is an array of (plug, plugname, optname, parms(dictionary)) tuples plugName = [] out = [] # get a sorted list of all plugin names for rrr in alloptions: if rrr[1] not in plugName: plugName.append(rrr[1]) plugName.sort() plugCbox = CheckboxTree(height=5, scroll=1) countOpt = -1 optDic = {} optDicCounter = 0 # iterate over all plugins with options for curPlugName in plugName: plugCbox.addItem(curPlugName, (snackArgs['append'],)) countOpt = countOpt+1 for opt in alloptions: if opt[1] != curPlugName: continue snt = opt[2] + " ("+opt[3]['desc']+") is " + opt[3]['speed'] plugCbox.addItem(snt, (countOpt, snackArgs['append']), item = optDicCounter, selected = opt[3]['enabled']) optDic[optDicCounter] = opt optDicCounter += 1 screen = SnackScreen() bb = ButtonBar(screen, (("Ok", "ok"), ("Cancel", "cancel"))) g = GridForm(screen, "Select Sosreport Options", 1, 10) g.add(plugCbox, 0, 0) g.add(bb, 0, 1, growx = 1) result = g.runOnce() screen.finish() if bb.buttonPressed(result) == "cancel": raise "Cancelled" for rrr in range(0, optDicCounter): optDic[rrr][3]['enabled'] = plugCbox.getEntryValue(rrr)[1] out.append((optDic[rrr])) return out class progressBar: def __init__(self, minValue = 0, maxValue = 10, totalWidth=12): self.progBar = "[]" # This holds the progress bar string self.min = minValue self.max = maxValue self.span = maxValue - minValue self.width = totalWidth self.amount = 0 # When amount == max, we are 100% done self.updateAmount(0) # Build progress bar string def updateAmount(self, newAmount = 0): if newAmount < self.min: newAmount = self.min if newAmount > self.max: newAmount = self.max self.amount = newAmount # Figure out the new percent done, round to an integer diffFromMin = float(self.amount - self.min) percentDone = (diffFromMin / float(self.span)) * 100.0 percentDone = round(percentDone) percentDone = int(percentDone) # Figure out how many hash bars the percentage should be allFull = self.width - 2 numHashes = (percentDone / 100.0) * allFull numHashes = int(round(numHashes)) # build a progress bar with hashes and spaces self.progBar = " [" + '#'*numHashes + ' '*(allFull-numHashes) + "]" # figure out where to put the percentage, roughly centered percentPlace = (len(self.progBar) / 2) - len(str(percentDone)) percentString = str(percentDone) + "%" # slice the percentage into the bar self.progBar = " Progress" + self.progBar[0:percentPlace] + percentString + self.progBar[percentPlace+len(percentString):] def incAmount(self, toInc = 1): self.updateAmount(self.amount+toInc) def finished(self): self.updateAmount(self.max) sys.stdout.write(self.progBar + '\n') sys.stdout.flush() def update(self): sys.stdout.write(self.progBar + '\r') sys.stdout.flush() def sosreport(): # pylint: disable-msg = R0912 # pylint: disable-msg = R0914 # pylint: disable-msg = R0915 """ This is the top-level function that gathers and processes all sosreport information """ loadedplugins = [] alloptions = [] # perhaps we should automatically locate the policy module?? policy = sos.policyredhat.SosPolicy() # find the plugins path paths = sys.path for path in paths: if path.strip()[-len("site-packages"):] == "site-packages": pluginpath = path + "/sos/plugins" reporterpath = path + "/sos/reporters" # Set up common info and create destinations dstroot = sosFindTmpDir() cmddir = os.path.join(dstroot, "sos_commands") logdir = os.path.join(dstroot, "sos_logs") rptdir = os.path.join(dstroot, "sos_reports") os.mkdir(cmddir, 0755) os.mkdir(logdir, 0755) os.mkdir(rptdir, 0755) # open log file logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s: %(message)s', filename=logdir + "/sos.log", filemode='w') # define a Handler which writes INFO messages or higher to the sys.stderr console = logging.StreamHandler() console.setLevel(logging.INFO) console.setFormatter(logging.Formatter('%(message)s')) logging.getLogger('').addHandler(console) logging.addLevelName(17,"verbose") logging.addLevelName(16,"verbose2") logging.addLevelName(15,"verbose3") soslog = logging.getLogger('') # set up dict so everyone can share the following commons = {'dstroot': dstroot, 'cmddir': cmddir, 'logdir': logdir, 'rptdir': rptdir, 'soslog': soslog, 'policy': policy, 'verbosity' : __cmdLineOpts__.verbosity} # Make policy aware of the commons policy.setCommons(commons) # validate and load plugins plugins = os.listdir(pluginpath) if __cmdLineOpts__.listPlugins: for plug in plugins: try: if ((plug[-3:] == '.py') and (plug != "__init__.py")): plugbase = plug[:-3] pidot = "sos.plugins." + plugbase print plugbase except: print "ouch" sys.exit() for plug in plugins: if ((plug[-3:] == '.py') and (plug != "__init__.py") and (not __cmdLineOpts__.noplugins or (plug[:-3] not in __cmdLineOpts__.noplugins)) and (not __cmdLineOpts__.onlyplugins or (plug[:-3] in __cmdLineOpts__.onlyplugins)) ): try: plugbase = plug[:-3] pidot = "sos.plugins." + plugbase #print "importing plugin: %s" % plugbase try: if policy.validatePlugin(pluginpath + plug): pluginClass = importPlugin(pidot, plugbase) else: soslog.warning("Plugin %s does not validate, skipping" % plug) continue if pluginClass(plugbase, commons).checkenabled: soslog.debug("Plugin %s auto-disabled itself." % plug) continue loadedplugins.append((plugbase, pluginClass(plugbase, commons))) except: soslog.warning("Plugin %s does not install, skipping" % plug) raise except: if __raisePlugins__: raise soslog.warning("plugin load failed for %s" % plug) if not len(loadedplugins): soslog.error("no valid plugins were enabled") sys.exit(1) soslog.info("Welcome to sosreport (%d plugins loaded)" % (len(loadedplugins))) sys.stdout.write("\n") # Iterate over plugins for each stage # First, gather and process options for plugname, plug in loadedplugins: if __cmdLineOpts__.verbosity > 2: soslog.verbose3("processing options from plugin: %s" % plugname) try: len(__cmdLineOpts__.noplugins) if plugname not in __cmdLineOpts__.noplugins: names, parms = plug.getAllOptions() for optname, optparm in zip(names, parms): alloptions.append((plug, plugname, optname, optparm)) except: names, parms = plug.getAllOptions() for optname, optparm in zip(names, parms): alloptions.append((plug, plugname, optname, optparm)) if not __cmdLineOpts__.fastoptions and not __cmdLineOpts__.usealloptions: if len(alloptions): try: get_curse_options(alloptions) except "Cancelled": sys.exit("Exiting.") else: soslog.info("no options available for selected plugins") elif __cmdLineOpts__.fastoptions: for i in range(len(alloptions)): for plug, plugname, optname, optparm in alloptions: if optparm['speed'] == 'fast': plug.setOption(optname, 1) elif __cmdLineOpts__.usealloptions: for i in range(len(alloptions)): for plug, plugname, optname, optparm in alloptions: plug.setOption(optname, 1) # Setup the progress bar if __cmdLineOpts__.progressbar: pbar = progressBar(0, len(loadedplugins) * 33, 40) # Call the setup method for each plugin for plugname, plug in loadedplugins: if __cmdLineOpts__.verbosity > 1: soslog.verbose2("Setting up plugin module %s" % plugname) plug.setup() if __cmdLineOpts__.progressbar: pbar.incAmount() pbar.update() # Call the collect method for each plugin for plugname, plug in loadedplugins: if __cmdLineOpts__.verbosity > 0: soslog.verbose("Executing plugin %s" % plugname) if __cmdLineOpts__.multithread: plug.doCollect() else: plug.copyStuff() if __cmdLineOpts__.progressbar: if __cmdLineOpts__.multithread: pbar.incAmount() else: pbar.incAmount(30) pbar.update() # Wait for all the collection threads to exit if __cmdLineOpts__.multithread: for plugname, plug in loadedplugins: if __cmdLineOpts__.verbosity > 1: soslog.verbose2("Waiting for plugin %s to return" % plugname) plug.wait() if __cmdLineOpts__.progressbar: pbar.incAmount(30) pbar.update() # Call the analyze method for each plugin for plugname, plug in loadedplugins: if __cmdLineOpts__.verbosity > 1: soslog.verbose2("Analyzing results of plugin %s" % plugname,) plug.analyze() if __cmdLineOpts__.progressbar: pbar.incAmount() pbar.update() if __cmdLineOpts__.progressbar: pbar.finished() sys.stdout.write("\n") # Sort the module names to do the report in alphabetical order loadedplugins.sort() # Generate the header for the html output file rfd = open(rptdir + "/" + "sosreport.html", "w") rfd.write(""" Sos System Report """) # Make a pass to gather Alerts and a list of module names allAlerts = [] plugNames = [] for plugname, plug in loadedplugins: for alert in plug.alerts: allAlerts.append('%s: %s' % (plugname, plugname, alert)) plugNames.append(plugname) # Create a table of links to the module info rfd.write("

Loaded Plugins:

") rfd.write("\n") rr = 0 for i in range(len(plugNames)): rfd.write('\n' % (plugNames[i], plugNames[i])) rr = divmod(i, 4)[1] if (rr == 3): rfd.write('') if not (rr == 3): rfd.write('') rfd.write('
%s
\n') rfd.write('

Alerts:

') rfd.write('') # Call the report method for each plugin for plugname, plug in loadedplugins: html = plug.report() rfd.write(html) rfd.write("") rfd.close() # Collect any needed user information (name, etc) # Call the postproc method for each plugin for plugname, plug in loadedplugins: plug.postproc() if __cmdLineOpts__.gatheronly: soslog.info("Collected information is in " + dstroot) soslog.info("Your html report is in " + rptdir + "/" + "sosreport.html") else: # package up the results for the support organization policy.packageResults() # delete gathered files os.system("rm -rf %s" % dstroot) # automated submission will go here # Close all log files and perform any cleanup logging.shutdown() if __name__ == '__main__': try: sosreport() except KeyboardInterrupt: doExitCode()