Reports a Windows machine the way the Linux agent does, replacing the
"planned" stub in agent/windows: scheduled tasks plus hostname, IPv4
addresses, CPU model/cores/current load, memory, every fixed disk, and
TCP/UDP listening ports with the owning process (which feed the Ports
card, localhost-only listeners included).
Scripts (plain ASCII by design -- they are downloaded as text and Windows
PowerShell 5.1 reads BOM-less files as ANSI):
- report-tasks.ps1: collects and POSTs to /api/agent/report. Works in
Windows PowerShell 5.1 and PowerShell 7. -DryRun prints the JSON.
Microsoft's own \Microsoft\ tasks (hundreds) are left out unless
INCLUDE_MICROSOFT_TASKS is set. Triggers are turned into readable text
("Weekly on Mon, Wed at 03:00", "At logon", "..., repeating every 15 min").
Self-signed certificates work via API_INSECURE on both PowerShell
versions (they need different mechanisms).
- install.ps1: elevated only; downloads the agent to ProgramData, writes
agent.json with permissions locked to SYSTEM and Administrators *before*
the token goes in, and registers a SYSTEM scheduled task (every 15 min
plus at startup with a 2 min delay). Reinstalling replaces the task.
- uninstall.ps1: removes the task and only the files the agent installed.
Server: accepts schedule_type "windows_task"; a server can be registered
as Windows (Add a server has an operating system choice); an agent's
reported os_type ("linux"/"windows", anything else ignored) corrects the
stored one. The Servers page shows the right install and uninstall
command for each OS (Windows PowerShell 5.1 one-liners, with a self-signed
variant and a note about PowerShell 7), and Windows tasks are labelled
"Windows scheduled tasks". The Linux commands are unchanged.
Verified on this Windows machine, in both PowerShell 5.1 and 7:
- Real dry runs found and fixed bugs before anything shipped: tasks and
ports came out as one nested item (return , $out wrapped twice), integer
keys in an ordered dictionary index by position (wrong weekday names),
and generic "Trigger" labels.
- End to end against the real agent-report router: HTTP, self-signed HTTPS
refused by default and accepted with API_INSECURE, wrong token gives a
clear one-line error and exit 1, and Swedish letters plus a euro sign
survive JSON -> UTF-8 -> HTTP -> SQLite.
- 35 checks on trigger/action/duration descriptions, 20 on the installer's
building blocks (task parts built but not registered, credentials file
content and ACL, download over HTTP and self-signed HTTPS), 18 on the
server rules, and the generated one-liners run through PowerShell's
parser. The documented one-liners were run through iex and stop at the
administrator check without changing anything.
- Found that PowerShell 7 ignores the ServicePointManager certificate
override, so the installer's own download now uses -SkipCertificateCheck
there.
NOT verified: the elevated install itself. Registering a SYSTEM scheduled
task needs elevation and changes the machine, so it was not run: the task
registration, that the repeating trigger really runs indefinitely, and
the agent running as SYSTEM under Task Scheduler have not been exercised.
Windows 10 / Server 2016 or newer is assumed; older is untested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
7.0 KiB
PowerShell
124 lines
7.0 KiB
PowerShell
# Installs the Homelab Manager agent on Windows as a scheduled task that runs as SYSTEM.
|
|
#
|
|
# Run in an ELEVATED Windows PowerShell (Run as administrator), with your server's URL and this
|
|
# server's token from the Servers page:
|
|
#
|
|
# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
# $env:API_URL = 'https://homelab.example.lan'; $env:API_TOKEN = 'hlm_xxx'
|
|
# iex ((New-Object Net.WebClient).DownloadString("$env:API_URL/agent/windows/install.ps1"))
|
|
#
|
|
# If Homelab Manager uses a self-signed certificate, also set API_INSECURE (this skips certificate checks
|
|
# for every request the agent makes - only on a trusted LAN) and skip the check for the download itself,
|
|
# since fetching this very script hits the same certificate:
|
|
#
|
|
# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
# [Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
|
|
# $env:API_URL = 'https://homelab.example.lan'; $env:API_TOKEN = 'hlm_xxx'; $env:API_INSECURE = 'true'
|
|
# iex ((New-Object Net.WebClient).DownloadString("$env:API_URL/agent/windows/install.ps1"))
|
|
#
|
|
# Optional: INTERVAL_MINUTES (default 15).
|
|
#
|
|
# NOTE: keep this file plain ASCII (it is downloaded as text).
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$script:TaskName = 'Homelab Manager Agent'
|
|
$script:InstallDir = Join-Path $env:ProgramData 'HomelabManager'
|
|
|
|
function Test-Administrator {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
return ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
}
|
|
|
|
function Test-Truthy([string]$Value) { return ($Value -match '^(1|true|yes)$') }
|
|
|
|
# Downloads a file. Windows PowerShell 5.1 needs TLS 1.2 switched on and takes the self-signed-certificate override through
|
|
# ServicePointManager; PowerShell 7 ignores that setting and has its own switch.
|
|
function Save-Url([string]$Url, [string]$Path, [bool]$Insecure) {
|
|
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
$params = @{ Uri = $Url; OutFile = $Path }
|
|
if ($Insecure) { $params['SkipCertificateCheck'] = $true }
|
|
Invoke-WebRequest @params
|
|
return
|
|
}
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
if ($Insecure) { [Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } }
|
|
$client = New-Object System.Net.WebClient
|
|
try { $client.DownloadFile($Url, $Path) } finally { $client.Dispose() }
|
|
}
|
|
|
|
# Extra principals (as "*SID:(F)") allowed to write the credentials file. Empty in real use - an elevated installer already
|
|
# holds Administrators - and only there so a test running without elevation can still write to its own temporary file.
|
|
$script:ExtraConfigGrants = @()
|
|
|
|
# The credentials file holds the token, so only SYSTEM and Administrators may read it. Identified by SID so this works on any Windows language.
|
|
function Save-AgentConfig([string]$Path, [string]$ApiUrl, [string]$ApiToken, [bool]$Insecure) {
|
|
$config = [ordered]@{ apiUrl = $ApiUrl; apiToken = $ApiToken; insecure = $Insecure }
|
|
# Write with restrictive permissions already in place, so the token is never readable by anyone else, even briefly.
|
|
[System.IO.File]::WriteAllText($Path, '', (New-Object System.Text.UTF8Encoding($false)))
|
|
$grants = @('*S-1-5-18:(F)', '*S-1-5-32-544:(F)') + @($script:ExtraConfigGrants)
|
|
& icacls.exe $Path /inheritance:r /grant:r $grants | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "Couldn't restrict permissions on $Path (icacls exit $LASTEXITCODE)." }
|
|
[System.IO.File]::WriteAllText($Path, (ConvertTo-Json -InputObject $config), (New-Object System.Text.UTF8Encoding($false)))
|
|
}
|
|
|
|
# The pieces of the scheduled task, built but not registered.
|
|
function New-AgentTaskParts([string]$AgentScript, [int]$IntervalMinutes) {
|
|
$argument = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}"' -f $AgentScript
|
|
$repeat = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(1)) -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes)
|
|
$boot = New-ScheduledTaskTrigger -AtStartup
|
|
$boot.Delay = 'PT2M' # let the network come up before the first report
|
|
return @{
|
|
Action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $argument
|
|
Triggers = @($repeat, $boot)
|
|
Principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
|
Settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
|
}
|
|
}
|
|
|
|
function Install-Agent {
|
|
if (-not (Test-Administrator)) {
|
|
throw 'This installer must be run as Administrator (it registers a scheduled task that runs as SYSTEM). Open PowerShell with "Run as administrator" and try again.'
|
|
}
|
|
|
|
$apiUrl = ([string]$env:API_URL).TrimEnd('/')
|
|
$apiToken = [string]$env:API_TOKEN
|
|
if (-not $apiUrl) { throw 'Set API_URL to your Homelab Manager URL, e.g. $env:API_URL = ''https://homelab.example.lan''' }
|
|
if (-not $apiToken) { throw 'Set API_TOKEN to the per-server token from the Servers page, e.g. $env:API_TOKEN = ''hlm_xxx''' }
|
|
$insecure = Test-Truthy $env:API_INSECURE
|
|
|
|
$interval = 15
|
|
if ($env:INTERVAL_MINUTES) {
|
|
if (-not ([int]::TryParse($env:INTERVAL_MINUTES, [ref]$interval)) -or $interval -lt 1 -or $interval -gt 1440) {
|
|
throw 'INTERVAL_MINUTES must be a whole number from 1 to 1440.'
|
|
}
|
|
}
|
|
|
|
Write-Output "Installing Homelab Manager agent from $apiUrl ..."
|
|
|
|
New-Item -ItemType Directory -Force -Path $script:InstallDir | Out-Null
|
|
$agentScript = Join-Path $script:InstallDir 'homelab-manager-agent.ps1'
|
|
Save-Url "$apiUrl/agent/windows/report-tasks.ps1" $agentScript $insecure
|
|
|
|
Save-AgentConfig (Join-Path $script:InstallDir 'agent.json') $apiUrl $apiToken $insecure
|
|
|
|
# Reinstalling replaces the task rather than failing on it.
|
|
if (Get-ScheduledTask -TaskName $script:TaskName -ErrorAction SilentlyContinue) {
|
|
Unregister-ScheduledTask -TaskName $script:TaskName -Confirm:$false
|
|
}
|
|
$parts = New-AgentTaskParts $agentScript $interval
|
|
$null = Register-ScheduledTask -TaskName $script:TaskName -Action $parts.Action -Trigger $parts.Triggers -Principal $parts.Principal -Settings $parts.Settings -Description 'Reports scheduled tasks and system information to Homelab Manager.'
|
|
|
|
Write-Output 'Installed. Running an initial report now...'
|
|
& powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $agentScript
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warning "The initial report failed (see above). The agent is installed and will keep trying every $interval minute(s) - check API_URL and API_TOKEN."
|
|
}
|
|
|
|
Write-Output "Done. The agent reports every $interval minute(s) through the '$script:TaskName' scheduled task."
|
|
Write-Output "To remove it later, run in an elevated PowerShell: iex ((New-Object Net.WebClient).DownloadString('$apiUrl/agent/windows/uninstall.ps1'))"
|
|
}
|
|
|
|
# Dot-sourcing (for tests) defines the functions without running anything.
|
|
if ($MyInvocation.InvocationName -ne '.') { Install-Agent }
|