Adjust config
[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, 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)