Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ cae67d7b

History | View | Annotate | Download (27.5 kB)

1
#!/usr/bin/env python
2

    
3
# Copyright 2011 GRNET S.A. All rights reserved.
4
#
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
#
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
#
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
#
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35

    
36
"""
37
To add a command create a new class and add a 'command' decorator. The class
38
must have a 'main' method which will contain the code to be executed.
39
Optionally a command can implement an 'update_parser' class method in order
40
to add command line arguments, or modify the OptionParser in any way.
41

42
The name of the class is important and it will determine the name and grouping
43
of the command. This behavior can be overriden with the 'group' and 'name'
44
decorator arguments:
45

46
    @command(api='compute')
47
    class server_list(object):
48
        # This command will be named 'list' under group 'server'
49
        ...
50

51
    @command(api='compute', name='ls')
52
    class server_list(object):
53
        # This command will be named 'ls' under group 'server'
54
        ...
55

56
The docstring of a command class will be used as the command description in
57
help messages, unless overriden with the 'description' decorator argument.
58

59
The syntax of a command will be generated dynamically based on the signature
60
of the 'main' method, unless overriden with the 'syntax' decorator argument:
61

62
    def main(self, server_id, network=None):
63
        # This syntax of this command will be: '<server id> [network]'
64
        ...
65

66
The order of commands is important, it will be preserved in the help output.
67
"""
68

    
69
import inspect
70
import logging
71
import os
72

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

    
80
from clint.textui import puts, puts_err, indent
81
from clint.textui.cols import columns
82

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

    
87

    
88
# Path to the file that stores the configuration
89
CONFIG_PATH = expanduser('~/.kamakirc')
90

    
91
# Name of a shell variable to bypass the CONFIG_PATH value
92
CONFIG_ENV = 'KAMAKI_CONFIG'
93

    
94

    
95
_commands = OrderedDict()
96

    
97

    
98
GROUPS = {
99
    'config': "Configuration commands",
100
    'server': "Compute API server commands",
101
    'flavor': "Compute API flavor commands",
102
    'image': "Compute API image commands",
103
    'network': "Compute API network commands (Cyclades extension)",
104
    'glance': "Image API commands",
105
    'store': "Storage API commands"}
106

    
107

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

    
144

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

    
160

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

    
172

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

    
183

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

    
194

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

    
207

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

    
216

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

    
259

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

    
267

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

    
275

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

    
287

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

    
295

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

    
303

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

    
312

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

    
320

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

    
330

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

    
339

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

    
348

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

    
358

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

    
366

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

    
375

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

    
388

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

    
397

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

    
410

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

    
419

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

    
427

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

    
436

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

    
445

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

    
455

    
456
@command(api='compute')
457
class image_delmeta(object):
458
    """delete image metadata"""
459
    
460
    def main(self, image_id, key):
461
        self.client.delete_image_metadata(image_id, key)
462

    
463

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

    
476

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

    
485

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

    
494

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

    
502

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

    
510

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

    
518

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

    
526

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

    
562

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

    
571

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

    
613

    
614
@command(api='image')
615
class glance_members(object):
616
    """get image members"""
617
    
618
    def main(self, image_id):
619
        members = self.client.list_members(image_id)
620
        for member in members:
621
            print member['member_id']
622

    
623

    
624
@command(api='image')
625
class glance_shared(object):
626
    """list shared images"""
627
    
628
    def main(self, member):
629
        images = self.client.list_shared(member)
630
        for image in images:
631
            print image['image_id']
632

    
633

    
634
@command(api='image')
635
class glance_addmember(object):
636
    """add a member to an image"""
637
    
638
    def main(self, image_id, member):
639
        self.client.add_member(image_id, member)
640

    
641

    
642
@command(api='image')
643
class glance_delmember(object):
644
    """remove a member from an image"""
645
    
646
    def main(self, image_id, member):
647
        self.client.remove_member(image_id, member)
648

    
649

    
650
@command(api='image')
651
class glance_setmembers(object):
652
    """set the members of an image"""
653
    
654
    def main(self, image_id, *member):
655
        self.client.set_members(image_id, member)
656

    
657

    
658
class store_command(object):
659
    """base class for all store_* commands"""
660
    
661
    def update_parser(cls, parser):
662
        parser.add_option('--account', dest='account', metavar='NAME',
663
                help='use account NAME')
664
        parser.add_option('--container', dest='container', metavar='NAME',
665
                help='use container NAME')
666
    
667
    def main(self):
668
        self.config.override('storage_account', self.options.account)
669
        self.config.override('storage_container', self.options.container)
670
        
671
        # Use the more efficient Pithos client if available
672
        if 'pithos' in self.config.get('apis').split():
673
            self.client = clients.PithosClient(self.config)
674

    
675

    
676
@command(api='storage')
677
class store_create(object):
678
    """create a container"""
679
    
680
    def update_parser(cls, parser):
681
        parser.add_option('--account', dest='account', metavar='ACCOUNT',
682
                help='use account ACCOUNT')
683
    
684
    def main(self, container):
685
        self.config.override('storage_account', self.options.account)
686
        self.client.create_container(container)
687

    
688

    
689
@command(api='storage')
690
class store_container(store_command):
691
    """get container info"""
692
    
693
    def main(self):
694
        store_command.main(self)
695
        reply = self.client.get_container_meta()
696
        print_dict(reply)
697

    
698

    
699
@command(api='storage')
700
class store_upload(store_command):
701
    """upload a file"""
702
    
703
    def main(self, path, remote_path=None):
704
        store_command.main(self)
705
        if remote_path is None:
706
            remote_path = basename(path)
707
        with open(path) as f:
708
            self.client.create_object(remote_path, f)
709

    
710

    
711
@command(api='storage')
712
class store_download(store_command):
713
    """download a file"""
714
    
715
    def main(self, remote_path, local_path):
716
        store_command.main(self)
717
        f = self.client.get_object(remote_path)
718
        out = open(local_path, 'w') if local_path != '-' else stdout
719
        block = 4096
720
        data = f.read(block)
721
        while data:
722
            out.write(data)
723
            data = f.read(block)
724

    
725

    
726
@command(api='storage')
727
class store_delete(store_command):
728
    """delete a file"""
729
    
730
    def main(self, path):
731
        store_command.main(self)
732
        self.client.delete_object(path)
733

    
734

    
735
def print_groups():
736
    puts('\nGroups:')
737
    with indent(2):
738
        for group in _commands:
739
            description = GROUPS.get(group, '')
740
            puts(columns([group, 12], [description, 60]))
741

    
742

    
743
def print_commands(group):
744
    description = GROUPS.get(group, '')
745
    if description:
746
        puts('\n' + description)
747
    
748
    puts('\nCommands:')
749
    with indent(2):
750
        for name, cls in _commands[group].items():
751
            puts(columns([name, 12], [cls.description, 60]))
752

    
753

    
754
def main():
755
    parser = OptionParser(add_help_option=False)
756
    parser.usage = '%prog <group> <command> [options]'
757
    parser.add_option('-h', '--help', dest='help', action='store_true',
758
                      default=False,
759
                      help="Show this help message and exit")
760
    parser.add_option('--config', dest='config', metavar='PATH',
761
                      help="Specify the path to the configuration file")
762
    parser.add_option('-i', '--include', dest='include', action='store_true',
763
                      default=False,
764
                      help="Include protocol headers in the output")
765
    parser.add_option('-s', '--silent', dest='silent', action='store_true',
766
                      default=False,
767
                      help="Silent mode, don't output anything")
768
    parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
769
                      default=False,
770
                      help="Make the operation more talkative")
771
    parser.add_option('-V', '--version', dest='version', action='store_true',
772
                      default=False,
773
                      help="Show version number and quit")
774
    parser.add_option('-o', dest='options', action='append',
775
                      default=[], metavar="KEY=VAL",
776
                      help="Override a config values")
777
    
778
    if args.contains(['-V', '--version']):
779
        import kamaki
780
        print "kamaki %s" % kamaki.__version__
781
        exit(0)
782
    
783
    if args.contains(['-s', '--silent']):
784
        level = logging.CRITICAL
785
    elif args.contains(['-v', '--verbose']):
786
        level = logging.INFO
787
    else:
788
        level = logging.WARNING
789
    
790
    logging.basicConfig(level=level, format='%(message)s')
791
    
792
    if '--config' in args:
793
        config_path = args.grouped['--config'].get(0)
794
    else:
795
        config_path = os.environ.get(CONFIG_ENV, CONFIG_PATH)
796
    
797
    config = Config(config_path)
798
    
799
    for option in args.grouped.get('-o', []):
800
        keypath, sep, val = option.partition('=')
801
        if not sep:
802
            log.error("Invalid option '%s'", option)
803
            exit(1)
804
        section, sep, key = keypath.partition('.')
805
        if not sep:
806
            log.error("Invalid option '%s'", option)
807
            exit(1)
808
        config.override(section.strip(), key.strip(), val.strip())
809
    
810
    apis = set(['config'])
811
    for api in ('compute', 'image', 'storage'):
812
        if config.getboolean(api, 'enable'):
813
            apis.add(api)
814
    if config.getboolean('compute', 'cyclades_extensions'):
815
        apis.add('cyclades')
816
    if config.getboolean('storage', 'pithos_extensions'):
817
        apis.add('pithos')
818
    
819
    # Remove commands that belong to APIs that are not included
820
    for group, group_commands in _commands.items():
821
        for name, cls in group_commands.items():
822
            if cls.api not in apis:
823
                del group_commands[name]
824
        if not group_commands:
825
            del _commands[group]
826
    
827
    if not args.grouped['_']:
828
        parser.print_help()
829
        print_groups()
830
        exit(0)
831
    
832
    group = args.grouped['_'][0]
833
    
834
    if group not in _commands:
835
        parser.print_help()
836
        print_groups()
837
        exit(1)
838
    
839
    parser.usage = '%%prog %s <command> [options]' % group
840
    
841
    if len(args.grouped['_']) == 1:
842
        parser.print_help()
843
        print_commands(group)
844
        exit(0)
845
    
846
    name = args.grouped['_'][1]
847
    
848
    if name not in _commands[group]:
849
        parser.print_help()
850
        print_commands(group)
851
        exit(1)
852
    
853
    cmd = _commands[group][name]()
854
    
855
    syntax = '%s [options]' % cmd.syntax if cmd.syntax else '[options]'
856
    parser.usage = '%%prog %s %s %s' % (group, name, syntax)
857
    parser.description = cmd.description
858
    parser.epilog = ''
859
    if hasattr(cmd, 'update_parser'):
860
        cmd.update_parser(parser)
861
    
862
    if args.contains(['-h', '--help']):
863
        parser.print_help()
864
        exit(0)
865
    
866
    cmd.options, cmd.args = parser.parse_args(argv)
867
    
868
    api = cmd.api
869
    if api == 'config':
870
        cmd.config = config
871
    elif api in ('compute', 'image', 'storage'):
872
        token = config.get(api, 'token') or config.get('gobal', 'token')
873
        url = config.get(api, 'url')
874
        client_cls = getattr(clients, api)
875
        kwargs = dict(base_url=url, token=token)
876
        
877
        # Special cases
878
        if api == 'compute' and config.getboolean(api, 'cyclades_extensions'):
879
            client_cls = clients.cyclades
880
        elif api == 'storage':
881
            kwargs['account'] = config.get(api, 'account')
882
            kwargs['container'] = config.get(api, 'container')
883
            if config.getboolean(api, 'pithos_extensions'):
884
                client_cls = clients.pithos
885
        
886
        cmd.client = client_cls(**kwargs)
887
        
888
    try:
889
        ret = cmd.main(*args.grouped['_'][2:])
890
        exit(ret)
891
    except TypeError as e:
892
        if e.args and e.args[0].startswith('main()'):
893
            parser.print_help()
894
            exit(1)
895
        else:
896
            raise
897
    except clients.ClientError, err:
898
        log.error('%s', err.message)
899
        log.info('%s', err.details)
900
        exit(2)
901

    
902

    
903
if __name__ == '__main__':
904
    main()