From 4de1c3745abb158f46d76c30d04d9c520419b2ee Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 15 Apr 2026 20:56:58 +0100 Subject: [PATCH] Preflight disk space before Pulse updates --- install.sh | 97 +++++++++++++++++++++++++++ internal/updates/adapter_installsh.go | 2 +- scripts/tests/test-server-install.sh | 64 ++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index f0784a464..592547920 100755 --- a/install.sh +++ b/install.sh @@ -31,6 +31,8 @@ CONTAINER_CREATED_FOR_CLEANUP=false BUILD_FROM_SOURCE_MARKER="$INSTALL_DIR/BUILD_FROM_SOURCE" DETECTED_CTID="" STOPPED_PULSE_SERVICE="" +UPDATE_MIN_TEMP_FREE_BYTES=$((900 * 1024 * 1024)) +UPDATE_MIN_INSTALL_FREE_BYTES=$((256 * 1024 * 1024)) # Installer version - the major version this script is bundled with INSTALLER_MAJOR_VERSION=5 @@ -42,6 +44,97 @@ AUTO_NODE_REGISTER_ERROR="" DEBIAN_TEMPLATE_FALLBACK="debian-12-standard_12.12-1_amd64.tar.zst" DEBIAN_TEMPLATE="" +bytes_to_human() { + local bytes="${1:-0}" + + if [[ ! "$bytes" =~ ^[0-9]+$ ]]; then + printf '%s\n' "$bytes" + return 0 + fi + + local units=("B" "KB" "MB" "GB" "TB") + local value="$bytes" + local unit_index=0 + + while (( value >= 1024 && unit_index < ${#units[@]} - 1 )); do + value=$((value / 1024)) + ((unit_index += 1)) + done + + printf '%s%s\n' "$value" "${units[$unit_index]}" +} + +get_available_bytes_for_path() { + local path="$1" + local available_kb="" + + available_kb=$(df -Pk "$path" 2>/dev/null | awk 'NR==2 {print $4}') + if [[ ! "$available_kb" =~ ^[0-9]+$ ]]; then + return 1 + fi + + printf '%s\n' $((available_kb * 1024)) +} + +get_filesystem_device_for_path() { + local path="$1" + local filesystem="" + + filesystem=$(df -Pk "$path" 2>/dev/null | awk 'NR==2 {print $1}') + if [[ -z "$filesystem" ]]; then + return 1 + fi + + printf '%s\n' "$filesystem" +} + +ensure_update_disk_headroom() { + local temp_path="${1:-/tmp}" + local install_path="${2:-$INSTALL_DIR}" + local temp_fs="" + local install_fs="" + local temp_free_bytes="" + local install_free_bytes="" + local combined_required_bytes=$((UPDATE_MIN_TEMP_FREE_BYTES + UPDATE_MIN_INSTALL_FREE_BYTES)) + + temp_fs=$(get_filesystem_device_for_path "$temp_path" 2>/dev/null || true) + install_fs=$(get_filesystem_device_for_path "$install_path" 2>/dev/null || true) + temp_free_bytes=$(get_available_bytes_for_path "$temp_path" 2>/dev/null || true) + install_free_bytes=$(get_available_bytes_for_path "$install_path" 2>/dev/null || true) + + if [[ -z "$temp_free_bytes" || -z "$install_free_bytes" ]]; then + print_warn "Could not determine available disk space for the update preflight; continuing anyway" + return 0 + fi + + if [[ -n "$temp_fs" && "$temp_fs" == "$install_fs" ]]; then + if (( temp_free_bytes < combined_required_bytes )); then + print_error "Not enough free disk space to stage the Pulse update" + print_info "The same filesystem backs $temp_path and $install_path" + print_info "Available: $(bytes_to_human "$temp_free_bytes"), required: $(bytes_to_human "$combined_required_bytes")" + print_info "Free disk space and retry the update" + return 1 + fi + return 0 + fi + + if (( temp_free_bytes < UPDATE_MIN_TEMP_FREE_BYTES )); then + print_error "Not enough free disk space in $temp_path to stage the Pulse update" + print_info "Available: $(bytes_to_human "$temp_free_bytes"), required: $(bytes_to_human "$UPDATE_MIN_TEMP_FREE_BYTES")" + print_info "Free disk space under $temp_path and retry the update" + return 1 + fi + + if (( install_free_bytes < UPDATE_MIN_INSTALL_FREE_BYTES )); then + print_error "Not enough free disk space in $install_path to apply the Pulse update" + print_info "Available: $(bytes_to_human "$install_free_bytes"), required: $(bytes_to_human "$UPDATE_MIN_INSTALL_FREE_BYTES")" + print_info "Free disk space under $install_path and retry the update" + return 1 + fi + + return 0 +} + get_latest_release_from_redirect() { # Follow the GitHub "latest" redirect and extract the tag in a way that # tolerates intermediate redirects that omit /tag/ (issue #698). @@ -2604,6 +2697,10 @@ download_pulse() { rm -f "$BUILD_FROM_SOURCE_MARKER" + if ! ensure_update_disk_headroom "/tmp" "$INSTALL_DIR"; then + exit 1 + fi + EXISTING_SERVICE=$(detect_service_name) stop_pulse_service_for_update "$EXISTING_SERVICE" || true diff --git a/internal/updates/adapter_installsh.go b/internal/updates/adapter_installsh.go index f2fa5fc7c..848b9fefa 100644 --- a/internal/updates/adapter_installsh.go +++ b/internal/updates/adapter_installsh.go @@ -59,7 +59,7 @@ func (a *InstallShAdapter) PrepareUpdate(ctx context.Context, request UpdateRequ Prerequisites: []string{ "Root access (sudo)", "Internet connection", - "At least 100MB free disk space", + "About 1.2GB free disk space for update staging", }, } diff --git a/scripts/tests/test-server-install.sh b/scripts/tests/test-server-install.sh index c9211f561..1cd4500b0 100755 --- a/scripts/tests/test-server-install.sh +++ b/scripts/tests/test-server-install.sh @@ -42,6 +42,68 @@ test_infer_release_from_archive_name_supports_prerelease() { ) } +test_ensure_update_disk_headroom_fails_when_tmp_and_install_share_full_filesystem() { + ( + load_installer + + UPDATE_MIN_TEMP_FREE_BYTES=$((100 * 1024)) + UPDATE_MIN_INSTALL_FREE_BYTES=$((80 * 1024)) + + print_error() { :; } + print_info() { :; } + print_warn() { :; } + df() { + if [[ "$1" == "-Pk" ]]; then + case "$2" in + /tmp|/opt/pulse) + printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\n' + printf '/dev/shared 1000 0 150 0%% /\n' + return 0 + ;; + esac + fi + command df "$@" + } + + if ensure_update_disk_headroom /tmp /opt/pulse; then + echo "ensure_update_disk_headroom unexpectedly passed on a shared full filesystem" >&2 + return 1 + fi + ) +} + +test_ensure_update_disk_headroom_accepts_separate_filesystems_with_sufficient_space() { + ( + load_installer + + UPDATE_MIN_TEMP_FREE_BYTES=$((100 * 1024)) + UPDATE_MIN_INSTALL_FREE_BYTES=$((80 * 1024)) + + print_error() { :; } + print_info() { :; } + print_warn() { :; } + df() { + if [[ "$1" == "-Pk" ]]; then + case "$2" in + /tmp) + printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\n' + printf '/dev/tmp 1000 0 120 0%% /tmp\n' + return 0 + ;; + /opt/pulse) + printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\n' + printf '/dev/root 1000 0 90 0%% /\n' + return 0 + ;; + esac + fi + command df "$@" + } + + ensure_update_disk_headroom /tmp /opt/pulse + ) +} + test_download_pulse_installs_from_local_archive_without_network() { ( load_installer @@ -266,6 +328,8 @@ test_install_additional_agent_binaries_skips_network_when_local_extras_are_missi main() { assert_success "infer_release_from_archive_name parses prerelease tarballs" test_infer_release_from_archive_name_supports_prerelease + assert_success "update disk preflight fails on shared low-space filesystems" test_ensure_update_disk_headroom_fails_when_tmp_and_install_share_full_filesystem + assert_success "update disk preflight passes on separate filesystems with enough headroom" test_ensure_update_disk_headroom_accepts_separate_filesystems_with_sufficient_space assert_success "download_pulse installs from local archive without network" test_download_pulse_installs_from_local_archive_without_network assert_success "prefetch helper writes archive path via output variable" test_prefetch_pulse_archive_for_container_sets_output_var assert_success "wrong-arch archives fail before replacing the installed binary" test_install_pulse_archive_rejects_mismatched_arch_without_replacing_existing_binary