mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user