installer 1.33.0 (pushed, not published): the data volume by the ruled table; step 5b the internal data place; OMV guide second-disk line
gates / gates (push) Successful in 6m42s

architecture/16 §5, §7 (build B). Publishing (tag + webpage.yaml, R-110) waits for the operator's vouch of agent 0.158.1.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-16 04:08:39 +02:00
parent 394498d06e
commit d45384bf95
4 changed files with 199 additions and 16 deletions
+92
View File
@@ -14,6 +14,7 @@ right place) — a helper defined and never called is the seam-built-never-wired
Rows: R-275, R-276, R-881 (uninstall residue); R-306 (--preflight-only writes no state); R-130 (lvm minimum wording); R-180 (archive storage outside the ACL set); R-179 (NAS units); R-310 (golden refusal wording, the uninstall's terminal); R-274 (local golden reuse).
Run: python3 scripts/test_hostinstall.py
"""
import json
import os
import re
import shutil
@@ -35,6 +36,7 @@ BUNDLE_DESTS = [
"/usr/local/sbin/felhom-pbs-apply",
"/usr/local/sbin/felhom-backup-target-apply",
"/usr/local/sbin/felhom-vzdump-partial-clean", # agent v0.156.1 (R-1006)
"/usr/local/sbin/felhom-internal-place", # agent v0.158.0 (felhom.eu `16` §5)
"/usr/local/sbin/felhom-os-apply",
"/usr/local/sbin/felhom-crash-guard",
"/usr/local/sbin/felhom-priv-apply",
@@ -606,6 +608,96 @@ def test_byo_disclosure_names_the_golden_reuse():
assert re.search(r"reuse:.*golden.*vouched", body, re.S), "the BYO disclosure does not say a local golden is reused (R-274)"
# ── felhom.eu architecture/16 §5, §7 (installer 1.33.0): the data volume by the ruled table; the internal place ───
def test_datavol_follows_the_ruled_table():
sb = Sandbox()
try:
cases = [(53, 50), (113, 50), (299, 50), (300, 100), (349, 100), (799, 100), (800, 130), (1720, 130)]
script = func("felhom_datavol_gib") + "".join('echo "%d $(felhom_datavol_gib %d)"\n' % (p, p) for p, _ in cases)
rc, out = sb.run(script)
assert rc == 0, out
got = dict(tuple(map(int, l.split())) for l in out.split("\n") if l.strip())
assert got == dict(cases), got
finally:
sb.close()
def test_step_grows_sizes_the_data_volume_from_the_pool():
sb = Sandbox()
try:
sb.stub("lvs", 'echo " 349.00"')
rc, out = sb.run(prelude() + func("felhom_datavol_gib") + "_state_mark() { :; }\n" + func("step_grows") +
'ROOTFS_GROW=; DATAVOL_GROW=; SYSDATA_GROW=\nstep_grows\necho "GROW=$DATAVOL_GROW"\n')
assert rc == 0, out
assert "GROW=76" in out, out # 100 GiB data volume = the golden's 24 + 76
finally:
sb.close()
def _internal_place(mode="appliance", helper_out=None, helper_rc=0, guests="", have_helper=True, dry=False):
"""Run step_internal_place with the helper replaced by a stub; returns (rc, out, record or None, helper calls)."""
sb = Sandbox()
try:
helper = sb.path("felhom-internal-place")
if have_helper:
with open(helper, "w") as f:
f.write('#!/bin/sh\necho "helper $*" >> "%s"\n%s\nexit %d\n' % (sb.calls, 'echo "%s"' % helper_out if helper_out else "", helper_rc))
os.chmod(helper, 0o755)
sb.stub("install"); sb.stub("chown")
body = func("step_internal_place").replace("/usr/local/sbin/felhom-internal-place", helper)
assert helper in body, "the step no longer names the helper's path"
script = (prelude(dry) + "_state_mark() { echo \"MARK $1\"; }\nfelhom_guests() { echo '%s'; }\n" % guests + body +
'MODE=%s; AGENT_USER=felhom-agent; AGENT_STATE_DIR="$SB"; ROOTFS_GROW=0; DATAVOL_GROW=76; SYSDATA_GROW=0\n'
'step_internal_place\necho "RC=$?"\n' % mode)
rc, out = sb.run(script)
rec_path = sb.path("internal-place.json")
rec = json.load(open(rec_path)) if os.path.exists(rec_path) else None
calls = [l for l in sb.logged().splitlines() if l.startswith("helper ")]
return rc, out, rec, calls
finally:
sb.close()
def test_internal_place_created_and_recorded():
rc, out, rec, calls = _internal_place(helper_out="created uuid=0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c gib=182")
assert rc == 0 and "RC=0" in out, out
assert calls == ["helper create 132"], calls # 32 rootfs + 24 + 76: the guest's thin space still to come
assert rec and rec["fs_uuid"] == "0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c" and rec["gib"] == 182 and rec["made_by"] == "installer", rec
assert "MARK internal_place" in out, out
def test_internal_place_never_fatal_and_writes_nothing_when_not_made():
for kw in ({"helper_out": "skipped only 7 GiB free for it (pool 53, reserve 16, promised 0, planned 82; needs 20)"},
{"helper_out": "felhom-internal-place: REFUSED: something", "helper_rc": 1},
{"have_helper": False},
{"mode": "byo", "helper_out": "created uuid=0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c gib=182"}):
rc, out, rec, calls = _internal_place(**kw)
assert rc == 0 and "RC=0" in out, (kw, out)
assert rec is None, (kw, rec)
if kw.get("mode") == "byo":
assert calls == [], ("byo: the owner's pool is never touched", calls)
def test_internal_place_counts_an_existing_guest_once():
rc, out, rec, calls = _internal_place(helper_out="existing uuid=0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c gib=182", guests="9201")
assert calls == ["helper create 0"], calls
assert rec and rec["gib"] == 182, rec
def test_internal_place_dry_run_creates_nothing():
rc, out, rec, calls = _internal_place(dry=True, helper_out="plan pool=349 reserve=34 promised=0 planned=132 internal=183 exists=false")
assert rec is None and all(not c.startswith("helper create") for c in calls), (calls, out)
def test_internal_place_runs_after_the_bundle_and_before_provision():
main = SRC[SRC.index("step_token\nstep_grows\n"):]
call = "should_skip internal_place || step_internal_place"
assert call in main, "the internal place step is not called from the main flow"
i_bundle, i_place, i_prov = main.find("step_agent_install"), main.find(call), main.find("step_provision")
assert -1 < i_bundle < i_place < i_prov, (i_bundle, i_place, i_prov)
def main():
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
fails = 0