#!/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 from stat import * from time import strftime, localtime, time from pwd import getpwuid import gettext from threading import Semaphore __version__ = 1.7 __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(): if thread.getName() != "MainThread": thread.join() 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 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. # 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",) def take_action(self, action, dest, opt, value, values, parser): if action == "extend": try: lvalue = value.split(",") except: pass else: 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", type="string", \ help="skip these plugins", default = []) __cmdParser__.add_option("-o", "--onlyplugin", action="extend", \ dest="onlyplugins", type="string", \ help="enable these plugins only", default = []) __cmdParser__.add_option("-e", "--enableplugin", action="extend", \ dest="enableplugins", type="string", \ help="list of inactive plugins to be enabled", default = []) __cmdParser__.add_option("-k", "--pluginopts", action="extend", \ dest="plugopts", type="string", \ help="plugin options in plugin_name.option=value format") __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("-c", "--curses", action="store_true", \ dest="use_curses", default=False, \ help="Display a text GUI menu to modify plugin options.") __cmdParser__.add_option("--no-progressbar", action="store_false", \ dest="progressbar", default=True, \ help="Do not display a progress bar.") __cmdParser__.add_option("--no-multithread", action="store_true", \ dest="nomultithread", \ help="Disable multi-threaded gathering mode (slower)", default=False) (__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=40): self.progBar = "[]" # This holds the progress bar string self.min = minValue self.max = maxValue self.width = totalWidth self.amount = 0 # When amount == max, we are 100% done self.time_start = time() self.eta = 0 self.last_amount_update = time() self.update() def updateAmount(self, newAmount = 0): if newAmount < self.min: newAmount = self.min if newAmount > self.max: newAmount = self.max if self.amount != newAmount: self.last_amount_update = time() self.amount = newAmount last_update_relative = round(self.last_amount_update - self.time_start) self.eta = round(last_update_relative * self.max / self.amount) # generate ETA timeElapsed = round(time() - self.time_start) last_update_relative = round(self.last_amount_update - self.time_start) if timeElapsed >= 10 and self.amount > 0: percentDone = round(timeElapsed * 100 / self.eta) if percentDone > 100: percentDone = 100 ETA = timeElapsed elif self.eta < timeElapsed: ETA = timeElapsed else: ETA = self.eta ETA = "[%02d:%02d/%02d:%02d]" % (round(timeElapsed/60), timeElapsed % 60, round(ETA/60), ETA % 60) else: ETA = "[%02d:%02d/--:--]" % (round(timeElapsed/60), timeElapsed % 60) if self.amount < self.max: percentDone = 0 else: percentDone = 100 # 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):] + ETA 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): self.updateAmount(self.amount) sys.stdout.write(self.progBar + '\r') sys.stdout.flush() class XmlReport: def __init__(self): try: import libxml2 except: self.enabled = False return else: self.enabled = True self.doc = libxml2.newDoc("1.0") self.root = self.doc.newChild(None, "sos", None) self.commands = self.root.newChild(None, "commands", None) self.files = self.root.newChild(None, "files", None) def add_command(self,cmdline,exitcode,stdout = None,stderr = None,f_stdout=None,f_stderr=None, runtime=None): if not self.enabled: return cmd = self.commands.newChild(None, "cmd", None) cmd.setNsProp(None, "cmdline", cmdline) cmdchild = cmd.newChild(None, "exitcode", str(exitcode)) if runtime: cmd.newChild(None, "runtime", str(runtime)) if stdout or f_stdout: cmdchild = cmd.newChild(None, "stdout", stdout) if f_stdout: cmdchild.setNsProp(None, "file", f_stdout) if stderr or f_stderr: cmdchild = cmd.newChild(None, "stderr", stderr) if f_stderr: cmdchild.setNsProp(None, "file", f_stderr) def add_file(self,fname,stats): if not self.enabled: return cfile = self.files.newChild(None,"file",None) cfile.setNsProp(None, "fname", fname) cchild = cfile.newChild(None, "uid", str(stats[ST_UID])) cchild.setNsProp(None,"name", getpwuid(stats[ST_UID])[0]) cchild = cfile.newChild(None, "gid", str(stats[ST_GID])) cchild.setNsProp(None,"name", getpwuid(stats[ST_GID])[0]) cfile.newChild(None, "mode", str(oct(S_IMODE(stats[ST_MODE])))) cchild = cfile.newChild(None, "ctime", strftime('%a %b %d %H:%M:%S %Y', localtime(stats[ST_CTIME]))) cchild.setNsProp(None,"tstamp", str(stats[ST_CTIME])) cchild = cfile.newChild(None, "atime", strftime('%a %b %d %H:%M:%S %Y', localtime(stats[ST_ATIME]))) cchild.setNsProp(None,"tstamp", str(stats[ST_ATIME])) cchild = cfile.newChild(None, "mtime", strftime('%a %b %d %H:%M:%S %Y', localtime(stats[ST_MTIME]))) cchild.setNsProp(None,"tstamp", str(stats[ST_MTIME])) def serialize(self): if not self.enabled: return print self.doc.serialize(None, 1) def serialize_to_file(self,fname): if not self.enabled: return outfn = open(fname,"w") outfn.write(self.doc.serialize(None,1)) outfn.close() 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 = [] skippedplugins = [] 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) # initialize i18n language localization gettext.install('sos', '/usr/share/locale', unicode=False) # initialize logging soslog = logging.getLogger('sos') soslog.setLevel(logging.DEBUG) # log to a file flog = logging.FileHandler(logdir + "/sos.log") flog.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s')) flog.setLevel(logging.DEBUG) soslog.addHandler(flog) # define a Handler which writes INFO messages or higher to the sys.stderr console = logging.StreamHandler(sys.stderr) if __cmdLineOpts__.verbosity > 0: console.setLevel(20 - __cmdLineOpts__.verbosity) __cmdLineOpts__.progressbar = False else: console.setLevel(logging.INFO) console.setFormatter(logging.Formatter('%(message)s')) soslog.addHandler(console) logging.VERBOSE = logging.INFO - 1 logging.VERBOSE2 = logging.INFO - 2 logging.VERBOSE3 = logging.INFO - 3 logging.addLevelName(logging.VERBOSE, "verbose") logging.addLevelName(logging.VERBOSE2,"verbose2") logging.addLevelName(logging.VERBOSE3,"verbose3") xmlrep = XmlReport() # 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, 'xmlreport' : xmlrep } # Make policy aware of the commons policy.setCommons(commons) print soslog.info ( _("sosreport (version %s)") % __version__) print # generate list of available plugins plugins = os.listdir(pluginpath) plugins.sort() # validate and load plugins for plug in plugins: plugbase = plug[:-3] if not plug[-3:] == '.py' or plugbase == "__init__": continue try: #print "importing plugin: %s" % plugbase try: if policy.validatePlugin(pluginpath + plug): pluginClass = importPlugin("sos.plugins." + plugbase, plugbase) else: soslog.warning(_("plugin %s does not validate, skipping") % plug) skippedplugins.append((plugbase, pluginClass(plugbase, commons))) continue if plugbase in __cmdLineOpts__.noplugins: soslog.log(logging.VERBOSE, _("plug %s skipped (noplugins)") % plugbase) skippedplugins.append((plugbase, pluginClass(plugbase, commons))) continue if not pluginClass(plugbase, commons).checkenabled() and not plugbase in __cmdLineOpts__.enableplugins and not plugbase in __cmdLineOpts__.onlyplugins: soslog.log(logging.VERBOSE, _("plugin %s is inactive (use -e or -o to enable).") % plug) skippedplugins.append((plugbase, pluginClass(plugbase, commons))) continue if not pluginClass(plugbase, commons).defaultenabled() and not plugbase in __cmdLineOpts__.enableplugins and not plugbase in __cmdLineOpts__.onlyplugins: soslog.log(logging.VERBOSE, "plugin %s not loaded by default (use -e or -o to enable)." % plug) skippedplugins.append((plugbase, pluginClass(plugbase, commons))) continue if __cmdLineOpts__.onlyplugins and not plugbase in __cmdLineOpts__.onlyplugins: soslog.log(logging.VERBOSE, _("plugin %s not specified in --onlyplugin list") % plug) skippedplugins.append((plugbase, pluginClass(plugbase, commons))) continue loadedplugins.append((plugbase, pluginClass(plugbase, commons))) except: soslog.warning(_("plugin %s does not install, skipping") % plug) raise except: soslog.warning(_("could not load plugin %s") % plug) if __raisePlugins__: raise # First, gather and process options for plugname, plug in loadedplugins: soslog.log(logging.VERBOSE3, _("processing options from plugin: %s") % plugname) names, parms = plug.getAllOptions() for optname, optparm in zip(names, parms): alloptions.append((plug, plugname, optname, optparm)) if __cmdLineOpts__.listPlugins: if not len(loadedplugins) and not len(skippedplugins): soslog.error(_("no valid plugins found")) sys.exit(1) if len(loadedplugins): print _("The following plugins are currently enabled:") print for (plugname,plug) in loadedplugins: print " %-25s %s" % (plugname,plug.get_description()) else: print _("No plugin enabled.") print if len(alloptions): print _("The following plugin options are available:") print for (plug, plugname, optname, optparm) in alloptions: print " %-25s %s [%d]" % (plugname + "." + optname, optparm["desc"], optparm["enabled"]) else: print _("No plugin options available.") if len(skippedplugins): print print _("The following plugins are currently disabled:") print for (plugname,plugclass) in skippedplugins: print " %-25s %s" % (plugname,plugclass.get_description()) print sys.exit() # to go anywhere further than listing the plugins we will need root permissions. # if os.getuid() != 0: print _('sosreport requires root permissions to run.') sys.exit(1) # we don't need to keep in memory plugins we are not going to use del skippedplugins if not len(loadedplugins): soslog.error(_("no valid plugins were enabled")) sys.exit(1) try: raw_input(_("""This utility will collect some detailed information about the hardware and setup of your Red Hat Enterprise Linux system. This information will be used to diagnose problems with your system and will be considered confidential information. Red Hat will use this information for diagnostic purposes ONLY. This process may take a while to complete. No changes will be made to your system. Press ENTER to continue, or CTRL-C to quit. """)) except KeyboardInterrupt: print sys.exit(0) # setup plugin options if __cmdLineOpts__.plugopts: opts = {} for opt in __cmdLineOpts__.plugopts: try: opt, val = opt.split("=") except: val=1 plug, opt = opt.split(".") try: val = int(val) # try to convert string "val" to int() except: pass try: opts[plug] except KeyError: opts[plug] = [] opts[plug].append( (opt,val) ) for plugname, plug in loadedplugins: if opts.has_key(plugname): for opt,val in opts[plugname]: soslog.log(logging.VERBOSE, "setting option %s for plugin %s to %s" % (plugname,opt,val)) plug.setOption(opt,val) del opt,opts,val elif not __cmdLineOpts__.fastoptions and not __cmdLineOpts__.usealloptions: if len(alloptions) and __cmdLineOpts__.use_curses: try: get_curse_options(alloptions) except "Cancelled": sys.exit(_("Exiting.")) elif __cmdLineOpts__.fastoptions: for i in range(len(alloptions)): for plug, plugname, optname, optparm in alloptions: if optparm['speed'] == 'fast': plug.setOption(optname, 1) else: plug.setOption(optname, 0) elif __cmdLineOpts__.usealloptions: for i in range(len(alloptions)): for plug, plugname, optname, optparm in alloptions: plug.setOption(optname, 1) # Call the diagnose() method for each plugin tmpcount = 0 for plugname, plug in loadedplugins: soslog.log(logging.VERBOSE2, "Performing sanity check for plugin %s" % plugname) plug.diagnose() tmpcount += len(plug.diagnose_msgs) if tmpcount > 0: print _("One or more plugin has detected a problem in your configuration.") print _("Please review the following messages:") print for plugname, plug in loadedplugins: for msg in plug.diagnose_msgs: soslog.warning(" * %s: %s", plugname, msg) print try: raw_input( _("Press ENTER to continue, or CTRL-C to quit.\n") ) except KeyboardInterrupt: print sys.exit(0) # Call the setup() method for each plugin for plugname, plug in loadedplugins: soslog.log(logging.VERBOSE2, "Preloading files and commands to be gathered by plugin %s" % plugname) plug.setup() # Setup the progress bar if __cmdLineOpts__.progressbar: # gather information useful for generating ETA eta_weight = len(loadedplugins) for plugname, plug in loadedplugins: eta_weight += plug.eta_weight pbar = progressBar(minValue = 0, maxValue = eta_weight) # pbar.max = number_of_plugins + weight (default 1 per plugin) if __cmdLineOpts__.nomultithread: soslog.log(logging.VERBOSE, "using single-threading") else: soslog.log(logging.VERBOSE, "using multi-threading") # Call the collect method for each plugin plugrunning = Semaphore(2) for plugname, plug in loadedplugins: soslog.log(logging.VERBOSE, "executing plugin %s" % plugname) if not __cmdLineOpts__.nomultithread: plug.copyStuff(threaded = True, semaphore = plugrunning) else: plug.copyStuff() if __cmdLineOpts__.progressbar: pbar.incAmount(plug.eta_weight) pbar.update() del plugrunning # Wait for all the collection threads to exit if not __cmdLineOpts__.nomultithread: finishedplugins = [] while len(loadedplugins) > 0: plugname, plug = loadedplugins.pop(0) if not plug.wait(0.5): finishedplugins.append((plugname,plug)) soslog.log(logging.VERBOSE2, "plugin %s has returned" % plugname) if __cmdLineOpts__.progressbar: pbar.incAmount(plug.eta_weight) else: soslog.log(logging.VERBOSE3, "plugin %s still hasn't returned" % plugname) loadedplugins.append((plugname,plug)) if __cmdLineOpts__.progressbar: pbar.update() loadedplugins = finishedplugins del finishedplugins xmlrep.serialize_to_file(rptdir + "/" + "sosreport.xml") # Call the analyze method for each plugin for plugname, plug in loadedplugins: soslog.log(logging.VERBOSE2, "Analyzing results of plugin %s" % plugname,) try: plug.analyze() except: # catch exceptions in analyse() and keep working pass 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("/bin/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()