Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 3dabe5d2

History | View | Annotate | Download (12.5 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, argv
47

    
48
from kamaki.cli.errors import CLIError, CLICmdSpecError
49
from kamaki.cli.utils import magenta, red, yellow, print_dict, remove_colors
50
from kamaki.cli.command_tree import CommandTree
51
from kamaki.cli.argument import _arguments, parse_known_args
52
from kamaki.cli.history import History
53

    
54
cmd_spec_locations = [
55
    'kamaki.cli.commands',
56
    'kamaki.commands',
57
    'kamaki.cli',
58
    'kamaki',
59
    '']
60
_commands = CommandTree(name='kamaki',
61
    description='A command line tool for poking clouds')
62

    
63
# If empty, all commands are loaded, if not empty, only commands in this list
64
# e.g. [store, lele, list, lolo] is good to load store_list but not list_store
65
# First arg should always refer to a group
66
candidate_command_terms = []
67
allow_no_commands = False
68
allow_all_commands = False
69
allow_subclass_signatures = False
70

    
71

    
72
def _allow_class_in_cmd_tree(cls):
73
    global allow_all_commands
74
    if allow_all_commands:
75
        return True
76
    global allow_no_commands
77
    if allow_no_commands:
78
        return False
79

    
80
    term_list = cls.__name__.split('_')
81
    global candidate_command_terms
82
    index = 0
83
    for term in candidate_command_terms:
84
        try:
85
            index += 1 if term_list[index] == term else 0
86
        except IndexError:  # Whole term list matched!
87
            return True
88
    if allow_subclass_signatures:
89
        if index == len(candidate_command_terms) and len(term_list) > index:
90
            try:  # is subterm already in _commands?
91
                _commands.get_command('_'.join(term_list[:index + 1]))
92
            except KeyError:  # No, so it must be placed there
93
                return True
94
        return False
95

    
96
    return True if index == len(term_list) else False
97

    
98

    
99
def command():
100
    """Class decorator that registers a class as a CLI command"""
101

    
102
    def decorator(cls):
103
        """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
104

    
105
        if not _allow_class_in_cmd_tree(cls):
106
            return cls
107

    
108
        cls.description, sep, cls.long_description\
109
            = cls.__doc__.partition('\n')
110

    
111
        # Generate a syntax string based on main's arguments
112
        spec = getargspec(cls.main.im_func)
113
        args = spec.args[1:]
114
        n = len(args) - len(spec.defaults or ())
115
        required = ' '.join('<%s>' % x\
116
            .replace('____', '[:')\
117
            .replace('___', ':')\
118
            .replace('__', ']').\
119
            replace('_', ' ') for x in args[:n])
120
        optional = ' '.join('[%s]' % x\
121
            .replace('____', '[:')\
122
            .replace('___', ':')\
123
            .replace('__', ']').\
124
            replace('_', ' ') for x in args[n:])
125
        cls.syntax = ' '.join(x for x in [required, optional] if x)
126
        if spec.varargs:
127
            cls.syntax += ' <%s ...>' % spec.varargs
128

    
129
        # store each term, one by one, first
130
        _commands.add_command(cls.__name__, cls.description, cls)
131

    
132
        return cls
133
    return decorator
134

    
135

    
136
def _update_parser(parser, arguments):
137
    for name, argument in arguments.items():
138
        try:
139
            argument.update_parser(parser, name)
140
        except ArgumentError:
141
            pass
142

    
143

    
144
def _init_parser(exe):
145
    parser = ArgumentParser(add_help=False)
146
    parser.prog = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
147
    _update_parser(parser, _arguments)
148
    return parser
149

    
150

    
151
def _print_error_message(cli_err):
152
    errmsg = '%s (%s)' % (cli_err, cli_err.status if cli_err.status else ' ')
153
    if cli_err.importance == 1:
154
        errmsg = magenta(errmsg)
155
    elif cli_err.importance == 2:
156
        errmsg = yellow(errmsg)
157
    elif cli_err.importance > 2:
158
        errmsg = red(errmsg)
159
    stdout.write(errmsg)
160
    if cli_err.details is not None and len(cli_err.details) > 0:
161
        print(': %s' % cli_err.details)
162
    else:
163
        print()
164

    
165

    
166
def get_command_group(unparsed):
167
    groups = _arguments['config'].get_groups()
168
    for grp_candidate in unparsed:
169
        if grp_candidate in groups:
170
            unparsed.remove(grp_candidate)
171
            return grp_candidate
172
    return None
173

    
174

    
175
def load_command(group, unparsed, reload_package=False):
176
    global candidate_command_terms
177
    candidate_command_terms = [group] + unparsed
178
    load_group_package(group, reload_package)
179

    
180
    #From all possible parsed commands, chose the first match in user string
181
    final_cmd = _commands.get_command(group)
182
    for term in unparsed:
183
        cmd = final_cmd.get_subcmd(term)
184
        if cmd is not None:
185
            final_cmd = cmd
186
            unparsed.remove(cmd.name)
187
    return final_cmd
188

    
189

    
190
def shallow_load():
191
    """Load only group names and descriptions"""
192
    global allow_no_commands
193
    allow_no_commands = True  # load only descriptions
194
    for grp in _arguments['config'].get_groups():
195
        load_group_package(grp)
196
    allow_no_commands = False
197

    
198

    
199
def load_group_package(group, reload_package=False):
200
    spec_pkg = _arguments['config'].value.get(group, 'cli')
201
    if spec_pkg is None:
202
        return None
203
    for location in cmd_spec_locations:
204
        location += spec_pkg if location == '' else ('.' + spec_pkg)
205
        try:
206
            package = __import__(location, fromlist=['API_DESCRIPTION'])
207
        except ImportError:
208
            continue
209
        if reload_package:
210
            reload(package)
211
        for grp, descr in package.API_DESCRIPTION.items():
212
            _commands.add_command(grp, descr)
213
        return package
214
    raise CLICmdSpecError(details='Cmd Spec Package %s load failed' % spec_pkg)
215

    
216

    
217
def print_commands(prefix=None, full_depth=False):
218
    cmd_list = _commands.get_groups() if prefix is None\
219
        else _commands.get_subcommands(prefix)
220
    cmds = {}
221
    for subcmd in cmd_list:
222
        if subcmd.sublen() > 0:
223
            sublen_str = '( %s more terms ... )' % subcmd.sublen()
224
            cmds[subcmd.name] = [subcmd.help, sublen_str]\
225
                if subcmd.has_description else sublen_str
226
        else:
227
            cmds[subcmd.name] = subcmd.help
228
    if len(cmds) > 0:
229
        print('\nOptions:')
230
        print_dict(cmds, ident=12)
231
    if full_depth:
232
        _commands.pretty_print()
233

    
234

    
235
def setup_logging(silent=False, debug=False, verbose=False, include=False):
236
    """handle logging for clients package"""
237

    
238
    def add_handler(name, level, prefix=''):
239
        h = logging.StreamHandler()
240
        fmt = logging.Formatter(prefix + '%(message)s')
241
        h.setFormatter(fmt)
242
        logger = logging.getLogger(name)
243
        logger.addHandler(h)
244
        logger.setLevel(level)
245

    
246
    if silent:
247
        add_handler('', logging.CRITICAL)
248
    elif debug:
249
        add_handler('requests', logging.INFO, prefix='* ')
250
        add_handler('clients.send', logging.DEBUG, prefix='> ')
251
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
252
    elif verbose:
253
        add_handler('requests', logging.INFO, prefix='* ')
254
        add_handler('clients.send', logging.INFO, prefix='> ')
255
        add_handler('clients.recv', logging.INFO, prefix='< ')
256
    elif include:
257
        add_handler('clients.recv', logging.INFO)
258
    else:
259
        add_handler('', logging.WARNING)
260

    
261

    
262
def _exec_cmd(instance, cmd_args, help_method):
263
    try:
264
        return instance.main(*cmd_args)
265
    except TypeError as err:
266
        if err.args and err.args[0].startswith('main()'):
267
            print(magenta('Syntax error'))
268
            if instance.get_argument('verbose'):
269
                print(unicode(err))
270
            help_method()
271
        else:
272
            raise
273
    except CLIError as err:
274
        if instance.get_argument('debug'):
275
            raise
276
        _print_error_message(err)
277
    return 1
278

    
279

    
280
def one_command():
281
    _debug = False
282
    _help = False
283
    _verbose = False
284
    try:
285
        exe = basename(argv[0])
286
        parser = _init_parser(exe)
287
        parsed, unparsed = parse_known_args(parser, _arguments)
288
        _colors = _arguments['config'].get('global', 'colors')
289
        if _colors != 'on':
290
            remove_colors()
291
        _history = History(_arguments['config'].get('history', 'file'))
292
        _history.add(' '.join([exe] + argv[1:]))
293
        _debug = _arguments['debug'].value
294
        _help = _arguments['help'].value
295
        _verbose = _arguments['verbose'].value
296
        if _arguments['version'].value:
297
            exit(0)
298

    
299
        group = get_command_group(unparsed)
300
        if group is None:
301
            parser.print_help()
302
            shallow_load()
303
            print_commands(full_depth=_debug)
304
            exit(0)
305

    
306
        cmd = load_command(group, unparsed)
307
        if _help or not cmd.is_command:
308
            if cmd.has_description:
309
                parser.description = cmd.help
310
            else:
311
                try:
312
                    parser.description\
313
                        = _commands.get_closest_ancestor_command(cmd.path).help
314
                except KeyError:
315
                    parser.description = ' '
316
            parser.prog = '%s %s ' % (exe, cmd.path.replace('_', ' '))
317
            if cmd.is_command:
318
                cli = cmd.get_class()
319
                parser.prog += cli.syntax
320
                _update_parser(parser, cli().arguments)
321
            else:
322
                parser.prog += '[...]'
323
            parser.print_help()
324

    
325
            # load one more level just to see what is missing
326
            global allow_subclass_signatures
327
            allow_subclass_signatures = True
328
            load_command(group, cmd.path.split('_')[1:], reload_package=True)
329

    
330
            print_commands(cmd.path, full_depth=_debug)
331
            exit(0)
332

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

    
352
from command_shell import _fix_arguments, Shell
353

    
354

    
355
def _start_shell():
356
    shell = Shell()
357
    shell.set_prompt(basename(argv[0]))
358
    from kamaki import __version__ as version
359
    shell.greet(version)
360
    shell.do_EOF = shell.do_exit
361
    return shell
362

    
363

    
364
def run_shell():
365
    _fix_arguments()
366
    shell = _start_shell()
367
    _config = _arguments['config']
368
    _config.value = None
369
    for grp in _config.get_groups():
370
        global allow_all_commands
371
        allow_all_commands = True
372
        load_group_package(grp)
373
    setup_logging(silent=_arguments['silent'].value,
374
        debug=_arguments['debug'].value,
375
        verbose=_arguments['verbose'].value,
376
        include=_arguments['include'].value)
377
    shell.cmd_tree = _commands
378
    shell.run()
379

    
380

    
381
def main():
382

    
383
    if len(argv) <= 1:
384
        run_shell()
385
    else:
386
        one_command()