Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 863dd770

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
from __future__ import print_function
70

    
71
import inspect
72
import logging
73
import os
74

    
75
from base64 import b64encode
76
from grp import getgrgid
77
from optparse import OptionParser
78
from os.path import abspath, basename, exists
79
from pwd import getpwuid
80
from sys import argv, exit, stdout, stderr
81

    
82
from clint import args
83
from colors import magenta, red, yellow
84
from progress.bar import IncrementalBar
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
class ProgressBar(IncrementalBar):
106
    suffix = '%(percent)d%% - %(eta)ds'
107

    
108

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

    
143

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

    
159

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

    
171

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

    
182

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

    
193

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

    
206

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

    
215

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

    
258

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

    
266

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

    
274

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

    
286

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

    
294

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

    
302

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

    
311

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

    
319

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

    
329

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

    
338

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

    
347

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

    
357

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

    
365

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

    
374

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

    
387

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

    
396

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

    
409

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

    
418

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

    
426

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

    
435

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

    
444

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

    
454

    
455
@command(api='compute')
456
class image_delproperty(object):
457
    """Delete an image property"""
458
    
459
    def main(self, image_id, key):
460
        self.client.delete_image_metadata(image_id, key)
461

    
462

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

    
475

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

    
484

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

    
493

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

    
501

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

    
509

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

    
517

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

    
525

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

    
561

    
562
@command(api='image')
563
class image_meta(object):
564
    """Get image metadata"""
565
    
566
    def main(self, image_id):
567
        image = self.client.get_meta(image_id)
568
        print_dict(image)
569

    
570

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

    
620

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

    
630

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

    
640

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

    
648

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

    
656

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

    
664

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

    
689

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

    
703

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

    
712

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

    
722

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

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

    
760

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

    
786

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

    
795

    
796
@command(api='storage')
797
class store_purge(_store_account_command):
798
    """Purge a container"""
799
    
800
    def main(self, container):
801
        super(store_purge, self).main()
802
        self.client.purge_container(container)
803

    
804

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

    
813

    
814
def print_groups():
815
    print('\nGroups:')
816
    for group in _commands:
817
        description = GROUPS.get(group, '')
818
        print(' ', group.ljust(12), description)
819

    
820

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

    
830

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

    
839

    
840
def main():
841
    parser = OptionParser(add_help_option=False)
842
    parser.usage = '%prog <group> <command> [options]'
843
    parser.add_option('-h', '--help', dest='help', action='store_true',
844
                      default=False,
845
                      help="Show this help message and exit")
846
    parser.add_option('--config', dest='config', metavar='PATH',
847
                      help="Specify the path to the configuration file")
848
    parser.add_option('-d', '--debug', dest='debug', action='store_true',
849
                      default=False,
850
                      help="Include debug output")
851
    parser.add_option('-i', '--include', dest='include', action='store_true',
852
                      default=False,
853
                      help="Include protocol headers in the output")
854
    parser.add_option('-s', '--silent', dest='silent', action='store_true',
855
                      default=False,
856
                      help="Silent mode, don't output anything")
857
    parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
858
                      default=False,
859
                      help="Make the operation more talkative")
860
    parser.add_option('-V', '--version', dest='version', action='store_true',
861
                      default=False,
862
                      help="Show version number and quit")
863
    parser.add_option('-o', dest='options', action='append',
864
                      default=[], metavar="KEY=VAL",
865
                      help="Override a config values")
866
    
867
    if args.contains(['-V', '--version']):
868
        import kamaki
869
        print("kamaki %s" % kamaki.__version__)
870
        exit(0)
871
    
872
    if '--config' in args:
873
        config = Config(args.grouped['--config'].get(0))
874
    else:
875
        config = Config()
876

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

    
1015

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