Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ b3b32add

History | View | Annotate | Download (24.1 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='cyclades')
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='cyclades')
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='cyclades')
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='cyclades')
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='cyclades')
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='cyclades')
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_delete(object):
396
    """delete image"""
397
    
398
    def main(self, image_id):
399
        self.client.delete_image(image_id)
400

    
401

    
402
@command(api='compute')
403
class image_meta(object):
404
    """get image metadata"""
405
    
406
    def main(self, image_id, key=None):
407
        reply = self.client.get_image_metadata(image_id, key)
408
        print_dict(reply)
409

    
410

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

    
419

    
420
@command(api='compute')
421
class image_setmeta(object):
422
    """update image metadata"""
423
    
424
    def main(self, image_id, key, val):
425
        metadata = {key: val}
426
        reply = self.client.update_image_metadata(image_id, **metadata)
427
        print_dict(reply)
428

    
429

    
430
@command(api='compute')
431
class image_delmeta(object):
432
    """delete image metadata"""
433
    
434
    def main(self, image_id, key):
435
        self.client.delete_image_metadata(image_id, key)
436

    
437

    
438
@command(api='cyclades')
439
class network_list(object):
440
    """list networks"""
441
    
442
    @classmethod
443
    def update_parser(cls, parser):
444
        parser.add_option('-l', dest='detail', action='store_true',
445
                default=False, help='show detailed output')
446
    
447
    def main(self):
448
        networks = self.client.list_networks(self.options.detail)
449
        print_items(networks)
450

    
451

    
452
@command(api='cyclades')
453
class network_create(object):
454
    """create a network"""
455
    
456
    def main(self, name):
457
        reply = self.client.create_network(name)
458
        print_dict(reply)
459

    
460

    
461
@command(api='cyclades')
462
class network_info(object):
463
    """get network details"""
464
    
465
    def main(self, network_id):
466
        network = self.client.get_network_details(network_id)
467
        print_dict(network)
468

    
469

    
470
@command(api='cyclades')
471
class network_rename(object):
472
    """update network name"""
473
    
474
    def main(self, network_id, new_name):
475
        self.client.update_network_name(network_id, new_name)
476

    
477

    
478
@command(api='cyclades')
479
class network_delete(object):
480
    """delete a network"""
481
    
482
    def main(self, network_id):
483
        self.client.delete_network(network_id)
484

    
485

    
486
@command(api='cyclades')
487
class network_connect(object):
488
    """connect a server to a network"""
489
    
490
    def main(self, server_id, network_id):
491
        self.client.connect_server(server_id, network_id)
492

    
493

    
494
@command(api='cyclades')
495
class network_disconnect(object):
496
    """disconnect a server from a network"""
497
    
498
    def main(self, server_id, network_id):
499
        self.client.disconnect_server(server_id, network_id)
500

    
501

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

    
538

    
539
@command(api='image')
540
class glance_meta(object):
541
    """get image metadata"""
542
    
543
    def main(self, image_id):
544
        image = self.client.get_meta(image_id)
545
        print_dict(image)
546

    
547

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

    
590

    
591
@command(api='image')
592
class glance_members(object):
593
    """get image members"""
594
    
595
    def main(self, image_id):
596
        members = self.client.list_members(image_id)
597
        for member in members:
598
            print member['member_id']
599

    
600

    
601
@command(api='image')
602
class glance_shared(object):
603
    """list shared images"""
604
    
605
    def main(self, member):
606
        images = self.client.list_shared(member)
607
        for image in images:
608
            print image['image_id']
609

    
610

    
611
@command(api='image')
612
class glance_addmember(object):
613
    """add a member to an image"""
614
    
615
    def main(self, image_id, member):
616
        self.client.add_member(image_id, member)
617

    
618

    
619
@command(api='image')
620
class glance_delmember(object):
621
    """remove a member from an image"""
622
    
623
    def main(self, image_id, member):
624
        self.client.remove_member(image_id, member)
625

    
626

    
627
@command(api='image')
628
class glance_setmembers(object):
629
    """set the members of an image"""
630
    
631
    def main(self, image_id, *member):
632
        self.client.set_members(image_id, member)
633

    
634

    
635
class store_command(object):
636
    """base class for all store_* commands"""
637
    
638
    @classmethod
639
    def update_parser(cls, parser):
640
        parser.add_option('--account', dest='account', metavar='ACCOUNT',
641
                help='use account ACCOUNT')
642
        parser.add_option('--container', dest='container', metavar='CONTAINER',
643
                help='use container CONTAINER')
644
    
645
    def main(self):
646
        self.config.override('storage_account', self.options.account)
647
        self.config.override('storage_container', self.options.container)
648
        
649
        # Use the more efficient Pithos client if available
650
        if 'pithos' in self.config.get('apis').split():
651
            self.client = clients.PithosClient(self.config)
652

    
653

    
654
@command(api='storage')
655
class store_container(store_command):
656
    """get container info"""
657
    
658
    def main(self):
659
        store_command.main(self)
660
        reply = self.client.get_container_meta()
661
        print_dict(reply)
662

    
663

    
664
@command(api='storage')
665
class store_upload(store_command):
666
    """upload a file"""
667
    
668
    def main(self, path, remote_path=None):
669
        store_command.main(self)
670
        if remote_path is None:
671
            remote_path = basename(path)
672
        with open(path) as f:
673
            self.client.create_object(remote_path, f)
674

    
675

    
676
@command(api='storage')
677
class store_download(store_command):
678
    """download a file"""
679
    
680
    def main(self, remote_path, local_path):
681
        store_command.main(self)
682
        f = self.client.get_object(remote_path)
683
        out = open(local_path, 'w') if local_path != '-' else stdout
684
        block = 4096
685
        data = f.read(block)
686
        while data:
687
            out.write(data)
688
            data = f.read(block)
689

    
690

    
691
@command(api='storage')
692
class store_delete(store_command):
693
    """delete a file"""
694
    
695
    def main(self, path):
696
        store_command.main(self)
697
        self.client.delete_object(path)
698

    
699

    
700
def print_groups(groups):
701
    print
702
    print 'Groups:'
703
    for group in groups:
704
        print '  %s' % group
705

    
706

    
707
def print_commands(group, commands):
708
    print
709
    print 'Commands:'
710
    for name, cls in _commands[group].items():
711
        if name in commands:
712
            print '  %s %s' % (name.ljust(10), cls.description)
713

    
714

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

    
829

    
830
if __name__ == '__main__':
831
    main()