def test_actions_map_import_error(mocker): from moulinette.interfaces.api import ActionsMapParser amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser()) from moulinette.core import MoulinetteLock mocker.patch.object(MoulinetteLock, "_is_son_of", return_value=False) orig_import = __import__ def import_mock(name, globals={}, locals={}, fromlist=[], level=-1): if name == "moulitest.testauth": mocker.stopall() raise ImportError("Yoloswag") return orig_import(name, globals, locals, fromlist, level) mocker.patch("builtins.__import__", side_effect=import_mock) with pytest.raises(MoulinetteError) as exception: amap.process({}, timeout=30, route=("GET", "/test-auth/none")) expected_msg = "unable to load function {}.{} because: {}".format( "moulitest", "testauth_none", "Yoloswag", ) assert expected_msg in str(exception)
def test_actions_map_unknown_authenticator(monkeypatch, tmp_path): from moulinette.interfaces.api import ActionsMapParser amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser()) with pytest.raises(MoulinetteError) as exception: amap.get_authenticator("unknown") assert "No module named" in str(exception)
def test_actions_map_cli(): from moulinette.interfaces.cli import ActionsMapParser import argparse top_parser = argparse.ArgumentParser(add_help=False) top_parser.add_argument( "--debug", action="store_true", default=False, help="Log and print debug messages", ) parser = ActionsMapParser(top_parser=top_parser) amap = ActionsMap("test/actionsmap/moulitest.yml", parser) assert amap.namespace == "moulitest" assert amap.default_authentication == "dummy" assert "testauth" in amap.parser._subparsers.choices assert "none" in amap.parser._subparsers.choices["testauth"]._actions[ 1].choices assert "subcat" in amap.parser._subparsers.choices["testauth"]._actions[ 1].choices assert ("default" in amap.parser._subparsers.choices["testauth"]. _actions[1].choices["subcat"]._actions[1].choices) assert parser.auth_method(["testauth", "default"]) == "dummy" assert parser.auth_method(["testauth", "only-api"]) is None assert parser.auth_method(["testauth", "only-cli"]) == "dummy"
def __init__( self, top_parser=None, load_only_category=None, actionsmap=None, locales_dir=None, ): # Set user locale m18n.set_locale(get_locale()) self.actionsmap = ActionsMap( actionsmap, ActionsMapParser(top_parser=top_parser), load_only_category=load_only_category, ) Moulinette._interface = self
def __init__(self, routes={}, actionsmap=None): actionsmap = ActionsMap(actionsmap, ActionsMapParser()) # Attempt to retrieve log queues from an APIQueueHandler handler = log.getHandlersByClass(APIQueueHandler, limit=1) if handler: log_queues = handler.queues handler.actionsmap = actionsmap # TODO: Return OK to 'OPTIONS' xhr requests (l173) app = Bottle(autojson=True) # Wrapper which sets proper header def apiheader(callback): def wrapper(*args, **kwargs): response.set_header("Access-Control-Allow-Origin", "*") return callback(*args, **kwargs) return wrapper # Attempt to retrieve and set locale def api18n(callback): def wrapper(*args, **kwargs): try: locale = request.params.pop("locale") except KeyError: locale = m18n.default_locale m18n.set_locale(locale) return callback(*args, **kwargs) return wrapper # Install plugins app.install(filter_csrf) app.install(apiheader) app.install(api18n) actionsmapplugin = _ActionsMapPlugin(actionsmap, log_queues) app.install(actionsmapplugin) self.authenticate = actionsmapplugin.authenticate self.display = actionsmapplugin.display self.prompt = actionsmapplugin.prompt # Append additional routes # TODO: Add optional authentication to those routes? for (m, p), c in routes.items(): app.route(p, method=m, callback=c, skip=["actionsmap"]) self._app = app Moulinette._interface = self
def test_actions_map_api(): from moulinette.interfaces.api import ActionsMapParser parser = ActionsMapParser() amap = ActionsMap("test/actionsmap/moulitest.yml", parser) assert amap.namespace == "moulitest" assert amap.default_authentication == "dummy" assert ("GET", "/test-auth/default") in amap.parser.routes assert ("POST", "/test-auth/subcat/post") in amap.parser.routes assert parser.auth_method(None, ("GET", "/test-auth/default")) == "dummy" assert parser.auth_method(None, ("GET", "/test-auth/only-api")) == "dummy" assert parser.auth_method(None, ("GET", "/test-auth/only-cli")) is None
def init_interface(name, kwargs={}, actionsmap={}): """Return a new interface instance Retrieve the given interface module and return a new instance of its Interface class. It is initialized with arguments 'kwargs' and connected to 'actionsmap' if it's an ActionsMap object, otherwise a new ActionsMap instance will be initialized with arguments 'actionsmap'. Keyword arguments: - name -- The interface name - kwargs -- A dict of arguments to pass to Interface - actionsmap -- Either an ActionsMap instance or a dict of arguments to pass to ActionsMap """ from moulinette.actionsmap import ActionsMap try: mod = import_module('moulinette.interfaces.%s' % name) except ImportError: logger.exception("unable to load interface '%s'", name) raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) else: try: # Retrieve interface classes parser = mod.ActionsMapParser interface = mod.Interface except AttributeError: logger.exception("unable to retrieve classes of interface '%s'", name) raise MoulinetteError(errno.EIO, m18n.g('error_see_log')) # Instantiate or retrieve ActionsMap if isinstance(actionsmap, dict): amap = ActionsMap(actionsmap.pop('parser', parser), **actionsmap) elif isinstance(actionsmap, ActionsMap): amap = actionsmap else: logger.error("invalid actionsmap value %r", actionsmap) raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log')) return interface(amap, **kwargs)
class Interface: """Command-line Interface for the moulinette Initialize an interface connected to the standard input/output stream and to a given actions map. Keyword arguments: - actionsmap -- The ActionsMap instance to connect to """ type = "cli" def __init__( self, top_parser=None, load_only_category=None, actionsmap=None, locales_dir=None, ): # Set user locale m18n.set_locale(get_locale()) self.actionsmap = ActionsMap( actionsmap, ActionsMapParser(top_parser=top_parser), load_only_category=load_only_category, ) Moulinette._interface = self def run(self, args, output_as=None, timeout=None): """Run the moulinette Process the action corresponding to the given arguments 'args' and print the result. Keyword arguments: - args -- A list of argument strings - output_as -- Output result in another format. Possible values: - json: return a JSON encoded string - plain: return a script-readable output - none: do not output the result - timeout -- Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock """ if output_as and output_as not in ["json", "plain", "none"]: raise MoulinetteValidationError("invalid_usage") try: ret = self.actionsmap.process(args, timeout=timeout) except (KeyboardInterrupt, EOFError): raise MoulinetteError("operation_interrupted") if ret is None or output_as == "none": return # Format and print result if output_as: if output_as == "json": import json print(json.dumps(ret, cls=JSONExtendedEncoder)) else: plain_print_dict(ret) elif isinstance(ret, dict): pretty_print_dict(ret) else: print(ret) def authenticate(self, authenticator): # Hmpf we have no-use case in yunohost anymore where we need to auth # because everything is run as root ... # I guess we could imagine some yunohost-independant use-case where # moulinette is used to create a CLI for non-root user that needs to # auth somehow but hmpf -.- msg = m18n.g("password") credentials = self.prompt(msg, True, False, color="yellow") return authenticator.authenticate_credentials(credentials=credentials) def prompt( self, message, is_password=False, confirm=False, color="blue", prefill="", is_multiline=False, autocomplete=[], help=None, ): """Prompt for a value Keyword arguments: - color -- The color to use for prompting message """ if not os.isatty(1): raise MoulinetteError("Not a tty, can't do interactive prompts", raw_msg=True) def _prompt(message): if not is_multiline: import prompt_toolkit from prompt_toolkit.completion import WordCompleter from prompt_toolkit.styles import Style autocomplete_ = WordCompleter(autocomplete) style = Style.from_dict({ "": "", "message": f"#ansi{color} bold", }) if help: def bottom_toolbar(): return [("class:", help)] else: bottom_toolbar = None colored_message = [ ("class:message", message), ("class:", ": "), ] return prompt_toolkit.prompt( colored_message, bottom_toolbar=bottom_toolbar, style=style, default=prefill, completer=autocomplete_, complete_while_typing=True, is_password=is_password, ) else: while True: value = input( colorize(m18n.g("edit_text_question", message), color)) value = value.lower().strip() if value in ["", "n", "no"]: return prefill elif value in ["y", "yes"]: break initial_message = prefill.encode("utf-8") with tempfile.NamedTemporaryFile(suffix=".tmp") as tf: tf.write(initial_message) tf.flush() call(["editor", tf.name]) tf.seek(0) edited_message = tf.read() return edited_message.decode("utf-8") value = _prompt(message) if confirm: m = message[0].lower() + message[1:] if _prompt(m18n.g("confirm", prompt=m)) != value: raise MoulinetteValidationError("values_mismatch") return value def display(self, message, style="info"): # i18n: info """Display a message""" if style == "success": print("{} {}".format(colorize(m18n.g("success"), "green"), message)) elif style == "warning": print("{} {}".format(colorize(m18n.g("warning"), "yellow"), message)) elif style == "error": print("{} {}".format(colorize(m18n.g("error"), "red"), message)) else: print(message)