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 AstakosClient as AuthCachedClient
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(x for x in [required, optional] if x)
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.9:
201 print('Config file format version >= 9.0 is required')
202 print('Configuration file: %s' % cnf.path)
203 print('Attempting to fix this:')
204 print('Calculating changes while preserving information')
205 lost_terms = cnf.rescue_old_file()
208 print 'The following information will NOT be preserved:'
209 print '\t', '\n\t'.join(lost_terms)
210 print('Kamaki is ready to convert the config file')
211 stdout.write('Create (overwrite) file %s ? [y/N] ' % cnf.path)
212 from sys import stdin
213 reply = stdin.readline()
214 if reply in ('Y\n', 'y\n'):
218 print('... ABORTING')
220 'Invalid format for config file %s' % cnf.path,
221 importance=3, details=[
222 'Please, update config file',
223 'For automatic conversion, rerun and say Y'])
226 def _init_session(arguments, is_non_API=False):
231 _help = arguments['help'].value
233 _debug = arguments['debug'].value
235 _verbose = arguments['verbose'].value
236 _cnf = arguments['config']
238 _silent = arguments['silent'].value
239 _setup_logging(_silent, _debug, _verbose)
241 if _help or is_non_API:
244 _check_config_version(_cnf.value)
247 _colors = _cnf.value.get('global', 'colors')
248 if not (stdout.isatty() and _colors == 'on'):
249 from kamaki.cli.utils import remove_colors
252 cloud = arguments['cloud'].value or _cnf.value.get(
253 'global', 'default_cloud')
255 num_of_clouds = len(_cnf.value.keys('cloud'))
256 if num_of_clouds == 1:
257 cloud = _cnf.value.keys('cloud')[0]
258 elif num_of_clouds > 1:
260 'Found %s clouds but none of them is set as default' % (
262 importance=2, details=[
263 'Please, choose one of the following cloud names:',
264 ', '.join(_cnf.value.keys('cloud')),
265 'To see all cloud settings:',
266 ' kamaki config get cloud.<cloud name>',
267 'To set a default cloud:',
268 ' kamaki config set default_cloud <cloud name>',
269 'To pick a cloud for the current session, use --cloud:',
270 ' kamaki --cloud=<cloud name> ...'])
271 if not cloud in _cnf.value.keys('cloud'):
273 'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''),
274 importance=3, details=[
275 'To configure a new cloud "%s", find and set the' % (
276 cloud or '<cloud name>'),
277 'single authentication URL and token:',
278 ' kamaki config set cloud.%s.url <URL>' % (
279 cloud or '<cloud name>'),
280 ' kamaki config set cloud.%s.token <t0k3n>' % (
281 cloud or '<cloud name>')])
283 for term in ('url', 'token'):
285 auth_args[term] = _cnf.get_cloud(cloud, term)
286 except KeyError or IndexError:
288 if not auth_args[term]:
290 'No authentication %s provided for cloud "%s"' % (
291 term.upper(), cloud),
292 importance=3, details=[
293 'Set a %s for cloud %s:' % (term.upper(), cloud),
294 ' kamaki config set cloud.%s.%s <%s>' % (
295 cloud, term, term.upper())])
299 def init_cached_authenticator(config_argument, cloud, logger):
301 _cnf = config_argument.value
302 url = _cnf.get_cloud(cloud, 'url')
303 tokens = _cnf.get_cloud(cloud, 'token').split()
304 auth_base, failed = None, []
308 auth_base.authenticate(token)
310 tmp_base = AuthCachedClient(url, token)
311 from kamaki.cli.commands import _command_init
312 fake_cmd = _command_init(dict(config=config_argument))
313 fake_cmd.client = auth_base
314 fake_cmd._set_log_params()
315 fake_cmd._update_max_threads()
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 run_one_cmd(exe_string, parser, cloud):
492 token = parser.arguments['config'].get_cloud(cloud, 'token').split()[0]
496 parser.arguments['config'].get('global', 'history_file'), token=token)
497 _history.add(' '.join([exe_string] + argv[1:]))
498 from kamaki.cli import one_command
499 one_command.run(cloud, parser, _help)
502 def run_shell(exe_string, parser, cloud):
503 from command_shell import _init_shell
505 _cnf = parser.arguments['config']
506 auth_base = init_cached_authenticator(_cnf, cloud, kloger)
509 auth_base.user_term('name'), auth_base.user_term('id'))
511 username, userid = '', ''
512 shell = _init_shell(exe_string, parser, username, userid)
513 _load_all_commands(shell.cmd_tree, parser.arguments)
514 shell.run(auth_base, cloud, parser)
517 def is_non_API(parser):
518 nonAPIs = ('history', 'config')
519 for term in parser.unparsed:
520 if not term.startswith('-'):
529 exe = basename(argv[0])
530 parser = ArgumentParseManager(exe)
532 if parser.arguments['version'].value:
535 _cnf = parser.arguments['config']
536 log_file = _cnf.get('global', 'log_file')
538 logger.set_log_filename(log_file)
540 filelog = logger.add_file_logger(__name__.split('.')[0])
541 filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
543 cloud = _init_session(parser.arguments, is_non_API(parser))
544 from kamaki.cli.utils import suggest_missing
546 exclude = ['ansicolors'] if not _colors == 'on' else []
547 suggest_missing(exclude=exclude)
550 run_one_cmd(exe, parser, cloud)
552 parser.parser.print_help()
553 _groups_help(parser.arguments)
555 run_shell(exe, parser, cloud)
556 except CLIError as err:
557 print_error_message(err)
561 except Exception as er:
562 print('Unknown Error: %s' % er)