Interuption - Haven't finished pithos_cli adjust
[kamaki] / kamaki / cli / __init__.py
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
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     try:
162         for name, argument in arguments.items():
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 = '%s'%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)