This patch adds the vmtest-tool subdirectory under contrib which tests BPF programs under a live kernel using a QEMU VM. It automatically builds the specified kernel version with eBPF support enabled and stores it under "~/.vmtest-tool", which is reused for future invocations.
It can also compile BPF C source files or BPF bytecode objects and test them against the kernel verifier for errors. When a BPF program is rejected by the kernel verifier, the verifier logs are displayed. $ python3 main.py -k 6.15 --bpf-src assets/ebpf-programs/fail.c BPF program failed to load Verifier logs: btf_vmlinux is malformed 0: R1=ctx() R10=fp0 0: (81) r0 = *(s32 *)(r10 +4) invalid read from stack R10 off=4 size=4 processed 1 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0 See the README for more examples. The script uses vmtest (https://github.com/danobi/vmtest) to boot the VM and run the program. By default, it uses the host's root ("/") as the VM rootfs via the 9p filesystem, so only the kernel is replaced during testing. Tested with Python 3.10 and above. contrib/ChangeLog: * vmtest-tool/.gitignore: New file. * vmtest-tool/.pre-commit-config.yaml: New file. * vmtest-tool/.python-version: New file. * vmtest-tool/README: New file. * vmtest-tool/__init__.py: New file. * vmtest-tool/bpf.py: New file. * vmtest-tool/config.py: New file. * vmtest-tool/kernel.py: New file. * vmtest-tool/main.py: New file. * vmtest-tool/pyproject.toml: New file. * vmtest-tool/requirements-dev.txt: New file. * vmtest-tool/tests/test_cli.py: New file. * vmtest-tool/utils.py: New file. * vmtest-tool/uv.lock: New file. * vmtest-tool/vm.py: New file. Signed-off-by: Piyush Raj <piyushraj92...@gmail.com> --- contrib/vmtest-tool/.gitignore | 23 ++ contrib/vmtest-tool/.pre-commit-config.yaml | 32 ++ contrib/vmtest-tool/.python-version | 1 + contrib/vmtest-tool/README | 75 ++++ contrib/vmtest-tool/__init__.py | 0 contrib/vmtest-tool/bpf.py | 193 ++++++++++ contrib/vmtest-tool/config.py | 11 + contrib/vmtest-tool/kernel.py | 209 +++++++++++ contrib/vmtest-tool/main.py | 101 ++++++ contrib/vmtest-tool/pyproject.toml | 36 ++ contrib/vmtest-tool/requirements-dev.txt | 198 ++++++++++ contrib/vmtest-tool/tests/test_cli.py | 170 +++++++++ contrib/vmtest-tool/utils.py | 26 ++ contrib/vmtest-tool/uv.lock | 380 ++++++++++++++++++++ contrib/vmtest-tool/vm.py | 154 ++++++++ 15 files changed, 1609 insertions(+) create mode 100644 contrib/vmtest-tool/.gitignore create mode 100644 contrib/vmtest-tool/.pre-commit-config.yaml create mode 100644 contrib/vmtest-tool/.python-version create mode 100644 contrib/vmtest-tool/README create mode 100644 contrib/vmtest-tool/__init__.py create mode 100644 contrib/vmtest-tool/bpf.py create mode 100644 contrib/vmtest-tool/config.py create mode 100644 contrib/vmtest-tool/kernel.py create mode 100644 contrib/vmtest-tool/main.py create mode 100644 contrib/vmtest-tool/pyproject.toml create mode 100644 contrib/vmtest-tool/requirements-dev.txt create mode 100644 contrib/vmtest-tool/tests/test_cli.py create mode 100644 contrib/vmtest-tool/utils.py create mode 100644 contrib/vmtest-tool/uv.lock create mode 100644 contrib/vmtest-tool/vm.py diff --git a/contrib/vmtest-tool/.gitignore b/contrib/vmtest-tool/.gitignore new file mode 100644 index 00000000000..9cec867f093 --- /dev/null +++ b/contrib/vmtest-tool/.gitignore @@ -0,0 +1,23 @@ +.gitignore_local + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# Unit test / coverage reports +.pytest_cache/ + + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Ruff stuff: +.ruff_cache/ diff --git a/contrib/vmtest-tool/.pre-commit-config.yaml b/contrib/vmtest-tool/.pre-commit-config.yaml new file mode 100644 index 00000000000..26cb68389ba --- /dev/null +++ b/contrib/vmtest-tool/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +fail_fast: true +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.11.13 + hooks: + # Run the linter. + - id: ruff-check + args: [ --fix ] + # Run the formatter. + - id: ruff-format +- repo: local + hooks: + - id: uv-export + name: uv-export + description: "update requirements-dev.txt" + entry: uv export + language: system + files: ^contrib/vmtest-tool/uv\.lock$ + args: ["--directory=contrib/vmtest-tool","--frozen", "--output-file=requirements-dev.txt"] + pass_filenames: false + additional_dependencies: [] + minimum_pre_commit_version: "2.9.2" + - id: pytest-check + name: pytest-check + stages: [pre-commit] + types: [python] + entry: bash -c 'cd contrib/vmtest-tool && export PATH="$PWD/bin:$PATH" && uv run pytest' + language: system + pass_filenames: false + always_run: true + verbose: true diff --git a/contrib/vmtest-tool/.python-version b/contrib/vmtest-tool/.python-version new file mode 100644 index 00000000000..24ee5b1be99 --- /dev/null +++ b/contrib/vmtest-tool/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/contrib/vmtest-tool/README b/contrib/vmtest-tool/README new file mode 100644 index 00000000000..550950d0b63 --- /dev/null +++ b/contrib/vmtest-tool/README @@ -0,0 +1,75 @@ +This directory contains a Python script to run BPF programs or shell commands +under a live Linux kernel using QEMU virtual machines. + +USAGE +===== + +To run a shell command inside a live kernel VM: + + python main.py -k 6.15 -r / -c "uname -a" + +To run a BPF source file in the VM: + + python main.py --kernel-image 6.15 --bpf-src fail.c + +To run a precompiled BPF object file: + + python main.py --kernel-image 6.15 --bpf-obj fail.bpf.o + +The tool will download and build the specified kernel version from: + + https://www.kernel.org/pub/linux/kernel + +A prebuilt `bzImage` can be supplied using the `--kernel-image` flag. + +NOTE +==== +- Only x86_64 is supported +- Only "/" (the root filesystem) is currently supported as the VM rootfs when +running or testing BPF programs using `--bpf-src` or `--bpf-obj`. + +DEPENDENCIES +============ + +- Python >= 3.10 +- vmtest >= v0.18.0 (https://github.com/danobi/vmtest) + - QEMU + - qemu-guest-agent + +For compiling kernel +- https://docs.kernel.org/process/changes.html#current-minimal-requirements +For compiling and loading BPF programs: + +- libbpf +- bpftool +- gcc-bpf-unknown-none + (https://gcc.gnu.org/wiki/BPFBackEnd#Where_to_find_GCC_BPF) +- vmlinux.h + Can be generated using: + + bpftool btf dump file /sys/kernel/btf/vmlinux format c > \ + /usr/local/include/vmlinux.h + + Or downloaded from https://github.com/libbpf/vmlinux.h/tree/main + +DEVELOPMENT +=========== + +This tool uses `uv` (https://github.com/astral-sh/uv) for virtual environment +and dependency management. + +To install development dependencies: + + uv sync + +To run the test suite: + + uv run pytest + +A `.pre-commit-config.yaml` is provided to assist with development. +Pre-commit hooks will auto-generate `requirements-dev.txt` and lint python +files. + +To enable pre-commit hooks: + + uv run pre-commit install diff --git a/contrib/vmtest-tool/__init__.py b/contrib/vmtest-tool/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/contrib/vmtest-tool/bpf.py b/contrib/vmtest-tool/bpf.py new file mode 100644 index 00000000000..291e251e64c --- /dev/null +++ b/contrib/vmtest-tool/bpf.py @@ -0,0 +1,193 @@ +import re +import subprocess +import logging +from pathlib import Path +import tempfile +import utils +import config + +logger = logging.getLogger(__name__) +# https://git.sr.ht/~brianwitte/gcc-bpf-example/tree/master/item/Makefile +BPF_INCLUDES = ["-I/usr/local/include", "-I/usr/include"] + + +def generate_sanitized_name(path): + """generate sanitized c variable name""" + name = re.sub(r"\W", "_", path.stem) + if name and name[0].isdigit(): + name = "_" + name + return name + + +class BPFProgram: + tmp_base_dir = tempfile.TemporaryDirectory(prefix="vmtest") + tmp_base_dir_path = Path(tmp_base_dir.name) + + def __init__( + self, + source_path: Path | None = None, + bpf_bytecode_path: Path | None = None, + use_temp_dir: bool = False, + ): + path = source_path or bpf_bytecode_path + self.name = generate_sanitized_name(path) + self.build_dir = self.__class__.tmp_base_dir_path / "ebpf_programs" / self.name + + if source_path: + self.bpf_src = source_path + self.bpf_obj = self.build_dir / f"{self.name}.bpf.o" + else: + self.bpf_obj = bpf_bytecode_path + self.build_dir.mkdir(parents=True, exist_ok=True) + self.bpf_skel = self.build_dir / f"{self.name}.skel.h" + self.loader_src = self.build_dir / f"{self.name}-loader.c" + self.output = self.build_dir / f"{self.name}.o" + + @classmethod + def from_source(cls, source_path: Path): + self = cls(source_path=source_path) + self._compile_bpf() + self._compile_from_bpf_bytecode() + return self.output + + @classmethod + def from_bpf_obj(cls, obj_path: Path): + self = cls(bpf_bytecode_path=obj_path) + self._compile_from_bpf_bytecode() + return self.output + + def _compile_from_bpf_bytecode(self): + self._generate_skeleton() + self._compile_loader() + + def _compile_bpf(self): + """Compile the eBPF program using gcc""" + logger.info(f"Compiling eBPF source: {self.bpf_src}") + cmd = [ + "bpf-unknown-none-gcc", + "-g", + "-O2", + "-std=gnu17", + f"-D__TARGET_ARCH_{config.ARCH}", + "-gbtf", + "-Wno-error=attributes", + "-Wno-error=address-of-packed-member", + "-Wno-compare-distinct-pointer-types", + *BPF_INCLUDES, + "-c", + str(self.bpf_src), + "-o", + str(self.bpf_obj), + ] + logger.debug("".join(cmd)) + utils.run_command(cmd) + logger.info(f"eBPF compiled: {self.bpf_obj}") + + def _generate_skeleton(self): + """Generate the BPF skeleton header using bpftool""" + logger.info(f"Generating skeleton: {self.bpf_skel}") + cmd = ["bpftool", "gen", "skeleton", str(self.bpf_obj), "name", self.name] + try: + result = utils.run_command(cmd) + with open(self.bpf_skel, "w") as f: + f.write(result.stdout) + logger.info("Skeleton generated.") + logger.debug("bpftool output:\n%s", result.stdout) + if result.stderr: + logger.debug("bpftool output:\n%s", result.stderr) + except subprocess.CalledProcessError as e: + logger.error("Failed to generate skeleton.") + logger.error("stdout:\n%s", e.stdout) + logger.error("stderr:\n%s", e.stderr) + raise + + def _compile_loader(self): + """Compile the C loader program""" + self.generate_loader() + logger.info(f"Compiling loader: {self.loader_src}") + cmd = [ + "gcc", + "-g", + "-Wall", + "-I", + str(self.build_dir), + str(self.loader_src), + "-lelf", + "-lz", + "-lbpf", + "-o", + str(self.output), + ] + utils.run_command(cmd) + logger.info("Compilation complete") + + def generate_loader(self): + """ + Generate a loader C file for the given BPF skeleton. + + Args: + bpf_name (str): Name of the BPF program (e.g. "prog"). + output_path (str): Path to write loader.c. + """ + skeleton_header = f"{self.name}.skel.h" + loader_code = f"""\ + #include <stdio.h> + #include <stdlib.h> + #include <signal.h> + #include <unistd.h> + #include <bpf/libbpf.h> + #include "{skeleton_header}" + + #define LOG_BUF_SIZE 1024 * 1024 + + static volatile sig_atomic_t stop; + static char log_buf[LOG_BUF_SIZE]; + + void handle_sigint(int sig) {{ + stop = 1; + }} + + int main() {{ + struct {self.name} *skel; + struct bpf_program *prog; + int err; + + signal(SIGINT, handle_sigint); + + skel = {self.name}__open(); // STEP 1: open only + if (!skel) {{ + fprintf(stderr, "Failed to open BPF skeleton\\n"); + return 1; + }} + + // STEP 2: Get the bpf_program object for the main program + bpf_object__for_each_program(prog, skel->obj) {{ + bpf_program__set_log_buf(prog, log_buf, sizeof(log_buf)); + bpf_program__set_log_level(prog, 1); // optional: verbose logs + }} + + // STEP 3: Load the program (this will trigger verifier log output) + err = {self.name}__load(skel); + fprintf( + stderr, + "--- Verifier log start ---\\n" + "%s\\n" + "--- Verifier log end ---\\n", + log_buf + ); + if (err) {{ + fprintf(stderr, "Failed to load BPF skeleton: %d\\n", err); + {self.name}__destroy(skel); + return 1; + }} + + printf("BPF program loaded successfully.\\n"); + + {self.name}__destroy(skel); + return 0; + }} + + """ + with open(self.loader_src, "w") as f: + f.write(loader_code) + logger.info(f"Generated loader at {self.loader_src}") diff --git a/contrib/vmtest-tool/config.py b/contrib/vmtest-tool/config.py new file mode 100644 index 00000000000..ba913bce9d6 --- /dev/null +++ b/contrib/vmtest-tool/config.py @@ -0,0 +1,11 @@ +import platform +from pathlib import Path + +KERNEL_TARBALL_PREFIX_URL = "https://cdn.kernel.org/pub/linux/kernel/" +BASE_DIR = Path.home() / ".vmtest-tool" +ARCH = platform.machine() +KCONFIG_REL_PATHS = [ + "tools/testing/selftests/bpf/config", + "tools/testing/selftests/bpf/config.vm", + f"tools/testing/selftests/bpf/config.{ARCH}", +] diff --git a/contrib/vmtest-tool/kernel.py b/contrib/vmtest-tool/kernel.py new file mode 100644 index 00000000000..2974948130e --- /dev/null +++ b/contrib/vmtest-tool/kernel.py @@ -0,0 +1,209 @@ +import logging +import os +import shutil +import subprocess +from pathlib import Path +import re +from urllib.parse import urljoin +from urllib.request import urlretrieve +from typing import Optional, List +from dataclasses import dataclass + +from config import ARCH, BASE_DIR, KCONFIG_REL_PATHS, KERNEL_TARBALL_PREFIX_URL +import utils + +logger = logging.getLogger(__name__) +KERNELS_DIR = BASE_DIR / "kernels" + + +@dataclass +class KernelSpec: + """Immutable kernel specification""" + + version: str + arch: str = ARCH + + def __post_init__(self): + self.major = self.version.split(".")[0] + + def __str__(self): + return f"{self.version}-{self.arch}" + + @property + def bzimage_path(self) -> Path: + return KERNELS_DIR / f"bzImage-{self}" + + @property + def tarball_path(self) -> Path: + return KERNELS_DIR / f"linux-{self.version}.tar.xz" + + @property + def kernel_dir(self) -> Path: + return KERNELS_DIR / f"linux-{self.version}" + + +class KernelImage: + """Represents a compiled kernel image""" + + def __init__(self, path: Path): + if not isinstance(path, Path): + path = Path(path) + + if not path.exists(): + raise FileNotFoundError(f"Kernel image not found: {path}") + + self.path = path + + def __str__(self): + return str(self.path) + + +class KernelCompiler: + """Handles complete kernel compilation process including download and build""" + + def compile_from_version(self, spec: KernelSpec) -> KernelImage: + """Complete compilation process from kernel version""" + if spec.bzimage_path.exists(): + logger.info(f"Kernel {spec} already exists, skipping compilation") + return KernelImage(spec.bzimage_path) + + try: + self._download_source(spec) + self._extract_source(spec) + self._configure_kernel(spec) + self._compile_kernel(spec) + self._copy_bzimage(spec) + + logger.info(f"Successfully compiled kernel {spec}") + return KernelImage(spec.bzimage_path) + + except Exception as e: + logger.error(f"Failed to compile kernel {spec}: {e}") + raise + finally: + # Always cleanup temporary files + self._cleanup(spec) + + def _download_source(self, spec: KernelSpec) -> None: + """Download kernel source tarball""" + if spec.tarball_path.exists(): + logger.info(f"Tarball already exists: {spec.tarball_path}") + return + + url_suffix = f"v{spec.major}.x/linux-{spec.version}.tar.xz" + url = urljoin(KERNEL_TARBALL_PREFIX_URL, url_suffix) + + logger.info(f"Downloading kernel from {url}") + spec.tarball_path.parent.mkdir(parents=True, exist_ok=True) + urlretrieve(url, spec.tarball_path) + logger.info("Kernel source downloaded") + + def _extract_source(self, spec: KernelSpec) -> None: + """Extract kernel source tarball""" + logger.info(f"Extracting kernel source to {spec.kernel_dir}") + spec.kernel_dir.mkdir(parents=True, exist_ok=True) + + utils.run_command( + [ + "tar", + "-xf", + str(spec.tarball_path), + "-C", + str(spec.kernel_dir), + "--strip-components=1", + ] + ) + + def _configure_kernel(self, spec: KernelSpec) -> None: + """Configure kernel with provided config files""" + config_path = spec.kernel_dir / ".config" + + with open(config_path, "wb") as kconfig: + for config_rel_path in KCONFIG_REL_PATHS: + config_abs_path = spec.kernel_dir / config_rel_path + if config_abs_path.exists(): + with open(config_abs_path, "rb") as conf: + kconfig.write(conf.read()) + + logger.info("Updated kernel configuration") + + def _compile_kernel(self, spec: KernelSpec) -> None: + """Compile the kernel""" + logger.info(f"Compiling kernel in {spec.kernel_dir}") + old_cwd = os.getcwd() + + try: + os.chdir(spec.kernel_dir) + utils.run_command(["make", "olddefconfig"]) + utils.run_command(["make", f"-j{os.cpu_count()}", "bzImage"]) + except subprocess.CalledProcessError as e: + logger.error(f"Kernel compilation failed: {e}") + raise + finally: + os.chdir(old_cwd) + + def _copy_bzimage(self, spec: KernelSpec) -> None: + """Copy compiled bzImage to final location""" + src = spec.kernel_dir / "arch/x86/boot/bzImage" + dest = spec.bzimage_path + dest.parent.mkdir(parents=True, exist_ok=True) + + shutil.copy2(src, dest) + logger.info(f"Stored bzImage at {dest}") + + def _cleanup(self, spec: KernelSpec) -> None: + """Clean up temporary files""" + if spec.tarball_path.exists(): + spec.tarball_path.unlink() + logger.info("Removed tarball") + + if spec.kernel_dir.exists(): + shutil.rmtree(spec.kernel_dir) + logger.info("Removed kernel source directory") + + +class KernelManager: + """Main interface for kernel management""" + + def __init__(self): + self.compiler = KernelCompiler() + + def get_kernel_image( + self, + version: Optional[str] = None, + kernel_image_path: Optional[str] = None, + arch: str = ARCH, + ) -> KernelImage: + """Get kernel image from version or existing file""" + + # Validate inputs + if not version and not kernel_image_path: + raise ValueError("Must provide either 'version' or 'kernel_image_path'") + + if version and kernel_image_path: + raise ValueError("Provide only one of 'version' or 'kernel_image_path'") + + # Handle existing kernel image + if kernel_image_path: + path = Path(kernel_image_path) + if not path.exists(): + raise FileNotFoundError(f"Kernel image not found: {kernel_image_path}") + return KernelImage(path) + + # Handle version-based compilation + if version: + spec = KernelSpec(version=version, arch=arch) + return self.compiler.compile_from_version(spec) + + def list_available_kernels(self) -> List[str]: + """List all available compiled kernels""" + if not KERNELS_DIR.exists(): + return [] + + kernels = [] + for file in KERNELS_DIR.glob("bzImage-*"): + match = re.match(r"bzImage-(.*)", file.name) + if match: + kernels.append(match.group(1)) + + return sorted(kernels) diff --git a/contrib/vmtest-tool/main.py b/contrib/vmtest-tool/main.py new file mode 100644 index 00000000000..bd408badef1 --- /dev/null +++ b/contrib/vmtest-tool/main.py @@ -0,0 +1,101 @@ +import argparse +import logging +from pathlib import Path +import textwrap + +import bpf +import kernel +import vm + + +def main(): + parser = argparse.ArgumentParser() + kernel_group = parser.add_mutually_exclusive_group(required=True) + kernel_group.add_argument( + "-k", + "--kernel", + help="Kernel version to boot in the vm", + metavar="VERSION", + type=str, + ) + kernel_group.add_argument( + "--kernel-image", + help="Kernel image to boot in the vm", + metavar="PATH", + type=str, + ) + parser.add_argument( + "-r", "--rootfs", help="rootfs to mount in the vm", default="/", metavar="PATH" + ) + parser.add_argument( + "-v", + "--log-level", + help="Log level", + metavar="DEBUG|INFO|WARNING|ERROR", + choices=["DEBUG", "INFO", "WARNING", "ERROR"], + default="ERROR", + ) + command_group = parser.add_mutually_exclusive_group(required=True) + command_group.add_argument( + "--bpf-src", + help="Path to BPF C source file", + metavar="PATH", + type=str, + ) + command_group.add_argument( + "--bpf-obj", + help="Path to bpf bytecode object", + metavar="PATH", + type=str, + ) + command_group.add_argument( + "-c", "--command", help="command to run in the vm", metavar="COMMAND" + ) + command_group.add_argument( + "-s", "--shell", help="open interactive shell in the vm", action="store_true" + ) + + args = parser.parse_args() + + logging.basicConfig(level=args.log_level) + logger = logging.getLogger(__name__) + kmanager = kernel.KernelManager() + + if args.kernel: + kernel_image = kmanager.get_kernel_image(version=args.kernel) + elif args.kernel_image: + kernel_image = kmanager.get_kernel_image(kernel_image_path=args.kernel_image) + + if args.bpf_src: + command = bpf.BPFProgram.from_source(Path(args.bpf_src)) + elif args.bpf_obj: + command = bpf.BPFProgram.from_bpf_obj(Path(args.bpf_obj)) + elif args.command: + command = args.command + elif args.shell: + # todo: somehow pass to hyperwiser that you need to attach stdin as well + # command = "/bin/bash" + raise NotImplementedError + + virtual_machine = vm.VirtualMachine(kernel_image, args.rootfs, str(command)) + try: + result = virtual_machine.execute() + except vm.BootFailedError as e: + logger.error("VM boot failure: execution aborted. See logs for details.") + print(e) + exit(e.returncode) + if args.bpf_src or args.bpf_obj: + if result.returncode == 0: + print("BPF programs succesfully loaded") + else: + if "Failed to load BPF skeleton" in result.stdout: + print("BPF program failed to load") + print("Verifier logs:") + print(textwrap.indent(vm.bpf_verifier_logs(result.stdout), "\t")) + elif args.command: + print(result.stdout) + exit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/contrib/vmtest-tool/pyproject.toml b/contrib/vmtest-tool/pyproject.toml new file mode 100644 index 00000000000..e4701ec6e8f --- /dev/null +++ b/contrib/vmtest-tool/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "vmtest-tool" +version = "0.1.0" +description = "Test BPF code against live kernels" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [] + +[dependency-groups] +dev = [ + "pre-commit>=4.2.0", + "pytest>=8.4.0", + "pytest-sugar>=1.0.0", + "ruff>=0.11.13", + "tox>=4.26.0", +] + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] +testpaths = ["tests"] +pythonpath = ["."] + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", +] +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" \ No newline at end of file diff --git a/contrib/vmtest-tool/requirements-dev.txt b/contrib/vmtest-tool/requirements-dev.txt new file mode 100644 index 00000000000..eec7e90fbe0 --- /dev/null +++ b/contrib/vmtest-tool/requirements-dev.txt @@ -0,0 +1,198 @@ +# This file was autogenerated by uv via the following command: +# uv export --directory=contrib/vmtest-tool --frozen --output-file=requirements-dev.txt +cachetools==6.0.0 \ + --hash=sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e \ + --hash=sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf + # via tox +cfgv==3.4.0 \ + --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ + --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 + # via pre-commit +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 + # via tox +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # pytest + # tox +distlib==0.3.9 \ + --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ + --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 + # via virtualenv +exceptiongroup==1.3.0 ; python_full_version < '3.11' \ + --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ + --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 + # via pytest +filelock==3.18.0 \ + --hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \ + --hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de + # via + # tox + # virtualenv +identify==2.6.12 \ + --hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \ + --hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6 + # via pre-commit +iniconfig==2.1.0 \ + --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ + --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + # via pytest +nodeenv==1.9.1 \ + --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ + --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 + # via pre-commit +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f + # via + # pyproject-api + # pytest + # pytest-sugar + # tox +platformdirs==4.3.8 \ + --hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \ + --hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4 + # via + # tox + # virtualenv +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via + # pytest + # tox +pre-commit==4.2.0 \ + --hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \ + --hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd +pygments==2.19.1 \ + --hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \ + --hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c + # via pytest +pyproject-api==1.9.1 \ + --hash=sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335 \ + --hash=sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948 + # via tox +pytest==8.4.0 \ + --hash=sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6 \ + --hash=sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e + # via pytest-sugar +pytest-sugar==1.0.0 \ + --hash=sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a \ + --hash=sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd +pyyaml==6.0.2 \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via pre-commit +ruff==0.11.13 \ + --hash=sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629 \ + --hash=sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432 \ + --hash=sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514 \ + --hash=sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3 \ + --hash=sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc \ + --hash=sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46 \ + --hash=sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9 \ + --hash=sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492 \ + --hash=sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b \ + --hash=sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165 \ + --hash=sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71 \ + --hash=sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc \ + --hash=sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250 \ + --hash=sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a \ + --hash=sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48 \ + --hash=sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b \ + --hash=sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7 \ + --hash=sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933 +termcolor==3.1.0 \ + --hash=sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa \ + --hash=sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970 + # via pytest-sugar +tomli==2.2.1 ; python_full_version < '3.11' \ + --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ + --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ + --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ + --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ + --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ + --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ + --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ + --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ + --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ + --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ + --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ + --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ + --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ + --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ + --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ + --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ + --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ + --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ + --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ + --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ + --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ + --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ + --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ + --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ + --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ + --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ + --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ + --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ + --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ + --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ + --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ + --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 + # via + # pyproject-api + # pytest + # tox +tox==4.26.0 \ + --hash=sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224 \ + --hash=sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca +typing-extensions==4.14.0 ; python_full_version < '3.11' \ + --hash=sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4 \ + --hash=sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af + # via + # exceptiongroup + # tox +virtualenv==20.31.2 \ + --hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \ + --hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af + # via + # pre-commit + # tox diff --git a/contrib/vmtest-tool/tests/test_cli.py b/contrib/vmtest-tool/tests/test_cli.py new file mode 100644 index 00000000000..261ce4b9534 --- /dev/null +++ b/contrib/vmtest-tool/tests/test_cli.py @@ -0,0 +1,170 @@ +import sys +from unittest import mock +import pytest +from bpf import BPFProgram +import kernel +import main +import logging + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def openat_bpf_source(tmp_path): + openat_bpf = tmp_path / "openat_bpf.c" + openat_bpf.write_text(r""" + #include "vmlinux.h" + #include <bpf/bpf_helpers.h> + #include <bpf/bpf_tracing.h> + #include <bpf/bpf_core_read.h> + + char LICENSE[] SEC("license") = "GPL"; + + int example_pid = 0; + + SEC("tracepoint/syscalls/sys_enter_openat") + int handle_openat(struct trace_event_raw_sys_enter *ctx) + { + int pid = bpf_get_current_pid_tgid() >> 32; + char filename[256]; // filename buffer + bpf_probe_read_user(&filename, sizeof(filename), (void *)ctx->args[1]); + bpf_printk("sys_enter_openat() called from PID %d for file: %s\n", pid, + filename); + + return 0; + } + + """) + return openat_bpf + + +@pytest.fixture +def openat_bpf_obj(openat_bpf_source): + bpf_program = BPFProgram(source_path=openat_bpf_source) + bpf_program._compile_bpf() + return bpf_program.bpf_obj + + +@pytest.fixture +def invalid_memory_access_bpf_source(tmp_path): + invalid_memory_access_bpf = tmp_path / "invalid_memory_access_bpf.c" + invalid_memory_access_bpf.write_text(r""" + #include "vmlinux.h" + #include <bpf/bpf_helpers.h> + #include <bpf/bpf_tracing.h> + + char LICENSE[] SEC("license") = "GPL"; + + SEC("tracepoint/syscalls/sys_enter_openat") + int bpf_prog(struct trace_event_raw_sys_enter *ctx) { + int arr[4] = {1, 2, 3, 4}; + + // Invalid memory access: out-of-bounds + int val = arr[5]; // This causes the verifier to fail + + return val; + } + """) + return invalid_memory_access_bpf + + +@pytest.fixture +def invalid_memory_access_bpf_obj(invalid_memory_access_bpf_source): + bpf_program = BPFProgram(source_path=invalid_memory_access_bpf_source) + bpf_program._compile_bpf() + return bpf_program.bpf_obj + + +def run_main_with_args_and_capture_output(args, capsys): + with mock.patch.object(sys, "argv", args): + try: + main.main() + except SystemExit as e: + result = capsys.readouterr() + output = result.out.rstrip() + error = result.err.rstrip() + logger.debug("STDOUT:\n%s", output) + logger.debug("STDERR:\n%s", error) + return e.code, output, error + + +kernel_image_path = kernel.KernelManager().get_kernel_image(version="6.15") +kernel_cli_flags = [["--kernel", "6.15"], ["--kernel-image", f"{kernel_image_path}"]] + + +@pytest.mark.parametrize("kernel_args", kernel_cli_flags) +class TestCLI: + def test_main_with_valid_bpf(self, kernel_args, openat_bpf_source, capsys): + args = [ + "main.py", + *kernel_args, + "--rootfs", + "/", + "--bpf-src", + str(openat_bpf_source), + ] + code, output, _ = run_main_with_args_and_capture_output(args, capsys) + assert code == 0 + assert "BPF programs succesfully loaded" == output + + def test_main_with_valid_bpf_obj(self, kernel_args, openat_bpf_obj, capsys): + args = [ + "main.py", + *kernel_args, + "--rootfs", + "/", + "--bpf-obj", + str(openat_bpf_obj), + ] + code, output, _ = run_main_with_args_and_capture_output(args, capsys) + assert code == 0 + assert "BPF programs succesfully loaded" == output + + def test_main_with_invalid_bpf( + self, kernel_args, invalid_memory_access_bpf_source, capsys + ): + args = [ + "main.py", + *kernel_args, + "--rootfs", + "/", + "--bpf-src", + str(invalid_memory_access_bpf_source), + ] + code, output, _ = run_main_with_args_and_capture_output(args, capsys) + output_lines = output.splitlines() + assert code == 1 + assert "BPF program failed to load" == output_lines[0] + assert "Verifier logs:" == output_lines[1] + + def test_main_with_invalid_bpf_obj( + self, kernel_args, invalid_memory_access_bpf_obj, capsys + ): + args = [ + "main.py", + *kernel_args, + "--rootfs", + "/", + "--bpf-obj", + str(invalid_memory_access_bpf_obj), + ] + code, output, _ = run_main_with_args_and_capture_output(args, capsys) + output_lines = output.splitlines() + assert code == 1 + assert "BPF program failed to load" == output_lines[0] + assert "Verifier logs:" == output_lines[1] + + def test_main_with_valid_command(self, kernel_args, capsys): + args = ["main.py", *kernel_args, "--rootfs", "/", "-c", "uname -r"] + code, output, error = run_main_with_args_and_capture_output(args, capsys) + assert code == 0 + assert "6.15.0" == output + + def test_main_with_invalid_command(self, kernel_args, capsys): + args = ["main.py", *kernel_args, "--rootfs", "/", "-c", "NotImplemented"] + code, output, error = run_main_with_args_and_capture_output(args, capsys) + assert code != 0 + assert f"Command failed with exit code: {code}" in output + + # def test_main_with_interupts(): + # raise NotImplementedError diff --git a/contrib/vmtest-tool/utils.py b/contrib/vmtest-tool/utils.py new file mode 100644 index 00000000000..fe80a648b21 --- /dev/null +++ b/contrib/vmtest-tool/utils.py @@ -0,0 +1,26 @@ +import subprocess +import logging + +logger = logging.getLogger(__name__) + + +def run_command(cmd, **kwargs): + logger.debug(f"Running command: {' '.join(cmd)}") + try: + logger.debug(f"running command: {cmd}") + result = subprocess.run( + cmd, + text=True, + check=True, + capture_output=True, + shell=False, + **kwargs, + ) + logger.debug("Command stdout:\n" + result.stdout.strip()) + if result.stderr: + logger.debug("Command stderr:\n" + result.stderr.strip()) + return result + except subprocess.CalledProcessError as e: + logger.error("Command failed with stdout:\n" + e.stdout.strip()) + logger.error("Command failed with stderr:\n" + e.stderr.strip()) + raise diff --git a/contrib/vmtest-tool/uv.lock b/contrib/vmtest-tool/uv.lock new file mode 100644 index 00000000000..3ee8ba8ed85 --- /dev/null +++ b/contrib/vmtest-tool/uv.lock @@ -0,0 +1,380 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "cachetools" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/b0/f539a1ddff36644c28a61490056e5bae43bd7386d9f9c69beae2d7e7d6d1/cachetools-6.0.0.tar.gz", hash = "sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf", size = 30160, upload-time = "2025-05-23T20:01:13.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c3/8bb087c903c95a570015ce84e0c23ae1d79f528c349cbc141b5c4e250293/cachetools-6.0.0-py3-none-any.whl", hash = "sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e", size = 10964, upload-time = "2025-05-23T20:01:11.323Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "identify" +version = "2.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, +] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +] + +[[package]] +name = "termcolor" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tox" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/dcec0c00321a107f7f697fd00754c5112572ea6dcacb40b16d8c3eea7c37/tox-4.26.0.tar.gz", hash = "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca", size = 197260, upload-time = "2025-05-13T15:04:28.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/14/f58b4087cf248b18c795b5c838c7a8d1428dfb07cb468dad3ec7f54041ab/tox-4.26.0-py3-none-any.whl", hash = "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224", size = 172761, upload-time = "2025-05-13T15:04:26.207Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, +] + +[[package]] +name = "vmtest-tool" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-sugar" }, + { name = "ruff" }, + { name = "tox" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest-sugar", specifier = ">=1.0.0" }, + { name = "ruff", specifier = ">=0.11.13" }, + { name = "tox", specifier = ">=4.26.0" }, +] diff --git a/contrib/vmtest-tool/vm.py b/contrib/vmtest-tool/vm.py new file mode 100644 index 00000000000..5d4a5747f0b --- /dev/null +++ b/contrib/vmtest-tool/vm.py @@ -0,0 +1,154 @@ +import logging +import subprocess +from typing import List + +from kernel import KernelImage + +logger = logging.getLogger(__name__) + + +class VMConfig: + """Configuration container for VM settings""" + + def __init__( + self, kernel_image: KernelImage, rootfs_path: str, command: str, **kwargs + ): + self.kernel = kernel_image + self.kernel_path = str(kernel_image.path) + self.rootfs_path = rootfs_path + self.command = command + self.memory_mb = kwargs.get("memory_mb", 512) + self.cpu_count = kwargs.get("cpu_count", 1) + self.extra_args = kwargs.get("extra_args", {}) + + +def bpf_verifier_logs(output: str) -> str: + start_tag = "--- Verifier log start ---" + end_tag = "--- Verifier log end ---" + + start_idx = output.find(start_tag) + end_idx = output.find(end_tag) + + if start_idx != -1 and end_idx != -1: + # Extract between the tags (excluding the markers themselves) + log_body = output[start_idx + len(start_tag) : end_idx].strip() + return log_body + else: + return "No verifier log found in the output." + + +class Vmtest: + """vmtest backend implementation""" + + def __init__(self): + pass + + def _boot_command(self, vm_config: VMConfig): + vmtest_command = ["vmtest"] + vmtest_command.extend(["-k", vm_config.kernel_path]) + vmtest_command.extend(["-r", vm_config.rootfs_path]) + vmtest_command.append(vm_config.command) + return vmtest_command + + def _remove_boot_log(self, full_output: str) -> str: + """ + Filters QEMU and kernel boot logs, returning only the output after the + `===> Running command` marker. + """ + marker = "===> Running command" + lines = full_output.splitlines() + + try: + start_index = next(i for i, line in enumerate(lines) if marker in line) + # Return everything after that marker (excluding the marker itself) + return "\n".join(lines[start_index + 1 :]).strip() + except StopIteration: + return full_output.strip() + + def run_command(self, vm_config): + vm = None + try: + logger.info(f"Booting VM with kernel: {vm_config.kernel_path}") + logger.info(f"Using rootfs: {vm_config.rootfs_path}") + vm = subprocess.run( + self._boot_command(vm_config), + check=True, + text=True, + capture_output=True, + shell=False, + ) + vm_stdout = vm.stdout + logger.debug(vm_stdout) + return VMCommandResult( + vm.returncode, self._remove_boot_log(vm_stdout), None + ) + except subprocess.CalledProcessError as e: + out = e.stdout + err = e.stderr + # when the command in the vm fails we consider it as a succesfull boot + if "===> Running command" not in out: + raise BootFailedError("Boot failed", out, err, e.returncode) + logger.debug("STDOUT: \n%s", out) + logger.debug("STDERR: \n%s", err) + return VMCommandResult(e.returncode, self._remove_boot_log(out), err) + + +class VMCommandResult: + def __init__(self, returncode, stdout, stderr) -> None: + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +class VirtualMachine: + """Main VM class - simple interface for end users""" + + # Registry of available hypervisors + _hypervisors = { + "vmtest": Vmtest, + } + + def __init__( + self, + kernel_image: KernelImage, + rootfs_path: str, + command: str, + hypervisor_type: str = "vmtest", + **kwargs, + ): + self.config = VMConfig(kernel_image, rootfs_path, command, **kwargs) + + if hypervisor_type not in self._hypervisors: + raise ValueError(f"Unsupported hypervisor: {hypervisor_type}") + + self.hypervisor = self._hypervisors[hypervisor_type]() + + @classmethod + def list_hypervisors(cls) -> List[str]: + """List available hypervisors""" + return list(cls._hypervisors.keys()) + + def execute(self): + """Execute command in VM""" + return self.hypervisor.run_command(self.config) + + +class BootFailedError(Exception): + """Raised when VM fails to boot properly (before command execution).""" + + def __init__( + self, message: str, stdout: str = "", stderr: str = "", returncode: int = -1 + ): + super().__init__(message) + self.stdout = stdout + self.stderr = stderr + self.returncode = returncode + + def __str__(self): + base = super().__str__() + return ( + f"{base}\n" + f"Return code: {self.returncode}\n" + f"--- STDOUT ---\n{self.stdout}\n" + f"--- STDERR ---\n{self.stderr}" + ) -- 2.49.0