Catalog: setup.machine-name leaves for the rootfs lint and test_image.py

Its checks, and what holds each now:
- the name is forgefirm-<the last four hex digits of the MAC>, wlan0
  first and eth0 on a machine with no WiFi, and the console banner
  offers the addresses and nothing else: test_image.py's
  MachineNameTests run forgefirm-hostname and forgefirm-banner as the
  image runs them, on a scratch /sys/class/net;
- /etc/hostname is the bind-mounted copy: image.health;
- the DHCP request sends the name, and no mDNS responder is on the
  image: the rootfs lint's dhcp-name and mdns rules (08c7df9), which
  image 20261015142442 ran in do_rootfs and which passed campaign
  c-20261015143634-68f0, 48 of 48.
It covered no component path, so no HOST_TESTED row. Its helpers
mac_suffix and cmdlines had no other caller and go with it, with
test_setup_suite's cases of them and of its registration; setup.py's re
and lan_ip imports went with them.

Fingerprints: every test in setup.py moves with the module text.

Proof: the coverage lint over the tree manifest from the pins finds 0
uncovered paths and 0 host-tested row problems (47 tests); pyflakes on
the touched files is clean; forgetest's unit suite under Linux passes
(329).
This commit is contained in:
ScottW514
2026-10-15 11:28:25 -04:00
parent 08c7df9cbb
commit c40dc6c24c
2 changed files with 2 additions and 134 deletions
-87
View File
@@ -28,7 +28,6 @@ import http.client
import json
import os
import random
import re
import socket
import ssl
import time
@@ -36,7 +35,6 @@ import urllib.parse
from ..catalog import test
from .. import hw
from .forgectrl import lan_ip
RECORD = "setup.json"
USERS = "users"
@@ -535,88 +533,3 @@ def login_checks(ctx, name, pw):
w = decode(body)
ctx.check(isinstance(w, dict) and w.get("session") is False, "the session survives the logout")
ctx.log("session set by the login, seen by GET /wiz, ended by the logout")
# --------------------------------------------------- the machine's name
def mac_suffix():
"""The last four hex digits of the MAC address the hostname is built
from: wlan0, or eth0 on a machine with no WiFi."""
for dev in ("wlan0", "eth0"):
raw = read_file("/sys/class/net/%s/address" % dev)
mac = (raw or b"").decode("ascii", "replace").strip().replace(":", "").lower()
if mac and mac != "0" * 12:
return mac[-4:]
return ""
def cmdlines():
"""The command line of every process on the machine, one string
each."""
out = []
for name in os.listdir("/proc"):
if not name.isdigit():
continue
raw = read_file("/proc/%s/cmdline" % name)
if raw:
out.append(raw.decode("utf-8", "replace").replace("\0", " ").strip())
return out
@test("setup.machine-name", title="The machine names itself from its MAC address",
subsystem="setup", kind="auto", est_min=1,
requires=["forgectrl.auth"],
description="The machine calls itself forgefirm-<xxxx>, where xxxx is the last four hex "
"digits of its WiFi MAC address, so two machines on one network answer to "
"different names and no name carries a serial number. The live name, "
"/etc/hostname (a bind-mounted copy, because the rootfs is read-only) and the "
"MAC address agree; the DHCP client sends the name as the hostname option, "
"which is what a network with dynamic DNS publishes; the console banner offers "
"the machine's addresses and nothing else, with no marker line and no .local "
"name; and no mDNS responder is on the image. No component covers this: the "
"hostname and banner scripts are layer content, in the platform identity of "
"every fingerprint.")
def machine_name(ctx):
ev = ctx.evidence
# 1. the name, the file and the MAC address agree
suffix = mac_suffix()
ev["mac_suffix"] = suffix
ctx.check(suffix, "no MAC address to build a name from")
live = socket.gethostname()
on_file = ((read_file("/etc/hostname") or b"").decode("utf-8", "replace")).strip()
ev["hostname"] = live
ev["hostname_file"] = on_file
ctx.log("hostname %r (MAC suffix %s)", live, suffix or "none")
ctx.check(live == "forgefirm-" + suffix,
"the hostname is %r, expected forgefirm-%s", live, suffix)
ctx.check(on_file == live, "/etc/hostname holds %r, the live name is %r", on_file, live)
# 2. the DHCP client sends it (option 12), so dynamic DNS can publish it
dhcp = [c for c in cmdlines() if c.split(" ")[0].split("/")[-1] == "udhcpc"]
ev["udhcpc"] = dhcp
ctx.check(dhcp, "no DHCP client is running")
ctx.check(any("-x hostname:" + live in c for c in dhcp),
"the DHCP client does not send the machine's name: %s", dhcp)
# 3. the console banner: the addresses, and nothing that is not the banner
issue = (read_file("/etc/issue") or b"").decode("utf-8", "replace")
ev["issue"] = issue
ip = lan_ip()
ev["lan_ip"] = ip
ctx.check(ip, "cannot determine the board's LAN address")
ctx.check("Control panel:" in issue, "the console banner names no control panel")
ctx.check("https://%s/" % ip in issue,
"the console banner does not carry the LAN address %s", ip)
for junk in ("# end", "# ForgeFIRM addresses", ".local"):
ctx.check(junk not in issue, "the console banner shows %r", junk)
urls = re.findall(r"https://(\S+)/", issue)
ev["banner_urls"] = urls
for u in urls:
literal = u.startswith("[") or all(c.isdigit() or c == "." for c in u)
ctx.check(literal, "the console banner offers %r, which is not an address", u)
# 4. no mDNS on the image; the name is the only name the machine has
ev["avahi_pids"] = hw.pidof("avahi-daemon")
ctx.check(not ev["avahi_pids"], "an mDNS responder is running: %s", ev["avahi_pids"])
ctx.check(not os.path.exists("/etc/avahi"), "/etc/avahi is on the image")
+2 -47
View File
@@ -5,8 +5,7 @@
"""The setup.* suite on the host: the registration (ids, kinds, the
takeover tests, the operator tests' hands), the record builders, the
machine's name, the cookie parsing, and the settle rule for a gated
supervisor."""
cookie parsing, and the settle rule for a gated supervisor."""
import json
import os
import shutil
@@ -19,7 +18,7 @@ import helpers # noqa: E402
from forgetest import baseline, catalog # noqa: E402
from forgetest.suite import setup # noqa: E402
IDS = ("setup.account-login", "setup.machine-name")
IDS = ("setup.account-login",)
class RegistrationTests(unittest.TestCase):
@@ -60,9 +59,6 @@ class RegistrationTests(unittest.TestCase):
and t.kind != "auto" and not t.fixture_runnable(("button", "lid", "interlock")))
self.assertEqual(attended, ["setup.cloud-header-capture", "setup.focus"])
def test_the_machine_name_covers_nothing_by_design(self):
self.assertEqual(self.reg["setup.machine-name"].covers, ())
def test_the_login_test_makes_its_own_account(self):
# No bench credentials, no precheck: the test installs a temporary
# account under a takeover and restores the real one.
@@ -170,47 +166,6 @@ class RecordTests(unittest.TestCase):
os.environ.pop("GF_RUN_DIR", None)
class MachineNameTests(unittest.TestCase):
def setUp(self):
self.root = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.root, ignore_errors=True)
def _net(self, **devs):
"""A /sys/class/net tree; setup.mac_suffix reads it through
setup.read_file, which takes an absolute path."""
for dev, mac in devs.items():
d = os.path.join(self.root, dev)
os.makedirs(d)
with open(os.path.join(d, "address"), "w") as f:
f.write(mac + "\n")
real = setup.read_file
def read(path):
head = "/sys/class/net/"
if path.startswith(head):
return real(os.path.join(self.root, path[len(head):]))
return real(path)
setup.read_file = read
self.addCleanup(setattr, setup, "read_file", real)
def test_the_wifi_address_names_the_machine(self):
self._net(wlan0="2C:6B:7D:0D:B0:0A", eth0="00:11:22:33:44:55")
self.assertEqual(setup.mac_suffix(), "b00a")
def test_a_machine_with_no_wifi_falls_back_to_the_wired_address(self):
self._net(eth0="00:11:22:33:44:55")
self.assertEqual(setup.mac_suffix(), "4455")
def test_an_unread_address_is_no_address(self):
self._net(wlan0="00:00:00:00:00:00")
self.assertEqual(setup.mac_suffix(), "")
def test_no_interface_is_no_address(self):
self._net()
self.assertEqual(setup.mac_suffix(), "")
class SmallHelpersTests(unittest.TestCase):
def test_cookie_parsing(self):
sid = "ab" * 32