This commit is contained in:
2026-08-02 09:10:55 +02:00
parent 95c34d4db0
commit 2e0e67ca87
10 changed files with 79 additions and 25 deletions

View File

@@ -100,7 +100,9 @@ def parse_args() -> argparse.Namespace:
if bool(args.snapshot) != bool(args.snapshot_subject): if bool(args.snapshot) != bool(args.snapshot_subject):
p.error("--snapshot and --snapshot-subject must be given together") p.error("--snapshot and --snapshot-subject must be given together")
if args.snapshot and args.shutdown: if args.snapshot and args.shutdown:
p.error("--shutdown is meaningless with --snapshot: containers are never stopped") p.error(
"--shutdown is meaningless with --snapshot: containers are never stopped"
)
if args.snapshot and args.hard_restart_projects: if args.snapshot and args.hard_restart_projects:
p.error( p.error(
"--hard-restart-projects is meaningless with --snapshot: the flag exists " "--hard-restart-projects is meaningless with --snapshot: the flag exists "

View File

@@ -1,4 +1,5 @@
"""Fixtures and paths the e2e suite builds its scenarios from.""" """Fixtures and paths the e2e suite builds its scenarios from."""
from __future__ import annotations from __future__ import annotations
import shutil import shutil

View File

@@ -1,4 +1,5 @@
"""Process, docker and readiness helpers for the e2e suite.""" """Process, docker and readiness helpers for the e2e suite."""
from __future__ import annotations from __future__ import annotations
import subprocess import subprocess

View File

@@ -25,7 +25,9 @@ GENERATION = f"{VERSIONS}/20260731"
def shell(command: str) -> list[str]: def shell(command: str) -> list[str]:
proc = subprocess.run(command, shell=True, capture_output=True, text=True) proc = subprocess.run(command, shell=True, capture_output=True, text=True)
if proc.returncode != 0: if proc.returncode != 0:
raise SnapshotError(f"{command} exited {proc.returncode}: {proc.stderr.strip()}") raise SnapshotError(
f"{command} exited {proc.returncode}: {proc.stderr.strip()}"
)
return proc.stdout.splitlines() return proc.stdout.splitlines()

View File

@@ -22,7 +22,9 @@ EXPECT = sys.argv[3]
def shell(command: str) -> list[str]: def shell(command: str) -> list[str]:
proc = subprocess.run(command, shell=True, capture_output=True, text=True) proc = subprocess.run(command, shell=True, capture_output=True, text=True)
if proc.returncode != 0: if proc.returncode != 0:
raise SnapshotError(f"{command} exited {proc.returncode}: {proc.stderr.strip()}") raise SnapshotError(
f"{command} exited {proc.returncode}: {proc.stderr.strip()}"
)
return proc.stdout.splitlines() return proc.stdout.splitlines()
@@ -55,5 +57,8 @@ with volume_snapshot(KIND, SUBJECT, "e2e", run=shell) as resolve:
root = Path(resolve(SUBJECT)) root = Path(resolve(SUBJECT))
check("the snapshot is removed afterwards", not root.exists() or not (root / "volumes").exists()) check(
"the snapshot is removed afterwards",
not root.exists() or not (root / "volumes").exists(),
)
print("ALL OK", flush=True) print("ALL OK", flush=True)

View File

@@ -29,7 +29,7 @@ LOOP_FS = {
"ext4": "mkdir -p /subject/docker", "ext4": "mkdir -p /subject/docker",
} }
# A container carries no /lib/modules, so modprobe fails even on a loaded module. # 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 ]' ZFS_READY = "{ [ -c /dev/zfs ] || modprobe zfs 2>/dev/null; }; [ -c /dev/zfs ]"
def mount_script(fstype: str) -> str: def mount_script(fstype: str) -> str:
@@ -51,7 +51,13 @@ def zfs_usable() -> bool:
"""Whether this host's kernel can serve zfs to a privileged container.""" """Whether this host's kernel can serve zfs to a privileged container."""
proc = run( proc = run(
[ [
"docker", "run", "--rm", "--privileged", IMAGE, "sh", "-lc", "docker",
"run",
"--rm",
"--privileged",
IMAGE,
"sh",
"-lc",
f"apk add -q zfs >/dev/null 2>&1 && {ZFS_READY}", f"apk add -q zfs >/dev/null 2>&1 && {ZFS_READY}",
], ],
capture=True, capture=True,
@@ -87,11 +93,20 @@ def drive(fstype: str, kind: str, expect: str) -> str:
try: try:
proc = run( proc = run(
[ [
"docker", "run", "--rm", "--privileged", "docker",
"--name", staged.name, "run",
"-v", f"{staged / 'src'}:/src:ro", "--rm",
"-v", f"{staged / 'driver.py'}:/driver.py:ro", "--privileged",
IMAGE, "sh", "-lc", script, "--name",
staged.name,
"-v",
f"{staged / 'src'}:/src:ro",
"-v",
f"{staged / 'driver.py'}:/driver.py:ro",
IMAGE,
"sh",
"-lc",
script,
], ],
capture=True, capture=True,
check=False, check=False,
@@ -99,7 +114,9 @@ def drive(fstype: str, kind: str, expect: str) -> str:
finally: finally:
shutil.rmtree(staged, ignore_errors=True) shutil.rmtree(staged, ignore_errors=True)
if proc.returncode != 0: if proc.returncode != 0:
raise AssertionError(f"{fstype}/{kind} driver failed:\n{proc.stdout}\n{proc.stderr}") raise AssertionError(
f"{fstype}/{kind} driver failed:\n{proc.stdout}\n{proc.stderr}"
)
return proc.stdout return proc.stdout
@@ -125,7 +142,9 @@ class TestE2ESnapshot(unittest.TestCase):
"E2E_REQUIRE_FILESYSTEMS demands zfs, but this kernel provides no " "E2E_REQUIRE_FILESYSTEMS demands zfs, but this kernel provides no "
"zfs module; load it before running the suite" "zfs module; load it before running the suite"
) )
self.skipTest("this kernel provides no zfs module, so no pool can be created") self.skipTest(
"this kernel provides no zfs module, so no pool can be created"
)
self.assert_freezes("zfs") self.assert_freezes("zfs")
def test_ext4_has_no_snapshot_and_says_so(self) -> None: def test_ext4_has_no_snapshot_and_says_so(self) -> None:

View File

@@ -68,11 +68,20 @@ class TestE2ESnapshotDatabase(unittest.TestCase):
try: try:
proc = run( proc = run(
[ [
"docker", "run", "--rm", "--privileged", "docker",
"--name", staged.name, "run",
"-v", f"{staged / 'src'}:/src:ro", "--rm",
"-v", f"{staged / 'driver.py'}:/driver.py:ro", "--privileged",
IMAGE, "sh", "-lc", SCRIPT, "--name",
staged.name,
"-v",
f"{staged / 'src'}:/src:ro",
"-v",
f"{staged / 'driver.py'}:/driver.py:ro",
IMAGE,
"sh",
"-lc",
SCRIPT,
], ],
capture=True, capture=True,
check=False, check=False,

View File

@@ -12,6 +12,7 @@ from baudolo.backup.snapshot import volume_snapshot
def stubbed_snapshot(kind: str, subject: str, tag: str): def stubbed_snapshot(kind: str, subject: str, tag: str):
return volume_snapshot(kind, subject, tag, run=lambda command: []) return volume_snapshot(kind, subject, tag, run=lambda command: [])
ARGV = [ ARGV = [
"baudolo", "baudolo",
"--compose-dir", "--compose-dir",

View File

@@ -39,7 +39,9 @@ class TestSnapshotFlags(unittest.TestCase):
parse("--snapshot", "ext4", "--snapshot-subject", "/var/lib/docker") parse("--snapshot", "ext4", "--snapshot-subject", "/var/lib/docker")
def test_zfs_is_accepted(self) -> None: def test_zfs_is_accepted(self) -> None:
self.assertEqual(parse("--snapshot", "zfs", "--snapshot-subject", "/d").snapshot, "zfs") self.assertEqual(
parse("--snapshot", "zfs", "--snapshot-subject", "/d").snapshot, "zfs"
)
def test_shutdown_is_rejected_because_nothing_is_stopped(self) -> None: def test_shutdown_is_rejected_because_nothing_is_stopped(self) -> None:
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):

View File

@@ -35,11 +35,15 @@ class TestBtrfs(unittest.TestCase):
run = Runner() run = Runner()
with volume_snapshot("btrfs", "/var/lib/docker", "20260731", run=run): with volume_snapshot("btrfs", "/var/lib/docker", "20260731", run=run):
pass pass
self.assertEqual(run.calls[-1], "btrfs subvolume delete /var/lib/docker/.baudolo-20260731") self.assertEqual(
run.calls[-1], "btrfs subvolume delete /var/lib/docker/.baudolo-20260731"
)
def test_it_maps_a_volume_path_into_the_snapshot(self) -> None: def test_it_maps_a_volume_path_into_the_snapshot(self) -> None:
run = Runner() run = Runner()
with volume_snapshot("btrfs", "/var/lib/docker", "20260731", run=run) as resolve: with volume_snapshot(
"btrfs", "/var/lib/docker", "20260731", run=run
) as resolve:
self.assertEqual( self.assertEqual(
resolve("/var/lib/docker/volumes/postgres_data/_data"), resolve("/var/lib/docker/volumes/postgres_data/_data"),
"/var/lib/docker/.baudolo-20260731/volumes/postgres_data/_data", "/var/lib/docker/.baudolo-20260731/volumes/postgres_data/_data",
@@ -47,7 +51,9 @@ class TestBtrfs(unittest.TestCase):
def test_it_keeps_the_trailing_slash_rsync_reads_as_contents(self) -> None: def test_it_keeps_the_trailing_slash_rsync_reads_as_contents(self) -> None:
run = Runner() run = Runner()
with volume_snapshot("btrfs", "/var/lib/docker", "20260731", run=run) as resolve: with volume_snapshot(
"btrfs", "/var/lib/docker", "20260731", run=run
) as resolve:
self.assertEqual( self.assertEqual(
resolve("/var/lib/docker/volumes/postgres_data/_data/"), resolve("/var/lib/docker/volumes/postgres_data/_data/"),
"/var/lib/docker/.baudolo-20260731/volumes/postgres_data/_data/", "/var/lib/docker/.baudolo-20260731/volumes/postgres_data/_data/",
@@ -102,14 +108,20 @@ class TestRejections(unittest.TestCase):
def test_a_path_outside_the_subject_is_rejected(self) -> None: def test_a_path_outside_the_subject_is_rejected(self) -> None:
run = Runner() run = Runner()
with volume_snapshot("btrfs", "/var/lib/docker", "20260731", run=run) as resolve: with volume_snapshot(
"btrfs", "/var/lib/docker", "20260731", run=run
) as resolve:
with self.assertRaises(SnapshotError): with self.assertRaises(SnapshotError):
resolve("/etc/passwd") resolve("/etc/passwd")
def test_the_subject_itself_resolves_to_the_snapshot_root(self) -> None: def test_the_subject_itself_resolves_to_the_snapshot_root(self) -> None:
run = Runner() run = Runner()
with volume_snapshot("btrfs", "/var/lib/docker", "20260731", run=run) as resolve: with volume_snapshot(
self.assertEqual(resolve("/var/lib/docker"), "/var/lib/docker/.baudolo-20260731") "btrfs", "/var/lib/docker", "20260731", run=run
) as resolve:
self.assertEqual(
resolve("/var/lib/docker"), "/var/lib/docker/.baudolo-20260731"
)
class Busy(Runner): class Busy(Runner):