Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ fd5db045

History | View | Annotate | Download (12.7 kB)

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

    
35
from __future__ import print_function
36

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

    
41
import logging
42

    
43
from inspect import getargspec
44
from argparse import ArgumentParser, ArgumentError
45
from os.path import basename
46
from sys import exit, stdout, stderr, argv
47

    
48
try:
49
    from collections import OrderedDict
50
except ImportError:
51
    from ordereddict import OrderedDict
52

    
53
#from kamaki import clients
54
from kamaki.cli.errors import CLIError, CLISyntaxError,\
55
    CLICmdIncompleteError, CLICmdSpecError
56
from kamaki.cli.utils import bold, magenta, red, yellow,\
57
    print_list, print_dict, remove_colors
58
from kamaki.cli.command_tree import CommandTree
59
from kamaki.cli.argument import _arguments, parse_known_args
60
from kamaki.cli.history import History
61

    
62
cmd_spec_locations = [
63
    'kamaki.cli.commands',
64
    'kamaki.commands',
65
    'kamaki.cli',
66
    'kamaki',
67
    '']
68
_commands = CommandTree(name='kamaki',
69
    description='A command line tool for poking clouds')
70

    
71
# If empty, all commands are loaded, if not empty, only commands in this list
72
# e.g. [store, lele, list, lolo] is good to load store_list but not list_store
73
# First arg should always refer to a group
74
candidate_command_terms = []
75
allow_no_commands = False
76
allow_all_commands = False
77
allow_subclass_signatures = False
78

    
79

    
80
def _allow_class_in_cmd_tree(cls):
81
    global allow_all_commands
82
    if allow_all_commands:
83
        return True
84
    global allow_no_commands
85
    if allow_no_commands:
86
        return False
87

    
88
    term_list = cls.__name__.split('_')
89
    global candidate_command_terms
90
    index = 0
91
    for term in candidate_command_terms:
92
        try:
93
            index += 1 if term_list[index] == term else 0
94
        except IndexError:  # Whole term list matched!
95
            return True
96
    if allow_subclass_signatures:
97
        if index == len(candidate_command_terms) and len(term_list) > index:
98
            try:  # is subterm already in _commands?
99
                _commands.get_command('_'.join(term_list[:index + 1]))
100
            except KeyError:  # No, so it must be placed there
101
                return True
102
        return False
103

    
104
    return True if index == len(term_list) else False
105

    
106

    
107
def command():
108
    """Class decorator that registers a class as a CLI command"""
109

    
110
    def decorator(cls):
111
        """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
112

    
113
        if not _allow_class_in_cmd_tree(cls):
114
            return cls
115

    
116
        cls.description, sep, cls.long_description\
117
            = cls.__doc__.partition('\n')
118

    
119
        # Generate a syntax string based on main's arguments
120
        spec = getargspec(cls.main.im_func)
121
        args = spec.args[1:]
122
        n = len(args) - len(spec.defaults or ())
123
        required = ' '.join('<%s>' % x\
124
            .replace('____', '[:')\
125
            .replace('___', ':')\
126
            .replace('__', ']').\
127
            replace('_', ' ') for x in args[:n])
128
        optional = ' '.join('[%s]' % x\
129
            .replace('____', '[:')\
130
            .replace('___', ':')\
131
            .replace('__', ']').\
132
            replace('_', ' ') for x in args[n:])
133
        cls.syntax = ' '.join(x for x in [required, optional] if x)
134
        if spec.varargs:
135
            cls.syntax += ' <%s ...>' % spec.varargs
136

    
137
        # store each term, one by one, first
138
        _commands.add_command(cls.__name__, cls.description, cls)
139

    
140
        return cls
141
    return decorator
142

    
143

    
144
def _update_parser(parser, arguments):
145
    for name, argument in arguments.items():
146
        try:
147
            argument.update_parser(parser, name)
148
        except ArgumentError:
149
            pass
150

    
151

    
152
def _init_parser(exe):
153
    parser = ArgumentParser(add_help=False)
154
    parser.prog = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
155
    _update_parser(parser, _arguments)
156
    return parser
157

    
158

    
159
def _print_error_message(cli_err):
160
    errmsg = '%s (%s)' % (cli_err, cli_err.status if cli_err.status else ' ')
161
    if cli_err.importance == 1:
162
        errmsg = magenta(errmsg)
163
    elif cli_err.importance == 2:
164
        errmsg = yellow(errmsg)
165
    elif cli_err.importance > 2:
166
        errmsg = red(errmsg)
167
    stdout.write(errmsg)
168
    if cli_err.details is not None and len(cli_err.details) > 0:
169
        print(': %s' % cli_err.details)
170
    else:
171
        print()
172

    
173

    
174
def get_command_group(unparsed):
175
    groups = _arguments['config'].get_groups()
176
    for grp_candidate in unparsed:
177
        if grp_candidate in groups:
178
            unparsed.remove(grp_candidate)
179
            return grp_candidate
180
    return None
181

    
182

    
183
def load_command(group, unparsed, reload_package=False):
184
    global candidate_command_terms
185
    candidate_command_terms = [group] + unparsed
186
    pkg = load_group_package(group, reload_package)
187

    
188
    #From all possible parsed commands, chose the first match in user string
189
    final_cmd = _commands.get_command(group)
190
    for term in unparsed:
191
        cmd = final_cmd.get_subcmd(term)
192
        if cmd is not None:
193
            final_cmd = cmd
194
            unparsed.remove(cmd.name)
195
    return final_cmd
196

    
197

    
198
def shallow_load():
199
    """Load only group names and descriptions"""
200
    global allow_no_commands
201
    allow_no_commands = True  # load only descriptions
202
    for grp in _arguments['config'].get_groups():
203
        load_group_package(grp)
204
    allow_no_commands = False
205

    
206

    
207
def load_group_package(group, reload_package=False):
208
    spec_pkg = _arguments['config'].value.get(group, 'cli')
209
    if spec_pkg is None:
210
        return None
211
    for location in cmd_spec_locations:
212
        location += spec_pkg if location == '' else ('.' + spec_pkg)
213
        try:
214
            package = __import__(location, fromlist=['API_DESCRIPTION'])
215
        except ImportError:
216
            continue
217
        if reload_package:
218
            reload(package)
219
        for grp, descr in package.API_DESCRIPTION.items():
220
            _commands.add_command(grp, descr)
221
        return package
222
    raise CLICmdSpecError(details='Cmd Spec Package %s load failed' % spec_pkg)
223

    
224

    
225
def print_commands(prefix=None, full_depth=False):
226
    cmd_list = _commands.get_groups() if prefix is None\
227
        else _commands.get_subcommands(prefix)
228
    cmds = {}
229
    for subcmd in cmd_list:
230
        if subcmd.sublen() > 0:
231
            sublen_str = '( %s more terms ... )' % subcmd.sublen()
232
            cmds[subcmd.name] = [subcmd.help, sublen_str]\
233
                if subcmd.has_description else subcmd_str
234
        else:
235
            cmds[subcmd.name] = subcmd.help
236
    if len(cmds) > 0:
237
        print('\nOptions:')
238
        print_dict(cmds, ident=12)
239
    if full_depth:
240
        _commands.pretty_print()
241

    
242

    
243
def setup_logging(silent=False, debug=False, verbose=False, include=False):
244
    """handle logging for clients package"""
245

    
246
    def add_handler(name, level, prefix=''):
247
        h = logging.StreamHandler()
248
        fmt = logging.Formatter(prefix + '%(message)s')
249
        h.setFormatter(fmt)
250
        logger = logging.getLogger(name)
251
        logger.addHandler(h)
252
        logger.setLevel(level)
253

    
254
    if silent:
255
        add_handler('', logging.CRITICAL)
256
    elif debug:
257
        add_handler('requests', logging.INFO, prefix='* ')
258
        add_handler('clients.send', logging.DEBUG, prefix='> ')
259
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
260
    elif verbose:
261
        add_handler('requests', logging.INFO, prefix='* ')
262
        add_handler('clients.send', logging.INFO, prefix='> ')
263
        add_handler('clients.recv', logging.INFO, prefix='< ')
264
    elif include:
265
        add_handler('clients.recv', logging.INFO)
266
    else:
267
        add_handler('', logging.WARNING)
268

    
269

    
270
def _exec_cmd(instance, cmd_args, help_method):
271
    try:
272
        return instance.main(*cmd_args)
273
    except TypeError as err:
274
        if err.args and err.args[0].startswith('main()'):
275
            print(magenta('Syntax error'))
276
            if instance.get_argument('verbose'):
277
                print(unicode(err))
278
            help_method()
279
        else:
280
            raise
281
    except CLIError as err:
282
        if instance.get_argument('debug'):
283
            raise
284
        _print_error_message(err)
285
    return 1
286

    
287

    
288
def one_command():
289
    _debug = False
290
    _help = False
291
    _verbose = False
292
    try:
293
        exe = basename(argv[0])
294
        parser = _init_parser(exe)
295
        parsed, unparsed = parse_known_args(parser, _arguments)
296
        _colors = _arguments['config'].get('global', 'colors')
297
        if _colors != 'on':
298
            remove_colors()
299
        _history = History(_arguments['config'].get('history', 'file'))
300
        _history.add(' '.join([exe] + argv[1:]))
301
        _debug = _arguments['debug'].value
302
        _help = _arguments['help'].value
303
        _verbose = _arguments['verbose'].value
304
        if _arguments['version'].value:
305
            exit(0)
306

    
307
        group = get_command_group(unparsed)
308
        if group is None:
309
            parser.print_help()
310
            shallow_load()
311
            print_commands(full_depth=_debug)
312
            exit(0)
313

    
314
        cmd = load_command(group, unparsed)
315
        if _help or not cmd.is_command:
316
            if cmd.has_description:
317
                parser.description = cmd.help
318
            else:
319
                try:
320
                    parser.description\
321
                        = _commands.get_closest_ancestor_command(cmd.path).help
322
                except KeyError:
323
                    parser.description = ' '
324
            parser.prog = '%s %s ' % (exe, cmd.path.replace('_', ' '))
325
            if cmd.is_command:
326
                cli = cmd.get_class()
327
                parser.prog += cli.syntax
328
                _update_parser(parser, cli().arguments)
329
            else:
330
                parser.prog += '[...]'
331
            parser.print_help()
332

    
333
            # load one more level just to see what is missing
334
            global allow_subclass_signatures
335
            allow_subclass_signatures = True
336
            load_command(group, cmd.path.split('_')[1:], reload_package=True)
337

    
338
            print_commands(cmd.path, full_depth=_debug)
339
            exit(0)
340

    
341
        setup_logging(silent=_arguments['silent'].value,
342
            debug=_debug,
343
            verbose=_verbose,
344
            include=_arguments['include'].value)
345
        cli = cmd.get_class()
346
        executable = cli(_arguments)
347
        _update_parser(parser, executable.arguments)
348
        parser.prog = '%s %s %s'\
349
            % (exe, cmd.path.replace('_', ' '), cli.syntax)
350
        parsed, new_unparsed = parse_known_args(parser, _arguments)
351
        unparsed = [term for term in unparsed if term in new_unparsed]
352
        ret = _exec_cmd(executable, unparsed, parser.print_help)
353
        exit(ret)
354
    except CLIError as err:
355
        if _debug:
356
            raise
357
        _print_error_message(err)
358
        exit(1)
359

    
360
from command_shell import _fix_arguments, Shell
361

    
362

    
363
def _start_shell():
364
    shell = Shell()
365
    shell.set_prompt(basename(argv[0]))
366
    from kamaki import __version__ as version
367
    shell.greet(version)
368
    shell.do_EOF = shell.do_exit
369
    return shell
370

    
371

    
372
def run_shell():
373
    _fix_arguments()
374
    shell = _start_shell()
375
    _config = _arguments['config']
376
    _config.value = None
377
    for grp in _config.get_groups():
378
        global allow_all_commands
379
        allow_all_commands = True
380
        load_group_package(grp)
381
    setup_logging(silent=_arguments['silent'].value,
382
        debug=_arguments['debug'].value,
383
        verbose=_arguments['verbose'].value,
384
        include=_arguments['include'].value)
385
    shell.cmd_tree = _commands
386
    shell.run()
387

    
388

    
389
def main():
390

    
391
    if len(argv) <= 1:
392
        run_shell()
393
    else:
394
        one_command()