ovs-bugtool: Indiscriminately capture all openvswitch log files.
[sliver-openvswitch.git] / debian / ovs-bugtool
1 #!/usr/bin/env python
2
3 # This library is free software; you can redistribute it and/or
4 # modify it under the terms of version 2.1 of the GNU Lesser General Public
5 # License as published by the Free Software Foundation.
6 #
7 # This library is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
10 # Lesser General Public License for more details.
11 #
12 # You should have received a copy of the GNU Lesser General Public
13 # License along with this library; if not, write to the Free Software
14 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
15 #
16 # Copyright (c) 2005, 2007 XenSource Ltd.
17 # Copyright (c) 2010, Nicira Networks.
18
19 #
20 # To add new entries to the bugtool, you need to:
21 #
22 # Create a new capability.  These declare the new entry to the GUI, including
23 # the expected size, time to collect, privacy implications, and whether the
24 # capability should be selected by default.  One capability may refer to
25 # multiple files, assuming that they can be reasonably grouped together, and
26 # have the same privacy implications.  You need:
27 #
28 #   A new CAP_ constant.
29 #   A cap() invocation to declare the capability.
30 #
31 # You then need to add calls to main() to collect the files.  These will
32 # typically be calls to the helpers file_output(), tree_output(), cmd_output(),
33 # or func_output().
34 #
35
36 import getopt
37 import re
38 import os
39 import StringIO
40 import sys
41 import tarfile
42 import time
43 import commands
44 import pprint
45 from xml.dom.minidom import parse, getDOMImplementation
46 import zipfile
47 from subprocess import Popen, PIPE
48 from select import select
49 from signal import SIGTERM, SIGUSR1
50 import md5
51 import platform
52 import fcntl
53 import glob
54 import urllib
55 import socket
56 import base64
57
58 sys.path.append('/usr/lib/python')
59 sys.path.append('/usr/lib64/python')
60
61 OS_RELEASE = platform.release()
62
63 #
64 # Files & directories
65 #
66
67 BUG_DIR = "/var/log/ovs-bugtool"
68 PLUGIN_DIR = "/etc/openvswitch/bugtool"
69 GRUB_CONFIG = '/boot/grub/menu.lst'
70 BOOT_KERNEL = '/boot/vmlinuz-' + OS_RELEASE
71 BOOT_INITRD = '/boot/initrd-' + OS_RELEASE + '.img'
72 PROC_PARTITIONS = '/proc/partitions'
73 FSTAB = '/etc/fstab'
74 PROC_MOUNTS = '/proc/mounts'
75 PROC_CPUINFO = '/proc/cpuinfo'
76 PROC_MEMINFO = '/proc/meminfo'
77 PROC_IOPORTS = '/proc/ioports'
78 PROC_INTERRUPTS = '/proc/interrupts'
79 PROC_SCSI = '/proc/scsi/scsi'
80 PROC_VERSION = '/proc/version'
81 PROC_MODULES = '/proc/modules'
82 PROC_DEVICES = '/proc/devices'
83 PROC_FILESYSTEMS = '/proc/filesystems'
84 PROC_CMDLINE = '/proc/cmdline'
85 PROC_CONFIG = '/proc/config.gz'
86 PROC_USB_DEV = '/proc/bus/usb/devices'
87 PROC_NET_SOFTNET_STAT = '/proc/net/softnet_stat'
88 MODPROBE_DIR = '/etc/modprobe.d'
89 RESOLV_CONF = '/etc/resolv.conf'
90 NSSWITCH_CONF = '/etc/nsswitch.conf'
91 NTP_CONF = '/etc/ntp.conf'
92 HOSTS = '/etc/hosts'
93 HOSTS_ALLOW = '/etc/hosts.allow'
94 HOSTS_DENY = '/etc/hosts.deny'
95 DHCP_LEASE_DIR = '/var/lib/dhcp3'
96 OPENVSWITCH_LOG_DIR = '/var/log/openvswitch'
97 OPENVSWITCH_DEFAULT_SWITCH = '/etc/default/openvswitch-switch'
98 OPENVSWITCH_DEFAULT_CONTROLLER = '/etc/default/openvswitch-controller'
99 OPENVSWITCH_CONF_DB = '/etc/openvswitch/conf.db'
100 OPENVSWITCH_VSWITCHD_PID = '/var/run/openvswitch/ovs-vswitchd.pid'
101 COLLECTD_LOGS_DIR = '/var/lib/collectd/rrd'
102 VAR_LOG_DIR = '/var/log/'
103 VAR_LOG_CORE_DIR = '/var/log/core'
104 X11_LOGS_DIR = VAR_LOG_DIR
105 X11_LOGS_RE = re.compile(r'.*/Xorg\..*$')
106 X11_AUTH_DIR = '/root/'
107 X11_AUTH_RE = re.compile(r'.*/\.((Xauthority)|(serverauth\.[0-9]*))$')
108 PAM_DIR = '/etc/pam.d'
109
110 #
111 # External programs
112 #
113
114 ARP = '/usr/sbin/arp'
115 CAT = '/bin/cat'
116 DF = '/bin/df'
117 DMESG = '/bin/dmesg'
118 DMIDECODE = '/usr/sbin/dmidecode'
119 FDISK = '/sbin/fdisk'
120 FIND = '/usr/bin/find'
121 IFCONFIG = '/sbin/ifconfig'
122 IPTABLES = '/sbin/iptables'
123 LOSETUP = '/sbin/losetup'
124 LS = '/bin/ls'
125 LSPCI = '/usr/bin/lspci'
126 MD5SUM = '/usr/bin/md5sum'
127 MODINFO = '/sbin/modinfo'
128 NETSTAT = '/bin/netstat'
129 OVS_DPCTL = '/usr/sbin/ovs-dpctl'
130 OVS_OFCTL = '/usr/sbin/ovs-ofctl'
131 OVS_VSCTL = '/usr/sbin/ovs-vsctl'
132 OVS_APPCTL = '/usr/sbin/ovs-appctl'
133 PS = '/bin/ps'
134 ROUTE = '/sbin/route'
135 SYSCTL = '/sbin/sysctl'
136 TC = '/sbin/tc'
137 UPTIME = '/usr/bin/uptime'
138 ZCAT = '/bin/zcat'
139
140 ETHTOOL = '/sbin/ethtool'
141 # ETHTOOL recently moved from /usr/sbin to /sbin in debian
142 if not os.path.isfile(ETHTOOL):
143     ETHTOOL = '/usr/sbin/ethtool'
144
145 #
146 # PII -- Personally identifiable information.  Of particular concern are
147 # things that would identify customers, or their network topology.
148 # Passwords are never to be included in any bug report, regardless of any PII
149 # declaration.
150 #
151 # NO            -- No PII will be in these entries.
152 # YES           -- PII will likely or certainly be in these entries.
153 # MAYBE         -- The user may wish to audit these entries for PII.
154 # IF_CUSTOMIZED -- If the files are unmodified, then they will contain no PII,
155 # but since we encourage customers to edit these files, PII may have been
156 # introduced by the customer.  This is used in particular for the networking
157 # scripts in dom0.
158 #
159
160 PII_NO            = 'no'
161 PII_YES           = 'yes'
162 PII_MAYBE         = 'maybe'
163 PII_IF_CUSTOMIZED = 'if_customized'
164 KEY      = 0
165 PII      = 1
166 MIN_SIZE = 2
167 MAX_SIZE = 3
168 MIN_TIME = 4
169 MAX_TIME = 5
170 MIME     = 6
171 CHECKED  = 7
172 HIDDEN   = 8
173
174 MIME_DATA = 'application/data'
175 MIME_TEXT = 'text/plain'
176
177 INVENTORY_XML_ROOT = "system-status-inventory"
178 INVENTORY_XML_SUMMARY = 'system-summary'
179 INVENTORY_XML_ELEMENT = 'inventory-entry'
180 CAP_XML_ROOT = "system-status-capabilities"
181 CAP_XML_ELEMENT = 'capability'
182
183
184 CAP_BLOBS                = 'blobs'
185 CAP_BOOT_LOADER          = 'boot-loader'
186 CAP_COLLECTD_LOGS        = 'collectd-logs'
187 CAP_DISK_INFO            = 'disk-info'
188 CAP_FIRSTBOOT            = 'firstboot'
189 CAP_HARDWARE_INFO        = 'hardware-info'
190 CAP_HIGH_AVAILABILITY    = 'high-availability'
191 CAP_HOST_CRASHDUMP_DUMPS = 'host-crashdump-dumps'
192 CAP_HOST_CRASHDUMP_LOGS  = 'host-crashdump-logs'
193 CAP_KERNEL_INFO          = 'kernel-info'
194 CAP_LOSETUP_A            = 'loopback-devices'
195 CAP_NETWORK_CONFIG       = 'network-config'
196 CAP_NETWORK_STATUS       = 'network-status'
197 CAP_OEM                  = 'oem'
198 CAP_PAM                  = 'pam'
199 CAP_PROCESS_LIST         = 'process-list'
200 CAP_PERSISTENT_STATS     = 'persistent-stats'
201 CAP_SYSTEM_LOGS          = 'system-logs'
202 CAP_SYSTEM_SERVICES      = 'system-services'
203 CAP_VNCTERM              = 'vncterm'
204 CAP_WLB                  = 'wlb'
205 CAP_X11_LOGS             = 'X11'
206 CAP_X11_AUTH             = 'X11-auth'
207
208 KB = 1024
209 MB = 1024 * 1024
210
211 caps = {}
212 cap_sizes = {}
213 unlimited_data = False
214 dbg = False
215
216 def cap(key, pii=PII_MAYBE, min_size=-1, max_size=-1, min_time=-1,
217         max_time=-1, mime=MIME_TEXT, checked=True, hidden=False):
218     caps[key] = (key, pii, min_size, max_size, min_time, max_time, mime,
219                  checked, hidden)
220     cap_sizes[key] = 0
221
222
223 cap(CAP_BLOBS,               PII_NO,                    max_size=5*MB)
224 cap(CAP_BOOT_LOADER,         PII_NO,                    max_size=3*KB,
225     max_time=5)
226 cap(CAP_COLLECTD_LOGS,       PII_MAYBE,                 max_size=50*MB,
227     max_time=5)
228 cap(CAP_DISK_INFO,           PII_MAYBE,                 max_size=25*KB,
229     max_time=20)
230 cap(CAP_FIRSTBOOT,           PII_YES,   min_size=60*KB, max_size=80*KB)
231 cap(CAP_HARDWARE_INFO,       PII_MAYBE,                 max_size=30*KB,
232     max_time=20)
233 cap(CAP_HIGH_AVAILABILITY,   PII_MAYBE,                 max_size=5*MB)
234 cap(CAP_HOST_CRASHDUMP_DUMPS,PII_YES, checked = False)
235 cap(CAP_HOST_CRASHDUMP_LOGS, PII_NO)
236 cap(CAP_KERNEL_INFO,         PII_MAYBE,                 max_size=120*KB,
237     max_time=5)
238 cap(CAP_LOSETUP_A,           PII_MAYBE,                 max_size=KB, max_time=5)
239 cap(CAP_NETWORK_CONFIG,      PII_IF_CUSTOMIZED,
240                                         min_size=0,     max_size=20*KB)
241 cap(CAP_NETWORK_STATUS,      PII_YES,                   max_size=19*KB,
242     max_time=30)
243 cap(CAP_PAM,                 PII_NO,                    max_size=30*KB)
244 cap(CAP_PERSISTENT_STATS,    PII_MAYBE,                 max_size=50*MB,
245     max_time=60)
246 cap(CAP_PROCESS_LIST,        PII_YES,                   max_size=30*KB,
247     max_time=20)
248 cap(CAP_SYSTEM_LOGS,         PII_MAYBE,                 max_size=50*MB,
249     max_time=5)
250 cap(CAP_SYSTEM_SERVICES,     PII_NO,                    max_size=5*KB,
251     max_time=20)
252 cap(CAP_VNCTERM,             PII_MAYBE, checked = False)
253 cap(CAP_WLB,                 PII_NO,                    max_size=3*MB,
254     max_time=20)
255 cap(CAP_X11_LOGS,            PII_NO,                    max_size=100*KB)
256 cap(CAP_X11_AUTH,            PII_NO,                    max_size=100*KB)
257
258 ANSWER_YES_TO_ALL = False
259 SILENT_MODE = False
260 entries = None
261 data = {}
262 dev_null = open('/dev/null', 'r+')
263
264 def output(x):
265     global SILENT_MODE
266     if not SILENT_MODE:
267         print x
268
269 def output_ts(x):
270     output("[%s]  %s" % (time.strftime("%x %X %Z"), x))
271
272 def cmd_output(cap, args, label = None, filter = None):
273     if cap in entries:
274         if not label:
275             if isinstance(args, list):
276                 a = [aa for aa in args]
277                 a[0] = os.path.basename(a[0])
278                 label = ' '.join(a)
279             else:
280                 label = args
281         data[label] = {'cap': cap, 'cmd_args': args, 'filter': filter}
282
283 def file_output(cap, path_list):
284     if cap in entries:
285         for p in path_list:
286             if os.path.exists(p):
287                 if unlimited_data or caps[cap][MAX_SIZE] == -1 or \
288                         cap_sizes[cap] < caps[cap][MAX_SIZE]:
289                     data[p] = {'cap': cap, 'filename': p}
290                     try:
291                         s = os.stat(p)
292                         cap_sizes[cap] += s.st_size
293                     except:
294                         pass
295                 else:
296                     output("Omitting %s, size constraint of %s exceeded" % (p, cap))
297
298 def tree_output(cap, path, pattern = None, negate = False):
299     if cap in entries:
300         if os.path.exists(path):
301             for f in os.listdir(path):
302                 fn = os.path.join(path, f)
303                 if os.path.isfile(fn) and matches(fn, pattern, negate):
304                     file_output(cap, [fn])
305                 elif os.path.isdir(fn):
306                     tree_output(cap, fn, pattern, negate)
307
308 def func_output(cap, label, func):
309     if cap in entries:
310         t = str(func).split()
311         data[label] = {'cap': cap, 'func': func}
312
313 def collect_data():
314     process_lists = {}
315
316     for (k, v) in data.items():
317         cap = v['cap']
318         if v.has_key('cmd_args'):
319             v['output'] = StringIOmtime()
320             if not process_lists.has_key(cap):
321                 process_lists[cap] = []
322             process_lists[cap].append(ProcOutput(v['cmd_args'], caps[cap][MAX_TIME], v['output'], v['filter']))
323         elif v.has_key('filename') and v['filename'].startswith('/proc/'):
324             # proc files must be read into memory
325             try:
326                 f = open(v['filename'], 'r')
327                 s = f.read()
328                 f.close()
329                 if unlimited_data or caps[cap][MAX_SIZE] == -1 or \
330                         cap_sizes[cap] < caps[cap][MAX_SIZE]:
331                     v['output'] = StringIOmtime(s)
332                     cap_sizes[cap] += len(s)
333                 else:
334                     output("Omitting %s, size constraint of %s exceeded" % (v['filename'], cap))
335             except:
336                 pass
337         elif v.has_key('func'):
338             try:
339                 s = v['func'](cap)
340             except Exception, e:
341                 s = str(e)
342             if unlimited_data or caps[cap][MAX_SIZE] == -1 or \
343                     cap_sizes[cap] < caps[cap][MAX_SIZE]:
344                 v['output'] = StringIOmtime(s)
345                 cap_sizes[cap] += len(s)
346             else:
347                 output("Omitting %s, size constraint of %s exceeded" % (k, cap))
348
349     run_procs(process_lists.values())
350
351
352 def main(argv = None):
353     global ANSWER_YES_TO_ALL, SILENT_MODE
354     global entries, data, dbg
355
356     # we need access to privileged files, exit if we are not running as root
357     if os.getuid() != 0:
358         print >>sys.stderr, "Error: ovs-bugtool must be run as root"
359         return 1
360
361     output_type = 'tar.bz2'
362     output_fd = -1
363
364     if argv is None:
365         argv = sys.argv
366
367     try:
368         (options, params) = getopt.gnu_getopt(
369             argv, 'sy', ['capabilities', 'silent', 'yestoall', 'entries=',
370                          'output=', 'outfd=', 'all', 'unlimited', 'debug'])
371     except getopt.GetoptError, opterr:
372         print >>sys.stderr, opterr
373         return 2
374
375     try:
376         load_plugins(True)
377     except:
378         pass
379
380     entries = [e for e in caps.keys() if caps[e][CHECKED]]
381
382     for (k, v) in options:
383         if k == '--capabilities':
384             update_capabilities()
385             print_capabilities()
386             return 0
387
388         if k == '--output':
389             if  v in ['tar', 'tar.bz2', 'zip']:
390                 output_type = v
391             else:
392                 print >>sys.stderr, "Invalid output format '%s'" % v
393                 return 2
394
395         # "-s" or "--silent" means suppress output (except for the final
396         # output filename at the end)
397         if k in ['-s', '--silent']:
398             SILENT_MODE = True
399
400         if k == '--entries' and v != '':
401             entries = v.split(',')
402
403         # If the user runs the script with "-y" or "--yestoall" we don't ask
404         # all the really annoying questions.
405         if k in ['-y', '--yestoall']:
406             ANSWER_YES_TO_ALL = True
407
408         if k == '--outfd':
409             output_fd = int(v)
410             try:
411                 old = fcntl.fcntl(output_fd, fcntl.F_GETFD)
412                 fcntl.fcntl(output_fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
413             except:
414                 print >>sys.stderr, "Invalid output file descriptor", output_fd
415                 return 2
416
417         elif k == '--all':
418             entries = caps.keys()
419         elif k == '--unlimited':
420             unlimited_data = True
421         elif k == '--debug':
422             dbg = True
423             ProcOutput.debug = True
424
425     if len(params) != 1:
426         print >>sys.stderr, "Invalid additional arguments", str(params)
427         return 2
428
429     if output_fd != -1 and output_type != 'tar':
430         print >>sys.stderr, "Option '--outfd' only valid with '--output=tar'"
431         return 2
432
433     if ANSWER_YES_TO_ALL:
434         output("Warning: '--yestoall' argument provided, will not prompt for individual files.")
435
436     output('''
437 This application will collate dmesg output, details of the
438 hardware configuration of your machine, information about the build of
439 openvswitch that you are using, plus, if you allow it, various logs.
440
441 The collated information will be saved as a .%s for archiving or
442 sending to a Technical Support Representative.
443
444 The logs may contain private information, and if you are at all
445 worried about that, you should exit now, or you should explicitly
446 exclude those logs from the archive.
447
448 ''' % output_type)
449
450     # assemble potential data
451
452     file_output(CAP_BOOT_LOADER, [GRUB_CONFIG])
453     cmd_output(CAP_BOOT_LOADER, [LS, '-lR', '/boot'])
454     cmd_output(CAP_BOOT_LOADER, [MD5SUM, BOOT_KERNEL, BOOT_INITRD], label='vmlinuz-initrd.md5sum')
455
456     tree_output(CAP_COLLECTD_LOGS, COLLECTD_LOGS_DIR)
457     cmd_output(CAP_DISK_INFO, [FDISK, '-l'])
458     file_output(CAP_DISK_INFO, [PROC_PARTITIONS, PROC_MOUNTS])
459     file_output(CAP_DISK_INFO, [FSTAB])
460     cmd_output(CAP_DISK_INFO, [DF, '-alT'])
461     cmd_output(CAP_DISK_INFO, [DF, '-alTi'])
462     cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/scsi_host'])
463     cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/scsi_disk'])
464     cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/fc_transport'])
465     func_output(CAP_DISK_INFO, 'scsi-hosts', dump_scsi_hosts)
466
467
468     file_output(CAP_HARDWARE_INFO, [PROC_CPUINFO, PROC_MEMINFO, PROC_IOPORTS, PROC_INTERRUPTS])
469     cmd_output(CAP_HARDWARE_INFO, [DMIDECODE])
470     cmd_output(CAP_HARDWARE_INFO, [LSPCI, '-n'])
471     cmd_output(CAP_HARDWARE_INFO, [LSPCI, '-vv'])
472     file_output(CAP_HARDWARE_INFO, [PROC_USB_DEV, PROC_SCSI])
473     cmd_output(CAP_HARDWARE_INFO, [LS, '-lR', '/dev'])
474
475     file_output(CAP_KERNEL_INFO, [PROC_VERSION, PROC_MODULES, PROC_DEVICES,
476                                   PROC_FILESYSTEMS, PROC_CMDLINE])
477     cmd_output(CAP_KERNEL_INFO, [ZCAT, PROC_CONFIG], label='config')
478     cmd_output(CAP_KERNEL_INFO, [SYSCTL, '-A'])
479     tree_output(CAP_KERNEL_INFO, MODPROBE_DIR)
480     func_output(CAP_KERNEL_INFO, 'modinfo', module_info)
481
482     cmd_output(CAP_LOSETUP_A, [LOSETUP, '-a'])
483
484     file_output(CAP_NETWORK_CONFIG, [RESOLV_CONF, NSSWITCH_CONF, HOSTS])
485     file_output(CAP_NETWORK_CONFIG, [NTP_CONF, HOSTS_ALLOW, HOSTS_DENY])
486     file_output(CAP_NETWORK_CONFIG, [OPENVSWITCH_DEFAULT_SWITCH,
487         OPENVSWITCH_DEFAULT_CONTROLLER, OPENVSWITCH_CONF_DB])
488
489     cmd_output(CAP_NETWORK_STATUS, [IFCONFIG, '-a'])
490     cmd_output(CAP_NETWORK_STATUS, [ROUTE, '-n'])
491     cmd_output(CAP_NETWORK_STATUS, [ARP, '-n'])
492     cmd_output(CAP_NETWORK_STATUS, [NETSTAT, '-an'])
493     tree_output(CAP_NETWORK_STATUS, DHCP_LEASE_DIR)
494     cmd_output(CAP_NETWORK_STATUS, [IPTABLES, '-nL'])
495     for p in os.listdir('/sys/class/net/'):
496         try:
497             f = open('/sys/class/net/%s/type' % p, 'r')
498             t = f.readline()
499             f.close()
500             if int(t) == 1:
501                 # ARPHRD_ETHER
502                 cmd_output(CAP_NETWORK_STATUS, [ETHTOOL, p])
503                 cmd_output(CAP_NETWORK_STATUS, [ETHTOOL, '-S', p])
504                 cmd_output(CAP_NETWORK_STATUS, [ETHTOOL, '-k', p])
505                 cmd_output(CAP_NETWORK_STATUS, [ETHTOOL, '-i', p])
506                 cmd_output(CAP_NETWORK_STATUS, [ETHTOOL, '-c', p])
507                 cmd_output(CAP_NETWORK_STATUS,
508                            [TC, '-s', '-d', 'class', 'show', 'dev', p])
509         except:
510             pass
511     cmd_output(CAP_NETWORK_STATUS, [TC, '-s', 'qdisc'])
512     file_output(CAP_NETWORK_STATUS, [PROC_NET_SOFTNET_STAT])
513     tree_output(CAP_NETWORK_STATUS, OPENVSWITCH_LOG_DIR)
514     if os.path.exists(OPENVSWITCH_VSWITCHD_PID):
515         cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'show'])
516         for d in dp_list():
517             cmd_output(CAP_NETWORK_STATUS, [OVS_OFCTL, 'show', d])
518             cmd_output(CAP_NETWORK_STATUS, [OVS_OFCTL, 'status', d])
519             cmd_output(CAP_NETWORK_STATUS, [OVS_OFCTL, 'dump-flows', d])
520             cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'dump-flows', d])
521         try:
522             vspidfile = open(OPENVSWITCH_VSWITCHD_PID)
523             vspid = int(vspidfile.readline().strip())
524             vspidfile.close()
525             for b in bond_list(vspid):
526                 cmd_output(CAP_NETWORK_STATUS,
527                            [OVS_APPCTL, '-t', '/var/run/ovs-vswitchd.%s.ctl' % vspid, '-e' 'bond/show %s' % b],
528                            'ovs-appctl-bond-show-%s.out' % b)
529         except e:
530             pass
531
532     tree_output(CAP_PAM, PAM_DIR)
533
534     cmd_output(CAP_PROCESS_LIST, [PS, 'wwwaxf', '-eo', 'pid,tty,stat,time,nice,psr,pcpu,pmem,nwchan,wchan:25,args'], label='process-tree')
535     func_output(CAP_PROCESS_LIST, 'fd_usage', fd_usage)
536
537     file_output(CAP_SYSTEM_LOGS,
538          [ VAR_LOG_DIR + x for x in
539            [ 'kern.log', 'daemon.log', 'user.log', 'syslog', 'messages',
540              'debug', 'dmesg', 'boot'] +
541            [ f % n for n in range(1, 20) \
542                  for f in ['kern.log.%d', 'kern.log.%d.gz',
543                            'daemon.log.%d', 'daemon.log.%d.gz',
544                            'user.log.%d', 'user.log.%d.gz',
545                            'messages.%d', 'messages.%d.gz']]])
546     if not os.path.exists('/var/log/dmesg') and not os.path.exists('/var/log/boot'):
547         cmd_output(CAP_SYSTEM_LOGS, [DMESG])
548
549
550     tree_output(CAP_X11_LOGS, X11_LOGS_DIR, X11_LOGS_RE)
551     tree_output(CAP_X11_AUTH, X11_AUTH_DIR, X11_AUTH_RE)
552     tree_output(CAP_SYSTEM_LOGS, VAR_LOG_CORE_DIR)
553
554
555     try:
556         load_plugins()
557     except:
558         pass
559
560     # permit the user to filter out data
561     for k in sorted(data.keys()):
562         if not ANSWER_YES_TO_ALL and not yes("Include '%s'? [Y/n]: " % k):
563             del data[k]
564
565     # collect selected data now
566     output_ts('Running commands to collect data')
567     collect_data()
568
569     subdir = "bug-report-%s" % time.strftime("%Y%m%d%H%M%S")
570
571     # include inventory
572     data['inventory.xml'] = {'cap': None, 'output': StringIOmtime(make_inventory(data, subdir))}
573
574     # create archive
575     if output_fd == -1 and not os.path.exists(BUG_DIR):
576         try:
577             os.makedirs(BUG_DIR)
578         except:
579             pass
580
581     if output_fd == -1:
582         output_ts('Creating output file')
583
584     if output_type.startswith('tar'):
585         make_tar(subdir, output_type, output_fd)
586     else:
587         make_zip(subdir)
588
589     clean_tapdisk_logs()
590
591     if dbg:
592         print >>sys.stderr, "Category sizes (max, actual):\n"
593         for c in caps.keys():
594             print >>sys.stderr, "    %s (%d, %d)" % (c, caps[c][MAX_SIZE],
595                                                      cap_sizes[c])
596     return 0
597
598 def find_tapdisk_logs():
599     return glob.glob('/var/log/blktap/*.log*')
600
601 def generate_tapdisk_logs():
602     for pid in pidof('tapdisk'):
603         try:
604             os.kill(pid, SIGUSR1)
605             output_ts("Including logs for tapdisk process %d" % pid)
606         except :
607             pass
608     # give processes a second to write their logs
609     time.sleep(1)
610
611 def clean_tapdisk_logs():
612     for filename in find_tapdisk_logs():
613         try:
614             os.remove(filename)
615         except :
616             pass
617
618 def filter_db_pii(str, state):
619     if 'in_secret_table' not in state:
620         state['in_secret_table'] = False
621
622     if str.startswith('<table ') and 'name="secret"' in str:
623         state['in_secret_table'] = True
624     elif str.startswith('</table>'):
625         state['in_secret_table'] = False
626
627     if state['in_secret_table'] and str.startswith("<row"): # match only on DB rows
628         str = re.sub(r'(value=")[^"]+(")', r'\1REMOVED\2', str)
629     return str
630
631 def dump_scsi_hosts(cap):
632     output = ''
633     l = os.listdir('/sys/class/scsi_host')
634     l.sort()
635
636     for h in l:
637         procname = ''
638         try:
639                 f = open('/sys/class/scsi_host/%s/proc_name' % h)
640                 procname = f.readline().strip("\n")
641                 f.close()
642         except:
643                 pass
644         modelname = None
645         try:
646                 f = open('/sys/class/scsi_host/%s/model_name' % h)
647                 modelname = f.readline().strip("\n")
648                 f.close()
649         except:
650                 pass
651
652         output += "%s:\n" %h
653         output += "    %s%s\n" % (procname, modelname and (" -> %s" % modelname) or '')
654
655     return output
656
657 def module_info(cap):
658     output = StringIO.StringIO()
659     modules = open(PROC_MODULES, 'r')
660     procs = []
661
662     for line in modules:
663         module = line.split()[0]
664         procs.append(ProcOutput([MODINFO, module], caps[cap][MAX_TIME], output))
665     modules.close()
666
667     run_procs([procs])
668
669     return output.getvalue()
670
671 def dp_list():
672     output = StringIO.StringIO()
673     procs = [ProcOutput([OVS_DPCTL, 'dump-dps'], caps[CAP_NETWORK_STATUS][MAX_TIME], output)]
674
675     run_procs([procs])
676
677     if not procs[0].timed_out:
678         return output.getvalue().splitlines()
679     return []
680
681 def bond_list(pid):
682     output = StringIO.StringIO()
683     procs = [ProcOutput([OVS_APPCTL, '-t', '/var/run/ovs-vswitchd.%s.ctl' % pid, '-e' 'bond/list'], caps[CAP_NETWORK_STATUS][MAX_TIME], output)]
684
685     run_procs([procs])
686
687     if not procs[0].timed_out:
688         bonds = output.getvalue().splitlines()[1:]
689         return [x.split('\t')[1] for x in bonds]
690     return []
691
692 def fd_usage(cap):
693     output = ''
694     fd_dict = {}
695     for d in [p for p in os.listdir('/proc') if p.isdigit()]:
696         try:
697             fh = open('/proc/'+d+'/cmdline')
698             name = fh.readline()
699             num_fds = len(os.listdir(os.path.join('/proc/'+d+'/fd')))
700             if num_fds > 0:
701                 if not num_fds in fd_dict:
702                     fd_dict[num_fds] = []
703                 fd_dict[num_fds].append(name.replace('\0', ' ').strip())
704         finally:
705             fh.close()
706     keys = fd_dict.keys()
707     keys.sort(lambda a, b: int(b) - int(a))
708     for k in keys:
709         output += "%s: %s\n" % (k, str(fd_dict[k]))
710     return output
711
712 def load_plugins(just_capabilities = False):
713     def getText(nodelist):
714         rc = ""
715         for node in nodelist:
716             if node.nodeType == node.TEXT_NODE:
717                 rc += node.data
718         return rc.encode()
719
720     def getBoolAttr(el, attr, default = False):
721         ret = default
722         val = el.getAttribute(attr).lower()
723         if val in ['true', 'false', 'yes', 'no']:
724             ret = val in ['true', 'yes']
725         return ret
726
727     for dir in [d for d in os.listdir(PLUGIN_DIR) if os.path.isdir(os.path.join(PLUGIN_DIR, d))]:
728         if not caps.has_key(dir):
729             if not os.path.exists("%s/%s.xml" % (PLUGIN_DIR, dir)):
730                 continue
731             xmldoc = parse("%s/%s.xml" % (PLUGIN_DIR, dir))
732             assert xmldoc.documentElement.tagName == "capability"
733
734             pii, min_size, max_size, min_time, max_time, mime = \
735                  PII_MAYBE, -1,-1,-1,-1, MIME_TEXT
736
737             if xmldoc.documentElement.getAttribute("pii") in [PII_NO, PII_YES, PII_MAYBE, PII_IF_CUSTOMIZED]:
738                 pii = xmldoc.documentElement.getAttribute("pii")
739             if xmldoc.documentElement.getAttribute("min_size") != '':
740                 min_size = long(xmldoc.documentElement.getAttribute("min_size"))
741             if xmldoc.documentElement.getAttribute("max_size") != '':
742                 max_size = long(xmldoc.documentElement.getAttribute("max_size"))
743             if xmldoc.documentElement.getAttribute("min_time") != '':
744                 min_time = int(xmldoc.documentElement.getAttribute("min_time"))
745             if xmldoc.documentElement.getAttribute("max_time") != '':
746                 max_time = int(xmldoc.documentElement.getAttribute("max_time"))
747             if xmldoc.documentElement.getAttribute("mime") in [MIME_DATA, MIME_TEXT]:
748                 mime = xmldoc.documentElement.getAttribute("mime")
749             checked = getBoolAttr(xmldoc.documentElement, 'checked', True)
750             hidden = getBoolAttr(xmldoc.documentElement, 'hidden', False)
751
752             cap(dir, pii, min_size, max_size, min_time, max_time, mime, checked, hidden)
753
754         if just_capabilities:
755             continue
756
757         plugdir = os.path.join(PLUGIN_DIR, dir)
758         for file in [f for f in os.listdir(plugdir) if f.endswith('.xml')]:
759             xmldoc = parse(os.path.join(plugdir, file))
760             assert xmldoc.documentElement.tagName == "collect"
761
762             for el in xmldoc.documentElement.getElementsByTagName("*"):
763                 if el.tagName == "files":
764                     file_output(dir, getText(el.childNodes).split())
765                 elif el.tagName == "directory":
766                     pattern = el.getAttribute("pattern")
767                     if pattern == '': pattern = None
768                     negate = getBoolAttr(el, 'negate')
769                     tree_output(dir, getText(el.childNodes), pattern and re.compile(pattern) or None, negate)
770                 elif el.tagName == "command":
771                     label = el.getAttribute("label")
772                     if label == '': label = None
773                     cmd_output(dir, getText(el.childNodes), label)
774
775 def make_tar(subdir, suffix, output_fd):
776     global SILENT_MODE, data
777
778     mode = 'w'
779     if suffix == 'tar.bz2':
780         mode = 'w:bz2'
781     filename = "%s/%s.%s" % (BUG_DIR, subdir, suffix)
782
783     if output_fd == -1:
784         tf = tarfile.open(filename, mode)
785     else:
786         tf = tarfile.open(None, 'w', os.fdopen(output_fd, 'a'))
787
788     try:
789         for (k, v) in data.items():
790             try:
791                 tar_filename = os.path.join(subdir, construct_filename(k, v))
792                 ti = tarfile.TarInfo(tar_filename)
793
794                 ti.uname = 'root'
795                 ti.gname = 'root'
796
797                 if v.has_key('output'):
798                     ti.mtime = v['output'].mtime
799                     ti.size = len(v['output'].getvalue())
800                     v['output'].seek(0)
801                     tf.addfile(ti, v['output'])
802                 elif v.has_key('filename'):
803                     s = os.stat(v['filename'])
804                     ti.mtime = s.st_mtime
805                     ti.size = s.st_size
806                     tf.addfile(ti, file(v['filename']))
807             except:
808                 pass
809     finally:
810         tf.close()
811
812     if output_fd == -1:
813         output ('Writing tarball %s successful.' % filename)
814         if SILENT_MODE:
815             print filename
816
817
818 def make_zip(subdir):
819     global SILENT_MODE, data
820
821     filename = "%s/%s.zip" % (BUG_DIR, subdir)
822     zf = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED)
823
824     try:
825         for (k, v) in data.items():
826             try:
827                 dest = os.path.join(subdir, construct_filename(k, v))
828
829                 if v.has_key('output'):
830                     zf.writestr(dest, v['output'].getvalue())
831                 else:
832                     if os.stat(v['filename']).st_size < 50:
833                         compress_type = zipfile.ZIP_STORED
834                     else:
835                         compress_type = zipfile.ZIP_DEFLATED
836                     zf.write(v['filename'], dest, compress_type)
837             except:
838                 pass
839     finally:
840         zf.close()
841
842     output ('Writing archive %s successful.' % filename)
843     if SILENT_MODE:
844         print filename
845
846
847 def make_inventory(inventory, subdir):
848     document = getDOMImplementation().createDocument(
849         None, INVENTORY_XML_ROOT, None)
850
851     # create summary entry
852     s = document.createElement(INVENTORY_XML_SUMMARY)
853     user = os.getenv('SUDO_USER', os.getenv('USER'))
854     if user:
855         s.setAttribute('user', user)
856     s.setAttribute('date', time.strftime('%c'))
857     s.setAttribute('hostname', platform.node())
858     s.setAttribute('uname', ' '.join(platform.uname()))
859     s.setAttribute('uptime', commands.getoutput(UPTIME))
860     document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(s)
861
862     map(lambda (k, v): inventory_entry(document, subdir, k, v),
863         inventory.items())
864     return document.toprettyxml()
865
866 def inventory_entry(document, subdir, k, v):
867     try:
868         el = document.createElement(INVENTORY_XML_ELEMENT)
869         el.setAttribute('capability', v['cap'])
870         el.setAttribute('filename', os.path.join(subdir, construct_filename(k, v)))
871         el.setAttribute('md5sum', md5sum(v))
872         document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(el)
873     except:
874         pass
875
876
877 def md5sum(d):
878     m = md5.new()
879     if d.has_key('filename'):
880         f = open(d['filename'])
881         data = f.read(1024)
882         while len(data) > 0:
883             m.update(data)
884             data = f.read(1024)
885         f.close()
886     elif d.has_key('output'):
887         m.update(d['output'].getvalue())
888     return m.hexdigest()
889
890
891 def construct_filename(k, v):
892     if v.has_key('filename'):
893         if v['filename'][0] == '/':
894             return v['filename'][1:]
895         else:
896             return v['filename']
897     s = k.replace(' ', '-')
898     s = s.replace('--', '-')
899     s = s.replace('/', '%')
900     if s.find('.') == -1:
901         s += '.out'
902
903     return s
904
905 def update_capabilities():
906     pass
907
908 def update_cap_size(cap, size):
909     update_cap(cap, MIN_SIZE, size)
910     update_cap(cap, MAX_SIZE, size)
911     update_cap(cap, CHECKED, size > 0)
912
913
914 def update_cap(cap, k, v):
915     global caps
916     l = list(caps[cap])
917     l[k] = v
918     caps[cap] = tuple(l)
919
920
921 def size_of_dir(d, pattern = None, negate = False):
922     if os.path.isdir(d):
923         return size_of_all([os.path.join(d, fn) for fn in os.listdir(d)],
924                            pattern, negate)
925     else:
926         return 0
927
928
929 def size_of_all(files, pattern = None, negate = False):
930     return sum([size_of(f, pattern, negate) for f in files])
931
932
933 def matches(f, pattern, negate):
934     if negate:
935         return not matches(f, pattern, False)
936     else:
937         return pattern is None or pattern.match(f)
938
939
940 def size_of(f, pattern, negate):
941     if os.path.isfile(f) and matches(f, pattern, negate):
942         return os.stat(f)[6]
943     else:
944         return size_of_dir(f, pattern, negate)
945
946
947 def print_capabilities():
948     document = getDOMImplementation().createDocument(
949         "ns", CAP_XML_ROOT, None)
950     map(lambda key: capability(document, key), [k for k in caps.keys() if not caps[k][HIDDEN]])
951     print document.toprettyxml()
952
953 def capability(document, key):
954     c = caps[key]
955     el = document.createElement(CAP_XML_ELEMENT)
956     el.setAttribute('key', c[KEY])
957     el.setAttribute('pii', c[PII])
958     el.setAttribute('min-size', str(c[MIN_SIZE]))
959     el.setAttribute('max-size', str(c[MAX_SIZE]))
960     el.setAttribute('min-time', str(c[MIN_TIME]))
961     el.setAttribute('max-time', str(c[MAX_TIME]))
962     el.setAttribute('content-type', c[MIME])
963     el.setAttribute('default-checked', c[CHECKED] and 'yes' or 'no')
964     document.getElementsByTagName(CAP_XML_ROOT)[0].appendChild(el)
965
966
967 def prettyDict(d):
968     format = '%%-%ds: %%s' % max(map(len, [k for k, _ in d.items()]))
969     return '\n'.join([format % i for i in d.items()]) + '\n'
970
971
972 def yes(prompt):
973     yn = raw_input(prompt)
974
975     return len(yn) == 0 or yn.lower()[0] == 'y'
976
977
978 partition_re = re.compile(r'(.*[0-9]+$)|(^xvd)')
979
980 def disk_list():
981     disks = []
982     try:
983         f = open('/proc/partitions')
984         f.readline()
985         f.readline()
986         for line in f.readlines():
987             (major, minor, blocks, name) = line.split()
988             if int(major) < 254 and not partition_re.match(name):
989                 disks.append(name)
990         f.close()
991     except:
992         pass
993     return disks
994
995
996 class ProcOutput:
997     debug = False
998
999     def __init__(self, command, max_time, inst=None, filter=None):
1000         self.command = command
1001         self.max_time = max_time
1002         self.inst = inst
1003         self.running = False
1004         self.status = None
1005         self.timed_out = False
1006         self.failed = False
1007         self.timeout = int(time.time()) + self.max_time
1008         self.filter = filter
1009         self.filter_state = {}
1010
1011     def __del__(self):
1012         self.terminate()
1013
1014     def cmdAsStr(self):
1015         return isinstance(self.command, list) and ' '.join(self.command) or self.command
1016
1017     def run(self):
1018         self.timed_out = False
1019         try:
1020             if ProcOutput.debug:
1021                 output_ts("Starting '%s'" % self.cmdAsStr())
1022             self.proc = Popen(self.command, bufsize=1, stdin=dev_null, stdout=PIPE, stderr=dev_null, shell=isinstance(self.command, str))
1023             old = fcntl.fcntl(self.proc.stdout.fileno(), fcntl.F_GETFD)
1024             fcntl.fcntl(self.proc.stdout.fileno(), fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
1025             self.running = True
1026             self.failed = False
1027         except:
1028             output_ts("'%s' failed" % self.cmdAsStr())
1029             self.running = False
1030             self.failed = True
1031
1032     def terminate(self):
1033         if self.running:
1034             try:
1035                 os.kill(self.proc.pid, SIGTERM)
1036             except:
1037                 pass
1038             self.proc = None
1039             self.running = False
1040             self.status = SIGTERM
1041
1042     def read_line(self):
1043         assert self.running
1044         line = self.proc.stdout.readline()
1045         if line == '':
1046             # process exited
1047             self.status = self.proc.wait()
1048             self.proc = None
1049             self.running = False
1050         else:
1051             if self.filter:
1052                 line = self.filter(line, self.filter_state)
1053             if self.inst:
1054                 self.inst.write(line)
1055
1056 def run_procs(procs):
1057     while True:
1058         pipes = []
1059         active_procs = []
1060
1061         for pp in procs:
1062             for p in pp:
1063                 if p.running:
1064                     active_procs.append(p)
1065                     pipes.append(p.proc.stdout)
1066                     break
1067                 elif p.status == None and not p.failed and not p.timed_out:
1068                     p.run()
1069                     if p.running:
1070                         active_procs.append(p)
1071                         pipes.append(p.proc.stdout)
1072                         break
1073
1074         if len(pipes) == 0:
1075             # all finished
1076             break
1077
1078         (i, o, x) = select(pipes, [], [], 1.0)
1079         now = int(time.time())
1080
1081         # handle process output
1082         for p in active_procs:
1083             if p.proc.stdout in i:
1084                 p.read_line()
1085
1086             # handle timeout
1087             if p.running and now > p.timeout:
1088                 output_ts("'%s' timed out" % p.cmdAsStr())
1089                 if p.inst:
1090                     p.inst.write("\n** timeout **\n")
1091                 p.timed_out = True
1092                 p.terminate()
1093
1094
1095 def pidof(name):
1096     pids = []
1097
1098     for d in [p for p in os.listdir('/proc') if p.isdigit()]:
1099         try:
1100             if os.path.basename(os.readlink('/proc/%s/exe' % d)) == name:
1101                 pids.append(int(d))
1102         except:
1103             pass
1104
1105     return pids
1106
1107
1108 class StringIOmtime(StringIO.StringIO):
1109     def __init__(self, buf = ''):
1110         StringIO.StringIO.__init__(self, buf)
1111         self.mtime = time.time()
1112
1113     def write(self, s):
1114         StringIO.StringIO.write(self, s)
1115         self.mtime = time.time()
1116
1117
1118 if __name__ == "__main__":
1119     try:
1120         sys.exit(main())
1121     except KeyboardInterrupt:
1122         print "\nInterrupted."
1123         sys.exit(3)