mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-05 07:16:21 +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')
|
stale_nodes = data.get('stale_nodes', '0')
|
||||||
components = data.get('components', 'none')
|
components = data.get('components', 'none')
|
||||||
duration = data.get('duration', 'unknown')
|
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(
|
notification_manager.emit_event(
|
||||||
event_type='system_restore_completed',
|
event_type='system_restore_completed',
|
||||||
severity='INFO',
|
severity=severity,
|
||||||
data={
|
data={
|
||||||
'hostname': hostname,
|
'hostname': hostname,
|
||||||
'guests': guests,
|
'guests': guests,
|
||||||
@@ -1597,6 +1605,8 @@ def internal_restore_event():
|
|||||||
'stale_nodes': stale_nodes,
|
'stale_nodes': stale_nodes,
|
||||||
'components': components,
|
'components': components,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
|
'warnings': warnings,
|
||||||
|
'warnings_block': warnings_block,
|
||||||
},
|
},
|
||||||
source='proxmenux',
|
source='proxmenux',
|
||||||
entity='node',
|
entity='node',
|
||||||
|
|||||||
@@ -9576,7 +9576,10 @@ def api_smart_run_test(disk_name):
|
|||||||
capture_output=True, text=True, timeout=10
|
capture_output=True, text=True, timeout=10
|
||||||
)
|
)
|
||||||
if check_proc.returncode != 0:
|
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
|
# Check if device supports self-test by looking at OACS field
|
||||||
# OACS bit 4 (0x10) indicates Device Self-test support
|
# OACS bit 4 (0x10) indicates Device Self-test support
|
||||||
@@ -9616,8 +9619,8 @@ def api_smart_run_test(disk_name):
|
|||||||
# Check for permission errors
|
# Check for permission errors
|
||||||
if 'permission' in error_msg.lower() or 'operation not permitted' in error_msg.lower():
|
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'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
|
# Start background monitor to save JSON when test completes
|
||||||
# Check 'Current Device Self-Test Operation' field - if > 0, test is running
|
# Check 'Current Device Self-Test Operation' field - if > 0, test is running
|
||||||
sleep_interval = 10 if test_type == 'short' else 60
|
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
|
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
|
# Start background monitor to save JSON when test completes
|
||||||
sleep_interval = 10 if test_type == 'short' else 60
|
sleep_interval = 10 if test_type == 'short' else 60
|
||||||
|
|||||||
@@ -901,7 +901,16 @@ TEMPLATES = {
|
|||||||
},
|
},
|
||||||
'system_restore_completed': {
|
'system_restore_completed': {
|
||||||
'title': '{hostname}: Host restore finished',
|
'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',
|
'label': 'Host restore completed',
|
||||||
'group': 'services',
|
'group': 'services',
|
||||||
'default_enabled': True,
|
'default_enabled': True,
|
||||||
|
|||||||
@@ -412,6 +412,49 @@ if [[ -n "$ROLLBACK_PLAN_FILE" ]] && command -v jq >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
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 ───────────────────
|
# ── Notify ProxMenux Monitor that we're done ───────────────────
|
||||||
# Routes through the user's configured channels (Telegram, Discord,
|
# Routes through the user's configured channels (Telegram, Discord,
|
||||||
# ntfy, etc.). Localhost-only endpoint, no auth needed. We try
|
# 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
|
fi
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
if command -v curl >/dev/null 2>&1; then
|
||||||
PAYLOAD=$(printf '{"hostname":"%s","guests":"%s","stubs":"%s","stale_nodes":"%s","components":"%s","duration":"%s"}' \
|
# jq builds a proper JSON when available so SANITY_WARNINGS with
|
||||||
"$(hostname)" \
|
# special chars can't break the payload. Falls back to printf on
|
||||||
"${copied_guests:-0}" \
|
# hosts without jq — we already restrict SANITY_WARNINGS content
|
||||||
"${stub_created:-0}" \
|
# to plain ASCII in the sanity check above, so the fallback is
|
||||||
"${removed_nodes:-0}" \
|
# safe too.
|
||||||
"${COMPONENTS_REINSTALLED_CSV:-none}" \
|
if command -v jq >/dev/null 2>&1; then
|
||||||
"$POSTBOOT_DURATION_FMT")
|
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}' \
|
NOTIFY_HTTP=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
-X POST "http://127.0.0.1:8008/api/internal/restore-event" \
|
-X POST "http://127.0.0.1:8008/api/internal/restore-event" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ if [[ -f "$PLAN_ENV" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
: "${HB_RESTORE_INCLUDE_ZFS:=0}"
|
: "${HB_RESTORE_INCLUDE_ZFS:=0}"
|
||||||
|
: "${HB_COMPAT_CROSS_VERSION:=0}"
|
||||||
|
|
||||||
if [[ ! -f "$APPLY_LIST" ]]; then
|
if [[ ! -f "$APPLY_LIST" ]]; then
|
||||||
echo "Apply list missing: $APPLY_LIST"
|
echo "Apply list missing: $APPLY_LIST"
|
||||||
@@ -46,9 +47,10 @@ if [[ ! -f "$APPLY_LIST" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Pending dir: $PENDING_DIR"
|
echo "Pending dir: $PENDING_DIR"
|
||||||
echo "Apply list: $APPLY_LIST"
|
echo "Apply list: $APPLY_LIST"
|
||||||
echo "Include ZFS: $HB_RESTORE_INCLUDE_ZFS"
|
echo "Include ZFS: $HB_RESTORE_INCLUDE_ZFS"
|
||||||
|
echo "Cross-version: $HB_COMPAT_CROSS_VERSION"
|
||||||
|
|
||||||
# Hardware-drift skips persisted by _rs_prepare_pending_restore.
|
# Hardware-drift skips persisted by _rs_prepare_pending_restore.
|
||||||
# Each line is an absolute path; we drop any rel path that matches
|
# 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"
|
SKIP_PATHS_FILE="$PENDING_DIR/rs-skip-paths.txt"
|
||||||
if [[ -f "$SKIP_PATHS_FILE" ]]; then
|
if [[ -f "$SKIP_PATHS_FILE" ]]; then
|
||||||
RS_SKIP_PATHS=$(cat "$SKIP_PATHS_FILE")
|
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
|
fi
|
||||||
|
|
||||||
echo "running" >"$STATE_FILE"
|
echo "running" >"$STATE_FILE"
|
||||||
@@ -265,6 +272,7 @@ EOF
|
|||||||
printf 'PENDING_DIR=%s\n' "$PENDING_DIR"
|
printf 'PENDING_DIR=%s\n' "$PENDING_DIR"
|
||||||
printf 'NEEDS_INITRAMFS=%s\n' "$NEEDS_INITRAMFS"
|
printf 'NEEDS_INITRAMFS=%s\n' "$NEEDS_INITRAMFS"
|
||||||
printf 'NEEDS_GRUB=%s\n' "$NEEDS_GRUB"
|
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
|
} > /var/lib/proxmenux/cluster-apply-pending
|
||||||
chmod 600 /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}
|
CREATED_AT=${created_at}
|
||||||
HB_RESTORE_INCLUDE_ZFS=${HB_RESTORE_INCLUDE_ZFS:-0}
|
HB_RESTORE_INCLUDE_ZFS=${HB_RESTORE_INCLUDE_ZFS:-0}
|
||||||
HB_ROLLBACK_EXECUTE=${HB_ROLLBACK_EXECUTE:-0}
|
HB_ROLLBACK_EXECUTE=${HB_ROLLBACK_EXECUTE:-0}
|
||||||
|
HB_COMPAT_CROSS_VERSION=${HB_COMPAT_CROSS_VERSION:-0}
|
||||||
EOF
|
EOF
|
||||||
# Persist hardware-drift skips so apply_pending_restore.sh can filter
|
# 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
|
# 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"
|
(( dialog_signal == 1 )) && RS_DRIFT_SUMMARY="$plan_body"
|
||||||
fi
|
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
|
# Build the rich confirmation body. Replaces the previous 4-strategy
|
||||||
# menu — by design a Proxmox host restore always requires a reboot
|
# menu — by design a Proxmox host restore always requires a reboot
|
||||||
# for predictable end state (pmxcfs live writes + initramfs + driver
|
# for predictable end state (pmxcfs live writes + initramfs + driver
|
||||||
@@ -2222,6 +2289,13 @@ _rs_run_complete_guided() {
|
|||||||
body+=" • \Zb${label}\ZB (${eta})"$'\n'
|
body+=" • \Zb${label}\ZB (${eta})"$'\n'
|
||||||
done
|
done
|
||||||
fi
|
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
|
# If smart restore flagged drift skips earlier, surface them here
|
||||||
# so the operator sees everything in one screen instead of two
|
# so the operator sees everything in one screen instead of two
|
||||||
# consecutive yes/no popups.
|
# 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')
|
_drift_bullets=$(printf '%s\n' "$RS_DRIFT_SUMMARY" | sed -n '/\Z1•\Zn/,$p')
|
||||||
body+="$_drift_bullets"$'\n'
|
body+="$_drift_bullets"$'\n'
|
||||||
fi
|
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+=$'\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+="$(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"
|
body+="\Zb$(translate "Continue?")\ZB"
|
||||||
@@ -2799,6 +2879,13 @@ restore_menu() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ $ok -eq 1 ]] && _rs_check_layout "$staging_root"; then
|
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
|
# Run the compatibility check BEFORE the apply menu so
|
||||||
# the operator sees PVE-version / hostname / network /
|
# the operator sees PVE-version / hostname / network /
|
||||||
# storage drift up front. This also sets
|
# storage drift up front. This also sets
|
||||||
|
|||||||
@@ -2539,10 +2539,148 @@ hb_ensure_pv() {
|
|||||||
# storage / network / VMID drift BEFORE the apply menu opens.
|
# storage / network / VMID drift BEFORE the apply menu opens.
|
||||||
#
|
#
|
||||||
# After running hb_compat_check, the caller can read:
|
# 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
|
# NIC REMAP PLAN + APPLY
|
||||||
# HB_COMPAT_ANY_WARN → 1 if at least one WARN was raised
|
# ==========================================================
|
||||||
# HB_COMPAT_RESULTS[] → array of "STATUS|category|message" entries
|
# 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
|
# Use hb_show_compat_report to surface the result and let the user
|
||||||
# decide whether to continue.
|
# decide whether to continue.
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -2552,6 +2690,7 @@ hb_compat_check() {
|
|||||||
HB_COMPAT_SAME_HOST=0
|
HB_COMPAT_SAME_HOST=0
|
||||||
HB_COMPAT_ANY_FAIL=0
|
HB_COMPAT_ANY_FAIL=0
|
||||||
HB_COMPAT_ANY_WARN=0
|
HB_COMPAT_ANY_WARN=0
|
||||||
|
HB_COMPAT_CROSS_VERSION=0
|
||||||
|
|
||||||
local meta="$staging_root/metadata"
|
local meta="$staging_root/metadata"
|
||||||
local rootfs="$staging_root/rootfs"
|
local rootfs="$staging_root/rootfs"
|
||||||
@@ -2594,8 +2733,13 @@ hb_compat_check() {
|
|||||||
elif [[ "$bk_major" == "$cur_major" ]]; then
|
elif [[ "$bk_major" == "$cur_major" ]]; then
|
||||||
HB_COMPAT_RESULTS+=("PASS|PVE version|$(hb_translate "Same major series:") $bk_pve → $cur_pve")
|
HB_COMPAT_RESULTS+=("PASS|PVE version|$(hb_translate "Same major series:") $bk_pve → $cur_pve")
|
||||||
else
|
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)")")
|
# Major PVE bump — used to hard-FAIL, but the restore now
|
||||||
HB_COMPAT_ANY_FAIL=1
|
# 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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -2615,8 +2759,10 @@ hb_compat_check() {
|
|||||||
if [[ "$bk_kmaj" == "$cur_kmaj" ]]; then
|
if [[ "$bk_kmaj" == "$cur_kmaj" ]]; then
|
||||||
HB_COMPAT_RESULTS+=("PASS|Kernel|$(hb_translate "Same major.minor:") $bk_kernel → $cur_kernel")
|
HB_COMPAT_RESULTS+=("PASS|Kernel|$(hb_translate "Same major.minor:") $bk_kernel → $cur_kernel")
|
||||||
else
|
else
|
||||||
HB_COMPAT_RESULTS+=("WARN|Kernel|$(hb_translate "Different kernel:") $bk_kernel → $cur_kernel")
|
# Kernel major.minor drift — same reasoning as the
|
||||||
HB_COMPAT_ANY_WARN=1
|
# 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
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -2676,7 +2822,19 @@ hb_compat_check() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
# Classify each missing NIC as wired vs orphan declaration.
|
# 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
|
for i in "${missing_ifaces[@]}"; do
|
||||||
|
[[ -n "${_remap_old[$i]:-}" ]] && continue
|
||||||
# Match: `auto <nic>`, `bridge-ports ... <nic>`, `bridge_ports ... <nic>`,
|
# Match: `auto <nic>`, `bridge-ports ... <nic>`, `bridge_ports ... <nic>`,
|
||||||
# `bond-slaves ... <nic>`, `bond_slaves ... <nic>`, `slaves ... <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
|
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
|
fi
|
||||||
done
|
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
|
if [[ ${#bk_ifaces[@]} -eq 0 ]]; then
|
||||||
: # nothing to check
|
: # nothing to check
|
||||||
elif [[ ${#missing_ifaces[@]} -eq 0 ]]; then
|
elif [[ ${#missing_ifaces[@]} -eq 0 ]]; then
|
||||||
@@ -2727,8 +2904,10 @@ hb_compat_check() {
|
|||||||
else
|
else
|
||||||
list_str="${missing_pkgs[*]:0:6}… (+ $((${#missing_pkgs[@]} - 6)) $(hb_translate "more"))"
|
list_str="${missing_pkgs[*]:0:6}… (+ $((${#missing_pkgs[@]} - 6)) $(hb_translate "more"))"
|
||||||
fi
|
fi
|
||||||
HB_COMPAT_RESULTS+=("WARN|Packages|${#missing_pkgs[@]} $(hb_translate "user-installed packages from backup are missing here:") $list_str")
|
# Missing packages are auto-installed by _rs_run_complete_extras
|
||||||
HB_COMPAT_ANY_WARN=1
|
# 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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -2766,13 +2945,14 @@ hb_compat_check() {
|
|||||||
# report and lets the user proceed; an all-PASS report only shows up
|
# report and lets the user proceed; an all-PASS report only shows up
|
||||||
# briefly so the user can see it succeeded.
|
# briefly so the user can see it succeeded.
|
||||||
hb_show_compat_report() {
|
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=""
|
local report=""
|
||||||
for line in "${HB_COMPAT_RESULTS[@]}"; do
|
for line in "${HB_COMPAT_RESULTS[@]}"; do
|
||||||
status="${line%%|*}"; rest="${line#*|}"
|
status="${line%%|*}"; rest="${line#*|}"
|
||||||
cat="${rest%%|*}"; msg="${rest#*|}"
|
cat="${rest%%|*}"; msg="${rest#*|}"
|
||||||
case "$status" in
|
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' ;;
|
WARN) ((warn++)); report+=$' [WARN] '"${cat}"$' — '"${msg}"$'\n' ;;
|
||||||
FAIL) ((fail++)); report+=$' [FAIL] '"${cat}"$' — '"${msg}"$'\n' ;;
|
FAIL) ((fail++)); report+=$' [FAIL] '"${cat}"$' — '"${msg}"$'\n' ;;
|
||||||
esac
|
esac
|
||||||
@@ -2780,7 +2960,7 @@ hb_show_compat_report() {
|
|||||||
|
|
||||||
local summary
|
local summary
|
||||||
summary="$(hb_translate "Compatibility check"): "
|
summary="$(hb_translate "Compatibility check"): "
|
||||||
summary+="${pass} pass, ${warn} warn, ${fail} fail"
|
summary+="${pass} pass, ${info} info, ${warn} warn, ${fail} fail"
|
||||||
|
|
||||||
local tmpfile
|
local tmpfile
|
||||||
tmpfile=$(mktemp)
|
tmpfile=$(mktemp)
|
||||||
|
|||||||
Reference in New Issue
Block a user