<# ============================================================================= Monitoring agent installer for a Windows host. Installs Grafana Alloy as the only agent. It collects metrics (windows_exporter is built into Alloy) and the Windows Event Log (Application, System), and PUSHES both over HTTPS to the monitoring server. Nothing listens on this host; it only needs outbound 443. 1. On the monitoring server: ./host.sh add win-app01 prints a one-time enrollment code and these commands. 2. On this host, in an elevated PowerShell: curl.exe -fsSLo install.ps1 https:///install-windows.ps1 powershell -ExecutionPolicy Bypass -File .\install.ps1 -EnrollCode The served copy already knows -IngestUrl. The host's name comes from the enrollment. Re-run without -EnrollCode to upgrade/repair (the token is kept); with a new code to re-enroll. ============================================================================= #> #Requires -RunAsAdministrator param( # One-time code from `./host.sh add ` on the monitoring server. [string]$EnrollCode = "", # The served copy of this script fills in this default. [string]$IngestUrl = "" # @default:IngestUrl ) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # PS 5.1 downloads crawl with the progress bar on [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Pinned version. downloads/fetch.sh on the server reads it to build the # download site, so changing it here changes what the server serves. $AlloyVer = "1.5.1" function Log($m) { Write-Host "[install] $m" -ForegroundColor Green } function Warn($m) { Write-Host "[warn] $m" -ForegroundColor Yellow } $IngestUrl = $IngestUrl.TrimEnd('/') if (-not $IngestUrl) { throw "-IngestUrl is required (https://), or use the copy served by the monitoring server." } if (-not $IngestUrl.StartsWith("https://")) { throw "-IngestUrl must be https:// (got $IngestUrl)" } $alloyDir = Join-Path ${env:ProgramFiles} "GrafanaLabs\Alloy" $tokenFile = Join-Path $alloyDir "monitor-token" $nameFile = Join-Path $alloyDir "monitor-node-name" if (-not $EnrollCode -and -not (Test-Path -LiteralPath $tokenFile)) { throw "No enrollment code. On the monitoring server run: ./host.sh add and use the command it prints." } $tmp = Join-Path $env:TEMP ("mon-agent-" + [guid]::NewGuid()) New-Item -ItemType Directory -Path $tmp | Out-Null # Get-Artifact : download from the server, verify against its SHA256SUMS. function Get-Artifact($Path, $Dest) { Invoke-WebRequest -UseBasicParsing -Uri "$IngestUrl/$Path" -OutFile $Dest $sumFile = Join-Path $tmp "SHA256SUMS" if (-not (Test-Path $sumFile)) { Invoke-WebRequest -UseBasicParsing -Uri "$IngestUrl/SHA256SUMS" -OutFile $sumFile } $want = Get-Content $sumFile | ForEach-Object { $p = $_ -split '\s+', 2; if ($p[1] -eq $Path) { $p[0] } } | Select-Object -First 1 if (-not $want) { throw "$Path is not listed in the server's SHA256SUMS" } $got = (Get-FileHash -Algorithm SHA256 -LiteralPath $Dest).Hash.ToLower() if ($got -ne $want.ToLower()) { throw "CHECKSUM MISMATCH for $Path" } } # Restrict a file to SYSTEM (the Alloy service) and Administrators. function Protect-File($Path) { & icacls.exe $Path /inheritance:r /grant:r "*S-1-5-18:R" "*S-1-5-32-544:F" | Out-Null } try { # ------------------------------------------------------------------ # 1) Enroll first, so a bad code fails before anything is installed. # ------------------------------------------------------------------ if ($EnrollCode) { Log "Enrolling with $IngestUrl ..." try { $r = Invoke-WebRequest -UseBasicParsing -Method Post -TimeoutSec 30 -Uri "$IngestUrl/enroll" ` -Body @{ code = $EnrollCode; kind = "windows" } } catch { $why = if ($_.ErrorDetails.Message) { $_.ErrorDetails.Message.Trim() } else { $_.Exception.Message } throw "enrollment failed: $why" } $kv = ConvertFrom-StringData ($r.Content.Trim()) if (-not $kv.name -or -not $kv.token) { throw "unexpected enrollment response" } $NodeName = $kv.name New-Item -ItemType Directory -Force -Path $alloyDir | Out-Null Set-Content -LiteralPath $tokenFile -Value $kv.token -NoNewline -Encoding ASCII Set-Content -LiteralPath $nameFile -Value $NodeName -NoNewline -Encoding ASCII Protect-File $tokenFile Log "Enrolled as '$NodeName'." } else { $NodeName = (Get-Content -LiteralPath $nameFile -Raw).Trim() Log "Keeping the existing enrollment ('$NodeName')." } # ------------------------------------------------------------------ # 2) Grafana Alloy # ------------------------------------------------------------------ Log "Downloading Grafana Alloy $AlloyVer ..." $zip = Join-Path $tmp "alloy.zip" Get-Artifact "bin/alloy-$AlloyVer-installer-windows-amd64.exe.zip" $zip Expand-Archive -Path $zip -DestinationPath $tmp -Force Log "Installing Alloy service ..." Start-Process (Join-Path $tmp "alloy-installer-windows-amd64.exe") -ArgumentList "/S" -Wait Log "Writing Alloy config ..." $tpl = Join-Path $PSScriptRoot "config.alloy.template" if (-not (Test-Path -LiteralPath $tpl)) { $tpl = Join-Path $tmp "config.alloy.template" Get-Artifact "windows/config.alloy.template" $tpl } $cfg = (Get-Content $tpl -Raw).Replace("__NODE_NAME__", $NodeName). Replace("__INGEST_URL__", $IngestUrl). Replace("__TOKEN_FILE__", $tokenFile.Replace('\', '/')) Set-Content -Path (Join-Path $alloyDir "config.alloy") -Value $cfg -Encoding UTF8 Log "Restarting Alloy ..." Restart-Service -Name "Alloy" # ------------------------------------------------------------------ # 3) The old pull agent (windows_exporter on :9182) is no longer needed. # ------------------------------------------------------------------ if (Get-Service -Name "windows_exporter" -ErrorAction SilentlyContinue) { Warn "windows_exporter (the old pull agent on :9182) is still installed. Alloy replaces it;" Warn "remove it via Apps & features, and the 'windows_exporter (Prometheus)' firewall rule." } # ------------------------------------------------------------------ # 4) Confirm the server is receiving data. # ------------------------------------------------------------------ Log "Waiting for the first push to reach the server ..." $token = (Get-Content -LiteralPath $tokenFile -Raw).Trim() $ok = $false for ($i = 0; $i -lt 30 -and -not $ok; $i++) { try { $s = Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 -Uri "$IngestUrl/status" ` -Headers @{ Authorization = "Bearer $token" } $age = [int](ConvertFrom-StringData ($s.Content.Trim())).last_push_age if ($age -ge 0 -and $age -le 90) { $ok = $true; break } } catch { } Start-Sleep -Seconds 3 } Write-Host "" if ($ok) { Log "Done. The server is receiving data from '$NodeName' - it appears in Grafana within a minute." } else { Warn "Alloy is installed, but the server has not received a push from '$NodeName' yet." Warn "Check the 'Alloy' service and the Application event log (this host needs outbound HTTPS to $IngestUrl)." } } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue }