Log initial call arguments in first log
[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.cli import logger
44
45 _help = False
46 _debug = False
47 _include = False
48 _verbose = False
49 _colors = False
50 kloger = None
51 filelog = None
52
53 #  command auxiliary methods
54
55 _best_match = []
56
57
58 def _arg2syntax(arg):
59     return arg.replace(
60         '____', '[:').replace(
61             '___', ':').replace(
62                 '__', ']').replace(
63                     '_', ' ')
64
65
66 def _construct_command_syntax(cls):
67         spec = getargspec(cls.main.im_func)
68         args = spec.args[1:]
69         n = len(args) - len(spec.defaults or ())
70         required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
71         optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
72         cls.syntax = ' '.join(x for x in [required, optional] if x)
73         if spec.varargs:
74             cls.syntax += ' <%s ...>' % spec.varargs
75
76
77 def _num_of_matching_terms(basic_list, attack_list):
78     if not attack_list:
79         return len(basic_list)
80
81     matching_terms = 0
82     for i, term in enumerate(basic_list):
83         try:
84             if term != attack_list[i]:
85                 break
86         except IndexError:
87             break
88         matching_terms += 1
89     return matching_terms
90
91
92 def _update_best_match(name_terms, prefix=[]):
93     if prefix:
94         pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
95     else:
96         pref_list = []
97
98     num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
99     global _best_match
100     if not prefix:
101         _best_match = []
102
103     if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
104         if len(_best_match) < num_of_matching_terms:
105             _best_match = name_terms[:num_of_matching_terms]
106         return True
107     return False
108
109
110 def command(cmd_tree, prefix='', descedants_depth=1):
111     """Load a class as a command
112         e.g. spec_cmd0_cmd1 will be command spec cmd0
113
114         :param cmd_tree: is initialized in cmd_spec file and is the structure
115             where commands are loaded. Var name should be _commands
116         :param prefix: if given, load only commands prefixed with prefix,
117         :param descedants_depth: is the depth of the tree descedants of the
118             prefix command. It is used ONLY if prefix and if prefix is not
119             a terminal command
120
121         :returns: the specified class object
122     """
123
124     def wrap(cls):
125         global kloger
126         cls_name = cls.__name__
127
128         if not cmd_tree:
129             if _debug:
130                 kloger.warning('command %s found but not loaded' % cls_name)
131             return cls
132
133         name_terms = cls_name.split('_')
134         if not _update_best_match(name_terms, prefix):
135             if _debug:
136                 kloger.warning('%s failed to update_best_match' % cls_name)
137             return None
138
139         global _best_match
140         max_len = len(_best_match) + descedants_depth
141         if len(name_terms) > max_len:
142             partial = '_'.join(name_terms[:max_len])
143             if not cmd_tree.has_command(partial):  # add partial path
144                 cmd_tree.add_command(partial)
145             if _debug:
146                 kloger.warning('%s failed max_len test' % cls_name)
147             return None
148
149         (
150             cls.description, sep, cls.long_description
151         ) = cls.__doc__.partition('\n')
152         _construct_command_syntax(cls)
153
154         cmd_tree.add_command(cls_name, cls.description, cls)
155         return cls
156     return wrap
157
158
159 cmd_spec_locations = [
160     'kamaki.cli.commands',
161     'kamaki.commands',
162     'kamaki.cli',
163     'kamaki',
164     '']
165
166
167 #  Generic init auxiliary functions
168
169
170 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
171     """handle logging for clients package"""
172
173     if silent:
174         logger.add_stream_logger(__name__, logging.CRITICAL)
175         return
176
177     sfmt, rfmt = '> %(message)s', '< %(message)s'
178     if debug:
179         print('Logging location: %s' % logger.get_log_filename())
180         logger.add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
181         logger.add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
182         logger.add_stream_logger(__name__, logging.DEBUG)
183     elif verbose:
184         logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
185         logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
186         logger.add_stream_logger(__name__, logging.INFO)
187     if include:
188         logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
189         logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
190     logger.add_stream_logger(__name__, logging.WARNING)
191     global kloger
192     kloger = logger.get_logger(__name__)
193
194
195 def _init_session(arguments):
196     global _help
197     _help = arguments['help'].value
198     global _debug
199     _debug = arguments['debug'].value
200     global _include
201     _include = arguments['include'].value
202     global _verbose
203     _verbose = arguments['verbose'].value
204     global _colors
205     _colors = arguments['config'].get('global', 'colors')
206     if not (stdout.isatty() and _colors == 'on'):
207         from kamaki.cli.utils import remove_colors
208         remove_colors()
209     _silent = arguments['silent'].value
210     _setup_logging(_silent, _debug, _verbose, _include)
211
212
213 def _load_spec_module(spec, arguments, module):
214     spec_name = arguments['config'].get(spec, 'cli')
215     if spec_name is None:
216         return None
217     pkg = None
218     for location in cmd_spec_locations:
219         location += spec_name if location == '' else '.%s' % spec_name
220         try:
221             pkg = __import__(location, fromlist=[module])
222             return pkg
223         except ImportError:
224             continue
225     return pkg
226
227
228 def _groups_help(arguments):
229     global _debug
230     global kloger
231     descriptions = {}
232     for spec in arguments['config'].get_groups():
233         pkg = _load_spec_module(spec, arguments, '_commands')
234         if pkg:
235             cmds = None
236             try:
237                 _cnf = arguments['config']
238                 cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
239                     cmd.name, 'cli')]
240             except AttributeError:
241                 if _debug:
242                     kloger.warning('No description for %s' % spec)
243             try:
244                 for cmd in cmds:
245                     descriptions[cmd.name] = cmd.description
246             except TypeError:
247                 if _debug:
248                     kloger.warning('no cmd specs in module %s' % spec)
249         elif _debug:
250             kloger.warning('Loading of %s cmd spec failed' % spec)
251     print('\nOptions:\n - - - -')
252     print_dict(descriptions)
253
254
255 def _load_all_commands(cmd_tree, arguments):
256     _cnf = arguments['config']
257     specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
258     for spec in specs:
259         try:
260             spec_module = _load_spec_module(spec, arguments, '_commands')
261             spec_commands = getattr(spec_module, '_commands')
262         except AttributeError:
263             if _debug:
264                 global kloger
265                 kloger.warning('No valid description for %s' % spec)
266             continue
267         for spec_tree in spec_commands:
268             if spec_tree.name == spec:
269                 cmd_tree.add_tree(spec_tree)
270                 break
271
272
273 #  Methods to be used by CLI implementations
274
275
276 def print_subcommands_help(cmd):
277     printout = {}
278     for subcmd in cmd.get_subcommands():
279         spec, sep, print_path = subcmd.path.partition('_')
280         printout[print_path.replace('_', ' ')] = subcmd.description
281     if printout:
282         print('\nOptions:\n - - - -')
283         print_dict(printout)
284
285
286 def update_parser_help(parser, cmd):
287     global _best_match
288     parser.syntax = parser.syntax.split('<')[0]
289     parser.syntax += ' '.join(_best_match)
290
291     description = ''
292     if cmd.is_command:
293         cls = cmd.get_class()
294         parser.syntax += ' ' + cls.syntax
295         parser.update_arguments(cls().arguments)
296         description = getattr(cls, 'long_description', '')
297         description = description.strip()
298     else:
299         parser.syntax += ' <...>'
300     if cmd.has_description:
301         parser.parser.description = cmd.help + (
302             ('\n%s' % description) if description else '')
303     else:
304         parser.parser.description = description
305
306
307 def print_error_message(cli_err):
308     errmsg = '%s' % cli_err
309     if cli_err.importance == 1:
310         errmsg = magenta(errmsg)
311     elif cli_err.importance == 2:
312         errmsg = yellow(errmsg)
313     elif cli_err.importance > 2:
314         errmsg = red(errmsg)
315     stdout.write(errmsg)
316     for errmsg in cli_err.details:
317         print('| %s' % errmsg)
318
319
320 def exec_cmd(instance, cmd_args, help_method):
321     try:
322         return instance.main(*cmd_args)
323     except TypeError as err:
324         if err.args and err.args[0].startswith('main()'):
325             print(magenta('Syntax error'))
326             if _debug:
327                 raise err
328             if _verbose:
329                 print(unicode(err))
330             help_method()
331         else:
332             raise
333     return 1
334
335
336 def get_command_group(unparsed, arguments):
337     groups = arguments['config'].get_groups()
338     for term in unparsed:
339         if term.startswith('-'):
340             continue
341         if term in groups:
342             unparsed.remove(term)
343             return term
344         return None
345     return None
346
347
348 def set_command_params(parameters):
349     """Add a parameters list to a command
350
351     :param paramters: (list of str) a list of parameters
352     """
353     global command
354     def_params = list(command.func_defaults)
355     def_params[0] = parameters
356     command.func_defaults = tuple(def_params)
357
358
359 #  CLI Choice:
360
361 def run_one_cmd(exe_string, parser):
362     global _history
363     _history = History(
364         parser.arguments['config'].get('history', 'file'))
365     _history.add(' '.join([exe_string] + argv[1:]))
366     from kamaki.cli import one_command
367     one_command.run(parser, _help)
368
369
370 def run_shell(exe_string, parser):
371     from command_shell import _init_shell
372     shell = _init_shell(exe_string, parser)
373     _load_all_commands(shell.cmd_tree, parser.arguments)
374     shell.run(parser)
375
376
377 def main():
378     try:
379         exe = basename(argv[0])
380         parser = ArgumentParseManager(exe)
381
382         if parser.arguments['version'].value:
383             exit(0)
384
385         log_file = parser.arguments['config'].get('global', 'log_file')
386         if log_file:
387             logger.set_log_filename(log_file)
388         global filelog
389         filelog = logger.add_file_logger(__name__.split('.')[0])
390         filelog.info('Log call: %s' % ' '.join(argv))
391
392         _init_session(parser.arguments)
393
394         from kamaki.cli.utils import suggest_missing
395         suggest_missing()
396
397         if parser.unparsed:
398             run_one_cmd(exe, parser)
399         elif _help:
400             parser.parser.print_help()
401             _groups_help(parser.arguments)
402         else:
403             run_shell(exe, parser)
404     except CLIError as err:
405         print_error_message(err)
406         if _debug:
407             raise err
408         exit(1)
409     except Exception as er:
410         print('Unknown Error: %s' % er)
411         if _debug:
412             raise
413         exit(1)