Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ f6d137ea

History | View | Annotate | Download (30.8 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 sys
74

    
75
from argparse import ArgumentParser
76
from base64 import b64encode
77
from os.path import abspath, basename, exists
78
from sys import exit, stdout, stderr
79

    
80
from colors import magenta, red, yellow
81
from progress.bar import IncrementalBar
82
from requests.exceptions import ConnectionError
83

    
84
from kamaki import clients
85
from kamaki.config import Config
86
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items
87

    
88

    
89
_commands = OrderedDict()
90

    
91

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

    
101

    
102
class ProgressBar(IncrementalBar):
103
    suffix = '%(percent)d%% - %(eta)ds'
104

    
105

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

    
140

    
141
@command(api='config')
142
class config_list(object):
143
    """List configuration options"""
144
    
145
    def update_parser(self, parser):
146
        parser.add_argument('-a', dest='all', action='store_true',
147
                          default=False, help='include default values')
148

    
149
    def main(self):
150
        include_defaults = self.args.all
151
        for section in sorted(self.config.sections()):
152
            items = self.config.items(section, include_defaults)
153
            for key, val in sorted(items):
154
                print('%s.%s = %s' % (section, key, val))
155

    
156

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

    
168

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

    
179

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

    
190

    
191
@command(api='compute')
192
class server_list(object):
193
    """List servers"""
194
    
195
    def update_parser(self, parser):
196
        parser.add_argument('-l', dest='detail', action='store_true',
197
                default=False, help='show detailed output')
198

    
199
    def main(self):
200
        servers = self.client.list_servers(self.args.detail)
201
        print_items(servers)
202

    
203

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

    
212

    
213
@command(api='compute')
214
class server_create(object):
215
    """Create a server"""
216
    
217
    def update_parser(self, parser):
218
        parser.add_argument('--personality', dest='personalities',
219
                          action='append', default=[],
220
                          metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
221
                          help='add a personality file')
222

    
223
    def main(self, name, flavor_id, image_id):
224
        personalities = []
225
        for personality in self.args.personalities:
226
            p = personality.split(',')
227
            p.extend([None] * (5 - len(p)))     # Fill missing fields with None
228
            
229
            path = p[0]
230
            
231
            if not path:
232
                print("Invalid personality argument '%s'" % p)
233
                return 1
234
            if not exists(path):
235
                print("File %s does not exist" % path)
236
                return 1
237
            
238
            with open(path) as f:
239
                contents = b64encode(f.read())
240

    
241
            d = {'path': p[1] or abspath(path), 'contents': contents}
242
            if p[2]:
243
                d['owner'] = p[2]
244
            if p[3]:
245
                d['group'] = p[3]
246
            if p[4]:
247
                d['mode'] = int(p[4])
248
            personalities.append(d)
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_argument('-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.args.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_argument('-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.args.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_argument('-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.args.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_argument('-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.args.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_argument('-l', dest='detail', action='store_true',
528
                default=False, help='show detailed output')
529
        parser.add_argument('--container-format', dest='container_format',
530
                metavar='FORMAT', help='filter by container format')
531
        parser.add_argument('--disk-format', dest='disk_format',
532
                metavar='FORMAT', help='filter by disk format')
533
        parser.add_argument('--name', dest='name', metavar='NAME',
534
                help='filter by name')
535
        parser.add_argument('--size-min', dest='size_min', metavar='BYTES',
536
                help='filter by minimum size')
537
        parser.add_argument('--size-max', dest='size_max', metavar='BYTES',
538
                help='filter by maximum size')
539
        parser.add_argument('--status', dest='status', metavar='STATUS',
540
                help='filter by status')
541
        parser.add_argument('--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.args, filter, None)
549
            if val is not None:
550
                filters[filter] = val
551
        
552
        order = self.args.order or ''
553
        images = self.client.list_public(self.args.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_argument('--checksum', dest='checksum', metavar='CHECKSUM',
573
                help='set image checksum')
574
        parser.add_argument('--container-format', dest='container_format',
575
                metavar='FORMAT', help='set container format')
576
        parser.add_argument('--disk-format', dest='disk_format',
577
                metavar='FORMAT', help='set disk format')
578
        parser.add_argument('--id', dest='id',
579
                metavar='ID', help='set image ID')
580
        parser.add_argument('--owner', dest='owner',
581
                metavar='USER', help='set image owner (admin only)')
582
        parser.add_argument('--property', dest='properties', action='append',
583
                metavar='KEY=VAL',
584
                help='add a property (can be used multiple times)')
585
        parser.add_argument('--public', dest='is_public', action='store_true',
586
                help='mark image as public')
587
        parser.add_argument('--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.args, key)
600
            if val is not None:
601
                params[key] = val
602
        
603
        if self.args.is_public:
604
            params['is_public'] = 'true'
605
        
606
        properties = {}
607
        for property in self.args.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_argument('--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
        
673
        def progress_gen(n):
674
            msg = message.ljust(MESSAGE_LENGTH)
675
            for i in ProgressBar(msg).iter(range(n)):
676
                yield
677
            yield
678
        
679
        return progress_gen
680
    
681
    def main(self):
682
        if self.args.account is not None:
683
            self.client.account = self.args.account
684

    
685

    
686
class _store_container_command(_store_account_command):
687
    """Base class for container level storage commands"""
688
    
689
    def update_parser(self, parser):
690
        super(_store_container_command, self).update_parser(parser)
691
        parser.add_argument('--container', dest='container', metavar='NAME',
692
                          help="Specify a container to use")
693

    
694
    def main(self):
695
        super(_store_container_command, self).main()
696
        if self.args.container is not None:
697
            self.client.container = self.args.container
698

    
699

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

    
708

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

    
718

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

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

    
756

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

    
782

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

    
791

    
792
@command(api='storage')
793
class store_purge(_store_account_command):
794
    """Purge a container"""
795
    
796
    def main(self, container):
797
        super(store_purge, self).main()
798
        self.client.purge_container(container)
799

    
800

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

    
809

    
810
def print_groups():
811
    print('\nGroups:')
812
    for group in _commands:
813
        description = GROUPS.get(group, '')
814
        print(' ', group.ljust(12), description)
815

    
816

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

    
826

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

    
835

    
836
def main():
837
    exe = basename(sys.argv[0])
838
    parser = ArgumentParser(add_help=False)
839
    parser.prog = '%s <group> <command>' % exe
840
    parser.add_argument('-h', '--help', dest='help', action='store_true',
841
                      default=False,
842
                      help="Show this help message and exit")
843
    parser.add_argument('--config', dest='config', metavar='PATH',
844
                      help="Specify the path to the configuration file")
845
    parser.add_argument('-d', '--debug', dest='debug', action='store_true',
846
                      default=False,
847
                      help="Include debug output")
848
    parser.add_argument('-i', '--include', dest='include', action='store_true',
849
                      default=False,
850
                      help="Include protocol headers in the output")
851
    parser.add_argument('-s', '--silent', dest='silent', action='store_true',
852
                      default=False,
853
                      help="Silent mode, don't output anything")
854
    parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
855
                      default=False,
856
                      help="Make the operation more talkative")
857
    parser.add_argument('-V', '--version', dest='version', action='store_true',
858
                      default=False,
859
                      help="Show version number and quit")
860
    parser.add_argument('-o', dest='options', action='append',
861
                      default=[], metavar="KEY=VAL",
862
                      help="Override a config values")
863

    
864
    args, argv = parser.parse_known_args()
865

    
866
    if args.version:
867
        import kamaki
868
        print("kamaki %s" % kamaki.__version__)
869
        exit(0)
870

    
871
    config = Config(args.config) if args.config else Config()
872

    
873
    for option in args.options:
874
        keypath, sep, val = option.partition('=')
875
        if not sep:
876
            print("Invalid option '%s'" % option)
877
            exit(1)
878
        section, sep, key = keypath.partition('.')
879
        if not sep:
880
            print("Invalid option '%s'" % option)
881
            exit(1)
882
        config.override(section.strip(), key.strip(), val.strip())
883
    
884
    apis = set(['config'])
885
    for api in ('compute', 'image', 'storage', 'astakos'):
886
        if config.getboolean(api, 'enable'):
887
            apis.add(api)
888
    if config.getboolean('compute', 'cyclades_extensions'):
889
        apis.add('cyclades')
890
    if config.getboolean('storage', 'pithos_extensions'):
891
        apis.add('pithos')
892
    
893
    # Remove commands that belong to APIs that are not included
894
    for group, group_commands in _commands.items():
895
        for name, cls in group_commands.items():
896
            if cls.api not in apis:
897
                del group_commands[name]
898
        if not group_commands:
899
            del _commands[group]
900

    
901
    group = argv.pop(0) if argv else None
902

    
903
    if not group:
904
        parser.print_help()
905
        print_groups()
906
        exit(0)
907

    
908
    if group not in _commands:
909
        parser.print_help()
910
        print_groups()
911
        exit(1)
912

    
913
    parser.prog = '%s %s <command>' % (exe, group)
914
    command = argv.pop(0) if argv else None
915

    
916
    if not command:
917
        parser.print_help()
918
        print_commands(group)
919
        exit(0)
920

    
921
    if command not in _commands[group]:
922
        parser.print_help()
923
        print_commands(group)
924
        exit(1)
925
    
926
    cmd = _commands[group][command]()
927

    
928
    parser.prog = '%s %s %s' % (exe, group, command)
929
    if cmd.syntax:
930
        parser.prog += '  %s' % cmd.syntax
931
    parser.description = cmd.description
932
    parser.epilog = ''
933
    if hasattr(cmd, 'update_parser'):
934
        cmd.update_parser(parser)
935
    
936
    args, argv = parser.parse_known_args()
937
    
938
    if args.help:
939
        parser.print_help()
940
        exit(0)
941
    
942
    if args.silent:
943
        add_handler('', logging.CRITICAL)
944
    elif args.debug:
945
        add_handler('requests', logging.INFO, prefix='* ')
946
        add_handler('clients.send', logging.DEBUG, prefix='> ')
947
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
948
    elif args.verbose:
949
        add_handler('requests', logging.INFO, prefix='* ')
950
        add_handler('clients.send', logging.INFO, prefix='> ')
951
        add_handler('clients.recv', logging.INFO, prefix='< ')
952
    elif args.include:
953
        add_handler('clients.recv', logging.INFO)
954
    else:
955
        add_handler('', logging.WARNING)
956
    
957
    api = cmd.api
958
    if api in ('compute', 'cyclades'):
959
        url = config.get('compute', 'url')
960
        token = config.get('compute', 'token') or config.get('global', 'token')
961
        if config.getboolean('compute', 'cyclades_extensions'):
962
            cmd.client = clients.cyclades(url, token)
963
        else:
964
            cmd.client = clients.compute(url, token)
965
    elif api in ('storage', 'pithos'):
966
        url = config.get('storage', 'url')
967
        token = config.get('storage', 'token') or config.get('global', 'token')
968
        account = config.get('storage', 'account')
969
        container = config.get('storage', 'container')
970
        if config.getboolean('storage', 'pithos_extensions'):
971
            cmd.client = clients.pithos(url, token, account, container)
972
        else:
973
            cmd.client = clients.storage(url, token, account, container)
974
    elif api == 'image':
975
        url = config.get('image', 'url')
976
        token = config.get('image', 'token') or config.get('global', 'token')
977
        cmd.client = clients.image(url, token)
978
    elif api == 'astakos':
979
        url = config.get('astakos', 'url')
980
        token = config.get('astakos', 'token') or config.get('global', 'token')
981
        cmd.client = clients.astakos(url, token)
982
    
983
    cmd.args = args
984
    cmd.config = config
985
    
986
    try:
987
        ret = cmd.main(*argv[2:])
988
        exit(ret)
989
    except TypeError as e:
990
        if e.args and e.args[0].startswith('main()'):
991
            parser.print_help()
992
            exit(1)
993
        else:
994
            raise
995
    except clients.ClientError as err:
996
        if err.status == 404:
997
            message = yellow(err.message)
998
        elif 500 <= err.status < 600:
999
            message = magenta(err.message)
1000
        else:
1001
            message = red(err.message)
1002
        
1003
        print(message, file=stderr)
1004
        if err.details and (args.verbose or args.debug):
1005
            print(err.details, file=stderr)
1006
        exit(2)
1007
    except ConnectionError as err:
1008
        print(red("Connection error"), file=stderr)
1009
        exit(1)
1010

    
1011

    
1012
if __name__ == '__main__':
1013
    main()