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:
@@ -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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user