Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 454799cd

History | View | Annotate | Download (33.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 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
try:
81
    from collections import OrderedDict
82
except ImportError:
83
    from ordereddict import OrderedDict
84

    
85
from colors import magenta, red, yellow
86
from progress.bar import IncrementalBar
87
from requests.exceptions import ConnectionError
88

    
89
from . import clients
90
from .config import Config
91
from .utils import print_addresses, print_dict, print_items, format_size, dict_from_args
92

    
93

    
94
_commands = OrderedDict()
95

    
96

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

    
106

    
107
class ProgressBar(IncrementalBar):
108
    suffix = '%(percent)d%% - %(eta)ds'
109

    
110

    
111
def command(api=None, group=None, name=None, syntax=None):
112
    """Class decorator that registers a class as a CLI command."""
113

    
114
    def decorator(cls):
115
        grp, sep, cmd = cls.__name__.partition('_')
116
        if not sep:
117
            grp, cmd = None, cls.__name__
118

    
119
        cls.api = api
120
        cls.group = group or grp
121
        cls.name = name or cmd
122

    
123
        short_description, sep, long_description = cls.__doc__.partition('\n')
124
        cls.description = short_description
125
        cls.long_description = long_description or short_description
126

    
127
        cls.syntax = syntax
128
        if cls.syntax is None:
129
            # Generate a syntax string based on main's arguments
130
            spec = inspect.getargspec(cls.main.im_func)
131
            args = spec.args[1:]
132
            n = len(args) - len(spec.defaults or ())
133
            required = ' '.join('<%s>' % x.replace('_', ' ') for x in args[:n])
134
            optional = ' '.join('[%s]' % x.replace('_', ' ') for x in args[n:])
135
            cls.syntax = ' '.join(x for x in [required, optional] if x)
136
            if spec.varargs:
137
                cls.syntax += ' <%s ...>' % spec.varargs
138

    
139
        if cls.group not in _commands:
140
            _commands[cls.group] = OrderedDict()
141
        _commands[cls.group][cls.name] = cls
142
        return cls
143
    return decorator
144

    
145

    
146
@command(api='config')
147
class config_list(object):
148
    """List configuration options"""
149

    
150
    def update_parser(self, parser):
151
        parser.add_argument('-a', dest='all', action='store_true',
152
                          default=False, help='include default values')
153

    
154
    def main(self):
155
        include_defaults = self.args.all
156
        for section in sorted(self.config.sections()):
157
            items = self.config.items(section, include_defaults)
158
            for key, val in sorted(items):
159
                print('%s.%s = %s' % (section, key, val))
160

    
161

    
162
@command(api='config')
163
class config_get(object):
164
    """Show a configuration option"""
165

    
166
    def main(self, option):
167
        section, sep, key = option.rpartition('.')
168
        section = section or 'global'
169
        value = self.config.get(section, key)
170
        if value is not None:
171
            print(value)
172

    
173

    
174
@command(api='config')
175
class config_set(object):
176
    """Set a configuration option"""
177

    
178
    def main(self, option, value):
179
        section, sep, key = option.rpartition('.')
180
        section = section or 'global'
181
        self.config.set(section, key, value)
182
        self.config.write()
183

    
184

    
185
@command(api='config')
186
class config_delete(object):
187
    """Delete a configuration option (and use the default value)"""
188

    
189
    def main(self, option):
190
        section, sep, key = option.rpartition('.')
191
        section = section or 'global'
192
        self.config.remove_option(section, key)
193
        self.config.write()
194

    
195

    
196
@command(api='compute')
197
class server_list(object):
198
    """List servers"""
199

    
200
    def update_parser(self, parser):
201
        parser.add_argument('-l', dest='detail', action='store_true',
202
                default=False, help='show detailed output')
203

    
204
    def main(self):
205
        servers = self.client.list_servers(self.args.detail)
206
        print_items(servers)
207

    
208

    
209
@command(api='compute')
210
class server_info(object):
211
    """Get server details"""
212

    
213
    def main(self, server_id):
214
        server = self.client.get_server_details(int(server_id))
215
        print_dict(server)
216

    
217

    
218
@command(api='compute')
219
class server_create(object):
220
    """Create a server"""
221

    
222
    def update_parser(self, parser):
223
        parser.add_argument('--personality', dest='personalities',
224
                          action='append', default=[],
225
                          metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
226
                          help='add a personality file')
227

    
228
    def main(self, name, flavor_id, image_id):
229
        personalities = []
230
        for personality in self.args.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
            d = {'path': p[1] or abspath(path), 'contents': contents}
247
            if p[2]:
248
                d['owner'] = p[2]
249
            if p[3]:
250
                d['group'] = p[3]
251
            if p[4]:
252
                d['mode'] = int(p[4])
253
            personalities.append(d)
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 a server's 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 a 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 a server"""
279

    
280
    def update_parser(self, parser):
281
        parser.add_argument('-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.args.hard)
286

    
287

    
288
@command(api='cyclades')
289
class server_start(object):
290
    """Start a 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 a 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 server's 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 a server's 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 a server's 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's 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(self, parser):
381
        parser.add_argument('-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.args.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(self, parser):
403
        parser.add_argument('-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.args.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_properties(object):
430
    """Get image properties"""
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_addproperty(object):
439
    """Add an image property"""
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_setproperty(object):
448
    """Update an image property"""
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_delproperty(object):
458
    """Delete an image property"""
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(self, parser):
469
        parser.add_argument('-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.args.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 image_public(object):
529
    """List public images"""
530

    
531
    def update_parser(self, parser):
532
        parser.add_argument('-l', dest='detail', action='store_true',
533
                default=False, help='show detailed output')
534
        parser.add_argument('--container-format', dest='container_format',
535
                metavar='FORMAT', help='filter by container format')
536
        parser.add_argument('--disk-format', dest='disk_format',
537
                metavar='FORMAT', help='filter by disk format')
538
        parser.add_argument('--name', dest='name', metavar='NAME',
539
                help='filter by name')
540
        parser.add_argument('--size-min', dest='size_min', metavar='BYTES',
541
                help='filter by minimum size')
542
        parser.add_argument('--size-max', dest='size_max', metavar='BYTES',
543
                help='filter by maximum size')
544
        parser.add_argument('--status', dest='status', metavar='STATUS',
545
                help='filter by status')
546
        parser.add_argument('--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.args, filter, None)
554
            if val is not None:
555
                filters[filter] = val
556

    
557
        order = self.args.order or ''
558
        images = self.client.list_public(self.args.detail, filters=filters,
559
                                         order=order)
560
        print_items(images, title=('name',))
561

    
562

    
563
@command(api='image')
564
class image_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 image_register(object):
574
    """Register an image"""
575

    
576
    def update_parser(self, parser):
577
        parser.add_argument('--checksum', dest='checksum', metavar='CHECKSUM',
578
                help='set image checksum')
579
        parser.add_argument('--container-format', dest='container_format',
580
                metavar='FORMAT', help='set container format')
581
        parser.add_argument('--disk-format', dest='disk_format',
582
                metavar='FORMAT', help='set disk format')
583
        parser.add_argument('--id', dest='id',
584
                metavar='ID', help='set image ID')
585
        parser.add_argument('--owner', dest='owner',
586
                metavar='USER', help='set image owner (admin only)')
587
        parser.add_argument('--property', dest='properties', action='append',
588
                metavar='KEY=VAL',
589
                help='add a property (can be used multiple times)')
590
        parser.add_argument('--public', dest='is_public', action='store_true',
591
                help='mark image as public')
592
        parser.add_argument('--size', dest='size', metavar='SIZE',
593
                help='set image size')
594

    
595
    def main(self, name, location):
596
        if not location.startswith('pithos://'):
597
            account = self.config.get('storage', 'account')
598
            container = self.config.get('storage', 'container')
599
            location = 'pithos://%s/%s/%s' % (account, container, location)
600

    
601
        params = {}
602
        for key in ('checksum', 'container_format', 'disk_format', 'id',
603
                    'owner', 'size'):
604
            val = getattr(self.args, key)
605
            if val is not None:
606
                params[key] = val
607

    
608
        if self.args.is_public:
609
            params['is_public'] = 'true'
610

    
611
        properties = {}
612
        for property in self.args.properties or []:
613
            key, sep, val = property.partition('=')
614
            if not sep:
615
                print("Invalid property '%s'" % property)
616
                return 1
617
            properties[key.strip()] = val.strip()
618

    
619
        self.client.register(name, location, params, properties)
620

    
621

    
622
@command(api='image')
623
class image_members(object):
624
    """Get image members"""
625

    
626
    def main(self, image_id):
627
        members = self.client.list_members(image_id)
628
        for member in members:
629
            print(member['member_id'])
630

    
631

    
632
@command(api='image')
633
class image_shared(object):
634
    """List shared images"""
635

    
636
    def main(self, member):
637
        images = self.client.list_shared(member)
638
        for image in images:
639
            print(image['image_id'])
640

    
641

    
642
@command(api='image')
643
class image_addmember(object):
644
    """Add a member to an image"""
645

    
646
    def main(self, image_id, member):
647
        self.client.add_member(image_id, member)
648

    
649

    
650
@command(api='image')
651
class image_delmember(object):
652
    """Remove a member from an image"""
653

    
654
    def main(self, image_id, member):
655
        self.client.remove_member(image_id, member)
656

    
657

    
658
@command(api='image')
659
class image_setmembers(object):
660
    """Set the members of an image"""
661

    
662
    def main(self, image_id, *member):
663
        self.client.set_members(image_id, member)
664

    
665

    
666
class _store_account_command(object):
667
    """Base class for account level storage commands"""
668

    
669
    def update_parser(self, parser):
670
        parser.add_argument('--account', dest='account', metavar='NAME',
671
                          help="Specify an account to use")
672

    
673
    def progress(self, message):
674
        """Return a generator function to be used for progress tracking"""
675

    
676
        MESSAGE_LENGTH = 25
677

    
678
        def progress_gen(n):
679
            msg = message.ljust(MESSAGE_LENGTH)
680
            for i in ProgressBar(msg).iter(range(n)):
681
                yield
682
            yield
683

    
684
        return progress_gen
685

    
686
    def main(self):
687
        if self.args.account is not None:
688
            self.client.account = self.args.account
689

    
690

    
691
class _store_container_command(_store_account_command):
692
    """Base class for container level storage commands"""
693

    
694
    def update_parser(self, parser):
695
        super(_store_container_command, self).update_parser(parser)
696
        parser.add_argument('--container', dest='container', metavar='NAME',
697
                          help="Specify a container to use")
698

    
699
    def main(self):
700
        super(_store_container_command, self).main()
701
        if self.args.container is not None:
702
            self.client.container = self.args.container
703

    
704
@command(api='storage')
705
class store_info(_store_account_command):
706
    """Get information for account [, container [or object]]"""
707

    
708
    def main(self, container=None, object=None):
709
        super(store_info, self).main()
710
        if container is None:
711
            reply = self.client.get_account_info()
712
        elif object is None:
713
            reply = self.client.get_container_info(container)
714
        else:
715
            self.client.container = container
716
            reply = self.client.get_object_info(object)
717
        print_dict(reply)
718

    
719
@command(api='storage')
720
class store_meta(_store_account_command):
721
    """Get custom meta-content for account [, container [or object]]"""
722

    
723
    def main(self, container = None, object = None):
724
        super(store_meta, self).main()
725
        if container is None:
726
            reply = self.client.get_account_meta()
727
        elif object is None:
728
            reply = self.client.get_container_object_meta(container)
729
            print_dict(reply)
730
            reply = self.client.get_container_meta(container)
731
        else:
732
            self.client.container = container
733
            reply = self.client.get_object_meta(object)
734
        print_dict(reply)
735

    
736
@command(api='storage')
737
class store_setmeta(_store_account_command):
738
    """Set a new metadatum for account [, container [or object]]"""
739

    
740
    def main(self, metakey, metavalue, container=None, object=None):
741
        super(store_setmeta, self).main()
742
        if container is None:
743
            self.client.set_account_meta({metakey:metavalue})
744
        else:
745
            self.client.container = container
746
            if object is None:
747
                self.client.set_container_meta({metakey:metavalue})
748
            else:
749
                self.client.set_object_meta(object, {metakey:metavalue})
750

    
751
@command(api='storage')
752
class store_policy(_store_account_command):
753
    """Get  policy for account [, container [or object]]"""
754

    
755
    def main(self, container = None):
756
        super(store_policy, self).main()
757
        if container is None:
758
            reply = self.client.get_account_policy()
759
        else:
760
            reply = self.client.get_container_policy(container)
761
        print_dict(reply)
762

    
763
@command(api='storage')
764
class store_create(_store_account_command):
765
    """Create a container [or a directory object]"""
766

    
767
    def main(self, container, directory_object=None):
768
        super(store_create, self).main()
769
        if directory_object is None:
770
            self.client.create_container(container)
771
        else:
772
            self.client.container = container
773
            self.client.create_directory(directory_object)
774

    
775
@command(api='storage')
776
class store_list(_store_container_command):
777
    """List containers [, object trees [or objects in a directory]]"""
778

    
779
    def print_objects(self, object_list):
780
        for obj in object_list:
781
            size = format_size(obj['bytes']) if 0 < obj['bytes'] else 'D'
782
            print('%6s %s' % (size, obj['name']))
783

    
784
    def print_containers(self, container_list):
785
        for container in container_list:
786
            size = format_size(container['bytes'])
787
            print('%s (%s, %s objects)' % (container['name'], size, container['count']))
788
            
789

    
790
    def main(self, container=None, object=None):
791
        super(store_list, self).main()
792
        if container is None:
793
            reply = self.client.list_containers()
794
            self.print_containers(reply)
795
        else:
796
            self.client.container = container
797
            reply = self.client.list_objects() if object is None \
798
                else self.client.list_objects_in_path(path_prefix=object)
799
            self.print_objects(reply)
800

    
801
@command(api='storage')
802
class store_upload(_store_container_command):
803
    """Upload a file"""
804

    
805
    def main(self, container, path, remote_path=None):
806
        super(store_upload, self).main()
807

    
808
        self.client.container = container
809
        if remote_path is None:
810
            remote_path = basename(path)
811
        with open(path) as f:
812
            hash_cb = self.progress('Calculating block hashes')
813
            upload_cb = self.progress('Uploading blocks')
814
            self.client.create_object(remote_path, f, hash_cb=hash_cb,
815
                                      upload_cb=upload_cb)
816

    
817
@command(api='storage')
818
class store_download(_store_container_command):
819
    """Download a file"""
820

    
821
    def main(self, container,remote_path, local_path='-'):
822
        super(store_download, self).main()
823

    
824
        self.client.container = container
825
        f, size = self.client.get_object(remote_path)
826
        out = open(local_path, 'w') if local_path != '-' else stdout
827

    
828
        blocksize = 4 * 1024 ** 2
829
        nblocks = 1 + (size - 1) // blocksize
830

    
831
        cb = self.progress('Downloading blocks') if local_path != '-' else None
832
        if cb:
833
            gen = cb(nblocks)
834
            gen.next()
835

    
836
        data = f.read(blocksize)
837
        while data:
838
            out.write(data)
839
            data = f.read(blocksize)
840
            if cb:
841
                gen.next()
842

    
843
@command(api='storage')
844
class store_delete(_store_container_command):
845
    """Delete a container [or an object]"""
846

    
847
    def main(self, container, object=None):
848
        super(store_delete, self).main()
849
        if object is None:
850
            self.client.delete_container(container)
851
        else:
852
            self.client.container = container
853
            self.client.delete_object(object)
854

    
855
@command(api='storage')
856
class store_purge(_store_account_command):
857
    """Purge a container"""
858

    
859
    def main(self, container):
860
        super(store_purge, self).main()
861
        self.client.purge_container(container)
862

    
863
@command(api='astakos')
864
class astakos_authenticate(object):
865
    """Authenticate a user"""
866

    
867
    def main(self):
868
        reply = self.client.authenticate()
869
        print_dict(reply)
870

    
871

    
872
def print_groups():
873
    print('\nGroups:')
874
    for group in _commands:
875
        description = GROUPS.get(group, '')
876
        print(' ', group.ljust(12), description)
877

    
878

    
879
def print_commands(group):
880
    description = GROUPS.get(group, '')
881
    if description:
882
        print('\n' + description)
883

    
884
    print('\nCommands:')
885
    for name, cls in _commands[group].items():
886
        print(' ', name.ljust(14), cls.description)
887

    
888

    
889
def add_handler(name, level, prefix=''):
890
    h = logging.StreamHandler()
891
    fmt = logging.Formatter(prefix + '%(message)s')
892
    h.setFormatter(fmt)
893
    logger = logging.getLogger(name)
894
    logger.addHandler(h)
895
    logger.setLevel(level)
896

    
897

    
898
def main():
899
    exe = basename(sys.argv[0])
900
    parser = ArgumentParser(add_help=False)
901
    parser.prog = '%s <group> <command>' % exe
902
    parser.add_argument('-h', '--help', dest='help', action='store_true',
903
                      default=False,
904
                      help="Show this help message and exit")
905
    parser.add_argument('--config', dest='config', metavar='PATH',
906
                      help="Specify the path to the configuration file")
907
    parser.add_argument('-d', '--debug', dest='debug', action='store_true',
908
                      default=False,
909
                      help="Include debug output")
910
    parser.add_argument('-i', '--include', dest='include', action='store_true',
911
                      default=False,
912
                      help="Include protocol headers in the output")
913
    parser.add_argument('-s', '--silent', dest='silent', action='store_true',
914
                      default=False,
915
                      help="Silent mode, don't output anything")
916
    parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
917
                      default=False,
918
                      help="Make the operation more talkative")
919
    parser.add_argument('-V', '--version', dest='version', action='store_true',
920
                      default=False,
921
                      help="Show version number and quit")
922
    parser.add_argument('-o', dest='options', action='append',
923
                      default=[], metavar="KEY=VAL",
924
                      help="Override a config values")
925

    
926
    args, argv = parser.parse_known_args()
927

    
928
    if args.version:
929
        import kamaki
930
        print("kamaki %s" % kamaki.__version__)
931
        exit(0)
932

    
933
    config = Config(args.config) if args.config else Config()
934

    
935
    for option in args.options:
936
        keypath, sep, val = option.partition('=')
937
        if not sep:
938
            print("Invalid option '%s'" % option)
939
            exit(1)
940
        section, sep, key = keypath.partition('.')
941
        if not sep:
942
            print("Invalid option '%s'" % option)
943
            exit(1)
944
        config.override(section.strip(), key.strip(), val.strip())
945

    
946
    apis = set(['config'])
947
    for api in ('compute', 'image', 'storage', 'astakos'):
948
        if config.getboolean(api, 'enable'):
949
            apis.add(api)
950
    if config.getboolean('compute', 'cyclades_extensions'):
951
        apis.add('cyclades')
952
    if config.getboolean('storage', 'pithos_extensions'):
953
        apis.add('pithos')
954

    
955
    # Remove commands that belong to APIs that are not included
956
    for group, group_commands in _commands.items():
957
        for name, cls in group_commands.items():
958
            if cls.api not in apis:
959
                del group_commands[name]
960
        if not group_commands:
961
            del _commands[group]
962

    
963
    group = argv.pop(0) if argv else None
964

    
965
    if not group:
966
        parser.print_help()
967
        print_groups()
968
        exit(0)
969

    
970
    if group not in _commands:
971
        parser.print_help()
972
        print_groups()
973
        exit(1)
974

    
975
    parser.prog = '%s %s <command>' % (exe, group)
976
    command = argv.pop(0) if argv else None
977

    
978
    if not command:
979
        parser.print_help()
980
        print_commands(group)
981
        exit(0)
982

    
983
    if command not in _commands[group]:
984
        parser.print_help()
985
        print_commands(group)
986
        exit(1)
987

    
988
    cmd = _commands[group][command]()
989

    
990
    parser.prog = '%s %s %s' % (exe, group, command)
991
    if cmd.syntax:
992
        parser.prog += '  %s' % cmd.syntax
993
    parser.description = cmd.description
994
    parser.epilog = ''
995
    if hasattr(cmd, 'update_parser'):
996
        cmd.update_parser(parser)
997

    
998
    args, argv = parser.parse_known_args()
999

    
1000
    if args.help:
1001
        parser.print_help()
1002
        exit(0)
1003

    
1004
    if args.silent:
1005
        add_handler('', logging.CRITICAL)
1006
    elif args.debug:
1007
        add_handler('requests', logging.INFO, prefix='* ')
1008
        add_handler('clients.send', logging.DEBUG, prefix='> ')
1009
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
1010
    elif args.verbose:
1011
        add_handler('requests', logging.INFO, prefix='* ')
1012
        add_handler('clients.send', logging.INFO, prefix='> ')
1013
        add_handler('clients.recv', logging.INFO, prefix='< ')
1014
    elif args.include:
1015
        add_handler('clients.recv', logging.INFO)
1016
    else:
1017
        add_handler('', logging.WARNING)
1018

    
1019
    api = cmd.api
1020
    if api in ('compute', 'cyclades'):
1021
        url = config.get('compute', 'url')
1022
        token = config.get('compute', 'token') or config.get('global', 'token')
1023
        if config.getboolean('compute', 'cyclades_extensions'):
1024
            cmd.client = clients.cyclades(url, token)
1025
        else:
1026
            cmd.client = clients.compute(url, token)
1027
    elif api in ('storage', 'pithos'):
1028
        url = config.get('storage', 'url')
1029
        token = config.get('storage', 'token') or config.get('global', 'token')
1030
        account = config.get('storage', 'account')
1031
        container = config.get('storage', 'container')
1032
        if config.getboolean('storage', 'pithos_extensions'):
1033
            cmd.client = clients.pithos(url, token, account, container)
1034
        else:
1035
            cmd.client = clients.storage(url, token, account, container)
1036
    elif api == 'image':
1037
        url = config.get('image', 'url')
1038
        token = config.get('image', 'token') or config.get('global', 'token')
1039
        cmd.client = clients.image(url, token)
1040
    elif api == 'astakos':
1041
        url = config.get('astakos', 'url')
1042
        token = config.get('astakos', 'token') or config.get('global', 'token')
1043
        cmd.client = clients.astakos(url, token)
1044

    
1045
    cmd.args = args
1046
    cmd.config = config
1047

    
1048
    try:
1049
        ret = cmd.main(*argv[2:])
1050
        exit(ret)
1051
    except TypeError as e:
1052
        if e.args and e.args[0].startswith('main()'):
1053
            parser.print_help()
1054
            exit(1)
1055
        else:
1056
            raise
1057
    except clients.ClientError as err:
1058
        if err.status == 404:
1059
            message = yellow(err.message)
1060
        elif 500 <= err.status < 600:
1061
            message = magenta(err.message)
1062
        else:
1063
            message = red(err.message)
1064

    
1065
        print(message, file=stderr)
1066
        if err.details and (args.verbose or args.debug):
1067
            print(err.details, file=stderr)
1068
        exit(2)
1069
    except ConnectionError as err:
1070
        print(red("Connection error"), file=stderr)
1071
        exit(1)
1072

    
1073

    
1074
if __name__ == '__main__':
1075
    main()