Private/Kinds/DiskHealth.ps1
|
# The DiskHealth Kind: what the drive itself says about how much life it has left. # # The Storage Kind reports that a disk is Healthy, which is Windows repeating what the # driver told it. This reads the reliability counters underneath: wear, uncorrected read # errors, temperature and hours powered on. A solid-state drive at 85 % wear reports # Healthy right up until it does not, and this is the Check that gives a Technician the # weeks of notice that makes the difference between a replacement and a data recovery. # Declared for the Kind vocabulary: the reliability counters are not readable without # admin rights, so this Check belongs to the Elevated Part. See Private/Kind.ps1. $script:DiskHealthNeedsAdmin = $true function Get-DiskHealthData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $disks = @() try { $disks = @(Get-PhysicalDisk -ErrorAction Stop | ForEach-Object { $device = $_ $counter = $null $readable = $true try { $counter = $device | Get-StorageReliabilityCounter -ErrorAction Stop } catch { # Plenty of drives and controllers simply do not expose these counters, and # a Run without admin rights cannot read them at all. Which of the two this # was is the Judge's call, from the Privilege the Part carries. $readable = $false } [pscustomobject]@{ DeviceId = $device.DeviceId FriendlyName = "$($device.FriendlyName)" MediaType = "$($device.MediaType)" CountersReadable = $readable Wear = $counter.Wear TemperatureC = $counter.Temperature ReadErrorsUncorrected = $counter.ReadErrorsUncorrected PowerOnHours = $counter.PowerOnHours } }) } catch { } # Which drives were attached, and when: Windows writes one event per drive it sees, # with model and serial number, into a log only an administrator can read. The # Stability Check uses it to say which drive an error was about - one that has been # unplugged since is otherwise only "Datentraeger 1". $history = @(Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-Partition/Diagnostic'; Id = 1006 } ` -MaxEvents 500 -ErrorAction SilentlyContinue | ForEach-Object { $fields = @{} try { foreach ($node in ([xml]$_.ToXml()).Event.EventData.Data) { $fields["$($node.Name)"] = "$($node.'#text')" } } catch { } [pscustomobject]@{ Time = $_.TimeCreated DiskNumber = $fields['DiskNumber'] Manufacturer = "$($fields['Manufacturer'])".Trim() Model = "$($fields['Model'])".Trim() SerialNumber = "$($fields['SerialNumber'])".Trim() SizeBytes = $fields['Capacity'] } }) [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.DiskHealth' PhysicalDisks = $disks DiskHistory = $history # Which rights the readings were taken with: the difference between "this drive # does not report wear" and "nobody was allowed to ask". Elevated = (Get-CurrentPrivilege) -eq 'admin' } } function Get-DiskHealthValue { <# .SYNOPSIS What a drive reported about itself, and what it did not. Pure. .DESCRIPTION A drive reports some of its counters and not others: an NVMe drive behind one controller gives wear and temperature and no power-on hours. The Value used to be one sentence with a place for each, so a counter that was missing left a hole - "Nicht korrigierte Lesefehler | Betriebsstunden h" - which reads as a fault in the Report rather than as a drive that does not say. Each counter is now stated when it is there, and the ones that are not are named as not reported. A temperature of zero is a counter that is not there, not a drive at freezing. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Disk) $wear = ConvertTo-Number (Get-DataProperty $Disk 'Wear') $temp = ConvertTo-Number (Get-DataProperty $Disk 'TemperatureC') $errors = ConvertTo-Number (Get-DataProperty $Disk 'ReadErrorsUncorrected') $hours = ConvertTo-Number (Get-DataProperty $Disk 'PowerOnHours') if ($null -ne $temp -and $temp -le 0) { $temp = $null } $said = @() $missing = @() if ($null -ne $wear) { $said += Get-Text 'Value.DiskHealth.Wear' $wear } else { $missing += Get-Text 'Value.DiskHealth.Name.Wear' } if ($null -ne $temp) { $said += Get-Text 'Value.DiskHealth.Temperature' $temp } else { $missing += Get-Text 'Value.DiskHealth.Name.Temperature' } if ($null -ne $errors) { $said += Get-Text 'Value.DiskHealth.ReadErrors' $errors } else { $missing += Get-Text 'Value.DiskHealth.Name.ReadErrors' } if ($null -ne $hours) { $said += Get-Text 'Value.DiskHealth.PowerOnHours' $hours } else { $missing += Get-Text 'Value.DiskHealth.Name.PowerOnHours' } if ($missing.Count) { $said += Get-Text 'Value.DiskHealth.NotReported' ($missing -join ', ') } $said -join ' | ' } function ConvertTo-DiskHealthFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) $wearWarn = Get-Parameter $Parameters 'DiskWearWarnAbovePercent' 49 $wearFail = Get-Parameter $Parameters 'DiskWearFailAbovePercent' 79 $tempWarn = Get-Parameter $Parameters 'DiskTemperatureWarnAboveC' 60 $tempFail = Get-Parameter $Parameters 'DiskTemperatureFailAboveC' 70 $disks = Get-DataCollection $Data 'PhysicalDisks' if (-not $disks.Count) { return New-UnavailableFinding -Category Storage -Check (Get-Text 'Check.DiskHealth.DiskHealth') ` -Hint (Get-Text 'Hint.DiskHealth.NoPhysicalDiskCouldBe') } $elevated = [bool](Get-DataProperty $Data 'Elevated') foreach ($disk in $disks) { $check = (Get-Text 'Check.DiskHealth.DiskWearErrors') -f $disk.DeviceId if (-not $disk.CountersReadable) { $why = $(if ($elevated) { (Get-Text 'Value.DiskHealth.NotSupported') } else { (Get-Text 'Value.Shared.NeedsAdmin') }) # What this drive says of itself was not read: nothing here is "in order". New-Finding -Category Storage -Check $check -Severity INFO -Value $why ` -Hint $(if ($elevated) { Get-Text 'Hint.DiskHealth.UseVendorTool' } else { Get-Text 'Hint.DiskHealth.RerunWithAdmin' }) ` -Unobserved 'drive-health-warning' -Subject "$($disk.DeviceId)" continue } $wear = ConvertTo-Number $disk.Wear $errors = ConvertTo-Number $disk.ReadErrorsUncorrected $temp = ConvertTo-Number $disk.TemperatureC # Wear and uncorrected read errors both end in the same place. A single # uncorrected read error is data the drive could not give back, so it fails on its # own however young the drive is. $wearSeverity = 'OK' if ($null -ne $wear) { $wearSeverity = Get-Severity $wear $wearWarn $wearFail } $errorSeverity = 'OK' if ($null -ne $errors -and $errors -gt 0) { $errorSeverity = 'FAIL' } $tempSeverity = 'OK' if ($null -ne $temp -and $temp -gt 0) { $tempSeverity = Get-Severity $temp $tempWarn $tempFail } $severity = Get-WorstSeverity $wearSeverity $errorSeverity $tempSeverity $signal = @() if ($severity -in 'WARN', 'FAIL') { $signal = @('drive-health-warning') } New-Finding -Category Storage -Check $check -Severity $severity ` -Value (Get-DiskHealthValue -Disk $disk) ` -Hint (Get-Text 'Hint.DiskHealth.SSDWornOutOrReporting') ` -Signal $signal -Subject "$($disk.DeviceId)" -Reference @(New-Reference -Signal 'disk-errors') } } |