Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 67cea04c

History | View | Annotate | Download (11.1 kB)

1
#!/usr/bin/env python
2

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

    
36
from __future__ import print_function
37

    
38
import gevent.monkey
39
#Monkey-patch everything for gevent early on
40
gevent.monkey.patch_all()
41

    
42
import logging
43

    
44
from inspect import getargspec
45
from argparse import ArgumentParser, ArgumentError
46
from os.path import basename
47
from sys import exit, stdout, stderr, argv
48

    
49
try:
50
        from collections import OrderedDict
51
except ImportError:
52
        from ordereddict import OrderedDict
53

    
54
#from kamaki import clients
55
from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError, CLICmdSpecError
56
from .utils import bold, magenta, red, yellow, print_list, print_dict, remove_colors
57
from .command_tree import CommandTree
58
from .argument import _arguments, parse_known_args
59
from .history import History
60

    
61
cmd_spec_locations = [
62
        'kamaki.cli.commands',
63
        'kamaki.commands',
64
        'kamaki.cli',
65
        'kamaki',
66
        '']
67
_commands = CommandTree(name='kamaki', description='A command line tool for poking clouds')
68

    
69
#If empty, all commands are loaded, if not empty, only commands in this list
70
#e.g. [store, lele, list, lolo] is good to load store_list but not list_store
71
#First arg should always refer to a group
72
candidate_command_terms = []
73
allow_no_commands = False
74
allow_all_commands = False
75
allow_subclass_signatures = False
76

    
77
def _allow_class_in_cmd_tree(cls):
78
        global allow_all_commands
79
        if allow_all_commands:
80
                return True
81
        global allow_no_commands 
82
        if allow_no_commands:
83
                return False
84

    
85
        term_list = cls.__name__.split('_')
86
        global candidate_command_terms
87
        index = 0
88
        for term in candidate_command_terms:
89
                try:
90
                        index += 1 if term_list[index] == term else 0
91
                except IndexError: #Whole term list matched!
92
                        return True
93
        if allow_subclass_signatures:
94
                if index == len(candidate_command_terms) and len(term_list) > index:
95
                        try: #is subterm already in _commands?
96
                                _commands.get_command('_'.join(term_list[:index+1]))
97
                        except KeyError: #No, so it must be placed there
98
                                return True
99
                return False
100

    
101
        return True if index == len(term_list) else False
102

    
103
def command():
104
        """Class decorator that registers a class as a CLI command"""
105

    
106
        def decorator(cls):
107
                """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
108

    
109
                if not _allow_class_in_cmd_tree(cls):
110
                        return cls
111

    
112
                cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
113

    
114
                # Generate a syntax string based on main's arguments
115
                spec = getargspec(cls.main.im_func)
116
                args = spec.args[1:]
117
                n = len(args) - len(spec.defaults or ())
118
                required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
119
                        replace('_', ' ') for x in args[:n])
120
                optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
121
                        replace('_', ' ') for x in args[n:])
122
                cls.syntax = ' '.join(x for x in [required, optional] if x)
123
                if spec.varargs:
124
                        cls.syntax += ' <%s ...>' % spec.varargs
125

    
126
                #store each term, one by one, first
127
                _commands.add_command(cls.__name__, cls.description, cls)
128

    
129
                return cls
130
        return decorator
131

    
132
def _update_parser(parser, arguments):
133
        for name, argument in arguments.items():
134
                try:
135
                        argument.update_parser(parser, name)
136
                except ArgumentError:
137
                        pass
138

    
139
def _init_parser(exe):
140
        parser = ArgumentParser(add_help=False)
141
        parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
142
        _update_parser(parser, _arguments)
143
        return parser
144

    
145
def _print_error_message(cli_err):
146
        errmsg = unicode(cli_err) + (' (%s)'%cli_err.status if cli_err.status else ' ')
147
        if cli_err.importance == 1:
148
                errmsg = magenta(errmsg)
149
        elif cli_err.importance == 2:
150
                errmsg = yellow(errmsg)
151
        elif cli_err.importance > 2:
152
                errmsg = red(errmsg)
153
        stdout.write(errmsg)
154
        if cli_err.details is not None and len(cli_err.details) > 0:
155
                print(': %s'%cli_err.details)
156
        else:
157
                print()
158

    
159
def get_command_group(unparsed):
160
        groups = _arguments['config'].get_groups()
161
        for grp_candidate in unparsed:
162
                if grp_candidate in groups:
163
                        unparsed.remove(grp_candidate)
164
                        return grp_candidate
165
        return None
166

    
167
def load_command(group, unparsed, reload_package=False):
168
        global candidate_command_terms
169
        candidate_command_terms = [group] + unparsed
170
        pkg = load_group_package(group, reload_package)
171

    
172
        #From all possible parsed commands, chose the first match in user string
173
        final_cmd = _commands.get_command(group)
174
        for term in unparsed:
175
                cmd = final_cmd.get_subcmd(term)
176
                if cmd is not None:
177
                        final_cmd = cmd
178
                        unparsed.remove(cmd.name)
179
        return final_cmd
180

    
181
def shallow_load():
182
        """Load only group names and descriptions"""
183
        global allow_no_commands 
184
        allow_no_commands = True#load only descriptions
185
        for grp in _arguments['config'].get_groups():
186
                load_group_package(grp)
187
        allow_no_commands = False
188

    
189
def load_group_package(group, reload_package=False):
190
        spec_pkg = _arguments['config'].value.get(group, 'cli')
191
        if spec_pkg is None:
192
                return None
193
        for location in cmd_spec_locations:
194
                location += spec_pkg if location == '' else ('.'+spec_pkg)
195
                try:
196
                        package = __import__(location, fromlist=['API_DESCRIPTION'])
197
                except ImportError:
198
                        continue
199
                if reload_package:
200
                        reload(package)
201
                for grp, descr in package.API_DESCRIPTION.items():
202
                        _commands.add_command(grp, descr)
203
                return package
204
        raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
205

    
206
def print_commands(prefix=None, full_depth=False):
207
        cmd_list = _commands.get_groups() if prefix is None else _commands.get_subcommands(prefix)
208
        cmds = {}
209
        for subcmd in cmd_list:
210
                if subcmd.sublen() > 0:
211
                        sublen_str = '( %s more terms ... )'%subcmd.sublen()
212
                        cmds[subcmd.name] = [subcmd.help, sublen_str] if subcmd.has_description else subcmd_str
213
                else:
214
                        cmds[subcmd.name] = subcmd.help
215
        if len(cmds) > 0:
216
                print('\nOptions:')
217
                print_dict(cmds, ident=12)
218
        if full_depth:
219
                _commands.pretty_print()
220

    
221
def setup_logging(silent=False, debug=False, verbose=False, include=False):
222
        """handle logging for clients package"""
223

    
224
        def add_handler(name, level, prefix=''):
225
                h = logging.StreamHandler()
226
                fmt = logging.Formatter(prefix + '%(message)s')
227
                h.setFormatter(fmt)
228
                logger = logging.getLogger(name)
229
                logger.addHandler(h)
230
                logger.setLevel(level)
231

    
232
        if silent:
233
                add_handler('', logging.CRITICAL)
234
        elif debug:
235
                add_handler('requests', logging.INFO, prefix='* ')
236
                add_handler('clients.send', logging.DEBUG, prefix='> ')
237
                add_handler('clients.recv', logging.DEBUG, prefix='< ')
238
        elif verbose:
239
                add_handler('requests', logging.INFO, prefix='* ')
240
                add_handler('clients.send', logging.INFO, prefix='> ')
241
                add_handler('clients.recv', logging.INFO, prefix='< ')
242
        elif include:
243
                add_handler('clients.recv', logging.INFO)
244
        else:
245
                add_handler('', logging.WARNING)
246

    
247
def _exec_cmd(instance, cmd_args, help_method):
248
        try:
249
                return instance.main(*cmd_args)
250
        except TypeError as err:
251
                if err.args and err.args[0].startswith('main()'):
252
                        print(magenta('Syntax error'))
253
                        if instance.get_argument('verbose'):
254
                                print(unicode(err))
255
                        help_method()
256
                else:
257
                        raise
258
        except CLIError as err:
259
                if instance.get_argument('debug'):
260
                        raise
261
                _print_error_message(err)
262
        return 1
263

    
264
def one_command():
265
        _debug = False
266
        _help = False
267
        _verbose = False
268
        try:
269
                exe = basename(argv[0])
270
                parser = _init_parser(exe)
271
                parsed, unparsed = parse_known_args(parser, _arguments)
272
                _colors = _arguments['config'].get('global', 'colors')
273
                if _colors!='on':
274
                        remove_colors()
275
                _history = History(_arguments['config'].get('history', 'file'))
276
                _history.add(' '.join([exe]+argv[1:]))
277
                _debug = _arguments['debug'].value
278
                _help = _arguments['help'].value
279
                _verbose = _arguments['verbose'].value
280
                if _arguments['version'].value:
281
                        exit(0)
282

    
283
                group = get_command_group(unparsed)
284
                if group is None:
285
                        parser.print_help()
286
                        shallow_load()
287
                        print_commands(full_depth=_debug)
288
                        exit(0)
289

    
290
                cmd = load_command(group, unparsed)
291
                if _help or not cmd.is_command:
292
                        if cmd.has_description:
293
                                parser.description = cmd.help 
294
                        else:
295
                                try:
296
                                        parser.description = _commands.get_closest_ancestor_command(cmd.path).help
297
                                except KeyError:
298
                                        parser.description = ' '
299
                        parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
300
                        if cmd.is_command:
301
                                cli = cmd.get_class()
302
                                parser.prog += cli.syntax
303
                                _update_parser(parser, cli().arguments)
304
                        else:
305
                                parser.prog += '[...]'
306
                        parser.print_help()
307

    
308
                        #Shuuuut, we now have to load one more level just to see what is missing
309
                        global allow_subclass_signatures 
310
                        allow_subclass_signatures = True
311
                        load_command(group, cmd.path.split('_')[1:], reload_package=True)
312

    
313
                        print_commands(cmd.path, full_depth=_debug)
314
                        exit(0)
315

    
316
                setup_logging(silent=_arguments['silent'].value, debug=_debug, verbose=_verbose,
317
                        include=_arguments['include'].value)
318
                cli = cmd.get_class()
319
                executable = cli(_arguments)
320
                _update_parser(parser, executable.arguments)
321
                parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
322
                parsed, new_unparsed = parse_known_args(parser, _arguments)
323
                unparsed = [term for term in unparsed if term in new_unparsed]
324
                ret = _exec_cmd(executable, unparsed, parser.print_help)
325
                exit(ret)
326
        except CLIError as err:
327
                if _debug:
328
                        raise
329
                _print_error_message(err)
330
                exit(1)
331

    
332
from command_shell import _fix_arguments ,Shell
333

    
334
def _start_shell():
335
        shell = Shell()
336
        shell.set_prompt(basename(argv[0]))
337
        from kamaki import __version__ as version
338
        shell.greet(version)
339
        shell.do_EOF = shell.do_exit
340
        return shell
341

    
342
def run_shell():
343
        _fix_arguments()
344
        shell = _start_shell()
345
        _config = _arguments['config']
346
        _config.value = None
347
        for grp in _config.get_groups():
348
                global allow_all_commands
349
                allow_all_commands = True
350
                load_group_package(grp)
351
        setup_logging(silent=_arguments['silent'].value, debug=_arguments['debug'].value,
352
                verbose=_arguments['verbose'].value, include=_arguments['include'].value)
353
        shell.cmd_tree = _commands
354
        shell.run()
355

    
356
def main():
357

    
358
        if len(argv) <= 1:
359
                run_shell()
360
        else:
361
                one_command()