Normalize the offline image root path passed to DISM (fixes exit code 2 on driver injection)
Invoke-VirtIODrivers
Downloads the latest VirtIO ISO if necessary, copies the ISO locally if necessary, mounts the ISO automatically, dynamically detects the operating system, and installs the correct drivers for Proxmox virtual machines into the Windows driver store using DISM or PNPUTIL. The QEMU guest agent can optionally be installed directly from the mounted ISO when running within the full operating system.
This script can be run from a WindowsPE boot image after the operating system has been deployed to the fixed disk and the target volume will be located automatically by locating a valid installation of Windows.
This script can also be directly used within the full operating system to install the drivers after the fact or before a hypervisor migration. (VMWare to Proxmox).
Structure
The script follows the standardized script template and toolkit flow. The Toolkit\Toolkit.ps1 script is dot sourced during initialization and provides the logging, error handling, environment detection, and function/module/library loading infrastructure. The reusable helper functions live within Toolkit\Functions, the registry parsing libraries live within Toolkit\Libraries\Registry, the disk image cmdlet definition lives within Toolkit\Libraries\Storage, and the archive utility lives within Toolkit\Tools.
The ISO is mounted through the storage CIM provider directly rather than through the Storage PowerShell module, because that module is not present within every WindowsPE boot image. The MSFT_DiskImage class is projected as cmdlets using the Toolkit\Libraries\Storage\DiskImage.cdxml definition, so only the storage CIM provider itself is required.
The script is compatible with both Windows PowerShell 5.1 (including the WindowsPE PowerShell optional component) and PowerShell 7.
Usage
WindowsPE (boot image) — DeployR boot images only contain PowerShell 7, so use pwsh.exe:
pwsh.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File ".\Invoke-VirtIODrivers.ps1" -Install -InstallGuestAgent -InstallSpiceGuestTools
Full operating system:
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File ".\Invoke-VirtIODrivers.ps1" -Install -InstallGuestAgent -InstallSpiceGuestTools
The same switches are safe to use in both places. Everything is gated in code, so nothing breaks:
- Within WindowsPE,
-InstallGuestAgentand-InstallSpiceGuestToolsare skipped automatically (both require the full operating system) and the skip is logged. Only the driver injection runs. - Within the full operating system, all three run.
- The guest agent is skipped when the installed version is already current, and the SPICE guest tools are skipped when already installed.
That means you can use one command line for both passes and let the script decide what applies.
| Parameter | Description |
|---|---|
-Install |
Download the VirtIO driver ISO (if required) and install the relevant drivers for the detected operating system. |
-InstallGuestAgent |
Install the QEMU guest agent from the mounted ISO. Recommended on every run: the installation is skipped automatically within WindowsPE (where the Windows Installer service is unavailable) and when the installed version is already current. |
-InstallSpiceGuestTools |
Download and install the SPICE guest tools. Skipped automatically within WindowsPE and when already installed. The driver signing certificates are imported into the trusted publishers store beforehand so the installation is completely silent. |
-DownloadURL |
Override the VirtIO ISO download URL. Defaults to the latest stable VirtIO ISO. |
-DownloadDestinationDirectory |
Override the ISO download destination. Defaults to Content\ISOs beside the script. Any ISO already present there is used as-is regardless of its file name (the latest one wins), and the download is skipped entirely. |
-SpiceGuestToolsDownloadURL |
Override the SPICE guest tools download URL. |
-SpiceGuestToolsDestinationDirectory |
Override the SPICE guest tools download destination. |
-ArchiveUtilityDownloadURL |
Override the archive utility download URL, used only when no local copy is present. |
-LogDirectory |
Override the log directory. Sensible defaults are used for WindowsPE, task sequence, and full operating system scenarios. |
The ISO download automatically detects and uses the system default proxy with default credentials.
Anything downloaded at runtime is written beneath %WinDir%\Temp\Invoke-VirtIODrivers, so running the script from a read-only or UNC location is not a problem.
SPICE guest tools
The SPICE guest tools installer is not digitally signed and exposes no version resource, so the driver signing certificates are read from the driver catalogs contained within the installer itself. The installer is expanded using the archive utility in Toolkit\Tools, every distinct signing certificate is imported into the local machine trusted publishers store, the expanded content is removed, and the installer is then executed using its own normal installation method. The result is a completely silent installation with no driver prompts.
The archive utility is located in Toolkit\Tools\<ProcessorArchitecture> (for example X64), then Toolkit\Tools\All, and finally the local download directory. A copy ships with the toolkit, so no download is required; if one is ever needed, the package is expanded using an administrative installation and the product itself is never installed.
OS deployment scenarios (DeployR, MDT, SCCM)
During OS deployment the script needs to run twice:
- Boot image (WindowsPE) — after the operating system image has been applied, run with
-Install -InstallGuestAgent -InstallSpiceGuestToolsto inject the VirtIO drivers into the offline operating system using DISM, so the deployed OS can boot on VirtIO virtual hardware on first startup (the guest agent and SPICE guest tools portions are skipped automatically within WindowsPE). - Full operating system — run the same command again to register the drivers with pnputil, install the QEMU guest agent, and install the SPICE guest tools.
OS detection is handled automatically in both passes, so the command line is the same aside from the interpreter. DeployR boot images only contain PowerShell 7, so the script must be launched with pwsh.exe there:
pwsh.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File ".\Invoke-VirtIODrivers.ps1" -Install -InstallGuestAgent -InstallSpiceGuestTools
Note: the boot image itself must already contain the VirtIO drivers (added ahead of time using your preferred boot image servicing method), otherwise WindowsPE cannot see VirtIO SCSI disks or network adapters. Alternatively, the virtual machine can use non-VirtIO virtual hardware (for example SATA disks and an emulated E1000 network adapter), which works without servicing the boot image but carries a performance penalty.
Virtual machine templates and hypervisor migration
Template creation: run the script within the full operating system with -Install -InstallGuestAgent before converting the virtual machine into a template, so every clone comes up VirtIO-ready with the QEMU guest agent already installed.
Hypervisor migration (VMware ESXi to Proxmox): run the script within the full operating system with -Install -InstallGuestAgent before the migration. The VirtIO devices do not exist yet at that point, so pnputil simply stages the driver packages into the Windows driver store. On first boot under the new hypervisor, Plug and Play matches the staged drivers automatically and the VM boots on VirtIO SCSI disk controllers and virtual network adapters without a recovery pass. No more blue screens!
This exact flow was used in a production migration from VMware ESXi to Proxmox: the script was run within the full operating system of each VM before migration, and after cutover, over 30 Windows VMs came up without issue.
Re-running the script is always safe: the Windows driver store handles already-present driver packages (pnputil and DISM skip or version-rank existing packages rather than creating conflicts), so drivers that are already installed are never a problem.
Note: Invoke-VirtIODrivers.exe is just there as a powershell bootstrapper. If you double click the executable, it simply executes the Powershell script with the same name automatically and shows the execution window. Nothing more.
Offline registry hive detection
Within WindowsPE, the deployed operating system details are read directly from the offline SOFTWARE registry hive of the deployed volume without loading or mounting the hive, using the Eric Zimmerman Registry library (version 2026.5.0, netstandard2.0), which parses the raw REGF hive file format. The library and its dependency closure are stored within Toolkit\Libraries\Registry and are loaded into memory in dependency order (no file locks): byte loaded within Windows PowerShell, and stream loaded into a dedicated assembly load context within PowerShell 7.
This is sample output of the Windows Driver Store using Powershell after the drivers have been installed.
Command: Get-WindowsDriver -Online | Where-Object {($_.ProviderName -imatch '(.*Red.*Hat.*)')} | Select-Object -Property @('OriginalFileName', 'ClassName', 'BootCritical', 'ProviderName', 'Version', 'ClassGUID')
| BootCritical | ClassGuid | ClassName | OriginalFileName | ProviderName | Version |
|---|---|---|---|---|---|
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\balloon.inf_amd64_eaf9fe5ccc46cea0\balloon.inf | Red Hat, Inc. | 100.94.104.24800 |
| False | {4D36E972-E325-11CE-BFC1-08002BE10318} | Net | C:\Windows\System32\DriverStore\FileRepository\netkvm.inf_amd64_108667f5ebeb0ad0\netkvm.inf | Red Hat, Inc. | 100.94.104.24800 |
| False | {4D36E975-E325-11CE-BFC1-08002BE10318} | NetTrans | C:\Windows\System32\DriverStore\FileRepository\vioprot.inf_amd64_5abf6da903f19370\vioprot.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\viofs.inf_amd64_9d8003dbf4948969\viofs.inf | Red Hat, Inc. | 100.94.104.24800 |
| False | {4D36E968-E325-11CE-BFC1-08002BE10318} | Display | C:\Windows\System32\DriverStore\FileRepository\viogpudo.inf_amd64_d108681ae5f48232\viogpudo.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {745A17A0-74D3-11D0-B6FE-00A0C90F57DA} | HIDClass | C:\Windows\System32\DriverStore\FileRepository\vioinput.inf_amd64_32a7b4d6e1632c93\vioinput.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\viorng.inf_amd64_dfa2dff76d3c06c9\viorng.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\vioser.inf_amd64_650b7c25b9f9e8bc\vioser.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97B-E325-11CE-BFC1-08002BE10318} | SCSIAdapter | C:\Windows\System32\DriverStore\FileRepository\vioscsi.inf_amd64_9717e9d0dbb31583\vioscsi.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97B-E325-11CE-BFC1-08002BE10318} | SCSIAdapter | C:\Windows\System32\DriverStore\FileRepository\viostor.inf_amd64_3e677331b798639a\viostor.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\fwcfg.inf_amd64_c9590a85c7935d96\fwcfg.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\pvpanic.inf_amd64_5041c2d4340b58fb\pvpanic.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\pvpanic-pci.inf_amd64_d9c6c27cd62af2f6\pvpanic-pci.inf | Red Hat, Inc. | 100.94.104.24800 |
| True | {4D36E97D-E325-11CE-BFC1-08002BE10318} | System | C:\Windows\System32\DriverStore\FileRepository\smbus.inf_amd64_5f03787cbdf7a56d\smbus.inf | Red Hat, Inc. | 100.0.0.0 |