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