Use < and > in stream logger for send/recv resp.
[kamaki] / kamaki / cli / __init__.py
1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
33
34 import logging
35 from sys import argv, exit, stdout
36 from os.path import basename
37 from inspect import getargspec
38
39 from kamaki.cli.argument import ArgumentParseManager
40 from kamaki.cli.history import History
41 from kamaki.cli.utils import print_dict, red, magenta, yellow
42 from kamaki.cli.errors import CLIError
43 from kamaki.logger import add_stream_logger, get_logger
44
45 _help = False
46 _debug = False
47 _include = False
48 _verbose = False
49 _colors = False
50 kloger = None
51
52 #  command auxiliary methods
53
54 _best_match = []
55
56
57 def _arg2syntax(arg):
58     return arg.replace(
59         '____', '[:').replace(
60             '___', ':').replace(
61                 '__', ']').replace(
62                     '_', ' ')
63
64
65 def _construct_command_syntax(cls):
66         spec = getargspec(cls.main.im_func)
67         args = spec.args[1:]
68         n = len(args) - len(spec.defaults or ())
69         required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
70         optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
71         cls.syntax = ' '.join(x for x in [required, optional] if x)
72         if spec.varargs:
73             cls.syntax += ' <%s ...>' % spec.varargs
74
75
76 def _num_of_matching_terms(basic_list, attack_list):
77     if not attack_list:
78         return len(basic_list)
79
80     matching_terms = 0
81     for i, term in enumerate(basic_list):
82         try:
83             if term != attack_list[i]:
84                 break
85         except IndexError:
86             break
87         matching_terms += 1
88     return matching_terms
89
90
91 def _update_best_match(name_terms, prefix=[]):
92     if prefix:
93         pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
94     else:
95         pref_list = []
96
97     num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
98     global _best_match
99     if not prefix:
100         _best_match = []
101
102     if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
103         if len(_best_match) < num_of_matching_terms:
104             _best_match = name_terms[:num_of_matching_terms]
105         return True
106     return False
107
108
109 def command(cmd_tree, prefix='', descedants_depth=1):
110     """Load a class as a command
111         e.g. spec_cmd0_cmd1 will be command spec cmd0
112
113         :param cmd_tree: is initialized in cmd_spec file and is the structure
114             where commands are loaded. Var name should be _commands
115         :param prefix: if given, load only commands prefixed with prefix,
116         :param descedants_depth: is the depth of the tree descedants of the
117             prefix command. It is used ONLY if prefix and if prefix is not
118             a terminal command
119
120         :returns: the specified class object
121     """
122
123     def wrap(cls):
124         global kloger
125         cls_name = cls.__name__
126
127         if not cmd_tree:
128             if _debug:
129                 kloger.warning('command %s found but not loaded' % cls_name)
130             return cls
131
132         name_terms = cls_name.split('_')
133         if not _update_best_match(name_terms, prefix):
134             if _debug:
135                 kloger.warning('%s failed to update_best_match' % cls_name)
136             return None
137
138         global _best_match
139         max_len = len(_best_match) + descedants_depth
140         if len(name_terms) > max_len:
141             partial = '_'.join(name_terms[:max_len])
142             if not cmd_tree.has_command(partial):  # add partial path
143                 cmd_tree.add_command(partial)
144             if _debug:
145                 kloger.warning('%s failed max_len test' % cls_name)
146             return None
147
148         (
149             cls.description, sep, cls.long_description
150         ) = cls.__doc__.partition('\n')
151         _construct_command_syntax(cls)
152
153         cmd_tree.add_command(cls_name, cls.description, cls)
154         return cls
155     return wrap
156
157
158 cmd_spec_locations = [
159     'kamaki.cli.commands',
160     'kamaki.commands',
161     'kamaki.cli',
162     'kamaki',
163     '']
164
165
166 #  Generic init auxiliary functions
167
168
169 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
170     """handle logging for clients package"""
171
172     if silent:
173         add_stream_logger(__name__, logging.CRITICAL)
174         return
175
176     sfmt, rfmt = '> %(message)s', '< %(message)s'
177     if debug:
178         add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
179         add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
180         add_stream_logger(__name__, logging.DEBUG)
181     elif verbose:
182         add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
183         add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
184         add_stream_logger(__name__, logging.INFO)
185     if include:
186         add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
187         add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
188     add_stream_logger(__name__, logging.WARNING)
189     global kloger
190     kloger = get_logger(__name__)
191
192
193 def _init_session(arguments):
194     global _help
195     _help = arguments['help'].value
196     global _debug
197     _debug = arguments['debug'].value
198     global _include
199     _include = arguments['include'].value
200     global _verbose
201     _verbose = arguments['verbose'].value
202     global _colors
203     _colors = arguments['config'].get('global', 'colors')
204     if not (stdout.isatty() and _colors == 'on'):
205         from kamaki.cli.utils import remove_colors
206         remove_colors()
207     _silent = arguments['silent'].value
208     _setup_logging(_silent, _debug, _verbose, _include)
209
210
211 def _load_spec_module(spec, arguments, module):
212     spec_name = arguments['config'].get(spec, 'cli')
213     if spec_name is None:
214         return None
215     pkg = None
216     for location in cmd_spec_locations:
217         location += spec_name if location == '' else '.%s' % spec_name
218         try:
219             pkg = __import__(location, fromlist=[module])
220             return pkg
221         except ImportError:
222             continue
223     return pkg
224
225
226 def _groups_help(arguments):
227     global _debug
228     global kloger
229     descriptions = {}
230     for spec in arguments['config'].get_groups():
231         pkg = _load_spec_module(spec, arguments, '_commands')
232         if pkg:
233             cmds = None
234             try:
235                 _cnf = arguments['config']
236                 cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
237                     cmd.name, 'cli')]
238             except AttributeError:
239                 if _debug:
240                     kloger.warning('No description for %s' % spec)
241             try:
242                 for cmd in cmds:
243                     descriptions[cmd.name] = cmd.description
244             except TypeError:
245                 if _debug:
246                     kloger.warning('no cmd specs in module %s' % spec)
247         elif _debug:
248             kloger.warning('Loading of %s cmd spec failed' % spec)
249     print('\nOptions:\n - - - -')
250     print_dict(descriptions)
251
252
253 def _load_all_commands(cmd_tree, arguments):
254     _cnf = arguments['config']
255     specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
256     for spec in specs:
257         try:
258             spec_module = _load_spec_module(spec, arguments, '_commands')
259             spec_commands = getattr(spec_module, '_commands')
260         except AttributeError:
261             if _debug:
262                 global kloger
263                 kloger.warning('No valid description for %s' % spec)
264             continue
265         for spec_tree in spec_commands:
266             if spec_tree.name == spec:
267                 cmd_tree.add_tree(spec_tree)
268                 break
269
270
271 #  Methods to be used by CLI implementations
272
273
274 def print_subcommands_help(cmd):
275     printout = {}
276     for subcmd in cmd.get_subcommands():
277         spec, sep, print_path = subcmd.path.partition('_')
278         printout[print_path.replace('_', ' ')] = subcmd.description
279     if printout:
280         print('\nOptions:\n - - - -')
281         print_dict(printout)
282
283
284 def update_parser_help(parser, cmd):
285     global _best_match
286     parser.syntax = parser.syntax.split('<')[0]
287     parser.syntax += ' '.join(_best_match)
288
289     description = ''
290     if cmd.is_command:
291         cls = cmd.get_class()
292         parser.syntax += ' ' + cls.syntax
293         parser.update_arguments(cls().arguments)
294         description = getattr(cls, 'long_description', '')
295         description = description.strip()
296     else:
297         parser.syntax += ' <...>'
298     if cmd.has_description:
299         parser.parser.description = cmd.help + (
300             ('\n%s' % description) if description else '')
301     else:
302         parser.parser.description = description
303
304
305 def print_error_message(cli_err):
306     errmsg = '%s' % cli_err
307     if cli_err.importance == 1:
308         errmsg = magenta(errmsg)
309     elif cli_err.importance == 2:
310         errmsg = yellow(errmsg)
311     elif cli_err.importance > 2:
312         errmsg = red(errmsg)
313     stdout.write(errmsg)
314     for errmsg in cli_err.details:
315         print('| %s' % errmsg)
316
317
318 def exec_cmd(instance, cmd_args, help_method):
319     try:
320         return instance.main(*cmd_args)
321     except TypeError as err:
322         if err.args and err.args[0].startswith('main()'):
323             print(magenta('Syntax error'))
324             if _debug:
325                 raise err
326             if _verbose:
327                 print(unicode(err))
328             help_method()
329         else:
330             raise
331     return 1
332
333
334 def get_command_group(unparsed, arguments):
335     groups = arguments['config'].get_groups()
336     for term in unparsed:
337         if term.startswith('-'):
338             continue
339         if term in groups:
340             unparsed.remove(term)
341             return term
342         return None
343     return None
344
345
346 def set_command_params(parameters):
347     """Add a parameters list to a command
348
349     :param paramters: (list of str) a list of parameters
350     """
351     global command
352     def_params = list(command.func_defaults)
353     def_params[0] = parameters
354     command.func_defaults = tuple(def_params)
355
356
357 #  CLI Choice:
358
359 def run_one_cmd(exe_string, parser):
360     global _history
361     _history = History(
362         parser.arguments['config'].get('history', 'file'))
363     _history.add(' '.join([exe_string] + argv[1:]))
364     from kamaki.cli import one_command
365     one_command.run(parser, _help)
366
367
368 def run_shell(exe_string, parser):
369     from command_shell import _init_shell
370     shell = _init_shell(exe_string, parser)
371     _load_all_commands(shell.cmd_tree, parser.arguments)
372     shell.run(parser)
373
374
375 def main():
376     try:
377         exe = basename(argv[0])
378         parser = ArgumentParseManager(exe)
379
380         if parser.arguments['version'].value:
381             exit(0)
382
383         log_file = parser.arguments['config'].get('global', 'log_file')
384         if log_file:
385             from kamaki.logger import set_log_filename
386             set_log_filename(log_file)
387
388         _init_session(parser.arguments)
389
390         from kamaki.cli.utils import suggest_missing
391         suggest_missing()
392
393         if parser.unparsed:
394             run_one_cmd(exe, parser)
395         elif _help:
396             parser.parser.print_help()
397             _groups_help(parser.arguments)
398         else:
399             run_shell(exe, parser)
400     except CLIError as err:
401         print_error_message(err)
402         if _debug:
403             raise err
404         exit(1)
405     except Exception as er:
406         print('Unknown Error: %s' % er)
407         if _debug:
408             raise
409         exit(1)