Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ a71bb904

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, 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
    description = ''
295
    if cmd.is_command:
296
        cls = cmd.get_class()
297
        parser.syntax += ' ' + cls.syntax
298
        parser.update_arguments(cls().arguments)
299
        description = getattr(cls, 'long_description', '')
300
        description = description.strip()
301
    else:
302
        parser.syntax += ' <...>'
303
    if cmd.has_description:
304
        parser.parser.description = cmd.help\
305
        + ((' . . . %s' % description) if description else '')
306

    
307

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

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