Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 9986e569

History | View | Annotate | Download (12.3 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
    if debug:
177
        add_stream_logger('kamaki.clients.send', logging.DEBUG)
178
        add_stream_logger('kamaki.clients.recv', logging.DEBUG)
179
        add_stream_logger(__name__, logging.DEBUG)
180
    elif verbose:
181
        add_stream_logger('kamaki.clients.send', logging.INFO)
182
        add_stream_logger('kamaki.clients.recv', logging.INFO)
183
        add_stream_logger(__name__, logging.INFO)
184
    if include:
185
        add_stream_logger('kamaki.clients.send', logging.INFO)
186
        add_stream_logger('kamaki.clients.recv', logging.INFO)
187
    add_stream_logger(__name__, logging.WARNING)
188
    global kloger
189
    kloger = get_logger(__name__)
190

    
191

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

    
209

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

    
224

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

    
251

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

    
269

    
270
#  Methods to be used by CLI implementations
271

    
272

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

    
282

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

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

    
303

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

    
316

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

    
332

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

    
344

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

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

    
355

    
356
#  CLI Choice:
357

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

    
366

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

    
373

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

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

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

    
387
        _init_session(parser.arguments)
388

    
389
        from kamaki.cli.utils import suggest_missing
390
        suggest_missing()
391

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