Skip to content

PoC: Windows Guest Support via autounattend.xml (Issue #4852) #5013

Draft
liketosweep wants to merge 1 commit into
lima-vm:masterfrom
liketosweep:poc
Draft

PoC: Windows Guest Support via autounattend.xml (Issue #4852) #5013
liketosweep wants to merge 1 commit into
lima-vm:masterfrom
liketosweep:poc

Conversation

@liketosweep
Copy link
Copy Markdown

Summary

This PoC implements fully automated, unattended Windows Server 2025 installation inside a QEMU VM by generating and delivering a Windows Answer File (autounattend.xml) .


Approach

Two new Go packages -

pkg/windows/autounattend - a typed, validated XML generator using html/template. takes a Config struct (hostname, credentials, SSH public keys, locale, edition index, extra commands) and renders a complete three-pass answer file. Passwords are never stored as plaintext they are encoded as base64(UTF-16LE(password + "Password")) per the Microsoft answer file specification before being written into XML, enforced by a dedicated security test.

pkg/windows/cidata - a Go ISO builder that stages autounattend.xml and first_logon.ps1 into a temp directory, detects whichever ISO tool is available on the host (genisoimage, mkisofs, or xorriso), builds the ISO atomically via a .tmp rename, and validates the output is non-empty before returning. This replaces ad-hoc shell calls with a reusable, error-handled package.

Why ISO delivery, not floppy -

The Windows installer (WinPE) scans all CD/DVD drives for autounattend.xml at boot. Modern WinPE builds do not reliably scan floppy devices. The answer file and provisioning script are bundled into a single ISO image (win-cidata.iso) and presented to QEMU as a third CD-ROM drive, which the installer finds and processes automatically before showing any UI.

VirtIO driver injection in windowsPE pass -

Without loading the viostor (VirtIO storage) driver during the windowsPE setup pass, Windows PE cannot see a virtio-backed disk and installation fails. The PnpCustomizationsWinPE component in the answer file points to the VirtIO-Win driver ISO (mounted as a second CD-ROM) so the disk is visible to the installer from the start.

Partition layout -

A two-partition MBR layout (100 MB NTFS System + remainder as Windows C:) is used. This is compatible with the legacy BIOS boot mode QEMU defaults to without OVMF firmware, making the setup dependency-free — no swtpm, no OVMF binaries required.

Structured provisioning via first_logon.ps1 -

A single <FirstLogonCommands> entry calls first_logon.ps1 from the cidata ISO. The script runs under Set-StrictMode -Version Latest and $ErrorActionPreference = 'Stop', wrapping each phase (OpenSSH install, firewall rule, SSH key injection, WinFSP, VirtIO-FS service) in an Invoke-Step helper that logs success or failure per step and exits immediately on any error. The SSH public key is passed as a -SSHKey parameter rather than read from a separate file, keeping the provisioning interface clean.


Results

Verification Result
Go unit tests (9/9) PASS — including PasswordNeverPlaintext security test
go build ./pkg/windows/... Exit code 0
xmllint --noout autounattend.xml Valid
isoinfo -l shows AUTOUNA.XML + FIRST_LO.PS1 Confirmed
Windows Server 2025 installed without any UI interaction Confirmed
Get-Service sshd → Running Confirmed
administrators_authorized_keys contains Lima SSH public key Confirmed
C:\lima-setup.logAll steps completed successfully Confirmed
SSH from WSL host into guest (ssh -p 60022 limauser@localhost) Confirmed

Files Changed

pkg/windows/autounattend/autounattend.go   — Config struct, generator, UTF-16LE encoder, hostname sanitiser
pkg/windows/autounattend/template.xml      — embedded answer file template (three-pass, VirtIO drivers, encoded password)
pkg/windows/autounattend/autounattend_test.go — 9 unit tests including security assertion
pkg/windows/cidata/cidata.go               — ISO builder: tool detection, atomic write, output validation
poc-windows-guest/autounattend.xml         — pre-rendered answer file for manual PoC testing
poc-windows-guest/first_logon.ps1          — structured provisioning script with StrictMode + per-step error handling
poc-windows-guest/run.sh                   — launch script with trap-based cleanup, prereq validation, colour output

Next Steps (full implementation)

  • Wire pkg/windows/autounattend and pkg/windows/cidata into Lima's instance creation flow via pkg/cidata/cidata.go dispatch on guestOS: windows
  • Add guestOS field and windowsEditionIndex param to limayaml
  • Add Windows-specific QEMU args (machine type, CD-ROM ordering) to pkg/qemu/qemu.go
  • Add windows.yaml example template to examples/
  • Extend integration tests for Windows guest lifecycle

Image Proofs

Go Package Tests - 90.2% statement coverage, all 9 tests pass

image

ISO Contents and Answer File Verification

image

Unattended Installation - Windows installing autonomously from autounattend.xml

image

Fully Automated Boot - Windows Server 2025 running with hostname set from config

image

SSH Access - Remote login from WSL host into Windows guest

image

Provisioning Log - All first_logon.ps1 steps completed successfully

image

Proposed solution for other issues as defined in #4907

Issue #4961 - Windows host support without depending on QEMU (Hyper-V / HCS)

I intend to use Microsoft's hcsshim library (github.com/microsoft/hcsshim) to create a new vmType: hcs driver in pkg/vz-style that calls hcsshim.CreateComputeSystem with a JSON schema document.

Issue #4820 - Autoappend %PROGRAMFILES%\QEMU etc. to %PATH% on Windows hosts

I intend to add to Lima's QEMU binary resolution logic: on runtime.GOOS == "windows" , probe a list of well-known install locations (%PROGRAMFILES%\QEMU, %PROGRAMFILES(X86)%\QEMU , the Chocolatey shim path) using os.Stat before falling back to exec.LookPath , and prepend any found directory to the resolved path.

Issue #4819 - Drop dependency on cygpath.exe, MSYS2, and Git for Windows

I intend to use Go's standard library tools filepath.ToSlash , filepath.FromSlash , strings.TrimPrefix , filepath.VolumeName to produce /c/Users/... -style paths natively. Replacing the WindowsSubsystemPath shell-out in pkg/ioutilx/ioutilx.go with a pure-Go implementation .

##Seperate Prs would be raised for catering these issues


Assistance taken from Claude opus for the documentation of this Pr.

…s issue lima-vm#4852.

Signed-off-by: liketosweep <liketosweep@gmail.com>
@jandubois
Copy link
Copy Markdown
Member

@jandubois jandubois marked this pull request as draft May 19, 2026 22:20
@liketosweep
Copy link
Copy Markdown
Author

Hey @jandubois thanks for the review , I've gone through the report and working on resolving the associated issues .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants