Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli.py @ 16ce7b91

History | View | Annotate | Download (42.8 kB)

1
#!/usr/bin/env python
2

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

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

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

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

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

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

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

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

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

    
69
from __future__ import print_function
70

    
71
import inspect
72
import logging
73
import sys
74

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

    
80
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.del_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
        DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
1038
            "%A, %d-%b-%y %H:%M:%S GMT",
1039
            "%a, %d %b %Y %H:%M:%S GMT"]
1040
        import time, datetime
1041
        t = datetime.datetime.utcnow()
1042
        ts = t.strftime(self.client.DATE_FORMATS[0])
1043
        p = t - datetime.timedelta(minutes=15000000)
1044
        past = p.strftime(self.client.DATE_FORMATS[0])
1045
        self.client.container = 'testCo'
1046
        self.client.container_head(until=100000000)
1047

    
1048
@command(api='storage')
1049
class store_group(_store_account_command):
1050
    """Get user groups details for account"""
1051

    
1052
    def main(self):
1053
        super(store_group, self).main()
1054
        reply = self.client.get_account_group()
1055
        print_dict(reply)
1056

    
1057
@command(api='storage')
1058
class store_setgroup(_store_account_command):
1059
    """Create/update a new user group on account"""
1060

    
1061
    def main(self, groupname, *users):
1062
        super(store_setgroup, self).main()
1063
        self.client.set_account_group(groupname, users)
1064

    
1065
@command(api='storage')
1066
class store_delgroup(_store_account_command):
1067
    """Delete a user group on an account"""
1068

    
1069
    def main(self, groupname):
1070
        super(store_delgroup, self).main()
1071
        self.client.del_account_group(groupname)
1072

    
1073
@command(api='astakos')
1074
class astakos_authenticate(object):
1075
    """Authenticate a user"""
1076

    
1077
    def main(self):
1078
        reply = self.client.authenticate()
1079
        print_dict(reply)
1080

    
1081
def print_groups():
1082
    print('\nGroups:')
1083
    for group in _commands:
1084
        description = GROUPS.get(group, '')
1085
        print(' ', group.ljust(12), description)
1086

    
1087
def print_commands(group):
1088
    description = GROUPS.get(group, '')
1089
    if description:
1090
        print('\n' + description)
1091

    
1092
    print('\nCommands:')
1093
    for name, cls in _commands[group].items():
1094
        print(' ', name.ljust(14), cls.description)
1095

    
1096
def add_handler(name, level, prefix=''):
1097
    h = logging.StreamHandler()
1098
    fmt = logging.Formatter(prefix + '%(message)s')
1099
    h.setFormatter(fmt)
1100
    logger = logging.getLogger(name)
1101
    logger.addHandler(h)
1102
    logger.setLevel(level)
1103

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

    
1132
    args, argv = parser.parse_known_args()
1133

    
1134
    if args.version:
1135
        import kamaki
1136
        print("kamaki %s" % kamaki.__version__)
1137
        exit(0)
1138

    
1139
    config = Config(args.config) if args.config else Config()
1140

    
1141
    for option in args.options:
1142
        keypath, sep, val = option.partition('=')
1143
        if not sep:
1144
            print("Invalid option '%s'" % option)
1145
            exit(1)
1146
        section, sep, key = keypath.partition('.')
1147
        if not sep:
1148
            print("Invalid option '%s'" % option)
1149
            exit(1)
1150
        config.override(section.strip(), key.strip(), val.strip())
1151

    
1152
    apis = set(['config'])
1153
    for api in ('compute', 'image', 'storage', 'astakos'):
1154
        if config.getboolean(api, 'enable'):
1155
            apis.add(api)
1156
    if config.getboolean('compute', 'cyclades_extensions'):
1157
        apis.add('cyclades')
1158
    if config.getboolean('storage', 'pithos_extensions'):
1159
        apis.add('pithos')
1160

    
1161
    # Remove commands that belong to APIs that are not included
1162
    for group, group_commands in _commands.items():
1163
        for name, cls in group_commands.items():
1164
            if cls.api not in apis:
1165
                del group_commands[name]
1166
        if not group_commands:
1167
            del _commands[group]
1168

    
1169
    group = argv.pop(0) if argv else None
1170

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

    
1176
    if group not in _commands:
1177
        parser.print_help()
1178
        print_groups()
1179
        exit(1)
1180

    
1181
    parser.prog = '%s %s <command>' % (exe, group)
1182
    command = argv.pop(0) if argv else None
1183

    
1184
    if not command:
1185
        parser.print_help()
1186
        print_commands(group)
1187
        exit(0)
1188

    
1189
    if command not in _commands[group]:
1190
        parser.print_help()
1191
        print_commands(group)
1192
        exit(1)
1193

    
1194
    cmd = _commands[group][command]()
1195

    
1196
    parser.prog = '%s %s %s' % (exe, group, command)
1197
    if cmd.syntax:
1198
        parser.prog += '  %s' % cmd.syntax
1199
    parser.description = cmd.description
1200
    parser.epilog = ''
1201
    if hasattr(cmd, 'update_parser'):
1202
        cmd.update_parser(parser)
1203

    
1204
    args, argv = parser.parse_known_args()
1205

    
1206
    if args.help:
1207
        parser.print_help()
1208
        exit(0)
1209

    
1210
    if args.silent:
1211
        add_handler('', logging.CRITICAL)
1212
    elif args.debug:
1213
        add_handler('requests', logging.INFO, prefix='* ')
1214
        add_handler('clients.send', logging.DEBUG, prefix='> ')
1215
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
1216
    elif args.verbose:
1217
        add_handler('requests', logging.INFO, prefix='* ')
1218
        add_handler('clients.send', logging.INFO, prefix='> ')
1219
        add_handler('clients.recv', logging.INFO, prefix='< ')
1220
    elif args.include:
1221
        add_handler('clients.recv', logging.INFO)
1222
    else:
1223
        add_handler('', logging.WARNING)
1224

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

    
1251
    cmd.args = args
1252
    cmd.config = config
1253

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

    
1271
        print(message, file=stderr)
1272
        if err.details and (args.verbose or args.debug):
1273
            print(err.details, file=stderr)
1274
        exit(2)
1275
    except ConnectionError as err:
1276
        print(red("Connection error"), file=stderr)
1277
        exit(1)
1278

    
1279
if __name__ == '__main__':
1280
    main()