Files
weewx/bin/wee_debug
2019-02-24 06:01:25 -08:00

436 lines
16 KiB
Python
Executable File

#!/usr/bin/env python
#
# Copyright (c) 2009-2019 Gary Roderick, Tom Keffer <tkeffer@gmail.com> and
# Matthew Wall
#
# See the file LICENSE.txt for your rights.
#
""" Generate weewx debug info """
from __future__ import print_function
from __future__ import absolute_import
import copy
import optparse
import os
import platform
import sys
import syslog
import six
from configobj import ConfigObj
# weewx imports
import weecfg
import weedb
import weewx.manager
import weewx.units
from weecfg.extension import ExtensionEngine
from weeutil.weeutil import TimeSpan, timestamp_to_string
from weewx.manager import DaySummaryManager
VERSION = weewx.__version__
WEE_DEBUG_VERSION = '0.8'
# keys/setting names to obfuscate in weewx.conf, key value will be obfuscated
# if the key starts any element in the list. Can add additional string elements
# to list if required
OBFUSCATE_MAP = {
"obfuscate": [
"apiKey", "api_key", "app_key", "archive_security_key", "id", "key",
"oauth_token", "password", "raw_security_key", "token", "user",
"server_url", "station"],
"do_not_obfuscate": [
"station_type"]
}
# weewx archive field to use as the basis of counting number of archive records
# Can be any field but dateTime is preferred as it should be in every weewx
# archive
COUNT_FIELD = 'dateTime'
# Redirect the import of setup (needed to get extension info)
sys.modules['setup'] = weecfg.extension
usage = """wee_debug --help
wee_debug --info
[CONFIG_FILE|--config=CONFIG_FILE]
[--output|--output DEBUG_PATH]
[--verbosity=0|1|2]
wee_debug --version
Description:
Generate a standard suite of system/weewx information to aid in remote
debugging. The wee_debug output consists of two parts, the first part containing
a snapshot of relevant system/weewx information and the second part a parsed and
obfuscated copy of weewx.conf. This output can be redirected to file and posted
when seeking assistance via forums or email.
Actions:
--info Generate a debug report."""
epilog = """wee_debug will attempt to obfuscate obvious personal/private
information in weewx.conf such as user names, passwords and API keys; however,
the user should thoroughly check the generated output for personal/private
information before posting the information publicly."""
def main():
# Set defaults for the system logger:
syslog.openlog('wee_debug', syslog.LOG_PID|syslog.LOG_CONS)
# Create a command line parser:
parser = optparse.OptionParser(usage = usage,
epilog = epilog)
# Add the various options:
parser.add_option("--config", dest="config_path", type=str,
metavar="CONFIG_FILE",
help="Use configuration file CONFIG_FILE.")
parser.add_option("--info", dest="info", action='store_true',
help="Generate weewx debug output.")
parser.add_option('--output', action='callback',
callback=optional_arg('/var/tmp/weewx.debug'),
dest='debug_file', metavar="DEBUG_PATH",
help="Write wee_debug output to DEBUG_PATH. DEBUG_PATH "
"includes path and file name. Default is "
"/var/tmp/weewx.debug.")
parser.add_option('--verbosity', type=int, default=1,
metavar="N", help="How much detail to display, "
"0-2, default=1.")
parser.add_option('--version', dest='version', action='store_true',
help='Display wee_debug version number.')
# Now we are ready to parse the command line:
(options, args) = parser.parse_args() # @UnusedVariable
# check weewx version number for compatibility
if int(VERSION[0]) < 3:
# incompatible version of weewx
print("Incompatible version of weewx detected (%s). "
"Weewx v3.0.0 or greater required." % VERSION)
print("Nothing done, exiting.")
exit(1)
# get config_dict to use
config_path, config_dict = weecfg.read_config(options.config_path, args)
# display wee_debug version info
if options.version:
print("wee_debug version: %s" % WEE_DEBUG_VERSION)
exit(1)
# display debug info
if options.info:
# first a message re verbosity
if options.verbosity == 0:
print("Using verbosity=0, displaying minimal info")
elif options.verbosity == 1:
print("Using verbosity=1, displaying most info")
else:
print("Using verbosity=2, displaying all info")
print()
# then a message re output destination
if options.debug_file is not None:
print("wee_debug output will be written to %s" % options.debug_file)
else:
print("wee_debug output will be sent to stdout(console)")
print()
# get some key weewx parameters
db_binding_wx = get_binding(config_dict)
database_wx = config_dict['DataBindings'][db_binding_wx]['database']
# generate our debug info sending it to file or console
if options.debug_file is not None:
# save stdout for when we clean up
old_stdout = sys.stdout
# open our debug file for writing
_debug_file = open(options.debug_file, 'w')
# redirect stdout to our file
sys.stdout = _debug_file
print("Using configuration file %s" % config_path)
print("Using database binding '%s', which is bound to database '%s'" % (db_binding_wx,
database_wx))
print()
# generate our debug info
generateDebugInfo(config_dict,
config_path,
db_binding_wx,
options.verbosity)
print()
print("Parsed and obfuscated weewx.conf")
# generate our obfuscated weewx.conf
generateDebugConf(config_dict)
if options.debug_file is not None:
# close our file
_debug_file.close()
# revert stdout
sys.stdout = old_stdout
print("wee_debug output successfully written to %s" % options.debug_file)
else:
print()
print("wee_debug report successfully generated")
exit(1)
# if we have a compatible weewx version but did not use --version or --info
# then show the wee_debug help
parser.print_help()
def optional_arg(arg_default):
""" Callback function to implement optparse command line parameters that
support default values and optional parameters.
http://stackoverflow.com/questions/1229146/parsing-empty-options-in-python
"""
def func(option, opt_str, value, parser): # @UnusedVariable
if parser.rargs and not parser.rargs[0].startswith('-'):
val = parser.rargs[0]
parser.rargs.pop(0)
else:
val = arg_default
setattr(parser.values, option.dest, val)
return func
def generateDebugInfo(config_dict, config_path, db_binding_wx, verbosity):
""" Generate system/weewx debug info """
# system/OS info
generateSysInfo(verbosity)
# weewx version info
print("General Weewx info")
print(" Weewx version %s detected." % VERSION)
print()
# station info
print("Station info")
stationType = config_dict['Station']['station_type']
print(" Station type: %s" % stationType)
print(" Driver: %s" % config_dict[stationType]['driver'])
print()
# driver info
if verbosity > 0:
print("Driver info")
driver_dict = {stationType: config_dict[stationType]}
_config = ConfigObj(driver_dict)
if six.PY2:
_config.write(sys.stdout)
else:
# The ConfigObj.write() method always writes in binary,
# which is not accepted by Python 3 for output to stdout
# So write to sys.stdout.buffer
_config.write(sys.stdout.buffer)
print()
# installed extensions info
print("Currently installed extensions")
ext = ExtensionEngine(config_path=config_path,
config_dict=config_dict)
ext.enumerate_extensions()
print()
# weewx archive info
try:
manager_info_dict = getManagerInfo(config_dict, db_binding_wx)
except weedb.CannotConnect as e:
print("Unable to connect to database:", e)
print()
except weedb.OperationalError as e:
print("Error hitting database. It may not be properly initialized:")
print(e)
print()
else:
units_nickname = weewx.units.unit_nicknames.get(manager_info_dict['units'], "Unknown unit constant")
print("Archive info")
print(" Database name: %s" % manager_info_dict['db_name'])
print(" Table name: %s" % manager_info_dict['table_name'])
print(" Version %s" % manager_info_dict['version'])
print(" Unit system: %s (%s)" % (manager_info_dict['units'],
units_nickname))
print(" First good timestamp: %s" % timestamp_to_string(manager_info_dict['first_ts']))
print(" Last good timestamp: %s" % timestamp_to_string(manager_info_dict['last_ts']))
if manager_info_dict['ts_count']:
print(" Number of records: %s" % manager_info_dict['ts_count'].value)
else:
print(" Number of records: %s (no archive records found)" % \
manager_info_dict['ts_count'])
# if we have a database and a table but no start or stop ts and no records
# inform the user that the database/table exists but appears empty
if (manager_info_dict['db_name'] and manager_info_dict['table_name']) and \
not (manager_info_dict['ts_count'] or manager_info_dict['units'] or \
manager_info_dict['first_ts'] or manager_info_dict['last_ts']):
print(" It is likely that the database (%s) archive table (%s)" % \
(manager_info_dict['db_name'], manager_info_dict['table_name']))
print(" exists but contains no data.")
print(" weewx (weewx.conf) is set to use an archive interval of %s seconds." % \
config_dict['StdArchive']['archive_interval'])
print(" The station hardware was not interrogated in determining archive interval.")
print()
# weewx database info
if verbosity > 0:
print("Databases configured in weewx.conf")
for db_keys in config_dict['Databases']:
database_dict = weewx.manager.get_database_dict_from_config(config_dict,
db_keys)
_ = sorted(database_dict.keys())
print(" Database name: %s" % database_dict['database_name'])
print(" Database driver: %s" % database_dict['driver'])
if 'host' in database_dict:
print(" Database host: %s" % database_dict['host'])
print()
# sqlkeys/obskeys info
if verbosity > 1:
print("Supported SQL keys")
formatListCols(manager_info_dict['sqlkeys'], 3)
print()
print("Supported observation keys")
formatListCols(manager_info_dict['obskeys'], 3)
print()
def generateDebugConf(config_dict):
""" Generate a parsed and obfuscated weewx.conf and write to sys.stdout """
# obfuscate config_dict
_obs_config_dict = obfuscateKey(config_dict,
OBFUSCATE_MAP['obfuscate'],
OBFUSCATE_MAP['do_not_obfuscate'])
# put obfuscated config_dict into weewx.conf form
if six.PY2:
_obs_config_dict.write(sys.stdout)
else:
# The ConfigObj.write() method always writes in binary,
# which is not accepted by Python 3 for output to stdout.
# So write to sys.stdout.buffer
_obs_config_dict.write(sys.stdout.buffer)
def generateSysInfo(verbosity):
# % string operator deprecated Python 3.1 and up.
print("System info")
if verbosity > 0:
print(" Platform: " + platform.platform())
print(" Python Version: " + platform.python_version())
# environment
if verbosity > 1:
print("\nEnvironment")
for n in os.environ:
print(" %s=%s" % (n, os.environ[n]))
# load info
try:
loadavg = '%.2f %.2f %.2f' % os.getloadavg()
(load1, load5, load15) = loadavg.split(" ")
print("\nLoad Information\n 1 minute load average: " + load1)
print(" 5 minute load average: " + load5)
print(" 15 minute load average: " + load15)
except OSError:
print("Sorry, the load average not available on this platform")
print()
def obfuscateKey(src_dict, obfuscate_list, retain_list):
""" Obfuscate any dictionary items whose key is contained in passed list.
Uses recursion to obfuscate any nested keys.
"""
# make a copy of our source dict as we may need to alter some entries
_dict = copy.deepcopy(src_dict)
# step through each key and value pair
for k, v in src_dict.items():
# if its a dict then recurse
if isinstance(v, dict):
_dict[k] = obfuscateKey(v, obfuscate_list, retain_list)
# must be a value so obfuscate if in our list or leave it if its not
else:
if (any(k.startswith(key) for key in obfuscate_list)
and not k in retain_list):
_dict[k] = "XXX obfuscated by wee_debug XXX"
else:
_dict[k] = v
return _dict
def get_binding(config_dict):
""" Get db_binding for the weewx database """
# Extract our binding from the StdArchive section of the config file. If
# it's missing, return None.
if 'StdArchive' in config_dict:
db_binding_wx = config_dict['StdArchive'].get('data_binding', 'wx_binding')
else:
db_binding_wx = None
return db_binding_wx
def getManagerInfo(config_dict, db_binding_wx):
""" Get info from the manager of a weewx archive for inclusion in debug
report
"""
with weewx.manager.open_manager_with_config(config_dict, db_binding_wx) as dbmanager_wx:
info = {
'db_name' : dbmanager_wx.database_name,
'table_name' : dbmanager_wx.table_name,
'version' : getattr(dbmanager_wx, 'version', 'unknown'),
'units' : dbmanager_wx.std_unit_system,
'first_ts' : dbmanager_wx.first_timestamp,
'last_ts' : dbmanager_wx.last_timestamp,
'sqlkeys' : dbmanager_wx.sqlkeys,
'obskeys' : dbmanager_wx.obskeys
}
# do we have any records in our archive?
if info['first_ts'] and info['last_ts']:
# We have some records so proceed to count them.
# Since we are (more than likely) using archive field 'dateTime' for
# our record count we need to call the getAggregate() method from our
# parent class. Note that if we change to some other field the 'count'
# might take a while longer depending on the archive size.
info['ts_count'] = super(DaySummaryManager, dbmanager_wx).getAggregate(TimeSpan(info['first_ts'], info['last_ts']),
COUNT_FIELD,
'count')
else:
info['ts_count'] = None
return info
def _readproc_dict(filename):
""" Read proc file that has 'name:value' format for each line """
info = {}
with open(filename) as fp:
for line in fp:
if line.find(':') >= 0:
(n,v) = line.split(':',1)
info[n.strip()] = v.strip()
return info
def _readproc_line(filename):
""" Read single line proc file, return the string """
with open(filename) as fp:
info = fp.read()
return info
def formatListCols(the_list, cols):
""" Format a list of strings into a given number of columns respecting the
width of the largest list entry
"""
max_width = max([len(x) for x in the_list])
justifyList = [x.ljust(max_width) for x in the_list]
lines = (' '.join(justifyList[i:i + cols])
for i in range(0, len(justifyList), cols))
print("\n".join(lines))
if __name__=="__main__" :
main()