From 8bd60e2f5240f15907077985c7722056c00f7701 Mon Sep 17 00:00:00 2001 From: David Schroeder Date: Wed, 29 Jul 2026 17:17:56 -0500 Subject: [PATCH] update --- README.md | 7 +-- defaults.inc | 2 +- inc/cpu-compat.inc | 96 ++++++++++++++++++++++++++++++++++++++++ proxmenu-scripts.sh | 79 +++++++++++++++++++++++++++++++-- tests/test-cpu-compat.sh | 47 ++++++++++++++++++++ 5 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 inc/cpu-compat.inc create mode 100644 tests/test-cpu-compat.sh diff --git a/README.md b/README.md index ba816d4..8fd7429 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,10 @@ ISO repository. The legacy Dell OMSA installer is limited to supported PowerEdge x30/x40 systems running Proxmox VE 9 on Debian 13 (Trixie), amd64. -CPU compatibility detection previews cluster-wide QEMU VM and template -changes before applying the ProxCLMC recommendation through the Proxmox CLI. -Running VMs are not restarted automatically. +CPU compatibility analysis compares every cluster host's physical processor, +processor generation/family, and supported generic VM CPU baseline. It then previews +QEMU VM and template changes before applying the highest baseline shared by +all nodes through the Proxmox CLI. Running VMs are not restarted automatically. The native TAPM host configuration workflow replaces the former ProxMenux post-install dependency. It can audit a host, apply the recommended PVE 9 diff --git a/defaults.inc b/defaults.inc index 4390656..a4158af 100755 --- a/defaults.inc +++ b/defaults.inc @@ -3,7 +3,7 @@ action="${1:-}" FOLDER='/opt/idssys/ta-proxmenu' -VERS='2026.7.28-10' +VERS='2026.7.28-11' noupdate=' ' diff --git a/inc/cpu-compat.inc b/inc/cpu-compat.inc new file mode 100644 index 0000000..2f60edb --- /dev/null +++ b/inc/cpu-compat.inc @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# Parse the per-node rows printed by ProxCLMC. Fields are separated with an +# ASCII unit separator so processor descriptions can safely contain spaces. +TAPM_PROXCLMC_HOST_ROWS() { + awk -F '|' ' +function trim(value) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + return value +} + +{ + node = trim($1) + address = trim($2) + level = trim($3) + + if (node ~ /^[[:alnum:]_.-]+$/ && level ~ /^x86-64-v(1|2-AES|3|4)$/) { + printf "%s\034%s\034%s\n", node, address, level + } +} +' +} + +# Produce a conservative generation label from the processor name exposed by +# Proxmox. Ambiguous products are described as a family instead of being given +# a potentially incorrect codename. +TAPM_CPU_GENERATION_FROM_NAME() { + local cpu_name="${1:-}" + local normalized + local generation="" + local model_number="" + + normalized="$(printf '%s' "$cpu_name" | tr '[:upper:]' '[:lower:]')" + + if [[ "$normalized" =~ xeon.*e5-[[:digit:]]+[[:space:]]+v([1-4]) ]]; then + generation="${BASH_REMATCH[1]}" + case "$generation" in + 1) printf '%s\n' "Sandy Bridge-EP" ;; + 2) printf '%s\n' "Ivy Bridge-EP" ;; + 3) printf '%s\n' "Haswell-EP" ;; + 4) printf '%s\n' "Broadwell-EP" ;; + esac + return 0 + fi + + if [[ "$normalized" =~ xeon.*(bronze|silver|gold|platinum)[[:space:]]+([[:digit:]]{4}) ]]; then + model_number="${BASH_REMATCH[2]}" + generation="${model_number:1:1}" + case "$generation" in + 1) printf '%s\n' "1st Gen Xeon Scalable (Skylake-SP)" ;; + 2) printf '%s\n' "2nd Gen Xeon Scalable (Cascade Lake)" ;; + 3) printf '%s\n' "3rd Gen Xeon Scalable (Ice Lake)" ;; + 4) printf '%s\n' "4th Gen Xeon Scalable (Sapphire Rapids)" ;; + 5) printf '%s\n' "5th Gen Xeon Scalable (Emerald Rapids)" ;; + *) printf '%s\n' "Intel Xeon Scalable family" ;; + esac + return 0 + fi + + if [[ "$normalized" =~ epyc[[:space:]]+([[:digit:]]{4}) ]]; then + model_number="${BASH_REMATCH[1]}" + generation="${model_number:3:1}" + case "$generation" in + 1) printf '%s\n' "1st Gen EPYC (Naples / Zen)" ;; + 2) printf '%s\n' "2nd Gen EPYC (Rome / Zen 2)" ;; + 3) printf '%s\n' "3rd Gen EPYC (Milan / Zen 3)" ;; + 4) printf '%s\n' "4th Gen EPYC (Zen 4 family)" ;; + 5) printf '%s\n' "5th Gen EPYC (Zen 5 family)" ;; + *) printf '%s\n' "AMD EPYC family" ;; + esac + return 0 + fi + + if [[ "$normalized" =~ core.*i[3579]-([[:digit:]]{4,5}) ]]; then + model_number="${BASH_REMATCH[1]}" + if (( ${#model_number} == 4 )); then + generation="${model_number:0:1}" + else + generation="${model_number:0:2}" + fi + printf '%s\n' "Intel Core Gen ${generation}" + return 0 + fi + + if [[ "$normalized" =~ ryzen.*[[:space:]]([[:digit:]]{4,5}) ]]; then + model_number="${BASH_REMATCH[1]}" + printf '%s\n' "AMD Ryzen ${model_number:0:1}000 family" + return 0 + fi + + case "$normalized" in + *intel*) printf '%s\n' "Intel generation unknown" ;; + *amd*) printf '%s\n' "AMD generation unknown" ;; + *) printf '%s\n' "Generation unknown" ;; + esac +} diff --git a/proxmenu-scripts.sh b/proxmenu-scripts.sh index 9124b28..8771d1d 100755 --- a/proxmenu-scripts.sh +++ b/proxmenu-scripts.sh @@ -10,6 +10,7 @@ source /opt/idssys/ta-proxmenu/inc/header-info.inc source /opt/idssys/ta-proxmenu/inc/post-install.inc source /opt/idssys/ta-proxmenu/inc/rmm.inc source /opt/idssys/ta-proxmenu/inc/secure-input.inc +source /opt/idssys/ta-proxmenu/inc/cpu-compat.inc source /opt/idssys/ta-proxmenu/inc/deploy-iso-nfs-lxc.sh source /opt/idssys/ta-proxmenu/inc/deploy-pulse-lxc.sh source /opt/idssys/ta-proxmenu/inc/virtio-helpers.inc @@ -1225,6 +1226,73 @@ TAPM_SET_QEMU_CPU_MODEL() { --cpu "$cpu_model" } +TAPM_NODE_CPU_NAME() { + local node="${1:?node is required}" + local status_json + + status_json="$( + pvesh get "/nodes/${node}/status" --output-format json 2>/dev/null + )" || return 1 + + TAPM_NODE_STATUS_JSON="$status_json" python3 -c ' +import json +import os + +try: + status = json.loads(os.environ["TAPM_NODE_STATUS_JSON"]) +except (KeyError, TypeError, ValueError): + raise SystemExit(1) + +cpu_info = status.get("cpuinfo") or {} +model = str(cpu_info.get("model") or "").strip() +if not model: + raise SystemExit(1) + +print(" ".join(model.split())) +' 2>/dev/null +} + +TAPM_SHOW_CLUSTER_CPU_TABLE() { + local proxclmc_output + local host_rows + local node + local address + local host_max + local cpu_name + local generation + + proxclmc_output="$(proxclmc 2>/dev/null)" || proxclmc_output="" + host_rows="$(printf '%s\n' "$proxclmc_output" | TAPM_PROXCLMC_HOST_ROWS)" + + printf '\nCluster host CPU capabilities:\n\n' + + if [[ -z "$host_rows" ]]; then + echo -e " ${idsCL[Yellow]}Per-host CPU details were unavailable from ProxCLMC.${idsCL[Default]}" + return 0 + fi + + printf ' %-16s %-42s %-34s %-14s\n' \ + 'HOST' 'PHYSICAL CPU' 'GENERATION / FAMILY' 'BEST VM MODEL' + printf ' %-16s %-42s %-34s %-14s\n' \ + '----------------' '------------------------------------------' \ + '----------------------------------' '--------------' + + while IFS=$'\034' read -r node address host_max; do + [[ -n "$node" ]] || continue + + cpu_name="$(TAPM_NODE_CPU_NAME "$node" 2>/dev/null || true)" + if [[ -n "$cpu_name" ]]; then + generation="$(TAPM_CPU_GENERATION_FROM_NAME "$cpu_name")" + else + cpu_name='Unavailable' + generation='Unavailable' + fi + + printf ' %-16.16s %-42.42s %-34.34s %-14s\n' \ + "$node" "$cpu_name" "$generation" "$host_max" + done <<< "$host_rows" +} + DETECT_CPU() { local answer local cpu_model @@ -1266,6 +1334,12 @@ DETECT_CPU() { return fi + TAPM_SHOW_CLUSTER_CPU_TABLE + printf '\nRecommended cluster-wide VM CPU baseline: %s\n' "$cpu_model" + printf '%s\n' \ + 'This is the highest generic model shared by every node for HA placement,' \ + 'load balancing, and moving workloads between cluster hosts.' + guest_output="$(TAPM_CLUSTER_QEMU_GUESTS)" || { echo -e "${idsCL[LightRed]}Could not read cluster QEMU resources.${idsCL[Default]}" FINISH_FAILED_ACTION @@ -1296,7 +1370,6 @@ DETECT_CPU() { return fi - printf '\nRecommended cluster CPU model: %s\n' "$cpu_model" printf '\n%-7s %-28s %-20s %-10s %-24s %s\n' \ 'VMID' 'NAME' 'NODE' 'TEMPLATE' 'CURRENT CPU' 'PROPOSED CPU' printf '%-7s %-28s %-20s %-10s %-24s %s\n' \ @@ -1308,7 +1381,7 @@ DETECT_CPU() { "$vmid" "${name:-unnamed}" "$node" "$template" "$current_cpu" "$cpu_model" done - echo -en "\n${idsCL[LightCyan]}Apply this CPU model to ${#changes[@]} VM(s) and template(s) (y/N)?${idsCL[Default]} " + echo -en "\n${idsCL[LightCyan]}Apply cluster-wide baseline ${cpu_model} to ${#changes[@]} VM(s) and template(s) (y/N)?${idsCL[Default]} " read -r -n 1 answer echo [[ "$answer" =~ ^[Yy]$ ]] || return @@ -2269,7 +2342,7 @@ HOST_SETUP_MENU() { values=("post_install") TAPM_POST_PROXMENUX_DETECTED && labels[0]="TAPM Host Configuration — ProxMenux migration recommended" - labels+=("Detect CPU model for live migrations") + labels+=("Analyze VM CPU compatibility for HA and workload mobility") values+=("cpu") # Discover ISO storage only after the VirtIO submenu is selected. diff --git a/tests/test-cpu-compat.sh b/tests/test-cpu-compat.sh new file mode 100644 index 0000000..0712bc4 --- /dev/null +++ b/tests/test-cpu-compat.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "${TEST_DIR}/.." && pwd)" + +source "${REPO_DIR}/inc/cpu-compat.inc" + +assert_equal() { + local expected="${1:?expected value is required}" + local actual="${2:?actual value is required}" + local description="${3:?description is required}" + + if [[ "$actual" != "$expected" ]]; then + printf 'FAIL: %s\nExpected: %s\nActual: %s\n' \ + "$description" "$expected" "$actual" >&2 + exit 1 + fi +} + +assert_equal \ + "Broadwell-EP" \ + "$(TAPM_CPU_GENERATION_FROM_NAME "Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz")" \ + "identifies Xeon E5 v4" + +assert_equal \ + "3rd Gen Xeon Scalable (Ice Lake)" \ + "$(TAPM_CPU_GENERATION_FROM_NAME "Intel(R) Xeon(R) Gold 6338 CPU @ 2.00GHz")" \ + "identifies third-generation Xeon Scalable" + +assert_equal \ + "3rd Gen EPYC (Milan / Zen 3)" \ + "$(TAPM_CPU_GENERATION_FROM_NAME "AMD EPYC 7543 32-Core Processor")" \ + "identifies EPYC Milan" + +proxclmc_fixture='node-a | 10.0.0.1 | x86-64-v4 +node-b | 10.0.0.2 | x86-64-v3 + +Cluster CPU type: x86-64-v3' + +expected_rows=$'node-a\03410.0.0.1\034x86-64-v4\nnode-b\03410.0.0.2\034x86-64-v3' +actual_rows="$(printf '%s\n' "$proxclmc_fixture" | TAPM_PROXCLMC_HOST_ROWS)" + +assert_equal "$expected_rows" "$actual_rows" "parses ProxCLMC host rows" + +printf 'CPU compatibility tests passed.\n'