Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 451a7992

History | View | Annotate | Download (13.8 kB)

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

    
34
import logging
35
from sys import argv, exit, stdout
36
from os.path import basename
37
from inspect import getargspec
38

    
39
from kamaki.cli.argument import ArgumentParseManager
40
from kamaki.cli.history import History
41
from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
42
from kamaki.cli.errors import CLIError
43

    
44
_help = False
45
_debug = False
46
_verbose = False
47
_colors = False
48
kloger = None
49

    
50

    
51
def _construct_command_syntax(cls):
52
        spec = getargspec(cls.main.im_func)
53
        args = spec.args[1:]
54
        n = len(args) - len(spec.defaults or ())
55
        required = ' '.join('<%s>' % x\
56
            .replace('____', '[:')\
57
            .replace('___', ':')\
58
            .replace('__', ']').\
59
            replace('_', ' ') for x in args[:n])
60
        optional = ' '.join('[%s]' % x\
61
            .replace('____', '[:')\
62
            .replace('___', ':')\
63
            .replace('__', ']').\
64
            replace('_', ' ') for x in args[n:])
65
        cls.syntax = ' '.join(x for x in [required, optional] if x)
66
        if spec.varargs:
67
            cls.syntax += ' <%s ...>' % spec.varargs
68

    
69

    
70
def _get_cmd_tree_from_spec(spec, cmd_tree_list):
71
    for tree in cmd_tree_list:
72
        if tree.name == spec:
73
            return tree
74
    return None
75

    
76

    
77
_best_match = []
78

    
79

    
80
def _num_of_matching_terms(basic_list, attack_list):
81
    if not attack_list:
82
        return len(basic_list)
83

    
84
    matching_terms = 0
85
    for i, term in enumerate(basic_list):
86
        try:
87
            if term != attack_list[i]:
88
                break
89
        except IndexError:
90
            break
91
        matching_terms += 1
92
    return matching_terms
93

    
94

    
95
def _update_best_match(name_terms, prefix=[]):
96
    if prefix:
97
        pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
98
    else:
99
        pref_list = []
100

    
101
    num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
102
    global _best_match
103
    if not prefix:
104
        _best_match = []
105

    
106
    if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
107
        if len(_best_match) < num_of_matching_terms:
108
            _best_match = name_terms[:num_of_matching_terms]
109
        return True
110
    return False
111

    
112

    
113
def command(cmd_tree, prefix='', descedants_depth=1):
114
    """Load a class as a command
115
        e.g. spec_cmd0_cmd1 will be command spec cmd0
116

117
        :param cmd_tree: is initialized in cmd_spec file and is the structure
118
            where commands are loaded. Var name should be _commands
119
        :param prefix: if given, load only commands prefixed with prefix,
120
        :param descedants_depth: is the depth of the tree descedants of the
121
            prefix command. It is used ONLY if prefix and if prefix is not
122
            a terminal command
123

124
        :returns: the specified class object
125
    """
126

    
127
    def wrap(cls):
128
        global kloger
129
        cls_name = cls.__name__
130

    
131
        if not cmd_tree:
132
            if _debug:
133
                kloger.warning('command %s found but not loaded' % cls_name)
134
            return cls
135

    
136
        name_terms = cls_name.split('_')
137
        if not _update_best_match(name_terms, prefix):
138
            if _debug:
139
                kloger.warning('%s failed to update_best_match' % cls_name)
140
            return None
141

    
142
        global _best_match
143
        max_len = len(_best_match) + descedants_depth
144
        if len(name_terms) > max_len:
145
            partial = '_'.join(name_terms[:max_len])
146
            if not cmd_tree.has_command(partial):  # add partial path
147
                cmd_tree.add_command(partial)
148
            if _debug:
149
                kloger.warning('%s failed max_len test' % cls_name)
150
            return None
151

    
152
        cls.description, sep, cls.long_description\
153
        = cls.__doc__.partition('\n')
154
        _construct_command_syntax(cls)
155

    
156
        cmd_tree.add_command(cls_name, cls.description, cls)
157
        return cls
158
    return wrap
159

    
160

    
161
def get_cmd_terms():
162
    global command
163
    return [term for term in command.func_defaults[0]\
164
        if not term.startswith('-')]
165

    
166
cmd_spec_locations = [
167
    'kamaki.cli.commands',
168
    'kamaki.commands',
169
    'kamaki.cli',
170
    'kamaki',
171
    '']
172

    
173

    
174
def _setup_logging(silent=False, debug=False, verbose=False, include=False):
175
    """handle logging for clients package"""
176

    
177
    def add_handler(name, level, prefix=''):
178
        h = logging.StreamHandler()
179
        fmt = logging.Formatter(prefix + '%(message)s')
180
        h.setFormatter(fmt)
181
        logger = logging.getLogger(name)
182
        logger.addHandler(h)
183
        logger.setLevel(level)
184

    
185
    if silent:
186
        add_handler('', logging.CRITICAL)
187
        return
188

    
189
    if debug:
190
        add_handler('requests', logging.INFO, prefix='* ')
191
        add_handler('clients.send', logging.DEBUG, prefix='> ')
192
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
193
        add_handler('kamaki', logging.DEBUG, prefix='DEBUG: ')
194
    elif verbose:
195
        add_handler('requests', logging.INFO, prefix='* ')
196
        add_handler('clients.send', logging.INFO, prefix='> ')
197
        add_handler('clients.recv', logging.INFO, prefix='< ')
198
        add_handler('kamaki', logging.INFO, prefix='INFO: ')
199
    elif include:
200
        add_handler('clients.recv', logging.INFO)
201
    add_handler('kamaki', logging.WARNING, prefix='WARNING: ')
202
    global kloger
203
    kloger = logging.getLogger('kamaki.warning')
204

    
205

    
206
def _init_session(arguments):
207
    global _help
208
    _help = arguments['help'].value
209
    global _debug
210
    _debug = arguments['debug'].value
211
    global _verbose
212
    _verbose = arguments['verbose'].value
213
    global _colors
214
    _colors = arguments['config'].get('global', 'colors')
215
    if not (stdout.isatty() and _colors == 'on'):
216
        from kamaki.cli.utils import remove_colors
217
        remove_colors()
218
    _silent = arguments['silent'].value
219
    _include = arguments['include'].value
220
    _setup_logging(_silent, _debug, _verbose, _include)
221

    
222

    
223
def get_command_group(unparsed, arguments):
224
    groups = arguments['config'].get_groups()
225
    for term in unparsed:
226
        if term.startswith('-'):
227
            continue
228
        if term in groups:
229
            unparsed.remove(term)
230
            return term
231
        return None
232
    return None
233

    
234

    
235
def _load_spec_module(spec, arguments, module):
236
    spec_name = arguments['config'].get(spec, 'cli')
237
    if spec_name is None:
238
        return None
239
    pkg = None
240
    for location in cmd_spec_locations:
241
        location += spec_name if location == '' else '.%s' % spec_name
242
        try:
243
            pkg = __import__(location, fromlist=[module])
244
            return pkg
245
        except ImportError:
246
            continue
247
    return pkg
248

    
249

    
250
def _groups_help(arguments):
251
    global _debug
252
    global kloger
253
    descriptions = {}
254
    for spec in arguments['config'].get_groups():
255
        pkg = _load_spec_module(spec, arguments, '_commands')
256
        if pkg:
257
            cmds = None
258
            try:
259
                cmds = [
260
                    cmd for cmd in getattr(pkg, '_commands')\
261
                    if arguments['config'].get(cmd.name, 'cli')
262
                ]
263
            except AttributeError:
264
                if _debug:
265
                    kloger.warning('No description for %s' % spec)
266
            try:
267
                for cmd in cmds:
268
                    descriptions[cmd.name] = cmd.description
269
            except TypeError:
270
                if _debug:
271
                    kloger.warning('no cmd specs in module %s' % spec)
272
        elif _debug:
273
            kloger.warning('Loading of %s cmd spec failed' % spec)
274
    print('\nOptions:\n - - - -')
275
    print_dict(descriptions)
276

    
277

    
278
def _print_subcommands_help(cmd):
279
    printout = {}
280
    for subcmd in cmd.get_subcommands():
281
        spec, sep, print_path = subcmd.path.partition('_')
282
        printout[print_path.replace('_', ' ')] = subcmd.description
283
    if printout:
284
        print('\nOptions:\n - - - -')
285
        print_dict(printout)
286

    
287

    
288
def _update_parser_help(parser, cmd):
289
    global _best_match
290
    parser.syntax = parser.syntax.split('<')[0]
291
    parser.syntax += ' '.join(_best_match)
292

    
293
    if cmd.is_command:
294
        cls = cmd.get_class()
295
        parser.syntax += ' ' + cls.syntax
296
        parser.update_arguments(cls().arguments)
297
        # arguments = cls().arguments
298
        # update_arguments(parser, arguments)
299
    else:
300
        parser.syntax += ' <...>'
301
    if cmd.has_description:
302
        parser.parser.description = cmd.help
303

    
304

    
305
def _print_error_message(cli_err):
306
    errmsg = '%s' % cli_err
307
    if cli_err.importance == 1:
308
        errmsg = magenta(errmsg)
309
    elif cli_err.importance == 2:
310
        errmsg = yellow(errmsg)
311
    elif cli_err.importance > 2:
312
        errmsg = red(errmsg)
313
    stdout.write(errmsg)
314
    print_list(cli_err.details)
315

    
316

    
317
def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
318
    matched = [term for term in unparsed if not term.startswith('-')]
319
    while matched:
320
        try:
321
            return cmd_tree.get_command('_'.join(matched))
322
        except KeyError:
323
            matched = matched[:-1]
324
    return None
325

    
326

    
327
def _exec_cmd(instance, cmd_args, help_method):
328
    try:
329
        return instance.main(*cmd_args)
330
    except TypeError as err:
331
        if err.args and err.args[0].startswith('main()'):
332
            print(magenta('Syntax error'))
333
            if _debug:
334
                raise err
335
            if _verbose:
336
                print(unicode(err))
337
            help_method()
338
        else:
339
            raise
340
    return 1
341

    
342

    
343
def set_command_params(parameters):
344
    """Add a parameters list to a command
345

346
    :param paramters: (list of str) a list of parameters
347
    """
348
    global command
349
    def_params = list(command.func_defaults)
350
    def_params[0] = parameters
351
    command.func_defaults = tuple(def_params)
352

    
353

    
354
#def one_cmd(parser, unparsed, arguments):
355
def one_cmd(parser):
356
    group = get_command_group(list(parser.unparsed), parser.arguments)
357
    if not group:
358
        parser.parser.print_help()
359
        _groups_help(parser.arguments)
360
        exit(0)
361

    
362
    nonargs = [term for term in parser.unparsed if not term.startswith('-')]
363
    set_command_params(nonargs)
364

    
365
    global _best_match
366
    _best_match = []
367

    
368
    spec_module = _load_spec_module(group, parser.arguments, '_commands')
369

    
370
    cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
371

    
372
    if _best_match:
373
        cmd = cmd_tree.get_command('_'.join(_best_match))
374
    else:
375
        cmd = _get_best_match_from_cmd_tree(cmd_tree, parser.unparsed)
376
        _best_match = cmd.path.split('_')
377
    if cmd is None:
378
        if _debug or _verbose:
379
            print('Unexpected error: failed to load command')
380
        exit(1)
381

    
382
    _update_parser_help(parser, cmd)
383

    
384
    if _help or not cmd.is_command:
385
        parser.parser.print_help()
386
        _print_subcommands_help(cmd)
387
        exit(0)
388

    
389
    cls = cmd.get_class()
390
    executable = cls(parser.arguments)
391
    parser.update_arguments(executable.arguments)
392
    #parsed, unparsed = parse_known_args(parser, executable.arguments)
393
    for term in _best_match:
394
        parser.unparsed.remove(term)
395
    _exec_cmd(executable, parser.unparsed, parser.parser.print_help)
396

    
397

    
398
def _load_all_commands(cmd_tree, arguments):
399
    _config = arguments['config']
400
    for spec in [spec for spec in _config.get_groups()\
401
            if _config.get(spec, 'cli')]:
402
        try:
403
            spec_module = _load_spec_module(spec, arguments, '_commands')
404
            spec_commands = getattr(spec_module, '_commands')
405
        except AttributeError:
406
            if _debug:
407
                global kloger
408
                kloger.warning('No valid description for %s' % spec)
409
            continue
410
        for spec_tree in spec_commands:
411
            if spec_tree.name == spec:
412
                cmd_tree.add_tree(spec_tree)
413
                break
414

    
415

    
416
def run_shell(exe_string, parser):
417
    from command_shell import _init_shell
418
    shell = _init_shell(exe_string, parser)
419
    _load_all_commands(shell.cmd_tree, parser.arguments)
420
    shell.run(parser)
421

    
422

    
423
def main():
424
    try:
425
        exe = basename(argv[0])
426
        parser = ArgumentParseManager(exe)
427

    
428
        if parser.arguments['version'].value:
429
            exit(0)
430

    
431
        _init_session(parser.arguments)
432

    
433
        if parser.unparsed:
434
            _history = History(
435
                parser.arguments['config'].get('history', 'file'))
436
            _history.add(' '.join([exe] + argv[1:]))
437
            one_cmd(parser)
438
        elif _help:
439
            parser.parser.print_help()
440
            _groups_help(parser.arguments)
441
        else:
442
            run_shell(exe, parser)
443
    except CLIError as err:
444
        _print_error_message(err)
445
        if _debug:
446
            raise err
447
        exit(1)
448
    except Exception as er:
449
        print('Unknown Error: %s' % er)
450
        if _debug:
451
            raise
452
        exit(1)