Fix MARS collector against a real agent's object model

Checked with Get-MarsCollectorDiagnostics.ps1 output from MARS agent
2.0.9274 (VA-FS-01):

- JobStatus.StartTime/EndTime are UTC values not marked as UTC
  (DisplayTime is the local one); they were read as local, putting
  runs 6 hours off. Lookback filtering now compares in UTC too
- ErrorInfo is always present with ErrorCode 0 on success; every
  completed backup would have been a warning. Only non-zero error
  codes count
- Schedule times are TimeSpans; casting them to DateTime failed, so
  next_run_at was always empty
- DatasourceStatus.Datasource is an object; use its DataSourceName
- Also report FileProgress.Failed file counts and FailedFileLog
- Mock rebuilt on the real shapes; diagnostics handle no System State
  policy

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-07 15:40:08 -06:00
co-authored by Claude Opus 5.5
parent 10181e014b
commit 5982fc9434
4 changed files with 118 additions and 49 deletions
+43 -21
View File
@@ -1,56 +1,78 @@
# Stand-ins for the MARS agent cmdlets, shaped like the properties the collector reads.
# Property names are best guesses until checked against Get-MarsCollectorDiagnostics.ps1 output.
# Stand-ins for the MARS agent cmdlets. Object shapes follow Get-MarsCollectorDiagnostics.ps1
# output from a real agent (2.0.9274): job times are UTC values with no UTC marking, "not set" is
# 1601-01-01, ErrorInfo is always present (ErrorCode 0 = no error), Datasource is an object, and
# schedule times are TimeSpans.
#
# . ./tests/MockMars.ps1
# ./Send-MarsBackupStatus.ps1 -ConfigPath ./my-test-config.json -DryRun
# Fixed to the top of the hour, and IDs derived from times, so repeated runs see the same jobs
$now = Get-Date -Minute 0 -Second 0 -Millisecond 0
$notSet = [datetime]::new(1601, 1, 1)
function New-StableGuid([string]$Seed) {
[guid]::new([Security.Cryptography.MD5]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($Seed)))
}
function New-MockDatasource($Name, [long]$Done, [long]$Failed = 0, $ErrorInfo = $null) {
# Local time -> the UTC value MARS reports, with Kind Unspecified like the real objects
function ConvertTo-MarsTime([datetime]$Local) {
[datetime]::SpecifyKind($Local.ToUniversalTime(), [DateTimeKind]::Unspecified)
}
function New-MockErrorInfo([int]$Code = 0, $Params = @()) {
[pscustomobject]@{ DetailedErrorCode = 0; DetailedErrorSource = $null; ErrorCode = $Code; ErrorParamList = $Params }
}
function New-MockDatasource($Name, [uint64]$Done, [uint64]$FailedBytes = 0, [uint64]$FailedFiles = 0, $ErrorInfo = (New-MockErrorInfo)) {
[pscustomobject]@{
Datasource = $Name
ByteProgress = [pscustomobject]@{ Total = $Done + $Failed; Progress = $Done; Failed = $Failed }
ByteProgress = [pscustomobject]@{ Changed = $Done; Failed = $FailedBytes; Progress = $Done; Total = 2TB }
Datasource = [pscustomobject]@{ DataSourceName = $Name; DataSourceId = 6310158806455554328 }
ErrorInfo = $ErrorInfo
FileProgress = [pscustomobject]@{ Changed = 100; CurrentFile = ''; Failed = $FailedFiles; Progress = 100; Total = 100 }
JobState = 'Completed'
}
}
function New-MockJob($Type, $Start, $End, $State, $Datasources = @(), $ErrorInfo = $null) {
function New-MockJob($Type, [datetime]$Start, $End, $State, $Datasources = @(), $ErrorInfo = (New-MockErrorInfo), $FailedFileLog = '') {
$endValue = if ($End) { ConvertTo-MarsTime $End } else { $notSet }
[pscustomobject]@{
JobId = New-StableGuid "$Type|$($Start.Ticks)"
JobType = $Type
JobStatus = [pscustomobject]@{
JobState = $State; StartTime = $Start; EndTime = $End
DatasourceStatus = $Datasources; ErrorInfo = $ErrorInfo
IsRetried = $false
JobId = New-StableGuid "$Type|$($Start.Ticks)"
JobType = $Type
DisplayTime = $Start # the only local time on the object
JobStatus = [pscustomobject]@{
DatasourceStatus = $Datasources
EndTime = $endValue
ErrorInfo = $ErrorInfo
FailedFileLog = $FailedFileLog
JobState = $State
StartTime = ConvertTo-MarsTime $Start
}
}
}
$global:MockMarsRunning = @(
(New-MockJob 'Backup' $now.AddMinutes(-10) $null 'InProgress' @((New-MockDatasource 'D:\Shares' 2GB)))
(New-MockJob 'Backup' $now.AddMinutes(-10) $null 'InProgress' @((New-MockDatasource 'E:\' 2GB)))
)
$global:MockMarsPrevious = @(
(New-MockJob 'Backup' $now.AddHours(-12) $now.AddHours(-11) 'Completed' @(
(New-MockDatasource 'D:\Shares' 12GB), (New-MockDatasource 'System State' 8GB))),
(New-MockJob 'Backup' $now.AddHours(-12) $now.AddHours(-11.5) 'Completed' @((New-MockDatasource 'E:\' 523407872))),
# Completed, but some files failed
(New-MockJob 'Backup' $now.AddHours(-36) $now.AddHours(-35) 'Completed' @(
(New-MockDatasource 'D:\Shares' 3GB 40MB))), # completed, but some files failed
(New-MockDatasource 'E:\' 3GB 40MB 12)) -FailedFileLog 'C:\Program Files\Microsoft Azure Recovery Services Agent\Temp\Errors.log'),
(New-MockJob 'Backup' $now.AddHours(-30) $now.AddHours(-29.5) 'Failed' @() `
([pscustomobject]@{ ErrorCode = 100050; ErrorParamList = @([pscustomobject]@{ Name = 'Volume'; Value = 'D:' }) })),
(New-MockErrorInfo 100050 @([pscustomobject]@{ Name = 'Volume'; Value = 'E:' }))),
(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
)
# Daily at 21:00 and 03:00, weekdays only
$global:MockMarsPolicy = [pscustomobject]@{ PolicyName = 'Files' }
# Every day at 21:00 local, as on the real server
$global:MockMarsSchedule = [pscustomobject]@{
ScheduleRunDays = @([DayOfWeek]::Monday, [DayOfWeek]::Tuesday, [DayOfWeek]::Wednesday, [DayOfWeek]::Thursday, [DayOfWeek]::Friday)
ScheduleRunTimes = @($now.Date.AddHours(21), $now.Date.AddHours(3))
ScheduleRunDays = [DayOfWeek[]](0..6)
ScheduleRunTimes = [timespan[]]@([timespan]::FromHours(21))
ScheduleWeeklyFrequency = 1
TypeOfBackupSchedule = 'Daily'
}
function Get-OBJob { param([int]$Previous) if ($PSBoundParameters.ContainsKey('Previous')) { $global:MockMarsPrevious } else { $global:MockMarsRunning } }
function Get-OBPolicy { $global:MockMarsPolicy }
function Get-OBPolicy { [pscustomobject]@{ PolicyName = 'policy'; BackupSchedule = $global:MockMarsSchedule } }
function Get-OBSchedule { param($Policy) $global:MockMarsSchedule }
function Get-OBSystemStatePolicy { $null } # none configured, as on the real server