diff --git a/template/Install-CIToolchain-Linux2404.sh b/template/Install-CIToolchain-Linux2404.sh index 4b95962..41cad11 100644 --- a/template/Install-CIToolchain-Linux2404.sh +++ b/template/Install-CIToolchain-Linux2404.sh @@ -170,51 +170,16 @@ sudo netplan generate assert_step "99-ci-dhcp-allnics.yaml exists" test -f /etc/netplan/99-ci-dhcp-allnics.yaml # --- Step 7b: CI IP reporter via VMware guestinfo --- -# The host reads the guest IP via `vmrun readVariable guestVar ci-ip`. -# IMPORTANT: vmware-rpctool sets 'guestinfo.ci-ip' but vmrun reads it as just -# 'ci-ip' under the guestVar namespace (the 'guestinfo.' prefix is implicit). -# This is the official VMware VMCI channel — works even when TCP/IP is not yet -# fully initialised on the host. Used as primary; getGuestIPAddress is fallback. +# Supports two modes: +# Static (ip_pool): reads guestinfo.ip-assignment injected by the host +# orchestrator, applies static NIC config, reports back. +# DHCP fallback: waits for DHCP address, reports via guestinfo.ci-ip. +# The script and service are sourced from template/guest-setup/linux/. echo "" -echo "=== Step 7b: CI IP reporter ===" -sudo tee /usr/local/bin/ci-report-ip.sh > /dev/null << 'EOSCRIPT' -#!/bin/bash -# Writes the VM's primary IPv4 to VMware guestinfo.ci-ip on every boot. -# Called by ci-report-ip.service. -# Retries for up to 60s to tolerate open-vm-tools re-init after UUID change -# (linked clones get a new UUID — vmware-rpctool may fail briefly on first boot). -for i in $(seq 1 30); do - IP=$(ip -4 route get 1.0.0.0 2>/dev/null \ - | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' \ - | head -1) - if [ -n "$IP" ]; then - if /usr/bin/vmware-rpctool "info-set guestinfo.ci-ip $IP" 2>/dev/null; then - echo "ci-report-ip: set guestinfo.ci-ip=$IP (attempt $i)" | systemd-cat -t ci-report-ip - exit 0 - fi - fi - sleep 2 -done -echo "ci-report-ip: failed to set guestinfo.ci-ip after 30 attempts" | systemd-cat -t ci-report-ip -exit 1 -EOSCRIPT +echo "=== Step 7b: CI IP reporter (with static IP support) ===" +sudo cp "$(dirname "$0")/guest-setup/linux/ci-report-ip.sh" /usr/local/bin/ci-report-ip.sh sudo chmod +x /usr/local/bin/ci-report-ip.sh - -sudo tee /etc/systemd/system/ci-report-ip.service > /dev/null << 'EOSERVICE' -[Unit] -Description=Report CI VM IP to VMware host via guestinfo -After=open-vm-tools.service network.target -Wants=open-vm-tools.service - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/ci-report-ip.sh -RemainAfterExit=yes -TimeoutStartSec=120 - -[Install] -WantedBy=multi-user.target -EOSERVICE +sudo cp "$(dirname "$0")/guest-setup/linux/ci-report-ip.service" /etc/systemd/system/ci-report-ip.service sudo systemctl daemon-reload sudo systemctl enable ci-report-ip.service @@ -268,4 +233,4 @@ echo "" echo "=== Install-CIToolchain-Linux2404.sh complete ===" echo " All steps passed. VM is ready for BaseClean-Linux snapshot." echo " Run Prepare-LinuxBuild2404.ps1 --Step7Shutdown + snapshot from host." - + diff --git a/template/guest-setup/linux/ci-report-ip.service b/template/guest-setup/linux/ci-report-ip.service new file mode 100644 index 0000000..19f7c08 --- /dev/null +++ b/template/guest-setup/linux/ci-report-ip.service @@ -0,0 +1,16 @@ +[Unit] +Description=Report CI VM IP to VMware host via guestinfo +# Run after open-vm-tools so vmware-rpctool is available. +# Do NOT add network-online.target — we want to run BEFORE the network stack +# settles so static IP is applied before SSH/other services start. +After=open-vm-tools.service +Wants=open-vm-tools.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/ci-report-ip.sh +RemainAfterExit=yes +TimeoutStartSec=120 + +[Install] +WantedBy=multi-user.target diff --git a/template/guest-setup/linux/ci-report-ip.sh b/template/guest-setup/linux/ci-report-ip.sh new file mode 100644 index 0000000..4adb50b --- /dev/null +++ b/template/guest-setup/linux/ci-report-ip.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# CI VM IP reporter — called by ci-report-ip.service at boot. +# +# Phase C extension: reads guestinfo.ip-assignment from the VMX (written by the +# host orchestrator before vmrun start). If present, applies a static IP to +# the primary NIC and reports it back immediately via guestinfo.ci-ip, skipping +# DHCP entirely. If absent, falls back to the original DHCP-detect behaviour. +# +# The VMCI channel (vmware-rpctool) does not require TCP/IP to be initialised — +# it communicates via the VMware virtual hardware interface. This means the +# static IP can be configured before network-online.target, so by the time SSH +# starts listening the NIC already has the correct address. +# +# Install: +# sudo cp ci-report-ip.sh /usr/local/bin/ci-report-ip.sh +# sudo chmod +x /usr/local/bin/ci-report-ip.sh +# sudo cp ci-report-ip.service /etc/systemd/system/ci-report-ip.service +# sudo systemctl daemon-reload +# sudo systemctl enable ci-report-ip.service + +RPCTOOL=/usr/bin/vmware-rpctool +[ -x "$RPCTOOL" ] || exit 0 + +# ── Check for pre-assigned IP from orchestrator ─────────────────────────────── +assigned_ip=$("$RPCTOOL" "info-get guestinfo.ip-assignment" 2>/dev/null | tr -d '[:space:]') + +if [[ "$assigned_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + netmask=$("$RPCTOOL" "info-get guestinfo.netmask" 2>/dev/null | tr -d '[:space:]') + gateway=$("$RPCTOOL" "info-get guestinfo.gateway" 2>/dev/null | tr -d '[:space:]') + + # Resolve primary NIC (first non-loopback). + iface=$(ip -o link show | awk -F': ' '$2 != "lo" {print $2; exit}') + if [ -z "$iface" ]; then + echo "ci-report-ip: no non-loopback interface found" | systemd-cat -t ci-report-ip + exit 1 + fi + + # Compute prefix length from netmask (default /24). + prefix=24 + if [ -n "$netmask" ]; then + prefix=$(python3 -c \ + "import ipaddress; print(ipaddress.IPv4Network('0.0.0.0/$netmask',strict=False).prefixlen)" \ + 2>/dev/null) || prefix=24 + fi + + # Flush any existing config (DHCP or stale static) and apply. + ip addr flush dev "$iface" 2>/dev/null || true + ip addr add "${assigned_ip}/${prefix}" dev "$iface" + ip link set "$iface" up + + if [[ "$gateway" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + ip route add default via "$gateway" dev "$iface" 2>/dev/null || true + fi + + # Report IP back to host via guestinfo.ci-ip. + "$RPCTOOL" "info-set guestinfo.ci-ip $assigned_ip" 2>/dev/null + echo "ci-report-ip: applied static IP $assigned_ip/$prefix via guestinfo" \ + | systemd-cat -t ci-report-ip + exit 0 +fi + +# ── Fallback: DHCP — wait for IP and report ─────────────────────────────────── +# Retries for up to 60 s to tolerate open-vm-tools re-init after UUID change +# (linked clones get a new UUID — vmware-rpctool may fail briefly on first boot). +for i in $(seq 1 30); do + IP=$(ip -4 route get 1.0.0.0 2>/dev/null \ + | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' \ + | head -1) + if [ -n "$IP" ]; then + if "$RPCTOOL" "info-set guestinfo.ci-ip $IP" 2>/dev/null; then + echo "ci-report-ip: set guestinfo.ci-ip=$IP via DHCP (attempt $i)" \ + | systemd-cat -t ci-report-ip + exit 0 + fi + fi + sleep 2 +done + +echo "ci-report-ip: failed to set guestinfo.ci-ip after 30 attempts" \ + | systemd-cat -t ci-report-ip +exit 1 diff --git a/template/guest-setup/windows/ci-static-ip.ps1 b/template/guest-setup/windows/ci-static-ip.ps1 new file mode 100644 index 0000000..9c1b948 --- /dev/null +++ b/template/guest-setup/windows/ci-static-ip.ps1 @@ -0,0 +1,107 @@ +#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' + +$vmtoolsRpc = 'C:\Program Files\VMware\VMware Tools\vmware-rpctool.exe' +if (-not (Test-Path $vmtoolsRpc -PathType Leaf)) { exit 0 } + +function Read-GuestInfo { + param([string]$Key) + $savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue' + $out = & $vmtoolsRpc "info-get guestinfo.$Key" 2>&1 + $ErrorActionPreference = $savedEap + if ($LASTEXITCODE -eq 0 -and $out -notmatch 'No value found|^$') { + return ($out | Out-String).Trim() + } + 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. +$adapter | Set-NetIPInterface -Dhcp Disabled -ErrorAction SilentlyContinue +$adapter | Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue + +# Apply static IP. +$ipParams = @{ + InterfaceAlias = $adapter.Name + IPAddress = $ip + PrefixLength = $prefix + ErrorAction = 'Stop' +} +if ($gateway -match '^\d{1,3}(\.\d{1,3}){3}$') { + $ipParams['DefaultGateway'] = $gateway +} +New-NetIPAddress @ipParams | 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' +& $vmtoolsRpc "info-set guestinfo.ci-ip $ip" 2>&1 | Out-Null +$ErrorActionPreference = $savedEap + +exit 0