Collect physical disk health from two sources
ATA SMART alone is not enough. MSStorageDriver_FailurePredictStatus returns zero instances on an NVMe machine, verified on a two-NVMe box where both drives were present and healthy, so an agent collecting only that class would report an empty disk health list on every modern endpoint and read as no problems. MSFT_PhysicalDisk is the only NVMe-aware source; Win32_DiskDrive.Status covers ATA/SATA and hosts where the Storage namespace is unavailable. Both are collected and neither is reconciled away, so a disagreement between them stays visible. MSFT_PhysicalDisk returns raw enum integers rather than the friendly strings Get-PhysicalDisk prints, so the mapping is done here to keep the wire self-describing. predictfailure is precomputed so an alert is a boolean test instead of a string match against a Windows enum; null means the drive did not answer, which is unknown rather than healthy.
This commit is contained in:
@@ -50,6 +50,13 @@ public sealed class EndpointReport
|
||||
// reports, and on a shop floor PC nobody reboots voluntarily.
|
||||
[JsonPropertyName("pendingreboot")] public bool? PendingReboot { get; set; }
|
||||
|
||||
// Physical drive health from two sources: MSFT_PhysicalDisk (the only one
|
||||
// that reports NVMe) and Win32_DiskDrive.Status (ATA/SATA and fallback).
|
||||
// Each entry carries a precomputed predictfailure so an alert is a boolean
|
||||
// test rather than a string match against an enum. Null predictfailure
|
||||
// means the drive did not answer - unknown, not healthy.
|
||||
[JsonPropertyName("diskhealth")] public object[]? DiskHealth { get; set; }
|
||||
|
||||
// Session-scoped data gathered by a probe running as the signed-in user:
|
||||
// mapped drives and user-owned listeners. Null when nobody is signed in or
|
||||
// the probe did not produce a result this cycle - absent means "not
|
||||
|
||||
@@ -66,6 +66,7 @@ public sealed class EndpointCollector : ICollector
|
||||
Interfaces = GetInterfaces(),
|
||||
Events = GetCuratedEvents(),
|
||||
Disks = GetDisks(),
|
||||
DiskHealth = GetDiskHealth(),
|
||||
Bios = GetBios(),
|
||||
Memory = GetMemory(),
|
||||
Processor = GetProcessor(),
|
||||
@@ -1019,6 +1020,129 @@ public sealed class EndpointCollector : ICollector
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
// Physical drive health, so a failing disk is caught before it takes the
|
||||
// machine and whatever was local on it.
|
||||
//
|
||||
// Two sources, because neither covers the fleet alone:
|
||||
//
|
||||
// MSFT_PhysicalDisk (Storage namespace) is the only one that reports NVMe.
|
||||
// The classic SMART class, MSStorageDriver_FailurePredictStatus, is ATA/SATA
|
||||
// only and returns NOTHING on an NVMe box - verified on a two-NVMe machine
|
||||
// here, where it yielded zero instances while the drives were present and
|
||||
// healthy. Collecting only that class would have reported an empty disk
|
||||
// health list on every modern endpoint and looked like "no problems".
|
||||
//
|
||||
// Win32_DiskDrive.Status is the old reliable string ("OK"/"Pred Fail") and
|
||||
// covers anything the Storage namespace does not, including machines where
|
||||
// that namespace is missing or blocked.
|
||||
//
|
||||
// Keyed by DeviceId so the server can pair the two views per drive the same
|
||||
// way mapped drives are paired.
|
||||
private object[] GetDiskHealth()
|
||||
{
|
||||
var result = new List<object>();
|
||||
|
||||
// MSFT_PhysicalDisk returns raw enum INTEGERS through
|
||||
// ManagementObjectSearcher, not the friendly strings Get-PhysicalDisk
|
||||
// prints. Verified on a live box: Health=0 Op=2 for a healthy NVMe.
|
||||
// Mapping them here keeps the wire self-describing; shipping bare
|
||||
// integers would make the server guess at an undocumented enum.
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(
|
||||
@"root\Microsoft\Windows\Storage",
|
||||
"SELECT DeviceId, FriendlyName, MediaType, BusType, " +
|
||||
"HealthStatus, OperationalStatus FROM MSFT_PhysicalDisk");
|
||||
foreach (var mo in searcher.Get())
|
||||
{
|
||||
var health = ToInt(mo["HealthStatus"]);
|
||||
result.Add(new
|
||||
{
|
||||
deviceid = mo["DeviceId"]?.ToString()?.Trim() ?? "",
|
||||
model = mo["FriendlyName"]?.ToString()?.Trim() ?? "",
|
||||
mediatype = PhysicalDiskMediaType(ToInt(mo["MediaType"])),
|
||||
bustype = PhysicalDiskBusType(ToInt(mo["BusType"])),
|
||||
healthstatus = PhysicalDiskHealth(health),
|
||||
// The actionable bit, precomputed so an alert is a direct
|
||||
// boolean test rather than a string comparison against an
|
||||
// enum the server would have to keep in sync with this file.
|
||||
// Null when health is unreadable: unknown is not healthy,
|
||||
// but it is not a confident failure either.
|
||||
predictfailure = health is null ? (bool?)null : health != 0,
|
||||
source = "physicaldisk",
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
logger.LogDebug(error, "Physical disk health query failed");
|
||||
}
|
||||
|
||||
// Win32_DiskDrive as the ATA/SATA and fallback view. Status is a plain
|
||||
// string here, no enum mapping needed.
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(
|
||||
"SELECT DeviceID, Model, Status FROM Win32_DiskDrive");
|
||||
foreach (var mo in searcher.Get())
|
||||
{
|
||||
var status = mo["Status"]?.ToString()?.Trim() ?? "";
|
||||
result.Add(new
|
||||
{
|
||||
deviceid = mo["DeviceID"]?.ToString()?.Trim() ?? "",
|
||||
model = mo["Model"]?.ToString()?.Trim() ?? "",
|
||||
mediatype = "",
|
||||
bustype = "",
|
||||
healthstatus = status,
|
||||
// "OK" is the only healthy value this class reports;
|
||||
// "Pred Fail" is the SMART trip. An empty status means the
|
||||
// drive did not answer, which is unknown rather than failed.
|
||||
predictfailure = status.Length == 0
|
||||
? (bool?)null
|
||||
: !status.Equals("OK", StringComparison.OrdinalIgnoreCase),
|
||||
source = "diskdrive",
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
logger.LogDebug(error, "Disk drive status query failed");
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
// MSFT_PhysicalDisk HealthStatus: 0 Healthy, 1 Warning, 2 Unhealthy.
|
||||
private static string PhysicalDiskHealth(int? value) => value switch
|
||||
{
|
||||
0 => "Healthy",
|
||||
1 => "Warning",
|
||||
2 => "Unhealthy",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
// MSFT_PhysicalDisk MediaType: 3 HDD, 4 SSD, 5 SCM.
|
||||
private static string PhysicalDiskMediaType(int? value) => value switch
|
||||
{
|
||||
3 => "HDD",
|
||||
4 => "SSD",
|
||||
5 => "SCM",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
// MSFT_PhysicalDisk BusType. Only the values that actually appear on an
|
||||
// endpoint are named; anything else stays numeric rather than guessed at.
|
||||
private static string PhysicalDiskBusType(int? value) => value switch
|
||||
{
|
||||
3 => "ATA",
|
||||
7 => "USB",
|
||||
8 => "RAID",
|
||||
9 => "iSCSI",
|
||||
11 => "SATA",
|
||||
17 => "NVMe",
|
||||
_ => value is null ? "Unknown" : value.Value.ToString(),
|
||||
};
|
||||
|
||||
// Firmware identity from Win32_BIOS: vendor, version string, and release
|
||||
// date. Lets the server track BIOS/UEFI levels for a fleet update campaign.
|
||||
private object? GetBios()
|
||||
@@ -1155,6 +1279,14 @@ public sealed class EndpointCollector : ICollector
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static int? ToInt(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
return null;
|
||||
try { return Convert.ToInt32(value); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// Win32 CIM datetimes (yyyymmddHHMMSS.ffffff+zzz) render as an ISO-8601 UTC
|
||||
// string; anything unparseable passes through untouched so nothing is lost.
|
||||
private string? ToBiosDate(string? raw)
|
||||
|
||||
Reference in New Issue
Block a user