Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 6e1f863b

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
from kamaki.logger import add_stream_logger, get_logger
44

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

    
52
#  command auxiliary methods
53

    
54
_best_match = []
55

    
56

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

    
64

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

    
75

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

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

    
90

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

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

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

    
108

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

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

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

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

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

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

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

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

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

    
157

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

    
165

    
166
#  Generic init auxiliary functions
167

    
168

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

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

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

    
192

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

    
210

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

    
225

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

    
252

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

    
270

    
271
#  Methods to be used by CLI implementations
272

    
273

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

    
283

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

    
289
    description = ''
290
    if cmd.is_command:
291
        cls = cmd.get_class()
292
        parser.syntax += ' ' + cls.syntax
293
        parser.update_arguments(cls().arguments)
294
        description = getattr(cls, 'long_description', '')
295
        description = description.strip()
296
    else:
297
        parser.syntax += ' <...>'
298
    if cmd.has_description:
299
        parser.parser.description = cmd.help + (
300
            ('\n%s' % description) if description else '')
301
    else:
302
        parser.parser.description = description
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
    for errmsg in cli_err.details:
315
        print('| %s' % errmsg)
316

    
317

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

    
333

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

    
345

    
346
def set_command_params(parameters):
347
    """Add a parameters list to a command
348

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

    
356

    
357
#  CLI Choice:
358

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

    
367

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

    
374

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

    
380
        if parser.arguments['version'].value:
381
            exit(0)
382

    
383
        log_file = parser.arguments['config'].get('global', 'log_file')
384
        if log_file:
385
            from kamaki.logger import set_log_filename
386
            set_log_filename(log_file)
387

    
388
        _init_session(parser.arguments)
389

    
390
        from kamaki.cli.utils import suggest_missing
391
        suggest_missing()
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)