From 33752ce01b6b7791dd9544f9f879b7abe437658a Mon Sep 17 00:00:00 2001 From: Jerome Lebleu Date: Mon, 17 Mar 2014 00:47:33 +0100 Subject: [PATCH] Implement global/actions configuration and MoulinetteCLI * Modify global configuration in the actions map * Implement getter/setter for global and action configuration * Implement quickly authenticators classes and add a todo LDAP authenticator * Implement an actions map signals system and add some signals * Add a Moulinette Interface for the cli and make it support signals * Add a test namespace which implements configuration and authentication --- bin/yunohost | 4 +- data/actionsmap/test.yml | 48 ++++ data/actionsmap/yunohost.yml | 16 +- lib/test/__init__.py | 0 lib/test/test.py | 12 + src/moulinette/__init__.py | 13 +- src/moulinette/actionsmap.py | 404 +++++++++++++++++++++++++++++--- src/moulinette/core.py | 74 ++++++ src/moulinette/interface/api.py | 4 +- src/moulinette/interface/cli.py | 128 +++++++++- 10 files changed, 655 insertions(+), 48 deletions(-) create mode 100644 data/actionsmap/test.yml create mode 100755 lib/test/__init__.py create mode 100644 lib/test/test.py diff --git a/bin/yunohost b/bin/yunohost index 78a354bf..39dfaf97 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -7,7 +7,7 @@ import os.path # Run from source basedir = os.path.abspath('%s/../' % os.path.dirname(__file__)) if os.path.isdir('%s/src' % basedir): - sys.path.append('%s/src' % basedir) + sys.path.insert(0, '%s/src' % basedir) from moulinette import init, cli, MoulinetteError from moulinette.helpers import YunoHostError, colorize @@ -35,7 +35,7 @@ if __name__ == '__main__': raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'")) # Execute the action - cli(['yunohost'], args, use_cache) + cli(['yunohost', 'test'], args, use_cache) except MoulinetteError as e: print(e.colorize()) sys.exit(e.code) diff --git a/data/actionsmap/test.yml b/data/actionsmap/test.yml new file mode 100644 index 00000000..d3947cd1 --- /dev/null +++ b/data/actionsmap/test.yml @@ -0,0 +1,48 @@ + +############################# +# Global parameters # +############################# +_global: + configuration: + authenticate: + - api + authenticator: + default: + type: ldap + help: Admin Password + parameters: + uri: ldap://localhost:389 + base: dc=yunohost,dc=org + anonymous: false + ldap-anonymous: + type: ldap + parameters: + uri: ldap://localhost:389 + base: dc=yunohost,dc=org + anonymous: true + argument_auth: true + +############################# +# Test Actions # +############################# +test: + actions: + non-auth: + api: GET /test/non-auth + configuration: + authenticate: false + auth: + api: GET /test/auth + configuration: + authenticate: all + auth-cli: + api: GET /test/auth-cli + configuration: + authenticate: + - cli + anonymous: + api: GET /test/anon + configuration: + authenticate: all + authenticator: ldap-anonymous + argument_auth: false diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 351eae68..cbb06872 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -34,8 +34,22 @@ ############################# _global: configuration: - auth: + authenticate: - api + authenticator: + default: + type: ldap + help: Admin Password + parameters: + uri: ldap://localhost:389 + base: dc=yunohost,dc=org + anonymous: false + ldap-anonymous: + type: ldap + parameters: + uri: ldap://localhost:389 + base: dc=yunohost,dc=org + anonymous: true arguments: -v: full: --version diff --git a/lib/test/__init__.py b/lib/test/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/lib/test/test.py b/lib/test/test.py new file mode 100644 index 00000000..c22c3e9c --- /dev/null +++ b/lib/test/test.py @@ -0,0 +1,12 @@ + +def test_non_auth(): + print('non-auth') + +def test_auth(auth): + print('[default] / all / auth: %r' % auth) + +def test_auth_cli(): + print('[default] / cli') + +def test_anonymous(): + print('[ldap-anonymous] / all') diff --git a/src/moulinette/__init__.py b/src/moulinette/__init__.py index 56e9c582..4e73f6db 100755 --- a/src/moulinette/__init__.py +++ b/src/moulinette/__init__.py @@ -57,7 +57,7 @@ def init(**kwargs): install_i18n() # Add library directory to python path - sys.path.append(pkg.libdir) + sys.path.insert(0, pkg.libdir) ## Easy access to interfaces @@ -99,10 +99,9 @@ def cli(namespaces, args, use_cache=True): """ from .actionsmap import ActionsMap - from .helpers import pretty_print_dict + from .interface.cli import MoulinetteCLI - try: - amap = ActionsMap('cli', namespaces, use_cache) - pretty_print_dict(amap.process(args)) - except KeyboardInterrupt, EOFError: - raise MoulinetteError(125, _("Interrupted")) + amap = ActionsMap('cli', namespaces, use_cache) + moulinette = MoulinetteCLI(amap) + + moulinette.run(args) diff --git a/src/moulinette/actionsmap.py b/src/moulinette/actionsmap.py index 21b73ab6..21e704a7 100644 --- a/src/moulinette/actionsmap.py +++ b/src/moulinette/actionsmap.py @@ -10,7 +10,91 @@ from collections import OrderedDict import logging from . import __version__ -from .core import MoulinetteError, MoulinetteLock +from .core import MoulinetteError, MoulinetteLock, init_authenticator + +## Actions map Signals ------------------------------------------------- + +class _AMapSignals(object): + """Actions map's Signals interface + + Allow to easily connect signals of the actions map to handlers. They + can be given as arguments in the form of { signal: handler }. + + """ + def __init__(self, **kwargs): + # Initialize handlers + for s in self.signals: + self.clear_handler(s) + + # Iterate over signals to connect + for s, h in kwargs.items(): + self.set_handler(s, h) + + def set_handler(self, signal, handler): + """Set the handler for a signal""" + if signal not in self.signals: + raise ValueError("unknown signal '%s'" % signal) + setattr(self, '_%s' % signal, handler) + + def clear_handler(self, signal): + """Clear the handler of a signal""" + if signal not in self.signals: + raise ValueError("unknown signal '%s'" % signal) + setattr(self, '_%s' % signal, self._notimplemented) + + + ## Signals definitions + + """The list of available signals""" + signals = { 'authenticate', 'prompt' } + + def authenticate(self, authenticator, name, help): + """Process the authentication + + Attempt to authenticate to the given authenticator and return + it. + It is called when authentication is needed (e.g. to process an + action). + + Keyword arguments: + - authenticator -- The authenticator to use + - name -- The authenticator name in the actions map + - help -- A help message for the authenticator + + Returns: + The authenticator object + + """ + if authenticator.is_authenticated: + return authenticator + return self._authenticate(authenticator, name, help) + + def prompt(self, message, is_password=False, confirm=False): + """Prompt for a value + + Prompt the interface for a parameter value which is a password + if 'is_password' and must be confirmed if 'confirm'. + Is is called when a parameter value is needed and when the + current interface should allow user interaction (e.g. to parse + extra parameter 'ask' in the cli). + + Keyword arguments: + - message -- The message to display + - is_password -- True if the parameter is a password + - confirm -- True if the value must be confirmed + + Returns: + The collected value + + """ + return self._prompt(message, is_password, confirm) + + @staticmethod + def _notimplemented(**kwargs): + raise NotImplementedError("this signal is not handled") + +shandler = _AMapSignals() + ## Interfaces' Actions map Parser -------------------------------------- @@ -22,9 +106,24 @@ class _AMapParser(object): global arguments, categories and actions). """ + def __init__(self, parent=None): + if parent: + self._o = parent + else: + self._o = self + self._global_conf = {} + self._conf = {} + + + ## Virtual properties + # Each parser classes must implement these properties. + + """The name of the interface for which it is the parser""" + name = None + ## Virtual methods - # Each parser classes can implement these methods. + # Each parser classes must implement these methods. @staticmethod def format_arg_names(name, full): @@ -72,13 +171,18 @@ class _AMapParser(object): raise NotImplementedError("derived class '%s' must override this method" % \ self.__class__.__name__) - def add_action_parser(self, name, **kwargs): + def add_action_parser(self, name, tid, conf=None, **kwargs): """Add a parser for an action - Create a new action and return an argument parser for it. + Create a new action and return an argument parser for it. It + should set the configuration 'conf' for the action which can be + identified by the tuple identifier 'tid' - it is usually in the + form of (namespace, category, action). Keyword arguments: - name -- The action name + - tid -- The tuple identifier of the action + - conf -- A dict of configuration for the action Returns: An ArgumentParser based object @@ -103,16 +207,194 @@ class _AMapParser(object): raise NotImplementedError("derived class '%s' must override this method" % \ self.__class__.__name__) + + ## Configuration access + + @property + def global_conf(self): + """Return the global configuration of the parser""" + return self._o._global_conf + + def get_global_conf(self, name, profile='default'): + """Get the global value of a configuration + + Return the formated global value of the configuration 'name' for + the given profile. If the configuration doesn't provide profile, + the formated default value is returned. + + Keyword arguments: + - name -- The configuration name + - profile -- The profile of the configuration + + """ + try: + if name == 'authenticator': + value = self.global_conf[name][profile] + else: + value = self.global_conf[name] + except KeyError: + return None + else: + return self._format_conf(name, value) + + def set_global_conf(self, configuration): + """Set global configuration + + Set the global configuration to use for the parser. + + Keyword arguments: + - configuration -- The global configuration + + """ + self._o._global_conf.update(self._validate_conf(configuration, True)) + + def get_conf(self, action, name): + """Get the value of an action configuration + + Return the formated value of configuration 'name' for the action + identified by 'action'. If the configuration for the action is + not set, the default one is returned. + + Keyword arguments: + - action -- An action identifier + - name -- The configuration name + + """ + try: + value = self._o._conf[action][name] + except KeyError: + return self.get_global_conf(name) + else: + return self._format_conf(name, value) + + def set_conf(self, action, configuration): + """Set configuration for an action + + Set the configuration to use for a given action identified by + 'action' which is specific to the parser. + + Keyword arguments: + - action -- The action identifier + - configuration -- The configuration for the action + + """ + self._o._conf[action] = self._validate_conf(configuration) + + + def _validate_conf(self, configuration, is_global=False): + """Validate configuration for the parser + + Return the validated configuration for the interface's actions + map parser. + + Keyword arguments: + - configuration -- The configuration to pre-format + + """ + conf = {} + + # -- 'authenficate' + try: + ifaces = configuration['authenticate'] + except KeyError: + pass + else: + if ifaces == 'all': + conf['authenticate'] = ifaces + elif ifaces == False: + conf['authenticate'] = False + elif isinstance(ifaces, list): + # Store only if authentication is needed + conf['authenticate'] = True if self.name in ifaces else False + else: + # TODO: Log error instead and tell valid values + raise MoulinetteError(22, "Invalid value '%r' for configuration 'authenticate'" % ifaces) + + # -- 'authenticator' + try: + auth = configuration['authenticator'] + except KeyError: + pass + else: + if not is_global and isinstance(auth, str): + try: + # Store parameters of the required authenticator + conf['authenticator'] = self.global_conf['authenticator'][auth] + except KeyError: + raise MoulinetteError(22, "Authenticator '%s' is not defined in global configuration" % auth) + elif is_global and isinstance(auth, dict): + if len(auth) == 0: + logging.warning('no authenticator defined in global configuration') + else: + auths = {} + for auth_name, auth_conf in auth.items(): + # Add authenticator name + auths[auth_name] = ({ 'name': auth_name, + 'type': auth_conf.get('type'), + 'help': auth_conf.get('help', None) + }, + auth_conf.get('parameters', {})) + conf['authenticator'] = auths + else: + # TODO: Log error instead and tell valid values + raise MoulinetteError(22, "Invalid value '%r' for configuration 'authenticator'" % auth) + + # -- 'argument_auth' + try: + arg_auth = configuration['argument_auth'] + except KeyError: + pass + else: + if isinstance(arg_auth, bool): + conf['argument_auth'] = arg_auth + else: + # TODO: Log error instead and tell valid values + raise MoulinetteError(22, "Invalid value '%r' for configuration 'argument_auth'" % arg_auth) + + return conf + + def _format_conf(self, name, value): + """Format a configuration value + + Return the formated value of the configuration 'name' from its + given value. + + Keyword arguments: + - name -- The name of the configuration + - value -- The value to format + + """ + if name == 'authenticator' and value: + auth_conf, auth_params = value + auth_type = auth_conf.pop('type') + + # Return authenticator configuration and an instanciator for + # it as a 2-tuple + return (auth_conf, + lambda: init_authenticator(auth_type, **auth_params)) + + return value + # CLI Actions map Parser class CLIAMapParser(_AMapParser): """Actions map's CLI Parser """ - def __init__(self, parser=None): + def __init__(self, parent=None, parser=None): + super(CLIAMapParser, self).__init__(parent) + self._parser = parser or argparse.ArgumentParser() self._subparsers = self._parser.add_subparsers() + + ## Implement virtual properties + + name = 'cli' + + + ## Implement virtual methods + @staticmethod def format_arg_names(name, full): if name[0] == '-' and full: @@ -133,9 +415,9 @@ class CLIAMapParser(_AMapParser): """ parser = self._subparsers.add_parser(name, help=category_help) - return self.__class__(parser) + return self.__class__(self, parser) - def add_action_parser(self, name, action_help=None, **kwargs): + def add_action_parser(self, name, tid, conf=None, action_help=None, **kwargs): """Add a parser for an action Keyword arguments: @@ -145,10 +427,27 @@ class CLIAMapParser(_AMapParser): A new argparse.ArgumentParser object for the action """ + if conf: + self.set_conf(tid, conf) return self._subparsers.add_parser(name, help=action_help) def parse_args(self, args, **kwargs): - return self._parser.parse_args(args) + ret = self._parser.parse_args(args) + + # Perform authentication if needed + if self.get_conf(ret._tid, 'authenticate'): + auth_conf, klass = self.get_conf(ret._tid, 'authenticator') + + # TODO: Catch errors + auth = shandler.authenticate(klass(), **auth_conf) + if not auth.is_authenticated: + # TODO: Set proper error code + raise MoulinetteError(1, _("This action need authentication")) + if self.get_conf(ret._tid, 'argument_auth') and \ + self.get_conf(ret._tid, 'authenticate') == 'all': + ret.auth = auth + + return ret # API Actions map Parser @@ -226,13 +525,20 @@ class APIAMapParser(_AMapParser): """ def __init__(self): - self._parsers = {} # dict({(method, path): _HTTPArgumentParser}) + super(APIAMapParser, self).__init__() + + self._parsers = {} # dict({(method, path): _HTTPArgumentParser}) @property def routes(self): """Get current routes""" return self._parsers.keys() + + ## Implement virtual properties + + name = 'api' + ## Implement virtual methods @@ -252,7 +558,7 @@ class APIAMapParser(_AMapParser): def add_category_parser(self, name, **kwargs): return self - def add_action_parser(self, name, api=None, **kwargs): + def add_action_parser(self, name, tid, conf=None, api=None, **kwargs): """Add a parser for an action Keyword arguments: @@ -262,12 +568,13 @@ class APIAMapParser(_AMapParser): A new _HTTPArgumentParser object for the route """ - if not api: + try: + # Validate action route + m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api) + except TypeError: raise AttributeError("the action '%s' doesn't provide api access" % name) - - # Validate action route - m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api) if not m: + # TODO: Log error raise ValueError("the action '%s' doesn't provide api access" % name) # Check if a parser already exists for the route @@ -278,6 +585,8 @@ class APIAMapParser(_AMapParser): # Create and append parser parser = _HTTPArgumentParser() self._parsers[key] = parser + if conf: + self.set_conf(key, conf) # Return the created parser return parser @@ -293,6 +602,8 @@ class APIAMapParser(_AMapParser): if route not in self.routes: raise MoulinetteError(22, "No parser for '%s %s' found" % key) + # TODO: Implement authentication + return self._parsers[route].parse_args(args) """ @@ -385,9 +696,11 @@ class AskParameter(_ExtraParameter): if arg_value: return arg_value - # Ask for the argument value - ret = raw_input(colorize(message + ': ', 'cyan')) - return ret + try: + # Ask for the argument value + return shandler.prompt(message) + except NotImplementedError: + return arg_value @classmethod def validate(klass, value, arg_name): @@ -415,12 +728,11 @@ class PasswordParameter(AskParameter): if arg_value: return arg_value - # Ask for the password - pwd1 = getpass.getpass(colorize(message + ': ', 'cyan')) - pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan')) - if pwd1 != pwd2: - raise MoulinetteError(22, _("Passwords don't match")) - return pwd1 + try: + # Ask for the password + return shandler.prompt(message, True, True) + except NotImplementedError: + return arg_value class PatternParameter(_ExtraParameter): """ @@ -552,6 +864,7 @@ class ActionsMap(object): """ def __init__(self, interface, namespaces=[], use_cache=True): self.use_cache = use_cache + self.interface = interface try: # Retrieve the interface parser @@ -563,7 +876,7 @@ class ActionsMap(object): if len(namespaces) == 0: namespaces = self.get_namespaces() - actionsmaps = {} + actionsmaps = OrderedDict() # Iterate over actions map namespaces for n in namespaces: @@ -585,7 +898,26 @@ class ActionsMap(object): # Generate parsers self.extraparser = ExtraArgumentParser(interface) - self.parser = self._construct_parser(actionsmaps) + self._parser = self._construct_parser(actionsmaps) + + @property + def parser(self): + """Return the instance of the interface's actions map parser""" + return self._parser + + def connect(self, signal, handler): + """Connect a signal to a handler + + Connect a signal emitted by actions map while processing to a + handler. Note that some signals need a return value. + + Keyword arguments: + - signal -- The name of the signal + - handler -- The method to handle the signal + + """ + global shandler + shandler.set_handler(signal, handler) def process(self, args, timeout=0, **kwargs): """ @@ -604,7 +936,7 @@ class ActionsMap(object): arguments[an] = self.extraparser.parse(an, arguments[an], parameters) # Retrieve action information - namespace, category, action = arguments.pop('_id') + namespace, category, action = arguments.pop('_tid') func_name = '%s_%s' % (category, action.replace('-', '_')) # Lock the moulinette for the namespace @@ -710,7 +1042,9 @@ class ActionsMap(object): _global = actionsmap.pop('_global', {}) # -- Parse global configuration - # TODO + if 'configuration' in _global: + # Set global configuration + top_parser.set_global_conf(_global['configuration']) # -- Parse global arguments if 'arguments' in _global: @@ -737,20 +1071,22 @@ class ActionsMap(object): # -- Parse actions for an, ap in actions.items(): - arguments = ap.pop('arguments', {}) + conf = ap.pop('configuration', None) + args = ap.pop('arguments', {}) + tid = (n, cn, an) try: # Get action parser - parser = cat_parser.add_action_parser(an, **ap) + parser = cat_parser.add_action_parser(an, tid, conf, **ap) except AttributeError: # No parser for the action continue - except ValueError: - # TODO: Log error + except ValueError as e: + logging.warning("cannot add action (%s, %s, %s): %s" % (n, cn, an, e)) continue else: - # Store action identification and add arguments - parser.set_defaults(_id=(n, cn, an)) - _add_arguments(parser, arguments) + # Store action identifier and add arguments + parser.set_defaults(_tid=tid) + _add_arguments(parser, args) return top_parser diff --git a/src/moulinette/core.py b/src/moulinette/core.py index 41e53ed8..35d52432 100644 --- a/src/moulinette/core.py +++ b/src/moulinette/core.py @@ -126,6 +126,80 @@ class Package(object): return open('%s/%s' % (self.get_cachedir(subdir), filename), mode) +# Authenticators ------------------------------------------------------- + +class _BaseAuthenticator(object): + + ## Virtual properties + # Each authenticator classes must implement these properties. + + """The name of the authenticator""" + name = None + + @property + def is_authenticated(self): + """Either the instance is authenticated or not""" + raise NotImplementedError("derived class '%s' must override this property" % \ + self.__class__.__name__) + + + ## Virtual methods + # Each authenticator classes must implement these methods. + + def authenticate(password=None, token=None): + """Attempt to authenticate + + Attempt to authenticate with given password or session token. + + Keyword arguments: + - password -- A clear text password + - token -- A session token + + Returns: + An optional session token + + """ + raise NotImplementedError("derived class '%s' must override this method" % \ + self.__class__.__name__) + + +class LDAPAuthenticator(object): + + def __init__(self, uri, base, anonymous=False): + # TODO: Initialize LDAP connection + + if anonymous: + self._authenticated = True + else: + self._authenticated = False + + + ## Implement virtual properties + + name = 'ldap' + + @property + def is_authenticated(self): + return self._authenticated + + + ## Implement virtual methods + + def authenticate(self, password=None, token=None): + # TODO: Perform LDAP authentication + if password == 'test': + self._authenticated = True + else: + raise MoulinetteError(13, _("Invalid password")) + + return self + + +def init_authenticator(_name, **kwargs): + if _name == 'ldap': + return LDAPAuthenticator(**kwargs) + + # Moulinette core classes ---------------------------------------------- class MoulinetteError(Exception): diff --git a/src/moulinette/interface/api.py b/src/moulinette/interface/api.py index 60e72228..a8136933 100644 --- a/src/moulinette/interface/api.py +++ b/src/moulinette/interface/api.py @@ -213,11 +213,11 @@ class MoulinetteAPI(object): """ if category is None: - with open('%s/doc/resources.json' % pkg.datadir) as f: + with open('%s/../doc/resources.json' % pkg.datadir) as f: return f.read() try: - with open('%s/doc/%s.json' % (pkg.datadir, category)) as f: + with open('%s/../doc/%s.json' % (pkg.datadir, category)) as f: return f.read() except IOError: return 'unknown' diff --git a/src/moulinette/interface/cli.py b/src/moulinette/interface/cli.py index e10b0a25..481413c0 100644 --- a/src/moulinette/interface/cli.py +++ b/src/moulinette/interface/cli.py @@ -1,5 +1,129 @@ # -*- coding: utf-8 -*- +import getpass + +from ..core import MoulinetteError + +# CLI helpers ---------------------------------------------------------- + +colors_codes = { + 'red' : 31, + 'green' : 32, + 'yellow': 33, + 'cyan' : 34, + 'purple': 35 +} + +def colorize(astr, color): + """Colorize a string + + Return a colorized string for printing in shell with style ;) + + Keyword arguments: + - astr -- String to colorize + - color -- Name of the color + + """ + return '\033[{:d}m\033[1m{:s}\033[m'.format(colors_codes[color], astr) + +def pretty_print_dict(d, depth=0): + """Print a dictionary recursively + + Print a dictionary recursively with colors to the standard output. + + Keyword arguments: + - d -- The dictionary to print + - depth -- The recursive depth of the dictionary + + """ + for k,v in sorted(d.items(), key=lambda x: x[0]): + k = colorize(str(k), 'purple') + if isinstance(v, list) and len(v) == 1: + v = v[0] + if isinstance(v, dict): + print((" ") * depth + ("%s: " % str(k))) + pretty_print_dict(v, depth+1) + elif isinstance(v, list): + print((" ") * depth + ("%s: " % str(k))) + for key, value in enumerate(v): + if isinstance(value, tuple): + pretty_print_dict({value[0]: value[1]}, depth+1) + elif isinstance(value, dict): + pretty_print_dict({key: value}, depth+1) + else: + print((" ") * (depth+1) + "- " +str(value)) + else: + if not isinstance(v, basestring): + v = str(v) + print((" ") * depth + "%s: %s" % (str(k), v)) + + +# Moulinette Interface ------------------------------------------------- + class MoulinetteCLI(object): - # TODO: Implement this class - pass + """Moulinette command-line Interface + + Initialize an interface connected to the standard input and output + stream which allows to process moulinette action. + + Keyword arguments: + - actionsmap -- The interface relevant ActionsMap instance + + """ + def __init__(self, actionsmap): + # Connect signals to handlers + actionsmap.connect('authenticate', self._do_authenticate) + actionsmap.connect('prompt', self._do_prompt) + + self.actionsmap = actionsmap + + def run(self, args): + """Run the moulinette + + Process the action corresponding to the given arguments 'args' + and print the result. + + Keyword arguments: + - args -- A list of argument strings + + """ + try: + ret = self.actionsmap.process(args, timeout=5) + except KeyboardInterrupt, EOFError: + raise MoulinetteError(125, _("Interrupted")) + + if isinstance(ret, dict): + pretty_print_dict(ret) + elif ret: + print(ret) + + + ## Signals handlers + + def _do_authenticate(self, authenticator, name, help): + """Process the authentication + + Handle the actionsmap._AMapSignals.authenticate signal. + + """ + # TODO: Allow token authentication? + msg = help or _("Password") + return authenticator.authenticate(password=self._do_prompt(msg, True, False)) + + def _do_prompt(self, message, is_password, confirm): + """Prompt for a value + + Handle the actionsmap._AMapSignals.prompt signal. + + """ + if is_password: + prompt = lambda m: getpass.getpass(colorize(_('%s: ') % m, 'cyan')) + else: + prompt = lambda m: raw_input(colorize(_('%s: ') % m, 'cyan')) + value = prompt(message) + + if confirm: + if prompt(_('Retype %s: ') % message) != value: + raise MoulinetteError(22, _("Values don't match")) + + return value