#!/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 <sconklin@redhat.com>
### 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
from optparse import OptionParser, Option
import sos.policyredhat
from sos.helpers import *
from snack import *
from threading import Thread, activeCount
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 OptionParser_extended(OptionParser):
def print_help(self):
OptionParser.print_help(self)
print
print "Some examples:"
print
print " enable cluster plugin only and collect dlm lockdumps:"
print " # sosreport -o cluster -k cluster.lockdump"
print
print " disable memory and samba plugins, turn off rpm -Va collection:"
print " # sosreport -n memory,samba -k rpm.rpmva=off"
print
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_extended(option_class=SosOption)
__cmdParser__.add_option("-l", "--list-plugins", action="store_true", \
dest="listPlugins", default=False, \
help="list plugins and available plugin options")
__cmdParser__.add_option("-n", action="extend", \
dest="noplugins", type="string", \
help="skip these plugins", default = [])
__cmdParser__.add_option("-e", action="extend", \
dest="enableplugins", type="string", \
help="enable these plugins", default = [])
__cmdParser__.add_option("-o", action="extend", \
dest="onlyplugins", type="string", \
help="enable these plugins only", default = [])
__cmdParser__.add_option("-k", action="extend", \
dest="plugopts", type="string", \
help="plugin options in plugname.option=value format (see -l)")
__cmdParser__.add_option("-a", "--alloptions", action="store_true", \
dest="usealloptions", default=False, \
help="enable all options for loaded plugins")
__cmdParser__.add_option("-v", "--verbose", action="count", \
dest="verbosity", \
help="increase verbosity")
__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 textcolor(text, fg, raw=0):
colors = { "black":"30", "red":"31", "green":"32", "brown":"33", "blue":"34",
"purple":"35", "cyan":"36", "lgray":"37", "gray":"1;30", "lred":"1;31",
"lgreen":"1;32", "yellow":"1;33", "lblue":"1;34", "pink":"1;35",
"lcyan":"1;36", "white":"1;37" }
opencol = "\033["
closecol = "m"
clear = opencol + "0" + closecol
f = opencol + colors[fg] + closecol
return "%s%s%s" % (f, text, clear)
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]" % (int(timeElapsed/60), timeElapsed % 60, round(ETA/60), ETA % 60)
else:
ETA = "[%02d:%02d/--:--]" % (int(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))
self.progBar = ""
for inc in range(0,allFull):
if inc == int(allFull / 2):
self.progBar = self.progBar + textcolor("%d%%" % percentDone, "blue")
elif inc < numHashes:
self.progBar = self.progBar + textcolor('#', "green")
else:
self.progBar = self.progBar + ' '
self.progBar = " Progress [" + self.progBar + "]" + 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"
# 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)
# FIXME: strip colours if terminal doesn't allow it
# 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
# using the options specified in the command line (if any)
if __cmdLineOpts__.usealloptions:
for plugname, plug in loadedplugins:
for name, parms in zip(plug.optNames, plug.optParms):
if type(parms["enabled"])==bool:
parms["enabled"] = True
if __cmdLineOpts__.plugopts:
opts = {}
for opt in __cmdLineOpts__.plugopts:
# split up "general.syslogsize=5"
try:
opt, val = opt.split("=")
except:
val=True
else:
if val == "off" or val == "disable" or val == "disabled":
val = False
else:
# try to convert string "val" to int()
try: val = int(val)
except: pass
# split up "general.syslogsize"
plug, opt = opt.split(".")
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
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))
# when --listplugins is specified we do a dry-run
# which tells the user which plugins are going to be enabled
# and with what options.
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" % (textcolor(plugname,"lblue"),plug.get_description())
else:
print _("No plugin enabled.")
print
if len(skippedplugins):
print _("The following plugins are currently disabled:")
print
for (plugname,plugclass) in skippedplugins:
print " %-25s %s" % (textcolor(plugname,"lgray"),plugclass.get_description())
print
if len(alloptions):
print _("The following plugin options are available:")
print
for (plug, plugname, optname, optparm) in alloptions:
# format and colorize option value based on its type (int or bool)
if type(optparm["enabled"])==bool:
if optparm["enabled"]==True:
tmpopt = textcolor("on","lred")
else:
tmpopt = textcolor("off","red")
elif type(optparm["enabled"])==int:
if optparm["enabled"] > 0:
tmpopt = textcolor(optparm["enabled"],"lred")
else:
tmpopt = textcolor(optparm["enabled"],"red")
else:
tmpopt = optparm["enabled"]
print " %-21s %-5s %s" % (plugname + "." + optname, tmpopt, optparm["desc"])
del tmpopt
else:
print _("No plugin options available.")
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)
# 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)
try:
plug.diagnose()
except:
if __raisePlugins__:
raise
tmpcount += len(plug.diagnose_msgs)
if tmpcount > 0:
print _("One or more plugins have detected a problem in your configuration.")
print _("Please review the following messages:")
print
for plugname, plug in loadedplugins:
for tmpcount2 in range(0,len(plug.diagnose_msgs)):
if tmpcount2 == 0:
soslog.warning( textcolor("%s:" % plugname, "red") )
soslog.warning(" * %s" % plug.diagnose_msgs[tmpcount2])
print
try:
while True:
yorno = raw_input( _("Are you sure you would like to continue (Y/n) ? ") )
if yorno == "y" or yorno == "Y":
del yorno
print
break
else:
sys.exit(0)
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)
try:
plug.setup()
except:
if __raisePlugins__:
raise
# 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")
# Generate the header for the html output file
rfd = open(rptdir + "/" + "sosreport.html", "w")
rfd.write("""
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<link rel="stylesheet" type="text/css" media="screen" href="donot.css" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Sos System Report</title>
</head>
<body>
""")
# 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('<a href="#%s">%s</a>: %s' % (plugname, plugname, alert))
plugNames.append(plugname)
# Create a table of links to the module info
rfd.write("<hr/><h3>Loaded Plugins:</h3>")
rfd.write("<table><tr>\n")
rr = 0
for i in range(len(plugNames)):
rfd.write('<td><a href="#%s">%s</a></td>\n' % (plugNames[i], plugNames[i]))
rr = divmod(i, 4)[1]
if (rr == 3):
rfd.write('</tr>')
if not (rr == 3):
rfd.write('</tr>')
rfd.write('</table>\n')
rfd.write('<hr/><h3>Alerts:</h3>')
rfd.write('<ul>')
for alert in allAlerts:
rfd.write('<li>%s</li>' % alert)
rfd.write('</ul>')
# Call the report method for each plugin
for plugname, plug in loadedplugins:
html = plug.report()
rfd.write(html)
rfd.write("</body></html>")
rfd.close()
# Collect any needed user information (name, etc)
# Call the postproc method for each plugin
for plugname, plug in loadedplugins:
try:
plug.postproc()
except:
if __raisePlugins__:
raise
# 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()