diff --git a/Install-ZBXAgentManual.ps1 b/Install-ZBXAgentManual.ps1 new file mode 100644 index 0000000..798f7d7 --- /dev/null +++ b/Install-ZBXAgentManual.ps1 @@ -0,0 +1,274 @@ +<# +.SYNOPSIS + Downloads and silently installs Zabbix Agent 2 for Windows, configured for a + Zabbix Proxy/Server, and opens the firewall for passive checks. + +.DESCRIPTION + Downloads the Zabbix Agent 2 Windows MSI from cdn.zabbix.com and installs it silently via msiexec. + Hostname is set to the local computer name; both Server and ServerActive are set to the Zabbix Proxy/Server IP + address supplied by the caller (interactively, via -ZabbixProxyIPAddress, or via + $env:ZabbixProxyIPAddress). After a successful install it creates a Windows + Firewall rule allowing inbound TCP 10050 (Zabbix Agent passive checks) from that + same IP address only. Must be run elevated (Administrator). + +.PARAMETER ZabbixProxyIPAddress + IP address of the Zabbix Proxy or Zabbix Server. Used for both the agent's Server + and ServerActive settings, and as the only allowed remote address for the TCP + 10050 firewall rule. Omit this parameter to be prompted interactively (or to fall + back to $env:ZabbixProxyIPAddress if that is set). + +.PARAMETER ZabbixAgentMsiUri + URI of the Zabbix Agent 2 Windows MSI to download and install. Defaults to version + 7.2.13. Override this to install a different version/build, e.g. one of the other + files listed under https://cdn.zabbix.com/zabbix/binaries/stable///. + +.EXAMPLE + PS> .\Install-ZBXAgentManual.ps1 + Downloads and installs Zabbix Agent 2, prompting for the Zabbix Proxy/Server IP + address. + +.EXAMPLE + PS> .\Install-ZBXAgentManual.ps1 -ZabbixProxyIPAddress '192.168.10.50' + Downloads and installs Zabbix Agent 2 non-interactively. + +.EXAMPLE + PS> .\Install-ZBXAgentManual.ps1 -ZabbixProxyIPAddress '192.168.10.50' -ZabbixAgentMsiUri 'https://cdn.zabbix.com/zabbix/binaries/stable/7.0/7.0.14/zabbix_agent2-7.0.14-windows-amd64-openssl.msi' + Installs a different Zabbix Agent 2 version instead of the default 7.2.13. + +.EXAMPLE + PS> irm https://s.tslab.cz/ZBXAgent | iex + Shortest way to run this directly from the repo without saving it locally first; + prompts interactively for the Zabbix Proxy/Server IP address and installs the + default MSI version. + +.NOTES + Author: Petr Stepan + Created: 2026-07-21 + Version: 1.1.0 + Changelog: + 1.0.0 - Initial version + 1.1.0 - Made ZabbixAgentMsiUri a parameter (was hardcoded) so a different + agent version can be installed without editing the script +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false, HelpMessage = 'IP address of the Zabbix Proxy or Server. Used for Server, ServerActive and the TCP 10050 firewall rule.')] + [string] + $ZabbixProxyIPAddress, + + [Parameter(Mandatory = $false, HelpMessage = 'URI of the Zabbix Agent 2 Windows MSI to download and install.')] + [ValidateNotNullOrEmpty()] + [string] + $ZabbixAgentMsiUri = 'https://cdn.zabbix.com/zabbix/binaries/stable/7.2/7.2.13/zabbix_agent2-7.2.13-windows-amd64-openssl.msi' +) + +# --- Safety preamble ------------------------------------------------------- +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Older Windows Server releases don't enable TLS 1.2 by default, which would make the +# download below fail silently against a TLS-1.2-only server. +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +$MsiPath = Join-Path -Path $env:TEMP -ChildPath (Split-Path -Path $ZabbixAgentMsiUri -Leaf) +$MsiLogPath = Join-Path -Path $env:TEMP -ChildPath 'zabbix_agent2_install.log' +$FirewallRuleName = 'Zabbix Agent (TCP-In)' +$FirewallPort = 10050 + +# --- Functions --------------------------------------------------------- + +function Test-CurrentUserIsAdministrator { + <# + .SYNOPSIS + Returns whether the current process is running elevated (Administrator). + #> + [CmdletBinding()] + param() + + $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object -TypeName System.Security.Principal.WindowsPrincipal -ArgumentList $identity + return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Invoke-RemoteFileDownload { + <# + .SYNOPSIS + Downloads a single file from a URI to a local path. + .PARAMETER Uri + Source URI to download from. + .PARAMETER OutFile + Local destination file path. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Uri, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $OutFile + ) + + try { + Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing + Write-Host "Downloaded '$Uri' to '$OutFile'." + } + catch { + Write-Error "Failed to download '$Uri' to '$OutFile': $_" + throw + } +} + +function Install-ZabbixAgentMsi { + <# + .SYNOPSIS + Silently installs the Zabbix Agent 2 MSI with the given Hostname/Server settings. + .PARAMETER MsiPath + Path to the downloaded zabbix_agent2 MSI file. + .PARAMETER HostName + Value for the agent's Hostname setting. + .PARAMETER ServerAddress + Value used for both the Server and ServerActive settings. + .PARAMETER LogPath + Path for the msiexec verbose install log. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -Path $_ -PathType Leaf })] + [string] + $MsiPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $HostName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $ServerAddress, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $LogPath + ) + + # Passed as an array, not one concatenated string, so Start-Process quotes each + # argument correctly even if $MsiPath/$LogPath contain spaces. + $msiArgs = @( + '/i', $MsiPath + '/qn' + "HOSTNAME=$HostName" + "SERVER=$ServerAddress" + "SERVERACTIVE=$ServerAddress" + '/l*v', $LogPath + ) + + Write-Host "Installing Zabbix Agent 2 (Hostname=$HostName, Server/ServerActive=$ServerAddress)..." + $process = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow + + # 3010 = success, reboot required - still a successful install. + if ($process.ExitCode -notin @(0, 3010)) { + throw "msiexec failed with exit code $($process.ExitCode). See log: $LogPath" + } + + Write-Host "Zabbix Agent 2 installed successfully (msiexec exit code $($process.ExitCode))." +} + +function New-ZabbixAgentFirewallRule { + <# + .SYNOPSIS + Creates an inbound firewall rule for the Zabbix Agent listener, restricted to + a single remote address. Does nothing if a rule with the same name already + exists. + .PARAMETER Name + Display name of the firewall rule. + .PARAMETER Port + TCP port to allow. + .PARAMETER RemoteAddress + Only remote address allowed to reach that port. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [int] + $Port, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $RemoteAddress + ) + + if (Get-NetFirewallRule -DisplayName $Name -ErrorAction SilentlyContinue) { + Write-Host "Firewall rule '$Name' already exists - skipping." + return + } + + New-NetFirewallRule -DisplayName $Name -Direction Inbound -Action Allow -Protocol TCP ` + -LocalPort $Port -RemoteAddress $RemoteAddress -Group 'Zabbix Agent' ` + -Description "Allow the Zabbix Proxy/Server ($RemoteAddress) to reach the Zabbix Agent passive check listener." | Out-Null + + Write-Host "Firewall rule '$Name' created (TCP $Port, remote address $RemoteAddress)." +} + +# --- Main -------------------------------------------------------------- +try { + if (-not (Test-CurrentUserIsAdministrator)) { + throw 'This script must be run elevated (as Administrator).' + } + + # Only fall back to the environment variable / prompt when the caller did not pass + # the parameter at all. $env:ZabbixProxyIPAddress lets a plain "irm | iex" + # one-liner supply a value without needing "& { ... } -Param" - piping into iex + # runs the downloaded param() block as a plain statement with no bound arguments, + # so -ZabbixProxyIPAddress itself can only be set that way when the script is + # invoked as a real command (locally or via "& { ... }"). + if (-not $PSBoundParameters.ContainsKey('ZabbixProxyIPAddress')) { + if ($env:ZabbixProxyIPAddress) { + $ZabbixProxyIPAddress = $env:ZabbixProxyIPAddress + } + else { + $ZabbixProxyIPAddress = Read-Host -Prompt 'Enter Zabbix Proxy/Server IP address' + } + } + + $parsedIp = $null + if (-not [System.Net.IPAddress]::TryParse($ZabbixProxyIPAddress, [ref] $parsedIp)) { + throw "'$ZabbixProxyIPAddress' is not a valid IP address." + } + + Invoke-RemoteFileDownload -Uri $ZabbixAgentMsiUri -OutFile $MsiPath + + Install-ZabbixAgentMsi -MsiPath $MsiPath -HostName $env:COMPUTERNAME -ServerAddress $ZabbixProxyIPAddress -LogPath $MsiLogPath + + New-ZabbixAgentFirewallRule -Name $FirewallRuleName -Port $FirewallPort -RemoteAddress $ZabbixProxyIPAddress + + Remove-Item -Path $MsiPath -Force -ErrorAction SilentlyContinue +} +catch { + Write-Error "Script failed: $_" + # $PSCommandPath is only set when this runs as an actual .ps1 file. When run via + # "irm | iex" there is no backing file - the code executes in the caller's + # own session, so calling exit here would close their whole PowerShell window + # instead of just ending this script. + if ($PSCommandPath) { + exit 1 + } + return +} + +if ($PSCommandPath) { + exit 0 +}