mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-29 11:58:25 +00:00
update 1.2.2.2 beta
This commit is contained in:
@@ -1586,10 +1586,18 @@ def internal_restore_event():
|
||||
stale_nodes = data.get('stale_nodes', '0')
|
||||
components = data.get('components', 'none')
|
||||
duration = data.get('duration', 'unknown')
|
||||
# Boot sanity-check warnings surfaced by apply_cluster_postboot.sh.
|
||||
# Empty on a clean restore; populated on the cross-version path
|
||||
# when the sanity check found something the operator should know
|
||||
# about (missing /lib/modules for the default kernel, no ESP
|
||||
# configured, dangling /vmlinuz, ...).
|
||||
warnings = data.get('warnings', '').strip()
|
||||
severity = 'WARNING' if warnings else 'INFO'
|
||||
warnings_block = f'\n⚠️ Boot sanity: {warnings}\n' if warnings else ''
|
||||
|
||||
notification_manager.emit_event(
|
||||
event_type='system_restore_completed',
|
||||
severity='INFO',
|
||||
severity=severity,
|
||||
data={
|
||||
'hostname': hostname,
|
||||
'guests': guests,
|
||||
@@ -1597,6 +1605,8 @@ def internal_restore_event():
|
||||
'stale_nodes': stale_nodes,
|
||||
'components': components,
|
||||
'duration': duration,
|
||||
'warnings': warnings,
|
||||
'warnings_block': warnings_block,
|
||||
},
|
||||
source='proxmenux',
|
||||
entity='node',
|
||||
|
||||
@@ -9576,7 +9576,10 @@ def api_smart_run_test(disk_name):
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if check_proc.returncode != 0:
|
||||
return jsonify({'error': f'Cannot access NVMe device: {check_proc.stderr.strip() or "Device not responding"}'}), 500
|
||||
# Device-level failure, not a server bug — return 400 so
|
||||
# the UI surfaces the real stderr instead of a generic
|
||||
# "500 INTERNAL SERVER ERROR".
|
||||
return jsonify({'error': f'Cannot access NVMe device: {check_proc.stderr.strip() or "Device not responding"}'}), 400
|
||||
|
||||
# Check if device supports self-test by looking at OACS field
|
||||
# OACS bit 4 (0x10) indicates Device Self-test support
|
||||
@@ -9616,8 +9619,8 @@ def api_smart_run_test(disk_name):
|
||||
# Check for permission errors
|
||||
if 'permission' in error_msg.lower() or 'operation not permitted' in error_msg.lower():
|
||||
return jsonify({'error': f'Permission denied. Run as root: {error_msg}'}), 403
|
||||
return jsonify({'error': f'Failed to start test: {error_msg}'}), 500
|
||||
|
||||
return jsonify({'error': f'Failed to start test: {error_msg}'}), 400
|
||||
|
||||
# Start background monitor to save JSON when test completes
|
||||
# Check 'Current Device Self-Test Operation' field - if > 0, test is running
|
||||
sleep_interval = 10 if test_type == 'short' else 60
|
||||
@@ -9648,7 +9651,21 @@ def api_smart_run_test(disk_name):
|
||||
)
|
||||
|
||||
if proc.returncode not in (0, 4): # 4 = test started successfully
|
||||
return jsonify({'error': f'Failed to start test: {proc.stderr}'}), 500
|
||||
# smartctl scribbles the useful diagnostic on stderr for
|
||||
# most failures but some device-level errors (USB SATA
|
||||
# bridges that reject the SMART command, drives whose
|
||||
# ATA passthrough is broken, ...) print the reason on
|
||||
# stdout instead. Concatenate both so the operator sees
|
||||
# the real reason in the toast rather than an opaque
|
||||
# "500 INTERNAL SERVER ERROR". Status is 400: the smartctl
|
||||
# process ran fine, the device is what rejected the test.
|
||||
err_detail = (proc.stderr or '').strip()
|
||||
out_detail = (proc.stdout or '').strip()
|
||||
combined = err_detail
|
||||
if out_detail and out_detail not in err_detail:
|
||||
combined = f'{err_detail}\n{out_detail}' if err_detail else out_detail
|
||||
combined = combined or f'smartctl exited with code {proc.returncode}'
|
||||
return jsonify({'error': f'Failed to start test: {combined}'}), 400
|
||||
|
||||
# Start background monitor to save JSON when test completes
|
||||
sleep_interval = 10 if test_type == 'short' else 60
|
||||
|
||||
@@ -901,7 +901,16 @@ TEMPLATES = {
|
||||
},
|
||||
'system_restore_completed': {
|
||||
'title': '{hostname}: Host restore finished',
|
||||
'body': 'Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n\nThe node is now fully ready to use.',
|
||||
'body': (
|
||||
'Post-restore tasks completed in background.\n\n'
|
||||
'Guests applied: {guests}\n'
|
||||
'Bind-mount stubs: {stubs}\n'
|
||||
'Stale node dirs removed: {stale_nodes}\n'
|
||||
'Components reinstalled: {components}\n'
|
||||
'Duration: {duration}\n'
|
||||
'{warnings_block}\n'
|
||||
'The node is now fully ready to use.'
|
||||
),
|
||||
'label': 'Host restore completed',
|
||||
'group': 'services',
|
||||
'default_enabled': True,
|
||||
|
||||
@@ -412,6 +412,49 @@ if [[ -n "$ROLLBACK_PLAN_FILE" ]] && command -v jq >/dev/null 2>&1; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Boot sanity check ──────────────────────────────────────────
|
||||
# Cross-version restores are the most likely path to a broken
|
||||
# boot: even with the safe-restore filter, catch situations where
|
||||
# the default kernel has no matching /lib/modules, no ESP is
|
||||
# configured, or a stale reference survived. Runs on every restore
|
||||
# (cheap); warnings feed the completion notification so it tells
|
||||
# the truth instead of a blanket "all fine".
|
||||
SANITY_WARNINGS=""
|
||||
_sanity_warn() {
|
||||
if [[ -z "$SANITY_WARNINGS" ]]; then
|
||||
SANITY_WARNINGS="$1"
|
||||
else
|
||||
SANITY_WARNINGS="${SANITY_WARNINGS}; $1"
|
||||
fi
|
||||
}
|
||||
|
||||
if command -v proxmox-boot-tool >/dev/null 2>&1; then
|
||||
if ! proxmox-boot-tool status 2>/dev/null | grep -q 'configured with'; then
|
||||
_sanity_warn "proxmox-boot-tool reports no ESP configured"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -d /boot ]]; then
|
||||
for _vmlinuz in /boot/vmlinuz-*; do
|
||||
[[ -e "$_vmlinuz" ]] || continue
|
||||
_kver="${_vmlinuz##*vmlinuz-}"
|
||||
if [[ ! -d "/lib/modules/$_kver" ]]; then
|
||||
_sanity_warn "kernel $_kver has no /lib/modules"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -L /vmlinuz && ! -e /vmlinuz ]]; then
|
||||
_sanity_warn "/vmlinuz symlink is dangling"
|
||||
fi
|
||||
|
||||
if [[ -n "$SANITY_WARNINGS" ]]; then
|
||||
echo ""
|
||||
echo "── Boot sanity check ──"
|
||||
echo "$SANITY_WARNINGS"
|
||||
echo "Cross-version: ${HB_COMPAT_CROSS_VERSION:-0}"
|
||||
fi
|
||||
|
||||
# ── Notify ProxMenux Monitor that we're done ───────────────────
|
||||
# Routes through the user's configured channels (Telegram, Discord,
|
||||
# ntfy, etc.). Localhost-only endpoint, no auth needed. We try
|
||||
@@ -429,13 +472,31 @@ if command -v jq >/dev/null 2>&1 && [[ -f "$COMPONENTS_STATUS" ]]; then
|
||||
fi
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
PAYLOAD=$(printf '{"hostname":"%s","guests":"%s","stubs":"%s","stale_nodes":"%s","components":"%s","duration":"%s"}' \
|
||||
"$(hostname)" \
|
||||
"${copied_guests:-0}" \
|
||||
"${stub_created:-0}" \
|
||||
"${removed_nodes:-0}" \
|
||||
"${COMPONENTS_REINSTALLED_CSV:-none}" \
|
||||
"$POSTBOOT_DURATION_FMT")
|
||||
# jq builds a proper JSON when available so SANITY_WARNINGS with
|
||||
# special chars can't break the payload. Falls back to printf on
|
||||
# hosts without jq — we already restrict SANITY_WARNINGS content
|
||||
# to plain ASCII in the sanity check above, so the fallback is
|
||||
# safe too.
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
PAYLOAD=$(jq -cn \
|
||||
--arg hostname "$(hostname)" \
|
||||
--arg guests "${copied_guests:-0}" \
|
||||
--arg stubs "${stub_created:-0}" \
|
||||
--arg stale_nodes "${removed_nodes:-0}" \
|
||||
--arg components "${COMPONENTS_REINSTALLED_CSV:-none}" \
|
||||
--arg duration "$POSTBOOT_DURATION_FMT" \
|
||||
--arg warnings "$SANITY_WARNINGS" \
|
||||
'{hostname:$hostname, guests:$guests, stubs:$stubs, stale_nodes:$stale_nodes, components:$components, duration:$duration, warnings:$warnings}')
|
||||
else
|
||||
PAYLOAD=$(printf '{"hostname":"%s","guests":"%s","stubs":"%s","stale_nodes":"%s","components":"%s","duration":"%s","warnings":"%s"}' \
|
||||
"$(hostname)" \
|
||||
"${copied_guests:-0}" \
|
||||
"${stub_created:-0}" \
|
||||
"${removed_nodes:-0}" \
|
||||
"${COMPONENTS_REINSTALLED_CSV:-none}" \
|
||||
"$POSTBOOT_DURATION_FMT" \
|
||||
"$SANITY_WARNINGS")
|
||||
fi
|
||||
NOTIFY_HTTP=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
-X POST "http://127.0.0.1:8008/api/internal/restore-event" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
@@ -39,6 +39,7 @@ if [[ -f "$PLAN_ENV" ]]; then
|
||||
fi
|
||||
|
||||
: "${HB_RESTORE_INCLUDE_ZFS:=0}"
|
||||
: "${HB_COMPAT_CROSS_VERSION:=0}"
|
||||
|
||||
if [[ ! -f "$APPLY_LIST" ]]; then
|
||||
echo "Apply list missing: $APPLY_LIST"
|
||||
@@ -46,9 +47,10 @@ 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 "Pending dir: $PENDING_DIR"
|
||||
echo "Apply list: $APPLY_LIST"
|
||||
echo "Include ZFS: $HB_RESTORE_INCLUDE_ZFS"
|
||||
echo "Cross-version: $HB_COMPAT_CROSS_VERSION"
|
||||
|
||||
# Hardware-drift skips persisted by _rs_prepare_pending_restore.
|
||||
# Each line is an absolute path; we drop any rel path that matches
|
||||
@@ -60,7 +62,12 @@ RS_SKIP_PATHS=""
|
||||
SKIP_PATHS_FILE="$PENDING_DIR/rs-skip-paths.txt"
|
||||
if [[ -f "$SKIP_PATHS_FILE" ]]; then
|
||||
RS_SKIP_PATHS=$(cat "$SKIP_PATHS_FILE")
|
||||
echo "Skip paths: $(wc -l <"$SKIP_PATHS_FILE") entries (drift)"
|
||||
# Skips come from two sources today: hardware drift and
|
||||
# cross-version safe restore. The label is generic so the log
|
||||
# is truthful without having to reconcile the two upstream.
|
||||
_skip_source_label="drift"
|
||||
[[ "${HB_COMPAT_CROSS_VERSION:-0}" == "1" ]] && _skip_source_label="drift + cross-version"
|
||||
echo "Skip paths: $(wc -l <"$SKIP_PATHS_FILE") entries (${_skip_source_label})"
|
||||
fi
|
||||
|
||||
echo "running" >"$STATE_FILE"
|
||||
@@ -265,6 +272,7 @@ EOF
|
||||
printf 'PENDING_DIR=%s\n' "$PENDING_DIR"
|
||||
printf 'NEEDS_INITRAMFS=%s\n' "$NEEDS_INITRAMFS"
|
||||
printf 'NEEDS_GRUB=%s\n' "$NEEDS_GRUB"
|
||||
printf 'HB_COMPAT_CROSS_VERSION=%s\n' "${HB_COMPAT_CROSS_VERSION:-0}"
|
||||
} > /var/lib/proxmenux/cluster-apply-pending
|
||||
chmod 600 /var/lib/proxmenux/cluster-apply-pending
|
||||
|
||||
|
||||
@@ -2057,6 +2057,7 @@ RESTORE_ID=${restore_id}
|
||||
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}
|
||||
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
|
||||
@@ -2179,6 +2180,72 @@ _rs_run_complete_guided() {
|
||||
(( dialog_signal == 1 )) && RS_DRIFT_SUMMARY="$plan_body"
|
||||
fi
|
||||
|
||||
# ── NIC remap (silent, staging_root) ─────────────────────
|
||||
# If hb_plan_nic_remaps found NIC(s) with the same MAC as the
|
||||
# backup but a different ifname on the target (motherboard
|
||||
# swap that shifted PCI addresses), rewrite the staging config
|
||||
# in-place so the restored /etc/network/interfaces references
|
||||
# the ifname that actually exists on this host. Also builds
|
||||
# a small summary block for the confirm dialog — informative
|
||||
# only, no yes/no.
|
||||
RS_NIC_REMAP_SUMMARY=""
|
||||
if (( ${#HB_NIC_REMAP[@]:-0} > 0 || ${#HB_NIC_MAC_CHANGED[@]:-0} > 0 )); then
|
||||
local nr_body entry old_if new_if nic_mac old_mac
|
||||
nr_body="\Zb$(translate "NIC changes detected — adjusted automatically")\ZB"$'\n\n'
|
||||
for entry in "${HB_NIC_REMAP[@]}"; do
|
||||
IFS='|' read -r old_if new_if nic_mac <<<"$entry"
|
||||
nr_body+=" \Z4•\Zn $(translate "Renamed"): \Zb${old_if}\ZB → \Zb${new_if}\ZB ($(translate "same MAC"): ${nic_mac})"$'\n'
|
||||
done
|
||||
for entry in "${HB_NIC_MAC_CHANGED[@]}"; do
|
||||
IFS='|' read -r new_if old_mac nic_mac <<<"$entry"
|
||||
nr_body+=" \Z4•\Zn \Zb${new_if}\ZB $(translate "has a new MAC"): ${nic_mac} ($(translate "was") ${old_mac})"$'\n'
|
||||
nr_body+=" $(translate "If your DHCP has a static reservation for the old MAC, update it.")"$'\n'
|
||||
done
|
||||
RS_NIC_REMAP_SUMMARY="$nr_body"
|
||||
hb_apply_nic_remaps "$staging_root"
|
||||
fi
|
||||
|
||||
# ── Cross-version safe-restore filter ────────────────────
|
||||
# When the backup was taken on a different PVE major or a
|
||||
# different kernel major.minor, restoring boot/kernel/apt
|
||||
# config on top of the current install is what causes the
|
||||
# post-reboot kernel panic. Fold those paths into RS_SKIP_PATHS
|
||||
# so the same downstream machinery that already honours skips
|
||||
# (_rs_apply, _rs_collect_pending_paths, apply_pending_restore)
|
||||
# keeps them out of the restore.
|
||||
RS_CROSS_VERSION_SKIPS=""
|
||||
if [[ "${HB_COMPAT_CROSS_VERSION:-0}" == "1" ]]; then
|
||||
local -a cv_skipped=()
|
||||
local cv_line cv_path cv_reason cv_rel
|
||||
while IFS=$'\t' read -r cv_path cv_reason; do
|
||||
[[ -z "$cv_path" ]] && continue
|
||||
cv_rel="${cv_path#/}"
|
||||
# Only skip when the backup actually carries the path —
|
||||
# otherwise the "excluded" line is misleading.
|
||||
if [[ -e "$staging_root/rootfs/$cv_rel" ]]; then
|
||||
cv_skipped+=("${cv_path}"$'\t'"${cv_reason}")
|
||||
RS_SKIP_PATHS+="${cv_path}"$'\n'
|
||||
fi
|
||||
done < <(hb_unsafe_paths_cross_version)
|
||||
|
||||
if (( ${#cv_skipped[@]} > 0 )); then
|
||||
local cv_body
|
||||
cv_body="\Zb$(translate "Cross-version detected — safe restore mode")\ZB"$'\n\n'
|
||||
cv_body+="$(translate "The backup was taken on a different PVE or kernel major.minor. These paths will be SKIPPED to keep the boot safe:")"$'\n\n'
|
||||
for cv_line in "${cv_skipped[@]}"; do
|
||||
cv_path="${cv_line%%$'\t'*}"
|
||||
cv_reason="${cv_line#*$'\t'}"
|
||||
cv_body+=" \Z1•\Zn \Zb${cv_path}\ZB"$'\n'
|
||||
cv_body+=" $(translate "${cv_reason}")"$'\n\n'
|
||||
done
|
||||
RS_CROSS_VERSION_SKIPS="$cv_body"
|
||||
fi
|
||||
# Trim any leading duplicate newlines the concat may introduce.
|
||||
RS_SKIP_PATHS="${RS_SKIP_PATHS#$'\n'}"
|
||||
RS_SKIP_PATHS="${RS_SKIP_PATHS%$'\n'}"
|
||||
export RS_SKIP_PATHS
|
||||
fi
|
||||
|
||||
# Build the rich confirmation body. Replaces the previous 4-strategy
|
||||
# menu — by design a Proxmox host restore always requires a reboot
|
||||
# for predictable end state (pmxcfs live writes + initramfs + driver
|
||||
@@ -2222,6 +2289,13 @@ _rs_run_complete_guided() {
|
||||
body+=" • \Zb${label}\ZB (${eta})"$'\n'
|
||||
done
|
||||
fi
|
||||
# Silent NIC remap block first (informational, no action needed
|
||||
# from the operator — the staging config has already been
|
||||
# rewritten). Kept above drift/cross-version because it's the
|
||||
# least alarming and the most common on hardware refreshes.
|
||||
if [[ -n "${RS_NIC_REMAP_SUMMARY:-}" ]]; then
|
||||
body+=$'\n'"${RS_NIC_REMAP_SUMMARY}"
|
||||
fi
|
||||
# If smart restore flagged drift skips earlier, surface them here
|
||||
# so the operator sees everything in one screen instead of two
|
||||
# consecutive yes/no popups.
|
||||
@@ -2233,6 +2307,12 @@ _rs_run_complete_guided() {
|
||||
_drift_bullets=$(printf '%s\n' "$RS_DRIFT_SUMMARY" | sed -n '/\Z1•\Zn/,$p')
|
||||
body+="$_drift_bullets"$'\n'
|
||||
fi
|
||||
# Same treatment for cross-version skips (PVE major or kernel
|
||||
# major.minor differ between backup and target). The stashed
|
||||
# body already carries its own header — merge it as-is.
|
||||
if [[ -n "${RS_CROSS_VERSION_SKIPS:-}" ]]; then
|
||||
body+=$'\n'"${RS_CROSS_VERSION_SKIPS}"
|
||||
fi
|
||||
body+=$'\n'"\Zb\Z4$(translate "A reboot is required to finish the restore.")\Zn"$'\n\n'
|
||||
body+="$(translate "If notifications are enabled (Telegram/Discord/ntfy/...), you will receive a \"Host restore finished\" message when all background tasks complete.")"$'\n\n'
|
||||
body+="\Zb$(translate "Continue?")\ZB"
|
||||
@@ -2799,6 +2879,13 @@ restore_menu() {
|
||||
esac
|
||||
|
||||
if [[ $ok -eq 1 ]] && _rs_check_layout "$staging_root"; then
|
||||
# Plan NIC remaps FIRST so hb_compat_check knows which
|
||||
# "missing" NICs are actually renames (same MAC, new
|
||||
# ifname after a motherboard swap) and can downgrade
|
||||
# them from FAIL to INFO. Also fills HB_NIC_MAC_CHANGED
|
||||
# for same-name-different-MAC hosts.
|
||||
hb_plan_nic_remaps "$staging_root"
|
||||
|
||||
# Run the compatibility check BEFORE the apply menu so
|
||||
# the operator sees PVE-version / hostname / network /
|
||||
# storage drift up front. This also sets
|
||||
|
||||
@@ -2539,10 +2539,148 @@ hb_ensure_pv() {
|
||||
# storage / network / VMID drift BEFORE the apply menu opens.
|
||||
#
|
||||
# After running hb_compat_check, the caller can read:
|
||||
# HB_COMPAT_SAME_HOST → 1 if backup's hostname matches current
|
||||
# HB_COMPAT_ANY_FAIL → 1 if at least one FAIL was raised
|
||||
# HB_COMPAT_ANY_WARN → 1 if at least one WARN was raised
|
||||
# HB_COMPAT_RESULTS[] → array of "STATUS|category|message" entries
|
||||
# ==========================================================
|
||||
# NIC REMAP PLAN + APPLY
|
||||
# ==========================================================
|
||||
# Reconciles NICs recorded in the backup manifest against the
|
||||
# live host. Covers the "motherboard swap" scenario: same NIC
|
||||
# chip lands on a different PCI slot after the swap, so the
|
||||
# ifname changes (enp0s31f6 → enp0s25) even though the MAC is
|
||||
# the same. Without a remap the restored /etc/network/interfaces
|
||||
# references a name that no longer exists → no network at boot.
|
||||
#
|
||||
# Populates arrays (only when jq + manifest are available):
|
||||
# HB_NIC_REMAP[i]="<old_ifname>|<new_ifname>|<mac>"
|
||||
# — same MAC, different ifname. Rewrites the staging config
|
||||
# so the restored files use <new_ifname>.
|
||||
# HB_NIC_ORPHAN[i]="<old_ifname>|<mac>"
|
||||
# — MAC from the backup not present on target at all.
|
||||
# Real hardware removal, left to hb_compat_check to
|
||||
# report as WARN/FAIL depending on wired-ness.
|
||||
# HB_NIC_MAC_CHANGED[i]="<ifname>|<old_mac>|<new_mac>"
|
||||
# — same ifname on target, but MAC differs. Motherboard
|
||||
# replacement with same PCI layout but different NIC chip.
|
||||
# Boot succeeds; surfaces INFO so the operator can update
|
||||
# any DHCP static reservation that keyed on the old MAC.
|
||||
# ==========================================================
|
||||
hb_plan_nic_remaps() {
|
||||
local staging_root="$1"
|
||||
HB_NIC_REMAP=()
|
||||
HB_NIC_ORPHAN=()
|
||||
HB_NIC_MAC_CHANGED=()
|
||||
|
||||
local manifest="$staging_root/manifest.json"
|
||||
[[ -f "$manifest" ]] || return 0
|
||||
command -v jq >/dev/null 2>&1 || return 0
|
||||
|
||||
declare -A _dest_mac_by_if=()
|
||||
declare -A _dest_if_by_mac=()
|
||||
local dev_path _if _mac
|
||||
for dev_path in /sys/class/net/*; do
|
||||
_if="$(basename "$dev_path")"
|
||||
case "$_if" in
|
||||
lo|veth*|tap*|fwln*|fwbr*|fwpr*|vmbr*|bond*) continue ;;
|
||||
esac
|
||||
[[ -e "$dev_path/device" ]] || continue
|
||||
_mac="$(cat "$dev_path/address" 2>/dev/null || true)"
|
||||
[[ -z "$_mac" ]] && continue
|
||||
_dest_mac_by_if["$_if"]="$_mac"
|
||||
_dest_if_by_mac["$_mac"]="$_if"
|
||||
done
|
||||
|
||||
local src_if src_mac
|
||||
while IFS=$'\t' read -r src_if src_mac; do
|
||||
[[ -z "$src_if" || -z "$src_mac" ]] && continue
|
||||
# 1) Same ifname on target — compare MAC.
|
||||
if [[ -n "${_dest_mac_by_if[$src_if]:-}" ]]; then
|
||||
local cur_mac="${_dest_mac_by_if[$src_if]}"
|
||||
if [[ "$cur_mac" != "$src_mac" ]]; then
|
||||
HB_NIC_MAC_CHANGED+=("${src_if}|${src_mac}|${cur_mac}")
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
# 2) MAC found on a different ifname — remap.
|
||||
if [[ -n "${_dest_if_by_mac[$src_mac]:-}" ]]; then
|
||||
HB_NIC_REMAP+=("${src_if}|${_dest_if_by_mac[$src_mac]}|${src_mac}")
|
||||
continue
|
||||
fi
|
||||
# 3) Neither ifname nor MAC — real removal.
|
||||
HB_NIC_ORPHAN+=("${src_if}|${src_mac}")
|
||||
done < <(jq -r '.hardware_inventory.nic[]? | "\(.ifname)\t\(.mac)"' "$manifest" 2>/dev/null)
|
||||
}
|
||||
|
||||
# Rewrites NIC names in the staging_root's network config so the
|
||||
# restored /etc/network/interfaces (and interfaces.d/*, and
|
||||
# systemd-networkd .link files if present) uses the new names.
|
||||
# Only touches the staging copy — nothing on the live host changes
|
||||
# until the normal apply step runs. GNU sed word boundaries (\b)
|
||||
# keep eth10 safe when renaming eth1.
|
||||
hb_apply_nic_remaps() {
|
||||
local staging_root="$1"
|
||||
(( ${#HB_NIC_REMAP[@]} == 0 )) && return 0
|
||||
|
||||
local -a targets=()
|
||||
[[ -f "$staging_root/rootfs/etc/network/interfaces" ]] && \
|
||||
targets+=("$staging_root/rootfs/etc/network/interfaces")
|
||||
if [[ -d "$staging_root/rootfs/etc/network/interfaces.d" ]]; then
|
||||
while IFS= read -r -d '' f; do targets+=("$f"); done \
|
||||
< <(find "$staging_root/rootfs/etc/network/interfaces.d" -type f -print0 2>/dev/null)
|
||||
fi
|
||||
if [[ -d "$staging_root/rootfs/etc/systemd/network" ]]; then
|
||||
while IFS= read -r -d '' f; do targets+=("$f"); done \
|
||||
< <(find "$staging_root/rootfs/etc/systemd/network" -type f -print0 2>/dev/null)
|
||||
fi
|
||||
(( ${#targets[@]} == 0 )) && return 0
|
||||
|
||||
local entry old_if new_if _mac t
|
||||
for entry in "${HB_NIC_REMAP[@]}"; do
|
||||
IFS='|' read -r old_if new_if _mac <<<"$entry"
|
||||
[[ -z "$old_if" || -z "$new_if" ]] && continue
|
||||
for t in "${targets[@]}"; do
|
||||
sed -i "s/\\b${old_if}\\b/${new_if}/g" "$t" 2>/dev/null || true
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# ==========================================================
|
||||
# CROSS-VERSION UNSAFE PATHS
|
||||
# ==========================================================
|
||||
# Curated list of paths that reliably break the boot when a backup
|
||||
# taken on PVE major X or kernel major.minor Y is applied on a
|
||||
# different X/Y. Restoring these is what caused the field-reported
|
||||
# kernel panic (backup on 9.1.9 / kernel 6.x restored on 9.2.3 /
|
||||
# kernel 7.x). We ship a static list rather than a heuristic so
|
||||
# behaviour is auditable — the operator can inspect it once and
|
||||
# know what "safe restore mode" means.
|
||||
#
|
||||
# Output: one line per path, tab-separated: <abs_path>\t<reason>
|
||||
# The reason is a short label used verbatim in the confirm dialog.
|
||||
# ==========================================================
|
||||
hb_unsafe_paths_cross_version() {
|
||||
cat <<'EOF'
|
||||
/etc/default/grub GRUB defaults tied to prior kernel order
|
||||
/etc/kernel proxmox-boot-tool state (cmdline, ESP UUIDs, hooks)
|
||||
/etc/modules-load.d autoload list may reference renamed modules
|
||||
/etc/modprobe.d module options may not apply on new kernel
|
||||
/etc/apt source suites may trigger downgrade on next upgrade
|
||||
/etc/initramfs-tools initramfs hooks tied to prior kernel
|
||||
/etc/fstab UUIDs may not exist on this install
|
||||
/etc/multipath multipath drivers change between kernels
|
||||
/etc/iscsi iSCSI parameters evolve between kernels
|
||||
/etc/udev/rules.d udev rules may bind to nonexistent subsystems
|
||||
/etc/zfs zpool.cache + hostid can lock the pool as foreign
|
||||
EOF
|
||||
}
|
||||
|
||||
# HB_COMPAT_SAME_HOST → 1 if backup's hostname matches current
|
||||
# HB_COMPAT_ANY_FAIL → 1 if at least one FAIL was raised
|
||||
# HB_COMPAT_ANY_WARN → 1 if at least one WARN was raised
|
||||
# HB_COMPAT_CROSS_VERSION → 1 if PVE major OR kernel major.minor differs
|
||||
# (triggers the safe-restore path filter that
|
||||
# skips grub/kernel/apt/dkms/... — see
|
||||
# hb_unsafe_paths_cross_version)
|
||||
# HB_COMPAT_RESULTS[] → array of "STATUS|category|message" entries
|
||||
# where STATUS ∈ {PASS, INFO, WARN, FAIL}
|
||||
# Use hb_show_compat_report to surface the result and let the user
|
||||
# decide whether to continue.
|
||||
# ==========================================================
|
||||
@@ -2552,6 +2690,7 @@ hb_compat_check() {
|
||||
HB_COMPAT_SAME_HOST=0
|
||||
HB_COMPAT_ANY_FAIL=0
|
||||
HB_COMPAT_ANY_WARN=0
|
||||
HB_COMPAT_CROSS_VERSION=0
|
||||
|
||||
local meta="$staging_root/metadata"
|
||||
local rootfs="$staging_root/rootfs"
|
||||
@@ -2594,8 +2733,13 @@ hb_compat_check() {
|
||||
elif [[ "$bk_major" == "$cur_major" ]]; then
|
||||
HB_COMPAT_RESULTS+=("PASS|PVE version|$(hb_translate "Same major series:") $bk_pve → $cur_pve")
|
||||
else
|
||||
HB_COMPAT_RESULTS+=("FAIL|PVE version|$(hb_translate "Major version mismatch:") $bk_pve → $cur_pve $(hb_translate "(default paths and packages may have changed)")")
|
||||
HB_COMPAT_ANY_FAIL=1
|
||||
# Major PVE bump — used to hard-FAIL, but the restore now
|
||||
# automatically drops boot/kernel/apt-critical paths in
|
||||
# this case (see hb_unsafe_paths_cross_version). Downgrade
|
||||
# to INFO so the user knows what's happening; the safe
|
||||
# filter does the actual protection.
|
||||
HB_COMPAT_RESULTS+=("INFO|PVE version|$(hb_translate "Major version differs:") $bk_pve → $cur_pve $(hb_translate "— safe restore mode will skip boot/kernel/apt config")")
|
||||
HB_COMPAT_CROSS_VERSION=1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -2615,8 +2759,10 @@ hb_compat_check() {
|
||||
if [[ "$bk_kmaj" == "$cur_kmaj" ]]; then
|
||||
HB_COMPAT_RESULTS+=("PASS|Kernel|$(hb_translate "Same major.minor:") $bk_kernel → $cur_kernel")
|
||||
else
|
||||
HB_COMPAT_RESULTS+=("WARN|Kernel|$(hb_translate "Different kernel:") $bk_kernel → $cur_kernel")
|
||||
HB_COMPAT_ANY_WARN=1
|
||||
# Kernel major.minor drift — same reasoning as the
|
||||
# PVE case above. Safe filter handles it.
|
||||
HB_COMPAT_RESULTS+=("INFO|Kernel|$(hb_translate "Different kernel major.minor:") $bk_kernel → $cur_kernel $(hb_translate "— safe restore mode will skip kernel-tied config")")
|
||||
HB_COMPAT_CROSS_VERSION=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -2676,7 +2822,19 @@ hb_compat_check() {
|
||||
fi
|
||||
done
|
||||
# Classify each missing NIC as wired vs orphan declaration.
|
||||
# A "missing" NIC that hb_plan_nic_remaps already matched by
|
||||
# MAC to a new ifname is NOT missing in any meaningful sense
|
||||
# — the restored config will be rewritten to use the new
|
||||
# name before apply. Skip it here.
|
||||
local -A _remap_old=()
|
||||
local _re_entry _re_old
|
||||
for _re_entry in "${HB_NIC_REMAP[@]:-}"; do
|
||||
[[ -z "$_re_entry" ]] && continue
|
||||
_re_old="${_re_entry%%|*}"
|
||||
[[ -n "$_re_old" ]] && _remap_old["$_re_old"]=1
|
||||
done
|
||||
for i in "${missing_ifaces[@]}"; do
|
||||
[[ -n "${_remap_old[$i]:-}" ]] && continue
|
||||
# Match: `auto <nic>`, `bridge-ports ... <nic>`, `bridge_ports ... <nic>`,
|
||||
# `bond-slaves ... <nic>`, `bond_slaves ... <nic>`, `slaves ... <nic>`
|
||||
if grep -qE "(^auto[[:space:]]+${i}\$|bridge[-_]ports[[:space:]]+.*\b${i}\b|bond[-_]slaves[[:space:]]+.*\b${i}\b|^[[:space:]]*slaves[[:space:]]+.*\b${i}\b)" "$ifaces_file"; then
|
||||
@@ -2686,6 +2844,25 @@ hb_compat_check() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Surface renames + MAC changes as INFO so the panel shows them
|
||||
# if there's something else to see, but doesn't force it open.
|
||||
# Separated `local` declarations because bash `set -u` in caller
|
||||
# scopes gets grumpy about chained `local a=$b c=$a` forms.
|
||||
local _re _o _rest _n
|
||||
for _re in "${HB_NIC_REMAP[@]:-}"; do
|
||||
[[ -z "$_re" ]] && continue
|
||||
_o="${_re%%|*}"
|
||||
_rest="${_re#*|}"
|
||||
_n="${_rest%%|*}"
|
||||
HB_COMPAT_RESULTS+=("INFO|Network|$(hb_translate "Renamed NIC:") ${_o} → ${_n} $(hb_translate "(same MAC — restored config adjusted automatically)")")
|
||||
done
|
||||
local _mc _ifn
|
||||
for _mc in "${HB_NIC_MAC_CHANGED[@]:-}"; do
|
||||
[[ -z "$_mc" ]] && continue
|
||||
_ifn="${_mc%%|*}"
|
||||
HB_COMPAT_RESULTS+=("INFO|Network|$(hb_translate "NIC") ${_ifn} $(hb_translate "has a different MAC than the backup — update any DHCP static reservation")")
|
||||
done
|
||||
|
||||
if [[ ${#bk_ifaces[@]} -eq 0 ]]; then
|
||||
: # nothing to check
|
||||
elif [[ ${#missing_ifaces[@]} -eq 0 ]]; then
|
||||
@@ -2727,8 +2904,10 @@ hb_compat_check() {
|
||||
else
|
||||
list_str="${missing_pkgs[*]:0:6}… (+ $((${#missing_pkgs[@]} - 6)) $(hb_translate "more"))"
|
||||
fi
|
||||
HB_COMPAT_RESULTS+=("WARN|Packages|${#missing_pkgs[@]} $(hb_translate "user-installed packages from backup are missing here:") $list_str")
|
||||
HB_COMPAT_ANY_WARN=1
|
||||
# Missing packages are auto-installed by _rs_run_complete_extras
|
||||
# regardless of restore strategy — the user doesn't have to
|
||||
# decide anything, so INFO instead of WARN.
|
||||
HB_COMPAT_RESULTS+=("INFO|Packages|${#missing_pkgs[@]} $(hb_translate "user packages missing — will be installed automatically:") $list_str")
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -2766,13 +2945,14 @@ hb_compat_check() {
|
||||
# report and lets the user proceed; an all-PASS report only shows up
|
||||
# briefly so the user can see it succeeded.
|
||||
hb_show_compat_report() {
|
||||
local pass=0 warn=0 fail=0 line status rest cat msg
|
||||
local pass=0 info=0 warn=0 fail=0 line status rest cat msg
|
||||
local report=""
|
||||
for line in "${HB_COMPAT_RESULTS[@]}"; do
|
||||
status="${line%%|*}"; rest="${line#*|}"
|
||||
cat="${rest%%|*}"; msg="${rest#*|}"
|
||||
case "$status" in
|
||||
PASS) ((pass++)); report+=$' [OK] '"${cat}"$' — '"${msg}"$'\n' ;;
|
||||
PASS) ((pass++)); report+=$' [OK] '"${cat}"$' — '"${msg}"$'\n' ;;
|
||||
INFO) ((info++)); report+=$' [INFO] '"${cat}"$' — '"${msg}"$'\n' ;;
|
||||
WARN) ((warn++)); report+=$' [WARN] '"${cat}"$' — '"${msg}"$'\n' ;;
|
||||
FAIL) ((fail++)); report+=$' [FAIL] '"${cat}"$' — '"${msg}"$'\n' ;;
|
||||
esac
|
||||
@@ -2780,7 +2960,7 @@ hb_show_compat_report() {
|
||||
|
||||
local summary
|
||||
summary="$(hb_translate "Compatibility check"): "
|
||||
summary+="${pass} pass, ${warn} warn, ${fail} fail"
|
||||
summary+="${pass} pass, ${info} info, ${warn} warn, ${fail} fail"
|
||||
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp)
|
||||
|
||||
Reference in New Issue
Block a user