Catalog: image.license-bundle and image.network-boot leave; the network boot's board checks fold into image.health

Each left after its host proofs passed, with the coverage lint clean at
the pins, its only failure the known setup.check-lens entry on
src/lensfind.*:

- image.license-bundle: the pack half is test_image.py's
  PackLicensesTests on pack-licenses.sh (forgefirm 0d63f87); the serve
  half is host_routes.sh's license cases, the manifest, the bundle and
  the Licenses page read with no token (forgectrl c9e3347). Its
  src/main.c stays covered by image.health, and its three pages by
  forgectrl.panel-serves (src/ui/**). The 40-package and 5-text counts
  leave with it.
- image.network-boot: the interfaces file's DHCPv6 check is
  NetworkBootTests' case on the BSP layer's file (forgefirm 0d63f87).
  The board checks move into image.health, unchanged: wlan0 in ifup's
  state file and no ifup or ifdown running; a getty on ttymxc0; at least
  one udhcpc, each with -b as its own word and parent pid 1; no udhcpc6,
  dhcp6c or odhcp6c running or on the PATH, and no
  /usr/share/udhcpc/default6.script; on wlan0 disable_ipv6 0, accept_ra
  neither empty nor 0, and a link-local address, the global address
  logged. image.health's description says so. The interfaces file
  carries no -b, so -b is checked here, on the client that runs.

No HOST_TESTED row: network-boot covered nothing, and license-bundle's
paths stay covered by the two tests that keep them. image.health's
covers are unchanged. The helpers ifstate_names, dhcp6_in_interfaces,
parse_stat, link_addrs, _procs and _read, and the module's constants,
stay as they were; only the two functions and the inside of
image_health changed.

Fingerprints: image.health's moved and no other id's (the module's
shared text, which every exthost test reads through its import of
kernel_config, is unchanged). image.health is always-required, so the
next campaign reruns it.

Proof: the coverage lint, 53 tests (forgectrl, src/lensfind.* its one
entry); pyflakes; forgetest's unit suite on Linux (python3 -B -m
unittest discover -s tests), 335 OK.
This commit is contained in:
ScottW514
2026-10-14 21:46:58 -04:00
parent 0d63f8740e
commit 7f2da68e34
+51 -117
View File
@@ -127,7 +127,13 @@ def fds_of(pid):
"stops before it on a machine with no package installed and nothing running under a "
"pool account, the data directory open for search on a machine with an account, the file modes the release depends on, and the mounts: the "
"rootfs read-only, /data writable, the account files and the banner rendered "
"into tmpfs, the sshd host keys on /data, the factory slots on the dev image only.")
"into tmpfs, the sshd host keys on /data, the factory slots on the dev image only. "
"And the boot does not wait on the network: ifup has returned for wlan0 (it is "
"in the state file and no ifup is running) and the console login is up; each "
"DHCP client runs with -b and has left ifup (its parent is pid 1); no DHCPv6 "
"client is running or on the image, nor its hook script; and IPv6 is the "
"kernel's own: enabled on wlan0, router advertisements accepted, a link-local "
"address up (a global address is logged, not checked).")
def image_health(ctx):
ev = ctx.evidence
manifest = ctx.runner.manifest
@@ -399,59 +405,52 @@ def image_health(ctx):
ctx.check("linux-fslc" in manifest.components, "manifest lacks the kernel entry")
ctx.log("manifest %s: %d components", manifest.content_sha[:12], len(manifest.components))
# 10. the network boot. init starts the rest of the boot, the console
# login last, only after the networking script returns, so a client
# that waits in ifup's foreground on a server's answer holds the
# whole machine. The interfaces file starts no DHCPv6 client
# (test_image.py reads the layer's file); the bounded DHCP client
# (-b, which leaves ifup after three unanswered discovers) is checked
# here, on the client that runs.
procs = _procs()
up = ifstate_names(_read(IFSTATE, ""))
ev["ifstate"] = sorted(up)
ctx.check("wlan0" in up, "ifup has not finished wlan0: %s holds %s", IFSTATE, sorted(up))
stuck = [p for p in procs if p[2] in ("ifup", "ifdown")]
ev["ifup_running"] = [p[3] for p in stuck]
ctx.check(not stuck, "ifup is still running: %s", [p[3] for p in stuck])
logins = [p[3] for p in procs if p[2] == "getty"]
ev["getty"] = logins
ctx.check(any("ttymxc0" in c for c in logins), "no console login is running: %s", logins)
v4 = [p for p in procs if p[2] == "udhcpc"]
ev["udhcpc"] = [p[3] for p in v4]
ctx.check(v4, "no DHCP client is running")
for pid, ppid, _comm, cmd in v4:
ctx.check("-b" in cmd.split(), "the DHCP client runs without -b: %s", cmd)
ctx.check(ppid == 1, "the DHCP client (pid %d) is still a child of pid %d", pid, ppid)
# no DHCPv6 client: not running, not on the image
alive = [p[3] for p in procs if p[2] in DHCP6_CLIENTS]
ev["dhcp6_running"] = alive
ctx.check(not alive, "a DHCPv6 client is running: %s", alive)
for name in DHCP6_CLIENTS:
ctx.check(shutil.which(name) is None, "%s is on the image", name)
ctx.check(not os.path.exists("/usr/share/udhcpc/default6.script"),
"the DHCPv6 hook script is on the image")
# IPv6 is the kernel's own. A global address is evidence only: a
# network whose router advertisement offers no SLAAC prefix gives none.
conf = "/proc/sys/net/ipv6/conf/wlan0/"
ev["disable_ipv6"] = (_read(conf + "disable_ipv6", "") or "").strip()
ev["accept_ra"] = (_read(conf + "accept_ra", "") or "").strip()
ctx.check(ev["disable_ipv6"] == "0", "IPv6 is off on wlan0 (disable_ipv6=%r)", ev["disable_ipv6"])
ctx.check(ev["accept_ra"] not in ("", "0"),
"wlan0 does not accept router advertisements (accept_ra=%r)", ev["accept_ra"])
local, world = link_addrs(_read("/proc/net/if_inet6", ""), "wlan0")
ev["link_local"] = local
ev["global"] = world
ctx.check(local, "wlan0 has no link-local IPv6 address")
ctx.log("wlan0: %d link-local, %d global IPv6 address(es)", len(local), len(world))
@test("image.license-bundle", title="The license texts travel with the image",
subsystem="image", kind="auto", est_min=1,
covers=[("forgectrl", "src/main.c"), ("forgectrl", "src/ui/index.html"),
("forgectrl", "src/ui/wizard.html"), ("forgectrl", "src/ui/login.html")],
requires=["forgectrl.panel-serves"],
description="The image carries /usr/share/forgefirm/licenses.tar.gz, a gzip tar with "
"the license manifest (every installed package with its license) and the "
"license texts, and no loose common-licenses tree; the panel serves the "
"bundle, the manifest as text, and the Licenses page every footer links, "
"each without a login.")
def license_bundle(ctx):
import io
import tarfile
fc = ctx.forgectrl
ev = ctx.evidence
path = "/usr/share/forgefirm/licenses.tar.gz"
ctx.check(os.path.isfile(path), "%s is missing", path)
ctx.check(not os.path.isdir("/usr/share/common-licenses"),
"the loose common-licenses tree is still on the rootfs")
size = os.path.getsize(path)
ev["bundle_bytes"] = size
ctx.log("%s: %d bytes", path, size)
with tarfile.open(path, "r:gz") as t:
names = t.getnames()
ctx.check("common-licenses/license.manifest" in names, "no license.manifest in the bundle")
manifest = t.extractfile("common-licenses/license.manifest").read().decode("utf-8", "replace")
packages = manifest.count("PACKAGE NAME: ")
texts = sum(1 for n in names if "/generic_" in n and not n.endswith("/"))
ev["packages"] = packages
ev["generic_texts"] = texts
ctx.log("manifest lists %d packages; %d generic license entries", packages, texts)
ctx.check(packages >= 40, "only %d packages in the manifest", packages)
ctx.check(texts >= 5, "only %d generic license entries", texts)
for name in ("forgectrl", "grblhal-glowforge", "kernel-module-glowforge"):
ctx.check("RECIPE NAME: %s\n" % name in manifest, "%s is not in the manifest", name)
st, body = fc.get("/system/licenses/manifest", raw=True)
ctx.check(st == 200, "GET /system/licenses/manifest -> %s", st)
ctx.check(isinstance(body, (bytes, bytearray)) and b"PACKAGE NAME: " in body,
"the served manifest is not the manifest")
st, body = fc.get("/system/licenses", raw=True)
ctx.check(st == 200, "GET /system/licenses -> %s", st)
ctx.check(isinstance(body, (bytes, bytearray)) and len(body) == size,
"the served bundle is %s bytes, the file %d", len(body) if body else None, size)
with tarfile.open(fileobj=io.BytesIO(bytes(body)), mode="r:gz") as t:
ctx.check("common-licenses/license.manifest" in t.getnames(),
"the served bundle holds no manifest")
st, body = fc.get("/licenses", raw=True)
ctx.check(st == 200, "GET /licenses -> %s", st)
text = bytes(body).decode("utf-8", "replace") if body else ""
ctx.check("/system/licenses" in text and "PACKAGE NAME: " in text,
"the Licenses page lacks the download link or the manifest")
# ------------------------------------------------------- the network boot
@@ -524,68 +523,3 @@ def link_addrs(text, iface):
return local, world
@test("image.network-boot", title="The boot does not wait on the network",
subsystem="image", kind="auto", est_min=1,
description="The network comes up without the boot waiting on a server. init starts the "
"rest of the boot, the console login last, only after the networking script "
"returns, so a client that waits in ifup's foreground on a server's answer "
"holds the whole machine: no login, no SSH, no control panel. ifup has "
"returned for wlan0 (it is in the state file and no ifup is running) and the "
"console login is up; the DHCP client is the bounded one (-b, which leaves "
"ifup after three unanswered discovers) and has left ifup; no DHCPv6 client "
"is named in the interfaces file, running, or on the image; and IPv6 is the "
"kernel's own: enabled on wlan0, router advertisements accepted, a link-local "
"address up. A global address is evidence only, because a network whose "
"router advertisement offers no SLAAC prefix gives none. What a hostile "
"server does to a client is a bench drill, not a test. No component covers "
"this: the interfaces file is layer content, in the platform identity of "
"every fingerprint.")
def network_boot(ctx):
ev = ctx.evidence
procs = _procs()
# 1. ifup came back, and the boot went on to the console login
state = ifstate_names(_read(IFSTATE, ""))
ev["ifstate"] = sorted(state)
ctx.check("wlan0" in state, "ifup has not finished wlan0: %s holds %s", IFSTATE, sorted(state))
stuck = [p for p in procs if p[2] in ("ifup", "ifdown")]
ev["ifup_running"] = [p[3] for p in stuck]
ctx.check(not stuck, "ifup is still running: %s", [p[3] for p in stuck])
logins = [p[3] for p in procs if p[2] == "getty"]
ev["getty"] = logins
ctx.check(any("ttymxc0" in c for c in logins), "no console login is running: %s", logins)
# 2. the DHCP client is the bounded one, and it has left ifup
v4 = [p for p in procs if p[2] == "udhcpc"]
ev["udhcpc"] = [p[3] for p in v4]
ctx.check(v4, "no DHCP client is running")
for pid, ppid, _comm, cmd in v4:
ctx.check("-b" in cmd.split(), "the DHCP client runs without -b: %s", cmd)
ctx.check(ppid == 1, "the DHCP client (pid %d) is still a child of pid %d", pid, ppid)
# 3. no DHCPv6 client: not started, not running, not on the image
text = _read(INTERFACES)
ctx.check(text is not None, "%s is missing", INTERFACES)
named = dhcp6_in_interfaces(text)
ev["interfaces_dhcp6"] = named
ctx.check(not named, "%s starts a DHCPv6 client: %s", INTERFACES, named)
alive = [p[3] for p in procs if p[2] in DHCP6_CLIENTS]
ev["dhcp6_running"] = alive
ctx.check(not alive, "a DHCPv6 client is running: %s", alive)
for name in DHCP6_CLIENTS:
ctx.check(shutil.which(name) is None, "%s is on the image", name)
ctx.check(not os.path.exists("/usr/share/udhcpc/default6.script"),
"the DHCPv6 hook script is on the image")
# 4. IPv6 is the kernel's own
conf = "/proc/sys/net/ipv6/conf/wlan0/"
ev["disable_ipv6"] = (_read(conf + "disable_ipv6", "") or "").strip()
ev["accept_ra"] = (_read(conf + "accept_ra", "") or "").strip()
ctx.check(ev["disable_ipv6"] == "0", "IPv6 is off on wlan0 (disable_ipv6=%r)", ev["disable_ipv6"])
ctx.check(ev["accept_ra"] not in ("", "0"),
"wlan0 does not accept router advertisements (accept_ra=%r)", ev["accept_ra"])
local, world = link_addrs(_read("/proc/net/if_inet6", ""), "wlan0")
ev["link_local"] = local
ev["global"] = world
ctx.check(local, "wlan0 has no link-local IPv6 address")
ctx.log("wlan0: %d link-local, %d global IPv6 address(es)", len(local), len(world))