Private/Kinds/Storage.ps1
|
# The Storage Kind: what the machine stores things on, and whether there is room. # # The reliability counters behind a solid-state drive's wear need admin rights and ship # as their own Kind in the Elevated Part. What is here is what a Technician can see # without elevating: the device, its media, its free space, and whether it is thrashing. $script:StorageSampleCount = 3 function Get-StorageData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $disks = @() try { $disks = @(Get-PhysicalDisk -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ DeviceId = $_.DeviceId FriendlyName = $_.FriendlyName MediaType = "$($_.MediaType)" BusType = "$($_.BusType)" SizeBytes = $_.Size HealthStatus = "$($_.HealthStatus)" } }) } catch { } $systemDrive = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'" -ErrorAction SilentlyContinue $filter = "Name='_Total'" # The first read of a formatted-performance class is primed with zeroes; discard it. [void](Get-CimInstance Win32_PerfFormattedData_PerfDisk_PhysicalDisk -Filter $filter -ErrorAction SilentlyContinue) $samples = @(1..$script:StorageSampleCount | ForEach-Object { Write-CheckProgress -Status ((Get-Text 'Console.Progress.Sample') -f $_, $script:StorageSampleCount) ` -Step ($_ - 1) -Of $script:StorageSampleCount Wait-Sample Get-CimInstance Win32_PerfFormattedData_PerfDisk_PhysicalDisk -Filter $filter -ErrorAction SilentlyContinue }) [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Storage' PhysicalDisks = $disks SystemDriveLetter = $env:SystemDrive SystemDriveFreeBytes = $systemDrive.FreeSpace SystemDriveSizeBytes = $systemDrive.Size # The limits this Check was given, for a later Check that asks whether the drive # is short of space: see Get-StorageFreeSpace. FreeSpaceLimit = Get-StorageFreeSpaceLimit -Parameters $Parameters IdleTimeSamples = @($samples | ForEach-Object { $_.PercentIdleTime }) } } function ConvertTo-StorageFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{}, [AllowNull()]$Situation ) New-PhysicalDiskFinding -Data $Data -Parameters $Parameters -Situation $Situation New-FreeSpaceFinding -Data $Data -Parameters $Parameters New-DiskBusyFinding -Data $Data -Parameters $Parameters } function Test-StorageVirtualDisk { <# .SYNOPSIS Whether a disk is a virtual one: by what it is called and how it is attached, or because the machine is a virtual machine. Pure. .DESCRIPTION A hypervisor presents a file on its own storage as a disk, and what that disk calls itself - "HDD" as a rule - says nothing about what it lies on. Whether it is fast, and whether it is failing, the host knows and Windows does not. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$Disk, [AllowNull()]$Situation) $machine = Select-SituationText -Situation $Situation -Fact Form -Text @{ Virtual = 'yes'; Unknown = '' } if ($machine) { return $true } if ("$(Get-DataProperty $Disk 'BusType')" -match '(?i)Virtual|Spaces') { return $true } "$(Get-DataProperty $Disk 'FriendlyName')" -match '(?i)\bVirtual\b|VMware|\bQEMU\b|\bVBOX\b|\bXen\b|Msft' } function New-PhysicalDiskFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters, [AllowNull()]$Situation) $disks = (Get-DataCollection $Data 'PhysicalDisks') if (-not $disks.Count) { # Named so it cannot be mistaken for a device: the per-device Findings below are # "Disk 0", "Disk 1". The script abandoned the whole Check here and lost free # space and disk-busy with it; a Check that could not run belongs in the Report. return New-UnavailableFinding -Category Storage -Check (Get-Text 'Check.Storage.PhysicalDisks') ` -Hint (Get-Text 'Hint.Storage.NoPhysicalDiskCouldBe') } # One Finding per device: a machine with a healthy SSD and a failing HDD is not # summarised honestly by either one of them. foreach ($disk in $disks) { $virtual = Test-StorageVirtualDisk -Disk $disk -Situation $Situation $severity = 'OK' $meaning = '' $hint = '' # Two different things, and they were one sentence: a disk that is failing, and a # disk that is merely slow by construction. And a virtual disk is neither for # calling itself an HDD. if ("$($disk.HealthStatus)" -ne 'Healthy') { $severity = 'FAIL' $meaning = $(if ($virtual) { Get-Text 'Meaning.Storage.VirtualUnhealthy' } else { Get-Text 'Meaning.Storage.Unhealthy' }) $hint = $(if ($virtual) { Get-Text 'Hint.Storage.VirtualUnhealthy' } else { Get-Text 'Hint.Storage.Unhealthy' }) } elseif ("$($disk.MediaType)" -eq 'HDD' -and -not $virtual) { $severity = 'FAIL' $meaning = Get-Text 'Meaning.Storage.Hdd' $hint = Get-Text 'Hint.Storage.Hdd' } New-Finding -Category Storage -Check ((Get-Text 'Check.Storage.Disk') -f $disk.DeviceId) -Severity $severity ` -Value ('{0} | {1} | {2} | {3} GB | Health: {4}' -f $disk.FriendlyName, $disk.MediaType, $disk.BusType, [math]::Round(($disk.SizeBytes / 1GB)), $disk.HealthStatus) ` -Meaning $meaning -Hint $hint -Subject "$($disk.DeviceId)" } } function Get-StorageFreeSpaceLimit { <# .SYNOPSIS The three limits free space on the system drive is judged by, as a Check Definition gives them or as Gutcheck ships them. Pure. .DESCRIPTION Named as the parameters are, so that what comes back reads as parameters again. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][hashtable]$Parameters) [pscustomobject]@{ FreeSpaceFailBelowGB = Get-Parameter $Parameters 'FreeSpaceFailBelowGB' 15 FreeSpaceFailBelowPercent = Get-Parameter $Parameters 'FreeSpaceFailBelowPercent' 10 FreeSpaceWarnBelowPercent = Get-Parameter $Parameters 'FreeSpaceWarnBelowPercent' 20 } } function Get-StorageFreeSpace { <# .SYNOPSIS How much of the system drive is free, and what Severity that earns. Nothing where the drive could not be read. Pure. .DESCRIPTION The one place that says whether the system drive is short of space. The Storage Judge asks it with the parameters of its Check Definition. A Check that is handed what the Storage Check gathered asks it without any, and is then answered by the limits the Storage Check was given, which it handed on with what it gathered - so that a Customer who set its own limits is not judged by two. .PARAMETER Parameters The parameters of the Storage Check. Left out: those in the data, and where the data holds none, what Gutcheck ships. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [AllowNull()][hashtable]$Parameters) if ($null -eq $Parameters) { $Parameters = ConvertTo-ParameterHashtable (Get-DataProperty $Data 'FreeSpaceLimit') } $limit = Get-StorageFreeSpaceLimit -Parameters $Parameters $free = ConvertTo-Number (Get-DataProperty $Data 'SystemDriveFreeBytes') $size = ConvertTo-Number (Get-DataProperty $Data 'SystemDriveSizeBytes') if ($null -eq $free -or $null -eq $size -or $size -le 0) { return $null } $freeGB = $free / 1GB $freePercent = $free / $size * 100 # Two thresholds that disagree are resolved the way the script resolved them: whichever # is worse wins. 12 GB free on a 2 TB disk is a comfortable-looking 0.6 % and still # fatal, and 9 % of a 1 TB disk is 90 comfortable-looking gigabytes and still fatal. $severity = Get-SeverityBelow $freePercent $limit.FreeSpaceWarnBelowPercent $limit.FreeSpaceFailBelowPercent if ($freeGB -lt $limit.FreeSpaceFailBelowGB) { $severity = 'FAIL' } [pscustomobject]@{ Drive = "$(Get-DataProperty $Data 'SystemDriveLetter')" FreeGB = $freeGB FreePercent = $freePercent Severity = $severity } } function New-FreeSpaceFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) if ($null -eq $Parameters) { $Parameters = @{} } $drive = Get-DataProperty $Data 'SystemDriveLetter' $space = Get-StorageFreeSpace -Data $Data -Parameters $Parameters if ($null -eq $space) { return New-UnavailableFinding -Category Storage -Check ((Get-Text 'Check.Storage.FreeSpace') -f $drive) ` -Hint (Get-Text 'Hint.Storage.TheSystemDriveDidNot') } # Declared where the drive is short of space and not where it has room: what a # Finding about something that takes room refers to. $signal = @() if ($space.Severity -in 'WARN', 'FAIL') { $signal = @('system-drive-low') } New-Finding -Category Storage -Check ((Get-Text 'Check.Storage.FreeSpace') -f $drive) -Severity $space.Severity ` -Value ('{0:N1} GB ({1:N0} %)' -f $space.FreeGB, $space.FreePercent) ` -Hint (Get-Text 'Hint.Storage.FullSSDsGetSlowUpdates') -Signal $signal } function New-DiskBusyFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'DiskBusyAtRestWarnPercent' 40 $fail = Get-Parameter $Parameters 'DiskBusyAtRestFailPercent' 80 $idle = Get-SampleAverage (Get-DataProperty $Data 'IdleTimeSamples') if ($null -eq $idle) { return New-UnavailableFinding -Category Storage -Check (Get-Text 'Check.Storage.DiskBusyAtRest') ` -Hint (Get-Text 'Hint.Storage.TheDiskPerformanceCountersDid') } $busy = 100 - $idle New-Finding -Category Storage -Check (Get-Text 'Check.Storage.DiskBusyAtRest') -Severity (Get-Severity $busy $warn $fail) ` -Value ('{0:N0} %' -f $busy) ` -Hint (Get-Text 'Hint.Storage.DiskPermanentlyBusyWhileIdle') } |