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