Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 062b1d0a

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 logging
38

    
39
from inspect import getargspec
40
from argparse import ArgumentParser, ArgumentError
41
from os.path import basename
42
from sys import exit, stdout, argv
43

    
44
from kamaki.cli.errors import CLIError, CLICmdSpecError
45
from kamaki.cli.utils import magenta, red, yellow, print_dict, print_list,\
46
    remove_colors
47
from kamaki.cli.command_tree import CommandTree
48
from kamaki.cli.argument import _arguments, parse_known_args
49
from kamaki.cli.history import History
50

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

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

    
68

    
69
def _allow_class_in_cmd_tree(cls):
70
    global allow_all_commands
71
    if allow_all_commands:
72
        return True
73
    global allow_no_commands
74
    if allow_no_commands:
75
        return False
76

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

    
93
    return True if index == len(term_list) else False
94

    
95

    
96
def command():
97
    """Class decorator that registers a class as a CLI command"""
98

    
99
    def decorator(cls):
100
        """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
101

    
102
        if not _allow_class_in_cmd_tree(cls):
103
            return cls
104

    
105
        cls.description, sep, cls.long_description\
106
            = cls.__doc__.partition('\n')
107

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

    
126
        # store each term, one by one, first
127
        _commands.add_command(cls.__name__, cls.description, cls)
128

    
129
        return cls
130
    return decorator
131

    
132

    
133
def _update_parser(parser, arguments):
134
    for name, argument in arguments.items():
135
        try:
136
            argument.update_parser(parser, name)
137
        except ArgumentError:
138
            pass
139

    
140

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

    
147

    
148
def _print_error_message(cli_err):
149
    errmsg = '%s' % cli_err
150
    if cli_err.importance == 1:
151
        errmsg = magenta(errmsg)
152
    elif cli_err.importance == 2:
153
        errmsg = yellow(errmsg)
154
    elif cli_err.importance > 2:
155
        errmsg = red(errmsg)
156
    stdout.write(errmsg)
157
    print_list(cli_err.details)
158

    
159

    
160
def get_command_group(unparsed):
161
    groups = _arguments['config'].get_groups()
162
    for grp_candidate in unparsed:
163
        if grp_candidate in groups:
164
            unparsed.remove(grp_candidate)
165
            return grp_candidate
166
    return None
167

    
168

    
169
def load_command(group, unparsed, reload_package=False):
170
    global candidate_command_terms
171
    candidate_command_terms = [group] + unparsed
172
    load_group_package(group, reload_package)
173

    
174
    #From all possible parsed commands, chose the first match in user string
175
    final_cmd = _commands.get_command(group)
176
    for term in unparsed:
177
        cmd = final_cmd.get_subcmd(term)
178
        if cmd is not None:
179
            final_cmd = cmd
180
            unparsed.remove(cmd.name)
181
    return final_cmd
182

    
183

    
184
def shallow_load():
185
    """Load only group names and descriptions"""
186
    global allow_no_commands
187
    allow_no_commands = True  # load only descriptions
188
    for grp in _arguments['config'].get_groups():
189
        load_group_package(grp)
190
    allow_no_commands = False
191

    
192

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

    
210

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

    
228

    
229
def setup_logging(silent=False, debug=False, verbose=False, include=False):
230
    """handle logging for clients package"""
231

    
232
    def add_handler(name, level, prefix=''):
233
        h = logging.StreamHandler()
234
        fmt = logging.Formatter(prefix + '%(message)s')
235
        h.setFormatter(fmt)
236
        logger = logging.getLogger(name)
237
        logger.addHandler(h)
238
        logger.setLevel(level)
239

    
240
    if silent:
241
        add_handler('', logging.CRITICAL)
242
    elif debug:
243
        add_handler('requests', logging.INFO, prefix='* ')
244
        add_handler('clients.send', logging.DEBUG, prefix='> ')
245
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
246
    elif verbose:
247
        add_handler('requests', logging.INFO, prefix='* ')
248
        add_handler('clients.send', logging.INFO, prefix='> ')
249
        add_handler('clients.recv', logging.INFO, prefix='< ')
250
    elif include:
251
        add_handler('clients.recv', logging.INFO)
252
    else:
253
        add_handler('', logging.WARNING)
254

    
255

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

    
273

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

    
293
        group = get_command_group(unparsed)
294
        if group is None:
295
            parser.print_help()
296
            shallow_load()
297
            print_commands(full_depth=_debug)
298
            exit(0)
299

    
300
        cmd = load_command(group, unparsed)
301
        # Find the most specific subcommand
302
        for term in list(unparsed):
303
            if cmd.is_command:
304
                break
305
            if cmd.contains(term):
306
                cmd = cmd.get_subcmd(term)
307
                unparsed.remove(term)
308

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

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

    
332
            print_commands(cmd.path, full_depth=_debug)
333
            exit(0)
334

    
335
        setup_logging(silent=_arguments['silent'].value,
336
            debug=_debug,
337
            verbose=_verbose,
338
            include=_arguments['include'].value)
339
        cli = cmd.get_class()
340
        executable = cli(_arguments)
341
        _update_parser(parser, executable.arguments)
342
        parser.prog = '%s %s %s'\
343
            % (exe, cmd.path.replace('_', ' '), cli.syntax)
344
        parsed, new_unparsed = parse_known_args(parser, _arguments)
345
        unparsed = [term for term in unparsed if term in new_unparsed]
346
        ret = _exec_cmd(executable, unparsed, parser.print_help)
347
        exit(ret)
348
    except Exception as err:
349
        if _debug:
350
            from traceback import print_stack
351
            print_stack()
352
            raise
353
        err = err if isinstance(err, CLIError)\
354
            else CLIError('Unexpected Error (%s): %s' % (type(err), err))
355
        _print_error_message(err)
356
        exit(1)
357

    
358
from command_shell import _fix_arguments, Shell
359

    
360

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

    
369

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

    
386

    
387
def main():
388

    
389
    if len(argv) <= 1:
390
        run_shell()
391
    else:
392
        one_command()