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