Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 2bcb595a

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

    
690

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

    
704

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

    
714

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

    
725

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

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

    
763

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

    
789

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

    
798

    
799
def print_groups():
800
    puts('\nGroups:')
801
    with indent(2):
802
        for group in _commands:
803
            description = GROUPS.get(group, '')
804
            puts(columns([group, 12], [description, 60]))
805

    
806

    
807
def print_commands(group):
808
    description = GROUPS.get(group, '')
809
    if description:
810
        puts('\n' + description)
811
    
812
    puts('\nCommands:')
813
    with indent(2):
814
        for name, cls in _commands[group].items():
815
            puts(columns([name, 12], [cls.description, 60]))
816

    
817

    
818
def add_handler(name, level, prefix=''):
819
    h = logging.StreamHandler()
820
    fmt = logging.Formatter(prefix + '%(message)s')
821
    h.setFormatter(fmt)
822
    logger = logging.getLogger(name)
823
    logger.addHandler(h)
824
    logger.setLevel(level)
825

    
826

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

    
1000

    
1001
if __name__ == '__main__':
1002
    main()