import logging
from sys import argv, exit, stdout
-from os.path import basename
+from os.path import basename, exists
from inspect import getargspec
-from kamaki.cli.argument import _arguments, parse_known_args, init_parser,\
- update_arguments
+from kamaki.cli.argument import ArgumentParseManager
from kamaki.cli.history import History
-from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
+from kamaki.cli.utils import print_dict, red, magenta, yellow
from kamaki.cli.errors import CLIError
+from kamaki.cli import logger
_help = False
_debug = False
+_include = False
_verbose = False
_colors = False
+kloger = None
+filelog = None
+
+# command auxiliary methods
+
+_best_match = []
+
+
+def _arg2syntax(arg):
+ return arg.replace(
+ '____', '[:').replace(
+ '___', ':').replace(
+ '__', ']').replace(
+ '_', ' ')
def _construct_command_syntax(cls):
spec = getargspec(cls.main.im_func)
args = spec.args[1:]
n = len(args) - len(spec.defaults or ())
- required = ' '.join('<%s>' % x\
- .replace('____', '[:')\
- .replace('___', ':')\
- .replace('__', ']').\
- replace('_', ' ') for x in args[:n])
- optional = ' '.join('[%s]' % x\
- .replace('____', '[:')\
- .replace('___', ':')\
- .replace('__', ']').\
- replace('_', ' ') for x in args[n:])
+ required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
+ optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
cls.syntax = ' '.join(x for x in [required, optional] if x)
if spec.varargs:
cls.syntax += ' <%s ...>' % spec.varargs
-def _get_cmd_tree_from_spec(spec, cmd_tree_list):
- for tree in cmd_tree_list:
- if tree.name == spec:
- return tree
- return None
-
-
-_best_match = []
-
-
def _num_of_matching_terms(basic_list, attack_list):
if not attack_list:
return len(basic_list)
num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
global _best_match
+ if not prefix:
+ _best_match = []
if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
if len(_best_match) < num_of_matching_terms:
def command(cmd_tree, prefix='', descedants_depth=1):
"""Load a class as a command
- spec_cmd0_cmd1 will be command spec cmd0
- @cmd_tree is initialized in cmd_spec file and is the structure
+ e.g. spec_cmd0_cmd1 will be command spec cmd0
+
+ :param cmd_tree: is initialized in cmd_spec file and is the structure
where commands are loaded. Var name should be _commands
- @param prefix if given, load only commands prefixed with prefix,
- @param descedants_depth is the depth of the tree descedants of the
+ :param prefix: if given, load only commands prefixed with prefix,
+ :param descedants_depth: is the depth of the tree descedants of the
prefix command. It is used ONLY if prefix and if prefix is not
a terminal command
+
+ :returns: the specified class object
"""
def wrap(cls):
+ global kloger
cls_name = cls.__name__
if not cmd_tree:
if _debug:
- print('Warning: command %s found but not loaded' % cls_name)
+ kloger.warning('command %s found but not loaded' % cls_name)
return cls
name_terms = cls_name.split('_')
if not _update_best_match(name_terms, prefix):
+ if _debug:
+ kloger.warning('%s failed to update_best_match' % cls_name)
return None
global _best_match
partial = '_'.join(name_terms[:max_len])
if not cmd_tree.has_command(partial): # add partial path
cmd_tree.add_command(partial)
+ if _debug:
+ kloger.warning('%s failed max_len test' % cls_name)
return None
- cls.description, sep, cls.long_description\
- = cls.__doc__.partition('\n')
+ (
+ cls.description, sep, cls.long_description
+ ) = cls.__doc__.partition('\n')
_construct_command_syntax(cls)
cmd_tree.add_command(cls_name, cls.description, cls)
return wrap
-def get_cmd_terms():
- global command
- return [term for term in command.func_defaults[0]\
- if not term.startswith('-')]
-
cmd_spec_locations = [
'kamaki.cli.commands',
'kamaki.commands',
'']
+# Generic init auxiliary functions
+
+
def _setup_logging(silent=False, debug=False, verbose=False, include=False):
"""handle logging for clients package"""
- def add_handler(name, level, prefix=''):
- h = logging.StreamHandler()
- fmt = logging.Formatter(prefix + '%(message)s')
- h.setFormatter(fmt)
- logger = logging.getLogger(name)
- logger.addHandler(h)
- logger.setLevel(level)
-
if silent:
- add_handler('', logging.CRITICAL)
- elif debug:
- add_handler('requests', logging.INFO, prefix='* ')
- add_handler('clients.send', logging.DEBUG, prefix='> ')
- add_handler('clients.recv', logging.DEBUG, prefix='< ')
+ logger.add_stream_logger(__name__, logging.CRITICAL)
+ return
+
+ sfmt, rfmt = '> %(message)s', '< %(message)s'
+ if debug:
+ print('Logging location: %s' % logger.get_log_filename())
+ logger.add_stream_logger('kamaki.clients.send', logging.DEBUG, sfmt)
+ logger.add_stream_logger('kamaki.clients.recv', logging.DEBUG, rfmt)
+ logger.add_stream_logger(__name__, logging.DEBUG)
elif verbose:
- add_handler('requests', logging.INFO, prefix='* ')
- add_handler('clients.send', logging.INFO, prefix='> ')
- add_handler('clients.recv', logging.INFO, prefix='< ')
- elif include:
- add_handler('clients.recv', logging.INFO)
- else:
- add_handler('', logging.WARNING)
+ logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
+ logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
+ logger.add_stream_logger(__name__, logging.INFO)
+ if include:
+ logger.add_stream_logger('kamaki.clients.send', logging.INFO, sfmt)
+ logger.add_stream_logger('kamaki.clients.recv', logging.INFO, rfmt)
+ logger.add_stream_logger(__name__, logging.WARNING)
+ global kloger
+ kloger = logger.get_logger(__name__)
+
+
+def _check_config_version(cnf):
+ guess = cnf.guess_version()
+ if exists(cnf.path) and guess < 0.9:
+ print('Config file format version >= 9.0 is required')
+ print('Configuration file: %s' % cnf.path)
+ print('but kamaki can fix this:')
+ print('Calculating changes while preserving information')
+ lost_terms = cnf.rescue_old_file()
+ print('... DONE')
+ if lost_terms:
+ print 'The following information will NOT be preserved:'
+ print '\t', '\n\t'.join(lost_terms)
+ print('Kamaki is ready to convert the config file to version 3.0')
+ stdout.write('Create (overwrite) file %s ? [y/N] ' % cnf.path)
+ from sys import stdin
+ reply = stdin.readline()
+ if reply in ('Y\n', 'y\n'):
+ cnf.write()
+ print('... DONE')
+ else:
+ print('... ABORTING')
+ raise CLIError(
+ 'Invalid format for config file %s' % cnf.path,
+ importance=3, details=[
+ 'Please, update config file to v3.0',
+ 'For automatic conversion, rerun and say Y'])
-def _init_session(arguments):
+def _init_session(arguments, is_non_API=False):
+ """
+ :returns: (AuthCachedClient, str) authenticator and cloud name
+ """
global _help
_help = arguments['help'].value
global _debug
_debug = arguments['debug'].value
+ global _include
+ _include = arguments['include'].value
global _verbose
_verbose = arguments['verbose'].value
+ _cnf = arguments['config']
+
+ if _help or is_non_API:
+ return None, None
+
+ _check_config_version(_cnf.value)
+
global _colors
- _colors = arguments['config'].get('global', 'colors')
+ _colors = _cnf.value.get_global('colors')
if not (stdout.isatty() and _colors == 'on'):
from kamaki.cli.utils import remove_colors
remove_colors()
_silent = arguments['silent'].value
- _include = arguments['include'].value
_setup_logging(_silent, _debug, _verbose, _include)
-
-def get_command_group(unparsed, arguments):
- groups = arguments['config'].get_groups()
- for term in unparsed:
- if term.startswith('-'):
- continue
- if term in groups:
- unparsed.remove(term)
- return term
- return None
- return None
+ cloud = arguments['cloud'].value or _cnf.value.get(
+ 'global', 'default_cloud')
+ if not cloud:
+ num_of_clouds = len(_cnf.value.keys('cloud'))
+ if num_of_clouds == 1:
+ cloud = _cnf.value.keys('cloud')[0]
+ elif num_of_clouds > 1:
+ raise CLIError(
+ 'Found %s clouds but none of them is set as default' % (
+ num_of_clouds),
+ importance=2, details=[
+ 'Please, choose one of the following cloud names:',
+ ', '.join(_cnf.value.keys('cloud')),
+ 'To see all cloud settings:',
+ ' kamaki config get cloud.<cloud name>',
+ 'To set a default cloud:',
+ ' kamaki config set default_cloud <cloud name>',
+ 'To pick a cloud for the current session, use --cloud:',
+ ' kamaki --cloud=<cloud name> ...'])
+ if not cloud in _cnf.value.keys('cloud'):
+ raise CLIError(
+ 'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''),
+ importance=3, details=[
+ 'To configure a new cloud "%s", find and set the' % (
+ cloud or '<cloud name>'),
+ 'single authentication URL and token:',
+ ' kamaki config set cloud.%s.url <URL>' % (
+ cloud or '<cloud name>'),
+ ' kamaki config set cloud.%s.token <t0k3n>' % (
+ cloud or '<cloud name>')])
+ auth_args = dict()
+ for term in ('url', 'token'):
+ try:
+ auth_args[term] = _cnf.get_cloud(cloud, term)
+ except KeyError:
+ auth_args[term] = ''
+ if not auth_args[term]:
+ raise CLIError(
+ 'No authentication %s provided for cloud "%s"' % (term, cloud),
+ importance=3, details=[
+ 'Set a %s for cloud %s:' % (term, cloud),
+ ' kamaki config set cloud.%s.%s <%s>' % (
+ cloud, term, term)])
+
+ from kamaki.clients.astakos import AstakosClient as AuthCachedClient
+ try:
+ return AuthCachedClient(auth_args['url'], auth_args['token']), cloud
+ except AssertionError as ae:
+ kloger.warning('WARNING: Failed to load authenticator [%s]' % ae)
+ return None, cloud
def _load_spec_module(spec, arguments, module):
- spec_name = arguments['config'].get(spec, 'cli')
- if spec_name is None:
+ if not spec:
return None
pkg = None
for location in cmd_spec_locations:
- location += spec_name if location == '' else '.%s' % spec_name
+ location += spec if location == '' else '.%s' % spec
try:
pkg = __import__(location, fromlist=[module])
return pkg
- except ImportError:
+ except ImportError as ie:
continue
+ if not pkg:
+ kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
return pkg
def _groups_help(arguments):
global _debug
+ global kloger
descriptions = {}
- for spec in arguments['config'].get_groups():
+ for cmd_group, spec in arguments['config'].get_cli_specs():
pkg = _load_spec_module(spec, arguments, '_commands')
if pkg:
- cmds = None
- try:
- cmds = [
- cmd for cmd in getattr(pkg, '_commands')\
- if arguments['config'].get(cmd.name, 'cli')
- ]
- except AttributeError:
- if _debug:
- print('Warning: No description for %s' % spec)
+ cmds = getattr(pkg, '_commands')
try:
for cmd in cmds:
descriptions[cmd.name] = cmd.description
except TypeError:
if _debug:
- print('Warning: no cmd specs in module %s' % spec)
+ kloger.warning(
+ 'No cmd description for module %s' % cmd_group)
elif _debug:
- print('Warning: Loading of %s cmd spec failed' % spec)
+ kloger.warning('Loading of %s cmd spec failed' % cmd_group)
print('\nOptions:\n - - - -')
print_dict(descriptions)
-def _print_subcommands_help(cmd):
+def _load_all_commands(cmd_tree, arguments):
+ _cnf = arguments['config']
+ for cmd_group, spec in _cnf.get_cli_specs():
+ try:
+ spec_module = _load_spec_module(spec, arguments, '_commands')
+ spec_commands = getattr(spec_module, '_commands')
+ except AttributeError:
+ if _debug:
+ global kloger
+ kloger.warning('No valid description for %s' % cmd_group)
+ continue
+ for spec_tree in spec_commands:
+ if spec_tree.name == cmd_group:
+ cmd_tree.add_tree(spec_tree)
+ break
+
+
+# Methods to be used by CLI implementations
+
+
+def print_subcommands_help(cmd):
printout = {}
for subcmd in cmd.get_subcommands():
spec, sep, print_path = subcmd.path.partition('_')
print_dict(printout)
-def _update_parser_help(parser, cmd):
+def update_parser_help(parser, cmd):
global _best_match
- parser.prog = parser.prog.split('<')[0]
- parser.prog += ' '.join(_best_match)
+ parser.syntax = parser.syntax.split('<')[0]
+ parser.syntax += ' '.join(_best_match)
+ description = ''
if cmd.is_command:
cls = cmd.get_class()
- parser.prog += ' ' + cls.syntax
- arguments = cls().arguments
- update_arguments(parser, arguments)
+ parser.syntax += ' ' + cls.syntax
+ parser.update_arguments(cls().arguments)
+ description = getattr(cls, 'long_description', '')
+ description = description.strip()
else:
- parser.prog += ' <...>'
+ parser.syntax += ' <...>'
if cmd.has_description:
- parser.description = cmd.help
+ parser.parser.description = cmd.help + (
+ ('\n%s' % description) if description else '')
+ else:
+ parser.parser.description = description
-def _print_error_message(cli_err):
+def print_error_message(cli_err):
errmsg = '%s' % cli_err
if cli_err.importance == 1:
errmsg = magenta(errmsg)
elif cli_err.importance > 2:
errmsg = red(errmsg)
stdout.write(errmsg)
- print_list(cli_err.details)
+ for errmsg in cli_err.details:
+ print('| %s' % errmsg)
-def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
- matched = [term for term in unparsed if not term.startswith('-')]
- while matched:
- try:
- return cmd_tree.get_command('_'.join(matched))
- except KeyError:
- matched = matched[:-1]
- return None
-
-
-def _exec_cmd(instance, cmd_args, help_method):
+def exec_cmd(instance, cmd_args, help_method):
try:
return instance.main(*cmd_args)
except TypeError as err:
return 1
-def set_command_param(param, value):
- if param == 'prefix':
- pos = 0
- elif param == 'descedants_depth':
- pos = 1
- else:
- return
- global command
- def_params = list(command.func_defaults)
- def_params[pos] = value
- command.func_defaults = tuple(def_params)
-
-
-def one_cmd(parser, unparsed, arguments):
- group = get_command_group(list(unparsed), arguments)
- if not group:
- parser.print_help()
- _groups_help(arguments)
- exit(0)
-
- set_command_param(
- 'prefix',
- [term for term in unparsed if not term.startswith('-')]
- )
- global _best_match
- _best_match = []
-
- spec_module = _load_spec_module(group, arguments, '_commands')
-
- cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
+def get_command_group(unparsed, arguments):
+ groups = arguments['config'].get_groups()
+ for term in unparsed:
+ if term.startswith('-'):
+ continue
+ if term in groups:
+ unparsed.remove(term)
+ return term
+ return None
+ return None
- if _best_match:
- cmd = cmd_tree.get_command('_'.join(_best_match))
- else:
- cmd = _get_best_match_from_cmd_tree(cmd_tree, unparsed)
- _best_match = cmd.path.split('_')
- if cmd is None:
- if _debug or _verbose:
- print('Unexpected error: failed to load command')
- exit(1)
- _update_parser_help(parser, cmd)
+def set_command_params(parameters):
+ """Add a parameters list to a command
- if _help or not cmd.is_command:
- parser.print_help()
- _print_subcommands_help(cmd)
- exit(0)
+ :param paramters: (list of str) a list of parameters
+ """
+ global command
+ def_params = list(command.func_defaults)
+ def_params[0] = parameters
+ command.func_defaults = tuple(def_params)
- cls = cmd.get_class()
- executable = cls(arguments)
- parsed, unparsed = parse_known_args(parser, executable.arguments)
- for term in _best_match:
- unparsed.remove(term)
- _exec_cmd(executable, unparsed, parser.print_help)
+# CLI Choice:
-def _load_all_commands(cmd_tree, arguments):
- _config = arguments['config']
- for spec in [spec for spec in _config.get_groups()\
- if _config.get(spec, 'cli')]:
- try:
- spec_module = _load_spec_module(spec, arguments, '_commands')
- spec_commands = getattr(spec_module, '_commands')
- except AttributeError:
- if _debug:
- print('Warning: No valid description for %s' % spec)
- continue
- for spec_tree in spec_commands:
- if spec_tree.name == spec:
- cmd_tree.add_tree(spec_tree)
- break
+def run_one_cmd(exe_string, parser, auth_base, cloud):
+ global _history
+ _history = History(
+ parser.arguments['config'].get_global('history_file'))
+ _history.add(' '.join([exe_string] + argv[1:]))
+ from kamaki.cli import one_command
+ one_command.run(auth_base, cloud, parser, _help)
-def run_shell(exe_string, arguments):
+def run_shell(exe_string, parser, auth_base, cloud):
from command_shell import _init_shell
- shell = _init_shell(exe_string, arguments)
- _load_all_commands(shell.cmd_tree, arguments)
- shell.run()
+ shell = _init_shell(exe_string, parser)
+ _load_all_commands(shell.cmd_tree, parser.arguments)
+ shell.run(auth_base, cloud, parser)
+
+
+def is_non_API(parser):
+ nonAPIs = ('history', 'config')
+ for term in parser.unparsed:
+ if not term.startswith('-'):
+ if term in nonAPIs:
+ return True
+ return False
+ return False
def main():
try:
exe = basename(argv[0])
- parser = init_parser(exe, _arguments)
- parsed, unparsed = parse_known_args(parser, _arguments)
+ parser = ArgumentParseManager(exe)
- if _arguments['version'].value:
+ if parser.arguments['version'].value:
exit(0)
- _init_session(_arguments)
+ log_file = parser.arguments['config'].get_global('log_file')
+ if log_file:
+ logger.set_log_filename(log_file)
+ global filelog
+ filelog = logger.add_file_logger(__name__.split('.')[0])
+ filelog.info('* Initial Call *\n%s\n- - -' % ' '.join(argv))
+
+ auth_base, cloud = _init_session(parser.arguments, is_non_API(parser))
+
+ from kamaki.cli.utils import suggest_missing
+ global _colors
+ exclude = ['ansicolors'] if not _colors == 'on' else []
+ suggest_missing(exclude=exclude)
- if unparsed:
- _history = History(_arguments['config'].get('history', 'file'))
- _history.add(' '.join([exe] + argv[1:]))
- one_cmd(parser, unparsed, _arguments)
+ if parser.unparsed:
+ run_one_cmd(exe, parser, auth_base, cloud)
elif _help:
- parser.print_help()
- _groups_help(_arguments)
+ parser.parser.print_help()
+ _groups_help(parser.arguments)
else:
- run_shell(exe, _arguments)
+ run_shell(exe, parser, auth_base, cloud)
except CLIError as err:
+ print_error_message(err)
if _debug:
raise err
- _print_error_message(err)
exit(1)
- except Exception as err:
+ except Exception as er:
+ print('Unknown Error: %s' % er)
if _debug:
- raise err
- print('Unknown Error: %s' % err)
+ raise
+ exit(1)