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 _init_session(arguments):
197 _help = arguments['help'].value
199 _debug = arguments['debug'].value
201 _include = arguments['include'].value
203 _verbose = arguments['verbose'].value
204 _cnf = arguments['config']
206 _colors = _cnf.get('global', 'colors')
207 if not (stdout.isatty() and _colors == 'on'):
208 from kamaki.cli.utils import remove_colors
210 _silent = arguments['silent'].value
211 _setup_logging(_silent, _debug, _verbose, _include)
212 picked_cloud = arguments['cloud'].value
214 global_url = _cnf.get('remotes', picked_cloud)
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"' % (
223 ' /config set remotes.%s <URI>' % picked_cloud
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)
232 def _load_spec_module(spec, arguments, module):
233 #spec_name = arguments['config'].get('cli', spec)
237 for location in cmd_spec_locations:
238 location += spec if location == '' else '.%s' % spec
240 pkg = __import__(location, fromlist=[module])
242 except ImportError as ie:
245 kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
249 def _groups_help(arguments):
253 for cmd_group, spec in arguments['config'].get_cli_specs():
254 pkg = _load_spec_module(spec, arguments, '_commands')
256 cmds = getattr(pkg, '_commands')
258 # #_cnf = arguments['config']
259 # #cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
260 # # 'cli', cmd.name)]
261 #except AttributeError:
263 # kloger.warning('No description for %s' % cmd_group)
266 descriptions[cmd.name] = cmd.description
270 'No cmd description for module %s' % cmd_group)
272 kloger.warning('Loading of %s cmd spec failed' % cmd_group)
273 print('\nOptions:\n - - - -')
274 print_dict(descriptions)
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():
282 spec_module = _load_spec_module(spec, arguments, '_commands')
283 spec_commands = getattr(spec_module, '_commands')
284 except AttributeError:
287 kloger.warning('No valid description for %s' % cmd_group)
289 for spec_tree in spec_commands:
290 if spec_tree.name == cmd_group:
291 cmd_tree.add_tree(spec_tree)
295 # Methods to be used by CLI implementations
298 def print_subcommands_help(cmd):
300 for subcmd in cmd.get_subcommands():
301 spec, sep, print_path = subcmd.path.partition('_')
302 printout[print_path.replace('_', ' ')] = subcmd.description
304 print('\nOptions:\n - - - -')
308 def update_parser_help(parser, cmd):
310 parser.syntax = parser.syntax.split('<')[0]
311 parser.syntax += ' '.join(_best_match)
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()
321 parser.syntax += ' <...>'
322 if cmd.has_description:
323 parser.parser.description = cmd.help + (
324 ('\n%s' % description) if description else '')
326 parser.parser.description = description
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:
338 for errmsg in cli_err.details:
339 print('| %s' % errmsg)
342 def exec_cmd(instance, cmd_args, help_method):
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'))
358 def get_command_group(unparsed, arguments):
359 groups = arguments['config'].get_groups()
360 for term in unparsed:
361 if term.startswith('-'):
364 unparsed.remove(term)
370 def set_command_params(parameters):
371 """Add a parameters list to a command
373 :param paramters: (list of str) a list of parameters
376 def_params = list(command.func_defaults)
377 def_params[0] = parameters
378 command.func_defaults = tuple(def_params)
383 def run_one_cmd(exe_string, parser, auth_base):
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)
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)
401 exe = basename(argv[0])
402 parser = ArgumentParseManager(exe)
404 if parser.arguments['version'].value:
407 log_file = parser.arguments['config'].get('global', 'log_file')
409 logger.set_log_filename(log_file)
411 filelog = logger.add_file_logger(__name__.split('.')[0])
412 filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
414 auth_base = _init_session(parser.arguments)
416 from kamaki.cli.utils import suggest_missing
420 run_one_cmd(exe, parser, auth_base)
422 parser.parser.print_help()
423 _groups_help(parser.arguments)
425 run_shell(exe, parser, auth_base)
426 except CLIError as err:
427 print_error_message(err)
431 except Exception as er:
432 print('Unknown Error: %s' % er)