Skip to content

Instantly share code, notes, and snippets.

@hypnguyen1209
Last active September 5, 2025 06:59
Show Gist options
  • Save hypnguyen1209/b5bf7f256a7581cbcccbd32b18d9038c to your computer and use it in GitHub Desktop.
Save hypnguyen1209/b5bf7f256a7581cbcccbd32b18d9038c to your computer and use it in GitHub Desktop.
Script to upgrade Proxmox Virtual Environment version 8.4 to the latest version 9
#!/usr/bin/env bash
set -Eeuo pipefail
PVE_REPO_MODE="${PVE_REPO_MODE:-no-subscription}" # no-subscription | enterprise
ENABLE_CEPH="${ENABLE_CEPH:-auto}" # auto | true | false
DPKG_KEEP_LOCAL="${DPKG_KEEP_LOCAL:-true}" # true | false
AUTO_REBOOT="${AUTO_REBOOT:-true}" # true | false
WORKDIR="${WORKDIR:-/root/pve8-to-9-upgrade}"
mkdir -p "$WORKDIR"
log() { echo -e "\e[1;32m[+] $*\e[0m"; }
warn(){ echo -e "\e[1;33m[!] $*\e[0m"; }
err() { echo -e "\e[1;31m[✗] $*\e[0m" >&2; }
require_root() {
if [[ $EUID -ne 0 ]]; then
err "Root required"
exit 1
fi
}
check_cmd() { command -v "$1" >/dev/null 2>&1; }
require_root
log "Check system requirements:"
pveversion -v | tee "$WORKDIR/pveversion-before.txt" || {
err "No pveversion found. Is this a PVE host?"
exit 1
}
uname -a | tee "$WORKDIR/uname-before.txt"
if check_cmd pvecm && pvecm status >/dev/null 2>&1; then
warn "Nodes belong to cluster. Upgrade EACH NODE, and migrate/roll back workloads accordingly."
pvecm status | tee "$WORKDIR/pvecm-status.txt" || true
fi
export DEBIAN_FRONTEND=noninteractive
apt-get update -y || true
apt-get -o Dpkg::Options::="--force-confold" full-upgrade -y
PVE_MGR_VER="$(pveversion | awk -F'[ /]' '/pve-manager/{print $2}')"
if ! dpkg --compare-versions "$PVE_MGR_VER" ge "8.4.1"; then
err "pve-manager is now $PVE_MGR_VER (< 8.4.1). Please update and run the script."
exit 1
fi
if ! check_cmd pve8to9; then
err "There is no pve8to9 command. Make sure PVE 8.4 is fully updated."
exit 1
fi
if ! pve8to9 --full | tee "$WORKDIR/pve8to9-full.txt"; then
warn "pve8to9 returned non-zero code - check log $WORKDIR/pve8to9-full.txt"
fi
if grep -qE '^(ERROR|FATAL):' "$WORKDIR/pve8to9-full.txt"; then
err "There is an ERROR in the pve8to9 checklist. Please fix it and run again. (See $WORKDIR/pve8to9-full.txt)"
exit 1
fi
USE_CEPH=false
if [[ "$ENABLE_CEPH" == "true" ]]; then
USE_CEPH=true
elif [[ "$ENABLE_CEPH" == "auto" ]]; then
if check_cmd ceph; then USE_CEPH=true; fi
fi
if $USE_CEPH; then
log "Detect Ceph. Check version (require Ceph 18.x Squid before upgrading PVE)."
if ceph --version | tee "$WORKDIR/ceph-version.txt" | grep -qE '\b15\b|\b16\b|\b17\b'; then
err "Ceph is not Squid 18.x yet. Upgrade Ceph to Squid 18.x first and then run again."
exit 1
fi
fi
tar czf "$WORKDIR/backup-config.tgz" \
/etc/pve \
/etc/apt \
/etc/network/interfaces \
/etc/hosts \
/etc/resolv.conf \
|| warn "Backup configuration encountered warning (continue)."
apt-get install -y --no-install-recommends \
proxmox-archive-keyring debian-archive-keyring ca-certificates
DEB_SOURCES_DIR="/etc/apt/sources.list.d"
DEB_SOURCES_FILE="$DEB_SOURCES_DIR/debian.sources"
mkdir -p "$DEB_SOURCES_DIR"
find /etc/apt -maxdepth 2 -type f -name '*.list' -print0 | while IFS= read -r -d '' f; do
if grep -qiE 'bookworm|debian|security|updates|backports' "$f"; then
cp -a "$f" "$f.bak.$(date +%F-%H%M%S)"
mv "$f" "$f.disabled"
echo "# moved to $f.disabled" > "$f"
fi
done
cat > "$DEB_SOURCES_FILE" <<'EOF'
Types: deb
URIs: https://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
URIs: https://security.debian.org/debian-security
Suites: trixie-security
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
# Proxmox repo (deb822)
log "Configure Proxmox repo (${PVE_REPO_MODE})."
PVE_SOURCES_FILE="$DEB_SOURCES_DIR/proxmox.sources"
case "$PVE_REPO_MODE" in
"enterprise")
cat > "$PVE_SOURCES_FILE" <<'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
;;
"no-subscription")
cat > "$PVE_SOURCES_FILE" <<'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
;;
*)
err "Invalid PVE_REPO_MODE: $PVE_REPO_MODE"
exit 1
;;
esac
find /etc/apt/sources.list.d -maxdepth 1 -type f -name 'pve-*.list' -o -name '*proxmox*.list' | while read -r f; do
cp -a "$f" "$f.bak.$(date +%F-%H%M%S)" || true
mv "$f" "$f.disabled" || true
done
if $USE_CEPH; then
CEPH_SOURCES_FILE="$DEB_SOURCES_DIR/ceph.sources"
if [[ "$PVE_REPO_MODE" == "enterprise" ]]; then
cat > "$CEPH_SOURCES_FILE" <<'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
else
cat > "$CEPH_SOURCES_FILE" <<'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
fi
if [[ -f /etc/apt/sources.list.d/ceph.list ]]; then
mv /etc/apt/sources.list.d/ceph.list{,.disabled}
fi
fi
if dpkg -l | awk '{print $2}' | grep -qx "linux-image-amd64"; then
warn "Detected linux-image-amd64 (installed on bare Debian). Removed to avoid meta-package conflicts."
apt-get remove -y linux-image-amd64 || true
fi
sed -i 's/^deb .*backports/# &/g' /etc/apt/sources.list || true
sed -i 's/^deb .*backports/# &/g' /etc/apt/sources.list.d/*.list 2>/dev/null || true
if grep -q 'glusterfs' /etc/pve/storage.cfg 2>/dev/null || dpkg -l | grep -qi gluster; then
err "GlusterFS detected in config/system. PVE 9 does not support it. Please migrate data and uninstall Gluster before upgrading."
exit 1
fi
apt-get update
apt-cache policy | tee "$WORKDIR/apt-policy.txt"
DPKG_OPT="--force-confold"
$DPKG_KEEP_LOCAL || DPKG_OPT="--force-confnew"
apt-get -o Dpkg::Options::="$DPKG_OPT" \
-o Dpkg::Options::="--force-confdef" \
dist-upgrade -y
pveversion -v | tee "$WORKDIR/pveversion-after.txt"
uname -r | tee "$WORKDIR/uname-after.txt"
if ! pveversion | grep -q "pve-manager/9."; then
warn "No pve-manager/9.x found (may need reboot to complete)."
fi
warn "After reboot: Ctrl+Shift+R to refresh Web UI; check HA Rules (PVE 9 replaces HA groups)."
if [[ "$AUTO_REBOOT" == "true" ]]; then
warn "Will reboot in 10 seconds... (CTRL+C to cancel)"
sleep 10
systemctl reboot
else
log "Upgrade complete. Manually reboot to run kernel 6.14 and finalize all changes."
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment