Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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"
Exemplo n.º 4
0
    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
Exemplo n.º 5
0
    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
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
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)