-#!/usr/bin/env python
-
-# Copyright 2011-2012 GRNET S.A. All rights reserved.
+# Copyright 2012-2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
-# or implied, of GRNET S.A.
-
-from __future__ import print_function
+# or implied, of GRNET S.A.command
-import gevent.monkey
-#Monkey-patch everything for gevent early on
-gevent.monkey.patch_all()
-
-import inspect
import logging
-import sys
-
-from argparse import ArgumentParser
-from base64 import b64encode
-from os.path import abspath, basename, exists
-from sys import exit, stdout, stderr
-
-try:
- from collections import OrderedDict
-except ImportError:
- from ordereddict import OrderedDict
-
-from colors import magenta, red, yellow, bold
-
-from kamaki import clients
-from .config import Config
-
-_commands = OrderedDict()
-
-GROUPS = {}
-CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
-
-class CLIError(Exception):
- def __init__(self, message, status=0, details='', importance=0):
- """importance is set by the raiser
- 0 is the lowest possible importance
- Suggested values: 0, 1, 2, 3
- """
- super(CLIError, self).__init__(message, status, details)
- self.message = message
- self.status = status
- self.details = details
- self.importance = importance
-
- def __unicode__(self):
- return unicode(self.message)
-
-def command(group=None, name=None, syntax=None):
- """Class decorator that registers a class as a CLI command."""
-
- def decorator(cls):
- grp, sep, cmd = cls.__name__.partition('_')
- if not sep:
- grp, cmd = None, cls.__name__
-
- #cls.api = api
- cls.group = group or grp
- cls.name = name or cmd
-
- short_description, sep, long_description = cls.__doc__.partition('\n')
- cls.description = short_description
- cls.long_description = long_description or short_description
-
- cls.syntax = syntax
- if cls.syntax is None:
- # Generate a syntax string based on main's arguments
- spec = inspect.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:])
- cls.syntax = ' '.join(x for x in [required, optional] if x)
- if spec.varargs:
- cls.syntax += ' <%s ...>' % spec.varargs
-
- if cls.group not in _commands:
- _commands[cls.group] = OrderedDict()
- _commands[cls.group][cls.name] = cls
+from sys import argv, exit, stdout
+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, 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>' % _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 _num_of_matching_terms(basic_list, attack_list):
+ if not attack_list:
+ return len(basic_list)
+
+ matching_terms = 0
+ for i, term in enumerate(basic_list):
+ try:
+ if term != attack_list[i]:
+ break
+ except IndexError:
+ break
+ matching_terms += 1
+ return matching_terms
+
+
+def _update_best_match(name_terms, prefix=[]):
+ if prefix:
+ pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
+ else:
+ pref_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:
+ _best_match = name_terms[:num_of_matching_terms]
+ return True
+ return False
+
+
+def command(cmd_tree, prefix='', descedants_depth=1):
+ """Load a class as a command
+ 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
+ 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:
+ 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
+ max_len = len(_best_match) + descedants_depth
+ if len(name_terms) > max_len:
+ 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')
+ _construct_command_syntax(cls)
+
+ cmd_tree.add_command(cls_name, cls.description, cls)
return cls
- return decorator
-
-def set_api_description(api, description):
- """Method to be called by api CLIs
- Each CLI can set more than one api descriptions"""
- GROUPS[api] = description
+ return wrap
+
+
+cmd_spec_locations = [
+ 'kamaki.cli.commands',
+ 'kamaki.commands',
+ 'kamaki.cli',
+ 'kamaki',
+ '']
+
+
+# Generic init auxiliary functions
+
+
+def _setup_logging(silent=False, debug=False, verbose=False, include=False):
+ """handle logging for clients package"""
+
+ if silent:
+ 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:
+ 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, 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 = _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
+ _setup_logging(_silent, _debug, _verbose, _include)
+
+ 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 main():
- def print_groups():
- print('\nGroups:')
- for group in _commands:
- description = GROUPS.get(group, '')
- print(' ', group.ljust(12), description)
-
- def print_commands(group):
- description = GROUPS.get(group, '')
- if description:
- print('\n' + description)
-
- print('\nCommands:')
- for name, cls in _commands[group].items():
- print(' ', name.ljust(14), cls.description)
-
- def manage_logging_handlers(args):
- """This is mostly to 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 args.silent:
- add_handler('', logging.CRITICAL)
- elif args.debug:
- add_handler('requests', logging.INFO, prefix='* ')
- add_handler('clients.send', logging.DEBUG, prefix='> ')
- add_handler('clients.recv', logging.DEBUG, prefix='< ')
- elif args.verbose:
- add_handler('requests', logging.INFO, prefix='* ')
- add_handler('clients.send', logging.INFO, prefix='> ')
- add_handler('clients.recv', logging.INFO, prefix='< ')
- elif args.include:
- add_handler('clients.recv', logging.INFO)
+def _load_spec_module(spec, arguments, module):
+ if not spec:
+ return None
+ pkg = None
+ for location in cmd_spec_locations:
+ location += spec if location == '' else '.%s' % spec
+ try:
+ pkg = __import__(location, fromlist=[module])
+ return pkg
+ 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 cmd_group, spec in arguments['config'].get_cli_specs():
+ pkg = _load_spec_module(spec, arguments, '_commands')
+ if pkg:
+ cmds = getattr(pkg, '_commands')
+ try:
+ for cmd in cmds:
+ descriptions[cmd.name] = cmd.description
+ except TypeError:
+ if _debug:
+ kloger.warning(
+ 'No cmd description for module %s' % cmd_group)
+ elif _debug:
+ kloger.warning('Loading of %s cmd spec failed' % cmd_group)
+ print('\nOptions:\n - - - -')
+ print_dict(descriptions)
+
+
+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('_')
+ printout[print_path.replace('_', ' ')] = subcmd.description
+ if printout:
+ print('\nOptions:\n - - - -')
+ print_dict(printout)
+
+
+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)
+ description = getattr(cls, 'long_description', '')
+ description = description.strip()
+ else:
+ parser.syntax += ' <...>'
+ if cmd.has_description:
+ parser.parser.description = cmd.help + (
+ ('\n%s' % description) if description else '')
+ else:
+ parser.parser.description = description
+
+
+def print_error_message(cli_err):
+ errmsg = '%s' % cli_err
+ if cli_err.importance == 1:
+ errmsg = magenta(errmsg)
+ elif cli_err.importance == 2:
+ errmsg = yellow(errmsg)
+ elif cli_err.importance > 2:
+ errmsg = red(errmsg)
+ stdout.write(errmsg)
+ for errmsg in cli_err.details:
+ print('| %s' % errmsg)
+
+
+def exec_cmd(instance, cmd_args, help_method):
+ try:
+ return instance.main(*cmd_args)
+ except TypeError as err:
+ if err.args and err.args[0].startswith('main()'):
+ print(magenta('Syntax error'))
+ if _debug:
+ raise err
+ if _verbose:
+ print(unicode(err))
+ help_method()
else:
- add_handler('', logging.WARNING)
-
- def load_groups(config):
- """load groups and import CLIs and Modules"""
- loaded_modules = {}
- for api in config.apis():
- api_cli = config.get(api, 'cli')
- if None == api_cli or len(api_cli)==0:
- print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
- print('\t(cli option in config file)')
- continue
- if not loaded_modules.has_key(api_cli):
- loaded_modules[api_cli] = False
- for location in CLI_LOCATIONS:
- location += api_cli if location == '' else '.%s'%api_cli
- try:
- __import__(location)
- loaded_modules[api_cli] = True
- break
- except ImportError:
- pass
- if not loaded_modules[api_cli]:
- print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
- print('\t(No suitable cli in known paths)')
- continue
- if not GROUPS.has_key(api):
- GROUPS[api] = 'No description (interface: %s)'%api_cli
-
- def init_parser(exe):
- parser = ArgumentParser(add_help=False)
- parser.prog = '%s <group> <command>' % exe
- parser.add_argument('-h', '--help', dest='help', action='store_true',
- default=False,
- help="Show this help message and exit")
- parser.add_argument('--config', dest='config', metavar='PATH',
- help="Specify the path to the configuration file")
- parser.add_argument('-d', '--debug', dest='debug', action='store_true',
- default=False,
- help="Include debug output")
- parser.add_argument('-i', '--include', dest='include', action='store_true',
- default=False,
- help="Include protocol headers in the output")
- parser.add_argument('-s', '--silent', dest='silent', action='store_true',
- default=False,
- help="Silent mode, don't output anything")
- parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
- default=False,
- help="Make the operation more talkative")
- parser.add_argument('-V', '--version', dest='version', action='store_true',
- default=False,
- help="Show version number and quit")
- parser.add_argument('-o', dest='options', action='append',
- default=[], metavar="KEY=VAL",
- help="Override a config value")
- return parser
-
- def find_term_in_args(arg_list, term_list):
- """find an arg_list term in term_list. All other terms up to found
- term are rearanged at the end of arg_list, preserving relative order
- """
- arg_tail = []
- while len(arg_list) > 0:
- group = arg_list.pop(0)
- if group not in term_list:
- arg_tail.append(group)
- else:
- arg_list += arg_tail
- return group
+ raise
+ 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
- """Main Code"""
- exe = basename(sys.argv[0])
- parser = init_parser(exe)
- args, argv = parser.parse_known_args()
-
- #print version
- if args.version:
- import kamaki
- print("kamaki %s" % kamaki.__version__)
- exit(0)
-
- config = Config(args.config) if args.config else Config()
-
- #load config options from command line
- for option in args.options:
- keypath, sep, val = option.partition('=')
- if not sep:
- print("Invalid option '%s'" % option)
- exit(1)
- section, sep, key = keypath.partition('.')
- if not sep:
- print("Invalid option '%s'" % option)
- exit(1)
- config.override(section.strip(), key.strip(), val.strip())
-
- load_groups(config)
- group = find_term_in_args(argv, _commands)
- if not group:
- parser.print_help()
- print_groups()
- exit(0)
-
- parser.prog = '%s %s <command>' % (exe, group)
- command = find_term_in_args(argv, _commands[group])
-
- if not command:
- parser.print_help()
- print_commands(group)
- exit(0)
-
- cmd = _commands[group][command]()
-
- parser.prog = '%s %s %s' % (exe, group, command)
- if cmd.syntax:
- parser.prog += ' %s' % cmd.syntax
- parser.description = cmd.description
- parser.epilog = ''
- if hasattr(cmd, 'update_parser'):
- cmd.update_parser(parser)
-
- #check other args
- args, argv = parser.parse_known_args()
- if group != argv[0]:
- errmsg = red('Invalid command group '+argv[0])
- print(errmsg, file=stderr)
- exit(1)
- if command != argv[1]:
- errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
- print(errmsg, file=stderr)
- exit(1)
- if args.help:
- parser.print_help()
- exit(0)
+def set_command_params(parameters):
+ """Add a parameters list to a command
- manage_logging_handlers(args)
- cmd.args = args
- cmd.config = config
+ :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)
+
+
+# CLI Choice:
+
+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, 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(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:
- ret = cmd.main(*argv[2:])
- exit(ret)
- except TypeError as e:
- if e.args and e.args[0].startswith('main()'):
- parser.print_help()
- exit(1)
+ exe = basename(argv[0])
+ parser = ArgumentParseManager(exe)
+
+ if parser.arguments['version'].value:
+ exit(0)
+
+ 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:
+ run_one_cmd(exe, parser, auth_base, cloud)
+ elif _help:
+ parser.parser.print_help()
+ _groups_help(parser.arguments)
else:
- raise
+ run_shell(exe, parser, auth_base, cloud)
except CLIError as err:
- errmsg = 'CLI Error '
- errmsg += '(%s): '%err.status if err.status else ': '
- errmsg += unicode(err.message) if err.message else ''
- if err.importance == 1:
- errmsg = yellow(errmsg)
- elif err.importance == 2:
- errmsg = magenta(errmsg)
- elif err.importance > 2:
- errmsg = red(errmsg)
- print(errmsg, file=stderr)
+ print_error_message(err)
+ if _debug:
+ raise err
+ exit(1)
+ except Exception as er:
+ print('Unknown Error: %s' % er)
+ if _debug:
+ raise
exit(1)
-
-if __name__ == '__main__':
- main()