# # Copyright (c) 2009-2023 Gary Roderick, Tom Keffer , and Matthew Wall # # See the file LICENSE.txt for your rights. # """ Generate weewx debug info """ import optparse import os import platform import sys from configobj import ConfigObj # weewx imports import weecfg import weedb import weeutil.config 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 = '1.0' # 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 = """%prog --help %prog --info [CONFIG_FILE|--config=CONFIG_FILE] [--output|--output DEBUG_PATH] [--verbosity=0|1|2] %prog --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(): # 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) # 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) print(f" {db_keys}:") for k in database_dict: print(f"{k:>20s} {database_dict[k]:<20s}") 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 """ # Make a deep copy first, as we may be altering values. config_dict_copy = weeutil.config.deep_copy(config_dict) # Turn off interpolation on the copy, so it doesn't interfere with faithful representation of # the values config_dict_copy.interpolation = False # Now obfuscate any sensitive keys obfuscate_dict(config_dict_copy, OBFUSCATE_MAP['obfuscate'], OBFUSCATE_MAP['do_not_obfuscate']) # put obfuscated config_dict into weewx.conf form # 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_dict_copy.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 obfuscate_dict(src_dict, obfuscate_list, retain_list): """ Obfuscate any dictionary items whose key is contained in passed list. """ # We need a function to be passed on to the 'walk()' function. def obfuscate_value(section, key): # Check to see if the key is in the obfuscation list. If so, then obfuscate it. if any(key.startswith(k) for k in obfuscate_list) and key not in retain_list: section[key]= "XXX obfuscated by wee_debug XXX" # Now walk the configuration dictionary, using the function src_dict.walk(obfuscate_value) 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()