__init__.py 16.7 KB
Newer Older
1
# Copyright 2012-2013 GRNET S.A. All rights reserved.
2
3
4
5
6
7
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
8
9
#      copyright notice, this list of conditions and the following
#      disclaimer.
10
11
#
#   2. Redistributions in binary form must reproduce the above
12
13
14
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# 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, exists
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, CLICmdSpecError
43
from kamaki.cli import logger
44

45
46
_help = False
_debug = False
47
_include = False
48
49
_verbose = False
_colors = False
Stavros Sachtouris's avatar
Stavros Sachtouris committed
50
kloger = None
51
filelog = None
52

53
54
55
56
#  command auxiliary methods

_best_match = []

57

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


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


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

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

91

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

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

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


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

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

        :returns: the specified class object
122
123
124
    """

    def wrap(cls):
125
        global kloger
126
127
128
129
        cls_name = cls.__name__

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

133
134
        name_terms = cls_name.split('_')
        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
140
141
142
143
144
        global _best_match
        max_len = len(_best_match) + descedants_depth
        if len(name_terms) > max_len:
            partial = '_'.join(name_terms[:max_len])
            if not cmd_tree.has_command(partial):  # add partial path
                cmd_tree.add_command(partial)
145
            if _debug:
146
                kloger.warning('%s failed max_len test' % cls_name)
147
            return None
148

149
150
151
152
153
154
155
        try:
            (
                cls.description, sep, cls.long_description
            ) = cls.__doc__.partition('\n')
        except AttributeError:
            raise CLICmdSpecError(
                'No commend in %s (acts as cmd description)' % cls.__name__)
156
        _construct_command_syntax(cls)
157

158
159
160
        cmd_tree.add_command(cls_name, cls.description, cls)
        return cls
    return wrap
161

162

163
164
165
166
167
168
cmd_spec_locations = [
    'kamaki.cli.commands',
    'kamaki.commands',
    'kamaki.cli',
    'kamaki',
    '']
169

170

171
172
173
#  Generic init auxiliary functions


174
def _setup_logging(silent=False, debug=False, verbose=False, include=False):
175
176
177
    """handle logging for clients package"""

    if silent:
178
        logger.add_stream_logger(__name__, logging.CRITICAL)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
179
180
        return

181
    sfmt, rfmt = '> %(message)s', '< %(message)s'
Stavros Sachtouris's avatar
Stavros Sachtouris committed
182
    if debug:
183
184
185
186
        print('Logging location: %s' % logger.get_log_filename())
        logger.add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
        logger.add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
        logger.add_stream_logger(__name__, logging.DEBUG)
187
    elif verbose:
188
189
190
        logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
        logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
        logger.add_stream_logger(__name__, logging.INFO)
191
    if include:
192
193
194
        logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
        logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
    logger.add_stream_logger(__name__, logging.WARNING)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
195
    global kloger
196
    kloger = logger.get_logger(__name__)
197

198

199
200
def _check_config_version(cnf):
    guess = cnf.guess_version()
201
    if exists(cnf.path) and guess < 0.9:
202
        print('Config file format version >= 9.0 is required')
203
        print('Configuration file: %s' % cnf.path)
204
        print('Attempting to fix this:')
205
        print('Calculating changes while preserving information')
206
207
        lost_terms = cnf.rescue_old_file()
        print('... DONE')
208
209
210
        if lost_terms:
            print 'The following information will NOT be preserved:'
            print '\t', '\n\t'.join(lost_terms)
211
        print('Kamaki is ready to convert the config file')
212
        stdout.write('Create (overwrite) file %s ? [y/N] ' % cnf.path)
213
214
215
216
217
218
219
220
221
        from sys import stdin
        reply = stdin.readline()
        if reply in ('Y\n', 'y\n'):
            cnf.write()
            print('... DONE')
        else:
            print('... ABORTING')
            raise CLIError(
                'Invalid format for config file %s' % cnf.path,
222
                importance=3, details=[
223
                    'Please, update config file',
224
                    'For automatic conversion, rerun and say Y'])
225
226


227
def _init_session(arguments, is_non_API=False):
228
    """
229
    :returns: (AuthCachedClient, str) authenticator and cloud name
230
    """
231
232
233
234
    global _help
    _help = arguments['help'].value
    global _debug
    _debug = arguments['debug'].value
235
236
    global _include
    _include = arguments['include'].value
237
238
    global _verbose
    _verbose = arguments['verbose'].value
239
    _cnf = arguments['config']
240
241
242
243

    if _help or is_non_API:
        return None, None

244
    _check_config_version(_cnf.value)
245

246
    global _colors
247
    _colors = _cnf.value.get_global('colors')
248
    if not (stdout.isatty() and _colors == 'on'):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
249
250
        from kamaki.cli.utils import remove_colors
        remove_colors()
251
252
    _silent = arguments['silent'].value
    _setup_logging(_silent, _debug, _verbose, _include)
253

254
255
256
257
258
259
    cloud = arguments['cloud'].value or _cnf.value.get(
        'global', 'default_cloud')
    if not cloud:
        num_of_clouds = len(_cnf.value.keys('cloud'))
        if num_of_clouds == 1:
            cloud = _cnf.value.keys('cloud')[0]
260
        elif num_of_clouds > 1:
261
            raise CLIError(
262
263
                'Found %s clouds but none of them is set as default' % (
                    num_of_clouds),
264
265
266
                importance=2, details=[
                    'Please, choose one of the following cloud names:',
                    ', '.join(_cnf.value.keys('cloud')),
267
268
                    'To see all cloud settings:',
                    '  kamaki config get cloud.<cloud name>',
269
270
271
272
                    'To set a default cloud:',
                    '  kamaki config set default_cloud <cloud name>',
                    'To pick a cloud for the current session, use --cloud:',
                    '  kamaki --cloud=<cloud name> ...'])
273
    if not cloud in _cnf.value.keys('cloud'):
274
        raise CLIError(
275
            'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''),
276
            importance=3, details=[
277
278
                'To configure a new cloud "%s", find and set the' % (
                    cloud or '<cloud name>'),
279
                'single authentication URL and token:',
280
281
282
283
                '  kamaki config set cloud.%s.url <URL>' % (
                    cloud or '<cloud name>'),
                '  kamaki config set cloud.%s.token <t0k3n>' % (
                    cloud or '<cloud name>')])
284
285
    auth_args = dict()
    for term in ('url', 'token'):
286
287
288
289
        try:
            auth_args[term] = _cnf.get_cloud(cloud, term)
        except KeyError:
            auth_args[term] = ''
290
291
        if not auth_args[term]:
            raise CLIError(
292
293
                'No authentication %s provided for cloud "%s"' % (
                    term.upper(), cloud),
294
                importance=3, details=[
295
                    'Set a %s for cloud %s:' % (term.upper(), cloud),
296
                    '  kamaki config set cloud.%s.%s <%s>' % (
297
                        cloud, term, term.upper())])
298

299
    from kamaki.clients.astakos import AstakosClient as AuthCachedClient
300
    try:
301
        return AuthCachedClient(auth_args['url'], auth_args['token']), cloud
302
    except AssertionError as ae:
303
304
        kloger.warning('WARNING: Failed to load authenticator [%s]' % ae)
        return None, cloud
305
306
307


def _load_spec_module(spec, arguments, module):
308
    if not spec:
309
310
311
        return None
    pkg = None
    for location in cmd_spec_locations:
312
        location += spec if location == '' else '.%s' % spec
313
314
315
        try:
            pkg = __import__(location, fromlist=[module])
            return pkg
316
        except ImportError as ie:
317
            continue
318
319
    if not pkg:
        kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
320
321
322
323
324
    return pkg


def _groups_help(arguments):
    global _debug
Stavros Sachtouris's avatar
Stavros Sachtouris committed
325
    global kloger
326
    descriptions = {}
327
    for cmd_group, spec in arguments['config'].get_cli_specs():
328
329
        pkg = _load_spec_module(spec, arguments, '_commands')
        if pkg:
330
            cmds = getattr(pkg, '_commands')
331
332
333
334
335
            try:
                for cmd in cmds:
                    descriptions[cmd.name] = cmd.description
            except TypeError:
                if _debug:
336
337
                    kloger.warning(
                        'No cmd description for module %s' % cmd_group)
338
        elif _debug:
339
            kloger.warning('Loading of %s cmd spec failed' % cmd_group)
340
341
342
343
    print('\nOptions:\n - - - -')
    print_dict(descriptions)


344
def _load_all_commands(cmd_tree, arguments):
345
    _cnf = arguments['config']
346
    for cmd_group, spec in _cnf.get_cli_specs():
347
348
349
350
351
352
        try:
            spec_module = _load_spec_module(spec, arguments, '_commands')
            spec_commands = getattr(spec_module, '_commands')
        except AttributeError:
            if _debug:
                global kloger
353
                kloger.warning('No valid description for %s' % cmd_group)
354
355
            continue
        for spec_tree in spec_commands:
356
            if spec_tree.name == cmd_group:
357
358
359
360
361
362
363
364
                cmd_tree.add_tree(spec_tree)
                break


#  Methods to be used by CLI implementations


def print_subcommands_help(cmd):
365
366
    printout = {}
    for subcmd in cmd.get_subcommands():
Stavros Sachtouris's avatar
Stavros Sachtouris committed
367
368
        spec, sep, print_path = subcmd.path.partition('_')
        printout[print_path.replace('_', ' ')] = subcmd.description
369
370
371
372
373
    if printout:
        print('\nOptions:\n - - - -')
        print_dict(printout)


374
def update_parser_help(parser, cmd):
375
    global _best_match
376
377
    parser.syntax = parser.syntax.split('<')[0]
    parser.syntax += ' '.join(_best_match)
378

379
    description = ''
380
381
    if cmd.is_command:
        cls = cmd.get_class()
382
383
        parser.syntax += ' ' + cls.syntax
        parser.update_arguments(cls().arguments)
384
385
        description = getattr(cls, 'long_description', '')
        description = description.strip()
386
    else:
387
        parser.syntax += ' <...>'
388
    if cmd.has_description:
389
390
        parser.parser.description = cmd.help + (
            ('\n%s' % description) if description else '')
391
392
    else:
        parser.parser.description = description
393
394


395
def print_error_message(cli_err):
396
397
398
399
400
401
402
403
    errmsg = '%s' % cli_err
    if cli_err.importance == 1:
        errmsg = magenta(errmsg)
    elif cli_err.importance == 2:
        errmsg = yellow(errmsg)
    elif cli_err.importance > 2:
        errmsg = red(errmsg)
    stdout.write(errmsg)
404
    for errmsg in cli_err.details:
405
        print('|  %s' % errmsg)
406
407


408
def exec_cmd(instance, cmd_args, help_method):
409
410
411
412
413
    try:
        return instance.main(*cmd_args)
    except TypeError as err:
        if err.args and err.args[0].startswith('main()'):
            print(magenta('Syntax error'))
414
415
416
            if _debug:
                raise err
            if _verbose:
417
418
419
420
421
422
                print(unicode(err))
            help_method()
        else:
            raise
    return 1

423

424
425
426
427
428
429
430
431
432
433
434
435
def get_command_group(unparsed, arguments):
    groups = arguments['config'].get_groups()
    for term in unparsed:
        if term.startswith('-'):
            continue
        if term in groups:
            unparsed.remove(term)
            return term
        return None
    return None


436
437
438
439
440
def set_command_params(parameters):
    """Add a parameters list to a command

    :param paramters: (list of str) a list of parameters
    """
441
442
    global command
    def_params = list(command.func_defaults)
443
    def_params[0] = parameters
444
445
446
    command.func_defaults = tuple(def_params)


447
#  CLI Choice:
448

449
def run_one_cmd(exe_string, parser, auth_base, cloud):
450
451
    global _history
    _history = History(
452
        parser.arguments['config'].get_global('history_file'))
453
454
    _history.add(' '.join([exe_string] + argv[1:]))
    from kamaki.cli import one_command
455
    one_command.run(auth_base, cloud, parser, _help)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
456
457


458
def run_shell(exe_string, parser, auth_base, cloud):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
459
    from command_shell import _init_shell
460
461
    shell = _init_shell(exe_string, parser)
    _load_all_commands(shell.cmd_tree, parser.arguments)
462
    shell.run(auth_base, cloud, parser)
463

464

465
466
467
468
469
470
471
472
473
474
def is_non_API(parser):
    nonAPIs = ('history', 'config')
    for term in parser.unparsed:
        if not term.startswith('-'):
            if term in nonAPIs:
                return True
            return False
    return False


475
def main():
476
477
    try:
        exe = basename(argv[0])
Stavros Sachtouris's avatar
Stavros Sachtouris committed
478
        parser = ArgumentParseManager(exe)
479

480
        if parser.arguments['version'].value:
481
482
            exit(0)

483
        log_file = parser.arguments['config'].get_global('log_file')
484
        if log_file:
485
            logger.set_log_filename(log_file)
486
487
        global filelog
        filelog = logger.add_file_logger(__name__.split('.')[0])
488
        filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
489

490
        auth_base, cloud = _init_session(parser.arguments, is_non_API(parser))
491

492
        from kamaki.cli.utils import suggest_missing
493
494
495
        global _colors
        exclude = ['ansicolors'] if not _colors == 'on' else []
        suggest_missing(exclude=exclude)
496

497
        if parser.unparsed:
498
            run_one_cmd(exe, parser, auth_base, cloud)
499
        elif _help:
Stavros Sachtouris's avatar
Stavros Sachtouris committed
500
            parser.parser.print_help()
501
            _groups_help(parser.arguments)
502
        else:
503
            run_shell(exe, parser, auth_base, cloud)
504
    except CLIError as err:
505
        print_error_message(err)
506
507
508
        if _debug:
            raise err
        exit(1)
509
510
    except Exception as er:
        print('Unknown Error: %s' % er)
511
        if _debug:
512
513
            raise
        exit(1)