fix(collector): recover storage model for NVMe drives exposed as SCSI
An NVMe SSD passed through a RAID/HBA as /dev/sdX (not /dev/nvmeX) gets smartctl'd over the SCSI protocol, which reports its model in scsi_model_name/scsi_vendor/scsi_product instead of the ATA/NVMe model_name field. smartctlInfo only mapped model_name, so the drive's model was silently dropped from reanimator.json even though smartctl ran successfully and the serial number came through fine — caught via a support bundle where lsblk showed "SSSTC CA6-8D1024" for a drive but reanimator.json's storage entry had no model at all. Add scsi_model_name/scsi_vendor/scsi_product to smartctlInfo and fall back to them, in that order, when model_name is empty. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9add5616dd
commit
a09201c7ab
@@ -270,9 +270,16 @@ func mergeStorageDevice(existing, incoming lsblkDevice) lsblkDevice {
|
||||
|
||||
// smartctlInfo is the subset of smartctl -j -a output we care about.
|
||||
type smartctlInfo struct {
|
||||
ModelFamily string `json:"model_family"`
|
||||
ModelName string `json:"model_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
ModelFamily string `json:"model_family"`
|
||||
ModelName string `json:"model_name"`
|
||||
// ScsiModelName is set instead of ModelName when smartctl reports over the
|
||||
// SCSI protocol (device.protocol == "SCSI") — this includes NVMe drives
|
||||
// exposed through a RAID/HBA passthrough as /dev/sdX rather than
|
||||
// /dev/nvmeX, which report no ModelName at all.
|
||||
ScsiModelName string `json:"scsi_model_name"`
|
||||
ScsiVendor string `json:"scsi_vendor"`
|
||||
ScsiProduct string `json:"scsi_product"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
FirmwareVer string `json:"firmware_version"`
|
||||
RotationRate int `json:"rotation_rate"`
|
||||
Temperature struct {
|
||||
@@ -299,6 +306,21 @@ type smartctlInfo struct {
|
||||
PowerCycleCount int `json:"power_cycle_count"`
|
||||
}
|
||||
|
||||
// smartctlModelName picks the drive model out of a parsed smartctl -j -a
|
||||
// document. ATA/NVMe devices report it as model_name; devices smartctl talks
|
||||
// to over the SCSI protocol (including NVMe drives exposed through a
|
||||
// RAID/HBA passthrough as /dev/sdX) leave model_name empty and report
|
||||
// scsi_model_name, or failing that scsi_vendor/scsi_product, instead.
|
||||
func smartctlModelName(info smartctlInfo) string {
|
||||
if v := cleanDMIValue(info.ModelName); v != "" {
|
||||
return v
|
||||
}
|
||||
if v := cleanDMIValue(info.ScsiModelName); v != "" {
|
||||
return v
|
||||
}
|
||||
return cleanDMIValue(strings.TrimSpace(info.ScsiVendor + " " + info.ScsiProduct))
|
||||
}
|
||||
|
||||
func enrichWithSmartctl(dev lsblkDevice) schema.HardwareStorage {
|
||||
present := true
|
||||
s := schema.HardwareStorage{Present: &present}
|
||||
@@ -349,7 +371,7 @@ func enrichWithSmartctl(dev lsblkDevice) schema.HardwareStorage {
|
||||
var raw map[string]any
|
||||
_ = json.Unmarshal(out, &raw)
|
||||
if err := json.Unmarshal(out, &info); err == nil {
|
||||
if v := cleanDMIValue(info.ModelName); v != "" {
|
||||
if v := smartctlModelName(info); v != "" {
|
||||
s.Model = &v
|
||||
}
|
||||
if v := cleanDMIValue(info.SerialNumber); v != "" {
|
||||
|
||||
@@ -99,3 +99,51 @@ func TestApplySCSISmartctlTelemetryDoesNotOverwriteExistingValues(t *testing.T)
|
||||
t.Fatalf("life_used_pct=%v want 50", disk.LifeUsedPct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmartctlModelNamePrefersATAModelName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
info := smartctlInfo{
|
||||
ModelName: "Samsung SSD 970 EVO",
|
||||
ScsiModelName: "should not be used",
|
||||
}
|
||||
if got := smartctlModelName(info); got != "Samsung SSD 970 EVO" {
|
||||
t.Fatalf("model=%q want %q", got, "Samsung SSD 970 EVO")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmartctlModelNameFallsBackToScsiModelName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// NVMe drive exposed through a RAID/HBA passthrough as /dev/sdX:
|
||||
// smartctl reports it over the SCSI protocol, so model_name is empty
|
||||
// and the model only shows up as scsi_model_name.
|
||||
info := smartctlInfo{
|
||||
ScsiModelName: "NVMe SSSTC CA6-8D1024",
|
||||
ScsiVendor: "NVMe",
|
||||
ScsiProduct: "SSSTC CA6-8D1024",
|
||||
}
|
||||
if got := smartctlModelName(info); got != "NVMe SSSTC CA6-8D1024" {
|
||||
t.Fatalf("model=%q want %q", got, "NVMe SSSTC CA6-8D1024")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmartctlModelNameFallsBackToScsiVendorProduct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
info := smartctlInfo{
|
||||
ScsiVendor: "NVMe",
|
||||
ScsiProduct: "SSSTC CA6-8D1024",
|
||||
}
|
||||
if got := smartctlModelName(info); got != "NVMe SSSTC CA6-8D1024" {
|
||||
t.Fatalf("model=%q want %q", got, "NVMe SSSTC CA6-8D1024")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmartctlModelNameEmptyWhenNoFieldsSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := smartctlModelName(smartctlInfo{}); got != "" {
|
||||
t.Fatalf("model=%q want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user