Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 98615068

History | View | Annotate | Download (31.5 kB)

1
#!/usr/bin/env python
2

    
3
# Copyright 2011-2012 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 or Glance API image commands",
107
    'network': "Compute API network commands (Cyclades extension)",
108
    'store': "Storage API commands",
109
    'astakos': "Astakos 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
                print "Invalid personality argument '%s'" % p
241
                return 1
242
            if not exists(path):
243
                print "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_properties(object):
432
    """Get image properties"""
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_addproperty(object):
441
    """Add an image property"""
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_setproperty(object):
450
    """Update an image property"""
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_delproperty(object):
460
    """Delete an image property"""
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 image_public(object):
531
    """List public 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 image_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 image_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
        if not location.startswith('pithos://'):
599
            account = self.config.get('storage', 'account')
600
            container = self.config.get('storage', 'container')
601
            location = 'pithos://%s/%s/%s' % (account, container, location)
602
        
603
        params = {}
604
        for key in ('checksum', 'container_format', 'disk_format', 'id',
605
                    'owner', 'size'):
606
            val = getattr(self.options, key)
607
            if val is not None:
608
                params[key] = val
609
        
610
        if self.options.is_public:
611
            params['is_public'] = 'true'
612
        
613
        properties = {}
614
        for property in self.options.properties or []:
615
            key, sep, val = property.partition('=')
616
            if not sep:
617
                print "Invalid property '%s'" % property
618
                return 1
619
            properties[key.strip()] = val.strip()
620
        
621
        self.client.register(name, location, params, properties)
622

    
623

    
624
@command(api='image')
625
class image_members(object):
626
    """Get image members"""
627
    
628
    def main(self, image_id):
629
        members = self.client.list_members(image_id)
630
        for member in members:
631
            print member['member_id']
632

    
633

    
634
@command(api='image')
635
class image_shared(object):
636
    """List shared images"""
637
    
638
    def main(self, member):
639
        images = self.client.list_shared(member)
640
        for image in images:
641
            print image['image_id']
642

    
643

    
644
@command(api='image')
645
class image_addmember(object):
646
    """Add a member to an image"""
647
    
648
    def main(self, image_id, member):
649
        self.client.add_member(image_id, member)
650

    
651

    
652
@command(api='image')
653
class image_delmember(object):
654
    """Remove a member from an image"""
655
    
656
    def main(self, image_id, member):
657
        self.client.remove_member(image_id, member)
658

    
659

    
660
@command(api='image')
661
class image_setmembers(object):
662
    """Set the members of an image"""
663
    
664
    def main(self, image_id, *member):
665
        self.client.set_members(image_id, member)
666

    
667

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

    
695

    
696
class _store_container_command(_store_account_command):
697
    """Base class for container level storage commands"""
698
    
699
    def update_parser(self, parser):
700
        super(_store_container_command, self).update_parser(parser)
701
        parser.add_option('--container', dest='container', metavar='NAME',
702
                          help="Specify a container to use")
703
    
704
    def main(self):
705
        super(_store_container_command, self).main()
706
        if self.options.container is not None:
707
            self.client.container = self.options.container
708

    
709

    
710
@command(api='storage')
711
class store_create(_store_account_command):
712
    """Create a container"""
713
    
714
    def main(self, container):
715
        if self.options.account:
716
            self.client.account = self.options.account
717
        self.client.create_container(container)
718

    
719

    
720
@command(api='storage')
721
class store_container(_store_account_command):
722
    """Get container info"""
723
    
724
    def main(self, container):
725
        if self.options.account:
726
            self.client.account = self.options.account
727
        reply = self.client.get_container_meta(container)
728
        print_dict(reply)
729

    
730

    
731
@command(api='storage')
732
class store_list(_store_container_command):
733
    """List objects"""
734
    
735
    def format_size(self, size):
736
        units = ('B', 'K', 'M', 'G', 'T')
737
        size = float(size)
738
        for unit in units:
739
            if size <= 1024:
740
                break
741
            size /= 1024
742
        s = ('%.1f' % size).rstrip('.0')
743
        return s + unit
744
    
745
    
746
    def main(self, path=''):
747
        super(store_list, self).main()
748
        for object in self.client.list_objects():
749
            size = self.format_size(object['bytes'])
750
            print '%6s %s' % (size, object['name'])
751
        
752

    
753
@command(api='storage')
754
class store_upload(_store_container_command):
755
    """Upload a file"""
756
    
757
    def main(self, path, remote_path=None):
758
        super(store_upload, self).main()
759
        
760
        if remote_path is None:
761
            remote_path = basename(path)
762
        with open(path) as f:
763
            hash_cb = self.progress('Calculating block hashes')
764
            upload_cb = self.progress('Uploading blocks')
765
            self.client.create_object(remote_path, f, hash_cb=hash_cb,
766
                                      upload_cb=upload_cb)
767

    
768

    
769
@command(api='storage')
770
class store_download(_store_container_command):
771
    """Download a file"""
772
        
773
    def main(self, remote_path, local_path='-'):
774
        super(store_download, self).main()
775
        
776
        f, size = self.client.get_object(remote_path)
777
        out = open(local_path, 'w') if local_path != '-' else stdout
778
        
779
        blocksize = 4 * 1024**2
780
        nblocks = 1 + (size - 1) // blocksize
781
        
782
        cb = self.progress('Downloading blocks') if local_path != '-' else None
783
        if cb:
784
            gen = cb(nblocks)
785
            gen.next()
786
        
787
        data = f.read(blocksize)
788
        while data:
789
            out.write(data)
790
            data = f.read(blocksize)
791
            if cb:
792
                gen.next()
793

    
794

    
795
@command(api='storage')
796
class store_delete(_store_container_command):
797
    """Delete a file"""
798
    
799
    def main(self, path):
800
        store_command.main(self)
801
        self.client.delete_object(path)
802

    
803

    
804
@command(api='astakos')
805
class astakos_authenticate(object):
806
    """Authenticate a user"""
807
    
808
    def main(self):
809
        reply = self.client.authenticate()
810
        print_dict(reply)
811

    
812

    
813
def print_groups():
814
    puts('\nGroups:')
815
    with indent(2):
816
        for group in _commands:
817
            description = GROUPS.get(group, '')
818
            puts(columns([group, 12], [description, 60]))
819

    
820

    
821
def print_commands(group):
822
    description = GROUPS.get(group, '')
823
    if description:
824
        puts('\n' + description)
825
    
826
    puts('\nCommands:')
827
    with indent(2):
828
        for name, cls in _commands[group].items():
829
            puts(columns([name, 14], [cls.description, 60]))
830

    
831

    
832
def add_handler(name, level, prefix=''):
833
    h = logging.StreamHandler()
834
    fmt = logging.Formatter(prefix + '%(message)s')
835
    h.setFormatter(fmt)
836
    logger = logging.getLogger(name)
837
    logger.addHandler(h)
838
    logger.setLevel(level)
839

    
840

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

    
1018

    
1019
if __name__ == '__main__':
1020
    main()