"""WizardKit: Debug Functions""" # vim: sts=2 sw=2 ts=2 import inspect import logging import lzma import os import pickle import platform import re import socket import sys import time import requests from wk.cfg.net import CRASH_SERVER from wk.log import get_log_filepath, get_root_logger_path # Classes class Debug(): """Object used when dumping debug data.""" def method(self): """Dummy method used to identify functions vs data.""" # STATIC VARIABLES LOG = logging.getLogger(__name__) DEBUG_CLASS = Debug() METHOD_TYPE = type(DEBUG_CLASS.method) # Functions def generate_debug_report(): """Generate debug report, returns str.""" platform_function_list = ( 'architecture', 'machine', 'platform', 'python_version', ) report = [] # Logging data log_path = get_log_filepath() if log_path: report.append('------ Start Log -------') report.append('') with open(log_path, 'r', encoding='utf-8') as log_file: report.extend(log_file.read().splitlines()) report.append('') report.append('------- End Log --------') # System report.append('--- Start debug info ---') report.append('') report.append('[System]') report.append(f' {"FQDN":<24} {socket.getfqdn()}') for func in platform_function_list: func_name = func.replace('_', ' ').capitalize() func_result = getattr(platform, func)() report.append(f' {func_name:<24} {func_result}') report.append(f' {"Python sys.argv":<24} {sys.argv}') report.append('') # Environment report.append('[Environment Variables]') for key, value in sorted(os.environ.items()): report.append(f' {key:<24} {value}') report.append('') # Done report.append('---- End debug info ----') return '\n'.join(report) def generate_object_report(obj, indent=0): """Generate debug report for obj, returns list.""" report = [] attr_list = [] # Get attribute list if hasattr(obj, '__slots__'): attr_list = list(obj.__slots__) else: attr_list = [name for name in dir(obj) if not name.startswith('_')] # Dump object data for name in attr_list: attr = getattr(obj, name) # Skip methods if isinstance(attr, METHOD_TYPE): continue # Add attribute to report (expanded if necessary) if isinstance(attr, dict): report.append(f'{name}:') for key, value in attr.items(): report.append(f'{" "*(indent+1)}{key}: {str(value)}') else: report.append(f'{" "*indent}{name}: {str(attr)}') # Done return report def save_pickles(obj_dict, out_path=None): """Save dict of objects using pickle.""" LOG.info('Saving pickles') # Set path if not out_path: out_path = get_root_logger_path() out_path = out_path.parent.joinpath('../debug').resolve() # Save pickles try: for name, obj in obj_dict.copy().items(): if name.startswith('__') or inspect.ismodule(obj): continue with open(f'{out_path}/{name}.pickle', 'wb') as _f: pickle.dump(obj, _f, protocol=pickle.HIGHEST_PROTOCOL) except Exception: LOG.error('Failed to save all the pickles', exc_info=True) def upload_debug_report(report, compress=True, reason='DEBUG'): """Upload debug report to CRASH_SERVER as specified in wk.cfg.main.""" LOG.info('Uploading debug report to %s', CRASH_SERVER.get('Name', '?')) headers = CRASH_SERVER.get('Headers', {'X-Requested-With': 'XMLHttpRequest'}) if compress: headers['Content-Type'] = 'application/octet-stream' # Check if the required server details are available if not all(CRASH_SERVER.get(key, False) for key in ('Name', 'Url', 'User')): msg = 'Server details missing, aborting upload.' print(msg) raise RuntimeError(msg) # Set filename (based on the logging config if possible) filename = 'Unknown' log_path = get_log_filepath() if log_path: # Strip everything but the prefix filename = re.sub(r'^(.*)_(\d{4}-\d{2}-\d{2}.*)', r'\1', log_path.name) filename = f'{filename}_{reason}_{time.strftime("%Y-%m-%d_%H%M%S%z")}.log' LOG.debug('filename: %s', filename) # Compress report if compress: filename += '.xz' xz_report = lzma.compress(report.encode('utf8')) # Upload report url = f'{CRASH_SERVER["Url"]}/{filename}' response = requests.put( url, auth=(CRASH_SERVER['User'], CRASH_SERVER.get('Pass', '')), data=xz_report if compress else report, headers=headers, timeout=60, ) # Check response if not response.ok: raise RuntimeError('Failed to upload report') if __name__ == '__main__': print("This file is not meant to be called directly.")