Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ f3e94e06

History | View | Annotate | Download (11.4 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 .config import Config #TO BE REMOVED
57
from .utils import bold, magenta, red, yellow, print_list, print_dict
58
from .command_tree import CommandTree
59
from argument import _arguments, parse_known_args
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
        return cls
129
    return decorator
130

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

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

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

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

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

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

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

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

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

    
218
def setup_logging(silent=False, debug=False, verbose=False, include=False):
219
    """handle logging for clients package"""
220

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

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

    
244
def one_command():
245
    _debug = False
246
    _help = False
247
    _verbose = False
248
    try:
249

    
250
        exe = basename(argv[0])
251
        parser = _init_parser(exe)
252
        parsed, unparsed = parse_known_args(parser)
253
        _history = _arguments['history']
254
        if _history.value:
255
            cmd_list = [term for term in argv if term not in _history.parsed_name]
256
            print_list(_history.get(' '.join(cmd_list)))
257
            _arguments['history'].add(' '.join(argv))
258
            exit(0)
259
        _arguments['history'].add(' '.join(argv))
260
        _debug = _arguments['debug'].value
261
        _help = _arguments['help'].value
262
        _verbose = _arguments['verbose'].value
263
        if _arguments['version'].value:
264
            exit(0)
265

    
266
        group = get_command_group(unparsed)
267
        if group is None:
268
            parser.print_help()
269
            shallow_load()
270
            print_commands(full_depth=_debug)
271
            exit(0)
272

    
273
        cmd = load_command(group, unparsed)
274
        if _help or not cmd.is_command:
275
            if cmd.has_description:
276
                parser.description = cmd.help 
277
            else:
278
                try:
279
                    parser.description = _commands.get_closest_ancestor_command(cmd.path).help
280
                except KeyError:
281
                    parser.description = ' '
282
            parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
283
            if cmd.is_command:
284
                cli = cmd.get_class()
285
                parser.prog += cli.syntax
286
                _update_parser(parser, cli().arguments)
287
            else:
288
                parser.prog += '[...]'
289
            parser.print_help()
290

    
291
            #Shuuuut, we now have to load one more level just to see what is missing
292
            global allow_subclass_signatures 
293
            allow_subclass_signatures = True
294
            load_command(group, cmd.path.split('_')[1:], reload_package=True)
295

    
296
            print_commands(cmd.path, full_depth=_debug)
297
            exit(0)
298

    
299
        setup_logging(silent=_arguments['silent'].value, debug=_debug, verbose=_verbose,
300
            include=_arguments['include'].value)
301
        cli = cmd.get_class()
302
        executable = cli(_arguments)
303
        _update_parser(parser, executable.arguments)
304
        parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
305
        parsed, new_unparsed = parse_known_args(parser)
306
        unparsed = [term for term in unparsed if term in new_unparsed]
307
        try:
308
            ret = executable.main(*unparsed)
309
            exit(ret)
310
        except TypeError as e:
311
            if e.args and e.args[0].startswith('main()'):
312
                parser.print_help()
313
                exit(1)
314
            else:
315
                raise
316
    except CLIError as err:
317
        if _debug:
318
            raise
319
        _print_error_message(err, verbose=_verbose)
320
        exit(1)