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
+7 -3
View File
@@ -173,9 +173,13 @@ Lépések:
- **Mentések:** egy lemezen a Felhom a saját mentéseit ugyanarra a lemezre teszi. Ez véd a véletlen törlés és a hibás
fájlok ellen, de **nem véd az NVMe meghibásodása ellen**. Ezt a Felhom vezérlőpultja ki is írja. Ehhez kell egy
második meghajtó vagy a távoli mentés — ezt beszéljük meg.
- **Jellyfin (filmek):** egy lemezzel a Jellyfin telepítése most egy plusz lépést kér a vezérlőpulton (egy adattároló
kijelölését). **Ezt a hívás közben együtt csináljuk.** A Felhom Jellyfinje az OMV hálózati megosztását ma **nem**
tudja filmforrásként használni.
- **Második lemez a Felhom gépének:** csak akkor véd lemezhiba ellen, ha az OMV gépében egy **másik fizikai lemez**, és
**egészében** adod át (nem egy fájl ugyanazon a lemezen), **Bus: `sata`**. Egy virtuális gépen belül a Felhom ezt
nem tudja ellenőrizni, ezért a második lemezre tett másolatot nem számolja második másolatnak, és ezt ki is írja.
- **Jellyfin (filmek):** az új telepítő egy lemezen is létrehoz egy saját adatterületet („Belső tárhely"), így a
Jellyfin és a többi adatot tároló alkalmazás plusz lépés nélkül települ, és a filmek nem kerülnek bele a teljes
rendszermentésbe. Ha a géped egy korábbi telepítővel készült, az adatterületet mi adjuk hozzá távolról — szólj.
A Felhom Jellyfinje az OMV hálózati megosztását ma **nem** tudja filmforrásként használni.
- **A Toshiba lemez** az OMV-nél marad. Ha később mégis a Felhomnak adnád, a **Bus** legyen **`sata`** (a `virtio`-t
a Felhom nem tudja biztonságosan azonosítani). A Felhom egy virtuális gépen belül nem látja a lemezek
állapotjelzőit (SMART) — azokat az OMV figyeli.
+15
View File
@@ -1,3 +1,18 @@
## 2026-10-16 — felhom-host-install.sh 1.33.0: the data volume by the ruled table; the internal data place (step 5b)
`architecture/16-storage-physical-identity.md` §5, §7 (sizes ruled by the operator 2026-10-16 after the build B space check,
`documentation/audits/storage-identity-buildB-2026-10-15/SPACE-CHECK.md`). **Pushed, NOT published** — publishing is the
`installer-v1.33.0` tag + `manifests/webpage.yaml` (R-110), and waits for the operator's vouch of agent 0.158.1: before it,
step 5b only logs that the vouched bundle has no helper, and the smaller data volume would leave the rest of the pool unused.
- **Step 3** sizes the data volume by the thin POOL: 50 GiB below 300, 100 up to 800, 130 from 800 (`felhom_datavol_gib`;
before: 70/130/250 by free space).
- **Step 5b (new)**, appliance mode only: the bundle's root helper `felhom-internal-place create <32 + data volume>` makes the
internal place in the rest of the pool (reserve max(16 GiB, 10 %) ≤ 48 GiB; only when ≥ 20 GiB) and the step writes the
record the agent mounts from. Never fatal; a byo host's pool is never divided; a re-run never formats again.
- The uninstall removes the helper and names the internal place's LV under KEPT (household data — never removed).
- Tests: `scripts/test_hostinstall.py` (the ruled table, step 3 from the pool, step 5b in every outcome, its place in the
flow), each red-proved by a mutation.
## 2026-10-15 (night) — read_credential.py refuses to write over the credentials file (R-1088)
- `read_credential.py KEY ~/.config/credentials` — the credentials path given as the OUTFILE — truncated the operator's
+85 -13
View File
@@ -184,7 +184,7 @@
set -euo pipefail
SCRIPT_VERSION="1.32.0" # the SINGLE version source (F-1): -h and the run banners follow it.
SCRIPT_VERSION="1.33.0" # the SINGLE version source (F-1): -h and the run banners follow it.
# The hub used to carry a copy for its Setup tab; R-94 DELETED it
# (2026-08-02) because the hub cannot know which version a box runs —
# the Setup command fetches this script at run time. scripts/
@@ -866,6 +866,10 @@ _uninstall_statement() {
else
echo " - the enrolled drives + ALL data under /mnt/felhom-drives — left MOUNTED (remaining guests may use them)"
fi
if lvs pve/felhom-belso >/dev/null 2>&1; then # 1.33.0 (`16` §5)
echo " - the internal data place (LV pve/felhom-belso, household data) — NEVER removed by an uninstall; once the"
echo " data is saved elsewhere: lvremove pve/felhom-belso"
fi
if pvesm status 2>/dev/null | awk '$2=="pbs"{found=1} END{exit !found}'; then
echo " - the PBS backups + this customer's namespace on the PBS side — delete there if wanted"
fi
@@ -1353,6 +1357,8 @@ run_uninstall() {
if [[ -f /usr/local/sbin/felhom-backup-target-apply ]]; then run rm -f /usr/local/sbin/felhom-backup-target-apply; else log_skip " felhom-backup-target-apply already absent"; fi
# agent v0.156.1 (R-1006): the dead half-written vzdump remover the config bundle installs.
if [[ -f /usr/local/sbin/felhom-vzdump-partial-clean ]]; then run rm -f /usr/local/sbin/felhom-vzdump-partial-clean; else log_skip " felhom-vzdump-partial-clean already absent"; fi
# 1.33.0 (`16` §5): the internal-place helper. Its VOLUME (pve/felhom-belso) holds household data and is KEPT.
if [[ -f /usr/local/sbin/felhom-internal-place ]]; then run rm -f /usr/local/sbin/felhom-internal-place; else log_skip " felhom-internal-place already absent"; fi
if [[ -f /usr/local/sbin/felhom-os-apply ]]; then run rm -f /usr/local/sbin/felhom-os-apply; else log_skip " felhom-os-apply already absent"; fi
# R-881: the content checker the config bundle installs since agent v0.146.1 (R-861).
if [[ -f /usr/local/sbin/felhom-priv-apply ]]; then run rm -f /usr/local/sbin/felhom-priv-apply; else log_skip " felhom-priv-apply already absent"; fi
@@ -2245,6 +2251,16 @@ step_token() {
_state_mark token
}
# felhom_datavol_gib <thin pool GiB> → the guest's data volume (mp0) in GiB (felhom.eu architecture/16 §7, ruled
# 2026-10-16): 50 below a 300 GiB pool, 100 up to 800, 130 from 800. Pinned by scripts/test_hostinstall.py.
felhom_datavol_gib() {
local pool=${1:-0}
if (( pool >= 800 )); then echo 130
elif (( pool >= 300 )); then echo 100
else echo 50
fi
}
#-------------------------------------------------------------------------------
# STEP 3 — compute grows (floors) if not passed
#-------------------------------------------------------------------------------
@@ -2259,20 +2275,16 @@ step_grows() {
# space. (Before R-165 this same block already did the deriving; the merge only collapsed its
# 80/20 docker-vs-sysdata split into a single total.)
if [[ -z "$ROOTFS_GROW$DATAVOL_GROW$SYSDATA_GROW" ]]; then
local free_gib
free_gib=$(lvs --noheadings --units g -o lv_size,data_percent /dev/pve/data 2>/dev/null | awk '{gsub(/[^0-9.]/,"",$1); used=$2; print int($1*(100-used)/100)}' 2>/dev/null || echo 0)
# Reserve headroom; the totals below are the pre-merge pair SUMMED, so an appliance gets the
# same capacity it did before — it is simply no longer split by a wall.
# 1.33.0 (felhom.eu architecture/16 §7, sizes ruled by the operator 2026-10-16 after the build B space check,
# audits/storage-identity-buildB-2026-10-15/SPACE-CHECK.md): the data volume is sized by the thin POOL, and
# capped — the whole catalog needs ≈ 74 GB; the rest of the pool goes to the internal data place (step 5b),
# which is outside every whole-box copy. Before 1.33.0: 70/130/250 by FREE space.
local pool_gib
pool_gib=$(lvs --noheadings --units g --nosuffix -o lv_size /dev/pve/data 2>/dev/null | awk '{print int($1)}' 2>/dev/null || echo 0)
ROOTFS_GROW=0
if [[ "${free_gib:-0}" -ge 300 ]]; then
DATAVOL_GROW=226 # 184+42 -> the standard 250G appliance (was 200G+50G)
elif [[ "${free_gib:-0}" -ge 150 ]]; then
DATAVOL_GROW=106 # 84+22
else
DATAVOL_GROW=46 # 34+12 — minimal floor
fi
DATAVOL_GROW=$(( $(felhom_datavol_gib "${pool_gib:-0}") - 24 )) # the golden's data volume is 24G
SYSDATA_GROW=0
log_info " auto-computed from ~${free_gib} GiB free (ONE volume since R-165)"
log_info " auto-computed from a ${pool_gib:-?} GiB thin pool (16 §7: 50/100/130 GiB)"
fi
ROOTFS_GROW="${ROOTFS_GROW:-0}"; DATAVOL_GROW="${DATAVOL_GROW:-0}"; SYSDATA_GROW="${SYSDATA_GROW:-0}"
# A hand-passed --sysdata-grow is still ACCEPTED and still counts: the agent folds it into the one
@@ -2281,6 +2293,65 @@ step_grows() {
_state_mark grows
}
#-------------------------------------------------------------------------------
# STEP 5b — the internal data place („Belső tárhely", felhom.eu architecture/16 §5, §7; installer 1.33.0)
#-------------------------------------------------------------------------------
# A second thin volume in local-lvm for the household's own data (films, photos, documents): media and data-folder
# apps install there with no extra step, and it is outside every whole-box copy (a host mount, like a drive). The
# root helper from the agent's config bundle (felhom-internal-place) does the work — the SAME helper a signed
# add_internal_place job runs on an old box — so the size rule and the only mkfs live in one place. The installer
# passes the thin space the guest will still take (32 + the data volume) and writes the record the agent mounts from.
# Never fatal: a box without the place still works (a media app then asks for a drive in words, `16` §5).
step_internal_place() {
log_step "5b/8 internal data place (Belső tárhely)"
if [[ "$MODE" != "appliance" ]]; then
log_skip " byo: the host owner's thin pool is not ours to divide — no internal place"
_state_mark internal_place; return 0
fi
local helper=/usr/local/sbin/felhom-internal-place
if [[ ! -x "$helper" ]]; then
log_warn " the vouched agent's config bundle has no internal-place helper (agent older than 0.158.0) — skipped; a signed add_internal_place job can add it later"
_state_mark internal_place; return 0
fi
local planned=$(( 32 + ROOTFS_GROW + 24 + DATAVOL_GROW + SYSDATA_GROW ))
# A guest that already exists (a re-run after provision) is counted by the helper itself — plan nothing twice.
[[ -n "$(felhom_guests)" ]] && planned=0
if $DRY_RUN; then
log_dry "$helper create $planned ; write $AGENT_STATE_DIR/internal-place.json (0600, $AGENT_USER)"
log_info " $("$helper" plan "$planned" 2>&1 | head -1)"
return 0
fi
local out
if ! out=$("$helper" create "$planned" 2>&1); then
log_warn " the internal place was NOT made: ${out}"
_state_mark internal_place; return 0
fi
case "$out" in
created\ *|existing\ *|formatted\ *)
local uuid gib
uuid=$(sed -n 's/.* uuid=\([0-9a-fA-F-]*\) .*/\1/p' <<<"$out")
gib=$(sed -n 's/.* gib=\([0-9]*\)$/\1/p' <<<"$out")
[[ -n "$uuid" && -n "$gib" ]] || { log_warn " unexpected helper output: $out"; _state_mark internal_place; return 0; }
install -d -o "$AGENT_USER" -g "$AGENT_USER" -m 0700 "$AGENT_STATE_DIR"
UUID="$uuid" GIB="$gib" OUT="$AGENT_STATE_DIR/internal-place.json" python3 - <<'PY'
import json, os, time
rec = {"fs_uuid": os.environ["UUID"], "gib": int(os.environ["GIB"]), "made_by": "installer",
"made_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
tmp = os.environ["OUT"] + ".tmp"
with open(tmp, "w") as f:
json.dump(rec, f, indent=2); f.write("\n")
os.chmod(tmp, 0o600)
os.replace(tmp, os.environ["OUT"])
PY
chown "$AGENT_USER:$AGENT_USER" "$AGENT_STATE_DIR/internal-place.json"
log_success " internal place: ${out%% *}, ${gib} GiB (the agent mounts it at /mnt/belso)"
;;
skipped\ *) log_info " no internal place: ${out#skipped }" ;;
*) log_warn " unexpected helper output: $out" ;;
esac
_state_mark internal_place
}
#-------------------------------------------------------------------------------
# STEP 4 — host enroll (option C; single secret, no global key)
#-------------------------------------------------------------------------------
@@ -3541,6 +3612,7 @@ else
log_skip "break-glass (step 4b) SKIPPED (byo: the host owner manages break-glass; root@pam untouched, nothing vaulted)"
fi
should_skip agent_install || step_agent_install
should_skip internal_place || step_internal_place
should_skip agent_config || step_agent_config
should_skip golden || step_golden
if $SKIP_PROVISION; then
+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