Add OVA/OVF import tools and virtio-win downloader

Two implementations of the same OVA/OVF import flow: a Python version that
parses the OVF with ElementTree and detects UEFI, and a shell version using
xmllint that handles multi-disk appliances. Both check free space on the
target storage before importing.

download-virtio-win-progress.py fetches the latest virtio-win ISO, skipping
the download when the local copy is already current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ergosteur
2026-08-06 15:44:49 +00:00
co-authored by Claude Opus 5
parent 3ef068e846
commit 25b96af194
3 changed files with 380 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
#!/bin/bash
set -euo pipefail
# === OVF/OVA Importer for Proxmox ===
# Supports: --force, OVF parsing, accurate storage check, multi-disk VMDK, CPU/RAM
FORCE=0
POSITIONAL=()
# === Parse args manually to allow --force anywhere ===
for arg in "$@"; do
case "$arg" in
--force)
FORCE=1
;;
*)
POSITIONAL+=("$arg")
;;
esac
done
set -- "${POSITIONAL[@]}"
if [[ $# -lt 3 ]]; then
echo "Usage: $0 <ova|ovf-file> <vmid> <storage> [vm-name] [--force]"
exit 1
fi
INPUT="$1"
VMID="$2"
STORAGE="$3"
VMNAME="${4:-}"
# === Auto-suggest VM name from filename if not given ===
if [[ -z "$VMNAME" || "$VMNAME" == "--force" ]]; then
BASENAME=$(basename "$INPUT")
BASENAME="${BASENAME%.*}"
VMNAME=$(echo "$BASENAME" | sed -E 's/[^a-zA-Z0-9]+/-/g' | sed -E 's/^-+|-+$//g' | tr '[:upper:]' '[:lower:]')
echo "==> No VM name provided."
echo "==> Suggested VM Name: $VMNAME"
echo "==> Continuing in 5 seconds... (Ctrl-C to cancel)"
sleep 5
fi
# === Validate VM ID ===
if qm status "$VMID" &>/dev/null; then
echo "❌ ERROR: VM ID $VMID already exists"
exit 1
else
echo "✅ VM ID $VMID is available"
fi
# === Validate storage ===
if ! pvesm status | awk '{print $1}' | grep -qx "$STORAGE"; then
echo "❌ ERROR: Storage '$STORAGE' not found!"
echo "👉 Available storages:"
pvesm status | awk 'NR>1 {print $1}'
exit 1
else
echo "✅ Storage '$STORAGE' exists"
fi
# === Get free space in bytes from pvesm (field 6 × 1024) ===
STORAGE_FREE_KB=$(pvesm status | awk -v storage="$STORAGE" '$1 == storage {print $6}')
if [[ -z "$STORAGE_FREE_KB" || "$STORAGE_FREE_KB" -eq 0 ]]; then
echo "❌ ERROR: Could not determine available space for '$STORAGE'"
exit 1
fi
STORAGE_FREE=$((STORAGE_FREE_KB * 1024))
# === Temp workspace ===
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo "==> Preparing to import VM $VMID from $INPUT (name: $VMNAME)"
# === Extract OVA or OVF ===
if [[ "$INPUT" == *.ova ]]; then
echo "==> Detected OVA bundle. Extracting..."
tar -xvf "$INPUT" -C "$TMPDIR"
elif [[ "$INPUT" == *.ovf ]]; then
echo "==> Detected OVF file. Copying OVF and VMDKs..."
cp "$INPUT" "$TMPDIR"
for vmdk in "${INPUT%.ovf}"*.vmdk; do cp "$vmdk" "$TMPDIR"; done
else
echo "❌ ERROR: Input must be .ova or .ovf"
exit 1
fi
cd "$TMPDIR"
OVF=$(find . -name "*.ovf" | head -n 1)
[[ -f "$OVF" ]] || { echo "❌ ERROR: OVF file not found!"; exit 1; }
# === Get total declared capacity from OVF ===
REQUIRED_CAPACITY=$(grep -oP 'ovf:capacity="\K[0-9]+' "$OVF" | awk '{sum+=$1} END{print sum}')
REQUIRED_FMT=$(numfmt --to=iec "$REQUIRED_CAPACITY" 2>/dev/null || echo "$REQUIRED_CAPACITY B")
STORAGE_FMT=$(numfmt --to=iec "$STORAGE_FREE" 2>/dev/null || echo "$STORAGE_FREE B")
echo "🧮 Storage free space: $STORAGE_FMT"
echo "📦 Total VMDK declared size: $REQUIRED_FMT"
if [[ "$FORCE" -eq 1 ]]; then
echo "⚠️ --force enabled, skipping free space check"
elif [[ -z "$REQUIRED_CAPACITY" || -z "$STORAGE_FREE" ]]; then
echo "❌ ERROR: Capacity values missing"
exit 1
elif ! [[ "$REQUIRED_CAPACITY" =~ ^[0-9]+$ && "$STORAGE_FREE" =~ ^[0-9]+$ ]]; then
echo "❌ ERROR: Capacity values not integers"
exit 1
elif (( STORAGE_FREE < REQUIRED_CAPACITY )); then
echo "❌ Not enough free space on storage '$STORAGE'"
exit 1
else
echo "✅ Storage has enough capacity"
fi
# === Parse RAM and CPU ===
OVF_RAM=$(xmllint --xpath 'string(//*[local-name()="Item"][.//*[local-name()="ResourceType"]="4"]/*[local-name()="VirtualQuantity"])' "$OVF" 2>/dev/null || echo "2048")
OVF_CPU=$(xmllint --xpath 'string(//*[local-name()="Item"][.//*[local-name()="ResourceType"]="3"]/*[local-name()="VirtualQuantity"])' "$OVF" 2>/dev/null || echo "2")
[[ "$OVF_RAM" =~ ^[0-9]+$ ]] || OVF_RAM=2048
[[ "$OVF_CPU" =~ ^[0-9]+$ ]] || OVF_CPU=2
echo "🧠 Parsed RAM from OVF: ${OVF_RAM} MB"
echo "🧮 Parsed CPU count from OVF: $OVF_CPU"
# === Create VM ===
echo "==> Creating VM $VMID..."
qm create "$VMID" --name "$VMNAME" --memory "$OVF_RAM" --cores "$OVF_CPU" --net0 virtio,bridge=vmbr0 --ostype l26
# === Parse VMDK files reliably ===
mapfile -t VMDK_FILES < <(xmllint --xpath "//*[local-name()='File']/@*" "$OVF" 2>/dev/null \
| grep -oE 'href="[^"]+\.vmdk"' | sed -E 's/^href="|"$//g')
if [[ ${#VMDK_FILES[@]} -eq 0 ]]; then
echo "❌ ERROR: No VMDK files found in OVF"
exit 1
fi
echo "📄 VMDK files to import:"
for f in "${VMDK_FILES[@]}"; do echo " - $f"; done
i=0
for VMDK in "${VMDK_FILES[@]}"; do
VMDK_FILE=$(basename "$VMDK")
[[ -f "$VMDK_FILE" ]] || { echo "❌ Missing VMDK file: $VMDK_FILE"; exit 1; }
echo "💾 Importing $VMDK_FILE to $STORAGE..."
qm importdisk "$VMID" "$VMDK_FILE" "$STORAGE" --format qcow2
echo "🔗 Attaching disk as scsi$i..."
qm set "$VMID" --scsi$i "$STORAGE:vm-$VMID-disk-$i"
((i++))
done
# === Finalize configuration ===
qm set "$VMID" --scsihw virtio-scsi-pci --boot c --bootdisk scsi0
qm set "$VMID" --vga std
echo "✅ VM $VMID ('$VMNAME') successfully imported and ready to start!"