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