Add Veeam Backup for Microsoft 365 collector
PowerShell script for the VB365 server, run hourly as a SYSTEM scheduled
task (Windows PowerShell 5.1 by default, -PwshPath for PowerShell 7).
One dashboard job per VB365 backup job and backup copy job.
- Sessions from Get-VBOJobSession (and Get-VBOCopyJobSession where
available) within the lookback window; status from session Status,
message from the session log's warning/error lines, size from
Statistics.TransferredData ("1.5 GB" -> bytes)
- One VB365 server can protect several Microsoft 365 organizations, so
the dashboard company is chosen per job: OrganizationTenants mapping,
else Tenant, else the organization's name
- Next run from the job's NextRun; empty for disabled jobs
- Get-Vb365CollectorDiagnostics.ps1 dumps jobs and sessions, showing
each DateTime's Kind (local vs UTC); installer adapted from MARS
Not yet run against a real server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read-only dump of what the Veeam Backup for Microsoft 365 cmdlets return, for troubleshooting
|
||||
the collector.
|
||||
|
||||
.DESCRIPTION
|
||||
The collector reads properties of Get-VBOJob / Get-VBOJobSession results whose names aren't
|
||||
fully documented. This prints those objects' real structure (property names, types, values and
|
||||
each date's Kind, a few levels deep) for a few jobs and their latest sessions. Nothing is
|
||||
changed. Output is also saved to a text file.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-Vb365CollectorDiagnostics.ps1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$Jobs = 3,
|
||||
[int]$SessionsPerJob = 2,
|
||||
[string]$OutFile = (Join-Path $env:TEMP 'vb365-collector-diagnostics.txt')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$MaxDepth = 4
|
||||
|
||||
if (-not (Get-Command Get-VBOJob -ErrorAction SilentlyContinue)) {
|
||||
if (Get-Module -ListAvailable -Name Veeam.Archiver.PowerShell) {
|
||||
Import-Module Veeam.Archiver.PowerShell
|
||||
} else {
|
||||
Import-Module (Join-Path $env:ProgramFiles 'Veeam\Backup365\Veeam.Archiver.PowerShell\Veeam.Archiver.PowerShell.psd1')
|
||||
}
|
||||
}
|
||||
|
||||
function Test-Scalar($Value) {
|
||||
return $null -eq $Value -or $Value -is [string] -or $Value -is [ValueType] -or $Value -is [guid]
|
||||
}
|
||||
|
||||
# Recursive "name (type) = value" listing. Collections show their first 3 items.
|
||||
function Show-Object($Value, [string]$Indent = ' ', [int]$Depth = 0) {
|
||||
if (Test-Scalar $Value) { return }
|
||||
if ($Depth -ge $MaxDepth) { "$Indent..."; return }
|
||||
if ($Value -is [Collections.IEnumerable]) {
|
||||
$items = @($Value)
|
||||
"$Indent[$($items.Count) item(s)]"
|
||||
$i = 0
|
||||
foreach ($item in $items | Select-Object -First 3) {
|
||||
"$Indent[$i] ($($item.GetType().FullName))$(if (Test-Scalar $item) { " = $item" })"
|
||||
Show-Object $item "$Indent " ($Depth + 1)
|
||||
$i++
|
||||
}
|
||||
return
|
||||
}
|
||||
foreach ($p in $Value.PSObject.Properties) {
|
||||
try { $v = $p.Value } catch { "$Indent$($p.Name) = <error: $($_.Exception.Message)>"; continue }
|
||||
$type = if ($null -eq $v) { 'null' } else { $v.GetType().FullName }
|
||||
if (Test-Scalar $v) {
|
||||
$kind = if ($v -is [datetime]) { " [Kind=$($v.Kind)]" } else { '' }
|
||||
"$Indent$($p.Name) ($type) = $v$kind"
|
||||
} else {
|
||||
$text = [string]$v
|
||||
if ($text.Length -gt 100) { $text = $text.Substring(0, 97) + '...' }
|
||||
"$Indent$($p.Name) ($type) = $text"
|
||||
Show-Object $v "$Indent " ($Depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Section([string]$Title, [scriptblock]$Get) {
|
||||
"== $Title"
|
||||
try {
|
||||
$result = @(& $Get)
|
||||
if ($result.Count -eq 0 -or ($result.Count -eq 1 -and $null -eq $result[0])) { ' (nothing returned)' }
|
||||
$n = 0
|
||||
foreach ($item in $result) {
|
||||
if ($null -eq $item) { continue }
|
||||
" [$n] ($($item.GetType().FullName))"
|
||||
Show-Object $item ' '
|
||||
$n++
|
||||
}
|
||||
} catch {
|
||||
" error: $($_.Exception.Message)"
|
||||
}
|
||||
''
|
||||
}
|
||||
|
||||
$report = & {
|
||||
$module = Get-Module Veeam.Archiver.PowerShell
|
||||
"Generated $(Get-Date -Format o) on $env:COMPUTERNAME, PowerShell $($PSVersionTable.PSVersion), module $($module.Version)"
|
||||
"Local time zone: $([TimeZoneInfo]::Local.Id), UTC offset now $([TimeZoneInfo]::Local.GetUtcOffset((Get-Date))), culture $([Globalization.CultureInfo]::CurrentCulture.Name)"
|
||||
''
|
||||
"== Organizations"
|
||||
try {
|
||||
Get-VBOOrganization | ForEach-Object { " $($_.Name) [$($_.Type)]" }
|
||||
} catch { " error: $($_.Exception.Message)" }
|
||||
''
|
||||
$allJobs = @(Get-VBOJob)
|
||||
"== Jobs: $($allJobs.Count) in total"
|
||||
$allJobs | ForEach-Object { " $($_.Name) [$($_.Id)] enabled=$($_.IsEnabled) next=$($_.NextRun)" }
|
||||
''
|
||||
Show-Section "Get-VBOJob (first $Jobs, full detail)" { $allJobs | Select-Object -First $Jobs }
|
||||
foreach ($j in $allJobs | Select-Object -First $Jobs) {
|
||||
Show-Section "Get-VBOJobSession -Job '$($j.Name)' (latest $SessionsPerJob)" {
|
||||
Get-VBOJobSession -Job $j | Sort-Object CreationTime -Descending | Select-Object -First $SessionsPerJob
|
||||
}
|
||||
}
|
||||
if (Get-Command Get-VBOCopyJob -ErrorAction SilentlyContinue) {
|
||||
Show-Section 'Get-VBOCopyJob (first 2)' { Get-VBOCopyJob | Select-Object -First 2 }
|
||||
if (Get-Command Get-VBOCopyJobSession -ErrorAction SilentlyContinue) {
|
||||
Show-Section 'Get-VBOCopyJobSession (latest 2)' {
|
||||
Get-VBOCopyJobSession | Sort-Object CreationTime -Descending | Select-Object -First 2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
'== Get-VBOCopyJob: not available'
|
||||
}
|
||||
} | Out-String -Width 300
|
||||
|
||||
$report
|
||||
$report | Set-Content -Path $OutFile -Encoding UTF8
|
||||
Write-Host "Saved to $OutFile"
|
||||
Reference in New Issue
Block a user