2026-09-14 11:50:22 +01:00
|
|
|
"""Fault-injection checks: python3 scripts/deploy-check.py.
|
|
|
|
|
|
|
|
|
|
All system/profile/bootloader operations are replaced with filesystem fixtures.
|
|
|
|
|
No Nix build, systemd unit, network request or production activation is performed.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import contextlib
|
|
|
|
|
import importlib.util
|
|
|
|
|
import io
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import signal
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import tempfile
|
|
|
|
|
import time
|
|
|
|
|
import unittest
|
|
|
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
|
|
|
"deploy", Path(__file__).with_name("deploy.py")
|
|
|
|
|
)
|
|
|
|
|
m = importlib.util.module_from_spec(spec)
|
|
|
|
|
spec.loader.exec_module(m)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Fixture(m.Deployment):
|
|
|
|
|
def __init__(self, root):
|
|
|
|
|
self.calls = []
|
|
|
|
|
self.fail = set()
|
|
|
|
|
self.health_calls = 0
|
|
|
|
|
self.root = root
|
|
|
|
|
self.candidate = root / "candidate"
|
|
|
|
|
self.old_running = root / "running"
|
|
|
|
|
self.old_boot = root / "boot"
|
|
|
|
|
recovery = self.candidate / "etc/systemd/system/fred-deploy-recover.service"
|
|
|
|
|
recovery.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
recovery.write_text("fixture")
|
|
|
|
|
for system in (self.candidate, self.old_running, self.old_boot):
|
|
|
|
|
(system / "bin").mkdir(parents=True, exist_ok=True)
|
|
|
|
|
(system / "bin/switch-to-configuration").write_text("fixture")
|
|
|
|
|
runner = root / "tools/bin/worker"
|
|
|
|
|
runner.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
runner.write_text("fixture")
|
|
|
|
|
super().__init__(
|
|
|
|
|
runner, root / "state", root / "roots", root / "lock", self.fake
|
|
|
|
|
)
|
|
|
|
|
self.current = root / "current"
|
|
|
|
|
self.profile = root / "profile"
|
|
|
|
|
if not self.current.is_symlink():
|
|
|
|
|
self.current.symlink_to(self.old_running)
|
|
|
|
|
if not self.profile.is_symlink():
|
|
|
|
|
self.profile.symlink_to(self.old_boot)
|
|
|
|
|
self.boot_default = self.old_boot
|
|
|
|
|
|
|
|
|
|
def fake(self, args, timeout=120):
|
|
|
|
|
args = list(map(str, args))
|
|
|
|
|
self.calls.append(args)
|
|
|
|
|
step = args[0]
|
|
|
|
|
if args[:2] == ["nix", "build"]:
|
|
|
|
|
step = "build"
|
|
|
|
|
self.pin("candidate", self.candidate)
|
|
|
|
|
elif args[0] == "systemd-run":
|
|
|
|
|
step = "arm"
|
|
|
|
|
elif args[0] == "nix-env":
|
|
|
|
|
target = Path(args[-1])
|
|
|
|
|
self.profile.unlink()
|
|
|
|
|
self.profile.symlink_to(target)
|
|
|
|
|
step = "commit-profile" if target == self.candidate else "restore-profile"
|
|
|
|
|
elif args[0].endswith("switch-to-configuration"):
|
|
|
|
|
target = Path(args[0]).parents[1]
|
|
|
|
|
if args[1] == "test":
|
|
|
|
|
self.current.unlink()
|
|
|
|
|
self.current.symlink_to(target)
|
|
|
|
|
else:
|
|
|
|
|
self.boot_default = target
|
|
|
|
|
step = ("candidate-" if target == self.candidate else "restore-") + args[1]
|
|
|
|
|
elif args[:2] == ["systemctl", "stop"]:
|
|
|
|
|
step = "disarm" if args[-1].endswith(".timer") else "stop-worker"
|
|
|
|
|
elif args[:2] == ["systemctl", "show"]:
|
|
|
|
|
return "not-found"
|
|
|
|
|
if step in self.fail:
|
|
|
|
|
raise RuntimeError("Injected failure after side effect: " + step)
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
def healthy(self):
|
|
|
|
|
self.health_calls += 1
|
|
|
|
|
if self.current.resolve() == self.candidate:
|
|
|
|
|
if "health" in self.fail or (
|
|
|
|
|
"final-health" in self.fail and self.health_calls == 2
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("Injected unhealthy router")
|
|
|
|
|
elif "restore-health" in self.fail:
|
|
|
|
|
raise RuntimeError("Restored configuration is unhealthy")
|
|
|
|
|
|
|
|
|
|
def wait_healthy(self, duration=90):
|
|
|
|
|
self.healthy()
|
|
|
|
|
|
|
|
|
|
def deploy(self, ident="a" * 32, flake="fixture", reboot=False):
|
2026-09-14 15:07:40 +01:00
|
|
|
super().deploy(ident, flake, reboot)
|
2026-09-14 11:50:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Transactions(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.temp = tempfile.TemporaryDirectory(prefix="fred-deploy-check-")
|
|
|
|
|
self.addCleanup(self.temp.cleanup)
|
|
|
|
|
self.f = Fixture(Path(self.temp.name))
|
|
|
|
|
self.output = contextlib.redirect_stdout(io.StringIO())
|
|
|
|
|
self.output.__enter__()
|
|
|
|
|
self.addCleanup(self.output.__exit__, None, None, None)
|
|
|
|
|
|
|
|
|
|
def assert_restored(self):
|
|
|
|
|
self.assertEqual(self.f.current.resolve(), self.f.old_running)
|
|
|
|
|
self.assertEqual(self.f.profile.resolve(), self.f.old_boot)
|
|
|
|
|
self.assertEqual(self.f.boot_default, self.f.old_boot)
|
|
|
|
|
|
|
|
|
|
def pending(self, phase="activating", boot_id=None):
|
|
|
|
|
self.f.pin("old-running", self.f.old_running)
|
|
|
|
|
self.f.pin("old-boot", self.f.old_boot)
|
|
|
|
|
record = {
|
|
|
|
|
"id": "a" * 32,
|
|
|
|
|
"unit": "fred-deploy-" + "a" * 32 + ".service",
|
|
|
|
|
"old_running": str(self.f.old_running),
|
|
|
|
|
"old_boot": str(self.f.old_boot),
|
|
|
|
|
"candidate": str(self.f.candidate),
|
|
|
|
|
"boot_id": boot_id or self.f.boot_id(),
|
|
|
|
|
}
|
|
|
|
|
self.f.save(record, phase)
|
|
|
|
|
self.f.current.unlink()
|
|
|
|
|
self.f.current.symlink_to(self.f.candidate)
|
|
|
|
|
return record
|
|
|
|
|
|
|
|
|
|
def test_success_orders_test_health_and_boot_commit(self):
|
|
|
|
|
self.f.deploy()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "committed")
|
|
|
|
|
self.assertEqual(self.f.profile.resolve(), self.f.candidate)
|
|
|
|
|
commands = self.f.calls
|
|
|
|
|
arm = next(i for i, c in enumerate(commands) if c[0] == "systemd-run")
|
|
|
|
|
test = commands.index(
|
|
|
|
|
[str(self.f.candidate / "bin/switch-to-configuration"), "test"]
|
|
|
|
|
)
|
|
|
|
|
boot = commands.index(
|
|
|
|
|
[str(self.f.candidate / "bin/switch-to-configuration"), "boot"]
|
|
|
|
|
)
|
|
|
|
|
self.assertLess(arm, test)
|
|
|
|
|
self.assertLess(test, boot)
|
|
|
|
|
self.assertEqual(self.f.health_calls, 2)
|
|
|
|
|
self.assertFalse(list(self.f.roots.iterdir()))
|
|
|
|
|
|
|
|
|
|
def test_build_failure_never_activates_or_arms(self):
|
|
|
|
|
self.f.fail.add("build")
|
|
|
|
|
with self.assertRaises(RuntimeError):
|
|
|
|
|
self.f.deploy()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "build-failed")
|
|
|
|
|
self.assert_restored()
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
any(
|
|
|
|
|
c[0] == "systemd-run" or c[0].endswith("switch-to-configuration")
|
|
|
|
|
for c in self.f.calls
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_candidate_must_retain_boot_recovery(self):
|
|
|
|
|
(self.f.candidate / "etc/systemd/system/fred-deploy-recover.service").unlink()
|
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "missing the boot recovery"):
|
|
|
|
|
self.f.deploy()
|
|
|
|
|
self.assert_restored()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "build-failed")
|
|
|
|
|
|
|
|
|
|
def test_activation_and_commit_failure_paths_restore_both_systems(self):
|
|
|
|
|
for failure in [
|
|
|
|
|
"arm",
|
|
|
|
|
"candidate-test",
|
|
|
|
|
"health",
|
|
|
|
|
"commit-profile",
|
|
|
|
|
"candidate-boot",
|
|
|
|
|
"final-health",
|
|
|
|
|
]:
|
|
|
|
|
with self.subTest(failure=failure), tempfile.TemporaryDirectory() as temp:
|
|
|
|
|
f = Fixture(Path(temp))
|
|
|
|
|
f.fail.add(failure)
|
|
|
|
|
with self.assertRaises(RuntimeError):
|
|
|
|
|
f.deploy()
|
|
|
|
|
self.assertEqual(f.read()["phase"], "rolled-back")
|
|
|
|
|
self.assertEqual(f.current.resolve(), f.old_running)
|
|
|
|
|
self.assertEqual(f.profile.resolve(), f.old_boot)
|
|
|
|
|
self.assertEqual(f.boot_default, f.old_boot)
|
|
|
|
|
|
|
|
|
|
def test_failed_boot_restore_still_restores_runtime_and_retains_roots(self):
|
|
|
|
|
self.f.fail.update({"candidate-test", "restore-boot"})
|
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "needs attention"):
|
|
|
|
|
self.f.deploy()
|
|
|
|
|
self.assertEqual(self.f.current.resolve(), self.f.old_running)
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "rollback-failed")
|
|
|
|
|
self.assertTrue((self.f.roots / "old-boot").exists())
|
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "needs recovery"):
|
|
|
|
|
self.f.deploy("b" * 32)
|
|
|
|
|
self.f.fail.clear()
|
|
|
|
|
self.f.recover()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "rolled-back")
|
|
|
|
|
|
|
|
|
|
def test_unhealthy_restored_router_is_not_reported_as_success(self):
|
|
|
|
|
self.f.fail.update({"candidate-test", "restore-health"})
|
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "needs attention"):
|
|
|
|
|
self.f.deploy()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "rollback-failed")
|
|
|
|
|
|
|
|
|
|
def test_late_timer_cannot_undo_commit(self):
|
|
|
|
|
self.f.fail.add("disarm")
|
|
|
|
|
self.f.deploy()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "committed")
|
|
|
|
|
before = len(self.f.calls)
|
|
|
|
|
self.f.recover("a" * 32)
|
|
|
|
|
self.assertEqual(len(self.f.calls), before)
|
|
|
|
|
|
|
|
|
|
def test_stale_timer_cannot_touch_new_transaction(self):
|
|
|
|
|
self.pending()
|
|
|
|
|
self.f.recover("b" * 32)
|
|
|
|
|
self.assertFalse(self.f.calls)
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "activating")
|
|
|
|
|
|
|
|
|
|
def test_boot_service_does_not_cancel_its_own_installation(self):
|
|
|
|
|
self.pending()
|
|
|
|
|
self.f.recover(boot=True)
|
|
|
|
|
self.assertFalse(self.f.calls)
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "activating")
|
|
|
|
|
|
|
|
|
|
def test_next_boot_recovers_even_if_worker_unit_was_collected(self):
|
|
|
|
|
self.pending(boot_id="previous-boot")
|
|
|
|
|
self.f.fail.add("stop-worker")
|
|
|
|
|
self.f.recover(boot=True)
|
|
|
|
|
self.assert_restored()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "rolled-back")
|
|
|
|
|
|
|
|
|
|
def test_interrupted_build_can_be_retried_without_activation(self):
|
|
|
|
|
self.pending(phase="building")
|
|
|
|
|
# A building record never altered the running system.
|
|
|
|
|
self.f.current.unlink()
|
|
|
|
|
self.f.current.symlink_to(self.f.old_running)
|
|
|
|
|
self.f.deploy("b" * 32)
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "committed")
|
|
|
|
|
|
|
|
|
|
def test_boot_time_deployment_recovers_previous_boot_first(self):
|
|
|
|
|
self.pending(boot_id="previous-boot")
|
|
|
|
|
self.f.deploy("b" * 32)
|
|
|
|
|
restore = self.f.calls.index(
|
|
|
|
|
[str(self.f.old_running / "bin/switch-to-configuration"), "test"]
|
|
|
|
|
)
|
|
|
|
|
build = next(
|
|
|
|
|
i for i, args in enumerate(self.f.calls) if args[:2] == ["nix", "build"]
|
|
|
|
|
)
|
|
|
|
|
self.assertLess(restore, build)
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "committed")
|
|
|
|
|
|
|
|
|
|
def test_failed_state_write_does_not_prevent_restoring_system(self):
|
|
|
|
|
record = self.pending()
|
|
|
|
|
save = self.f.save
|
|
|
|
|
|
|
|
|
|
def fail_start(record, phase, **fields):
|
|
|
|
|
if phase == "rolling-back":
|
|
|
|
|
raise OSError("disk full")
|
|
|
|
|
return save(record, phase, **fields)
|
|
|
|
|
|
|
|
|
|
self.f.save = fail_start
|
|
|
|
|
self.f.rollback_locked(record)
|
|
|
|
|
self.assert_restored()
|
|
|
|
|
|
|
|
|
|
def test_recovery_rereads_state_after_stopping_worker(self):
|
|
|
|
|
record = self.pending()
|
|
|
|
|
command = self.f.command
|
|
|
|
|
|
|
|
|
|
def race(args, **kw):
|
|
|
|
|
result = command(args, **kw)
|
|
|
|
|
if args[:2] == ["systemctl", "stop"]:
|
|
|
|
|
self.f.save({**record, "id": "b" * 32}, "armed")
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
self.f.command = race
|
|
|
|
|
self.f.recover("a" * 32)
|
|
|
|
|
self.assertEqual(self.f.read()["id"], "b" * 32)
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
any(c[0].endswith("switch-to-configuration") for c in self.f.calls)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_transaction_lock_excludes_another_process(self):
|
|
|
|
|
marker = self.f.root / "entered"
|
|
|
|
|
code = "import fcntl,pathlib,sys; f=open(sys.argv[1],'a'); fcntl.flock(f,fcntl.LOCK_EX); pathlib.Path(sys.argv[2]).touch()"
|
|
|
|
|
with self.f.exclusive():
|
|
|
|
|
child = subprocess.Popen(
|
|
|
|
|
[sys.executable, "-c", code, str(self.f.lock), str(marker)]
|
|
|
|
|
)
|
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
self.assertFalse(marker.exists())
|
|
|
|
|
child.wait(timeout=3)
|
|
|
|
|
self.assertEqual(child.returncode, 0)
|
|
|
|
|
self.assertTrue(marker.exists())
|
|
|
|
|
|
|
|
|
|
def test_independent_recovery_stops_hung_worker_before_taking_lock(self):
|
|
|
|
|
self.pending()
|
|
|
|
|
marker = self.f.root / "holding-lock"
|
|
|
|
|
code = "import fcntl,pathlib,sys,time; f=open(sys.argv[1],'a'); fcntl.flock(f,fcntl.LOCK_EX); pathlib.Path(sys.argv[2]).touch(); time.sleep(30)"
|
|
|
|
|
worker = subprocess.Popen(
|
|
|
|
|
[sys.executable, "-c", code, str(self.f.lock), str(marker)]
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
deadline = time.monotonic() + 3
|
|
|
|
|
while not marker.exists() and time.monotonic() < deadline:
|
|
|
|
|
time.sleep(0.01)
|
|
|
|
|
self.assertTrue(marker.exists())
|
|
|
|
|
recovery = subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
sys.executable,
|
|
|
|
|
"-B",
|
|
|
|
|
__file__,
|
|
|
|
|
"recover-fixture",
|
|
|
|
|
str(self.f.root),
|
|
|
|
|
str(worker.pid),
|
|
|
|
|
],
|
|
|
|
|
timeout=5,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(recovery.returncode, 0, recovery.stdout + recovery.stderr)
|
|
|
|
|
worker.wait(timeout=2)
|
|
|
|
|
self.assertEqual(worker.returncode, -signal.SIGKILL)
|
|
|
|
|
self.assert_restored()
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "rolled-back")
|
|
|
|
|
finally:
|
|
|
|
|
if worker.poll() is None:
|
|
|
|
|
worker.kill()
|
|
|
|
|
worker.wait()
|
|
|
|
|
|
|
|
|
|
def test_command_timeout_kills_descendants(self):
|
|
|
|
|
marker = self.f.root / "unexpected-child-write"
|
|
|
|
|
child = (
|
|
|
|
|
"import pathlib,time,sys; time.sleep(.5); pathlib.Path(sys.argv[1]).touch()"
|
|
|
|
|
)
|
|
|
|
|
parent = "import subprocess,sys,time; subprocess.Popen([sys.executable,'-c',sys.argv[1],sys.argv[2]]); time.sleep(30)"
|
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "timed out"):
|
|
|
|
|
m.run([sys.executable, "-c", parent, child, str(marker)], timeout=0.1)
|
|
|
|
|
time.sleep(0.6)
|
|
|
|
|
self.assertFalse(marker.exists())
|
|
|
|
|
|
|
|
|
|
def test_reboot_is_requested_only_after_commit(self):
|
|
|
|
|
self.f.deploy(reboot=True)
|
|
|
|
|
self.assertEqual(self.f.calls[-1], ["systemctl", "reboot", "--no-block"])
|
|
|
|
|
self.assertEqual(self.f.read()["phase"], "committed")
|
|
|
|
|
|
|
|
|
|
def test_router_health_rejects_missing_essential_functions(self):
|
|
|
|
|
for failure in [
|
|
|
|
|
None,
|
|
|
|
|
"service",
|
|
|
|
|
"address",
|
|
|
|
|
"route",
|
|
|
|
|
"firewall",
|
|
|
|
|
"nat",
|
|
|
|
|
"wan",
|
|
|
|
|
"dns",
|
|
|
|
|
"ssh",
|
|
|
|
|
"proxy",
|
|
|
|
|
]:
|
|
|
|
|
with self.subTest(failure=failure):
|
|
|
|
|
|
|
|
|
|
def health_command(args, timeout=120):
|
|
|
|
|
if args[0] == "systemctl":
|
|
|
|
|
self.assertNotIn("jellyfin", args)
|
|
|
|
|
if failure == "service":
|
|
|
|
|
raise RuntimeError("inactive")
|
|
|
|
|
return ""
|
|
|
|
|
if args[:4] == ["ip", "-j", "-4", "address"]:
|
|
|
|
|
return json.dumps(
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"addr_info": []
|
|
|
|
|
if failure == "address"
|
|
|
|
|
else [{"local": "10.0.0.1", "prefixlen": 24}]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
if args[0] == "ip":
|
|
|
|
|
return json.dumps(
|
|
|
|
|
[]
|
|
|
|
|
if failure == "route"
|
|
|
|
|
else [{"dst": "default", "dev": "eno1"}]
|
|
|
|
|
)
|
|
|
|
|
if args[0] == "nft":
|
|
|
|
|
if args[-1] == "filter":
|
|
|
|
|
return json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"nftables": [
|
|
|
|
|
{
|
|
|
|
|
"chain": {
|
|
|
|
|
"hook": hook,
|
|
|
|
|
"policy": "accept"
|
|
|
|
|
if failure == "firewall"
|
|
|
|
|
else "drop",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for hook in ["input", "forward"]
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"nftables": [
|
|
|
|
|
{"chain": {"hook": hook}}
|
|
|
|
|
for hook in ["prerouting", "postrouting"]
|
|
|
|
|
]
|
|
|
|
|
+ (
|
|
|
|
|
[]
|
|
|
|
|
if failure == "nat"
|
|
|
|
|
else [
|
|
|
|
|
{
|
|
|
|
|
"rule": {
|
|
|
|
|
"expr": [
|
|
|
|
|
{"dnat": {}},
|
|
|
|
|
{"masquerade": None},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
if args[0] == "ping":
|
|
|
|
|
if failure == "wan":
|
|
|
|
|
raise RuntimeError("unreachable")
|
|
|
|
|
return ""
|
|
|
|
|
if args[0] == "dig":
|
|
|
|
|
return "" if failure == "dns" else "93.184.215.14"
|
|
|
|
|
if args[0] == "curl":
|
|
|
|
|
return "503" if failure == "proxy" else "302"
|
|
|
|
|
raise AssertionError(args)
|
|
|
|
|
|
|
|
|
|
self.f.command = health_command
|
|
|
|
|
ssh = MagicMock()
|
|
|
|
|
ssh.__enter__.return_value.recv.return_value = (
|
|
|
|
|
b"HTTP/1.1" if failure == "ssh" else b"SSH-2.0-fixture"
|
|
|
|
|
)
|
|
|
|
|
read_text = Path.read_text
|
|
|
|
|
|
|
|
|
|
def read(path, *args, **kw):
|
|
|
|
|
if str(path) == "/proc/sys/net/ipv4/ip_forward":
|
|
|
|
|
return "1"
|
|
|
|
|
return read_text(path, *args, **kw)
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch.object(m.socket, "create_connection", return_value=ssh),
|
|
|
|
|
patch.object(Path, "read_text", read),
|
|
|
|
|
):
|
|
|
|
|
if failure:
|
|
|
|
|
with self.assertRaises(RuntimeError):
|
|
|
|
|
m.Deployment.healthy(self.f)
|
|
|
|
|
else:
|
|
|
|
|
m.Deployment.healthy(self.f)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
if len(sys.argv) > 1 and sys.argv[1] == "recover-fixture":
|
|
|
|
|
fixture = Fixture(Path(sys.argv[2]))
|
|
|
|
|
worker_pid = int(sys.argv[3])
|
|
|
|
|
fake = fixture.command
|
|
|
|
|
|
|
|
|
|
def stop_fixture_worker(args, **kwargs):
|
|
|
|
|
if args[:2] == ["systemctl", "stop"] and args[-1].endswith(".service"):
|
|
|
|
|
os.kill(worker_pid, signal.SIGKILL)
|
|
|
|
|
return fake(args, **kwargs)
|
|
|
|
|
|
|
|
|
|
fixture.command = stop_fixture_worker
|
|
|
|
|
fixture.recover("a" * 32)
|
|
|
|
|
else:
|
|
|
|
|
unittest.main()
|