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