Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 334338ce

History | View | Annotate | Download (12.6 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
from kamaki.cli import logger
44

    
45
_help = False
46
_debug = False
47
_include = False
48
_verbose = False
49
_colors = False
50
kloger = None
51
filelog = None
52

    
53
#  command auxiliary methods
54

    
55
_best_match = []
56

    
57

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

    
65

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

    
76

    
77
def _num_of_matching_terms(basic_list, attack_list):
78
    if not attack_list:
79
        return len(basic_list)
80

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

    
91

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

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

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

    
109

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

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

121
        :returns: the specified class object
122
    """
123

    
124
    def wrap(cls):
125
        global kloger
126
        cls_name = cls.__name__
127

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

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

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

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

    
154
        cmd_tree.add_command(cls_name, cls.description, cls)
155
        return cls
156
    return wrap
157

    
158

    
159
cmd_spec_locations = [
160
    'kamaki.cli.commands',
161
    'kamaki.commands',
162
    'kamaki.cli',
163
    'kamaki',
164
    '']
165

    
166

    
167
#  Generic init auxiliary functions
168

    
169

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

    
173
    if silent:
174
        logger.add_stream_logger(__name__, logging.CRITICAL)
175
        return
176

    
177
    sfmt, rfmt = '> %(message)s', '< %(message)s'
178
    if debug:
179
        print('Logging location: %s' % logger.get_log_filename())
180
        logger.add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
181
        logger.add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
182
        logger.add_stream_logger(__name__, logging.DEBUG)
183
    elif verbose:
184
        logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
185
        logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
186
        logger.add_stream_logger(__name__, logging.INFO)
187
    if include:
188
        logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
189
        logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
190
    logger.add_stream_logger(__name__, logging.WARNING)
191
    global kloger
192
    kloger = logger.get_logger(__name__)
193

    
194

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

    
212

    
213
def _load_spec_module(spec, arguments, module):
214
    spec_name = arguments['config'].get(spec, 'cli')
215
    if spec_name is None:
216
        return None
217
    pkg = None
218
    for location in cmd_spec_locations:
219
        location += spec_name if location == '' else '.%s' % spec_name
220
        try:
221
            pkg = __import__(location, fromlist=[module])
222
            return pkg
223
        except ImportError:
224
            continue
225
    return pkg
226

    
227

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

    
254

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

    
272

    
273
#  Methods to be used by CLI implementations
274

    
275

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

    
285

    
286
def update_parser_help(parser, cmd):
287
    global _best_match
288
    parser.syntax = parser.syntax.split('<')[0]
289
    parser.syntax += ' '.join(_best_match)
290

    
291
    description = ''
292
    if cmd.is_command:
293
        cls = cmd.get_class()
294
        parser.syntax += ' ' + cls.syntax
295
        parser.update_arguments(cls().arguments)
296
        description = getattr(cls, 'long_description', '')
297
        description = description.strip()
298
    else:
299
        parser.syntax += ' <...>'
300
    if cmd.has_description:
301
        parser.parser.description = cmd.help + (
302
            ('\n%s' % description) if description else '')
303
    else:
304
        parser.parser.description = description
305

    
306

    
307
def print_error_message(cli_err):
308
    errmsg = '%s' % cli_err
309
    if cli_err.importance == 1:
310
        errmsg = magenta(errmsg)
311
    elif cli_err.importance == 2:
312
        errmsg = yellow(errmsg)
313
    elif cli_err.importance > 2:
314
        errmsg = red(errmsg)
315
    stdout.write(errmsg)
316
    for errmsg in cli_err.details:
317
        print('| %s' % errmsg)
318

    
319

    
320
def exec_cmd(instance, cmd_args, help_method):
321
    try:
322
        return instance.main(*cmd_args)
323
    except TypeError as err:
324
        if err.args and err.args[0].startswith('main()'):
325
            print(magenta('Syntax error'))
326
            if _debug:
327
                raise err
328
            if _verbose:
329
                print(unicode(err))
330
            help_method()
331
        else:
332
            raise
333
    return 1
334

    
335

    
336
def get_command_group(unparsed, arguments):
337
    groups = arguments['config'].get_groups()
338
    for term in unparsed:
339
        if term.startswith('-'):
340
            continue
341
        if term in groups:
342
            unparsed.remove(term)
343
            return term
344
        return None
345
    return None
346

    
347

    
348
def set_command_params(parameters):
349
    """Add a parameters list to a command
350

351
    :param paramters: (list of str) a list of parameters
352
    """
353
    global command
354
    def_params = list(command.func_defaults)
355
    def_params[0] = parameters
356
    command.func_defaults = tuple(def_params)
357

    
358

    
359
#  CLI Choice:
360

    
361
def run_one_cmd(exe_string, parser):
362
    global _history
363
    _history = History(
364
        parser.arguments['config'].get('history', 'file'))
365
    _history.add(' '.join([exe_string] + argv[1:]))
366
    from kamaki.cli import one_command
367
    one_command.run(parser, _help)
368

    
369

    
370
def run_shell(exe_string, parser):
371
    from command_shell import _init_shell
372
    shell = _init_shell(exe_string, parser)
373
    _load_all_commands(shell.cmd_tree, parser.arguments)
374
    shell.run(parser)
375

    
376

    
377
def main():
378
    try:
379
        exe = basename(argv[0])
380
        parser = ArgumentParseManager(exe)
381

    
382
        if parser.arguments['version'].value:
383
            exit(0)
384

    
385
        log_file = parser.arguments['config'].get('global', 'log_file')
386
        if log_file:
387
            logger.set_log_filename(log_file)
388
        global filelog
389
        filelog = logger.add_file_logger(__name__.split('.')[0])
390
        filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
391

    
392
        _init_session(parser.arguments)
393

    
394
        from kamaki.cli.utils import suggest_missing
395
        suggest_missing()
396

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