additional read/write access tests
[pithos] / tools / store
1 #!/usr/bin/env python
2
3 # Copyright 2011 GRNET S.A. All rights reserved.
4
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
7 # conditions are met:
8
9 #   1. Redistributions of source code must retain the above
10 #      copyright notice, this list of conditions and the following
11 #      disclaimer.
12
13 #   2. Redistributions in binary form must reproduce the above
14 #      copyright notice, this list of conditions and the following
15 #      disclaimer in the documentation and/or other materials
16 #      provided with the distribution.
17
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
35
36 from getpass import getuser
37 from optparse import OptionParser
38 from os import environ
39 from sys import argv, exit, stdin, stdout
40 from datetime import datetime
41 from lib.client import Pithos_Client, Fault
42 from lib.util import get_user, get_auth, get_server, get_api
43
44 import json
45 import logging
46 import types
47 import re
48 import time as _time
49 import os
50
51 _cli_commands = {}
52
53 def cli_command(*args):
54     def decorator(cls):
55         cls.commands = args
56         for name in args:
57             _cli_commands[name] = cls
58         return cls
59     return decorator
60
61 def class_for_cli_command(name):
62     return _cli_commands[name]
63
64 class Command(object):
65     syntax = ''
66     
67     def __init__(self, name, argv):
68         parser = OptionParser('%%prog %s [options] %s' % (name, self.syntax))
69         parser.add_option('--host', dest='host', metavar='HOST',
70                           default=get_server(), help='use server HOST')
71         parser.add_option('--user', dest='user', metavar='USERNAME',
72                           default=get_user(),
73                           help='use account USERNAME')
74         parser.add_option('--token', dest='token', metavar='AUTH',
75                           default=get_auth(),
76                           help='use account AUTH')
77         parser.add_option('--api', dest='api', metavar='API',
78                           default=get_api(), help='use api API')
79         parser.add_option('-v', action='store_true', dest='verbose',
80                           default=False, help='use verbose output')
81         parser.add_option('-d', action='store_true', dest='debug',
82                           default=False, help='use debug output')
83         self.add_options(parser)
84         options, args = parser.parse_args(argv)
85         
86         # Add options to self
87         for opt in parser.option_list:
88             key = opt.dest
89             if key:
90                 val = getattr(options, key)
91                 setattr(self, key, val)
92         
93         self.client = Pithos_Client(self.host, self.token, self.user, self.api, self.verbose,
94                              self.debug)
95         
96         self.parser = parser
97         self.args = args
98     
99     def _build_args(self, attrs):
100         args = {}
101         for a in [a for a in attrs if getattr(self, a)]:
102             args[a] = getattr(self, a)
103         return args
104
105     def add_options(self, parser):
106         pass
107     
108     def execute(self, *args):
109         pass
110
111 @cli_command('list', 'ls')
112 class List(Command):
113     syntax = '[<container>[/<object>]]'
114     description = 'list containers or objects'
115     
116     def add_options(self, parser):
117         parser.add_option('-l', action='store_true', dest='detail',
118                           default=False, help='show detailed output')
119         parser.add_option('-n', action='store', type='int', dest='limit',
120                           default=10000, help='show limited output')
121         parser.add_option('--marker', action='store', type='str',
122                           dest='marker', default=None,
123                           help='show output greater then marker')
124         parser.add_option('--prefix', action='store', type='str',
125                           dest='prefix', default=None,
126                           help='show output starting with prefix')
127         parser.add_option('--delimiter', action='store', type='str',
128                           dest='delimiter', default=None,
129                           help='show output up to the delimiter')
130         parser.add_option('--path', action='store', type='str',
131                           dest='path', default=None,
132                           help='show output starting with prefix up to /')
133         parser.add_option('--meta', action='store', type='str',
134                           dest='meta', default=None,
135                           help='show output having the specified meta keys')
136         parser.add_option('--if-modified-since', action='store', type='str',
137                           dest='if_modified_since', default=None,
138                           help='show output if modified since then')
139         parser.add_option('--if-unmodified-since', action='store', type='str',
140                           dest='if_unmodified_since', default=None,
141                           help='show output if not modified since then')
142         parser.add_option('--until', action='store', dest='until',
143                           default=None, help='show metadata until that date')
144         parser.add_option('--format', action='store', dest='format',
145                           default='%d/%m/%Y', help='format to parse until date')
146     
147     def execute(self, container=None):
148         if container:
149             self.list_objects(container)
150         else:
151             self.list_containers()
152     
153     def list_containers(self):
154         attrs = ['limit', 'marker', 'if_modified_since',
155                  'if_unmodified_since']
156         args = self._build_args(attrs)
157         args['format'] = 'json' if self.detail else 'text'
158         
159         if getattr(self, 'until'):
160             t = _time.strptime(self.until, self.format)
161             args['until'] = int(_time.mktime(t))
162         
163         l = self.client.list_containers(**args)
164         print_list(l)
165     
166     def list_objects(self, container):
167         #prepate params
168         params = {}
169         attrs = ['limit', 'marker', 'prefix', 'delimiter', 'path',
170                  'meta', 'if_modified_since', 'if_unmodified_since']
171         args = self._build_args(attrs)
172         args['format'] = 'json' if self.detail else 'text'
173         
174         if self.until:
175             t = _time.strptime(self.until, self.format)
176             args['until'] = int(_time.mktime(t))
177         
178         container, sep, object = container.partition('/')
179         if object:
180             return
181         
182         detail = 'json'
183         #if request with meta quering disable trash filtering
184         show_trashed = True if self.meta else False
185         l = self.client.list_objects(container, **args)
186         print_list(l, detail=self.detail)
187
188 @cli_command('meta')
189 class Meta(Command):
190     syntax = '[<container>[/<object>]]'
191     description = 'get account/container/object metadata'
192     
193     def add_options(self, parser):
194         parser.add_option('-r', action='store_true', dest='restricted',
195                           default=False, help='show only user defined metadata')
196         parser.add_option('--until', action='store', dest='until',
197                           default=None, help='show metadata until that date')
198         parser.add_option('--format', action='store', dest='format',
199                           default='%d/%m/%Y', help='format to parse until date')
200         parser.add_option('--version', action='store', dest='version',
201                           default=None, help='show specific version \
202                                   (applies only for objects)')
203     
204     def execute(self, path=''):
205         container, sep, object = path.partition('/')
206         args = {'restricted':self.restricted}
207         if getattr(self, 'until'):
208             t = _time.strptime(self.until, self.format)
209             args['until'] = int(_time.mktime(t))
210         
211         if object:
212             meta = self.client.retrieve_object_metadata(container, object,
213                                                         self.restricted,
214                                                         self.version)
215         elif container:
216             meta = self.client.retrieve_container_metadata(container, **args)
217         else:
218             meta = self.client.retrieve_account_metadata(**args)
219         if meta == None:
220             print 'Entity does not exist'
221         else:
222             print_dict(meta, header=None)
223
224 @cli_command('create')
225 class CreateContainer(Command):
226     syntax = '<container> [key=val] [...]'
227     description = 'create a container'
228     
229     def execute(self, container, *args):
230         meta = {}
231         for arg in args:
232             key, sep, val = arg.partition('=')
233             meta[key] = val
234         ret = self.client.create_container(container, **meta)
235         if not ret:
236             print 'Container already exists'
237
238 @cli_command('delete', 'rm')
239 class Delete(Command):
240     syntax = '<container>[/<object>]'
241     description = 'delete a container or an object'
242     
243     def add_options(self, parser):
244         parser.add_option('--until', action='store', dest='until',
245                           default=None, help='remove history until that date')
246         parser.add_option('--format', action='store', dest='format',
247                           default='%d/%m/%Y', help='format to parse until date')
248     
249     def execute(self, path):
250         container, sep, object = path.partition('/')
251         until = None
252         if getattr(self, 'until'):
253             t = _time.strptime(self.until, self.format)
254             until = int(_time.mktime(t))
255         
256         if object:
257             self.client.delete_object(container, object, until)
258         else:
259             self.client.delete_container(container, until)
260
261 @cli_command('get')
262 class GetObject(Command):
263     syntax = '<container>/<object>'
264     description = 'get the data of an object'
265     
266     def add_options(self, parser):
267         parser.add_option('-l', action='store_true', dest='detail',
268                           default=False, help='show detailed output')
269         parser.add_option('--range', action='store', dest='range',
270                           default=None, help='show range of data')
271         parser.add_option('--if-range', action='store', dest='if_range',
272                           default=None, help='show range of data')
273         parser.add_option('--if-match', action='store', dest='if_match',
274                           default=None, help='show output if ETags match')
275         parser.add_option('--if-none-match', action='store',
276                           dest='if_none_match', default=None,
277                           help='show output if ETags don\'t match')
278         parser.add_option('--if-modified-since', action='store', type='str',
279                           dest='if_modified_since', default=None,
280                           help='show output if modified since then')
281         parser.add_option('--if-unmodified-since', action='store', type='str',
282                           dest='if_unmodified_since', default=None,
283                           help='show output if not modified since then')
284         parser.add_option('-o', action='store', type='str',
285                           dest='file', default=None,
286                           help='save output in file')
287         parser.add_option('--version', action='store', type='str',
288                           dest='version', default=None,
289                           help='get the specific \
290                                version')
291         parser.add_option('--versionlist', action='store_true',
292                           dest='versionlist', default=False,
293                           help='get the full object version list')
294     
295     def execute(self, path):
296         attrs = ['if_match', 'if_none_match', 'if_modified_since',
297                  'if_unmodified_since']
298         args = self._build_args(attrs)
299         args['format'] = 'json' if self.detail else 'text'
300         if self.range:
301             args['range'] = 'bytes=%s' %self.range
302         if getattr(self, 'if_range'):
303             args['if-range'] = 'If-Range:%s' % getattr(self, 'if_range')
304         
305         container, sep, object = path.partition('/')
306         data = None
307         if self.versionlist:
308             if 'detail' in args.keys():
309                 args.pop('detail')
310             data = self.client.retrieve_object_versionlist(container, object, **args)
311         elif self.version:
312             data = self.client.retrieve_object_version(container, object,
313                                                        self.version, **args)
314         else:
315             data = self.client.retrieve_object(container, object, **args)    
316         
317         f = self.file and open(self.file, 'w') or stdout
318         if self.detail:
319             data = json.loads(data)
320             if self.versionlist:
321                 print_versions(data, f=f)
322             else:
323                 print_dict(data, f=f)
324         else:
325             f.write(data)
326         f.close()
327
328 @cli_command('mkdir')
329 class PutMarker(Command):
330     syntax = '<container>/<directory marker>'
331     description = 'create a directory marker'
332     
333     def execute(self, path):
334         container, sep, object = path.partition('/')
335         self.client.create_directory_marker(container, object)
336
337 @cli_command('put')
338 class PutObject(Command):
339     syntax = '<container>/<object> [key=val] [...]'
340     description = 'create/override object'
341     
342     def add_options(self, parser):
343         parser.add_option('--use_hashes', action='store_true', dest='use_hashes',
344                           default=False, help='provide hashmap instead of data')
345         parser.add_option('--chunked', action='store_true', dest='chunked',
346                           default=False, help='set chunked transfer mode')
347         parser.add_option('--etag', action='store', dest='etag',
348                           default=None, help='check written data')
349         parser.add_option('--content-encoding', action='store',
350                           dest='content_encoding', default=None,
351                           help='provide the object MIME content type')
352         parser.add_option('--content-disposition', action='store', type='str',
353                           dest='content_disposition', default=None,
354                           help='provide the presentation style of the object')
355         #parser.add_option('-S', action='store',
356         #                  dest='segment_size', default=False,
357         #                  help='use for large file support')
358         parser.add_option('--manifest', action='store',
359                           dest='x_object_manifest', default=None,
360                           help='upload a manifestation file')
361         parser.add_option('--content-type', action='store',
362                           dest='content_type', default=None,
363                           help='create object with specific content type')
364         parser.add_option('--sharing', action='store',
365                           dest='x_object_sharing', default=None,
366                           help='define sharing object policy')
367         parser.add_option('-f', action='store',
368                           dest='srcpath', default=None,
369                           help='file descriptor to read from (pass - for standard input)')
370         parser.add_option('--public', action='store_true',
371                           dest='x_object_public', default=False,
372                           help='make object publicly accessible')
373     
374     def execute(self, path, *args):
375         if path.find('=') != -1:
376             raise Fault('Missing path argument')
377         
378         #prepare user defined meta
379         meta = {}
380         for arg in args:
381             key, sep, val = arg.partition('=')
382             meta[key] = val
383         
384         attrs = ['etag', 'content_encoding', 'content_disposition',
385                  'content_type', 'x_object_sharing', 'x_object_public']
386         args = self._build_args(attrs)
387         
388         container, sep, object = path.partition('/')
389         
390         f = None
391         if self.srcpath:
392             f = open(self.srcpath) if self.srcpath != '-' else stdin
393         
394         if self.use_hashes and not f:
395             raise Fault('Illegal option combination')
396         
397         if self.chunked:
398             self.client.create_object_using_chunks(container, object, f,
399                                                     meta=meta, **args)
400         elif self.use_hashes:
401             format = 'json' if detail else 'text'
402             self.client.create_object_by_hashmap(container, object, f, format,
403                                  meta=meta, **args)
404         elif self.x_object_manifest:
405             self.client.create_manifestation(container, object, self.x_object_manifest)
406         elif not f:
407             self.client.create_zero_length_object(container, object, meta=meta, **args)
408         else:
409             self.client.create_object(container, object, f, meta=meta, **args)
410         if f:
411             f.close()
412
413 @cli_command('copy', 'cp')
414 class CopyObject(Command):
415     syntax = '<src container>/<src object> [<dst container>/]<dst object> [key=val] [...]'
416     description = 'copy an object to a different location'
417     
418     def add_options(self, parser):
419         parser.add_option('--version', action='store',
420                           dest='version', default=False,
421                           help='copy specific version')
422         parser.add_option('--public', action='store_true',
423                           dest='public', default=False,
424                           help='make object publicly accessible')
425     
426     def execute(self, src, dst, *args):
427         src_container, sep, src_object = src.partition('/')
428         dst_container, sep, dst_object = dst.partition('/')
429         
430         #prepare user defined meta
431         meta = {}
432         for arg in args:
433             key, sep, val = arg.partition('=')
434             meta[key] = val
435         
436         if not sep:
437             dst_container = src_container
438             dst_object = dst
439         
440         self.client.copy_object(src_container, src_object, dst_container,
441                                 dst_object, meta, self.public, self.version, **meta)
442
443 @cli_command('set')
444 class SetMeta(Command):
445     syntax = '[<container>[/<object>]] key=val [key=val] [...]'
446     description = 'set account/container/object metadata'
447     
448     def execute(self, path, *args):
449         #in case of account fix the args
450         if path.find('=') != -1:
451             args = list(args)
452             args.append(path)
453             args = tuple(args)
454             path = ''
455         meta = {}
456         for arg in args:
457             key, sep, val = arg.partition('=')
458             meta[key.strip()] = val.strip()
459         container, sep, object = path.partition('/')
460         if object:
461             self.client.update_object_metadata(container, object, **meta)
462         elif container:
463             self.client.update_container_metadata(container, **meta)
464         else:
465             self.client.update_account_metadata(**meta)
466
467 @cli_command('update')
468 class UpdateObject(Command):
469     syntax = '<container>/<object> path [key=val] [...]'
470     description = 'update object metadata/data (default mode: append)'
471     
472     def add_options(self, parser):
473         parser.add_option('-a', action='store_true', dest='append',
474                           default=True, help='append data')
475         parser.add_option('--offset', action='store',
476                           dest='offset',
477                           default=None, help='starting offest to be updated')
478         parser.add_option('--range', action='store', dest='content-range',
479                           default=None, help='range of data to be updated')
480         parser.add_option('--chunked', action='store_true', dest='chunked',
481                           default=False, help='set chunked transfer mode')
482         parser.add_option('--content-encoding', action='store',
483                           dest='content_encoding', default=None,
484                           help='provide the object MIME content type')
485         parser.add_option('--content-disposition', action='store', type='str',
486                           dest='content_disposition', default=None,
487                           help='provide the presentation style of the object')
488         parser.add_option('--manifest', action='store', type='str',
489                           dest='x_object_manifest', default=None,
490                           help='use for large file support')        
491         parser.add_option('--sharing', action='store',
492                           dest='x_object_sharing', default=None,
493                           help='define sharing object policy')
494         parser.add_option('--nosharing', action='store_true',
495                           dest='no_sharing', default=None,
496                           help='clear object sharing policy')
497         parser.add_option('-f', action='store',
498                           dest='srcpath', default=None,
499                           help='file descriptor to read from: pass - for standard input')
500         parser.add_option('--public', action='store_true',
501                           dest='x_object_public', default=False,
502                           help='make object publicly accessible')
503     
504     def execute(self, path, *args):
505         if path.find('=') != -1:
506             raise Fault('Missing path argument')
507         
508         #prepare user defined meta
509         meta = {}
510         for arg in args:
511             key, sep, val = arg.partition('=')
512             meta[key] = val
513         
514         if self.no_sharing:
515             self.x_object_sharing = ''
516         
517         attrs = ['content_encoding', 'content_disposition', 'x_object_sharing',
518                  'x_object_public']
519         args = self._build_args(attrs)
520         
521         container, sep, object = path.partition('/')
522         
523         f = None
524         if self.srcpath:
525             f = open(self.srcpath) if self.srcpath != '-' else stdin
526         
527         if self.chunked:
528             self.client.update_object_using_chunks(container, object, f,
529                                                     meta=meta, **args)
530         else:
531             self.client.update_object(container, object, f, meta=meta, **args)
532         if f:
533             f.close()
534
535 @cli_command('move', 'mv')
536 class MoveObject(Command):
537     syntax = '<src container>/<src object> [<dst container>/]<dst object>'
538     description = 'move an object to a different location'
539     
540     def add_options(self, parser):
541         parser.add_option('--version', action='store',
542                           dest='version', default=None,
543                           help='move a specific object version')
544         parser.add_option('--public', action='store_true',
545                           dest='public', default=False,
546                           help='make object publicly accessible')
547     
548     def execute(self, src, dst, *args):
549         src_container, sep, src_object = src.partition('/')
550         dst_container, sep, dst_object = dst.partition('/')
551         if not sep:
552             dst_container = src_container
553             dst_object = dst
554         
555         #prepare user defined meta
556         meta = {}
557         for arg in args:
558             key, sep, val = arg.partition('=')
559             meta[key] = val
560         
561         self.client.move_object(src_container, src_object, dst_container,
562                                 dst_object, meta, self.public, self.version)
563
564 @cli_command('remove')
565 class TrashObject(Command):
566     syntax = '<container>/<object>'
567     description = 'trash an object'
568     
569     def execute(self, src):
570         src_container, sep, src_object = src.partition('/')
571         
572         self.client.trash_object(src_container, src_object)
573
574 @cli_command('restore')
575 class RestoreObject(Command):
576     syntax = '<container>/<object>'
577     description = 'restore a trashed object'
578     
579     def execute(self, src):
580         src_container, sep, src_object = src.partition('/')
581         
582         self.client.restore_object(src_container, src_object)
583
584 @cli_command('unset')
585 class UnsetObject(Command):
586     syntax = '<container>/[<object>] key [key] [...]'
587     description = 'delete metadata info'
588     
589     def execute(self, path, *args):
590         #in case of account fix the args
591         if len(args) == 0:
592             args = list(args)
593             args.append(path)
594             args = tuple(args)
595             path = ''
596         meta = []
597         for key in args:
598             meta.append(key)
599         container, sep, object = path.partition('/')
600         if object:
601             self.client.delete_object_metadata(container, object, meta)
602         elif container:
603             self.client.delete_container_metadata(container, meta)
604         else:
605             self.client.delete_account_metadata(meta)
606
607 @cli_command('group')
608 class CreateGroup(Command):
609     syntax = 'key=val [key=val] [...]'
610     description = 'create account groups'
611     
612     def execute(self, *args):
613         groups = {}
614         for arg in args:
615             key, sep, val = arg.partition('=')
616             groups[key] = val
617         self.client.set_account_groups(**groups)
618
619 @cli_command('ungroup')
620 class DeleteGroup(Command):
621     syntax = 'key [key] [...]'
622     description = 'delete account groups'
623     
624     def execute(self, *args):
625         groups = []
626         for arg in args:
627             groups.append(arg)
628         self.client.unset_account_groups(groups)
629
630 @cli_command('policy')
631 class SetPolicy(Command):
632     syntax = 'container key=val [key=val] [...]'
633     description = 'set container policies'
634     
635     def execute(self, path, *args):
636         if path.find('=') != -1:
637             raise Fault('Missing container argument')
638         
639         container, sep, object = path.partition('/')
640         
641         if object:
642             raise Fault('Only containers have policies')
643         
644         policies = {}
645         for arg in args:
646             key, sep, val = arg.partition('=')
647             policies[key] = val
648         
649         self.client.set_container_policies(container, **policies)
650
651 @cli_command('publish')
652 class PublishObject(Command):
653     syntax = '<container>/<object>'
654     description = 'publish an object'
655     
656     def execute(self, src):
657         src_container, sep, src_object = src.partition('/')
658         
659         self.client.publish_object(src_container, src_object)
660
661 @cli_command('unpublish')
662 class UnpublishObject(Command):
663     syntax = '<container>/<object>'
664     description = 'unpublish an object'
665     
666     def execute(self, src):
667         src_container, sep, src_object = src.partition('/')
668         
669         self.client.unpublish_object(src_container, src_object)
670
671 def print_usage():
672     cmd = Command('', [])
673     parser = cmd.parser
674     parser.usage = '%prog <command> [options]'
675     parser.print_help()
676     
677     commands = []
678     for cls in set(_cli_commands.values()):
679         name = ', '.join(cls.commands)
680         description = getattr(cls, 'description', '')
681         commands.append('  %s %s' % (name.ljust(12), description))
682     print '\nCommands:\n' + '\n'.join(sorted(commands))
683
684 def print_dict(d, header='name', f=stdout, detail=True):
685     header = header if header in d else 'subdir'
686     if header and header in d:
687         f.write('%s\n' %d.pop(header).encode('utf8'))
688     if detail:
689         patterns = ['^x_(account|container|object)_meta_(\w+)$']
690         patterns.append(patterns[0].replace('_', '-'))
691         for key, val in sorted(d.items()):
692             f.write('%s: %s\n' % (key.rjust(30), val))
693
694 def print_list(l, verbose=False, f=stdout, detail=True):
695     for elem in l:
696         #if it's empty string continue
697         if not elem:
698             continue
699         if type(elem) == types.DictionaryType:
700             print_dict(elem, f=f, detail=detail)
701         elif type(elem) == types.StringType:
702             if not verbose:
703                 elem = elem.split('Traceback')[0]
704             f.write('%s\n' % elem)
705         else:
706             f.write('%s\n' % elem)
707
708 def print_versions(data, f=stdout):
709     if 'versions' not in data:
710         f.write('%s\n' %data)
711         return
712     f.write('versions:\n')
713     for id, t in data['versions']:
714         f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
715
716 def main():
717     try:
718         name = argv[1]
719         cls = class_for_cli_command(name)
720     except (IndexError, KeyError):
721         print_usage()
722         exit(1)
723     
724     cmd = cls(name, argv[2:])
725     
726     try:
727         cmd.execute(*cmd.args)
728     except TypeError, e:
729         cmd.parser.print_help()
730         exit(1)
731     except Fault, f:
732         status = f.status and '%s ' % f.status or ''
733         print '%s%s' % (status, f.data)
734
735 if __name__ == '__main__':
736     main()