feat(agent): collecte SMART via smartctl -j

This commit is contained in:
Gilles Soulier
2026-05-22 11:36:51 +02:00
parent b78788b931
commit b53456dad8
2 changed files with 134 additions and 4 deletions
+89 -4
View File
@@ -1,13 +1,98 @@
use serde::Deserialize;
use crate::payload::SmartMetrics;
pub fn is_available() -> bool {
false
#[derive(Deserialize)]
struct SmartJson {
smart_status: SmartStatus,
temperature: Option<SmartTemp>,
ata_smart_attributes: Option<SmartAttrs>,
nvme_smart_health_information_log: Option<NvmeHealth>,
}
pub fn parse_json(_: &str) -> Result<SmartMetrics, serde_json::Error> {
serde_json::from_str("null")
#[derive(Deserialize)]
struct SmartStatus { passed: bool }
#[derive(Deserialize)]
struct SmartTemp { current: i64 }
#[derive(Deserialize)]
struct SmartAttrs { table: Vec<SmartAttr> }
#[derive(Deserialize)]
struct SmartAttr {
id: u8,
raw: SmartAttrRaw,
value: Option<i64>,
}
#[derive(Deserialize)]
struct SmartAttrRaw { value: i64 }
#[derive(Deserialize)]
struct NvmeHealth {
percentage_used: Option<i64>,
temperature: Option<i64>,
}
pub fn is_available() -> bool {
std::process::Command::new("which")
.arg("smartctl")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn parse_json(json: &str) -> Result<SmartMetrics, serde_json::Error> {
let s: SmartJson = serde_json::from_str(json)?;
let temperature = s.temperature.as_ref().map(|t| t.current)
.or_else(|| s.nvme_smart_health_information_log.as_ref()?.temperature);
let mut reallocated = None;
let mut power_hours = None;
let mut wear = None;
if let Some(attrs) = &s.ata_smart_attributes {
for attr in &attrs.table {
match attr.id {
5 => reallocated = Some(attr.raw.value),
9 => power_hours = Some(attr.raw.value),
177 => wear = attr.value,
_ => {}
}
}
}
if wear.is_none() {
if let Some(nvme) = &s.nvme_smart_health_information_log {
if let Some(pct) = nvme.percentage_used {
wear = Some(100 - pct);
}
}
}
Ok(SmartMetrics {
passed: s.smart_status.passed,
temperature,
reallocated_sectors: reallocated,
power_on_hours: power_hours,
wear_level: wear,
})
}
pub fn collect() -> Option<SmartMetrics> {
if !is_available() {
return None;
}
for dev in &["/dev/sda", "/dev/nvme0"] {
let output = std::process::Command::new("smartctl")
.args(["-j", dev])
.output()
.ok()?;
let json = String::from_utf8_lossy(&output.stdout);
if let Ok(metrics) = parse_json(&json) {
return Some(metrics);
}
}
None
}
+45
View File
@@ -0,0 +1,45 @@
use nanometrics_agent::metrics::smart;
const SMART_JSON_OK: &str = r#"{
"smart_status": {"passed": true},
"temperature": {"current": 34},
"ata_smart_attributes": {
"table": [
{"id": 5, "name": "Reallocated_Sector_Ct", "raw": {"value": 0}, "value": 200},
{"id": 9, "name": "Power_On_Hours", "raw": {"value": 4213}, "value": 77},
{"id": 177, "name": "Wear_Leveling_Count", "raw": {"value": 0}, "value": 98}
]
}
}"#;
#[test]
fn test_parse_smart_json_ok() {
let result = smart::parse_json(SMART_JSON_OK).unwrap();
assert!(result.passed);
assert_eq!(result.temperature, Some(34));
assert_eq!(result.reallocated_sectors, Some(0));
assert_eq!(result.power_on_hours, Some(4213));
assert_eq!(result.wear_level, Some(98));
}
const SMART_JSON_FAIL: &str = r#"{
"smart_status": {"passed": false},
"temperature": {"current": 52},
"ata_smart_attributes": {
"table": [
{"id": 5, "name": "Reallocated_Sector_Ct", "raw": {"value": 47}, "value": 150}
]
}
}"#;
#[test]
fn test_parse_smart_json_fail() {
let result = smart::parse_json(SMART_JSON_FAIL).unwrap();
assert!(!result.passed);
assert_eq!(result.reallocated_sectors, Some(47));
}
#[test]
fn test_smart_disponible() {
let _ = smart::is_available();
}