Private/Data.ps1
|
# Reading gathered data. # # Judges are handed whatever a Gatherer produced on a real machine, and a real machine # produces absent counters, unreadable devices and empty collections. These helpers let a # Judge ask for a value without first asking whether it is there, so a Judge's logic # stays about thresholds rather than about null. function Get-DataProperty { <# .SYNOPSIS Reads a property off gathered data, returning $null when it is not there. .DESCRIPTION Gathered data reaches a Judge either as the object a Gatherer built or, for the Elevated Part, as its CliXML round trip. Neither is guaranteed to carry every property: a counter class that would not load leaves its properties absent altogether rather than null. #> [CmdletBinding()] param( [AllowNull()]$InputObject, [Parameter(Mandatory)][string]$Name ) if ($null -eq $InputObject) { return $null } if ($InputObject -is [hashtable]) { if ($InputObject.ContainsKey($Name)) { return $InputObject[$Name] } return $null } $property = $InputObject.PSObject.Properties[$Name] if ($property) { return $property.Value } $null } function ConvertTo-Number { <# .SYNOPSIS Returns the value as a double, or $null when it is not a number. .DESCRIPTION Zero is a number and must survive; an empty string, a null and a counter that reported text are not, and a Judge must be able to tell those apart from a genuine reading of zero. #> [CmdletBinding()] [OutputType([double])] param([AllowNull()]$Value) if ($null -eq $Value) { return $null } # A bool is not a reading, and neither is a date. if ($Value -is [bool] -or $Value -is [datetime]) { return $null } # Already a number: convert it as one, never through its text. PowerShell keeps the # source text of a suffixed numeric literal, so a value holding 2000GB can stringify # to "2000GB", which parses as nothing - and a Judge reaching for that text would # report a Check unavailable on a machine that answered perfectly well. ( -is unwraps # a PSObject on its own, so a wrapped number lands here too.) if ($Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or $Value -is [uint16] -or $Value -is [int32] -or $Value -is [uint32] -or $Value -is [int64] -or $Value -is [uint64] -or $Value -is [single] -or $Value -is [double] -or $Value -is [decimal]) { return [double]$Value } $number = 0.0 if ([double]::TryParse( [string]$Value, [Globalization.NumberStyles]::Float, [Globalization.CultureInfo]::InvariantCulture, [ref]$number)) { return $number } $null } function Get-SampleAverage { <# .SYNOPSIS The average of the readings in a sample window, or $null when none is a number. .DESCRIPTION Several Checks average a counter across a window rather than trusting one reading. A real machine returns windows with holes in them - a counter that would not load leaves a null, an unreadable one leaves text - so the readable readings are judged and the rest ignored, and a window with nothing readable in it is not a zero. #> [CmdletBinding()] param([AllowNull()][AllowEmptyCollection()]$Sample) $readings = @(@($Sample) | ForEach-Object { ConvertTo-Number $_ } | Where-Object { $null -ne $_ }) if (-not $readings.Count) { return $null } ($readings | Measure-Object -Average).Average } function Get-Severity { <# .SYNOPSIS The Severity a value earns against a WARN and a FAIL threshold. .DESCRIPTION Thresholds are exclusive: a value sitting exactly on the WARN threshold is still OK, which is what the Gutcheck script has always done and what the Report's documented boundaries mean. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)][double]$Value, [Parameter(Mandatory)][double]$WarnAbove, [Parameter(Mandatory)][double]$FailAbove ) if ($Value -gt $FailAbove) { return 'FAIL' } if ($Value -gt $WarnAbove) { return 'WARN' } 'OK' } function Get-SeverityBelow { <# .SYNOPSIS The Severity a value earns when less of it is worse. .DESCRIPTION Installed memory and a processor's performance limit are graded downwards: it is the small number that is the problem. Thresholds stay exclusive in the same sense as Get-Severity, so a value sitting exactly on the WARN threshold is still OK. A Check that only ever warned in the Gutcheck script keeps only its WARN threshold, and its FAIL threshold defaults to a value no reading can go below - still a named parameter a Customer could set, but inert until they do. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)][double]$Value, [Parameter(Mandatory)][double]$WarnBelow, [Parameter(Mandatory)][double]$FailBelow ) if ($Value -lt $FailBelow) { return 'FAIL' } if ($Value -lt $WarnBelow) { return 'WARN' } 'OK' } function Get-SamplePercentile { <# .SYNOPSIS The reading at the given percentile of a sample window, or $null when none is a number. .DESCRIPTION An average hides the tail, and the tail is what a Technician feels: a disk whose typical write takes 0.2 ms and whose worst takes 90 ms is a disk that stutters. Unreadable readings are ignored the same way Get-SampleAverage ignores them, so a window with holes in it still yields a percentile rather than nothing. Nearest-rank, which is what a percentile over a few hundred readings should be: the value returned is one that was actually measured rather than an interpolation between two that were. #> [CmdletBinding()] param( [AllowNull()][AllowEmptyCollection()]$Sample, [Parameter(Mandatory)][ValidateRange(0, 100)][double]$Percentile ) $readings = @(@($Sample) | ForEach-Object { ConvertTo-Number $_ } | Where-Object { $null -ne $_ } | Sort-Object) if (-not $readings.Count) { return $null } $rank = [int][math]::Ceiling($Percentile / 100 * $readings.Count) if ($rank -lt 1) { $rank = 1 } if ($rank -gt $readings.Count) { $rank = $readings.Count } $readings[$rank - 1] } function Get-WorstSeverity { <# .SYNOPSIS The worst of the Severities handed to it, or OK when none was. .DESCRIPTION Several Checks grade one Finding against two independent measurements - a link that loses packets and a link that is slow are both bad links, and a gateway doing both is not excused by either. Whichever reads worse wins, which is how the Gutcheck script resolved the same disagreement. #> [CmdletBinding()] [OutputType([string])] param([Parameter(ValueFromRemainingArguments)][AllowNull()][AllowEmptyCollection()]$Severity) $worst = 'OK' $rank = $script:SeverityValues.IndexOf('OK') foreach ($value in @($Severity | Where-Object { $_ })) { $index = $script:SeverityValues.IndexOf([string]$value) # $script:SeverityValues is ordered worst first, so a lower index is worse. An # unrecognised Severity is ignored rather than allowed to rank as the worst. if ($index -ge 0 -and $index -lt $rank) { $rank = $index; $worst = [string]$value } } $worst } function Get-DataCollection { <# .SYNOPSIS A property that should hold a list, as a real array with the holes taken out. .DESCRIPTION @($null) has one element, not none. So a Judge writing @(Get-DataProperty $Data 'Crashes').Count against data whose Crashes property is absent - a Gatherer that could not read that log, or a CliXML round trip that dropped an empty collection - counts one crash that never happened, and reports a machine as unstable because a Check failed to run. Every list a Judge reads comes through here. #> [CmdletBinding()] [OutputType([object[]])] param( [AllowNull()]$InputObject, [Parameter(Mandatory)][string]$Name ) , @(@(Get-DataProperty -InputObject $InputObject -Name $Name) | Where-Object { $null -ne $_ }) } |