From 5837e4893f584da1ca812169e278090c7a6461fe Mon Sep 17 00:00:00 2001 From: Samuel Dobron Date: Thu, 7 Nov 2024 11:22:46 +0100 Subject: [PATCH] Containerized controller Adding support (and docs) of running LNST controller in podman/docker container. --- container_files/controller/Dockerfile | 32 +++++ .../controller/container_runner.py | 108 ++++++++++++++ container_files/controller/entrypoint.sh | 3 + container_files/controller/pool/.gitkeep | 0 docs/source/extensions.rst | 136 +++++++++++++++++- docs/source/installation.rst | 11 +- 6 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 container_files/controller/Dockerfile create mode 100644 container_files/controller/container_runner.py create mode 100755 container_files/controller/entrypoint.sh create mode 100644 container_files/controller/pool/.gitkeep diff --git a/container_files/controller/Dockerfile b/container_files/controller/Dockerfile new file mode 100644 index 000000000..491343e39 --- /dev/null +++ b/container_files/controller/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +FROM fedora:41 + +RUN dnf install -y initscripts \ + iputils \ + python3.9 \ + python-pip \ + gcc \ + python-devel \ + libxml2-devel \ + libxslt-devel \ + libnl3 \ + lksctp-tools-devel \ + git \ + libnl3-devel && \ + curl -sSL https://install.python-poetry.org | \ + python3 - --version 1.8.3 + +RUN mkdir -p /root/.lnst +COPY . /lnst +COPY container_files/controller/pool /root/.lnst/pool + +RUN cd /lnst && \ + /root/.local/bin/poetry config virtualenvs.path /root/lnst_venv && \ + /root/.local/bin/poetry config virtualenvs.in-project false && \ + /root/.local/bin/poetry install +# setting in-project to false to prevent poetry from +# using in-project .venv which might be present if +# user has mounted host-machine's lnst dir to /lnst + +WORKDIR /lnst +CMD ["/lnst/container_files/controller/entrypoint.sh"] diff --git a/container_files/controller/container_runner.py b/container_files/controller/container_runner.py new file mode 100644 index 000000000..bb8ab448f --- /dev/null +++ b/container_files/controller/container_runner.py @@ -0,0 +1,108 @@ +import os +import sys +import traceback +from typing import Any, Type, Optional + +from lnst.Recipes.ENRT import * +from lnst.Controller.Recipe import BaseRecipe +from lnst.Controller.Controller import Controller +from lnst.Controller.RecipeResults import ResultLevel, ResultType +from lnst.Controller.MachineMapper import ContainerMapper +from lnst.Controller.ContainerPoolManager import ContainerPoolManager + +from lnst.Controller.RunSummaryFormatters import * +from lnst.Controller.RunSummaryFormatters.RunSummaryFormatter import RunSummaryFormatter + + +class ContainerRunner: + """This class is responsible for running the LNST controller in a container. + + Environment variables: + + * DEBUG: Set to 1 to enable debug mode + * RECIPE: Name of the recipe class to run + * RECIPE_PARAMS: Parameters to pass to the recipe class + * FORMATTERS: List of formatters to use + * MULTIMATCH: Set to 1 to enable multimatch mode + + Agents in containers-specific environment variables: + + * PODMAN_URI: URI of the Podman socket + * IMAGE_NAME: Name of the container image + """ + + def __init__(self) -> None: + self._controller = Controller(**self._parse_controller_params()) + self._recipe_params: dict[str, Any] = self._parse_recipe_params() + + if not os.getenv("RECIPE"): + raise ValueError("RECIPE environment variable is not set") + self._recipe_cls: Type[BaseRecipe] = eval(os.getenv("RECIPE", "")) + self._recipe: Optional[BaseRecipe] = None + + self._formatters: list[Type[RunSummaryFormatter]] = self._parse_formatters() + + def _parse_controller_params(self) -> dict: + params = { + "debug": bool(os.getenv("DEBUG", 0)), + } + + if "PODMAN_URI" in os.environ: + return params | { + "podman_uri": os.getenv("PODMAN_URI"), + "image": os.getenv("IMAGE_NAME", "lnst"), + "network_plugin": "cni", + "poolMgr": ContainerPoolManager, + "mapper": ContainerMapper, + } + + return params + + def _parse_recipe_params(self) -> dict[str, Any]: + params = {} + for param in os.getenv("RECIPE_PARAMS", "").split(";"): + if not param: + continue + key, value = param.split("=") + params[key] = eval(value) + + return params + + def _parse_formatters(self) -> list[Type[RunSummaryFormatter]]: + return [ + eval(formatter) + for formatter in os.getenv("FORMATTERS", "").split(";") + if formatter + ] + + def run(self) -> ResultType: + """Initialize recipe class with parameters provided in `RECIPE_PARAMS` + and execute. Function returns overall result. + """ + overall_result = ResultType.PASS + + try: + self._recipe = self._recipe_cls(**self._recipe_params) + self._controller.run( + self._recipe, multimatch=bool(os.getenv("MULTIMATCH", False)) + ) + except Exception: + print("LNST Controller crashed with an exception:", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + exit(ResultType.FAIL) + + for formatter in self._formatters: + fmt = formatter(level=ResultLevel.IMPORTANT) + for run in self._recipe.runs: + print(fmt.format_run(run)) + overall_result = ResultType.max_severity( + overall_result, run.overall_result + ) + + return overall_result + + +if __name__ == "__main__": + runner = ContainerRunner() + exit_code = 0 if runner.run() == ResultType.PASS else 1 + exit(exit_code) diff --git a/container_files/controller/entrypoint.sh b/container_files/controller/entrypoint.sh new file mode 100755 index 000000000..b73c7ff26 --- /dev/null +++ b/container_files/controller/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +PYTHON_PATH=$(/root/.local/bin/poetry env info -p)/bin/python +exec "$PYTHON_PATH" /lnst/container_files/controller/container_runner.py diff --git a/container_files/controller/pool/.gitkeep b/container_files/controller/pool/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docs/source/extensions.rst b/docs/source/extensions.rst index be4a969a8..3dba42ad9 100644 --- a/docs/source/extensions.rst +++ b/docs/source/extensions.rst @@ -1,11 +1,46 @@ +.. _containerized: + LNST in containers -^^^^^^^^^^^^^^^^^^ -LNST supports running agents in containers at the host machine. +================== +LNST supports running both agents and controller in containers at the host machine. +LNST uses custom RPC protocol to communicate between controller and agents which +uses separate network interface to not interfere with test and so, it doesn't matter +where controller and agents are running as long as they can communicate. + +With support of running LNST in containers your machine setup might look like this: + +1. both controller and agents are running on your baremetal machines + +2. controller is running on your baremetal machine and agents are running in containers + +3. controller is running in container and agents are running on your baremetal machine + +4. both controller and agents are running in containers + + +This article describes how to run individual parts of LNST in containers. If you want +to run either controller or agents on baremetal see :ref:`installation` section. + + +Common requirements +------------------- +We recommend to use Podman as this was developed and tested with Podman but should +work with Docker as well. + +If you want to use Podman, follow installation steps on +`official Podman installation page `_. + + +.. _containerized_agents: + +Containerized agents +-------------------- + Containers and networks are dynamically created based on recipe requirements. Containers are also automatically connected to networks. Requirements ------------- +```````````` The first requirement is **Podman**, follow installation steps on `official Podman installation page `_. @@ -65,7 +100,7 @@ Socket URL could be found at the top of logs generated by this command. The usual URL is `unix:/run/podman/podman.sock` Build LNST agent image ----------------------- +`````````````````````` Currently, LNST does not support automated building, so build LNST agent machine image. @@ -103,10 +138,101 @@ Only initialization of `Controller()` object has to be changed: And run the script. Classes documentation ---------------------- +````````````````````` .. autoclass:: lnst.Controller.MachineMapper.ContainerMapper :members: .. automodule:: lnst.Controller.ContainerPoolManager :members: ContainerPoolManager + + +Containerized controller +------------------------ + +Using containerized agents +`````````````````````````` + +Before proceeding with containerized controller, you need to build LNST +agent image (see :ref:`containerized_agents`) you also need to provide +following parameters as environment variables to controller container: + +* `PODMAN_URI` - URI to Podman socket, e.g. `tcp://localhost[:port]`. This needs to be accessible from container. +* `IMAGE_NAME` - name of the image you built for agents. + +It expects that you use CNI as network backend for Podman. + + +Using baremetal agents +`````````````````````` +Firstly, you need to prepare machine XMLs if you decide to run agents on baremetal +machines (see :ref:`machines-pool`). Instead of putting them into `~/.lnst/pool` +directory, you need to put them into `container_files/controller/pool` directory. +Machine XMLs are copied to container during build process from +`container_files/controller/pool`. +Podman doesn't support copying files located outside of build context, so you +need to put it to LNST project directory. + +.. note:: + To avoid having to deal with pool files you can simply mount your `~/.lnst/pool` directory + to `/root/.lnst/pool/` in the container (read-only access is sufficient). + + +Build and run controller +```````````````````````` + +Build the controller image: + +.. code-block:: bash + + cd your_lnst_project_directory + podman build . -t lnst_controller -f container_files/controller/Dockerfile + +This will copy pool files to `/root/.lnst/pool/` in container and LNST from +`your_lnst_project_directory` to `/lnst` in container. + +.. note:: + If you want to avoid rebuilding the image every time you change your LNST project (e.g. during + development), you can mount `your_lnst_project_directory` to `/lnst` in container. The LNST's + virtual environment is located outside of `/lnst/` directory, so if your changes requires + reinstallation fo LNST and/or its dependencies, you need to rebuild the image. + + +Before running the container, you need to provide environment variables: + +* `RECIPE` - name of recipe class, these are loaded from `lnst.Recipes.ENRT` as wildcard import +* `RECIPE_PARAMS` - `;` separated list of parameters for recipe. Each parameter is in format `key=value` +* `FORMATTERS` - `;` separated list of formatters, these are loaded from `lnst.Formatters` as wildcard import +* `DEBUG` - enables/disables LNST's debug mode + +.. warning:: + `RECIPE`, `RECIPE_PARAMS` and `FORMATTERS` are parsed using Python's `eval` function, + which is a security risk. Make sure you trust the source of these variables. + +Now, you can run the controller: + +.. code-block:: bash + + podman run -e RECIPE=SimpleNetworkRecipe -e RECIPE_PARAMS="perf_iterations=1;perf_duration=10" -e DEBUG=1 --rm --name lnst_controller lnst_controller + + +.. note:: + Podman containers are by default NATed, so you may need to use some other `--network` mode + to make agent machines reachable from controller container. If agent machines are reachable + from your host machine, `--network=host` should do the job. Read + `Podman's documentation `_ + first. + + +Or you can run more complex recipes: + +.. code-block:: bash + + podman run -e RECIPE=XDPDropRecipe -e RECIPE_PARAMS="perf_iterations=1;perf_tool_cpu=[0,1];multi_dev_interrupt_config={'host1':{'eth0':{'cpus':[0],'policy':'round-robin'}}}" --rm --name lnst_controller lnst_controller + + +Classes documentation +````````````````````` +.. autoclass:: container_files.controller.container_runner.ContainerRunner + :members: + diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 7ac5aabd5..7a6f6b576 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -1,3 +1,5 @@ +.. _installation: + Install LNST and Hello world ============================ @@ -11,7 +13,9 @@ LNST is logically split into two separate application use cases: Codebases for both use cases are developed in this repository and as we currently don't have a stable release yet, the recommended method of -installation involves the following steps: +installation is either to install both Agents and Controller on your +local machines or to use docker/podman. Manual installation on your +machine involves the following steps: .. code-block:: bash @@ -20,6 +24,8 @@ installation involves the following steps: poetry install +For installation of containerized version, see :ref:`containerized`. + This installs both the Controller and the Agent code, and you'll need to run this on all the test machines that you want to use as well as the machine which you want to use as the Controller. Optionally a Controller and a Agent CAN run @@ -112,6 +118,9 @@ configured pools, since we didn't configure any yet, this is quite expected. But running this script did take care of creating a default configuration file and directory where we'll now be able to create our machine pool. + +.. _machines-pool: + Creating a simple machine pool ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^