#!/bin/bash # felhom-internal-place — make the one-disk box's internal data place („Belső tárhely"), felhom.eu # architecture/16-storage-physical-identity.md §5, §7 (sizes ruled by the operator 2026-10-16). Installed as # /usr/local/sbin/felhom-internal-place (config bundle). Called by felhom-host-install.sh (a new box) and by the agent's # signed `add_internal_place` job (an old box) — ONE implementation of the size rule and of the only mkfs it does. # # felhom-internal-place plan → prints the numbers, changes nothing # felhom-internal-place create → makes the place (or finds it), prints the result # # is thin space still to be promised to the guest (rootfs + data volume) that does not exist yet: # the installer passes 32 + the data volume; an old box passes 0 (its guest's volumes are already counted). # # The rule (16 §7, ruled): reserve = max(16 GiB, 10 % of the pool), at most 48 GiB; internal = pool − reserve − # every thin volume already promised in the pool − , made only when ≥ 20 GiB. So nothing in the pool is ever # promised beyond pool − reserve: a pool at 100 % stops every volume on the box. # # It trusts nothing it is told. Every rule is checked here, as root, and any doubt REFUSES (exit 1): # - the pool is the `lvmthin: local-lvm` section of /etc/pve/storage.cfg (its vgname/thinpool), nothing else; # - the only volume it creates, formats or reads is /felhom-belso; # - it formats ONLY a felhom-belso that carries no signature at all (blkid -p finds nothing) — the one it just made, # or one a crash left between lvcreate and mkfs; any other content REFUSES; it never resizes or removes anything. # Output, one line: „created uuid= gib=", „formatted uuid=… gib=…" (a crash left it empty), „existing uuid= gib=", „skipped " (exit 0), # or „plan pool=… reserve=… promised=… planned=… internal=…". Pinned by configs/test_felhom_internal_place.py. set -u STORAGE_CFG=${FELHOM_STORAGE_CFG:-/etc/pve/storage.cfg} LV_NAME=felhom-belso LABEL=felhom-belso MIN_GIB=20 die() { echo "felhom-internal-place: REFUSED: $*" >&2; exit 1; } pool_of_local_lvm() { # prints " " of the lvmthin storage local-lvm, or nothing awk ' /^[a-z]+:[ \t]/ { split($0, h, /:[ \t]+/); type = h[1]; id = h[2]; sub(/[ \t]+$/, "", id); in_s = (type == "lvmthin" && id == "local-lvm"); next } in_s && /^[ \t]+vgname[ \t]/ { vg = $2 } in_s && /^[ \t]+thinpool[ \t]/ { tp = $2 } END { if (vg != "" && tp != "") print vg, tp } ' "$STORAGE_CFG" } gib_floor() { echo $(( $1 / 1073741824 )); } [[ $# -eq 2 ]] || die "usage: felhom-internal-place plan|create " MODE=$1; PLANNED=$2 [[ "$MODE" == plan || "$MODE" == create ]] || die "mode must be plan or create" [[ "$PLANNED" =~ ^(0|[1-9][0-9]{0,5})$ ]] || die "planned GiB must be a whole number" read -r VG POOL < <(pool_of_local_lvm) || true [[ -n "${VG:-}" && -n "${POOL:-}" ]] || die "no lvmthin storage local-lvm in $STORAGE_CFG" [[ "$VG" =~ ^[A-Za-z0-9_.+-]+$ && "$POOL" =~ ^[A-Za-z0-9_.+-]+$ ]] || die "unexpected vg/pool name" pool_b=$(lvs --noheadings --nosuffix --units b -o lv_size "$VG/$POOL" 2>/dev/null | tr -d ' ') [[ "$pool_b" =~ ^[0-9]+$ ]] || die "cannot read the size of $VG/$POOL" pool_gib=$(gib_floor "$pool_b") # Every thin volume promised in the pool (its virtual size) — except the place itself and vzdump's short-lived # snapshots (snap_*_vzdump), which exist only during a whole-box copy and are what the reserve is for. # FAIL CLOSED: a failed or empty listing must never read as „nothing promised" (that would size the place too big). listing=$(lvs --noheadings --nosuffix --units b -o lv_name,pool_lv,lv_size "$VG" 2>/dev/null) || die "cannot list the volumes of $VG" pool_seen=false; promised_b=0; have_lv=false while read -r name pl size; do [[ -n "${name:-}" ]] || continue [[ "$name" == "$POOL" ]] && pool_seen=true [[ "$pl" == "$POOL" ]] || continue if [[ "$name" == "$LV_NAME" ]]; then have_lv=true; continue; fi [[ "$name" =~ ^snap_.*_vzdump$ ]] && continue [[ "$size" =~ ^[0-9]+$ ]] || die "cannot read the size of $VG/$name" promised_b=$(( promised_b + size )) done <<< "$listing" $pool_seen || die "the listing of $VG does not contain the pool $POOL — refusing to guess what is promised" promised_gib=$(( (promised_b + 1073741823) / 1073741824 )) # rounded UP: never promise a partial GiB twice reserve=$(( pool_gib / 10 )); (( reserve < 16 )) && reserve=16; (( reserve > 48 )) && reserve=48 internal=$(( pool_gib - reserve - promised_gib - PLANNED )) if [[ "$MODE" == plan ]]; then echo "plan pool=$pool_gib reserve=$reserve promised=$promised_gib planned=$PLANNED internal=$internal exists=$have_lv" exit 0 fi DEV=/dev/$VG/$LV_NAME is_block() { [[ "$(stat -L -c %F "$1" 2>/dev/null)" == "block special file" ]]; } fs_uuid() { blkid -p -s UUID -o value "$DEV" 2>/dev/null; } fs_type() { blkid -p -s TYPE -o value "$DEV" 2>/dev/null; } fs_label() { blkid -p -s LABEL -o value "$DEV" 2>/dev/null; } # has_signature: FAIL CLOSED. blkid -p exits 0 when it found something and 2 when it found NOTHING; any other result (an # error, an ambivalent probe, blkid missing) must never read as „empty" — that is the only gate in front of mkfs. has_signature() { local rc=0 blkid -p -o value "$DEV" >/dev/null 2>&1 || rc=$? case $rc in 0) return 0 ;; 2) return 1 ;; *) die "cannot probe $DEV for a signature (blkid exit $rc) — refusing" ;; esac } lv_gib() { gib_floor "$(lvs --noheadings --nosuffix --units b -o lv_size "$VG/$LV_NAME" 2>/dev/null | tr -d ' ')"; } if $have_lv; then is_block "$DEV" || die "$DEV is not an active block device" t=$(fs_type) if [[ -n "$t" ]]; then [[ "$t" == ext4 && "$(fs_label)" == "$LABEL" ]] || die "$DEV holds something that is not Felhom's ($t) — left alone" echo "existing uuid=$(fs_uuid) gib=$(lv_gib)"; exit 0 fi # A felhom-belso with NO signature: a crash between lvcreate and mkfs. Nothing can be on it — format it. has_signature && die "$DEV carries a signature blkid cannot name — left alone" else (( internal >= MIN_GIB )) || { echo "skipped only $internal GiB free for it (pool $pool_gib, reserve $reserve, promised $promised_gib, planned $PLANNED; needs $MIN_GIB)"; exit 0; } lvcreate -q -y -V "${internal}G" -T "$VG/$POOL" -n "$LV_NAME" >/dev/null || die "lvcreate failed" is_block "$DEV" || die "$DEV did not appear after lvcreate" has_signature && die "the new $DEV already carries a signature — left alone" made=created fi mkfs.ext4 -q -L "$LABEL" "$DEV" >/dev/null || die "mkfs.ext4 failed on $DEV" u=$(fs_uuid) [[ "$u" =~ ^[0-9a-fA-F-]{36}$ ]] || die "no filesystem UUID after mkfs on $DEV" echo "${made:-formatted} uuid=$u gib=$(lv_gib)"