08c6fca45164784504e3239fa0bd9b2206edd7b1
[kamaki] / kamaki / cli.py
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')
613             container = self.config.get('storage', 'container')
614             location = 'pithos://%s/%s/%s' % (account, container, location)
615
616         params = {}
617         for key in ('checksum', 'container_format', 'disk_format', 'id',
618                     'owner', 'size'):
619             val = getattr(self.args, key)
620             if val is not None:
621                 params[key] = val
622
623         if self.args.is_public:
624             params['is_public'] = 'true'
625
626         properties = {}
627         for property in self.args.properties or []:
628             key, sep, val = property.partition('=')
629             if not sep:
630                 print("Invalid property '%s'" % property)
631                 return 1
632             properties[key.strip()] = val.strip()
633
634         self.client.register(name, location, params, properties)
635
636 @command(api='image')
637 class image_members(object):
638     """Get image members"""
639
640     def main(self, image_id):
641         members = self.client.list_members(image_id)
642         for member in members:
643             print(member['member_id'])
644
645 @command(api='image')
646 class image_shared(object):
647     """List shared images"""
648
649     def main(self, member):
650         images = self.client.list_shared(member)
651         for image in images:
652             print(image['image_id'])
653
654 @command(api='image')
655 class image_addmember(object):
656     """Add a member to an image"""
657
658     def main(self, image_id, member):
659         self.client.add_member(image_id, member)
660
661 @command(api='image')
662 class image_delmember(object):
663     """Remove a member from an image"""
664
665     def main(self, image_id, member):
666         self.client.remove_member(image_id, member)
667
668 @command(api='image')
669 class image_setmembers(object):
670     """Set the members of an image"""
671
672     def main(self, image_id, *member):
673         self.client.set_members(image_id, member)
674
675 class _store_account_command(object):
676     """Base class for account level storage commands"""
677
678     def update_parser(self, parser):
679         parser.add_argument('--account', dest='account', metavar='NAME',
680                           help="Specify an account to use")
681
682     def progress(self, message):
683         """Return a generator function to be used for progress tracking"""
684
685         MESSAGE_LENGTH = 25
686
687         def progress_gen(n):
688             msg = message.ljust(MESSAGE_LENGTH)
689             for i in ProgressBar(msg).iter(range(n)):
690                 yield
691             yield
692
693         return progress_gen
694
695     def main(self):
696         if self.args.account is not None:
697             self.client.account = self.args.account
698
699 class _store_container_command(_store_account_command):
700     """Base class for container level storage commands"""
701
702     def update_parser(self, parser):
703         super(_store_container_command, self).update_parser(parser)
704         parser.add_argument('--container', dest='container', metavar='NAME',
705                           help="Specify a container to use")
706
707     def extract_container_and_path(self, container_with_path):
708         assert isinstance(container_with_path, str)
709         cnp = container_with_path.split(':')
710         self.container = cnp[0]
711         self.path = cnp[1] if len(cnp) > 1 else None
712             
713
714     def main(self, container_with_path=None):
715         super(_store_container_command, self).main()
716         if container_with_path is not None:
717             self.extract_container_and_path(container_with_path)
718             self.client.container = self.container
719         elif self.args.container is not None:
720             self.client.container = self.args.container
721         else:
722             self.container = None
723
724 @command(api='storage')
725 class store_list(_store_container_command):
726     """List containers, object trees or objects in a directory
727     """
728
729     def print_objects(self, object_list):
730         for obj in object_list:
731             size = format_size(obj['bytes']) if 0 < obj['bytes'] else 'D'
732             print('%6s %s' % (size, obj['name']))
733
734     def print_containers(self, container_list):
735         for container in container_list:
736             size = format_size(container['bytes'])
737             print('%s (%s, %s objects)' % (container['name'], size, container['count']))
738             
739     def main(self, container____path__=None):
740         super(store_list, self).main(container____path__)
741         if self.container is None:
742             reply = self.client.list_containers()
743             self.print_containers(reply)
744         else:
745             reply = self.client.list_objects() if self.path is None \
746                 else self.client.list_objects_in_path(path_prefix=self.path)
747             self.print_objects(reply)
748
749 @command(api='storage')
750 class store_create(_store_container_command):
751     """Create a container or a directory object"""
752
753     def main(self, container____directory__):
754         super(store_create, self).main(container____directory__)
755         if self.path is None:
756             self.client.create_container(self.container)
757         else:
758             self.client.create_directory(self.path)
759
760 @command(api='storage')
761 class store_copy(_store_container_command):
762     """Copy an object"""
763
764     def main(self, source_container___path, destination_container____path__):
765         super(store_copy, self).main(source_container___path)
766         dst = destination_container____path__.split(':')
767         dst_cont = dst[0]
768         dst_path = dst[1] if len(dst) > 1 else False
769         self.client.copy_object(src_container = self.container, src_object = self.path, dst_container = dst_cont, dst_object = dst_path)
770
771 @command(api='storage')
772 class store_move(_store_container_command):
773     """Move an object"""
774
775     def main(self, source_container___path, destination_container____path__):
776         super(store_move, self).main(source_container___path)
777         dst = destination_container____path__.split(':')
778         dst_cont = dst[0]
779         dst_path = dst[1] if len(dst) > 1 else False
780         self.client.move_object(src_container = self.container, src_object = self.path, dst_container = dst_cont, dst_object = dst_path)
781
782 @command(api='storage')
783 class store_append(_store_container_command):
784     """Append local file to (existing) remote object"""
785
786     def main(self, local_path, container___path):
787         super(store_append, self).main(container___path)
788         f = open(local_path, 'r')
789         upload_cb = self.progress('Appending blocks')
790         self.client.append_object(object=self.path, source_file = f, upload_cb = upload_cb)
791
792 @command(api='storage')
793 class store_truncate(_store_container_command):
794     """Truncate remote file up to a size"""
795
796     def main(self, container___path, size=0):
797         super(store_truncate, self).main(container___path)
798         self.client.truncate_object(self.path, size)
799
800 @command(api='storage')
801 class store_overwrite(_store_container_command):
802     """Overwrite part (from start to end) of a remote file"""
803
804     def main(self, local_path, container___path, start, end):
805         super(store_overwrite, self).main(container___path)
806         f = open(local_path, 'r')
807         upload_cb = self.progress('Overwritting blocks')
808         self.client.overwrite_object(object=self.path, start=start, end=end, source_file=f, upload_cb = upload_cb)
809
810 @command(api='storage')
811 class store_upload(_store_container_command):
812     """Upload a file"""
813
814     def main(self, local_path, container____path__):
815         super(store_upload, self).main(container____path__)
816         remote_path = basename(local_path) if self.path is None else self.path
817         with open(local_path) as f:
818             hash_cb = self.progress('Calculating block hashes')
819             upload_cb = self.progress('Uploading blocks')
820             self.client.create_object(remote_path, f, hash_cb=hash_cb, upload_cb=upload_cb)
821
822 @command(api='storage')
823 class store_download(_store_container_command):
824     """Download a file"""
825
826     def main(self, container___path, local_path='-'):
827         super(store_download, self).main(container___path)
828         f, size = self.client.get_object(self.path)
829         out = open(local_path, 'w') if local_path != '-' else stdout
830
831         blocksize = 4 * 1024 ** 2
832         nblocks = 1 + (size - 1) // blocksize
833
834         cb = self.progress('Downloading blocks') if local_path != '-' else None
835         if cb:
836             gen = cb(nblocks)
837             gen.next()
838
839         data = f.read(blocksize)
840         while data:
841             out.write(data)
842             data = f.read(blocksize)
843             if cb:
844                 gen.next()
845
846 @command(api='storage')
847 class store_delete(_store_container_command):
848     """Delete a container [or an object]"""
849
850     def main(self, container____path__):
851         super(store_delete, self).main(container____path__)
852         if object is None:
853             self.client.delete_container(self.container)
854         else:
855             self.client.delete_object(self.path)
856
857 @command(api='storage')
858 class store_purge(_store_account_command):
859     """Purge a container"""
860
861     def main(self, container):
862         super(store_purge, self).main()
863         self.client.container = container
864         self.client.purge_container()
865
866 @command(api='storage')
867 class store_publish(_store_container_command):
868     """Publish an object"""
869
870     def main(self, container___path):
871         super(store_publish, self).main(container___path)
872         self.client.publish_object(self.path)
873
874 @command(api='storage')
875 class store_unpublish(_store_container_command):
876     """Unpublish an object"""
877
878     def main(self, container___path):
879         super(store_unpublish, self).main(container___path)
880         self.client.unpublish_object(self.path)
881
882 @command(api='storage')
883 class store_permitions(_store_container_command):
884     """Get object read/write permitions"""
885
886     def main(self, container___path):
887         super(store_permitions, self).main(container___path)
888         reply = self.client.get_object_sharing(self.path)
889         print_dict(reply)
890
891 @command(api='storage')
892 class store_setpermitions(_store_container_command):
893     """Set sharing permitions"""
894
895     def main(self, container___path, *permitions):
896         super(store_setpermitions, self).main(container___path)
897         read = False
898         write = False
899         for perms in permitions:
900             splstr = perms.split('=')
901             if 'read' == splstr[0]:
902                 read = [user_or_group.strip() for user_or_group in splstr[1].split(',')]
903             elif 'write' == splstr[0]:
904                 write = [user_or_group.strip() for user_or_group in splstr[1].split(',')]
905             else:
906                 read = False
907                 write = False
908         if not read and not write:
909             print(u'Read/write permitions are given in the following format:')
910             print(u'\tread=username,groupname,...')
911             print(u'and/or')
912             print(u'\twrite=username,groupname,...')
913             return
914         self.client.set_object_sharing(self.path, read_permition=read, write_permition=write)
915
916 @command(api='storage')
917 class store_delpermitions(_store_container_command):
918     """Delete all sharing permitions"""
919
920     def main(self, container___path):
921         super(store_delpermitions, self).main(container___path)
922         self.client.del_object_sharing(self.path)
923
924 @command(api='storage')
925 class store_info(_store_container_command):
926     """Get information for account [, container [or object]]"""
927
928     def main(self, container____path__=None):
929         super(store_info, self).main(container____path__)
930         if self.container is None:
931             reply = self.client.get_account_info()
932         elif self.path is None:
933             reply = self.client.get_container_info(self.container)
934         else:
935             reply = self.client.get_object_info(self.path)
936         print_dict(reply)
937
938 @command(api='storage')
939 class store_meta(_store_container_command):
940     """Get custom meta-content for account [, container [or object]]"""
941
942     def main(self, container____path__ = None):
943         super(store_meta, self).main(container____path__)
944         if self.container is None:
945             reply = self.client.get_account_meta()
946         elif self.path is None:
947             reply = self.client.get_container_object_meta(self.container)
948             print_dict(reply)
949             reply = self.client.get_container_meta(self.container)
950         else:
951             reply = self.client.get_object_meta(self.path)
952         print_dict(reply)
953
954 @command(api='storage')
955 class store_setmeta(_store_container_command):
956     """Set a new metadatum for account [, container [or object]]"""
957
958     def main(self, metakey, metavalue, container____path__=None):
959         super(store_setmeta, self).main(container____path__)
960         if self.container is None:
961             self.client.set_account_meta({metakey:metavalue})
962         elif self.path is None:
963             self.client.set_container_meta({metakey:metavalue})
964         else:
965             self.client.set_object_meta(self.path, {metakey:metavalue})
966
967 @command(api='storage')
968 class store_delmeta(_store_container_command):
969     """Delete an existing metadatum of account [, container [or object]]"""
970
971     def main(self, metakey, container____path__=None):
972         super(store_delmeta, self).main(container____path__)
973         if self.container is None:
974             self.client.delete_account_meta(metakey)
975         elif self.path is None:
976             self.client.delete_container_meta(metakey)
977         else:
978             self.client.delete_object_meta(metakey, self.path)
979
980 @command(api='storage')
981 class store_quota(_store_account_command):
982     """Get  quota for account [or container]"""
983
984     def main(self, container = None):
985         super(store_quota, self).main()
986         if container is None:
987             reply = self.client.get_account_quota()
988         else:
989             reply = self.client.get_container_quota(container)
990         print_dict(reply)
991
992 @command(api='storage')
993 class store_setquota(_store_account_command):
994     """Set new quota (in KB) for account [or container]"""
995
996     def main(self, quota, container = None):
997         super(store_setquota, self).main()
998         if container is None:
999             self.client.set_account_quota(quota)
1000         else:
1001             self.client.container = container
1002             self.client.set_container_quota(quota)
1003
1004 @command(api='storage')
1005 class store_versioning(_store_account_command):
1006     """Get  versioning for account [or container ]"""
1007
1008     def main(self, container = None):
1009         super(store_versioning, self).main()
1010         if container is None:
1011             reply = self.client.get_account_versioning()
1012         else:
1013             reply = self.client.get_container_versioning(container)
1014         print_dict(reply)
1015
1016 @command(api='storage')
1017 class store_setversioning(_store_account_command):
1018     """Set new versioning (auto, none) for account [or container]"""
1019
1020     def main(self, versioning, container = None):
1021         super(store_setversioning, self).main()
1022         if container is None:
1023             self.client.set_account_versioning(versioning)
1024         else:
1025             self.client.container = container
1026             self.client.set_container_versioning(versioning)
1027
1028 @command(api='storage')
1029 class store_group(_store_account_command):
1030     """Get user groups details for account"""
1031
1032     def main(self):
1033         super(store_group, self).main()
1034         reply = self.client.get_account_group()
1035         print_dict(reply)
1036
1037 @command(api='storage')
1038 class store_setgroup(_store_account_command):
1039     """Create/update a new user group on account"""
1040
1041     def main(self, groupname, *users):
1042         super(store_setgroup, self).main()
1043         self.client.set_account_group(groupname, users)
1044
1045 @command(api='storage')
1046 class store_delgroup(_store_account_command):
1047     """Delete a user group on an account"""
1048
1049     def main(self, groupname):
1050         super(store_delgroup, self).main()
1051         self.client.del_account_group(groupname)
1052
1053 @command(api='astakos')
1054 class astakos_authenticate(object):
1055     """Authenticate a user"""
1056
1057     def main(self):
1058         reply = self.client.authenticate()
1059         print_dict(reply)
1060
1061 def print_groups():
1062     print('\nGroups:')
1063     for group in _commands:
1064         description = GROUPS.get(group, '')
1065         print(' ', group.ljust(12), description)
1066
1067 def print_commands(group):
1068     description = GROUPS.get(group, '')
1069     if description:
1070         print('\n' + description)
1071
1072     print('\nCommands:')
1073     for name, cls in _commands[group].items():
1074         print(' ', name.ljust(14), cls.description)
1075
1076 def add_handler(name, level, prefix=''):
1077     h = logging.StreamHandler()
1078     fmt = logging.Formatter(prefix + '%(message)s')
1079     h.setFormatter(fmt)
1080     logger = logging.getLogger(name)
1081     logger.addHandler(h)
1082     logger.setLevel(level)
1083
1084 def main():
1085     exe = basename(sys.argv[0])
1086     parser = ArgumentParser(add_help=False)
1087     parser.prog = '%s <group> <command>' % exe
1088     parser.add_argument('-h', '--help', dest='help', action='store_true',
1089                       default=False,
1090                       help="Show this help message and exit")
1091     parser.add_argument('--config', dest='config', metavar='PATH',
1092                       help="Specify the path to the configuration file")
1093     parser.add_argument('-d', '--debug', dest='debug', action='store_true',
1094                       default=False,
1095                       help="Include debug output")
1096     parser.add_argument('-i', '--include', dest='include', action='store_true',
1097                       default=False,
1098                       help="Include protocol headers in the output")
1099     parser.add_argument('-s', '--silent', dest='silent', action='store_true',
1100                       default=False,
1101                       help="Silent mode, don't output anything")
1102     parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
1103                       default=False,
1104                       help="Make the operation more talkative")
1105     parser.add_argument('-V', '--version', dest='version', action='store_true',
1106                       default=False,
1107                       help="Show version number and quit")
1108     parser.add_argument('-o', dest='options', action='append',
1109                       default=[], metavar="KEY=VAL",
1110                       help="Override a config values")
1111
1112     args, argv = parser.parse_known_args()
1113
1114     if args.version:
1115         import kamaki
1116         print("kamaki %s" % kamaki.__version__)
1117         exit(0)
1118
1119     config = Config(args.config) if args.config else Config()
1120
1121     for option in args.options:
1122         keypath, sep, val = option.partition('=')
1123         if not sep:
1124             print("Invalid option '%s'" % option)
1125             exit(1)
1126         section, sep, key = keypath.partition('.')
1127         if not sep:
1128             print("Invalid option '%s'" % option)
1129             exit(1)
1130         config.override(section.strip(), key.strip(), val.strip())
1131
1132     apis = set(['config'])
1133     for api in ('compute', 'image', 'storage', 'astakos'):
1134         if config.getboolean(api, 'enable'):
1135             apis.add(api)
1136     if config.getboolean('compute', 'cyclades_extensions'):
1137         apis.add('cyclades')
1138     if config.getboolean('storage', 'pithos_extensions'):
1139         apis.add('pithos')
1140
1141     # Remove commands that belong to APIs that are not included
1142     for group, group_commands in _commands.items():
1143         for name, cls in group_commands.items():
1144             if cls.api not in apis:
1145                 del group_commands[name]
1146         if not group_commands:
1147             del _commands[group]
1148
1149     group = argv.pop(0) if argv else None
1150
1151     if not group:
1152         parser.print_help()
1153         print_groups()
1154         exit(0)
1155
1156     if group not in _commands:
1157         parser.print_help()
1158         print_groups()
1159         exit(1)
1160
1161     parser.prog = '%s %s <command>' % (exe, group)
1162     command = argv.pop(0) if argv else None
1163
1164     if not command:
1165         parser.print_help()
1166         print_commands(group)
1167         exit(0)
1168
1169     if command not in _commands[group]:
1170         parser.print_help()
1171         print_commands(group)
1172         exit(1)
1173
1174     cmd = _commands[group][command]()
1175
1176     parser.prog = '%s %s %s' % (exe, group, command)
1177     if cmd.syntax:
1178         parser.prog += '  %s' % cmd.syntax
1179     parser.description = cmd.description
1180     parser.epilog = ''
1181     if hasattr(cmd, 'update_parser'):
1182         cmd.update_parser(parser)
1183
1184     args, argv = parser.parse_known_args()
1185
1186     if args.help:
1187         parser.print_help()
1188         exit(0)
1189
1190     if args.silent:
1191         add_handler('', logging.CRITICAL)
1192     elif args.debug:
1193         add_handler('requests', logging.INFO, prefix='* ')
1194         add_handler('clients.send', logging.DEBUG, prefix='> ')
1195         add_handler('clients.recv', logging.DEBUG, prefix='< ')
1196     elif args.verbose:
1197         add_handler('requests', logging.INFO, prefix='* ')
1198         add_handler('clients.send', logging.INFO, prefix='> ')
1199         add_handler('clients.recv', logging.INFO, prefix='< ')
1200     elif args.include:
1201         add_handler('clients.recv', logging.INFO)
1202     else:
1203         add_handler('', logging.WARNING)
1204
1205     api = cmd.api
1206     if api in ('compute', 'cyclades'):
1207         url = config.get('compute', 'url')
1208         token = config.get('compute', 'token') or config.get('global', 'token')
1209         if config.getboolean('compute', 'cyclades_extensions'):
1210             cmd.client = clients.cyclades(url, token)
1211         else:
1212             cmd.client = clients.compute(url, token)
1213     elif api in ('storage', 'pithos'):
1214         url = config.get('storage', 'url')
1215         token = config.get('storage', 'token') or config.get('global', 'token')
1216         account = config.get('storage', 'account')
1217         container = config.get('storage', 'container')
1218         if config.getboolean('storage', 'pithos_extensions'):
1219             cmd.client = clients.pithos(url, token, account, container)
1220         else:
1221             cmd.client = clients.storage(url, token, account, container)
1222     elif api == 'image':
1223         url = config.get('image', 'url')
1224         token = config.get('image', 'token') or config.get('global', 'token')
1225         cmd.client = clients.image(url, token)
1226     elif api == 'astakos':
1227         url = config.get('astakos', 'url')
1228         token = config.get('astakos', 'token') or config.get('global', 'token')
1229         cmd.client = clients.astakos(url, token)
1230
1231     cmd.args = args
1232     cmd.config = config
1233
1234     try:
1235         ret = cmd.main(*argv[2:])
1236         exit(ret)
1237     except TypeError as e:
1238         if e.args and e.args[0].startswith('main()'):
1239             parser.print_help()
1240             exit(1)
1241         else:
1242             raise
1243     except clients.ClientError as err:
1244         if err.status == 404:
1245             message = yellow(err.message)
1246         elif 500 <= err.status < 600:
1247             message = magenta(err.message)
1248         else:
1249             message = red(err.message)
1250
1251         print(message, file=stderr)
1252         if err.details and (args.verbose or args.debug):
1253             print(err.details, file=stderr)
1254         exit(2)
1255     except ConnectionError as err:
1256         print(red("Connection error"), file=stderr)
1257         exit(1)
1258
1259 if __name__ == '__main__':
1260     main()