Windows VM Patterns
iGPU PCI Passthrough
Bind iGPU to vfio-pci via runtime sysfs manipulation (echo to
/sys/bus/pci/drivers/). Do NOT write modprobe config files (blacklist-igpu.conf,vfio-pci-igpu.conf). Sysfs-only binding is reversible without initramfs updates.NEVER bind the sole GPU on an AMD host to vfio-pci. The amdgpu driver removal triggers a kernel panic when it is the only framebuffer. Hard-fail if
lspci | grep -c 'VGA compatible controller'< 2 and vendor is AMD. Intel single-GPU hosts survive (SSH stays up).Cleanup MUST
modprobe -rthe GPU driver BEFOREmodprobeto re-probe devices. Simply runningmodprobe i915after vfio-pci unbind is a no-op if the module is already loaded. Sequence: unbind →modprobe -r→ PCI rescan →modprobe→ wait for DRI.Previous bug: cleanup removed
/etc/modprobe.d/blacklist-igpu.conf— a file the role never created. Cleanup MUST only remove artifacts the deploy creates.
Per-Host VMID Computation
When multiple hosts share a Proxmox node (common in test environments), compute VMID as
base + groups['flavor_group'].index(inventory_hostname). Apply the same computation in provision, verify, AND cleanup.The default cleanup list must include the full range of possible per-host VMIDs (base, base+1, base+2, ...) to cover all group sizes.
Image Upload: Use /var/tmp, Not /tmp
Proxmox
/tmpis often tmpfs (~7.8 GB). Windows qcow2 images are 8-18 GB. ALWAYS use/var/tmp/(real disk) for image uploads andqemu-img convertoutput.Previous bug:
qemu-img convertto/tmp/failed with "No space left on device" on a host with 8 GB tmpfs.
QEMU Guest Agent Communication
Use
qm guest cmd <vmid> pingto detect GA readiness. Useqm guest exec <vmid> --timeout N -- cmd /c "..."for in-VM commands. These bypass the network entirely — no SSH, no sshpass, no ProxyJump needed.IP discovery:
qm guest cmd <vmid> network-get-interfacesreturns JSON. Parse with Python, skip Loopback interfaces AND 169.254.x.x (APIPA/link-local) addresses, extract first routable IPv4 address. Retry until a DHCP lease is obtained.Previous bug: Guest Agent responded to ping before Windows DHCP client obtained a lease. The first IP returned was 169.254.143.117 (APIPA). SSH to this link-local address timed out. Fix: filter
169.254.prefix in the Python parser and increase retries.qm guest cmd pingis a simple command — useansible.builtin.command, notansible.builtin.shell. Only use shell when there's an actual pipeline.
Windows Unattended Installation
Disable Windows Defender and Windows Update in the
specializepass viaRunSynchronous+reg addto Group Policy keys. Do NOT useFirstLogonCommands— Tamper Protection blocks Defender changes from user-context commands.Re-enable Defender and set Windows Update to Manual at the end of
post-install.ps1. This prevents AV interference during setup but leaves the system in a safe state.Post-install completion: write a marker file (
C:\post-install-done.txt) at the end ofpost-install.ps1. Poll withqm guest execchecking for the marker. Hard-fail if marker not found within timeout — proceeding with an incomplete image violates the one-path principle.
PowerShell via Ansible
NEVER wrap
ansible.builtin.rawcommands inpowershell -Command "..."when the SSH shell is already PowerShell. The outer shell expands$variablesin double-quoted strings before passing to the inner PowerShell instance.Previous bug:
$svc = Get-Service ...inpowershell -Command "... $svc ..."— the outer PowerShell expanded$svcto empty, causing syntax errors.
EFI Disk Format
- On LVM-thin storage, EFI disks MUST use
format=raw. Theqcow2format is unsupported for EFI disks on LVM-thin and causes "unsupported format" errors.
LAN Host VM Connectivity
- VMs on LAN hosts get LAN IPs (e.g., 10.10.10.x) not directly routable from the controller. Add conditional
ProxyJumpvia PRIMARY_HOST when the host is inlan_hosts.