Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 0238c167

History | View | Annotate | Download (12.3 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
        if _help or not cmd.is_command:
302
            if cmd.has_description:
303
                parser.description = cmd.help
304
            else:
305
                try:
306
                    parser.description\
307
                        = _commands.get_closest_ancestor_command(cmd.path).help
308
                except KeyError:
309
                    parser.description = ' '
310
            parser.prog = '%s %s ' % (exe, cmd.path.replace('_', ' '))
311
            if cmd.is_command:
312
                cli = cmd.get_class()
313
                parser.prog += cli.syntax
314
                _update_parser(parser, cli().arguments)
315
            else:
316
                parser.prog += '[...]'
317
            parser.print_help()
318

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

    
324
            print_commands(cmd.path, full_depth=_debug)
325
            exit(0)
326

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

    
346
from command_shell import _fix_arguments, Shell
347

    
348

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

    
357

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

    
374

    
375
def main():
376

    
377
    if len(argv) <= 1:
378
        run_shell()
379
    else:
380
        one_command()