Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 144b3551

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

    
223

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

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

    
247
    if _help or is_non_API:
248
        return None, None
249

    
250
    cloud = arguments['cloud'].value or 'default'
251
    if not cloud in _cnf.value.keys('cloud'):
252
        raise CLIError(
253
            'No cloud "%s" is configured' % cloud,
254
            importance=3, details=[
255
                'To configure a new cloud, find and set the',
256
                'single authentication URL and token:',
257
                '  kamaki config set cloud.%s.url <URL>' % cloud,
258
                '  kamaki config set cloud.%s.token <t0k3n>' % cloud])
259
    auth_args = dict()
260
    for term in ('url', 'token'):
261
        auth_args[term] = _cnf.get_cloud(cloud, term)
262
        if not auth_args[term]:
263
            raise CLIError(
264
                'No authentication %s provided for %s cloud' % (term, cloud),
265
                importance=3, details=[
266
                    'Get and set a %s for %s cloud:' % (term, cloud),
267
                    '  kamaki config set cloud.%s.%s <t0k3n>' % (term, cloud)
268
                ])
269

    
270
    from kamaki.clients.astakos import AstakosClient as AuthCachedClient
271
    try:
272
        return AuthCachedClient(auth_args['url'], auth_args['token']), cloud
273
    except AssertionError as ae:
274
        kloger.warning('WARNING: Failed to load authenticator [%s]' % ae)
275
        return None, cloud
276

    
277

    
278
def _load_spec_module(spec, arguments, module):
279
    if not spec:
280
        return None
281
    pkg = None
282
    for location in cmd_spec_locations:
283
        location += spec if location == '' else '.%s' % spec
284
        try:
285
            pkg = __import__(location, fromlist=[module])
286
            return pkg
287
        except ImportError as ie:
288
            continue
289
    if not pkg:
290
        kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
291
    return pkg
292

    
293

    
294
def _groups_help(arguments):
295
    global _debug
296
    global kloger
297
    descriptions = {}
298
    for cmd_group, spec in arguments['config'].get_cli_specs():
299
        pkg = _load_spec_module(spec, arguments, '_commands')
300
        if pkg:
301
            cmds = getattr(pkg, '_commands')
302
            try:
303
                for cmd in cmds:
304
                    descriptions[cmd.name] = cmd.description
305
            except TypeError:
306
                if _debug:
307
                    kloger.warning(
308
                        'No cmd description for module %s' % cmd_group)
309
        elif _debug:
310
            kloger.warning('Loading of %s cmd spec failed' % cmd_group)
311
    print('\nOptions:\n - - - -')
312
    print_dict(descriptions)
313

    
314

    
315
def _load_all_commands(cmd_tree, arguments):
316
    _cnf = arguments['config']
317
    for cmd_group, spec in _cnf.get_cli_specs():
318
        try:
319
            spec_module = _load_spec_module(spec, arguments, '_commands')
320
            spec_commands = getattr(spec_module, '_commands')
321
        except AttributeError:
322
            if _debug:
323
                global kloger
324
                kloger.warning('No valid description for %s' % cmd_group)
325
            continue
326
        for spec_tree in spec_commands:
327
            if spec_tree.name == cmd_group:
328
                cmd_tree.add_tree(spec_tree)
329
                break
330

    
331

    
332
#  Methods to be used by CLI implementations
333

    
334

    
335
def print_subcommands_help(cmd):
336
    printout = {}
337
    for subcmd in cmd.get_subcommands():
338
        spec, sep, print_path = subcmd.path.partition('_')
339
        printout[print_path.replace('_', ' ')] = subcmd.description
340
    if printout:
341
        print('\nOptions:\n - - - -')
342
        print_dict(printout)
343

    
344

    
345
def update_parser_help(parser, cmd):
346
    global _best_match
347
    parser.syntax = parser.syntax.split('<')[0]
348
    parser.syntax += ' '.join(_best_match)
349

    
350
    description = ''
351
    if cmd.is_command:
352
        cls = cmd.get_class()
353
        parser.syntax += ' ' + cls.syntax
354
        parser.update_arguments(cls().arguments)
355
        description = getattr(cls, 'long_description', '')
356
        description = description.strip()
357
    else:
358
        parser.syntax += ' <...>'
359
    if cmd.has_description:
360
        parser.parser.description = cmd.help + (
361
            ('\n%s' % description) if description else '')
362
    else:
363
        parser.parser.description = description
364

    
365

    
366
def print_error_message(cli_err):
367
    errmsg = '%s' % cli_err
368
    if cli_err.importance == 1:
369
        errmsg = magenta(errmsg)
370
    elif cli_err.importance == 2:
371
        errmsg = yellow(errmsg)
372
    elif cli_err.importance > 2:
373
        errmsg = red(errmsg)
374
    stdout.write(errmsg)
375
    for errmsg in cli_err.details:
376
        print('|  %s' % errmsg)
377

    
378

    
379
def exec_cmd(instance, cmd_args, help_method):
380
    try:
381
        return instance.main(*cmd_args)
382
    except TypeError as err:
383
        if err.args and err.args[0].startswith('main()'):
384
            print(magenta('Syntax error'))
385
            if _debug:
386
                raise err
387
            if _verbose:
388
                print(unicode(err))
389
            help_method()
390
        else:
391
            raise
392
    return 1
393

    
394

    
395
def get_command_group(unparsed, arguments):
396
    groups = arguments['config'].get_groups()
397
    for term in unparsed:
398
        if term.startswith('-'):
399
            continue
400
        if term in groups:
401
            unparsed.remove(term)
402
            return term
403
        return None
404
    return None
405

    
406

    
407
def set_command_params(parameters):
408
    """Add a parameters list to a command
409

410
    :param paramters: (list of str) a list of parameters
411
    """
412
    global command
413
    def_params = list(command.func_defaults)
414
    def_params[0] = parameters
415
    command.func_defaults = tuple(def_params)
416

    
417

    
418
#  CLI Choice:
419

    
420
def run_one_cmd(exe_string, parser, auth_base, cloud):
421
    global _history
422
    _history = History(
423
        parser.arguments['config'].get_global('history_file'))
424
    _history.add(' '.join([exe_string] + argv[1:]))
425
    from kamaki.cli import one_command
426
    one_command.run(auth_base, cloud, parser, _help)
427

    
428

    
429
def run_shell(exe_string, parser, auth_base, cloud):
430
    from command_shell import _init_shell
431
    shell = _init_shell(exe_string, parser)
432
    _load_all_commands(shell.cmd_tree, parser.arguments)
433
    shell.run(auth_base, cloud, parser)
434

    
435

    
436
def is_non_API(parser):
437
    nonAPIs = ('history', 'config')
438
    for term in parser.unparsed:
439
        if not term.startswith('-'):
440
            if term in nonAPIs:
441
                return True
442
            return False
443
    return False
444

    
445

    
446
def main():
447
    try:
448
        exe = basename(argv[0])
449
        parser = ArgumentParseManager(exe)
450

    
451
        if parser.arguments['version'].value:
452
            exit(0)
453

    
454
        log_file = parser.arguments['config'].get_global('log_file')
455
        if log_file:
456
            logger.set_log_filename(log_file)
457
        global filelog
458
        filelog = logger.add_file_logger(__name__.split('.')[0])
459
        filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
460

    
461
        auth_base, cloud = _init_session(parser.arguments, is_non_API(parser))
462

    
463
        from kamaki.cli.utils import suggest_missing
464
        suggest_missing()
465

    
466
        if parser.unparsed:
467
            run_one_cmd(exe, parser, auth_base, cloud)
468
        elif _help:
469
            parser.parser.print_help()
470
            _groups_help(parser.arguments)
471
        else:
472
            run_shell(exe, parser, auth_base, cloud)
473
    except CLIError as err:
474
        print_error_message(err)
475
        if _debug:
476
            raise err
477
        exit(1)
478
    except Exception as er:
479
        print('Unknown Error: %s' % er)
480
        if _debug:
481
            raise
482
        exit(1)