Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 176894c1

History | View | Annotate | Download (24.3 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
"""
37
To add a command create a new class and add a 'command' decorator. The class
38
must have a 'main' method which will contain the code to be executed.
39
Optionally a command can implement an 'update_parser' class method in order
40
to add command line arguments, or modify the OptionParser in any way.
41

42
The name of the class is important and it will determine the name and grouping
43
of the command. This behavior can be overriden with the 'group' and 'name'
44
decorator arguments:
45

46
    @command(api='compute')
47
    class server_list(object):
48
        # This command will be named 'list' under group 'server'
49
        ...
50

51
    @command(api='compute', name='ls')
52
    class server_list(object):
53
        # This command will be named 'ls' under group 'server'
54
        ...
55

56
The docstring of a command class will be used as the command description in
57
help messages, unless overriden with the 'description' decorator argument.
58

59
The syntax of a command will be generated dynamically based on the signature
60
of the 'main' method, unless overriden with the 'syntax' decorator argument:
61

62
    def main(self, server_id, network=None):
63
        # This syntax of this command will be: '<server id> [network]'
64
        ...
65

66
The order of commands is important, it will be preserved in the help output.
67
"""
68

    
69
import inspect
70
import logging
71
import os
72

    
73
from base64 import b64encode
74
from grp import getgrgid
75
from optparse import OptionParser
76
from os.path import abspath, basename, exists
77
from pwd import getpwuid
78
from sys import argv, exit, stdout
79

    
80
from kamaki import clients
81
from kamaki.config import Config, ConfigError
82
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items
83

    
84

    
85
log = logging.getLogger('kamaki')
86

    
87
_commands = OrderedDict()
88

    
89

    
90
def command(api=None, group=None, name=None, description=None, syntax=None):
91
    """Class decorator that registers a class as a CLI command."""
92
    
93
    def decorator(cls):
94
        grp, sep, cmd = cls.__name__.partition('_')
95
        if not sep:
96
            grp, cmd = None, cls.__name__
97
        
98
        cls.api = api
99
        cls.group = group or grp
100
        cls.name = name or cmd
101
        cls.description = description or cls.__doc__
102
        cls.syntax = syntax
103
        
104
        if cls.syntax is None:
105
            # Generate a syntax string based on main's arguments
106
            spec = inspect.getargspec(cls.main.im_func)
107
            args = spec.args[1:]
108
            n = len(args) - len(spec.defaults or ())
109
            required = ' '.join('<%s>' % x.replace('_', ' ') for x in args[:n])
110
            optional = ' '.join('[%s]' % x.replace('_', ' ') for x in args[n:])
111
            cls.syntax = ' '.join(x for x in [required, optional] if x)
112
            if spec.varargs:
113
                cls.syntax += ' <%s ...>' % spec.varargs
114
        
115
        if cls.group not in _commands:
116
            _commands[cls.group] = OrderedDict()
117
        _commands[cls.group][cls.name] = cls
118
        return cls
119
    return decorator
120

    
121

    
122
@command()
123
class config_list(object):
124
    """list configuration options"""
125
    
126
    @classmethod
127
    def update_parser(cls, parser):
128
        parser.add_option('-a', dest='all', action='store_true',
129
                default=False, help='include empty values')
130
    
131
    def main(self):
132
        for key, val in sorted(self.config.items()):
133
            if not val and not self.options.all:
134
                continue
135
            print '%s=%s' % (key, val)
136

    
137

    
138
@command()
139
class config_get(object):
140
    """get a configuration option"""
141
    
142
    def main(self, key):
143
        val = self.config.get(key)
144
        if val is not None:
145
            print val
146

    
147

    
148
@command()
149
class config_set(object):
150
    """set a configuration option"""
151
    
152
    def main(self, key, val):
153
        self.config.set(key, val)
154

    
155

    
156
@command()
157
class config_del(object):
158
    """delete a configuration option"""
159
    
160
    def main(self, key):
161
        self.config.delete(key)
162

    
163

    
164
@command(api='compute')
165
class server_list(object):
166
    """list servers"""
167
    
168
    @classmethod
169
    def update_parser(cls, parser):
170
        parser.add_option('-l', dest='detail', action='store_true',
171
                default=False, help='show detailed output')
172
    
173
    def main(self):
174
        servers = self.client.list_servers(self.options.detail)
175
        print_items(servers)
176

    
177

    
178
@command(api='compute')
179
class server_info(object):
180
    """get server details"""
181
    
182
    def main(self, server_id):
183
        server = self.client.get_server_details(int(server_id))
184
        print_dict(server)
185

    
186

    
187
@command(api='compute')
188
class server_create(object):
189
    """create server"""
190
    
191
    @classmethod
192
    def update_parser(cls, parser):
193
        parser.add_option('--personality', dest='personalities',
194
                action='append', default=[],
195
                metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
196
                help='add a personality file')
197
        parser.epilog = "If missing, optional personality values will be " \
198
                "filled based on the file at PATH if missing."
199
    
200
    def main(self, name, flavor_id, image_id):
201
        personalities = []
202
        for personality in self.options.personalities:
203
            p = personality.split(',')
204
            p.extend([None] * (5 - len(p)))     # Fill missing fields with None
205
            
206
            path = p[0]
207
            
208
            if not path:
209
                log.error("Invalid personality argument '%s'", p)
210
                return 1
211
            if not exists(path):
212
                log.error("File %s does not exist", path)
213
                return 1
214
            
215
            with open(path) as f:
216
                contents = b64encode(f.read())
217
            
218
            st = os.stat(path)
219
            personalities.append({
220
                'path': p[1] or abspath(path),
221
                'owner': p[2] or getpwuid(st.st_uid).pw_name,
222
                'group': p[3] or getgrgid(st.st_gid).gr_name,
223
                'mode': int(p[4]) if p[4] else 0x7777 & st.st_mode,
224
                'contents': contents})
225
        
226
        reply = self.client.create_server(name, int(flavor_id), image_id,
227
                personalities)
228
        print_dict(reply)
229

    
230

    
231
@command(api='compute')
232
class server_rename(object):
233
    """update server name"""
234
    
235
    def main(self, server_id, new_name):
236
        self.client.update_server_name(int(server_id), new_name)
237

    
238

    
239
@command(api='compute')
240
class server_delete(object):
241
    """delete server"""
242
    
243
    def main(self, server_id):
244
        self.client.delete_server(int(server_id))
245

    
246

    
247
@command(api='compute')
248
class server_reboot(object):
249
    """reboot server"""
250
    
251
    @classmethod
252
    def update_parser(cls, parser):
253
        parser.add_option('-f', dest='hard', action='store_true',
254
                default=False, help='perform a hard reboot')
255
    
256
    def main(self, server_id):
257
        self.client.reboot_server(int(server_id), self.options.hard)
258

    
259

    
260
@command(api='asterias')
261
class server_start(object):
262
    """start server"""
263
    
264
    def main(self, server_id):
265
        self.client.start_server(int(server_id))
266

    
267

    
268
@command(api='asterias')
269
class server_shutdown(object):
270
    """shutdown server"""
271
    
272
    def main(self, server_id):
273
        self.client.shutdown_server(int(server_id))
274

    
275

    
276
@command(api='asterias')
277
class server_console(object):
278
    """get a VNC console"""
279
    
280
    def main(self, server_id):
281
        reply = self.client.get_server_console(int(server_id))
282
        print_dict(reply)
283

    
284

    
285
@command(api='asterias')
286
class server_firewall(object):
287
    """set the firewall profile"""
288
    
289
    def main(self, server_id, profile):
290
        self.client.set_firewall_profile(int(server_id), profile)
291

    
292

    
293
@command(api='asterias')
294
class server_addr(object):
295
    """list server addresses"""
296
    
297
    def main(self, server_id, network=None):
298
        reply = self.client.list_server_addresses(int(server_id), network)
299
        margin = max(len(x['name']) for x in reply)
300
        print_addresses(reply, margin)
301

    
302

    
303
@command(api='compute')
304
class server_meta(object):
305
    """get server metadata"""
306
    
307
    def main(self, server_id, key=None):
308
        reply = self.client.get_server_metadata(int(server_id), key)
309
        print_dict(reply)
310

    
311

    
312
@command(api='compute')
313
class server_addmeta(object):
314
    """add server metadata"""
315
    
316
    def main(self, server_id, key, val):
317
        reply = self.client.create_server_metadata(int(server_id), key, val)
318
        print_dict(reply)
319

    
320

    
321
@command(api='compute')
322
class server_setmeta(object):
323
    """update server metadata"""
324
    
325
    def main(self, server_id, key, val):
326
        metadata = {key: val}
327
        reply = self.client.update_server_metadata(int(server_id), **metadata)
328
        print_dict(reply)
329

    
330

    
331
@command(api='compute')
332
class server_delmeta(object):
333
    """delete server metadata"""
334
    
335
    def main(self, server_id, key):
336
        self.client.delete_server_metadata(int(server_id), key)
337

    
338

    
339
@command(api='asterias')
340
class server_stats(object):
341
    """get server statistics"""
342
    
343
    def main(self, server_id):
344
        reply = self.client.get_server_stats(int(server_id))
345
        print_dict(reply, exclude=('serverRef',))
346

    
347

    
348
@command(api='compute')
349
class flavor_list(object):
350
    """list flavors"""
351
    
352
    @classmethod
353
    def update_parser(cls, parser):
354
        parser.add_option('-l', dest='detail', action='store_true',
355
                default=False, help='show detailed output')
356
    
357
    def main(self):
358
        flavors = self.client.list_flavors(self.options.detail)
359
        print_items(flavors)
360

    
361

    
362
@command(api='compute')
363
class flavor_info(object):
364
    """get flavor details"""
365
    
366
    def main(self, flavor_id):
367
        flavor = self.client.get_flavor_details(int(flavor_id))
368
        print_dict(flavor)
369

    
370

    
371
@command(api='compute')
372
class image_list(object):
373
    """list images"""
374
    
375
    @classmethod
376
    def update_parser(cls, parser):
377
        parser.add_option('-l', dest='detail', action='store_true',
378
                default=False, help='show detailed output')
379
    
380
    def main(self):
381
        images = self.client.list_images(self.options.detail)
382
        print_items(images)
383

    
384

    
385
@command(api='compute')
386
class image_info(object):
387
    """get image details"""
388
    
389
    def main(self, image_id):
390
        image = self.client.get_image_details(image_id)
391
        print_dict(image)
392

    
393

    
394
@command(api='compute')
395
class image_create(object):
396
    """create image"""
397
    
398
    def main(self, server_id, name):
399
        reply = self.client.create_image(int(server_id), name)
400
        print_dict(reply)
401

    
402

    
403
@command(api='compute')
404
class image_delete(object):
405
    """delete image"""
406
    
407
    def main(self, image_id):
408
        self.client.delete_image(image_id)
409

    
410

    
411
@command(api='compute')
412
class image_meta(object):
413
    """get image metadata"""
414
    
415
    def main(self, image_id, key=None):
416
        reply = self.client.get_image_metadata(image_id, key)
417
        print_dict(reply)
418

    
419

    
420
@command(api='compute')
421
class image_addmeta(object):
422
    """add image metadata"""
423
    
424
    def main(self, image_id, key, val):
425
        reply = self.client.create_image_metadata(image_id, key, val)
426
        print_dict(reply)
427

    
428

    
429
@command(api='compute')
430
class image_setmeta(object):
431
    """update image metadata"""
432
    
433
    def main(self, image_id, key, val):
434
        metadata = {key: val}
435
        reply = self.client.update_image_metadata(image_id, **metadata)
436
        print_dict(reply)
437

    
438

    
439
@command(api='compute')
440
class image_delmeta(object):
441
    """delete image metadata"""
442
    
443
    def main(self, image_id, key):
444
        self.client.delete_image_metadata(image_id, key)
445

    
446

    
447
@command(api='asterias')
448
class network_list(object):
449
    """list networks"""
450
    
451
    @classmethod
452
    def update_parser(cls, parser):
453
        parser.add_option('-l', dest='detail', action='store_true',
454
                default=False, help='show detailed output')
455
    
456
    def main(self):
457
        networks = self.client.list_networks(self.options.detail)
458
        print_items(networks)
459

    
460

    
461
@command(api='asterias')
462
class network_create(object):
463
    """create a network"""
464
    
465
    def main(self, name):
466
        reply = self.client.create_network(name)
467
        print_dict(reply)
468

    
469

    
470
@command(api='asterias')
471
class network_info(object):
472
    """get network details"""
473
    
474
    def main(self, network_id):
475
        network = self.client.get_network_details(network_id)
476
        print_dict(network)
477

    
478

    
479
@command(api='asterias')
480
class network_rename(object):
481
    """update network name"""
482
    
483
    def main(self, network_id, new_name):
484
        self.client.update_network_name(network_id, new_name)
485

    
486

    
487
@command(api='asterias')
488
class network_delete(object):
489
    """delete a network"""
490
    
491
    def main(self, network_id):
492
        self.client.delete_network(network_id)
493

    
494

    
495
@command(api='asterias')
496
class network_connect(object):
497
    """connect a server to a network"""
498
    
499
    def main(self, server_id, network_id):
500
        self.client.connect_server(server_id, network_id)
501

    
502

    
503
@command(api='asterias')
504
class network_disconnect(object):
505
    """disconnect a server from a network"""
506
    
507
    def main(self, server_id, network_id):
508
        self.client.disconnect_server(server_id, network_id)
509

    
510

    
511
@command(api='image')
512
class glance_list(object):
513
    """list images"""
514
    
515
    @classmethod
516
    def update_parser(cls, parser):
517
        parser.add_option('-l', dest='detail', action='store_true',
518
                default=False, help='show detailed output')
519
        parser.add_option('--container-format', dest='container_format',
520
                metavar='FORMAT', help='filter by container format')
521
        parser.add_option('--disk-format', dest='disk_format',
522
                metavar='FORMAT', help='filter by disk format')
523
        parser.add_option('--name', dest='name', metavar='NAME',
524
                help='filter by name')
525
        parser.add_option('--size-min', dest='size_min', metavar='BYTES',
526
                help='filter by minimum size')
527
        parser.add_option('--size-max', dest='size_max', metavar='BYTES',
528
                help='filter by maximum size')
529
        parser.add_option('--status', dest='status', metavar='STATUS',
530
                help='filter by status')
531
        parser.add_option('--order', dest='order', metavar='FIELD',
532
                help='order by FIELD (use a - prefix to reverse order)')
533
    
534
    def main(self):
535
        filters = {}
536
        for filter in ('container_format', 'disk_format', 'name', 'size_min',
537
                       'size_max', 'status'):
538
            val = getattr(self.options, filter, None)
539
            if val is not None:
540
                filters[filter] = val
541
        
542
        order = self.options.order or ''
543
        images = self.client.list_public(self.options.detail, filters=filters,
544
                                         order=order)
545
        print_items(images, title=('name',))
546

    
547

    
548
@command(api='image')
549
class glance_meta(object):
550
    """get image metadata"""
551
    
552
    def main(self, image_id):
553
        image = self.client.get_meta(image_id)
554
        print_dict(image)
555

    
556

    
557
@command(api='image')
558
class glance_register(object):
559
    """register an image"""
560
    
561
    @classmethod
562
    def update_parser(cls, parser):
563
        parser.add_option('--checksum', dest='checksum', metavar='CHECKSUM',
564
                help='set image checksum')
565
        parser.add_option('--container-format', dest='container_format',
566
                metavar='FORMAT', help='set container format')
567
        parser.add_option('--disk-format', dest='disk_format',
568
                metavar='FORMAT', help='set disk format')
569
        parser.add_option('--id', dest='id',
570
                metavar='ID', help='set image ID')
571
        parser.add_option('--owner', dest='owner',
572
                metavar='USER', help='set image owner (admin only)')
573
        parser.add_option('--property', dest='properties', action='append',
574
                metavar='KEY=VAL',
575
                help='add a property (can be used multiple times)')
576
        parser.add_option('--public', dest='is_public', action='store_true',
577
                help='mark image as public')
578
        parser.add_option('--size', dest='size', metavar='SIZE',
579
                help='set image size')
580
    
581
    def main(self, name, location):
582
        params = {}
583
        for key in ('checksum', 'container_format', 'disk_format', 'id',
584
                    'owner', 'is_public', 'size'):
585
            val = getattr(self.options, key)
586
            if val is not None:
587
                params[key] = val
588
        
589
        properties = {}
590
        for property in self.options.properties or []:
591
            key, sep, val = property.partition('=')
592
            if not sep:
593
                log.error("Invalid property '%s'", property)
594
                return 1
595
            properties[key.strip()] = val.strip()
596
        
597
        self.client.register(name, location, params, properties)
598

    
599

    
600
@command(api='image')
601
class glance_members(object):
602
    """get image members"""
603
    
604
    def main(self, image_id):
605
        members = self.client.list_members(image_id)
606
        for member in members:
607
            print member['member_id']
608

    
609

    
610
@command(api='image')
611
class glance_shared(object):
612
    """list shared images"""
613
    
614
    def main(self, member):
615
        images = self.client.list_shared(member)
616
        for image in images:
617
            print image['image_id']
618

    
619

    
620
@command(api='image')
621
class glance_addmember(object):
622
    """add a member to an image"""
623
    
624
    def main(self, image_id, member):
625
        self.client.add_member(image_id, member)
626

    
627

    
628
@command(api='image')
629
class glance_delmember(object):
630
    """remove a member from an image"""
631
    
632
    def main(self, image_id, member):
633
        self.client.remove_member(image_id, member)
634

    
635

    
636
@command(api='image')
637
class glance_setmembers(object):
638
    """set the members of an image"""
639
    
640
    def main(self, image_id, *member):
641
        self.client.set_members(image_id, member)
642

    
643

    
644
class store_command(object):
645
    """base class for all store_* commands"""
646
    
647
    @classmethod
648
    def update_parser(cls, parser):
649
        parser.add_option('--account', dest='account', metavar='ACCOUNT',
650
                help='use account ACCOUNT')
651
        parser.add_option('--container', dest='container', metavar='CONTAINER',
652
                help='use container CONTAINER')
653
    
654
    def main(self):
655
        self.config.override('storage_account', self.options.account)
656
        self.config.override('storage_container', self.options.container)
657
        
658
        # Use the more efficient Pithos client if available
659
        if 'pithos' in self.config.get('apis').split():
660
            self.client = clients.PithosClient(self.config)
661

    
662

    
663
@command(api='storage')
664
class store_container(store_command):
665
    """get container info"""
666
    
667
    def main(self):
668
        store_command.main(self)
669
        reply = self.client.get_container_meta()
670
        print_dict(reply)
671

    
672

    
673
@command(api='storage')
674
class store_upload(store_command):
675
    """upload a file"""
676
    
677
    def main(self, path, remote_path=None):
678
        store_command.main(self)
679
        if remote_path is None:
680
            remote_path = basename(path)
681
        with open(path) as f:
682
            self.client.create_object(remote_path, f)
683

    
684

    
685
@command(api='storage')
686
class store_download(store_command):
687
    """download a file"""
688
    
689
    def main(self, remote_path, local_path):
690
        store_command.main(self)
691
        f = self.client.get_object(remote_path)
692
        out = open(local_path, 'w') if local_path != '-' else stdout
693
        block = 4096
694
        data = f.read(block)
695
        while data:
696
            out.write(data)
697
            data = f.read(block)
698

    
699

    
700
@command(api='storage')
701
class store_delete(store_command):
702
    """delete a file"""
703
    
704
    def main(self, path):
705
        store_command.main(self)
706
        self.client.delete_object(path)
707

    
708

    
709
def print_groups(groups):
710
    print
711
    print 'Groups:'
712
    for group in groups:
713
        print '  %s' % group
714

    
715

    
716
def print_commands(group, commands):
717
    print
718
    print 'Commands:'
719
    for name, cls in _commands[group].items():
720
        if name in commands:
721
            print '  %s %s' % (name.ljust(10), cls.description)
722

    
723

    
724
def main():
725
    ch = logging.StreamHandler()
726
    ch.setFormatter(logging.Formatter('%(message)s'))
727
    log.addHandler(ch)
728
    
729
    parser = OptionParser(add_help_option=False)
730
    parser.usage = '%prog <group> <command> [options]'
731
    parser.add_option('--help', dest='help', action='store_true',
732
            default=False, help='show this help message and exit')
733
    parser.add_option('-v', dest='verbose', action='store_true', default=False,
734
            help='use verbose output')
735
    parser.add_option('-d', dest='debug', action='store_true', default=False,
736
            help='use debug output')
737
    
738
    # Do a preliminary parsing, ignore any errors since we will print help
739
    # anyway if we don't reach the main parsing.
740
    _error = parser.error
741
    parser.error = lambda msg: None
742
    options, args = parser.parse_args(argv)
743
    parser.error = _error
744
    
745
    if options.debug:
746
        log.setLevel(logging.DEBUG)
747
    elif options.verbose:
748
        log.setLevel(logging.INFO)
749
    else:
750
        log.setLevel(logging.WARNING)
751
    
752
    try:
753
        config = Config()
754
    except ConfigError, e:
755
        log.error('%s', e.args[0])
756
        exit(1)
757
    
758
    apis = config.get('apis').split()
759
    
760
    # Find available groups based on the given APIs
761
    available_groups = []
762
    for group, group_commands in _commands.items():
763
        for name, cls in group_commands.items():
764
            if cls.api is None or cls.api in apis:
765
                available_groups.append(group)
766
                break
767
    
768
    if len(args) < 2:
769
        parser.print_help()
770
        print_groups(available_groups)
771
        exit(0)
772
    
773
    group = args[1]
774
    
775
    if group not in available_groups:
776
        parser.print_help()
777
        print_groups(available_groups)
778
        exit(1)
779
    
780
    # Find available commands based on the given APIs
781
    available_commands = []
782
    for name, cls in _commands[group].items():
783
        if cls.api is None or cls.api in apis:
784
            available_commands.append(name)
785
            continue
786
    
787
    parser.usage = '%%prog %s <command> [options]' % group
788
    
789
    if len(args) < 3:
790
        parser.print_help()
791
        print_commands(group, available_commands)
792
        exit(0)
793
    
794
    name = args[2]
795
    
796
    if name not in available_commands:
797
        parser.print_help()
798
        print_commands(group, available_commands)
799
        exit(1)
800
    
801
    cls = _commands[group][name]
802
    
803
    syntax = '%s [options]' % cls.syntax if cls.syntax else '[options]'
804
    parser.usage = '%%prog %s %s %s' % (group, name, syntax)
805
    parser.epilog = ''
806
    if hasattr(cls, 'update_parser'):
807
        cls.update_parser(parser)
808
    
809
    options, args = parser.parse_args(argv)
810
    if options.help:
811
        parser.print_help()
812
        exit(0)
813
    
814
    cmd = cls()
815
    cmd.config = config
816
    cmd.options = options
817
    
818
    if cmd.api:
819
        client_name = cmd.api.capitalize() + 'Client'
820
        client = getattr(clients, client_name, None)
821
        if client:
822
            cmd.client = client(config)
823
    
824
    try:
825
        ret = cmd.main(*args[3:])
826
        exit(ret)
827
    except TypeError as e:
828
        if e.args and e.args[0].startswith('main()'):
829
            parser.print_help()
830
            exit(1)
831
        else:
832
            raise
833
    except clients.ClientError, err:
834
        log.error('%s', err.message)
835
        log.info('%s', err.details)
836
        exit(2)
837

    
838

    
839
if __name__ == '__main__':
840
    main()