Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
334 lines
16 KiB
Markdown
334 lines
16 KiB
Markdown
# Installing Windows via netboot.xyz
|
||
|
||
Works for KVM VMs and physical machines. Windows Setup needs a real filesystem for the
|
||
~4 GB `install.wim`, so the flow is: **wimboot → WinPE (HTTP) → SMB media → setup.exe**.
|
||
|
||
> This covers **NT6+ (Vista/7/8/10/11, Server 2008–2025)**. Older Windows works completely
|
||
> differently — see **`NT5.md`** for Windows 2000/XP/Server 2003, and **`9x.md`** for
|
||
> Windows 95/98/ME (via win98-quickinstall's Linux installer).
|
||
|
||
```
|
||
iPXE Windows menu
|
||
└─ wimboot loads WinPE (boot.wim) over HTTP from this host (assets/WinPE/x64/)
|
||
└─ WinPE boots to a cmd prompt; wpeinit brings up the NIC
|
||
└─ net use → mount the SMB share with the extracted ISO
|
||
└─ setup.exe → installs Windows to the local disk
|
||
```
|
||
|
||
## Already set up on the server (done)
|
||
|
||
- **SMB share** `\\192.168.10.127\win` — **guests get read-only, passwordless** (WinPE mounts
|
||
it this way); the AD user **`panxiao81` has read-write** (`write list = DDUPAN\panxiao81`) for
|
||
staging images/media. Backed by ZFS dataset `data/win` → `/mnt/pool/win`. Holds `menu.cmd`
|
||
(the version picker) and the `winpe-build/` driver kit. Managed by the `samba_member` Ansible
|
||
role (`samba-ad/`), not a hand-edited `smb.conf`.
|
||
- **`win_base_url`** = `http://192.168.10.127:8080/WinPE` — set in both
|
||
`config/menus/local-vars.ipxe` and `config/menus/boot.cfg` (the latter covers clients whose
|
||
firmware is already iPXE and skips `local-vars`).
|
||
- **WinPE** built into `assets/WinPE/x64/` (base PE extracted from a Server 2025 ISO; NIC/
|
||
storage drivers injected with **DISM** — see step 1 / Driver notes), with a `startnet.cmd`
|
||
that brings up networking, auto-mounts the share, and launches the picker.
|
||
- Only remaining step for a real install: drop a version folder onto the share (step 2).
|
||
|
||
## Verified (PXE-tested)
|
||
|
||
Driven end-to-end in a KVM VM on `br0`: iPXE Windows menu → wimboot loaded the WinPE over
|
||
HTTP → WinPE booted → `startnet.cmd` ran `wpeinit`, mounted `\\192.168.10.127\win`, and
|
||
launched `menu.cmd`, which showed the (empty) picker. So the whole path works; adding a
|
||
version folder makes it installable. Note: give the target **≥4 GB RAM** (2 GB bugchecks the
|
||
RAM-loaded WinPE and reboots).
|
||
|
||
## What you do
|
||
|
||
### 1. Build WinPE
|
||
|
||
The WinPE at `assets/WinPE/x64/` is **already built and PXE-tested** (see "Verified" below).
|
||
Two ways to (re)build it:
|
||
|
||
**A. On Linux, no Windows box needed (how it was built here).** A Windows installation ISO's
|
||
`sources/boot.wim` *is* a modern WinPE. Extract its bare-PE image with `wimlib-imagex` and
|
||
inject a startup script that auto-mounts the share and runs the picker:
|
||
|
||
```bash
|
||
ISO=~/zh-cn_windows_server_2025_..._x64_dvd.iso # any modern Windows/Server ISO
|
||
OUT=/home/panxiao81/services/apps/netboot/assets/WinPE/x64
|
||
sudo mount -o loop,ro "$ISO" /mnt/winiso
|
||
mkdir -p "$OUT/boot" "$OUT/sources"
|
||
cp /mnt/winiso/bootmgr "$OUT/bootmgr"
|
||
cp /mnt/winiso/bootmgr.efi "$OUT/bootmgr.efi"
|
||
cp /mnt/winiso/boot/bcd "$OUT/boot/bcd"
|
||
cp /mnt/winiso/boot/boot.sdi "$OUT/boot/boot.sdi"
|
||
# export image 1 ("Windows PE") as a single bootable wim
|
||
wimlib-imagex export /mnt/winiso/sources/boot.wim 1 "$OUT/sources/boot.wim" --boot
|
||
# auto-run our startup: wpeinit + mount \\host\win + launch menu.cmd (startnet.cmd is CRLF)
|
||
wimlib-imagex update "$OUT/sources/boot.wim" 1 --command="delete --force /Windows/System32/startnet.cmd"
|
||
wimlib-imagex update "$OUT/sources/boot.wim" 1 --command="add /path/to/startnet.cmd /Windows/System32/startnet.cmd"
|
||
sudo umount /mnt/winiso
|
||
```
|
||
|
||
Use the **newest** Windows/Server ISO you have — a WinPE installs any OS at or below its
|
||
version. Drivers are injected separately with **DISM** (see **Driver notes**), so the baked-in
|
||
`startnet.cmd` no longer needs `drvload` — it just brings up networking (with a DHCP retry
|
||
loop, since a freshly-loaded NIC can be a few seconds behind the first DISCOVER) and launches
|
||
the picker:
|
||
|
||
```bat
|
||
wpeinit REM PnP auto-loads the DISM-injected NIC driver
|
||
reg add HKLM\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters /v AllowInsecureGuestAuth /t REG_DWORD /d 1 /f
|
||
:netwait REM retry until a real 192.168.10.x lease appears
|
||
wpeutil InitializeNetwork
|
||
ipconfig | find "192.168.10." >nul && goto neton
|
||
ipconfig /renew >nul & ping 127.0.0.1 -n 4 >nul & goto netwait
|
||
:neton
|
||
net use Z: \\192.168.10.127\win
|
||
if exist Z:\menu.cmd call Z:\menu.cmd
|
||
cmd
|
||
```
|
||
|
||
**B. On a Windows box (Windows ADK).** `copype amd64 C:\winpe` → `MakeWinPEMedia /ISO ...`,
|
||
then copy the ISO/media contents into `assets/WinPE/x64/`. Edit `boot.wim`'s
|
||
`Windows\System32\startnet.cmd` to the same script as above. Use this if you want ADK's
|
||
optional components or a custom PE. (This same ADK box is where drivers get DISM-injected —
|
||
see Driver notes.)
|
||
|
||
Either way the tree must be:
|
||
|
||
```
|
||
assets/WinPE/x64/
|
||
├── bootmgr
|
||
├── bootmgr.efi
|
||
├── boot/bcd (BCD store — the menu also tries Boot/BCD)
|
||
├── boot/boot.sdi
|
||
└── sources/boot.wim (your WinPE image, single bootable index)
|
||
```
|
||
|
||
netboot.xyz loads exactly those five files from `${win_base_url}/x64/`. (`wimboot` itself is
|
||
fetched from public `boot.netboot.xyz` — fine as long as the host has internet.) Give WinPE
|
||
**≥4 GB RAM** on the target — the wim is RAM-loaded and 2 GB bugchecks → reboot.
|
||
|
||
### 2. Populate the SMB share with install media
|
||
|
||
Put each Windows version in **its own subfolder** under `/mnt/pool/win` — extract the ISO
|
||
*files* (not the .iso). One WinPE installs all of them; you do NOT need a WinPE per version.
|
||
|
||
```bash
|
||
sudo mount -o loop Win11_24H2.iso /mnt/iso
|
||
mkdir -p /mnt/pool/win/win11-24h2
|
||
cp -a /mnt/iso/. /mnt/pool/win/win11-24h2/
|
||
sudo umount /mnt/iso
|
||
# repeat for win10-22h2/, server2022/, server2025/, ...
|
||
```
|
||
|
||
Result:
|
||
|
||
```
|
||
/mnt/pool/win/
|
||
├── menu.cmd ← version picker (already installed)
|
||
├── win11-24h2/ ← setup.exe, sources/install.wim, ...
|
||
├── win10-22h2/
|
||
└── server2025/
|
||
```
|
||
|
||
Editions (Home/Pro/Enterprise) usually live inside one ISO's `install.wim`; `setup.exe`
|
||
lets you pick, so they don't need separate folders. See "Multiple versions" below.
|
||
|
||
### 3. Boot a target → install
|
||
|
||
1. PXE boot → **Windows** → **Load Microsoft Windows Installer** (uses `win_base_url`).
|
||
On real hardware confirm WinPE actually got a `192.168.10.x` (`ipconfig`); if the onboard
|
||
NIC won't network, use a **USB Ethernet dongle** — see **Driver notes**.
|
||
2. At the WinPE `cmd` prompt (startnet usually does this for you):
|
||
```bat
|
||
wpeinit
|
||
net use Z: \\192.168.10.127\win
|
||
Z:\menu.cmd REM pick a version; launches <folder>\sources\setup.exe
|
||
```
|
||
3. Pick a version → click through Setup → install to the local disk.
|
||
4. **After Setup's first reboot, boot the LOCAL DISK, not PXE** — otherwise it loops back into
|
||
netboot and the install looks like it "restarted." (One-time boot menu, or move the disk
|
||
above the network in the BIOS boot order.)
|
||
|
||
> **Win11 24H2/25H2 gotcha — launch `sources\setup.exe`, not the media-root `setup.exe`.** In
|
||
> 24H2+ the root `setup.exe` is the new "modern setup" front-end, meant for booting from real
|
||
> USB/DVD media or upgrading from within Windows; started from a bare WinPE prompt it **exits
|
||
> partway** ("quits in half"). The classic engine at `<folder>\sources\setup.exe` is PE-friendly.
|
||
> `menu.cmd` already prefers `sources\setup.exe` (falling back to the root one for older media).
|
||
|
||
## Multiple Windows versions
|
||
|
||
One x64 WinPE handles every x64 Windows (10/11, Server 2019–2025, all editions) — as long
|
||
as the WinPE is at least as new as the newest OS you install. Manage versions purely as the
|
||
folder library on the share; `menu.cmd` auto-lists every subfolder that contains a
|
||
`setup.exe` and launches the one you choose.
|
||
|
||
**Make it hands-off** by baking the mount + picker into WinPE so every boot lands on the
|
||
menu. When building WinPE, edit `mount\Windows\System32\startnet.cmd` (in the mounted
|
||
`boot.wim`) to:
|
||
|
||
```bat
|
||
wpeinit
|
||
rem allow passwordless (guest) SMB from WinPE
|
||
reg add HKLM\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters /v AllowInsecureGuestAuth /t REG_DWORD /d 1 /f
|
||
net use Z: \\192.168.10.127\win
|
||
Z:\menu.cmd
|
||
```
|
||
|
||
**Unattended per version:** drop an `autounattend.xml` in a version folder and launch it with
|
||
`setup.exe /unattend:%~dp0autounattend.xml` (you can add per-folder entries to `menu.cmd`).
|
||
Each version can have its own answer file (edition index, product key, partitioning).
|
||
|
||
**x86 / ARM64:** only these need a second WinPE — place it in `assets/WinPE/x86/` (the Windows
|
||
menu's arch toggle switches `${win_arch}`). Rarely needed.
|
||
|
||
**Advanced — per-version entries in the iPXE menu** (choose the version *before* WinPE, e.g.
|
||
for fully automated imaging): pass a config into WinPE via extra `initrd` lines in
|
||
`windows.ipxe` so WinPE auto-installs a specific folder. See
|
||
[netbootxyz discussion #757](https://github.com/netbootxyz/netboot.xyz/discussions/757).
|
||
For interactive use, the `menu.cmd` picker is simpler and needs no iPXE changes.
|
||
|
||
## Driver notes (mainly physical machines)
|
||
|
||
WinPE must have the target's **NIC driver** (to reach the share) and Setup must have the
|
||
**storage driver** (to see the disk). VMs rarely need this; real hardware often does.
|
||
|
||
The build's reduced WinPE driver set is missing most modern **Intel** desktop NICs: it ships
|
||
`e1i`/`e1e`/`e1g` (I350/82575/8257x-era) but **not** `e1d` (I217/I218/**I219**) or `e2f`
|
||
(**I225/I226** 2.5G). Symptom: WinPE boots but `net use` fails because there is no link —
|
||
no NIC was ever loaded. Realtek onboard NICs (RTL8111/8168/8125) are likewise absent, and so
|
||
is **virtio-net** (needed for KVM installs with a virtio NIC).
|
||
|
||
### How the current image gets its drivers: DISM injection on `winadmin`
|
||
|
||
Drivers are injected into `boot.wim`'s driver store with **DISM** on the Windows ADK box
|
||
(`winadmin`, `192.168.10.6`). This is the proper method: they become real PnP drivers that load
|
||
automatically at boot — no `drvload`. A ready-to-run **build kit** lives on the share at
|
||
`\\192.168.10.127\win\winpe-build\`:
|
||
|
||
```
|
||
winpe-build/
|
||
├── boot.wim ← image to service (copy of the live one)
|
||
├── drivers/
|
||
│ ├── Intel-1G/ e1dn (I219 — but see ⚠ box), e1r (I210/211/350), v1q (82575/6/80)
|
||
│ ├── Intel-2.5G/ e2f (I225/I226) NDIS68
|
||
│ ├── virtio-NetKVM/ netkvm (virtio-net) + netkvmp.exe/netkvmco.exe
|
||
│ ├── virtio-viostor/ viostor (virtio-blk)
|
||
│ └── virtio-vioscsi/ vioscsi (virtio-scsi)
|
||
├── startnet.cmd ← no-drvload version (PnP loads drivers; DHCP retry loop)
|
||
├── build-winpe.cmd ← one-click DISM script
|
||
└── READ-ME-FIRST.txt
|
||
```
|
||
|
||
Rebuild on `winadmin` (the DISM mount dir must be **local**, not the share):
|
||
|
||
```bat
|
||
robocopy \\192.168.10.127\win\winpe-build C:\winpe-build /E
|
||
:: Start menu -> "Deployment and Imaging Tools Environment" -> Run as administrator
|
||
cd /d C:\winpe-build
|
||
build-winpe.cmd :: mounts boot.wim, drops any old \Drivers tree, DISM /add-driver, commits
|
||
copy /y C:\winpe-build\boot.wim \\192.168.10.127\win\winpe-build\boot.new.wim
|
||
```
|
||
|
||
Then on this host, back up the live image and swap it in (netboot serves it statically — no
|
||
restart, and a size change is fine, the BCD loads `boot.wim` by name):
|
||
|
||
```bash
|
||
cd assets/WinPE/x64/sources
|
||
cp -a boot.wim boot.wim.prev
|
||
cp /mnt/pool/win/winpe-build/boot.new.wim boot.wim
|
||
```
|
||
|
||
`build-winpe.cmd` is essentially:
|
||
|
||
```bat
|
||
dism /Mount-Image /ImageFile:.\boot.wim /Index:1 /MountDir:.\mount
|
||
rmdir /s /q .\mount\Drivers :: drop any old drvload tree
|
||
copy /y .\startnet.cmd .\mount\Windows\System32\startnet.cmd
|
||
dism /Image:.\mount /Add-Driver /Driver:.\drivers /Recurse /ForceUnsigned
|
||
dism /Image:.\mount /Get-Drivers :: confirm the NIC driver is listed
|
||
dism /Unmount-Image /MountDir:.\mount /Commit
|
||
```
|
||
|
||
Use the **current Win11 24H2 / 10.1.26100 ADK** — servicing a 26100 `boot.wim` with an older
|
||
DISM fails (*"image version is higher than the DISM version"*).
|
||
|
||
> **⚠ Verdict — the onboard Intel I219 does NOT work in this (build-26100) WinPE with any
|
||
> driver. Use a USB Ethernet dongle.** On a real I219 (`DEV_550B`, recent Lenovo board) all
|
||
> three Intel drivers failed to move a single frame in *either* direction (no DHCP; a static-IP
|
||
> ping gets no ARP reply — confirmed with `tcpdump` on the host, which saw nothing from the NIC's
|
||
> MAC):
|
||
> - `e1dn` v20.0.3.24 **and** the Lenovo-OEM `e1dn` v20.0.2.19 → link shows "connected", **no traffic**.
|
||
> - `e1d` v12.19.2.65 → **no traffic**, and `netsh …set interface admin=disabled` **bugchecks with
|
||
> `PNP_WATCHDOG`** (the driver can't even cleanly stop the device).
|
||
>
|
||
> PXE firmware works on the same port (its own minimal driver), so it's specifically the I219
|
||
> datapath under build-26100 WinPE — a known regression on newer PE builds. **Fix: a USB GbE
|
||
> dongle** (Realtek RTL8153/8156 is inbox in WinPE 26100 — ours worked with nothing injected).
|
||
> The onboard I219 is fine once *real* Windows is installed. An older WinPE base (Win10 22H2 /
|
||
> Server 2022, build ≤20348) *might* also work but is untested.
|
||
|
||
Intel driver sources: **Intel Wired driver 31.2**
|
||
(`downloadmirror.intel.com/921523/Wired_driver_31.2_x64.zip`), `PRO1000\Winx64\NDIS68` +
|
||
`PRO2500\Winx64\NDIS68` subfolders; the I219 `e1dn` in the current kit is the Lenovo OEM package
|
||
(`e1dn` 20.0.2.19). virtio drivers from `virtio-win-0.1.285.iso` (`~/virtio-win-0.1.285.iso`),
|
||
`<driver>/w11/amd64` folders — **keep `netkvmp.exe`**, `netkvm.inf`'s `[CopyFiles]` requires it.
|
||
|
||
**State:** the live `boot.wim` is a DISM build whose driver store (`oem*.inf`) holds `e1dn`
|
||
(OEM I219 — moot, see box), `e1r`/`v1q` (I210/211/350), `e2f` (I225/226) and
|
||
`netkvm`/`viostor`/`vioscsi` (virtio) — so it still covers other Intel NICs and KVM VMs, and the
|
||
`e1d` that `PNP_WATCHDOG`'d is deliberately excluded. The I219 machine installs via a **USB
|
||
dongle**, and Setup ran once the picker used `sources\setup.exe`. (An earlier `drvload` build was
|
||
VM-verified with a virtio-net NIC reaching the share.)
|
||
|
||
### Alternative: `drvload` at runtime (Linux build, no Windows box)
|
||
|
||
The image can also be built entirely on Linux with `wimlib-imagex`: stage the driver
|
||
`.inf`/`.sys`/`.cat` files inside `boot.wim` under `\Drivers` and `drvload` them from
|
||
`startnet.cmd` before networking. This is how it was *first* built — it's the fallback, since
|
||
`drvload` only loads into the running PE (no persistent driver store) and can load several
|
||
matching drivers at once, leaving PnP to bind whichever it ranks highest (which is how the flaky
|
||
`e1dn` got picked early on):
|
||
|
||
```bat
|
||
for /r X:\Drivers %%i in (*.inf) do drvload "%%i" REM filename-agnostic; non-matching INFs fail harmlessly
|
||
wpeinit
|
||
wpeutil InitializeNetwork
|
||
```
|
||
```bash
|
||
WIM=assets/WinPE/x64/sources/boot.wim
|
||
printf '%s\n' \
|
||
"add /path/to/stage/Drivers /Drivers" \
|
||
"delete --force /Windows/System32/startnet.cmd" \
|
||
"add /path/to/startnet.cmd /Windows/System32/startnet.cmd" \
|
||
| wimlib-imagex update "$WIM" 1
|
||
```
|
||
|
||
> **Gotcha `drvload` fails with `0x80070002` (FILE_NOT_FOUND):** the INF's `[CopyFiles]`
|
||
> references a file you trimmed (e.g. NetKVM's `netkvmp.exe`). Keep the whole driver folder —
|
||
> strip only `*.pdb`/readme.
|
||
|
||
### Storage drivers
|
||
|
||
- Storage/RAID (Intel VMD/RST) drivers usually also need loading in Setup (or slipstream into
|
||
`install.wim`). NVMe/AHCI are typically inbox.
|
||
- **KVM:** using virtio disk/NIC → the virtio drivers above are baked in; or give the VM a
|
||
**SATA disk + e1000 NIC** (both inbox) to skip driver work entirely.
|
||
|
||
## Gotcha: guest SMB from WinPE
|
||
|
||
Modern Windows blocks "insecure guest" logons by policy. WinPE usually allows it, but if
|
||
`net use` fails with **system error 1272 / 5**, enable it in the running WinPE:
|
||
|
||
```bat
|
||
reg add HKLM\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters /v AllowInsecureGuestAuth /t REG_DWORD /d 1 /f
|
||
```
|
||
|
||
(To make it permanent, set the same key offline in the mounted `boot.wim`.) Alternatively,
|
||
switch the share to a real user + password — but do it in the `samba_member` role
|
||
(`samba-ad/`), **not** `/etc/samba/smb.conf` directly (Ansible regenerates that file). The
|
||
`[win]` share already grants the AD user `panxiao81` read-write via `write list`.
|
||
|
||
## Later: unattended installs
|
||
|
||
Drop an `autounattend.xml` at the root of the SMB media (or bake into WinPE via
|
||
`startnet.cmd` running `wpeinit` + `net use` + `setup.exe /unattend:...`) for zero-touch.
|