From f42a0dc7fefbc0f989d7e0136bd08832360c303f Mon Sep 17 00:00:00 2001 From: didericis Date: Sat, 11 Jul 2026 17:28:17 -0400 Subject: [PATCH] fix(firecracker): status() defers unverifiable nft, like the preflight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `status()` hard-failed when it couldn't confirm the nft table, but listing nftables usually needs root — so an unprivileged `backend status` reported "not ready" even with the pool fully up, making it useless as a launch gate (and skipping the firecracker integration test on a set-up host). Base readiness on what the launch preflight actually hard-requires: the TAP pool present (unprivileged, authoritative) + no range overlap. Report the nft table state (present / unverified / not-confirmable-unprivileged) but don't let it flip readiness — the post-boot isolation probe is the authoritative isolation check, same as the preflight's deferral. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck --- bot_bottle/backend/firecracker/setup.py | 19 ++++++++--- tests/unit/test_firecracker_backend.py | 43 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/bot_bottle/backend/firecracker/setup.py b/bot_bottle/backend/firecracker/setup.py index 931055a..c52de23 100644 --- a/bot_bottle/backend/firecracker/setup.py +++ b/bot_bottle/backend/firecracker/setup.py @@ -143,12 +143,13 @@ def teardown() -> int: def status() -> int: + # Readiness == what the launch preflight hard-requires: the TAP pool + # present (unprivileged, authoritative) and no range overlap. Listing + # the nft table usually needs root, so — like the preflight — an + # unconfirmable table is reported but NOT treated as not-ready; the + # post-boot isolation probe is the authoritative check. This keeps an + # unprivileged `backend status` usable as a launch gate. ok = True - if netpool.nft_table_present(): - sys.stderr.write(f"nft table inet {netpool.NFT_TABLE}: present\n") - else: - sys.stderr.write(f"nft table inet {netpool.NFT_TABLE}: MISSING\n") - ok = False missing = netpool.missing_taps() total = netpool.pool_size() if missing: @@ -157,6 +158,14 @@ def status() -> int: ok = False else: sys.stderr.write(f"TAP pool: {total}/{total} present\n") + if shutil.which("nft") is None: + sys.stderr.write(f"nft table inet {netpool.NFT_TABLE}: unverified " + f"(nft not on PATH; enforced + checked post-boot)\n") + elif netpool.nft_table_present(): + sys.stderr.write(f"nft table inet {netpool.NFT_TABLE}: present\n") + else: + sys.stderr.write(f"nft table inet {netpool.NFT_TABLE}: not confirmable " + f"unprivileged (listing needs root; verified post-boot)\n") conflicts = netpool.overlapping_routes() if conflicts: detail = ", ".join(f"{c.dst} dev {c.dev}" for c in conflicts) diff --git a/tests/unit/test_firecracker_backend.py b/tests/unit/test_firecracker_backend.py index 28f92c7..58c19a8 100644 --- a/tests/unit/test_firecracker_backend.py +++ b/tests/unit/test_firecracker_backend.py @@ -74,6 +74,49 @@ class TestNetpoolRenderers(unittest.TestCase): self.assertIn("firecracker-netpool.sh up", out) +class TestFirecrackerStatus(unittest.TestCase): + """`status()` gates launch: ready == TAP pool present + no overlap. + An nft table that can't be confirmed unprivileged is reported, not + treated as not-ready (the post-boot probe is authoritative).""" + + def _run(self): + import contextlib + import io + from bot_bottle.backend.firecracker import setup as fc_setup + buf = io.StringIO() + with contextlib.redirect_stderr(buf): + rc = fc_setup.status() + return rc, buf.getvalue() + + def test_ready_when_taps_present_even_if_nft_unverifiable(self): + from bot_bottle.backend.firecracker import setup as fc_setup + with patch.object(fc_setup.netpool, "missing_taps", return_value=[]), \ + patch.object(fc_setup.netpool, "overlapping_routes", return_value=[]), \ + patch.object(fc_setup.shutil, "which", return_value=None): + rc, out = self._run() + self.assertEqual(0, rc) + self.assertIn("unverified", out) + + def test_not_ready_when_taps_missing(self): + from bot_bottle.backend.firecracker import setup as fc_setup + with patch.object(fc_setup.netpool, "missing_taps", return_value=["bbfc0"]), \ + patch.object(fc_setup.netpool, "overlapping_routes", return_value=[]), \ + patch.object(fc_setup.shutil, "which", return_value=None): + rc, _ = self._run() + self.assertEqual(1, rc) + + def test_not_ready_on_range_overlap(self): + from bot_bottle.backend.firecracker import netpool + from bot_bottle.backend.firecracker import setup as fc_setup + conflict = netpool.RouteConflict(dst="10.243.0.0/24", dev="eth0") + with patch.object(fc_setup.netpool, "missing_taps", return_value=[]), \ + patch.object(fc_setup.netpool, "overlapping_routes", return_value=[conflict]), \ + patch.object(fc_setup.shutil, "which", return_value=None): + rc, out = self._run() + self.assertEqual(1, rc) + self.assertIn("CLASHES", out) + + class TestNetpoolOverlap(unittest.TestCase): """`overlapping_routes()` flags a pool base that collides with an existing host route (Tailscale CGNAT peer, docker/libvirt bridge,