Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save BasvanH/2229ca0d5d7b17df72a412db36d7bc5c to your computer and use it in GitHub Desktop.

Select an option

Save BasvanH/2229ca0d5d7b17df72a412db36d7bc5c to your computer and use it in GitHub Desktop.
PRTG Advanced sensor for Veeam Backup for Microsoft 365
<#
.SYNOPSIS
PRTG Veeam Backup for Microsoft 365 Advanced Sensor.
.DESCRIPTION
Advanced Sensor will Report Job status, job nested status, repository statistics and proxy status.
- If not already done, enable the the API in VBO https://helpcenter.veeam.com/docs/vbo365/rest/enable_restful_api.html?ver=20
- On your probe, add script to 'Custom Sensors\EXEXML' folder
- In PRTG, on your probe add EXE/Script Advanced sensor
- Name the sensor eg: Veeam Backup for Office 365
- In the EXE/Script dropdown, select the script
- In parameters set: -username "%windowsdomain\%windowsuser" -password "%windowspassword" -apiUrl "https://<url-to-vbo-api>:443"
- This way the Windows user defined on the probe is used for authenticating to VBO API, make sure the correct permissions are set in VBO for this user
- Set preferred timeout and interval
- I've set some default limits on the channels, change them to your preferred levels
.NOTES
For issues, suggetions and forking please use Github.
.LINK
https://github.com/BasvanH
https://gist.github.com/BasvanH
#>
param (
[string]$apiUrl = $(throw "<prtg><error>1</error><text>-apiUrl is missing in parameters</text></prtg>"),
[string]$username = $(throw "<prtg><error>1</error><text>-username is missing in parameters</text></prtg>"),
[string]$password = $(throw "<prtg><error>1</error><text>-password is missing in parameters</text></prtg>")
)
$vboJobs = @()
$vboRepositories = @()
$vboProxies = @()
#region: Authenticate
$url = '/v6/Token'
$body = @{
"username" = $username;
"password" = $password;
"grant_type" = "password";
}
$headers = @{
"Content-Type"= "multipart/form-data"
}
Try {
$jsonResult = Invoke-WebRequest -Uri $apiUrl$url -Body $body -Headers $headers -Method Post -UseBasicParsing
} Catch {
Write-Error "Error invoking web request"
}
Try {
$authResult = ConvertFrom-Json($jsonResult.Content)
$accessToken = $authResult.access_token
} Catch {
Write-Error "Error authentication result"
}
#endregion
#region: Get VBO Jobs
$url = '/v6/Jobs?limit=1000000'
$headers = @{
"Content-Type"= "multipart/form-data";
"Authorization" = "Bearer $accessToken";
}
$jsonResult = Invoke-WebRequest -Uri $apiUrl$url -Headers $headers -Method Get -UseBasicParsing
Try {
$jobs = ConvertFrom-Json($jsonResult.Content)
} Catch {
Write-Error "Error in jobs result"
Exit 1
}
#endregion
#region: Loop jobs and process session results
ForEach ($job in $jobs) {
# Sessions
$url = '/v6/Jobs/' + $job.id + '/JobSessions'
$headers = @{
"Content-Type"= "multipart/form-data";
"Authorization" = "Bearer $accessToken";
}
$jsonResult = Invoke-WebRequest -Uri $apiUrl$url -Headers $headers -Method Get -UseBasicParsing
Try {
$sessions = (ConvertFrom-Json($jsonResult.Content)).results
} Catch {
Write-Error "Error in jobsession result"
Exit 1
}
# Skip session currently active or user aborted, get last known run status
if ($sessions[0].status.ToLower() -in @('running', 'queued', 'stopped')) {
$session = $sessions[1]
} else {
$session = $sessions[0]
}
# Log items
$url = '/v6/JobSessions/' + $session.id + '/LogItems?limit=1000000'
$headers = @{
"Content-Type"= "multipart/form-data";
"Authorization" = "Bearer $accessToken";
}
$jsonResult = Invoke-WebRequest -Uri $apiUrl$url -Headers $headers -Method Get -UseBasicParsing
Try {
$logItems = (ConvertFrom-Json($jsonResult.Content)).results
} Catch {
Write-Error "Error in logitems result"
Exit 1
}
# Log items to object
ForEach ($logItem in $logItems) {
$sCnt = 0;$wCnt = 0;$fCnt = 0
Switch -wildcard ($logItem.title.ToLower()) {
'*success*' {$sCnt++}
'*warning*' {$wCnt++}
'*failed*' {$fCnt++}
}
}
Switch -wildcard ($session.status.ToLower()) {
'*success*' {$jobStatus = 0}
'*warning*' {$jobStatus = 1}
'*failed*' {$jobStatus = 2}
default {$jobStatus = 3}
}
# Thank you Veeam for fixing this!
$transferred = $session.statistics.transferredDataBytes
$myObj = "" | Select Jobname, Status, Start, End, Transferred, Success, Warning, Failed
$myObj.Jobname = $job.name
$myObj.Status = $jobStatus
$myObj.Start = Get-Date($session.creationTime)
$myObj.End = Get-Date($session.endTime)
$myObj.Transferred = $transferred
$myObj.Success = $sCnt
$myObj.Warning = $wCnt
$myObj.Failed = $fCnt
$vboJobs += $myObj
}
#region: VBO Repositories
$url = '/v6/BackupRepositories'
$headers = @{
"Content-Type"= "multipart/form-data";
"Authorization" = "Bearer $accessToken";
}
$jsonResult = Invoke-WebRequest -Uri $apiUrl$url -Headers $headers -Method Get -UseBasicParsing
Try {
$repositories = ConvertFrom-Json($jsonResult.Content)
} Catch {
Write-Error "Error in repositories result"
}
ForEach ($repository in $repositories) {
$myObj = "" | Select Name, Capacity, Free
$myObj.Name = $repository.name
$myObj.Capacity = $repository.capacityBytes
$myObj.Free = $repository.freeSpaceBytes
$vboRepositories += $myObj
}
#endregion
#region: VBO Proxies
$url = '/v6/Proxies'
$headers = @{
"Content-Type"= "multipart/form-data";
"Authorization" = "Bearer $accessToken";
}
$jsonResult = Invoke-WebRequest -Uri $apiUrl$url -Headers $headers -Method Get -UseBasicParsing
Try {
$proxies = ConvertFrom-Json($jsonResult.Content)
} Catch {
Write-Error "Error in proxies result"
Exit 1
}
ForEach ($proxy in $proxies) {
$myObj = "" | Select Name, Status
$myObj.Name = $proxy.hostName
$myObj.Status = $proxy.status
$vboProxies += $myObj
}
#endregion
#region: Jobs to PRTG results
Write-Host "<prtg>"
ForEach ($job in $vboJobs) {
$channel = "Job - " + $job.Jobname + " - Status"
$value = $job.Status
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>One</unit>"
"<showChart>0</showChart>"
"<showTable>1</showTable>"
"<LimitMaxWarning>1</LimitMaxWarning>"
"<LimitMaxError>2</LimitMaxError>"
"<LimitMode>1</LimitMode>"
"</result>"
$channel = "Job - " + $job.Jobname + " - Runtime"
$value = [math]::Round(($job.end - $job.start).TotalSeconds)
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>TimeSeconds</unit>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"</result>"
$channel = "Job - " + $job.Jobname + " - Transferred"
$value = [long]$job.Transferred
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>BytesDisk</unit>"
"<VolumeSize>Byte</VolumeSize>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"<LimitMinWarning>20971520</LimitMinWarning>"
"<LimitMinError>10485760</LimitMinError>"
"<LimitMode>1</LimitMode>"
"</result>"
$channel = "Job - " + $job.Jobname + " - Success"
$value = $job.Success
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>Count</unit>"
"<VolumeSize>One</VolumeSize>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"</result>"
$channel = "Job - " + $job.Jobname + " - Warning"
$value = $job.Warning
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>Count</unit>"
"<VolumeSize>One</VolumeSize>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"<LimitMaxWarning>10</LimitMaxWarning>"
"<LimitMaxError>20</LimitMaxError>"
"<LimitMode>1</LimitMode>"
"</result>"
$channel = "Job - " + $job.Jobname + " - Failed"
$value = $job.Failed
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>Count</unit>"
"<VolumeSize>One</VolumeSize>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"<LimitMaxWarning>1</LimitMaxWarning>"
"<LimitMaxError>2</LimitMaxError>"
"<LimitMode>1</LimitMode>"
"</result>"
}
#region: VBO Reposities to PRTG results
ForEach ($repository in $vboRepositories) {
$channel = "Repository - " + $repository.Name + " - Capacity"
$value = $repository.Capacity
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>BytesDisk</unit>"
"<VolumeSize>GigaByte</VolumeSize>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"</result>"
$channel = "Repository - " + $repository.Name + " - Free"
$value = $repository.Free
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<unit>BytesDisk</unit>"
"<VolumeSize>GigaByte</VolumeSize>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"<LimitMinWarning>1073741824</LimitMinWarning>"
"<LimitMinError>536870912</LimitMinError>"
"<LimitMode>1</LimitMode>"
"</result>"
}
#endregion
#region: VBO Proxies to PRTG results
ForEach ($proxy in $vboProxies) {
$channel = "Proxy - " + $proxy.Name + " - Status"
$value = [int]($proxy.Status -like "*Online*")
Write-Host "<result>"
"<channel>$channel</channel>"
"<value>$value</value>"
"<customunit>Status</customunit>"
"<showChart>1</showChart>"
"<showTable>1</showTable>"
"<LimitMinError>0</LimitMinError>"
"<LimitMode>1</LimitMode>"
"</result>"
}
#endregion
Write-Host "</prtg>"
@AksiOperations
Copy link

Great script very usefull
Did have to replace the "Write-Host" by "write-output" to get the script working in PRTG.

Kind regards,

@TOAOQ
Copy link

TOAOQ commented Feb 12, 2025

HI,

Script/sensor worked well until someone upgraded our Veeam from version 7 to version 8. Did some troubleshooting and changed the script a little but it now keeps stopping with errors:

Write-Error: Error invoking web request
Write-Error: Error authentication result
Invoke-WebRequest: **********TestVeeamSensor.ps1:70:15
Line |
70 | … sonResult = Invoke-WebRequest -Uri $apiUrl$url -Headers $headers -Met …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch
Write-Error: Error in jobs result

Tried numerous ways of bypassing the certificate check but keeps failing there.
Anyone got it to work with Veeam for M365 V8?

Thanks in advance!

Q

@berseker6666
Copy link

berseker6666 commented May 5, 2025

Hi,
@BasvanH this script is compatible with version 8 of VBO ?

@TS-Steff
Copy link

TS-Steff commented May 6, 2025

@berseker6666 and @TOAOQ
Based on @BasvanH gist i created a sensor to monitor Veeam with PRTG. You can find it at https://github.com/TS-Steff/PRTG-Veeam-MS365-Tenant
This is V8 compatible.

@BasvanH If it's not ok to link to my skript, please feel free to remove the link.

@BasvanH
Copy link
Author

BasvanH commented May 6, 2025

@TS-Steff, no worries. I no longer have access to PRTG, so testing new VBO releases with the script was becoming quite challenging. If you’re able to continue, that’s perfectly fine with me.

@berseker6666
Copy link

berseker6666 commented May 13, 2025

@berseker6666 and @TOAOQ Based on @BasvanH gist i created a sensor to monitor Veeam with PRTG. You can find it at https://github.com/TS-Steff/PRTG-Veeam-MS365-Tenant This is V8 compatible.

@BasvanH If it's not ok to link to my skript, please feel free to remove the link.

Hi @TS-Steff
Hello.
Here's what I got as a result in PRTG with the AllTenants script

@TS-Steff
Copy link

Hi @berseker6666
did you import the ts.veeam.MS365.JobStatus.ovl file?

@berseker6666
Copy link

berseker6666 commented May 14, 2025 via email

@TS-Steff
Copy link

On the Server in the following path
C:\Program Files (x86)\PRTG Network Monitor\lookups

@TOAOQ
Copy link

TOAOQ commented May 27, 2025

@berseker6666 and @TOAOQ Based on @BasvanH gist i created a sensor to monitor Veeam with PRTG. You can find it at https://github.com/TS-Steff/PRTG-Veeam-MS365-Tenant This is V8 compatible.

@BasvanH If it's not ok to link to my skript, please feel free to remove the link.

Just saw your reply and had also started using your version. We encountered some issues where lookups seemed to fail somehow due to changed API replies (or something, not exactly my cup of tea).
Saw something mentioned here about a lookup file, will try this as well.

On the Server in the following path C:\Program Files (x86)\PRTG Network Monitor\lookups

Hej TS,

Can you please put the import-part into your manual?
We do not have a folder \lookups on our Probeserver, where to put it and what does it do/fix?

@TS-Steff
Copy link

Hi @TOAOQ
I've just updated the manual and also added the PRTG Manual link.

Kind regards, Stefan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment