__init__.py 11.9 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
132
        return cls
    return decorator

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

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

146
def _print_error_message(cli_err):
147
    errmsg = unicode(cli_err) + (' (%s)'%cli_err.status if cli_err.status else ' ')
148
149
150
151
152
153
154
    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)
155
    if cli_err.details is not None and len(cli_err.details) > 0:
156
157
        print(': %s'%cli_err.details)
    else:
158
        print()
159

160
161
162
163
164
165
166
167
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

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

173
174
175
176
177
178
179
    #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)
180
    return final_cmd
181

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

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

207
208
209
210
211
212
213
214
215
216
217
218
219
220
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()
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
247
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)

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

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

271
272
273
274
        cmd = load_command(group, unparsed)
        if _help or not cmd.is_command:
            if cmd.has_description:
                parser.description = cmd.help 
275
            else:
276
277
278
279
280
281
282
                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()
283
284
                parser.prog += cli.syntax
                _update_parser(parser, cli().arguments)
285
286
            else:
                parser.prog += '[...]'
287
288
289
            parser.print_help()

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

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

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

320
from command_shell import Shell, _fix_arguments
321

322
323
324
325
326
327
def _start_shell():
    shell = Shell()
    shell.set_prompt(basename(argv[0]))
    from kamaki import __version__ as version
    shell.greet(version)
    shell.do_EOF = shell.do_exit
328
    return shell
329

Stavros Sachtouris's avatar
Stavros Sachtouris committed
330
def run_shell():
331
    _fix_arguments()
332
    shell = _start_shell()
Stavros Sachtouris's avatar
Stavros Sachtouris committed
333
334
335
    _config = _arguments['config']
    _config.value = None
    for grp in _config.get_groups():
336
337
        global allow_all_commands
        allow_all_commands = True
Stavros Sachtouris's avatar
Stavros Sachtouris committed
338
        load_group_package(grp)
339
340

    shell.kamaki_loop(_commands)
341
342

def main():
343

344
    if len(argv) <= 1:
Stavros Sachtouris's avatar
Stavros Sachtouris committed
345
        run_shell()
346
347
    else:
        one_command()