#Requires -Version 5.1 <# .SYNOPSIS Applies a pre-assigned static IP from VMware guestinfo at boot. .DESCRIPTION Called by the CI-StaticIp Task Scheduler task at system startup (SYSTEM account). Reads guestinfo.ip-assignment written by the host orchestrator before vmrun start. If present, disables DHCP and applies the static IP before WinRM has a chance to start on a DHCP address, eliminating the 20-180 s VMware-Tools IP-detect polling loop. If guestinfo.ip-assignment is absent (template used standalone or ip_pool not configured), exits 0 immediately — DHCP runs normally. Install into the template VM: 1. Copy this script to C:\CI\ci-static-ip.ps1 2. Create a Task Scheduler task: Name: CI-StaticIp Trigger: At System Startup Action: powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\CI\ci-static-ip.ps1 Run As: SYSTEM Run with highest privileges: Yes 3. Take the BaseClean snapshot from powered-OFF state. The VMCI channel (vmware-rpctool) does not require TCP/IP — it works via the VMware virtual hardware interface, so guestinfo is readable before the NIC is configured. #> Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # vmware-rpctool.exe was removed in recent VMware Tools versions; use vmtoolsd --cmd instead. $vmtoolsd = 'C:\Program Files\VMware\VMware Tools\vmtoolsd.exe' if (-not (Test-Path $vmtoolsd -PathType Leaf)) { exit 0 } function Read-GuestInfo { param([string]$Key) # vmtoolsd writes to a file handle but not to a PS pipeline — use cmd.exe redirection. $tmp = [System.IO.Path]::GetTempFileName() try { $savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue' cmd /c "`"$vmtoolsd`" --cmd `"info-get guestinfo.$Key`" > `"$tmp`" 2>&1" $ErrorActionPreference = $savedEap $text = (Get-Content $tmp -ErrorAction SilentlyContinue | Out-String).Trim() if ($text -and $text -notmatch 'No value found|Error') { return $text } } finally { Remove-Item $tmp -ErrorAction SilentlyContinue } return '' } $ip = Read-GuestInfo 'ip-assignment' $netmask = Read-GuestInfo 'netmask' $gateway = Read-GuestInfo 'gateway' # No pre-assigned IP — let DHCP run normally. if (-not ($ip -match '^\d{1,3}(\.\d{1,3}){3}$')) { exit 0 } # Convert dotted-decimal netmask to prefix length (default /24). function Get-PrefixLength { param([string]$Mask) if (-not ($Mask -match '^\d{1,3}(\.\d{1,3}){3}$')) { return 24 } $bits = 0 foreach ($octet in $Mask.Split('.')) { $b = [int]$octet while ($b -gt 0) { $bits += ($b -band 1); $b = $b -shr 1 } } return $bits } $prefix = Get-PrefixLength $netmask # Target the first Up adapter (skip loopback / Teredo / Bluetooth). $adapter = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.InterfaceDescription -notmatch 'Loopback|Bluetooth|Teredo' } | Select-Object -First 1 if (-not $adapter) { Write-EventLog -LogName Application -Source 'ci-static-ip' -EventId 1 ` -EntryType Error -Message 'No Up adapter found.' -ErrorAction SilentlyContinue exit 1 } # Disable DHCP and remove any existing IPv4 addresses and routes. $adapter | Set-NetIPInterface -Dhcp Disabled -ErrorAction SilentlyContinue Get-NetRoute -InterfaceAlias $adapter.Name -AddressFamily IPv4 -ErrorAction SilentlyContinue | Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue $adapter | Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue # Apply static IP (no DefaultGateway here — add route separately to avoid conflicts). New-NetIPAddress -InterfaceAlias $adapter.Name -IPAddress $ip ` -PrefixLength $prefix -ErrorAction Stop | Out-Null # Add default route if gateway supplied. if ($gateway -match '^\d{1,3}(\.\d{1,3}){3}$') { New-NetRoute -InterfaceAlias $adapter.Name -DestinationPrefix '0.0.0.0/0' ` -NextHop $gateway -ErrorAction SilentlyContinue | Out-Null } # Use the gateway as DNS (fallback: Google DNS). $dns = @() if ($gateway -match '^\d{1,3}(\.\d{1,3}){3}$') { $dns += $gateway } $dns += '8.8.8.8' Set-DnsClientServerAddress -InterfaceAlias $adapter.Name ` -ServerAddresses $dns -ErrorAction SilentlyContinue # Report IP back via guestinfo.ci-ip so the host also has it. $savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue' & $vmtoolsd --cmd "info-set guestinfo.ci-ip $ip" 2>&1 | Out-Null $ErrorActionPreference = $savedEap exit 0