Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 43ca98ee

History | View | Annotate | Download (31.3 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 API image commands",
107
    'network': "Compute API network commands (Cyclades extension)",
108
    'glance': "Image API commands",
109
    'store': "Storage API commands",
110
    'astakos': "Astakos API commands"}
111

    
112

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

    
147

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

    
163

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

    
175

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

    
186

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

    
197

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

    
210

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

    
219

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

    
262

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

    
270

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

    
278

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

    
290

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

    
298

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

    
306

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

    
315

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

    
323

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

    
333

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

    
342

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

    
351

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

    
361

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

    
369

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

    
378

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

    
391

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

    
400

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

    
413

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

    
422

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

    
430

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

    
439

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

    
448

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

    
458

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

    
466

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

    
479

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

    
488

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

    
497

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

    
505

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

    
513

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

    
521

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

    
529

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

    
565

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

    
574

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

    
619

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

    
629

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

    
639

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

    
647

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

    
655

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

    
663

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

    
691

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

    
705

    
706
@command(api='storage')
707
class store_create(_store_account_command):
708
    """Create a container"""
709
    
710
    def main(self, container):
711
        if self.options.account:
712
            self.client.account = self.options.account
713
        self.client.create_container(container)
714

    
715

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

    
726

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

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

    
764

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

    
790

    
791
@command(api='storage')
792
class store_delete(_store_container_command):
793
    """Delete a file"""
794
    
795
    def main(self, path):
796
        store_command.main(self)
797
        self.client.delete_object(path)
798

    
799

    
800
@command(api='astakos')
801
class astakos_authenticate(object):
802
    """Authenticate a user"""
803
    
804
    def main(self):
805
        reply = self.client.authenticate()
806
        print_dict(reply)
807

    
808

    
809
def print_groups():
810
    puts('\nGroups:')
811
    with indent(2):
812
        for group in _commands:
813
            description = GROUPS.get(group, '')
814
            puts(columns([group, 12], [description, 60]))
815

    
816

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

    
827

    
828
def add_handler(name, level, prefix=''):
829
    h = logging.StreamHandler()
830
    fmt = logging.Formatter(prefix + '%(message)s')
831
    h.setFormatter(fmt)
832
    logger = logging.getLogger(name)
833
    logger.addHandler(h)
834
    logger.setLevel(level)
835

    
836

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

    
1014

    
1015
if __name__ == '__main__':
1016
    main()