Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / __init__.py @ e3d4d442

History | View | Annotate | Download (12.1 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, CommandTree, print_list, print_dict
59
from argument import _arguments, parse_known_args
60

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

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

    
76
def _put_subclass_signatures_in_commands(cls):
77
    global candidate_command_terms
78

    
79
    part_name = '_'.join(candidate_command_terms)
80
    try:
81
        empty, same, rest = cls.__name__.partition(part_name)
82
    except ValueError:
83
        return False
84
    if len(empty) != 0:
85
        return False
86
    if len(rest) == 0:
87
        _commands.add_path(cls.__name__, (cls.__doc__.partition('\n'))[0])
88
    else:
89
        rest_terms = rest[1:].split('_')
90
        new_name = part_name+'_'+rest_terms[0]
91
        desc = cls.__doc__.partition('\n')[0] if new_name == cls.__name__ else ''
92
        _commands.add_path(new_name, desc)
93
    return True
94

    
95

    
96
def _put_class_path_in_commands(cls):
97
    #Maybe I should apologise for the globals, but they are used in a smart way, so...
98
    global candidate_command_terms
99
    term_list = cls.__name__.split('_')
100

    
101
    tmp_tree = _commands
102
    if len(candidate_command_terms) > 0:
103
        #This is the case of a one-command execution: discard if not requested
104
        if term_list[0] != candidate_command_terms[0]:
105
            return False
106
        i = 0
107
        for term in term_list:
108
            #check if the term is requested by user
109
            if term not in candidate_command_terms[i:]:
110
                return False
111
            i = 1+candidate_command_terms.index(term)
112
            #now, put the term in the tree
113
            if term not in tmp_tree.get_command_names():
114
                tmp_tree.add_command(term)
115
            tmp_tree = tmp_tree.get_command(term)
116
    else:
117
        #Just insert everything in the tree
118
        for term in term_list:
119
            if term not in tmp_tree.get_command_names():
120
                tmp_tree.add_command(term)
121
            tmp_tree = tmp_tree.get_command()
122
    return True
123

    
124
def command():
125
    """Class decorator that registers a class as a CLI command"""
126

    
127
    def decorator(cls):
128
        """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
129
        global do_no_load_commands
130
        if do_no_load_commands:
131
            return cls
132

    
133
        global put_subclass_signatures_in_commands
134
        if put_subclass_signatures_in_commands:
135
            _put_subclass_signatures_in_commands(cls)
136
            return cls
137

    
138
        if not _put_class_path_in_commands(cls):
139
            return cls
140

    
141
        cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
142

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

    
155
        #store each term, one by one, first
156
        _commands.add_command(cls.__name__, cls.description, cls)
157
        return cls
158
    return decorator
159

    
160
def _update_parser(parser, arguments):
161
    for name, argument in arguments.items():
162
        try:
163
            argument.update_parser(parser, name)
164
        except ArgumentError:
165
            pass
166

    
167
def _init_parser(exe):
168
    parser = ArgumentParser(add_help=False)
169
    parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
170
    _update_parser(parser, _arguments)
171
    return parser
172

    
173
def _print_error_message(cli_err):
174
    errmsg = unicode(cli_err) + (' (%s)'%cli_err.status if cli_err.status else ' ')
175
    if cli_err.importance == 1:
176
        errmsg = magenta(errmsg)
177
    elif cli_err.importance == 2:
178
        errmsg = yellow(errmsg)
179
    elif cli_err.importance > 2:
180
        errmsg = red(errmsg)
181
    stdout.write(errmsg)
182
    if cli_err.details is not None and len(cli_err.details) > 0:
183
        print(': %s'%cli_err.details)
184
    else:
185
        print()
186

    
187
def _expand_cmd(cmd_prefix, unparsed):
188
    if len(unparsed) == 0:
189
        return None
190
    prefix = (cmd_prefix+'_') if len(cmd_prefix) > 0 else ''
191
    for term in _commands.list(cmd_prefix):
192
        try:
193
            unparsed.remove(term)
194
        except ValueError:
195
            continue
196
        return prefix+term
197
    return None
198

    
199
def _retrieve_cmd(unparsed):
200
    cmd_str = None
201
    cur_cmd = _expand_cmd('', unparsed)
202
    while cur_cmd is not None:
203
        cmd_str = cur_cmd
204
        cur_cmd = _expand_cmd(cur_cmd, unparsed)
205
    if cmd_str is None:
206
        print(bold('Command groups:'))
207
        print_list(_commands.get_groups(), ident=14)
208
        print
209
        return None
210
    try:
211
        return _commands.get_class(cmd_str)
212
    except CLICmdIncompleteError:
213
        print(bold('%s:'%cmd_str))
214
        print_list(_commands.list(cmd_str))
215
    return None
216

    
217
def get_command_group(unparsed):
218
    groups = _arguments['config'].get_groups()
219
    for grp_candidate in unparsed:
220
        if grp_candidate in groups:
221
            unparsed.remove(grp_candidate)
222
            return grp_candidate
223
    return None
224

    
225
def _order_in_list(list1, list2):
226
    order = 0
227
    for i,term in enumerate(list1):
228
        order += len(list2)*i*list2.index(term)
229
    return order
230

    
231
def load_command(group, unparsed, reload_package=False):
232
    global candidate_command_terms
233
    candidate_command_terms = [group] + unparsed
234
    pkg = load_group_package(group, reload_package)
235

    
236
    #From all possible parsed commands, chose one
237
    final_cmd = group
238
    next_names = [None]
239
    next_names = _commands.get_command_names(final_cmd)
240
    while len(next_names) > 0:
241
        if len(next_names) == 1:
242
            final_cmd+='_'+next_names[0]
243
        else:#choose the first in user string
244
            try:
245
                pos = unparsed.index(next_names[0])
246
            except ValueError:
247
                return final_cmd
248
            choice = 0
249
            for i, name in enumerate(next_names[1:]):
250
                tmp_index = unparsed.index(name)
251
                if tmp_index < pos:
252
                    pos = tmp_index
253
                    choice = i+1
254
            final_cmd+='_'+next_names[choice]
255
        next_names = _commands.get_command_names(final_cmd)
256
    return final_cmd
257

    
258
def shallow_load():
259
    """Load only group names and descriptions"""
260
    global do_no_load_commands
261
    do_no_load_commands = True#load only descriptions
262
    for grp in _arguments['config'].get_groups():
263
        load_group_package(grp)
264
    do_no_load_commands = False
265

    
266
def load_group_package(group, reload_package=False):
267
    spec_pkg = _arguments['config'].value.get(group, 'cli')
268
    for location in cmd_spec_locations:
269
        location += spec_pkg if location == '' else ('.'+spec_pkg)
270
        try:
271
            package = __import__(location, fromlist=['API_DESCRIPTION'])
272
            if reload_package:
273
                reload(package)
274
        except ImportError:
275
            continue
276
        for grp, descr in package.API_DESCRIPTION.items():
277
            _commands.add_command(grp, descr)
278
        return package
279
    raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
280

    
281
def print_commands(prefix=[], full_tree=False):
282
    cmd = _commands.get_command(prefix)
283
    grps = {' . ':cmd.description} if cmd.is_command else {}
284
    for grp in cmd.get_command_names():
285
        grps[grp] = cmd.get_description(grp)
286
    print('\nOptions:')
287
    print_dict(grps, ident=12)
288
    if full_tree:
289
        _commands.print_tree(level=-1)
290

    
291
def one_command():
292
    _debug = False
293
    _help = False
294
    try:
295
        exe = basename(argv[0])
296
        parser = _init_parser(exe)
297
        parsed, unparsed = parse_known_args(parser)
298
        _debug = _arguments['debug'].value
299
        _help = _arguments['help'].value
300
        if _arguments['version'].value:
301
            exit(0)
302

    
303
        group = get_command_group(unparsed)
304
        if group is None:
305
            parser.print_help()
306
            shallow_load()
307
            print_commands(full_tree=_arguments['verbose'].value)
308
            print()
309
            exit(0)
310

    
311
        command_path = load_command(group, unparsed)
312
        cli = _commands.get_class(command_path)
313
        if cli is None or _help: #Not a complete command or help
314
            parser.description = _commands.closest_description(command_path)
315
            parser.prog = '%s %s '%(exe, command_path.replace('_', ' '))
316
            if cli is None:
317
                parser.prog += '<...>'
318
            else:
319
                parser.prog += cli.syntax
320
                _update_parser(parser, cli().arguments)
321
            parser.print_help()
322

    
323
            #Shuuuut, we now have to load one more level just to see what is missing
324
            global put_subclass_signatures_in_commands
325
            put_subclass_signatures_in_commands = True
326
            load_command(group, command_path.split('_')[1:], reload_package=True)
327

    
328
            print_commands(command_path, full_tree=_arguments['verbose'].value)
329
            exit(0)
330

    
331
        #Now, load the cmd
332
        cmd = cli(_arguments)
333
        _update_parser(parser, cmd.arguments)
334
        parser.prog = '%s %s %s'%(exe, command_path.replace('_', ' '), cli.syntax)
335
        parsed, unparsed = parse_known_args(parser)
336
        for term in command_path.split('_'):
337
            unparsed.remove(term)
338
        try:
339
            ret = cmd.main(*unparsed)
340
            exit(ret)
341
        except TypeError as e:
342
            if e.args and e.args[0].startswith('main()'):
343
                parser.print_help()
344
                exit(1)
345
            else:
346
                raise
347
    except CLIError as err:
348
        if _debug:
349
            raise
350
        _print_error_message(err)
351
        exit(1)