__init__.py 11.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python

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

from __future__ import print_function

import gevent.monkey
#Monkey-patch everything for gevent early on
gevent.monkey.patch_all()

import logging

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
50
51
52
53

try:
    from collections import OrderedDict
except ImportError:
    from ordereddict import OrderedDict

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
Stavros Sachtouris's avatar
Stavros Sachtouris committed
60
from .history import History
61

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

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

def _allow_class_in_cmd_tree(cls):
    global allow_all_commands
    if allow_all_commands:
        return True
    global allow_no_commands 
    if allow_no_commands:
84
85
86
        return False

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

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

104
105
def command():
    """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
114
115
116
117
118
119
120
121
122
123
124
125
126
        cls.description, sep, cls.long_description = cls.__doc__.partition('\n')

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

127
128
        #store each term, one by one, first
        _commands.add_command(cls.__name__, cls.description, cls)
129
130
131
        return cls
    return decorator

132
def _update_parser(parser, arguments):
133
134
    for name, argument in arguments.items():
        try:
135
            argument.update_parser(parser, name)
136
137
        except ArgumentError:
            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
144
    return parser

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

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

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

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

181
def shallow_load():
182
    """Load only group names and descriptions"""
183
184
    global allow_no_commands 
    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')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
191
192
    if spec_pkg is None:
        return None
193
194
195
    for location in cmd_spec_locations:
        location += spec_pkg if location == '' else ('.'+spec_pkg)
        try:
196
            package = __import__(location, fromlist=['API_DESCRIPTION'])
197
198
        except ImportError:
            continue
199
200
        if reload_package:
            reload(package)
201
202
        for grp, descr in package.API_DESCRIPTION.items():
            _commands.add_command(grp, descr)
203
204
205
        return package
    raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)

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

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
def setup_logging(silent=False, debug=False, verbose=False, include=False):
    """handle logging for clients package"""

    def add_handler(name, level, prefix=''):
        h = logging.StreamHandler()
        fmt = logging.Formatter(prefix + '%(message)s')
        h.setFormatter(fmt)
        logger = logging.getLogger(name)
        logger.addHandler(h)
        logger.setLevel(level)

    if silent:
        add_handler('', logging.CRITICAL)
    elif debug:
        add_handler('requests', logging.INFO, prefix='* ')
        add_handler('clients.send', logging.DEBUG, prefix='> ')
        add_handler('clients.recv', logging.DEBUG, prefix='< ')
    elif verbose:
        add_handler('requests', logging.INFO, prefix='* ')
        add_handler('clients.send', logging.INFO, prefix='> ')
        add_handler('clients.recv', logging.INFO, prefix='< ')
    elif include:
        add_handler('clients.recv', logging.INFO)
    else:
        add_handler('', logging.WARNING)

247
def one_command():
248
    _debug = False
249
    _help = False
250
    _verbose = False
251
    try:
252
253
254
        exe = basename(argv[0])
        parser = _init_parser(exe)
        parsed, unparsed = parse_known_args(parser)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
255
256
        _history = History(_arguments['config'].get('history', 'file'))
        _history.add(' '.join([exe]+argv[1:]))
257
258
        _debug = _arguments['debug'].value
        _help = _arguments['help'].value
259
        _verbose = _arguments['verbose'].value
260
261
        if _arguments['version'].value:
            exit(0)
262

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

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

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

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

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