Revision 7493ccb6

b/kamaki/cli/__init__.py
1
#!/usr/bin/env python
2

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

  
36
from __future__ import print_function
37

  
38
import gevent.monkey
39
#Monkey-patch everything for gevent early on
40
gevent.monkey.patch_all()
41

  
42
import inspect
43
import logging
44
import sys
45

  
46
from argparse import ArgumentParser
47
from base64 import b64encode
48
from os.path import abspath, basename, exists
49
from sys import exit, stdout, stderr
50

  
51
try:
52
    from collections import OrderedDict
53
except ImportError:
54
    from ordereddict import OrderedDict
55

  
56
from colors import magenta, red, yellow, bold
57

  
58
from kamaki import clients
59
from .config import Config
60

  
61
_commands = OrderedDict()
62

  
63
GROUPS = {}
64
CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
65

  
66
class CLIError(Exception):
67
    def __init__(self, message, status=0, details='', importance=0):
68
        """importance is set by the raiser
69
        0 is the lowest possible importance
70
        Suggested values: 0, 1, 2, 3
71
        """
72
        super(CLIError, self).__init__(message, status, details)
73
        self.message = message
74
        self.status = status
75
        self.details = details
76
        self.importance = importance
77

  
78
    def __unicode__(self):
79
        return unicode(self.message)
80

  
81
def command(group=None, name=None, syntax=None):
82
    """Class decorator that registers a class as a CLI command."""
83

  
84
    def decorator(cls):
85
        grp, sep, cmd = cls.__name__.partition('_')
86
        if not sep:
87
            grp, cmd = None, cls.__name__
88

  
89
        #cls.api = api
90
        cls.group = group or grp
91
        cls.name = name or cmd
92

  
93
        short_description, sep, long_description = cls.__doc__.partition('\n')
94
        cls.description = short_description
95
        cls.long_description = long_description or short_description
96

  
97
        cls.syntax = syntax
98
        if cls.syntax is None:
99
            # Generate a syntax string based on main's arguments
100
            spec = inspect.getargspec(cls.main.im_func)
101
            args = spec.args[1:]
102
            n = len(args) - len(spec.defaults or ())
103
            required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').replace('_', ' ') for x in args[:n])
104
            optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').replace('_', ' ') for x in args[n:])
105
            cls.syntax = ' '.join(x for x in [required, optional] if x)
106
            if spec.varargs:
107
                cls.syntax += ' <%s ...>' % spec.varargs
108

  
109
        if cls.group not in _commands:
110
            _commands[cls.group] = OrderedDict()
111
        _commands[cls.group][cls.name] = cls
112
        return cls
113
    return decorator
114

  
115
def set_api_description(api, description):
116
    """Method to be called by api CLIs
117
    Each CLI can set more than one api descriptions"""
118
    GROUPS[api] = description
119

  
120
def main():
121

  
122
    def print_groups():
123
        print('\nGroups:')
124
        for group in _commands:
125
            description = GROUPS.get(group, '')
126
            print(' ', group.ljust(12), description)
127

  
128
    def print_commands(group):
129
        description = GROUPS.get(group, '')
130
        if description:
131
            print('\n' + description)
132

  
133
        print('\nCommands:')
134
        for name, cls in _commands[group].items():
135
            print(' ', name.ljust(14), cls.description)
136

  
137
    def manage_logging_handlers(args):
138
        """This is mostly to handle logging for clients package"""
139

  
140
        def add_handler(name, level, prefix=''):
141
            h = logging.StreamHandler()
142
            fmt = logging.Formatter(prefix + '%(message)s')
143
            h.setFormatter(fmt)
144
            logger = logging.getLogger(name)
145
            logger.addHandler(h)
146
            logger.setLevel(level)
147

  
148
        if args.silent:
149
            add_handler('', logging.CRITICAL)
150
        elif args.debug:
151
            add_handler('requests', logging.INFO, prefix='* ')
152
            add_handler('clients.send', logging.DEBUG, prefix='> ')
153
            add_handler('clients.recv', logging.DEBUG, prefix='< ')
154
        elif args.verbose:
155
            add_handler('requests', logging.INFO, prefix='* ')
156
            add_handler('clients.send', logging.INFO, prefix='> ')
157
            add_handler('clients.recv', logging.INFO, prefix='< ')
158
        elif args.include:
159
            add_handler('clients.recv', logging.INFO)
160
        else:
161
            add_handler('', logging.WARNING)
162

  
163
    def load_groups(config):
164
        """load groups and import CLIs and Modules"""
165
        loaded_modules = {}
166
        for api in config.apis():
167
            api_cli = config.get(api, 'cli')
168
            if None == api_cli or len(api_cli)==0:
169
                print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
170
                print('\t(cli option in config file)')
171
                continue
172
            if not loaded_modules.has_key(api_cli):
173
                loaded_modules[api_cli] = False
174
                for location in CLI_LOCATIONS:
175
                    location += api_cli if location == '' else '.%s'%api_cli
176
                    try:
177
                        __import__(location)
178
                        loaded_modules[api_cli] = True
179
                        break
180
                    except ImportError:
181
                        pass
182
                if not loaded_modules[api_cli]:
183
                    print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
184
                    print('\t(No suitable cli in known paths)')
185
                    continue
186
            if not GROUPS.has_key(api):
187
                GROUPS[api] = 'No description (interface: %s)'%api_cli
188

  
189
    def init_parser(exe):
190
        parser = ArgumentParser(add_help=False)
191
        parser.prog = '%s <group> <command>' % exe
192
        parser.add_argument('-h', '--help', dest='help', action='store_true',
193
                          default=False,
194
                          help="Show this help message and exit")
195
        parser.add_argument('--config', dest='config', metavar='PATH',
196
                          help="Specify the path to the configuration file")
197
        parser.add_argument('-d', '--debug', dest='debug', action='store_true',
198
                          default=False,
199
                          help="Include debug output")
200
        parser.add_argument('-i', '--include', dest='include', action='store_true',
201
                          default=False,
202
                          help="Include protocol headers in the output")
203
        parser.add_argument('-s', '--silent', dest='silent', action='store_true',
204
                          default=False,
205
                          help="Silent mode, don't output anything")
206
        parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
207
                          default=False,
208
                          help="Make the operation more talkative")
209
        parser.add_argument('-V', '--version', dest='version', action='store_true',
210
                          default=False,
211
                          help="Show version number and quit")
212
        parser.add_argument('-o', dest='options', action='append',
213
                          default=[], metavar="KEY=VAL",
214
                          help="Override a config value")
215
        return parser
216

  
217
    def find_term_in_args(arg_list, term_list):
218
        """find an arg_list term in term_list. All other terms up to found
219
        term are rearanged at the end of arg_list, preserving relative order
220
        """
221
        arg_tail = []
222
        while len(arg_list) > 0:
223
            group = arg_list.pop(0)
224
            if group not in term_list:
225
                arg_tail.append(group)
226
            else:
227
                arg_list += arg_tail
228
                return group
229
        return None
230

  
231
    """Main Code"""
232
    exe = basename(sys.argv[0])
233
    parser = init_parser(exe)
234
    args, argv = parser.parse_known_args()
235

  
236
    #print version
237
    if args.version:
238
        import kamaki
239
        print("kamaki %s" % kamaki.__version__)
240
        exit(0)
241

  
242
    config = Config(args.config) if args.config else Config()
243

  
244
    #load config options from command line
245
    for option in args.options:
246
        keypath, sep, val = option.partition('=')
247
        if not sep:
248
            print("Invalid option '%s'" % option)
249
            exit(1)
250
        section, sep, key = keypath.partition('.')
251
        if not sep:
252
            print("Invalid option '%s'" % option)
253
            exit(1)
254
        config.override(section.strip(), key.strip(), val.strip())
255

  
256
    load_groups(config)
257
    group = find_term_in_args(argv, _commands)
258
    if not group:
259
        parser.print_help()
260
        print_groups()
261
        exit(0)
262

  
263
    parser.prog = '%s %s <command>' % (exe, group)
264
    command = find_term_in_args(argv, _commands[group])
265

  
266
    if not command:
267
        parser.print_help()
268
        print_commands(group)
269
        exit(0)
270

  
271
    cmd = _commands[group][command]()
272

  
273
    parser.prog = '%s %s %s' % (exe, group, command)
274
    if cmd.syntax:
275
        parser.prog += '  %s' % cmd.syntax
276
    parser.description = cmd.description
277
    parser.epilog = ''
278
    if hasattr(cmd, 'update_parser'):
279
        cmd.update_parser(parser)
280

  
281
    #check other args
282
    args, argv = parser.parse_known_args()
283
    if group != argv[0]:
284
        errmsg = red('Invalid command group '+argv[0])
285
        print(errmsg, file=stderr)
286
        exit(1)
287
    if command != argv[1]:
288
        errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
289
        print(errmsg, file=stderr)
290
        exit(1)
291

  
292
    if args.help:
293
        parser.print_help()
294
        exit(0)
295

  
296
    manage_logging_handlers(args)
297
    cmd.args = args
298
    cmd.config = config
299
    try:
300
        ret = cmd.main(*argv[2:])
301
        exit(ret)
302
    except TypeError as e:
303
        if e.args and e.args[0].startswith('main()'):
304
            parser.print_help()
305
            exit(1)
306
        else:
307
            raise
308
    except CLIError as err:
309
        errmsg = 'CLI Error '
310
        errmsg += '(%s): '%err.status if err.status else ': '
311
        errmsg += err.message if err.message else ''
312
        if err.importance == 1:
313
            errmsg = yellow(errmsg)
314
        elif err.importance == 2:
315
            errmsg = magenta(errmsg)
316
        elif err.importance > 2:
317
            errmsg = red(errmsg)
318
        print(errmsg, file=stderr)
319
        exit(1)
320

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

  
34

  
35
from kamaki.cli import command, set_api_description
36
set_api_description('astakos', 'Astakos API commands')
37
from kamaki.clients.astakos import AstakosClient, ClientError
38
from kamaki.cli.utils import raiseCLIError, print_dict
39

  
40
class _astakos_init(object):
41
	def main(self):
42
		token = self.config.get('astakos', 'token') or self.config.get('global', 'token')
43
		base_url = self.config.get('astakos', 'url') or self.config.get('global', 'url')
44
		if base_url is None:
45
			raise ClientError('no URL for astakos')
46
		self.client = AstakosClient(base_url=base_url, token=token)
47

  
48
@command()
49
class astakos_authenticate(_astakos_init):
50
    """Authenticate a user"""
51

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

  
34
from kamaki.cli import command, set_api_description
35
set_api_description('config', 'Configuration commands')
36

  
37
@command()
38
class config_list(object):
39
    """List configuration options"""
40

  
41
    def update_parser(self, parser):
42
        parser.add_argument('-a', dest='all', action='store_true',
43
                          default=False, help='include default values')
44

  
45
    def main(self):
46
        include_defaults = self.args.all
47
        for section in sorted(self.config.sections()):
48
            items = self.config.items(section, include_defaults)
49
            for key, val in sorted(items):
50
                print('%s.%s = %s' % (section, key, val))
51

  
52
@command()
53
class config_get(object):
54
    """Show a configuration option"""
55

  
56
    def main(self, option):
57
        section, sep, key = option.rpartition('.')
58
        section = section or 'global'
59
        value = self.config.get(section, key)
60
        if value is not None:
61
            print(value)
62

  
63
@command()
64
class config_set(object):
65
    """Set a configuration option"""
66

  
67
    def main(self, option, value):
68
        section, sep, key = option.rpartition('.')
69
        section = section or 'globail'
70
        self.config.set(section, key, value)
71
        self.config.write()
72

  
73
@command()
74
class config_delete(object):
75
    """Delete a configuration option (and use the default value)"""
76

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

  
34
from kamaki.cli import command, set_api_description, CLIError
35
from kamaki.cli.utils import print_dict, print_items, print_list, format_size, raiseCLIError
36
from colors import bold
37
set_api_description('server', "Compute/Cyclades API server commands")
38
set_api_description('flavor', "Compute/Cyclades API flavor commands")
39
set_api_description('image', "Compute/Cyclades or Glance API image commands")
40
set_api_description('network', "Compute/Cyclades API network commands")
41
from kamaki.clients.cyclades import CycladesClient, ClientError
42

  
43
class _init_cyclades(object):
44
    def main(self):
45
        token = self.config.get('compute', 'token') or self.config.get('global', 'token')
46
        base_url = self.config.get('compute', 'url') or self.config.get('global', 'url')
47
        self.client = CycladesClient(base_url=base_url, token=token)
48

  
49
@command()
50
class server_list(_init_cyclades):
51
    """List servers"""
52

  
53
    def _print(self, servers):
54
        for server in servers:
55
            sname = server.pop('name')
56
            sid = server.pop('id')
57
            print('%s (%s)'%(bold(sname), bold(unicode(sid))))
58
            if getattr(self.args, 'detail'):
59
                server_info._print(server)
60
                print('- - -')
61

  
62
    def update_parser(self, parser):
63
        parser.add_argument('-l', dest='detail', action='store_true',
64
                default=False, help='show detailed output')
65

  
66
    def main(self):
67
        super(self.__class__, self).main()
68
        try:
69
            servers = self.client.list_servers(self.args.detail)
70
            self._print(servers)
71
            #print_items(servers)
72
        except ClientError as err:
73
            raiseCLIError(err)
74

  
75
@command()
76
class server_info(_init_cyclades):
77
    """Get server details"""
78

  
79
    @classmethod
80
    def _print(self,server):
81
        addr_dict = {}
82
        if server.has_key('attachments'):
83
            for addr in server['attachments']['values']:
84
                ips = addr.pop('values', [])
85
                for ip in ips:
86
                    addr['IPv%s'%ip['version']] = ip['addr']
87
                if addr.has_key('firewallProfile'):
88
                    addr['firewall'] = addr.pop('firewallProfile')
89
                addr_dict[addr.pop('id')] = addr
90
            server['attachments'] = addr_dict if addr_dict is not {} else None
91
        if server.has_key('metadata'):
92
            server['metadata'] = server['metadata']['values']
93
        print_dict(server, ident=14)
94

  
95
    def main(self, server_id):
96
        super(self.__class__, self).main()
97
        try:
98
            server = self.client.get_server_details(int(server_id))
99
        except ClientError as err:
100
            raiseCLIError(err)
101
        except ValueError as err:
102
            raise CLIError(message='Server id must be positive integer',
103
                importance=1)
104
        self._print(server)
105

  
106
@command()
107
class server_create(_init_cyclades):
108
    """Create a server"""
109

  
110
    def update_parser(self, parser):
111
        parser.add_argument('--personality', dest='personalities',
112
                          action='append', default=[],
113
                          metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
114
                          help='add a personality file')
115

  
116
    def main(self, name, flavor_id, image_id):
117
        super(self.__class__, self).main()
118
        personalities = []
119
        for personality in self.args.personalities:
120
            p = personality.split(',')
121
            p.extend([None] * (5 - len(p)))     # Fill missing fields with None
122

  
123
            path = p[0]
124

  
125
            if not path:
126
                raise CLIError(message='Invalid personality argument %s'%p, importance=1)
127
            if not exists(path):
128
                raise CLIError(message="File %s does not exist" % path, importance=1)
129

  
130
            with open(path) as f:
131
                contents = b64encode(f.read())
132

  
133
            d = {'path': p[1] or abspath(path), 'contents': contents}
134
            if p[2]:
135
                d['owner'] = p[2]
136
            if p[3]:
137
                d['group'] = p[3]
138
            if p[4]:
139
                d['mode'] = int(p[4])
140
            personalities.append(d)
141

  
142
        try:
143
            reply = self.client.create_server(name, int(flavor_id), image_id,
144
                personalities)
145
        except ClientError as err:
146
            raiseCLIError(err)
147
        print_dict(reply)
148

  
149
@command()
150
class server_rename(_init_cyclades):
151
    """Update a server's name"""
152

  
153
    def main(self, server_id, new_name):
154
        super(self.__class__, self).main()
155
        try:
156
            self.client.update_server_name(int(server_id), new_name)
157
        except ClientError as err:
158
            raiseCLIError(err)
159
        except ValueError:
160
            raise CLIError(message='Server id must be positive integer', importance=1)
161

  
162
@command()
163
class server_delete(_init_cyclades):
164
    """Delete a server"""
165

  
166
    def main(self, server_id):
167
        super(self.__class__, self).main()
168
        try:
169
            self.client.delete_server(int(server_id))
170
        except ClientError as err:
171
            raiseCLIError(err)
172
        except ValueError:
173
            raise CLIError(message='Server id must be positive integer', importance=1)
174

  
175
@command()
176
class server_reboot(_init_cyclades):
177
    """Reboot a server"""
178

  
179
    def update_parser(self, parser):
180
        parser.add_argument('-f', dest='hard', action='store_true',
181
                default=False, help='perform a hard reboot')
182

  
183
    def main(self, server_id):
184
        super(self.__class__, self).main()
185
        try:
186
            self.client.reboot_server(int(server_id), self.args.hard)
187
        except ClientError as err:
188
            raiseCLIError(err)
189
        except ValueError:
190
            raise CLIError(message='Server id must be positive integer', importance=1)
191

  
192
@command()
193
class server_start(_init_cyclades):
194
    """Start a server"""
195

  
196
    def main(self, server_id):
197
        super(self.__class__, self).main()
198
        try:
199
            self.client.start_server(int(server_id))
200
        except ClientError as err:
201
            raiseCLIError(err)
202
        except ValueError:
203
            raise CLIError(message='Server id must be positive integer', importance=1)
204

  
205
@command()
206
class server_shutdown(_init_cyclades):
207
    """Shutdown a server"""
208

  
209
    def main(self, server_id):
210
        super(self.__class__, self).main()
211
        try:
212
            self.client.shutdown_server(int(server_id))
213
        except ClientError as err:
214
            raiseCLIError(err)
215
        except ValueError:
216
            raise CLIError(message='Server id must be positive integer', importance=1)
217

  
218
@command()
219
class server_console(_init_cyclades):
220
    """Get a VNC console"""
221

  
222
    def main(self, server_id):
223
        super(self.__class__, self).main()
224
        try:
225
            reply = self.client.get_server_console(int(server_id))
226
        except ClientError as err:
227
            raiseCLIError(err)
228
        except ValueError:
229
            raise CLIError(message='Server id must be positive integer', importance=1)
230
        print_dict(reply)
231

  
232
@command()
233
class server_firewall(_init_cyclades):
234
    """Set the server's firewall profile"""
235

  
236
    def main(self, server_id, profile):
237
        super(self.__class__, self).main()
238
        try:
239
            self.client.set_firewall_profile(int(server_id), profile)
240
        except ClientError as err:
241
            raiseCLIError(err)
242
        except ValueError:
243
            raise CLIError(message='Server id must be positive integer', importance=1)
244
@command()
245
class server_addr(_init_cyclades):
246
    """List a server's nic address"""
247

  
248
    def main(self, server_id):
249
        super(self.__class__, self).main()
250
        try:
251
            reply = self.client.list_server_nics(int(server_id))
252
        except ClientError as err:
253
            raiseCLIError(err)
254
        except ValueError:
255
            raise CLIError(message='Server id must be positive integer', importance=1)
256
        print_list(reply)
257

  
258
@command()
259
class server_meta(_init_cyclades):
260
    """Get a server's metadata"""
261

  
262
    def main(self, server_id, key=None):
263
        super(self.__class__, self).main()
264
        try:
265
            reply = self.client.get_server_metadata(int(server_id), key)
266
        except ClientError as err:
267
            raiseCLIError(err)
268
        except ValueError:
269
            raise CLIError(message='Server id must be positive integer', importance=1)
270
        print_dict(reply)
271

  
272
@command()
273
class server_addmeta(_init_cyclades):
274
    """Add server metadata"""
275

  
276
    def main(self, server_id, key, val):
277
        super(self.__class__, self).main()
278
        try:
279
            reply = self.client.create_server_metadata(int(server_id), key, val)
280
        except ClientError as err:
281
            raiseCLIError(err)
282
        except ValueError:
283
            raise CLIError(message='Server id must be positive integer', importance=1)
284
        print_dict(reply)
285

  
286
@command()
287
class server_setmeta(_init_cyclades):
288
    """Update server's metadata"""
289

  
290
    def main(self, server_id, key, val):
291
        super(self.__class__, self).main()
292
        metadata = {key: val}
293
        try:
294
            reply = self.client.update_server_metadata(int(server_id), **metadata)
295
        except ClientError as err:
296
            raiseCLIError(err)
297
        except ValueError:
298
            raise CLIError(message='Server id must be positive integer', importance=1)
299
        print_dict(reply)
300

  
301
@command()
302
class server_delmeta(_init_cyclades):
303
    """Delete server metadata"""
304

  
305
    def main(self, server_id, key):
306
        super(self.__class__, self).main()
307
        try:
308
            self.client.delete_server_metadata(int(server_id), key)
309
        except ClientError as err:
310
            raiseCLIError(err)
311
        except ValueError:
312
            raise CLIError(message='Server id must be positive integer', importance=1)
313

  
314
@command()
315
class server_stats(_init_cyclades):
316
    """Get server statistics"""
317

  
318
    def main(self, server_id):
319
        super(self.__class__, self).main()
320
        try:
321
            reply = self.client.get_server_stats(int(server_id))
322
        except ClientError as err:
323
            raiseCLIError(err)
324
        except ValueError:
325
            raise CLIError(message='Server id must be positive integer', importance=1)
326
        print_dict(reply, exclude=('serverRef',))
327

  
328
@command()
329
class flavor_list(_init_cyclades):
330
    """List flavors"""
331

  
332
    def update_parser(self, parser):
333
        parser.add_argument('-l', dest='detail', action='store_true',
334
                default=False, help='show detailed output')
335

  
336
    def main(self):
337
        super(self.__class__, self).main()
338
        try:
339
            flavors = self.client.list_flavors(self.args.detail)
340
        except ClientError as err:
341
            raiseCLIError(err)
342
        print_items(flavors)
343

  
344
@command()
345
class flavor_info(_init_cyclades):
346
    """Get flavor details"""
347

  
348
    def main(self, flavor_id):
349
        super(self.__class__, self).main()
350
        try:
351
            flavor = self.client.get_flavor_details(int(flavor_id))
352
        except ClientError as err:
353
            raiseCLIError(err)
354
        except ValueError:
355
            raise CLIError(message='Server id must be positive integer', importance=1)
356
        print_dict(flavor)
357

  
358
@command()
359
class image_list(_init_cyclades):
360
    """List images"""
361

  
362
    def update_parser(self, parser):
363
        parser.add_argument('-l', dest='detail', action='store_true',
364
                default=False, help='show detailed output')
365

  
366
    def _print(self, images):
367
        for img in images:
368
            iname = img.pop('name')
369
            iid = img.pop('id')
370
            print('%s (%s)'%(bold(iname), bold(unicode(iid))))
371
            if getattr(self.args, 'detail'):
372
                image_info._print(img)
373
                print('- - -')
374

  
375
    def main(self):
376
        super(self.__class__, self).main()
377
        try:
378
            images = self.client.list_images(self.args.detail)
379
        except ClientError as err:
380
            raiseCLIError(err)
381
        #print_items(images)
382
        self._print(images)
383

  
384
@command()
385
class image_info(_init_cyclades):
386
    """Get image details"""
387

  
388
    @classmethod
389
    def _print(self, image):
390
        if image.has_key('metadata'):
391
            image['metadata'] = image['metadata']['values']
392
        print_dict(image, ident=14)
393

  
394
    def main(self, image_id):
395
        super(self.__class__, self).main()
396
        try:
397
            image = self.client.get_image_details(image_id)
398
        except ClientError as err:
399
            raiseCLIError(err)
400
        self._print(image)
401

  
402
@command()
403
class image_delete(_init_cyclades):
404
    """Delete image"""
405

  
406
    def main(self, image_id):
407
        super(self.__class__, self).main()
408
        try:
409
            self.client.delete_image(image_id)
410
        except ClientError as err:
411
            raiseCLIError(err)
412

  
413
@command()
414
class image_properties(_init_cyclades):
415
    """Get image properties"""
416

  
417
    def main(self, image_id, key=None):
418
        super(self.__class__, self).main()
419
        try:
420
            reply = self.client.get_image_metadata(image_id, key)
421
        except ClientError as err:
422
            raiseCLIError(err)
423
        print_dict(reply)
424

  
425
@command()
426
class image_addproperty(_init_cyclades):
427
    """Add an image property"""
428

  
429
    def main(self, image_id, key, val):
430
        super(self.__class__, self).main()
431
        try:
432
            reply = self.client.create_image_metadata(image_id, key, val)
433
        except ClientError as err:
434
            raiseCLIError(err)
435
        print_dict(reply)
436

  
437
@command()
438
class image_setproperty(_init_cyclades):
439
    """Update an image property"""
440

  
441
    def main(self, image_id, key, val):
442
        super(self.__class__, self).main()
443
        metadata = {key: val}
444
        try:
445
            reply = self.client.update_image_metadata(image_id, **metadata)
446
        except ClientError as err:
447
            raiseCLIError(err)
448
        print_dict(reply)
449

  
450
@command()
451
class image_delproperty(_init_cyclades):
452
    """Delete an image property"""
453

  
454
    def main(self, image_id, key):
455
        super(self.__class__, self).main()
456
        try:
457
            self.client.delete_image_metadata(image_id, key)
458
        except ClientError as err:
459
            raiseCLIError(err)
460

  
461
@command()
462
class network_list(_init_cyclades):
463
    """List networks"""
464

  
465
    def update_parser(self, parser):
466
        parser.add_argument('-l', dest='detail', action='store_true',
467
                default=False, help='show detailed output')
468

  
469
    def print_networks(self, nets):
470
        for net in nets:
471
            netname = bold(net.pop('name'))
472
            netid = bold(unicode(net.pop('id')))
473
            print('%s (%s)'%(netname, netid))
474
            if getattr(self.args, 'detail'):
475
                network_info.print_network(net)
476
                print('- - -')
477

  
478
    def main(self):
479
        super(self.__class__, self).main()
480
        try:
481
            networks = self.client.list_networks(self.args.detail)
482
        except ClientError as err:
483
            raiseCLIError(err)
484
        self.print_networks(networks)
485

  
486
@command()
487
class network_create(_init_cyclades):
488
    """Create a network"""
489

  
490
    def update_parser(self, parser):
491
        try:
492
            super(self.__class__, self).update_parser(parser)
493
        except AttributeError:
494
            pass
495
        parser.add_argument('--with-cidr', action='store', dest='cidr', default=False,
496
            help='specific cidr for new network')
497
        parser.add_argument('--with-gateway', action='store', dest='gateway', default=False,
498
            help='specific getaway for new network')
499
        parser.add_argument('--with-dhcp', action='store', dest='dhcp', default=False,
500
            help='specific dhcp for new network')
501
        parser.add_argument('--with-type', action='store', dest='type', default=False,
502
            help='specific type for new network')
503

  
504
    def main(self, name):
505
        super(self.__class__, self).main()
506
        try:
507
            reply = self.client.create_network(name, cidr=getattr(self.args, 'cidr'),
508
                gateway=getattr(self.args, 'gateway'), dhcp=getattr(self.args, 'gateway'),
509
                type=getattr(self.args, 'type'))
510
        except ClientError as err:
511
            raiseCLIError(err)
512
        print_dict(reply)
513

  
514
@command()
515
class network_info(_init_cyclades):
516
    """Get network details"""
517

  
518
    @classmethod
519
    def print_network(self, net):
520
        if net.has_key('attachments'):
521
            att = net['attachments']['values']
522
            net['attachments'] = att if len(att) > 0 else None
523
        print_dict(net, ident=14)
524

  
525
    def main(self, network_id):
526
        super(self.__class__, self).main()
527
        try:
528
            network = self.client.get_network_details(network_id)
529
        except ClientError as err:
530
            raiseCLIError(err)
531
        network_info.print_network(network)
532

  
533
@command()
534
class network_rename(_init_cyclades):
535
    """Update network name"""
536

  
537
    def main(self, network_id, new_name):
538
        super(self.__class__, self).main()
539
        try:
540
            self.client.update_network_name(network_id, new_name)
541
        except ClientError as err:
542
            raiseCLIError(err)
543

  
544
@command()
545
class network_delete(_init_cyclades):
546
    """Delete a network"""
547

  
548
    def main(self, network_id):
549
        super(self.__class__, self).main()
550
        try:
551
            self.client.delete_network(network_id)
552
        except ClientError as err:
553
            raiseCLIError(err)
554

  
555
@command()
556
class network_connect(_init_cyclades):
557
    """Connect a server to a network"""
558

  
559
    def main(self, server_id, network_id):
560
        super(self.__class__, self).main()
561
        try:
562
            self.client.connect_server(server_id, network_id)
563
        except ClientError as err:
564
            raiseCLIError(err)
565

  
566
@command()
567
class network_disconnect(_init_cyclades):
568
    """Disconnect a nic that connects a server to a network"""
569

  
570
    def main(self, nic_id):
571
        super(self.__class__, self).main()
572
        try:
573
            server_id = nic_id.split('-')[1]
574
            self.client.disconnect_server(server_id, nic_id)
575
        except IndexError:
576
            raise CLIError(message='Incorrect nic format', importance=1,
577
                details='nid_id format: nic-<server_id>-<nic_index>')
578
        except ClientError as err:
579
            raiseCLIError(err)
b/kamaki/cli/commands/image_cli.py
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.command
33

  
34
from kamaki.cli import command, set_api_description
35
from kamaki.cli.utils import print_dict, print_items, raiseCLIError
36
set_api_description('image', "Compute/Cyclades or Glance API image commands")
37
from kamaki.clients.image import ImageClient, ClientError
38

  
39
class _init_image(object):
40
    def main(self):
41
        try:
42
            token = self.config.get('image', 'token') or self.config.get('global', 'token')
43
            base_url = self.config.get('image', 'url') or self.config.get('global', 'url')
44
            self.client = ImageClient(base_url=base_url, token=token)
45
        except ClientError as err:
46
            raiseCLIError(err)
47

  
48
@command()
49
class image_public(_init_image):
50
    """List public images"""
51

  
52
    def update_parser(self, parser):
53
        parser.add_argument('-l', dest='detail', action='store_true',
54
                default=False, help='show detailed output')
55
        parser.add_argument('--container-format', dest='container_format',
56
                metavar='FORMAT', help='filter by container format')
57
        parser.add_argument('--disk-format', dest='disk_format',
58
                metavar='FORMAT', help='filter by disk format')
59
        parser.add_argument('--name', dest='name', metavar='NAME',
60
                help='filter by name')
61
        parser.add_argument('--size-min', dest='size_min', metavar='BYTES',
62
                help='filter by minimum size')
63
        parser.add_argument('--size-max', dest='size_max', metavar='BYTES',
64
                help='filter by maximum size')
65
        parser.add_argument('--status', dest='status', metavar='STATUS',
66
                help='filter by status')
67
        parser.add_argument('--order', dest='order', metavar='FIELD',
68
                help='order by FIELD (use a - prefix to reverse order)')
69

  
70
    def main(self):
71
    	super(self.__class__, self).main()
72
        filters = {}
73
        for filter in ('container_format', 'disk_format', 'name', 'size_min',
74
                       'size_max', 'status'):
75
            val = getattr(self.args, filter, None)
76
            if val is not None:
77
                filters[filter] = val
78

  
79
        order = self.args.order or ''
80
        try:
81
            images = self.client.list_public(self.args.detail,
82
                filters=filters, order=order)
83
        except ClientError as err:
84
            raiseCLIError(err)
85
        print_items(images, title=('name',))
86

  
87
@command()
88
class image_meta(_init_image):
89
    """Get image metadata"""
90

  
91
    def main(self, image_id):
92
    	super(self.__class__, self).main()
93
        try:
94
            image = self.client.get_meta(image_id)
95
        except ClientError as err:
96
            raiseCLIError(err)
97
        print_dict(image)
98

  
99
@command()
100
class image_register(_init_image):
101
    """Register an image"""
102

  
103
    def update_parser(self, parser):
104
        parser.add_argument('--checksum', dest='checksum', metavar='CHECKSUM',
105
                help='set image checksum')
106
        parser.add_argument('--container-format', dest='container_format',
107
                metavar='FORMAT', help='set container format')
108
        parser.add_argument('--disk-format', dest='disk_format',
109
                metavar='FORMAT', help='set disk format')
110
        parser.add_argument('--id', dest='id',
111
                metavar='ID', help='set image ID')
112
        parser.add_argument('--owner', dest='owner',
113
                metavar='USER', help='set image owner (admin only)')
114
        parser.add_argument('--property', dest='properties', action='append',
115
                metavar='KEY=VAL',
116
                help='add a property (can be used multiple times)')
117
        parser.add_argument('--public', dest='is_public', action='store_true',
118
                help='mark image as public')
119
        parser.add_argument('--size', dest='size', metavar='SIZE',
120
                help='set image size')
121

  
122
    def main(self, name, location):
123
    	super(self.__class__, self).main()
124
        if not location.startswith('pithos://'):
125
            account = self.config.get('storage', 'account').split()[0]
126
            if account[-1] == '/':
127
                account = account[:-1]
128
            container = self.config.get('storage', 'container')
129
            location = 'pithos://%s/%s'%(account, location) \
130
                if container is None or len(container) == 0 \
131
                else 'pithos://%s/%s/%s' % (account, container, location)
132

  
133
        params = {}
134
        for key in ('checksum', 'container_format', 'disk_format', 'id',
135
                    'owner', 'size'):
136
            val = getattr(self.args, key)
137
            if val is not None:
138
                params[key] = val
139

  
140
        if self.args.is_public:
141
            params['is_public'] = 'true'
142

  
143
        properties = {}
144
        for property in self.args.properties or []:
145
            key, sep, val = property.partition('=')
146
            if not sep:
147
                raise CLIError(message="Invalid property '%s'" % property, importance=1)
148
            properties[key.strip()] = val.strip()
149

  
150
        try:
151
            self.client.register(name, location, params, properties)
152
        except ClientError as err:
153
            raiseCLIError(err)
154

  
155
@command()
156
class image_members(_init_image):
157
    """Get image members"""
158

  
159
    def main(self, image_id):
160
    	super(self.__class__, self).main()
161
        try:
162
            members = self.client.list_members(image_id)
163
        except ClientError as err:
164
            raiseCLIError(err)
165
        for member in members:
166
            print(member['member_id'])
167

  
168
@command()
169
class image_shared(_init_image):
170
    """List shared images"""
171

  
172
    def main(self, member):
173
    	super(self.__class__, self).main()
174
        try:
175
            images = self.client.list_shared(member)
176
        except ClientError as err:
177
            raiseCLIError(err)
178
        for image in images:
179
            print(image['image_id'])
180

  
181
@command()
182
class image_addmember(_init_image):
183
    """Add a member to an image"""
184

  
185
    def main(self, image_id, member):
186
    	super(self.__class__, self).main()
187
        try:
188
            self.client.add_member(image_id, member)
189
        except ClientError as err:
190
            raiseCLIError(err)
191

  
192
@command()
193
class image_delmember(_init_image):
194
    """Remove a member from an image"""
195

  
196
    def main(self, image_id, member):
197
        super(self.__class__, self).main()
198
        try:
199
            self.client.remove_member(image_id, member)
200
        except ClientError as err:
201
            raiseCLIError(err)
202

  
203
@command()
204
class image_setmembers(_init_image):
205
    """Set the members of an image"""
206

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

  
34
from kamaki.cli import command, set_api_description, CLIError
35
from kamaki.clients.utils import filter_in
36
from kamaki.cli.utils import format_size, raiseCLIError, print_dict, pretty_keys, print_list
37
set_api_description('store', 'Pithos+ storage commands')
38
from kamaki.clients.pithos import PithosClient, ClientError
39
from colors import bold
40
from sys import stdout, exit
41
import signal
42

  
43
from progress.bar import IncrementalBar
44

  
45

  
46
class ProgressBar(IncrementalBar):
47
    #suffix = '%(percent)d%% - %(eta)ds'
48
    suffix = '%(percent)d%%'
49

  
50
class _pithos_init(object):
51
    def main(self):
52
        self.token = self.config.get('store', 'token') or self.config.get('global', 'token')
53
        self.base_url = self.config.get('store', 'url') or self.config.get('global', 'url')
54
        self.account = self.config.get('store', 'account') or self.config.get('global', 'account')
55
        self.container = self.config.get('store', 'container') or self.config.get('global', 'container')
56
        self.client = PithosClient(base_url=self.base_url, token=self.token, account=self.account,
57
            container=self.container)
58

  
59
class _store_account_command(_pithos_init):
60
    """Base class for account level storage commands"""
61

  
62
    def update_parser(self, parser):
63
        parser.add_argument('--account', dest='account', metavar='NAME',
64
                          help="Specify an account to use")
65

  
66
    def progress(self, message):
67
        """Return a generator function to be used for progress tracking"""
68

  
69
        MESSAGE_LENGTH = 25
70

  
71
        def progress_gen(n):
72
            msg = message.ljust(MESSAGE_LENGTH)
73
            for i in ProgressBar(msg).iter(range(n)):
74
                yield
75
            yield
76

  
77
        return progress_gen
78

  
79
    def main(self):
80
        super(_store_account_command, self).main()
81
        if hasattr(self.args, 'account') and self.args.account is not None:
82
            self.client.account = self.args.account
83

  
84
class _store_container_command(_store_account_command):
85
    """Base class for container level storage commands"""
86

  
87
    def __init__(self):
88
        self.container = None
89
        self.path = None
90

  
91
    def update_parser(self, parser):
92
        super(_store_container_command, self).update_parser(parser)
93
        parser.add_argument('--container', dest='container', metavar='NAME', default=None,
94
            help="Specify a container to use")
95

  
96
    def extract_container_and_path(self, container_with_path, path_is_optional=True):
97
        assert isinstance(container_with_path, str)
98
        if ':' not in container_with_path:
99
            if hasattr(self.args, 'container'):
100
                self.container = getattr(self.args, 'container')
101
            else:
102
                self.container = self.client.container
103
            if self.container is None:
104
                self.container = container_with_path
105
            else:
106
                self.path = container_with_path
107
            if not path_is_optional and self.path is None:
108
                raise CLIError(message="Object path is missing", status=11)
109
            return
110
        cnp = container_with_path.split(':')
111
        self.container = cnp[0]
112
        try:
113
            self.path = cnp[1]
114
        except IndexError:
115
            if path_is_optional:
116
                self.path = None
117
            else:
118
                raise CLIError(message="Object path is missing", status=11)
119

  
120
    def main(self, container_with_path=None, path_is_optional=True):
121
        super(_store_container_command, self).main()
122
        if container_with_path is not None:
123
            self.extract_container_and_path(container_with_path, path_is_optional)
124
            self.client.container = self.container
125
        elif hasattr(self.args, 'container'):
126
            self.client.container = getattr(self.args,'container')
127
        self.container = self.client.container
128

  
129
"""
130
@command()
131
class store_test(_store_container_command):
132
    ""Test various stuff""
133

  
134
    def main(self):
135
        super(self.__class__, self).main('pithos')
136
        r = self.client.list_containers()
137
        for item in r:
138
            if item['name'].startswith('c1_') or item['name'].startswith('c2_') \
139
            or item['name'].startswith('c3_'):
140
                self.client.container = item['name']
141
                self.client.del_container(delimiter='/')
142
                self.client.del_container()
143
"""
144

  
145
@command()
146
class store_list(_store_container_command):
147
    """List containers, object trees or objects in a directory
148
    """
149

  
150
    def update_parser(self, parser):
151
        super(self.__class__, self).update_parser(parser)
152
        parser.add_argument('-l', action='store_true', dest='detail', default=False,
153
            help='show detailed output')
154
        parser.add_argument('-N', action='store', dest='show_size', default=1000,
155
            help='print output in chunks of size N')
156
        parser.add_argument('-n', action='store', dest='limit', default=None,
157
            help='show limited output')
158
        parser.add_argument('--marker', action='store', dest='marker', default=None,
159
            help='show output greater then marker')
160
        parser.add_argument('--prefix', action='store', dest='prefix', default=None,
161
            help='show output starting with prefix')
162
        parser.add_argument('--delimiter', action='store', dest='delimiter', default=None, 
163
            help='show output up to the delimiter')
164
        parser.add_argument('--path', action='store', dest='path', default=None, 
165
            help='show output starting with prefix up to /')
166
        parser.add_argument('--meta', action='store', dest='meta', default=None, 
167
            help='show output having the specified meta keys (e.g. --meta "meta1 meta2 ..."')
168
        parser.add_argument('--if-modified-since', action='store', dest='if_modified_since', 
169
            default=None, help='show output if modified since then')
170
        parser.add_argument('--if-unmodified-since', action='store', dest='if_unmodified_since',
171
            default=None, help='show output if not modified since then')
172
        parser.add_argument('--until', action='store', dest='until', default=None,
173
            help='show metadata until that date')
174
        dateformat = '%d/%m/%Y %H:%M:%S'
175
        parser.add_argument('--format', action='store', dest='format', default=dateformat,
176
            help='format to parse until date (default: d/m/Y H:M:S)')
177
        parser.add_argument('--shared', action='store_true', dest='shared', default=False,
178
            help='show only shared')
179
        parser.add_argument('--public', action='store_true', dest='public', default=False,
180
            help='show only public')
181

  
182
    def print_objects(self, object_list):
183
        import sys
184
        try:
185
            limit = getattr(self.args, 'show_size')
186
            limit = int(limit)
187
        except AttributeError:
188
            pass
189
        #index = 0
190
        for index,obj in enumerate(object_list):
191
            if not obj.has_key('content_type'):
192
                continue
193
            pretty_obj = obj.copy()
194
            index += 1
195
            empty_space = ' '*(len(str(len(object_list))) - len(str(index)))
196
            if obj['content_type'] == 'application/directory':
197
                isDir = True
198
                size = 'D'
199
            else:
200
                isDir = False
201
                size = format_size(obj['bytes'])
202
                pretty_obj['bytes'] = '%s (%s)'%(obj['bytes'],size)
203
            oname = bold(obj['name'])
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff