Greetings friends, over the years, I have been creating all sort of scripts, dashboards, or tiny applications to solve Customers problems, or Community requests.
A few weeks ago I got an interesting use-case. When you are running thousands of workloads in AWS and you want to audit, or submit to compliance the task can become a bit manual, going through your VB Appliance, etc.The specific use-case was that sometimes the team receives requests about data protection with an existing random vm names, with random dates to audit. That example right there is great, but there is no report that can cover that.
But, of course we have the powerful REST API for Veeam Backup for AWS:
- https://helpcenter.veeam.com/docs/vbaws/rest/rest_api_reference.html?ver=70
- https://www.veeam.com/veeam_backup_aws_7_0_rest_api_reference_map_ot.pdf
Custom script in PowerShell
When working with It professionals, I found much simpler to work with PowerShell, which by the way can handle perfectly fine querying API endpoints and operating with them right after. The only tricky part perhaps is to combine everything into an HTML later on, but more on that later, let’s dissect a bit the Script.
- Download the Script from here: https://raw.githubusercontent.com/jorgedlcruz/veeam-html-reporting/main/veeam-backup-for-aws/workload-protection-history/Veeam_Backup_AWS_WorkloadProtectionHistory.ps1
After all ran successfully, you should have something similar to this:
1.- Disable SSL verification
I know I know, you might think this is insecure, etc. But it is very common, and much more nowadays, that appliances that comes from vendors, like VB AWS, to just trust the SSL that comes with and omit the headache of maintaining a SSL. But if you want to resolve this, I got you covered with the next blog post using Let’s Encrypt.
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
2.- Variables for the script
This is where you will spend the most time right. Probably once for the user/pass, but coming often to edit the name of the VMs, and dates you want to audit on:
# Variables for API connection $veeamUsername = "YOURUSER" $veeamPassword = "YOURPASS" $veeamBackupAWSServer = "https://YOURVBIP" $veeamBackupAWSPort = "11005" $apiVersion = "1.4-rev0" # Make sure you use the API version according to your appliance https://helpcenter.veeam.com/docs/vbaws/rest/versioning.html?ver=70 $apiUrl = "$veeamBackupAWSServer`:$veeamBackupAWSPort/api/v1" $vmNames = @('MYVMANEM1','MYVMNAME2') $startDate = '2024-01-01T00:00:01' $endDate = '2024-02-21T23:59:59'
3.- Functions
Bunch of functions so we can call them on the gran finale!
3.1.1.- Get-ApiToken
A variable that connects to the auth endpoint, uses user/pass and gets an auth token for later:
# Function to get API token - Public API Function Get-ApiToken { Write-Host "Requesting API token..." $body = @{ username = $veeamUsername password = $veeamPassword grant_type = 'password' } $headers = @{ "Content-Type" = "application/x-www-form-urlencoded" "Accept" = "application/json" "x-api-version" = $apiVersion } $uri = "$apiUrl/token" # Using Invoke-RestMethod for API call $response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -Headers $headers -ContentType 'application/x-www-form-urlencoded' Write-Host "API token received." return $response.access_token } $token = Get-ApiToken
3.1.2.- Get-NewApiToken
You are not going to believe it, but we needed an additional endpoint that is not on public API, but still exists on the private one. The one where the UI is built on:
# Function to get API token - Private API Function Get-NewApiToken { Write-Host "Requesting new API token..." # Define the request URI and body $uri = "$veeamBackupAWSServer/api/oauth2/token" $body = "username=$veeamUsername&password=$veeamPassword&rememberMe=false&use_short_term_refresh=true&grant_type=password" # Define headers for the request $headers = @{ "Content-Type" = "application/x-www-form-urlencoded" "Accept" = "application/json" } # Make the request and extract the token try { $response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body Write-Host "New API token received." return $response.access_token } catch { Write-Host "Error obtaining new API token: $($_.Exception.Message)" } } $newToken = Get-NewApiToken
3.1.3. – GetVMIds
As we are introducing the VM names we need to audit, this function will help us to retrieve the VMIDs we need for future calls:
# Function to get VM IDs by name Function Get-VMIds { Param ( [string]$vmName ) Write-Host "Retrieving VM IDs for $vmName..." $headers = @{ 'Authorization' = "Bearer $token" 'x-api-version' = $apiVersion } $response = Invoke-RestMethod -Uri "$apiUrl/virtualMachines?SearchPattern=$vmName" -Headers $headers $vmIds = $response.results.id Write-Host "VM IDs for $vmName retrieved: $($vmIds -join ', ')" return $vmIds }
3.1.4. – GetPoliciesByVMid
A function that will query the policies per VMId, usually should be one, but sometimes you can have multiple policies for the same workload, perhaps with just snapshots, or just replicas, etc.
# Function to get policies by VM ID Function Get-PoliciesByVMId { Param ( [string]$vmId ) Write-Host "Retrieving policies for VM ID: $vmId..." $headers = @{ 'Authorization' = "Bearer $token" 'x-api-version' = $apiVersion } $policies = @() # Initialize an empty array to store policy objects try { $response = Invoke-RestMethod -Uri "$apiUrl/virtualMachines/policies?VirtualMachineId=$vmId" -Headers $headers if ($response.totalCount -gt 0) { foreach ($policy in $response.results) { $policyObj = [PSCustomObject]@{ 'ID' = $policy.id 'Name' = $policy.name } $policies += $policyObj } } else { Write-Host "No policies found for VM ID: $vmId" } } catch { Write-Host "Error retrieving policies for VM ID: $vmId - $($_.Exception.Message)" } return $policies }
3.1.5.- GetPolicyInstanceSessions
This is the part that is not supported currently on public API, so we ended calling the internal API to get something very simple, the policy sessions per policy ID and VM ID:
# Function to get policies sessions by Policy Id and VM ID Function Get-PolicyInstanceSessions { Param ( [string]$policyId, [string]$vmId ) Write-Host "Retrieving policy instance sessions for Policy ID: $policyId and VM ID: $vmId..." $headers = @{ "Authorization" = "Bearer $newToken" "Content-Type" = "application/json" } $body = @{ "skip" = 0 "count" = 200 "orderAsc" = $false "orderColumn" = 'startTimeUtc' "policyId" = $policyId "instanceId" = $vmId "filters" = @{ "timePeriod" = '' "statuses" = @() "sessionTypes" = @() } } | ConvertTo-Json try { $response = Invoke-RestMethod -Uri "$veeamBackupAWSServer/api/v1/reports/policyInstanceSessions" -Method "POST" -Headers $headers -ContentType "application/json" -Body $body $filteredSessions = @() foreach ($session in $response.data) { $sessionStopTimeStr = $session.stopTime -split '\.' | Select-Object -First 1 $sessionStopTime = [datetime]::ParseExact($sessionStopTimeStr, 'yyyy-MM-ddTHH:mm:ss', $null) if ($sessionStopTime -ge $startDate -and $sessionStopTime -le $endDate) { $filteredSessions += $session } } Write-Host "Filtered policy instance sessions retrieved successfully." return $filteredSessions } catch { Write-Host "Error retrieving policy instance sessions: $($_.Exception.Message)" return @() } }
4.- The grand finale! Putting all the functions to work
This is the script, now we are going to run through all the functions, get the token, get the VM Ids, check the policies where the VMs are, and finally check the sessions for those policies within the timeframe selected on topn:
# Initialize an empty array for all unique policy session objects $policySessionObjects = @() # Main script execution part foreach ($vmName in $vmNames) { Write-Host "Processing VM: $vmName" $vmIds = Get-VMIds -vmName $vmName foreach ($vmId in $vmIds) { Write-Host "Retrieving policies for VM ID: $vmId..." $policies = Get-PoliciesByVMId -vmId $vmId foreach ($policy in $policies) { $policyId = $policy.ID $policyName = $policy.Name Write-Host "Retrieving sessions for Policy: '$policyName' (ID: $policyId) and VM ID: $vmId..." $sessions = Get-PolicyInstanceSessions -policyId $policyId -vmId $vmId foreach ($session in $sessions) { $sessionObj = [PSCustomObject]@{ 'VM Name' = $vmName 'Policy ID' = $policyId 'Policy Name' = $policyName 'Session Type' = $session.sessionType 'Result' = $session.result 'Session ID' = $session.sessionId 'Start Time' = $session.startTime 'Stop Time' = $session.stopTime 'Duration (s)' = $session.duration } $policySessionObjects += $sessionObj } } } }
5.- Preparing the HTML/CSV and Export
An script is not an script without a proper and valid output. In this case I thought it could be cool to export as HTML, and CSV. Using the current time when the script was ran:
if ($policySessionObjects.Count -gt 0) { $htmlHeader = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Policy Session Report</title> <style> body { font-family: 'Open-Sans', sans-serif; padding: 10px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #dddddd; text-align: left; padding: 8px; } th { background-color: #f2f2f2; } .success { background-color: #28a745; color: white; font-weight: bold; } .failed { background-color: #dc3545; color: white; font-weight: bold; } .warning { background-color: #ffc107; color: black; font-weight: bold; } </style> </head> <body> <h1>Policy Session Report</h1> <table> <tr> <th>VM Name</th> <th>Policy Name</th> <th>Session Type</th> <th>Result</th> <th>Session ID</th> <th>Start Time</th> <th>Stop Time</th> <th>Duration (s)</th> </tr> "@ $htmlFooter = @" </table> </body> </html> "@ # Construct the HTML for each row based on the policy session objects $rowsHtml = $policySessionObjects | ForEach-Object { $rowData = $_ $resultStyle = switch ($rowData.'Result') { 'Success' { 'class="success"' } 'Failed' { 'class="failed"' } 'Warning' { 'class="warning"' } Default { '' } # Default, no additional class } $rowHtml = "<tr>" $rowHtml += "<td>$($rowData.'VM Name')</td>" $rowHtml += "<td>$($rowData.'Policy Name')</td>" $rowHtml += "<td>$($rowData.'Session Type')</td>" $rowHtml += "<td $resultStyle>$($rowData.'Result')</td>" # Apply conditional formatting here $rowHtml += "<td>$($rowData.'Session ID')</td>" $rowHtml += "<td>$($rowData.'Start Time')</td>" $rowHtml += "<td>$($rowData.'Stop Time')</td>" $rowHtml += "<td>$($rowData.'Duration (s)')</td>" $rowHtml += "</tr>" $rowHtml } # Building the Output $currentDate = Get-Date -Format "yyyy-MM-dd" # Define file names with the current date $htmlFileName = "$currentDate-PolicySessionReport.html" $csvFileName = "$currentDate-PolicySessionReport.csv" # Define file paths relative to the current script's directory $htmlFilePath = Join-Path -Path $PSScriptRoot -ChildPath $htmlFileName $csvFilePath = Join-Path -Path $PSScriptRoot -ChildPath $csvFileName # Combine all parts of the HTML $htmlContent = $htmlHeader + $rowsHtml + $htmlFooter # Output to HTML file $htmlContent | Out-File -FilePath $htmlFilePath Write-Host "HTML report saved to $htmlFilePath" # Export to CSV file $policySessionObjects | Export-Csv -Path $csvFilePath -NoTypeInformation Write-Host "CSV report saved to $csvFilePath" } else { Write-Host "No policy session information was found for any VMs." }
And that’s it. Most likely this logic, and the use-case will be added to Veeam ONE in the future, but meanwhile, solving real-use case scenarios with API, always makes me happy.
I hope this is useful for you, give it a try and let me know.
Wesley Martins says
Great post, Jorge.
I had a few customer asking for that and even to troubleshooting this is good to check when an instance failed or not.