Statistics
| Branch: | Tag: | Revision:

root / tools / store @ bcb7c5a8

History | View | Annotate | Download (26.4 kB)

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 pithos.lib.client import Client, Fault
41
from datetime import datetime
42

    
43
import json
44
import logging
45
import types
46
import re
47
import time as _time
48
import os
49

    
50
DEFAULT_HOST = 'pithos.dev.grnet.gr'
51
DEFAULT_API = 'v1'
52

    
53
_cli_commands = {}
54

    
55
def cli_command(*args):
56
    def decorator(cls):
57
        cls.commands = args
58
        for name in args:
59
            _cli_commands[name] = cls
60
        return cls
61
    return decorator
62

    
63
def class_for_cli_command(name):
64
    return _cli_commands[name]
65

    
66
class Command(object):
67
    syntax = ''
68
    
69
    def __init__(self, name, argv):
70
        parser = OptionParser('%%prog %s [options] %s' % (name, self.syntax))
71
        parser.add_option('--host', dest='host', metavar='HOST',
72
                          default=DEFAULT_HOST, help='use server HOST')
73
        parser.add_option('--user', dest='user', metavar='USERNAME',
74
                          default=_get_user(),
75
                          help='use account USERNAME')
76
        parser.add_option('--token', dest='token', metavar='AUTH',
77
                          default=_get_auth(),
78
                          help='use account AUTH')
79
        parser.add_option('--api', dest='api', metavar='API',
80
                          default=DEFAULT_API, help='use api API')
81
        parser.add_option('-v', action='store_true', dest='verbose',
82
                          default=False, help='use verbose output')
83
        parser.add_option('-d', action='store_true', dest='debug',
84
                          default=False, help='use debug output')
85
        self.add_options(parser)
86
        options, args = parser.parse_args(argv)
87
        
88
        # Add options to self
89
        for opt in parser.option_list:
90
            key = opt.dest
91
            if key:
92
                val = getattr(options, key)
93
                setattr(self, key, val)
94
        
95
        self.client = Client(self.host, self.token, self.user, self.api, self.verbose,
96
                             self.debug)
97
        
98
        self.parser = parser
99
        self.args = args
100
        
101
    def add_options(self, parser):
102
        pass
103
    
104
    def execute(self, *args):
105
        pass
106

    
107
@cli_command('list', 'ls')
108
class List(Command):
109
    syntax = '[<container>[/<object>]]'
110
    description = 'list containers or objects'
111
    
112
    def add_options(self, parser):
113
        parser.add_option('-l', action='store_true', dest='detail',
114
                          default=False, help='show detailed output')
115
        parser.add_option('-n', action='store', type='int', dest='limit',
116
                          default=1000, help='show limited output')
117
        parser.add_option('--marker', action='store', type='str',
118
                          dest='marker', default=None,
119
                          help='show output greater then marker')
120
        parser.add_option('--prefix', action='store', type='str',
121
                          dest='prefix', default=None,
122
                          help='show output starting with prefix')
123
        parser.add_option('--delimiter', action='store', type='str',
124
                          dest='delimiter', default=None,
125
                          help='show output up to the delimiter')
126
        parser.add_option('--path', action='store', type='str',
127
                          dest='path', default=None,
128
                          help='show output starting with prefix up to /')
129
        parser.add_option('--meta', action='store', type='str',
130
                          dest='meta', default=None,
131
                          help='show output having the specified meta keys')
132
        parser.add_option('--if-modified-since', action='store', type='str',
133
                          dest='if_modified_since', default=None,
134
                          help='show output if modified since then')
135
        parser.add_option('--if-unmodified-since', action='store', type='str',
136
                          dest='if_unmodified_since', default=None,
137
                          help='show output if not modified since then')
138
        parser.add_option('--until', action='store', dest='until',
139
                          default=False, help='show metadata until that date')
140
        parser.add_option('--format', action='store', dest='format',
141
                          default='%d/%m/%Y', help='format to parse until date')
142
    
143
    def execute(self, container=None):
144
        if container:
145
            self.list_objects(container)
146
        else:
147
            self.list_containers()
148
    
149
    def list_containers(self):
150
        params = {'limit':self.limit, 'marker':self.marker}
151
        headers = {'IF_MODIFIED_SINCE':self.if_modified_since,
152
                   'IF_UNMODIFIED_SINCE':self.if_unmodified_since}
153
        
154
        if self.until:
155
            t = _time.strptime(self.until, self.format)
156
            params['until'] = int(_time.mktime(t))
157
        
158
        l = self.client.list_containers(self.detail, params, headers)
159
        print_list(l)
160
    
161
    def list_objects(self, container):
162
        #prepate params
163
        params = {}
164
        attrs = ['limit', 'marker', 'prefix', 'delimiter', 'path', 'meta']
165
        for a in [a for a in attrs if getattr(self, a)]:
166
            params[a] = getattr(self, a)
167
        
168
        if self.until:
169
            t = _time.strptime(self.until, self.format)
170
            params['until'] = int(_time.mktime(t))
171
        
172
        headers = {'IF_MODIFIED_SINCE':self.if_modified_since,
173
                   'IF_UNMODIFIED_SINCE':self.if_unmodified_since}
174
        container, sep, object = container.partition('/')
175
        if object:
176
            return
177
        
178
        detail = 'json'
179
        #if request with meta quering disable trash filtering
180
        show_trashed = True if self.meta else False
181
        l = self.client.list_objects(container, detail, headers,
182
                                     include_trashed = show_trashed, **params)
183
        print_list(l, detail=self.detail)
184

    
185
@cli_command('meta')
186
class Meta(Command):
187
    syntax = '[<container>[/<object>]]'
188
    description = 'get the metadata of an account, a container or an object'
189
    
190
    def add_options(self, parser):
191
        parser.add_option('-r', action='store_true', dest='restricted',
192
                          default=False, help='show only user defined metadata')
193
        parser.add_option('--until', action='store', dest='until',
194
                          default=False, help='show metadata until that date')
195
        parser.add_option('--format', action='store', dest='format',
196
                          default='%d/%m/%Y', help='format to parse until date')
197
        parser.add_option('--version', action='store', dest='version',
198
                          default=None, help='show specific version \
199
                                  (applies only for objects)')
200
    
201
    def execute(self, path=''):
202
        container, sep, object = path.partition('/')
203
        if self.until:
204
            t = _time.strptime(self.until, self.format)
205
            self.until = int(_time.mktime(t))
206
        if object:
207
            meta = self.client.retrieve_object_metadata(container, object,
208
                                                        self.restricted,
209
                                                        self.version)
210
        elif container:
211
            meta = self.client.retrieve_container_metadata(container,
212
                                                           self.restricted,
213
                                                           self.until)
214
        else:
215
            meta = self.client.account_metadata(self.restricted, self.until)
216
        if meta == None:
217
            print 'Entity does not exist'
218
        else:
219
            print_dict(meta, header=None)
220

    
221
@cli_command('create')
222
class CreateContainer(Command):
223
    syntax = '<container> [key=val] [...]'
224
    description = 'create a container'
225
    
226
    def execute(self, container, *args):
227
        headers = {}
228
        meta = {}
229
        for arg in args:
230
            key, sep, val = arg.partition('=')
231
            meta[key] = val
232
        ret = self.client.create_container(container, headers, **meta)
233
        if not ret:
234
            print 'Container already exists'
235

    
236
@cli_command('delete', 'rm')
237
class Delete(Command):
238
    syntax = '<container>[/<object>]'
239
    description = 'delete a container or an object'
240
    
241
    def execute(self, path):
242
        container, sep, object = path.partition('/')
243
        if object:
244
            self.client.delete_object(container, object)
245
        else:
246
            self.client.delete_container(container)
247

    
248
@cli_command('get')
249
class GetObject(Command):
250
    syntax = '<container>/<object>'
251
    description = 'get the data of an object'
252
    
253
    def add_options(self, parser):
254
        parser.add_option('-l', action='store_true', dest='detail',
255
                          default=False, help='show detailed output')
256
        parser.add_option('--range', action='store', dest='range',
257
                          default=None, help='show range of data')
258
        parser.add_option('--if-match', action='store', dest='if-match',
259
                          default=None, help='show output if ETags match')
260
        parser.add_option('--if-none-match', action='store',
261
                          dest='if-none-match', default=None,
262
                          help='show output if ETags don\'t match')
263
        parser.add_option('--if-modified-since', action='store', type='str',
264
                          dest='if-modified-since', default=None,
265
                          help='show output if modified since then')
266
        parser.add_option('--if-unmodified-since', action='store', type='str',
267
                          dest='if-unmodified-since', default=None,
268
                          help='show output if not modified since then')
269
        parser.add_option('-o', action='store', type='str',
270
                          dest='file', default=None,
271
                          help='save output in file')
272
        parser.add_option('--version', action='store', type='str',
273
                          dest='version', default=None,
274
                          help='get the specific \
275
                               version')
276
        parser.add_option('--versionlist', action='store_true',
277
                          dest='versionlist', default=False,
278
                          help='get the full object version list')
279
    
280
    def execute(self, path):
281
        headers = {}
282
        if self.range:
283
            headers['RANGE'] = 'bytes=%s' %self.range
284
        attrs = ['if-match', 'if-none-match', 'if-modified-since',
285
                 'if-unmodified-since']
286
        attrs = [a for a in attrs if getattr(self, a)]
287
        for a in attrs:
288
            headers[a.replace('-', '_').upper()] = getattr(self, a)
289
        container, sep, object = path.partition('/')
290
        if self.versionlist:
291
            self.version = 'list'
292
            self.detail = True
293
        data = self.client.retrieve_object(container, object, self.detail,
294
                                          headers, self.version)
295
        f = self.file and open(self.file, 'w') or stdout
296
        if self.detail:
297
            data = json.loads(data)
298
            if self.versionlist:
299
                print_versions(data, f=f)
300
            else:
301
                print_dict(data, f=f)
302
        else:
303
            f.write(data)
304
        f.close()
305

    
306
@cli_command('mkdir')
307
class PutMarker(Command):
308
    syntax = '<container>/<directory marker>'
309
    description = 'create a directory marker'
310
    
311
    def execute(self, path):
312
        container, sep, object = path.partition('/')
313
        self.client.create_directory_marker(container, object)
314

    
315
@cli_command('put')
316
class PutObject(Command):
317
    syntax = '<container>/<object> <path> [key=val] [...]'
318
    description = 'create/override object with path contents or standard input'
319
    
320
    def add_options(self, parser):
321
        parser.add_option('--use_hashes', action='store_true', dest='use_hashes',
322
                          default=False, help='provide hashmap instead of data')
323
        parser.add_option('--chunked', action='store_true', dest='chunked',
324
                          default=False, help='set chunked transfer mode')
325
        parser.add_option('--etag', action='store', dest='etag',
326
                          default=None, help='check written data')
327
        parser.add_option('--content-encoding', action='store',
328
                          dest='content-encoding', default=None,
329
                          help='provide the object MIME content type')
330
        parser.add_option('--content-disposition', action='store', type='str',
331
                          dest='content-disposition', default=None,
332
                          help='provide the presentation style of the object')
333
        parser.add_option('-S', action='store',
334
                          dest='segment-size', default=False,
335
                          help='use for large file support')
336
        parser.add_option('--manifest', action='store_true',
337
                          dest='manifest', default=None,
338
                          help='upload a manifestation file')
339
        parser.add_option('--type', action='store',
340
                          dest='content-type', default=False,
341
                          help='create object with specific content type')
342
        #parser.add_option('--touch', action='store_true',
343
        #                  dest='touch', default=False,
344
        #                  help='create object with zero data')
345
        parser.add_option('--sharing', action='store',
346
                          dest='sharing', default=None,
347
                          help='define sharing object policy')
348
        parser.add_option('-f', action='store',
349
                          dest='srcpath', default=None,
350
                          help='file descriptor to read from: pass - for standard input')
351
        
352
    def execute(self, path, *args):
353
        if path.find('=') != -1:
354
            raise Fault('Missing path argument')
355
        
356
        #prepare user defined meta
357
        meta = {}
358
        for arg in args:
359
            key, sep, val = arg.partition('=')
360
            meta[key] = val
361
        
362
        headers = {}
363
        manifest = getattr(self, 'manifest')
364
        if manifest:
365
            # if it's manifestation file
366
            # send zero-byte data with X-Object-Manifest header
367
            self.touch = True
368
            headers['X_OBJECT_MANIFEST'] = manifest
369
        if self.sharing:
370
            headers['X_OBJECT_SHARING'] = self.sharing
371
        
372
        attrs = ['etag', 'content-encoding', 'content-disposition',
373
                 'content-type']
374
        attrs = [a for a in attrs if getattr(self, a)]
375
        for a in attrs:
376
            headers[a.replace('-', '_').upper()] = getattr(self, a)
377
        
378
        container, sep, object = path.partition('/')
379
        
380
        f = None
381
        if self.srcpath:
382
            f = open(self.srcpath) if self.srcpath != '-' else stdin
383
        
384
        if self.use_hashes and not f:
385
            raise Fault('Illegal option combination')
386
        
387
        self.client.create_object(container, object, f, chunked=self.chunked,
388
                                  headers=headers, use_hashes=self.use_hashes,
389
                                  **meta)
390
        if f:
391
            f.close()
392

    
393
@cli_command('copy', 'cp')
394
class CopyObject(Command):
395
    syntax = '<src container>/<src object> [<dst container>/]<dst object>'
396
    description = 'copies an object to a different location'
397
    
398
    def add_options(self, parser):
399
        parser.add_option('--version', action='store',
400
                          dest='version', default=False,
401
                          help='copy specific version')
402
    
403
    def execute(self, src, dst):
404
        src_container, sep, src_object = src.partition('/')
405
        dst_container, sep, dst_object = dst.partition('/')
406
        if not sep:
407
            dst_container = src_container
408
            dst_object = dst
409
        version = getattr(self, 'version')
410
        if version:
411
            headers = {}
412
            headers['X_SOURCE_VERSION'] = version
413
        self.client.copy_object(src_container, src_object, dst_container,
414
                                dst_object, headers)
415

    
416
@cli_command('set')
417
class SetMeta(Command):
418
    syntax = '[<container>[/<object>]] key=val [key=val] [...]'
419
    description = 'set metadata'
420
    
421
    def execute(self, path, *args):
422
        #in case of account fix the args
423
        if path.find('=') != -1:
424
            args = list(args)
425
            args.append(path)
426
            args = tuple(args)
427
            path = ''
428
        meta = {}
429
        for arg in args:
430
            key, sep, val = arg.partition('=')
431
            meta[key.strip()] = val.strip()
432
        container, sep, object = path.partition('/')
433
        if object:
434
            self.client.update_object_metadata(container, object, **meta)
435
        elif container:
436
            self.client.update_container_metadata(container, **meta)
437
        else:
438
            self.client.update_account_metadata(**meta)
439

    
440
@cli_command('update')
441
class UpdateObject(Command):
442
    syntax = '<container>/<object> path [key=val] [...]'
443
    description = 'update object metadata/data (default mode: append)'
444
    
445
    def add_options(self, parser):
446
        parser.add_option('-a', action='store_true', dest='append',
447
                          default=True, help='append data')
448
        parser.add_option('--offset', action='store',
449
                          dest='offset',
450
                          default=None, help='starting offest to be updated')
451
        parser.add_option('--range', action='store', dest='content-range',
452
                          default=None, help='range of data to be updated')
453
        parser.add_option('--chunked', action='store_true', dest='chunked',
454
                          default=False, help='set chunked transfer mode')
455
        parser.add_option('--content-encoding', action='store',
456
                          dest='content-encoding', default=None,
457
                          help='provide the object MIME content type')
458
        parser.add_option('--content-disposition', action='store', type='str',
459
                          dest='content-disposition', default=None,
460
                          help='provide the presentation style of the object')
461
        parser.add_option('--manifest', action='store', type='str',
462
                          dest='manifest', default=None,
463
                          help='use for large file support')        
464
        parser.add_option('--sharing', action='store',
465
                          dest='sharing', default=None,
466
                          help='define sharing object policy')
467
        parser.add_option('--nosharing', action='store_true',
468
                          dest='no_sharing', default=None,
469
                          help='clear object sharing policy')
470
        parser.add_option('-f', action='store',
471
                          dest='srcpath', default=None,
472
                          help='file descriptor to read from: pass - for standard input')
473
    
474
    def execute(self, path, *args):
475
        if path.find('=') != -1:
476
            raise Fault('Missing path argument')
477
        
478
        headers = {}
479
        if self.manifest:
480
            headers['X_OBJECT_MANIFEST'] = self.manifest
481
        if self.sharing:
482
            headers['X_OBJECT_SHARING'] = self.sharing
483
        
484
        if self.no_sharing:
485
            headers['X_OBJECT_SHARING'] = ''
486
        
487
        attrs = ['content-encoding', 'content-disposition']
488
        attrs = [a for a in attrs if getattr(self, a)]
489
        for a in attrs:
490
            headers[a.replace('-', '_').upper()] = getattr(self, a)
491
        
492
        #prepare user defined meta
493
        meta = {}
494
        for arg in args:
495
            key, sep, val = arg.partition('=')
496
            meta[key] = val
497
        
498
        container, sep, object = path.partition('/')
499
        
500
        f = None
501
        chunked = False
502
        if self.srcpath:
503
            f = self.srcpath != '-' and open(self.srcpath) or stdin
504
        if f:
505
            chunked = True if (self.chunked or f == stdin) else False
506
        self.client.update_object(container, object, f, chunked=chunked,
507
                                  headers=headers, offset=self.offset, **meta)
508
        if f:
509
            f.close()
510

    
511
@cli_command('move', 'mv')
512
class MoveObject(Command):
513
    syntax = '<src container>/<src object> [<dst container>/]<dst object>'
514
    description = 'moves an object to a different location'
515
    
516
    def execute(self, src, dst):
517
        src_container, sep, src_object = src.partition('/')
518
        dst_container, sep, dst_object = dst.partition('/')
519
        if not sep:
520
            dst_container = src_container
521
            dst_object = dst
522
        
523
        self.client.move_object(src_container, src_object, dst_container,
524
                                dst_object, headers)
525

    
526
@cli_command('remove')
527
class TrashObject(Command):
528
    syntax = '<container>/<object>'
529
    description = 'trashes an object'
530
    
531
    def execute(self, src):
532
        src_container, sep, src_object = src.partition('/')
533
        
534
        self.client.trash_object(src_container, src_object)
535

    
536
@cli_command('restore')
537
class TrashObject(Command):
538
    syntax = '<container>/<object>'
539
    description = 'trashes an object'
540
    
541
    def execute(self, src):
542
        src_container, sep, src_object = src.partition('/')
543
        
544
        self.client.restore_object(src_container, src_object)
545

    
546
@cli_command('unset')
547
class UnsetObject(Command):
548
    syntax = '<container>/[<object>] key [key] [...]'
549
    description = 'deletes metadata info'
550
    
551
    def execute(self, path, *args):
552
        #in case of account fix the args
553
        if len(args) == 0:
554
            args = list(args)
555
            args.append(path)
556
            args = tuple(args)
557
            path = ''
558
        meta = []
559
        for key in args:
560
            meta.append(key)
561
        container, sep, object = path.partition('/')
562
        if object:
563
            self.client.delete_object_metadata(container, object, meta)
564
        elif container:
565
            self.client.delete_container_metadata(container, meta)
566
        else:
567
            self.client.delete_account_metadata(meta)
568

    
569
@cli_command('group')
570
class SetGroup(Command):
571
    syntax = 'key=val [key=val] [...]'
572
    description = 'set group account info'
573
    
574
    def execute(self, *args):
575
        groups = {}
576
        for arg in args:
577
            key, sep, val = arg.partition('=')
578
            groups[key] = val
579
        self.client.set_account_groups(**groups)
580

    
581
@cli_command('policy')
582
class SetPolicy(Command):
583
    syntax = 'container key=val [key=val] [...]'
584
    description = 'set contianer policies'
585
    
586
    def execute(self, path, *args):
587
        if path.find('=') != -1:
588
            raise Fault('Missing container argument')
589
        
590
        container, sep, object = path.partition('/')
591
        
592
        if object:
593
            raise Fault('Only containers have policies')
594
        
595
        policies = {}
596
        for arg in args:
597
            key, sep, val = arg.partition('=')
598
            policies[key] = val
599
        
600
        self.client.set_container_policies(container, **policies)
601

    
602
def print_usage():
603
    cmd = Command('', [])
604
    parser = cmd.parser
605
    parser.usage = '%prog <command> [options]'
606
    parser.print_help()
607
    
608
    commands = []
609
    for cls in set(_cli_commands.values()):
610
        name = ', '.join(cls.commands)
611
        description = getattr(cls, 'description', '')
612
        commands.append('  %s %s' % (name.ljust(12), description))
613
    print '\nCommands:\n' + '\n'.join(sorted(commands))
614

    
615
def print_dict(d, header='name', f=stdout, detail=True):
616
    header = header in d and header or 'subdir'
617
    if header and header in d:
618
        f.write('%s\n' %d.pop(header))
619
    if detail:
620
        patterns = ['^x_(account|container|object)_meta_(\w+)$']
621
        patterns.append(patterns[0].replace('_', '-'))
622
        for key, val in sorted(d.items()):
623
            for p in patterns:
624
                p = re.compile(p)
625
                m = p.match(key)
626
                if m:
627
                    key = m.group(2)
628
            f.write('%s: %s\n' % (key.rjust(30), val))
629

    
630
def print_list(l, verbose=False, f=stdout, detail=True):
631
    for elem in l:
632
        #if it's empty string continue
633
        if not elem:
634
            continue
635
        if type(elem) == types.DictionaryType:
636
            print_dict(elem, f=f, detail=detail)
637
        elif type(elem) == types.StringType:
638
            if not verbose:
639
                elem = elem.split('Traceback')[0]
640
            f.write('%s\n' % elem)
641
        else:
642
            f.write('%s\n' % elem)
643

    
644
def print_versions(data, f=stdout):
645
    if 'versions' not in data:
646
        f.write('%s\n' %data)
647
        return
648
    f.write('versions:\n')
649
    for id, t in data['versions']:
650
        f.write('%s @ %s\n' % (str(id).rjust(30), datetime.fromtimestamp(t)))
651

    
652
def _get_user():
653
        try:
654
            return os.environ['PITHOS_USER']
655
        except KeyError:
656
            return getuser()
657

    
658
def _get_auth():
659
        try:
660
            return os.environ['PITHOS_AUTH']
661
        except KeyError:
662
            return '0000'
663
    
664

    
665
def main():
666
    try:
667
        name = argv[1]
668
        cls = class_for_cli_command(name)
669
    except (IndexError, KeyError):
670
        print_usage()
671
        exit(1)
672
    
673
    cmd = cls(name, argv[2:])
674
    
675
    try:
676
        cmd.execute(*cmd.args)
677
    except TypeError, e:
678
        cmd.parser.print_help()
679
        exit(1)
680
    except Fault, f:
681
        status = f.status and '%s ' % f.status or ''
682
        print '%s%s' % (status, f.data)
683

    
684
if __name__ == '__main__':
685
    main()