mountstats: Breaks on 3.1 kernels
[nfs-utils.git] / tools / mountstats / mountstats.py
1 #!/usr/bin/env python
2 # -*- python-mode -*-
3 """Parse /proc/self/mountstats and display it in human readable form
4 """
5
6 __copyright__ = """
7 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
8
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License version 2 as
11 published by the Free Software Foundation.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21 MA 02110-1301 USA
22 """
23
24 import sys, os, time
25
26 Mountstats_version = '0.2'
27
28 def difference(x, y):
29     """Used for a map() function
30     """
31     return x - y
32
33 class DeviceData:
34     """DeviceData objects provide methods for parsing and displaying
35     data for a single mount grabbed from /proc/self/mountstats
36     """
37     def __init__(self):
38         self.__nfs_data = dict()
39         self.__rpc_data = dict()
40         self.__rpc_data['ops'] = []
41
42     def __parse_nfs_line(self, words):
43         if words[0] == 'device':
44             self.__nfs_data['export'] = words[1]
45             self.__nfs_data['mountpoint'] = words[4]
46             self.__nfs_data['fstype'] = words[7]
47             if words[7].find('nfs') != -1:
48                 self.__nfs_data['statvers'] = words[8]
49         elif 'nfs' in words or 'nfs4' in words:
50             self.__nfs_data['export'] = words[0]
51             self.__nfs_data['mountpoint'] = words[3]
52             self.__nfs_data['fstype'] = words[6]
53             if words[6].find('nfs') != -1:
54                 self.__nfs_data['statvers'] = words[7]
55         elif words[0] == 'age:':
56             self.__nfs_data['age'] = long(words[1])
57         elif words[0] == 'opts:':
58             self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
59         elif words[0] == 'caps:':
60             self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
61         elif words[0] == 'nfsv4:':
62             self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
63         elif words[0] == 'sec:':
64             keys = ''.join(words[1:]).split(',')
65             self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
66             self.__nfs_data['pseudoflavor'] = 0
67             if self.__nfs_data['flavor'] == 6:
68                 self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
69         elif words[0] == 'events:':
70             self.__nfs_data['inoderevalidates'] = int(words[1])
71             self.__nfs_data['dentryrevalidates'] = int(words[2])
72             self.__nfs_data['datainvalidates'] = int(words[3])
73             self.__nfs_data['attrinvalidates'] = int(words[4])
74             self.__nfs_data['syncinodes'] = int(words[5])
75             self.__nfs_data['vfsopen'] = int(words[6])
76             self.__nfs_data['vfslookup'] = int(words[7])
77             self.__nfs_data['vfspermission'] = int(words[8])
78             self.__nfs_data['vfsreadpage'] = int(words[9])
79             self.__nfs_data['vfsreadpages'] = int(words[10])
80             self.__nfs_data['vfswritepage'] = int(words[11])
81             self.__nfs_data['vfswritepages'] = int(words[12])
82             self.__nfs_data['vfsreaddir'] = int(words[13])
83             self.__nfs_data['vfsflush'] = int(words[14])
84             self.__nfs_data['vfsfsync'] = int(words[15])
85             self.__nfs_data['vfslock'] = int(words[16])
86             self.__nfs_data['vfsrelease'] = int(words[17])
87             self.__nfs_data['setattrtrunc'] = int(words[18])
88             self.__nfs_data['extendwrite'] = int(words[19])
89             self.__nfs_data['sillyrenames'] = int(words[20])
90             self.__nfs_data['shortreads'] = int(words[21])
91             self.__nfs_data['shortwrites'] = int(words[22])
92             self.__nfs_data['delay'] = int(words[23])
93         elif words[0] == 'bytes:':
94             self.__nfs_data['normalreadbytes'] = long(words[1])
95             self.__nfs_data['normalwritebytes'] = long(words[2])
96             self.__nfs_data['directreadbytes'] = long(words[3])
97             self.__nfs_data['directwritebytes'] = long(words[4])
98             self.__nfs_data['serverreadbytes'] = long(words[5])
99             self.__nfs_data['serverwritebytes'] = long(words[6])
100
101     def __parse_rpc_line(self, words):
102         if words[0] == 'RPC':
103             self.__rpc_data['statsvers'] = float(words[3])
104             self.__rpc_data['programversion'] = words[5]
105         elif words[0] == 'xprt:':
106             self.__rpc_data['protocol'] = words[1]
107             if words[1] == 'udp':
108                 self.__rpc_data['port'] = int(words[2])
109                 self.__rpc_data['bind_count'] = int(words[3])
110                 self.__rpc_data['rpcsends'] = int(words[4])
111                 self.__rpc_data['rpcreceives'] = int(words[5])
112                 self.__rpc_data['badxids'] = int(words[6])
113                 self.__rpc_data['inflightsends'] = long(words[7])
114                 self.__rpc_data['backlogutil'] = long(words[8])
115             elif words[1] == 'tcp':
116                 self.__rpc_data['port'] = words[2]
117                 self.__rpc_data['bind_count'] = int(words[3])
118                 self.__rpc_data['connect_count'] = int(words[4])
119                 self.__rpc_data['connect_time'] = int(words[5])
120                 self.__rpc_data['idle_time'] = int(words[6])
121                 self.__rpc_data['rpcsends'] = int(words[7])
122                 self.__rpc_data['rpcreceives'] = int(words[8])
123                 self.__rpc_data['badxids'] = int(words[9])
124                 self.__rpc_data['inflightsends'] = long(words[10])
125                 self.__rpc_data['backlogutil'] = int(words[11])
126             elif words[1] == 'rdma':
127                 self.__rpc_data['port'] = words[2]
128                 self.__rpc_data['bind_count'] = int(words[3])
129                 self.__rpc_data['connect_count'] = int(words[4])
130                 self.__rpc_data['connect_time'] = int(words[5])
131                 self.__rpc_data['idle_time'] = int(words[6])
132                 self.__rpc_data['rpcsends'] = int(words[7])
133                 self.__rpc_data['rpcreceives'] = int(words[8])
134                 self.__rpc_data['badxids'] = int(words[9])
135                 self.__rpc_data['backlogutil'] = int(words[10])
136                 self.__rpc_data['read_chunks'] = int(words[11])
137                 self.__rpc_data['write_chunks'] = int(words[12])
138                 self.__rpc_data['reply_chunks'] = int(words[13])
139                 self.__rpc_data['total_rdma_req'] = int(words[14])
140                 self.__rpc_data['total_rdma_rep'] = int(words[15])
141                 self.__rpc_data['pullup'] = int(words[16])
142                 self.__rpc_data['fixup'] = int(words[17])
143                 self.__rpc_data['hardway'] = int(words[18])
144                 self.__rpc_data['failed_marshal'] = int(words[19])
145                 self.__rpc_data['bad_reply'] = int(words[20])
146         elif words[0] == 'per-op':
147             self.__rpc_data['per-op'] = words
148         else:
149             op = words[0][:-1]
150             self.__rpc_data['ops'] += [op]
151             self.__rpc_data[op] = [long(word) for word in words[1:]]
152
153     def parse_stats(self, lines):
154         """Turn a list of lines from a mount stat file into a 
155         dictionary full of stats, keyed by name
156         """
157         found = False
158         for line in lines:
159             words = line.split()
160             if len(words) == 0:
161                 continue
162             if (not found and words[0] != 'RPC'):
163                 self.__parse_nfs_line(words)
164                 continue
165
166             found = True
167             self.__parse_rpc_line(words)
168
169     def is_nfs_mountpoint(self):
170         """Return True if this is an NFS or NFSv4 mountpoint,
171         otherwise return False
172         """
173         if self.__nfs_data['fstype'] == 'nfs':
174             return True
175         elif self.__nfs_data['fstype'] == 'nfs4':
176             return True
177         return False
178
179     def display_nfs_options(self):
180         """Pretty-print the NFS options
181         """
182         print 'Stats for %s mounted on %s:' % \
183             (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
184
185         print '  NFS mount options: %s' % ','.join(self.__nfs_data['mountoptions'])
186         print '  NFS server capabilities: %s' % ','.join(self.__nfs_data['servercapabilities'])
187         if self.__nfs_data.has_key('nfsv4flags'):
188             print '  NFSv4 capability flags: %s' % ','.join(self.__nfs_data['nfsv4flags'])
189         if self.__nfs_data.has_key('pseudoflavor'):
190             print '  NFS security flavor: %d  pseudoflavor: %d' % \
191                 (self.__nfs_data['flavor'], self.__nfs_data['pseudoflavor'])
192         else:
193             print '  NFS security flavor: %d' % self.__nfs_data['flavor']
194
195     def display_nfs_events(self):
196         """Pretty-print the NFS event counters
197         """
198         print
199         print 'Cache events:'
200         print '  data cache invalidated %d times' % self.__nfs_data['datainvalidates']
201         print '  attribute cache invalidated %d times' % self.__nfs_data['attrinvalidates']
202         print '  inodes synced %d times' % self.__nfs_data['syncinodes']
203         print
204         print 'VFS calls:'
205         print '  VFS requested %d inode revalidations' % self.__nfs_data['inoderevalidates']
206         print '  VFS requested %d dentry revalidations' % self.__nfs_data['dentryrevalidates']
207         print
208         print '  VFS called nfs_readdir() %d times' % self.__nfs_data['vfsreaddir']
209         print '  VFS called nfs_lookup() %d times' % self.__nfs_data['vfslookup']
210         print '  VFS called nfs_permission() %d times' % self.__nfs_data['vfspermission']
211         print '  VFS called nfs_file_open() %d times' % self.__nfs_data['vfsopen']
212         print '  VFS called nfs_file_flush() %d times' % self.__nfs_data['vfsflush']
213         print '  VFS called nfs_lock() %d times' % self.__nfs_data['vfslock']
214         print '  VFS called nfs_fsync() %d times' % self.__nfs_data['vfsfsync']
215         print '  VFS called nfs_file_release() %d times' % self.__nfs_data['vfsrelease']
216         print
217         print 'VM calls:'
218         print '  VFS called nfs_readpage() %d times' % self.__nfs_data['vfsreadpage']
219         print '  VFS called nfs_readpages() %d times' % self.__nfs_data['vfsreadpages']
220         print '  VFS called nfs_writepage() %d times' % self.__nfs_data['vfswritepage']
221         print '  VFS called nfs_writepages() %d times' % self.__nfs_data['vfswritepages']
222         print
223         print 'Generic NFS counters:'
224         print '  File size changing operations:'
225         print '    truncating SETATTRs: %d  extending WRITEs: %d' % \
226             (self.__nfs_data['setattrtrunc'], self.__nfs_data['extendwrite'])
227         print '  %d silly renames' % self.__nfs_data['sillyrenames']
228         print '  short reads: %d  short writes: %d' % \
229             (self.__nfs_data['shortreads'], self.__nfs_data['shortwrites'])
230         print '  NFSERR_DELAYs from server: %d' % self.__nfs_data['delay']
231
232     def display_nfs_bytes(self):
233         """Pretty-print the NFS event counters
234         """
235         print
236         print 'NFS byte counts:'
237         print '  applications read %d bytes via read(2)' % self.__nfs_data['normalreadbytes']
238         print '  applications wrote %d bytes via write(2)' % self.__nfs_data['normalwritebytes']
239         print '  applications read %d bytes via O_DIRECT read(2)' % self.__nfs_data['directreadbytes']
240         print '  applications wrote %d bytes via O_DIRECT write(2)' % self.__nfs_data['directwritebytes']
241         print '  client read %d bytes via NFS READ' % self.__nfs_data['serverreadbytes']
242         print '  client wrote %d bytes via NFS WRITE' % self.__nfs_data['serverwritebytes']
243
244     def display_rpc_generic_stats(self):
245         """Pretty-print the generic RPC stats
246         """
247         sends = self.__rpc_data['rpcsends']
248
249         print
250         print 'RPC statistics:'
251
252         print '  %d RPC requests sent, %d RPC replies received (%d XIDs not found)' % \
253             (sends, self.__rpc_data['rpcreceives'], self.__rpc_data['badxids'])
254         if sends != 0:
255             print '  average backlog queue length: %d' % \
256                 (float(self.__rpc_data['backlogutil']) / sends)
257
258     def display_rpc_op_stats(self):
259         """Pretty-print the per-op stats
260         """
261         sends = self.__rpc_data['rpcsends']
262
263         # XXX: these should be sorted by 'count'
264         print
265         for op in self.__rpc_data['ops']:
266             stats = self.__rpc_data[op]
267             count = stats[0]
268             retrans = stats[1] - count
269             if count != 0:
270                 print '%s:' % op
271                 print '\t%d ops (%d%%)' % \
272                     (count, ((count * 100) / sends)),
273                 print '\t%d retrans (%d%%)' % (retrans, ((retrans * 100) / count)),
274                 print '\t%d major timeouts' % stats[2]
275                 print '\tavg bytes sent per op: %d\tavg bytes received per op: %d' % \
276                     (stats[3] / count, stats[4] / count)
277                 print '\tbacklog wait: %f' % (float(stats[5]) / count),
278                 print '\tRTT: %f' % (float(stats[6]) / count),
279                 print '\ttotal execute time: %f (milliseconds)' % \
280                     (float(stats[7]) / count)
281
282     def compare_iostats(self, old_stats):
283         """Return the difference between two sets of stats
284         """
285         result = DeviceData()
286
287         # copy self into result
288         for key, value in self.__nfs_data.iteritems():
289             result.__nfs_data[key] = value
290         for key, value in self.__rpc_data.iteritems():
291             result.__rpc_data[key] = value
292
293         # compute the difference of each item in the list
294         # note the copy loop above does not copy the lists, just
295         # the reference to them.  so we build new lists here
296         # for the result object.
297         for op in result.__rpc_data['ops']:
298             result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
299
300         # update the remaining keys we care about
301         result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
302         result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
303         result.__nfs_data['serverreadbytes'] -= old_stats.__nfs_data['serverreadbytes']
304         result.__nfs_data['serverwritebytes'] -= old_stats.__nfs_data['serverwritebytes']
305
306         return result
307
308     def display_iostats(self, sample_time):
309         """Display NFS and RPC stats in an iostat-like way
310         """
311         sends = float(self.__rpc_data['rpcsends'])
312         if sample_time == 0:
313             sample_time = float(self.__nfs_data['age'])
314
315         print
316         print '%s mounted on %s:' % \
317             (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
318
319         print '\top/s\trpc bklog'
320         print '\t%.2f' % (sends / sample_time), 
321         if sends != 0:
322             print '\t%.2f' % \
323                 ((float(self.__rpc_data['backlogutil']) / sends) / sample_time)
324         else:
325             print '\t0.00'
326
327         # reads:  ops/s, kB/s, avg rtt, and avg exe
328         # XXX: include avg xfer size and retransmits?
329         read_rpc_stats = self.__rpc_data['READ']
330         ops = float(read_rpc_stats[0])
331         kilobytes = float(self.__nfs_data['serverreadbytes']) / 1024
332         rtt = float(read_rpc_stats[6])
333         exe = float(read_rpc_stats[7])
334
335         print '\treads:\tops/s\t\tkB/s\t\tavg RTT (ms)\tavg exe (ms)'
336         print '\t\t%.2f' % (ops / sample_time),
337         print '\t\t%.2f' % (kilobytes / sample_time),
338         if ops != 0:
339             print '\t\t%.2f' % (rtt / ops),
340             print '\t\t%.2f' % (exe / ops)
341         else:
342             print '\t\t0.00',
343             print '\t\t0.00'
344
345         # writes:  ops/s, kB/s, avg rtt, and avg exe
346         # XXX: include avg xfer size and retransmits?
347         write_rpc_stats = self.__rpc_data['WRITE']
348         ops = float(write_rpc_stats[0])
349         kilobytes = float(self.__nfs_data['serverwritebytes']) / 1024
350         rtt = float(write_rpc_stats[6])
351         exe = float(write_rpc_stats[7])
352
353         print '\twrites:\tops/s\t\tkB/s\t\tavg RTT (ms)\tavg exe (ms)'
354         print '\t\t%.2f' % (ops / sample_time),
355         print '\t\t%.2f' % (kilobytes / sample_time),
356         if ops != 0:
357             print '\t\t%.2f' % (rtt / ops),
358             print '\t\t%.2f' % (exe / ops)
359         else:
360             print '\t\t0.00',
361             print '\t\t0.00'
362
363 def parse_stats_file(filename):
364     """pop the contents of a mountstats file into a dictionary,
365     keyed by mount point.  each value object is a list of the
366     lines in the mountstats file corresponding to the mount
367     point named in the key.
368     """
369     ms_dict = dict()
370     key = ''
371
372     f = file(filename)
373     for line in f.readlines():
374         words = line.split()
375         if len(words) == 0:
376             continue
377         if words[0] == 'device':
378             key = words[4]
379             new = [ line.strip() ]
380         elif 'nfs' in words or 'nfs4' in words:
381             key = words[3]
382             new = [ line.strip() ]
383         else:
384             new += [ line.strip() ]
385         ms_dict[key] = new
386     f.close
387
388     return ms_dict
389
390 def print_mountstats_help(name):
391     print 'usage: %s [ options ] <mount point>' % name
392     print
393     print ' Version %s' % Mountstats_version
394     print
395     print ' Display NFS client per-mount statistics.'
396     print
397     print '  --version    display the version of this command'
398     print '  --nfs        display only the NFS statistics'
399     print '  --rpc        display only the RPC statistics'
400     print '  --start      sample and save statistics'
401     print '  --end        resample statistics and compare them with saved'
402     print
403
404 def mountstats_command():
405     """Mountstats command
406     """
407     mountpoints = []
408     nfs_only = False
409     rpc_only = False
410
411     for arg in sys.argv:
412         if arg in ['-h', '--help', 'help', 'usage']:
413             print_mountstats_help(prog)
414             return
415
416         if arg in ['-v', '--version', 'version']:
417             print '%s version %s' % (sys.argv[0], Mountstats_version)
418             sys.exit(0)
419
420         if arg in ['-n', '--nfs']:
421             nfs_only = True
422             continue
423
424         if arg in ['-r', '--rpc']:
425             rpc_only = True
426             continue
427
428         if arg in ['-s', '--start']:
429             raise Exception, 'Sampling is not yet implemented'
430
431         if arg in ['-e', '--end']:
432             raise Exception, 'Sampling is not yet implemented'
433
434         if arg == sys.argv[0]:
435             continue
436
437         mountpoints += [arg]
438
439     if mountpoints == []:
440         print_mountstats_help(prog)
441         return
442
443     if rpc_only == True and nfs_only == True:
444         print_mountstats_help(prog)
445         return
446
447     mountstats = parse_stats_file('/proc/self/mountstats')
448
449     for mp in mountpoints:
450         if mp not in mountstats:
451             print 'Statistics for mount point %s not found' % mp
452             continue
453
454         stats = DeviceData()
455         stats.parse_stats(mountstats[mp])
456
457         if not stats.is_nfs_mountpoint():
458             print 'Mount point %s exists but is not an NFS mount' % mp
459             continue
460
461         if nfs_only:
462            stats.display_nfs_options()
463            stats.display_nfs_events()
464            stats.display_nfs_bytes()
465         elif rpc_only:
466            stats.display_rpc_generic_stats()
467            stats.display_rpc_op_stats()
468         else:
469            stats.display_nfs_options()
470            stats.display_nfs_bytes()
471            stats.display_rpc_generic_stats()
472            stats.display_rpc_op_stats()
473
474 def print_nfsstat_help(name):
475     print 'usage: %s [ options ]' % name
476     print
477     print ' Version %s' % Mountstats_version
478     print
479     print ' nfsstat-like program that uses NFS client per-mount statistics.'
480     print
481
482 def nfsstat_command():
483     print_nfsstat_help(prog)
484
485 def print_iostat_help(name):
486     print 'usage: %s [ <interval> [ <count> ] ] [ <mount point> ] ' % name
487     print
488     print ' Version %s' % Mountstats_version
489     print
490     print ' iostat-like program to display NFS client per-mount statistics.'
491     print
492     print ' The <interval> parameter specifies the amount of time in seconds between'
493     print ' each report.  The first report contains statistics for the time since each'
494     print ' file system was mounted.  Each subsequent report contains statistics'
495     print ' collected during the interval since the previous report.'
496     print
497     print ' If the <count> parameter is specified, the value of <count> determines the'
498     print ' number of reports generated at <interval> seconds apart.  If the interval'
499     print ' parameter is specified without the <count> parameter, the command generates'
500     print ' reports continuously.'
501     print
502     print ' If one or more <mount point> names are specified, statistics for only these'
503     print ' mount points will be displayed.  Otherwise, all NFS mount points on the'
504     print ' client are listed.'
505     print
506
507 def print_iostat_summary(old, new, devices, time):
508     for device in devices:
509         stats = DeviceData()
510         stats.parse_stats(new[device])
511         if not old:
512             stats.display_iostats(time)
513         else:
514             old_stats = DeviceData()
515             old_stats.parse_stats(old[device])
516             diff_stats = stats.compare_iostats(old_stats)
517             diff_stats.display_iostats(time)
518
519 def iostat_command():
520     """iostat-like command for NFS mount points
521     """
522     mountstats = parse_stats_file('/proc/self/mountstats')
523     devices = []
524     interval_seen = False
525     count_seen = False
526
527     for arg in sys.argv:
528         if arg in ['-h', '--help', 'help', 'usage']:
529             print_iostat_help(prog)
530             return
531
532         if arg in ['-v', '--version', 'version']:
533             print '%s version %s' % (sys.argv[0], Mountstats_version)
534             return
535
536         if arg == sys.argv[0]:
537             continue
538
539         if arg in mountstats:
540             devices += [arg]
541         elif not interval_seen:
542             interval = int(arg)
543             if interval > 0:
544                 interval_seen = True
545             else:
546                 print 'Illegal <interval> value'
547                 return
548         elif not count_seen:
549             count = int(arg)
550             if count > 0:
551                 count_seen = True
552             else:
553                 print 'Illegal <count> value'
554                 return
555
556     # make certain devices contains only NFS mount points
557     if len(devices) > 0:
558         check = []
559         for device in devices:
560             stats = DeviceData()
561             stats.parse_stats(mountstats[device])
562             if stats.is_nfs_mountpoint():
563                 check += [device]
564         devices = check
565     else:
566         for device, descr in mountstats.iteritems():
567             stats = DeviceData()
568             stats.parse_stats(descr)
569             if stats.is_nfs_mountpoint():
570                 devices += [device]
571     if len(devices) == 0:
572         print 'No NFS mount points were found'
573         return
574
575     old_mountstats = None
576     sample_time = 0
577
578     if not interval_seen:
579         print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
580         return
581
582     if count_seen:
583         while count != 0:
584             print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
585             old_mountstats = mountstats
586             time.sleep(interval)
587             sample_time = interval
588             mountstats = parse_stats_file('/proc/self/mountstats')
589             count -= 1
590     else: 
591         while True:
592             print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
593             old_mountstats = mountstats
594             time.sleep(interval)
595             sample_time = interval
596             mountstats = parse_stats_file('/proc/self/mountstats')
597
598 #
599 # Main
600 #
601 prog = os.path.basename(sys.argv[0])
602
603 try:
604     if prog == 'mountstats':
605         mountstats_command()
606     elif prog == 'ms-nfsstat':
607         nfsstat_command()
608     elif prog == 'ms-iostat':
609         iostat_command()
610 except KeyboardInterrupt:
611     print 'Caught ^C... exiting'
612     sys.exit(1)
613
614 sys.exit(0)