3 # Copyright 2011-2012 GRNET S.A. All rights reserved.
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
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.
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.
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.
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.
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'
46 @command(api='compute')
47 class server_list(object):
48 //This command will be named 'list' under group 'server'
51 @command(api='compute', name='ls')
52 class server_list(object):
53 //This command will be named 'ls' under group 'server'
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.
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:
62 def main(self, server_id, network=None):
63 // This syntax of this command will be: '<server id> [network]'
66 The order of commands is important, it will be preserved in the help output.
69 from __future__ import print_function
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
81 from collections import OrderedDict
83 from ordereddict import OrderedDict
85 from colors import magenta, red, yellow
86 from progress.bar import IncrementalBar
87 from requests.exceptions import ConnectionError
90 from .config import Config
91 from .utils import print_list, print_dict, print_items, format_size
93 _commands = OrderedDict()
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"}
105 class ProgressBar(IncrementalBar):
106 suffix = '%(percent)d%% - %(eta)ds'
108 def command(api=None, group=None, name=None, syntax=None):
109 """Class decorator that registers a class as a CLI command."""
112 grp, sep, cmd = cls.__name__.partition('_')
114 grp, cmd = None, cls.__name__
117 cls.group = group or grp
118 cls.name = name or cmd
120 short_description, sep, long_description = cls.__doc__.partition('\n')
121 cls.description = short_description
122 cls.long_description = long_description or short_description
125 if cls.syntax is None:
126 # Generate a syntax string based on main's arguments
127 spec = inspect.getargspec(cls.main.im_func)
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)
134 cls.syntax += ' <%s ...>' % spec.varargs
136 if cls.group not in _commands:
137 _commands[cls.group] = OrderedDict()
138 _commands[cls.group][cls.name] = cls
142 @command(api='config')
143 class config_list(object):
144 """List configuration options"""
146 def update_parser(self, parser):
147 parser.add_argument('-a', dest='all', action='store_true',
148 default=False, help='include default values')
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))
157 @command(api='config')
158 class config_get(object):
159 """Show a configuration option"""
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:
168 @command(api='config')
169 class config_set(object):
170 """Set a configuration option"""
172 def main(self, option, value):
173 section, sep, key = option.rpartition('.')
174 section = section or 'global'
175 self.config.set(section, key, value)
178 @command(api='config')
179 class config_delete(object):
180 """Delete a configuration option (and use the default value)"""
182 def main(self, option):
183 section, sep, key = option.rpartition('.')
184 section = section or 'global'
185 self.config.remove_option(section, key)
188 @command(api='compute')
189 class server_list(object):
192 def update_parser(self, parser):
193 parser.add_argument('-l', dest='detail', action='store_true',
194 default=False, help='show detailed output')
197 servers = self.client.list_servers(self.args.detail)
200 @command(api='compute')
201 class server_info(object):
202 """Get server details"""
204 def main(self, server_id):
206 server = self.client.get_server_details(int(server_id))
208 print(yellow('Server id must be a base10 integer'))
212 @command(api='compute')
213 class server_create(object):
214 """Create a server"""
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')
222 def main(self, name, flavor_id, image_id):
224 for personality in self.args.personalities:
225 p = personality.split(',')
226 p.extend([None] * (5 - len(p))) # Fill missing fields with None
231 print("Invalid personality argument '%s'" % p)
234 print("File %s does not exist" % path)
237 with open(path) as f:
238 contents = b64encode(f.read())
240 d = {'path': p[1] or abspath(path), 'contents': contents}
246 d['mode'] = int(p[4])
247 personalities.append(d)
249 reply = self.client.create_server(name, int(flavor_id), image_id,
253 @command(api='compute')
254 class server_rename(object):
255 """Update a server's name"""
257 def main(self, server_id, new_name):
259 self.client.update_server_name(int(server_id), new_name)
261 print(yellow('Server id must be a base10 integer'))
263 @command(api='compute')
264 class server_delete(object):
265 """Delete a server"""
267 def main(self, server_id):
269 self.client.delete_server(int(server_id))
271 print(yellow('Server id must be a base10 integer'))
273 @command(api='compute')
274 class server_reboot(object):
275 """Reboot a server"""
277 def update_parser(self, parser):
278 parser.add_argument('-f', dest='hard', action='store_true',
279 default=False, help='perform a hard reboot')
281 def main(self, server_id):
283 self.client.reboot_server(int(server_id), self.args.hard)
285 print(yellow('Server id must be a base10 integer'))
287 @command(api='cyclades')
288 class server_start(object):
291 def main(self, server_id):
293 self.client.start_server(int(server_id))
295 print(yellow('Server id must be a base10 integer'))
297 @command(api='cyclades')
298 class server_shutdown(object):
299 """Shutdown a server"""
301 def main(self, server_id):
303 self.client.shutdown_server(int(server_id))
305 print(yellow('Server id must be a base10 integer'))
307 @command(api='cyclades')
308 class server_console(object):
309 """Get a VNC console"""
311 def main(self, server_id):
313 reply = self.client.get_server_console(int(server_id))
315 print(yellow('Server id must be a base10 integer'))
319 @command(api='cyclades')
320 class server_firewall(object):
321 """Set the server's firewall profile"""
323 def main(self, server_id, profile):
325 self.client.set_firewall_profile(int(server_id), profile)
327 print(yellow('Server id must be a base10 integer'))
329 @command(api='cyclades')
330 class server_addr(object):
331 """List a server's addresses"""
333 def main(self, server_id, network=None):
335 reply = self.client.list_server_nic_details(int(server_id), network)
337 print(yellow('Server id must be a base10 integer'))
341 @command(api='compute')
342 class server_meta(object):
343 """Get a server's metadata"""
345 def main(self, server_id, key=None):
347 reply = self.client.get_server_metadata(int(server_id), key)
349 print(yellow('Server id must be a base10 integer'))
353 @command(api='compute')
354 class server_addmeta(object):
355 """Add server metadata"""
357 def main(self, server_id, key, val):
359 reply = self.client.create_server_metadata(int(server_id), key, val)
361 print(yellow('Server id must be a base10 integer'))
365 @command(api='compute')
366 class server_setmeta(object):
367 """Update server's metadata"""
369 def main(self, server_id, key, val):
370 metadata = {key: val}
372 reply = self.client.update_server_metadata(int(server_id), **metadata)
374 print(yellow('Server id must be a base10 integer'))
378 @command(api='compute')
379 class server_delmeta(object):
380 """Delete server metadata"""
382 def main(self, server_id, key):
384 self.client.delete_server_metadata(int(server_id), key)
386 print(yellow('Server id must be a base10 integer'))
389 @command(api='cyclades')
390 class server_stats(object):
391 """Get server statistics"""
393 def main(self, server_id):
395 reply = self.client.get_server_stats(int(server_id))
397 print(yellow('Server id must be a base10 integer'))
399 print_dict(reply, exclude=('serverRef',))
401 @command(api='compute')
402 class flavor_list(object):
405 def update_parser(self, parser):
406 parser.add_argument('-l', dest='detail', action='store_true',
407 default=False, help='show detailed output')
410 flavors = self.client.list_flavors(self.args.detail)
413 @command(api='compute')
414 class flavor_info(object):
415 """Get flavor details"""
417 def main(self, flavor_id):
419 flavor = self.client.get_flavor_details(int(flavor_id))
421 print(yellow('Flavor id must be a base10 integer'))
425 @command(api='compute')
426 class image_list(object):
429 def update_parser(self, parser):
430 parser.add_argument('-l', dest='detail', action='store_true',
431 default=False, help='show detailed output')
434 images = self.client.list_images(self.args.detail)
437 @command(api='compute')
438 class image_info(object):
439 """Get image details"""
441 def main(self, image_id):
442 image = self.client.get_image_details(image_id)
445 @command(api='compute')
446 class image_delete(object):
449 def main(self, image_id):
450 self.client.delete_image(image_id)
452 @command(api='compute')
453 class image_properties(object):
454 """Get image properties"""
456 def main(self, image_id, key=None):
457 reply = self.client.get_image_metadata(image_id, key)
460 @command(api='compute')
461 class image_addproperty(object):
462 """Add an image property"""
464 def main(self, image_id, key, val):
465 reply = self.client.create_image_metadata(image_id, key, val)
468 @command(api='compute')
469 class image_setproperty(object):
470 """Update an image property"""
472 def main(self, image_id, key, val):
473 metadata = {key: val}
474 reply = self.client.update_image_metadata(image_id, **metadata)
477 @command(api='compute')
478 class image_delproperty(object):
479 """Delete an image property"""
481 def main(self, image_id, key):
482 self.client.delete_image_metadata(image_id, key)
484 @command(api='cyclades')
485 class network_list(object):
488 def update_parser(self, parser):
489 parser.add_argument('-l', dest='detail', action='store_true',
490 default=False, help='show detailed output')
493 networks = self.client.list_networks(self.args.detail)
494 print_items(networks)
496 @command(api='cyclades')
497 class network_create(object):
498 """Create a network"""
500 def main(self, name):
501 reply = self.client.create_network(name)
504 @command(api='cyclades')
505 class network_info(object):
506 """Get network details"""
508 def main(self, network_id):
509 network = self.client.get_network_details(network_id)
512 @command(api='cyclades')
513 class network_rename(object):
514 """Update network name"""
516 def main(self, network_id, new_name):
517 self.client.update_network_name(network_id, new_name)
519 @command(api='cyclades')
520 class network_delete(object):
521 """Delete a network"""
523 def main(self, network_id):
524 self.client.delete_network(network_id)
526 @command(api='cyclades')
527 class network_connect(object):
528 """Connect a server to a network"""
530 def main(self, server_id, network_id):
531 self.client.connect_server(server_id, network_id)
533 @command(api='cyclades')
534 class network_disconnect(object):
535 """Disconnect a nic that connects a server to a network"""
537 def main(self, nic_id):
539 server_id = nic_id.split('-')[1]
540 self.client.disconnect_server(server_id, nic_id)
542 print(yellow('nid_id format: nic-<server_id>-<nic_index>'))
544 @command(api='image')
545 class image_public(object):
546 """List public images"""
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)')
568 for filter in ('container_format', 'disk_format', 'name', 'size_min',
569 'size_max', 'status'):
570 val = getattr(self.args, filter, None)
572 filters[filter] = val
574 order = self.args.order or ''
575 images = self.client.list_public(self.args.detail, filters=filters,
577 print_items(images, title=('name',))
579 @command(api='image')
580 class image_meta(object):
581 """Get image metadata"""
583 def main(self, image_id):
584 image = self.client.get_meta(image_id)
587 @command(api='image')
588 class image_register(object):
589 """Register an image"""
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',
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')
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)
617 for key in ('checksum', 'container_format', 'disk_format', 'id',
619 val = getattr(self.args, key)
623 if self.args.is_public:
624 params['is_public'] = 'true'
627 for property in self.args.properties or []:
628 key, sep, val = property.partition('=')
630 print("Invalid property '%s'" % property)
632 properties[key.strip()] = val.strip()
634 self.client.register(name, location, params, properties)
636 @command(api='image')
637 class image_members(object):
638 """Get image members"""
640 def main(self, image_id):
641 members = self.client.list_members(image_id)
642 for member in members:
643 print(member['member_id'])
645 @command(api='image')
646 class image_shared(object):
647 """List shared images"""
649 def main(self, member):
650 images = self.client.list_shared(member)
652 print(image['image_id'])
654 @command(api='image')
655 class image_addmember(object):
656 """Add a member to an image"""
658 def main(self, image_id, member):
659 self.client.add_member(image_id, member)
661 @command(api='image')
662 class image_delmember(object):
663 """Remove a member from an image"""
665 def main(self, image_id, member):
666 self.client.remove_member(image_id, member)
668 @command(api='image')
669 class image_setmembers(object):
670 """Set the members of an image"""
672 def main(self, image_id, *member):
673 self.client.set_members(image_id, member)
675 class _store_account_command(object):
676 """Base class for account level storage commands"""
678 def update_parser(self, parser):
679 parser.add_argument('--account', dest='account', metavar='NAME',
680 help="Specify an account to use")
682 def progress(self, message):
683 """Return a generator function to be used for progress tracking"""
688 msg = message.ljust(MESSAGE_LENGTH)
689 for i in ProgressBar(msg).iter(range(n)):
696 if self.args.account is not None:
697 self.client.account = self.args.account
699 class _store_container_command(_store_account_command):
700 """Base class for container level storage commands"""
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")
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
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
722 self.container = None
724 @command(api='storage')
725 class store_list(_store_container_command):
726 """List containers, object trees or objects in a directory
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']))
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']))
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)
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)
749 @command(api='storage')
750 class store_create(_store_container_command):
751 """Create a container or a directory object"""
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)
758 self.client.create_directory(self.path)
760 @command(api='storage')
761 class store_copy(_store_container_command):
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(':')
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)
771 @command(api='storage')
772 class store_move(_store_container_command):
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(':')
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)
782 @command(api='storage')
783 class store_append(_store_container_command):
784 """Append local file to (existing) remote object"""
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)
792 @command(api='storage')
793 class store_truncate(_store_container_command):
794 """Truncate remote file up to a size"""
796 def main(self, container___path, size=0):
797 super(store_truncate, self).main(container___path)
798 self.client.truncate_object(self.path, size)
800 @command(api='storage')
801 class store_overwrite(_store_container_command):
802 """Overwrite part (from start to end) of a remote file"""
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)
810 @command(api='storage')
811 class store_upload(_store_container_command):
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)
822 @command(api='storage')
823 class store_download(_store_container_command):
824 """Download a file"""
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
831 blocksize = 4 * 1024 ** 2
832 nblocks = 1 + (size - 1) // blocksize
834 cb = self.progress('Downloading blocks') if local_path != '-' else None
839 data = f.read(blocksize)
842 data = f.read(blocksize)
846 @command(api='storage')
847 class store_delete(_store_container_command):
848 """Delete a container [or an object]"""
850 def main(self, container____path__):
851 super(store_delete, self).main(container____path__)
853 self.client.delete_container(self.container)
855 self.client.delete_object(self.path)
857 @command(api='storage')
858 class store_purge(_store_account_command):
859 """Purge a container"""
861 def main(self, container):
862 super(store_purge, self).main()
863 self.client.container = container
864 self.client.purge_container()
866 @command(api='storage')
867 class store_publish(_store_container_command):
868 """Publish an object"""
870 def main(self, container___path):
871 super(store_publish, self).main(container___path)
872 self.client.publish_object(self.path)
874 @command(api='storage')
875 class store_unpublish(_store_container_command):
876 """Unpublish an object"""
878 def main(self, container___path):
879 super(store_unpublish, self).main(container___path)
880 self.client.unpublish_object(self.path)
882 @command(api='storage')
883 class store_permitions(_store_container_command):
884 """Get object read/write permitions"""
886 def main(self, container___path):
887 super(store_permitions, self).main(container___path)
888 reply = self.client.get_object_sharing(self.path)
891 @command(api='storage')
892 class store_setpermitions(_store_container_command):
893 """Set sharing permitions"""
895 def main(self, container___path, *permitions):
896 super(store_setpermitions, self).main(container___path)
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(',')]
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,...')
912 print(u'\twrite=username,groupname,...')
914 self.client.set_object_sharing(self.path, read_permition=read, write_permition=write)
916 @command(api='storage')
917 class store_delpermitions(_store_container_command):
918 """Delete all sharing permitions"""
920 def main(self, container___path):
921 super(store_delpermitions, self).main(container___path)
922 self.client.del_object_sharing(self.path)
924 @command(api='storage')
925 class store_info(_store_container_command):
926 """Get information for account [, container [or object]]"""
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)
935 reply = self.client.get_object_info(self.path)
938 @command(api='storage')
939 class store_meta(_store_container_command):
940 """Get custom meta-content for account [, container [or object]]"""
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)
949 reply = self.client.get_container_meta(self.container)
951 reply = self.client.get_object_meta(self.path)
954 @command(api='storage')
955 class store_setmeta(_store_container_command):
956 """Set a new metadatum for account [, container [or object]]"""
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})
965 self.client.set_object_meta(self.path, {metakey:metavalue})
967 @command(api='storage')
968 class store_delmeta(_store_container_command):
969 """Delete an existing metadatum of account [, container [or object]]"""
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)
978 self.client.delete_object_meta(metakey, self.path)
980 @command(api='storage')
981 class store_quota(_store_account_command):
982 """Get quota for account [or container]"""
984 def main(self, container = None):
985 super(store_quota, self).main()
986 if container is None:
987 reply = self.client.get_account_quota()
989 reply = self.client.get_container_quota(container)
992 @command(api='storage')
993 class store_setquota(_store_account_command):
994 """Set new quota (in KB) for account [or container]"""
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)
1001 self.client.container = container
1002 self.client.set_container_quota(quota)
1004 @command(api='storage')
1005 class store_versioning(_store_account_command):
1006 """Get versioning for account [or container ]"""
1008 def main(self, container = None):
1009 super(store_versioning, self).main()
1010 if container is None:
1011 reply = self.client.get_account_versioning()
1013 reply = self.client.get_container_versioning(container)
1016 @command(api='storage')
1017 class store_setversioning(_store_account_command):
1018 """Set new versioning (auto, none) for account [or container]"""
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)
1025 self.client.container = container
1026 self.client.set_container_versioning(versioning)
1028 @command(api='storage')
1029 class store_group(_store_account_command):
1030 """Get user groups details for account"""
1033 super(store_group, self).main()
1034 reply = self.client.get_account_group()
1037 @command(api='storage')
1038 class store_setgroup(_store_account_command):
1039 """Create/update a new user group on account"""
1041 def main(self, groupname, *users):
1042 super(store_setgroup, self).main()
1043 self.client.set_account_group(groupname, users)
1045 @command(api='storage')
1046 class store_delgroup(_store_account_command):
1047 """Delete a user group on an account"""
1049 def main(self, groupname):
1050 super(store_delgroup, self).main()
1051 self.client.del_account_group(groupname)
1053 @command(api='astakos')
1054 class astakos_authenticate(object):
1055 """Authenticate a user"""
1058 reply = self.client.authenticate()
1063 for group in _commands:
1064 description = GROUPS.get(group, '')
1065 print(' ', group.ljust(12), description)
1067 def print_commands(group):
1068 description = GROUPS.get(group, '')
1070 print('\n' + description)
1072 print('\nCommands:')
1073 for name, cls in _commands[group].items():
1074 print(' ', name.ljust(14), cls.description)
1076 def add_handler(name, level, prefix=''):
1077 h = logging.StreamHandler()
1078 fmt = logging.Formatter(prefix + '%(message)s')
1080 logger = logging.getLogger(name)
1081 logger.addHandler(h)
1082 logger.setLevel(level)
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',
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',
1095 help="Include debug output")
1096 parser.add_argument('-i', '--include', dest='include', action='store_true',
1098 help="Include protocol headers in the output")
1099 parser.add_argument('-s', '--silent', dest='silent', action='store_true',
1101 help="Silent mode, don't output anything")
1102 parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
1104 help="Make the operation more talkative")
1105 parser.add_argument('-V', '--version', dest='version', action='store_true',
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")
1112 args, argv = parser.parse_known_args()
1116 print("kamaki %s" % kamaki.__version__)
1119 config = Config(args.config) if args.config else Config()
1121 for option in args.options:
1122 keypath, sep, val = option.partition('=')
1124 print("Invalid option '%s'" % option)
1126 section, sep, key = keypath.partition('.')
1128 print("Invalid option '%s'" % option)
1130 config.override(section.strip(), key.strip(), val.strip())
1132 apis = set(['config'])
1133 for api in ('compute', 'image', 'storage', 'astakos'):
1134 if config.getboolean(api, 'enable'):
1136 if config.getboolean('compute', 'cyclades_extensions'):
1137 apis.add('cyclades')
1138 if config.getboolean('storage', 'pithos_extensions'):
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]
1149 group = argv.pop(0) if argv else None
1156 if group not in _commands:
1161 parser.prog = '%s %s <command>' % (exe, group)
1162 command = argv.pop(0) if argv else None
1166 print_commands(group)
1169 if command not in _commands[group]:
1171 print_commands(group)
1174 cmd = _commands[group][command]()
1176 parser.prog = '%s %s %s' % (exe, group, command)
1178 parser.prog += ' %s' % cmd.syntax
1179 parser.description = cmd.description
1181 if hasattr(cmd, 'update_parser'):
1182 cmd.update_parser(parser)
1184 args, argv = parser.parse_known_args()
1191 add_handler('', logging.CRITICAL)
1193 add_handler('requests', logging.INFO, prefix='* ')
1194 add_handler('clients.send', logging.DEBUG, prefix='> ')
1195 add_handler('clients.recv', logging.DEBUG, prefix='< ')
1197 add_handler('requests', logging.INFO, prefix='* ')
1198 add_handler('clients.send', logging.INFO, prefix='> ')
1199 add_handler('clients.recv', logging.INFO, prefix='< ')
1201 add_handler('clients.recv', logging.INFO)
1203 add_handler('', logging.WARNING)
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)
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)
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)
1235 ret = cmd.main(*argv[2:])
1237 except TypeError as e:
1238 if e.args and e.args[0].startswith('main()'):
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)
1249 message = red(err.message)
1251 print(message, file=stderr)
1252 if err.details and (args.verbose or args.debug):
1253 print(err.details, file=stderr)
1255 except ConnectionError as err:
1256 print(red("Connection error"), file=stderr)
1259 if __name__ == '__main__':