Files
ProxMenux/scripts/utilities/upgrade_pve8_to_pve9.sh
T
MacRimiandClaude Opus 5 da8a480eff Add audit and reports page, and a change journal
ProxMenux modifies the host: it rewrites configuration files, installs packages, enables services. Until now nobody could say afterwards what had changed, and showing the script does not answer that question — a four-hundred-line function may alter two values, and the reader has no way to know which two. This adds the two halves of an answer.

The change journal records what ProxMenux does as it does it. Eleven bash primitives capture the previous state, apply the change and record it in the same step, writing to a spool that the Monitor reads back. One hundred and thirteen functions across twenty-five scripts are instrumented, covering post-install, shared storage, security tooling, container conversions, disk operations and the PVE 8 to 9 upgrade path. The page shows the difference — rotate 7 becoming rotate 14 — and never the script. Restore and backup scripts are deliberately left out: a restore puts the host back to a state some other script already recorded.

The Audit and reports page answers the other half: what state is this host in, regardless of who put it there. Forty-three checks across seven areas read the host and classify each result as critical, warning, observation, conformant, unverified or not applicable, with the evidence they read attached to each one. A declared policy lets the reader say what this particular host is expected to do — which guests must have a backup, which storages are essential — so the report judges the host against its own intent rather than a generic template. An inventory records the hardware, network and guest topology behind those readings, a comparison shows what moved between two runs, and six report profiles produce a printable document scoped to what the reader needs. Everything is available in the eight supported languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:06:04 +02:00

1193 lines
38 KiB
Bash

#!/bin/bash
# ==========================================================
# ProxMenux - Upgrade PVE 8 to PVE 9
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
# Version : 1.0
# ==========================================================
# Description:
# Automated or interactive Proxmox VE 8 -> 9 major-version upgrade.
# Follows the official Proxmox upgrade procedure (Debian Bookworm ->
# Trixie + PVE 8 -> 9) with comprehensive safeguards, repository
# migration, Ceph version validation and post-upgrade verification.
#
# A major-version upgrade is destructive and not reversible -- the
# script enforces multiple safety gates and refuses to run from the
# Proxmox web terminal.
#
# Features:
# - Mode menu: Automatic (unattended), Interactive, Pre-check,
# Manual guide.
# - Blocks execution from the web terminal (termproxy / vncshell);
# requires SSH or physical / IPMI / iKVM console.
# - Pre-flight: pve8to9 --full, disk-space check
# (/var/cache/apt/archives >= 1024 MB), connectivity to
# download.proxmox.com, Ceph 19.x Squid validation when present.
# - Brings PVE 8 to the latest 8.4.x patch level first via the
# PVE 8 update worker.
# - Repository migration: sed bookworm -> trixie, deb822 .sources
# files for proxmox + debian + ceph, automatic fallback from
# Enterprise to no-subscription on 401 Unauthorized.
# - Validates 'proxmox-ve' candidate is 9.x and that the dry-run
# dist-upgrade does not propose removing it (would indicate
# broken repo config).
# - dist-upgrade with --force-confdef --force-confold in
# unattended mode; user-driven dpkg prompts in interactive mode.
# - Post-upgrade: installs grub-efi-amd64 on EFI hosts, restarts
# pve-manager, re-runs pve8to9 to surface residual issues.
# - Reboot prompt at the end.
# - Full log written to /var/log/pve8-a-pve9-<timestamp>.log.
# ==========================================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# Load shared global functions: cleanup_duplicate_repos and ensure_repositories
# are referenced by the auto-repair flow below and live in these files.
if [[ -f "$LOCAL_SCRIPTS/global/common-functions.sh" ]]; then
source "$LOCAL_SCRIPTS/global/common-functions.sh"
fi
if [[ -f "$LOCAL_SCRIPTS/global/utils-install-functions.sh" ]]; then
source "$LOCAL_SCRIPTS/global/utils-install-functions.sh"
fi
if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then
source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
fi
# ==========================================================
LOG="/var/log/pve8-a-pve9-$(date +%Y%m%d-%H%M%S).log"
: > "$LOG"
REPO_MODE="no-subscription"
DISABLE_AUDIT="1"
ASSUME_YES="0"
CEPH_ENFORCE="auto"
for arg in "$@"; do
case "$arg" in
--enterprise) REPO_MODE="enterprise" ;;
--no-subscription) REPO_MODE="no-subscription" ;;
--skip-audit-disable) DISABLE_AUDIT="0" ;;
--assume-yes|-y) ASSUME_YES="1" ;;
--ignore-ceph-check) CEPH_ENFORCE="skip" ;;
--warn-ceph-check) CEPH_ENFORCE="warn" ;;
*) ;;
esac
done
# ==========================================================
run_manual_guide() {
local SCRIPT_FILE="$LOCAL_SCRIPTS/utilities/proxmox-upgrade-pve8-to-pve9-manual-guide.sh"
if [[ ! -f "$SCRIPT_FILE" ]]; then
msg_error "$(translate "Manual guide script not found")"
return 1
fi
bash "$SCRIPT_FILE"
}
run_pve8to9_check() {
local SCRIPT_FILE="$LOCAL_SCRIPTS/utilities/pve8to9_check.sh"
if [[ ! -f "$SCRIPT_FILE" ]]; then
msg_error "$(translate "Check script not found")"
return 1
fi
bash "$SCRIPT_FILE"
}
ask_run_mode() {
if [[ "${ASSUME_YES:-0}" == "1" ]]; then
msg_ok "$(translate "Run mode: Unattended")"
export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none
exit 0
fi
while true; do
if command -v dialog >/dev/null 2>&1; then
local title text choice status
title="$(translate "Select run mode")"; [[ -z "$title" ]] && title="Select run mode"
text="$(translate "Choose how to perform the upgrade:")"; [[ -z "$text" ]] && text="Choose how to perform the upgrade:"
title=${title//$'\r'/}; title=${title//$'\n'/' '}
text=${text//$'\r'/}; text=${text//$'\n'/' '}
choice=$(
dialog --backtitle "ProxMenux" \
--title "$title" \
--menu "$text" 20 70 8 \
1 "$(translate "Automatic/Unattended")" \
2 "$(translate "Interactive (guided, prompts visible)")" \
3 "$(translate "Run PVE 8 to 9") check" \
4 "$(translate "Manual upgrade guide step by step")" \
3>&1 1>&2 2>&3
); status=$?
if [[ $status -ne 0 ]]; then
exit 0
fi
case "$choice" in
1)
ASSUME_YES="1"
show_proxmenux_logo || true
msg_title "$(translate "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)")"
msg_info2 "$(translate "Run mode selected: Automatic")"
export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none
break
;;
3)
run_pve8to9_check
continue
;;
4)
run_manual_guide
continue
;;
*)
show_proxmenux_logo || true
msg_title "$(translate "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)")"
msg_info2 "$(translate "Run mode selected: Interactive")"
break
;;
esac
elif command -v whiptail >/dev/null 2>&1; then
local choice
if ! choice=$(
whiptail --title "$(translate "Select run mode")" \
--menu "$(translate "Choose how to perform the upgrade:")" 20 70 8 \
"1" "$(translate "Automatic/Unattended")" \
"2" "$(translate "Interactive (guided, prompts visible)")" \
"3" "$(translate "Run PVE 8 to 9") check" \
"4" "$(translate "Manual upgrade guide step by step")" \
3>&1 1>&2 2>&3
); then
exit 0
fi
case "$choice" in
1)
ASSUME_YES="1"
show_proxmenux_logo || true
msg_title "$(translate "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)")"
msg_info2 "$(translate "Run mode selected: Automatic/Unattended")"
export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none
break
;;
3)
run_pve8to9_check
continue
;;
4)
run_manual_guide
continue
;;
*)
show_proxmenux_logo || true
msg_title "$(translate "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)")"
msg_info2 "$(translate "Run mode selected: Interactive")"
break
;;
esac
fi
done
}
# ==========================================================
confirm() {
local prompt="$1"
if [[ "$ASSUME_YES" == "1" ]]; then
return 0
fi
if command -v whiptail >/dev/null 2>&1; then
whiptail --title "$(translate "Confirmation")" --yesno "$prompt" 14 80
else
echo -n "$(translate "$prompt") [y/N]: "
read -r ans
[[ "$ans" =~ ^[Yy]$ ]]
fi
}
ask_choice_auto_or_manual() {
if [[ "$ASSUME_YES" == "1" ]]; then
echo "auto"; return 0
fi
if command -v whiptail >/dev/null 2>&1; then
if whiptail --title "$(translate "Cluster upgrade mode")" --yesno "$(translate "Pending upgrades detected on a clustered node.\n\nTo proceed safely, update this node to the latest Proxmox VE 8.x before switching to Trixie/PVE 9.\n\nSelect Yes for AUTOMATIC upgrade (recommended), or No for MANUAL instructions.")" 16 78; then
echo "auto"
else
echo "manual"
fi
else
echo -n "$(translate "Pending upgrades detected on a clustered node. Perform AUTOMATIC upgrade now? (y = automatic, n = manual): ")"
read -r ans
[[ "$ans" =~ ^[Yy]$ ]] && echo "auto" || echo "manual"
fi
}
run_step() {
local pre="$1"; local ok="$2"; local cmd="$3"
echo -ne "${TAB}${YW}$(translate "$pre")${CL}"
if bash -lc "set -o pipefail; $cmd 2>&1 | tee -a \"$LOG\""; then
echo -e "${BFR}${TAB}${CM}${GN}$(translate "$ok")${CL}"
return 0
else
echo -e "${BFR}${TAB}${RD}[ERROR] $(translate "Failed. See log:") $LOG${CL}"
exit 1
fi
}
run_step_interactive() {
local pre="$1"; local ok="$2"; shift 2
echo -ne "${TAB}${YW}$(translate "$pre")${CL}"
if "$@"; then
echo -e "${BFR}${TAB}${CM}${GN}$(translate "$ok")${CL}"
return 0
else
echo -e "${BFR}${TAB}${RD}[ERROR] $(translate "Failed. See log:") $LOG${CL}"
exit 1
fi
}
append_step() {
local pre="$1"; local ok="$2"; local cmd="$3"
echo -ne "${TAB}${YW}$(translate "$pre")${CL}"
if bash -lc "set -o pipefail; $cmd 2>&1 | tee -a \"$LOG\""; then
echo -e "${BFR}${TAB}${CM}${GN}$(translate "$ok")${CL}"
else
echo -e "${BFR}${TAB}${CM}${GN}$(translate "$ok")${CL}"
fi
}
file_contains() {
local file="$1"
local pattern="$2"
grep -qE "$pattern" "$file" 2>/dev/null
}
is_cluster_node() {
if command -v pvecm >/dev/null 2>&1 && pvecm status >> "$LOG" 2>&1; then
return 0
fi
return 1
}
is_hyperconverged_ceph_node() {
if command -v pveceph >/dev/null 2>&1; then
if pveceph status >> "$LOG" 2>&1; then
return 0
fi
fi
if systemctl list-units --type=service --all 2>/dev/null | grep -Eq 'ceph-(mon|osd|mgr)@'; then
return 0
fi
if ls /var/lib/ceph/osd/ceph-* >/dev/null 2>&1; then
return 0
fi
if [[ -f /etc/pve/ceph.conf ]]; then
return 0
fi
return 1
}
# ==========================================================
create_pve_repo_enterprise_if_missing() {
local f="/etc/apt/sources.list.d/pve-enterprise.sources"
[[ -f "$f" ]] && return 0
cat > "$f" << 'EOF'
Types: deb
URIs: https://enterprise.proxmox.com/debian/pve
Suites: trixie
Components: pve-enterprise
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
chmod 644 "$f"
}
create_pve_repo_nosub_if_missing() {
local f="/etc/apt/sources.list.d/proxmox.sources"
[[ -f "$f" ]] && return 0
cat > "$f" << 'EOF'
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
chmod 644 "$f"
}
create_ceph_repo_enterprise_if_missing() {
local f="/etc/apt/sources.list.d/ceph.sources"
[[ -f "$f" ]] && return 0
cat > "$f" << 'EOF'
Types: deb
URIs: https://enterprise.proxmox.com/debian/ceph-squid
Suites: trixie
Components: enterprise
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
chmod 644 "$f"
}
create_ceph_repo_nosub_if_missing() {
local f="/etc/apt/sources.list.d/ceph.sources"
[[ -f "$f" ]] && return 0
cat > "$f" << 'EOF'
Types: deb
URIs: http://download.proxmox.com/debian/ceph-squid
Suites: trixie
Components: no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
chmod 644 "$f"
}
disable_enterprise_repo_if_present() {
local FUNC_VERSION="1.0"
pmx_journal_context "disable_enterprise_repo_if_present" "$FUNC_VERSION"
local s="/etc/apt/sources.list.d/pve-enterprise.sources"
local l="/etc/apt/sources.list.d/pve-enterprise.list"
if [[ -f "$s" ]]; then
if grep -qi '^Enabled:' "$s"; then
pmx_edit_file "$s" 's/^Enabled:.*/Enabled: false/i'
else
echo "Enabled: false" | pmx_append_file "$s"
fi
fi
if [[ -f "$l" ]]; then
pmx_edit_file "$l" 's/^[[:space:]]*deb/# deb/'
fi
}
comment_legacy_pve8_lists() {
local FUNC_VERSION="1.0"
pmx_journal_context "comment_legacy_pve8_lists" "$FUNC_VERSION"
for f in /etc/apt/sources.list.d/pve-public-repo.list /etc/apt/sources.list.d/pve-install-repo.list; do
[[ -f "$f" ]] || continue
pmx_edit_file "$f" 's/^[[:space:]]*deb/# deb/' || true
done
}
comment_legacy_ceph_list() {
local FUNC_VERSION="1.0"
pmx_journal_context "comment_legacy_ceph_list" "$FUNC_VERSION"
local f="/etc/apt/sources.list.d/ceph.list"
[[ -f "$f" ]] || return 0
pmx_edit_file "$f" 's/^[[:space:]]*deb/# deb/' || true
}
apt_update_with_repo_fallback() {
local output status
output="$(apt-get update >/dev/null 2>&1 | tee -a "$LOG")"; status=${PIPESTATUS[0]}
if [[ $status -eq 0 ]]; then
msg_ok2 "$(translate "APT indexes updated for Trixie")"
return 0
fi
if [[ "$REPO_MODE" == "enterprise" ]] && { echo "$output" | grep -qE '401[[:space:]]+Unauthorized' || echo "$output" | grep -qi 'enterprise\.proxmox\.com'; }; then
if [[ "$ASSUME_YES" == "1" ]] || confirm "$(translate "Enterprise repository returned 401 Unauthorized (no valid subscription). Switch to the no-subscription repository and retry?")"; then
disable_enterprise_repo_if_present
create_pve_repo_nosub_if_missing
if [[ -f /etc/apt/sources.list.d/ceph.sources ]]; then
create_ceph_repo_nosub_if_missing
fi
REPO_MODE="no-subscription"
msg_ok2 "$(translate "Repositories switched to no-subscription")"
if apt-get update >> "$LOG" >/dev/null 2>&1; then
msg_ok2 "$(translate "APT indexes updated for Trixie (no-subscription)")"
return 0
else
echo -e "${BFR}${TAB}${RD}[ERROR] $(translate "APT index update failed after switching to no-subscription. See log:") $LOG${CL}"
exit 1
fi
else
echo -e "${BFR}${TAB}${RD}[ERROR] $(translate "Enterprise repository unauthorized and fallback declined by user") $LOG${CL}"
exit 1
fi
fi
echo -e "${BFR}${TAB}${RD}[ERROR] $(translate "APT index update failed. See log:") $LOG${CL}"
exit 1
}
# ==========================================================
proxmox_repo_candidate_ok() {
local cand
cand="$(apt-cache policy proxmox-ve 2>/dev/null | awk -F': ' '/Candidate:/{print $2}')"
[[ -n "$cand" && "$cand" != "(none)" && "$cand" =~ ^9 ]]
}
simulate_would_remove_proxmox_ve() {
apt-get -s dist-upgrade >/dev/null 2>&1 | grep -Eq 'Remv[[:space:]]+proxmox-ve|The following packages will be REMOVED:.*proxmox-ve'
}
guard_against_proxmox_ve_removal() {
msg_info "$(translate "Validating Proxmox 9 repositories (checking 'proxmox-ve' candidate)...")"
if proxmox_repo_candidate_ok; then
msg_ok "$(translate "Proxmox repository OK (candidate is 9.x)")"
else
if [[ "$REPO_MODE" == "enterprise" ]]; then
msg_warn "$(translate "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...")"
disable_enterprise_repo_if_present
create_pve_repo_nosub_if_missing
REPO_MODE="no-subscription"
apt_update_with_repo_fallback
if proxmox_repo_candidate_ok; then
msg_ok "$(translate "Proxmox repository fixed (no-subscription, candidate is 9.x)")"
else
msg_error "$(translate "Could not find a valid 'proxmox-ve' 9.x candidate after switching to no-subscription. Please verify your repository configuration and network, then retry.")"
exit 1
fi
else
msg_error "$(translate "Invalid 'proxmox-ve' candidate (not 9.x or none). Please verify your repository configuration and network, then retry.")"
exit 1
fi
fi
msg_info "$(translate "Running pre-upgrade simulation to verify 'proxmox-ve' will remain installed...")"
if simulate_would_remove_proxmox_ve; then
msg_error "$(translate "Pre-upgrade check FAILED: the simulation shows that 'proxmox-ve' would be REMOVED.\n This indicates a repository or dependency issue and upgrading now could break your Proxmox installation.")"
echo "---- $(translate "Recommended diagnostic commands") ----"
echo "apt-cache policy proxmox-ve"
echo "apt policy | sed -n '1,120p'"
echo "cat /etc/apt/sources.list"
echo "ls -l /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources"
exit 1
else
msg_ok "$(translate "Pre-upgrade simulation passed: 'proxmox-ve' will be kept or upgraded safely.")"
fi
}
# ==========================================================
check_not_web_terminal() {
[[ "${PVE_ALLOW_WEBTERM:-0}" = "1" ]] && return 0
[[ -n "${SSH_CONNECTION:-}" ]] && return 0
local pid ppid comm args i
pid=$$
show_web_terminal_block_msg() {
local title msg_line1 msg_line2 msg
title="$(translate "Unsupported Terminal")"
msg_line1="$(translate "This script cannot be executed from the Proxmox web terminal.")"
msg_line2="$(translate "Please use an SSH session (Linux, macOS, Windows/PuTTY) or a physical console to perform the upgrade.")"
msg="${msg_line1}"$'\n\n'"${msg_line2}"
whiptail --title "$title" --msgbox "$msg" 12 72
}
for i in {1..12}; do
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
[[ -z "$ppid" || "$ppid" -le 1 ]] && break
comm=$(ps -o comm= -p "$ppid" 2>/dev/null || true)
args=$(ps -o args= -p "$ppid" 2>/dev/null || true)
if echo "$comm $args" | grep -Eqi 'termproxy|vncshell'; then
show_web_terminal_block_msg
msg_warn "$(translate "Upgrade canceled by user")"
exit 1
fi
pid="$ppid"
done
}
ask_run_mode
check_not_web_terminal
export NCURSES_NO_UTF8_ACS=1
show_upgrade_confirm() {
local title
title="$(translate "Upgrade to Proxmox VE 9")"
local intro req l1 l2 l3 l4 cont
intro="$(translate "This will upgrade this node to Proxmox VE 9 on Debian Trixie.")"
req="$(translate "REQUIREMENTS:")"
l1="$(translate "Valid backups for all VMs/CTs")"
l2="$(translate "Run from console, or SSH inside tmux/screen")"
l3="$(translate "Migrate away any guests that must keep running")"
l4=""
cont="$(translate "Continue?")"
local bullet="•"
if ! locale charmap 2>/dev/null | grep -qi 'utf-8'; then
bullet="-"
fi
local msg
msg="${intro}"$'\n\n'"${req}"$'\n'
msg+="${bullet} ${l1}"$'\n'
msg+="${bullet} ${l2}"$'\n'
msg+="${bullet} ${l3}"
[[ -n "$l4" ]] && msg+=$'\n'"${bullet} ${l4}"
msg+=$'\n\n'"${cont}"
local cols width height
cols=$(tput cols 2>/dev/null || echo 80)
width=$(( cols < 78 ? (cols>40?cols-2:40) : 78 ))
height=18
if whiptail --title "$title" --yesno "$msg" "$height" "$width"; then
return 0
else
msg_warn "$(translate "Upgrade canceled by user")"
exit 0
fi
}
show_upgrade_confirm
# ---------------------------
# Step 1
# ---------------------------
apt_upgrade() {
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 "Unable to detect Proxmox version."
return 1
fi
if [[ "$pve_version" -ge 9 ]]; then
msg_info2 "$(translate "Proxmox 9 system update allready")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 1
else
bash "$LOCAL_SCRIPTS/global/update-pve8.sh"
hash -r
fi
}
apt_upgrade
# ---------------------------
regenerate_pve_cache() {
msg_info "$(translate "Regenerating PVE package cache...")"
mkdir -p /var/lib/pve-manager
chmod 755 /var/lib/pve-manager
systemctl restart pve-manager 2>/dev/null || true
sleep 2
timeout 10 pvesh get /nodes/localhost/apt/update 2>/dev/null || true
msg_ok "$(translate "PVE cache regenerated")"
}
regenerate_pve_cache
# ---------------------------
run_pve8to9_check() {
# Same recursion guard as in pve8to9_check.sh — auto-repair followed by
# re-check could otherwise loop indefinitely if repairs don't actually
# resolve the FAIL. Cap at 3 attempts. Audit Tier 6.
: "${_PVE8TO9_DEPTH:=0}"
_PVE8TO9_DEPTH=$((_PVE8TO9_DEPTH + 1))
if (( _PVE8TO9_DEPTH > 3 )); then
msg_error "$(translate "Maximum auto-repair attempts reached (3). Please review the log and run any remaining commands manually.")"
_PVE8TO9_DEPTH=0
return 1
fi
local tmp
tmp="$(mktemp)"
echo -e
set -o pipefail
pve8to9 --full 2>&1 | tee -a "$LOG" | tee "$tmp"
local rc=${PIPESTATUS[0]}
local fails warns
fails=$(grep -c 'FAIL:' "$tmp" || true)
warns=$(grep -c 'WARN:' "$tmp" || true)
if (( fails > 0 )); then
echo -e
echo -e "${BFR}${RD}[ERROR] $(translate "Pre-check found") $fails $(translate "blocking issue(s).")\n$(translate "Please resolve the problem(s) as described above, then re-run the upgrade script.")${CL}"
echo -e
local repair_commands=()
local repair_descriptions=()
# Error 1: systemd-boot meta-package
if grep -q 'systemd-boot meta-package installed' "$tmp"; then
repair_commands+=("apt remove -y systemd-boot")
repair_descriptions+=("$(translate "Remove obsolete systemd-boot meta-package")")
echo -e "${YW}$(translate "Fix systemd-boot:") ${CL}apt remove -y systemd-boot"
fi
# Error 2: Kernel version mismatch
if grep -q -E 'FAIL:.*(kernel.*mismatch|kernel.*version.*mismatch)' "$tmp"; then
repair_commands+=("update-grub")
repair_descriptions+=("$(translate "Update kernel to compatible version")")
echo -e "${YW}$(translate "Fix kernel version:") ${CL}update-grub"
fi
# Error 3: Ceph version incompatible
if grep -q -E '(ceph.*version|ceph.*incompatible)' "$tmp"; then
repair_commands+=("ceph versions && pveceph upgrade")
repair_descriptions+=("$(translate "Upgrade Ceph to compatible version")")
echo -e "${YW}$(translate "Fix Ceph version:") ${CL}ceph versions && pveceph upgrade"
fi
# Error 4: Repository configuration issues
if grep -q -E '(repository.*issue|repo.*problem|sources.*error)' "$tmp"; then
repair_commands+=("cleanup_duplicate_repos && ensure_repositories")
repair_descriptions+=("$(translate "Fix repository configuration")")
echo -e "${YW}$(translate "Fix repositories:") ${CL}cleanup_duplicate_repos && ensure_repositories"
fi
# Error 5: Package conflicts
if grep -q -E '(package.*conflict|dependency.*problem)' "$tmp"; then
repair_commands+=("apt update && apt autoremove -y && apt autoclean")
repair_descriptions+=("$(translate "Resolve package conflicts")")
echo -e "${YW}$(translate "Fix package conflicts:") ${CL}apt update && apt autoremove -y && apt autoclean"
fi
# Error 6: Disk space issues
if grep -q -E 'FAIL:.*(disk space|no space left|storage.*full)' "$tmp"; then
repair_commands+=("apt clean && apt autoremove -y && journalctl --vacuum-time=7d")
repair_descriptions+=("$(translate "Free up disk space")")
echo -e "${YW}$(translate "Fix disk space:") ${CL}apt clean && apt autoremove -y && journalctl --vacuum-time=7d"
fi
echo -e
if [[ ${#repair_commands[@]} -gt 0 ]]; then
echo -e "${BFR}${CY}$(translate "Repair Options:")${CL}"
echo -e "${TAB}${GN}1.${CL} $(translate "Try automatic repair of detected issues")"
echo -e "${TAB}${GN}2.${CL} $(translate "Show manual repair commands")"
echo -e "${TAB}${GN}3.${CL} $(translate "Exit and repair manually")"
echo -e
echo -n "$(translate "Select option [1-3] (default: 3): ")"
read -r repair_choice
case "$repair_choice" in
1)
echo -e
msg_info2 "$(translate "Attempting automatic repair...")"
local repair_success=0
for i in "${!repair_commands[@]}"; do
echo -e "${TAB}${YW}$(translate "Executing:") ${repair_descriptions[$i]}${CL}"
if eval "${repair_commands[$i]}"; then
msg_ok "${repair_descriptions[$i]} - $(translate "Success")"
else
msg_error "${repair_descriptions[$i]} - $(translate "Failed")"
repair_success=1
fi
done
if [[ $repair_success -eq 0 ]]; then
echo -e
msg_info2 "$(translate "Re-running pre-check after repairs...")"
sleep 2
run_pve8to9_check
return $?
else
echo -e
msg_error "$(translate "Some repairs failed. Please fix manually and re-run the script.")"
fi
;;
2)
echo -e
echo -e "$(translate "${BFR}${CY}Manual Repair Commands:${CL}")"
for i in "${!repair_commands[@]}"; do
echo -e "${TAB}${BL}# ${repair_descriptions[$i]}${CL}"
echo -e
echo -e "${TAB}${repair_commands[$i]}"
echo -e
done
echo -e
msg_info2 "$(translate "Once finished, re-run 'PVE 8 to 9 check' to verify that all issues are resolved \n before executing the PVE 8 → PVE 9 upgrade.")"
echo -e
msg_success "$(translate "Press Enter to exit the script after reading instructions...")"
read -r
rm -f "$tmp"
exit 1
;;
*)
echo -e
msg_info2 "$(translate "Exiting for manual repair...")"
;;
esac
fi
msg_success "$(translate "Press Enter to exit and repair")"
read -r
rm -f "$tmp"
exit 1
fi
echo -e
msg_ok "$(translate "Checklist pre-check finished. Warnings:") $warns"
rm -f "$tmp"
return $rc
}
run_pve8to9_check
# ---------------------------
# Step 2
# ---------------------------
run_step \
"" \
"Connectivity to Proxmox repository OK" \
"ping -c1 -W2 download.proxmox.com >/dev/null"
msg_info "$(translate "Checking free space in /var/cache/apt/archives...")"
FREE_MB=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}')
if [[ "${FREE_MB:-0}" -lt 1024 ]]; then
msg_error "$(translate "Insufficient space:") ${FREE_MB}MB $(translate "(need ≥ 1024MB)")"
exit 1
else
msg_ok "$(translate "Free space OK:") ${FREE_MB}MB"
fi
FUNC_VERSION="1.0"
pmx_journal_context "upgrade_pve8_to_pve9" "$FUNC_VERSION"
if [[ "$DISABLE_AUDIT" == "1" ]]; then
pmx_disable_service systemd-journald-audit.socket >> "$LOG" 2>&1 || true
echo -e "${BFR}${TAB}${CM}${GN}$(translate "Audit socket disabled or not required")${CL}"
fi
# ---------------------------
# Step 3
# ---------------------------
if command -v ceph >/dev/null 2>&1; then
if [[ "$CEPH_ENFORCE" == "skip" ]]; then
msg_ok2 "$(translate "Ceph check skipped by user flag (--ignore-ceph-check)")"
else
if is_hyperconverged_ceph_node; then
msg_info "$(translate "Ceph detected as hyper-converged on this node. Checking version (require 19.x Squid)...")"
CEPH_V=$(ceph --version 2>/dev/null | head -1 || true)
if echo "$CEPH_V" | grep -Eq 'ceph.*(version 19|squid)'; then
msg_ok "$(translate "Ceph version OK:") $CEPH_V"
else
if [[ "$CEPH_ENFORCE" == "warn" ]]; then
msg_warn "$(translate "Ceph is not 19.x (Squid). Proceeding due to --warn-ceph-check. Detected:") ${CEPH_V:-N/A}"
else
msg_error "$(translate "Ceph is not 19.x (Squid). Upgrade Ceph first. Detected:") ${CEPH_V:-N/A}"
exit 1
fi
fi
else
CEPH_V=$(ceph --version 2>/dev/null | head -1 || true)
echo -e "${BFR}${TAB}${CM}${GN}${CEPH_V:-N/A}${CL}"
fi
fi
fi
# ---------------------------
# Step 4
# ---------------------------
FUNC_VERSION="1.0"
pmx_journal_context "upgrade_pve8_to_pve9" "$FUNC_VERSION"
OS_FILE="/etc/apt/sources.list"
if [[ -f "$OS_FILE" ]]; then
msg_info "$(translate "Updating Debian Bookworm → Trixie in sources.list...")"
if pmx_edit_file "$OS_FILE" 's/bookworm/trixie/g'; then
msg_ok "$(translate "sources.list updated to Trixie")"
else
msg_ok "$(translate "sources.list update skipped (no change)")"
fi
else
msg_ok "$(translate "No main sources.list present (skipped)")"
fi
PVE_ENT_LIST="/etc/apt/sources.list.d/pve-enterprise.list"
msg_info "$(translate "Updating pve-enterprise.list (if present) to Trixie...")"
if [[ -f "$PVE_ENT_LIST" ]]; then
if pmx_edit_file "$PVE_ENT_LIST" 's/bookworm/trixie/g'; then
msg_ok "$(translate "pve-enterprise.list updated to Trixie")"
else
msg_ok "$(translate "pve-enterprise.list update skipped (no change)")"
fi
else
msg_ok "$(translate "No pve-enterprise.list present (skipped)")"
fi
msg_info "$(translate "Commenting any residual Bookworm lines in *.list...")"
for f in /etc/apt/sources.list.d/*.list; do
[[ -f "$f" ]] || continue
pmx_edit_file "$f" '/bookworm/s/^/# /' || true
done
pmx_edit_file "$OS_FILE" '/bookworm/s/^/# /' 2>/dev/null || true
msg_ok "$(translate "Residual Bookworm entries commented where applicable")"
# ---------------------------
# Step 5
# ---------------------------
if [[ "$REPO_MODE" == "enterprise" ]]; then
msg_info "$(translate "Ensuring pve-enterprise.sources (PVE 9, deb822) is present...")"
create_pve_repo_enterprise_if_missing
msg_ok "$(translate "Enterprise repository present")"
else
msg_info "$(translate "Ensuring proxmox.sources (PVE 9, no-subscription, deb822) is present...")"
create_pve_repo_nosub_if_missing
msg_ok "$(translate "No-subscription repository present")"
fi
msg_info "$(translate "Commenting legacy PVE 8 repository .list files (if any)...")"
comment_legacy_pve8_lists
msg_ok "$(translate "Legacy PVE 8 .list files commented or not present")"
# ---------------------------
# Step 6
# ---------------------------
if command -v ceph >/dev/null 2>&1; then
if [[ "$REPO_MODE" == "enterprise" ]]; then
msg_info "$(translate "Ensuring ceph.sources (enterprise, Trixie) is present...")"
create_ceph_repo_enterprise_if_missing
msg_ok "$(translate "Ceph enterprise repository present")"
else
msg_info "$(translate "Ensuring ceph.sources (no-subscription, Trixie) is present...")"
create_ceph_repo_nosub_if_missing
msg_ok "$(translate "Ceph no-subscription repository present")"
fi
msg_info "$(translate "Commenting legacy ceph.list (if present)...")"
comment_legacy_ceph_list
msg_ok "$(translate "Legacy ceph.list commented or not present")"
fi
# ---------------------------
# Step 7
# ---------------------------
apt_update_with_repo_fallback
run_step \
"" \
"Repository verification completed (see log)" \
"apt policy >/dev/null 2>&1"
# ---------------------------
# Step 8
# ---------------------------
guard_against_proxmox_ve_removal
disable_translation_post_upgrade() {
translate() { echo "$1"; }
}
disable_translation_post_upgrade
# ==========================================================
# ---------------------------
# Step 9
# ---------------------------
if [[ "$ASSUME_YES" == "1" ]]; then
run_step \
"" \
"System upgraded to Trixie/PVE 9" \
"DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt-get -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' dist-upgrade"
else
run_step_interactive \
"" \
"System upgraded to Trixie/PVE 9" \
bash -lc "apt-get dist-upgrade"
fi
# ==========================================================
# ---------------------------
# Step 10
# ---------------------------
if [[ -d /sys/firmware/efi ]]; then
if [[ "$ASSUME_YES" == "1" ]]; then
append_step \
"Installing grub-efi-amd64" \
"grub-efi-amd64 installed or already present" \
"apt-get install -y grub-efi-amd64 >/dev/null 2>&1"
else
run_step \
"Installing grub-efi-amd64" \
"grub-efi-amd64 installed or already present" \
"apt-get install grub-efi-amd64 >/dev/null 2>&1"
fi
fi
run_pve8to9_check2() {
local tmp
tmp="$(mktemp)"
echo -e
set -o pipefail
pve8to9 --full 2>&1 | tee -a "$LOG" | tee "$tmp"
local rc=${PIPESTATUS[0]}
local fails warns
fails=$(grep -c 'FAIL:' "$tmp" || true)
warns=$(grep -c 'WARN:' "$tmp" || true)
if (( fails > 0 )); then
echo -e
echo -e "${BFR}${RD}[ERROR] $(translate "Pre-check found") $fails $(translate "blocking issue(s).")\n$(translate "Please resolve the problem(s) as described above, then re-run the upgrade script.")${CL}"
echo -e
local repair_commands=()
local repair_descriptions=()
# Error 1: systemd-boot meta-package
if grep -q 'systemd-boot meta-package installed' "$tmp"; then
repair_commands+=("apt remove -y systemd-boot")
repair_descriptions+=("$(translate "Remove obsolete systemd-boot meta-package")")
echo -e "${YW}$(translate "Fix systemd-boot:") ${CL}apt remove -y systemd-boot"
fi
# Error 2: Kernel version mismatch
if grep -q -E 'FAIL:.*(kernel.*mismatch|kernel.*version.*mismatch)' "$tmp"; then
repair_commands+=("update-grub")
repair_descriptions+=("$(translate "Update kernel to compatible version")")
echo -e "${YW}$(translate "Fix kernel version:") ${CL}update-grub"
fi
# Error 3: Ceph version incompatible
if grep -q -E '(ceph.*version|ceph.*incompatible)' "$tmp"; then
repair_commands+=("ceph versions && pveceph upgrade")
repair_descriptions+=("$(translate "Upgrade Ceph to compatible version")")
echo -e "${YW}$(translate "Fix Ceph version:") ${CL}ceph versions && pveceph upgrade"
fi
# Error 4: Repository configuration issues
if grep -q -E '(repository.*issue|repo.*problem|sources.*error)' "$tmp"; then
repair_commands+=("cleanup_duplicate_repos && ensure_repositories")
repair_descriptions+=("$(translate "Fix repository configuration")")
echo -e "${YW}$(translate "Fix repositories:") ${CL}cleanup_duplicate_repos && ensure_repositories"
fi
# Error 5: Package conflicts
if grep -q -E '(package.*conflict|dependency.*problem)' "$tmp"; then
repair_commands+=("apt update && apt autoremove -y && apt autoclean")
repair_descriptions+=("$(translate "Resolve package conflicts")")
echo -e "${YW}$(translate "Fix package conflicts:") ${CL}apt update && apt autoremove -y && apt autoclean"
fi
# Error 6: Disk space issues
if grep -q -E 'FAIL:.*(disk space|no space left|storage.*full)' "$tmp"; then
repair_commands+=("apt clean && apt autoremove -y && journalctl --vacuum-time=7d")
repair_descriptions+=("$(translate "Free up disk space")")
echo -e "${YW}$(translate "Fix disk space:") ${CL}apt clean && apt autoremove -y && journalctl --vacuum-time=7d"
fi
echo -e
if [[ ${#repair_commands[@]} -gt 0 ]]; then
echo -e "${BFR}${CY}$(translate "Repair Options:")${CL}"
echo -e "${TAB}${GN}1.${CL} $(translate "Try automatic repair of detected issues")"
echo -e "${TAB}${GN}2.${CL} $(translate "Show manual repair commands")"
echo -e
echo -n "$(translate "Select option [1-2] (default: 2): ")"
read -r repair_choice
case "$repair_choice" in
1)
echo -e
msg_info2 "$(translate "Attempting automatic repair...")"
local repair_success=0
for i in "${!repair_commands[@]}"; do
echo -e "${TAB}${YW}$(translate "Executing:") ${repair_descriptions[$i]}${CL}"
if eval "${repair_commands[$i]}"; then
msg_ok "${repair_descriptions[$i]} - $(translate "Success")"
else
msg_error "${repair_descriptions[$i]} - $(translate "Failed")"
repair_success=1
fi
done
if [[ $repair_success -eq 0 ]]; then
echo -e
msg_info2 "$(translate "Re-running pre-check after repairs...")"
sleep 2
run_pve8to9_check
return $?
else
echo -e
msg_error "$(translate "Some repairs failed. Please fix manually and re-run the script.")"
fi
;;
2)
echo -e
echo -e "$(translate "${BFR}${CY}Manual Repair Commands:${CL}")"
for i in "${!repair_commands[@]}"; do
echo -e "${TAB}${BL}# ${repair_descriptions[$i]}${CL}"
echo -e
echo -e "${TAB}${repair_commands[$i]}"
echo -e
done
echo -e
msg_info2 "$(translate "Once finished, re-run the script 'PVE 8 to 9 check' to verify that all issues are resolved \n before rebooting.")"
echo -e
msg_success "$(translate "Press Enter to exit the script after reading instructions...")"
read -r
rm -f "$tmp"
exit 1
;;
esac
fi
msg_success "$(translate "Press Enter to continue")"
read -r
fi
echo -e
msg_ok "$(translate "Checklist post-upgrade finished. Warnings:") $warns"
rm -f "$tmp"
return $rc
}
run_pve8to9_check2
# ---------------------------
echo
echo
echo
echo "════════════════════════════════════════════════════════════════"
echo " UPGRADE TO PVE 9 COMPLETED (reboot required) "
echo "════════════════════════════════════════════════════════════════"
echo
echo
echo
msg_success "Press Enter to continue..."
read -r
whiptail --title "Reboot Required" \
--yesno "It is RECOMMENDED to reboot now to load the new kernel and services.\n\nReboot now?" \
12 70
if [ $? -eq 0 ]; then
echo -e
msg_warn "Rebooting the system..."
echo -e
reboot
else
msg_info2 "You can reboot later manually."
echo -e
msg_success "Press Enter to exit"
read -r
fi
exit 0