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
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()
198 print('Config file format version >= 9.0 is required')
199 print('Configuration file "%s" format is not up to date' % (
201 print('but kamaki can fix this:')
202 print('Calculating changes while preserving information')
203 lost_terms = cnf.rescue_old_file()
206 print 'The following information will NOT be preserved:'
207 print '\t', '\n\t'.join(lost_terms)
208 print('Kamaki is ready to convert the config file to version 3.0')
209 stdout.write('Create (overwrite) file %s ? [y/N] ' % cnf.path)
210 from sys import stdin
211 reply = stdin.readline()
212 if reply in ('Y\n', 'y\n'):
216 print('... ABORTING')
218 'Invalid format for config file %s' % cnf.path,
219 importance=3, details=[
220 'Please, update config file to v3.0',
221 'For automatic conversion, rerun and say Y'])
224 def _init_session(arguments, is_non_API=False):
226 :returns: (AuthCachedClient, str) authenticator and cloud name
229 _help = arguments['help'].value
231 _debug = arguments['debug'].value
233 _include = arguments['include'].value
235 _verbose = arguments['verbose'].value
236 _cnf = arguments['config']
237 _check_config_version(_cnf.value)
240 _colors = _cnf.value.get_global('colors')
241 if not (stdout.isatty() and _colors == 'on'):
242 from kamaki.cli.utils import remove_colors
244 _silent = arguments['silent'].value
245 _setup_logging(_silent, _debug, _verbose, _include)
247 if _help or is_non_API:
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]
258 'Found %s clouds but none of them is set as default',
259 importance=2, details=[
260 'Please, choose one of the following cloud names:',
261 ', '.join(_cnf.value.keys('cloud')),
262 'To set a default cloud:',
263 ' kamaki config set default_cloud <cloud name>',
264 'To pick a cloud for the current session, use --cloud:',
265 ' kamaki --cloud=<cloud name> ...'])
266 if not cloud in _cnf.value.keys('cloud'):
268 'No cloud "%s" is configured' % cloud,
269 importance=3, details=[
270 'To configure a new cloud, find and set the',
271 'single authentication URL and token:',
272 ' kamaki config set cloud.%s.url <URL>' % cloud,
273 ' kamaki config set cloud.%s.token <t0k3n>' % cloud])
275 for term in ('url', 'token'):
277 auth_args[term] = _cnf.get_cloud(cloud, term)
280 if not auth_args[term]:
282 'No authentication %s provided for cloud "%s"' % (term, cloud),
283 importance=3, details=[
284 'Set a %s for cloud %s:' % (term, cloud),
285 ' kamaki config set cloud.%s.%s <t0k3n>' % (cloud, term)])
287 from kamaki.clients.astakos import AstakosClient as AuthCachedClient
289 return AuthCachedClient(auth_args['url'], auth_args['token']), cloud
290 except AssertionError as ae:
291 kloger.warning('WARNING: Failed to load authenticator [%s]' % ae)
295 def _load_spec_module(spec, arguments, module):
299 for location in cmd_spec_locations:
300 location += spec if location == '' else '.%s' % spec
302 pkg = __import__(location, fromlist=[module])
304 except ImportError as ie:
307 kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
311 def _groups_help(arguments):
315 for cmd_group, spec in arguments['config'].get_cli_specs():
316 pkg = _load_spec_module(spec, arguments, '_commands')
318 cmds = getattr(pkg, '_commands')
321 descriptions[cmd.name] = cmd.description
325 'No cmd description for module %s' % cmd_group)
327 kloger.warning('Loading of %s cmd spec failed' % cmd_group)
328 print('\nOptions:\n - - - -')
329 print_dict(descriptions)
332 def _load_all_commands(cmd_tree, arguments):
333 _cnf = arguments['config']
334 for cmd_group, spec in _cnf.get_cli_specs():
336 spec_module = _load_spec_module(spec, arguments, '_commands')
337 spec_commands = getattr(spec_module, '_commands')
338 except AttributeError:
341 kloger.warning('No valid description for %s' % cmd_group)
343 for spec_tree in spec_commands:
344 if spec_tree.name == cmd_group:
345 cmd_tree.add_tree(spec_tree)
349 # Methods to be used by CLI implementations
352 def print_subcommands_help(cmd):
354 for subcmd in cmd.get_subcommands():
355 spec, sep, print_path = subcmd.path.partition('_')
356 printout[print_path.replace('_', ' ')] = subcmd.description
358 print('\nOptions:\n - - - -')
362 def update_parser_help(parser, cmd):
364 parser.syntax = parser.syntax.split('<')[0]
365 parser.syntax += ' '.join(_best_match)
369 cls = cmd.get_class()
370 parser.syntax += ' ' + cls.syntax
371 parser.update_arguments(cls().arguments)
372 description = getattr(cls, 'long_description', '')
373 description = description.strip()
375 parser.syntax += ' <...>'
376 if cmd.has_description:
377 parser.parser.description = cmd.help + (
378 ('\n%s' % description) if description else '')
380 parser.parser.description = description
383 def print_error_message(cli_err):
384 errmsg = '%s' % cli_err
385 if cli_err.importance == 1:
386 errmsg = magenta(errmsg)
387 elif cli_err.importance == 2:
388 errmsg = yellow(errmsg)
389 elif cli_err.importance > 2:
392 for errmsg in cli_err.details:
393 print('| %s' % errmsg)
396 def exec_cmd(instance, cmd_args, help_method):
398 return instance.main(*cmd_args)
399 except TypeError as err:
400 if err.args and err.args[0].startswith('main()'):
401 print(magenta('Syntax error'))
412 def get_command_group(unparsed, arguments):
413 groups = arguments['config'].get_groups()
414 for term in unparsed:
415 if term.startswith('-'):
418 unparsed.remove(term)
424 def set_command_params(parameters):
425 """Add a parameters list to a command
427 :param paramters: (list of str) a list of parameters
430 def_params = list(command.func_defaults)
431 def_params[0] = parameters
432 command.func_defaults = tuple(def_params)
437 def run_one_cmd(exe_string, parser, auth_base, cloud):
440 parser.arguments['config'].get_global('history_file'))
441 _history.add(' '.join([exe_string] + argv[1:]))
442 from kamaki.cli import one_command
443 one_command.run(auth_base, cloud, parser, _help)
446 def run_shell(exe_string, parser, auth_base, cloud):
447 from command_shell import _init_shell
448 shell = _init_shell(exe_string, parser)
449 _load_all_commands(shell.cmd_tree, parser.arguments)
450 shell.run(auth_base, cloud, parser)
453 def is_non_API(parser):
454 nonAPIs = ('history', 'config')
455 for term in parser.unparsed:
456 if not term.startswith('-'):
465 exe = basename(argv[0])
466 parser = ArgumentParseManager(exe)
468 if parser.arguments['version'].value:
471 log_file = parser.arguments['config'].get_global('log_file')
473 logger.set_log_filename(log_file)
475 filelog = logger.add_file_logger(__name__.split('.')[0])
476 filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
478 auth_base, cloud = _init_session(parser.arguments, is_non_API(parser))
480 from kamaki.cli.utils import suggest_missing
484 run_one_cmd(exe, parser, auth_base, cloud)
486 parser.parser.print_help()
487 _groups_help(parser.arguments)
489 run_shell(exe, parser, auth_base, cloud)
490 except CLIError as err:
491 print_error_message(err)
495 except Exception as er:
496 print('Unknown Error: %s' % er)