Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 2005b18e

History | View | Annotate | Download (12.4 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, red, magenta, yellow
42
from kamaki.cli.errors import CLIError
43

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

    
51
#  command auxiliary methods
52

    
53
_best_match = []
54

    
55

    
56
def _arg2syntax(arg):
57
    return arg.replace(
58
        '____', '[:').replace(
59
            '___', ':').replace(
60
                '__', ']').replace(
61
                    '_', ' ')
62

    
63

    
64
def _construct_command_syntax(cls):
65
        spec = getargspec(cls.main.im_func)
66
        args = spec.args[1:]
67
        n = len(args) - len(spec.defaults or ())
68
        required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
69
        optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
70
        cls.syntax = ' '.join(x for x in [required, optional] if x)
71
        if spec.varargs:
72
            cls.syntax += ' <%s ...>' % spec.varargs
73

    
74

    
75
def _num_of_matching_terms(basic_list, attack_list):
76
    if not attack_list:
77
        return len(basic_list)
78

    
79
    matching_terms = 0
80
    for i, term in enumerate(basic_list):
81
        try:
82
            if term != attack_list[i]:
83
                break
84
        except IndexError:
85
            break
86
        matching_terms += 1
87
    return matching_terms
88

    
89

    
90
def _update_best_match(name_terms, prefix=[]):
91
    if prefix:
92
        pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
93
    else:
94
        pref_list = []
95

    
96
    num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
97
    global _best_match
98
    if not prefix:
99
        _best_match = []
100

    
101
    if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
102
        if len(_best_match) < num_of_matching_terms:
103
            _best_match = name_terms[:num_of_matching_terms]
104
        return True
105
    return False
106

    
107

    
108
def command(cmd_tree, prefix='', descedants_depth=1):
109
    """Load a class as a command
110
        e.g. spec_cmd0_cmd1 will be command spec cmd0
111

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

119
        :returns: the specified class object
120
    """
121

    
122
    def wrap(cls):
123
        global kloger
124
        cls_name = cls.__name__
125

    
126
        if not cmd_tree:
127
            if _debug:
128
                kloger.warning('command %s found but not loaded' % cls_name)
129
            return cls
130

    
131
        name_terms = cls_name.split('_')
132
        if not _update_best_match(name_terms, prefix):
133
            if _debug:
134
                kloger.warning('%s failed to update_best_match' % cls_name)
135
            return None
136

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

    
147
        (
148
            cls.description, sep, cls.long_description
149
        ) = cls.__doc__.partition('\n')
150
        _construct_command_syntax(cls)
151

    
152
        cmd_tree.add_command(cls_name, cls.description, cls)
153
        return cls
154
    return wrap
155

    
156

    
157
cmd_spec_locations = [
158
    'kamaki.cli.commands',
159
    'kamaki.commands',
160
    'kamaki.cli',
161
    'kamaki',
162
    '']
163

    
164

    
165
#  Generic init auxiliary functions
166

    
167

    
168
def _setup_logging(silent=False, debug=False, verbose=False, include=False):
169
    """handle logging for clients package"""
170

    
171
    def add_handler(name, level, prefix=''):
172
        h = logging.StreamHandler()
173
        fmt = logging.Formatter(prefix + '%(message)s')
174
        h.setFormatter(fmt)
175
        logger = logging.getLogger(name)
176
        logger.addHandler(h)
177
        logger.setLevel(level)
178

    
179
    if silent:
180
        add_handler('', logging.CRITICAL)
181
        return
182

    
183
    if debug:
184
        add_handler('requests', logging.INFO, prefix='* ')
185
        add_handler('clients.send', logging.DEBUG, prefix='> ')
186
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
187
        add_handler('kamaki', logging.DEBUG, prefix='(debug): ')
188
    elif verbose:
189
        add_handler('requests', logging.INFO, prefix='* ')
190
        add_handler('clients.send', logging.INFO, prefix='> ')
191
        add_handler('clients.recv', logging.INFO, prefix='< ')
192
        add_handler('kamaki', logging.INFO, prefix='(i): ')
193
    if include:
194
        add_handler('data.send', logging.INFO, prefix='>[data]: ')
195
        add_handler('data.recv', logging.INFO, prefix='<[data]: ')
196
    add_handler('kamaki', logging.WARNING, prefix='(warning): ')
197
    global kloger
198
    kloger = logging.getLogger('kamaki')
199

    
200

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

    
218

    
219
def _load_spec_module(spec, arguments, module):
220
    spec_name = arguments['config'].get(spec, 'cli')
221
    if spec_name is None:
222
        return None
223
    pkg = None
224
    for location in cmd_spec_locations:
225
        location += spec_name if location == '' else '.%s' % spec_name
226
        try:
227
            pkg = __import__(location, fromlist=[module])
228
            return pkg
229
        except ImportError:
230
            continue
231
    return pkg
232

    
233

    
234
def _groups_help(arguments):
235
    global _debug
236
    global kloger
237
    descriptions = {}
238
    for spec in arguments['config'].get_groups():
239
        pkg = _load_spec_module(spec, arguments, '_commands')
240
        if pkg:
241
            cmds = None
242
            try:
243
                _cnf = arguments['config']
244
                cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
245
                    cmd.name, 'cli')]
246
            except AttributeError:
247
                if _debug:
248
                    kloger.warning('No description for %s' % spec)
249
            try:
250
                for cmd in cmds:
251
                    descriptions[cmd.name] = cmd.description
252
            except TypeError:
253
                if _debug:
254
                    kloger.warning('no cmd specs in module %s' % spec)
255
        elif _debug:
256
            kloger.warning('Loading of %s cmd spec failed' % spec)
257
    print('\nOptions:\n - - - -')
258
    print_dict(descriptions)
259

    
260

    
261
def _load_all_commands(cmd_tree, arguments):
262
    _cnf = arguments['config']
263
    specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
264
    for spec in specs:
265
        try:
266
            spec_module = _load_spec_module(spec, arguments, '_commands')
267
            spec_commands = getattr(spec_module, '_commands')
268
        except AttributeError:
269
            if _debug:
270
                global kloger
271
                kloger.warning('No valid description for %s' % spec)
272
            continue
273
        for spec_tree in spec_commands:
274
            if spec_tree.name == spec:
275
                cmd_tree.add_tree(spec_tree)
276
                break
277

    
278

    
279
#  Methods to be used by CLI implementations
280

    
281

    
282
def print_subcommands_help(cmd):
283
    printout = {}
284
    for subcmd in cmd.get_subcommands():
285
        spec, sep, print_path = subcmd.path.partition('_')
286
        printout[print_path.replace('_', ' ')] = subcmd.description
287
    if printout:
288
        print('\nOptions:\n - - - -')
289
        print_dict(printout)
290

    
291

    
292
def update_parser_help(parser, cmd):
293
    global _best_match
294
    parser.syntax = parser.syntax.split('<')[0]
295
    parser.syntax += ' '.join(_best_match)
296

    
297
    description = ''
298
    if cmd.is_command:
299
        cls = cmd.get_class()
300
        parser.syntax += ' ' + cls.syntax
301
        parser.update_arguments(cls().arguments)
302
        description = getattr(cls, 'long_description', '')
303
        description = description.strip()
304
    else:
305
        parser.syntax += ' <...>'
306
    if cmd.has_description:
307
        parser.parser.description = cmd.help + (
308
            ('\n%s' % description) if description else '')
309
    else:
310
        parser.parser.description = description
311

    
312

    
313
def print_error_message(cli_err):
314
    errmsg = '%s' % cli_err
315
    if cli_err.importance == 1:
316
        errmsg = magenta(errmsg)
317
    elif cli_err.importance == 2:
318
        errmsg = yellow(errmsg)
319
    elif cli_err.importance > 2:
320
        errmsg = red(errmsg)
321
    stdout.write(errmsg)
322
    for errmsg in cli_err.details:
323
        print('| %s' % errmsg)
324

    
325

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

    
341

    
342
def get_command_group(unparsed, arguments):
343
    groups = arguments['config'].get_groups()
344
    for term in unparsed:
345
        if term.startswith('-'):
346
            continue
347
        if term in groups:
348
            unparsed.remove(term)
349
            return term
350
        return None
351
    return None
352

    
353

    
354
def set_command_params(parameters):
355
    """Add a parameters list to a command
356

357
    :param paramters: (list of str) a list of parameters
358
    """
359
    global command
360
    def_params = list(command.func_defaults)
361
    def_params[0] = parameters
362
    command.func_defaults = tuple(def_params)
363

    
364

    
365
#  CLI Choice:
366

    
367
def run_one_cmd(exe_string, parser):
368
    global _history
369
    _history = History(
370
        parser.arguments['config'].get('history', 'file'))
371
    _history.add(' '.join([exe_string] + argv[1:]))
372
    from kamaki.cli import one_command
373
    one_command.run(parser, _help)
374

    
375

    
376
def run_shell(exe_string, parser):
377
    from command_shell import _init_shell
378
    shell = _init_shell(exe_string, parser)
379
    _load_all_commands(shell.cmd_tree, parser.arguments)
380
    shell.run(parser)
381

    
382

    
383
def main():
384
    try:
385
        exe = basename(argv[0])
386
        parser = ArgumentParseManager(exe)
387

    
388
        if parser.arguments['version'].value:
389
            exit(0)
390

    
391
        _init_session(parser.arguments)
392

    
393
        if parser.unparsed:
394
            run_one_cmd(exe, parser)
395
        elif _help:
396
            parser.parser.print_help()
397
            _groups_help(parser.arguments)
398
        else:
399
            run_shell(exe, parser)
400
    except CLIError as err:
401
        print_error_message(err)
402
        if _debug:
403
            raise err
404
        exit(1)
405
    except Exception as er:
406
        print('Unknown Error: %s' % er)
407
        if _debug:
408
            raise
409
        exit(1)