From d19dc5a92a0566ff2cd1afa9db4cc401a672c066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 18:05:42 +0000 Subject: [PATCH] ask where the ballast goes and how big it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three questions instead of one, because the two the original never asked are the two that decide whether the thing is useful. 1. Do you want one, with the explanation first. 2. Where. Home (easiest to find again months from now), beside Officer, or a path typed in. This is not tidiness: the checker measures its own directory, so a ballast only protects the filesystem it sits on. Choosing where it goes is choosing which mount is covered. 3. How much, as 5/10/20% — with the actual numbers, and with what would be LEFT rather than only what is taken: [1] 5% — reserves 2.9GB leaving 54.3GB free [2] 10% — reserves 5.8GB leaving 51.4GB free [3] 20% — reserves 11.5GB leaving 45.7GB free A percentage on its own is unanswerable. The number that decides it is the one on the right: the reserve has to be big enough to matter and small enough not to be the thing that filled the disk. The size is computed against the filesystem the chosen path lands on, after the location is known, so the percentages are of the right disk. ballast_free_kb walks up to a directory that exists, since nothing has created the target yet. An existing ballast in either default location is found and left alone rather than a second one being made beside it. Verified end to end in a temp home: created at the chosen path, 2.9G for 5% of 57.1GB, checker installed and reporting it present. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/setup/machine-setup/lib/system.sh | 25 +++-- scripts/setup/machine-setup/machine-setup.sh | 112 +++++++++++++++---- 2 files changed, 111 insertions(+), 26 deletions(-) diff --git a/scripts/setup/machine-setup/lib/system.sh b/scripts/setup/machine-setup/lib/system.sh index ffc261d8..ee1ec830 100644 --- a/scripts/setup/machine-setup/lib/system.sh +++ b/scripts/setup/machine-setup/lib/system.sh @@ -196,22 +196,33 @@ swappiness_set() { # already has passwordless sudo, but wrong, and not something to carry forward. # Both the script and the file now live in root-owned system paths. -BALLAST_FILE=/var/lib/machine-setup/ballast.bin +# Where it goes is asked rather than decided. A ballast only protects the +# filesystem it is ON — the checker measures its own directory — so the choice is +# also a choice of which mount is being protected. Defaults to the user's home, +# which on most machines is the same filesystem as / and is the easiest place to +# find it again months later. +BALLAST_FILE="" BALLAST_CHECKER=/usr/local/sbin/emergency-disk-check BALLAST_CRON=/etc/cron.d/emergency-disk-check BALLAST_THRESHOLD=10 +BALLAST_NAME=emergency-disk-ballast.bin -ballast_exists() { [[ -f "$BALLAST_FILE" ]]; } +ballast_exists() { [[ -n "$BALLAST_FILE" && -f "$BALLAST_FILE" ]]; } ballast_size_human() { du -h "$BALLAST_FILE" 2>/dev/null | cut -f1; } -# 10% of what is free right now, in MiB. -ballast_size_mb() { - local free_kb - free_kb="$(df -Pk /var/lib | awk 'NR == 2 { print $4 }')" - echo $((free_kb / 1024 / 10)) +# The nearest directory that exists, walking up. A path being chosen for the +# ballast does not mean anything has created it yet, and df cannot measure a +# directory that is not there. +existing_ancestor() { + local dir="$1" + while [[ ! -d "$dir" && "$dir" != "/" ]]; do dir="$(dirname "$dir")"; done + echo "$dir" } +# Free space in KiB on whichever filesystem would hold this path. +ballast_free_kb() { df -Pk "$(existing_ancestor "$1")" | awk 'NR == 2 { print $4 }'; } + ballast_create() { local mb="$1" mkdir -p "$(dirname "$BALLAST_FILE")" diff --git a/scripts/setup/machine-setup/machine-setup.sh b/scripts/setup/machine-setup/machine-setup.sh index c62ca90d..1ee807bf 100755 --- a/scripts/setup/machine-setup/machine-setup.sh +++ b/scripts/setup/machine-setup/machine-setup.sh @@ -430,34 +430,108 @@ step "Emergency disk ballast" if ! skip; then echo "" info "Emergency disk ballast — a reserve you can burn when the disk fills up" - echo " A junk file holding no data, sized at 10% of free disk. A root cron" - echo " checks every 10 minutes and deletes it if free space drops below" - echo " ${BALLAST_THRESHOLD}%, so you get room to log in and clean up instead of meeting a" - echo " wedged machine — Docker, journald and postgres all misbehave badly" - echo " at 100% full, and not all of them recover on their own." - echo " It is a one-shot valve: once spent, run this again to recreate it." + echo " A file holding nothing, whose only job is to be deleted. A root cron" + echo " checks every 10 minutes and removes it if free space drops below" + echo " ${BALLAST_THRESHOLD}%, which buys you room to log in and clean up rather than" + echo " meeting a wedged machine — Docker, journald and postgres all" + echo " misbehave badly at 100% full, and not all of them recover on their" + echo " own." echo "" + echo " It is a one-shot valve: once spent, run this again to recreate it." if is_server; then - echo " recommended for ${MACHINE_ROLE} — a full disk on an unattended box is the bad case" + echo " Worth having on ${MACHINE_ROLE} — a full disk on an unattended box is the bad case." else - echo " less useful on ${MACHINE_ROLE} — you are sitting at this machine and will notice" + echo " Less useful on ${MACHINE_ROLE} — you are sitting at this machine and will notice." fi + if [[ -f "${USER_HOME}/${BALLAST_NAME}" ]]; then BALLAST_FILE="${USER_HOME}/${BALLAST_NAME}"; fi + [[ -f "${OFFICER_ROOT}/${BALLAST_NAME}" ]] && BALLAST_FILE="${OFFICER_ROOT}/${BALLAST_NAME}" + if ballast_exists; then + echo "" echo " already present: $(ballast_size_human) at ${BALLAST_FILE}" SUMMARY+=("Disk ballast: already present ($(ballast_size_human))") + elif ! confirm "Create one?"; then + warn "skipped by request" + SUMMARY+=("Disk ballast: SKIPPED by request") else - BALLAST_MB="$(ballast_size_mb)" - echo " to create: ${BALLAST_MB}M at ${BALLAST_FILE}" - if confirm "Create it?"; then - ballast_create "$BALLAST_MB" - ballast_install_checker - ok "ballast $(ballast_size_human), checker at ${BALLAST_CHECKER}" - SUMMARY+=("Disk ballast: $(ballast_size_human), checked every 10 min") - else - warn "skipped by request" - SUMMARY+=("Disk ballast: SKIPPED by request") - fi + # ── where ── + # + # A ballast only protects the filesystem it sits on, because the checker + # measures its own directory. So this is also a choice of which mount is + # being protected, not just where the file is tidiest. + echo "" + info "Where should it go?" + echo " [1] ${USER_HOME}/${BALLAST_NAME}" + echo " your home — easiest to find again months from now" + echo " [2] ${OFFICER_ROOT}/${BALLAST_NAME}" + echo " beside Officer — same place as everything else it owns" + echo " [3] somewhere else, typed in" + echo "" + + BALLAST_FILE="" + while [[ -z "$BALLAST_FILE" ]]; do + if ! read -rp " Which one? (1/2/3) [1]: " BALLAST_WHERE; then + echo "" + fail "No answer." + fi + case "${BALLAST_WHERE:-1}" in + 1) BALLAST_FILE="${USER_HOME}/${BALLAST_NAME}" ;; + 2) BALLAST_FILE="${OFFICER_ROOT}/${BALLAST_NAME}" ;; + 3) + read -rp " Full path to the file: " BALLAST_TYPED || fail "No answer." + BALLAST_TYPED="${BALLAST_TYPED/#\~/$USER_HOME}" + if [[ "$BALLAST_TYPED" == /* ]]; then + BALLAST_FILE="$BALLAST_TYPED" + else + warn "That needs to be an absolute path, starting with /" + fi + ;; + *) warn "Pick 1, 2 or 3." ;; + esac + done + + # ── how much ── + # + # Percentages mean nothing without the numbers behind them, and the number + # that matters is what is LEFT — the point of the reserve is to be big enough + # to matter and small enough not to be the thing that filled the disk. + BALLAST_FREE_KB="$(ballast_free_kb "$(dirname "$BALLAST_FILE")")" + + echo "" + info "How much? ${BALLAST_FILE} sits on a filesystem with $(human_bytes $((BALLAST_FREE_KB * 1024))) free." + for i in 1 2 3; do + case $i in + 1) BP=5 ;; + 2) BP=10 ;; + 3) BP=20 ;; + esac + BSZ=$((BALLAST_FREE_KB * BP / 100)) + printf ' [%d] %3d%% — reserves %-8s leaving %s free\n' \ + "$i" "$BP" "$(human_bytes $((BSZ * 1024)))" "$(human_bytes $(((BALLAST_FREE_KB - BSZ) * 1024)))" + done + echo "" + + BALLAST_PCT="" + while [[ -z "$BALLAST_PCT" ]]; do + if ! read -rp " Which one? (1/2/3) [2]: " BALLAST_SIZE_CHOICE; then + echo "" + fail "No answer." + fi + case "${BALLAST_SIZE_CHOICE:-2}" in + 1) BALLAST_PCT=5 ;; + 2) BALLAST_PCT=10 ;; + 3) BALLAST_PCT=20 ;; + *) warn "Pick 1, 2 or 3." ;; + esac + done + + BALLAST_MB=$((BALLAST_FREE_KB * BALLAST_PCT / 100 / 1024)) + ballast_create "$BALLAST_MB" + ballast_install_checker + ok "ballast $(ballast_size_human) at ${BALLAST_FILE}, checked every 10 minutes" + ok "status any time: ${BALLAST_CHECKER} --status" + SUMMARY+=("Disk ballast: $(ballast_size_human) at ${BALLAST_FILE} (${BALLAST_PCT}%)") fi step_ok fi