#! @PYTHON3@ # This library is free software; you can redistribute it and/or # modify it under the terms of version 2.1 of the GNU Lesser General Public # License as published by the Free Software Foundation. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Copyright (c) 2005, 2007 XenSource Ltd. # Copyright (c) 2010, 2011, 2012, 2013, 2015, 2016, 2017 Nicira, Inc. # # To add new entries to the bugtool, you need to: # # Create a new capability. These declare the new entry to the GUI, including # the expected size, time to collect, privacy implications, and whether the # capability should be selected by default. One capability may refer to # multiple files, assuming that they can be reasonably grouped together, and # have the same privacy implications. You need: # # A new CAP_ constant. # A cap() invocation to declare the capability. # # You then need to add calls to main() to collect the files. These will # typically be calls to the helpers file_output(), tree_output(), cmd_output(), # or func_output(). # from io import BytesIO import fcntl import getopt import hashlib import os import platform import re import sys import tarfile import time import warnings import zipfile from select import select from signal import SIGTERM from subprocess import PIPE, Popen, check_output from xml.dom.minidom import getDOMImplementation, parse warnings.filterwarnings(action="ignore", category=DeprecationWarning) OS_RELEASE = platform.release() # # Files & directories # APT_SOURCES_LIST = "/etc/apt/sources.list" APT_SOURCES_LIST_D = "/etc/apt/sources.list.d" BUG_DIR = "/var/log/ovs-bugtool" PLUGIN_DIR = "@pkgdatadir@/bugtool-plugins" GRUB_CONFIG = '/boot/grub/menu.lst' BOOT_KERNEL = '/boot/vmlinuz-' + OS_RELEASE BOOT_INITRD = '/boot/initrd-' + OS_RELEASE + '.img' PROC_PARTITIONS = '/proc/partitions' FSTAB = '/etc/fstab' PROC_MOUNTS = '/proc/mounts' ISCSI_CONF = '/etc/iscsi/iscsid.conf' ISCSI_INITIATOR = '/etc/iscsi/initiatorname.iscsi' PROC_CPUINFO = '/proc/cpuinfo' PROC_MEMINFO = '/proc/meminfo' PROC_IOPORTS = '/proc/ioports' PROC_INTERRUPTS = '/proc/interrupts' PROC_SCSI = '/proc/scsi/scsi' PROC_VERSION = '/proc/version' PROC_MODULES = '/proc/modules' PROC_DEVICES = '/proc/devices' PROC_FILESYSTEMS = '/proc/filesystems' PROC_CMDLINE = '/proc/cmdline' PROC_CONFIG = '/proc/config.gz' PROC_USB_DEV = '/proc/bus/usb/devices' PROC_NET_BONDING_DIR = '/proc/net/bonding' IFCFG_RE = re.compile(r'^.*/ifcfg-.*') ROUTE_RE = re.compile(r'^.*/route-.*') SYSCONFIG_HWCONF = '/etc/sysconfig/hwconf' SYSCONFIG_NETWORK = '/etc/sysconfig/network' SYSCONFIG_NETWORK_SCRIPTS = '/etc/sysconfig/network-scripts' INTERFACES = '/etc/network/interfaces' INTERFACESD = '/etc/network/interfaces.d' PROC_NET_VLAN_DIR = '/proc/net/vlan' PROC_NET_SOFTNET_STAT = '/proc/net/softnet_stat' MODPROBE_CONF = '/etc/modprobe.conf' MODPROBE_DIR = '/etc/modprobe.d' RESOLV_CONF = '/etc/resolv.conf' MPP_CONF = '/etc/mpp.conf' MULTIPATH_CONF = '/etc/multipath.conf' NSSWITCH_CONF = '/etc/nsswitch.conf' NTP_CONF = '/etc/ntp.conf' IPTABLES_CONFIG = '/etc/sysconfig/iptables-config' HOSTS = '/etc/hosts' HOSTS_ALLOW = '/etc/hosts.allow' HOSTS_DENY = '/etc/hosts.deny' DHCP_LEASE_DIR = ['/var/lib/dhclient', '/var/lib/dhcp3'] OPENVSWITCH_LOG_DIR = '@LOGDIR@/' OPENVSWITCH_DEFAULT_SWITCH = '/etc/default/openvswitch-switch' # Debian OPENVSWITCH_SYSCONFIG_SWITCH = '/etc/sysconfig/openvswitch' # RHEL OPENVSWITCH_CONF_DB = '@DBDIR@/conf.db' OPENVSWITCH_COMPACT_DB = '@DBDIR@/bugtool-compact-conf.db' OPENVSWITCH_VSWITCHD_PID = '@RUNDIR@/ovs-vswitchd.pid' VAR_LOG_DIR = '/var/log/' VAR_LOG_CORE_DIR = '/var/log/core' YUM_LOG = '/var/log/yum.log' YUM_REPOS_DIR = '/etc/yum.repos.d' # # External programs # os.environ['PATH'] = '/usr/local/sbin:/usr/local/bin:' \ '/usr/sbin:/usr/bin:/sbin:/bin:@pkgdatadir@/scripts' ARP = 'arp' CAT = 'cat' CHKCONFIG = 'chkconfig' DATE = 'date' DF = 'df' DMESG = 'dmesg' DMIDECODE = 'dmidecode' DMSETUP = 'dmsetup' DPKG_QUERY = 'dpkg-query' ETHTOOL = 'ethtool' FDISK = 'fdisk' FIND = 'find' IP = 'ip' IPTABLES = 'iptables' ISCSIADM = 'iscsiadm' LOSETUP = 'losetup' LS = 'ls' LSPCI = 'lspci' MODINFO = 'modinfo' MPPUTIL = 'mppUtil' MULTIPATHD = 'multipathd' NETSTAT = 'netstat' OVS_DPCTL = 'ovs-dpctl' OVS_OFCTL = 'ovs-ofctl' OVS_VSCTL = 'ovs-vsctl' PS = 'ps' ROUTE = 'route' RPM = 'rpm' SG_MAP = 'sg_map' SHA256_SUM = 'sha256sum' SYSCTL = 'sysctl' TC = 'tc' UPTIME = 'uptime' ZCAT = 'zcat' # # PII -- Personally identifiable information. Of particular concern are # things that would identify customers, or their network topology. # Passwords are never to be included in any bug report, regardless of any PII # declaration. # # NO -- No PII will be in these entries. # YES -- PII will likely or certainly be in these entries. # MAYBE -- The user may wish to audit these entries for PII. # IF_CUSTOMIZED -- If the files are unmodified, then they will contain no PII, # but since we encourage customers to edit these files, PII may have been # introduced by the customer. This is used in particular for the networking # scripts in dom0. # PII_NO = 'no' PII_YES = 'yes' PII_MAYBE = 'maybe' PII_IF_CUSTOMIZED = 'if_customized' KEY = 0 PII = 1 MIN_SIZE = 2 MAX_SIZE = 3 MIN_TIME = 4 MAX_TIME = 5 MIME = 6 CHECKED = 7 HIDDEN = 8 MIME_DATA = 'application/data' MIME_TEXT = 'text/plain' INVENTORY_XML_ROOT = "system-status-inventory" INVENTORY_XML_SUMMARY = 'system-summary' INVENTORY_XML_ELEMENT = 'inventory-entry' CAP_XML_ROOT = "system-status-capabilities" CAP_XML_ELEMENT = 'capability' CAP_BOOT_LOADER = 'boot-loader' CAP_DISK_INFO = 'disk-info' CAP_HARDWARE_INFO = 'hardware-info' CAP_KERNEL_INFO = 'kernel-info' CAP_LOSETUP_A = 'loopback-devices' CAP_MULTIPATH = 'multipath' CAP_NETWORK_CONFIG = 'network-config' CAP_NETWORK_INFO = 'network-info' CAP_NETWORK_STATUS = 'network-status' CAP_OPENVSWITCH_LOGS = 'ovs-system-logs' CAP_PROCESS_LIST = 'process-list' CAP_SYSTEM_LOGS = 'system-logs' CAP_SYSTEM_SERVICES = 'system-services' CAP_YUM = 'yum' KB = 1024 MB = 1024 * 1024 caps = {} cap_sizes = {} unlimited_data = False dbg = False # Default value for the number of days to collect logs. log_days = 20 log_last_mod_time = None free_disk_space = None # Default value for delay between repeated commands command_delay = 10 def cap(key, pii=PII_MAYBE, min_size=-1, max_size=-1, min_time=-1, max_time=-1, mime=MIME_TEXT, checked=True, hidden=False): caps[key] = (key, pii, min_size, max_size, min_time, max_time, mime, checked, hidden) cap_sizes[key] = 0 cap(CAP_BOOT_LOADER, PII_NO, max_size=3 * KB, max_time=5) cap(CAP_DISK_INFO, PII_MAYBE, max_size=50 * KB, max_time=20) cap(CAP_HARDWARE_INFO, PII_MAYBE, max_size=2 * MB, max_time=20) cap(CAP_KERNEL_INFO, PII_MAYBE, max_size=120 * KB, max_time=5) cap(CAP_LOSETUP_A, PII_MAYBE, max_size=KB, max_time=5) cap(CAP_MULTIPATH, PII_MAYBE, max_size=20 * KB, max_time=10) cap(CAP_NETWORK_CONFIG, PII_IF_CUSTOMIZED, min_size=0, max_size=5 * MB) cap(CAP_NETWORK_INFO, PII_YES, max_size=50 * MB, max_time=30) cap(CAP_NETWORK_STATUS, PII_YES, max_size=-1, max_time=30) cap(CAP_OPENVSWITCH_LOGS, PII_MAYBE, max_size=-1, max_time=5) cap(CAP_PROCESS_LIST, PII_YES, max_size=30 * KB, max_time=20) cap(CAP_SYSTEM_LOGS, PII_MAYBE, max_size=200 * MB, max_time=5) cap(CAP_SYSTEM_SERVICES, PII_NO, max_size=5 * KB, max_time=20) cap(CAP_YUM, PII_IF_CUSTOMIZED, max_size=10 * KB, max_time=30) ANSWER_YES_TO_ALL = False SILENT_MODE = False entries = None data = {} dev_null = open('/dev/null', 'r+') def output(x): global SILENT_MODE if not SILENT_MODE: print(x) def output_ts(x): output("[%s] %s" % (time.strftime("%x %X %Z"), x)) def cmd_output(cap, args, label=None, filter=None, binary=False, repeat_count=1): if cap in entries: if not label: if isinstance(args, list): a = [aa for aa in args] a[0] = os.path.basename(a[0]) label = ' '.join(a) else: label = args data[label] = {'cap': cap, 'cmd_args': args, 'filter': filter, 'binary': binary, 'repeat_count': repeat_count} def file_output(cap, path_list, newest_first=False, last_mod_time=None): """ If newest_first is True, the list of files in path_list is sorted by file modification time in descending order, else its sorted in ascending order. """ if cap in entries: path_entries = [] for path in path_list: try: s = os.stat(path) except OSError: continue if last_mod_time is None or s.st_mtime >= last_mod_time: path_entries.append((path, s)) def mtime(arg): (path, stat) = arg return stat.st_mtime path_entries.sort(key=mtime, reverse=newest_first) for p in path_entries: if check_space(cap, p[0], p[1].st_size): data[p] = {'cap': cap, 'filename': p[0]} def tree_output(cap, path, pattern=None, negate=False, newest_first=False, last_mod_time=None): """ Walks the directory tree rooted at path. Files in current dir are processed before files in sub-dirs. """ if cap in entries: if os.path.exists(path): for root, dirs, files in os.walk(path): fns = [fn for fn in [os.path.join(root, f) for f in files] if os.path.isfile(fn) and matches(fn, pattern, negate)] file_output(cap, fns, newest_first=newest_first, last_mod_time=last_mod_time) def prefix_output(cap, prefix, newest_first=False, last_mod_time=None): """ Output files with the same prefix. """ fns = [] for root, dirs, files in os.walk(os.path.dirname(prefix)): fns += [fn for fn in [os.path.join(root, f) for f in files] if fn.startswith(prefix)] file_output(cap, fns, newest_first=newest_first, last_mod_time=last_mod_time) def func_output(cap, label, func): if cap in entries: data[label] = {'cap': cap, 'func': func} def collect_data(): first_run = True while True: process_lists = {} for (k, v) in data.items(): cap = v['cap'] if 'cmd_args' in v: if 'output' not in v.keys(): v['output'] = BytesIOmtime() if v['repeat_count'] > 0: if cap not in process_lists: process_lists[cap] = [] process_lists[cap].append( ProcOutput(v['cmd_args'], caps[cap][MAX_TIME], v['output'], v['filter'], v['binary'])) v['repeat_count'] -= 1 if bool(process_lists): if not first_run: output_ts("Waiting %d sec between repeated commands" % command_delay) time.sleep(command_delay) else: first_run = False run_procs(process_lists.values()) else: break for (k, v) in data.items(): cap = v['cap'] if 'filename' in v and v['filename'].startswith('/proc/'): # proc files must be read into memory try: f = open(v['filename'], 'rb') s = f.read() f.close() if check_space(cap, v['filename'], len(s)): v['output'] = BytesIOmtime(s) except: pass elif 'func' in v: try: s = v['func'](cap) except Exception as e: s = str(e).encode() if check_space(cap, k, len(s)): if isinstance(s, str): v['output'] = BytesIOmtime(s.encode()) else: v['output'] = BytesIOmtime(s) def main(argv=None): global ANSWER_YES_TO_ALL, SILENT_MODE global entries, data, dbg, unlimited_data, free_disk_space global log_days, log_last_mod_time, command_delay # Filter flags only_ovs_info = False collect_all_info = True if '--help' in sys.argv: print(""" %(argv0)s: create status report bundles to assist in problem diagnosis usage: %(argv0)s OPTIONS By default, %(argv0)s prompts for permission to collect each form of status information and produces a .tar.gz file as output. The following options are available. --help display this help message, then exit -s, --silent suppress most output to stdout Options for categories of data to collect: --entries=CAP_A,CAP_B,... set categories of data to collect --all collect all categories --ovs collect only directly OVS-related info --log-days=DAYS collect DAYS worth of old logs --delay=DELAY set delay between repeated command -y, --yestoall suppress prompts to confirm collection --capabilities print categories as XML on stdout, then exit Output options: --output=FORMAT set output format to one of tar tar.bz2 tar.gz zip --outfile=FILE write output to FILE --outfd=FD write output to FD (requires --output=tar) --unlimited ignore default limits on sizes of data collected --debug print ovs-bugtool debug info on stdout\ """ % {'argv0': sys.argv[0]}) sys.exit(0) # we need access to privileged files, exit if we are not running as root if os.getuid() != 0: print("Error: ovs-bugtool must be run as root", file=sys.stderr) return 1 output_file = None output_type = 'tar.gz' output_fd = -1 if argv is None: argv = sys.argv try: (options, params) = getopt.gnu_getopt( argv, 'sy', ['capabilities', 'silent', 'yestoall', 'entries=', 'output=', 'outfd=', 'outfile=', 'all', 'unlimited', 'debug', 'ovs', 'log-days=', 'delay=']) except getopt.GetoptError as opterr: print(opterr, file=sys.stderr) return 2 try: load_plugins(True) except: pass entries = [e for e in caps.keys() if caps[e][CHECKED]] for (k, v) in options: if k == '--capabilities': update_capabilities() print_capabilities() return 0 if k == '--output': if v in ['tar', 'tar.bz2', 'tar.gz', 'zip']: output_type = v else: print("Invalid output format '%s'" % v, file=sys.stderr) return 2 # "-s" or "--silent" means suppress output (except for the final # output filename at the end) if k in ['-s', '--silent']: SILENT_MODE = True if k == '--entries' and v != '': entries = v.split(',') # If the user runs the script with "-y" or "--yestoall" we don't ask # all the really annoying questions. if k in ['-y', '--yestoall']: ANSWER_YES_TO_ALL = True if k == '--outfd': output_fd = int(v) try: old = fcntl.fcntl(output_fd, fcntl.F_GETFD) fcntl.fcntl(output_fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC) except: print("Invalid output file descriptor", output_fd, file=sys.stderr) return 2 if k == '--outfile': output_file = v elif k == '--all': entries = caps.keys() elif k == '--unlimited': unlimited_data = True elif k == '--debug': dbg = True ProcOutput.debug = True if k == '--ovs': only_ovs_info = True collect_all_info = False if k == '--log-days': log_days = int(v) if k == '--delay': command_delay = int(v) if len(params) != 1: print("Invalid additional arguments", str(params), file=sys.stderr) return 2 if output_fd != -1 and output_type != 'tar': print("Option '--outfd' only valid with '--output=tar'", file=sys.stderr) return 2 if output_fd != -1 and output_file is not None: print("Cannot set both '--outfd' and '--outfile'", file=sys.stderr) return 2 if output_file is not None and not unlimited_data: free_disk_space = get_free_disk_space(output_file) * 90 / 100 log_last_mod_time = int(time.time()) - log_days * 86400 if ANSWER_YES_TO_ALL: output("Warning: '--yestoall' argument provided, will not prompt " "for individual files.") output(''' This application will collate dmesg output, details of the hardware configuration of your machine, information about the build of openvswitch that you are using, plus, if you allow it, various logs. The collated information will be saved as a .%s for archiving or sending to a Technical Support Representative. The logs may contain private information, and if you are at all worried about that, you should exit now, or you should explicitly exclude those logs from the archive. ''' % output_type) # assemble potential data file_output(CAP_BOOT_LOADER, [GRUB_CONFIG]) cmd_output(CAP_BOOT_LOADER, [LS, '-lR', '/boot']) cmd_output(CAP_BOOT_LOADER, [SHA256_SUM, BOOT_KERNEL, BOOT_INITRD], label='vmlinuz-initrd.sha256sum') cmd_output(CAP_DISK_INFO, [FDISK, '-l']) file_output(CAP_DISK_INFO, [PROC_PARTITIONS, PROC_MOUNTS]) file_output(CAP_DISK_INFO, [FSTAB, ISCSI_CONF, ISCSI_INITIATOR]) cmd_output(CAP_DISK_INFO, [DF, '-alT']) cmd_output(CAP_DISK_INFO, [DF, '-alTi']) if len(pidof('iscsid')) != 0: cmd_output(CAP_DISK_INFO, [ISCSIADM, '-m', 'node']) cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/scsi_host']) cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/scsi_disk']) cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/fc_transport']) cmd_output(CAP_DISK_INFO, [SG_MAP, '-x']) func_output(CAP_DISK_INFO, 'scsi-hosts', dump_scsi_hosts) file_output(CAP_HARDWARE_INFO, [PROC_CPUINFO, PROC_MEMINFO, PROC_IOPORTS, PROC_INTERRUPTS]) cmd_output(CAP_HARDWARE_INFO, [DMIDECODE]) cmd_output(CAP_HARDWARE_INFO, [LSPCI, '-n']) cmd_output(CAP_HARDWARE_INFO, [LSPCI, '-vv']) file_output(CAP_HARDWARE_INFO, [PROC_USB_DEV, PROC_SCSI]) file_output(CAP_HARDWARE_INFO, [SYSCONFIG_HWCONF]) cmd_output(CAP_HARDWARE_INFO, [LS, '-lR', '/dev']) file_output(CAP_KERNEL_INFO, [PROC_VERSION, PROC_MODULES, PROC_DEVICES, PROC_FILESYSTEMS, PROC_CMDLINE]) cmd_output(CAP_KERNEL_INFO, [ZCAT, PROC_CONFIG], label='config') cmd_output(CAP_KERNEL_INFO, [SYSCTL, '-A']) file_output(CAP_KERNEL_INFO, [MODPROBE_CONF]) tree_output(CAP_KERNEL_INFO, MODPROBE_DIR) func_output(CAP_KERNEL_INFO, 'modinfo', module_info) cmd_output(CAP_LOSETUP_A, [LOSETUP, '-a']) file_output(CAP_MULTIPATH, [MULTIPATH_CONF, MPP_CONF]) cmd_output(CAP_MULTIPATH, [DMSETUP, 'table']) func_output(CAP_MULTIPATH, 'multipathd_topology', multipathd_topology) cmd_output(CAP_MULTIPATH, [MPPUTIL, '-a']) if CAP_MULTIPATH in entries and collect_all_info: dump_rdac_groups(CAP_MULTIPATH) tree_output(CAP_NETWORK_CONFIG, SYSCONFIG_NETWORK_SCRIPTS, IFCFG_RE) tree_output(CAP_NETWORK_CONFIG, SYSCONFIG_NETWORK_SCRIPTS, ROUTE_RE) tree_output(CAP_NETWORK_CONFIG, INTERFACESD) file_output(CAP_NETWORK_CONFIG, [SYSCONFIG_NETWORK, RESOLV_CONF, NSSWITCH_CONF, HOSTS, INTERFACES]) file_output(CAP_NETWORK_CONFIG, [NTP_CONF, IPTABLES_CONFIG, HOSTS_ALLOW, HOSTS_DENY]) file_output(CAP_NETWORK_CONFIG, [OPENVSWITCH_DEFAULT_SWITCH, OPENVSWITCH_SYSCONFIG_SWITCH]) cmd_output(CAP_NETWORK_INFO, [IP, 'addr', 'show']) cmd_output(CAP_NETWORK_INFO, [ROUTE, '-n']) cmd_output(CAP_NETWORK_INFO, [ARP, '-n']) cmd_output(CAP_NETWORK_INFO, [NETSTAT, '-an']) for dir in DHCP_LEASE_DIR: tree_output(CAP_NETWORK_INFO, dir) for table in ['filter', 'nat', 'mangle', 'raw', 'security']: cmd_output(CAP_NETWORK_INFO, [IPTABLES, '-t', table, '-nL']) for p in os.listdir('/sys/class/net/'): try: f = open('/sys/class/net/%s/type' % p, 'r') t = f.readline() f.close() if os.path.islink('/sys/class/net/%s/device' % p) and int(t) == 1: # ARPHRD_ETHER cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-S', p]) if not p.startswith('vif') and not p.startswith('tap'): cmd_output(CAP_NETWORK_INFO, [ETHTOOL, p]) cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-k', p]) cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-i', p]) cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-c', p]) cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-l', p]) if int(t) == 1: cmd_output(CAP_NETWORK_INFO, [TC, '-s', '-d', 'class', 'show', 'dev', p]) except: pass tree_output(CAP_NETWORK_INFO, PROC_NET_BONDING_DIR) tree_output(CAP_NETWORK_INFO, PROC_NET_VLAN_DIR) cmd_output(CAP_NETWORK_INFO, [TC, '-s', 'qdisc']) file_output(CAP_NETWORK_INFO, [PROC_NET_SOFTNET_STAT]) collect_ovsdb() if os.path.exists(OPENVSWITCH_VSWITCHD_PID): cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'show', '-s']) for d in dp_list(): cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'dump-flows', '-m', d.decode()]) cmd_output(CAP_PROCESS_LIST, [PS, 'wwwaxf', '-eo', 'pid,tty,stat,time,nice,psr,pcpu,pmem,nwchan,wchan:25,args'], label='process-tree') func_output(CAP_PROCESS_LIST, 'fd_usage', fd_usage) system_logs = ([VAR_LOG_DIR + x for x in ['crit.log', 'kern.log', 'daemon.log', 'user.log', 'syslog', 'messages', 'secure', 'debug', 'dmesg', 'boot']]) for log in system_logs: prefix_output(CAP_SYSTEM_LOGS, log, last_mod_time=log_last_mod_time) ovs_logs = ([OPENVSWITCH_LOG_DIR + x for x in ['ovs-vswitchd.log', 'ovsdb-server.log', 'ovs-ctl.log']]) for log in ovs_logs: prefix_output(CAP_OPENVSWITCH_LOGS, log, last_mod_time=log_last_mod_time) cmd_output(CAP_SYSTEM_LOGS, [DMESG]) cmd_output(CAP_SYSTEM_SERVICES, [CHKCONFIG, '--list']) tree_output(CAP_SYSTEM_LOGS, VAR_LOG_CORE_DIR) file_output(CAP_YUM, [YUM_LOG]) tree_output(CAP_YUM, YUM_REPOS_DIR) cmd_output(CAP_YUM, [RPM, '-qa']) file_output(CAP_YUM, [APT_SOURCES_LIST]) tree_output(CAP_YUM, APT_SOURCES_LIST_D) cmd_output(CAP_YUM, [DPKG_QUERY, '-W', '-f=${Package} ${Version} ${Status}\n'], 'dpkg-packages') # Filter out ovs relevant information if --ovs option passed # else collect all information filters = set() if only_ovs_info: filters.add('ovs') ovs_info_caps = [CAP_NETWORK_STATUS, CAP_SYSTEM_LOGS, CAP_OPENVSWITCH_LOGS, CAP_NETWORK_CONFIG] ovs_info_list = ['process-tree'] # We cannot use items(), since we modify 'data' as we pass through for (k, v) in list(data.items()): cap = v['cap'] if 'filename' in v: info = k[0] else: info = k if info not in ovs_info_list and cap not in ovs_info_caps: del data[k] if filters: filter = ",".join(filters) else: filter = None try: load_plugins(filter=filter) except: pass # permit the user to filter out data # We cannot use items(), since we modify 'data' as we pass through for (k, v) in list(data.items()): cap = v['cap'] if 'filename' in v: key = k[0] else: key = k if not ANSWER_YES_TO_ALL and not yes("Include '%s'? [Y/n]: " % key): del data[k] # collect selected data now output_ts('Running commands to collect data') collect_data() subdir = "bug-report-%s" % time.strftime("%Y%m%d%H%M%S") # include inventory data['inventory.xml'] = {'cap': None, 'output': BytesIOmtime(make_inventory(data, subdir))} # create archive if output_fd == -1: if output_file is None: dirname = BUG_DIR else: dirname = os.path.dirname(output_file) if dirname and not os.path.exists(dirname): try: os.makedirs(dirname) except: pass if output_fd == -1: output_ts('Creating output file') if output_type.startswith('tar'): make_tar(subdir, output_type, output_fd, output_file) else: make_zip(subdir, output_file) if dbg: print("Category sizes (max, actual):\n", file=sys.stderr) for c in caps.keys(): print(" %s (%d, %d)" % (c, caps[c][MAX_SIZE], cap_sizes[c]), file=sys.stderr) cleanup_ovsdb() return 0 def dump_scsi_hosts(cap): output = '' scsi_list = os.listdir('/sys/class/scsi_host') scsi_list.sort() for h in scsi_list: procname = '' try: f = open('/sys/class/scsi_host/%s/proc_name' % h) procname = f.readline().strip("\n") f.close() except: pass modelname = None try: f = open('/sys/class/scsi_host/%s/model_name' % h) modelname = f.readline().strip("\n") f.close() except: pass output += "%s:\n" % h output += " %s%s\n" \ % (procname, modelname and (" -> %s" % modelname) or '') return output def module_info(cap): output = BytesIO() modules = open(PROC_MODULES, 'r') procs = [] for line in modules: module = line.split()[0] procs.append(ProcOutput([MODINFO, module], caps[cap][MAX_TIME], output)) modules.close() run_procs([procs]) return output.getvalue() def multipathd_topology(cap): pipe = Popen([MULTIPATHD, '-k'], bufsize=1, stdin=PIPE, stdout=PIPE, stderr=dev_null) stdout, stderr = pipe.communicate('show topology') return stdout def dp_list(): output = BytesIO() procs = [ProcOutput([OVS_DPCTL, 'dump-dps'], caps[CAP_NETWORK_STATUS][MAX_TIME], output)] run_procs([procs]) if not procs[0].timed_out: return output.getvalue().splitlines() return [] def collect_ovsdb(): if not os.path.isfile(OPENVSWITCH_CONF_DB): return max_size = 10 * MB try: if os.path.getsize(OPENVSWITCH_CONF_DB) > max_size: if os.path.isfile(OPENVSWITCH_COMPACT_DB): os.unlink(OPENVSWITCH_COMPACT_DB) output = BytesIO() max_time = 5 procs = [ProcOutput(['ovsdb-tool', 'compact', OPENVSWITCH_CONF_DB, OPENVSWITCH_COMPACT_DB], max_time, output)] run_procs([procs]) file_output(CAP_NETWORK_STATUS, [OPENVSWITCH_COMPACT_DB]) else: file_output(CAP_NETWORK_STATUS, [OPENVSWITCH_CONF_DB]) except OSError: return def cleanup_ovsdb(): try: if os.path.isfile(OPENVSWITCH_COMPACT_DB): os.unlink(OPENVSWITCH_COMPACT_DB) except: return def fd_usage(cap): output = '' fd_dict = {} for d in [p for p in os.listdir('/proc') if p.isdigit()]: try: fh = open('/proc/' + d + '/cmdline') name = fh.readline() num_fds = len(os.listdir(os.path.join('/proc/' + d + '/fd'))) if num_fds > 0: if num_fds not in fd_dict: fd_dict[num_fds] = [] fd_dict[num_fds].append(name.replace('\0', ' ').strip()) finally: fh.close() keys = fd_dict.keys() keys.sort(lambda a, b: int(b) - int(a)) for k in keys: output += "%s: %s\n" % (k, str(fd_dict[k])) return output def dump_rdac_groups(cap): output = BytesIO() procs = [ProcOutput([MPPUTIL, '-a'], caps[cap][MAX_TIME], output)] run_procs([procs]) if not procs[0].timed_out: proc_line = 0 for line in output.getvalue().splitlines(): if line.startswith('ID'): proc_line = 2 elif line.startswith('----'): proc_line -= 1 elif proc_line > 0: group, _ = line.split(None, 1) cmd_output(cap, [MPPUTIL, '-g', group]) def load_plugins(just_capabilities=False, filter=None): global log_last_mod_time def getText(nodelist): rc = "" for node in nodelist: if node.nodeType == node.TEXT_NODE: rc += node.data return rc def getBoolAttr(el, attr, default=False): ret = default val = el.getAttribute(attr).lower() if val in ['true', 'false', 'yes', 'no']: ret = val in ['true', 'yes'] return ret for dir in [d for d in os.listdir(PLUGIN_DIR) if os.path.isdir(os.path.join(PLUGIN_DIR, d))]: if dir not in caps: if not os.path.exists("%s/%s.xml" % (PLUGIN_DIR, dir)): continue xmldoc = parse("%s/%s.xml" % (PLUGIN_DIR, dir)) assert xmldoc.documentElement.tagName == "capability" pii, min_size, max_size, min_time, max_time, mime = \ PII_MAYBE, -1, -1, -1, -1, MIME_TEXT if xmldoc.documentElement.getAttribute("pii") in \ [PII_NO, PII_YES, PII_MAYBE, PII_IF_CUSTOMIZED]: pii = xmldoc.documentElement.getAttribute("pii") if xmldoc.documentElement.getAttribute("min_size") != '': min_size = int( xmldoc.documentElement.getAttribute("min_size")) if xmldoc.documentElement.getAttribute("max_size") != '': max_size = int( xmldoc.documentElement.getAttribute("max_size")) if xmldoc.documentElement.getAttribute("min_time") != '': min_time = int(xmldoc.documentElement.getAttribute("min_time")) if xmldoc.documentElement.getAttribute("max_time") != '': max_time = int(xmldoc.documentElement.getAttribute("max_time")) if xmldoc.documentElement.getAttribute("mime") in \ [MIME_DATA, MIME_TEXT]: mime = xmldoc.documentElement.getAttribute("mime") checked = getBoolAttr(xmldoc.documentElement, 'checked', True) hidden = getBoolAttr(xmldoc.documentElement, 'hidden', False) cap(dir, pii, min_size, max_size, min_time, max_time, mime, checked, hidden) if just_capabilities: continue plugdir = os.path.join(PLUGIN_DIR, dir) for file in [f for f in os.listdir(plugdir) if f.endswith('.xml')]: xmldoc = parse(os.path.join(plugdir, file)) assert xmldoc.documentElement.tagName == "collect" for el in xmldoc.documentElement.getElementsByTagName("*"): filters_tmp = el.getAttribute("filters") if filters_tmp == '': filters = [] else: filters = filters_tmp.split(',') if not (filter is None or filter in filters): continue if el.tagName == "files": newest_first = getBoolAttr(el, 'newest_first') if el.getAttribute("type") == "logs": for fn in getText(el.childNodes).split(): prefix_output(dir, fn, newest_first=newest_first, last_mod_time=log_last_mod_time) else: file_output(dir, getText(el.childNodes).split(), newest_first=newest_first) elif el.tagName == "directory": pattern = el.getAttribute("pattern") if pattern == '': pattern = None negate = getBoolAttr(el, 'negate') newest_first = getBoolAttr(el, 'newest_first') if el.getAttribute("type") == "logs": tree_output(dir, getText(el.childNodes), pattern and re.compile(pattern) or None, negate=negate, newest_first=newest_first, last_mod_time=log_last_mod_time) else: tree_output(dir, getText(el.childNodes), pattern and re.compile(pattern) or None, negate=negate, newest_first=newest_first) elif el.tagName == "command": label = el.getAttribute("label") if label == '': label = None binary = getBoolAttr(el, 'binary') try: repeat_count = int(el.getAttribute("repeat")) if repeat_count > 1: cmd_output(dir, DATE + ';' + getText(el.childNodes), label, binary=binary, repeat_count=repeat_count) else: cmd_output(dir, getText(el.childNodes), label, binary=binary, repeat_count=repeat_count) except: cmd_output(dir, getText(el.childNodes), label, binary=binary) def make_tar(subdir, suffix, output_fd, output_file): global SILENT_MODE, data mode = 'w' if suffix == 'tar.bz2': mode = 'w:bz2' elif suffix == 'tar.gz': mode = 'w:gz' if output_fd == -1: if output_file is None: filename = "%s/%s.%s" % (BUG_DIR, subdir, suffix) else: filename = output_file old_umask = os.umask(0o077) tf = tarfile.open(filename, mode) os.umask(old_umask) else: tf = tarfile.open(None, 'w', os.fdopen(output_fd, 'a')) try: for (k, v) in data.items(): try: tar_filename = os.path.join(subdir, construct_filename(k, v)) ti = tarfile.TarInfo(tar_filename) ti.uname = 'root' ti.gname = 'root' if 'output' in v: ti.mtime = v['output'].mtime ti.size = len(v['output'].getvalue()) v['output'].seek(0) tf.addfile(ti, v['output']) elif 'filename' in v: s = os.stat(v['filename']) ti.mtime = s.st_mtime ti.size = s.st_size tf.addfile(ti, open(v['filename'], 'rb')) except: pass finally: tf.close() if output_fd == -1: output('Writing tarball %s successful.' % filename) if SILENT_MODE: print(filename) def make_zip(subdir, output_file): global SILENT_MODE, data if output_file is None: filename = "%s/%s.zip" % (BUG_DIR, subdir) else: filename = output_file old_umask = os.umask(0o077) zf = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED) os.umask(old_umask) try: for (k, v) in data.items(): try: dest = os.path.join(subdir, construct_filename(k, v)) if 'output' in v: zf.writestr(dest, v['output'].getvalue()) else: if os.stat(v['filename']).st_size < 50: compress_type = zipfile.ZIP_STORED else: compress_type = zipfile.ZIP_DEFLATED zf.write(v['filename'], dest, compress_type) except: pass finally: zf.close() output('Writing archive %s successful.' % filename) if SILENT_MODE: print(filename) def make_inventory(inventory, subdir): document = getDOMImplementation().createDocument( None, INVENTORY_XML_ROOT, None) # create summary entry s = document.createElement(INVENTORY_XML_SUMMARY) user = os.getenv('SUDO_USER', os.getenv('USER')) if user: s.setAttribute('user', user) s.setAttribute('date', time.strftime('%c')) s.setAttribute('hostname', platform.node()) s.setAttribute('uname', ' '.join(platform.uname())) s.setAttribute('uptime', check_output(UPTIME).decode()) document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(s) map(lambda k_v: inventory_entry(document, subdir, k_v[0], k_v[1]), inventory.items()) return document.toprettyxml().encode() def inventory_entry(document, subdir, k, v): try: el = document.createElement(INVENTORY_XML_ELEMENT) el.setAttribute('capability', v['cap']) el.setAttribute('filename', os.path.join(subdir, construct_filename(k, v))) el.setAttribute('sha256sum', sha256(v)) document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(el) except: pass def sha256(d): m = hashlib.sha256() if 'filename' in d: f = open(d['filename']) data = f.read(1024) while len(data) > 0: m.update(data) data = f.read(1024) f.close() elif 'output' in d: m.update(d['output'].getvalue()) return m.hexdigest() def construct_filename(k, v): if 'filename' in v: if v['filename'][0] == '/': return v['filename'][1:] else: return v['filename'] s = k.replace(' ', '-') s = s.replace('--', '-') s = s.replace('/', '%') if s.find('.') == -1: s += '.out' return s def update_capabilities(): pass def update_cap_size(cap, size): update_cap(cap, MIN_SIZE, size) update_cap(cap, MAX_SIZE, size) update_cap(cap, CHECKED, size > 0) def update_cap(cap, k, v): global caps temp = list(caps[cap]) temp[k] = v caps[cap] = tuple(temp) def size_of_dir(d, pattern=None, negate=False): if os.path.isdir(d): return size_of_all([os.path.join(d, fn) for fn in os.listdir(d)], pattern, negate) else: return 0 def size_of_all(files, pattern=None, negate=False): return sum([size_of(f, pattern, negate) for f in files]) def matches(f, pattern, negate): if negate: return not matches(f, pattern, False) else: return pattern is None or pattern.match(f) def size_of(f, pattern, negate): if os.path.isfile(f) and matches(f, pattern, negate): return os.stat(f)[6] else: return size_of_dir(f, pattern, negate) def print_capabilities(): document = getDOMImplementation().createDocument( "ns", CAP_XML_ROOT, None) map(lambda key: capability(document, key), [k for k in caps.keys() if not caps[k][HIDDEN]]) print(document.toprettyxml()) def capability(document, key): c = caps[key] el = document.createElement(CAP_XML_ELEMENT) el.setAttribute('key', c[KEY]) el.setAttribute('pii', c[PII]) el.setAttribute('min-size', str(c[MIN_SIZE])) el.setAttribute('max-size', str(c[MAX_SIZE])) el.setAttribute('min-time', str(c[MIN_TIME])) el.setAttribute('max-time', str(c[MAX_TIME])) el.setAttribute('content-type', c[MIME]) el.setAttribute('default-checked', c[CHECKED] and 'yes' or 'no') document.getElementsByTagName(CAP_XML_ROOT)[0].appendChild(el) def prettyDict(d): format = '%%-%ds: %%s' % max(map(len, [k for k, _ in d.items()])) return '\n'.join([format % i for i in d.items()]) + '\n' def yes(prompt): yn = input(prompt) return len(yn) == 0 or yn.lower()[0] == 'y' partition_re = re.compile(r'(.*[0-9]+$)|(^xvd)') def disk_list(): disks = [] try: f = open('/proc/partitions') f.readline() f.readline() for line in f.readlines(): (major, minor, blocks, name) = line.split() if int(major) < 254 and not partition_re.match(name): disks.append(name) f.close() except: pass return disks class ProcOutput(object): debug = False def __init__(self, command, max_time, inst=None, filter=None, binary=False): self.command = command self.max_time = max_time self.inst = inst self.running = False self.status = None self.timed_out = False self.failed = False self.timeout = int(time.time()) + self.max_time self.filter = filter self.filter_state = {} if binary: self.bufsize = 1048576 # 1MB buffer else: self.bufsize = 1 # line buffered def __del__(self): self.terminate() def cmdAsStr(self): return isinstance(self.command, list) \ and ' '.join(self.command) or self.command def run(self): self.timed_out = False try: if ProcOutput.debug: output_ts("Starting '%s'" % self.cmdAsStr()) self.proc = Popen(self.command, bufsize=self.bufsize, stdin=dev_null, stdout=PIPE, stderr=dev_null, shell=isinstance(self.command, str)) old = fcntl.fcntl(self.proc.stdout.fileno(), fcntl.F_GETFD) fcntl.fcntl(self.proc.stdout.fileno(), fcntl.F_SETFD, old | fcntl.FD_CLOEXEC) self.running = True self.failed = False except: output_ts("'%s' failed" % self.cmdAsStr()) self.running = False self.failed = True def terminate(self): if self.running: try: self.proc.stdout.close() os.kill(self.proc.pid, SIGTERM) except: pass self.proc = None self.running = False self.status = SIGTERM def read_line(self): assert self.running if self.bufsize == 1: line = self.proc.stdout.readline() else: line = self.proc.stdout.read(self.bufsize) if line == b'': # process exited self.proc.stdout.close() self.status = self.proc.wait() self.proc = None self.running = False else: if self.filter: line = self.filter(line, self.filter_state) if self.inst: self.inst.write(line) def run_procs(procs): while True: pipes = [] active_procs = [] for pp in procs: for p in pp: if p.running: active_procs.append(p) pipes.append(p.proc.stdout) break elif p.status is None and not p.failed and not p.timed_out: p.run() if p.running: active_procs.append(p) pipes.append(p.proc.stdout) break if len(pipes) == 0: # all finished break (i, o, x) = select(pipes, [], [], 1.0) now = int(time.time()) # handle process output for p in active_procs: if p.proc.stdout in i: p.read_line() # handle timeout if p.running and now > p.timeout: output_ts("'%s' timed out" % p.cmdAsStr()) if p.inst: p.inst.write("\n** timeout **\n".encode()) p.timed_out = True p.terminate() def pidof(name): pids = [] for d in [p for p in os.listdir('/proc') if p.isdigit()]: try: if os.path.basename(os.readlink('/proc/%s/exe' % d)) == name: pids.append(int(d)) except: pass return pids def check_space(cap, name, size): global free_disk_space if free_disk_space is not None and size > free_disk_space: output("Omitting %s, out of disk space (requested: %u, allowed: %u)" % (name, size, free_disk_space)) return False elif unlimited_data or caps[cap][MAX_SIZE] == -1 or \ cap_sizes[cap] < caps[cap][MAX_SIZE]: cap_sizes[cap] += size if free_disk_space is not None: free_disk_space -= size return True else: output("Omitting %s, size constraint of %s exceeded" % (name, cap)) return False def get_free_disk_space(path): path = os.path.abspath(path) while not os.path.exists(path): path = os.path.dirname(path) s = os.statvfs(path) return s.f_frsize * s.f_bfree class BytesIOmtime(BytesIO): def __init__(self, buf=b''): BytesIO.__init__(self, buf) self.mtime = time.time() def write(self, s): BytesIO.write(self, s) self.mtime = time.time() if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: print("\nInterrupted.") sys.exit(3)