Files
ProxMenux/scripts/global/update-pve-safe.sh
2026-07-21 18:26:11 +02:00

273 lines
11 KiB
Bash

#!/bin/bash
# ==========================================================
# Proxmox VE Update Script — Safe / Non-Invasive Variant
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# ==========================================================
# Description:
# Update path intended for a Proxmox host ALREADY in
# production. Unlike scripts/global/update-pve8.sh and
# update-pve9_2.sh (invoked by post_install), this variant
# NEVER modifies the operator's own configuration:
#
# - Does NOT disable Enterprise / Ceph repositories
# - Does NOT delete legacy repo files
# - Does NOT overwrite proxmox.sources / debian.sources
# when they already exist
# - Does NOT purge alternative NTP services
# - Does NOT force-install zfsutils / chrony /
# proxmox-backup-restore-image
# - Does NOT write no-firmware-warnings.conf
#
# What it DOES:
# 1. Sanity checks (disk space, network)
# 2. ensure_repositories() — only when repos are MISSING
# 3. apt-get update, with automatic GPG key import when apt
# reports NO_PUBKEY (any repo, user's or ours)
# 4. cleanup_duplicate_repos() — exact URL+Suite+Component
# match against proxmox.sources / debian.sources; leaves
# unrelated custom `download.proxmox.com/*` and
# user-authored pve-*.list files alone; backs each file
# up before modifying
# 5. Detect pending upgrades + security count
# 6. Confirmation dialog
# 7. apt-get full-upgrade with --force-confdef / --force-confold
# (never overwrites the operator's edited config files)
# 8. lvm_repair_check() — refreshes VG metadata when disks
# passed through to guest VMs (DSM, TrueNAS, …) come back
# with old PV headers
# 9. apt-get autoremove + autoclean
#
# Reboot detection is handled by the caller (utilities/proxmox_update.sh).
# ==========================================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
APT_ENV="env DEBIAN_FRONTEND=noninteractive LC_ALL=C LANG=C"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
download_common_functions() {
if ! source "$LOCAL_SCRIPTS/global/common-functions.sh"; then
return 1
fi
}
# ensure_repositories() lives with the install helpers.
source_install_functions() {
local f="$LOCAL_SCRIPTS/global/utils-install-functions.sh"
if [[ -f "$f" ]]; then
source "$f"
fi
}
update_pve_safe() {
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
if [[ -z "$pve_version" ]]; then
msg_error "$(translate "Unable to detect Proxmox version")"
return 1
fi
local start_time
start_time=$(date +%s)
local log_file="/var/log/proxmox-update-$(date +%Y%m%d-%H%M%S).log"
# Screen capture: replay the pre-upgrade context lines after `clear`
# so the operator keeps the visual history around the noisy apt run.
local screen_capture="/tmp/proxmenux_screen_capture_$$.txt"
: > "$screen_capture"
download_common_functions
source_install_functions
{
msg_info2 "$(translate "Detected: Proxmox VE $pve_version — running safe update path")"
} | tee -a "$screen_capture"
# ── 1. Sanity checks ──
local available_space
available_space=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}')
if [ "$available_space" -lt 1024 ]; then
msg_error "$(translate "Insufficient disk space. Available: ${available_space}MB")"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
rm -f "$screen_capture"
return 1
fi
if ! ping -c 1 download.proxmox.com >/dev/null 2>&1; then
msg_error "$(translate "Cannot reach Proxmox repositories")"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
rm -f "$screen_capture"
return 1
fi
# ── 2. ensure_repositories: adds base Proxmox+Debian repos only if
# they don't already exist. On a configured host this is a no-op. ──
if declare -f ensure_repositories >/dev/null 2>&1; then
ensure_repositories
fi
# ── 3. apt-get update with automatic key recovery ──
local update_output update_exit_code
update_output=$(apt-get update 2>&1)
update_exit_code=$?
if [ $update_exit_code -eq 0 ]; then
msg_ok "$(translate "Package lists updated successfully")" | tee -a "$screen_capture"
else
if echo "$update_output" | grep -Eq "NO_PUBKEY|GPG error"; then
local key
key=$(echo "$update_output" | sed -n 's/.*NO_PUBKEY \([0-9A-F]\{8,40\}\).*/\1/p' | head -1)
if [ -n "$key" ]; then
mkdir -p /etc/apt/keyrings
if command -v gpg >/dev/null 2>&1; then
if gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" \
&& gpg --batch --export "$key" | gpg --dearmor -o "/etc/apt/keyrings/${key}.gpg"; then
msg_ok "$(translate "Imported missing GPG key: $key")" | tee -a "$screen_capture"
else
msg_warn "$(translate "Keyrings method failed; trying apt-key fallback")"
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
fi
else
msg_warn "$(translate "gpg not found; trying apt-key fallback")"
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
fi
fi
if apt-get update > "$log_file" 2>&1; then
msg_ok "$(translate "Package lists updated after GPG fix")" | tee -a "$screen_capture"
else
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
rm -f "$screen_capture"
return 1
fi
elif echo "$update_output" | grep -Eq "404|Failed to fetch"; then
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
else
msg_error "$(translate "Failed to update package lists. Check log: $log_file")"
echo "Error details: $update_output"
rm -f "$screen_capture"
return 1
fi
fi
# ── 4. Precise duplicate cleanup (exact URL+Suite+Component match,
# backs up files before modifying). Skipped if unavailable. ──
if declare -f cleanup_duplicate_repos >/dev/null 2>&1; then
cleanup_duplicate_repos
fi
# ── 5-6. Detect + confirm ──
local current_pve_version available_pve_version upgradable security_updates
current_pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
available_pve_version=$(apt-cache policy pve-manager 2>/dev/null | grep -oP 'Candidate: \K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
upgradable=$($APT_ENV apt list --upgradable 2>/dev/null | sed '1d' | sed '/^\s*$/d' | wc -l)
security_updates=$($APT_ENV apt list --upgradable 2>/dev/null | sed '1d' | grep -ci '\-security')
local menu_text
menu_text="$(translate "System Update Information")\n\n"
menu_text+="$(translate "Current PVE Version"): $current_pve_version\n"
if [ -n "$available_pve_version" ] && [ "$available_pve_version" != "$current_pve_version" ]; then
menu_text+="$(translate "Available PVE Version"): $available_pve_version\n"
fi
menu_text+="\n$(translate "Package Updates Available"): $upgradable\n"
menu_text+="$(translate "Security Updates"): $security_updates\n\n"
if [ "$upgradable" -eq 0 ]; then
menu_text+="$(translate "System is already up to date")"
whiptail --title "$(translate "Update Status")" --msgbox "$menu_text" 15 70
apt-get -y autoremove >/dev/null 2>&1 || true
apt-get -y autoclean >/dev/null 2>&1 || true
rm -f "$screen_capture"
return 0
fi
menu_text+="$(translate "Do you want to proceed with the system update?")"
if ! whiptail --title "$(translate "Proxmox Update")" --yesno "$menu_text" 18 70; then
msg_info2 "$(translate "Update cancelled by user")"
apt-get -y autoremove >/dev/null 2>&1 || true
apt-get -y autoclean >/dev/null 2>&1 || true
rm -f "$screen_capture"
return 0
fi
# ── 7. Full upgrade — --force-confdef/confold preserves user-edited configs ──
# Redraw the ProxMenux frame before apt starts printing so the operator
# keeps the visual context around the noisy upgrade output.
clear
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
cat "$screen_capture"
# apt's own progress bar (Progress: [ %]) prints on stderr and only
# when stdout is a TTY. We pipe stderr through tee to keep a log copy
# while letting apt keep its interactive stdout, so the native bar
# keeps rendering at the bottom of the terminal as the user expects.
DEBIAN_FRONTEND=noninteractive apt -y \
-o Dpkg::Options::='--force-confdef' \
-o Dpkg::Options::='--force-confold' \
full-upgrade 2> >(tee -a "$log_file" >&2)
local upgrade_exit_code=$?
echo -e
# Redraw once more so the wrap-up (LVM check, cleanup, summary) reads
# cleanly instead of scrolling under half-a-screen of apt noise.
clear
show_proxmenux_logo
msg_title "$(translate "$SCRIPT_TITLE")"
cat "$screen_capture"
if [ $upgrade_exit_code -ne 0 ]; then
msg_error "$(translate "System upgrade failed. Check log: $log_file")"
rm -f "$screen_capture"
return 1
fi
msg_ok "$(translate "System upgrade completed")"
# ── 8. LVM header repair (only touches VGs actually flagged as stale) ──
if declare -f lvm_repair_check >/dev/null 2>&1; then
lvm_repair_check
fi
# ── 9. DKMS driver rebuild if a new kernel was staged ──
if declare -f pmx_rebuild_dkms_after_kernel >/dev/null 2>&1; then
pmx_rebuild_dkms_after_kernel
fi
# ── 10. Final cleanup ──
apt-get -y autoremove >/dev/null 2>&1 || true
apt-get -y autoclean >/dev/null 2>&1 || true
msg_ok "$(translate "Cleanup finished")"
local end_time duration minutes seconds
end_time=$(date +%s)
duration=$((end_time - start_time))
minutes=$((duration / 60))
seconds=$((duration % 60))
echo -e "${TAB}${BGN}$(translate "====== PVE UPDATE COMPLETED ======")${CL}"
echo -e "${TAB}${GN}⏱️ $(translate "Duration")${CL}: ${BL}${minutes}m ${seconds}s${CL}"
echo -e "${TAB}${GN}📄 $(translate "Log file")${CL}: ${BL}$log_file${CL}"
echo -e "${TAB}${GN}📦 $(translate "Packages upgraded")${CL}: ${BL}$upgradable${CL}"
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}${available_pve_version:-$current_pve_version}${CL}"
msg_ok "$(translate "Proxmox VE safe update completed")"
rm -f "$screen_capture"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
update_pve_safe
fi