-
-
Save BasvanH/2229ca0d5d7b17df72a412db36d7bc5c to your computer and use it in GitHub Desktop.
<# | |
.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>" |
@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, 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
Do you have a real SSL certificate installed, if not see this comment how to bypass SSL checks in PS.
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
Great script very usefull
Did have to replace the "Write-Host" by "write-output" to get the script working in PRTG.
Kind regards,
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)