Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ b6a99832

History | View | Annotate | Download (12.2 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
#  command auxiliary methods
51

    
52
_best_match = []
53

    
54

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

    
73

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

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

    
88

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

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

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

    
106

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

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

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

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

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

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

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

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

    
150
        cmd_tree.add_command(cls_name, cls.description, cls)
151
        return cls
152
    return wrap
153

    
154

    
155
cmd_spec_locations = [
156
    'kamaki.cli.commands',
157
    'kamaki.commands',
158
    'kamaki.cli',
159
    'kamaki',
160
    '']
161

    
162

    
163
#  Generic init auxiliary functions
164

    
165

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

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

    
177
    if silent:
178
        add_handler('', logging.CRITICAL)
179
        return
180

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

    
197

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

    
214

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

    
229

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

    
257

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

    
275

    
276
#  Methods to be used by CLI implementations
277

    
278

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

    
288

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

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

    
305

    
306
def print_error_message(cli_err):
307
    errmsg = '%s' % cli_err
308
    if cli_err.importance == 1:
309
        errmsg = magenta(errmsg)
310
    elif cli_err.importance == 2:
311
        errmsg = yellow(errmsg)
312
    elif cli_err.importance > 2:
313
        errmsg = red(errmsg)
314
    stdout.write(errmsg)
315
    print_list(cli_err.details)
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
        _init_session(parser.arguments)
384

    
385
        if parser.unparsed:
386
            run_one_cmd(exe, parser)
387
        elif _help:
388
            parser.parser.print_help()
389
            _groups_help(parser.arguments)
390
        else:
391
            run_shell(exe, parser)
392
    except CLIError as err:
393
        print_error_message(err)
394
        if _debug:
395
            raise err
396
        exit(1)
397
    except Exception as er:
398
        print('Unknown Error: %s' % er)
399
        if _debug:
400
            raise
401
        exit(1)