Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 3f0eae61

History | View | Annotate | Download (13.8 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
    _cnf = arguments['config']
205

    
206
    guess = _cnf.value.guess_version()
207
    if guess < 3.0:
208
        print('PLEASE DO NOT PANIC: EXIT THE BUILDING QUIETLY')
209
    raise CLIError('STOP HERE, PLEASE %s' % guess)
210

    
211
    global _colors
212
    _colors = _cnf.get('global', 'colors')
213
    if not (stdout.isatty() and _colors == 'on'):
214
        from kamaki.cli.utils import remove_colors
215
        remove_colors()
216
    _silent = arguments['silent'].value
217
    _setup_logging(_silent, _debug, _verbose, _include)
218
    picked_cloud = arguments['cloud'].value
219
    if picked_cloud:
220
        global_url = _cnf.get('remotes', picked_cloud)
221
        if not global_url:
222
            raise CLIError(
223
                'No remote cloud "%s" in kamaki configuration' % picked_cloud,
224
                importance=3, details=[
225
                    'To check if this remote cloud alias is declared:',
226
                    '  /config get remotes.%s' % picked_cloud,
227
                    'To set a remote authentication URI aliased as "%s"' % (
228
                        picked_cloud),
229
                    '  /config set remotes.%s <URI>' % picked_cloud
230
                ])
231
    else:
232
        global_url = _cnf.get('global', 'auth_url')
233
    global_token = _cnf.get('global', 'token')
234
    from kamaki.clients.astakos import AstakosClient as AuthCachedClient
235
    try:
236
        return AuthCachedClient(global_url, global_token)
237
    except AssertionError as ae:
238
        kloger.warning('WARNING: Failed to load auth_url %s [ %s ]' % (
239
            global_url, ae))
240
        return None
241

    
242

    
243
def _load_spec_module(spec, arguments, module):
244
    #spec_name = arguments['config'].get('cli', spec)
245
    if not spec:
246
        return None
247
    pkg = None
248
    for location in cmd_spec_locations:
249
        location += spec if location == '' else '.%s' % spec
250
        try:
251
            pkg = __import__(location, fromlist=[module])
252
            return pkg
253
        except ImportError as ie:
254
            continue
255
    if not pkg:
256
        kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
257
    return pkg
258

    
259

    
260
def _groups_help(arguments):
261
    global _debug
262
    global kloger
263
    descriptions = {}
264
    for cmd_group, spec in arguments['config'].get_cli_specs():
265
        pkg = _load_spec_module(spec, arguments, '_commands')
266
        if pkg:
267
            cmds = getattr(pkg, '_commands')
268
            try:
269
                for cmd in cmds:
270
                    descriptions[cmd.name] = cmd.description
271
            except TypeError:
272
                if _debug:
273
                    kloger.warning(
274
                        'No cmd description for module %s' % cmd_group)
275
        elif _debug:
276
            kloger.warning('Loading of %s cmd spec failed' % cmd_group)
277
    print('\nOptions:\n - - - -')
278
    print_dict(descriptions)
279

    
280

    
281
def _load_all_commands(cmd_tree, arguments):
282
    _cnf = arguments['config']
283
    #specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
284
    for cmd_group, spec in _cnf.get_cli_specs():
285
        try:
286
            spec_module = _load_spec_module(spec, arguments, '_commands')
287
            spec_commands = getattr(spec_module, '_commands')
288
        except AttributeError:
289
            if _debug:
290
                global kloger
291
                kloger.warning('No valid description for %s' % cmd_group)
292
            continue
293
        for spec_tree in spec_commands:
294
            if spec_tree.name == cmd_group:
295
                cmd_tree.add_tree(spec_tree)
296
                break
297

    
298

    
299
#  Methods to be used by CLI implementations
300

    
301

    
302
def print_subcommands_help(cmd):
303
    printout = {}
304
    for subcmd in cmd.get_subcommands():
305
        spec, sep, print_path = subcmd.path.partition('_')
306
        printout[print_path.replace('_', ' ')] = subcmd.description
307
    if printout:
308
        print('\nOptions:\n - - - -')
309
        print_dict(printout)
310

    
311

    
312
def update_parser_help(parser, cmd):
313
    global _best_match
314
    parser.syntax = parser.syntax.split('<')[0]
315
    parser.syntax += ' '.join(_best_match)
316

    
317
    description = ''
318
    if cmd.is_command:
319
        cls = cmd.get_class()
320
        parser.syntax += ' ' + cls.syntax
321
        parser.update_arguments(cls().arguments)
322
        description = getattr(cls, 'long_description', '')
323
        description = description.strip()
324
    else:
325
        parser.syntax += ' <...>'
326
    if cmd.has_description:
327
        parser.parser.description = cmd.help + (
328
            ('\n%s' % description) if description else '')
329
    else:
330
        parser.parser.description = description
331

    
332

    
333
def print_error_message(cli_err):
334
    errmsg = '%s' % cli_err
335
    if cli_err.importance == 1:
336
        errmsg = magenta(errmsg)
337
    elif cli_err.importance == 2:
338
        errmsg = yellow(errmsg)
339
    elif cli_err.importance > 2:
340
        errmsg = red(errmsg)
341
    stdout.write(errmsg)
342
    for errmsg in cli_err.details:
343
        print('|  %s' % errmsg)
344

    
345

    
346
def exec_cmd(instance, cmd_args, help_method):
347
    try:
348
        return instance.main(*cmd_args)
349
    except TypeError as err:
350
        if err.args and err.args[0].startswith('main()'):
351
            print(magenta('Syntax error'))
352
            if _debug:
353
                raise err
354
            if _verbose:
355
                print(unicode(err))
356
            help_method()
357
        else:
358
            raise
359
    return 1
360

    
361

    
362
def get_command_group(unparsed, arguments):
363
    groups = arguments['config'].get_groups()
364
    for term in unparsed:
365
        if term.startswith('-'):
366
            continue
367
        if term in groups:
368
            unparsed.remove(term)
369
            return term
370
        return None
371
    return None
372

    
373

    
374
def set_command_params(parameters):
375
    """Add a parameters list to a command
376

377
    :param paramters: (list of str) a list of parameters
378
    """
379
    global command
380
    def_params = list(command.func_defaults)
381
    def_params[0] = parameters
382
    command.func_defaults = tuple(def_params)
383

    
384

    
385
#  CLI Choice:
386

    
387
def run_one_cmd(exe_string, parser, auth_base):
388
    global _history
389
    _history = History(
390
        parser.arguments['config'].get('history', 'file'))
391
    _history.add(' '.join([exe_string] + argv[1:]))
392
    from kamaki.cli import one_command
393
    one_command.run(auth_base, parser, _help)
394

    
395

    
396
def run_shell(exe_string, parser, auth_base):
397
    from command_shell import _init_shell
398
    shell = _init_shell(exe_string, parser)
399
    _load_all_commands(shell.cmd_tree, parser.arguments)
400
    shell.run(auth_base, parser)
401

    
402

    
403
def main():
404
    try:
405
        exe = basename(argv[0])
406
        parser = ArgumentParseManager(exe)
407

    
408
        if parser.arguments['version'].value:
409
            exit(0)
410

    
411
        log_file = parser.arguments['config'].get('global', 'log_file')
412
        if log_file:
413
            logger.set_log_filename(log_file)
414
        global filelog
415
        filelog = logger.add_file_logger(__name__.split('.')[0])
416
        filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
417

    
418
        auth_base = _init_session(parser.arguments)
419

    
420
        from kamaki.cli.utils import suggest_missing
421
        suggest_missing()
422

    
423
        if parser.unparsed:
424
            run_one_cmd(exe, parser, auth_base)
425
        elif _help:
426
            parser.parser.print_help()
427
            _groups_help(parser.arguments)
428
        else:
429
            run_shell(exe, parser, auth_base)
430
    except CLIError as err:
431
        print_error_message(err)
432
        if _debug:
433
            raise err
434
        exit(1)
435
    except Exception as er:
436
        print('Unknown Error: %s' % er)
437
        if _debug:
438
            raise
439
        exit(1)