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
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
43 from kamaki.cli import logger
53 # command auxiliary methods
60 '____', '[:').replace(
66 def _construct_command_syntax(cls):
67 spec = getargspec(cls.main.im_func)
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)
74 cls.syntax += ' <%s ...>' % spec.varargs
77 def _num_of_matching_terms(basic_list, attack_list):
79 return len(basic_list)
82 for i, term in enumerate(basic_list):
84 if term != attack_list[i]:
92 def _update_best_match(name_terms, prefix=[]):
94 pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
98 num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
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]
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
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
121 :returns: the specified class object
126 cls_name = cls.__name__
130 kloger.warning('command %s found but not loaded' % cls_name)
133 name_terms = cls_name.split('_')
134 if not _update_best_match(name_terms, prefix):
136 kloger.warning('%s failed to update_best_match' % cls_name)
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)
146 kloger.warning('%s failed max_len test' % cls_name)
150 cls.description, sep, cls.long_description
151 ) = cls.__doc__.partition('\n')
152 _construct_command_syntax(cls)
154 cmd_tree.add_command(cls_name, cls.description, cls)
159 cmd_spec_locations = [
160 'kamaki.cli.commands',
167 # Generic init auxiliary functions
170 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
171 """handle logging for clients package"""
174 logger.add_stream_logger(__name__, logging.CRITICAL)
177 sfmt, rfmt = '> %(message)s', '< %(message)s'
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)
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)
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)
192 kloger = logger.get_logger(__name__)
195 def _check_config_version(cnf):
196 guess = cnf.guess_version()
197 if exists(cnf.path) and guess < 0.9:
198 print('Config file format version >= 9.0 is required')
199 print('Configuration file: %s' % cnf.path)
200 print('Attempting to fix this:')
201 print('Calculating changes while preserving information')
202 lost_terms = cnf.rescue_old_file()
205 print 'The following information will NOT be preserved:'
206 print '\t', '\n\t'.join(lost_terms)
207 print('Kamaki is ready to convert the config file')
208 stdout.write('Create (overwrite) file %s ? [y/N] ' % cnf.path)
209 from sys import stdin
210 reply = stdin.readline()
211 if reply in ('Y\n', 'y\n'):
215 print('... ABORTING')
217 'Invalid format for config file %s' % cnf.path,
218 importance=3, details=[
219 'Please, update config file',
220 'For automatic conversion, rerun and say Y'])
223 def _init_session(arguments, is_non_API=False):
225 :returns: (AuthCachedClient, str) authenticator and cloud name
228 _help = arguments['help'].value
230 _debug = arguments['debug'].value
232 _include = arguments['include'].value
234 _verbose = arguments['verbose'].value
235 _cnf = arguments['config']
237 if _help or is_non_API:
240 _check_config_version(_cnf.value)
243 _colors = _cnf.value.get_global('colors')
244 if not (stdout.isatty() and _colors == 'on'):
245 from kamaki.cli.utils import remove_colors
247 _silent = arguments['silent'].value
248 _setup_logging(_silent, _debug, _verbose, _include)
250 cloud = arguments['cloud'].value or _cnf.value.get(
251 'global', 'default_cloud')
253 num_of_clouds = len(_cnf.value.keys('cloud'))
254 if num_of_clouds == 1:
255 cloud = _cnf.value.keys('cloud')[0]
256 elif num_of_clouds > 1:
258 'Found %s clouds but none of them is set as default' % (
260 importance=2, details=[
261 'Please, choose one of the following cloud names:',
262 ', '.join(_cnf.value.keys('cloud')),
263 'To see all cloud settings:',
264 ' kamaki config get cloud.<cloud name>',
265 'To set a default cloud:',
266 ' kamaki config set default_cloud <cloud name>',
267 'To pick a cloud for the current session, use --cloud:',
268 ' kamaki --cloud=<cloud name> ...'])
269 if not cloud in _cnf.value.keys('cloud'):
271 'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''),
272 importance=3, details=[
273 'To configure a new cloud "%s", find and set the' % (
274 cloud or '<cloud name>'),
275 'single authentication URL and token:',
276 ' kamaki config set cloud.%s.url <URL>' % (
277 cloud or '<cloud name>'),
278 ' kamaki config set cloud.%s.token <t0k3n>' % (
279 cloud or '<cloud name>')])
281 for term in ('url', 'token'):
283 auth_args[term] = _cnf.get_cloud(cloud, term)
286 if not auth_args[term]:
288 'No authentication %s provided for cloud "%s"' % (
289 term.upper(), cloud),
290 importance=3, details=[
291 'Set a %s for cloud %s:' % (term.upper(), cloud),
292 ' kamaki config set cloud.%s.%s <%s>' % (
293 cloud, term, term.upper())])
295 from kamaki.clients.astakos import AstakosClient as AuthCachedClient
297 return AuthCachedClient(auth_args['url'], auth_args['token']), cloud
298 except AssertionError as ae:
299 kloger.warning('WARNING: Failed to load authenticator [%s]' % ae)
303 def _load_spec_module(spec, arguments, module):
307 for location in cmd_spec_locations:
308 location += spec if location == '' else '.%s' % spec
310 pkg = __import__(location, fromlist=[module])
312 except ImportError as ie:
315 kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
319 def _groups_help(arguments):
323 for cmd_group, spec in arguments['config'].get_cli_specs():
324 pkg = _load_spec_module(spec, arguments, '_commands')
326 cmds = getattr(pkg, '_commands')
329 descriptions[cmd.name] = cmd.description
333 'No cmd description for module %s' % cmd_group)
335 kloger.warning('Loading of %s cmd spec failed' % cmd_group)
336 print('\nOptions:\n - - - -')
337 print_dict(descriptions)
340 def _load_all_commands(cmd_tree, arguments):
341 _cnf = arguments['config']
342 for cmd_group, spec in _cnf.get_cli_specs():
344 spec_module = _load_spec_module(spec, arguments, '_commands')
345 spec_commands = getattr(spec_module, '_commands')
346 except AttributeError:
349 kloger.warning('No valid description for %s' % cmd_group)
351 for spec_tree in spec_commands:
352 if spec_tree.name == cmd_group:
353 cmd_tree.add_tree(spec_tree)
357 # Methods to be used by CLI implementations
360 def print_subcommands_help(cmd):
362 for subcmd in cmd.get_subcommands():
363 spec, sep, print_path = subcmd.path.partition('_')
364 printout[print_path.replace('_', ' ')] = subcmd.description
366 print('\nOptions:\n - - - -')
370 def update_parser_help(parser, cmd):
372 parser.syntax = parser.syntax.split('<')[0]
373 parser.syntax += ' '.join(_best_match)
377 cls = cmd.get_class()
378 parser.syntax += ' ' + cls.syntax
379 parser.update_arguments(cls().arguments)
380 description = getattr(cls, 'long_description', '')
381 description = description.strip()
383 parser.syntax += ' <...>'
384 if cmd.has_description:
385 parser.parser.description = cmd.help + (
386 ('\n%s' % description) if description else '')
388 parser.parser.description = description
391 def print_error_message(cli_err):
392 errmsg = '%s' % cli_err
393 if cli_err.importance == 1:
394 errmsg = magenta(errmsg)
395 elif cli_err.importance == 2:
396 errmsg = yellow(errmsg)
397 elif cli_err.importance > 2:
400 for errmsg in cli_err.details:
401 print('| %s' % errmsg)
404 def exec_cmd(instance, cmd_args, help_method):
406 return instance.main(*cmd_args)
407 except TypeError as err:
408 if err.args and err.args[0].startswith('main()'):
409 print(magenta('Syntax error'))
420 def get_command_group(unparsed, arguments):
421 groups = arguments['config'].get_groups()
422 for term in unparsed:
423 if term.startswith('-'):
426 unparsed.remove(term)
432 def set_command_params(parameters):
433 """Add a parameters list to a command
435 :param paramters: (list of str) a list of parameters
438 def_params = list(command.func_defaults)
439 def_params[0] = parameters
440 command.func_defaults = tuple(def_params)
445 def run_one_cmd(exe_string, parser, auth_base, cloud):
448 parser.arguments['config'].get_global('history_file'))
449 _history.add(' '.join([exe_string] + argv[1:]))
450 from kamaki.cli import one_command
451 one_command.run(auth_base, cloud, parser, _help)
454 def run_shell(exe_string, parser, auth_base, cloud):
455 from command_shell import _init_shell
456 shell = _init_shell(exe_string, parser)
457 _load_all_commands(shell.cmd_tree, parser.arguments)
458 shell.run(auth_base, cloud, parser)
461 def is_non_API(parser):
462 nonAPIs = ('history', 'config')
463 for term in parser.unparsed:
464 if not term.startswith('-'):
473 exe = basename(argv[0])
474 parser = ArgumentParseManager(exe)
476 if parser.arguments['version'].value:
479 log_file = parser.arguments['config'].get_global('log_file')
481 logger.set_log_filename(log_file)
483 filelog = logger.add_file_logger(__name__.split('.')[0])
484 filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
486 auth_base, cloud = _init_session(parser.arguments, is_non_API(parser))
488 from kamaki.cli.utils import suggest_missing
490 exclude = ['ansicolors'] if not _colors == 'on' else []
491 suggest_missing(exclude=exclude)
494 run_one_cmd(exe, parser, auth_base, cloud)
496 parser.parser.print_help()
497 _groups_help(parser.arguments)
499 run_shell(exe, parser, auth_base, cloud)
500 except CLIError as err:
501 print_error_message(err)
505 except Exception as er:
506 print('Unknown Error: %s' % er)