Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ 26cd03ca

History | View | Annotate | Download (10 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 base64 import b64encode
47
from os.path import abspath, basename, exists
48
from sys import exit, stdout, stderr, argv
49

    
50
try:
51
    from collections import OrderedDict
52
except ImportError:
53
    from ordereddict import OrderedDict
54

    
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
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):
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 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
    for location in cmd_spec_locations:
192
        location += spec_pkg if location == '' else ('.'+spec_pkg)
193
        try:
194
            package = __import__(location, fromlist=['API_DESCRIPTION'])
195
        except ImportError:
196
            continue
197
        if reload_package:
198
            reload(package)
199
        for grp, descr in package.API_DESCRIPTION.items():
200
            _commands.add_command(grp, descr)
201
        return package
202
    raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
203

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

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

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

    
240
        cmd = load_command(group, unparsed)
241
        if _help or not cmd.is_command:
242
            if cmd.has_description:
243
                parser.description = cmd.help 
244
            else:
245
                try:
246
                    parser.description = _commands.get_closest_ancestor_command(cmd.path).help
247
                except KeyError:
248
                    parser.description = ' '
249
            parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
250
            if cmd.is_command:
251
                cli = cmd.get_class()
252
                parser.prog += cli.syntax
253
                _update_parser(parser, cli().arguments)
254
            else:
255
                parser.prog += '[...]'
256
            parser.print_help()
257

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

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

    
266
        cli = cmd.get_class()
267
        executable = cli(_arguments)
268
        _update_parser(parser, executable.arguments)
269
        parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
270
        parsed, new_unparsed = parse_known_args(parser)
271
        unparsed = [term for term in unparsed if term in new_unparsed]
272
        try:
273
            ret = executable.main(*unparsed)
274
            exit(ret)
275
        except TypeError as e:
276
            if e.args and e.args[0].startswith('main()'):
277
                parser.print_help()
278
                exit(1)
279
            else:
280
                raise
281
    except CLIError as err:
282
        if _debug:
283
            raise
284
        _print_error_message(err)
285
        exit(1)