Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 2d0b6dcc

History | View | Annotate | Download (31.4 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
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
_commands = OrderedDict()
93

    
94

    
95
GROUPS = {
96
    'config': "Configuration commands",
97
    'server': "Compute API server commands",
98
    'flavor': "Compute API flavor commands",
99
    'image': "Compute or Glance API image commands",
100
    'network': "Compute API network commands (Cyclades extension)",
101
    'store': "Storage API commands",
102
    'astakos': "Astakos API commands"}
103

    
104

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

    
139

    
140
@command(api='config')
141
class config_list(object):
142
    """List configuration options"""
143
    
144
    def update_parser(self, parser):
145
        parser.add_option('-a', dest='all', action='store_true',
146
                          default=False, help='include default values')
147
    
148
    def main(self):
149
        include_defaults = self.options.all
150
        for section in sorted(self.config.sections()):
151
            items = self.config.items(section, include_defaults)
152
            for key, val in sorted(items):
153
                puts('%s.%s = %s' % (section, key, val))
154

    
155

    
156
@command(api='config')
157
class config_get(object):
158
    """Show a configuration option"""
159
    
160
    def main(self, option):
161
        section, sep, key = option.rpartition('.')
162
        section = section or 'global'
163
        value = self.config.get(section, key)
164
        if value is not None:
165
            print value
166

    
167

    
168
@command(api='config')
169
class config_set(object):
170
    """Set a configuration option"""
171
    
172
    def main(self, option, value):
173
        section, sep, key = option.rpartition('.')
174
        section = section or 'global'
175
        self.config.set(section, key, value)
176
        self.config.write()
177

    
178

    
179
@command(api='config')
180
class config_delete(object):
181
    """Delete a configuration option (and use the default value)"""
182
    
183
    def main(self, option):
184
        section, sep, key = option.rpartition('.')
185
        section = section or 'global'
186
        self.config.remove_option(section, key)
187
        self.config.write()
188

    
189

    
190
@command(api='compute')
191
class server_list(object):
192
    """List servers"""
193
    
194
    def update_parser(self, parser):
195
        parser.add_option('-l', dest='detail', action='store_true',
196
                default=False, help='show detailed output')
197
    
198
    def main(self):
199
        servers = self.client.list_servers(self.options.detail)
200
        print_items(servers)
201

    
202

    
203
@command(api='compute')
204
class server_info(object):
205
    """Get server details"""
206
    
207
    def main(self, server_id):
208
        server = self.client.get_server_details(int(server_id))
209
        print_dict(server)
210

    
211

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

    
254

    
255
@command(api='compute')
256
class server_rename(object):
257
    """Update a server's name"""
258
    
259
    def main(self, server_id, new_name):
260
        self.client.update_server_name(int(server_id), new_name)
261

    
262

    
263
@command(api='compute')
264
class server_delete(object):
265
    """Delete a server"""
266
    
267
    def main(self, server_id):
268
        self.client.delete_server(int(server_id))
269

    
270

    
271
@command(api='compute')
272
class server_reboot(object):
273
    """Reboot a server"""
274
    
275
    def update_parser(self, parser):
276
        parser.add_option('-f', dest='hard', action='store_true',
277
                default=False, help='perform a hard reboot')
278
    
279
    def main(self, server_id):
280
        self.client.reboot_server(int(server_id), self.options.hard)
281

    
282

    
283
@command(api='cyclades')
284
class server_start(object):
285
    """Start a server"""
286
    
287
    def main(self, server_id):
288
        self.client.start_server(int(server_id))
289

    
290

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

    
298

    
299
@command(api='cyclades')
300
class server_console(object):
301
    """Get a VNC console"""
302
    
303
    def main(self, server_id):
304
        reply = self.client.get_server_console(int(server_id))
305
        print_dict(reply)
306

    
307

    
308
@command(api='cyclades')
309
class server_firewall(object):
310
    """Set the server's firewall profile"""
311
    
312
    def main(self, server_id, profile):
313
        self.client.set_firewall_profile(int(server_id), profile)
314

    
315

    
316
@command(api='cyclades')
317
class server_addr(object):
318
    """List a server's addresses"""
319
    
320
    def main(self, server_id, network=None):
321
        reply = self.client.list_server_addresses(int(server_id), network)
322
        margin = max(len(x['name']) for x in reply)
323
        print_addresses(reply, margin)
324

    
325

    
326
@command(api='compute')
327
class server_meta(object):
328
    """Get a server's metadata"""
329
    
330
    def main(self, server_id, key=None):
331
        reply = self.client.get_server_metadata(int(server_id), key)
332
        print_dict(reply)
333

    
334

    
335
@command(api='compute')
336
class server_addmeta(object):
337
    """Add server metadata"""
338
    
339
    def main(self, server_id, key, val):
340
        reply = self.client.create_server_metadata(int(server_id), key, val)
341
        print_dict(reply)
342

    
343

    
344
@command(api='compute')
345
class server_setmeta(object):
346
    """Update server's metadata"""
347
    
348
    def main(self, server_id, key, val):
349
        metadata = {key: val}
350
        reply = self.client.update_server_metadata(int(server_id), **metadata)
351
        print_dict(reply)
352

    
353

    
354
@command(api='compute')
355
class server_delmeta(object):
356
    """Delete server metadata"""
357
    
358
    def main(self, server_id, key):
359
        self.client.delete_server_metadata(int(server_id), key)
360

    
361

    
362
@command(api='cyclades')
363
class server_stats(object):
364
    """Get server statistics"""
365
    
366
    def main(self, server_id):
367
        reply = self.client.get_server_stats(int(server_id))
368
        print_dict(reply, exclude=('serverRef',))
369

    
370

    
371
@command(api='compute')
372
class flavor_list(object):
373
    """List flavors"""
374
    
375
    def update_parser(self, parser):
376
        parser.add_option('-l', dest='detail', action='store_true',
377
                default=False, help='show detailed output')
378
    
379
    def main(self):
380
        flavors = self.client.list_flavors(self.options.detail)
381
        print_items(flavors)
382

    
383

    
384
@command(api='compute')
385
class flavor_info(object):
386
    """Get flavor details"""
387
    
388
    def main(self, flavor_id):
389
        flavor = self.client.get_flavor_details(int(flavor_id))
390
        print_dict(flavor)
391

    
392

    
393
@command(api='compute')
394
class image_list(object):
395
    """List images"""
396
    
397
    def update_parser(self, parser):
398
        parser.add_option('-l', dest='detail', action='store_true',
399
                default=False, help='show detailed output')
400
    
401
    def main(self):
402
        images = self.client.list_images(self.options.detail)
403
        print_items(images)
404

    
405

    
406
@command(api='compute')
407
class image_info(object):
408
    """Get image details"""
409
    
410
    def main(self, image_id):
411
        image = self.client.get_image_details(image_id)
412
        print_dict(image)
413

    
414

    
415
@command(api='compute')
416
class image_delete(object):
417
    """Delete image"""
418
    
419
    def main(self, image_id):
420
        self.client.delete_image(image_id)
421

    
422

    
423
@command(api='compute')
424
class image_properties(object):
425
    """Get image properties"""
426
    
427
    def main(self, image_id, key=None):
428
        reply = self.client.get_image_metadata(image_id, key)
429
        print_dict(reply)
430

    
431

    
432
@command(api='compute')
433
class image_addproperty(object):
434
    """Add an image property"""
435
    
436
    def main(self, image_id, key, val):
437
        reply = self.client.create_image_metadata(image_id, key, val)
438
        print_dict(reply)
439

    
440

    
441
@command(api='compute')
442
class image_setproperty(object):
443
    """Update an image property"""
444
    
445
    def main(self, image_id, key, val):
446
        metadata = {key: val}
447
        reply = self.client.update_image_metadata(image_id, **metadata)
448
        print_dict(reply)
449

    
450

    
451
@command(api='compute')
452
class image_delproperty(object):
453
    """Delete an image property"""
454
    
455
    def main(self, image_id, key):
456
        self.client.delete_image_metadata(image_id, key)
457

    
458

    
459
@command(api='cyclades')
460
class network_list(object):
461
    """List networks"""
462
    
463
    def update_parser(self, parser):
464
        parser.add_option('-l', dest='detail', action='store_true',
465
                default=False, help='show detailed output')
466
    
467
    def main(self):
468
        networks = self.client.list_networks(self.options.detail)
469
        print_items(networks)
470

    
471

    
472
@command(api='cyclades')
473
class network_create(object):
474
    """Create a network"""
475
    
476
    def main(self, name):
477
        reply = self.client.create_network(name)
478
        print_dict(reply)
479

    
480

    
481
@command(api='cyclades')
482
class network_info(object):
483
    """Get network details"""
484
    
485
    def main(self, network_id):
486
        network = self.client.get_network_details(network_id)
487
        print_dict(network)
488

    
489

    
490
@command(api='cyclades')
491
class network_rename(object):
492
    """Update network name"""
493
    
494
    def main(self, network_id, new_name):
495
        self.client.update_network_name(network_id, new_name)
496

    
497

    
498
@command(api='cyclades')
499
class network_delete(object):
500
    """Delete a network"""
501
    
502
    def main(self, network_id):
503
        self.client.delete_network(network_id)
504

    
505

    
506
@command(api='cyclades')
507
class network_connect(object):
508
    """Connect a server to a network"""
509
    
510
    def main(self, server_id, network_id):
511
        self.client.connect_server(server_id, network_id)
512

    
513

    
514
@command(api='cyclades')
515
class network_disconnect(object):
516
    """Disconnect a server from a network"""
517
    
518
    def main(self, server_id, network_id):
519
        self.client.disconnect_server(server_id, network_id)
520

    
521

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

    
557

    
558
@command(api='image')
559
class image_meta(object):
560
    """Get image metadata"""
561
    
562
    def main(self, image_id):
563
        image = self.client.get_meta(image_id)
564
        print_dict(image)
565

    
566

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

    
616

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

    
626

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

    
636

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

    
644

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

    
652

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

    
660

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

    
688

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

    
702

    
703
@command(api='storage')
704
class store_create(_store_account_command):
705
    """Create a container"""
706
    
707
    def main(self, container):
708
        super(store_create, self).main()
709
        self.client.create_container(container)
710

    
711

    
712
@command(api='storage')
713
class store_container(_store_account_command):
714
    """Get container info"""
715
    
716
    def main(self, container):
717
        super(store_container, self).main()
718
        reply = self.client.get_container_meta(container)
719
        print_dict(reply)
720

    
721

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

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

    
759

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

    
785

    
786
@command(api='storage')
787
class store_delete(_store_container_command):
788
    """Delete a file"""
789
    
790
    def main(self, path):
791
        super(store_delete, self).main()
792
        self.client.delete_object(path)
793

    
794

    
795
@command(api='storage')
796
class store_purge(_store_account_command):
797
    """Purge a container"""
798
    
799
    def main(self, container):
800
        super(store_purge, self).main()
801
        self.client.purge_container(container)
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 = Config(args.grouped['--config'].get(0))
875
    else:
876
        config = Config()
877

    
878
    for option in args.grouped.get('-o', []):
879
        keypath, sep, val = option.partition('=')
880
        if not sep:
881
            print "Invalid option '%s'" % option
882
            exit(1)
883
        section, sep, key = keypath.partition('.')
884
        if not sep:
885
            print "Invalid option '%s'" % option
886
            exit(1)
887
        config.override(section.strip(), key.strip(), val.strip())
888
    
889
    apis = set(['config'])
890
    for api in ('compute', 'image', 'storage', 'astakos'):
891
        if config.getboolean(api, 'enable'):
892
            apis.add(api)
893
    if config.getboolean('compute', 'cyclades_extensions'):
894
        apis.add('cyclades')
895
    if config.getboolean('storage', 'pithos_extensions'):
896
        apis.add('pithos')
897
    
898
    # Remove commands that belong to APIs that are not included
899
    for group, group_commands in _commands.items():
900
        for name, cls in group_commands.items():
901
            if cls.api not in apis:
902
                del group_commands[name]
903
        if not group_commands:
904
            del _commands[group]
905
    
906
    if not args.grouped['_']:
907
        parser.print_help()
908
        print_groups()
909
        exit(0)
910
    
911
    group = args.grouped['_'][0]
912
    
913
    if group not in _commands:
914
        parser.print_help()
915
        print_groups()
916
        exit(1)
917
    
918
    parser.usage = '%%prog %s <command> [options]' % group
919
    
920
    if len(args.grouped['_']) == 1:
921
        parser.print_help()
922
        print_commands(group)
923
        exit(0)
924
    
925
    name = args.grouped['_'][1]
926
    
927
    if name not in _commands[group]:
928
        parser.print_help()
929
        print_commands(group)
930
        exit(1)
931
    
932
    cmd = _commands[group][name]()
933
    
934
    syntax = '%s [options]' % cmd.syntax if cmd.syntax else '[options]'
935
    parser.usage = '%%prog %s %s %s' % (group, name, syntax)
936
    parser.description = cmd.description
937
    parser.epilog = ''
938
    if hasattr(cmd, 'update_parser'):
939
        cmd.update_parser(parser)
940
    
941
    options, arguments = parser.parse_args(argv)
942
    
943
    if options.help:
944
        parser.print_help()
945
        exit(0)
946
    
947
    if options.silent:
948
        add_handler('', logging.CRITICAL)
949
    elif options.debug:
950
        add_handler('requests', logging.INFO, prefix='* ')
951
        add_handler('clients.send', logging.DEBUG, prefix='> ')
952
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
953
    elif options.verbose:
954
        add_handler('requests', logging.INFO, prefix='* ')
955
        add_handler('clients.send', logging.INFO, prefix='> ')
956
        add_handler('clients.recv', logging.INFO, prefix='< ')
957
    elif options.include:
958
        add_handler('clients.recv', logging.INFO)
959
    else:
960
        add_handler('', logging.WARNING)
961
    
962
    api = cmd.api
963
    if api in ('compute', 'cyclades'):
964
        url = config.get('compute', 'url')
965
        token = config.get('compute', 'token') or config.get('global', 'token')
966
        if config.getboolean('compute', 'cyclades_extensions'):
967
            cmd.client = clients.cyclades(url, token)
968
        else:
969
            cmd.client = clients.compute(url, token)
970
    elif api in ('storage', 'pithos'):
971
        url = config.get('storage', 'url')
972
        token = config.get('storage', 'token') or config.get('global', 'token')
973
        account = config.get('storage', 'account')
974
        container = config.get('storage', 'container')
975
        if config.getboolean('storage', 'pithos_extensions'):
976
            cmd.client = clients.pithos(url, token, account, container)
977
        else:
978
            cmd.client = clients.storage(url, token, account, container)
979
    elif api == 'image':
980
        url = config.get('image', 'url')
981
        token = config.get('image', 'token') or config.get('global', 'token')
982
        cmd.client = clients.image(url, token)
983
    elif api == 'astakos':
984
        url = config.get('astakos', 'url')
985
        token = config.get('astakos', 'token') or config.get('global', 'token')
986
        cmd.client = clients.astakos(url, token)
987
    
988
    cmd.options = options
989
    cmd.config = config
990
    
991
    try:
992
        ret = cmd.main(*arguments[3:])
993
        exit(ret)
994
    except TypeError as e:
995
        if e.args and e.args[0].startswith('main()'):
996
            parser.print_help()
997
            exit(1)
998
        else:
999
            raise
1000
    except clients.ClientError as err:
1001
        if err.status == 404:
1002
            color = yellow
1003
        elif 500 <= err.status < 600:
1004
            color = magenta
1005
        else:
1006
            color = red
1007
        
1008
        puts_err(color(err.message))
1009
        if err.details and (options.verbose or options.debug):
1010
            puts_err(err.details)
1011
        exit(2)
1012
    except ConnectionError as err:
1013
        puts_err(red("Connection error"))
1014
        exit(1)
1015

    
1016

    
1017
if __name__ == '__main__':
1018
    main()