Files
Veeam_jobs_disable/Monitor-VeeamJobs.ps1
2026-07-16 13:07:09 +07:00

789 lines
22 KiB
PowerShell

<#
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 '^(?<host>.+):(?<port>\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 '<unknown>'
}
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
}