Private/Kinds/Server.ps1
|
# The Server Kind: whether the machines an application depends on can be reached. # # The first Kind whose parameters are genuinely per-Customer. Every other Kind asks the # same questions of every machine; this one cannot be performed at all without knowing # which Servers matter, and those differ per Customer by definition. It is therefore also # the first real exercise of a Check Definition configuring rather than merely tuning. # # The same Kind serves an application's Servers and Servers a Technician supplied directly. # There is no difference between them worth a second implementation: a Server is a host, # optionally with ports, and the questions are the same either way. # How many times each port is tried and how many pings are sent. Not Check Definition # parameters, for the reason Private/Sampling.ps1 gives: a Customer may disagree about a # threshold, not about how many attempts make a measurement. One connect is not evidence - # a firewall that drops one connection in five is exactly what makes an application feel # unreliable, and a single successful probe would report it healthy. $script:ServerTcpAttempts = 5 $script:ServerPingCount = 20 function ConvertTo-ServerTarget { <# .SYNOPSIS Parses one Server entry into a host and its ports. Pure, and reaches nothing. .DESCRIPTION A Server is written as "host", "host:port" or "host:port,port". Parsing is separate from probing so that what Gutcheck understood from a Check Definition can be tested without a network, which matters because a mistyped entry in the Checks Repo would otherwise surface as a Customer's server being unreachable. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyString()][string]$Entry) if (-not $Entry -or -not "$Entry".Trim()) { return } $parts = "$Entry".Trim() -split ':', 2 $hostName = $parts[0].Trim() if (-not $hostName) { return } $ports = @() if ($parts.Count -gt 1) { $ports = @($parts[1] -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ } | Where-Object { $_ -ge 1 -and $_ -le 65535 }) } [pscustomobject]@{ PSTypeName = 'Gutcheck.ServerTarget' Entry = "$Entry".Trim() HostName = $hostName Ports = $ports # An address literal has nothing to resolve, and reporting DNS for one would be # reporting on a lookup that never happened. IsIpLiteral = $hostName -match '^\d{1,3}(\.\d{1,3}){3}$' } } function Get-ServerData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $entries = @(Get-Parameter $Parameters 'Servers' @()) $servers = @(foreach ($entry in $entries) { $target = ConvertTo-ServerTarget -Entry $entry if (-not $target) { continue } $resolution = $null if (-not $target.IsIpLiteral) { $resolution = Resolve-HostAddress -HostName $target.HostName } # A host that does not resolve cannot be connected to or pinged by name, so the # remaining probes would only produce noise about a name that does not exist. $resolved = $target.IsIpLiteral -or $resolution.Resolved $portResults = @() $latency = $null if ($resolved) { $portResults = @(foreach ($port in $target.Ports) { $times = @(1..$script:ServerTcpAttempts | ForEach-Object { $ms = Measure-TcpConnect -HostName $target.HostName -Port $port Start-Sleep -Milliseconds 200 $ms }) $ok = @($times | Where-Object { $null -ne $_ }) [pscustomobject]@{ Port = $port Attempts = $script:ServerTcpAttempts Successes = $ok.Count AverageMs = $(if ($ok.Count) { [math]::Round(($ok | Measure-Object -Average).Average, 1) } else { $null }) MaximumMs = $(if ($ok.Count) { ($ok | Measure-Object -Maximum).Maximum } else { $null }) } }) $latency = Measure-Latency -Target $target.HostName -Count $script:ServerPingCount } [pscustomobject]@{ Entry = $target.Entry HostName = $target.HostName Ports = $target.Ports IsIpLiteral = $target.IsIpLiteral Resolution = $resolution PortResults = $portResults Latency = $latency } }) [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Server' Servers = $servers } } function ConvertTo-ServerFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) $servers = Get-DataCollection $Data 'Servers' if (-not $servers.Count) { # A Check Definition that names a Kind but supplies it nothing has not been # written yet, and a Report that simply omits it lets that go unnoticed. return New-Finding -Category Network -Check (Get-Text 'Check.Server.Servers') -Severity INFO -Value (Get-Text 'Value.Server.NoneConfigured') ` -Hint (Get-Text 'Hint.Server.ThisCheckDefinitionNamesNo') } foreach ($server in $servers) { New-ServerDnsFinding -Server $server -Parameters $Parameters # Everything below needs a name that resolved. Asking anything else of a host that # does not exist produces Findings about a typo, dressed as Findings about a # network - which is how a Technician ends up looking at a firewall for an hour. if (-not (Test-ServerResolved -Server $server)) { continue } New-ServerPortFinding -Server $server -Parameters $Parameters New-ServerPingFinding -Server $server -Parameters $Parameters } } function ConvertTo-ServerSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $latency = @((Get-DataCollection $Data 'Servers') | ForEach-Object { $_.Latency } | Where-Object { $_ }) if (-not $latency.Count) { return } New-Section -Title (Get-Text 'Title.Server.LatencyTestsServers') -Row @( $latency | ForEach-Object { $_ | Select-Object Target, Sent, Lost, LossPercent, AverageMs, MaximumMs } ) } function Test-ServerResolved { <# .SYNOPSIS Whether this Server has a name that can be reached at all. #> [CmdletBinding()] [OutputType([bool])] param([Parameter(Mandatory)]$Server) if (Get-DataProperty $Server 'IsIpLiteral') { return $true } [bool](Get-DataProperty (Get-DataProperty $Server 'Resolution') 'Resolved') } function New-ServerDnsFinding { [CmdletBinding()] param([Parameter(Mandatory)]$Server, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'ServerDnsWarnMs' 500 $fail = Get-Parameter $Parameters 'ServerDnsFailMs' ([double]::MaxValue) # Nothing to resolve, so nothing to report. An address literal is not a DNS success. if (Get-DataProperty $Server 'IsIpLiteral') { return } $hostName = Get-DataProperty $Server 'HostName' $resolution = Get-DataProperty $Server 'Resolution' if (-not (Get-DataProperty $resolution 'Resolved')) { return New-Finding -Category Network -Check ((Get-Text 'Check.Server.Dns') -f $hostName) -Severity FAIL ` -Value (Get-Text 'Value.Server.CannotBeResolved') ` -Hint (Get-Text 'Hint.Server.WrongNameDNSProblemOr') } $ms = ConvertTo-Number (Get-DataProperty $resolution 'Milliseconds') if ($null -eq $ms) { return New-UnavailableFinding -Category Network -Check ((Get-Text 'Check.Server.Dns') -f $hostName) ` -Hint (Get-Text 'Hint.Shared.NameResolvedNotTimed') } New-Finding -Category Network -Check ((Get-Text 'Check.Server.Dns') -f $hostName) ` -Severity (Get-Severity $ms $warn $fail) -Value ('{0:N0} ms' -f $ms) ` -Hint (Get-Text 'Hint.Server.SlowNameResolutionDelaysEvery') } function New-ServerPortFinding { [CmdletBinding()] param([Parameter(Mandatory)]$Server, [hashtable]$Parameters) $latencyWarn = Get-Parameter $Parameters 'ServerTcpWarnMs' 150 $latencyFail = Get-Parameter $Parameters 'ServerTcpFailMs' ([double]::MaxValue) $hostName = Get-DataProperty $Server 'HostName' foreach ($port in (Get-DataCollection $Server 'PortResults')) { $check = 'TCP {0}:{1}' -f $hostName, $port.Port $successes = ConvertTo-Number $port.Successes $attempts = ConvertTo-Number $port.Attempts if ($null -eq $attempts -or $attempts -le 0) { $attempts = $script:ServerTcpAttempts } if (-not $successes) { # Refused outright: a distinct Finding from a port that answers slowly or # intermittently, because the thing to go and look at is different. New-Finding -Category Network -Check $check -Severity FAIL ` -Value ((Get-Text 'Value.Server.NoConnection') -f $attempts) ` -Hint (Get-Text 'Hint.Server.NothingIsListeningOrA') continue } $average = ConvertTo-Number $port.AverageMs # Two independent ways for a port to be bad: some attempts failed, or the ones that # succeeded were slow. Whichever reads worse decides. $missed = 'OK' if ($successes -lt $attempts) { $missed = 'WARN' } $slow = 'OK' if ($null -ne $average) { $slow = Get-Severity $average $latencyWarn $latencyFail } New-Finding -Category Network -Check $check -Severity (Get-WorstSeverity $missed $slow) ` -Value ((Get-Text 'Value.Server.ConnectResult') -f $average, $port.MaximumMs, $successes, $attempts) ` -Hint (Get-Text 'Hint.Server.FailedOrSlowConnectsFirewall') } } function New-ServerPingFinding { [CmdletBinding()] param([Parameter(Mandatory)]$Server, [hashtable]$Parameters) $lossWarn = Get-Parameter $Parameters 'ServerLossWarnPercent' 0 $lossFail = Get-Parameter $Parameters 'ServerLossFailPercent' 2 $msWarn = Get-Parameter $Parameters 'ServerLatencyWarnMs' 50 $msFail = Get-Parameter $Parameters 'ServerLatencyFailMs' ([double]::MaxValue) $hostName = Get-DataProperty $Server 'HostName' $check = 'Ping {0}' -f $hostName $latency = Get-DataProperty $Server 'Latency' if (-not $latency) { return } $loss = ConvertTo-Number (Get-DataProperty $latency 'LossPercent') $average = ConvertTo-Number (Get-DataProperty $latency 'AverageMs') $maximum = ConvertTo-Number (Get-DataProperty $latency 'MaximumMs') if ($null -eq $average) { # No reply at all. Whether that matters depends on whether anything else got # through: plenty of Customer networks block ICMP to servers that work perfectly. $ports = Get-DataCollection $Server 'PortResults' $anyPortOk = @($ports | Where-Object { (ConvertTo-Number $_.Successes) -gt 0 }).Count -gt 0 if ($ports.Count -and $anyPortOk) { return New-Finding -Category Network -Check $check -Severity INFO ` -Value (Get-Text 'Value.Server.NoIcmpReply') } return New-Finding -Category Network -Check $check -Severity FAIL -Value (Get-Text 'Value.Server.NoReply') ` -Hint (Get-Text 'Hint.Server.HostUnreachableOrPingBlocked') } $severity = Get-WorstSeverity (Get-Severity $loss $lossWarn $lossFail) (Get-Severity $average $msWarn $msFail) New-Finding -Category Network -Check $check -Severity $severity ` -Value ((Get-Text 'Value.Server.PingResult') -f $average, $maximum, $loss) ` -Hint (Get-Text 'Hint.Server.PacketLossMakesClientServer') } |