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