Implement --cloud arg to switch between clouds
[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     _cnf = arguments['config']
205     global _colors
206     _colors = _cnf.get('global', 'colors')
207     if not (stdout.isatty() and _colors == 'on'):
208         from kamaki.cli.utils import remove_colors
209         remove_colors()
210     _silent = arguments['silent'].value
211     _setup_logging(_silent, _debug, _verbose, _include)
212     picked_cloud = arguments['cloud'].value
213     if picked_cloud:
214         global_url = _cnf.get('remotes', picked_cloud)
215         if not global_url:
216             raise CLIError(
217                 'No remote cloud "%s" in kamaki configuration' % picked_cloud,
218                 importance=3, details=[
219                     'To check if this remote cloud alias is declared:',
220                     '  /config get remotes.%s' % picked_cloud,
221                     'To set a remote authentication URI aliased as "%s"' % (
222                         picked_cloud),
223                     '  /config set remotes.%s <URI>' % picked_cloud
224                 ])
225     else:
226         global_url = _cnf.get('global', 'auth_url')
227     global_token = _cnf.get('global', 'token')
228     from kamaki.clients.astakos import AstakosClient as AuthCachedClient
229     return AuthCachedClient(global_url, global_token)
230
231
232 def _load_spec_module(spec, arguments, module):
233     #spec_name = arguments['config'].get('cli', spec)
234     if not spec:
235         return None
236     pkg = None
237     for location in cmd_spec_locations:
238         location += spec if location == '' else '.%s' % spec
239         try:
240             pkg = __import__(location, fromlist=[module])
241             return pkg
242         except ImportError as ie:
243             continue
244     if not pkg:
245         kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
246     return pkg
247
248
249 def _groups_help(arguments):
250     global _debug
251     global kloger
252     descriptions = {}
253     for cmd_group, spec in arguments['config'].get_cli_specs():
254         pkg = _load_spec_module(spec, arguments, '_commands')
255         if pkg:
256             cmds = getattr(pkg, '_commands')
257             #try:
258             #   #_cnf = arguments['config']
259             #   #cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
260             #   #    'cli', cmd.name)]
261             #except AttributeError:
262             #   if _debug:
263             #       kloger.warning('No description for %s' % cmd_group)
264             try:
265                 for cmd in cmds:
266                     descriptions[cmd.name] = cmd.description
267             except TypeError:
268                 if _debug:
269                     kloger.warning(
270                         'No cmd description for module %s' % cmd_group)
271         elif _debug:
272             kloger.warning('Loading of %s cmd spec failed' % cmd_group)
273     print('\nOptions:\n - - - -')
274     print_dict(descriptions)
275
276
277 def _load_all_commands(cmd_tree, arguments):
278     _cnf = arguments['config']
279     #specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
280     for cmd_group, spec in _cnf.get_cli_specs():
281         try:
282             spec_module = _load_spec_module(spec, arguments, '_commands')
283             spec_commands = getattr(spec_module, '_commands')
284         except AttributeError:
285             if _debug:
286                 global kloger
287                 kloger.warning('No valid description for %s' % cmd_group)
288             continue
289         for spec_tree in spec_commands:
290             if spec_tree.name == cmd_group:
291                 cmd_tree.add_tree(spec_tree)
292                 break
293
294
295 #  Methods to be used by CLI implementations
296
297
298 def print_subcommands_help(cmd):
299     printout = {}
300     for subcmd in cmd.get_subcommands():
301         spec, sep, print_path = subcmd.path.partition('_')
302         printout[print_path.replace('_', ' ')] = subcmd.description
303     if printout:
304         print('\nOptions:\n - - - -')
305         print_dict(printout)
306
307
308 def update_parser_help(parser, cmd):
309     global _best_match
310     parser.syntax = parser.syntax.split('<')[0]
311     parser.syntax += ' '.join(_best_match)
312
313     description = ''
314     if cmd.is_command:
315         cls = cmd.get_class()
316         parser.syntax += ' ' + cls.syntax
317         parser.update_arguments(cls().arguments)
318         description = getattr(cls, 'long_description', '')
319         description = description.strip()
320     else:
321         parser.syntax += ' <...>'
322     if cmd.has_description:
323         parser.parser.description = cmd.help + (
324             ('\n%s' % description) if description else '')
325     else:
326         parser.parser.description = description
327
328
329 def print_error_message(cli_err):
330     errmsg = '%s' % cli_err
331     if cli_err.importance == 1:
332         errmsg = magenta(errmsg)
333     elif cli_err.importance == 2:
334         errmsg = yellow(errmsg)
335     elif cli_err.importance > 2:
336         errmsg = red(errmsg)
337     stdout.write(errmsg)
338     for errmsg in cli_err.details:
339         print('|  %s' % errmsg)
340
341
342 def exec_cmd(instance, cmd_args, help_method):
343     try:
344         return instance.main(*cmd_args)
345     except TypeError as err:
346         if err.args and err.args[0].startswith('main()'):
347             print(magenta('Syntax error'))
348             if _debug:
349                 raise err
350             if _verbose:
351                 print(unicode(err))
352             help_method()
353         else:
354             raise
355     return 1
356
357
358 def get_command_group(unparsed, arguments):
359     groups = arguments['config'].get_groups()
360     for term in unparsed:
361         if term.startswith('-'):
362             continue
363         if term in groups:
364             unparsed.remove(term)
365             return term
366         return None
367     return None
368
369
370 def set_command_params(parameters):
371     """Add a parameters list to a command
372
373     :param paramters: (list of str) a list of parameters
374     """
375     global command
376     def_params = list(command.func_defaults)
377     def_params[0] = parameters
378     command.func_defaults = tuple(def_params)
379
380
381 #  CLI Choice:
382
383 def run_one_cmd(exe_string, parser, auth_base):
384     global _history
385     _history = History(
386         parser.arguments['config'].get('history', 'file'))
387     _history.add(' '.join([exe_string] + argv[1:]))
388     from kamaki.cli import one_command
389     one_command.run(auth_base, parser, _help)
390
391
392 def run_shell(exe_string, parser, auth_base):
393     from command_shell import _init_shell
394     shell = _init_shell(exe_string, parser)
395     _load_all_commands(shell.cmd_tree, parser.arguments)
396     shell.run(auth_base, parser)
397
398
399 def main():
400     try:
401         exe = basename(argv[0])
402         parser = ArgumentParseManager(exe)
403
404         if parser.arguments['version'].value:
405             exit(0)
406
407         log_file = parser.arguments['config'].get('global', 'log_file')
408         if log_file:
409             logger.set_log_filename(log_file)
410         global filelog
411         filelog = logger.add_file_logger(__name__.split('.')[0])
412         filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
413
414         auth_base = _init_session(parser.arguments)
415
416         from kamaki.cli.utils import suggest_missing
417         suggest_missing()
418
419         if parser.unparsed:
420             run_one_cmd(exe, parser, auth_base)
421         elif _help:
422             parser.parser.print_help()
423             _groups_help(parser.arguments)
424         else:
425             run_shell(exe, parser, auth_base)
426     except CLIError as err:
427         print_error_message(err)
428         if _debug:
429             raise err
430         exit(1)
431     except Exception as er:
432         print('Unknown Error: %s' % er)
433         if _debug:
434             raise
435         exit(1)