Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions base/images/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ base/images/
| `installed_package_sizes` | session | `dict[str, int]` | Installed RPM name → on-disk size in bytes (`rpm --root`, `%{SIZE}`) |
| `disk_info` | session | `DiskInfo \| None` | VM only |
| `partition_table` | session | `list[PartitionInfo]` | VM only — auto-skips on containers |
| `boot_entry_option_lines` | session | `dict[Path, list[str]]` | BLS boot entry path → raw kernel option lines |
| `podman_client` | session | `DockerClient \| None` | python-on-whales Podman client; None for non-container images |
| `container_image_ref` | session | `str \| None` | Loaded image ID (cached); None for non-container |
| `running_container` | function | `ContainerInstance` | Fresh container per test — auto-skips on VMs |
Expand Down
30 changes: 30 additions & 0 deletions base/images/tests/cases/static/test_fips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: MIT
"""Validate kernel FIPS boot configuration."""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from utils.parsers import parse_boot_entry_kernel_options

if TYPE_CHECKING:
from pathlib import Path


@pytest.mark.require_capability("machine-bootable")
def test_kernel_fips_matches_image_variant(
boot_entry_option_lines: dict[Path, list[str]],
capabilities: set[str],
) -> None:
expected_fips_options = ["fips=1"] if "fips-enabled" in capabilities else []
options_by_entry = parse_boot_entry_kernel_options(boot_entry_option_lines)

for entry, options in options_by_entry.items():
fips_options = [
option for option in options if option.partition("=")[0] == "fips"
]
assert fips_options == expected_fips_options, (
"Kernel FIPS options do not match the image's capabilities "
f"in {entry}"
)
57 changes: 39 additions & 18 deletions base/images/tests/cases/static/test_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import pytest
from utils.extract import read_text_confined
from utils.parsers import parse_boot_entry_kernel_options

if TYPE_CHECKING:
from pathlib import Path
Expand All @@ -40,19 +41,27 @@
# such lines must be dropped before handing the text to it.
ZRAM_GLOBAL_DIRECTIVE_RE = re.compile(r"^\s*set!\S+\s*=.*$")

# BLS boot entries and the grub2 config both embed the effective kernel
# command line; zram-generator enables a default zram0 swap device when
# "systemd.zram" is present with no value, or any value other than "0"/
# "false" (see zram-generator.conf(5)).
KERNEL_CMDLINE_GLOB_PATHS = (
"boot/loader/entries/*.conf",
"boot/efi/loader/entries/*.conf",
"boot/grub2/grub.cfg",
)
# zram-generator enables a default zram0 swap device when "systemd.zram"
# is present with no value, or any value other than "0"/"false"
# (see zram-generator.conf(5)).
GRUB_CONFIG_PATH = "boot/grub2/grub.cfg"
SYSTEMD_ZRAM_OPTION_RE = re.compile(r"\bsystemd\.zram(?:=(\S+))?\b")
SYSTEMD_ZRAM_DISABLED_VALUES = {"0", "false", "no", "off"}


def _is_enabling_zram_option(option: str) -> bool:
name, separator, value = option.partition("=")
return (
name == "systemd.zram"
and (not separator or value.lower() not in SYSTEMD_ZRAM_DISABLED_VALUES)
)


def _find_enabling_zram_options(cmdline_text: str) -> list[str]:
options = (match.group(0) for match in SYSTEMD_ZRAM_OPTION_RE.finditer(cmdline_text))
return [option for option in options if _is_enabling_zram_option(option)]


@pytest.mark.require_capability("machine-bootable")
def test_no_swap_partition(partition_table: list[PartitionInfo]) -> None:
"""Bootable images must not include a swap partition."""
Expand Down Expand Up @@ -138,7 +147,10 @@ def test_no_zram_swap_device(rootfs: Path) -> None:


@pytest.mark.require_capability("machine-bootable")
def test_no_zram_swap_kernel_cmdline(rootfs: Path) -> None:
def test_no_zram_swap_kernel_cmdline(
rootfs: Path,
boot_entry_option_lines: dict[Path, list[str]],
) -> None:
"""Bootable images must not enable zram swap via the kernel command line.

zram-generator activates a default ``zram0`` swap device when the
Expand All @@ -147,14 +159,23 @@ def test_no_zram_swap_kernel_cmdline(rootfs: Path) -> None:
on-disk ``zram-generator.conf``.
"""
enabling_entries: list[str] = []
for glob_pattern in KERNEL_CMDLINE_GLOB_PATHS:
for conf_path in sorted(rootfs.glob(glob_pattern)):
rel_path = str(conf_path.relative_to(rootfs))
cmdline_text = read_text_confined(rootfs, rel_path)
for match in SYSTEMD_ZRAM_OPTION_RE.finditer(cmdline_text):
value = match.group(1)
if value is None or value.lower() not in SYSTEMD_ZRAM_DISABLED_VALUES:
enabling_entries.append(f"{rel_path}: {match.group(0)!r}")
options_by_entry = parse_boot_entry_kernel_options(boot_entry_option_lines)
for entry, options in options_by_entry.items():
rel_path = entry.relative_to(rootfs)
enabling_entries.extend(
f"{rel_path}: {option!r}"
for option in options
if _is_enabling_zram_option(option)
)

grub_config = rootfs / GRUB_CONFIG_PATH
if grub_config.exists():
enabling_entries.extend(
f"{GRUB_CONFIG_PATH}: {option!r}"
for option in _find_enabling_zram_options(
read_text_confined(rootfs, GRUB_CONFIG_PATH)
)
)

assert not enabling_entries, (
f"Expected no 'systemd.zram' kernel cmdline option enabling swap, found: {enabling_entries}"
Expand Down
26 changes: 26 additions & 0 deletions base/images/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,32 @@ def partition_table(disk_info: DiskInfo | None, image_type: str) -> list[Partiti
return disk_info.partitions


@pytest.fixture(scope="session")
def boot_entry_option_lines(rootfs: Path) -> dict[Path, list[str]]:
"""Raw kernel option lines keyed by BLS boot entry path."""
entries_dirs = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): I am not very comfortable with the implicit selection of whichever boot configs are found in the image. Much like image capability driving what to expect in the image can't we drive this fixture the same way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we should check everything and then caller can decide what to do. In our case, it feels very unlikely that any images will ever actually have multiple entrieson a freshly-baked image so we should be safe. But either way, in principle, I'd prefer to cover them all.

Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your argument is valid and that anticipation is why the comment is non-blocking. To illustrate what I am trying to convey if we look at the rootfs fixture, it checks the image type and does the mounting in different ways. Similarly, since we know what kind of image we are inspecting, we can look at the deterministic loader configuration path for that image. This definitely does not break the test and it is your choice whether to make a change.

rootfs / "boot" / "loader" / "entries",
rootfs / "boot" / "efi" / "loader" / "entries",
)
entries = sorted(
{
entry
for entries_dir in entries_dirs
for entry in entries_dir.glob("*.conf")
}
)
option_lines_by_entry: dict[Path, list[str]] = {}
for entry in entries:
option_lines: list[str] = []
relative_entry = entry.relative_to(rootfs).as_posix()
for line in read_text_confined(rootfs, relative_entry).splitlines():
fields = line.split(maxsplit=1)
if fields and fields[0] == "options":
option_lines.append(fields[1] if len(fields) > 1 else "")
option_lines_by_entry[entry] = option_lines
return option_lines_by_entry


# ---------------------------------------------------------------------------
# Container runtime fixtures
# ---------------------------------------------------------------------------
Expand Down
33 changes: 33 additions & 0 deletions base/images/tests/utils/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import logging
import shlex
import subprocess
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -45,6 +46,38 @@ def parse_os_release(content: str) -> dict[str, str]:
return result


def parse_boot_entry_kernel_options(
option_lines_by_entry: dict[Path, list[str]],
) -> dict[Path, list[str]]:
"""Parse one literal kernel option line from each BLS boot entry."""
if not option_lines_by_entry:
raise ValueError("no BLS boot loader entries found")

options_by_entry: dict[Path, list[str]] = {}
for entry, option_lines in option_lines_by_entry.items():
if len(option_lines) != 1:
raise ValueError(
f"expected exactly one options line in {entry}, "
f"found {len(option_lines)}"
)
try:
options = shlex.split(option_lines[0])
except ValueError as exc:
raise ValueError(
f"failed to parse boot loader options in {entry}: {exc}"
) from exc
if not options:
raise ValueError(f"boot loader options are empty in {entry}")
indirections = [option for option in options if option.startswith("$")]
if indirections:
raise ValueError(
f"boot loader option indirection is not supported in {entry}: "
f"{indirections}. This is likely a test issue."
)
options_by_entry[entry] = options
return options_by_entry


def query_rpm_package_sizes(rootfs: Path) -> dict[str, int]:
"""Query installed RPM package on-disk sizes via ``rpm --root``.

Expand Down
Loading