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