Establish clean homelab infrastructure baseline
Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user