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