feat(template): add Deploy-WinBuild2025.ps1 + autounattend; update TODO

template/Deploy-WinBuild2025.ps1:
  New host-side script that drives the unattended Windows install phase:
  creates the VM, injects autounattend.xml (disabling Defender/firewall/UAC
  before Setup-WinBuild2025.ps1 runs), boots from ISO, waits for first-boot.

template/autounattend.template.xml:
  WiM-based answer file template for Windows Server 2025 unattended install.
  Sets DisableAntiSpyware GPO + RTP off so Defender is fully off at first
  logon (prerequisite for Setup-WinBuild2025.ps1 Step-2 validation-only path).

TODO.md:
  - Date + wording updated (2026-05-10)
  - §1.1: file refs updated to post-refactor line numbers (Step 3 WinRM,
    Deploy-WinBuild2025 Enable-PSRemoting, Invoke-RemoteBuild, Get-BuildArtifacts)
  - §1.2: TrustedHosts audit status corrected (Setup-Host.ps1 never sets '*';
    Prepare appends IP and restores in finally)
  - §1.3: Python/dotnet/VS Build Tools line refs updated
  - §1.5: PAT security constraints expanded with grep-on-log safety net rule
  - §1.6: Defender/Firewall/UAC state updated to reflect Deploy vs Setup split
  - §3.3 deploy reference: VMX path corrected to CI-WinBuild.vmx
  - §3.3 doc ref: WINDOWS-TEMPLATE-SETUP updated to list Deploy step
This commit is contained in:
Simone
2026-05-10 01:01:43 +02:00
parent ab636a0ec1
commit 96b65d40de
3 changed files with 1324 additions and 38 deletions
+938
View File
@@ -0,0 +1,938 @@
#Requires -RunAsAdministrator
#Requires -Version 5.1
<#
.SYNOPSIS
Deploys an unattended Windows Server 2025 VM on VMware Workstation
and installs VMware Tools automatically.
.DESCRIPTION
Host-side orchestrator. Produces a ready-to-use template VM:
Step 1 — Validate prerequisites (vmrun, vmware-vdiskmanager, ISOs)
Step 2 — Render autounattend.xml from template + post-install.ps1
(placeholders substituted with parameter values)
Step 3 — Build autounattend ISO (autounattend.xml + post-install.ps1)
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)
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 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)
NIC strategy:
Boot install with e1000e (broad OOB Windows compatibility). VMware Tools
install brings the vmxnet3 driver. Post-Tools, the host swaps the .vmx
to vmxnet3 and reboots. This avoids driver injection in WinPE.
Disk layout (UEFI/GPT, 80 GB):
Part 1: EFI 100 MB FAT32 S:
Part 2: MSR 16 MB
Part 3: Primary rest NTFS C: ← Windows installs here
No Recovery partition (per user choice).
Hardening done by post-install.ps1 inside the guest:
- UAC off (EnableLUA=0, LocalAccountTokenFilterPolicy=1)
- Defender real-time protection off
- DiagTrack/Telemetry off
- Windows Update services disabled
- RDP enabled (no NLA, lab only) + firewall rule
- WinRM enabled HTTP 5985 + HTTPS 5986 (self-signed cert), Basic auth,
AllowUnencrypted, TrustedHosts=*
- ICMPv4 inbound allowed
- VMware Tools installed (Complete) from Tools ISO
- install_complete.flag dropped in C:\Windows\Temp
.PARAMETER WinISO
Path to the Windows Server 2025 install ISO.
.PARAMETER ToolsISO
Path to the VMware Tools ISO (windows.iso).
.PARAMETER VMXPath
Full path of the .vmx file to create. The folder is created if missing.
.PARAMETER VMName
Display name in the Workstation library. Defaults to the .vmx file basename.
.PARAMETER ProductKey
Windows Server 2025 product key to embed in autounattend.
.PARAMETER ComputerName
Hostname. Max 15 chars, no spaces.
.PARAMETER AdminUser
.PARAMETER AdminPassword
Local Administrator account credentials.
.PARAMETER ImageIndex
Index of the install.wim edition to deploy. Default: 2 (Server 2025
Standard Desktop Experience on the VOL ISO). Verify with:
dism /Get-WimInfo /WimFile:<isomount>\sources\install.wim
.PARAMETER DiskSizeGB
Virtual disk size in GiB. Default: 80.
.PARAMETER MemoryMB
Guest RAM in MiB. Default: 6144 (6 GB).
.PARAMETER VCPUCount
Total vCPU. Default: 2.
.PARAMETER CoresPerSocket
Cores per socket. Default: 2 (=> 1 socket × 2 cores).
.PARAMETER LocaleOS
OS UI / system locale. Default: en-US.
.PARAMETER LocaleUser
User locale. Default: it-IT.
.PARAMETER InputKeyboard
Keyboard layout id. Default: 0410:00000410 (Italian).
.PARAMETER TimeZone
Windows time zone id. Default: W. Europe Standard Time.
.PARAMETER SnapshotName
Snapshot name taken at the end. Default: PostInstall.
.PARAMETER VMwareWorkstationDir
Override Workstation install dir if not at the default location.
.PARAMETER GuestPollSeconds
Seconds between polls when waiting for install_complete.flag. Default: 30.
.PARAMETER GuestPollMaxMinutes
Hard timeout waiting for install_complete.flag. Default: 90.
.NOTES
Run from elevated PowerShell on the host. The VM IP detection (Step 9)
requires VMware Tools to be running in the guest — guaranteed because
Step 7 waited for the install_complete.flag dropped after Tools install.
#>
[CmdletBinding()]
param(
# ── ISOs ──
[string] $WinISO = 'F:\CI\ISO\26100.1742.240906-0331.ge_release_svc_refresh_SERVER_VOL_x64FRE_en-us.iso',
[string] $ToolsISO = 'F:\CI\ISO\VMware-tools-windows.iso',
# Cache path for the rebuilt no-prompt Windows ISO. Reused across runs when
# newer than the source. Empty string => rebuild every run inside the VM dir.
[string] $WinISONoPromptPath = '',
# ── VM identity ──
[string] $VMXPath = 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx',
[string] $VMName = 'WinBuild2025',
# ── Windows install ──
[string] $ProductKey = 'TVRH6-WHNXV-R9WG3-9XRFY-MY832',
[string] $ComputerName = 'WINBUILD-2025',
[string] $AdminUser = 'Administrator',
[string] $AdminPassword = 'WinBuild!',
[int] $ImageIndex = 2,
# ── Hardware ──
[int] $DiskSizeGB = 80,
[int] $MemoryMB = 6144,
[int] $VCPUCount = 2,
[int] $CoresPerSocket = 2,
# ── Locale ──
[string] $LocaleOS = 'en-US',
[string] $LocaleUser = 'it-IT',
[string] $InputKeyboard = '0410:00000410',
[string] $TimeZone = 'W. Europe Standard Time',
# ── Finalization ──
[string] $SnapshotName = 'PostInstall',
# ── Tooling ──
[string] $VMwareWorkstationDir = 'C:\Program Files (x86)\VMware\VMware Workstation',
# ── Polling ──
[int] $GuestPollSeconds = 30,
[int] $GuestPollMaxMinutes = 90,
# When set, vmrun powers on the VM with the Workstation GUI visible.
# Default: headless (nogui) for unattended pipelines.
[switch] $ShowGui
)
$vmrunUiMode = if ($ShowGui) { 'gui' } else { 'nogui' }
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────
function Write-Step {
param([string]$Message)
Write-Host "`n=== $Message ===" -ForegroundColor Cyan
}
function Assert-Step {
param(
[Parameter(Mandatory)] [string] $StepName,
[Parameter(Mandatory)] [string] $Description,
[Parameter(Mandatory)] [scriptblock] $Test
)
$ok = $false
try { $ok = [bool](& $Test) }
catch { throw "[$StepName] Validation threw on '$Description': $_" }
if (-not $ok) { throw "[$StepName] Validation failed: $Description" }
Write-Host " [OK] $Description" -ForegroundColor Green
}
# IMAPI2FS-based ISO writer. No external deps. Inline C# bridges IStream → file.
$isoWriterCs = @"
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public class IsoStreamWriter2 {
[DllImport("shlwapi.dll", CharSet=CharSet.Unicode, ExactSpelling=true)]
static extern int SHCreateStreamOnFileEx(string pszFile, uint grfMode, uint dwAttributes, bool fCreate, IntPtr pstmTemplate, out IStream ppstm);
public static IStream OpenFileAsStream(string path) {
IStream s;
// STGM_READ | STGM_SHARE_DENY_WRITE = 0x00000020
int hr = SHCreateStreamOnFileEx(path, 0x20, 0, false, IntPtr.Zero, out s);
if (hr != 0) throw new Exception("SHCreateStreamOnFileEx 0x" + hr.ToString("X"));
return s;
}
public static void Save(object streamObj, string path) {
IStream istream = streamObj as IStream;
if (istream == null) {
// Fallback: QueryInterface for IStream IID via Marshal.
IntPtr unk = Marshal.GetIUnknownForObject(streamObj);
try {
Guid iid = new Guid("0000000C-0000-0000-C000-000000000046");
IntPtr pStream;
int hr = Marshal.QueryInterface(unk, ref iid, out pStream);
if (hr != 0) throw new InvalidCastException("QueryInterface(IStream) failed: 0x" + hr.ToString("X"));
try {
istream = (IStream)Marshal.GetObjectForIUnknown(pStream);
} finally {
Marshal.Release(pStream);
}
} finally {
Marshal.Release(unk);
}
}
using (FileStream fs = File.Open(path, FileMode.Create, FileAccess.Write)) {
byte[] buf = new byte[2048 * 16];
IntPtr pcb = Marshal.AllocHGlobal(8);
try {
int n;
do {
istream.Read(buf, buf.Length, pcb);
n = Marshal.ReadInt32(pcb);
if (n > 0) fs.Write(buf, 0, n);
} while (n > 0);
} finally {
Marshal.FreeHGlobal(pcb);
}
}
}
}
"@
if (-not ([System.Management.Automation.PSTypeName]'IsoStreamWriter2').Type) {
Add-Type -TypeDefinition $isoWriterCs -Language CSharp
}
function New-IsoFromFolder {
param(
[Parameter(Mandatory)] [string] $SourceFolder,
[Parameter(Mandatory)] [string] $IsoPath,
[Parameter(Mandatory)] [string] $VolumeLabel
)
if (Test-Path $IsoPath) { Remove-Item $IsoPath -Force }
$fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage
$result = $null
$stream = $null
try {
$fsi.FileSystemsToCreate = 7 # ISO9660 | Joliet | UDF
$fsi.VolumeName = $VolumeLabel
$fsi.ChooseImageDefaultsForMediaType(13) # IMAPI_MEDIA_TYPE_DISK
$fsi.Root.AddTree($SourceFolder, $false)
$result = $fsi.CreateResultImage()
$stream = $result.ImageStream
[IsoStreamWriter2]::Save($stream, $IsoPath)
} finally {
# Release in reverse order. ImageStream holds file handles to source
# files added via AddTree — must be released before the source folder
# can be deleted.
if ($stream) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($stream) | Out-Null }
if ($result) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($result) | Out-Null }
[System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($fsi) | Out-Null
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect()
}
}
function New-WinIsoNoPrompt {
<#
Rebuilds a Windows install ISO replacing the EFI boot image with
efisys_noprompt.bin (extracted from the source ISO itself).
Result: UEFI boot proceeds straight into Windows Setup, no
"Press any key to boot from CD" prompt.
#>
param(
[Parameter(Mandatory)] [string] $SourceIsoPath,
[Parameter(Mandatory)] [string] $OutputIsoPath
)
if (Test-Path $OutputIsoPath) { Remove-Item $OutputIsoPath -Force }
$mount = Mount-DiskImage -ImagePath $SourceIsoPath -PassThru -StorageType ISO -Access ReadOnly
try {
$vol = $mount | Get-Volume
$letter = "$($vol.DriveLetter):\"
$bootImg = Join-Path $letter 'efi\microsoft\boot\efisys_noprompt.bin'
if (-not (Test-Path $bootImg)) {
throw "efisys_noprompt.bin not present in source ISO at $bootImg"
}
$label = $vol.FileSystemLabel
if ([string]::IsNullOrWhiteSpace($label)) { $label = 'WINSERVER' }
if ($label.Length -gt 32) { $label = $label.Substring(0,32) }
$fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage
try {
$fsi.FileSystemsToCreate = 7 # ISO9660 | Joliet | UDF
$fsi.VolumeName = $label
$fsi.ChooseImageDefaultsForMediaType(13)
# 5+ GB ISO needs explicit large-volume defaults if available.
try { $fsi.UDFRevision = 0x102 } catch {}
Write-Host " Adding ISO tree (this takes a few minutes for 5+ GB)..." -ForegroundColor Yellow
$fsi.Root.AddTree($letter, $false)
$bootStream = [IsoStreamWriter2]::OpenFileAsStream($bootImg)
$bootOpts = New-Object -ComObject IMAPI2FS.BootOptions
$bootOpts.AssignBootImage($bootStream)
$bootOpts.Emulation = 0 # IMAPI_BOOT_EMULATION_TYPE_NONE
$bootOpts.PlatformId = 0xEF # EFI
$bootOpts.Manufacturer = 'Microsoft'
# Single-element COM array for BootImageOptionsArray
$arr = New-Object 'object[]' 1
$arr[0] = $bootOpts
try { $fsi.BootImageOptionsArray = $arr }
catch { $fsi.BootImageOptions = $bootOpts } # fallback older IMAPI2FS
Write-Host " Building output ISO..." -ForegroundColor Yellow
$result = $fsi.CreateResultImage()
$stream = $result.ImageStream
[IsoStreamWriter2]::Save($stream, $OutputIsoPath)
} finally {
if ($stream) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($stream) | Out-Null }
if ($result) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($result) | Out-Null }
if ($bootStream) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($bootStream) | Out-Null }
if ($bootOpts) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($bootOpts) | Out-Null }
[System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($fsi) | Out-Null
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect()
}
} finally {
Dismount-DiskImage -ImagePath $SourceIsoPath -ErrorAction SilentlyContinue | Out-Null
}
}
function Invoke-Vmrun {
param([Parameter(Mandatory)] [string[]] $Arguments,
[switch] $IgnoreErrors)
$vmrun = Join-Path $VMwareWorkstationDir 'vmrun.exe'
$stdout = & $vmrun @Arguments 2>&1
$code = $LASTEXITCODE
if ($code -ne 0 -and -not $IgnoreErrors) {
throw "vmrun $($Arguments -join ' ') failed (exit $code): $stdout"
}
return $stdout
}
function Set-VmxKey {
param(
[Parameter(Mandatory)] [string] $VmxPath,
[Parameter(Mandatory)] [string] $Key,
[Parameter(Mandatory)] [string] $Value
)
$lines = Get-Content -LiteralPath $VmxPath
$pattern = "^\s*$([regex]::Escape($Key))\s*="
$found = $false
$newLines = foreach ($line in $lines) {
if ($line -match $pattern) { $found = $true; "$Key = `"$Value`"" }
else { $line }
}
if (-not $found) { $newLines = @($newLines) + "$Key = `"$Value`"" }
Set-Content -LiteralPath $VmxPath -Value $newLines -Encoding ASCII
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 1: Pre-flight validation
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 1: pre-flight validation'
$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'
Assert-Step 'PreFlight' "vmrun.exe found at $vmrunPath" { Test-Path $vmrunPath }
Assert-Step 'PreFlight' "vmware-vdiskmanager.exe found at $vdiskMgr" { Test-Path $vdiskMgr }
Assert-Step 'PreFlight' "Windows ISO exists: $WinISO" { Test-Path $WinISO }
Assert-Step 'PreFlight' "VMware Tools ISO exists: $ToolsISO" { Test-Path $ToolsISO }
Assert-Step 'PreFlight' "autounattend template exists: $xmlTemplate" { Test-Path $xmlTemplate }
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'
# ─────────────────────────────────────────────────────────────────────────────
# Step 2: render autounattend.xml + post-install.ps1
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 2: render autounattend.xml + post-install.ps1'
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
# autounattend.xml
$xmlText = Get-Content -LiteralPath $xmlTemplate -Raw
$xmlSubs = @{
'{{LOCALE_OS}}' = $LocaleOS
'{{LOCALE_USER}}' = $LocaleUser
'{{KEYBOARD}}' = $InputKeyboard
'{{IMAGE_INDEX}}' = "$ImageIndex"
'{{PRODUCT_KEY}}' = $ProductKey
'{{COMPUTER_NAME}}' = $ComputerName
'{{TIMEZONE}}' = $TimeZone
'{{ADMIN_USER}}' = $AdminUser
'{{ADMIN_PWD}}' = $AdminPassword
}
foreach ($k in $xmlSubs.Keys) { $xmlText = $xmlText.Replace($k, $xmlSubs[$k]) }
$xmlOut = Join-Path $stagingDir 'autounattend.xml'
Set-Content -LiteralPath $xmlOut -Value $xmlText -Encoding UTF8
Assert-Step 'Render' 'autounattend.xml written' { Test-Path $xmlOut }
Assert-Step 'Render' 'no placeholders left in xml' { -not (Select-String -Path $xmlOut -Pattern '\{\{[^}]+\}\}' -Quiet) }
# post-install.ps1 — drops into autounattend ISO root, runs at FirstLogonCommands.
# Hardening + WinRM + Tools install + flag file + reboot. All paths are absolute.
$postInstall = @"
# Auto-generated by Deploy-WinBuild2025.ps1 runs at first logon as Administrator.
`$ErrorActionPreference = 'Continue'
`$log = 'C:\Windows\Temp\post-install.log'
function L(`$m){ "`$(Get-Date -f s) `$m" | Tee-Object -FilePath `$log -Append }
L 'post-install start'
# KICK OFF VMware Tools install (async wrapper detaches MSI child)
# Started first so the MSI runs in the background while hardening tweaks
# below execute. Settled state verified at the very end before reboot.
`$selfDir = Split-Path -Parent `$MyInvocation.MyCommand.Path
`$toolsExe = `$null
foreach (`$name in 'setup64.exe','setup.exe') {
`$cand = Join-Path `$selfDir "Tools\`$name"
if (Test-Path `$cand) { `$toolsExe = `$cand; break }
}
if (-not `$toolsExe) {
L 'Bundled Tools not found, scanning CD drives...'
foreach (`$drv in (Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=5')) {
foreach (`$name in 'setup64.exe','setup.exe') {
`$cand = Join-Path (`$drv.DeviceID + '\') `$name
if (Test-Path `$cand) { `$toolsExe = `$cand; break }
}
if (`$toolsExe) { break }
}
}
L "Tools setup resolved to: `$toolsExe"
`$localRoot = 'C:\Windows\Temp\VMwareTools'
`$msiLog = 'C:\Windows\Temp\vmtools-msi.log'
`$exitCode = -1
if (`$toolsExe -and (Test-Path `$toolsExe)) {
# Copy payload to local writable disk wrapper extract from CD fails at
# FirstLogon phase due to user profile %TEMP% ACL bootstrap timing.
`$srcRoot = Split-Path -Parent `$toolsExe
if (Test-Path `$localRoot) { Remove-Item `$localRoot -Recurse -Force }
L "Copying Tools payload `$srcRoot -> `$localRoot"
Copy-Item -Path `$srcRoot -Destination `$localRoot -Recurse -Force
# PS argument parser strips inner quotes from /v"..."; --% stop-parsing
# token passes the tail literally to the EXE.
L "Tools cmd: .\setup.exe /s /v`"/qn REBOOT=R /l*v `$msiLog`""
Push-Location `$localRoot
try {
& .\setup.exe --% /s /v"/qn REBOOT=R /l*v C:\Windows\Temp\vmtools-msi.log"
`$exitCode = `$LASTEXITCODE
} finally { Pop-Location }
L "VMware Tools wrapper exit code: `$exitCode (MSI continues async)"
} else {
L 'ERROR: VMware Tools setup not found (bundled or on any CD-ROM)'
}
# UAC off (admin script automation)
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v EnableLUA /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f | Out-Null
L 'UAC disabled'
# Defender disabled (lab only) Setup-WinBuild2025 skips Defender step
# RTP off + tamper-protect GPO. With Defender fully off, exclusion paths
# become moot (no scanner to exclude from), so Setup-WinBuild2025 has been
# stripped of its Defender step.
try { Set-MpPreference -DisableRealtimeMonitoring `$true -ErrorAction Stop; L 'Defender RTP off' }
catch { L "Defender RTP off failed: `$(`$_.Exception.Message)" }
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows Defender' /v DisableAntiSpyware /t REG_DWORD /d 1 /f | Out-Null
# Telemetry / DiagTrack off
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection' /v AllowTelemetry /t REG_DWORD /d 0 /f | Out-Null
sc.exe config DiagTrack start= disabled | Out-Null
sc.exe stop DiagTrack | Out-Null
L 'Telemetry disabled'
# Windows Update services disabled
foreach (`$svc in 'wuauserv','UsoSvc','WaaSMedicSvc') {
sc.exe config `$svc start= disabled | Out-Null
sc.exe stop `$svc | Out-Null
}
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' /v NoAutoUpdate /t REG_DWORD /d 1 /f | Out-Null
L 'Windows Update disabled'
# Firewall: profiles stay default; allow RDP + ICMP + WinRM HTTPS
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server' /v fDenyTSConnections /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' /v UserAuthentication /t REG_DWORD /d 0 /f | Out-Null
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue
New-NetFirewallRule -DisplayName 'ICMPv4-In' -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow -Profile Any -ErrorAction SilentlyContinue | Out-Null
L 'RDP + ICMP allowed'
# WinRM HTTP + HTTPS, Basic+Unencrypted, TrustedHosts=* (lab only)
Enable-PSRemoting -Force -SkipNetworkProfileCheck | Out-Null
winrm set winrm/config/service '@{AllowUnencrypted="true"}' | Out-Null
winrm set winrm/config/service/auth '@{Basic="true"}' | Out-Null
winrm set winrm/config/client/auth '@{Basic="true"}' | Out-Null
winrm set winrm/config/client '@{TrustedHosts="*"}' | Out-Null
try {
`$cert = New-SelfSignedCertificate -DnsName `$env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint `$cert.Thumbprint -Force | Out-Null
New-NetFirewallRule -DisplayName 'WinRM-HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Any | Out-Null
L 'WinRM HTTPS listener configured'
} catch { L "WinRM HTTPS failed: `$(`$_.Exception.Message)" }
# Server Manager / WAC popup / first-logon UX
# Server Manager auto-start off machine-wide + current Administrator + Default
# profile hive (so future cloned/sysprep'd users inherit).
reg add 'HKLM\SOFTWARE\Microsoft\ServerManager' /v DoNotOpenServerManagerAtLogon /t REG_DWORD /d 1 /f | Out-Null
reg add 'HKCU\SOFTWARE\Microsoft\ServerManager' /v DoNotOpenServerManagerAtLogon /t REG_DWORD /d 1 /f | Out-Null
reg load HKU\DefaultUser 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null
reg add 'HKU\DefaultUser\SOFTWARE\Microsoft\ServerManager' /v DoNotOpenServerManagerAtLogon /t REG_DWORD /d 1 /f | Out-Null
reg unload HKU\DefaultUser 2>&1 | Out-Null
# Windows Admin Center prompt at SM launch off
reg add 'HKLM\SOFTWARE\Microsoft\ServerManager' /v DoNotPopWACConsoleAtSMLaunch /t REG_DWORD /d 1 /f | Out-Null
# DisableCAD no Ctrl+Alt+Del at logon (CI VMs never need it)
reg add 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' /v DisableCAD /t REG_DWORD /d 1 /f | Out-Null
# OOBE privacy experience suppressed
reg add 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OOBE' /v DisablePrivacyExperience /t REG_DWORD /d 1 /f | Out-Null
L 'Server Manager + WAC + CAD + OOBE prompts disabled'
# IE Enhanced Security Configuration off (Admin)
# Long-known CI papercut: ESC blocks every URL in admin sessions.
reg add 'HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}' /v IsInstalled /t REG_DWORD /d 0 /f | Out-Null
reg add 'HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}' /v IsInstalled /t REG_DWORD /d 0 /f | Out-Null
Stop-Process -Name iexplore -ErrorAction SilentlyContinue
L 'IE ESC disabled'
# 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
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c 2>&1 | Out-Null
powercfg /change monitor-timeout-ac 0 | Out-Null
powercfg /change disk-timeout-ac 0 | Out-Null
powercfg /change standby-timeout-ac 0 | Out-Null
powercfg /change hibernate-timeout-ac 0 | Out-Null
L 'Hibernate off, power plan High Performance'
# Unneeded services off (CI build VM, not a print/search/file server)
foreach (`$svc in 'WSearch','Spooler','SysMain','WerSvc','RemoteRegistry','Fax','MapsBroker','RetailDemo','XblAuthManager','XblGameSave','XboxNetApiSvc','XboxGipSvc') {
`$s = Get-Service -Name `$svc -ErrorAction SilentlyContinue
if (`$s) {
sc.exe config `$svc start= disabled | Out-Null
sc.exe stop `$svc | Out-Null
L " service disabled: `$svc"
}
}
# Windows Customer Experience tasks off
foreach (`$task in @(
'\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser',
'\Microsoft\Windows\Application Experience\ProgramDataUpdater',
'\Microsoft\Windows\Autochk\Proxy',
'\Microsoft\Windows\Customer Experience Improvement Program\Consolidator',
'\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip',
'\Microsoft\Windows\Feedback\Siuf\DmClient',
'\Microsoft\Windows\Feedback\Siuf\DmClientOnScenarioDownload'
)) {
schtasks /Change /TN `$task /Disable 2>&1 | Out-Null
}
L 'CEIP / feedback scheduled tasks 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)'
# 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)
# (b) vmtoolsd.exe on disk
# (c) VMTools service Running (not just registered install finalized)
# Hard timeout 15 min.
if (`$toolsExe) {
`$installDeadline = (Get-Date).AddMinutes(15)
while ((Get-Date) -lt `$installDeadline) {
Start-Sleep -Seconds 5
`$busy = Get-Process -ErrorAction SilentlyContinue |
Where-Object { `$_.ProcessName -in 'setup','setup64','VMware-Tools','VMware-Tools-Installer','vminst' }
`$vmtoolsd = Test-Path 'C:\Program Files\VMware\VMware Tools\vmtoolsd.exe'
`$svc = Get-Service -Name VMTools -ErrorAction SilentlyContinue
`$svcRunning = (`$svc -and `$svc.Status -eq 'Running')
L " install poll: busy=`$(`$busy.Count) vmtoolsd=`$vmtoolsd svc=`$(if (`$svc) { `$svc.Status } else { 'absent' })"
if (-not `$busy -and `$vmtoolsd -and `$svcRunning) { L 'Tools install settled.'; break }
}
if (Test-Path `$msiLog) {
L "---- MSI log tail (last 30 lines) ----"
Get-Content `$msiLog -Tail 30 | ForEach-Object { L `$_ }
L '---- end MSI log ----'
} else {
L "MSI log NOT created at `$msiLog (wrapper failed before MSI launched)"
}
if (Test-Path 'C:\Program Files\VMware\VMware Tools\vmtoolsd.exe') {
L 'vmtoolsd.exe present -> Tools installed OK'
} else {
L "WARNING: vmtoolsd.exe NOT present -> install failed (wrapper exit `$exitCode)"
}
}
# Drop completion flag (host polls for this via vmrun)
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"
"@
$psOut = Join-Path $stagingDir 'post-install.ps1'
Set-Content -LiteralPath $psOut -Value $postInstall -Encoding UTF8
Assert-Step 'Render' 'post-install.ps1 written' { Test-Path $psOut }
# Bundle VMware Tools contents under autounattend\Tools\ so post-install.ps1
# always finds setup64.exe on the same CD it boots from. Avoids the
# "third SATA CD-ROM disconnected at first logon" failure mode.
Write-Host ' Extracting VMware Tools ISO into autounattend staging...' -ForegroundColor Yellow
$toolsStage = Join-Path $stagingDir 'Tools'
New-Item -ItemType Directory -Path $toolsStage -Force | Out-Null
$toolsMount = Mount-DiskImage -ImagePath $ToolsISO -PassThru -Access ReadOnly
try {
$toolsLetter = "$(($toolsMount | Get-Volume).DriveLetter):\"
Copy-Item -Path (Join-Path $toolsLetter '*') -Destination $toolsStage -Recurse -Force
} finally {
Dismount-DiskImage -ImagePath $ToolsISO -ErrorAction SilentlyContinue | Out-Null
}
Assert-Step 'Render' 'Tools setup bundled in staging\Tools' {
(Test-Path (Join-Path $toolsStage 'setup.exe')) -or
(Test-Path (Join-Path $toolsStage 'setup64.exe'))
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 3: build autounattend ISO
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 3: build autounattend ISO via IMAPI2FS'
New-IsoFromFolder -SourceFolder $stagingDir -IsoPath $autounattendIso -VolumeLabel 'AUTOUNATTEND'
Assert-Step 'IsoBuild' "autounattend.iso created at $autounattendIso" { Test-Path $autounattendIso }
Assert-Step 'IsoBuild' 'autounattend.iso non-empty' { (Get-Item $autounattendIso).Length -gt 4096 }
# Cleanup staging — only ISO matters from here on. IMAPI2FS may hold a
# transient lock on bundled binaries; force GC then retry, then ignore.
[GC]::Collect(); [GC]::WaitForPendingFinalizers(); [GC]::Collect()
Start-Sleep -Seconds 2
Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path $stagingDir) {
Write-Host " [warn] staging dir not fully cleaned, leaving in place: $stagingDir" -ForegroundColor Yellow
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 4: create VMDK
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 4: create $DiskSizeGB GB VMDK (single-file growable)"
# -t 0 = single growable file (thin); -a lsilogic affects descriptor only,
# the disk attaches to the NVMe controller defined in the VMX.
$vdiskArgs = @('-c', '-s', "${DiskSizeGB}GB", '-a', 'lsilogic', '-t', '0', $vmdkPath)
& $vdiskMgr @vdiskArgs
if ($LASTEXITCODE -ne 0) { throw "vmware-vdiskmanager failed (exit $LASTEXITCODE)" }
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"
}
$srcMtime = (Get-Item $WinISO).LastWriteTime
$rebuild = $true
if (Test-Path $WinISONoPromptPath) {
if ((Get-Item $WinISONoPromptPath).LastWriteTime -ge $srcMtime) {
Write-Host " Cached no-prompt ISO is current, reusing: $WinISONoPromptPath" -ForegroundColor Green
$rebuild = $false
}
}
if ($rebuild) {
Write-Host " Building no-prompt ISO from $WinISO" -ForegroundColor Yellow
Write-Host " Output: $WinISONoPromptPath" -ForegroundColor Yellow
New-WinIsoNoPrompt -SourceIsoPath $WinISO -OutputIsoPath $WinISONoPromptPath
}
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 }
# ─────────────────────────────────────────────────────────────────────────────
# Step 5: generate VMX
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 5: generate VMX'
# 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'
# 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)
$vmxLines = @(
'.encoding = "windows-1252"',
'config.version = "8"',
'virtualHW.version = "21"',
"displayName = `"$VMName`"",
"guestOS = `"$guestOS`"",
'firmware = "efi"',
'uefi.secureBoot.enabled = "FALSE"',
"memSize = `"$MemoryMB`"",
"numvcpus = `"$VCPUCount`"",
"cpuid.coresPerSocket = `"$CoresPerSocket`"",
'tools.syncTime = "TRUE"',
'tools.upgrade.policy = "manual"',
'powerType.powerOff = "soft"',
'powerType.powerOn = "soft"',
'powerType.suspend = "soft"',
'powerType.reset = "soft"',
'uuid.action = "create"',
'',
'# PCIe root ports — required for explicit PCI slot assignments below.',
'# Each pcieRootPort exposes 8 slots → enough headroom for NVMe + SATA + NIC.',
'pciBridge0.present = "TRUE"',
'pciBridge4.present = "TRUE"',
'pciBridge4.virtualDev = "pcieRootPort"',
'pciBridge4.functions = "8"',
'pciBridge5.present = "TRUE"',
'pciBridge5.virtualDev = "pcieRootPort"',
'pciBridge5.functions = "8"',
'pciBridge6.present = "TRUE"',
'pciBridge6.virtualDev = "pcieRootPort"',
'pciBridge6.functions = "8"',
'pciBridge7.present = "TRUE"',
'pciBridge7.virtualDev = "pcieRootPort"',
'pciBridge7.functions = "8"',
'',
'# NVMe controller + 80 GB system disk',
'nvme0.present = "TRUE"',
'nvme0.pciSlotNumber = "224"',
'nvme0:0.present = "TRUE"',
"nvme0:0.fileName = `"$vmdkName`"",
'nvme0:0.deviceType = "disk"',
'',
'# Three SATA CD-ROMs: install / autounattend / Tools',
'sata0.present = "TRUE"',
'sata0.pciSlotNumber = "32"',
'sata0:0.present = "TRUE"',
'sata0:0.deviceType = "cdrom-image"',
"sata0:0.fileName = `"$WinISONoPromptPath`"",
'sata0:0.startConnected = "TRUE"',
'sata0:1.present = "TRUE"',
'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"',
'ethernet0.virtualDev = "e1000e"',
'ethernet0.connectionType = "nat"',
'ethernet0.addressType = "generated"',
'ethernet0.startConnected = "TRUE"',
'ethernet0.pciSlotNumber = "192"',
'',
'# USB / video',
'usb.present = "TRUE"',
'usb.pciSlotNumber = "34"',
'ehci.present = "TRUE"',
'ehci.pciSlotNumber = "35"',
'usb_xhci.present = "TRUE"',
'usb_xhci.pciSlotNumber = "33"',
'svga.present = "TRUE"',
'svga.graphicsMemoryKB = "262144"',
'',
'# No floppy / no sound (CI VM)',
'floppy0.present = "FALSE"',
'sound.present = "FALSE"',
'',
'# Misc',
'vmci0.present = "TRUE"',
'vmci0.pciSlotNumber = "36"',
'hpet0.present = "TRUE"',
'mks.enable3d = "FALSE"',
'snapshot.action = "keep"'
)
Set-Content -LiteralPath $VMXPath -Value $vmxLines -Encoding ASCII
Assert-Step 'Vmx' "VMX written: $VMXPath" { Test-Path $VMXPath }
# ─────────────────────────────────────────────────────────────────────────────
# 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) }
# ─────────────────────────────────────────────────────────────────────────────
# Step 7: wait for install_complete.flag inside guest
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 7: wait for guest install_complete.flag (max ${GuestPollMaxMinutes} min)"
$deadline = (Get-Date).AddMinutes($GuestPollMaxMinutes)
$flagPath = 'C:\Windows\Temp\install_complete.flag'
$flagSeen = $false
while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds $GuestPollSeconds
$toolsState = Invoke-Vmrun -Arguments @('-T','ws','checkToolsState',$VMXPath) -IgnoreErrors
Write-Host (" [{0:HH:mm:ss}] toolsState={1}" -f (Get-Date), ($toolsState -join ' '))
if ($toolsState -notmatch 'running') { continue }
# Tools running → can probe filesystem with guest creds.
$probe = & $vmrunPath -T ws -gu $AdminUser -gp $AdminPassword `
fileExistsInGuest $VMXPath $flagPath 2>&1
Write-Host (" fileExistsInGuest -> {0}" -f ($probe -join ' '))
if ($probe -match 'The file exists') { $flagSeen = $true; break }
}
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'
Invoke-Vmrun -Arguments @('-T','ws','stop',$VMXPath,'soft') -IgnoreErrors | Out-Null
# 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))
}
Set-VmxKey -VmxPath $VMXPath -Key 'ethernet0.virtualDev' -Value 'vmxnet3'
Assert-Step 'NicSwap' 'ethernet0.virtualDev = vmxnet3' {
(Get-Content -LiteralPath $VMXPath) -match '^\s*ethernet0\.virtualDev\s*=\s*"vmxnet3"\s*$'
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 9: power on, verify vmxnet3 + DHCP IP
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Step 9: power on with vmxnet3, verify DHCP IP'
Invoke-Vmrun -Arguments @('-T','ws','start',$VMXPath,$vmrunUiMode) | Out-Null
$ipDeadline = (Get-Date).AddMinutes(10)
$guestIP = $null
do {
Start-Sleep -Seconds 10
$guestIP = Invoke-Vmrun -Arguments @('-T','ws','getGuestIPAddress',$VMXPath,'-wait') -IgnoreErrors
Write-Host " guestIP probe -> $guestIP"
} 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+' }
# ─────────────────────────────────────────────────────────────────────────────
# Step 10: shutdown → snapshot (cold) → power on
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "Step 10: graceful shutdown, snapshot '$SnapshotName' (cold), power on"
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))
}
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
$ipDeadline = (Get-Date).AddMinutes(5)
do {
Start-Sleep -Seconds 10
$guestIP = Invoke-Vmrun -Arguments @('-T','ws','getGuestIPAddress',$VMXPath,'-wait') -IgnoreErrors
} 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+' }
# ─────────────────────────────────────────────────────────────────────────────
# Final summary
# ─────────────────────────────────────────────────────────────────────────────
Write-Step 'Done'
Write-Host ""
Write-Host " VMX : $VMXPath"
Write-Host " VMDK : $vmdkPath"
Write-Host " Hostname : $ComputerName"
Write-Host " Guest IP : $guestIP"
Write-Host " Admin user : $AdminUser"
Write-Host " Snapshot : $SnapshotName"
Write-Host ""
Write-Host " RDP : mstsc /v:$guestIP"
Write-Host " WinRM HTTP : 5985"
Write-Host " WinRM HTTPS : 5986 (self-signed)"
Write-Host ""
Write-Host " VM is powered on with vmxnet3 + DHCP. Live snapshot taken." -ForegroundColor Green
+195
View File
@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Windows Server 2025 unattended install template.
Double-brace placeholders are substituted by Deploy-WinBuild2025.ps1.
Boot NIC is e1000e (broad compat). VMware Tools install brings
vmxnet3 driver; the host script switches the VMX to vmxnet3
after Tools install completes.
Disk layout (UEFI/GPT, 80 GB):
Part 1: EFI 100 MB FAT32 S:
Part 2: MSR 16 MB (no fmt)
Part 3: Primary rest NTFS C:
No Recovery partition (per user choice).
Image index 2 = "Windows Server 2025 SERVERSTANDARD" (Desktop Experience)
on the VOL ISO. Verify with:
dism /Get-WimInfo /WimFile:D:\sources\install.wim
-->
<unattend xmlns="urn:schemas-microsoft-com:unattend"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS">
<SetupUILanguage>
<UILanguage>{{LOCALE_OS}}</UILanguage>
</SetupUILanguage>
<InputLocale>{{KEYBOARD}}</InputLocale>
<SystemLocale>{{LOCALE_OS}}</SystemLocale>
<UILanguage>{{LOCALE_OS}}</UILanguage>
<UILanguageFallback>{{LOCALE_OS}}</UILanguageFallback>
<UserLocale>{{LOCALE_USER}}</UserLocale>
</component>
<component name="Microsoft-Windows-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS">
<DiskConfiguration>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Type>EFI</Type>
<Size>100</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>2</Order>
<Type>MSR</Type>
<Size>16</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>3</Order>
<Type>Primary</Type>
<Extend>true</Extend>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order>
<PartitionID>1</PartitionID>
<Format>FAT32</Format>
<Label>System</Label>
<Letter>S</Letter>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order>
<PartitionID>2</PartitionID>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>3</Order>
<PartitionID>3</PartitionID>
<Format>NTFS</Format>
<Label>Windows</Label>
<Letter>C</Letter>
</ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/INDEX</Key>
<Value>{{IMAGE_INDEX}}</Value>
</MetaData>
</InstallFrom>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>3</PartitionID>
</InstallTo>
</OSImage>
</ImageInstall>
<UserData>
<AcceptEula>true</AcceptEula>
<FullName>CI</FullName>
<Organization>CI Lab</Organization>
<ProductKey>
<Key>{{PRODUCT_KEY}}</Key>
<WillShowUI>OnError</WillShowUI>
</ProductKey>
</UserData>
<UseConfigurationSet>false</UseConfigurationSet>
<DynamicUpdate>
<Enable>false</Enable>
<WillShowUI>Never</WillShowUI>
</DynamicUpdate>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS">
<ComputerName>{{COMPUTER_NAME}}</ComputerName>
<TimeZone>{{TIMEZONE}}</TimeZone>
<RegisteredOwner>CI</RegisteredOwner>
<RegisteredOrganization>CI Lab</RegisteredOrganization>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS">
<InputLocale>{{KEYBOARD}}</InputLocale>
<SystemLocale>{{LOCALE_OS}}</SystemLocale>
<UILanguage>{{LOCALE_OS}}</UILanguage>
<UILanguageFallback>{{LOCALE_OS}}</UILanguageFallback>
<UserLocale>{{LOCALE_USER}}</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS">
<UserAccounts>
<AdministratorPassword>
<Value>{{ADMIN_PWD}}</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
</UserAccounts>
<AutoLogon>
<Username>{{ADMIN_USER}}</Username>
<Password>
<Value>{{ADMIN_PWD}}</Value>
<PlainText>true</PlainText>
</Password>
<Enabled>true</Enabled>
<LogonCount>5</LogonCount>
</AutoLogon>
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>3</ProtectYourPC>
<SkipMachineOOBE>true</SkipMachineOOBE>
<SkipUserOOBE>true</SkipUserOOBE>
</OOBE>
<TimeZone>{{TIMEZONE}}</TimeZone>
<RegisteredOwner>CI</RegisteredOwner>
<RegisteredOrganization>CI Lab</RegisteredOrganization>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<CommandLine>cmd /c for %d in (C D E F G H I J K L M N O P Q R S T U V W X Y Z) do if exist %d:\post-install.ps1 powershell.exe -NoProfile -ExecutionPolicy Bypass -File %d:\post-install.ps1 ^&gt; C:\Windows\Temp\post-install.log 2^&gt;^&amp;1</CommandLine>
<Description>Run post-install script from autounattend CD</Description>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>