Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 77eaa0be

History | View | Annotate | Download (30.2 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, expanduser
77
from pwd import getpwuid
78
from sys import argv, exit, stdout
79

    
80
from clint import args
81
from clint.textui import puts, puts_err, indent, progress
82
from clint.textui.colored import magenta, red, yellow
83
from clint.textui.cols import columns
84

    
85
from requests.exceptions import ConnectionError
86

    
87
from kamaki import clients
88
from kamaki.config import Config
89
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items
90

    
91

    
92
# Path to the file that stores the configuration
93
CONFIG_PATH = expanduser('~/.kamakirc')
94

    
95
# Name of a shell variable to bypass the CONFIG_PATH value
96
CONFIG_ENV = 'KAMAKI_CONFIG'
97

    
98

    
99
_commands = OrderedDict()
100

    
101

    
102
GROUPS = {
103
    'config': "Configuration commands",
104
    'server': "Compute API server commands",
105
    'flavor': "Compute API flavor commands",
106
    'image': "Compute API image commands",
107
    'network': "Compute API network commands (Cyclades extension)",
108
    'glance': "Image API commands",
109
    'store': "Storage API commands"}
110

    
111

    
112
def command(api=None, group=None, name=None, syntax=None):
113
    """Class decorator that registers a class as a CLI command."""
114
    
115
    def decorator(cls):
116
        grp, sep, cmd = cls.__name__.partition('_')
117
        if not sep:
118
            grp, cmd = None, cls.__name__
119
        
120
        cls.api = api
121
        cls.group = group or grp
122
        cls.name = name or cmd
123
        
124
        short_description, sep, long_description = cls.__doc__.partition('\n')
125
        cls.description = short_description
126
        cls.long_description = long_description or short_description
127
        
128
        cls.syntax = syntax
129
        if cls.syntax is None:
130
            # Generate a syntax string based on main's arguments
131
            spec = inspect.getargspec(cls.main.im_func)
132
            args = spec.args[1:]
133
            n = len(args) - len(spec.defaults or ())
134
            required = ' '.join('<%s>' % x.replace('_', ' ') for x in args[:n])
135
            optional = ' '.join('[%s]' % x.replace('_', ' ') for x in args[n:])
136
            cls.syntax = ' '.join(x for x in [required, optional] if x)
137
            if spec.varargs:
138
                cls.syntax += ' <%s ...>' % spec.varargs
139
        
140
        if cls.group not in _commands:
141
            _commands[cls.group] = OrderedDict()
142
        _commands[cls.group][cls.name] = cls
143
        return cls
144
    return decorator
145

    
146

    
147
@command(api='config')
148
class config_list(object):
149
    """List configuration options"""
150
    
151
    def update_parser(self, parser):
152
        parser.add_option('-a', dest='all', action='store_true',
153
                          default=False, help='include default values')
154
    
155
    def main(self):
156
        include_defaults = self.options.all
157
        for section in sorted(self.config.sections()):
158
            items = self.config.items(section, include_defaults)
159
            for key, val in sorted(items):
160
                puts('%s.%s = %s' % (section, key, val))
161

    
162

    
163
@command(api='config')
164
class config_get(object):
165
    """Show a configuration option"""
166
    
167
    def main(self, option):
168
        section, sep, key = option.rpartition('.')
169
        section = section or 'global'
170
        value = self.config.get(section, key)
171
        if value is not None:
172
            print value
173

    
174

    
175
@command(api='config')
176
class config_set(object):
177
    """Set a configuration option"""
178
    
179
    def main(self, option, value):
180
        section, sep, key = option.rpartition('.')
181
        section = section or 'global'
182
        self.config.set(section, key, value)
183
        self.config.write()
184

    
185

    
186
@command(api='config')
187
class config_delete(object):
188
    """Delete a configuration option (and use the default value)"""
189
    
190
    def main(self, option):
191
        section, sep, key = option.rpartition('.')
192
        section = section or 'global'
193
        self.config.remove_option(section, key)
194
        self.config.write()
195

    
196

    
197
@command(api='compute')
198
class server_list(object):
199
    """List servers"""
200
    
201
    def update_parser(self, parser):
202
        parser.add_option('-l', dest='detail', action='store_true',
203
                default=False, help='show detailed output')
204
    
205
    def main(self):
206
        servers = self.client.list_servers(self.options.detail)
207
        print_items(servers)
208

    
209

    
210
@command(api='compute')
211
class server_info(object):
212
    """Get server details"""
213
    
214
    def main(self, server_id):
215
        server = self.client.get_server_details(int(server_id))
216
        print_dict(server)
217

    
218

    
219
@command(api='compute')
220
class server_create(object):
221
    """Create a server"""
222
    
223
    def update_parser(self, parser):
224
        parser.add_option('--personality', dest='personalities',
225
                          action='append', default=[],
226
                          metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
227
                          help='add a personality file')
228
        parser.epilog = "If missing, optional personality values will be " \
229
                        "filled based on the file at PATH."
230
    
231
    def main(self, name, flavor_id, image_id):
232
        personalities = []
233
        for personality in self.options.personalities:
234
            p = personality.split(',')
235
            p.extend([None] * (5 - len(p)))     # Fill missing fields with None
236
            
237
            path = p[0]
238
            
239
            if not path:
240
                log.error("Invalid personality argument '%s'", p)
241
                return 1
242
            if not exists(path):
243
                log.error("File %s does not exist", path)
244
                return 1
245
            
246
            with open(path) as f:
247
                contents = b64encode(f.read())
248
            
249
            st = os.stat(path)
250
            personalities.append({
251
                'path': p[1] or abspath(path),
252
                'owner': p[2] or getpwuid(st.st_uid).pw_name,
253
                'group': p[3] or getgrgid(st.st_gid).gr_name,
254
                'mode': int(p[4]) if p[4] else 0x7777 & st.st_mode,
255
                'contents': contents})
256
        
257
        reply = self.client.create_server(name, int(flavor_id), image_id,
258
                personalities)
259
        print_dict(reply)
260

    
261

    
262
@command(api='compute')
263
class server_rename(object):
264
    """Update a server's name"""
265
    
266
    def main(self, server_id, new_name):
267
        self.client.update_server_name(int(server_id), new_name)
268

    
269

    
270
@command(api='compute')
271
class server_delete(object):
272
    """Delete a server"""
273
    
274
    def main(self, server_id):
275
        self.client.delete_server(int(server_id))
276

    
277

    
278
@command(api='compute')
279
class server_reboot(object):
280
    """Reboot a server"""
281
    
282
    def update_parser(self, parser):
283
        parser.add_option('-f', dest='hard', action='store_true',
284
                default=False, help='perform a hard reboot')
285
    
286
    def main(self, server_id):
287
        self.client.reboot_server(int(server_id), self.options.hard)
288

    
289

    
290
@command(api='cyclades')
291
class server_start(object):
292
    """Start a server"""
293
    
294
    def main(self, server_id):
295
        self.client.start_server(int(server_id))
296

    
297

    
298
@command(api='cyclades')
299
class server_shutdown(object):
300
    """Shutdown a server"""
301
    
302
    def main(self, server_id):
303
        self.client.shutdown_server(int(server_id))
304

    
305

    
306
@command(api='cyclades')
307
class server_console(object):
308
    """Get a VNC console"""
309
    
310
    def main(self, server_id):
311
        reply = self.client.get_server_console(int(server_id))
312
        print_dict(reply)
313

    
314

    
315
@command(api='cyclades')
316
class server_firewall(object):
317
    """Set the server's firewall profile"""
318
    
319
    def main(self, server_id, profile):
320
        self.client.set_firewall_profile(int(server_id), profile)
321

    
322

    
323
@command(api='cyclades')
324
class server_addr(object):
325
    """List a server's addresses"""
326
    
327
    def main(self, server_id, network=None):
328
        reply = self.client.list_server_addresses(int(server_id), network)
329
        margin = max(len(x['name']) for x in reply)
330
        print_addresses(reply, margin)
331

    
332

    
333
@command(api='compute')
334
class server_meta(object):
335
    """Get a server's metadata"""
336
    
337
    def main(self, server_id, key=None):
338
        reply = self.client.get_server_metadata(int(server_id), key)
339
        print_dict(reply)
340

    
341

    
342
@command(api='compute')
343
class server_addmeta(object):
344
    """Add server metadata"""
345
    
346
    def main(self, server_id, key, val):
347
        reply = self.client.create_server_metadata(int(server_id), key, val)
348
        print_dict(reply)
349

    
350

    
351
@command(api='compute')
352
class server_setmeta(object):
353
    """Update server's metadata"""
354
    
355
    def main(self, server_id, key, val):
356
        metadata = {key: val}
357
        reply = self.client.update_server_metadata(int(server_id), **metadata)
358
        print_dict(reply)
359

    
360

    
361
@command(api='compute')
362
class server_delmeta(object):
363
    """Delete server metadata"""
364
    
365
    def main(self, server_id, key):
366
        self.client.delete_server_metadata(int(server_id), key)
367

    
368

    
369
@command(api='cyclades')
370
class server_stats(object):
371
    """Get server statistics"""
372
    
373
    def main(self, server_id):
374
        reply = self.client.get_server_stats(int(server_id))
375
        print_dict(reply, exclude=('serverRef',))
376

    
377

    
378
@command(api='compute')
379
class flavor_list(object):
380
    """List flavors"""
381
    
382
    def update_parser(self, parser):
383
        parser.add_option('-l', dest='detail', action='store_true',
384
                default=False, help='show detailed output')
385
    
386
    def main(self):
387
        flavors = self.client.list_flavors(self.options.detail)
388
        print_items(flavors)
389

    
390

    
391
@command(api='compute')
392
class flavor_info(object):
393
    """Get flavor details"""
394
    
395
    def main(self, flavor_id):
396
        flavor = self.client.get_flavor_details(int(flavor_id))
397
        print_dict(flavor)
398

    
399

    
400
@command(api='compute')
401
class image_list(object):
402
    """List images"""
403
    
404
    def update_parser(self, parser):
405
        parser.add_option('-l', dest='detail', action='store_true',
406
                default=False, help='show detailed output')
407
    
408
    def main(self):
409
        images = self.client.list_images(self.options.detail)
410
        print_items(images)
411

    
412

    
413
@command(api='compute')
414
class image_info(object):
415
    """Get image details"""
416
    
417
    def main(self, image_id):
418
        image = self.client.get_image_details(image_id)
419
        print_dict(image)
420

    
421

    
422
@command(api='compute')
423
class image_delete(object):
424
    """Delete image"""
425
    
426
    def main(self, image_id):
427
        self.client.delete_image(image_id)
428

    
429

    
430
@command(api='compute')
431
class image_meta(object):
432
    """Get image metadata"""
433
    
434
    def main(self, image_id, key=None):
435
        reply = self.client.get_image_metadata(image_id, key)
436
        print_dict(reply)
437

    
438

    
439
@command(api='compute')
440
class image_addmeta(object):
441
    """Add image metadata"""
442
    
443
    def main(self, image_id, key, val):
444
        reply = self.client.create_image_metadata(image_id, key, val)
445
        print_dict(reply)
446

    
447

    
448
@command(api='compute')
449
class image_setmeta(object):
450
    """Update image metadata"""
451
    
452
    def main(self, image_id, key, val):
453
        metadata = {key: val}
454
        reply = self.client.update_image_metadata(image_id, **metadata)
455
        print_dict(reply)
456

    
457

    
458
@command(api='compute')
459
class image_delmeta(object):
460
    """Delete image metadata"""
461
    
462
    def main(self, image_id, key):
463
        self.client.delete_image_metadata(image_id, key)
464

    
465

    
466
@command(api='cyclades')
467
class network_list(object):
468
    """List networks"""
469
    
470
    def update_parser(self, parser):
471
        parser.add_option('-l', dest='detail', action='store_true',
472
                default=False, help='show detailed output')
473
    
474
    def main(self):
475
        networks = self.client.list_networks(self.options.detail)
476
        print_items(networks)
477

    
478

    
479
@command(api='cyclades')
480
class network_create(object):
481
    """Create a network"""
482
    
483
    def main(self, name):
484
        reply = self.client.create_network(name)
485
        print_dict(reply)
486

    
487

    
488
@command(api='cyclades')
489
class network_info(object):
490
    """Get network details"""
491
    
492
    def main(self, network_id):
493
        network = self.client.get_network_details(network_id)
494
        print_dict(network)
495

    
496

    
497
@command(api='cyclades')
498
class network_rename(object):
499
    """Update network name"""
500
    
501
    def main(self, network_id, new_name):
502
        self.client.update_network_name(network_id, new_name)
503

    
504

    
505
@command(api='cyclades')
506
class network_delete(object):
507
    """Delete a network"""
508
    
509
    def main(self, network_id):
510
        self.client.delete_network(network_id)
511

    
512

    
513
@command(api='cyclades')
514
class network_connect(object):
515
    """Connect a server to a network"""
516
    
517
    def main(self, server_id, network_id):
518
        self.client.connect_server(server_id, network_id)
519

    
520

    
521
@command(api='cyclades')
522
class network_disconnect(object):
523
    """Disconnect a server from a network"""
524
    
525
    def main(self, server_id, network_id):
526
        self.client.disconnect_server(server_id, network_id)
527

    
528

    
529
@command(api='image')
530
class glance_list(object):
531
    """List images"""
532
    
533
    def update_parser(self, parser):
534
        parser.add_option('-l', dest='detail', action='store_true',
535
                default=False, help='show detailed output')
536
        parser.add_option('--container-format', dest='container_format',
537
                metavar='FORMAT', help='filter by container format')
538
        parser.add_option('--disk-format', dest='disk_format',
539
                metavar='FORMAT', help='filter by disk format')
540
        parser.add_option('--name', dest='name', metavar='NAME',
541
                help='filter by name')
542
        parser.add_option('--size-min', dest='size_min', metavar='BYTES',
543
                help='filter by minimum size')
544
        parser.add_option('--size-max', dest='size_max', metavar='BYTES',
545
                help='filter by maximum size')
546
        parser.add_option('--status', dest='status', metavar='STATUS',
547
                help='filter by status')
548
        parser.add_option('--order', dest='order', metavar='FIELD',
549
                help='order by FIELD (use a - prefix to reverse order)')
550
    
551
    def main(self):
552
        filters = {}
553
        for filter in ('container_format', 'disk_format', 'name', 'size_min',
554
                       'size_max', 'status'):
555
            val = getattr(self.options, filter, None)
556
            if val is not None:
557
                filters[filter] = val
558
        
559
        order = self.options.order or ''
560
        images = self.client.list_public(self.options.detail, filters=filters,
561
                                         order=order)
562
        print_items(images, title=('name',))
563

    
564

    
565
@command(api='image')
566
class glance_meta(object):
567
    """Get image metadata"""
568
    
569
    def main(self, image_id):
570
        image = self.client.get_meta(image_id)
571
        print_dict(image)
572

    
573

    
574
@command(api='image')
575
class glance_register(object):
576
    """Register an image"""
577
    
578
    def update_parser(self, parser):
579
        parser.add_option('--checksum', dest='checksum', metavar='CHECKSUM',
580
                help='set image checksum')
581
        parser.add_option('--container-format', dest='container_format',
582
                metavar='FORMAT', help='set container format')
583
        parser.add_option('--disk-format', dest='disk_format',
584
                metavar='FORMAT', help='set disk format')
585
        parser.add_option('--id', dest='id',
586
                metavar='ID', help='set image ID')
587
        parser.add_option('--owner', dest='owner',
588
                metavar='USER', help='set image owner (admin only)')
589
        parser.add_option('--property', dest='properties', action='append',
590
                metavar='KEY=VAL',
591
                help='add a property (can be used multiple times)')
592
        parser.add_option('--public', dest='is_public', action='store_true',
593
                help='mark image as public')
594
        parser.add_option('--size', dest='size', metavar='SIZE',
595
                help='set image size')
596
    
597
    def main(self, name, location):
598
        params = {}
599
        for key in ('checksum', 'container_format', 'disk_format', 'id',
600
                    'owner', 'size'):
601
            val = getattr(self.options, key)
602
            if val is not None:
603
                params[key] = val
604
        
605
        if self.options.is_public:
606
            params['is_public'] = 'true'
607
        
608
        properties = {}
609
        for property in self.options.properties or []:
610
            key, sep, val = property.partition('=')
611
            if not sep:
612
                log.error("Invalid property '%s'", property)
613
                return 1
614
            properties[key.strip()] = val.strip()
615
        
616
        self.client.register(name, location, params, properties)
617

    
618

    
619
@command(api='image')
620
class glance_members(object):
621
    """Get image members"""
622
    
623
    def main(self, image_id):
624
        members = self.client.list_members(image_id)
625
        for member in members:
626
            print member['member_id']
627

    
628

    
629
@command(api='image')
630
class glance_shared(object):
631
    """List shared images"""
632
    
633
    def main(self, member):
634
        images = self.client.list_shared(member)
635
        for image in images:
636
            print image['image_id']
637

    
638

    
639
@command(api='image')
640
class glance_addmember(object):
641
    """Add a member to an image"""
642
    
643
    def main(self, image_id, member):
644
        self.client.add_member(image_id, member)
645

    
646

    
647
@command(api='image')
648
class glance_delmember(object):
649
    """Remove a member from an image"""
650
    
651
    def main(self, image_id, member):
652
        self.client.remove_member(image_id, member)
653

    
654

    
655
@command(api='image')
656
class glance_setmembers(object):
657
    """Set the members of an image"""
658
    
659
    def main(self, image_id, *member):
660
        self.client.set_members(image_id, member)
661

    
662

    
663
class store_command(object):
664
    """base class for all store_* commands"""
665
    
666
    def update_parser(cls, parser):
667
        parser.add_option('--account', dest='account', metavar='NAME',
668
                          help="Specify an account to use")
669
        parser.add_option('--container', dest='container', metavar='NAME',
670
                          help="Specify a container to use")
671
    
672
    def progress(self, message):
673
        """Return a generator function to be used for progress tracking"""
674
        
675
        MESSAGE_LENGTH = 25
676
        MAX_PROGRESS_LENGTH = 32
677
        
678
        def progress_gen(n):
679
            msg = message.ljust(MESSAGE_LENGTH)
680
            width = min(n, MAX_PROGRESS_LENGTH)
681
            hide = self.config.get('global', 'silent') or (n < 2)
682
            for i in progress.bar(range(n), msg, width, hide):
683
                yield
684
            yield
685
        
686
        return progress_gen
687
    
688
    def main(self):
689
        if self.options.account is not None:
690
            self.client.account = self.options.account
691
        if self.options.container is not None:
692
            self.client.container = self.options.container
693

    
694

    
695
@command(api='storage')
696
class store_create(object):
697
    """Create a container"""
698
    
699
    def update_parser(cls, parser):
700
        parser.add_option('--account', dest='account', metavar='NAME',
701
                          help="Specify an account to use")
702
    
703
    def main(self, container):
704
        if self.options.account:
705
            self.client.account = self.options.account
706
        self.client.create_container(container)
707

    
708

    
709
@command(api='storage')
710
class store_container(object):
711
    """Get container info"""
712
    
713
    def update_parser(cls, parser):
714
        parser.add_option('--account', dest='account', metavar='NAME',
715
                          help="Specify an account to use")
716
    
717
    def main(self, container):
718
        if self.options.account:
719
            self.client.account = self.options.account
720
        reply = self.client.get_container_meta(container)
721
        print_dict(reply)
722

    
723

    
724
@command(api='storage')
725
class store_upload(store_command):
726
    """Upload a file"""
727
    
728
    def main(self, path, remote_path=None):
729
        super(store_upload, self).main()
730
        
731
        if remote_path is None:
732
            remote_path = basename(path)
733
        with open(path) as f:
734
            hash_cb = self.progress('Calculating block hashes')
735
            upload_cb = self.progress('Uploading blocks')
736
            self.client.create_object(remote_path, f, hash_cb=hash_cb,
737
                                      upload_cb=upload_cb)
738

    
739

    
740
@command(api='storage')
741
class store_download(store_command):
742
    """Download a file"""
743
        
744
    def main(self, remote_path, local_path='-'):
745
        super(store_download, self).main()
746
        
747
        f, size = self.client.get_object(remote_path)
748
        out = open(local_path, 'w') if local_path != '-' else stdout
749
        
750
        blocksize = 4 * 1024**2
751
        nblocks = 1 + (size - 1) // blocksize
752
        
753
        cb = self.progress('Downloading blocks') if local_path != '-' else None
754
        if cb:
755
            gen = cb(nblocks)
756
            gen.next()
757
        
758
        data = f.read(blocksize)
759
        while data:
760
            out.write(data)
761
            data = f.read(blocksize)
762
            if cb:
763
                gen.next()
764

    
765

    
766
@command(api='storage')
767
class store_delete(store_command):
768
    """Delete a file"""
769
    
770
    def main(self, path):
771
        store_command.main(self)
772
        self.client.delete_object(path)
773

    
774

    
775
def print_groups():
776
    puts('\nGroups:')
777
    with indent(2):
778
        for group in _commands:
779
            description = GROUPS.get(group, '')
780
            puts(columns([group, 12], [description, 60]))
781

    
782

    
783
def print_commands(group):
784
    description = GROUPS.get(group, '')
785
    if description:
786
        puts('\n' + description)
787
    
788
    puts('\nCommands:')
789
    with indent(2):
790
        for name, cls in _commands[group].items():
791
            puts(columns([name, 12], [cls.description, 60]))
792

    
793

    
794
def add_handler(name, level, prefix=''):
795
    h = logging.StreamHandler()
796
    fmt = logging.Formatter(prefix + '%(message)s')
797
    h.setFormatter(fmt)
798
    logger = logging.getLogger(name)
799
    logger.addHandler(h)
800
    logger.setLevel(level)
801

    
802

    
803
def main():
804
    parser = OptionParser(add_help_option=False)
805
    parser.usage = '%prog <group> <command> [options]'
806
    parser.add_option('-h', '--help', dest='help', action='store_true',
807
                      default=False,
808
                      help="Show this help message and exit")
809
    parser.add_option('--config', dest='config', metavar='PATH',
810
                      help="Specify the path to the configuration file")
811
    parser.add_option('-d', '--debug', dest='debug', action='store_true',
812
                      default=False,
813
                      help="Include debug output")
814
    parser.add_option('-i', '--include', dest='include', action='store_true',
815
                      default=False,
816
                      help="Include protocol headers in the output")
817
    parser.add_option('-s', '--silent', dest='silent', action='store_true',
818
                      default=False,
819
                      help="Silent mode, don't output anything")
820
    parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
821
                      default=False,
822
                      help="Make the operation more talkative")
823
    parser.add_option('-V', '--version', dest='version', action='store_true',
824
                      default=False,
825
                      help="Show version number and quit")
826
    parser.add_option('-o', dest='options', action='append',
827
                      default=[], metavar="KEY=VAL",
828
                      help="Override a config values")
829
    
830
    if args.contains(['-V', '--version']):
831
        import kamaki
832
        print "kamaki %s" % kamaki.__version__
833
        exit(0)
834
    
835
    if '--config' in args:
836
        config_path = args.grouped['--config'].get(0)
837
    else:
838
        config_path = os.environ.get(CONFIG_ENV, CONFIG_PATH)
839
    
840
    config = Config(config_path)
841
    
842
    for option in args.grouped.get('-o', []):
843
        keypath, sep, val = option.partition('=')
844
        if not sep:
845
            log.error("Invalid option '%s'", option)
846
            exit(1)
847
        section, sep, key = keypath.partition('.')
848
        if not sep:
849
            log.error("Invalid option '%s'", option)
850
            exit(1)
851
        config.override(section.strip(), key.strip(), val.strip())
852
    
853
    apis = set(['config'])
854
    for api in ('compute', 'image', 'storage'):
855
        if config.getboolean(api, 'enable'):
856
            apis.add(api)
857
    if config.getboolean('compute', 'cyclades_extensions'):
858
        apis.add('cyclades')
859
    if config.getboolean('storage', 'pithos_extensions'):
860
        apis.add('pithos')
861
    
862
    # Remove commands that belong to APIs that are not included
863
    for group, group_commands in _commands.items():
864
        for name, cls in group_commands.items():
865
            if cls.api not in apis:
866
                del group_commands[name]
867
        if not group_commands:
868
            del _commands[group]
869
    
870
    if not args.grouped['_']:
871
        parser.print_help()
872
        print_groups()
873
        exit(0)
874
    
875
    group = args.grouped['_'][0]
876
    
877
    if group not in _commands:
878
        parser.print_help()
879
        print_groups()
880
        exit(1)
881
    
882
    parser.usage = '%%prog %s <command> [options]' % group
883
    
884
    if len(args.grouped['_']) == 1:
885
        parser.print_help()
886
        print_commands(group)
887
        exit(0)
888
    
889
    name = args.grouped['_'][1]
890
    
891
    if name not in _commands[group]:
892
        parser.print_help()
893
        print_commands(group)
894
        exit(1)
895
    
896
    cmd = _commands[group][name]()
897
    
898
    syntax = '%s [options]' % cmd.syntax if cmd.syntax else '[options]'
899
    parser.usage = '%%prog %s %s %s' % (group, name, syntax)
900
    parser.description = cmd.description
901
    parser.epilog = ''
902
    if hasattr(cmd, 'update_parser'):
903
        cmd.update_parser(parser)
904
    
905
    options, arguments = parser.parse_args(argv)
906
    
907
    if options.help:
908
        parser.print_help()
909
        exit(0)
910
    
911
    if options.silent:
912
        add_handler('', logging.CRITICAL)
913
    elif options.debug:
914
        add_handler('requests', logging.INFO, prefix='* ')
915
        add_handler('clients.send', logging.DEBUG, prefix='> ')
916
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
917
    elif options.verbose:
918
        add_handler('requests', logging.INFO, prefix='* ')
919
        add_handler('clients.send', logging.INFO, prefix='> ')
920
        add_handler('clients.recv', logging.INFO, prefix='< ')
921
    elif options.include:
922
        add_handler('clients.recv', logging.INFO)
923
    else:
924
        add_handler('', logging.WARNING)
925
    
926
    api = cmd.api
927
    if api in ('compute', 'cyclades'):
928
        url = config.get('compute', 'url')
929
        token = config.get('compute', 'token') or config.get('global', 'token')
930
        if config.getboolean('compute', 'cyclades_extensions'):
931
            cmd.client = clients.cyclades(url, token)
932
        else:
933
            cmd.client = clients.compute(url, token)
934
    elif api in ('storage', 'pithos'):
935
        url = config.get('storage', 'url')
936
        token = config.get('storage', 'token') or config.get('global', 'token')
937
        account = config.get('storage', 'account')
938
        container = config.get('storage', 'container')
939
        if config.getboolean('storage', 'pithos_extensions'):
940
            cmd.client = clients.pithos(url, token, account, container)
941
        else:
942
            cmd.client = clients.storage(url, token, account, container)
943
    elif api == 'image':
944
        url = config.get('image', 'url')
945
        token = config.get('image', 'token') or config.get('global', 'token')
946
        cmd.client = clients.image(url, token)
947
    
948
    cmd.options = options
949
    cmd.config = config
950
    
951
    try:
952
        ret = cmd.main(*arguments[3:])
953
        exit(ret)
954
    except TypeError as e:
955
        if e.args and e.args[0].startswith('main()'):
956
            parser.print_help()
957
            exit(1)
958
        else:
959
            raise
960
    except clients.ClientError as err:
961
        if err.status == 404:
962
            color = yellow
963
        elif 500 <= err.status < 600:
964
            color = magenta
965
        else:
966
            color = red
967
        
968
        puts_err(color(err.message))
969
        if err.details and (options.verbose or options.debug):
970
            puts_err(err.details)
971
        exit(2)
972
    except ConnectionError as err:
973
        puts_err(red("Connection error"))
974
        exit(1)
975

    
976

    
977
if __name__ == '__main__':
978
    main()