diff --git a/docs/INSTALL.md b/docs/INSTALL.md index cb42156aa..9fbf8a3b8 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -14,7 +14,7 @@ curl -fsSL https://github.com/rcourtman/Pulse/releases/latest/download/install.s ``` > **Note**: The GitHub `install.sh` is the **server** installer. The agent installer is served from your Pulse server at `/install.sh` (see **Settings → Agents → Installation commands**). -> **Fallback**: If GitHub downloads are slow or blocked, download the matching Pulse release tarball onto the Proxmox host first and run `bash install.sh --archive /path/to/pulse-vX.Y.Z-linux-amd64.tar.gz`. The installer will push that archive into the LXC instead of making the container fetch it directly. +> **Fallback**: If GitHub downloads are slow or blocked, download the Pulse release tarball for the matching target architecture onto the Proxmox host first and run `bash install.sh --archive /path/to/pulse-vX.Y.Z-linux-amd64.tar.gz`. The installer will push that archive into the LXC instead of making the container fetch it directly. ### Docker Ideal for containerized environments or testing. diff --git a/install.sh b/install.sh index 1a0440c8f..c30c2c4e6 100755 --- a/install.sh +++ b/install.sh @@ -2095,13 +2095,13 @@ detect_pulse_architecture() { local raw_arch="${1:-$(uname -m)}" case $raw_arch in - x86_64) + x86_64|amd64) printf 'amd64\n' ;; - aarch64) + aarch64|arm64) printf 'arm64\n' ;; - armv7l) + armv7l|armv7) printf 'armv7\n' ;; *) @@ -2110,6 +2110,122 @@ detect_pulse_architecture() { esac } +find_pulse_binary_in_dir() { + local dir="$1" + + if [[ -f "$dir/bin/pulse" ]]; then + printf '%s\n' "$dir/bin/pulse" + return 0 + fi + if [[ -f "$dir/pulse" ]]; then + printf '%s\n' "$dir/pulse" + return 0 + fi + + return 1 +} + +detect_pulse_binary_architecture() { + local binary_path="$1" + local file_info="" + local machine="" + local machine_id="" + + if [[ ! -f "$binary_path" ]]; then + return 1 + fi + + if command -v readelf >/dev/null 2>&1; then + machine=$(LC_ALL=C readelf -h "$binary_path" 2>/dev/null | awk -F: '/Machine:/ {gsub(/^[[:space:]]+/, "", $2); print $2; exit}') + case "$machine" in + "Advanced Micro Devices X86-64") + printf 'amd64\n' + return 0 + ;; + "AArch64") + printf 'arm64\n' + return 0 + ;; + "ARM") + printf 'armv7\n' + return 0 + ;; + esac + fi + + if command -v file >/dev/null 2>&1; then + file_info=$(LC_ALL=C file -b "$binary_path" 2>/dev/null || true) + case "$file_info" in + *"x86-64"*) + printf 'amd64\n' + return 0 + ;; + *"aarch64"*|*"ARM64"*) + printf 'arm64\n' + return 0 + ;; + *" ARM "*|ARM,*|*" EABI5"*) + printf 'armv7\n' + return 0 + ;; + esac + fi + + machine_id=$(dd if="$binary_path" bs=1 skip=18 count=2 2>/dev/null | od -An -tu2 2>/dev/null | tr -d '[:space:]') + case "$machine_id" in + 62) + printf 'amd64\n' + return 0 + ;; + 183) + printf 'arm64\n' + return 0 + ;; + 40) + printf 'armv7\n' + return 0 + ;; + esac + + return 1 +} + +validate_pulse_binary_architecture() { + local binary_path="$1" + local target_arch="$2" + local archive_label="${3:-archive}" + local binary_arch="" + + if [[ -z "$target_arch" ]]; then + print_error "Target architecture is required for archive validation" + return 1 + fi + + binary_arch=$(detect_pulse_binary_architecture "$binary_path" 2>/dev/null || true) + if [[ -z "$binary_arch" ]]; then + print_error "Could not determine Pulse binary architecture from $archive_label" + print_info "Use an official Pulse Linux release tarball for this machine." + return 1 + fi + + if [[ "$binary_arch" != "$target_arch" ]]; then + print_error "Archive architecture mismatch: $archive_label contains $binary_arch but this target requires $target_arch" + print_info "Download the official Pulse release tarball for the target architecture." + return 1 + fi + + return 0 +} + +create_temp_archive_path() { + local prefix="$1" + local temp_base="" + + temp_base=$(mktemp "${prefix}-XXXXXX") || return 1 + rm -f "$temp_base" + printf '%s.tar.gz\n' "$temp_base" +} + resolve_target_release() { if [[ -n "${LATEST_RELEASE:-}" ]]; then return 0 @@ -2252,6 +2368,8 @@ install_pulse_archive() { local temp_extract="" local temp_extract2="" local installed_version="" + local pulse_binary_path="" + local target_arch="" if [[ ! -f "$archive_path" ]]; then print_error "Archive not found: $archive_path" @@ -2269,28 +2387,33 @@ install_pulse_archive() { return 1 fi + pulse_binary_path=$(find_pulse_binary_in_dir "$temp_extract" 2>/dev/null || true) + if [[ -z "$pulse_binary_path" ]]; then + print_error "Pulse binary not found in archive" + rm -rf "$temp_extract" + return 1 + fi + + target_arch=$(detect_pulse_architecture 2>/dev/null || true) + if [[ -z "$target_arch" ]]; then + print_error "Unsupported architecture for archive install: $(uname -m)" + rm -rf "$temp_extract" + return 1 + fi + + if ! validate_pulse_binary_architecture "$pulse_binary_path" "$target_arch" "$(basename "$archive_path")"; then + rm -rf "$temp_extract" + return 1 + fi + mkdir -p "$INSTALL_DIR/bin" if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then mv "$INSTALL_DIR/bin/pulse" "$INSTALL_DIR/bin/pulse.old" 2>/dev/null || true fi - if [[ -f "$temp_extract/bin/pulse" ]]; then - if ! cp "$temp_extract/bin/pulse" "$INSTALL_DIR/bin/pulse"; then - print_error "Failed to copy new binary to $INSTALL_DIR/bin/pulse" - [[ -f "$INSTALL_DIR/bin/pulse.old" ]] && mv "$INSTALL_DIR/bin/pulse.old" "$INSTALL_DIR/bin/pulse" - rm -rf "$temp_extract" - return 1 - fi - elif [[ -f "$temp_extract/pulse" ]]; then - if ! cp "$temp_extract/pulse" "$INSTALL_DIR/bin/pulse"; then - print_error "Failed to copy new binary to $INSTALL_DIR/bin/pulse" - [[ -f "$INSTALL_DIR/bin/pulse.old" ]] && mv "$INSTALL_DIR/bin/pulse.old" "$INSTALL_DIR/bin/pulse" - rm -rf "$temp_extract" - return 1 - fi - else - print_error "Pulse binary not found in archive" + if ! cp "$pulse_binary_path" "$INSTALL_DIR/bin/pulse"; then + print_error "Failed to copy new binary to $INSTALL_DIR/bin/pulse" [[ -f "$INSTALL_DIR/bin/pulse.old" ]] && mv "$INSTALL_DIR/bin/pulse.old" "$INSTALL_DIR/bin/pulse" rm -rf "$temp_extract" return 1 @@ -2349,10 +2472,9 @@ install_pulse_archive() { if ! tar -xzf "$archive_path" -C "$temp_extract2"; then print_warn "Failed to re-extract archive for version verification retry" else - if [[ -f "$temp_extract2/bin/pulse" ]]; then - cp -f "$temp_extract2/bin/pulse" "$INSTALL_DIR/bin/pulse" - elif [[ -f "$temp_extract2/pulse" ]]; then - cp -f "$temp_extract2/pulse" "$INSTALL_DIR/bin/pulse" + pulse_binary_path=$(find_pulse_binary_in_dir "$temp_extract2" 2>/dev/null || true) + if [[ -n "$pulse_binary_path" ]]; then + cp -f "$pulse_binary_path" "$INSTALL_DIR/bin/pulse" fi if [[ -f "$temp_extract2/bin/pulse-docker-agent" ]]; then @@ -2449,7 +2571,10 @@ download_pulse() { } print_info "Detected architecture: $raw_arch ($pulse_arch)" - archive_path=$(mktemp "/tmp/pulse-${LATEST_RELEASE}-${pulse_arch}-XXXXXX.tar.gz") + archive_path=$(create_temp_archive_path "/tmp/pulse-${LATEST_RELEASE}-${pulse_arch}") || { + print_error "Failed to create temporary archive path" + exit 1 + } archive_from_temp=true if ! download_release_archive "$LATEST_RELEASE" "$pulse_arch" "$archive_path"; then rm -f "$archive_path" @@ -2488,7 +2613,10 @@ prefetch_pulse_archive_for_container() { print_info "Prefetching Pulse release archive on Proxmox host..." print_info "Detected architecture: $raw_arch ($pulse_arch)" - archive_path=$(mktemp "/tmp/pulse-${LATEST_RELEASE}-${pulse_arch}-lxc-XXXXXX.tar.gz") + archive_path=$(create_temp_archive_path "/tmp/pulse-${LATEST_RELEASE}-${pulse_arch}-lxc") || { + print_error "Failed to create temporary archive path" + return 1 + } if ! download_release_archive "$LATEST_RELEASE" "$pulse_arch" "$archive_path"; then rm -f "$archive_path" return 1 diff --git a/scripts/tests/test-server-install.sh b/scripts/tests/test-server-install.sh index ed4d076eb..c382c98cd 100755 --- a/scripts/tests/test-server-install.sh +++ b/scripts/tests/test-server-install.sh @@ -78,6 +78,7 @@ EOF restore_selinux_contexts() { :; } install_additional_agent_binaries() { return 0; } deploy_agent_scripts() { return 0; } + validate_pulse_binary_architecture() { return 0; } chown() { :; } ln() { :; } curl() { echo "unexpected curl call" >&2; return 99; } @@ -113,6 +114,45 @@ test_prefetch_pulse_archive_for_container_sets_output_var() { ) } +test_install_pulse_archive_rejects_mismatched_arch_without_replacing_existing_binary() { + ( + load_installer + + local tmpdir archive_root archive_path + tmpdir="$(mktemp -d)" + archive_root="${tmpdir}/archive" + archive_path="${tmpdir}/pulse-v5.1.99-linux-arm64.tar.gz" + + mkdir -p "${archive_root}/bin" + cat > "${archive_root}/bin/pulse" <<'EOF' +#!/usr/bin/env bash +echo "v5.1.99" +EOF + chmod +x "${archive_root}/bin/pulse" + tar -czf "${archive_path}" -C "${archive_root}" . + + INSTALL_DIR="${tmpdir}/opt/pulse" + mkdir -p "${INSTALL_DIR}/bin" + cat > "${INSTALL_DIR}/bin/pulse" <<'EOF' +#!/usr/bin/env bash +echo "v5.1.10" +EOF + chmod +x "${INSTALL_DIR}/bin/pulse" + + validate_pulse_binary_architecture() { return 1; } + + if install_pulse_archive "${archive_path}" "v5.1.99"; then + echo "install_pulse_archive unexpectedly succeeded on mismatched archive" >&2 + rm -rf "${tmpdir}" + return 1 + fi + + [[ "$("${INSTALL_DIR}/bin/pulse" --version)" == "v5.1.10" ]] + [[ ! -e "${INSTALL_DIR}/bin/pulse.old" ]] + rm -rf "${tmpdir}" + ) +} + test_parse_args_rejects_archive_with_source() { local tmpdir output_file tmpdir="$(mktemp -d)" @@ -139,6 +179,7 @@ main() { assert_success "infer_release_from_archive_name parses prerelease tarballs" test_infer_release_from_archive_name_supports_prerelease 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 assert_success "parse_args rejects archive with source builds" test_parse_args_rejects_archive_with_source if (( failures > 0 )); then