Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 9f74ca46

History | View | Annotate | Download (42.6 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_list, print_dict, print_items, format_size
92

    
93
_commands = OrderedDict()
94

    
95

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

    
105
class ProgressBar(IncrementalBar):
106
    suffix = '%(percent)d%% - %(eta)ds'
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

    
120
        short_description, sep, long_description = cls.__doc__.partition('\n')
121
        cls.description = short_description
122
        cls.long_description = long_description or short_description
123

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

    
136
        if cls.group not in _commands:
137
            _commands[cls.group] = OrderedDict()
138
        _commands[cls.group][cls.name] = cls
139
        return cls
140
    return decorator
141

    
142
@command(api='config')
143
class config_list(object):
144
    """List configuration options"""
145

    
146
    def update_parser(self, parser):
147
        parser.add_argument('-a', dest='all', action='store_true',
148
                          default=False, help='include default values')
149

    
150
    def main(self):
151
        include_defaults = self.args.all
152
        for section in sorted(self.config.sections()):
153
            items = self.config.items(section, include_defaults)
154
            for key, val in sorted(items):
155
                print('%s.%s = %s' % (section, key, val))
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
@command(api='config')
169
class config_set(object):
170
    """Set a configuration option"""
171

    
172
    def main(self, option, value):
173
        section, sep, key = option.rpartition('.')
174
        section = section or 'global'
175
        self.config.set(section, key, value)
176
        self.config.write()
177

    
178
@command(api='config')
179
class config_delete(object):
180
    """Delete a configuration option (and use the default value)"""
181

    
182
    def main(self, option):
183
        section, sep, key = option.rpartition('.')
184
        section = section or 'global'
185
        self.config.remove_option(section, key)
186
        self.config.write()
187

    
188
@command(api='compute')
189
class server_list(object):
190
    """List servers"""
191

    
192
    def update_parser(self, parser):
193
        parser.add_argument('-l', dest='detail', action='store_true',
194
                default=False, help='show detailed output')
195

    
196
    def main(self):
197
        servers = self.client.list_servers(self.args.detail)
198
        print_items(servers)
199

    
200
@command(api='compute')
201
class server_info(object):
202
    """Get server details"""
203

    
204
    def main(self, server_id):
205
        try:
206
            server = self.client.get_server_details(int(server_id))
207
        except ValueError:
208
            print(yellow('Server id must be a base10 integer'))
209
            return
210
        print_dict(server)
211

    
212
@command(api='compute')
213
class server_create(object):
214
    """Create a server"""
215

    
216
    def update_parser(self, parser):
217
        parser.add_argument('--personality', dest='personalities',
218
                          action='append', default=[],
219
                          metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
220
                          help='add a personality file')
221

    
222
    def main(self, name, flavor_id, image_id):
223
        personalities = []
224
        for personality in self.args.personalities:
225
            p = personality.split(',')
226
            p.extend([None] * (5 - len(p)))     # Fill missing fields with None
227

    
228
            path = p[0]
229

    
230
            if not path:
231
                print("Invalid personality argument '%s'" % p)
232
                return 1
233
            if not exists(path):
234
                print("File %s does not exist" % path)
235
                return 1
236

    
237
            with open(path) as f:
238
                contents = b64encode(f.read())
239

    
240
            d = {'path': p[1] or abspath(path), 'contents': contents}
241
            if p[2]:
242
                d['owner'] = p[2]
243
            if p[3]:
244
                d['group'] = p[3]
245
            if p[4]:
246
                d['mode'] = int(p[4])
247
            personalities.append(d)
248

    
249
        reply = self.client.create_server(name, int(flavor_id), image_id,
250
                personalities)
251
        print_dict(reply)
252

    
253
@command(api='compute')
254
class server_rename(object):
255
    """Update a server's name"""
256

    
257
    def main(self, server_id, new_name):
258
        try:
259
            self.client.update_server_name(int(server_id), new_name)
260
        except ValueError:
261
            print(yellow('Server id must be a base10 integer'))
262

    
263
@command(api='compute')
264
class server_delete(object):
265
    """Delete a server"""
266

    
267
    def main(self, server_id):
268
        try:
269
            self.client.delete_server(int(server_id))
270
        except ValueError:
271
            print(yellow('Server id must be a base10 integer'))
272

    
273
@command(api='compute')
274
class server_reboot(object):
275
    """Reboot a server"""
276

    
277
    def update_parser(self, parser):
278
        parser.add_argument('-f', dest='hard', action='store_true',
279
                default=False, help='perform a hard reboot')
280

    
281
    def main(self, server_id):
282
        try:
283
            self.client.reboot_server(int(server_id), self.args.hard)
284
        except ValueError:
285
            print(yellow('Server id must be a base10 integer'))
286

    
287
@command(api='cyclades')
288
class server_start(object):
289
    """Start a server"""
290

    
291
    def main(self, server_id):
292
        try:
293
            self.client.start_server(int(server_id))
294
        except ValueError:
295
            print(yellow('Server id must be a base10 integer'))
296

    
297
@command(api='cyclades')
298
class server_shutdown(object):
299
    """Shutdown a server"""
300

    
301
    def main(self, server_id):
302
        try:
303
            self.client.shutdown_server(int(server_id))
304
        except ValueError:
305
            print(yellow('Server id must be a base10 integer'))
306

    
307
@command(api='cyclades')
308
class server_console(object):
309
    """Get a VNC console"""
310

    
311
    def main(self, server_id):
312
        try:
313
            reply = self.client.get_server_console(int(server_id))
314
        except ValueError:
315
            print(yellow('Server id must be a base10 integer'))
316
            return
317
        print_dict(reply)
318

    
319
@command(api='cyclades')
320
class server_firewall(object):
321
    """Set the server's firewall profile"""
322

    
323
    def main(self, server_id, profile):
324
        try:
325
            self.client.set_firewall_profile(int(server_id), profile)
326
        except ValueError:
327
            print(yellow('Server id must be a base10 integer'))
328

    
329
@command(api='cyclades')
330
class server_addr(object):
331
    """List a server's addresses"""
332

    
333
    def main(self, server_id, network=None):
334
        try:
335
            reply = self.client.list_server_nic_details(int(server_id), network)
336
        except ValueError:
337
            print(yellow('Server id must be a base10 integer'))
338
            return
339
        print_list(reply)
340

    
341
@command(api='compute')
342
class server_meta(object):
343
    """Get a server's metadata"""
344

    
345
    def main(self, server_id, key=None):
346
        try:
347
            reply = self.client.get_server_metadata(int(server_id), key)
348
        except ValueError:
349
            print(yellow('Server id must be a base10 integer'))
350
            return
351
        print_dict(reply)
352

    
353
@command(api='compute')
354
class server_addmeta(object):
355
    """Add server metadata"""
356

    
357
    def main(self, server_id, key, val):
358
        try:
359
            reply = self.client.create_server_metadata(int(server_id), key, val)
360
        except ValueError:
361
            print(yellow('Server id must be a base10 integer'))
362
            return
363
        print_dict(reply)
364

    
365
@command(api='compute')
366
class server_setmeta(object):
367
    """Update server's metadata"""
368

    
369
    def main(self, server_id, key, val):
370
        metadata = {key: val}
371
        try:
372
            reply = self.client.update_server_metadata(int(server_id), **metadata)
373
        except ValueError:
374
            print(yellow('Server id must be a base10 integer'))
375
            return
376
        print_dict(reply)
377

    
378
@command(api='compute')
379
class server_delmeta(object):
380
    """Delete server metadata"""
381

    
382
    def main(self, server_id, key):
383
        try:
384
            self.client.delete_server_metadata(int(server_id), key)
385
        except ValueError:
386
            print(yellow('Server id must be a base10 integer'))
387
            return
388

    
389
@command(api='cyclades')
390
class server_stats(object):
391
    """Get server statistics"""
392

    
393
    def main(self, server_id):
394
        try:
395
            reply = self.client.get_server_stats(int(server_id))
396
        except ValueError:
397
            print(yellow('Server id must be a base10 integer'))
398
            return
399
        print_dict(reply, exclude=('serverRef',))
400

    
401
@command(api='compute')
402
class flavor_list(object):
403
    """List flavors"""
404

    
405
    def update_parser(self, parser):
406
        parser.add_argument('-l', dest='detail', action='store_true',
407
                default=False, help='show detailed output')
408

    
409
    def main(self):
410
        flavors = self.client.list_flavors(self.args.detail)
411
        print_items(flavors)
412

    
413
@command(api='compute')
414
class flavor_info(object):
415
    """Get flavor details"""
416

    
417
    def main(self, flavor_id):
418
        try:
419
            flavor = self.client.get_flavor_details(int(flavor_id))
420
        except ValueError:
421
            print(yellow('Flavor id must be a base10 integer'))
422
            return
423
        print_dict(flavor)
424

    
425
@command(api='compute')
426
class image_list(object):
427
    """List images"""
428

    
429
    def update_parser(self, parser):
430
        parser.add_argument('-l', dest='detail', action='store_true',
431
                default=False, help='show detailed output')
432

    
433
    def main(self):
434
        images = self.client.list_images(self.args.detail)
435
        print_items(images)
436

    
437
@command(api='compute')
438
class image_info(object):
439
    """Get image details"""
440

    
441
    def main(self, image_id):
442
        image = self.client.get_image_details(image_id)
443
        print_dict(image)
444

    
445
@command(api='compute')
446
class image_delete(object):
447
    """Delete image"""
448

    
449
    def main(self, image_id):
450
        self.client.delete_image(image_id)
451

    
452
@command(api='compute')
453
class image_properties(object):
454
    """Get image properties"""
455

    
456
    def main(self, image_id, key=None):
457
        reply = self.client.get_image_metadata(image_id, key)
458
        print_dict(reply)
459

    
460
@command(api='compute')
461
class image_addproperty(object):
462
    """Add an image property"""
463

    
464
    def main(self, image_id, key, val):
465
        reply = self.client.create_image_metadata(image_id, key, val)
466
        print_dict(reply)
467

    
468
@command(api='compute')
469
class image_setproperty(object):
470
    """Update an image property"""
471

    
472
    def main(self, image_id, key, val):
473
        metadata = {key: val}
474
        reply = self.client.update_image_metadata(image_id, **metadata)
475
        print_dict(reply)
476

    
477
@command(api='compute')
478
class image_delproperty(object):
479
    """Delete an image property"""
480

    
481
    def main(self, image_id, key):
482
        self.client.delete_image_metadata(image_id, key)
483

    
484
@command(api='cyclades')
485
class network_list(object):
486
    """List networks"""
487

    
488
    def update_parser(self, parser):
489
        parser.add_argument('-l', dest='detail', action='store_true',
490
                default=False, help='show detailed output')
491

    
492
    def main(self):
493
        networks = self.client.list_networks(self.args.detail)
494
        print_items(networks)
495

    
496
@command(api='cyclades')
497
class network_create(object):
498
    """Create a network"""
499

    
500
    def main(self, name):
501
        reply = self.client.create_network(name)
502
        print_dict(reply)
503

    
504
@command(api='cyclades')
505
class network_info(object):
506
    """Get network details"""
507

    
508
    def main(self, network_id):
509
        network = self.client.get_network_details(network_id)
510
        print_dict(network)
511

    
512
@command(api='cyclades')
513
class network_rename(object):
514
    """Update network name"""
515

    
516
    def main(self, network_id, new_name):
517
        self.client.update_network_name(network_id, new_name)
518

    
519
@command(api='cyclades')
520
class network_delete(object):
521
    """Delete a network"""
522

    
523
    def main(self, network_id):
524
        self.client.delete_network(network_id)
525

    
526
@command(api='cyclades')
527
class network_connect(object):
528
    """Connect a server to a network"""
529

    
530
    def main(self, server_id, network_id):
531
        self.client.connect_server(server_id, network_id)
532

    
533
@command(api='cyclades')
534
class network_disconnect(object):
535
    """Disconnect a nic that connects a server to a network"""
536

    
537
    def main(self, nic_id):
538
        try:
539
            server_id = nic_id.split('-')[1]
540
            self.client.disconnect_server(server_id, nic_id)
541
        except IndexError:
542
            print(yellow('nid_id format: nic-<server_id>-<nic_index>'))
543

    
544
@command(api='image')
545
class image_public(object):
546
    """List public images"""
547

    
548
    def update_parser(self, parser):
549
        parser.add_argument('-l', dest='detail', action='store_true',
550
                default=False, help='show detailed output')
551
        parser.add_argument('--container-format', dest='container_format',
552
                metavar='FORMAT', help='filter by container format')
553
        parser.add_argument('--disk-format', dest='disk_format',
554
                metavar='FORMAT', help='filter by disk format')
555
        parser.add_argument('--name', dest='name', metavar='NAME',
556
                help='filter by name')
557
        parser.add_argument('--size-min', dest='size_min', metavar='BYTES',
558
                help='filter by minimum size')
559
        parser.add_argument('--size-max', dest='size_max', metavar='BYTES',
560
                help='filter by maximum size')
561
        parser.add_argument('--status', dest='status', metavar='STATUS',
562
                help='filter by status')
563
        parser.add_argument('--order', dest='order', metavar='FIELD',
564
                help='order by FIELD (use a - prefix to reverse order)')
565

    
566
    def main(self):
567
        filters = {}
568
        for filter in ('container_format', 'disk_format', 'name', 'size_min',
569
                       'size_max', 'status'):
570
            val = getattr(self.args, filter, None)
571
            if val is not None:
572
                filters[filter] = val
573

    
574
        order = self.args.order or ''
575
        images = self.client.list_public(self.args.detail, filters=filters,
576
                                         order=order)
577
        print_items(images, title=('name',))
578

    
579
@command(api='image')
580
class image_meta(object):
581
    """Get image metadata"""
582

    
583
    def main(self, image_id):
584
        image = self.client.get_meta(image_id)
585
        print_dict(image)
586

    
587
@command(api='image')
588
class image_register(object):
589
    """Register an image"""
590

    
591
    def update_parser(self, parser):
592
        parser.add_argument('--checksum', dest='checksum', metavar='CHECKSUM',
593
                help='set image checksum')
594
        parser.add_argument('--container-format', dest='container_format',
595
                metavar='FORMAT', help='set container format')
596
        parser.add_argument('--disk-format', dest='disk_format',
597
                metavar='FORMAT', help='set disk format')
598
        parser.add_argument('--id', dest='id',
599
                metavar='ID', help='set image ID')
600
        parser.add_argument('--owner', dest='owner',
601
                metavar='USER', help='set image owner (admin only)')
602
        parser.add_argument('--property', dest='properties', action='append',
603
                metavar='KEY=VAL',
604
                help='add a property (can be used multiple times)')
605
        parser.add_argument('--public', dest='is_public', action='store_true',
606
                help='mark image as public')
607
        parser.add_argument('--size', dest='size', metavar='SIZE',
608
                help='set image size')
609

    
610
    def main(self, name, location):
611
        if not location.startswith('pithos://'):
612
            account = self.config.get('storage', 'account').split()[0]
613
            if account[-1] == '/':
614
                account = account[:-1]
615
            container = self.config.get('storage', 'container')
616
            location = 'pithos://%s/%s'%(account, location) \
617
                if container is None or len(container) == 0 \
618
                else 'pithos://%s/%s/%s' % (account, container, location)
619

    
620
        params = {}
621
        for key in ('checksum', 'container_format', 'disk_format', 'id',
622
                    'owner', 'size'):
623
            val = getattr(self.args, key)
624
            if val is not None:
625
                params[key] = val
626

    
627
        if self.args.is_public:
628
            params['is_public'] = 'true'
629

    
630
        properties = {}
631
        for property in self.args.properties or []:
632
            key, sep, val = property.partition('=')
633
            if not sep:
634
                print("Invalid property '%s'" % property)
635
                return 1
636
            properties[key.strip()] = val.strip()
637

    
638
        self.client.register(name, location, params, properties)
639

    
640
@command(api='image')
641
class image_members(object):
642
    """Get image members"""
643

    
644
    def main(self, image_id):
645
        members = self.client.list_members(image_id)
646
        for member in members:
647
            print(member['member_id'])
648

    
649
@command(api='image')
650
class image_shared(object):
651
    """List shared images"""
652

    
653
    def main(self, member):
654
        images = self.client.list_shared(member)
655
        for image in images:
656
            print(image['image_id'])
657

    
658
@command(api='image')
659
class image_addmember(object):
660
    """Add a member to an image"""
661

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

    
665
@command(api='image')
666
class image_delmember(object):
667
    """Remove a member from an image"""
668

    
669
    def main(self, image_id, member):
670
        self.client.remove_member(image_id, member)
671

    
672
@command(api='image')
673
class image_setmembers(object):
674
    """Set the members of an image"""
675

    
676
    def main(self, image_id, *member):
677
        self.client.set_members(image_id, member)
678

    
679
class _store_account_command(object):
680
    """Base class for account level storage commands"""
681

    
682
    def update_parser(self, parser):
683
        parser.add_argument('--account', dest='account', metavar='NAME',
684
                          help="Specify an account to use")
685

    
686
    def progress(self, message):
687
        """Return a generator function to be used for progress tracking"""
688

    
689
        MESSAGE_LENGTH = 25
690

    
691
        def progress_gen(n):
692
            msg = message.ljust(MESSAGE_LENGTH)
693
            for i in ProgressBar(msg).iter(range(n)):
694
                yield
695
            yield
696

    
697
        return progress_gen
698

    
699
    def main(self):
700
        if self.args.account is not None:
701
            self.client.account = self.args.account
702

    
703
class _store_container_command(_store_account_command):
704
    """Base class for container level storage commands"""
705

    
706
    def update_parser(self, parser):
707
        super(_store_container_command, self).update_parser(parser)
708
        parser.add_argument('--container', dest='container', metavar='NAME',
709
                          help="Specify a container to use")
710

    
711
    def extract_container_and_path(self, container_with_path):
712
        assert isinstance(container_with_path, str)
713
        cnp = container_with_path.split(':')
714
        self.container = cnp[0]
715
        self.path = cnp[1] if len(cnp) > 1 else None
716
            
717

    
718
    def main(self, container_with_path=None):
719
        super(_store_container_command, self).main()
720
        if container_with_path is not None:
721
            self.extract_container_and_path(container_with_path)
722
            self.client.container = self.container
723
        elif self.args.container is not None:
724
            self.client.container = self.args.container
725
        else:
726
            self.container = None
727

    
728
@command(api='storage')
729
class store_list(_store_container_command):
730
    """List containers, object trees or objects in a directory
731
    """
732

    
733
    def print_objects(self, object_list):
734
        for obj in object_list:
735
            size = format_size(obj['bytes']) if 0 < obj['bytes'] else 'D'
736
            print('%6s %s' % (size, obj['name']))
737

    
738
    def print_containers(self, container_list):
739
        for container in container_list:
740
            size = format_size(container['bytes'])
741
            print('%s (%s, %s objects)' % (container['name'], size, container['count']))
742
            
743
    def main(self, container____path__=None):
744
        super(store_list, self).main(container____path__)
745
        if self.container is None:
746
            reply = self.client.list_containers()
747
            self.print_containers(reply)
748
        else:
749
            reply = self.client.list_objects() if self.path is None \
750
                else self.client.list_objects_in_path(path_prefix=self.path)
751
            self.print_objects(reply)
752

    
753
@command(api='storage')
754
class store_create(_store_container_command):
755
    """Create a container or a directory object"""
756

    
757
    def main(self, container____directory__):
758
        super(store_create, self).main(container____directory__)
759
        if self.path is None:
760
            self.client.create_container(self.container)
761
        else:
762
            self.client.create_directory(self.path)
763

    
764
@command(api='storage')
765
class store_copy(_store_container_command):
766
    """Copy an object"""
767

    
768
    def main(self, source_container___path, destination_container____path__):
769
        super(store_copy, self).main(source_container___path)
770
        dst = destination_container____path__.split(':')
771
        dst_cont = dst[0]
772
        dst_path = dst[1] if len(dst) > 1 else False
773
        self.client.copy_object(src_container = self.container, src_object = self.path, dst_container = dst_cont, dst_object = dst_path)
774

    
775
@command(api='storage')
776
class store_move(_store_container_command):
777
    """Move an object"""
778

    
779
    def main(self, source_container___path, destination_container____path__):
780
        super(store_move, self).main(source_container___path)
781
        dst = destination_container____path__.split(':')
782
        dst_cont = dst[0]
783
        dst_path = dst[1] if len(dst) > 1 else False
784
        self.client.move_object(src_container = self.container, src_object = self.path, dst_container = dst_cont, dst_object = dst_path)
785

    
786
@command(api='storage')
787
class store_append(_store_container_command):
788
    """Append local file to (existing) remote object"""
789

    
790
    def main(self, local_path, container___path):
791
        super(store_append, self).main(container___path)
792
        f = open(local_path, 'r')
793
        upload_cb = self.progress('Appending blocks')
794
        self.client.append_object(object=self.path, source_file = f, upload_cb = upload_cb)
795

    
796
@command(api='storage')
797
class store_truncate(_store_container_command):
798
    """Truncate remote file up to a size"""
799

    
800
    def main(self, container___path, size=0):
801
        super(store_truncate, self).main(container___path)
802
        self.client.truncate_object(self.path, size)
803

    
804
@command(api='storage')
805
class store_overwrite(_store_container_command):
806
    """Overwrite part (from start to end) of a remote file"""
807

    
808
    def main(self, local_path, container___path, start, end):
809
        super(store_overwrite, self).main(container___path)
810
        f = open(local_path, 'r')
811
        upload_cb = self.progress('Overwritting blocks')
812
        self.client.overwrite_object(object=self.path, start=start, end=end, source_file=f, upload_cb = upload_cb)
813

    
814
@command(api='storage')
815
class store_upload(_store_container_command):
816
    """Upload a file"""
817

    
818
    def main(self, local_path, container____path__):
819
        super(store_upload, self).main(container____path__)
820
        remote_path = basename(local_path) if self.path is None else self.path
821
        with open(local_path) as f:
822
            hash_cb = self.progress('Calculating block hashes')
823
            upload_cb = self.progress('Uploading blocks')
824
            self.client.create_object(remote_path, f, hash_cb=hash_cb, upload_cb=upload_cb)
825

    
826
@command(api='storage')
827
class store_download(_store_container_command):
828
    """Download a file"""
829

    
830
    def main(self, container___path, local_path='-'):
831
        super(store_download, self).main(container___path)
832
        f, size = self.client.get_object(self.path)
833
        out = open(local_path, 'w') if local_path != '-' else stdout
834

    
835
        blocksize = 4 * 1024 ** 2
836
        nblocks = 1 + (size - 1) // blocksize
837

    
838
        cb = self.progress('Downloading blocks') if local_path != '-' else None
839
        if cb:
840
            gen = cb(nblocks)
841
            gen.next()
842

    
843
        data = f.read(blocksize)
844
        while data:
845
            out.write(data)
846
            data = f.read(blocksize)
847
            if cb:
848
                gen.next()
849

    
850
@command(api='storage')
851
class store_delete(_store_container_command):
852
    """Delete a container [or an object]"""
853

    
854
    def main(self, container____path__):
855
        super(store_delete, self).main(container____path__)
856
        if self.path is None:
857
            self.client.delete_container(self.container)
858
        else:
859
            self.client.delete_object(self.path)
860

    
861
@command(api='storage')
862
class store_purge(_store_account_command):
863
    """Purge a container"""
864

    
865
    def main(self, container):
866
        super(store_purge, self).main()
867
        self.client.container = container
868
        self.client.purge_container()
869

    
870
@command(api='storage')
871
class store_publish(_store_container_command):
872
    """Publish an object"""
873

    
874
    def main(self, container___path):
875
        super(store_publish, self).main(container___path)
876
        self.client.publish_object(self.path)
877

    
878
@command(api='storage')
879
class store_unpublish(_store_container_command):
880
    """Unpublish an object"""
881

    
882
    def main(self, container___path):
883
        super(store_unpublish, self).main(container___path)
884
        self.client.unpublish_object(self.path)
885

    
886
@command(api='storage')
887
class store_permitions(_store_container_command):
888
    """Get object read/write permitions"""
889

    
890
    def main(self, container___path):
891
        super(store_permitions, self).main(container___path)
892
        reply = self.client.get_object_sharing(self.path)
893
        print_dict(reply)
894

    
895
@command(api='storage')
896
class store_setpermitions(_store_container_command):
897
    """Set sharing permitions"""
898

    
899
    def main(self, container___path, *permitions):
900
        super(store_setpermitions, self).main(container___path)
901
        read = False
902
        write = False
903
        for perms in permitions:
904
            splstr = perms.split('=')
905
            if 'read' == splstr[0]:
906
                read = [user_or_group.strip() for user_or_group in splstr[1].split(',')]
907
            elif 'write' == splstr[0]:
908
                write = [user_or_group.strip() for user_or_group in splstr[1].split(',')]
909
            else:
910
                read = False
911
                write = False
912
        if not read and not write:
913
            print(u'Read/write permitions are given in the following format:')
914
            print(u'\tread=username,groupname,...')
915
            print(u'and/or')
916
            print(u'\twrite=username,groupname,...')
917
            return
918
        self.client.set_object_sharing(self.path, read_permition=read, write_permition=write)
919

    
920
@command(api='storage')
921
class store_delpermitions(_store_container_command):
922
    """Delete all sharing permitions"""
923

    
924
    def main(self, container___path):
925
        super(store_delpermitions, self).main(container___path)
926
        self.client.del_object_sharing(self.path)
927

    
928
@command(api='storage')
929
class store_info(_store_container_command):
930
    """Get information for account [, container [or object]]"""
931

    
932
    def main(self, container____path__=None):
933
        super(store_info, self).main(container____path__)
934
        if self.container is None:
935
            reply = self.client.get_account_info()
936
        elif self.path is None:
937
            reply = self.client.get_container_info(self.container)
938
        else:
939
            reply = self.client.get_object_info(self.path)
940
        print_dict(reply)
941

    
942
@command(api='storage')
943
class store_meta(_store_container_command):
944
    """Get custom meta-content for account [, container [or object]]"""
945

    
946
    def main(self, container____path__ = None):
947
        super(store_meta, self).main(container____path__)
948
        if self.container is None:
949
            reply = self.client.get_account_meta()
950
        elif self.path is None:
951
            reply = self.client.get_container_object_meta(self.container)
952
            print_dict(reply)
953
            reply = self.client.get_container_meta(self.container)
954
        else:
955
            reply = self.client.get_object_meta(self.path)
956
        print_dict(reply)
957

    
958
@command(api='storage')
959
class store_setmeta(_store_container_command):
960
    """Set a new metadatum for account [, container [or object]]"""
961

    
962
    def main(self, metakey, metavalue, container____path__=None):
963
        super(store_setmeta, self).main(container____path__)
964
        if self.container is None:
965
            self.client.set_account_meta({metakey:metavalue})
966
        elif self.path is None:
967
            self.client.set_container_meta({metakey:metavalue})
968
        else:
969
            self.client.set_object_meta(self.path, {metakey:metavalue})
970

    
971
@command(api='storage')
972
class store_delmeta(_store_container_command):
973
    """Delete an existing metadatum of account [, container [or object]]"""
974

    
975
    def main(self, metakey, container____path__=None):
976
        super(store_delmeta, self).main(container____path__)
977
        if self.container is None:
978
            self.client.del_account_meta(metakey)
979
        elif self.path is None:
980
            self.client.del_container_meta(metakey)
981
        else:
982
            self.client.delete_object_meta(metakey, self.path)
983

    
984
@command(api='storage')
985
class store_quota(_store_account_command):
986
    """Get  quota for account [or container]"""
987

    
988
    def main(self, container = None):
989
        super(store_quota, self).main()
990
        if container is None:
991
            reply = self.client.get_account_quota()
992
        else:
993
            reply = self.client.get_container_quota(container)
994
        print_dict(reply)
995

    
996
@command(api='storage')
997
class store_setquota(_store_account_command):
998
    """Set new quota (in KB) for account [or container]"""
999

    
1000
    def main(self, quota, container = None):
1001
        super(store_setquota, self).main()
1002
        if container is None:
1003
            self.client.set_account_quota(quota)
1004
        else:
1005
            self.client.container = container
1006
            self.client.set_container_quota(quota)
1007

    
1008
@command(api='storage')
1009
class store_versioning(_store_account_command):
1010
    """Get  versioning for account [or container ]"""
1011

    
1012
    def main(self, container = None):
1013
        super(store_versioning, self).main()
1014
        if container is None:
1015
            reply = self.client.get_account_versioning()
1016
        else:
1017
            reply = self.client.get_container_versioning(container)
1018
        print_dict(reply)
1019

    
1020
@command(api='storage')
1021
class store_setversioning(_store_account_command):
1022
    """Set new versioning (auto, none) for account [or container]"""
1023

    
1024
    def main(self, versioning, container = None):
1025
        super(store_setversioning, self).main()
1026
        if container is None:
1027
            self.client.set_account_versioning(versioning)
1028
        else:
1029
            self.client.container = container
1030
            self.client.set_container_versioning(versioning)
1031

    
1032
@command(api='storage')
1033
class store_test(_store_account_command):
1034
    """Perform a developer-level custom test"""
1035
    def main(self):
1036
        super(store_test, self).main()
1037
        self.client.container = 'testCo'
1038

    
1039
        r = self.client.object_put('lali', content_length=1, data='a',
1040
            content_type='application/octet-stream', permitions={'read':'u1, u2', 'write':'u2, u3'})
1041
        print(unicode(r))
1042

    
1043
@command(api='storage')
1044
class store_group(_store_account_command):
1045
    """Get user groups details for account"""
1046

    
1047
    def main(self):
1048
        super(store_group, self).main()
1049
        reply = self.client.get_account_group()
1050
        print_dict(reply)
1051

    
1052
@command(api='storage')
1053
class store_setgroup(_store_account_command):
1054
    """Create/update a new user group on account"""
1055

    
1056
    def main(self, groupname, *users):
1057
        super(store_setgroup, self).main()
1058
        self.client.set_account_group(groupname, users)
1059

    
1060
@command(api='storage')
1061
class store_delgroup(_store_account_command):
1062
    """Delete a user group on an account"""
1063

    
1064
    def main(self, groupname):
1065
        super(store_delgroup, self).main()
1066
        self.client.del_account_group(groupname)
1067

    
1068
@command(api='astakos')
1069
class astakos_authenticate(object):
1070
    """Authenticate a user"""
1071

    
1072
    def main(self):
1073
        reply = self.client.authenticate()
1074
        print_dict(reply)
1075

    
1076
def print_groups():
1077
    print('\nGroups:')
1078
    for group in _commands:
1079
        description = GROUPS.get(group, '')
1080
        print(' ', group.ljust(12), description)
1081

    
1082
def print_commands(group):
1083
    description = GROUPS.get(group, '')
1084
    if description:
1085
        print('\n' + description)
1086

    
1087
    print('\nCommands:')
1088
    for name, cls in _commands[group].items():
1089
        print(' ', name.ljust(14), cls.description)
1090

    
1091
def add_handler(name, level, prefix=''):
1092
    h = logging.StreamHandler()
1093
    fmt = logging.Formatter(prefix + '%(message)s')
1094
    h.setFormatter(fmt)
1095
    logger = logging.getLogger(name)
1096
    logger.addHandler(h)
1097
    logger.setLevel(level)
1098

    
1099
def main():
1100
    exe = basename(sys.argv[0])
1101
    parser = ArgumentParser(add_help=False)
1102
    parser.prog = '%s <group> <command>' % exe
1103
    parser.add_argument('-h', '--help', dest='help', action='store_true',
1104
                      default=False,
1105
                      help="Show this help message and exit")
1106
    parser.add_argument('--config', dest='config', metavar='PATH',
1107
                      help="Specify the path to the configuration file")
1108
    parser.add_argument('-d', '--debug', dest='debug', action='store_true',
1109
                      default=False,
1110
                      help="Include debug output")
1111
    parser.add_argument('-i', '--include', dest='include', action='store_true',
1112
                      default=False,
1113
                      help="Include protocol headers in the output")
1114
    parser.add_argument('-s', '--silent', dest='silent', action='store_true',
1115
                      default=False,
1116
                      help="Silent mode, don't output anything")
1117
    parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
1118
                      default=False,
1119
                      help="Make the operation more talkative")
1120
    parser.add_argument('-V', '--version', dest='version', action='store_true',
1121
                      default=False,
1122
                      help="Show version number and quit")
1123
    parser.add_argument('-o', dest='options', action='append',
1124
                      default=[], metavar="KEY=VAL",
1125
                      help="Override a config values")
1126

    
1127
    args, argv = parser.parse_known_args()
1128

    
1129
    if args.version:
1130
        import kamaki
1131
        print("kamaki %s" % kamaki.__version__)
1132
        exit(0)
1133

    
1134
    config = Config(args.config) if args.config else Config()
1135

    
1136
    for option in args.options:
1137
        keypath, sep, val = option.partition('=')
1138
        if not sep:
1139
            print("Invalid option '%s'" % option)
1140
            exit(1)
1141
        section, sep, key = keypath.partition('.')
1142
        if not sep:
1143
            print("Invalid option '%s'" % option)
1144
            exit(1)
1145
        config.override(section.strip(), key.strip(), val.strip())
1146

    
1147
    apis = set(['config'])
1148
    for api in ('compute', 'image', 'storage', 'astakos'):
1149
        if config.getboolean(api, 'enable'):
1150
            apis.add(api)
1151
    if config.getboolean('compute', 'cyclades_extensions'):
1152
        apis.add('cyclades')
1153
    if config.getboolean('storage', 'pithos_extensions'):
1154
        apis.add('pithos')
1155

    
1156
    # Remove commands that belong to APIs that are not included
1157
    for group, group_commands in _commands.items():
1158
        for name, cls in group_commands.items():
1159
            if cls.api not in apis:
1160
                del group_commands[name]
1161
        if not group_commands:
1162
            del _commands[group]
1163

    
1164
    group = argv.pop(0) if argv else None
1165

    
1166
    if not group:
1167
        parser.print_help()
1168
        print_groups()
1169
        exit(0)
1170

    
1171
    if group not in _commands:
1172
        parser.print_help()
1173
        print_groups()
1174
        exit(1)
1175

    
1176
    parser.prog = '%s %s <command>' % (exe, group)
1177
    command = argv.pop(0) if argv else None
1178

    
1179
    if not command:
1180
        parser.print_help()
1181
        print_commands(group)
1182
        exit(0)
1183

    
1184
    if command not in _commands[group]:
1185
        parser.print_help()
1186
        print_commands(group)
1187
        exit(1)
1188

    
1189
    cmd = _commands[group][command]()
1190

    
1191
    parser.prog = '%s %s %s' % (exe, group, command)
1192
    if cmd.syntax:
1193
        parser.prog += '  %s' % cmd.syntax
1194
    parser.description = cmd.description
1195
    parser.epilog = ''
1196
    if hasattr(cmd, 'update_parser'):
1197
        cmd.update_parser(parser)
1198

    
1199
    args, argv = parser.parse_known_args()
1200

    
1201
    if args.help:
1202
        parser.print_help()
1203
        exit(0)
1204

    
1205
    if args.silent:
1206
        add_handler('', logging.CRITICAL)
1207
    elif args.debug:
1208
        add_handler('requests', logging.INFO, prefix='* ')
1209
        add_handler('clients.send', logging.DEBUG, prefix='> ')
1210
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
1211
    elif args.verbose:
1212
        add_handler('requests', logging.INFO, prefix='* ')
1213
        add_handler('clients.send', logging.INFO, prefix='> ')
1214
        add_handler('clients.recv', logging.INFO, prefix='< ')
1215
    elif args.include:
1216
        add_handler('clients.recv', logging.INFO)
1217
    else:
1218
        add_handler('', logging.WARNING)
1219

    
1220
    api = cmd.api
1221
    if api in ('compute', 'cyclades'):
1222
        url = config.get('compute', 'url')
1223
        token = config.get('compute', 'token') or config.get('global', 'token')
1224
        if config.getboolean('compute', 'cyclades_extensions'):
1225
            cmd.client = clients.cyclades(url, token)
1226
        else:
1227
            cmd.client = clients.compute(url, token)
1228
    elif api in ('storage', 'pithos'):
1229
        url = config.get('storage', 'url')
1230
        token = config.get('storage', 'token') or config.get('global', 'token')
1231
        account = config.get('storage', 'account')
1232
        container = config.get('storage', 'container')
1233
        if config.getboolean('storage', 'pithos_extensions'):
1234
            cmd.client = clients.pithos(url, token, account, container)
1235
        else:
1236
            cmd.client = clients.storage(url, token, account, container)
1237
    elif api == 'image':
1238
        url = config.get('image', 'url')
1239
        token = config.get('image', 'token') or config.get('global', 'token')
1240
        cmd.client = clients.image(url, token)
1241
    elif api == 'astakos':
1242
        url = config.get('astakos', 'url')
1243
        token = config.get('astakos', 'token') or config.get('global', 'token')
1244
        cmd.client = clients.astakos(url, token)
1245

    
1246
    cmd.args = args
1247
    cmd.config = config
1248

    
1249
    try:
1250
        ret = cmd.main(*argv[2:])
1251
        exit(ret)
1252
    except TypeError as e:
1253
        if e.args and e.args[0].startswith('main()'):
1254
            parser.print_help()
1255
            exit(1)
1256
        else:
1257
            raise
1258
    except clients.ClientError as err:
1259
        if err.status == 404:
1260
            message = yellow(err.message)
1261
        elif 500 <= err.status < 600:
1262
            message = magenta(err.message)
1263
        else:
1264
            message = red(err.message)
1265

    
1266
        print(message, file=stderr)
1267
        if err.details and (args.verbose or args.debug):
1268
            print(err.details, file=stderr)
1269
        exit(2)
1270
    except ConnectionError as err:
1271
        print(red("Connection error"), file=stderr)
1272
        exit(1)
1273

    
1274
if __name__ == '__main__':
1275
    main()