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 json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
import time
|
||||||
@@ -36,7 +35,6 @@ import urllib.parse
|
|||||||
|
|
||||||
from ..catalog import test
|
from ..catalog import test
|
||||||
from .. import hw
|
from .. import hw
|
||||||
from .forgectrl import lan_ip
|
|
||||||
|
|
||||||
RECORD = "setup.json"
|
RECORD = "setup.json"
|
||||||
USERS = "users"
|
USERS = "users"
|
||||||
@@ -535,88 +533,3 @@ def login_checks(ctx, name, pw):
|
|||||||
w = decode(body)
|
w = decode(body)
|
||||||
ctx.check(isinstance(w, dict) and w.get("session") is False, "the session survives the logout")
|
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")
|
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
|
"""The setup.* suite on the host: the registration (ids, kinds, the
|
||||||
takeover tests, the operator tests' hands), the record builders, the
|
takeover tests, the operator tests' hands), the record builders, the
|
||||||
machine's name, the cookie parsing, and the settle rule for a gated
|
cookie parsing, and the settle rule for a gated supervisor."""
|
||||||
supervisor."""
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -19,7 +18,7 @@ import helpers # noqa: E402
|
|||||||
from forgetest import baseline, catalog # noqa: E402
|
from forgetest import baseline, catalog # noqa: E402
|
||||||
from forgetest.suite import setup # noqa: E402
|
from forgetest.suite import setup # noqa: E402
|
||||||
|
|
||||||
IDS = ("setup.account-login", "setup.machine-name")
|
IDS = ("setup.account-login",)
|
||||||
|
|
||||||
|
|
||||||
class RegistrationTests(unittest.TestCase):
|
class RegistrationTests(unittest.TestCase):
|
||||||
@@ -60,9 +59,6 @@ class RegistrationTests(unittest.TestCase):
|
|||||||
and t.kind != "auto" and not t.fixture_runnable(("button", "lid", "interlock")))
|
and t.kind != "auto" and not t.fixture_runnable(("button", "lid", "interlock")))
|
||||||
self.assertEqual(attended, ["setup.cloud-header-capture", "setup.focus"])
|
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):
|
def test_the_login_test_makes_its_own_account(self):
|
||||||
# No bench credentials, no precheck: the test installs a temporary
|
# No bench credentials, no precheck: the test installs a temporary
|
||||||
# account under a takeover and restores the real one.
|
# account under a takeover and restores the real one.
|
||||||
@@ -170,47 +166,6 @@ class RecordTests(unittest.TestCase):
|
|||||||
os.environ.pop("GF_RUN_DIR", None)
|
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):
|
class SmallHelpersTests(unittest.TestCase):
|
||||||
def test_cookie_parsing(self):
|
def test_cookie_parsing(self):
|
||||||
sid = "ab" * 32
|
sid = "ab" * 32
|
||||||
|
|||||||
Reference in New Issue
Block a user