Windows launcher only creates a differential disk; seed via Unattend.xml

run-windows11.py no longer installs Windows or touches ISO/img files: it
creates a QCOW2 overlay on the base image (made by vm-images/build-windows-base.sh)
and, when the disk is created, mounts seed.iso. Windows reads the Unattend.xml
in its root during the specialize and OOBE passes (host name, account, auto
logon, Remote Desktop) and Configure-Windows.ps1 then installs OpenSSH with the
host's key, turns off Windows Update and copies the installer scripts.

The VM hardware uses devices with inbox Windows drivers (IDE/NVMe, e1000e,
ramfb on arm64), since the base image has no VirtIO drivers. Autounattend.xml
moves to vm-images; the vm-images pointer is updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GuofangandClaude Sonnet 5.5 committed 2026-11-02 11:19:48 +08:00
1 parent fe7250870e
commit a920ba0ca5
11 files changed
+456 -735

No files matched your search

+38 -33
View File
@@ -48,8 +48,7 @@ corresponding `*_STATE_DIR` environment variable:
With `--download-only`, a launcher only downloads the media it would fetch
automatically and then exits without creating a VM disk or starting QEMU: the
base image for Debian, AlmaLinux, Fedora and Ubuntu, and the VirtIO tools ISO
for Windows 11 (whose installation ISO and base QCOW2 are supplied manually):
base image for Debian, AlmaLinux, Fedora and Ubuntu:
```bash
./run-fedora44.py --download-only
@@ -165,43 +164,49 @@ from the shared Windows 11 QCOW2 base image:
./run-windows11.py
```
It creates a persistent 64G disk at `vm/windows11/disk.qcow2`, automatically
partitions it, completes OOBE, creates the local administrator `guofang` with
the initial password `wang`, and enables Remote Desktop on
`0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on the host itself). During the first login it also silently installs the VirtIO
drivers (the guest tools installer on amd64, the Arm64 driver packages from the
VirtIO ISO with `pnputil` on arm64), without which the guest has no network on
arm64. The installer includes the VirtIO drivers, SPICE vdagent, and QEMU Guest
Agent. The launcher exposes the vdagent channel for host/guest clipboard
integration and the QEMU Guest Agent at `vm/windows11/qemu-guest-agent.sock`.
Change the password after first login. The OpenSSH Server is installed during
the first logon from the Win32-OpenSSH zip for the host architecture, which the
seed ISO carries (downloaded by `vm-images/download-images.sh openssh-win`), so
Windows Update is not needed. Connect with `ssh guofang@localhost -p 2222`; the
seed ISO also carries the host's SSH public key (`$SSH_PUBLIC_KEY_FILE`, else the
first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, `id_rsa.pub`) for passwordless
administrator login.
It creates a persistent 64G disk at `vm/windows11/disk.qcow2` as a QCOW2
overlay on the base image and nothing else: the launcher only creates the
differential disk and starts QEMU. The base image is generic and holds nothing
but Windows (see `vm-images/README.md`): no account, no SSH, no VirtIO or other
hardware-specific drivers, no machine identity. Like a cloud image, each VM is
configured on its first boot, from the seed ISO.
Whenever the disk does not exist yet, the launcher also mounts the seed ISO
`vm/windows11/seed.iso` (beside the disk; built from `seediso/windows11/` when
missing or outdated, with the OpenSSH zip and the host's SSH public key,
`$SSH_PUBLIC_KEY_FILE` or the first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`,
`id_rsa.pub`). Windows reads the `Unattend.xml` in the root of that CD-ROM
during its specialize and OOBE passes: host name `WIN11`, time zone, Remote
Desktop on `0.0.0.0:3389` (all host interfaces; connect to `localhost:3389` on
the host itself), the local administrator `guofang` (initial password `wang`;
change it after first login) and automatic logon. At the first logon it runs
`Configure-Windows.ps1` from the seed ISO, which installs the OpenSSH Server
from the zip with the SSH key (connect with `ssh guofang@localhost -p 2222`),
turns off Windows Update and copies the numbered installer scripts
(`seediso/windows11/NN-*.bat` and `.ps1`) to the desktop folder `Tools`. Later
starts do not mount the seed ISO.
Because the image has no VirtIO drivers, the VM only uses devices that Windows
has inbox drivers for: an IDE disk and e1000e NIC on amd64, an NVMe disk, e1000e
NIC and ramfb display on arm64 (a fixed-size framebuffer there). The SPICE
vdagent and QEMU Guest Agent come with the VirtIO guest tools, so host/guest
clipboard integration and the guest agent are not available unless you install
those tools in the VM yourself.
The Windows VM hardware (8G memory, 8 CPUs, system disk, devices)
is defined in `qemu/windows11/config.ini`, with the accelerators and machine type in
`qemu/windows11/<os>-<arch>.ini`; the launcher renders the former to
`vm/windows11/qemu.ini` and loads both with `-readconfig`. Only the network
(see Port forwards) and the installation media are still set by the launcher. QEMU arguments after
(see Port forwards) and the seed ISO are still set by the launcher. QEMU arguments after
`--` override the config files, for example `./run-windows11.py -- -m 4G -smp 4`.
The generated answer-file ISO is stored beside the disk as
`vm/windows11/seed.iso`.
To install from the Windows ISO instead, use `./run-windows11.py
--install-from-iso`. This choice only applies while creating a missing system
disk; subsequent starts always boot the existing system disk normally. During
ISO installation, the launcher resolves its tools ISO through the shared image
helper and downloads it when missing. Override the base QCOW2, source ISO,
tools ISO, RDP port, or SSH port with the `WIN11_VM_` environment
variables shown by `./run-windows11.py --help`. For example, when the VirtIO ISO
is elsewhere:
```bash
WIN11_VM_TOOLS_ISO=/custom/path/virtio-win.iso ./run-windows11.py --install-from-iso
```
The launcher has no installation options and stops with a hint if the base image
is missing. Make the base image once with `vm-images/build-windows-base.sh`,
which is self-contained in `vm-images/` (its own answer file, QEMU config and
installation logic, written in bash): it installs Windows unattended from the
ISO into a scratch disk, runs `sysprep` as its last step and stores the disk as
the read-only base image. Override the base QCOW2, seed ISO, RDP port, or SSH
port with the `WIN11_VM_` environment variables shown by
`./run-windows11.py --help`.
## Port forwards