Files
docker-volume-backup/tests/e2e/test_e2e_snapshot.py
2026-08-02 09:10:55 +02:00

161 lines
5.1 KiB
Python

"""Snapshot capture against real filesystems.
Loop devices are only available to a privileged container, so each case builds
its filesystem inside one and drives snapshot_driver.py there. A filesystem
without snapshot support must fail loudly rather than degrade to a live copy,
which is the property that makes the mode safe to offer at all.
"""
from __future__ import annotations
import os
import shutil
import unittest
from pathlib import Path
from .helpers import require_docker, run, unique
REPO_SRC = Path(__file__).resolve().parents[2] / "src"
DRIVER = Path(__file__).resolve().parent / "snapshot_driver.py"
IMAGE = "alpine:3.20"
PACKAGES = "apk add -q btrfs-progs e2fsprogs zfs python3 util-linux"
ATTACH = (
"LOOP=$(losetup -f | awk '{print $1}') "
'&& { [ -b "$LOOP" ] || mknod "$LOOP" b 7 "${LOOP#/dev/loop}"; }; '
'losetup "$LOOP" /img'
)
LOOP_FS = {
"btrfs": "btrfs subvolume create /subject/docker >/dev/null",
"ext4": "mkdir -p /subject/docker",
}
# A container carries no /lib/modules, so modprobe fails even on a loaded module.
ZFS_READY = "{ [ -c /dev/zfs ] || modprobe zfs 2>/dev/null; }; [ -c /dev/zfs ]"
def mount_script(fstype: str) -> str:
"""Build a filesystem on a loop device and carve out the snapshot subject."""
if fstype == "zfs":
return (
f"{PACKAGES} && {ZFS_READY} && truncate -s 400M /img "
"&& zpool create -m none baudolo /img "
"&& zfs create -o mountpoint=/subject/docker baudolo/docker"
)
return (
f"{PACKAGES} && truncate -s 400M /img && mkfs.{fstype} -q /img "
f'&& mkdir -p /subject && {ATTACH} && mount -t {fstype} "$LOOP" /subject '
f"&& {LOOP_FS[fstype]}"
)
def zfs_usable() -> bool:
"""Whether this host's kernel can serve zfs to a privileged container."""
proc = run(
[
"docker",
"run",
"--rm",
"--privileged",
IMAGE,
"sh",
"-lc",
f"apk add -q zfs >/dev/null 2>&1 && {ZFS_READY}",
],
capture=True,
check=False,
)
return proc.returncode == 0
def required(fstype: str) -> bool:
"""Whether this run must cover ``fstype`` instead of skipping it.
CI sets E2E_REQUIRE_FILESYSTEMS so a missing kernel module fails the build
rather than passing it with a filesystem silently untested.
"""
demanded = os.environ.get("E2E_REQUIRE_FILESYSTEMS", "")
return fstype in demanded.replace(",", " ").split()
def stage() -> Path:
"""Copy source and driver under /tmp, the only path the DinD daemon shares."""
staged = Path("/tmp") / unique("baudolo-e2e-snapshot")
shutil.copytree(REPO_SRC, staged / "src")
shutil.copy(DRIVER, staged / "driver.py")
return staged
def drive(fstype: str, kind: str, expect: str) -> str:
staged = stage()
script = (
f"set -e; {mount_script(fstype)}; "
f"python3 /driver.py {kind} /subject/docker {expect}"
)
try:
proc = run(
[
"docker",
"run",
"--rm",
"--privileged",
"--name",
staged.name,
"-v",
f"{staged / 'src'}:/src:ro",
"-v",
f"{staged / 'driver.py'}:/driver.py:ro",
IMAGE,
"sh",
"-lc",
script,
],
capture=True,
check=False,
)
finally:
shutil.rmtree(staged, ignore_errors=True)
if proc.returncode != 0:
raise AssertionError(
f"{fstype}/{kind} driver failed:\n{proc.stdout}\n{proc.stderr}"
)
return proc.stdout
class TestE2ESnapshot(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
require_docker()
def assert_freezes(self, fstype: str) -> None:
output = drive(fstype, fstype, "supported")
self.assertIn("PASS the snapshot exposes the volume", output)
self.assertIn("PASS a later write does not reach the snapshot", output)
self.assertIn("PASS the snapshot is removed afterwards", output)
self.assertIn("ALL OK", output)
def test_btrfs_snapshot_freezes_the_volume(self) -> None:
self.assert_freezes("btrfs")
def test_zfs_snapshot_freezes_the_volume(self) -> None:
if not zfs_usable():
if required("zfs"):
self.fail(
"E2E_REQUIRE_FILESYSTEMS demands zfs, but this kernel provides no "
"zfs module; load it before running the suite"
)
self.skipTest(
"this kernel provides no zfs module, so no pool can be created"
)
self.assert_freezes("zfs")
def test_ext4_has_no_snapshot_and_says_so(self) -> None:
output = drive("ext4", "btrfs", "unsupported")
self.assertIn("PASS refused loudly", output)
def test_an_unknown_kind_is_refused_before_touching_the_filesystem(self) -> None:
output = drive("ext4", "lvm", "unsupported")
self.assertIn("PASS refused loudly", output)
if __name__ == "__main__":
unittest.main()