]> git.ipfire.org Git - thirdparty/shadow.git/commitdiff
tests: add privileged topology and test configuration
authorHadi Chokr <hadichokr@icloud.com>
Thu, 12 Feb 2026 11:16:39 +0000 (12:16 +0100)
committerIker Pedrosa <ikerpedrosam@gmail.com>
Mon, 2 Mar 2026 11:55:25 +0000 (12:55 +0100)
Extend the Python system test framework with a dedicated privileged
topology and configuration, while keeping privileged tests excluded
by default. And include a BTRFS Framework class for BTRFS Operations.

This ensures:
- Clear separation between safe and destructive tests
- No accidental execution of privileged tests in normal CI or local runs
- Explicit opt-in for privileged environments

Signed-off-by: Hadi Chokr <hadichokr@icloud.com>
tests/system/framework/hosts/base.py
tests/system/framework/hosts/btrfs.py [new file with mode: 0644]
tests/system/framework/topology.py
tests/system/mhc-privileged.yaml [new file with mode: 0644]
tests/system/mhc.yaml
tests/system/pytest.ini

index 9146c7d4e0debee862a808bde86cd66589a03157..c30000120b31f01d77fd987a6e4cb1c4d57fce86 100644 (file)
@@ -8,6 +8,7 @@ from pytest_mh import MultihostBackupHost, MultihostHost
 from pytest_mh.utils.fs import LinuxFileSystem
 
 from ..config import ShadowMultihostDomain
+from .btrfs import Btrfs
 
 __all__ = [
     "BaseHost",
@@ -43,6 +44,7 @@ class BaseLinuxHost(MultihostHost[ShadowMultihostDomain]):
         super().__init__(*args, **kwargs)
 
         self.fs: LinuxFileSystem = LinuxFileSystem(self)
+        self.btrfs: Btrfs = Btrfs(self)
         self._os_release: dict = {}
         self._distro_name: str = "unknown"
         self._distro_major: int = 0
diff --git a/tests/system/framework/hosts/btrfs.py b/tests/system/framework/hosts/btrfs.py
new file mode 100644 (file)
index 0000000..6c7732b
--- /dev/null
@@ -0,0 +1,109 @@
+from __future__ import annotations
+
+import uuid
+from contextlib import contextmanager
+from typing import Dict, List, Optional
+
+from pytest_mh import MultihostHost, MultihostUtility
+
+
+class Btrfs(MultihostUtility[MultihostHost]):
+    """
+    Utility for managing Btrfs subvolumes and loopback‑mounted Btrfs filesystems.
+
+    Each created resource is tracked and cleaned up immediately when its context
+    manager exits. The teardown() method acts as a safety net, cleaning any
+    leftover resources if the normal cleanup was skipped.
+    """
+
+    def __init__(self, host: MultihostHost) -> None:
+        super().__init__(host)
+        self._resources: List[Dict[str, Optional[str]]] = []
+
+    def subvolumes(self, path: str) -> List[str]:
+        result = self.host.conn.run(f"btrfs subvolume list {path}")
+        if result.rc != 0:
+            raise RuntimeError(f"Failed to list Btrfs subvolumes under {path}: {result.stderr}")
+        names = []
+        for line in result.stdout.strip().splitlines():
+            if " path " in line:
+                names.append(line.split(" path ", 1)[1].strip())
+        return names
+
+    def subvolume_exists(self, path: str, name: str) -> bool:
+        return name in self.subvolumes(path)
+
+    @contextmanager
+    def loopback_mount(self, mountpoint: str, size: str = "128M"):
+        image_path = f"/tmp/btrfs-{uuid.uuid4().hex}.img"
+        resource: Dict[str, Optional[str]] = {
+            "mountpoint": mountpoint,
+            "image_path": image_path,
+            "loop_dev": None,
+        }
+
+        setup_script = f"""
+            set -e
+            mountpoint -q {mountpoint} && umount {mountpoint} || true
+            [ -e /dev/loop-control ] || mknod /dev/loop-control c 10 237
+            for i in 0 1 2 3 4 5 6 7; do
+                [ -b /dev/loop$i ] || mknod /dev/loop$i b 7 $i
+            done
+            truncate -s {size} {image_path}
+            mkfs.btrfs -f {image_path}
+            LOOP_DEV=$(losetup --find --show {image_path})
+            echo $LOOP_DEV > {image_path}.loop
+            mount -t btrfs $LOOP_DEV {mountpoint}
+        """
+
+        self.logger.info(f"Setting up loop‑backed Btrfs at {mountpoint}")
+        result = self.host.conn.run(setup_script)
+        if result.rc != 0:
+            raise RuntimeError(f"Failed to set up loop‑backed Btrfs at {mountpoint}: {result.stderr}")
+
+        result = self.host.conn.run(f"cat {image_path}.loop")
+        if result.rc != 0:
+            raise RuntimeError(f"Failed to read loop device from {image_path}.loop: {result.stderr}")
+        resource["loop_dev"] = result.stdout.strip()
+
+        # Register the resource for safety net
+        self._resources.append(resource)
+
+        try:
+            yield image_path
+        finally:
+            # Clean up only this resource immediately
+            self._cleanup_resource(resource)
+
+    def _cleanup_resource(self, resource: Dict[str, Optional[str]]) -> None:
+        """Clean up a single resource."""
+        mountpoint = resource["mountpoint"]
+        image_path = resource["image_path"]
+        cleanup_script = f"""
+            set +e
+            mountpoint -q {mountpoint} && umount {mountpoint}
+            if [ -f {image_path}.loop ]; then
+                LOOP_DEV=$(cat {image_path}.loop)
+                losetup -d "$LOOP_DEV" 2>/dev/null || true
+                rm -f {image_path}.loop
+            fi
+            rm -f {image_path}
+        """
+        self.logger.info(f"Cleaning up loop‑backed Btrfs at {mountpoint}")
+        cleanup_result = self.host.conn.run(cleanup_script)
+        if cleanup_result.rc != 0:
+            self.logger.error(f"Cleanup of Btrfs resource {mountpoint} failed: {cleanup_result.stderr}")
+        # Remove from tracking list
+        if resource in self._resources:
+            self._resources.remove(resource)
+
+    def teardown(self) -> None:
+        """
+        Safety net: cleans up any Btrfs resources that were not properly released.
+        This runs during the utility teardown phase, after all tests.
+        """
+        if not self._resources:
+            return
+        self.logger.warning(f"Cleaning up {len(self._resources)} leftover Btrfs resources")
+        for res in self._resources[:]:
+            self._cleanup_resource(res)
index 88f01d01320a3fcb72043acafa26640c6e28c950..93997f2ed33089cac864ffb3110e9cae28bebbd6 100644 (file)
@@ -35,6 +35,12 @@ class KnownTopology(KnownTopologyBase):
         fixtures=dict(shadow="shadow.shadow[0]"),
     )
 
+    ShadowPrivileged = TopologyMark(
+        name="shadow-privileged",
+        topology=Topology(TopologyDomain("shadow-privileged", shadow=1)),
+        fixtures=dict(shadow="shadow-privileged.shadow[0]"),
+    )
+
 
 class KnownTopologyGroup(KnownTopologyGroupBase):
     """
diff --git a/tests/system/mhc-privileged.yaml b/tests/system/mhc-privileged.yaml
new file mode 100644 (file)
index 0000000..c26c0ce
--- /dev/null
@@ -0,0 +1,13 @@
+provisioned_topologies:
+- shadow-privileged
+
+domains:
+- id: shadow-privileged
+  hosts:
+  - hostname: shadow-priv.test
+    role: shadow
+    conn:
+      type: podman
+      container: builder-privileged
+    artifacts:
+    - /var/log/*
index 38107b67df1c1ed32ca7eba1b97ccd7937080dab..4a0c52524e6aaf860fe4d486b892fb312341b398 100644 (file)
@@ -1,5 +1,6 @@
 provisioned_topologies:
 - shadow
+
 domains:
 - id: shadow
   hosts:
@@ -9,4 +10,4 @@ domains:
       type: podman
       container: builder
     artifacts:
-    - /var/log/*
\ No newline at end of file
+    - /var/log/*
index bedde87c9e33404a2d54f785fc9848d440fd6fa9..d2b67994a221c8a289ad62159bc383eee00044a3 100644 (file)
@@ -3,3 +3,6 @@ pythonpath = . framework
 addopts = --strict-markers
 testpaths = tests
 ticket_tools = bz,gh,jira
+
+markers =
+    privileged: tests requiring elevated privileges (mounts, loop devices, BTRFS, etc.)