Skip to content

Commit f3cd7c9

Browse files
committed
poc: add Windows guest support via autounattend.xml
Add dual-architecture (amd64/arm64) unattended Windows installation using autounattend.xml templates. Includes Joliet ISO generation, architecture-specific template selection, unit tests, and a standalone QEMU test script (hack/gen-autounattend-iso.go) for both architectures. Key design decisions: - arm64 uses NVMe disk (built-in WinPE driver, no virtio-win dependency) - amd64 uses AHCI/SATA via q35 (built-in WinPE driver) - No product keys in either template - OpenSSH server installed during OOBE for Lima host-agent connectivity Tested end-to-end: - Windows 10 Pro x86_64 on QEMU (emulation) - Windows 11 Pro ARM64 on QEMU+HVF (Apple Silicon, native speed) Signed-off-by: ashwat287 <ashwatpas@gmail.com>
1 parent 50c551a commit f3cd7c9

9 files changed

Lines changed: 688 additions & 3 deletions

File tree

hack/gen-autounattend-iso.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// gen-autounattend-iso generates an autounattend.iso for testing the Windows
5+
// guest POC with QEMU.
6+
// Usage: go run hack/gen-autounattend-iso.go [-arch amd64|arm64] [-o autounattend.iso]
7+
package main
8+
9+
import (
10+
"bytes"
11+
"flag"
12+
"fmt"
13+
"os"
14+
15+
"github.com/lima-vm/lima/v2/pkg/cidata"
16+
"github.com/lima-vm/lima/v2/pkg/iso9660util"
17+
)
18+
19+
func main() {
20+
outPath := flag.String("o", "autounattend.iso", "output ISO file path")
21+
arch := flag.String("arch", "amd64", "target architecture: amd64 or arm64")
22+
flag.Parse()
23+
24+
if *arch != "amd64" && *arch != "arm64" {
25+
fmt.Fprintf(os.Stderr, "error: -arch must be amd64 or arm64, got %q\n", *arch)
26+
os.Exit(1)
27+
}
28+
29+
args := &cidata.TemplateArgs{
30+
Name: "windows-test",
31+
}
32+
33+
xmlBytes, err := cidata.ExecuteTemplateAutounattend(args, *arch)
34+
if err != nil {
35+
fmt.Fprintf(os.Stderr, "error rendering template: %v\n", err)
36+
os.Exit(1)
37+
}
38+
39+
// Also write the raw XML for inspection
40+
xmlPath := *outPath + ".xml"
41+
if err := os.WriteFile(xmlPath, xmlBytes, 0o644); err != nil {
42+
fmt.Fprintf(os.Stderr, "error writing XML: %v\n", err)
43+
os.Exit(1)
44+
}
45+
fmt.Fprintf(os.Stdout, "Wrote %s (%d bytes)\n", xmlPath, len(xmlBytes))
46+
47+
layout := []iso9660util.Entry{
48+
{
49+
Path: "autounattend.xml",
50+
Reader: bytes.NewReader(xmlBytes),
51+
},
52+
}
53+
54+
// Only include startup.nsh for amd64 (EFI Shell fallback for OVMF on x86_64)
55+
if *arch == "amd64" {
56+
layout = append(layout, iso9660util.Entry{
57+
Path: "startup.nsh",
58+
Reader: bytes.NewReader([]byte("@echo -off\r\n" +
59+
"echo Searching for Windows Boot Manager...\r\n" +
60+
"FS0:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
61+
"FS1:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
62+
"FS2:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
63+
"FS3:\\EFI\\BOOT\\BOOTX64.EFI\r\n" +
64+
"FS4:\\EFI\\BOOT\\BOOTX64.EFI\r\n")),
65+
})
66+
}
67+
68+
if err := iso9660util.Write(*outPath, "autounattend", layout, iso9660util.WithJoliet()); err != nil {
69+
fmt.Fprintf(os.Stderr, "error writing ISO: %v\n", err)
70+
os.Exit(1)
71+
}
72+
73+
fi, _ := os.Stat(*outPath)
74+
fmt.Fprintf(os.Stdout, "Wrote %s (%d bytes)\n", *outPath, fi.Size())
75+
76+
if *arch == "amd64" {
77+
printAmd64Usage(*outPath)
78+
} else {
79+
printArm64Usage(*outPath)
80+
}
81+
}
82+
83+
func printAmd64Usage(outPath string) {
84+
fmt.Fprintln(os.Stdout, "\nUsage with QEMU amd64 (UEFI boot required):")
85+
fmt.Fprintln(os.Stdout, " qemu-img create -f qcow2 disk.qcow2 64G")
86+
fmt.Fprintln(os.Stdout, " dd if=/dev/zero of=ovmf_vars.fd bs=1M count=4")
87+
fmt.Fprintln(os.Stdout, " qemu-system-x86_64 -m 4G -smp 2 -machine q35 \\")
88+
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \\")
89+
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,file=ovmf_vars.fd \\")
90+
fmt.Fprintln(os.Stdout, " -cdrom /path/to/windows.iso \\")
91+
fmt.Fprintf(os.Stdout, " -drive file=%s,media=cdrom,index=3 \\\n", outPath)
92+
fmt.Fprintln(os.Stdout, " -drive file=disk.qcow2,index=0,media=disk,format=qcow2 \\")
93+
fmt.Fprintln(os.Stdout, " -nic user,hostfwd=tcp::2222-:22")
94+
fmt.Fprintln(os.Stdout, "\n On macOS (Homebrew): replace OVMF paths with")
95+
fmt.Fprintln(os.Stdout, " /opt/homebrew/share/qemu/edk2-x86_64-code.fd")
96+
fmt.Fprintln(os.Stdout, "\n On Windows (PowerShell, WHPX): add -accel whpx")
97+
}
98+
99+
func printArm64Usage(outPath string) {
100+
fmt.Fprintln(os.Stdout, "\nUsage with QEMU aarch64 on Apple Silicon (HVF):")
101+
fmt.Fprintln(os.Stdout, " qemu-img create -f qcow2 disk.qcow2 64G")
102+
fmt.Fprintln(os.Stdout, " dd if=/dev/zero of=ovmf_vars.fd bs=1M count=64")
103+
fmt.Fprintln(os.Stdout, "")
104+
fmt.Fprintln(os.Stdout, " qemu-system-aarch64 \\")
105+
fmt.Fprintln(os.Stdout, " -machine virt,accel=hvf \\")
106+
fmt.Fprintln(os.Stdout, " -cpu host \\")
107+
fmt.Fprintln(os.Stdout, " -m 4G -smp 4 \\")
108+
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,readonly=on,file=/opt/homebrew/share/qemu/edk2-aarch64-code.fd \\")
109+
fmt.Fprintln(os.Stdout, " -drive if=pflash,format=raw,file=ovmf_vars.fd \\")
110+
fmt.Fprintln(os.Stdout, " -drive file=disk.qcow2,if=none,id=hd0,format=qcow2 \\")
111+
fmt.Fprintln(os.Stdout, " -device nvme,serial=lima0,drive=hd0 \\")
112+
fmt.Fprintln(os.Stdout, " -device qemu-xhci \\")
113+
fmt.Fprintln(os.Stdout, " -device usb-kbd \\")
114+
fmt.Fprintln(os.Stdout, " -device usb-tablet \\")
115+
fmt.Fprintln(os.Stdout, " -drive file=/path/to/Win11_ARM64.iso,if=none,id=cdrom0,media=cdrom,readonly=on \\")
116+
fmt.Fprintln(os.Stdout, " -device usb-storage,drive=cdrom0 \\")
117+
fmt.Fprintf(os.Stdout, " -drive file=%s,if=none,id=unattend,media=cdrom,readonly=on \\\n", outPath)
118+
fmt.Fprintln(os.Stdout, " -device usb-storage,drive=unattend \\")
119+
fmt.Fprintln(os.Stdout, " -device ramfb \\")
120+
fmt.Fprintln(os.Stdout, " -nic user,hostfwd=tcp::2222-:22")
121+
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Windows unattended answer file for Lima guest VMs (amd64).
3+
Ref: https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/update-windows-settings-and-scripts-create-your-own-answer-file-sxs -->
4+
<unattend xmlns="urn:schemas-microsoft-com:unattend">
5+
6+
<!-- windowsPE: locale, disk layout, image selection, hw bypass -->
7+
<settings pass="windowsPE">
8+
<component name="Microsoft-Windows-International-Core-WinPE"
9+
processorArchitecture="amd64"
10+
publicKeyToken="31bf3856ad364e35"
11+
language="neutral"
12+
versionScope="nonSxS"
13+
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
14+
<SetupUILanguage>
15+
<UILanguage>en-US</UILanguage>
16+
</SetupUILanguage>
17+
<InputLocale>en-US</InputLocale>
18+
<SystemLocale>en-US</SystemLocale>
19+
<UILanguage>en-US</UILanguage>
20+
<UserLocale>en-US</UserLocale>
21+
</component>
22+
23+
<component name="Microsoft-Windows-Setup"
24+
processorArchitecture="amd64"
25+
publicKeyToken="31bf3856ad364e35"
26+
language="neutral"
27+
versionScope="nonSxS"
28+
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
29+
30+
<!-- Bypass Win11 hardware checks (TPM, SecureBoot, RAM, etc.) — irrelevant in a VM -->
31+
<RunSynchronous>
32+
<RunSynchronousCommand wcm:action="add">
33+
<Order>1</Order>
34+
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
35+
</RunSynchronousCommand>
36+
<RunSynchronousCommand wcm:action="add">
37+
<Order>2</Order>
38+
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
39+
</RunSynchronousCommand>
40+
<RunSynchronousCommand wcm:action="add">
41+
<Order>3</Order>
42+
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
43+
</RunSynchronousCommand>
44+
<RunSynchronousCommand wcm:action="add">
45+
<Order>4</Order>
46+
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassCPUCheck /t REG_DWORD /d 1 /f</Path>
47+
</RunSynchronousCommand>
48+
<RunSynchronousCommand wcm:action="add">
49+
<Order>5</Order>
50+
<Path>reg add "HKLM\SYSTEM\Setup\LabConfig" /v BypassStorageCheck /t REG_DWORD /d 1 /f</Path>
51+
</RunSynchronousCommand>
52+
<RunSynchronousCommand wcm:action="add">
53+
<Order>6</Order>
54+
<Path>reg add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f</Path>
55+
</RunSynchronousCommand>
56+
</RunSynchronous>
57+
58+
<UserData>
59+
<AcceptEula>true</AcceptEula>
60+
<FullName>User</FullName>
61+
<Organization>Lima</Organization>
62+
<ProductKey>
63+
<WillShowUI>Never</WillShowUI>
64+
</ProductKey>
65+
</UserData>
66+
67+
<!-- GPT partition layout for UEFI boot.
68+
NOTE: On amd64, disk is attached via SATA/AHCI so WinPE can see it without extra drivers. -->
69+
<DiskConfiguration>
70+
<Disk wcm:action="add">
71+
<DiskID>0</DiskID>
72+
<WillWipeDisk>true</WillWipeDisk>
73+
<CreatePartitions>
74+
<CreatePartition wcm:action="add">
75+
<Order>1</Order>
76+
<Type>EFI</Type>
77+
<Size>100</Size>
78+
</CreatePartition>
79+
<CreatePartition wcm:action="add">
80+
<Order>2</Order>
81+
<Type>MSR</Type>
82+
<Size>16</Size>
83+
</CreatePartition>
84+
<CreatePartition wcm:action="add">
85+
<Order>3</Order>
86+
<Type>Primary</Type>
87+
<Extend>true</Extend>
88+
</CreatePartition>
89+
</CreatePartitions>
90+
<ModifyPartitions>
91+
<ModifyPartition wcm:action="add">
92+
<Order>1</Order>
93+
<PartitionID>1</PartitionID>
94+
<Label>System</Label>
95+
<Format>FAT32</Format>
96+
</ModifyPartition>
97+
<ModifyPartition wcm:action="add">
98+
<Order>2</Order>
99+
<PartitionID>2</PartitionID>
100+
</ModifyPartition>
101+
<ModifyPartition wcm:action="add">
102+
<Order>3</Order>
103+
<PartitionID>3</PartitionID>
104+
<Label>Windows</Label>
105+
<Format>NTFS</Format>
106+
<Letter>C</Letter>
107+
</ModifyPartition>
108+
</ModifyPartitions>
109+
</Disk>
110+
</DiskConfiguration>
111+
112+
<!-- Index 6 = Pro on Win10/Win11 x86_64 multi-edition ISOs -->
113+
<ImageInstall>
114+
<OSImage>
115+
<InstallTo>
116+
<DiskID>0</DiskID>
117+
<PartitionID>3</PartitionID>
118+
</InstallTo>
119+
<InstallToAvailablePartition>false</InstallToAvailablePartition>
120+
<OSImageIndex>6</OSImageIndex>
121+
</OSImage>
122+
</ImageInstall>
123+
124+
</component>
125+
</settings>
126+
127+
<!-- specialize: machine identity (runs after file copy, before first boot) -->
128+
<settings pass="specialize">
129+
<component name="Microsoft-Windows-Shell-Setup"
130+
processorArchitecture="amd64"
131+
publicKeyToken="31bf3856ad364e35"
132+
language="neutral"
133+
versionScope="nonSxS"
134+
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
135+
<ComputerName>lima-default</ComputerName>
136+
<TimeZone>UTC</TimeZone>
137+
</component>
138+
</settings>
139+
140+
<!-- oobeSystem: suppress OOBE prompts, create user, install OpenSSH -->
141+
<settings pass="oobeSystem">
142+
<component name="Microsoft-Windows-Shell-Setup"
143+
processorArchitecture="amd64"
144+
publicKeyToken="31bf3856ad364e35"
145+
language="neutral"
146+
versionScope="nonSxS"
147+
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
148+
<OOBE>
149+
<HideEULAPage>true</HideEULAPage>
150+
<HideLocalAccountScreen>false</HideLocalAccountScreen>
151+
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
152+
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
153+
<NetworkLocation>Work</NetworkLocation>
154+
<ProtectYourPC>3</ProtectYourPC>
155+
<SkipMachineOOBE>true</SkipMachineOOBE>
156+
<SkipUserOOBE>true</SkipUserOOBE>
157+
</OOBE>
158+
<UserAccounts>
159+
<LocalAccounts>
160+
<LocalAccount wcm:action="add">
161+
<Name>lima</Name>
162+
<DisplayName>lima</DisplayName>
163+
<Group>Administrators</Group>
164+
<Password>
165+
<Value></Value>
166+
<PlainText>true</PlainText>
167+
</Password>
168+
</LocalAccount>
169+
</LocalAccounts>
170+
</UserAccounts>
171+
<AutoLogon>
172+
<Enabled>true</Enabled>
173+
<Username>lima</Username>
174+
<Password>
175+
<Value></Value>
176+
<PlainText>true</PlainText>
177+
</Password>
178+
<LogonCount>3</LogonCount>
179+
</AutoLogon>
180+
181+
<!-- Install and configure OpenSSH server for Lima host-agent connectivity -->
182+
<FirstLogonCommands>
183+
<SynchronousCommand wcm:action="add">
184+
<Order>1</Order>
185+
<CommandLine>powershell.exe -NoProfile -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0"</CommandLine>
186+
<Description>Install OpenSSH Server</Description>
187+
</SynchronousCommand>
188+
<SynchronousCommand wcm:action="add">
189+
<Order>2</Order>
190+
<CommandLine>powershell.exe -NoProfile -Command "Start-Service sshd; Set-Service -Name sshd -StartupType Automatic"</CommandLine>
191+
<Description>Start and enable sshd</Description>
192+
</SynchronousCommand>
193+
<SynchronousCommand wcm:action="add">
194+
<Order>3</Order>
195+
<CommandLine>powershell.exe -NoProfile -Command "New-Item -Path 'C:\ProgramData\ssh' -ItemType Directory -Force"</CommandLine>
196+
<Description>Create SSH config directory</Description>
197+
</SynchronousCommand>
198+
<SynchronousCommand wcm:action="add">
199+
<Order>4</Order>
200+
<CommandLine>powershell.exe -NoProfile -Command "Set-Content -Path 'C:\ProgramData\ssh\administrators_authorized_keys' -Value 'ssh-rsa PLACEHOLDER'; icacls 'C:\ProgramData\ssh\administrators_authorized_keys' /inheritance:r /grant 'SYSTEM:(F)' /grant 'Administrators:(F)'"</CommandLine>
201+
<Description>Write authorized_keys with correct ACLs</Description>
202+
</SynchronousCommand>
203+
<SynchronousCommand wcm:action="add">
204+
<Order>5</Order>
205+
<CommandLine>powershell.exe -NoProfile -Command "$cfg = 'C:\ProgramData\ssh\sshd_config'; (Get-Content $cfg) -replace '#?AuthorizedKeysFile.*', 'AuthorizedKeysFile .ssh/authorized_keys' | Set-Content $cfg; Add-Content $cfg 'Match Group administrators`r`n AuthorizedKeysFile C:/ProgramData/ssh/administrators_authorized_keys'; Restart-Service sshd"</CommandLine>
206+
<Description>Configure sshd for admin key auth</Description>
207+
</SynchronousCommand>
208+
<SynchronousCommand wcm:action="add">
209+
<Order>6</Order>
210+
<CommandLine>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon /t REG_SZ /d 0 /f</CommandLine>
211+
<Description>Disable auto-logon after provisioning</Description>
212+
</SynchronousCommand>
213+
<SynchronousCommand wcm:action="add">
214+
<Order>7</Order>
215+
<CommandLine>powershell.exe -NoProfile -Command "New-NetFirewallRule -Name 'OpenSSH-Server' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22"</CommandLine>
216+
<Description>Allow SSH through firewall</Description>
217+
</SynchronousCommand>
218+
</FirstLogonCommands>
219+
</component>
220+
<component name="Microsoft-Windows-International-Core"
221+
processorArchitecture="amd64"
222+
publicKeyToken="31bf3856ad364e35"
223+
language="neutral"
224+
versionScope="nonSxS"
225+
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
226+
<InputLocale>en-US</InputLocale>
227+
<SystemLocale>en-US</SystemLocale>
228+
<UILanguage>en-US</UILanguage>
229+
<UserLocale>en-US</UserLocale>
230+
</component>
231+
</settings>
232+
233+
</unattend>

0 commit comments

Comments
 (0)