From 41a0a113df51d2163e91490869a2ed2aee4028a5 Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 24 May 2026 00:37:07 +0200 Subject: [PATCH] feat(templates): install ci-static-ip/ci-report-ip via Prepare+Install scripts Windows (Prepare-WinBuild2025/2022): read ci-static-ip.ps1 from guest-setup/windows/ on the host, push content over WinRM, register the CI-StaticIp scheduled task (AtStartup/SYSTEM). Adds StaticIpTask and StaticIpScript post-setup assertions. Linux (Install-CIToolchain-Linux2404.sh): embed ci-report-ip.sh and ci-report-ip.service content inline via heredoc tee, replacing the broken cp-from-relative-path approach (the script runs inside the VM where the host-side guest-setup/ directory does not exist). Co-Authored-By: Claude Sonnet 4.6 --- template/Install-CIToolchain-Linux2404.sh | 100 +++++++++++++++++++++- template/Prepare-WinBuild2022.ps1 | 45 ++++++++-- template/Prepare-WinBuild2025.ps1 | 49 +++++++++-- 3 files changed, 173 insertions(+), 21 deletions(-) diff --git a/template/Install-CIToolchain-Linux2404.sh b/template/Install-CIToolchain-Linux2404.sh index 41cad11..593e568 100644 --- a/template/Install-CIToolchain-Linux2404.sh +++ b/template/Install-CIToolchain-Linux2404.sh @@ -174,12 +174,106 @@ assert_step "99-ci-dhcp-allnics.yaml exists" test -f /etc/netplan/99-ci-dhcp-all # 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/. +# Content embedded inline — this script runs inside the VM where the host-side +# template/guest-setup/linux/ directory is not present. echo "" 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 tee /usr/local/bin/ci-report-ip.sh > /dev/null << 'EOREPORTIP' +#!/bin/bash +# CI VM IP reporter — called by ci-report-ip.service at boot. +# +# 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 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. + +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 +EOREPORTIP sudo chmod +x /usr/local/bin/ci-report-ip.sh -sudo cp "$(dirname "$0")/guest-setup/linux/ci-report-ip.service" /etc/systemd/system/ci-report-ip.service + +sudo tee /etc/systemd/system/ci-report-ip.service > /dev/null << 'EOSERVICE' +[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 +EOSERVICE sudo systemctl daemon-reload sudo systemctl enable ci-report-ip.service diff --git a/template/Prepare-WinBuild2022.ps1 b/template/Prepare-WinBuild2022.ps1 index 2acfb97..36e40ef 100644 --- a/template/Prepare-WinBuild2022.ps1 +++ b/template/Prepare-WinBuild2022.ps1 @@ -497,6 +497,29 @@ try { -Authentication Basic -ErrorAction Stop } + # ── Install CI-StaticIp scheduled task (ip_pool guestinfo static IP) ──────── + $staticIpScriptPath = Join-Path $scriptDir 'guest-setup\windows\ci-static-ip.ps1' + if (Test-Path $staticIpScriptPath -PathType Leaf) { + Write-Host "[Prepare] Installing CI-StaticIp scheduled task on guest..." + $staticIpContent = Get-Content -LiteralPath $staticIpScriptPath -Raw + Invoke-Command -Session $session -ScriptBlock { + param([string]$ScriptContent) + Set-Content -LiteralPath 'C:\CI\ci-static-ip.ps1' -Value $ScriptContent ` + -Encoding UTF8 -Force + $action = New-ScheduledTaskAction ` + -Execute 'powershell.exe' ` + -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\CI\ci-static-ip.ps1' + $trigger = New-ScheduledTaskTrigger -AtStartup + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' ` + -LogonType ServiceAccount -RunLevel Highest + Register-ScheduledTask -TaskName 'CI-StaticIp' -Action $action ` + -Trigger $trigger -Principal $principal -Force | Out-Null + } -ArgumentList $staticIpContent + Write-Host "[Prepare] CI-StaticIp task registered." -ForegroundColor Green + } else { + Write-Warning "[Prepare] guest-setup\windows\ci-static-ip.ps1 not found alongside this script — skipping CI-StaticIp task. ip_pool feature will not work for this template." + } + # ── Post-setup remote validation ────────────────────────────────────────── Write-Host "[Prepare] Running post-setup validation against guest..." $guestState = Invoke-Command -Session $session -ScriptBlock { @@ -505,15 +528,17 @@ try { $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH','User') [PSCustomObject]@{ - WinRMRunning = (Get-Service WinRM -ErrorAction SilentlyContinue).Status -eq 'Running' - UserExists = [bool](Get-LocalUser -Name $BuildUser -ErrorAction SilentlyContinue) - UserIsAdmin = [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUser) - UACDisabled = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA -eq 0 - FirewallOff = -not (Get-NetFirewallProfile | Where-Object Enabled -eq $true) - DotNetVersion = (Get-Command dotnet -ErrorAction SilentlyContinue) | ForEach-Object { (& $_.Source --version 2>&1) } - PythonExists = Test-Path 'C:\Python\python.exe' -PathType Leaf - MSBuildExists = Test-Path $msbuildPath -PathType Leaf - CIDirsPresent = (Test-Path 'C:\CI\build') -and (Test-Path 'C:\CI\output') -and (Test-Path 'C:\CI\scripts') + WinRMRunning = (Get-Service WinRM -ErrorAction SilentlyContinue).Status -eq 'Running' + UserExists = [bool](Get-LocalUser -Name $BuildUser -ErrorAction SilentlyContinue) + UserIsAdmin = [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUser) + UACDisabled = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA -eq 0 + FirewallOff = -not (Get-NetFirewallProfile | Where-Object Enabled -eq $true) + DotNetVersion = (Get-Command dotnet -ErrorAction SilentlyContinue) | ForEach-Object { (& $_.Source --version 2>&1) } + PythonExists = Test-Path 'C:\Python\python.exe' -PathType Leaf + MSBuildExists = Test-Path $msbuildPath -PathType Leaf + CIDirsPresent = (Test-Path 'C:\CI\build') -and (Test-Path 'C:\CI\output') -and (Test-Path 'C:\CI\scripts') + StaticIpTask = [bool](Get-ScheduledTask -TaskName 'CI-StaticIp' -ErrorAction SilentlyContinue) + StaticIpScript = Test-Path 'C:\CI\ci-static-ip.ps1' -PathType Leaf } } -ArgumentList $BuildUsername @@ -526,6 +551,8 @@ try { Assert-Step 'PostSetup' 'guest Python installed' { $guestState.PythonExists } Assert-Step 'PostSetup' 'guest MSBuild present' { $guestState.MSBuildExists } Assert-Step 'PostSetup' 'guest C:\CI\{build,output,scripts} present' { $guestState.CIDirsPresent } + Assert-Step 'PostSetup' 'guest CI-StaticIp task registered' { $guestState.StaticIpTask } + Assert-Step 'PostSetup' 'guest C:\CI\ci-static-ip.ps1 present' { $guestState.StaticIpScript } Write-Host "[Prepare] All post-setup validations passed (.NET $($guestState.DotNetVersion))." -ForegroundColor Green diff --git a/template/Prepare-WinBuild2025.ps1 b/template/Prepare-WinBuild2025.ps1 index d0d12bf..4623471 100644 --- a/template/Prepare-WinBuild2025.ps1 +++ b/template/Prepare-WinBuild2025.ps1 @@ -497,6 +497,33 @@ try { -Authentication Basic -ErrorAction Stop } + # ── Install CI-StaticIp scheduled task (ip_pool guestinfo static IP) ──────── + # Reads ci-static-ip.ps1 from alongside this script and installs it into the + # guest as C:\CI\ci-static-ip.ps1, then registers the CI-StaticIp task + # (At Startup, SYSTEM) so the guest applies a pre-assigned static IP from + # guestinfo.ip-assignment before WinRM starts on each clone boot. + $staticIpScriptPath = Join-Path $scriptDir 'guest-setup\windows\ci-static-ip.ps1' + if (Test-Path $staticIpScriptPath -PathType Leaf) { + Write-Host "[Prepare] Installing CI-StaticIp scheduled task on guest..." + $staticIpContent = Get-Content -LiteralPath $staticIpScriptPath -Raw + Invoke-Command -Session $session -ScriptBlock { + param([string]$ScriptContent) + Set-Content -LiteralPath 'C:\CI\ci-static-ip.ps1' -Value $ScriptContent ` + -Encoding UTF8 -Force + $action = New-ScheduledTaskAction ` + -Execute 'powershell.exe' ` + -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\CI\ci-static-ip.ps1' + $trigger = New-ScheduledTaskTrigger -AtStartup + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' ` + -LogonType ServiceAccount -RunLevel Highest + Register-ScheduledTask -TaskName 'CI-StaticIp' -Action $action ` + -Trigger $trigger -Principal $principal -Force | Out-Null + } -ArgumentList $staticIpContent + Write-Host "[Prepare] CI-StaticIp task registered." -ForegroundColor Green + } else { + Write-Warning "[Prepare] guest-setup\windows\ci-static-ip.ps1 not found alongside this script — skipping CI-StaticIp task. ip_pool feature will not work for this template." + } + # ── Post-setup remote validation ────────────────────────────────────────── Write-Host "[Prepare] Running post-setup validation against guest..." $guestState = Invoke-Command -Session $session -ScriptBlock { @@ -505,15 +532,17 @@ try { $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH','User') [PSCustomObject]@{ - WinRMRunning = (Get-Service WinRM -ErrorAction SilentlyContinue).Status -eq 'Running' - UserExists = [bool](Get-LocalUser -Name $BuildUser -ErrorAction SilentlyContinue) - UserIsAdmin = [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUser) - UACDisabled = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA -eq 0 - FirewallOff = -not (Get-NetFirewallProfile | Where-Object Enabled -eq $true) - DotNetVersion = (Get-Command dotnet -ErrorAction SilentlyContinue) | ForEach-Object { (& $_.Source --version 2>&1) } - PythonExists = Test-Path 'C:\Python\python.exe' -PathType Leaf - MSBuildExists = Test-Path $msbuildPath -PathType Leaf - CIDirsPresent = (Test-Path 'C:\CI\build') -and (Test-Path 'C:\CI\output') -and (Test-Path 'C:\CI\scripts') + WinRMRunning = (Get-Service WinRM -ErrorAction SilentlyContinue).Status -eq 'Running' + UserExists = [bool](Get-LocalUser -Name $BuildUser -ErrorAction SilentlyContinue) + UserIsAdmin = [bool](& net localgroup Administrators 2>&1 | Select-String -SimpleMatch $BuildUser) + UACDisabled = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA -eq 0 + FirewallOff = -not (Get-NetFirewallProfile | Where-Object Enabled -eq $true) + DotNetVersion = (Get-Command dotnet -ErrorAction SilentlyContinue) | ForEach-Object { (& $_.Source --version 2>&1) } + PythonExists = Test-Path 'C:\Python\python.exe' -PathType Leaf + MSBuildExists = Test-Path $msbuildPath -PathType Leaf + CIDirsPresent = (Test-Path 'C:\CI\build') -and (Test-Path 'C:\CI\output') -and (Test-Path 'C:\CI\scripts') + StaticIpTask = [bool](Get-ScheduledTask -TaskName 'CI-StaticIp' -ErrorAction SilentlyContinue) + StaticIpScript = Test-Path 'C:\CI\ci-static-ip.ps1' -PathType Leaf } } -ArgumentList $BuildUsername @@ -526,6 +555,8 @@ try { Assert-Step 'PostSetup' 'guest Python installed' { $guestState.PythonExists } Assert-Step 'PostSetup' 'guest MSBuild present' { $guestState.MSBuildExists } Assert-Step 'PostSetup' 'guest C:\CI\{build,output,scripts} present' { $guestState.CIDirsPresent } + Assert-Step 'PostSetup' 'guest CI-StaticIp task registered' { $guestState.StaticIpTask } + Assert-Step 'PostSetup' 'guest C:\CI\ci-static-ip.ps1 present' { $guestState.StaticIpScript } Write-Host "[Prepare] All post-setup validations passed (.NET $($guestState.DotNetVersion))." -ForegroundColor Green