fix(firecracker): status() defers unverifiable nft, like the preflight

`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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-11 17:28:17 -04:00
parent 480269b116
commit f42a0dc7fe
2 changed files with 57 additions and 5 deletions
+14 -5
View File
@@ -143,12 +143,13 @@ def teardown() -> int:
def status() -> 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 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() missing = netpool.missing_taps()
total = netpool.pool_size() total = netpool.pool_size()
if missing: if missing:
@@ -157,6 +158,14 @@ def status() -> int:
ok = False ok = False
else: else:
sys.stderr.write(f"TAP pool: {total}/{total} present\n") 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() conflicts = netpool.overlapping_routes()
if conflicts: if conflicts:
detail = ", ".join(f"{c.dst} dev {c.dev}" for c in conflicts) detail = ", ".join(f"{c.dst} dev {c.dev}" for c in conflicts)
+43
View File
@@ -74,6 +74,49 @@ class TestNetpoolRenderers(unittest.TestCase):
self.assertIn("firecracker-netpool.sh up", out) 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): class TestNetpoolOverlap(unittest.TestCase):
"""`overlapping_routes()` flags a pool base that collides with an """`overlapping_routes()` flags a pool base that collides with an
existing host route (Tailscale CGNAT peer, docker/libvirt bridge, existing host route (Tailscale CGNAT peer, docker/libvirt bridge,