refactor(firecracker): single-source the network-pool defaults
test / unit (pull_request) Successful in 54s
test / integration (pull_request) Successful in 15s
test / coverage (pull_request) Successful in 56s
lint / lint (push) Successful in 1m59s
test / unit (push) Successful in 53s
test / integration (push) Successful in 20s
test / coverage (push) Successful in 58s
Update Quality Badges / update-badges (push) Successful in 57s

The pool params (size, IP base, iface prefix, nft table) were triplicated
— hardcoded in netpool.py, scripts/firecracker-netpool.sh, and
nix/firecracker-netpool.nix — plus the IP math (3x) and the nft ruleset
(2x). Nothing enforced agreement; changing the base (ce3fad9, off CGNAT)
forced a coordinated three-file edit, and a missed one would silently
provision a range the launcher doesn't expect.

Collapse to one source of truth:

  * netpool.defaults.env — a plain KEY=VALUE file (bash-sourceable,
    systemd EnvironmentFile-compatible, Python- and Nix-parseable) holding
    the four defaults. A BOT_BOTTLE_FC_* env var still overrides any key.
  * netpool.py reads it for the Python defaults (missing file = hard
    error, not confusing empty defaults).
  * the shell script falls back to it (no literal `:-8` / `10.243.0.0`),
    and its `up` is now non-destructive/idempotent (only creates a
    missing TAP), so re-running never cuts a live VM.
  * the Nix module readFile-parses it for its option defaults and
    delegates bring-up to the SAME shell script (dropping its duplicate
    IP math, nft ruleset, and TAP loop) — passing every value as
    Environment= so the store-detached script never needs the file.

Net: defaults 3x -> 1x, nft ruleset 2x -> 1x, TAP loop 2x -> 1x. The one
remaining IP-math dup (Python launch-addressing vs bash bring-up) is
justified — different runtimes. Tests now guard the invariant (Python
reads the shared file; the script/module hold no literals) instead of
pinning duplicated strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit was merged in pull request #350.
This commit is contained in:
2026-07-12 17:33:27 -04:00
parent 1bfc6c5d16
commit 2b970d1170
5 changed files with 214 additions and 140 deletions
@@ -0,0 +1,17 @@
# Firecracker network-pool defaults — the SINGLE source of these values.
#
# Read by every consumer so they can't drift:
# * netpool.py — parses this for the Python defaults (below).
# * scripts/firecracker-netpool.sh — falls back to these when the
# matching BOT_BOTTLE_FC_* env var is unset.
# * nix/firecracker-netpool.nix — readFile-parses this for its option
# defaults, then passes the resolved values back as Environment=.
#
# Plain KEY=VALUE (no quoting, no inline comments, no spaces around `=`)
# so it is bash-sourceable, systemd EnvironmentFile-compatible, and
# trivially parseable from Python and Nix. A real BOT_BOTTLE_FC_* env
# var of the same name always overrides the value here.
BOT_BOTTLE_FC_POOL_SIZE=8
BOT_BOTTLE_FC_IP_BASE=10.243.0.0
BOT_BOTTLE_FC_IFACE_PREFIX=bbfc
BOT_BOTTLE_FC_NFT_TABLE=bot_bottle_fc
+40 -9
View File
@@ -4,11 +4,13 @@ config renderers (shell command + NixOS module) shown to operators.
The Firecracker backend needs a privileged one-time network setup:
a pool of point-to-point TAP devices (owned by the invoking user, so
`./cli.py start` never needs root) and a dedicated nftables table that
isolates every VM. This module is the single source of truth for the
pool parameters — the shell script (`scripts/firecracker-netpool.sh`),
the NixOS module (`nix/firecracker-netpool.nix`), and the backend's
fail-closed preflight all derive from these constants so they can't
drift.
isolates every VM. The pool parameters live in exactly one place —
`netpool.defaults.env`, a plain KEY=VALUE file next to this module —
and every consumer reads *that*: this module (below), the shell script
(`scripts/firecracker-netpool.sh`), and the NixOS module
(`nix/firecracker-netpool.nix`). A `BOT_BOTTLE_FC_*` env var of the
same name always overrides the file, and the backend's fail-closed
preflight derives from these accessors, so nothing can drift.
Topology (per slot i):
* TAP ``bbfc{i}`` — no shared bridge, so no docker0 / virbr0 / cni0
@@ -43,18 +45,47 @@ from typing import IO
from ...log import die
# The pool defaults live in one shared file (see module docstring); the
# shell script and NixOS module read the same file, so the values can't
# drift. This is a packaged data file — a missing/broken install is a
# hard error, surfaced here rather than as confusing empty defaults.
DEFAULTS_FILE = Path(__file__).with_name("netpool.defaults.env")
def _load_defaults() -> dict[str, str]:
out: dict[str, str] = {}
for raw in DEFAULTS_FILE.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
out[key.strip()] = value.strip()
return out
_DEFAULTS = _load_defaults()
def _cfg(key: str) -> str:
"""A `BOT_BOTTLE_FC_*` env var overrides the shared-file default."""
try:
return os.environ.get(key) or _DEFAULTS[key]
except KeyError:
die(f"{key} is missing from {DEFAULTS_FILE.name} (broken install)")
# Interface names are capped at 15 chars (IFNAMSIZ-1); "bbfc" + a small
# index stays well under that and is distinctive enough to grep for.
IFACE_PREFIX = os.environ.get("BOT_BOTTLE_FC_IFACE_PREFIX", "bbfc")
NFT_TABLE = "bot_bottle_fc"
IFACE_PREFIX = _cfg("BOT_BOTTLE_FC_IFACE_PREFIX")
NFT_TABLE = _cfg("BOT_BOTTLE_FC_NFT_TABLE")
def pool_size() -> int:
return int(os.environ.get("BOT_BOTTLE_FC_POOL_SIZE", "8"))
return int(_cfg("BOT_BOTTLE_FC_POOL_SIZE"))
def ip_base() -> str:
return os.environ.get("BOT_BOTTLE_FC_IP_BASE", "10.243.0.0")
return _cfg("BOT_BOTTLE_FC_IP_BASE")
# Sidecar ports the VM reaches at its host-side TAP IP. Kept in sync