Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ e02728f9

History | View | Annotate | Download (12.4 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, remove_colors
46
from kamaki.cli.command_tree import CommandTree
47
from kamaki.cli.argument import _arguments, parse_known_args
48
from kamaki.cli.history import History
49

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

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

    
67

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

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

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

    
94

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

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

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

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

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

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

    
128
        return cls
129
    return decorator
130

    
131

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

    
139

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

    
146

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

    
161

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

    
170

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

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

    
185

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

    
194

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

    
212

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

    
230

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

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

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

    
257

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

    
275

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

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

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

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

    
326
            print_commands(cmd.path, full_depth=_debug)
327
            exit(0)
328

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

    
348
from command_shell import _fix_arguments, Shell
349

    
350

    
351
def _start_shell():
352
    shell = Shell()
353
    shell.set_prompt(basename(argv[0]))
354
    from kamaki import __version__ as version
355
    shell.greet(version)
356
    shell.do_EOF = shell.do_exit
357
    return shell
358

    
359

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

    
376

    
377
def main():
378

    
379
    if len(argv) <= 1:
380
        run_shell()
381
    else:
382
        one_command()