<# .SYNOPSIS Read-only health report from the Veeam ONE REST API (v2.3, Veeam ONE 13.x). .DESCRIPTION Authenticates against the Veeam ONE REST API (OAuth 2.0 password grant on /api/token), then reads, with GET requests only: - /api/v2.3/about Veeam ONE server information - /api/v2.3/vbr/backupServers connected Veeam Backup & Replication servers - /api/v2.3/alarms/triggeredAlarms triggered alarms (Error/Warning kept by default) - /api/v2.3/protectedData/virtualMachines protected VMs and their latest restore point Collections are read page by page with the documented Offset and Limit query parameters. At the end the access token is revoked (/api/revoke). Nothing is changed in Veeam ONE. .PARAMETER Server DNS name or IP address of the machine running Veeam ONE Web Services. .PARAMETER Port REST API port chosen during setup. Veeam documents 1239 as the default. .PARAMETER Credential Account allowed to sign in to Veeam ONE. Prefer a dedicated, least-privileged account. .PARAMETER MfaCode One-time code, only if multi-factor authentication is enabled for the account (sent in the Veeam-Mfa-Code header documented for /api/token). .PARAMETER StaleHours A protected VM whose latest restore point is older than this is flagged as stale. Default 24. .PARAMETER IncludeAllAlarmStates Keep every triggered alarm (Resolved, Acknowledged, Information...). Default: Error and Warning only. .PARAMETER PageSize Items per request (Limit). Default 100, the documented default value. .PARAMETER OutputFolder Optional folder: one CSV per section is written there (semicolon separated, UTF-8). .PARAMETER SkipCertificateCheck PowerShell 7 only. Accepts a self-signed Veeam ONE certificate for this session. Prefer installing a trusted certificate on Veeam ONE Web Services instead. .EXAMPLE $cred = Get-Credential .\Get-VeeamOneHealthReport.ps1 -Server vone01.contoso.local -Credential $cred .EXAMPLE .\Get-VeeamOneHealthReport.ps1 -Server vone01.contoso.local -Credential $cred -StaleHours 36 -OutputFolder C:\Reports\VeeamONE .NOTES Read-only: GET requests on data endpoints; POST only on /api/token (sign-in) and /api/revoke (sign-out). Requires Windows PowerShell 5.1 or PowerShell 7. Endpoints and fields from the Veeam ONE 13 REST API reference (API version 2.3). Test against a non-production Veeam ONE server first. Source: ANYSYS ICT, https://www.anysys.be/articles/ #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Server, [ValidateRange(1, 65535)] [int]$Port = 1239, [Parameter(Mandatory)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, [string]$MfaCode, [ValidateRange(1, 8760)] [int]$StaleHours = 24, [switch]$IncludeAllAlarmStates, [ValidateRange(1, 1000)] [int]$PageSize = 100, [string]$OutputFolder, [switch]$SkipCertificateCheck ) $ErrorActionPreference = 'Stop' $baseUri = 'https://{0}:{1}' -f $Server, $Port $apiRoot = "$baseUri/api/v2.3" # Common parameters for every Invoke-RestMethod call $irmCommon = @{} if ($SkipCertificateCheck) { if ($PSVersionTable.PSVersion.Major -lt 7) { throw '-SkipCertificateCheck requires PowerShell 7. Install a trusted certificate on Veeam ONE Web Services instead.' } $irmCommon['SkipCertificateCheck'] = $true } if ($PSVersionTable.PSVersion.Major -lt 6) { # Windows PowerShell 5.1: make sure TLS 1.2 is allowed for this session only [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } function Get-VoneCollection { <# Reads every page of a collection endpoint using Offset/Limit and returns the items. #> param( [Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][hashtable]$Headers, [Parameter(Mandatory)][int]$Limit ) $offset = 0 $all = New-Object System.Collections.Generic.List[object] do { $uri = '{0}{1}?Offset={2}&Limit={3}' -f $apiRoot, $Path, $offset, $Limit $page = Invoke-RestMethod -Method Get -Uri $uri -Headers $Headers @irmCommon $items = @($page.items) foreach ($i in $items) { $all.Add($i) } $offset += $items.Count $total = [int]$page.totalCount } while ($items.Count -gt 0 -and $offset -lt $total) return $all.ToArray() } # 1. Sign in: OAuth 2.0 password grant (documented on /api/token) $tokenHeaders = @{} if ($MfaCode) { $tokenHeaders['Veeam-Mfa-Code'] = $MfaCode } $tokenBody = @{ grant_type = 'password' username = $Credential.UserName password = $Credential.GetNetworkCredential().Password } $token = Invoke-RestMethod -Method Post -Uri "$baseUri/api/token" -Headers $tokenHeaders ` -Body $tokenBody -ContentType 'application/x-www-form-urlencoded' @irmCommon $tokenBody = $null $headers = @{ Authorization = "Bearer $($token.access_token)" } Write-Verbose ("Signed in as {0} (role: {1})" -f $token.user, $token.user_role) try { # 2. Read-only data collection $about = Invoke-RestMethod -Method Get -Uri "$apiRoot/about" -Headers $headers @irmCommon $servers = Get-VoneCollection -Path '/vbr/backupServers' -Headers $headers -Limit $PageSize $alarms = Get-VoneCollection -Path '/alarms/triggeredAlarms' -Headers $headers -Limit $PageSize $vms = Get-VoneCollection -Path '/protectedData/virtualMachines' -Headers $headers -Limit $PageSize $backupServers = foreach ($s in $servers) { [pscustomobject]@{ Name = $s.name Version = $s.version Platform = $s.platform ConnectionState = $s.connectionState ConnectionError = $s.connectionError ConfigBackupEnabled = $s.isConfigurationBackupEnabled BestPracticeStatus = $s.bestPracticeCheckStatus LastBestPracticeCheck = $s.lastBestPracticeCheckDate IntelligentDiagnostics = $s.intelligentDiagnosticsEnabled } } $keptStates = @('Error', 'Warning') $activeAlarms = foreach ($a in $alarms) { if (-not $IncludeAllAlarmStates -and ($keptStates -notcontains [string]$a.status)) { continue } [pscustomobject]@{ Status = $a.status Name = $a.name Object = $a.alarmSource.objectName ObjectType = $a.alarmSource.objectType Triggered = $a.triggeredTime RepeatCount = $a.repeatCount Description = $a.description } } $activeAlarms = @($activeAlarms | Sort-Object Status, Triggered) $staleLimit = (Get-Date).ToUniversalTime().AddHours(-$StaleHours) $protectedVms = foreach ($v in $vms) { $last = $null if ($v.lastProtectedDate) { $last = ([datetime]$v.lastProtectedDate).ToUniversalTime() } [pscustomobject]@{ Name = $v.name Platform = $v.platform Host = $v.parentHostName BackupServer = $v.backupServerName Job = $v.jobName LastRestorePoint = $last Stale = ($null -eq $last) -or ($last -lt $staleLimit) } } $protectedVms = @($protectedVms) $staleVms = @($protectedVms | Where-Object Stale) # 3. Optional CSV export if ($OutputFolder) { $null = New-Item -ItemType Directory -Path $OutputFolder -Force $csv = @{ NoTypeInformation = $true; Delimiter = ';'; Encoding = 'UTF8' } $backupServers | Export-Csv -Path (Join-Path $OutputFolder 'vone-backup-servers.csv') @csv $activeAlarms | Export-Csv -Path (Join-Path $OutputFolder 'vone-alarms.csv') @csv $protectedVms | Export-Csv -Path (Join-Path $OutputFolder 'vone-protected-vms.csv') @csv } [pscustomobject]@{ VeeamOneServer = $about.machine VeeamOneVersion = $about.version CollectedAtUtc = (Get-Date).ToUniversalTime() BackupServers = $backupServers ServersNotOk = @($backupServers | Where-Object { $_.ConnectionState -ne 'Connected' }).Count Alarms = $activeAlarms AlarmErrors = @($activeAlarms | Where-Object { $_.Status -eq 'Error' }).Count AlarmWarnings = @($activeAlarms | Where-Object { $_.Status -eq 'Warning' }).Count ProtectedVms = $protectedVms StaleVms = $staleVms StaleThresholdH = $StaleHours } } finally { # 4. Sign out: revoke the access token (documented on /api/revoke) try { Invoke-RestMethod -Method Post -Uri "$baseUri/api/revoke" -Headers $headers ` -Body @{ token = $token.access_token } -ContentType 'application/x-www-form-urlencoded' @irmCommon | Out-Null } catch { Write-Warning "Token revocation failed: $($_.Exception.Message)" } }