Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ b46307af

History | View | Annotate | Download (11.3 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
from .history import History
61

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

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

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

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

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

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

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

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

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

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

    
127
        #store each term, one by one, first
128
        _commands.add_command(cls.__name__, cls.description, cls)
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, verbose=False):
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 verbose and 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 one_command():
248
    _debug = False
249
    _help = False
250
    _verbose = False
251
    try:
252
        exe = basename(argv[0])
253
        parser = _init_parser(exe)
254
        parsed, unparsed = parse_known_args(parser)
255
        _history = History(_arguments['config'].get('history', 'file'))
256
        _history.add(' '.join([exe]+argv[1:]))
257
        _debug = _arguments['debug'].value
258
        _help = _arguments['help'].value
259
        _verbose = _arguments['verbose'].value
260
        if _arguments['version'].value:
261
            exit(0)
262

    
263
        group = get_command_group(unparsed)
264
        if group is None:
265
            parser.print_help()
266
            shallow_load()
267
            print_commands(full_depth=_debug)
268
            exit(0)
269

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

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

    
293
            print_commands(cmd.path, full_depth=_debug)
294
            exit(0)
295

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