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 <noreply@anthropic.com>
This commit is contained in:
Maarten 2026-03-02 16:46:31 +01:00
parent a44458ae0c
commit a7eb09e248
1 changed files with 123 additions and 23 deletions

View File

@ -392,11 +392,43 @@ zpool list -H 2>/dev/null || true
echo '---PVE_STORAGE---' echo '---PVE_STORAGE---'
pvesm status 2>/dev/null || true pvesm status 2>/dev/null || true
echo '---ENTERPRISE_REPOS---' 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---' 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---' 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---' echo '---UPGRADABLE---'
apt list --upgradable 2>/dev/null | grep -c upgradable || true apt list --upgradable 2>/dev/null | grep -c upgradable || true
echo '---VMBR1---' echo '---VMBR1---'
@ -408,11 +440,14 @@ iptables -t nat -L POSTROUTING -n 2>/dev/null | grep -q MASQUERADE && echo 'yes'
echo '---WG_INSTALLED---' echo '---WG_INSTALLED---'
command -v wg >/dev/null 2>&1 && echo 'yes' || echo 'no' command -v wg >/dev/null 2>&1 && echo 'yes' || echo 'no'
echo '---WG_ACTIVE---' 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---' 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---' 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---' echo '---NFT_FILE---'
test -f /etc/nftables-hetzner.conf && echo 'yes' || echo 'no' test -f /etc/nftables-hetzner.conf && echo 'yes' || echo 'no'
echo '---NFT_RULES---' 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 \ | 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 'no'
echo '---AVAILABLE_DISKS---' 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 lsblk -dno NAME,SIZE,TYPE | while read name size type; do
[ "$type" != 'disk' ] && continue [ "$type" != 'disk' ] && continue
echo "$used_disks" | grep -qw "$name" && continue # Skip disks used by ZFS pools
lsblk -no MOUNTPOINT /dev/$name 2>/dev/null | grep -q '/' && continue echo "$zfs_disks" | grep -qw "$name" && continue
echo "$name $size" # 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 done
echo '---END---' echo '---END---'
DETECT DETECT
@ -482,10 +533,10 @@ _parse_section() {
ZPOOLS) DETECT_ZPOOLS="$content" ;; ZPOOLS) DETECT_ZPOOLS="$content" ;;
PVE_STORAGE) DETECT_PVE_STORAGE="$content" ;; PVE_STORAGE) DETECT_PVE_STORAGE="$content" ;;
ENTERPRISE_REPOS) ENTERPRISE_REPOS)
if [[ "$content" == "none" ]]; then if [[ "$content" == "yes" ]]; then
DETECT_ENTERPRISE_ACTIVE="" DETECT_ENTERPRISE_ACTIVE="yes"
else else
DETECT_ENTERPRISE_ACTIVE="$content" DETECT_ENTERPRISE_ACTIVE=""
fi fi
;; ;;
NOSUB_REPO) DETECT_NOSUB_EXISTS="$content" ;; NOSUB_REPO) DETECT_NOSUB_EXISTS="$content" ;;
@ -500,13 +551,13 @@ _parse_section() {
NAT_ACTIVE) DETECT_NAT_ACTIVE="$content" ;; NAT_ACTIVE) DETECT_NAT_ACTIVE="$content" ;;
WG_INSTALLED) DETECT_WG_INSTALLED="$content" ;; WG_INSTALLED) DETECT_WG_INSTALLED="$content" ;;
WG_ACTIVE) WG_ACTIVE)
if [[ "$content" != "none" ]]; then if [[ "$content" != "none" ]] && [[ -n "$content" ]]; then
DETECT_WG_ACTIVE="yes" DETECT_WG_ACTIVE="yes"
fi fi
;; ;;
WG_PEERS) DETECT_WG_PEERS="${content//[^0-9]/}" ;; WG_PEERS) DETECT_WG_PEERS="${content//[^0-9]/}" ;;
WG_HANDSHAKE) WG_HANDSHAKE)
if [[ "$content" != "none" ]]; then if [[ "$content" != "none" ]] && [[ -n "$content" ]]; then
DETECT_WG_HANDSHAKE="$content" DETECT_WG_HANDSHAKE="$content"
fi fi
;; ;;
@ -685,8 +736,8 @@ show_status() {
printf " %-20s %b" "Storage" "$(status_icon "$effective_storage")" printf " %-20s %b" "Storage" "$(status_icon "$effective_storage")"
case "$effective_storage" in case "$effective_storage" in
done) echo " ZFS pool registered" ;; done) echo " system pool registered, no extra disks" ;;
available) echo " ZFS pool exists, ${DETECT_AVAILABLE_DISK_COUNT} unused disk(s) available" ;; 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" ;; needed) echo " ${DETECT_AVAILABLE_DISK_COUNT} unused disk(s), no pool created" ;;
skipped) echo "" ;; skipped) echo "" ;;
esac esac
@ -787,13 +838,30 @@ apply_repos() {
fi fi
if [[ -n "$DETECT_ENTERPRISE_ACTIVE" ]]; then if [[ -n "$DETECT_ENTERPRISE_ACTIVE" ]]; then
remote "sed -i 's/^deb/# deb/' /etc/apt/sources.list.d/pve-enterprise.list 2>/dev/null || true" # Handle both .list (PVE 8 / bookworm) and .sources (PVE 9 / trixie) formats
remote "test -f /etc/apt/sources.list.d/ceph.list && sed -i 's/^deb/# deb/' /etc/apt/sources.list.d/ceph.list || true" # .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" success "Enterprise repos disabled"
fi fi
if [[ "$DETECT_NOSUB_EXISTS" != "yes" ]]; then 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" success "No-subscription repo added"
fi fi
@ -963,10 +1031,31 @@ apply_storage() {
fi fi
info "Available disks:" info "Available disks:"
local has_partitioned=false
echo "$DETECT_AVAILABLE_DISKS" | while IFS= read -r line; do 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 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_count="$DETECT_AVAILABLE_DISK_COUNT"
local disk_names local disk_names
disk_names=$(echo "$DETECT_AVAILABLE_DISKS" | awk 'NF{print "/dev/"$1}' | tr '\n' ' ') disk_names=$(echo "$DETECT_AVAILABLE_DISKS" | awk 'NF{print "/dev/"$1}' | tr '\n' ' ')
@ -984,7 +1073,11 @@ apply_storage() {
fi fi
local pool_name 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 if ! confirm "Create ZFS pool '${pool_name}' (${pool_type}) with: ${disk_names}?"; then
info "Skipped" info "Skipped"
@ -995,7 +1088,14 @@ apply_storage() {
return 0 return 0
fi 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 case "$pool_type" in
mirror) zpool_cmd="${zpool_cmd} mirror ${disk_names}" ;; mirror) zpool_cmd="${zpool_cmd} mirror ${disk_names}" ;;
raidz1) zpool_cmd="${zpool_cmd} raidz1 ${disk_names}" ;; raidz1) zpool_cmd="${zpool_cmd} raidz1 ${disk_names}" ;;