Fix all minor typos and modifications in tests
[kamaki] / kamaki / cli / __init__.py
index 2817d49..b9ef171 100644 (file)
 
 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 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)
@@ -111,27 +109,31 @@ def _update_best_match(name_terms, prefix=[]):
 
 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:
-                print('Warning: %s failed to update_best_match' % cls_name)
+                kloger.warning('%s failed to update_best_match' % cls_name)
             return None
 
         global _best_match
@@ -141,11 +143,12 @@ def command(cmd_tree, prefix='', descedants_depth=1):
             if not cmd_tree.has_command(partial):  # add partial path
                 cmd_tree.add_command(partial)
             if _debug:
-                print('Warning: %s failed max_len test' % cls_name)
+                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)
@@ -153,11 +156,6 @@ def command(cmd_tree, prefix='', descedants_depth=1):
     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',
@@ -166,105 +164,199 @@ cmd_spec_locations = [
     '']
 
 
+#  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('_')
@@ -274,24 +366,28 @@ def _print_subcommands_help(cmd):
         print_dict(printout)
 
 
-def _update_parser_help(parser, cmd):
+def update_parser_help(parser, cmd):
     global _best_match
     parser.syntax = parser.syntax.split('<')[0]
     parser.syntax += ' '.join(_best_match)
 
+    description = ''
     if cmd.is_command:
         cls = cmd.get_class()
         parser.syntax += ' ' + cls.syntax
         parser.update_arguments(cls().arguments)
-        # arguments = cls().arguments
-        # update_arguments(parser, arguments)
+        description = getattr(cls, 'long_description', '')
+        description = description.strip()
     else:
         parser.syntax += ' <...>'
     if cmd.has_description:
-        parser.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)
@@ -300,20 +396,11 @@ def _print_error_message(cli_err):
     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:
@@ -329,6 +416,18 @@ def _exec_cmd(instance, cmd_args, help_method):
     return 1
 
 
+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
+
+
 def set_command_params(parameters):
     """Add a parameters list to a command
 
@@ -340,72 +439,32 @@ def set_command_params(parameters):
     command.func_defaults = tuple(def_params)
 
 
-#def one_cmd(parser, unparsed, arguments):
-def one_cmd(parser):
-    group = get_command_group(list(parser.unparsed), parser.arguments)
-    if not group:
-        parser.parser.print_help()
-        _groups_help(parser.arguments)
-        exit(0)
-
-    nonargs = [term for term in parser.unparsed if not term.startswith('-')]
-    set_command_params(nonargs)
-
-    global _best_match
-    _best_match = []
-
-    spec_module = _load_spec_module(group, parser.arguments, '_commands')
-
-    cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
-
-    if _best_match:
-        cmd = cmd_tree.get_command('_'.join(_best_match))
-    else:
-        cmd = _get_best_match_from_cmd_tree(cmd_tree, parser.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)
-
-    if _help or not cmd.is_command:
-        parser.parser.print_help()
-        _print_subcommands_help(cmd)
-        exit(0)
-
-    cls = cmd.get_class()
-    executable = cls(parser.arguments)
-    parser.update_arguments(executable.arguments)
-    #parsed, unparsed = parse_known_args(parser, executable.arguments)
-    for term in _best_match:
-        parser.unparsed.remove(term)
-    _exec_cmd(executable, parser.unparsed, parser.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, parser):
+def run_shell(exe_string, parser, auth_base, cloud):
     from command_shell import _init_shell
     shell = _init_shell(exe_string, parser)
     _load_all_commands(shell.cmd_tree, parser.arguments)
-    shell.run(parser)
+    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():
@@ -416,24 +475,34 @@ def main():
         if parser.arguments['version'].value:
             exit(0)
 
-        _init_session(parser.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 parser.unparsed:
-            _history = History(
-                parser.arguments['config'].get('history', 'file'))
-            _history.add(' '.join([exe] + argv[1:]))
-            one_cmd(parser)
+            run_one_cmd(exe, parser, auth_base, cloud)
         elif _help:
             parser.parser.print_help()
             _groups_help(parser.arguments)
         else:
-            run_shell(exe, parser)
+            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)