From 2c98f176ad6af17e1bafc5adc0b73f2f8cbcf3a9 Mon Sep 17 00:00:00 2001 From: gabriel engvall Date: Wed, 10 Jun 2026 16:24:59 +0200 Subject: [PATCH] feat(cluster-templates): pick a per-node storage instead of one fixed name A single STORAGE name cannot fit a mixed cluster: a node installed with ZFS has local-zfs but no local-lvm, and the previous default ("local", a directory storage) is often not enabled for VM images at all, so qmrestore failed on such nodes. Selection order, evaluated on each node: 1. CF_TEMPLATE_STORAGE, when it is active on that node 2. local-lvm, then local-zfs (the standard installer storages) 3. last resort: the best active images-capable storage - local over shared, VM-native types (lvmthin/zfspool/btrfs/rbd/lvm) over directory storage, most free space first The default for CF_TEMPLATE_STORAGE changes from "local" to "local-lvm" to match what a stock Proxmox install actually uses for VM disks; nodes without it fall through the chain above instead of failing. --- scripts/cf-cluster-templates.sh | 34 +++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/scripts/cf-cluster-templates.sh b/scripts/cf-cluster-templates.sh index b4f7bda..4675c22 100755 --- a/scripts/cf-cluster-templates.sh +++ b/scripts/cf-cluster-templates.sh @@ -25,7 +25,10 @@ BASE_VMID="${CF_BUILT_VMID:?CF_BUILT_VMID not set}" DUMP_DIR="${PVE_DUMP_DIR:-/var/lib/vz/dump}" # --- knobs (edit to taste) ------------------------------------------------- -STORAGE="${CF_TEMPLATE_STORAGE:-local}" # per-node disk storage for the template +# Preferred per-node disk storage. Nodes that don't have it (e.g. a ZFS node +# with local-zfs instead of local-lvm) auto-pick their best images-capable +# storage: local over shared, then most free space. +STORAGE="${CF_TEMPLATE_STORAGE:-local-lvm}" OFFSET="${CF_TEMPLATE_VMID_OFFSET:-10000}" # per-node VMID spacing # --------------------------------------------------------------------------- @@ -48,7 +51,7 @@ mapfile -t NODES < <( ) [ "${#NODES[@]}" -gt 0 ] || { echo "cf-cluster-templates: no online cluster nodes found"; exit 0; } -echo "==> $BN -> clonable template on ${#NODES[@]} node(s) (storage=$STORAGE)" +echo "==> $BN -> clonable template on ${#NODES[@]} node(s) (preferred storage=$STORAGE)" for line in "${NODES[@]}"; do read -r ID IP <<<"$line" @@ -70,6 +73,29 @@ for line in "${NODES[@]}"; do # to skip the ssh roundtrip. RESTORE_SCRIPT=$(cat </dev/null | python3 -c " +import json, sys +rows = [s for s in json.load(sys.stdin) if s.get('active')] +names = [s['storage'] for s in rows] +for pref in ('$STORAGE', 'local-lvm', 'local-zfs'): + if pref in names: + print(pref) + break +else: + local = [s for s in rows if not s.get('shared')] + rows = local if local else rows + vm_native = ('lvmthin', 'zfspool', 'btrfs', 'rbd', 'lvm') + rows.sort(key=lambda s: (0 if s.get('type') in vm_native else 1, -s.get('avail', 0))) + print(rows[0]['storage'] if rows else '') +") +if [ -z "\$STG" ]; then + echo " [fail] no active images-capable storage on \$(hostname)" + exit 1 +fi if qm status $VMID >/dev/null 2>&1; then if ! qm config $VMID 2>/dev/null | grep -q '^template:'; then echo " [skip] VMID $VMID is a real (non-template) VM — leaving it alone" @@ -81,11 +107,11 @@ fi # qmrestore only accepts vzdump-style filenames, so rename before restoring VZ="$DUMP_DIR/vzdump-qemu-$VMID-$STAMP.vma.zst" mv "$DUMP_DIR/$BN" "\$VZ" -qmrestore "\$VZ" $VMID --storage $STORAGE --unique 1 >/dev/null +qmrestore "\$VZ" $VMID --storage "\$STG" --unique 1 >/dev/null rm -f "\$VZ" # cofoundry artifacts are already templates after restore; only convert if not qm config $VMID 2>/dev/null | grep -q '^template:' || qm template $VMID >/dev/null -echo " [ok] template $VMID on $STORAGE" +echo " [ok] template $VMID on \$STG" EOF ) if [[ "$LOCAL_IPS" == *" $IP "* ]]; then