os-apply: the host restart scan no longer hides lxc-start (skip ':/lxc/' not 'lxc'), and the host scans on every pass so a reboot clears 'reboot needed' (reboot_scanned reaches the hub) — both found live on demo-felhom
gates / gates (push) Successful in 18s
gates / gates (push) Successful in 18s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
+12
-1
@@ -58,6 +58,11 @@ INSTALL_STATE = "/var/lib/felhom-install/state.json"
|
||||
# reboot is needed for them to take effect, and a bad one can stop the box from booting.
|
||||
HOST_SLOW_RE = re.compile(r"^(linux-(image|headers|kbuild|modules|base)|proxmox-kernel|proxmox-default-kernel|pve-kernel|"
|
||||
r"pve-firmware|firmware-|grub|shim|systemd-boot|intel-microcode|amd64-microcode|efibootmgr)")
|
||||
# restart_needed() leaves out processes whose cgroup line matches (grep basic regex). Host: the LXC guests' own
|
||||
# processes (`0::/lxc/<vmid>/...`) -- NOT lxc-start itself, whose cgroup is `0::/lxc.monitor/<vmid>` (measured
|
||||
# 2026-10-04 on demo-felhom: the old pattern "lxc" hid lxc-start with 20 deleted maps, so "reboot needed" stayed false
|
||||
# after a libc6 update). Pinned by test_restart_skip_patterns_against_real_cgroups.
|
||||
RESTART_SKIP_CGROUP = {"guest": "docker", "host": ":/lxc/"}
|
||||
HOST_SERVICES = ["pveproxy", "pvedaemon", "pvestatd", "pve-cluster", "felhom-agent"]
|
||||
|
||||
|
||||
@@ -310,7 +315,7 @@ class Apply:
|
||||
def restart_needed(self):
|
||||
"""Processes still mapping deleted files, OUTSIDE containers (C11). Guest: outside docker; host: outside the
|
||||
LXC guests (the host's /proc shows guest processes too)."""
|
||||
skip = "docker" if self.layer == "guest" else "lxc"
|
||||
skip = RESTART_SKIP_CGROUP[self.layer]
|
||||
script = ('for p in /proc/[0-9]*; do grep -q "(deleted)" $p/maps 2>/dev/null || continue; '
|
||||
'grep -q "%s" $p/cgroup 2>/dev/null && continue; echo "${p#/proc/} $(cat $p/comm 2>/dev/null)"; done' % skip)
|
||||
rc, out, _ = self.x(["sh", "-c", script], timeout=120)
|
||||
@@ -392,6 +397,12 @@ class Apply:
|
||||
if rc:
|
||||
return rc
|
||||
self.report.update(self.inventory(installed_after))
|
||||
if self.layer == "host" and "reboot_needed" not in self.report:
|
||||
# The host is scanned on EVERY pass (local, no pct exec): a reboot must CLEAR "reboot needed", or the hub's
|
||||
# 14-day alarm fires on a host that was rebooted long ago. The guest still scans only after an install
|
||||
# (R-845; one pct exec, and no alarm reads it). Pinned by test_host_scans_every_pass_guest_only_after_install.
|
||||
self.report["restart_needed"], self.report["reboot_needed"] = self.restart_needed()
|
||||
self.report["reboot_scanned"] = "reboot_needed" in self.report
|
||||
self.report["health_after"] = self.health()
|
||||
return 0
|
||||
|
||||
|
||||
@@ -551,6 +551,27 @@ class HostLayer(unittest.TestCase):
|
||||
rc, rep = run(f)
|
||||
self.assertTrue(rep["reboot_needed"], rep)
|
||||
|
||||
def test_host_scans_every_pass_guest_only_after_install(self):
|
||||
# A host pass that installs nothing still scans, so a reboot clears the flag (the hub alarm reads it).
|
||||
f = Fake()
|
||||
f.plan["layer"] = "host"
|
||||
f.plan["mode"] = "inventory"
|
||||
f.restart_out = "2101 lxc-start\n"
|
||||
rc, rep = run(f)
|
||||
self.assertEqual(rc, 0, rep)
|
||||
self.assertTrue(rep["reboot_scanned"], rep)
|
||||
self.assertTrue(rep["reboot_needed"], rep)
|
||||
f = Fake()
|
||||
f.plan["layer"] = "host"
|
||||
f.plan["mode"] = "inventory"
|
||||
f.restart_out = "" # after the reboot: nothing maps a deleted file
|
||||
rc, rep = run(f)
|
||||
self.assertTrue(rep["reboot_scanned"] and rep["reboot_needed"] is False, rep)
|
||||
f = Fake()
|
||||
f.plan["mode"] = "inventory"
|
||||
rc, rep = run(f)
|
||||
self.assertFalse(rep["reboot_scanned"], "the guest scans only after an install (R-845)")
|
||||
|
||||
def test_no_reboot_for_ordinary_daemons(self):
|
||||
f = Fake()
|
||||
f.plan["layer"] = "host"
|
||||
@@ -586,5 +607,22 @@ class Failure(unittest.TestCase):
|
||||
self.assertTrue(any(l.startswith("os-apply: FAILED rc=100 step=install") for l in f.logs))
|
||||
|
||||
|
||||
|
||||
class RestartSkipPattern(unittest.TestCase):
|
||||
"""The cgroup filter runs as `grep -q PATTERN /proc/<pid>/cgroup`; check it with grep itself against the cgroup
|
||||
lines measured on demo-felhom 2026-10-04."""
|
||||
|
||||
def grep(self, pattern, line):
|
||||
return subprocess.run(["grep", "-q", pattern], input=line + "\n", text=True).returncode == 0
|
||||
|
||||
def test_restart_skip_patterns_against_real_cgroups(self):
|
||||
host = osapply.RESTART_SKIP_CGROUP["host"]
|
||||
self.assertTrue(self.grep(host, "0::/lxc/9201/ns/system.slice/docker.service"), "a guest process must be skipped")
|
||||
self.assertFalse(self.grep(host, "0::/lxc.monitor/9201"), "lxc-start must NOT be skipped (it runs the guest)")
|
||||
self.assertFalse(self.grep(host, "0::/system.slice/pve-cluster.service"), "a host daemon must NOT be skipped")
|
||||
guest = osapply.RESTART_SKIP_CGROUP["guest"]
|
||||
self.assertTrue(self.grep(guest, "0::/system.slice/docker-0123abcd.scope"))
|
||||
self.assertFalse(self.grep(guest, "0::/system.slice/cron.service"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user