fix(template): §7.4 e2e fixes + autologin + validation scripts

Fix emerged during §7.4 e2e test run (2026-05-10):
- Deploy: set UsoSvc to Manual explicitly (Server 2025 default = Automatic)
- Setup cleanup: re-apply Set-Service Disabled on wuauserv/UsoSvc after DISM
  StartComponentCleanup (WaaSMedicSvc resets StartType during component store cleanup)
- Deploy + Setup: add Administrator autologin (AutoAdminLogon, DefaultUserName,
  DefaultPassword, DefaultDomainName) in post-install.ps1; Assert-Step 5c validates it
- Add Validate-DeployState.ps1: standalone host-side check of all Deploy-set state
- Add Validate-SetupState.ps1: standalone host-side check of full post-Setup state
- Mark §7.4, §7.1 and §7.2 e2e validation items as complete in TODO.md
- Update docs: WINDOWS-TEMPLATE-SETUP.md (arch table, validation scripts section,
  autologin in confine Deploy/Setup); TEST-7.4-e2e.md checklist marked done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone
2026-05-10 11:30:11 +02:00
parent 57e4a9713e
commit 7d6ae42fcf
8 changed files with 762 additions and 106 deletions
+227 -82
View File
@@ -14,11 +14,11 @@
via IMAPI2FS COM (built-in Windows, zero deps)
Step 4 — Create the VMDK (80 GB single-file thin) via vmware-vdiskmanager
Step 5 — Generate the .vmx (UEFI no SecureBoot, NVMe disk, e1000e NIC,
three CD-ROMs: Win install + autounattend + VMware Tools)
Step 6 — Power on the VM (vmrun start nogui)
two CD-ROMs: Win install + autounattend; Tools bundled inside autounattend)
Step 6 — Power on the VM (vmrun start gui/nogui — async to avoid Workstation hang)
Step 7 — Wait for the install_complete.flag file inside the guest
(created by post-install.ps1 once Tools install finishes)
Step 8 — Graceful soft-stop, swap NIC e1000e → vmxnet3 in the .vmx
Step 8 — Graceful soft-stop, remove CD-ROM controller + USB from VMX, swap NIC e1000e → vmxnet3
Step 9 — Power on, wait for guest IP via vmrun (vmxnet3 verified)
Step 10 — Take snapshot named $SnapshotName
Final — Print summary (VM path, IP, snapshot name)
@@ -49,6 +49,9 @@
- Lock screen disabled, Server Manager (policy + HKLM + task + HKCU + Default hive),
DisableCAD (Policies\System + Winlogon), OOBE/telemetry suppressed;
Setup-WinBuild2025 Step 5b/5c validate all of these
- Taskbar search bar hidden, Task View button hidden
- Windows Terminal set as default terminal (DelegationConsole/Terminal HKCU + Default hive)
- Azure Arc auto-start, scheduled tasks, and himds service disabled
- VMware Tools installed (Complete) from Tools ISO
- install_complete.flag dropped in C:\Windows\Temp
@@ -56,7 +59,8 @@
Path to the Windows Server 2025 install ISO.
.PARAMETER ToolsISO
Path to the VMware Tools ISO (windows.iso).
Path to the VMware Tools ISO (windows.iso). Contents are extracted and bundled
inside the autounattend ISO at build time (Step 3); not mounted as a separate CD-ROM.
.PARAMETER VMXPath
Full path of the .vmx file to create. The folder is created if missing.
@@ -144,7 +148,7 @@ param(
# ── Hardware ──
[int] $DiskSizeGB = 80,
[int] $MemoryMB = 6144,
[int] $VCPUCount = 2,
[int] $VCPUCount = 4,
[int] $CoresPerSocket = 2,
# ── Locale ──
@@ -165,7 +169,14 @@ param(
# When set, vmrun powers on the VM with the Workstation GUI visible.
# Default: headless (nogui) for unattended pipelines.
[switch] $ShowGui
[switch] $ShowGui,
# Skip straight to this step number. All derived paths are still computed;
# artifacts from skipped steps must already exist on disk.
# Steps: 1=preflight 2=render XML 3=autounattend ISO 4=VMDK+noPromptISO
# 5=VMX 6=power on 7=wait flag 8=stop+NIC 9=power on vmxnet3 10=snapshot
[ValidateRange(1,10)]
[int] $StartFromStep = 1
)
$vmrunUiMode = if ($ShowGui) { 'gui' } else { 'nogui' }
@@ -178,8 +189,13 @@ $ErrorActionPreference = 'Stop'
# ─────────────────────────────────────────────────────────────────────────────
function Write-Step {
param([string]$Message)
Write-Host "`n=== $Message ===" -ForegroundColor Cyan
param([string] $Message, [int] $Step = 0)
$skipped = $Step -gt 0 -and $script:StartFromStep -gt $Step
if ($skipped) {
Write-Host "`n=== $Message === [SKIP]" -ForegroundColor DarkGray
} else {
Write-Host "`n=== $Message ===" -ForegroundColor Cyan
}
}
function Assert-Step {
@@ -350,8 +366,14 @@ function New-WinIsoNoPrompt {
function Invoke-Vmrun {
param([Parameter(Mandatory)] [string[]] $Arguments,
[switch] $IgnoreErrors)
[switch] $IgnoreErrors,
[switch] $Async)
$vmrun = Join-Path $VMwareWorkstationDir 'vmrun.exe'
if ($Async) {
# vmrun start can block indefinitely on some Workstation versions; fire async.
Start-Process -FilePath $vmrun -ArgumentList $Arguments -NoNewWindow
return
}
$stdout = & $vmrun @Arguments 2>&1
$code = $LASTEXITCODE
if ($code -ne 0 -and -not $IgnoreErrors) {
@@ -377,15 +399,80 @@ function Set-VmxKey {
Set-Content -LiteralPath $VmxPath -Value $newLines -Encoding ASCII
}
function Remove-VmxLines {
param([Parameter(Mandatory)] [string] $VmxPath,
[Parameter(Mandatory)] [string] $KeyPrefix)
$pat = "^\s*$([regex]::Escape($KeyPrefix))"
$lines = Get-Content -LiteralPath $VmxPath | Where-Object { $_ -notmatch $pat }
Set-Content -LiteralPath $VmxPath -Value $lines -Encoding ASCII
}
function Test-VmInList {
param([Parameter(Mandatory)] [string] $VmxPath)
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
$forward = $VmxPath -replace '\\', '/'
$back = $VmxPath -replace '/', '\'
return ($list -match [regex]::Escape($forward)) -or ($list -match [regex]::Escape($back))
}
function Invoke-VmrunBounded {
# Run vmrun synchronously but kill the vmrun process after $TimeoutSeconds.
# Useful for 'stop soft': vmrun blocks until guest shuts down, but the ACPI
# signal is sent within seconds — killing vmrun doesn't cancel the guest shutdown.
param([Parameter(Mandatory)] [string[]] $Arguments,
[int] $TimeoutSeconds = 30)
$exe = Join-Path $VMwareWorkstationDir 'vmrun.exe'
$proc = Start-Process -FilePath $exe -ArgumentList $Arguments -NoNewWindow -PassThru
$null = $proc.WaitForExit($TimeoutSeconds * 1000)
if (-not $proc.HasExited) { $proc.Kill() }
}
function Stop-Vm {
param([Parameter(Mandatory)] [string] $VmxPath,
[int] $SoftTimeoutMinutes = 5)
# Send soft-stop (ACPI); vmrun may block until guest finishes — kill vmrun after 30 s.
# Guest OS continues shutdown independently once the signal is received.
Invoke-VmrunBounded -Arguments @('-T','ws','stop',$VmxPath,'soft') -TimeoutSeconds 30
$deadline = (Get-Date).AddMinutes($SoftTimeoutMinutes)
do {
Start-Sleep -Seconds 5
} while ((Get-Date) -lt $deadline -and (Test-VmInList -VmxPath $VmxPath))
if (Test-VmInList -VmxPath $VmxPath) {
Write-Host ' soft stop timed out — hard stop'
Invoke-VmrunBounded -Arguments @('-T','ws','stop',$VmxPath,'hard') -TimeoutSeconds 15
Start-Sleep -Seconds 5
}
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 1: Pre-flight validation
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 1: pre-flight validation'
# ── Derived paths — always computed regardless of -StartFromStep ─────────────
$vmrunPath = Join-Path $VMwareWorkstationDir 'vmrun.exe'
$vdiskMgr = Join-Path $VMwareWorkstationDir 'vmware-vdiskmanager.exe'
$scriptRoot = Split-Path -Parent $PSCommandPath
$xmlTemplate = Join-Path $scriptRoot 'autounattend.template.xml'
$vmDir = Split-Path -Parent $VMXPath
$vmdkName = [IO.Path]::GetFileNameWithoutExtension($VMXPath) + '.vmdk'
$vmdkPath = Join-Path $vmDir $vmdkName
$autounattendIso = Join-Path $vmDir 'autounattend.iso'
$stagingDir = Join-Path $vmDir '_autounattend_staging'
$guestOS = 'windows2019srvnext-64'
$guestIP = $null # set in Step 9; pre-init for strict-mode safety
if ([string]::IsNullOrWhiteSpace($WinISONoPromptPath)) {
$srcDir = Split-Path -Parent $WinISO
$srcBase = [IO.Path]::GetFileNameWithoutExtension($WinISO)
$WinISONoPromptPath = Join-Path $srcDir "$srcBase.patched.iso"
}
if ($StartFromStep -gt 1) {
Write-Host " [SKIP] Steps 1..$($StartFromStep - 1) — starting from Step $StartFromStep" -ForegroundColor Yellow
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 1: pre-flight validation
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 1: pre-flight validation' -Step 1
if ($StartFromStep -le 1) {
Assert-Step 'PreFlight' "vmrun.exe found at $vmrunPath" { Test-Path $vmrunPath }
Assert-Step 'PreFlight' "vmware-vdiskmanager.exe found at $vdiskMgr" { Test-Path $vdiskMgr }
@@ -395,26 +482,21 @@ Assert-Step 'PreFlight' "autounattend template exists: $xmlTemplate" { Test-Pat
Assert-Step 'PreFlight' "ComputerName valid (1-15 chars, no spaces)" {
$ComputerName -match '^[A-Za-z0-9-]{1,15}$'
}
$vmDir = Split-Path -Parent $VMXPath
if (-not (Test-Path $vmDir)) {
New-Item -ItemType Directory -Path $vmDir -Force | Out-Null
}
Assert-Step 'PreFlight' "VM directory ready: $vmDir" { Test-Path $vmDir }
if (Test-Path $VMXPath) {
throw "[PreFlight] $VMXPath already exists. Remove the existing VM folder before re-running."
}
$vmdkName = [IO.Path]::GetFileNameWithoutExtension($VMXPath) + '.vmdk'
$vmdkPath = Join-Path $vmDir $vmdkName
$autounattendIso = Join-Path $vmDir 'autounattend.iso'
$stagingDir = Join-Path $vmDir '_autounattend_staging'
} # end Step 1
# ─────────────────────────────────────────────────────────────────────────────
# Step 2: render autounattend.xml + post-install.ps1
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 2: render autounattend.xml + post-install.ps1'
Write-Step 'Step 2: render autounattend.xml + post-install.ps1' -Step 2
if ($StartFromStep -le 2) {
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
@@ -513,14 +595,15 @@ sc.exe stop DiagTrack | Out-Null
L 'Telemetry disabled'
# Windows Update GPO locks services left Manual (§7.2 Strategy B)
# Services remain at Windows default (Manual/on-demand). Setup-WinBuild2025
# Step 6 invokes the WU COM API via a SYSTEM scheduled task; it clears these
# GPO keys first (Step A), applies updates, then permanently disables services
# in Step 6b (post-update lockdown). These keys block background auto-start
# between Deploy and Setup without breaking the on-demand COM API.
# Set both WU services to Manual so they can be invoked on-demand by Setup
# Step 6 (WU COM API via SYSTEM task) but won't auto-start between Deploy and Setup.
# UsoSvc defaults to Automatic on Windows Server 2025; must be explicit here.
# GPO keys below also block background triggers. Step 6b permanently disables both.
Set-Service -Name wuauserv -StartupType Manual -ErrorAction SilentlyContinue
Set-Service -Name UsoSvc -StartupType Manual -ErrorAction SilentlyContinue
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' /v DisableWindowsUpdateAccess /t REG_DWORD /d 1 /f | Out-Null
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null
L 'Windows Update GPO locks set (services remain Manual; Setup Step 6b permanently disables)'
L 'Windows Update GPO locks set + wuauserv/UsoSvc Manual (Setup Step 6b permanently disables)'
# Firewall: all profiles disabled + allow RDP + ICMP + WinRM HTTPS
# Disabling all profiles removes any block on subsequent WinRM config and WU
@@ -576,9 +659,15 @@ reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\ServerManage
schtasks /Change /TN '\Microsoft\Windows\Server Manager\ServerManager' /Disable 2>&1 | Out-Null
# DisableCAD in Winlogon (belt-and-suspenders with Policies\System key set above)
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DisableCAD /t REG_DWORD /d 1 /f | Out-Null
# Autologin Administrator auto-signs in after every boot (CI template convenience).
# Password stored plaintext in registry; acceptable for isolated VMnet8 NAT lab VM.
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v AutoAdminLogon /t REG_SZ /d 1 /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultUserName /t REG_SZ /d Administrator /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultPassword /t REG_SZ /d "$AdminPassword" /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v DefaultDomainName /t REG_SZ /d '.' /f | Out-Null
# OOBE non-policy key (supplements the Policies\Windows\OOBE key set above)
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
L 'Lock screen, SM GPO policy + task, CAD Winlogon, OOBE non-policy key set'
L 'Lock screen, SM GPO policy + task, CAD Winlogon, autologin Administrator, OOBE non-policy key set'
# IE Enhanced Security Configuration off (Admin)
# Long-known CI papercut: ESC blocks every URL in admin sessions.
@@ -587,6 +676,13 @@ reg add 'HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37E
Stop-Process -Name iexplore -ErrorAction SilentlyContinue
L 'IE ESC disabled'
# Remove drive letter from EFI System Partition
# Windows setup sometimes assigns 'S:' to the ESP making it visible in Explorer.
Get-Partition | Where-Object { `$_.DriveLetter -eq 'S' } | ForEach-Object {
`$_ | Remove-PartitionAccessPath -AccessPath 'S:\' -ErrorAction SilentlyContinue
}
L 'EFI partition drive letter removed (if any)'
# Hibernate off (frees ~RAM-size on C:, no need on VMs)
powercfg /h off 2>&1 | Out-Null
# Power plan High Performance never sleep, never throttle CPU
@@ -621,11 +717,51 @@ foreach (`$task in @(
}
L 'CEIP / feedback scheduled tasks disabled'
# Azure Arc auto-start / tray icon off
# Server 2025 ships with Azure Arc integration enabled; suppress for CI VMs.
foreach (`$arcTask in @(
'\Microsoft\Azure Arc\Onboarding\EnableAzureArcSetup',
'\Microsoft\Azure Arc\Onboarding\ArcScan',
'\Microsoft\AzureArcSeamlessOnboarding\ArcSetup'
)) {
schtasks /Change /TN `$arcTask /Disable 2>&1 | Out-Null
}
foreach (`$arcSvc in 'himds','ArcProxyAgent') {
`$s = Get-Service -Name `$arcSvc -ErrorAction SilentlyContinue
if (`$s) {
sc.exe config `$arcSvc start= disabled | Out-Null
sc.exe stop `$arcSvc 2>&1 | Out-Null
}
}
reg add 'HKLM\SOFTWARE\Microsoft\AzureArc' /v OnboardingState /t REG_DWORD /d 4 /f 2>&1 | Out-Null
reg delete 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' /v AzureArcSetup /f 2>&1 | Out-Null
L 'Azure Arc auto-start disabled'
# Explorer UX: show file extensions + open to This PC
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v HideFileExt /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v LaunchTo /t REG_DWORD /d 1 /f | Out-Null
L 'Explorer UX tweaks applied (HKCU)'
# Taskbar / terminal UX tweaks
# SearchboxTaskbarMode=0 hide search bar
# ShowTaskViewButton=0 hide Task View button
# DelegationConsole/Terminal Windows Terminal as default terminal
# LayoutModification.xml pre-pin Windows Terminal for new user profiles
# HKCU (SYSTEM context at FirstLogon)
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Search' /v SearchboxTaskbarMode /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v ShowTaskViewButton /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKCU\Console\%Startup' /v DelegationConsole /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
reg add 'HKCU\Console\%Startup' /v DelegationTerminal /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
# Default hive Administrator + any future user inherits on first interactive logon
reg load HKU\DefaultUser2 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null
reg add 'HKU\DefaultUser2\Software\Microsoft\Windows\CurrentVersion\Search' /v SearchboxTaskbarMode /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKU\DefaultUser2\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v ShowTaskViewButton /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKU\DefaultUser2\Console\%Startup' /v DelegationConsole /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
reg add 'HKU\DefaultUser2\Console\%Startup' /v DelegationTerminal /t REG_SZ /d '{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}' /f | Out-Null
reg unload HKU\DefaultUser2 2>&1 | Out-Null
L 'Taskbar: search hidden, task view hidden, WT default terminal'
# Wait for VMware Tools install to settle (kicked off at top)
# Wrapper returned immediately while msiexec runs async. Poll until:
# (a) no setup/setup64/vminst wrapper process (excludes system msiexec /V)
@@ -664,9 +800,11 @@ if (`$toolsExe) {
New-Item -ItemType File -Path 'C:\Windows\Temp\install_complete.flag' -Force | Out-Null
L 'install_complete.flag dropped'
# Reboot to apply Tools/UAC/services state cleanly
L 'rebooting in 15s'
shutdown /r /t 15 /f /c "post-install complete"
# Shut down (not restart) so host Step 8 can swap NIC and snapshot
# Delay 60s: host polls every 30s, needs one cycle after flag appears.
# Step 8 sends vmrun stop soft on top of this Windows handles both.
L 'shutting down in 60s'
shutdown /s /t 60 /f /c "post-install complete"
"@
$psOut = Join-Path $stagingDir 'post-install.ps1'
Set-Content -LiteralPath $psOut -Value $postInstall -Encoding UTF8
@@ -690,10 +828,13 @@ Assert-Step 'Render' 'Tools setup bundled in staging\Tools' {
(Test-Path (Join-Path $toolsStage 'setup64.exe'))
}
} # end Step 2
# ─────────────────────────────────────────────────────────────────────────────
# Step 3: build autounattend ISO
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 3: build autounattend ISO via IMAPI2FS'
Write-Step 'Step 3: build autounattend ISO via IMAPI2FS' -Step 3
if ($StartFromStep -le 3) {
New-IsoFromFolder -SourceFolder $stagingDir -IsoPath $autounattendIso -VolumeLabel 'AUTOUNATTEND'
Assert-Step 'IsoBuild' "autounattend.iso created at $autounattendIso" { Test-Path $autounattendIso }
@@ -708,10 +849,13 @@ if (Test-Path $stagingDir) {
Write-Host " [warn] staging dir not fully cleaned, leaving in place: $stagingDir" -ForegroundColor Yellow
}
} # end Step 3
# ─────────────────────────────────────────────────────────────────────────────
# Step 4: create VMDK
# Step 4: create VMDK + no-prompt Windows ISO
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)"
Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)" -Step 4
if ($StartFromStep -le 4) {
# -t 0 = single growable file (thin); -a lsilogic affects descriptor only,
# the disk attaches to the NVMe controller defined in the VMX.
@@ -723,13 +867,7 @@ Assert-Step 'Vmdk' "VMDK file present: $vmdkPath" { Test-Path $vmdkPath }
# ─────────────────────────────────────────────────────────────────────────────
# Step 4b: rebuild Windows ISO with EFI no-prompt boot image
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 4b: build no-prompt Windows ISO (skips "Press any key")'
if ([string]::IsNullOrWhiteSpace($WinISONoPromptPath)) {
$srcDir = Split-Path -Parent $WinISO
$srcBase = [IO.Path]::GetFileNameWithoutExtension($WinISO)
$WinISONoPromptPath = Join-Path $srcDir "$srcBase.patched.iso"
}
Write-Step 'Step 4b: build no-prompt Windows ISO (skips "Press any key")' -Step 4
$srcMtime = (Get-Item $WinISO).LastWriteTime
$rebuild = $true
@@ -747,21 +885,24 @@ if ($rebuild) {
Assert-Step 'NoPromptIso' "no-prompt ISO present: $WinISONoPromptPath" { Test-Path $WinISONoPromptPath }
Assert-Step 'NoPromptIso' 'no-prompt ISO non-empty (>500 MB)' { (Get-Item $WinISONoPromptPath).Length -gt 500MB }
} # end Step 4
# ─────────────────────────────────────────────────────────────────────────────
# Step 5: generate VMX
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 5: generate VMX'
Write-Step 'Step 5: generate VMX' -Step 5
if ($StartFromStep -le 5) {
# guestOS:
# "windows2019srvnext-64" is the generic forward-compat ID for new Windows
# Server releases on Workstation. Workstation 17.6+ accepts it for Server 2025.
# If your Workstation version supports the explicit ID, change it here.
$guestOS = 'windows2019srvnext-64'
# ($guestOS is set unconditionally in the derived-paths block above.)
# CD-ROM ordering (sata):
# sata0:0 = Windows install ISO (bootable) ← EFI picks this on first boot
# sata0:1 = autounattend ISO (non-bootable, scanned by Setup)
# sata0:2 = VMware Tools ISO (Tools installer source)
# sata0:1 = autounattend ISO (non-bootable, scanned by Setup; Tools bundled inside)
# Both disconnected in Step 8 after install — snapshot/clone boots clean with no ISOs.
$vmxLines = @(
'.encoding = "windows-1252"',
'config.version = "8"',
@@ -804,7 +945,7 @@ $vmxLines = @(
"nvme0:0.fileName = `"$vmdkName`"",
'nvme0:0.deviceType = "disk"',
'',
'# Three SATA CD-ROMs: install / autounattend / Tools',
'# Two SATA CD-ROMs: install ISO + autounattend ISO (Tools bundled in autounattend)',
'sata0.present = "TRUE"',
'sata0.pciSlotNumber = "32"',
'sata0:0.present = "TRUE"',
@@ -815,10 +956,6 @@ $vmxLines = @(
'sata0:1.deviceType = "cdrom-image"',
"sata0:1.fileName = `"$autounattendIso`"",
'sata0:1.startConnected = "TRUE"',
'sata0:2.present = "TRUE"',
'sata0:2.deviceType = "cdrom-image"',
"sata0:2.fileName = `"$ToolsISO`"",
'sata0:2.startConnected = "TRUE"',
'',
'# NIC: e1000e for install (broad compat); switched to vmxnet3 after Tools',
'ethernet0.present = "TRUE"',
@@ -852,19 +989,23 @@ $vmxLines = @(
Set-Content -LiteralPath $VMXPath -Value $vmxLines -Encoding ASCII
Assert-Step 'Vmx' "VMX written: $VMXPath" { Test-Path $VMXPath }
} # end Step 5
# ─────────────────────────────────────────────────────────────────────────────
# Step 6: power on VM
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 6: power on VM (vmrun start nogui)'
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
Start-Sleep -Seconds 5
$running = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
Assert-Step 'PowerOn' 'VM appears in vmrun list' { $running -match [regex]::Escape($VMXPath) }
Write-Step "Step 6: power on VM (vmrun start $vmrunUiMode)" -Step 6
if ($StartFromStep -le 6) {
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
Start-Sleep -Seconds 10
Assert-Step 'PowerOn' 'VM appears in vmrun list' { Test-VmInList -VmxPath $VMXPath }
} # end Step 6
# ─────────────────────────────────────────────────────────────────────────────
# Step 7: wait for install_complete.flag inside guest
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)"
Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)" -Step 7
if ($StartFromStep -le 7) {
$deadline = (Get-Date).AddMinutes($GuestPollMaxMinutes)
$flagPath = 'C:\Windows\Temp\install_complete.flag'
@@ -883,22 +1024,24 @@ while ((Get-Date) -lt $deadline) {
}
Assert-Step 'GuestReady' "install_complete.flag observed in guest" { $flagSeen }
# ─────────────────────────────────────────────────────────────────────────────
# Step 8: graceful soft-stop, swap NIC e1000e → vmxnet3
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 8: stop VM softly, swap NIC to vmxnet3'
} # end Step 7
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null
# ─────────────────────────────────────────────────────────────────────────────
# Step 8: stop VM, disconnect ISOs, swap NIC e1000e → vmxnet3
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 8: stop VM, disconnect ISOs, swap NIC to vmxnet3' -Step 8
if ($StartFromStep -le 8) {
# Wait for VM to drop out of running list (max 3 min).
$stopDeadline = (Get-Date).AddMinutes(3)
do {
Start-Sleep -Seconds 5
$still = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
} while ((Get-Date) -lt $stopDeadline -and ($still -match [regex]::Escape($VMXPath)))
Assert-Step 'Stop' 'VM no longer in vmrun list' {
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
-not ($list -match [regex]::Escape($VMXPath))
Stop-Vm -VmxPath $VMXPath
Assert-Step 'Stop' 'VM no longer in vmrun list' { -not (Test-VmInList -VmxPath $VMXPath) }
# Remove CD-ROM controller and USB from VMX — not needed post-install.
foreach ($prefix in 'sata0','usb','ehci','usb_xhci') {
Remove-VmxLines -VmxPath $VMXPath -KeyPrefix $prefix
}
Assert-Step 'CleanVmx' 'CD-ROM and USB entries removed from VMX' {
$vmx = Get-Content -LiteralPath $VMXPath -Raw
($vmx -notmatch '(?m)^\s*sata0') -and ($vmx -notmatch '(?m)^\s*usb')
}
Set-VmxKey -VmxPath $VMXPath -Key 'ethernet0.virtualDev' -Value 'vmxnet3'
@@ -906,12 +1049,16 @@ Assert-Step 'NicSwap' 'ethernet0.virtualDev = vmxnet3' {
(Get-Content -LiteralPath $VMXPath) -match '^\s*ethernet0\.virtualDev\s*=\s*"vmxnet3"\s*$'
}
} # end Step 8
# ─────────────────────────────────────────────────────────────────────────────
# Step 9: power on, verify vmxnet3 + DHCP IP
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP'
Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP' -Step 9
if ($StartFromStep -le 9) {
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
Start-Sleep -Seconds 10
$ipDeadline = (Get-Date).AddMinutes(10)
$guestIP = $null
do {
@@ -921,27 +1068,23 @@ do {
} while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+'))
Assert-Step 'IPCheck' 'Guest IP obtained on vmxnet3' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' }
} # end Step 9
# ─────────────────────────────────────────────────────────────────────────────
# Step 10: shutdown → snapshot (cold) → power on
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on"
Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on" -Step 10
if ($StartFromStep -le 10) {
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null
$stopDeadline = (Get-Date).AddMinutes(3)
do {
Start-Sleep -Seconds 5
$running = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
} while ((Get-Date) -lt $stopDeadline -and ($running -match [regex]::Escape($VMXPath)))
Assert-Step 'Snapshot' 'VM powered off before snapshot' {
$list = Invoke-Vmrun -Arguments @('-T','ws','list') -IgnoreErrors
-not ($list -match [regex]::Escape($VMXPath))
}
Stop-Vm -VmxPath $VMXPath
Assert-Step 'Snapshot' 'VM powered off before snapshot' { -not (Test-VmInList -VmxPath $VMXPath) }
Invoke-Vmrun -Arguments @('-T','ws','snapshot',$VMXPath,$SnapshotName) | Out-Null
$snaps = Invoke-Vmrun -Arguments @('-T','ws','listSnapshots',$VMXPath)
Assert-Step 'Snapshot' "Snapshot '$SnapshotName' present" { $snaps -match [regex]::Escape($SnapshotName) }
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) -Async
Start-Sleep -Seconds 10
$ipDeadline = (Get-Date).AddMinutes(5)
do {
Start-Sleep -Seconds 10
@@ -949,6 +1092,8 @@ do {
} while ((Get-Date) -lt $ipDeadline -and -not ($guestIP -match '^\d+\.\d+\.\d+\.\d+'))
Assert-Step 'Snapshot' 'VM back online after snapshot' { $guestIP -match '^\d+\.\d+\.\d+\.\d+' }
} # end Step 10
# ─────────────────────────────────────────────────────────────────────────────
# Final summary
# ─────────────────────────────────────────────────────────────────────────────