Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 8cec3671

History | View | Annotate | Download (13.9 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
    global _colors
206
    _colors = _cnf.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
    _setup_logging(_silent, _debug, _verbose, _include)
212
    picked_cloud = arguments['cloud'].value
213
    if picked_cloud:
214
        global_url = _cnf.get('remotes', picked_cloud)
215
        if not global_url:
216
            raise CLIError(
217
                'No remote cloud "%s" in kamaki configuration' % picked_cloud,
218
                importance=3, details=[
219
                    'To check if this remote cloud alias is declared:',
220
                    '  /config get remotes.%s' % picked_cloud,
221
                    'To set a remote authentication URI aliased as "%s"' % (
222
                        picked_cloud),
223
                    '  /config set remotes.%s <URI>' % picked_cloud
224
                ])
225
    else:
226
        global_url = _cnf.get('global', 'auth_url')
227
    global_token = _cnf.get('global', 'token')
228
    from kamaki.clients.astakos import AstakosClient as AuthCachedClient
229
    try:
230
        return AuthCachedClient(global_url, global_token)
231
    except AssertionError as ae:
232
        kloger.warning('WARNING: Failed to load auth_url %s [ %s ]' % (
233
            global_url, ae))
234
        return None
235

    
236

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

    
253

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

    
281

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

    
299

    
300
#  Methods to be used by CLI implementations
301

    
302

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

    
312

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

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

    
333

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

    
346

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

    
362

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

    
374

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

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

    
385

    
386
#  CLI Choice:
387

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

    
396

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

    
403

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

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

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

    
419
        auth_base = _init_session(parser.arguments)
420

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

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