From b53456dad8c3cc82ca05e6f368679d415071d171 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Fri, 22 May 2026 11:36:51 +0200 Subject: [PATCH] feat(agent): collecte SMART via smartctl -j --- agent/src/metrics/smart.rs | 93 ++++++++++++++++++++++++++++++++++++-- agent/tests/smart_test.rs | 45 ++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 agent/tests/smart_test.rs diff --git a/agent/src/metrics/smart.rs b/agent/src/metrics/smart.rs index da46d1e..e6e7fad 100644 --- a/agent/src/metrics/smart.rs +++ b/agent/src/metrics/smart.rs @@ -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, + ata_smart_attributes: Option, + nvme_smart_health_information_log: Option, } -pub fn parse_json(_: &str) -> Result { - serde_json::from_str("null") +#[derive(Deserialize)] +struct SmartStatus { passed: bool } + +#[derive(Deserialize)] +struct SmartTemp { current: i64 } + +#[derive(Deserialize)] +struct SmartAttrs { table: Vec } + +#[derive(Deserialize)] +struct SmartAttr { + id: u8, + raw: SmartAttrRaw, + value: Option, +} + +#[derive(Deserialize)] +struct SmartAttrRaw { value: i64 } + +#[derive(Deserialize)] +struct NvmeHealth { + percentage_used: Option, + temperature: Option, +} + +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 { + 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 { + 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 } diff --git a/agent/tests/smart_test.rs b/agent/tests/smart_test.rs new file mode 100644 index 0000000..cfcfbf4 --- /dev/null +++ b/agent/tests/smart_test.rs @@ -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(); +}