<# Monitors Veeam Backup & Replication jobs for Zabbix. Default mode prints one JSON object for a Zabbix Agent 2 UserParameter: UserParameter=veeam.scripts.monitor,powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Program Files\Zabbix Agent 2\scripts\Monitor-VeeamJobs.ps1" Sender mode pushes the same metrics with zabbix_sender, useful for Task Scheduler: powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Program Files\Zabbix Agent 2\scripts\Monitor-VeeamJobs.ps1" -Mode Sender #> [CmdletBinding()] param( [ValidateSet('Json', 'Sender')] [string]$Mode = 'Json', [string]$ScriptDir = $PSScriptRoot, [string]$JobExclusionFilterFile, [string]$ZabbixSender = 'C:\Program Files\Zabbix Agent 2\zabbix_sender.exe', [string]$ZabbixConfig = 'C:\Program Files\Zabbix Agent 2\zabbix_agent2.conf', [string]$ZabbixServer, [string]$ZabbixHost ) $ErrorActionPreference = 'Stop' if ([string]::IsNullOrWhiteSpace($ScriptDir)) { $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path } if ([string]::IsNullOrWhiteSpace($JobExclusionFilterFile)) { $JobExclusionFilterFile = Join-Path $ScriptDir 'NotifyOfDisabledJobs_EXCLUSIONS.IN' } function Import-VeeamPowerShell { Add-PSSnapin -Name VeeamPSSnapIn -ErrorAction SilentlyContinue if (-not (Get-Command Get-VBRJob -ErrorAction SilentlyContinue) -and -not (Get-Command Get-VBRComputerBackupJob -ErrorAction SilentlyContinue)) { Import-Module Veeam.Backup.PowerShell -ErrorAction SilentlyContinue } if (-not (Get-Command Get-VBRJob -ErrorAction SilentlyContinue) -and -not (Get-Command Get-VBRComputerBackupJob -ErrorAction SilentlyContinue)) { throw 'Veeam PowerShell commands are not available. Run the script on the Veeam server or install/load the Veeam PowerShell module.' } } function Get-ZabbixAgentConfigValue { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Name ) if (-not (Test-Path -Path $Path)) { return $null } $line = Get-Content -Path $Path | Where-Object { $_ -match "^\s*$([regex]::Escape($Name))\s*=" -and $_ -notmatch '^\s*#' } | Select-Object -Last 1 if (-not $line) { return $null } return (($line -split '=', 2)[1]).Trim() } function Get-ZabbixServerEndpoints { param( [Parameter(Mandatory = $true)] [string]$Servers ) $endpoints = @() foreach ($serverEndpoint in ($Servers -split ',')) { $serverEndpoint = $serverEndpoint.Trim() if ([string]::IsNullOrWhiteSpace($serverEndpoint)) { continue } $serverHost = $serverEndpoint $serverPort = $null if ($serverEndpoint -match '^(?.+):(?\d+)$') { $serverHost = $Matches.host $serverPort = $Matches.port } $endpoints += [pscustomobject]@{ Host = $serverHost Port = $serverPort } } return $endpoints } function Get-ObjectValue { param( [Parameter(Mandatory = $true)] [object]$InputObject, [Parameter(Mandatory = $true)] [string[]]$Paths ) foreach ($path in $Paths) { $current = $InputObject foreach ($part in ($path -split '\.')) { if ($null -eq $current) { break } $property = $current.PSObject.Properties[$part] if ($null -eq $property) { $current = $null break } $current = $property.Value } if ($null -ne $current) { return $current } } return $null } function Invoke-ObjectMethodValue { param( [Parameter(Mandatory = $true)] [object]$InputObject, [Parameter(Mandatory = $true)] [string[]]$Names ) foreach ($name in $Names) { if ($null -eq $InputObject) { return $null } $method = $InputObject.PSObject.Methods[$name] if ($null -eq $method) { continue } try { $value = $method.Invoke() if ($null -ne $value) { return $value } } catch { continue } } return $null } function Get-VeeamJobName { param( [Parameter(Mandatory = $true)] [object]$Job ) $name = Get-ObjectValue -InputObject $Job -Paths @('Name', 'Info.Name') if ([string]::IsNullOrWhiteSpace($name)) { return '' } return $name } function ConvertTo-ZabbixDateTime { param( [object]$Value ) if ($null -eq $Value) { return $null } if ($Value -is [datetime]) { if ($Value -eq [datetime]::MinValue -or $Value -eq [datetime]::MaxValue) { return $null } return $Value.ToString('yyyy-MM-dd HH:mm:ss') } $text = [string]$Value if ([string]::IsNullOrWhiteSpace($text)) { return $null } return $text } function ConvertTo-ZabbixJobKey { param( [Parameter(Mandatory = $true)] [string]$Name ) $bytes = [System.Text.Encoding]::UTF8.GetBytes($Name) return [Convert]::ToBase64String($bytes).TrimEnd('=') -replace '\+', '-' -replace '/', '_' } function ConvertTo-ZabbixText { param( [object]$Value ) if ($null -eq $Value) { return '-' } $text = [string]$Value if ([string]::IsNullOrWhiteSpace($text)) { return '-' } return $text } function Format-VeeamJobSummary { param( [Parameter(Mandatory = $true)] [string]$Enabled, [Parameter(Mandatory = $true)] [string]$LastRun, [Parameter(Mandatory = $true)] [string]$NextRun, [Parameter(Mandatory = $true)] [string]$Result, [Parameter(Mandatory = $true)] [string]$RestorePoints ) return 'Enabled: {0} | Last: {1} | Next: {2} | Result: {3} | Restore points: {4}' -f $Enabled, $LastRun, $NextRun, $Result, $RestorePoints } function Get-VeeamJobScheduleEnabled { param( [Parameter(Mandatory = $true)] [object]$Job ) return Get-ObjectValue -InputObject $Job -Paths @( 'Info.IsScheduleEnabled', 'IsScheduleEnabled', 'ScheduleEnabled', 'ScheduleOptions.Enabled', 'JobScheduleOptions.Enabled' ) } function Get-VeeamJobLastSession { param( [Parameter(Mandatory = $true)] [object]$Job ) return Invoke-ObjectMethodValue -InputObject $Job -Names @( 'FindLastSession', 'GetLastSession', 'FindLastBackupSession' ) } function Get-VeeamJobLastResult { param( [Parameter(Mandatory = $true)] [object]$Job, [object]$LastSession ) $value = Get-ObjectValue -InputObject $Job -Paths @( 'Info.LastResult', 'LastResult', 'Info.LatestRunResult', 'LatestRunResult' ) if ($null -ne $value) { return $value } $value = Invoke-ObjectMethodValue -InputObject $Job -Names @( 'GetLastResult', 'FindLastResult' ) if ($null -ne $value) { return $value } if ($null -eq $LastSession) { return $null } return Get-ObjectValue -InputObject $LastSession -Paths @( 'Result', 'Info.Result', 'JobResult', 'Status' ) } function Get-VeeamJobLastState { param( [Parameter(Mandatory = $true)] [object]$Job, [object]$LastSession ) $value = Get-ObjectValue -InputObject $Job -Paths @( 'Info.LastState', 'LastState', 'Info.LatestRunState', 'LatestRunState' ) if ($null -ne $value) { return $value } if ($null -eq $LastSession) { return $null } return Get-ObjectValue -InputObject $LastSession -Paths @( 'State', 'Info.State', 'Status' ) } function Get-VeeamJobLastRun { param( [Parameter(Mandatory = $true)] [object]$Job, [object]$LastSession ) $value = Get-ObjectValue -InputObject $Job -Paths @( 'Info.LatestRunLocal', 'LatestRunLocal', 'Info.LastRunLocal', 'LastRunLocal', 'Info.LastRun', 'LastRun', 'Info.LastStartTime', 'LastStartTime', 'Info.LastEndTime', 'LastEndTime' ) if ($null -eq $value -and $null -ne $LastSession) { $value = Get-ObjectValue -InputObject $LastSession -Paths @( 'CreationTime', 'CreationTimeLocal', 'EndTime', 'EndTimeLocal', 'Info.CreationTime', 'Info.EndTime' ) } return ConvertTo-ZabbixDateTime -Value $value } function Get-VeeamJobNextRun { param( [Parameter(Mandatory = $true)] [object]$Job ) $value = Get-ObjectValue -InputObject $Job -Paths @( 'Info.NextRun', 'NextRun', 'ScheduleOptions.NextRun', 'JobScheduleOptions.NextRun', 'ScheduleOptions.NextRunLocal', 'JobScheduleOptions.NextRunLocal' ) return ConvertTo-ZabbixDateTime -Value $value } function Get-VeeamBackupJobName { param( [Parameter(Mandatory = $true)] [object]$Backup ) return Get-ObjectValue -InputObject $Backup -Paths @( 'JobName', 'Info.JobName', 'Job.Name', 'Name' ) } function Get-VeeamRestorePointCounts { $counts = @{} if (-not (Get-Command Get-VBRBackup -ErrorAction SilentlyContinue) -or -not (Get-Command Get-VBRRestorePoint -ErrorAction SilentlyContinue)) { return $counts } foreach ($backup in @(Get-VBRBackup -WarningAction SilentlyContinue)) { $jobName = Get-VeeamBackupJobName -Backup $backup if ([string]::IsNullOrWhiteSpace($jobName)) { continue } try { $restorePointCount = @(Get-VBRRestorePoint -Backup $backup -WarningAction SilentlyContinue).Count if (-not $counts.ContainsKey($jobName)) { $counts[$jobName] = 0 } $counts[$jobName] += $restorePointCount } catch { if (-not $counts.ContainsKey($jobName)) { $counts[$jobName] = $null } } } return $counts } function Format-ZabbixTextValue { param( [object[]]$Values ) $text = @($Values | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique) -join '; ' if ([string]::IsNullOrWhiteSpace($text)) { return '-' } return $text } function Format-VeeamJobDetailsTable { param( [Parameter(Mandatory = $true)] [object[]]$JobDetails ) if ($JobDetails.Count -eq 0) { return '-' } $lines = @( 'Name | Enabled | Last run | Next run | Result | Restore points' '--- | --- | --- | --- | --- | ---' ) foreach ($job in ($JobDetails | Sort-Object -Property name)) { $enabled = if ($job.enabled -eq $true) { 'yes' } elseif ($job.enabled -eq $false) { 'no' } else { 'unknown' } $lastRun = if ([string]::IsNullOrWhiteSpace($job.last_run)) { '-' } else { $job.last_run } $nextRun = if ([string]::IsNullOrWhiteSpace($job.next_run)) { '-' } else { $job.next_run } $result = if ([string]::IsNullOrWhiteSpace($job.last_result)) { $job.last_state } else { $job.last_result } $restorePoints = if ($null -eq $job.restore_points) { '-' } else { $job.restore_points } $lines += ('{0} | {1} | {2} | {3} | {4} | {5}' -f $job.name, $enabled, $lastRun, $nextRun, $result, $restorePoints) } return ($lines -join [Environment]::NewLine) } function Format-VeeamJobsSummary { param( [Parameter(Mandatory = $true)] [object[]]$JobDetails ) if ($JobDetails.Count -eq 0) { return 'No Veeam jobs found' } $lines = @('Job | Enabled | Last run | Next run | Result | Restore points') foreach ($job in ($JobDetails | Sort-Object -Property name)) { $lines += ('{0} | {1} | {2} | {3} | {4} | {5}' -f $job.name, $job.enabled_text, $job.last_run, $job.next_run, $job.result, $job.restore_points_text) } return ($lines -join [Environment]::NewLine) } function Format-ZabbixSenderValue { param( [object]$Value ) if ($null -eq $Value) { return '-' } $text = [string]$Value $text = $text -replace "(`r`n|`n|`r)", ' / ' if ([string]::IsNullOrWhiteSpace($text)) { return '-' } return $text } function Get-VeeamJobs { Import-VeeamPowerShell $jobs = @() if (Get-Command Get-VBRJob -ErrorAction SilentlyContinue) { $jobs += @(Get-VBRJob -WarningAction SilentlyContinue) } if (Get-Command Get-VBRComputerBackupJob -ErrorAction SilentlyContinue) { $jobs += @(Get-VBRComputerBackupJob -WarningAction SilentlyContinue) } return $jobs } function Get-VeeamJobMetrics { $allJobs = @(Get-VeeamJobs) $restorePointCounts = Get-VeeamRestorePointCounts $jobDetails = @($allJobs | ForEach-Object { $jobName = Get-VeeamJobName -Job $_ $scheduleEnabled = Get-VeeamJobScheduleEnabled -Job $_ $lastSession = Get-VeeamJobLastSession -Job $_ $lastResult = Get-VeeamJobLastResult -Job $_ -LastSession $lastSession $lastState = Get-VeeamJobLastState -Job $_ -LastSession $lastSession $restorePoints = $null $enabledNumeric = if ($scheduleEnabled -eq $false) { 0 } else { 1 } $enabledText = if ($enabledNumeric -eq 1) { 'yes' } else { 'no' } $lastRunText = ConvertTo-ZabbixText -Value (Get-VeeamJobLastRun -Job $_ -LastSession $lastSession) $nextRunText = ConvertTo-ZabbixText -Value (Get-VeeamJobNextRun -Job $_) $lastResultText = ConvertTo-ZabbixText -Value $lastResult $lastStateText = ConvertTo-ZabbixText -Value $lastState $resultText = ConvertTo-ZabbixText -Value $(if ($null -ne $lastResult) { $lastResult } else { $lastState }) if ($restorePointCounts.ContainsKey($jobName)) { $restorePoints = $restorePointCounts[$jobName] } $restorePointsText = ConvertTo-ZabbixText -Value $restorePoints [ordered]@{ key = ConvertTo-ZabbixJobKey -Name $jobName name = $jobName enabled = $scheduleEnabled enabled_numeric = $enabledNumeric enabled_text = $enabledText last_run = $lastRunText next_run = $nextRunText last_result = $lastResultText last_state = $lastStateText result = $resultText restore_points = $restorePoints restore_points_text = $restorePointsText summary = Format-VeeamJobSummary -Enabled $enabledText -LastRun $lastRunText -NextRun $nextRunText -Result $resultText -RestorePoints $restorePointsText } }) $disabledJobs = @($allJobs | Where-Object { $scheduleEnabled = Get-VeeamJobScheduleEnabled -Job $_ $scheduleEnabled -eq $false }) $errorJobs = @($allJobs | Where-Object { $lastSession = Get-VeeamJobLastSession -Job $_ $lastState = Get-VeeamJobLastState -Job $_ -LastSession $lastSession $lastResult = Get-VeeamJobLastResult -Job $_ -LastSession $lastSession $lastState -eq 'Failed' -or $lastResult -eq 'Failed' }) $warningJobs = @($allJobs | Where-Object { $lastSession = Get-VeeamJobLastSession -Job $_ $lastState = Get-VeeamJobLastState -Job $_ -LastSession $lastSession $lastResult = Get-VeeamJobLastResult -Job $_ -LastSession $lastSession $lastState -eq 'Warning' -or $lastResult -eq 'Warning' }) $successfulJobs = @($jobDetails | Where-Object { $_.last_result -in @('Success', 'Succeeded') -or $_.last_state -in @('Success', 'Succeeded') }) $jobsLld = @($jobDetails | Sort-Object -Property name | ForEach-Object { [ordered]@{ '{#VEEAMJOBKEY}' = $_.key '{#VEEAMJOBNAME}' = $_.name } }) $disabledCount = $disabledJobs.Count $disabledExcludedJobs = @() if (Test-Path -Path $JobExclusionFilterFile) { $jobsToExclude = @(Get-Content -Path $JobExclusionFilterFile | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) if ($jobsToExclude.Count -gt 0) { $disabledExcludedJobs = @($disabledJobs | Where-Object { $jobName = Get-VeeamJobName -Job $_ $jobsToExclude | Where-Object { $jobName -like "*$_*" } }) $disabledJobsFiltered = @($disabledJobs | Where-Object { $jobName = Get-VeeamJobName -Job $_ -not ($jobsToExclude | Where-Object { $jobName -like "*$_*" }) }) $disabledJobs = $disabledJobsFiltered $disabledCount = $disabledJobsFiltered.Count } } [ordered]@{ total = $allJobs.Count disabled = $disabledCount successful = $successfulJobs.Count error = $errorJobs.Count warning = $warningJobs.Count disabled_names = Format-ZabbixTextValue -Values @($disabledJobs | ForEach-Object { Get-VeeamJobName -Job $_ }) disabled_excluded_names = Format-ZabbixTextValue -Values @($disabledExcludedJobs | ForEach-Object { Get-VeeamJobName -Job $_ }) error_names = Format-ZabbixTextValue -Values @($errorJobs | ForEach-Object { Get-VeeamJobName -Job $_ }) warning_names = Format-ZabbixTextValue -Values @($warningJobs | ForEach-Object { Get-VeeamJobName -Job $_ }) jobs_table = Format-VeeamJobDetailsTable -JobDetails $jobDetails jobs_summary = Format-VeeamJobsSummary -JobDetails $jobDetails jobs_lld = [ordered]@{ data = $jobsLld } jobs = $jobDetails } } function Send-ZabbixMetrics { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Metrics ) if (-not (Test-Path -Path $ZabbixSender)) { throw "zabbix_sender.exe was not found: $ZabbixSender" } if ([string]::IsNullOrWhiteSpace($ZabbixServer)) { $ZabbixServer = Get-ZabbixAgentConfigValue -Path $ZabbixConfig -Name 'ServerActive' } if ([string]::IsNullOrWhiteSpace($ZabbixServer)) { $ZabbixServer = Get-ZabbixAgentConfigValue -Path $ZabbixConfig -Name 'Server' } if ([string]::IsNullOrWhiteSpace($ZabbixHost)) { $ZabbixHost = Get-ZabbixAgentConfigValue -Path $ZabbixConfig -Name 'Hostname' } if ([string]::IsNullOrWhiteSpace($ZabbixHost)) { $hostnameItem = Get-ZabbixAgentConfigValue -Path $ZabbixConfig -Name 'HostnameItem' if ([string]::IsNullOrWhiteSpace($hostnameItem) -or $hostnameItem -eq 'system.hostname') { $ZabbixHost = $env:COMPUTERNAME } } if ([string]::IsNullOrWhiteSpace($ZabbixServer)) { throw "Zabbix server was not provided and was not found in $ZabbixConfig" } if ([string]::IsNullOrWhiteSpace($ZabbixHost)) { throw "Zabbix hostname was not provided, was not found in $ZabbixConfig, and COMPUTERNAME is empty. Pass -ZabbixHost with the exact Zabbix host name." } $serverEndpoints = @(Get-ZabbixServerEndpoints -Servers $ZabbixServer) if ($serverEndpoints.Count -eq 0) { throw "No usable Zabbix server endpoints were found in: $ZabbixServer" } $senderInput = New-TemporaryFile try { Write-Host "Sending as host: $ZabbixHost" @( "`"$ZabbixHost`" veeam.jobs.total $($Metrics.total)" "`"$ZabbixHost`" veeam.jobs.disabled $($Metrics.disabled)" "`"$ZabbixHost`" veeam.jobs.successful $($Metrics.successful)" "`"$ZabbixHost`" veeam.jobs.error $($Metrics.error)" "`"$ZabbixHost`" veeam.jobs.warning $($Metrics.warning)" "`"$ZabbixHost`" veeam.jobs.disabled.names $(Format-ZabbixSenderValue -Value $Metrics.disabled_names)" "`"$ZabbixHost`" veeam.jobs.disabled.excluded.names $(Format-ZabbixSenderValue -Value $Metrics.disabled_excluded_names)" "`"$ZabbixHost`" veeam.jobs.error.names $(Format-ZabbixSenderValue -Value $Metrics.error_names)" "`"$ZabbixHost`" veeam.jobs.warning.names $(Format-ZabbixSenderValue -Value $Metrics.warning_names)" "`"$ZabbixHost`" veeam.jobs.table $(Format-ZabbixSenderValue -Value $Metrics.jobs_table)" "`"$ZabbixHost`" veeam.jobs.summary $(Format-ZabbixSenderValue -Value $Metrics.jobs_summary)" ) | Set-Content -Path $senderInput -Encoding ASCII $failedEndpoints = @() foreach ($serverEndpoint in $serverEndpoints) { $senderArgs = @('-z', $serverEndpoint.Host, '-i', $senderInput) if (-not [string]::IsNullOrWhiteSpace($serverEndpoint.Port)) { $senderArgs += @('-p', $serverEndpoint.Port) } $displayEndpoint = $serverEndpoint.Host if (-not [string]::IsNullOrWhiteSpace($serverEndpoint.Port)) { $displayEndpoint = "$displayEndpoint`:$($serverEndpoint.Port)" } Write-Host "Sending to Zabbix endpoint: $displayEndpoint" & $ZabbixSender @senderArgs if ($LASTEXITCODE -eq 0) { return } $failedEndpoints += "$displayEndpoint (exit code $LASTEXITCODE)" } throw "zabbix_sender failed for all configured endpoints: $($failedEndpoints -join '; ')" } finally { Remove-Item -Path $senderInput -Force -ErrorAction SilentlyContinue } } try { $metrics = Get-VeeamJobMetrics if ($Mode -eq 'Sender') { Send-ZabbixMetrics -Metrics $metrics } else { $metrics | ConvertTo-Json -Depth 5 -Compress } } catch { Write-Error $_.Exception.Message exit 1 }