feat: Agent collects S.M.A.R.T. disk data via smartctl. Related to #907

- Add smartctl package to collect disk temperature and health data
- Add SMART field to agent Sensors struct
- Host agent now runs smartctl to collect disk temps when available
- Backend processes agent SMART data for temperature display
- Graceful fallback when smartctl not installed
This commit is contained in:
rcourtman
2025-12-25 11:37:53 +00:00
parent ea0fafd5c8
commit e1b3af7ca0
6 changed files with 354 additions and 12 deletions
+28 -4
View File
@@ -208,8 +208,29 @@ func convertHostSensorsToTemperature(sensors models.HostSensorSummary, lastSeen
}
}
// Convert S.M.A.R.T. data from host agent
if len(sensors.SMART) > 0 {
temp.SMART = make([]models.DiskTemp, 0, len(sensors.SMART))
for _, disk := range sensors.SMART {
// Skip disks in standby (no temperature data)
if disk.Standby {
continue
}
temp.SMART = append(temp.SMART, models.DiskTemp{
Device: "/dev/" + disk.Device,
Serial: disk.Serial,
WWN: disk.WWN,
Model: disk.Model,
Type: disk.Type,
Temperature: disk.Temperature,
LastUpdated: lastSeen,
})
}
temp.HasSMART = len(temp.SMART) > 0
}
// Validate we have at least some data
if !temp.HasCPU && !temp.HasGPU && !temp.HasNVMe {
if !temp.HasCPU && !temp.HasGPU && !temp.HasNVMe && !temp.HasSMART {
return nil
}
@@ -220,6 +241,7 @@ func convertHostSensorsToTemperature(sensors models.HostSensorSummary, lastSeen
Int("coreCount", len(temp.Cores)).
Int("nvmeCount", len(temp.NVMe)).
Int("gpuCount", len(temp.GPU)).
Int("smartCount", len(temp.SMART)).
Msg("Converted host agent sensors to temperature data")
return temp
@@ -258,7 +280,7 @@ func mergeTemperatureData(hostAgentTemp, proxyTemp *models.Temperature) *models.
HasCPU: hostAgentTemp.HasCPU,
HasGPU: hostAgentTemp.HasGPU,
HasNVMe: hostAgentTemp.HasNVMe,
HasSMART: proxyTemp.HasSMART, // SMART data only comes from proxy
HasSMART: hostAgentTemp.HasSMART || proxyTemp.HasSMART,
LastUpdate: hostAgentTemp.LastUpdate,
}
@@ -270,8 +292,10 @@ func mergeTemperatureData(hostAgentTemp, proxyTemp *models.Temperature) *models.
result.HasCPU = true
}
// Keep proxy SMART data (host agent doesn't have smartctl access currently)
if proxyTemp.HasSMART {
// Merge SMART data - prefer host agent if available, fall back to proxy
if hostAgentTemp.HasSMART {
result.SMART = hostAgentTemp.SMART
} else if proxyTemp.HasSMART {
result.SMART = proxyTemp.SMART
}
+22
View File
@@ -2226,6 +2226,7 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.
TemperatureCelsius: cloneStringFloatMap(report.Sensors.TemperatureCelsius),
FanRPM: cloneStringFloatMap(report.Sensors.FanRPM),
Additional: cloneStringFloatMap(report.Sensors.Additional),
SMART: convertAgentSMARTToModels(report.Sensors.SMART),
},
RAID: raid,
Ceph: cephData,
@@ -9605,6 +9606,27 @@ func isLegacyDockerAgent(agentType string) bool {
return agentType != "unified"
}
// convertAgentSMARTToModels converts agent report S.M.A.R.T. data to the models.HostDiskSMART format.
func convertAgentSMARTToModels(smart []agentshost.DiskSMART) []models.HostDiskSMART {
if len(smart) == 0 {
return nil
}
result := make([]models.HostDiskSMART, 0, len(smart))
for _, disk := range smart {
result = append(result, models.HostDiskSMART{
Device: disk.Device,
Model: disk.Model,
Serial: disk.Serial,
WWN: disk.WWN,
Type: disk.Type,
Temperature: disk.Temperature,
Health: disk.Health,
Standby: disk.Standby,
})
}
return result
}
// convertAgentCephToModels converts agent report Ceph data to the models.HostCephCluster format.
func convertAgentCephToModels(ceph *agentshost.CephCluster) *models.HostCephCluster {
if ceph == nil {