update 1.2.2.2 beta

This commit is contained in:
MacRimi
2026-07-02 23:52:04 +02:00
parent 153aae659c
commit c455d66b91
8 changed files with 643 additions and 11 deletions

View File

@@ -40,6 +40,9 @@ fi
: "${HB_RESTORE_INCLUDE_ZFS:=0}"
: "${HB_COMPAT_CROSS_VERSION:=0}"
: "${HB_KERNEL_PIN_ACTION:=none}"
: "${HB_KERNEL_PIN_KVER:=}"
: "${HB_KERNEL_PIN_PKG:=}"
if [[ ! -f "$APPLY_LIST" ]]; then
echo "Apply list missing: $APPLY_LIST"
@@ -47,10 +50,13 @@ if [[ ! -f "$APPLY_LIST" ]]; then
exit 1
fi
echo "Pending dir: $PENDING_DIR"
echo "Apply list: $APPLY_LIST"
echo "Include ZFS: $HB_RESTORE_INCLUDE_ZFS"
echo "Cross-version: $HB_COMPAT_CROSS_VERSION"
echo "Pending dir: $PENDING_DIR"
echo "Apply list: $APPLY_LIST"
echo "Include ZFS: $HB_RESTORE_INCLUDE_ZFS"
echo "Cross-version: $HB_COMPAT_CROSS_VERSION"
echo "Kernel pin act: $HB_KERNEL_PIN_ACTION"
echo "Kernel pin ver: $HB_KERNEL_PIN_KVER"
echo "Kernel pin pkg: $HB_KERNEL_PIN_PKG"
# Hardware-drift skips persisted by _rs_prepare_pending_restore.
# Each line is an absolute path; we drop any rel path that matches
@@ -304,6 +310,60 @@ UNITEOF
systemctl daemon-reload >/dev/null 2>&1 || true
systemctl enable proxmenux-apply-cluster-postboot.service >/dev/null 2>&1 || true
# ── Cross-kernel: install (if needed) + pin backup's kernel ──
# When the backup was taken on a different kernel major.minor
# than the target, the restore flow pre-computed a plan telling
# us whether the backup's kernel is already on the target or
# can be pulled from Proxmox repos. Acting here — during the
# onboot restore, before the cluster-postboot fires — gives us
# a controlled reboot into the pinned kernel, so the DKMS
# auto-reinstall in postboot builds every module against the
# right ABI (the kernel the operator's backup actually expects).
_kernel_pin_reboot_needed=0
if [[ "$HB_KERNEL_PIN_ACTION" != "none" && -n "$HB_KERNEL_PIN_KVER" ]]; then
echo ""
echo "── Cross-kernel: preparing kernel $HB_KERNEL_PIN_KVER ──"
if [[ "$HB_KERNEL_PIN_ACTION" == "installable" ]]; then
echo "Installing $HB_KERNEL_PIN_PKG from Proxmox repositories..."
apt-get update -qq 2>&1 | tail -5
if DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$HB_KERNEL_PIN_PKG" 2>&1 | tail -10; then
echo "$HB_KERNEL_PIN_PKG installed"
else
echo " ✗ apt install failed — pin will fall back to whatever kernels are present"
fi
fi
if command -v proxmox-boot-tool >/dev/null 2>&1; then
proxmox-boot-tool kernel pin "$HB_KERNEL_PIN_KVER" 2>&1 | sed 's/^/ pin: /'
proxmox-boot-tool refresh 2>&1 | sed 's/^/ refresh: /'
_kernel_pin_reboot_needed=1
else
echo " ! proxmox-boot-tool not available — kernel pin skipped"
fi
fi
if (( _kernel_pin_reboot_needed == 1 )); then
# Do NOT start the cluster-postboot service in this boot.
# We must reboot first so the kernel pin takes effect;
# once the machine is back up on the pinned kernel, the
# postboot service (enabled with a ConditionPathExists on
# cluster-apply-pending) will fire automatically and its
# DKMS reinstalls will target the correct kernel.
echo ""
echo "Rebooting into pinned kernel $HB_KERNEL_PIN_KVER..."
echo "completed" >"$STATE_FILE" 2>/dev/null || true
# Mark this restore as completed so the on-boot service
# doesn't re-fire after we reboot.
_rid="$(basename "$PENDING_DIR")"
mv "$PENDING_DIR" "${PENDING_BASE}/completed/${_rid}" >/dev/null 2>&1 || true
rm -f "$CURRENT_LINK" >/dev/null 2>&1 || true
systemctl disable proxmenux-restore-onboot.service >/dev/null 2>&1 || true
systemctl daemon-reload >/dev/null 2>&1 || true
sync
systemctl --no-block reboot
exit 0
fi
# Same-kernel path: fire cluster-postboot inline in this boot.
# `systemctl enable` only adds the unit to multi-user.target.wants/.
# It does NOT pull the unit into the currently-running boot
# transaction — by the time we run, multi-user.target may have

View File

@@ -2061,6 +2061,9 @@ CREATED_AT=${created_at}
HB_RESTORE_INCLUDE_ZFS=${HB_RESTORE_INCLUDE_ZFS:-0}
HB_ROLLBACK_EXECUTE=${HB_ROLLBACK_EXECUTE:-0}
HB_COMPAT_CROSS_VERSION=${HB_COMPAT_CROSS_VERSION:-0}
HB_KERNEL_PIN_ACTION=${HB_KERNEL_PIN_ACTION:-none}
HB_KERNEL_PIN_KVER=${HB_KERNEL_PIN_KVER:-}
HB_KERNEL_PIN_PKG=${HB_KERNEL_PIN_PKG:-}
EOF
# Persist hardware-drift skips so apply_pending_restore.sh can filter
# them at boot. The RS_SKIP_PATHS env var only lives in the restore
@@ -2226,6 +2229,15 @@ _rs_run_complete_guided() {
if [[ "${HB_COMPAT_CROSS_VERSION:-0}" == "1" ]]; then
local -a cv_skipped=()
local cv_line cv_path cv_reason cv_rel
# The drift block above trims the trailing newline off
# RS_SKIP_PATHS ("${skip_paths%$'\n'}"), so the first cv_path
# appended below would be pasted onto the previous drift path
# (bench-reproduced: `/etc/kernel/proxmox-boot-uuids/etc/default/grub`
# in rs-skip-paths.txt broke the match on /etc/default/grub,
# apply_pending_restore restored it from the backup, update-grub
# generated a boot config against the backup kernel and the
# host paniced on next reboot). Restore the missing separator.
[[ -n "$RS_SKIP_PATHS" && "${RS_SKIP_PATHS: -1}" != $'\n' ]] && RS_SKIP_PATHS+=$'\n'
while IFS=$'\t' read -r cv_path cv_reason; do
[[ -z "$cv_path" ]] && continue
cv_rel="${cv_path#/}"
@@ -2711,18 +2723,174 @@ _rs_run_complete_extras() {
rm -f "$cur_pkgs_file"
if [[ ${#missing[@]} -gt 0 ]]; then
echo
# Split into apt-known vs unknown so the install count
# announced below matches what apt-get will actually attempt.
local -a installable=() unknown=()
# ── Intelligent package filter (universal, not cross-version-only) ─
#
# `packages.manual.list` lists everything apt-marked as manual on
# the SOURCE host. A blind `apt install` of that list on the
# target can wreck it in two ways:
#
# 1. The backup pinned a package to an older SO-major (e.g.
# libzfs6linux from Proxmox 9.1) but the target already
# ships the newer sibling (libzfs7linux from 9.2). APT
# would resolve the older version by cascade-removing the
# newer one plus everything that depends on it
# (zfsutils-linux, zfs-initramfs, zfs-zed). The `zpool`
# binary vanishes → next reboot panics on rpool mount.
# Bench-reproduced on nvidia3 (9.2.3 / kernel 7.0.12) with
# an nvidia2 backup (9.1.9 / kernel 6.17.2).
#
# 2. Some packages are already present on the target because
# the fresh install included them — apt doesn't need us
# to re-request them, and re-requesting can pull config
# prompts we don't want during unattended restore.
#
# The filter runs three passes:
# (a) already_installed → dpkg -s says the pkg is on target
# (b) provided_by_newer → target already has libFOO<N>* where
# the backup's pkg is libFOO<M>* with
# M < N (SO-major bump between
# Proxmox releases)
# (c) cascade_risk → apt-get simulate proves the install
# would REMOVE something critical
#
# Each pass populates a report array. Nothing is silent — the
# operator sees the final tally with reasons.
local -a rep_skipped_installed=()
local -a rep_skipped_provided=()
local -a rep_skipped_cascade=()
local -a rep_unknown=()
local -a candidates=()
local pkg
for pkg in "${missing[@]}"; do
# Pass (a): already installed?
if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null \
| grep -q '^install ok installed$'; then
rep_skipped_installed+=("$pkg")
continue
fi
# Pass (b): provided by newer sibling? Only for
# `<base><N><suffix>` shaped names — the SO-major
# convention used by libzfs<N>linux, libzpool<N>linux,
# libnvpair<N>linux, libuutil<N>linux, libzstd<N>, etc.
if [[ "$pkg" =~ ^(lib[a-z]+)([0-9]+)([a-z]*[0-9]*)$ ]]; then
local _base="${BASH_REMATCH[1]}"
local _num="${BASH_REMATCH[2]}"
local _suffix="${BASH_REMATCH[3]}"
local _newer_found=""
# Only look up to +5 majors ahead — enough to cover any
# realistic Proxmox jump without probing forever.
local _n
for _n in $(seq $((_num + 1)) $((_num + 5))); do
local _sibling="${_base}${_n}${_suffix}"
if dpkg-query -W -f='${Status}' "$_sibling" 2>/dev/null \
| grep -q '^install ok installed$'; then
_newer_found="$_sibling"
break
fi
done
if [[ -n "$_newer_found" ]]; then
rep_skipped_provided+=("${pkg}|${_newer_found}")
continue
fi
fi
# Not skipped yet — needs the apt cache to know if it's
# installable at all. Unknown-to-apt goes to its own list.
if apt-cache show "$pkg" >/dev/null 2>&1; then
installable+=("$pkg")
candidates+=("$pkg")
else
unknown+=("$pkg")
rep_unknown+=("$pkg")
fi
done
# Pass (c): apt-simulate to catch cascade-remove risk. We
# simulate the full candidate set first; if apt would remove
# anything, iteratively drop the "first offender" (the pkg
# that appears earliest in the simulated remove path) and
# retry until the simulation is clean or the list is empty.
local -a installable=()
if (( ${#candidates[@]} > 0 )) && command -v apt-get >/dev/null 2>&1; then
local _work=("${candidates[@]}")
local _guard=0
while (( ${#_work[@]} > 0 && _guard < 20 )); do
_guard=$((_guard + 1))
local _sim_out
_sim_out=$(DEBIAN_FRONTEND=noninteractive \
apt-get install -y --simulate --no-install-recommends \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
"${_work[@]}" 2>&1)
# Look for removals: apt marks them as `Remv <pkg>...`
# in the plan output.
local -a _would_remove=()
mapfile -t _would_remove < <(printf '%s\n' "$_sim_out" \
| awk '/^Remv / {print $2}')
if (( ${#_would_remove[@]} == 0 )); then
installable=("${_work[@]}")
break
fi
# Pick the offender: a package in _work whose install
# is directly linked to the removal. Heuristic — if
# any _work package shares the base+suffix pattern
# with a would-remove package but a different major,
# that's the offender. Otherwise, drop the last one
# (least "essential" by convention of the list).
local _offender=""
local _rm
for _rm in "${_would_remove[@]}"; do
if [[ "$_rm" =~ ^(lib[a-z]+)([0-9]+)([a-z]*[0-9]*)$ ]]; then
local _rmbase="${BASH_REMATCH[1]}"
local _rmsuffix="${BASH_REMATCH[3]}"
local _cand
for _cand in "${_work[@]}"; do
if [[ "$_cand" =~ ^${_rmbase}[0-9]+${_rmsuffix}$ ]]; then
_offender="$_cand"; break
fi
done
fi
[[ -n "$_offender" ]] && break
done
if [[ -z "$_offender" ]]; then
# No obvious mapping — bail out safely: report
# everything as cascade risk and install nothing.
rep_skipped_cascade+=("${_work[@]/#/${_would_remove[0]}<-}")
_work=()
break
fi
rep_skipped_cascade+=("${_offender}|would remove ${_would_remove[*]:0:3}")
# Drop the offender and retry.
local -a _new_work=()
local _c
for _c in "${_work[@]}"; do
[[ "$_c" == "$_offender" ]] || _new_work+=("$_c")
done
_work=("${_new_work[@]}")
done
fi
local -a unknown=("${rep_unknown[@]}")
# ── Summarize what the filter decided ──
if (( ${#rep_skipped_installed[@]} > 0 )); then
local _prev="${rep_skipped_installed[*]:0:5}"
(( ${#rep_skipped_installed[@]} > 5 )) && _prev+=" … (+ $((${#rep_skipped_installed[@]} - 5)) more)"
echo -e "${TAB}${BGN}$(translate "Already installed — skipping"):${CL} ${BL}${_prev}${CL}"
fi
if (( ${#rep_skipped_provided[@]} > 0 )); then
echo -e "${TAB}${BGN}$(translate "Provided by newer version — skipping"):${CL}"
local _rec
for _rec in "${rep_skipped_provided[@]}"; do
echo -e "${TAB} ${DGN}${_rec%%|*}${CL}${BL}${_rec##*|}${CL}"
done
fi
if (( ${#rep_skipped_cascade[@]} > 0 )); then
echo -e "${TAB}${YWB}$(translate "Skipped to protect target system (would cascade-remove packages)"):${CL}"
local _rec
for _rec in "${rep_skipped_cascade[@]}"; do
echo -e "${TAB} ${YWB}${_rec%%|*}${CL}${_rec##*|}"
done
fi
if (( ${#installable[@]} > 0 )); then
local _preview="${installable[*]:0:6}"
(( ${#installable[@]} > 6 )) && _preview+=" … (+ $((${#installable[@]} - 6)) more)"
@@ -2902,6 +3070,40 @@ restore_menu() {
# (_rs_prompt_zfs_opt_in) read to choose between the
# silent same-host path and the loud cross-host path.
hb_compat_check "$staging_root"
# Cross-kernel restore requires the backup's kernel to be
# bootable on the target — either already installed or
# obtainable from the Proxmox repositories. When it's
# neither, refuse cleanly: continuing would leave the
# operator with drivers built against a kernel that isn't
# present, and DKMS would silently pin the wrong ABI.
if [[ "${HB_COMPAT_CROSS_VERSION:-0}" == "1" ]]; then
local _bk_kernel=""
if [[ -f "$staging_root/metadata/run_info.env" ]]; then
_bk_kernel=$(grep -m1 '^kernel=' "$staging_root/metadata/run_info.env" 2>/dev/null | cut -d= -f2-)
fi
if [[ -n "$_bk_kernel" ]]; then
hb_kernel_pin_plan "$_bk_kernel"
if [[ "$HB_KERNEL_PIN_ACTION" == "unavailable" ]]; then
# Log so the operator can inspect after the fact.
local _refuse_log="/var/log/proxmenux/restore-refused-$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$(dirname "$_refuse_log")" 2>/dev/null
{
echo "Refused at: $(date -Iseconds)"
echo "Backup kernel: $_bk_kernel"
echo "Target running: $(uname -r)"
echo "Reason: $HB_KERNEL_PIN_REASON"
} > "$_refuse_log"
dialog --backtitle "ProxMenux" --colors \
--title "$(translate "Restore refused — incompatible kernel")" \
--msgbox "\Zb\Z1$(translate "This backup was taken on kernel"):\Zn \Zb${_bk_kernel}\ZB"$'\n\n'"$(translate "That kernel is") ${HB_KERNEL_PIN_REASON}."$'\n\n'"$(translate "Restoring without a matching kernel would leave drivers and modules built against a kernel that is not present on this host — the system may fail to boot or run degraded.")"$'\n\n'"\Zb$(translate "How to proceed"):\ZB"$'\n'"$(translate "Install Proxmox VE from an ISO whose kernel matches")"$'\n'"$(translate "Or fetch the kernel manually:") apt install proxmox-kernel-${_bk_kernel}"$'\n'"$(translate "Then relaunch the restore.")"$'\n\n'"$(translate "Details logged at"): $_refuse_log" \
22 84
rm -rf "$staging_root"
continue
fi
fi
fi
if hb_show_compat_report; then
if _rs_apply_menu "$staging_root"; then
rm -rf "$staging_root"

View File

@@ -2711,6 +2711,85 @@ hb_apply_nic_remaps() {
# Output: one line per path, tab-separated: <abs_path>\t<reason>
# The reason is a short label used verbatim in the confirm dialog.
# ==========================================================
# ==========================================================
# KERNEL PIN PLAN — cross-kernel restore only
# ==========================================================
# When the backup was taken on a kernel major.minor different
# from the target's running kernel, the safe and universally
# correct action is to boot the target with the backup's kernel
# after the restore. That way every driver / DKMS module that
# the backup expects (nvidia, coral, intel_gpu, plus every
# out-of-tree ZFS build) targets a kernel that actually exists
# and matches, and the auto-reinstall on post-boot builds them
# against the right ABI. Universal: works for ZFS root, LVM/ext4
# root, BIOS or UEFI hosts alike.
#
# This function decides how to obtain that kernel on the target
# and publishes the plan for the restore flow to act on.
#
# Input:
# $1 bk_kernel — kernel release string from the backup
# (e.g. "6.17.2-1-pve")
# Output (env vars, all exported):
# HB_KERNEL_PIN_ACTION installed | installable | unavailable
# HB_KERNEL_PIN_PKG candidate package name to install (empty
# when installed / unavailable)
# HB_KERNEL_PIN_KVER the kernel release to pin (same as $1)
# Also fills HB_KERNEL_PIN_REASON with a short human message
# when action is "unavailable" (for the refuse dialog).
# ==========================================================
hb_kernel_pin_plan() {
local bk_kernel="$1"
export HB_KERNEL_PIN_ACTION="unavailable"
export HB_KERNEL_PIN_PKG=""
export HB_KERNEL_PIN_KVER="$bk_kernel"
export HB_KERNEL_PIN_REASON=""
[[ -z "$bk_kernel" ]] && {
HB_KERNEL_PIN_REASON="backup did not record its kernel release"
return 0
}
# Signed package name convention on modern Proxmox: the actual
# binary kernel image ships as `proxmox-kernel-<ver>-signed`, the
# unsigned build is `proxmox-kernel-<ver>` (only in special cases).
# `pve-kernel-<ver>` was the legacy name pre-Proxmox 8; try both
# so this works whether the backup came from an older PVE.
local -a candidates=(
"proxmox-kernel-${bk_kernel}-signed"
"proxmox-kernel-${bk_kernel}"
"pve-kernel-${bk_kernel}"
)
local pkg
for pkg in "${candidates[@]}"; do
# Already installed → nothing to fetch, just pin later.
if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null \
| grep -q '^install ok installed$'; then
HB_KERNEL_PIN_ACTION="installed"
HB_KERNEL_PIN_PKG="$pkg"
return 0
fi
done
# Not installed. Refresh apt metadata quietly, then look each
# candidate up. `apt-cache policy` shows a non-empty "Candidate"
# only when there IS an installable version in the enabled repos.
apt-get update -qq >/dev/null 2>&1 || true
for pkg in "${candidates[@]}"; do
local cand
cand=$(apt-cache policy "$pkg" 2>/dev/null \
| awk '/Candidate:/ {print $2; exit}')
if [[ -n "$cand" && "$cand" != "(none)" ]]; then
HB_KERNEL_PIN_ACTION="installable"
HB_KERNEL_PIN_PKG="$pkg"
return 0
fi
done
HB_KERNEL_PIN_ACTION="unavailable"
HB_KERNEL_PIN_REASON="not installed on this host and not available in the configured Proxmox repositories"
return 0
}
hb_unsafe_paths_cross_version() {
cat <<'EOF'
/etc/default/grub GRUB defaults tied to prior kernel order

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# ==========================================================
# kernel_pin_plan.sh — JSON reporter for cross-kernel restore
# ==========================================================
# Usage: bash kernel_pin_plan.sh <staging_root>
# Output: JSON on stdout — one of:
#
# {"detected":false}
# Backup + target run the same kernel major.minor.
# No action needed.
#
# {"detected":true,
# "backup_kernel":"6.17.2-1-pve",
# "target_kernel":"7.0.12-1-pve",
# "action":"installed",
# "pkg":"proxmox-kernel-6.17.2-1-pve-signed"}
# Cross-kernel. The backup's kernel is already installed on the
# target; the restore flow will pin it.
#
# {"detected":true, ...,
# "action":"installable",
# "pkg":"proxmox-kernel-6.17.2-1-pve-signed"}
# Cross-kernel. The kernel is available in Proxmox repos and
# will be installed before the pin.
#
# {"detected":true, ...,
# "action":"unavailable",
# "reason":"not installed on this host and not available in the
# configured Proxmox repositories"}
# Cross-kernel. The kernel is neither installed nor obtainable.
# The restore flow must refuse.
#
# The heavy lifting is delegated to hb_kernel_pin_plan in
# lib_host_backup_common.sh, so the CLI menu and the Web Monitor
# agree byte-for-byte on which restores are safe to start.
# ==========================================================
set -euo pipefail
staging_root="${1:-}"
[[ -z "$staging_root" || ! -d "$staging_root" ]] && {
printf '{"detected":false,"error":"staging_root missing"}\n'
exit 0
}
LIB="/usr/local/share/proxmenux/scripts/backup_restore/lib_host_backup_common.sh"
if [[ ! -f "$LIB" ]]; then
printf '{"detected":false,"error":"lib_host_backup_common.sh not found"}\n'
exit 0
fi
# shellcheck source=/dev/null
source "$LIB"
# Read the backup's kernel release.
bk_kernel=""
if [[ -f "$staging_root/metadata/run_info.env" ]]; then
bk_kernel=$(grep -m1 '^kernel=' "$staging_root/metadata/run_info.env" 2>/dev/null \
| cut -d= -f2-)
fi
cur_kernel=$(uname -r 2>/dev/null || echo "")
if [[ -z "$bk_kernel" || -z "$cur_kernel" ]]; then
printf '{"detected":false}\n'
exit 0
fi
# Compare major.minor — the same rule hb_compat_check uses.
bk_mm=$(echo "$bk_kernel" | cut -d. -f1-2)
cur_mm=$(echo "$cur_kernel" | cut -d. -f1-2)
if [[ "$bk_mm" == "$cur_mm" ]]; then
printf '{"detected":false}\n'
exit 0
fi
# Cross-kernel: compute the pin plan.
hb_kernel_pin_plan "$bk_kernel"
# Emit JSON. jq if we have it (safer with special chars), else printf.
if command -v jq >/dev/null 2>&1; then
jq -cn \
--arg bkk "$bk_kernel" \
--arg trk "$cur_kernel" \
--arg act "${HB_KERNEL_PIN_ACTION:-unavailable}" \
--arg pkg "${HB_KERNEL_PIN_PKG:-}" \
--arg rsn "${HB_KERNEL_PIN_REASON:-}" \
'{detected:true, backup_kernel:$bkk, target_kernel:$trk,
action:$act, pkg:$pkg, reason:$rsn}'
else
printf '{"detected":true,"backup_kernel":"%s","target_kernel":"%s","action":"%s","pkg":"%s","reason":"%s"}\n' \
"$bk_kernel" "$cur_kernel" \
"${HB_KERNEL_PIN_ACTION:-unavailable}" \
"${HB_KERNEL_PIN_PKG:-}" \
"${HB_KERNEL_PIN_REASON:-}"
fi