I used the Simos Windows VM creation guide as a blueprint to automate the creation of a Windows VM using Incus with a Windows Server Core ISO (without any GUI).
ISO: Windows Server Download | MAS
The instance configuration is as follows:
instance := api.InstancesPost{
Name: args.Name,
Type: api.InstanceTypeVM,
Source: api.InstanceSource{
Type: "none",
},
InstancePut: api.InstancePut{
Config: map[string]string{
"limits.cpu": "4",
"limits.memory": "4GiB",
// Expose QEMU monitor via TCP to send the "any key" bypass
"raw.qemu": "-device intel-hda -device hda-duplex -audio spice -monitor tcp:" + monitorAddr + ",server,nowait",
"raw.qemu.conf": "[audiodev \"qemu_spice-audiodev\"]\ndriver = \"none\"",
},
Devices: map[string]map[string]string{
"root": {
"type": "disk",
"pool": "default",
"path": "/",
"size": "25GiB",
},
"vtpm": {"type": "tpm"},
"eth0": {
"type": "nic",
"network": "incusbr0",
},
"install": {
"type": "disk",
"source": path.Join(downloadsDir, "windows-server.iso"),
"boot.priority": "10",
},
// ISO containing SSH and winfsp
"software": {
"type": "disk",
"io.bus": "usb",
"source": path.Join(isoDir, "software.iso"),
},
"autounattend": {
"type": "disk",
"io.bus": "usb",
"source": isoPath,
},
"virtio": {
"type": "disk",
// USB bus supported since incus v6.11
"io.bus": "usb",
"source": path.Join(downloadsDir, "virtio.iso"),
},
},
},
}
To automate the installation and setup of some software, I used an unattended ISO, which loads some Virtio drivers using a DriverPaths directive, enabling Windows to correctly detect the storage.
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
<Path>D:\vioscsi\w11\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
<Path>E:\vioscsi\w11\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
<Path>D:\vioscsi\2k25\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
<Path>E:\vioscsi\2k25\amd64</Path>
</PathAndCredentials>
</DriverPaths>
As for the install software script is as follows:
function Get-DriveByFile {
param([string]$FileName)
# Check PSDrives first, then Volumes
$drive = (Get-PSDrive | Where-Object { Test-Path "$($_.Name):\$FileName" }).Name
if (-not $drive) {
$drive = (Get-Volume | Where-Object { Test-Path "$($_.DriveLetter):\$FileName" }).DriveLetter
}
return $drive
}
function Install-VirtioTools {
$drive = Get-DriveByFile "virtio-win-guest-tools.exe"
if (-not $drive) {
Write-Warning "VirtIO ISO not found."; return
}
Write-Host "Found VirtIO ISO on $drive. Trusting certificates..."
$certFile = Get-ChildItem -Path "$($drive):\cert\*.cat" -Recurse | Select-Object -First 1
if ($certFile) {
certutil -addstore "TrustedPublisher" $certFile.FullName
}
Write-Host "Installing Guest Tools..."
Start-Process -FilePath "$($drive):\virtio-win-guest-tools.exe" -ArgumentList "/passive", "/norestart" -Wait
Start-Sleep -Seconds 10
}
# --- OPENSSH INSTALLATION ---
function Install-OpenSSH {
$drive = Get-DriveByFile "OpenSSH\install-sshd.ps1"
if (-not $drive) {
Write-Warning "OpenSSH source not found on any drive."; return
}
$dest = "C:\OpenSSH"
if (-not (Test-Path $dest)) { New-Item -ItemType Directory -Path $dest -Force }
Copy-Item -Path "$($drive):\OpenSSH\*" -Destination $dest -Recurse -Force
# Remove "Read-Only" attribute from all copied files
Get-ChildItem -Path $dest -Recurse | ForEach-Object {
if ($_.Attributes -match "ReadOnly") {
$_.Attributes = 'Archive'
}
}
# Run the official install script
Set-Location $dest
powershell.exe -ExecutionPolicy Bypass -File ".\install-sshd.ps1"
$psPath = (Get-Command powershell.exe).Source
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value $psPath -PropertyType String -Force
# Configure Service and Firewall
Set-Service -Name sshd -StartupType 'Automatic'
Start-Service sshd
if (!(Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
}
}
function Install-Redistribuable {
$drive = Get-DriveByFile "vc_redist.exe"
if (-not $drive) {
Write-Warning "vc_redist installer not found."; return
}
Start-Process -FilePath "$($drive):\vc_redist.exe" -ArgumentList "/install" "/passive", "/norestart" -Wait
Start-Sleep -Seconds 10
}
function Install-WinFSP {
# Searches for a file matching winfsp-*.msi
$drive = Get-DriveByFile "winfsp.msi"
if (-not $drive) {
Write-Warning "WinFSP installer not found."; return
}
$msiPath = Get-ChildItem -Path "$($drive):\winfsp.msi" | Select-Object -First 1
Write-Host "Installing WinFSP from $($msiPath.FullName)..."
Copy-Item $msiPath "C:\winfsp.msi"
# INSTALLLEVEL=1000 ensures all features (including FUSE and Developer tools) are installed
$arguments = "/i `"C:\winfsp.msi`" ADDLOCAL=ALL /qn /norestart"
Start-Process "msiexec.exe" -ArgumentList $arguments -Wait
Write-Host "WinFSP installation complete."
}
Install-Redistribuable
Install-WinFSP
Install-VirtioTools
Install-OpenSSH
Set-Service -Name "VirtioFsSvc" -StartupType Automatic
Start-Service -Name "VirtioFsSvc"
# Final Shutdown
Write-Output "Setup complete. Shutting down..."
Start-Sleep -Seconds 10
shutdown.exe /s /t 0 /f
The issue is that even if the VirtioFsSvc service is running when I run incus config device add win SHARED disk source=/home/…/ path=SHARED no volume letter is assigned, unlike what is shown in the guide capture.
I don’t know what’s wrong with my setup.
PS C:\Users\admin> Get-Service -Name VirtioFsSvc
Status Name DisplayName
------ ---- -----------
Running VirtioFsSvc VirtIO-FS Service
PS C:\Users\admin> Get-Volume
DriveLetter FriendlyName FileSystemType DriveType HealthStatus OperationalStatus SizeRemaining Size
----------- ------------ -------------- --------- ------------ ----------------- ------------- ----
SYSTEM FAT32 Fixed Healthy OK 65.12 MB 96 MB
C Windows NTFS Fixed Healthy OK 15.25 GB 24.88 GB
PS C:\Users\admin>