commit 7493ccb6cc89e7cacc3e1e12c71932cf50d8fd6c
Author: Stavros Sachtouris <saxtouri@admin.grnet.gr>
Date:   Tue Sep 4 18:57:39 2012 +0300

    Correct repackaging, minor server list improvement

diff --git a/kamaki/cli/__init__.py b/kamaki/cli/__init__.py
new file mode 100644
index 0000000..cd63a97
--- /dev/null
+++ b/kamaki/cli/__init__.py
@@ -0,0 +1,322 @@
+#!/usr/bin/env python
+
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from __future__ import print_function
+
+import gevent.monkey
+#Monkey-patch everything for gevent early on
+gevent.monkey.patch_all()
+
+import inspect
+import logging
+import sys
+
+from argparse import ArgumentParser
+from base64 import b64encode
+from os.path import abspath, basename, exists
+from sys import exit, stdout, stderr
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    from ordereddict import OrderedDict
+
+from colors import magenta, red, yellow, bold
+
+from kamaki import clients
+from .config import Config
+
+_commands = OrderedDict()
+
+GROUPS = {}
+CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
+
+class CLIError(Exception):
+    def __init__(self, message, status=0, details='', importance=0):
+        """importance is set by the raiser
+        0 is the lowest possible importance
+        Suggested values: 0, 1, 2, 3
+        """
+        super(CLIError, self).__init__(message, status, details)
+        self.message = message
+        self.status = status
+        self.details = details
+        self.importance = importance
+
+    def __unicode__(self):
+        return unicode(self.message)
+
+def command(group=None, name=None, syntax=None):
+    """Class decorator that registers a class as a CLI command."""
+
+    def decorator(cls):
+        grp, sep, cmd = cls.__name__.partition('_')
+        if not sep:
+            grp, cmd = None, cls.__name__
+
+        #cls.api = api
+        cls.group = group or grp
+        cls.name = name or cmd
+
+        short_description, sep, long_description = cls.__doc__.partition('\n')
+        cls.description = short_description
+        cls.long_description = long_description or short_description
+
+        cls.syntax = syntax
+        if cls.syntax is None:
+            # Generate a syntax string based on main's arguments
+            spec = inspect.getargspec(cls.main.im_func)
+            args = spec.args[1:]
+            n = len(args) - len(spec.defaults or ())
+            required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').replace('_', ' ') for x in args[:n])
+            optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').replace('_', ' ') for x in args[n:])
+            cls.syntax = ' '.join(x for x in [required, optional] if x)
+            if spec.varargs:
+                cls.syntax += ' <%s ...>' % spec.varargs
+
+        if cls.group not in _commands:
+            _commands[cls.group] = OrderedDict()
+        _commands[cls.group][cls.name] = cls
+        return cls
+    return decorator
+
+def set_api_description(api, description):
+    """Method to be called by api CLIs
+    Each CLI can set more than one api descriptions"""
+    GROUPS[api] = description
+
+def main():
+
+    def print_groups():
+        print('\nGroups:')
+        for group in _commands:
+            description = GROUPS.get(group, '')
+            print(' ', group.ljust(12), description)
+
+    def print_commands(group):
+        description = GROUPS.get(group, '')
+        if description:
+            print('\n' + description)
+
+        print('\nCommands:')
+        for name, cls in _commands[group].items():
+            print(' ', name.ljust(14), cls.description)
+
+    def manage_logging_handlers(args):
+        """This is mostly to handle logging for clients package"""
+
+        def add_handler(name, level, prefix=''):
+            h = logging.StreamHandler()
+            fmt = logging.Formatter(prefix + '%(message)s')
+            h.setFormatter(fmt)
+            logger = logging.getLogger(name)
+            logger.addHandler(h)
+            logger.setLevel(level)
+
+        if args.silent:
+            add_handler('', logging.CRITICAL)
+        elif args.debug:
+            add_handler('requests', logging.INFO, prefix='* ')
+            add_handler('clients.send', logging.DEBUG, prefix='> ')
+            add_handler('clients.recv', logging.DEBUG, prefix='< ')
+        elif args.verbose:
+            add_handler('requests', logging.INFO, prefix='* ')
+            add_handler('clients.send', logging.INFO, prefix='> ')
+            add_handler('clients.recv', logging.INFO, prefix='< ')
+        elif args.include:
+            add_handler('clients.recv', logging.INFO)
+        else:
+            add_handler('', logging.WARNING)
+
+    def load_groups(config):
+        """load groups and import CLIs and Modules"""
+        loaded_modules = {}
+        for api in config.apis():
+            api_cli = config.get(api, 'cli')
+            if None == api_cli or len(api_cli)==0:
+                print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
+                print('\t(cli option in config file)')
+                continue
+            if not loaded_modules.has_key(api_cli):
+                loaded_modules[api_cli] = False
+                for location in CLI_LOCATIONS:
+                    location += api_cli if location == '' else '.%s'%api_cli
+                    try:
+                        __import__(location)
+                        loaded_modules[api_cli] = True
+                        break
+                    except ImportError:
+                        pass
+                if not loaded_modules[api_cli]:
+                    print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
+                    print('\t(No suitable cli in known paths)')
+                    continue
+            if not GROUPS.has_key(api):
+                GROUPS[api] = 'No description (interface: %s)'%api_cli
+
+    def init_parser(exe):
+        parser = ArgumentParser(add_help=False)
+        parser.prog = '%s <group> <command>' % exe
+        parser.add_argument('-h', '--help', dest='help', action='store_true',
+                          default=False,
+                          help="Show this help message and exit")
+        parser.add_argument('--config', dest='config', metavar='PATH',
+                          help="Specify the path to the configuration file")
+        parser.add_argument('-d', '--debug', dest='debug', action='store_true',
+                          default=False,
+                          help="Include debug output")
+        parser.add_argument('-i', '--include', dest='include', action='store_true',
+                          default=False,
+                          help="Include protocol headers in the output")
+        parser.add_argument('-s', '--silent', dest='silent', action='store_true',
+                          default=False,
+                          help="Silent mode, don't output anything")
+        parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
+                          default=False,
+                          help="Make the operation more talkative")
+        parser.add_argument('-V', '--version', dest='version', action='store_true',
+                          default=False,
+                          help="Show version number and quit")
+        parser.add_argument('-o', dest='options', action='append',
+                          default=[], metavar="KEY=VAL",
+                          help="Override a config value")
+        return parser
+
+    def find_term_in_args(arg_list, term_list):
+        """find an arg_list term in term_list. All other terms up to found
+        term are rearanged at the end of arg_list, preserving relative order
+        """
+        arg_tail = []
+        while len(arg_list) > 0:
+            group = arg_list.pop(0)
+            if group not in term_list:
+                arg_tail.append(group)
+            else:
+                arg_list += arg_tail
+                return group
+        return None
+
+    """Main Code"""
+    exe = basename(sys.argv[0])
+    parser = init_parser(exe)
+    args, argv = parser.parse_known_args()
+
+    #print version
+    if args.version:
+        import kamaki
+        print("kamaki %s" % kamaki.__version__)
+        exit(0)
+
+    config = Config(args.config) if args.config else Config()
+
+    #load config options from command line
+    for option in args.options:
+        keypath, sep, val = option.partition('=')
+        if not sep:
+            print("Invalid option '%s'" % option)
+            exit(1)
+        section, sep, key = keypath.partition('.')
+        if not sep:
+            print("Invalid option '%s'" % option)
+            exit(1)
+        config.override(section.strip(), key.strip(), val.strip())
+
+    load_groups(config)
+    group = find_term_in_args(argv, _commands)
+    if not group:
+        parser.print_help()
+        print_groups()
+        exit(0)
+
+    parser.prog = '%s %s <command>' % (exe, group)
+    command = find_term_in_args(argv, _commands[group])
+
+    if not command:
+        parser.print_help()
+        print_commands(group)
+        exit(0)
+
+    cmd = _commands[group][command]()
+
+    parser.prog = '%s %s %s' % (exe, group, command)
+    if cmd.syntax:
+        parser.prog += '  %s' % cmd.syntax
+    parser.description = cmd.description
+    parser.epilog = ''
+    if hasattr(cmd, 'update_parser'):
+        cmd.update_parser(parser)
+
+    #check other args
+    args, argv = parser.parse_known_args()
+    if group != argv[0]:
+        errmsg = red('Invalid command group '+argv[0])
+        print(errmsg, file=stderr)
+        exit(1)
+    if command != argv[1]:
+        errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
+        print(errmsg, file=stderr)
+        exit(1)
+
+    if args.help:
+        parser.print_help()
+        exit(0)
+
+    manage_logging_handlers(args)
+    cmd.args = args
+    cmd.config = config
+    try:
+        ret = cmd.main(*argv[2:])
+        exit(ret)
+    except TypeError as e:
+        if e.args and e.args[0].startswith('main()'):
+            parser.print_help()
+            exit(1)
+        else:
+            raise
+    except CLIError as err:
+        errmsg = 'CLI Error '
+        errmsg += '(%s): '%err.status if err.status else ': '
+        errmsg += err.message if err.message else ''
+        if err.importance == 1:
+            errmsg = yellow(errmsg)
+        elif err.importance == 2:
+            errmsg = magenta(errmsg)
+        elif err.importance > 2:
+            errmsg = red(errmsg)
+        print(errmsg, file=stderr)
+        exit(1)
+
+if __name__ == '__main__':
+    main()
diff --git a/kamaki/cli/commands/__init__.py b/kamaki/cli/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/kamaki/cli/commands/astakos_cli.py b/kamaki/cli/commands/astakos_cli.py
new file mode 100644
index 0000000..52a3f2f
--- /dev/null
+++ b/kamaki/cli/commands/astakos_cli.py
@@ -0,0 +1,58 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.command
+
+
+from kamaki.cli import command, set_api_description
+set_api_description('astakos', 'Astakos API commands')
+from kamaki.clients.astakos import AstakosClient, ClientError
+from kamaki.cli.utils import raiseCLIError, print_dict
+
+class _astakos_init(object):
+	def main(self):
+		token = self.config.get('astakos', 'token') or self.config.get('global', 'token')
+		base_url = self.config.get('astakos', 'url') or self.config.get('global', 'url')
+		if base_url is None:
+			raise ClientError('no URL for astakos')
+		self.client = AstakosClient(base_url=base_url, token=token)
+
+@command()
+class astakos_authenticate(_astakos_init):
+    """Authenticate a user"""
+
+    def main(self):
+    	super(astakos_authenticate, self).main()
+    	try:
+        	reply = self.client.authenticate()
+        except ClientError as err:
+        	raiseCLIError(err)
+        print_dict(reply)
\ No newline at end of file
diff --git a/kamaki/cli/commands/config_cli.py b/kamaki/cli/commands/config_cli.py
new file mode 100644
index 0000000..9cb18d7
--- /dev/null
+++ b/kamaki/cli/commands/config_cli.py
@@ -0,0 +1,81 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from kamaki.cli import command, set_api_description
+set_api_description('config', 'Configuration commands')
+
+@command()
+class config_list(object):
+    """List configuration options"""
+
+    def update_parser(self, parser):
+        parser.add_argument('-a', dest='all', action='store_true',
+                          default=False, help='include default values')
+
+    def main(self):
+        include_defaults = self.args.all
+        for section in sorted(self.config.sections()):
+            items = self.config.items(section, include_defaults)
+            for key, val in sorted(items):
+                print('%s.%s = %s' % (section, key, val))
+
+@command()
+class config_get(object):
+    """Show a configuration option"""
+
+    def main(self, option):
+        section, sep, key = option.rpartition('.')
+        section = section or 'global'
+        value = self.config.get(section, key)
+        if value is not None:
+            print(value)
+
+@command()
+class config_set(object):
+    """Set a configuration option"""
+
+    def main(self, option, value):
+        section, sep, key = option.rpartition('.')
+        section = section or 'globail'
+        self.config.set(section, key, value)
+        self.config.write()
+
+@command()
+class config_delete(object):
+    """Delete a configuration option (and use the default value)"""
+
+    def main(self, option):
+        section, sep, key = option.rpartition('.')
+        section = section or 'global'
+        self.config.remove_option(section, key)
+        self.config.write()
diff --git a/kamaki/cli/commands/cyclades_cli.py b/kamaki/cli/commands/cyclades_cli.py
new file mode 100644
index 0000000..2df343a
--- /dev/null
+++ b/kamaki/cli/commands/cyclades_cli.py
@@ -0,0 +1,579 @@
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from kamaki.cli import command, set_api_description, CLIError
+from kamaki.cli.utils import print_dict, print_items, print_list, format_size, raiseCLIError
+from colors import bold
+set_api_description('server', "Compute/Cyclades API server commands")
+set_api_description('flavor', "Compute/Cyclades API flavor commands")
+set_api_description('image', "Compute/Cyclades or Glance API image commands")
+set_api_description('network', "Compute/Cyclades API network commands")
+from kamaki.clients.cyclades import CycladesClient, ClientError
+
+class _init_cyclades(object):
+    def main(self):
+        token = self.config.get('compute', 'token') or self.config.get('global', 'token')
+        base_url = self.config.get('compute', 'url') or self.config.get('global', 'url')
+        self.client = CycladesClient(base_url=base_url, token=token)
+
+@command()
+class server_list(_init_cyclades):
+    """List servers"""
+
+    def _print(self, servers):
+        for server in servers:
+            sname = server.pop('name')
+            sid = server.pop('id')
+            print('%s (%s)'%(bold(sname), bold(unicode(sid))))
+            if getattr(self.args, 'detail'):
+                server_info._print(server)
+                print('- - -')
+
+    def update_parser(self, parser):
+        parser.add_argument('-l', dest='detail', action='store_true',
+                default=False, help='show detailed output')
+
+    def main(self):
+        super(self.__class__, self).main()
+        try:
+            servers = self.client.list_servers(self.args.detail)
+            self._print(servers)
+            #print_items(servers)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class server_info(_init_cyclades):
+    """Get server details"""
+
+    @classmethod
+    def _print(self,server):
+        addr_dict = {}
+        if server.has_key('attachments'):
+            for addr in server['attachments']['values']:
+                ips = addr.pop('values', [])
+                for ip in ips:
+                    addr['IPv%s'%ip['version']] = ip['addr']
+                if addr.has_key('firewallProfile'):
+                    addr['firewall'] = addr.pop('firewallProfile')
+                addr_dict[addr.pop('id')] = addr
+            server['attachments'] = addr_dict if addr_dict is not {} else None
+        if server.has_key('metadata'):
+            server['metadata'] = server['metadata']['values']
+        print_dict(server, ident=14)
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            server = self.client.get_server_details(int(server_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError as err:
+            raise CLIError(message='Server id must be positive integer',
+                importance=1)
+        self._print(server)
+
+@command()
+class server_create(_init_cyclades):
+    """Create a server"""
+
+    def update_parser(self, parser):
+        parser.add_argument('--personality', dest='personalities',
+                          action='append', default=[],
+                          metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
+                          help='add a personality file')
+
+    def main(self, name, flavor_id, image_id):
+        super(self.__class__, self).main()
+        personalities = []
+        for personality in self.args.personalities:
+            p = personality.split(',')
+            p.extend([None] * (5 - len(p)))     # Fill missing fields with None
+
+            path = p[0]
+
+            if not path:
+                raise CLIError(message='Invalid personality argument %s'%p, importance=1)
+            if not exists(path):
+                raise CLIError(message="File %s does not exist" % path, importance=1)
+
+            with open(path) as f:
+                contents = b64encode(f.read())
+
+            d = {'path': p[1] or abspath(path), 'contents': contents}
+            if p[2]:
+                d['owner'] = p[2]
+            if p[3]:
+                d['group'] = p[3]
+            if p[4]:
+                d['mode'] = int(p[4])
+            personalities.append(d)
+
+        try:
+            reply = self.client.create_server(name, int(flavor_id), image_id,
+                personalities)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class server_rename(_init_cyclades):
+    """Update a server's name"""
+
+    def main(self, server_id, new_name):
+        super(self.__class__, self).main()
+        try:
+            self.client.update_server_name(int(server_id), new_name)
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+
+@command()
+class server_delete(_init_cyclades):
+    """Delete a server"""
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            self.client.delete_server(int(server_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+
+@command()
+class server_reboot(_init_cyclades):
+    """Reboot a server"""
+
+    def update_parser(self, parser):
+        parser.add_argument('-f', dest='hard', action='store_true',
+                default=False, help='perform a hard reboot')
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            self.client.reboot_server(int(server_id), self.args.hard)
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+
+@command()
+class server_start(_init_cyclades):
+    """Start a server"""
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            self.client.start_server(int(server_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+
+@command()
+class server_shutdown(_init_cyclades):
+    """Shutdown a server"""
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            self.client.shutdown_server(int(server_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+
+@command()
+class server_console(_init_cyclades):
+    """Get a VNC console"""
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.get_server_console(int(server_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+        print_dict(reply)
+
+@command()
+class server_firewall(_init_cyclades):
+    """Set the server's firewall profile"""
+
+    def main(self, server_id, profile):
+        super(self.__class__, self).main()
+        try:
+            self.client.set_firewall_profile(int(server_id), profile)
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+@command()
+class server_addr(_init_cyclades):
+    """List a server's nic address"""
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.list_server_nics(int(server_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+        print_list(reply)
+
+@command()
+class server_meta(_init_cyclades):
+    """Get a server's metadata"""
+
+    def main(self, server_id, key=None):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.get_server_metadata(int(server_id), key)
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+        print_dict(reply)
+
+@command()
+class server_addmeta(_init_cyclades):
+    """Add server metadata"""
+
+    def main(self, server_id, key, val):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.create_server_metadata(int(server_id), key, val)
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+        print_dict(reply)
+
+@command()
+class server_setmeta(_init_cyclades):
+    """Update server's metadata"""
+
+    def main(self, server_id, key, val):
+        super(self.__class__, self).main()
+        metadata = {key: val}
+        try:
+            reply = self.client.update_server_metadata(int(server_id), **metadata)
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+        print_dict(reply)
+
+@command()
+class server_delmeta(_init_cyclades):
+    """Delete server metadata"""
+
+    def main(self, server_id, key):
+        super(self.__class__, self).main()
+        try:
+            self.client.delete_server_metadata(int(server_id), key)
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+
+@command()
+class server_stats(_init_cyclades):
+    """Get server statistics"""
+
+    def main(self, server_id):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.get_server_stats(int(server_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+        print_dict(reply, exclude=('serverRef',))
+
+@command()
+class flavor_list(_init_cyclades):
+    """List flavors"""
+
+    def update_parser(self, parser):
+        parser.add_argument('-l', dest='detail', action='store_true',
+                default=False, help='show detailed output')
+
+    def main(self):
+        super(self.__class__, self).main()
+        try:
+            flavors = self.client.list_flavors(self.args.detail)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_items(flavors)
+
+@command()
+class flavor_info(_init_cyclades):
+    """Get flavor details"""
+
+    def main(self, flavor_id):
+        super(self.__class__, self).main()
+        try:
+            flavor = self.client.get_flavor_details(int(flavor_id))
+        except ClientError as err:
+            raiseCLIError(err)
+        except ValueError:
+            raise CLIError(message='Server id must be positive integer', importance=1)
+        print_dict(flavor)
+
+@command()
+class image_list(_init_cyclades):
+    """List images"""
+
+    def update_parser(self, parser):
+        parser.add_argument('-l', dest='detail', action='store_true',
+                default=False, help='show detailed output')
+
+    def _print(self, images):
+        for img in images:
+            iname = img.pop('name')
+            iid = img.pop('id')
+            print('%s (%s)'%(bold(iname), bold(unicode(iid))))
+            if getattr(self.args, 'detail'):
+                image_info._print(img)
+                print('- - -')
+
+    def main(self):
+        super(self.__class__, self).main()
+        try:
+            images = self.client.list_images(self.args.detail)
+        except ClientError as err:
+            raiseCLIError(err)
+        #print_items(images)
+        self._print(images)
+
+@command()
+class image_info(_init_cyclades):
+    """Get image details"""
+
+    @classmethod
+    def _print(self, image):
+        if image.has_key('metadata'):
+            image['metadata'] = image['metadata']['values']
+        print_dict(image, ident=14)
+
+    def main(self, image_id):
+        super(self.__class__, self).main()
+        try:
+            image = self.client.get_image_details(image_id)
+        except ClientError as err:
+            raiseCLIError(err)
+        self._print(image)
+
+@command()
+class image_delete(_init_cyclades):
+    """Delete image"""
+
+    def main(self, image_id):
+        super(self.__class__, self).main()
+        try:
+            self.client.delete_image(image_id)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class image_properties(_init_cyclades):
+    """Get image properties"""
+
+    def main(self, image_id, key=None):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.get_image_metadata(image_id, key)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class image_addproperty(_init_cyclades):
+    """Add an image property"""
+
+    def main(self, image_id, key, val):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.create_image_metadata(image_id, key, val)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class image_setproperty(_init_cyclades):
+    """Update an image property"""
+
+    def main(self, image_id, key, val):
+        super(self.__class__, self).main()
+        metadata = {key: val}
+        try:
+            reply = self.client.update_image_metadata(image_id, **metadata)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class image_delproperty(_init_cyclades):
+    """Delete an image property"""
+
+    def main(self, image_id, key):
+        super(self.__class__, self).main()
+        try:
+            self.client.delete_image_metadata(image_id, key)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class network_list(_init_cyclades):
+    """List networks"""
+
+    def update_parser(self, parser):
+        parser.add_argument('-l', dest='detail', action='store_true',
+                default=False, help='show detailed output')
+
+    def print_networks(self, nets):
+        for net in nets:
+            netname = bold(net.pop('name'))
+            netid = bold(unicode(net.pop('id')))
+            print('%s (%s)'%(netname, netid))
+            if getattr(self.args, 'detail'):
+                network_info.print_network(net)
+                print('- - -')
+
+    def main(self):
+        super(self.__class__, self).main()
+        try:
+            networks = self.client.list_networks(self.args.detail)
+        except ClientError as err:
+            raiseCLIError(err)
+        self.print_networks(networks)
+
+@command()
+class network_create(_init_cyclades):
+    """Create a network"""
+
+    def update_parser(self, parser):
+        try:
+            super(self.__class__, self).update_parser(parser)
+        except AttributeError:
+            pass
+        parser.add_argument('--with-cidr', action='store', dest='cidr', default=False,
+            help='specific cidr for new network')
+        parser.add_argument('--with-gateway', action='store', dest='gateway', default=False,
+            help='specific getaway for new network')
+        parser.add_argument('--with-dhcp', action='store', dest='dhcp', default=False,
+            help='specific dhcp for new network')
+        parser.add_argument('--with-type', action='store', dest='type', default=False,
+            help='specific type for new network')
+
+    def main(self, name):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.create_network(name, cidr=getattr(self.args, 'cidr'),
+                gateway=getattr(self.args, 'gateway'), dhcp=getattr(self.args, 'gateway'),
+                type=getattr(self.args, 'type'))
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class network_info(_init_cyclades):
+    """Get network details"""
+
+    @classmethod
+    def print_network(self, net):
+        if net.has_key('attachments'):
+            att = net['attachments']['values']
+            net['attachments'] = att if len(att) > 0 else None
+        print_dict(net, ident=14)
+
+    def main(self, network_id):
+        super(self.__class__, self).main()
+        try:
+            network = self.client.get_network_details(network_id)
+        except ClientError as err:
+            raiseCLIError(err)
+        network_info.print_network(network)
+
+@command()
+class network_rename(_init_cyclades):
+    """Update network name"""
+
+    def main(self, network_id, new_name):
+        super(self.__class__, self).main()
+        try:
+            self.client.update_network_name(network_id, new_name)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class network_delete(_init_cyclades):
+    """Delete a network"""
+
+    def main(self, network_id):
+        super(self.__class__, self).main()
+        try:
+            self.client.delete_network(network_id)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class network_connect(_init_cyclades):
+    """Connect a server to a network"""
+
+    def main(self, server_id, network_id):
+        super(self.__class__, self).main()
+        try:
+            self.client.connect_server(server_id, network_id)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class network_disconnect(_init_cyclades):
+    """Disconnect a nic that connects a server to a network"""
+
+    def main(self, nic_id):
+        super(self.__class__, self).main()
+        try:
+            server_id = nic_id.split('-')[1]
+            self.client.disconnect_server(server_id, nic_id)
+        except IndexError:
+            raise CLIError(message='Incorrect nic format', importance=1,
+                details='nid_id format: nic-<server_id>-<nic_index>')
+        except ClientError as err:
+            raiseCLIError(err)
diff --git a/kamaki/cli/commands/image_cli.py b/kamaki/cli/commands/image_cli.py
new file mode 100644
index 0000000..7b3de32
--- /dev/null
+++ b/kamaki/cli/commands/image_cli.py
@@ -0,0 +1,212 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.command
+
+from kamaki.cli import command, set_api_description
+from kamaki.cli.utils import print_dict, print_items, raiseCLIError
+set_api_description('image', "Compute/Cyclades or Glance API image commands")
+from kamaki.clients.image import ImageClient, ClientError
+
+class _init_image(object):
+    def main(self):
+        try:
+            token = self.config.get('image', 'token') or self.config.get('global', 'token')
+            base_url = self.config.get('image', 'url') or self.config.get('global', 'url')
+            self.client = ImageClient(base_url=base_url, token=token)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class image_public(_init_image):
+    """List public images"""
+
+    def update_parser(self, parser):
+        parser.add_argument('-l', dest='detail', action='store_true',
+                default=False, help='show detailed output')
+        parser.add_argument('--container-format', dest='container_format',
+                metavar='FORMAT', help='filter by container format')
+        parser.add_argument('--disk-format', dest='disk_format',
+                metavar='FORMAT', help='filter by disk format')
+        parser.add_argument('--name', dest='name', metavar='NAME',
+                help='filter by name')
+        parser.add_argument('--size-min', dest='size_min', metavar='BYTES',
+                help='filter by minimum size')
+        parser.add_argument('--size-max', dest='size_max', metavar='BYTES',
+                help='filter by maximum size')
+        parser.add_argument('--status', dest='status', metavar='STATUS',
+                help='filter by status')
+        parser.add_argument('--order', dest='order', metavar='FIELD',
+                help='order by FIELD (use a - prefix to reverse order)')
+
+    def main(self):
+    	super(self.__class__, self).main()
+        filters = {}
+        for filter in ('container_format', 'disk_format', 'name', 'size_min',
+                       'size_max', 'status'):
+            val = getattr(self.args, filter, None)
+            if val is not None:
+                filters[filter] = val
+
+        order = self.args.order or ''
+        try:
+            images = self.client.list_public(self.args.detail,
+                filters=filters, order=order)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_items(images, title=('name',))
+
+@command()
+class image_meta(_init_image):
+    """Get image metadata"""
+
+    def main(self, image_id):
+    	super(self.__class__, self).main()
+        try:
+            image = self.client.get_meta(image_id)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(image)
+
+@command()
+class image_register(_init_image):
+    """Register an image"""
+
+    def update_parser(self, parser):
+        parser.add_argument('--checksum', dest='checksum', metavar='CHECKSUM',
+                help='set image checksum')
+        parser.add_argument('--container-format', dest='container_format',
+                metavar='FORMAT', help='set container format')
+        parser.add_argument('--disk-format', dest='disk_format',
+                metavar='FORMAT', help='set disk format')
+        parser.add_argument('--id', dest='id',
+                metavar='ID', help='set image ID')
+        parser.add_argument('--owner', dest='owner',
+                metavar='USER', help='set image owner (admin only)')
+        parser.add_argument('--property', dest='properties', action='append',
+                metavar='KEY=VAL',
+                help='add a property (can be used multiple times)')
+        parser.add_argument('--public', dest='is_public', action='store_true',
+                help='mark image as public')
+        parser.add_argument('--size', dest='size', metavar='SIZE',
+                help='set image size')
+
+    def main(self, name, location):
+    	super(self.__class__, self).main()
+        if not location.startswith('pithos://'):
+            account = self.config.get('storage', 'account').split()[0]
+            if account[-1] == '/':
+                account = account[:-1]
+            container = self.config.get('storage', 'container')
+            location = 'pithos://%s/%s'%(account, location) \
+                if container is None or len(container) == 0 \
+                else 'pithos://%s/%s/%s' % (account, container, location)
+
+        params = {}
+        for key in ('checksum', 'container_format', 'disk_format', 'id',
+                    'owner', 'size'):
+            val = getattr(self.args, key)
+            if val is not None:
+                params[key] = val
+
+        if self.args.is_public:
+            params['is_public'] = 'true'
+
+        properties = {}
+        for property in self.args.properties or []:
+            key, sep, val = property.partition('=')
+            if not sep:
+                raise CLIError(message="Invalid property '%s'" % property, importance=1)
+            properties[key.strip()] = val.strip()
+
+        try:
+            self.client.register(name, location, params, properties)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class image_members(_init_image):
+    """Get image members"""
+
+    def main(self, image_id):
+    	super(self.__class__, self).main()
+        try:
+            members = self.client.list_members(image_id)
+        except ClientError as err:
+            raiseCLIError(err)
+        for member in members:
+            print(member['member_id'])
+
+@command()
+class image_shared(_init_image):
+    """List shared images"""
+
+    def main(self, member):
+    	super(self.__class__, self).main()
+        try:
+            images = self.client.list_shared(member)
+        except ClientError as err:
+            raiseCLIError(err)
+        for image in images:
+            print(image['image_id'])
+
+@command()
+class image_addmember(_init_image):
+    """Add a member to an image"""
+
+    def main(self, image_id, member):
+    	super(self.__class__, self).main()
+        try:
+            self.client.add_member(image_id, member)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class image_delmember(_init_image):
+    """Remove a member from an image"""
+
+    def main(self, image_id, member):
+        super(self.__class__, self).main()
+        try:
+            self.client.remove_member(image_id, member)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class image_setmembers(_init_image):
+    """Set the members of an image"""
+
+    def main(self, image_id, *member):
+        super(self.__class__, self).main()
+        try:
+            self.client.set_members(image_id, member)
+        except ClientError as err:
+            raiseCLIError(err)
diff --git a/kamaki/cli/commands/pithos_cli.py b/kamaki/cli/commands/pithos_cli.py
new file mode 100644
index 0000000..36fabf7
--- /dev/null
+++ b/kamaki/cli/commands/pithos_cli.py
@@ -0,0 +1,1002 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.command
+
+from kamaki.cli import command, set_api_description, CLIError
+from kamaki.clients.utils import filter_in
+from kamaki.cli.utils import format_size, raiseCLIError, print_dict, pretty_keys, print_list
+set_api_description('store', 'Pithos+ storage commands')
+from kamaki.clients.pithos import PithosClient, ClientError
+from colors import bold
+from sys import stdout, exit
+import signal
+
+from progress.bar import IncrementalBar
+
+
+class ProgressBar(IncrementalBar):
+    #suffix = '%(percent)d%% - %(eta)ds'
+    suffix = '%(percent)d%%'
+
+class _pithos_init(object):
+    def main(self):
+        self.token = self.config.get('store', 'token') or self.config.get('global', 'token')
+        self.base_url = self.config.get('store', 'url') or self.config.get('global', 'url')
+        self.account = self.config.get('store', 'account') or self.config.get('global', 'account')
+        self.container = self.config.get('store', 'container') or self.config.get('global', 'container')
+        self.client = PithosClient(base_url=self.base_url, token=self.token, account=self.account,
+            container=self.container)
+
+class _store_account_command(_pithos_init):
+    """Base class for account level storage commands"""
+
+    def update_parser(self, parser):
+        parser.add_argument('--account', dest='account', metavar='NAME',
+                          help="Specify an account to use")
+
+    def progress(self, message):
+        """Return a generator function to be used for progress tracking"""
+
+        MESSAGE_LENGTH = 25
+
+        def progress_gen(n):
+            msg = message.ljust(MESSAGE_LENGTH)
+            for i in ProgressBar(msg).iter(range(n)):
+                yield
+            yield
+
+        return progress_gen
+
+    def main(self):
+        super(_store_account_command, self).main()
+        if hasattr(self.args, 'account') and self.args.account is not None:
+            self.client.account = self.args.account
+
+class _store_container_command(_store_account_command):
+    """Base class for container level storage commands"""
+
+    def __init__(self):
+        self.container = None
+        self.path = None
+
+    def update_parser(self, parser):
+        super(_store_container_command, self).update_parser(parser)
+        parser.add_argument('--container', dest='container', metavar='NAME', default=None,
+            help="Specify a container to use")
+
+    def extract_container_and_path(self, container_with_path, path_is_optional=True):
+        assert isinstance(container_with_path, str)
+        if ':' not in container_with_path:
+            if hasattr(self.args, 'container'):
+                self.container = getattr(self.args, 'container')
+            else:
+                self.container = self.client.container
+            if self.container is None:
+                self.container = container_with_path
+            else:
+                self.path = container_with_path
+            if not path_is_optional and self.path is None:
+                raise CLIError(message="Object path is missing", status=11)
+            return
+        cnp = container_with_path.split(':')
+        self.container = cnp[0]
+        try:
+            self.path = cnp[1]
+        except IndexError:
+            if path_is_optional:
+                self.path = None
+            else:
+                raise CLIError(message="Object path is missing", status=11)
+
+    def main(self, container_with_path=None, path_is_optional=True):
+        super(_store_container_command, self).main()
+        if container_with_path is not None:
+            self.extract_container_and_path(container_with_path, path_is_optional)
+            self.client.container = self.container
+        elif hasattr(self.args, 'container'):
+            self.client.container = getattr(self.args,'container')
+        self.container = self.client.container
+
+"""
+@command()
+class store_test(_store_container_command):
+    ""Test various stuff""
+
+    def main(self):
+        super(self.__class__, self).main('pithos')
+        r = self.client.list_containers()
+        for item in r:
+            if item['name'].startswith('c1_') or item['name'].startswith('c2_') \
+            or item['name'].startswith('c3_'):
+                self.client.container = item['name']
+                self.client.del_container(delimiter='/')
+                self.client.del_container()
+"""
+
+@command()
+class store_list(_store_container_command):
+    """List containers, object trees or objects in a directory
+    """
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('-l', action='store_true', dest='detail', default=False,
+            help='show detailed output')
+        parser.add_argument('-N', action='store', dest='show_size', default=1000,
+            help='print output in chunks of size N')
+        parser.add_argument('-n', action='store', dest='limit', default=None,
+            help='show limited output')
+        parser.add_argument('--marker', action='store', dest='marker', default=None,
+            help='show output greater then marker')
+        parser.add_argument('--prefix', action='store', dest='prefix', default=None,
+            help='show output starting with prefix')
+        parser.add_argument('--delimiter', action='store', dest='delimiter', default=None, 
+            help='show output up to the delimiter')
+        parser.add_argument('--path', action='store', dest='path', default=None, 
+            help='show output starting with prefix up to /')
+        parser.add_argument('--meta', action='store', dest='meta', default=None, 
+            help='show output having the specified meta keys (e.g. --meta "meta1 meta2 ..."')
+        parser.add_argument('--if-modified-since', action='store', dest='if_modified_since', 
+            default=None, help='show output if modified since then')
+        parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
+            default=None, help='show output if not modified since then')
+        parser.add_argument('--until', action='store', dest='until', default=None,
+            help='show metadata until that date')
+        dateformat = '%d/%m/%Y %H:%M:%S'
+        parser.add_argument('--format', action='store', dest='format', default=dateformat,
+            help='format to parse until date (default: d/m/Y H:M:S)')
+        parser.add_argument('--shared', action='store_true', dest='shared', default=False,
+            help='show only shared')
+        parser.add_argument('--public', action='store_true', dest='public', default=False,
+            help='show only public')
+
+    def print_objects(self, object_list):
+        import sys
+        try:
+            limit = getattr(self.args, 'show_size')
+            limit = int(limit)
+        except AttributeError:
+            pass
+        #index = 0
+        for index,obj in enumerate(object_list):
+            if not obj.has_key('content_type'):
+                continue
+            pretty_obj = obj.copy()
+            index += 1
+            empty_space = ' '*(len(str(len(object_list))) - len(str(index)))
+            if obj['content_type'] == 'application/directory':
+                isDir = True
+                size = 'D'
+            else:
+                isDir = False
+                size = format_size(obj['bytes'])
+                pretty_obj['bytes'] = '%s (%s)'%(obj['bytes'],size)
+            oname = bold(obj['name'])
+            if getattr(self.args, 'detail'):
+                print('%s%s. %s'%(empty_space, index, oname))
+                print_dict(pretty_keys(pretty_obj), exclude=('name'))
+                print
+            else:
+                oname = '%s%s. %6s %s'%(empty_space, index, size, oname)
+                oname += '/' if isDir else ''
+                print(oname)
+            if limit <= index < len(object_list) and index%limit == 0:
+                print('(press "enter" to continue)')
+                sys.stdin.read(1)
+
+    def print_containers(self, container_list):
+        import sys
+        try:
+            limit = getattr(self.args, 'show_size')
+            limit = int(limit)
+        except AttributeError:
+            pass
+        for index,container in enumerate(container_list):
+            if container.has_key('bytes'):
+                size = format_size(container['bytes']) 
+            cname = '%s. %s'%(index+1, bold(container['name']))
+            if getattr(self.args, 'detail'):
+                print(cname)
+                pretty_c = container.copy()
+                if container.has_key('bytes'):
+                    pretty_c['bytes'] = '%s (%s)'%(container['bytes'], size)
+                print_dict(pretty_keys(pretty_c), exclude=('name'))
+                print
+            else:
+                if container.has_key('count') and container.has_key('bytes'):
+                    print('%s (%s, %s objects)' % (cname, size, container['count']))
+                else:
+                    print(cname)
+            if limit <= index < len(container_list) and index%limit == 0:
+                print('(press "enter" to continue)')
+                sys.stdin.read(1)
+
+    def getuntil(self, orelse=None):
+        if hasattr(self.args, 'until'):
+            import time
+            until = getattr(self.args, 'until')
+            if until is None:
+                return None
+            format = getattr(self.args, 'format')
+            #except TypeError:
+            try:
+                t = time.strptime(until, format)
+            except ValueError as err:
+                raise CLIError(message='in --until: '+unicode(err), importance=1)
+            return int(time.mktime(t))
+        return orelse
+   
+    def getmeta(self, orelse=[]):
+        if hasattr(self.args, 'meta'):
+            meta = getattr(self.args, 'meta')
+            if meta is None:
+                return []
+            return meta.split(' ')
+        return orelse
+
+    def getpath(self, orelse=None):
+        if self.path is not None:
+            return self.path
+        if hasattr(self.args, 'path'):
+            return getattr(self.args, 'path')
+        return orelse
+
+    def main(self, container____path__=None):
+        super(self.__class__, self).main(container____path__)
+        try:
+            if self.container is None:
+                r = self.client.account_get(limit=getattr(self.args, 'limit', None),
+                    marker=getattr(self.args, 'marker', None),
+                    if_modified_since=getattr(self.args, 'if_modified_since', None),
+                    if_unmodified_since=getattr(self.args, 'if_unmodified_since', None),
+                    until=self.getuntil(),
+                    show_only_shared=getattr(self.args, 'shared', False))
+                self.print_containers(r.json)
+            else:
+                r = self.client.container_get(limit=getattr(self.args, 'limit', None),
+                    marker=getattr(self.args, 'marker', None),
+                    prefix=getattr(self.args, 'prefix', None),
+                    delimiter=getattr(self.args, 'delimiter', None), path=self.getpath(orelse=None),
+                    if_modified_since=getattr(self.args, 'if_modified_since', None),
+                    if_unmodified_since=getattr(self.args, 'if_unmodified_since', None),
+                    until=self.getuntil(),
+                    meta=self.getmeta(),
+                    show_only_shared=getattr(self.args, 'shared', False))
+                self.print_objects(r.json)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_mkdir(_store_container_command):
+    """Create a directory"""
+
+    def main(self, container___directory):
+        super(self.__class__, self).main(container___directory, path_is_optional=False)
+        try:
+            self.client.create_directory(self.path)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_create(_store_container_command):
+    """Create a container or a directory object"""
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('--versioning', action='store', dest='versioning', default=None,
+            help='set container versioning (auto/none)')
+        parser.add_argument('--quota', action='store', dest='quota', default=None,
+            help='set default container quota')
+        parser.add_argument('--meta', action='store', dest='meta', default=None,
+            help='set container metadata ("key1:val1 key2:val2 ...")')
+
+    def getmeta(self, orelse=None):
+        try:
+            meta = getattr(self.args,'meta')
+            metalist = meta.split(' ')
+        except AttributeError:
+            return orelse
+        metadict = {}
+        for metastr in metalist:
+            (key,val) = metastr.split(':')
+            metadict[key] = val
+        return metadict
+
+    def main(self, container____directory__):
+        super(self.__class__, self).main(container____directory__)
+        try:
+            if self.path is None:
+                self.client.container_put(quota=getattr(self.args, 'quota'),
+                    versioning=getattr(self.args, 'versioning'), metadata=self.getmeta())
+            else:
+                self.client.create_directory(self.path)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_copy(_store_container_command):
+    """Copy an object"""
+
+    def update_parser(self, parser):
+        super(store_copy, self).update_parser(parser)
+        parser.add_argument('--source-version', action='store', dest='source_version', default=None,
+            help='copy specific version')
+        parser.add_argument('--public', action='store_true', dest='public', default=False,
+            help='make object publicly accessible')
+        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
+            help='change object\'s content type')
+        parser.add_argument('--delimiter', action='store', dest='delimiter', default=None,
+            help=u'mass copy objects with path staring with src_object + delimiter')
+        parser.add_argument('-r', action='store_true', dest='recursive', default=False,
+            help='mass copy with delimiter /')
+
+    def getdelimiter(self):
+        if getattr(self.args, 'recursive'):
+            return '/'
+        return getattr(self.args, 'delimiter')
+
+    def main(self, source_container___path, destination_container____path__):
+        super(self.__class__, self).main(source_container___path, path_is_optional=False)
+        try:
+            dst = destination_container____path__.split(':')
+            dst_cont = dst[0]
+            dst_path = dst[1] if len(dst) > 1 else False
+            self.client.copy_object(src_container = self.container, src_object = self.path,
+                dst_container = dst_cont, dst_object = dst_path,
+                source_version=getattr(self.args, 'source_version'),
+                public=getattr(self.args, 'public'),
+                content_type=getattr(self.args,'content_type'), delimiter=self.getdelimiter())
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_move(_store_container_command):
+    """Copy an object"""
+
+    def update_parser(self, parser):
+        super(store_move, self).update_parser(parser)
+        parser.add_argument('--source-version', action='store', dest='source_version', default=None,
+            help='copy specific version')
+        parser.add_argument('--public', action='store_true', dest='public', default=False,
+            help='make object publicly accessible')
+        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
+            help='change object\'s content type')
+        parser.add_argument('--delimiter', action='store', dest='delimiter', default=None,
+            help=u'mass copy objects with path staring with src_object + delimiter')
+        parser.add_argument('-r', action='store_true', dest='recursive', default=False,
+            help='mass copy with delimiter /')
+
+    def getdelimiter(self):
+        if getattr(self.args, 'recursive'):
+            return '/'
+        return getattr(self.args, 'delimiter')
+
+    def main(self, source_container___path, destination_container____path__):
+        super(self.__class__, self).main(source_container___path, path_is_optional=False)
+        try:
+            dst = destination_container____path__.split(':')
+            dst_cont = dst[0]
+            dst_path = dst[1] if len(dst) > 1 else False
+            self.client.move_object(src_container = self.container, src_object = self.path,
+                dst_container = dst_cont, dst_object = dst_path,
+                source_version=getattr(self.args, 'source_version'),
+                public=getattr(self.args, 'public'),
+                content_type=getattr(self.args,'content_type'), delimiter=self.getdelimiter())
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_append(_store_container_command):
+    """Append local file to (existing) remote object"""
+
+    def main(self, local_path, container___path):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            f = open(local_path, 'r')
+            upload_cb = self.progress('Appending blocks')
+            self.client.append_object(object=self.path, source_file = f, upload_cb = upload_cb)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_truncate(_store_container_command):
+    """Truncate remote file up to a size"""
+
+    
+    def main(self, container___path, size=0):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            self.client.truncate_object(self.path, size)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_overwrite(_store_container_command):
+    """Overwrite part (from start to end) of a remote file"""
+
+    def main(self, local_path, container___path, start, end):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            f = open(local_path, 'r')
+            upload_cb = self.progress('Overwritting blocks')
+            self.client.overwrite_object(object=self.path, start=start, end=end,
+                source_file=f, upload_cb = upload_cb)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_manifest(_store_container_command):
+    """Create a remote file with uploaded parts by manifestation"""
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('--etag', action='store', dest='etag', default=None,
+            help='check written data')
+        parser.add_argument('--content-encoding', action='store', dest='content_encoding',
+            default=None, help='provide the object MIME content type')
+        parser.add_argument('--content-disposition', action='store', dest='content_disposition',
+            default=None, help='provide the presentation style of the object')
+        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
+            help='create object with specific content type')
+        parser.add_argument('--sharing', action='store', dest='sharing', default=None,
+            help='define sharing object policy ( "read=user1,grp1,user2,... write=user1,grp2,...')
+        parser.add_argument('--public', action='store_true', dest='public', default=False,
+            help='make object publicly accessible')
+
+    def getsharing(self, orelse={}):
+        permstr = getattr(self.args, 'sharing')
+        if permstr is None:
+            return orelse
+        perms = {}
+        for p in permstr.split(' '):
+            (key, val) = p.split('=')
+            if key.lower() not in ('read', 'write'):
+                raise CLIError(message='in --sharing: Invalid permition key', importance=1)
+            val_list = val.split(',')
+            if not perms.has_key(key):
+                perms[key]=[]
+            for item in val_list:
+                if item not in perms[key]:
+                    perms[key].append(item)
+        return perms
+        
+    def main(self, container___path):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            self.client.create_object_by_manifestation(self.path,
+                content_encoding=getattr(self.args, 'content_encoding'),
+                content_disposition=getattr(self.args, 'content_disposition'),
+                content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
+                public=getattr(self.args, 'public'))
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_upload(_store_container_command):
+    """Upload a file"""
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('--use_hashes', action='store_true', dest='use_hashes', default=False,
+            help='provide hashmap file instead of data')
+        parser.add_argument('--unchunked', action='store_true', dest='unchunked', default=False,
+            help='avoid chunked transfer mode')
+        parser.add_argument('--etag', action='store', dest='etag', default=None,
+            help='check written data')
+        parser.add_argument('--content-encoding', action='store', dest='content_encoding',
+            default=None, help='provide the object MIME content type')
+        parser.add_argument('--content-disposition', action='store', dest='content_disposition',
+            default=None, help='provide the presentation style of the object')
+        parser.add_argument('--content-type', action='store', dest='content_type', default=None,
+            help='create object with specific content type')
+        parser.add_argument('--sharing', action='store', dest='sharing', default=None,
+            help='define sharing object policy ( "read=user1,grp1,user2,... write=user1,grp2,...')
+        parser.add_argument('--public', action='store_true', dest='public', default=False,
+            help='make object publicly accessible')
+
+    def getsharing(self, orelse={}):
+        permstr = getattr(self.args, 'sharing')
+        if permstr is None:
+            return orelse
+        perms = {}
+        for p in permstr.split(' '):
+            (key, val) = p.split('=')
+            if key.lower() not in ('read', 'write'):
+                raise CLIError(message='in --sharing: Invalid permition key', importance=1)
+            val_list = val.split(',')
+            if not perms.has_key(key):
+                perms[key]=[]
+            for item in val_list:
+                if item not in perms[key]:
+                    perms[key].append(item)
+        return perms
+
+    def main(self, local_path, container____path__):
+        super(self.__class__, self).main(container____path__)
+        remote_path = local_path if self.path is None else self.path
+        try:
+            with open(local_path) as f:
+                if getattr(self.args, 'unchunked'):
+                    self.client.upload_object_unchunked(remote_path, f,
+                    etag=getattr(self.args, 'etag'), withHashFile=getattr(self.args, 'use_hashes'),
+                    content_encoding=getattr(self.args, 'content_encoding'),
+                    content_disposition=getattr(self.args, 'content_disposition'),
+                    content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
+                    public=getattr(self.args, 'public'))
+                else:
+                    hash_cb = self.progress('Calculating block hashes')
+                    upload_cb = self.progress('Uploading blocks')
+                    self.client.upload_object(remote_path, f, hash_cb=hash_cb, upload_cb=upload_cb,
+                    content_encoding=getattr(self.args, 'content_encoding'),
+                    content_disposition=getattr(self.args, 'content_disposition'),
+                    content_type=getattr(self.args, 'content_type'), sharing=self.getsharing(),
+                    public=getattr(self.args, 'public'))
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_download(_store_container_command):
+    """Download a file"""
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('--no-progress-bar', action='store_true', dest='no_progress_bar',
+            default=False, help='Dont display progress bars')
+        parser.add_argument('--overide', action='store_true', dest='overide', default=False,
+            help='Force download to overide an existing file')
+        parser.add_argument('--range', action='store', dest='range', default=None,
+            help='show range of data')
+        parser.add_argument('--if-match', action='store', dest='if_match', default=None,
+            help='show output if ETags match')
+        parser.add_argument('--if-none-match', action='store', dest='if_none_match', default=None,
+            help='show output if ETags don\'t match')
+        parser.add_argument('--if-modified-since', action='store', dest='if_modified_since',
+            default=None, help='show output if modified since then')
+        parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
+            default=None, help='show output if not modified since then')
+        parser.add_argument('--object-version', action='store', dest='object_version', default=None,
+            help='get the specific version')
+
+    def main(self, container___path, local_path=None):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+
+        #setup output stream
+        if local_path is None:
+            out = stdout
+        else:
+            try:
+                if getattr(self.args, 'overide'):
+                    out = open(local_path, 'wb+')
+                else:
+                    out = open(local_path, 'ab+')
+            except IOError as err:
+                raise CLIError(message='Cannot write to file %s - %s'%(local_path,unicode(err)),
+                    importance=1)
+        download_cb = None if getattr(self.args, 'no_progress_bar') \
+            else self.progress('Downloading')
+
+        try:
+            self.client.download_object(self.path, out, download_cb, 
+                range=getattr(self.args, 'range'), version=getattr(self.args,'object_version'),
+                if_match=getattr(self.args, 'if_match'), overide=getattr(self.args, 'overide'),
+                if_none_match=getattr(self.args, 'if_none_match'),
+                if_modified_since=getattr(self.args, 'if_modified_since'),
+                if_unmodified_since=getattr(self.args, 'if_unmodified_since'))
+        except ClientError as err:
+            raiseCLIError(err)
+        except KeyboardInterrupt:
+            print('\ndownload canceled by user')
+            if local_path is not None:
+                print('re-run command to resume')
+        print
+
+@command()
+class store_hashmap(_store_container_command):
+    """Get the hashmap of an object"""
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('--if-match', action='store', dest='if_match', default=None,
+            help='show output if ETags match')
+        parser.add_argument('--if-none-match', action='store', dest='if_none_match', default=None,
+            help='show output if ETags dont match')
+        parser.add_argument('--if-modified-since', action='store', dest='if_modified_since',
+            default=None, help='show output if modified since then')
+        parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
+            default=None, help='show output if not modified since then')
+        parser.add_argument('--object-version', action='store', dest='object_version', default=None,
+            help='get the specific version')
+
+    def main(self, container___path):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            data = self.client.get_object_hashmap(self.path,
+                version=getattr(self.args, 'object_version'),
+                if_match=getattr(self.args, 'if_match'),
+                if_none_match=getattr(self.args, 'if_none_match'),
+                if_modified_since=getattr(self.args, 'if_modified_since'),
+                if_unmodified_since=getattr(self.args, 'if_unmodified_since'))
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(data)
+
+@command()
+class store_delete(_store_container_command):
+    """Delete a container [or an object]"""
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('--until', action='store', dest='until', default=None,
+            help='remove history until that date')
+        parser.add_argument('--format', action='store', dest='format', default='%d/%m/%Y %H:%M:%S',
+            help='format to parse until date (default: d/m/Y H:M:S)')
+        parser.add_argument('--delimiter', action='store', dest='delimiter',
+            default=None, 
+            help='mass delete objects with path staring with <object><delimiter>')
+        parser.add_argument('-r', action='store_true', dest='recursive', default=False,
+            help='empty dir or container and delete (if dir)')
+    
+    def getuntil(self, orelse=None):
+        if hasattr(self.args, 'until'):
+            import time
+            until = getattr(self.args, 'until')
+            if until is None:
+                return None
+            format = getattr(self.args, 'format')
+            try:
+                t = time.strptime(until, format)
+            except ValueError as err:
+                raise CLIError(message='in --until: '+unicode(err), importance=1)
+            return int(time.mktime(t))
+        return orelse
+
+    def getdelimiter(self, orelse=None):
+        try:
+            dlm = getattr(self.args, 'delimiter')
+            if dlm is None:
+                return '/' if getattr(self.args, 'recursive') else orelse
+        except AttributeError:
+            return orelse
+        return dlm
+
+    def main(self, container____path__):
+        super(self.__class__, self).main(container____path__)
+        try:
+            if self.path is None:
+                self.client.del_container(until=self.getuntil(), delimiter=self.getdelimiter())
+            else:
+                #self.client.delete_object(self.path)
+                self.client.del_object(self.path, until=self.getuntil(),
+                    delimiter=self.getdelimiter())
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_purge(_store_container_command):
+    """Purge a container"""
+    
+    def main(self, container):
+        super(self.__class__, self).main()
+        try:
+            self.client.purge_container()
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_publish(_store_container_command):
+    """Publish an object"""
+
+    def main(self, container___path):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            self.client.publish_object(self.path)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_unpublish(_store_container_command):
+    """Unpublish an object"""
+
+    def main(self, container___path):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            self.client.unpublish_object(self.path)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_permitions(_store_container_command):
+    """Get object read/write permitions"""
+
+    def main(self, container___path):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            reply = self.client.get_object_sharing(self.path)
+            print_dict(reply)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_setpermitions(_store_container_command):
+    """Set sharing permitions"""
+
+    def format_permition_dict(self,permitions):
+        read = False
+        write = False
+        for perms in permitions:
+            splstr = perms.split('=')
+            if 'read' == splstr[0]:
+                read = [user_or_group.strip() \
+                for user_or_group in splstr[1].split(',')]
+            elif 'write' == splstr[0]:
+                write = [user_or_group.strip() \
+                for user_or_group in splstr[1].split(',')]
+            else:
+                read = False
+                write = False
+        if not read and not write:
+            raise CLIError(message='Usage:\tread=<groups,users> write=<groups,users>',
+                importance=0)
+        return (read,write)
+
+    def main(self, container___path, *permitions):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        (read, write) = self.format_permition_dict(permitions)
+        try:
+            self.client.set_object_sharing(self.path,
+                read_permition=read, write_permition=write)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_delpermitions(_store_container_command):
+    """Delete all sharing permitions"""
+
+    def main(self, container___path):
+        super(self.__class__, self).main(container___path, path_is_optional=False)
+        try:
+            self.client.del_object_sharing(self.path)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_info(_store_container_command):
+    """Get information for account [, container [or object]]"""
+
+    
+    def main(self, container____path__=None):
+        super(self.__class__, self).main(container____path__)
+        try:
+            if self.container is None:
+                reply = self.client.get_account_info()
+            elif self.path is None:
+                reply = self.client.get_container_info(self.container)
+            else:
+                reply = self.client.get_object_info(self.path)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class store_meta(_store_container_command):
+    """Get custom meta-content for account [, container [or object]]"""
+
+    def update_parser(self, parser):
+        super(self.__class__, self).update_parser(parser)
+        parser.add_argument('-l', action='store_true', dest='detail', default=False,
+            help='show detailed output')
+        parser.add_argument('--until', action='store', dest='until', default=None,
+            help='show metadata until that date')
+        dateformat='%d/%m/%Y %H:%M:%S'
+        parser.add_argument('--format', action='store', dest='format', default=dateformat,
+            help='format to parse until date (default: "d/m/Y H:M:S")')
+        parser.add_argument('--object_version', action='store', dest='object_version', default=None,
+            help='show specific version \ (applies only for objects)')
+
+    def getuntil(self, orelse=None):
+        if hasattr(self.args, 'until'):
+            import time
+            until = getattr(self.args, 'until')
+            if until is None:
+                return None
+            format = getattr(self.args, 'format')
+            #except TypeError:
+            try:
+                t = time.strptime(until, format)
+            except ValueError as err:
+                raise CLIError(message='in --until: '+unicode(err), importance=1)
+            return int(time.mktime(t))
+        return orelse
+
+    def main(self, container____path__ = None):
+        super(self.__class__, self).main(container____path__)
+
+        detail = getattr(self.args, 'detail')
+        try:
+            if self.container is None:
+                print(bold(self.client.account))
+                if detail:
+                    reply = self.client.get_account_info(until=self.getuntil())
+                else:
+                    reply = self.client.get_account_meta(until=self.getuntil())
+                    reply = pretty_keys(reply, '-')
+            elif self.path is None:
+                print(bold(self.client.account+': '+self.container))
+                if detail:
+                    reply = self.client.get_container_info(until = self.getuntil())
+                else:
+                    cmeta = self.client.get_container_meta(until=self.getuntil())
+                    ometa = self.client.get_container_object_meta(until=self.getuntil())
+                    reply = {'container-meta':pretty_keys(cmeta, '-'),
+                        'object-meta':pretty_keys(ometa, '-')}
+            else:
+                print(bold(self.client.account+': '+self.container+':'+self.path))
+                version=getattr(self.args, 'object_version')
+                if detail:
+                    reply = self.client.get_object_info(self.path, version = version)
+                else:
+                    reply = self.client.get_object_meta(self.path, version=version)
+                    reply = pretty_keys(pretty_keys(reply, '-'))
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class store_setmeta(_store_container_command):
+    """Set a new metadatum for account [, container [or object]]"""
+
+    def main(self, metakey___metaval, container____path__=None):
+        super(self.__class__, self).main(container____path__)
+        try:
+            metakey, metavalue = metakey___metaval.split(':')
+        except ValueError:
+            raise CLIError(message='Meta variables should be formated as metakey:metavalue',
+                importance=1)
+        try:
+            if self.container is None:
+                self.client.set_account_meta({metakey:metavalue})
+            elif self.path is None:
+                self.client.set_container_meta({metakey:metavalue})
+            else:
+                self.client.set_object_meta(self.path, {metakey:metavalue})
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_delmeta(_store_container_command):
+    """Delete an existing metadatum of account [, container [or object]]"""
+
+    def main(self, metakey, container____path__=None):
+        super(self.__class__, self).main(container____path__)
+        try:
+            if self.container is None:
+                self.client.del_account_meta(metakey)
+            elif self.path is None:
+                self.client.del_container_meta(metakey)
+            else:
+                self.client.del_object_meta(metakey, self.path)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_quota(_store_account_command):
+    """Get  quota for account [or container]"""
+
+    def main(self, container = None):
+        super(self.__class__, self).main()
+        try:
+            if container is None:
+                reply = self.client.get_account_quota()
+            else:
+                reply = self.client.get_container_quota(container)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class store_setquota(_store_account_command):
+    """Set new quota (in KB) for account [or container]"""
+
+    def main(self, quota, container = None):
+        super(self.__class__, self).main()
+        try:
+            if container is None:
+                self.client.set_account_quota(quota)
+            else:
+                self.client.container = container
+                self.client.set_container_quota(quota)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_versioning(_store_account_command):
+    """Get  versioning for account [or container ]"""
+
+    def main(self, container = None):
+        super(self.__class__, self).main()
+        try:
+            if container is None:
+                reply = self.client.get_account_versioning()
+            else:
+                reply = self.client.get_container_versioning(container)
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class store_setversioning(_store_account_command):
+    """Set new versioning (auto, none) for account [or container]"""
+
+    def main(self, versioning, container = None):
+        super(self.__class__, self).main()
+        try:
+            if container is None:
+                self.client.set_account_versioning(versioning)
+            else:
+                self.client.container = container
+                self.client.set_container_versioning(versioning)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_group(_store_account_command):
+    """Get user groups details for account"""
+
+    def main(self):
+        super(self.__class__, self).main()
+        try:
+            reply = self.client.get_account_group()
+        except ClientError as err:
+            raiseCLIError(err)
+        print_dict(reply)
+
+@command()
+class store_setgroup(_store_account_command):
+    """Create/update a new user group on account"""
+
+    def main(self, groupname, *users):
+        super(self.__class__, self).main()
+        try:
+            self.client.set_account_group(groupname, users)
+        except ClientError as err:
+            raiseCLIError(err)
+
+@command()
+class store_delgroup(_store_account_command):
+    """Delete a user group on an account"""
+
+    def main(self, groupname):
+        super(self.__class__, self).main()
+        try:
+            self.client.del_account_group(groupname)
+        except ClientError as err:
+            raiseCLIError(err)
diff --git a/kamaki/cli/commands/pithos_sh_cli.py b/kamaki/cli/commands/pithos_sh_cli.py
new file mode 100644
index 0000000..99c1fba
--- /dev/null
+++ b/kamaki/cli/commands/pithos_sh_cli.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+# 
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+# 
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+# 
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# 
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from kamaki.cli import command, CLIError
+from kamaki.cli.utils import print_list, dict2file, list2file
+from .pithos_cli import _store_container_command, _store_account_command
+from colors import bold
+
+from sys import stdout#argv, exit, stdin, stdout
+
+from kamaki.clients.pithos_sh_lib.client import Pithos_Client, Fault
+from kamaki.clients.pithos_sh_lib.transfer import download, cat#,upload
+
+class _pithos_sh_account_command(_store_account_command):
+
+    def update_parser(self, parser):
+        super(_pithos_sh_account_command, self).update_parser(parser)
+
+    def main(self):
+        super(_pithos_sh_account_command, self).main()
+        self.client = Pithos_Client(self.base_url, self.token, self.account)
+
+class _pithos_sh_container_command(_store_container_command):
+
+    def update_parser(self, parser):
+        super(_pithos_sh_container_command, self).update_parser(parser)
+
+    def main(self, container_with_path, path_is_optional=True):
+        super(_pithos_sh_container_command, self).main(container_with_path, path_is_optional)
+        self.client = Pithos_Client(self.base_url, self.token, self.account)
+
+def _build_args(arglist, attrs):
+    args = {}
+    for a in [a for a in attrs if getattr(arglist, a)]:
+        args[a] = getattr(arglist, a)
+    return args
+
+@command()
+class store_versions(_pithos_sh_container_command):
+    """Get the version list of an object"""
+
+    def main(self, container___path):
+        super(store_versions, self).main(container___path)
+        try:
+            data = self.client.retrieve_object_versionlist(self.container, self.path)
+        except Fault as err:
+            raise CLIError(message=unicode(err), status=err.status)
+        from time import localtime, strftime
+        print('%s:%s version ids:'%(self.container,self.path))
+        for vitem in data['versions']:
+            t = localtime(float(vitem[1]))
+            vid = bold(unicode(vitem[0]))
+            print('\t%s \t(%s)'%(vid, strftime('%d-%m-%Y %H:%M:%S', t)))
+
+@command()
+class store_sharers(_pithos_sh_account_command):
+    """list accounts who share objects with current account"""
+    
+    def update_parser(self, parser):
+        super(store_sharers, self).update_parser(parser)
+        parser.add_argument('-l', action='store_true', dest='detail', default=False,
+            help='show detailed output')
+        parser.add_argument('-n', action='store',  dest='limit', default=10000,
+            help='show limited output')
+        parser.add_argument('--marker', action='store', dest='marker', default=None,
+            help='show output greater then marker')
+        
+    def main(self):
+        super(store_sharers, self).main()
+        attrs = ['limit', 'marker']
+        args = _build_args(self.args, attrs)
+        args['format'] = 'json' if getattr(self.args, 'detail') else 'text'
+    
+        try:    
+            print_list(self.client.list_shared_by_others(**args))
+        except Fault as err:
+            raise CLIError(message=unicode(err), status=err.status, importance=err.status/100)
diff --git a/kamaki/cli/config.py b/kamaki/cli/config.py
new file mode 100644
index 0000000..ced5b8a
--- /dev/null
+++ b/kamaki/cli/config.py
@@ -0,0 +1,119 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+import os
+
+from collections import defaultdict
+from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    from ordereddict import OrderedDict
+
+
+# Path to the file that stores the configuration
+CONFIG_PATH = os.path.expanduser('~/.kamakirc')
+
+# Name of a shell variable to bypass the CONFIG_PATH value
+CONFIG_ENV = 'KAMAKI_CONFIG'
+
+HEADER = """
+# Kamaki configuration file
+"""
+
+DEFAULTS = {
+    'global': {
+        'colors': 'on',
+        'token': ''
+    },
+    'config': {
+        'cli': 'config_cli',
+        'description': 'Configuration commands'
+    }
+}
+
+
+class Config(RawConfigParser):
+    def __init__(self, path=None):
+        RawConfigParser.__init__(self, dict_type=OrderedDict)
+        self.path = path or os.environ.get(CONFIG_ENV, CONFIG_PATH)
+        self._overrides = defaultdict(dict)
+        self._load_defaults()
+        self.read(self.path)
+
+    def _load_defaults(self):
+        for section, options in DEFAULTS.items():
+            for option, val in options.items():
+                self.set(section, option, val)
+
+    def apis(self):
+        return [api for api in self.sections() if api != 'global']
+
+    def get(self, section, option):
+        value = self._overrides.get(section, {}).get(option)
+        if value is not None:
+            return value
+
+        try:
+            return RawConfigParser.get(self, section, option)
+        except (NoSectionError, NoOptionError):
+            return DEFAULTS.get(section, {}).get(option)
+
+    def set(self, section, option, value):
+        if section not in RawConfigParser.sections(self):
+            self.add_section(section)
+        RawConfigParser.set(self, section, option, value)
+
+    def remove_option(self, section, option):
+        try:
+            RawConfigParser.remove_option(self, section, option)
+        except NoSectionError:
+            pass
+
+    def items(self, section, include_defaults=False):
+        d = dict(DEFAULTS[section]) if include_defaults else {}
+        try:
+            d.update(RawConfigParser.items(self, section))
+        except NoSectionError:
+            pass
+        return d.items()
+
+    def override(self, section, option, value):
+        self._overrides[section][option] = value
+
+    def write(self):
+        with open(self.path, 'w') as f:
+            os.chmod(self.path, 0600)
+            f.write(HEADER.lstrip())
+            RawConfigParser.write(self, f)
diff --git a/kamaki/cli/utils.py b/kamaki/cli/utils.py
new file mode 100644
index 0000000..509fa34
--- /dev/null
+++ b/kamaki/cli/utils.py
@@ -0,0 +1,155 @@
+# Copyright 2011 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+from . import CLIError
+from colors import underline
+
+def raiseCLIError(err, importance = -1):
+    if importance < 0:
+        if err.status <= 0:
+            importance = 0
+        elif err.status <= 400:
+            importance = 1
+        elif err.status <= 500:
+            importance = 2
+        else:
+            importance = 3
+    raise CLIError(err.message, err.status, err.details, importance)
+
+def pretty_keys(d, delim='_', recurcive=False):
+    """Transform keys of a dict from the form
+    str1_str2_..._strN to the form strN
+    where _ is the delimeter
+    """
+    new_d = {}
+    for key, val in d.items():
+        new_key = key.split(delim)[-1]
+        if recurcive and isinstance(val, dict):
+            new_val = pretty_keys(val, delim, recurcive) 
+        else:
+            new_val = val
+        new_d[new_key] = new_val
+    return new_d
+
+def print_dict(d, exclude=(), ident= 0):
+    if not isinstance(d, dict):
+        raise CLIError(message='Cannot dict_print a non-dict object')
+    try:
+        margin = max(
+            1 + max(len(unicode(key).strip()) for key in d.keys() \
+                if not isinstance(key, dict) and not isinstance(key, list)),
+            ident)
+    except ValueError:
+        margin = ident
+
+    for key, val in sorted(d.items()):
+        if key in exclude:
+            continue
+        print_str = '%s:' % unicode(key).strip()
+        if isinstance(val, dict):
+            print(print_str.rjust(margin)+' {')
+            print_dict(val, exclude = exclude, ident = margin + 6)
+            print '}'.rjust(margin)
+        elif isinstance(val, list):
+            print(print_str.rjust(margin)+' [')
+            print_list(val, exclude = exclude, ident = margin + 6)
+            print ']'.rjust(margin)
+        else:
+            print print_str.rjust(margin)+' '+unicode(val).strip()
+
+def print_list(l, exclude=(), ident = 0):
+    if not isinstance(l, list):
+        raise CLIError(message='Cannot list_print a non-list object')
+    try:
+        margin = max(
+            1 + max(len(unicode(item).strip()) for item in l \
+                if not isinstance(item, dict) and not isinstance(item, list)),
+            ident)
+    except ValueError:
+        margin = ident
+
+    for item in sorted(l):
+        if item in exclude:
+            continue
+        if isinstance(item, dict):
+            print('{'.rjust(margin))
+            print_dict(item, exclude = exclude, ident = margin + 6)
+            print '}'.rjust(margin)
+        elif isinstance(item, list):
+            print '['.rjust(margin)
+            print_list(item, exclude = exclude, ident = margin + 6)
+            print ']'.rjust(margin)
+        else:
+            print unicode(item).rjust(margin)
+
+def print_items(items, title=('id', 'name')):
+    for item in items:
+        if isinstance(item, dict) or isinstance(item, list):
+            print ' '.join(unicode(item.pop(key)) for key in title if key in item)
+        if isinstance(item, dict):
+            print_dict(item)
+
+def format_size(size):
+    units = ('B', 'K', 'M', 'G', 'T')
+    try:
+        size = float(size)
+    except ValueError:
+        raise CLIError(message='Cannot format %s in bytes'%size)
+    for unit in units:
+        if size < 1024:
+            break
+        size /= 1024
+    s = ('%.1f' % size)
+    if '.0' == s[-2:]:
+        s = s[:-2]
+    return s + unit
+
+def dict2file(d, f, depth = 0):
+    for k, v in d.items():
+        f.write('%s%s: '%('\t'*depth, k))
+        if type(v) is dict:
+            f.write('\n')
+            dict2file(v, f, depth+1)
+        elif type(v) is list:
+            f.write('\n')
+            list2file(v, f, depth+1)
+        else:
+            f.write(' %s\n'%unicode(v))
+
+def list2file(l, f, depth = 1):
+    for item in l:
+        if type(item) is dict:
+            dict2file(item, f, depth+1)
+        elif type(item) is list:
+            list2file(item, f, depth+1)
+        else:
+            f.write('%s%s\n'%('\t'*depth, unicode(item)))
\ No newline at end of file
