feat(agent v0.1.6): métriques réseau enrichies + hardware dmidecode
- Nouveaux types payload: NetworkInterface, HardwareInfo - Config: slow_daily_time (HH:MM), network_info, hardware_info - Module network_info: interfaces locales, type ETH/WIFI, speed, MAC, WoL, iperf3 - Module hardware: dmidecode (carte mère, CPU, slots RAM, type/vitesse) - Scheduler: collecte au démarrage + 1×/jour à l'heure configurée - install.sh: ajout iperf3, dmidecode dans paquets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
49626ddb9e
commit
0430c0f2a8
@@ -0,0 +1,72 @@
|
||||
fn run_dmidecode(type_num: u8) -> String {
|
||||
std::process::Command::new("dmidecode")
|
||||
.args(["-t", &type_num.to_string()])
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn extract_field(text: &str, key: &str) -> Option<String> {
|
||||
for line in text.lines() {
|
||||
let t = line.trim();
|
||||
if t.starts_with(key) {
|
||||
let val = t[key.len()..].trim().trim_start_matches(':').trim();
|
||||
if !val.is_empty() && val != "Not Specified" && val != "Unknown"
|
||||
&& val != "To Be Filled By O.E.M." {
|
||||
return Some(val.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_available() -> bool {
|
||||
std::process::Command::new("which")
|
||||
.arg("dmidecode")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn collect() -> Option<crate::payload::HardwareInfo> {
|
||||
if !is_available() { return None; }
|
||||
|
||||
let board = run_dmidecode(2); // Baseboard
|
||||
let cpu = run_dmidecode(4); // Processor
|
||||
let mem = run_dmidecode(17); // Memory Device
|
||||
|
||||
let mut slots_total: i64 = 0;
|
||||
let mut slots_used: i64 = 0;
|
||||
let mut ram_type: Option<String> = None;
|
||||
let mut ram_speed: Option<i64> = None;
|
||||
|
||||
for block in mem.split("\n\n") {
|
||||
if !block.contains("Memory Device") { continue; }
|
||||
slots_total += 1;
|
||||
if let Some(size) = extract_field(block, "Size") {
|
||||
if !size.contains("No Module") && size != "0" {
|
||||
slots_used += 1;
|
||||
}
|
||||
}
|
||||
if ram_type.is_none() {
|
||||
ram_type = extract_field(block, "Type")
|
||||
.filter(|t| t != "Unknown" && t != "Other");
|
||||
}
|
||||
if ram_speed.is_none() {
|
||||
if let Some(spd) = extract_field(block, "Speed") {
|
||||
ram_speed = spd.split_whitespace().next()
|
||||
.and_then(|s| s.parse().ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(crate::payload::HardwareInfo {
|
||||
motherboard_vendor: extract_field(&board, "Manufacturer"),
|
||||
motherboard_model: extract_field(&board, "Product Name"),
|
||||
cpu_model: extract_field(&cpu, "Version").or_else(|| extract_field(&cpu, "Family")),
|
||||
ram_type,
|
||||
ram_speed_mhz: ram_speed,
|
||||
ram_slots_used: if slots_total > 0 { Some(slots_used) } else { None },
|
||||
ram_slots_total: if slots_total > 0 { Some(slots_total) } else { None },
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod cpu;
|
||||
pub mod disk;
|
||||
pub mod hardware;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod network_info;
|
||||
pub mod smart;
|
||||
pub mod temperature;
|
||||
pub mod uptime;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
fn local_hhmm() -> (u32, u32) {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
let mut tm = MaybeUninit::<libc::tm>::uninit();
|
||||
unsafe { libc::localtime_r(&now, tm.as_mut_ptr()) };
|
||||
let tm = unsafe { tm.assume_init() };
|
||||
(tm.tm_hour as u32, tm.tm_min as u32)
|
||||
}
|
||||
|
||||
pub fn current_hhmm() -> (u32, u32) {
|
||||
local_hhmm()
|
||||
}
|
||||
|
||||
pub fn current_yday() -> u32 {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
let mut tm = MaybeUninit::<libc::tm>::uninit();
|
||||
unsafe { libc::localtime_r(&now, tm.as_mut_ptr()) };
|
||||
unsafe { tm.assume_init() }.tm_yday as u32
|
||||
}
|
||||
|
||||
fn is_physical(name: &str) -> bool {
|
||||
if name == "lo" { return false; }
|
||||
for prefix in &["veth", "docker", "br-", "virbr", "vir", "tun", "tap", "dummy", "bond"] {
|
||||
if name.starts_with(prefix) { return false; }
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn read_sysfs(iface: &str, file: &str) -> Option<String> {
|
||||
std::fs::read_to_string(format!("/sys/class/net/{}/{}", iface, file))
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn is_wifi(name: &str) -> bool {
|
||||
std::path::Path::new(&format!("/sys/class/net/{}/wireless", name)).exists()
|
||||
}
|
||||
|
||||
fn wol_status(name: &str) -> Option<bool> {
|
||||
let out = std::process::Command::new("ethtool").arg(name).output().ok()?;
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
for line in text.lines() {
|
||||
let t = line.trim();
|
||||
if t.starts_with("Wake-on:") {
|
||||
let val = t.split(':').nth(1)?.trim().to_string();
|
||||
return Some(val != "d" && !val.is_empty());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn iperf_mbps(server_ip: &str) -> Option<f64> {
|
||||
std::process::Command::new("which").arg("iperf3")
|
||||
.output().ok()
|
||||
.filter(|o| o.status.success())?;
|
||||
let out = std::process::Command::new("iperf3")
|
||||
.args(["-c", server_ip, "-J", "-t", "5", "-P", "1"])
|
||||
.output().ok()?;
|
||||
let json = String::from_utf8_lossy(&out.stdout);
|
||||
let v: serde_json::Value = serde_json::from_str(&json).ok()?;
|
||||
let bps = v["end"]["sum_received"]["bits_per_second"].as_f64()?;
|
||||
Some((bps / 1_000_000.0 * 10.0).round() / 10.0)
|
||||
}
|
||||
|
||||
pub fn collect(server_ip: &str) -> Vec<crate::payload::NetworkInterface> {
|
||||
let entries = match std::fs::read_dir("/sys/class/net") {
|
||||
Ok(e) => e,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
let mut ifaces: Vec<String> = entries
|
||||
.flatten()
|
||||
.map(|e| e.file_name().into_string().unwrap_or_default())
|
||||
.filter(|n| is_physical(n))
|
||||
.collect();
|
||||
ifaces.sort();
|
||||
if ifaces.is_empty() { return vec![]; }
|
||||
|
||||
let iperf = iperf_mbps(server_ip);
|
||||
|
||||
ifaces.iter().map(|name| {
|
||||
let speed = read_sysfs(name, "speed")
|
||||
.and_then(|s| s.parse::<i64>().ok())
|
||||
.filter(|&v| v > 0);
|
||||
let mac = read_sysfs(name, "address").unwrap_or_default();
|
||||
let wifi = is_wifi(name);
|
||||
crate::payload::NetworkInterface {
|
||||
name: name.clone(),
|
||||
if_type: if wifi { "wifi".to_string() } else { "ethernet".to_string() },
|
||||
speed_mbps: speed,
|
||||
mac,
|
||||
wol: if wifi { None } else { wol_status(name) },
|
||||
iperf_mbps: iperf,
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
Reference in New Issue
Block a user