From a7eb09e248b98e8b0e79fa0b55a89805b3053228 Mon Sep 17 00:00:00 2001 From: Maarten Date: Mon, 2 Mar 2026 16:46:31 +0100 Subject: [PATCH] Fix detection: PVE 9 deb822 repos, ZFS partition matching, WireGuard state Tested against live aether-lab host. Fixes: 1. Repo detection: PVE 9 uses .sources (deb822) format, not .list files. Now checks both formats, reads Enabled: field for deb822 enterprise repos, and greps for pve-no-subscription across all formats. 2. Available disk detection: zpool status shows partition names (nvme0n1p3), not whole-disk names. Now strips partition suffix to get parent disk, correctly excluding system pool disks. Also detects existing partition tables (linux_raid_member, etc.) on available disks and warns about them. 3. WireGuard detection: empty output from "wg show" piped through "head -1" produced empty string (not "none"), which was parsed as active. Now explicitly checks for empty output. 4. Storage apply: detects and warns about disks with existing partitions, offers to wipe before ZFS pool creation. Default pool name changed to "vm-storage" (since "local-zfs" is the system pool). 5. Repo apply: handles both .list and .sources formats for disabling enterprise repos. Generates correct deb822 format for PVE 9 when adding no-subscription repo. 6. Fixed invalid syntax: "for f in ... 2>/dev/null; do" is not valid bash -- removed the redirect, file existence is checked with -f. Co-Authored-By: Claude Opus 4.6 --- hetzner/proxmox-setup | 146 +++++++++++++++++++++++++++++++++++------- 1 file changed, 123 insertions(+), 23 deletions(-) diff --git a/hetzner/proxmox-setup b/hetzner/proxmox-setup index 7401227..e3cad15 100755 --- a/hetzner/proxmox-setup +++ b/hetzner/proxmox-setup @@ -392,11 +392,43 @@ zpool list -H 2>/dev/null || true echo '---PVE_STORAGE---' pvesm status 2>/dev/null || true echo '---ENTERPRISE_REPOS---' -grep -rl '^deb.*enterprise' /etc/apt/sources.list.d/*.list 2>/dev/null || echo 'none' +# Check both .list (PVE 8) and .sources/deb822 (PVE 9) formats +enterprise_active='no' +# Old .list format: uncommented deb lines with enterprise +if grep -rl '^deb.*enterprise' /etc/apt/sources.list.d/*.list 2>/dev/null | head -1 | grep -q .; then + enterprise_active='yes' +fi +# New .sources/deb822 format: check for enterprise files that are NOT disabled +for f in /etc/apt/sources.list.d/*enterprise*.sources /etc/apt/sources.list.d/*enterprise*.list; do + [ -f "$f" ] || continue + # deb822: Enabled: false means disabled + if grep -qi '^Enabled:.*false' "$f" 2>/dev/null; then + continue + fi + # deb822: if it has enterprise in URIs and no Enabled: false, it's active + if grep -qi 'enterprise' "$f" 2>/dev/null; then + enterprise_active='yes' + fi +done +echo "$enterprise_active" echo '---NOSUB_REPO---' -test -f /etc/apt/sources.list.d/pve-no-subscription.list && echo 'yes' || echo 'no' +# Check both .list and .sources formats for no-subscription repo +nosub='no' +if grep -rq 'pve-no-subscription' /etc/apt/sources.list.d/ 2>/dev/null; then + nosub='yes' +fi +echo "$nosub" echo '---NAG_PATCHED---' -grep -q 'void({' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2>/dev/null && echo 'yes' || echo 'no' +# Check if the subscription nag popup has been patched +# PVE 8: void({ replaces Ext.Msg.show +# PVE 9: same pattern or similar +if grep -q 'void({' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2>/dev/null; then + echo 'yes' +elif ! grep -q "No valid subscription" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2>/dev/null; then + echo 'yes' # nag text removed entirely +else + echo 'no' +fi echo '---UPGRADABLE---' apt list --upgradable 2>/dev/null | grep -c upgradable || true echo '---VMBR1---' @@ -408,11 +440,14 @@ iptables -t nat -L POSTROUTING -n 2>/dev/null | grep -q MASQUERADE && echo 'yes' echo '---WG_INSTALLED---' command -v wg >/dev/null 2>&1 && echo 'yes' || echo 'no' echo '---WG_ACTIVE---' -wg show wg0 2>/dev/null | head -1 || echo 'none' +wg_out=$(wg show wg0 2>/dev/null | head -1) || true +if [ -n "$wg_out" ]; then echo "$wg_out"; else echo 'none'; fi echo '---WG_PEERS---' -wg show wg0 peers 2>/dev/null | wc -l || echo '0' +wg_peers=$(wg show wg0 peers 2>/dev/null | wc -l) || true +echo "${wg_peers:-0}" echo '---WG_HANDSHAKE---' -wg show wg0 latest-handshakes 2>/dev/null | head -1 || echo 'none' +wg_hs=$(wg show wg0 latest-handshakes 2>/dev/null | head -1) || true +if [ -n "$wg_hs" ]; then echo "$wg_hs"; else echo 'none'; fi echo '---NFT_FILE---' test -f /etc/nftables-hetzner.conf && echo 'yes' || echo 'no' echo '---NFT_RULES---' @@ -430,12 +465,28 @@ pveum user token list automation@pve --output-format json 2>/dev/null \ | python3 -c 'import sys,json; print("yes" if any(t["tokenid"]=="deploy" for t in json.load(sys.stdin)) else "no")' 2>/dev/null \ || echo 'no' echo '---AVAILABLE_DISKS---' -used_disks=$(zpool status 2>/dev/null | grep -oP '/dev/\S+' | sed 's|/dev/||' || true) +# Build list of whole-disk names that are part of ZFS pools. +# zpool status shows partition names (e.g. nvme0n1p3), so strip the +# partition suffix to get the parent disk name. +zfs_disks=$(zpool status 2>/dev/null \ + | grep -oP '(nvme\S+|sd[a-z]+\S*|vd[a-z]+\S*)' \ + | sed -E 's/p?[0-9]+$//' \ + | sort -u || true) lsblk -dno NAME,SIZE,TYPE | while read name size type; do [ "$type" != 'disk' ] && continue - echo "$used_disks" | grep -qw "$name" && continue - lsblk -no MOUNTPOINT /dev/$name 2>/dev/null | grep -q '/' && continue - echo "$name $size" + # Skip disks used by ZFS pools + echo "$zfs_disks" | grep -qw "$name" && continue + # Skip disks with active mount points + lsblk -no MOUNTPOINT "/dev/$name" 2>/dev/null | grep -q '/' && continue + # Check for existing partition table / signatures + partcount=$(lsblk -no NAME "/dev/$name" 2>/dev/null | wc -l) + if [ "$partcount" -gt 1 ]; then + # Has partitions -- detect type + fstype=$(lsblk -no FSTYPE "/dev/$name" 2>/dev/null | grep -v '^$' | head -1) + echo "$name $size partitioned:${fstype:-unknown}" + else + echo "$name $size clean" + fi done echo '---END---' DETECT @@ -482,10 +533,10 @@ _parse_section() { ZPOOLS) DETECT_ZPOOLS="$content" ;; PVE_STORAGE) DETECT_PVE_STORAGE="$content" ;; ENTERPRISE_REPOS) - if [[ "$content" == "none" ]]; then - DETECT_ENTERPRISE_ACTIVE="" + if [[ "$content" == "yes" ]]; then + DETECT_ENTERPRISE_ACTIVE="yes" else - DETECT_ENTERPRISE_ACTIVE="$content" + DETECT_ENTERPRISE_ACTIVE="" fi ;; NOSUB_REPO) DETECT_NOSUB_EXISTS="$content" ;; @@ -500,13 +551,13 @@ _parse_section() { NAT_ACTIVE) DETECT_NAT_ACTIVE="$content" ;; WG_INSTALLED) DETECT_WG_INSTALLED="$content" ;; WG_ACTIVE) - if [[ "$content" != "none" ]]; then + if [[ "$content" != "none" ]] && [[ -n "$content" ]]; then DETECT_WG_ACTIVE="yes" fi ;; WG_PEERS) DETECT_WG_PEERS="${content//[^0-9]/}" ;; WG_HANDSHAKE) - if [[ "$content" != "none" ]]; then + if [[ "$content" != "none" ]] && [[ -n "$content" ]]; then DETECT_WG_HANDSHAKE="$content" fi ;; @@ -685,8 +736,8 @@ show_status() { printf " %-20s %b" "Storage" "$(status_icon "$effective_storage")" case "$effective_storage" in - done) echo " ZFS pool registered" ;; - available) echo " ZFS pool exists, ${DETECT_AVAILABLE_DISK_COUNT} unused disk(s) available" ;; + done) echo " system pool registered, no extra disks" ;; + available) echo " system pool registered, ${DETECT_AVAILABLE_DISK_COUNT} extra disk(s) available" ;; needed) echo " ${DETECT_AVAILABLE_DISK_COUNT} unused disk(s), no pool created" ;; skipped) echo "" ;; esac @@ -787,13 +838,30 @@ apply_repos() { fi if [[ -n "$DETECT_ENTERPRISE_ACTIVE" ]]; then - remote "sed -i 's/^deb/# deb/' /etc/apt/sources.list.d/pve-enterprise.list 2>/dev/null || true" - remote "test -f /etc/apt/sources.list.d/ceph.list && sed -i 's/^deb/# deb/' /etc/apt/sources.list.d/ceph.list || true" + # Handle both .list (PVE 8 / bookworm) and .sources (PVE 9 / trixie) formats + # .list format: comment out deb lines + remote "for f in /etc/apt/sources.list.d/*enterprise*.list; do [ -f \"\$f\" ] && sed -i 's/^deb/# deb/' \"\$f\"; done 2>/dev/null || true" + # .sources/deb822 format: set Enabled: false + remote "for f in /etc/apt/sources.list.d/*enterprise*.sources; do [ -f \"\$f\" ] && sed -i '/^Enabled:/d' \"\$f\" && echo 'Enabled: false' >> \"\$f\"; done 2>/dev/null || true" success "Enterprise repos disabled" fi if [[ "$DETECT_NOSUB_EXISTS" != "yes" ]]; then - remote "echo 'deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription' > /etc/apt/sources.list.d/pve-no-subscription.list" + # Detect PVE version to choose format + local pve_major="${DETECT_PVE_VERSION%%.*}" + if [[ "${pve_major:-8}" -ge 9 ]]; then + # PVE 9+ uses deb822 .sources format + remote "cat > /etc/apt/sources.list.d/proxmox.sources <<'REPO' +Types: deb +URIs: http://download.proxmox.com/debian/pve +Suites: trixie +Components: pve-no-subscription +Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg +REPO" + else + # PVE 8 uses traditional .list format + remote "echo 'deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription' > /etc/apt/sources.list.d/pve-no-subscription.list" + fi success "No-subscription repo added" fi @@ -963,10 +1031,31 @@ apply_storage() { fi info "Available disks:" + local has_partitioned=false echo "$DETECT_AVAILABLE_DISKS" | while IFS= read -r line; do - if [[ -n "$line" ]]; then echo " /dev/$line"; fi + if [[ -n "$line" ]]; then + local dname dsize dstate + dname=$(echo "$line" | awk '{print $1}') + dsize=$(echo "$line" | awk '{print $2}') + dstate=$(echo "$line" | awk '{print $3}') + if [[ "$dstate" == partitioned:* ]]; then + local fsinfo="${dstate#partitioned:}" + echo -e " /dev/${dname} ${dsize} ${YELLOW}has existing partitions (${fsinfo})${RESET}" + else + echo " /dev/${dname} ${dsize} clean" + fi + fi done + # Check for partitioned disks that need wiping + local partitioned_disks + partitioned_disks=$(echo "$DETECT_AVAILABLE_DISKS" | awk '/partitioned:/{print "/dev/"$1}' | tr '\n' ' ') + if [[ -n "${partitioned_disks// /}" ]]; then + has_partitioned=true + warn "Disk(s) with existing partitions must be wiped before use in a ZFS pool" + warn "Affected: ${partitioned_disks}" + fi + local disk_count="$DETECT_AVAILABLE_DISK_COUNT" local disk_names disk_names=$(echo "$DETECT_AVAILABLE_DISKS" | awk 'NF{print "/dev/"$1}' | tr '\n' ' ') @@ -984,7 +1073,11 @@ apply_storage() { fi local pool_name - pool_name=$(prompt_value "Pool name" "local-zfs") + pool_name=$(prompt_value "Pool name" "vm-storage") + + if [[ "$has_partitioned" == true ]]; then + warn "This will wipe partition tables on: ${partitioned_disks}" + fi if ! confirm "Create ZFS pool '${pool_name}' (${pool_type}) with: ${disk_names}?"; then info "Skipped" @@ -995,7 +1088,14 @@ apply_storage() { return 0 fi - local zpool_cmd="zpool create ${pool_name}" + # Wipe partition tables on disks that have them + for disk in $partitioned_disks; do + info "Wiping partition table on ${disk}..." + remote "wipefs -a ${disk}" + remote "sgdisk -Z ${disk} 2>/dev/null || true" + done + + local zpool_cmd="zpool create -f ${pool_name}" case "$pool_type" in mirror) zpool_cmd="${zpool_cmd} mirror ${disk_names}" ;; raidz1) zpool_cmd="${zpool_cmd} raidz1 ${disk_names}" ;;