1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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
35 from sys import argv, exit, stdout, stderr
36 from os.path import basename, exists
37 from inspect import getargspec
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, CLICmdSpecError
43 from kamaki.cli import logger
44 from kamaki.clients.astakos import CachedAstakosClient
45 from kamaki.clients import ClientError
54 # command auxiliary methods
61 '____', '[:').replace(
67 def _construct_command_syntax(cls):
68 spec = getargspec(cls.main.im_func)
70 n = len(args) - len(spec.defaults or ())
71 required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
72 optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
73 cls.syntax = ' '.join([required, optional])
75 cls.syntax += ' <%s ...>' % spec.varargs
78 def _num_of_matching_terms(basic_list, attack_list):
80 return len(basic_list)
83 for i, term in enumerate(basic_list):
85 if term != attack_list[i]:
93 def _update_best_match(name_terms, prefix=[]):
95 pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
99 num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
104 if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
105 if len(_best_match) < num_of_matching_terms:
106 _best_match = name_terms[:num_of_matching_terms]
111 def command(cmd_tree, prefix='', descedants_depth=1):
112 """Load a class as a command
113 e.g., spec_cmd0_cmd1 will be command spec cmd0
115 :param cmd_tree: is initialized in cmd_spec file and is the structure
116 where commands are loaded. Var name should be _commands
117 :param prefix: if given, load only commands prefixed with prefix,
118 :param descedants_depth: is the depth of the tree descedants of the
119 prefix command. It is used ONLY if prefix and if prefix is not
122 :returns: the specified class object
127 cls_name = cls.__name__
131 kloger.warning('command %s found but not loaded' % cls_name)
134 name_terms = cls_name.split('_')
135 if not _update_best_match(name_terms, prefix):
137 kloger.warning('%s failed to update_best_match' % cls_name)
141 max_len = len(_best_match) + descedants_depth
142 if len(name_terms) > max_len:
143 partial = '_'.join(name_terms[:max_len])
144 if not cmd_tree.has_command(partial): # add partial path
145 cmd_tree.add_command(partial)
147 kloger.warning('%s failed max_len test' % cls_name)
152 cls.description, sep, cls.long_description
153 ) = cls.__doc__.partition('\n')
154 except AttributeError:
155 raise CLICmdSpecError(
156 'No commend in %s (acts as cmd description)' % cls.__name__)
157 _construct_command_syntax(cls)
159 cmd_tree.add_command(
160 cls_name, cls.description, cls, cls.long_description)
165 cmd_spec_locations = [
166 'kamaki.cli.commands',
173 # Generic init auxiliary functions
176 def _setup_logging(silent=False, debug=False, verbose=False):
177 """handle logging for clients package"""
180 logger.add_stream_logger(__name__, logging.CRITICAL)
183 sfmt, rfmt = '> %(message)s', '< %(message)s'
185 print('Logging location: %s' % logger.get_log_filename())
186 logger.add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
187 logger.add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
188 logger.add_stream_logger(__name__, logging.DEBUG)
190 logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
191 logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
192 logger.add_stream_logger(__name__, logging.INFO)
193 logger.add_stream_logger(__name__, logging.WARNING)
195 kloger = logger.get_logger(__name__)
198 def _check_config_version(cnf):
199 guess = cnf.guess_version()
200 if exists(cnf.path) and guess < 0.12:
201 print('Config file format version >= 0.12 is required (%s found)' % (
203 print('Configuration file: %s' % cnf.path)
204 print('Attempting to fix this:')
205 print('Calculating changes while preserving information')
206 lost_terms = cnf.rescue_old_file()
209 print 'The following information will NOT be preserved:'
210 print '\t', '\n\t'.join(lost_terms)
211 print('Kamaki is ready to convert the config file')
212 stdout.write('Create (overwrite) file %s ? [y/N] ' % cnf.path)
213 from sys import stdin
214 reply = stdin.readline()
215 if reply in ('Y\n', 'y\n'):
219 print('... ABORTING')
221 'Invalid format for config file %s' % cnf.path,
222 importance=3, details=[
223 'Please, update config file',
224 'For automatic conversion, rerun and say Y'])
227 def _init_session(arguments, is_non_API=False):
232 _help = arguments['help'].value
234 _debug = arguments['debug'].value
236 _verbose = arguments['verbose'].value
237 _cnf = arguments['config']
239 _silent = arguments['silent'].value
240 _setup_logging(_silent, _debug, _verbose)
242 if _help or is_non_API:
245 _check_config_version(_cnf.value)
248 _colors = _cnf.value.get('global', 'colors')
249 if not (stdout.isatty() and _colors == 'on'):
250 from kamaki.cli.utils import remove_colors
253 cloud = arguments['cloud'].value or _cnf.value.get(
254 'global', 'default_cloud')
256 num_of_clouds = len(_cnf.value.keys('cloud'))
257 if num_of_clouds == 1:
258 cloud = _cnf.value.keys('cloud')[0]
259 elif num_of_clouds > 1:
261 'Found %s clouds but none of them is set as default' % (
263 importance=2, details=[
264 'Please, choose one of the following cloud names:',
265 ', '.join(_cnf.value.keys('cloud')),
266 'To see all cloud settings:',
267 ' kamaki config get cloud.<cloud name>',
268 'To set a default cloud:',
269 ' kamaki config set default_cloud <cloud name>',
270 'To pick a cloud for the current session, use --cloud:',
271 ' kamaki --cloud=<cloud name> ...'])
272 if not cloud in _cnf.value.keys('cloud'):
274 'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''),
275 importance=3, details=[
276 'To configure a new cloud "%s", find and set the' % (
277 cloud or '<cloud name>'),
278 'single authentication URL and token:',
279 ' kamaki config set cloud.%s.url <URL>' % (
280 cloud or '<cloud name>'),
281 ' kamaki config set cloud.%s.token <t0k3n>' % (
282 cloud or '<cloud name>')])
284 for term in ('url', 'token'):
286 auth_args[term] = _cnf.get_cloud(cloud, term)
287 except KeyError or IndexError:
289 if not auth_args[term]:
291 'No authentication %s provided for cloud "%s"' % (
292 term.upper(), cloud),
293 importance=3, details=[
294 'Set a %s for cloud %s:' % (term.upper(), cloud),
295 ' kamaki config set cloud.%s.%s <%s>' % (
296 cloud, term, term.upper())])
300 def init_cached_authenticator(config_argument, cloud, logger):
302 _cnf = config_argument.value
303 url = _cnf.get_cloud(cloud, 'url')
304 tokens = _cnf.get_cloud(cloud, 'token').split()
305 auth_base, failed = None, []
309 auth_base.authenticate(token)
311 tmp_base = CachedAstakosClient(url, token)
312 from kamaki.cli.commands import _command_init
313 fake_cmd = _command_init(dict(config=config_argument))
314 fake_cmd.client = auth_base
315 fake_cmd._set_log_params()
316 tmp_base.authenticate(token)
318 except ClientError as ce:
319 if ce.status in (401, ):
321 'WARNING: Failed to authenticate token %s' % token)
327 'Token %s failed to authenticate. Remove it? [y/N]: ' % token)
330 if set(failed).difference(tokens):
331 _cnf.set_cloud(cloud, 'token', ' '.join(tokens))
335 logger.warning('WARNING: cloud.%s.token is now empty' % cloud)
336 except AssertionError as ae:
337 logger.warning('WARNING: Failed to load authenticator [%s]' % ae)
341 def _load_spec_module(spec, arguments, module):
346 for location in cmd_spec_locations:
347 location += spec if location == '' else '.%s' % spec
349 kloger.debug('Import %s from %s' % ([module], location))
350 pkg = __import__(location, fromlist=[module])
351 kloger.debug('\t...OK')
353 except ImportError as ie:
354 kloger.debug('\t...Failed')
357 msg = 'Loading command group %s failed: %s' % (spec, ie)
358 msg += '\nHINT: use a text editor to remove all global.*_cli'
359 msg += '\n\tsettings from the configuration file'
364 def _groups_help(arguments):
368 acceptable_groups = arguments['config'].groups
369 for cmd_group, spec in arguments['config'].cli_specs:
370 pkg = _load_spec_module(spec, arguments, '_commands')
372 cmds = getattr(pkg, '_commands')
374 for cmd_tree in cmds:
375 if cmd_tree.name in acceptable_groups:
376 descriptions[cmd_tree.name] = cmd_tree.description
380 'No cmd description (help) for module %s' % cmd_group)
382 kloger.warning('Loading of %s cmd spec failed' % cmd_group)
383 print('\nOptions:\n - - - -')
384 print_dict(descriptions)
387 def _load_all_commands(cmd_tree, arguments):
388 _cnf = arguments['config']
389 for cmd_group, spec in _cnf.cli_specs:
391 spec_module = _load_spec_module(spec, arguments, '_commands')
392 spec_commands = getattr(spec_module, '_commands')
393 except AttributeError:
396 kloger.warning('No valid description for %s' % cmd_group)
398 for spec_tree in spec_commands:
399 if spec_tree.name == cmd_group:
400 cmd_tree.add_tree(spec_tree)
404 # Methods to be used by CLI implementations
407 def print_subcommands_help(cmd):
409 for subcmd in cmd.subcommands.values():
410 spec, sep, print_path = subcmd.path.partition('_')
411 printout[print_path.replace('_', ' ')] = subcmd.help
413 print('\nOptions:\n - - - -')
417 def update_parser_help(parser, cmd):
419 parser.syntax = parser.syntax.split('<')[0]
420 parser.syntax += ' '.join(_best_match)
425 parser.syntax += ' ' + cls.syntax
426 parser.update_arguments(cls().arguments)
427 description = getattr(cls, 'long_description', '').strip()
429 parser.syntax += ' <...>'
430 parser.parser.description = (
431 cmd.help + ('\n' if description else '')) if cmd.help else description
434 def print_error_message(cli_err, out=stderr):
435 errmsg = '%s' % cli_err
436 if cli_err.importance == 1:
437 errmsg = magenta(errmsg)
438 elif cli_err.importance == 2:
439 errmsg = yellow(errmsg)
440 elif cli_err.importance > 2:
443 for errmsg in cli_err.details:
444 out.write('| %s\n' % errmsg)
448 def exec_cmd(instance, cmd_args, help_method):
450 return instance.main(*cmd_args)
451 except TypeError as err:
452 if err.args and err.args[0].startswith('main()'):
453 print(magenta('Syntax error'))
464 def get_command_group(unparsed, arguments):
465 groups = arguments['config'].groups
466 for term in unparsed:
467 if term.startswith('-'):
470 unparsed.remove(term)
476 def set_command_params(parameters):
477 """Add a parameters list to a command
479 :param paramters: (list of str) a list of parameters
482 def_params = list(command.func_defaults)
483 def_params[0] = parameters
484 command.func_defaults = tuple(def_params)
489 def is_non_API(parser):
490 nonAPIs = ('history', 'config')
491 for term in parser.unparsed:
492 if not term.startswith('-'):
502 exe = basename(argv[0])
503 parser = ArgumentParseManager(exe)
505 if parser.arguments['version'].value:
508 _cnf = parser.arguments['config']
509 log_file = _cnf.get('global', 'log_file')
511 logger.set_log_filename(log_file)
513 filelog = logger.add_file_logger(__name__.split('.')[0])
514 filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
516 from kamaki.cli.utils import suggest_missing
518 exclude = ['ansicolors'] if not _colors == 'on' else []
519 suggest_missing(exclude=exclude)
521 except CLIError as err:
522 print_error_message(err)
526 except KeyboardInterrupt:
527 print('Canceled by user')
529 except Exception as er:
530 print('Unknown Error: %s' % er)
538 def run_shell(exe, parser):
539 parser.arguments['help'].value = False
540 cloud = _init_session(parser.arguments)
541 from command_shell import _init_shell
543 _cnf = parser.arguments['config']
544 auth_base = init_cached_authenticator(_cnf, cloud, kloger)
547 auth_base.user_term('name'), auth_base.user_term('id'))
549 username, userid = '', ''
550 shell = _init_shell(exe, parser, username, userid)
551 _load_all_commands(shell.cmd_tree, parser.arguments)
552 shell.run(auth_base, cloud, parser)
556 def run_one_cmd(exe, parser):
557 cloud = _init_session(parser.arguments, is_non_API(parser))
561 token = parser.arguments['config'].get_cloud(
562 cloud, 'token').split()[0]
566 parser.arguments['config'].get('global', 'history_file'),
568 _history.add(' '.join([exe] + argv[1:]))
569 from kamaki.cli import one_command
570 one_command.run(cloud, parser, _help)
573 _groups_help(parser.arguments)
574 print('kamaki-shell: An interactive command line shell')