__init__.py 10 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
47
from base64 import b64encode
from os.path import abspath, basename, exists
48
from sys import exit, stdout, stderr, argv
49
50
51
52
53
54

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

55
#from kamaki import clients
56
from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError, CLICmdSpecError
57
from .config import Config #TO BE REMOVED
58
from .utils import bold, magenta, red, yellow, print_list, print_dict
59
from .command_tree import CommandTree
60
from argument import _arguments, parse_known_args
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
145
    return parser

def _print_error_message(cli_err):
146
    errmsg = unicode(cli_err) + (' (%s)'%cli_err.status if cli_err.status else ' ')
147
148
149
150
151
152
153
154
155
156
    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)
    if cli_err.details is not None and len(cli_err.details) > 0:
        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
191
192
193
    spec_pkg = _arguments['config'].value.get(group, 'cli')
    for location in cmd_spec_locations:
        location += spec_pkg if location == '' else ('.'+spec_pkg)
        try:
194
            package = __import__(location, fromlist=['API_DESCRIPTION'])
195
196
        except ImportError:
            continue
197
198
        if reload_package:
            reload(package)
199
200
        for grp, descr in package.API_DESCRIPTION.items():
            _commands.add_command(grp, descr)
201
202
203
        return package
    raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)

204
205
206
207
208
209
210
211
212
213
214
215
216
217
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()
218

219
def one_command():
220
    _debug = False
221
    _help = False
222
    _verbose = False
223
    try:
224
225
226
        exe = basename(argv[0])
        parser = _init_parser(exe)
        parsed, unparsed = parse_known_args(parser)
227
228
        _debug = _arguments['debug'].value
        _help = _arguments['help'].value
229
        _verbose = _arguments['verbose'].value
230
231
        if _arguments['version'].value:
            exit(0)
232

233
234
        group = get_command_group(unparsed)
        if group is None:
235
            parser.print_help()
236
            shallow_load()
237
            print_commands(full_depth=_verbose)
238
239
            exit(0)

240
241
242
243
        cmd = load_command(group, unparsed)
        if _help or not cmd.is_command:
            if cmd.has_description:
                parser.description = cmd.help 
244
            else:
245
246
247
248
249
250
251
                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()
252
253
                parser.prog += cli.syntax
                _update_parser(parser, cli().arguments)
254
255
            else:
                parser.prog += '[...]'
256
257
258
            parser.print_help()

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

263
            print_commands(cmd.path, full_depth=_verbose)
264
265
            exit(0)

266
267
268
269
        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
270
271
        parsed, new_unparsed = parse_known_args(parser)
        unparsed = [term for term in unparsed if term in new_unparsed]
272
        try:
273
            ret = executable.main(*unparsed)
274
275
276
277
278
279
280
            exit(ret)
        except TypeError as e:
            if e.args and e.args[0].startswith('main()'):
                parser.print_help()
                exit(1)
            else:
                raise
281
    except CLIError as err:
282
283
        if _debug:
            raise
284
        _print_error_message(err)
285
        exit(1)