Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ b4ed3a7e

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

    
222

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

    
237
    if _help or is_non_API:
238
        return None, None
239

    
240
    _check_config_version(_cnf.value)
241

    
242
    global _colors
243
    _colors = _cnf.value.get_global('colors')
244
    if not (stdout.isatty() and _colors == 'on'):
245
        from kamaki.cli.utils import remove_colors
246
        remove_colors()
247
    _silent = arguments['silent'].value
248
    _setup_logging(_silent, _debug, _verbose, _include)
249

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

    
295
    from kamaki.clients.astakos import AstakosClient as AuthCachedClient
296
    try:
297
        return AuthCachedClient(auth_args['url'], auth_args['token']), cloud
298
    except AssertionError as ae:
299
        kloger.warning('WARNING: Failed to load authenticator [%s]' % ae)
300
        return None, cloud
301

    
302

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

    
318

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

    
339

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

    
356

    
357
#  Methods to be used by CLI implementations
358

    
359

    
360
def print_subcommands_help(cmd):
361
    printout = {}
362
    for subcmd in cmd.get_subcommands():
363
        spec, sep, print_path = subcmd.path.partition('_')
364
        printout[print_path.replace('_', ' ')] = subcmd.description
365
    if printout:
366
        print('\nOptions:\n - - - -')
367
        print_dict(printout)
368

    
369

    
370
def update_parser_help(parser, cmd):
371
    global _best_match
372
    parser.syntax = parser.syntax.split('<')[0]
373
    parser.syntax += ' '.join(_best_match)
374

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

    
390

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

    
403

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

    
419

    
420
def get_command_group(unparsed, arguments):
421
    groups = arguments['config'].get_groups()
422
    for term in unparsed:
423
        if term.startswith('-'):
424
            continue
425
        if term in groups:
426
            unparsed.remove(term)
427
            return term
428
        return None
429
    return None
430

    
431

    
432
def set_command_params(parameters):
433
    """Add a parameters list to a command
434

435
    :param paramters: (list of str) a list of parameters
436
    """
437
    global command
438
    def_params = list(command.func_defaults)
439
    def_params[0] = parameters
440
    command.func_defaults = tuple(def_params)
441

    
442

    
443
#  CLI Choice:
444

    
445
def run_one_cmd(exe_string, parser, auth_base, cloud):
446
    global _history
447
    _history = History(
448
        parser.arguments['config'].get_global('history_file'))
449
    _history.add(' '.join([exe_string] + argv[1:]))
450
    from kamaki.cli import one_command
451
    one_command.run(auth_base, cloud, parser, _help)
452

    
453

    
454
def run_shell(exe_string, parser, auth_base, cloud):
455
    from command_shell import _init_shell
456
    shell = _init_shell(exe_string, parser)
457
    _load_all_commands(shell.cmd_tree, parser.arguments)
458
    shell.run(auth_base, cloud, parser)
459

    
460

    
461
def is_non_API(parser):
462
    nonAPIs = ('history', 'config')
463
    for term in parser.unparsed:
464
        if not term.startswith('-'):
465
            if term in nonAPIs:
466
                return True
467
            return False
468
    return False
469

    
470

    
471
def main():
472
    try:
473
        exe = basename(argv[0])
474
        parser = ArgumentParseManager(exe)
475

    
476
        if parser.arguments['version'].value:
477
            exit(0)
478

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

    
486
        auth_base, cloud = _init_session(parser.arguments, is_non_API(parser))
487

    
488
        from kamaki.cli.utils import suggest_missing
489
        global _colors
490
        exclude = ['ansicolors'] if not _colors == 'on' else []
491
        suggest_missing(exclude=exclude)
492

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