Fix WSB collector against a real server's object model
Checked with Get-WsbCollectorDiagnostics.ps1 output from a Server 2019
Hyper-V host (AAOG-EDM-HVR1):
- WBJob StartTime/EndTime are culture-formatted local-time strings
("10/6/2026 9:00 PM"). Parse them with the current culture: the
[datetime] cast uses the invariant (US) format and would misread
dd/MM dates, e.g. on en-CA/en-GB servers. Confirmed local, not UTC,
against the Microsoft-Windows-Backup event log
- Hyper-V backups nest VMs under a VirtualMachineList item's
SubItemList; failures, names and the items list now come from the
leaf items, falling back to a parent's SubItemFailed count
- Report size from the top-level items' BytesProcessed
- FailureLogPath is set even on success: running backups get no
message, and the log is only cited for warnings/failures
- HRESULTs are strings; conversion tolerates hex and bad values
- Mock rebuilt on the real shapes; tested under en-US and en-GB
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# Stand-ins for the WindowsServerBackup cmdlets. Object shapes are best guesses until checked
|
||||
# against Get-WsbCollectorDiagnostics.ps1 output from a real server.
|
||||
# Stand-ins for the WindowsServerBackup cmdlets. Object shapes follow Get-WsbCollectorDiagnostics.ps1
|
||||
# output from a real Hyper-V host (Server 2019): job times are culture-formatted local-time strings
|
||||
# to the minute ("10/6/2026 9:00 PM" on en-US), HRESULTs are strings, and a Hyper-V backup has one
|
||||
# VirtualMachineList item whose SubItemList holds the VMs. When nothing is running, Get-WBJob
|
||||
# returns an empty job (JobType None).
|
||||
#
|
||||
# . ./tests/MockWsb.ps1 # nothing running
|
||||
# . ./tests/MockWsb.ps1 -WithRunningJob # a backup in progress
|
||||
@@ -9,50 +12,84 @@ param([switch]$WithRunningJob)
|
||||
# Fixed to the top of the hour so repeated runs see the same jobs
|
||||
$now = Get-Date -Minute 0 -Second 0 -Millisecond 0
|
||||
|
||||
function New-MockItem($Name, [int]$HResult = 0, $ErrorDescription = '') {
|
||||
[pscustomobject]@{ Name = $Name; Type = 'Volume'; State = $(if ($HResult) { 'Failed' } else { 'Completed' })
|
||||
HResult = $HResult; ErrorDescription = $ErrorDescription }
|
||||
# Same format as the real cmdlet: short date + short time in the current culture
|
||||
function Format-WbTime($Value) {
|
||||
if ($null -eq $Value) { return '' }
|
||||
return ([datetime]$Value).ToString('g', [Globalization.CultureInfo]::CurrentCulture)
|
||||
}
|
||||
|
||||
function New-MockJob($Type, $Start, $End, $State, [int]$HResult = 0, [int]$DetailedHResult = 0, $ErrorDescription = '', $Items = @()) {
|
||||
function New-MockItem($Name, $Type = 'Volume', [long]$Bytes = 0, [string]$HResult = '0', $ErrorDescription = '', $SubItems = $null) {
|
||||
$failedSubs = @($SubItems | Where-Object { $_ -and $_.HResult -ne '0' }).Count
|
||||
[pscustomobject]@{
|
||||
Name = $Name
|
||||
Type = $Type
|
||||
State = $(if ($HResult -ne '0') { 'Failed' } else { 'Completed' })
|
||||
HResult = $HResult
|
||||
DetailedHResult = '0'
|
||||
ErrorDescription = $ErrorDescription
|
||||
BytesProcessed = $Bytes
|
||||
TotalBytes = $Bytes
|
||||
CurrentItem = $null
|
||||
SubItemProcessed = [long]@($SubItems | Where-Object { $_ }).Count
|
||||
SubItemFailed = [long]$failedSubs
|
||||
TotalSubItem = [long]@($SubItems | Where-Object { $_ }).Count
|
||||
SubItemList = $SubItems
|
||||
}
|
||||
}
|
||||
|
||||
function New-VmList([long]$VmBytes, [string]$VmHResult = '0', $VmError = '') {
|
||||
$vms = @(
|
||||
(New-MockItem 'AAOG-EDM-AD1' 'Component' $VmBytes $VmHResult $VmError),
|
||||
(New-MockItem 'Host Component' 'Component' 447729)
|
||||
)
|
||||
New-MockItem 'VirtualMachineList' 'VirtualmachineList' ($VmBytes + 447729) -SubItems $vms
|
||||
}
|
||||
|
||||
function New-MockJob($Type, $Start, $End, $State, [string]$HResult = '0', [string]$DetailedHResult = '0', $ErrorDescription = '', $Items = $null) {
|
||||
[pscustomobject]@{
|
||||
JobType = $Type
|
||||
StartTime = $Start
|
||||
EndTime = $(if ($End) { $End } else { [datetime]::MinValue })
|
||||
StartTime = Format-WbTime $Start
|
||||
EndTime = Format-WbTime $End
|
||||
JobState = $State
|
||||
CurrentOperation = ''
|
||||
HResult = $HResult
|
||||
DetailedHResult = $DetailedHResult
|
||||
ErrorDescription = $ErrorDescription
|
||||
JobItems = $Items
|
||||
VersionId = $(if ($End) { $Start.ToUniversalTime().ToString('MM/dd/yyyy-HH:mm') } else { '' })
|
||||
SuccessLogPath = 'C:\Windows\Logs\WindowsServerBackup\Backup-success.log'
|
||||
FailureLogPath = $(if ($HResult -or $DetailedHResult) { 'C:\Windows\Logs\WindowsServerBackup\Backup_Error.log' } else { '' })
|
||||
VersionId = $(if ($End) { $Start.ToUniversalTime().ToString('MM/dd/yyyy-HH:mm', [Globalization.CultureInfo]::InvariantCulture) } else { '' })
|
||||
SuccessLogPath = 'C:\Windows\Logs\WindowsServerBackup\Backup.log'
|
||||
# Populated even for successful backups, as on the real server
|
||||
FailureLogPath = 'C:\Windows\Logs\WindowsServerBackup\Backup_Error.log'
|
||||
}
|
||||
}
|
||||
|
||||
$global:MockWsbRunning = if ($WithRunningJob) {
|
||||
@(New-MockJob 'Backup' $now.AddMinutes(-10) $null 'Running' -Items @((New-MockItem 'C:'), (New-MockItem 'D:')))
|
||||
@(New-MockJob 'Backup' $now.AddMinutes(-10) $null 'Running' -Items @(New-VmList 10GB))
|
||||
} else {
|
||||
# What an idle server might return: an empty job rather than nothing
|
||||
@([pscustomobject]@{ JobType = 'None'; StartTime = [datetime]::MinValue; EndTime = [datetime]::MinValue; JobState = 'Unknown' })
|
||||
@(New-MockJob 'None' $null $null 'Unknown')
|
||||
}
|
||||
|
||||
$global:MockWsbPrevious = @(
|
||||
(New-MockJob 'Backup' $now.AddHours(-12) $now.AddHours(-11) 'Completed' -Items @((New-MockItem 'C:'), (New-MockItem 'D:'))),
|
||||
# Completed overall, but one volume failed
|
||||
(New-MockJob 'Backup' $now.AddHours(-36) $now.AddHours(-35) 'Completed' 0 -2147024891 '' @(
|
||||
(New-MockItem 'C:'), (New-MockItem 'D:' -2147024891 'Access is denied.'))),
|
||||
(New-MockJob 'Backup' $now.AddHours(-12) $now.AddHours(-11.6) 'Completed' -Items @(New-VmList 124756706036)),
|
||||
# Completed overall, but one VM failed
|
||||
(New-MockJob 'Backup' $now.AddHours(-36) $now.AddHours(-35.6) 'Completed' '0' '0' '' @(
|
||||
New-VmList 0 '-2147024891' 'Access is denied.')),
|
||||
# Failed outright: backup target not found (0x80042302)
|
||||
(New-MockJob 'Backup' $now.AddHours(-30) $now.AddHours(-29.9) 'Completed' -2147212542 0 `
|
||||
(New-MockJob 'Backup' $now.AddHours(-30) $now.AddHours(-29.9) 'Completed' '-2147212542' '0' `
|
||||
'The backup storage location could not be found.'),
|
||||
# A volume-based backup, for servers that aren't Hyper-V hosts
|
||||
(New-MockJob 'Backup' $now.AddHours(-20) $now.AddHours(-19.5) 'Completed' -Items @(
|
||||
(New-MockItem 'C:' 'Volume' 40GB), (New-MockItem 'D:' 'Volume' 200GB))),
|
||||
(New-MockJob 'Recovery' $now.AddHours(-5) $now.AddHours(-4) 'Completed'), # a restore: must be ignored
|
||||
(New-MockJob 'Backup' $now.AddDays(-5) $now.AddDays(-5).AddHours(1) 'Completed') # outside 48h lookback
|
||||
)
|
||||
|
||||
$global:MockWsbSummary = [pscustomobject]@{
|
||||
NextBackupTime = $now.Date.AddHours(21).AddDays($(if ($now.Hour -ge 21) { 1 } else { 0 }))
|
||||
LastSuccessfulBackupTime = $now.AddHours(-11)
|
||||
LastBackupResultHR = 0
|
||||
NumberOfVersions = 1
|
||||
LastSuccessfulBackupTime = $now.AddHours(-12)
|
||||
LastBackupResultHR = [uint32]0
|
||||
CurrentOperationStatus = $(if ($WithRunningJob) { 'BackupInProgress' } else { 'NoOperationInProgress' })
|
||||
}
|
||||
|
||||
function Get-WBJob { param([int]$Previous) if ($PSBoundParameters.ContainsKey('Previous')) { $global:MockWsbPrevious } else { $global:MockWsbRunning } }
|
||||
|
||||
Reference in New Issue
Block a user