Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ c4ab2af9

History | View | Annotate | Download (24.8 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='NAME',
641
                help='use account NAME')
642
        parser.add_option('--container', dest='container', metavar='NAME',
643
                help='use container NAME')
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_create(object):
656
    """create a container"""
657
    
658
    @classmethod
659
    def update_parser(cls, parser):
660
        parser.add_option('--account', dest='account', metavar='ACCOUNT',
661
                help='use account ACCOUNT')
662
    
663
    def main(self, container):
664
        self.config.override('storage_account', self.options.account)
665
        self.client.create_container(container)
666

    
667

    
668
@command(api='storage')
669
class store_container(store_command):
670
    """get container info"""
671
    
672
    def main(self):
673
        store_command.main(self)
674
        reply = self.client.get_container_meta()
675
        print_dict(reply)
676

    
677

    
678
@command(api='storage')
679
class store_upload(store_command):
680
    """upload a file"""
681
    
682
    def main(self, path, remote_path=None):
683
        store_command.main(self)
684
        if remote_path is None:
685
            remote_path = basename(path)
686
        with open(path) as f:
687
            self.client.create_object(remote_path, f)
688

    
689

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

    
704

    
705
@command(api='storage')
706
class store_delete(store_command):
707
    """delete a file"""
708
    
709
    def main(self, path):
710
        store_command.main(self)
711
        self.client.delete_object(path)
712

    
713

    
714
def print_groups(groups):
715
    print
716
    print 'Groups:'
717
    for group in groups:
718
        print '  %s' % group
719

    
720

    
721
def print_commands(group, commands):
722
    print
723
    print 'Commands:'
724
    for name, cls in _commands[group].items():
725
        if name in commands:
726
            print '  %s %s' % (name.ljust(10), cls.description)
727

    
728

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

    
853

    
854
if __name__ == '__main__':
855
    main()