Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save BasvanH/2229ca0d5d7b17df72a412db36d7bc5c to your computer and use it in GitHub Desktop.
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>"
@BasvanH
Copy link
Author

BasvanH commented Feb 5, 2019

Updated the script.

  • Added logic for session handling while job is running.
  • Added conversion to bytes for session transferred value.

@BasvanH
Copy link
Author

BasvanH commented May 7, 2019

Updated the script.

  • It's now talking to v3 API.

@mcahenzli
Copy link

hi basvan
since we use veeam o365 version 5.x, we had some problems with your script. from prtg, we receive the following error:
XML: Junk after document element

Is this a known error with the version 5 of veeam? or is this a problem on our side?
Best regards
Michael

@BasvanH
Copy link
Author

BasvanH commented Jun 16, 2021

XML: Junk after document element

This means there's more info in the scripts result than PRTG can parse, non XML data, I need to take a closer look at this.

@iain1337
Copy link

iain1337 commented Dec 9, 2021

Hi BasvanH

I just stumbled upon your script here, when I was researching about a way to monitor my Veeam O365 Backup Jobs.
Then I implemented your script to my PRTG and added the needed credentials but I always get the following error:

No "result" or "error" in XML response.

Any idea what I might be doing wrong or where I could get more information about the error?
Thanks a lot in advance!

Best regards
Iain

@BasvanH
Copy link
Author

BasvanH commented Dec 15, 2021

I have updated the script to work with v5 of Veeam Backup for Office 365, @mcahenzli & @iain1337 please try it again with the new script.

@erikplekenpol
Copy link

I've had the same 'No "result" or "error" in XML response.'
Make sure you use the correct port. V5 documentation mentions port 4443 instead of port 443: https://helpcenter.veeam.com/docs/vbo365/guide/vbo_rest_api_settings.html?ver=50
Also be aware that the probe computer must trust the certificate of the Veaam Office 365 Backup API.
Or add this code after the param ( [...] ) block: (this skips the certificate check, at your own peril !)
add-type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

Source: http://vcloud-lab.com/entries/powershell/powershell-invoke-webrequest-the-underlying-connection-was-closed-could-not-establish-trust-relationship-for-the-ssl-tls-secure-channel-

@BasvanH
Copy link
Author

BasvanH commented Dec 23, 2021

Those are off-course depended on how you deploy the API endpoint. I've got it configured internet facing at 443 and configured with a public certificate. But thanks for your input @erikplekenpol, it will sure help others.

@andrea321p
Copy link

Hello BasvanH,

we have Veeam Backup for Office 365 v6 and we receive the following error:

Response not well-formed
(code: PE132).

Can you help us ?
Thanks a lot in advance!

Best Regards,
Andrea

@Fanman76
Copy link

Not working with the curent v6 version of Backup for Office 365

@fh19496
Copy link

fh19496 commented Jul 14, 2022

Is there an upcoming update for VBO v6?

@heocom
Copy link

heocom commented Aug 29, 2022

A version of the script for Veeam V6 would also be interesting for us and certainly for many more.

@BasvanH
Copy link
Author

BasvanH commented Oct 14, 2022

@fh19496 and @heocom, added v6 support. Could you check if it works properly, I no longer have PRTG myself.

@Fanman76
Copy link

@fh19496 and @heocom, added v6 support. Could you check if it works properly, I no longer have PRTG myself.

XML: The returned XML does not match the expected schema. (code: PE233) -- JSON: The returned JSON does not match the expected structure (Invalid JSON.). (code: PE231)

@BasvanH
Copy link
Author

BasvanH commented Oct 14, 2022

@Fanman76, could you run the script within PS ISE. Remove the the parameter part (line 26-30) and paste this there. Fill in the necessary details.

$apiUrl = "https://<url>"
$username = "username"
$password = "password"

This should give more answers in whats going wrong. Probably more information is returned so the echoed XML is broken.

@Fanman76
Copy link

@Fanman76, could you run the script within PS ISE. Remove the the parameter part (line 26-30) and paste this there. Fill in the necessary details.

$apiUrl = "https://<url>"
$username = "username"
$password = "password"

This should give more answers in whats going wrong. Probably more information is returned so the echoed XML is broken.

Looks like a connection cannot be made. If I manualy do a query to the API, I'm getting normal results.

Te responce of the script:

D:\scripts\M365_Backup-stats.ps1 : Error invoking web request
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,M365_Backup-stats.ps1

D:\scripts\M365_Backup-stats.ps1 : Error authentication result
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,M365_Backup-stats.ps1

Invoke-WebRequest : The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.
At D:\scripts\M365_Backup-stats.ps1:73 char:15

  • ... sonResult = Invoke-WebRequest -Uri $apiUrl$url -Headers $headers -Met ...
  •             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    • FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

D:\scripts\M365_Backup-stats.ps1 : Error in jobs result
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,M365_Backup-stats.ps1

@BasvanH
Copy link
Author

BasvanH commented Oct 18, 2022

Do you have a real SSL certificate installed, if not see this comment how to bypass SSL checks in PS.

https://gist.github.com/BasvanH/2229ca0d5d7b17df72a412db36d7bc5c?permalink_comment_id=4005390#gistcomment-4005390

@TS-Steff
Copy link

TS-Steff commented Jan 25, 2023

First of all thank you. This is awesome

Did any one create lookups for the different values? What are the possible values? I just found the States (success, warning, faild, default probably unknown) for the job.status. Are there others? I'll make the ovl files.

Then we changed the script for our needs. We backup multiple customers and wanted to add this script per customer without the proxies. You may have a look at it at https://github.com/TS-Steff/PRTG-Veeam-MS365-Tenant
Keep in mind, this is probably pretty hacky.

Kind regards,
Stefan

@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,

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