6a4316affcf0f892978e66e0a58fa59828340c16
[sliver-openvswitch.git] / utilities / bugtool / ovs-bugtool.in
1 #! @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 OS_RELEASE = platform.release()
62
63 #
64 # Files & directories
65 #
66
67 APT_SOURCES_LIST = "/etc/apt/sources.list"
68 APT_SOURCES_LIST_D = "/etc/apt/sources.list.d"
69 BUG_DIR = "/var/log/ovs-bugtool"
70 PLUGIN_DIR = "@sysconfdir@/openvswitch/bugtool-plugins"
71 GRUB_CONFIG = '/boot/grub/menu.lst'
72 BOOT_KERNEL = '/boot/vmlinuz-' + OS_RELEASE
73 BOOT_INITRD = '/boot/initrd-' + OS_RELEASE + '.img'
74 PROC_PARTITIONS = '/proc/partitions'
75 FSTAB = '/etc/fstab'
76 PROC_MOUNTS = '/proc/mounts'
77 ISCSI_CONF = '/etc/iscsi/iscsid.conf'
78 ISCSI_INITIATOR = '/etc/iscsi/initiatorname.iscsi'
79 LVM_CACHE = '/etc/lvm/cache/.cache'
80 LVM_CONFIG = '/etc/lvm/lvm.conf'
81 PROC_CPUINFO = '/proc/cpuinfo'
82 PROC_MEMINFO = '/proc/meminfo'
83 PROC_IOPORTS = '/proc/ioports'
84 PROC_INTERRUPTS = '/proc/interrupts'
85 PROC_SCSI = '/proc/scsi/scsi'
86 PROC_VERSION = '/proc/version'
87 PROC_MODULES = '/proc/modules'
88 PROC_DEVICES = '/proc/devices'
89 PROC_FILESYSTEMS = '/proc/filesystems'
90 PROC_CMDLINE = '/proc/cmdline'
91 PROC_CONFIG = '/proc/config.gz'
92 PROC_USB_DEV = '/proc/bus/usb/devices'
93 PROC_XEN_BALLOON = '/proc/xen/balloon'
94 PROC_NET_BONDING_DIR = '/proc/net/bonding'
95 IFCFG_RE = re.compile(r'^.*/ifcfg-.*')
96 ROUTE_RE = re.compile(r'^.*/route-.*')
97 SYSCONFIG_HWCONF = '/etc/sysconfig/hwconf'
98 SYSCONFIG_NETWORK = '/etc/sysconfig/network'
99 SYSCONFIG_NETWORK_SCRIPTS = '/etc/sysconfig/network-scripts'
100 PROC_NET_VLAN_DIR = '/proc/net/vlan'
101 PROC_NET_SOFTNET_STAT = '/proc/net/softnet_stat'
102 MODPROBE_CONF = '/etc/modprobe.conf'
103 MODPROBE_DIR = '/etc/modprobe.d'
104 RESOLV_CONF = '/etc/resolv.conf'
105 MPP_CONF = '/etc/mpp.conf'
106 MULTIPATH_CONF = '/etc/multipath.conf'
107 NSSWITCH_CONF = '/etc/nsswitch.conf'
108 NTP_CONF = '/etc/ntp.conf'
109 IPTABLES_CONFIG = '/etc/sysconfig/iptables-config'
110 HOSTS = '/etc/hosts'
111 HOSTS_ALLOW = '/etc/hosts.allow'
112 HOSTS_DENY = '/etc/hosts.deny'
113 DHCP_LEASE_DIR = ['/var/lib/dhclient', '/var/lib/dhcp3']
114 OPENVSWITCH_LOG_DIR = '@LOGDIR@/'
115 OPENVSWITCH_DEFAULT_SWITCH = '/etc/default/openvswitch-switch' # Debian
116 OPENVSWITCH_SYSCONFIG_SWITCH = '/etc/sysconfig/openvswitch'    # RHEL
117 OPENVSWITCH_DEFAULT_CONTROLLER = '/etc/default/openvswitch-controller'
118 OPENVSWITCH_CONF_DB = '@sysconfdir@/openvswitch/conf.db'
119 OPENVSWITCH_VSWITCHD_PID = '@RUNDIR@/ovs-vswitchd.pid'
120 COLLECTD_LOGS_DIR = '/var/lib/collectd/rrd'
121 VAR_LOG_DIR = '/var/log/'
122 VAR_LOG_CORE_DIR = '/var/log/core'
123 X11_LOGS_DIR = VAR_LOG_DIR
124 X11_LOGS_RE = re.compile(r'.*/Xorg\..*$')
125 X11_AUTH_DIR = '/root/'
126 X11_AUTH_RE = re.compile(r'.*/\.((Xauthority)|(serverauth\.[0-9]*))$')
127 YUM_LOG = '/var/log/yum.log'
128 YUM_REPOS_DIR = '/etc/yum.repos.d'
129 PAM_DIR = '/etc/pam.d'
130 KRB5_CONF = '/etc/krb5.conf'
131
132 #
133 # External programs
134 #
135
136 os.environ['PATH'] = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:@pkgdatadir@/scripts'
137 ARP = 'arp'
138 BRCTL = 'brctl'
139 CAT = 'cat'
140 CHKCONFIG = 'chkconfig'
141 DF = 'df'
142 DMESG = 'dmesg'
143 DMIDECODE = 'dmidecode'
144 DMSETUP = 'dmsetup'
145 DPKG_QUERY = 'dpkg-query'
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, unlimited_data
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     if os.path.exists(OPENVSWITCH_VSWITCHD_PID):
590         cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'show', '-s'])
591         for d in dp_list():
592             cmd_output(CAP_NETWORK_STATUS, [OVS_OFCTL, 'show', d])
593             cmd_output(CAP_NETWORK_STATUS, [OVS_OFCTL, 'dump-flows', d])
594             cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'dump-flows', d])
595         try:
596             vspidfile = open(OPENVSWITCH_VSWITCHD_PID)
597             vspid = int(vspidfile.readline().strip())
598             vspidfile.close()
599             for b in bond_list(vspid):
600                 cmd_output(CAP_NETWORK_STATUS,
601                            [OVS_APPCTL, '-t', '@RUNDIR@/ovs-vswitchd.%s.ctl' % vspid, '-e' 'bond/show %s' % b],
602                            'ovs-appctl-bond-show-%s.out' % b)
603         except e:
604             pass
605
606     tree_output(CAP_PAM, PAM_DIR)
607     file_output(CAP_PAM, [KRB5_CONF])
608
609     cmd_output(CAP_PROCESS_LIST, [PS, 'wwwaxf', '-eo', 'pid,tty,stat,time,nice,psr,pcpu,pmem,nwchan,wchan:25,args'], label='process-tree')
610     func_output(CAP_PROCESS_LIST, 'fd_usage', fd_usage)
611
612     logs = ([ VAR_LOG_DIR + x for x in
613              [ 'crit.log', 'kern.log', 'daemon.log', 'user.log',
614              'syslog', 'messages', 'secure', 'debug', 'dmesg', 'boot' ]]
615             + [ OPENVSWITCH_LOG_DIR + x for x in
616                 [ 'ovs-vswitchd.log', 'ovs-brcompatd.log', 'ovsdb-server.log',
617                   'ovs-xapi-sync.log', 'ovs-monitor-ipsec.log' ]])
618     file_output(CAP_SYSTEM_LOGS, logs)
619     file_output(CAP_SYSTEM_LOGS,
620                 [ '%s.%d' % (f, n) for n in range(20) for f in logs ])
621     file_output(CAP_SYSTEM_LOGS,
622                 [ '%s.%d.gz' % (f, n) for n in range(20) for f in logs ])
623
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     file_output(CAP_YUM, [APT_SOURCES_LIST])
637     tree_output(CAP_YUM, APT_SOURCES_LIST_D)
638     cmd_output(CAP_YUM, [DPKG_QUERY, '-W', '-f=${Package} ${Version} ${Status}\n'], 'dpkg-packages')
639
640     try:
641         load_plugins()
642     except:
643         pass
644     
645     # permit the user to filter out data
646     for k in sorted(data.keys()):
647         if not ANSWER_YES_TO_ALL and not yes("Include '%s'? [Y/n]: " % k):
648             del data[k]
649
650     # collect selected data now
651     output_ts('Running commands to collect data')
652     collect_data()
653
654     subdir = "bug-report-%s" % time.strftime("%Y%m%d%H%M%S")
655
656     # include inventory
657     data['inventory.xml'] = {'cap': None, 'output': StringIOmtime(make_inventory(data, subdir))}
658
659     # create archive
660     if output_fd == -1:
661         if output_file is None:
662             dirname = BUG_DIR
663         else:
664             dirname = os.path.dirname(output_file)
665         if dirname and not os.path.exists(dirname):
666             try:
667                 os.makedirs(dirname)
668             except:
669                 pass
670
671     if output_fd == -1:
672         output_ts('Creating output file')
673
674     if output_type.startswith('tar'):
675         make_tar(subdir, output_type, output_fd, output_file)
676     else:
677         make_zip(subdir, output_file)
678
679     clean_tapdisk_logs()
680
681     if dbg:
682         print >>sys.stderr, "Category sizes (max, actual):\n"
683         for c in caps.keys():
684             print >>sys.stderr, "    %s (%d, %d)" % (c, caps[c][MAX_SIZE],
685                                                      cap_sizes[c])
686     return 0
687
688 def find_tapdisk_logs():
689     return glob.glob('/var/log/blktap/*.log*')
690
691 def generate_tapdisk_logs():
692     for pid in pidof('tapdisk'):
693         try:
694             os.kill(pid, SIGUSR1)
695             output_ts("Including logs for tapdisk process %d" % pid)
696         except :
697             pass
698     # give processes a second to write their logs
699     time.sleep(1)
700
701 def clean_tapdisk_logs():
702     for filename in find_tapdisk_logs():
703         try:
704             os.remove(filename)
705         except :
706             pass
707
708 def filter_db_pii(str, state):
709     if 'in_secret_table' not in state:
710         state['in_secret_table'] = False
711
712     if str.startswith('<table ') and 'name="secret"' in str:
713         state['in_secret_table'] = True
714     elif str.startswith('</table>'):
715         state['in_secret_table'] = False
716
717     if state['in_secret_table'] and str.startswith("<row"): # match only on DB rows
718         str = re.sub(r'(value=")[^"]+(")', r'\1REMOVED\2', str)
719     return str
720
721 def dump_scsi_hosts(cap):
722     output = ''
723     l = os.listdir('/sys/class/scsi_host')
724     l.sort()
725
726     for h in l:
727         procname = ''
728         try:
729                 f = open('/sys/class/scsi_host/%s/proc_name' % h)
730                 procname = f.readline().strip("\n")
731                 f.close()
732         except:
733                 pass
734         modelname = None
735         try:
736                 f = open('/sys/class/scsi_host/%s/model_name' % h)
737                 modelname = f.readline().strip("\n")
738                 f.close()
739         except:
740                 pass
741
742         output += "%s:\n" %h
743         output += "    %s%s\n" % (procname, modelname and (" -> %s" % modelname) or '')
744
745     return output
746
747 def module_info(cap):
748     output = StringIO.StringIO()
749     modules = open(PROC_MODULES, 'r')
750     procs = []
751
752     for line in modules:
753         module = line.split()[0]
754         procs.append(ProcOutput([MODINFO, module], caps[cap][MAX_TIME], output))
755     modules.close()
756
757     run_procs([procs])
758
759     return output.getvalue()
760
761
762 def multipathd_topology(cap):
763     pipe = Popen([MULTIPATHD, '-k'], bufsize=1, stdin=PIPE, 
764                      stdout=PIPE, stderr=dev_null)
765     stdout, stderr = pipe.communicate('show topology')
766
767     return stdout
768
769 def dp_list():
770     output = StringIO.StringIO()
771     procs = [ProcOutput([OVS_DPCTL, 'dump-dps'], caps[CAP_NETWORK_STATUS][MAX_TIME], output)]
772
773     run_procs([procs])
774
775     if not procs[0].timed_out:
776         return output.getvalue().splitlines()
777     return []
778
779 def bond_list(pid):
780     output = StringIO.StringIO()
781     procs = [ProcOutput([OVS_APPCTL, '-t', '@RUNDIR@/ovs-vswitchd.%s.ctl' % pid, '-e' 'bond/list'], caps[CAP_NETWORK_STATUS][MAX_TIME], output)]
782
783     run_procs([procs])
784
785     if not procs[0].timed_out:
786         bonds = output.getvalue().splitlines()[1:]
787         return [x.split('\t')[1] for x in bonds]
788     return []
789
790 def fd_usage(cap):
791     output = ''
792     fd_dict = {}
793     for d in [p for p in os.listdir('/proc') if p.isdigit()]:
794         try:
795             fh = open('/proc/'+d+'/cmdline')
796             name = fh.readline()
797             num_fds = len(os.listdir(os.path.join('/proc/'+d+'/fd')))
798             if num_fds > 0:
799                 if not num_fds in fd_dict:
800                     fd_dict[num_fds] = []
801                 fd_dict[num_fds].append(name.replace('\0', ' ').strip())
802         finally:
803             fh.close()
804     keys = fd_dict.keys()
805     keys.sort(lambda a, b: int(b) - int(a))
806     for k in keys:
807         output += "%s: %s\n" % (k, str(fd_dict[k]))
808     return output
809
810 def dump_rdac_groups(cap):
811     output = StringIO.StringIO()
812     procs = [ProcOutput([MPPUTIL, '-a'], caps[cap][MAX_TIME], output)]
813
814     run_procs([procs])
815
816     if not procs[0].timed_out:
817         proc_line = 0
818         for line in output.getvalue().splitlines():
819             if line.startswith('ID'):
820                 proc_line = 2
821             elif line.startswith('----'):
822                 proc_line -= 1
823             elif proc_line > 0:
824                 group, _ = line.split(None, 1)
825                 cmd_output(cap, [MPPUTIL, '-g', group])
826
827 def load_plugins(just_capabilities = False):
828     def getText(nodelist):
829         rc = ""
830         for node in nodelist:
831             if node.nodeType == node.TEXT_NODE:
832                 rc += node.data
833         return rc.encode()
834
835     def getBoolAttr(el, attr, default = False):
836         ret = default
837         val = el.getAttribute(attr).lower()
838         if val in ['true', 'false', 'yes', 'no']:
839             ret = val in ['true', 'yes']
840         return ret
841         
842     for dir in [d for d in os.listdir(PLUGIN_DIR) if os.path.isdir(os.path.join(PLUGIN_DIR, d))]:
843         if not caps.has_key(dir):
844             if not os.path.exists("%s/%s.xml" % (PLUGIN_DIR, dir)):
845                 continue
846             xmldoc = parse("%s/%s.xml" % (PLUGIN_DIR, dir))
847             assert xmldoc.documentElement.tagName == "capability"
848
849             pii, min_size, max_size, min_time, max_time, mime = \
850                  PII_MAYBE, -1,-1,-1,-1, MIME_TEXT
851
852             if xmldoc.documentElement.getAttribute("pii") in [PII_NO, PII_YES, PII_MAYBE, PII_IF_CUSTOMIZED]:
853                 pii = xmldoc.documentElement.getAttribute("pii")
854             if xmldoc.documentElement.getAttribute("min_size") != '':
855                 min_size = long(xmldoc.documentElement.getAttribute("min_size"))
856             if xmldoc.documentElement.getAttribute("max_size") != '':
857                 max_size = long(xmldoc.documentElement.getAttribute("max_size"))
858             if xmldoc.documentElement.getAttribute("min_time") != '':
859                 min_time = int(xmldoc.documentElement.getAttribute("min_time"))
860             if xmldoc.documentElement.getAttribute("max_time") != '':
861                 max_time = int(xmldoc.documentElement.getAttribute("max_time"))
862             if xmldoc.documentElement.getAttribute("mime") in [MIME_DATA, MIME_TEXT]:
863                 mime = xmldoc.documentElement.getAttribute("mime")
864             checked = getBoolAttr(xmldoc.documentElement, 'checked', True)
865             hidden = getBoolAttr(xmldoc.documentElement, 'hidden', False)
866
867             cap(dir, pii, min_size, max_size, min_time, max_time, mime, checked, hidden)
868
869         if just_capabilities:
870             continue
871                     
872         plugdir = os.path.join(PLUGIN_DIR, dir)
873         for file in [f for f in os.listdir(plugdir) if f.endswith('.xml')]:
874             xmldoc = parse(os.path.join(plugdir, file))
875             assert xmldoc.documentElement.tagName == "collect"
876
877             for el in xmldoc.documentElement.getElementsByTagName("*"):
878                 if el.tagName == "files":
879                     file_output(dir, getText(el.childNodes).split())
880                 elif el.tagName == "directory":
881                     pattern = el.getAttribute("pattern")
882                     if pattern == '': pattern = None
883                     negate = getBoolAttr(el, 'negate')
884                     tree_output(dir, getText(el.childNodes), pattern and re.compile(pattern) or None, negate)
885                 elif el.tagName == "command":
886                     label = el.getAttribute("label")
887                     if label == '': label = None
888                     cmd_output(dir, getText(el.childNodes), label)
889
890 def make_tar(subdir, suffix, output_fd, output_file):
891     global SILENT_MODE, data
892
893     mode = 'w'
894     if suffix == 'tar.bz2':
895         mode = 'w:bz2'
896     elif suffix == 'tar.gz':
897         mode = 'w:gz'
898
899     if output_fd == -1:
900         if output_file is None:
901             filename = "%s/%s.%s" % (BUG_DIR, subdir, suffix)
902         else:
903             filename = output_file
904         old_umask = os.umask(0077)
905         tf = tarfile.open(filename, mode)
906         os.umask(old_umask)
907     else:
908         tf = tarfile.open(None, 'w', os.fdopen(output_fd, 'a'))
909
910     try:
911         for (k, v) in data.items():
912             try:
913                 tar_filename = os.path.join(subdir, construct_filename(k, v))
914                 ti = tarfile.TarInfo(tar_filename)
915
916                 ti.uname = 'root'
917                 ti.gname = 'root'
918
919                 if v.has_key('output'):
920                     ti.mtime = v['output'].mtime
921                     ti.size = len(v['output'].getvalue())
922                     v['output'].seek(0)
923                     tf.addfile(ti, v['output'])
924                 elif v.has_key('filename'):
925                     s = os.stat(v['filename'])
926                     ti.mtime = s.st_mtime
927                     ti.size = s.st_size
928                     tf.addfile(ti, file(v['filename']))
929             except:
930                 pass
931     finally:
932         tf.close()
933
934     if output_fd == -1:
935         output ('Writing tarball %s successful.' % filename)
936         if SILENT_MODE:
937             print filename
938
939
940 def make_zip(subdir, output_file):
941     global SILENT_MODE, data
942
943     if output_file is None:
944         filename = "%s/%s.zip" % (BUG_DIR, subdir)
945     else:
946         filename = output_file
947     old_umask = os.umask(0077)
948     zf = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED)
949     os.umask(old_umask)
950
951     try:
952         for (k, v) in data.items():
953             try:
954                 dest = os.path.join(subdir, construct_filename(k, v))
955
956                 if v.has_key('output'):
957                     zf.writestr(dest, v['output'].getvalue())
958                 else:
959                     if os.stat(v['filename']).st_size < 50:
960                         compress_type = zipfile.ZIP_STORED
961                     else:
962                         compress_type = zipfile.ZIP_DEFLATED
963                     zf.write(v['filename'], dest, compress_type)
964             except:
965                 pass
966     finally:
967         zf.close()
968     
969     output ('Writing archive %s successful.' % filename)
970     if SILENT_MODE:
971         print filename
972
973
974 def make_inventory(inventory, subdir):
975     document = getDOMImplementation().createDocument(
976         None, INVENTORY_XML_ROOT, None)
977
978     # create summary entry
979     s = document.createElement(INVENTORY_XML_SUMMARY)
980     user = os.getenv('SUDO_USER', os.getenv('USER'))
981     if user:
982         s.setAttribute('user', user)
983     s.setAttribute('date', time.strftime('%c'))
984     s.setAttribute('hostname', platform.node())
985     s.setAttribute('uname', ' '.join(platform.uname()))
986     s.setAttribute('uptime', commands.getoutput(UPTIME))
987     document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(s)
988
989     map(lambda (k, v): inventory_entry(document, subdir, k, v),
990         inventory.items())
991     return document.toprettyxml()
992
993 def inventory_entry(document, subdir, k, v):
994     try:
995         el = document.createElement(INVENTORY_XML_ELEMENT)
996         el.setAttribute('capability', v['cap'])
997         el.setAttribute('filename', os.path.join(subdir, construct_filename(k, v)))
998         el.setAttribute('md5sum', md5sum(v))
999         document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(el)
1000     except:
1001         pass
1002
1003
1004 def md5sum(d):
1005     m = md5.new()
1006     if d.has_key('filename'):
1007         f = open(d['filename'])
1008         data = f.read(1024)
1009         while len(data) > 0:
1010             m.update(data)
1011             data = f.read(1024)
1012         f.close()
1013     elif d.has_key('output'):
1014         m.update(d['output'].getvalue())
1015     return m.hexdigest()
1016
1017
1018 def construct_filename(k, v):
1019     if v.has_key('filename'):
1020         if v['filename'][0] == '/':
1021             return v['filename'][1:]
1022         else:
1023             return v['filename']
1024     s = k.replace(' ', '-')
1025     s = s.replace('--', '-')
1026     s = s.replace('/', '%')
1027     if s.find('.') == -1:
1028         s += '.out'
1029
1030     return s
1031
1032 def update_capabilities():
1033     pass
1034
1035 def update_cap_size(cap, size):
1036     update_cap(cap, MIN_SIZE, size)
1037     update_cap(cap, MAX_SIZE, size)
1038     update_cap(cap, CHECKED, size > 0)
1039
1040
1041 def update_cap(cap, k, v):
1042     global caps
1043     l = list(caps[cap])
1044     l[k] = v
1045     caps[cap] = tuple(l)
1046
1047
1048 def size_of_dir(d, pattern = None, negate = False):
1049     if os.path.isdir(d):
1050         return size_of_all([os.path.join(d, fn) for fn in os.listdir(d)],
1051                            pattern, negate)
1052     else:
1053         return 0
1054
1055
1056 def size_of_all(files, pattern = None, negate = False):
1057     return sum([size_of(f, pattern, negate) for f in files])
1058
1059
1060 def matches(f, pattern, negate):
1061     if negate:
1062         return not matches(f, pattern, False)
1063     else:
1064         return pattern is None or pattern.match(f)
1065
1066
1067 def size_of(f, pattern, negate):
1068     if os.path.isfile(f) and matches(f, pattern, negate):
1069         return os.stat(f)[6]
1070     else:
1071         return size_of_dir(f, pattern, negate)
1072
1073
1074 def print_capabilities():
1075     document = getDOMImplementation().createDocument(
1076         "ns", CAP_XML_ROOT, None)
1077     map(lambda key: capability(document, key), [k for k in caps.keys() if not caps[k][HIDDEN]])
1078     print document.toprettyxml()
1079
1080 def capability(document, key):
1081     c = caps[key]
1082     el = document.createElement(CAP_XML_ELEMENT)
1083     el.setAttribute('key', c[KEY])
1084     el.setAttribute('pii', c[PII])
1085     el.setAttribute('min-size', str(c[MIN_SIZE]))
1086     el.setAttribute('max-size', str(c[MAX_SIZE]))
1087     el.setAttribute('min-time', str(c[MIN_TIME]))
1088     el.setAttribute('max-time', str(c[MAX_TIME]))
1089     el.setAttribute('content-type', c[MIME])
1090     el.setAttribute('default-checked', c[CHECKED] and 'yes' or 'no')
1091     document.getElementsByTagName(CAP_XML_ROOT)[0].appendChild(el)
1092
1093
1094 def prettyDict(d):
1095     format = '%%-%ds: %%s' % max(map(len, [k for k, _ in d.items()]))
1096     return '\n'.join([format % i for i in d.items()]) + '\n'
1097
1098
1099 def yes(prompt):
1100     yn = raw_input(prompt)
1101
1102     return len(yn) == 0 or yn.lower()[0] == 'y'
1103
1104
1105 partition_re = re.compile(r'(.*[0-9]+$)|(^xvd)')
1106
1107 def disk_list():
1108     disks = []
1109     try:
1110         f = open('/proc/partitions')
1111         f.readline()
1112         f.readline()
1113         for line in f.readlines():
1114             (major, minor, blocks, name) = line.split()
1115             if int(major) < 254 and not partition_re.match(name):
1116                 disks.append(name)
1117         f.close()
1118     except:
1119         pass
1120     return disks
1121
1122
1123 class ProcOutput:
1124     debug = False
1125
1126     def __init__(self, command, max_time, inst=None, filter=None):
1127         self.command = command
1128         self.max_time = max_time
1129         self.inst = inst
1130         self.running = False
1131         self.status = None
1132         self.timed_out = False
1133         self.failed = False
1134         self.timeout = int(time.time()) + self.max_time
1135         self.filter = filter
1136         self.filter_state = {}
1137
1138     def __del__(self):
1139         self.terminate()
1140
1141     def cmdAsStr(self):
1142         return isinstance(self.command, list) and ' '.join(self.command) or self.command
1143
1144     def run(self):
1145         self.timed_out = False
1146         try:
1147             if ProcOutput.debug:
1148                 output_ts("Starting '%s'" % self.cmdAsStr())
1149             self.proc = Popen(self.command, bufsize=1, stdin=dev_null, stdout=PIPE, stderr=dev_null, shell=isinstance(self.command, str))
1150             old = fcntl.fcntl(self.proc.stdout.fileno(), fcntl.F_GETFD)
1151             fcntl.fcntl(self.proc.stdout.fileno(), fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
1152             self.running = True
1153             self.failed = False
1154         except:
1155             output_ts("'%s' failed" % self.cmdAsStr())
1156             self.running = False
1157             self.failed = True
1158
1159     def terminate(self):
1160         if self.running:
1161             try:
1162                 os.kill(self.proc.pid, SIGTERM)
1163             except:
1164                 pass
1165             self.proc = None
1166             self.running = False
1167             self.status = SIGTERM
1168
1169     def read_line(self):
1170         assert self.running
1171         line = self.proc.stdout.readline()
1172         if line == '':
1173             # process exited
1174             self.status = self.proc.wait()
1175             self.proc = None
1176             self.running = False
1177         else:
1178             if self.filter:
1179                 line = self.filter(line, self.filter_state)
1180             if self.inst:
1181                 self.inst.write(line)
1182
1183 def run_procs(procs):
1184     while True:
1185         pipes = []
1186         active_procs = []
1187
1188         for pp in procs:
1189             for p in pp:
1190                 if p.running:
1191                     active_procs.append(p)
1192                     pipes.append(p.proc.stdout)
1193                     break
1194                 elif p.status == None and not p.failed and not p.timed_out:
1195                     p.run()
1196                     if p.running:
1197                         active_procs.append(p)
1198                         pipes.append(p.proc.stdout)
1199                         break
1200
1201         if len(pipes) == 0:
1202             # all finished
1203             break
1204
1205         (i, o, x) = select(pipes, [], [], 1.0)
1206         now = int(time.time())
1207
1208         # handle process output
1209         for p in active_procs:
1210             if p.proc.stdout in i:
1211                 p.read_line()
1212
1213             # handle timeout
1214             if p.running and now > p.timeout:
1215                 output_ts("'%s' timed out" % p.cmdAsStr())
1216                 if p.inst:
1217                     p.inst.write("\n** timeout **\n")
1218                 p.timed_out = True
1219                 p.terminate()
1220
1221
1222 def pidof(name):
1223     pids = []
1224
1225     for d in [p for p in os.listdir('/proc') if p.isdigit()]:
1226         try:
1227             if os.path.basename(os.readlink('/proc/%s/exe' % d)) == name:
1228                 pids.append(int(d))
1229         except:
1230             pass
1231
1232     return pids
1233
1234
1235 class StringIOmtime(StringIO.StringIO):
1236     def __init__(self, buf = ''):
1237         StringIO.StringIO.__init__(self, buf)
1238         self.mtime = time.time()
1239
1240     def write(self, s):
1241         StringIO.StringIO.write(self, s)
1242         self.mtime = time.time()
1243
1244
1245 if __name__ == "__main__":
1246     try:
1247         sys.exit(main())
1248     except KeyboardInterrupt:
1249         print "\nInterrupted."
1250         sys.exit(3)