feat(agent): collecte SMART via smartctl -j
This commit is contained in:
@@ -1,13 +1,98 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
use crate::payload::SmartMetrics;
|
use crate::payload::SmartMetrics;
|
||||||
|
|
||||||
pub fn is_available() -> bool {
|
#[derive(Deserialize)]
|
||||||
false
|
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> {
|
#[derive(Deserialize)]
|
||||||
serde_json::from_str("null")
|
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> {
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user