Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db6fc65ee1 | |||
| 1002a6be68 | |||
| 017d7bb1bb | |||
| 5ee8b66464 | |||
| c238e9f2b8 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nanometrics-agent"
|
||||
version = "0.1.6"
|
||||
version = "0.1.9"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -13,8 +13,12 @@ pub struct Config {
|
||||
pub struct ServerConfig {
|
||||
pub ip: String,
|
||||
pub port: u16,
|
||||
#[serde(default = "default_iperf3_port")]
|
||||
pub iperf3_port: u16,
|
||||
}
|
||||
|
||||
fn default_iperf3_port() -> u16 { 5201 }
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
pub struct ProtocolsConfig {
|
||||
#[serde(default)]
|
||||
|
||||
+49
-5
@@ -10,22 +10,66 @@ extern "C" fn handle_signal(_: libc::c_int) {
|
||||
RUNNING.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn physical_ipv4_addrs() -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
unsafe {
|
||||
let mut ifap = std::ptr::null_mut::<libc::ifaddrs>();
|
||||
if libc::getifaddrs(&mut ifap) != 0 { return result; }
|
||||
let mut ifa = ifap;
|
||||
while !ifa.is_null() {
|
||||
let flags = (*ifa).ifa_flags as i32;
|
||||
let up = flags & libc::IFF_UP as i32 != 0;
|
||||
let loopback = flags & libc::IFF_LOOPBACK as i32 != 0;
|
||||
let pointop = flags & libc::IFF_POINTOPOINT as i32 != 0;
|
||||
if !up || loopback || pointop { ifa = (*ifa).ifa_next; continue; }
|
||||
|
||||
let name = std::ffi::CStr::from_ptr((*ifa).ifa_name)
|
||||
.to_string_lossy().into_owned();
|
||||
|
||||
// Type 1 = ARPHRD_ETHER (Ethernet + WiFi), exclut WireGuard (65534), tunnels, etc.
|
||||
let itype: u32 = std::fs::read_to_string(
|
||||
format!("/sys/class/net/{}/type", name))
|
||||
.ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
|
||||
if itype != 1 { ifa = (*ifa).ifa_next; continue; }
|
||||
|
||||
// Exclut bridges et interfaces Docker par nom
|
||||
let is_virtual = name.starts_with("br-") || name.starts_with("docker")
|
||||
|| name.starts_with("virbr") || name.starts_with("veth");
|
||||
if is_virtual { ifa = (*ifa).ifa_next; continue; }
|
||||
|
||||
if let Some(addr) = (*ifa).ifa_addr.as_ref() {
|
||||
if addr.sa_family as i32 == libc::AF_INET {
|
||||
let sin = addr as *const _ as *const libc::sockaddr_in;
|
||||
let b = (*sin).sin_addr.s_addr.to_ne_bytes();
|
||||
result.push(format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3]));
|
||||
}
|
||||
}
|
||||
ifa = (*ifa).ifa_next;
|
||||
}
|
||||
libc::freeifaddrs(ifap);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn get_local_ip(server_ip: &str) -> String {
|
||||
let physical = physical_ipv4_addrs();
|
||||
|
||||
use std::net::UdpSocket;
|
||||
// Try server IP first (always reachable), then internet fallback
|
||||
for target in &[format!("{}:80", server_ip), "8.8.8.8:80".to_string()] {
|
||||
if let Ok(s) = UdpSocket::bind("0.0.0.0:0") {
|
||||
if s.connect(target.as_str()).is_ok() {
|
||||
if let Ok(addr) = s.local_addr() {
|
||||
let ip = addr.ip().to_string();
|
||||
if ip != "0.0.0.0" {
|
||||
// N'accepte que si c'est une vraie interface physique
|
||||
if ip != "0.0.0.0" && physical.contains(&ip) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"0.0.0.0".to_string()
|
||||
// Fallback : première IP physique disponible
|
||||
physical.into_iter().next().unwrap_or_else(|| "0.0.0.0".to_string())
|
||||
}
|
||||
|
||||
fn apply_config_update(cfg: &mut config::Config, data: &[u8]) {
|
||||
@@ -86,7 +130,7 @@ fn main() {
|
||||
let mut startup_net: Option<Vec<payload::NetworkInterface>> = None;
|
||||
let mut startup_hw: Option<payload::HardwareInfo> = None;
|
||||
if cfg.metrics.network_info.udp || cfg.metrics.network_info.mqtt {
|
||||
let ni = metrics::network_info::collect(&cfg.server.ip);
|
||||
let ni = metrics::network_info::collect(&cfg.server.ip, cfg.server.iperf3_port);
|
||||
if !ni.is_empty() { startup_net = Some(ni); }
|
||||
}
|
||||
if cfg.metrics.hardware_info.udp || cfg.metrics.hardware_info.mqtt {
|
||||
@@ -116,7 +160,7 @@ fn main() {
|
||||
if ch == slow_time.0 && cm == slow_time.1 {
|
||||
slow_daily_done = true;
|
||||
if cfg.metrics.network_info.udp || cfg.metrics.network_info.mqtt {
|
||||
let ni = metrics::network_info::collect(&cfg.server.ip);
|
||||
let ni = metrics::network_info::collect(&cfg.server.ip, cfg.server.iperf3_port);
|
||||
if !ni.is_empty() { daily_net = Some(ni); }
|
||||
}
|
||||
if cfg.metrics.hardware_info.udp || cfg.metrics.hardware_info.mqtt {
|
||||
|
||||
@@ -26,11 +26,13 @@ pub fn current_yday() -> 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
|
||||
// Type 1 = ARPHRD_ETHER (Ethernet + WiFi). WireGuard = 65534, tunnels = autres.
|
||||
let itype: u32 = std::fs::read_to_string(format!("/sys/class/net/{}/type", name))
|
||||
.ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
|
||||
if itype != 1 { return false; }
|
||||
// Exclut bridges et interfaces Docker par nom (type 1 aussi)
|
||||
!name.starts_with("br-") && !name.starts_with("docker")
|
||||
&& !name.starts_with("virbr") && !name.starts_with("veth")
|
||||
}
|
||||
|
||||
fn read_sysfs(iface: &str, file: &str) -> Option<String> {
|
||||
@@ -56,12 +58,13 @@ fn wol_status(name: &str) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn iperf_mbps(server_ip: &str) -> Option<f64> {
|
||||
fn iperf_mbps(server_ip: &str, port: u16) -> Option<f64> {
|
||||
std::process::Command::new("which").arg("iperf3")
|
||||
.output().ok()
|
||||
.filter(|o| o.status.success())?;
|
||||
let port_str = port.to_string();
|
||||
let out = std::process::Command::new("iperf3")
|
||||
.args(["-c", server_ip, "-J", "-t", "5", "-P", "1"])
|
||||
.args(["-c", server_ip, "-p", &port_str, "-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()?;
|
||||
@@ -69,7 +72,7 @@ fn iperf_mbps(server_ip: &str) -> Option<f64> {
|
||||
Some((bps / 1_000_000.0 * 10.0).round() / 10.0)
|
||||
}
|
||||
|
||||
pub fn collect(server_ip: &str) -> Vec<crate::payload::NetworkInterface> {
|
||||
pub fn collect(server_ip: &str, iperf3_port: u16) -> Vec<crate::payload::NetworkInterface> {
|
||||
let entries = match std::fs::read_dir("/sys/class/net") {
|
||||
Ok(e) => e,
|
||||
Err(_) => return vec![],
|
||||
@@ -82,7 +85,7 @@ pub fn collect(server_ip: &str) -> Vec<crate::payload::NetworkInterface> {
|
||||
ifaces.sort();
|
||||
if ifaces.is_empty() { return vec![]; }
|
||||
|
||||
let iperf = iperf_mbps(server_ip);
|
||||
let iperf = iperf_mbps(server_ip, iperf3_port);
|
||||
|
||||
ifaces.iter().map(|name| {
|
||||
let speed = read_sysfs(name, "speed")
|
||||
|
||||
@@ -89,15 +89,29 @@ pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
|
||||
.flatten()
|
||||
.flatten()
|
||||
.map(|e| e.file_name().into_string().unwrap_or_default())
|
||||
.filter(|n| n.starts_with("sd") || n.starts_with("nvme"))
|
||||
.map(|n| format!("/dev/{}", n))
|
||||
.filter_map(|n| {
|
||||
if n.starts_with("sd") {
|
||||
Some(format!("/dev/{}", n))
|
||||
} else if n.starts_with("nvme") && !n.contains('n') {
|
||||
// /sys/block contient nvme0n1 (namespace) — on utilise le contrôleur nvme0
|
||||
Some(format!("/dev/{}", n))
|
||||
} else if n.starts_with("nvme") && n.contains('n') {
|
||||
// Déduit le contrôleur depuis nvme0n1 → nvme0
|
||||
let ctrl = n.split('n').next()?;
|
||||
Some(format!("/dev/{}", ctrl))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
devs.sort();
|
||||
|
||||
let mut results = Vec::new();
|
||||
for dev in &devs {
|
||||
let Ok(output) = std::process::Command::new("smartctl")
|
||||
.args(["-j", dev])
|
||||
.args(["-a", "-j", dev])
|
||||
.output() else { continue };
|
||||
let json = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(metrics) = parse_json(&json) {
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
src: url('../fonts/jetbrains-mono.woff2') format('woff2');
|
||||
font-weight: 400 700;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
src: url('../fonts/jetbrains-mono-bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ const Popups = (() => {
|
||||
let _currentAgentId = null;
|
||||
let _agentCfgData = null;
|
||||
let _resizeObs = null;
|
||||
let _resizeTimer = null;
|
||||
|
||||
// ══ POPUP DÉTAIL ══
|
||||
async function showDetail(agentId) {
|
||||
@@ -214,11 +215,14 @@ const Popups = (() => {
|
||||
if (_resizeObs) _resizeObs.disconnect();
|
||||
const pd = document.getElementById('popup-detail');
|
||||
_resizeObs = new ResizeObserver(() => {
|
||||
API.putServerConfig({
|
||||
...App.serverConfig,
|
||||
popup_detail_w: pd.offsetWidth,
|
||||
popup_detail_h: pd.offsetHeight,
|
||||
}).catch(() => {});
|
||||
clearTimeout(_resizeTimer);
|
||||
_resizeTimer = setTimeout(() => {
|
||||
API.putServerConfig({
|
||||
...App.serverConfig,
|
||||
popup_detail_w: pd.offsetWidth,
|
||||
popup_detail_h: pd.offsetHeight,
|
||||
}).catch(() => {});
|
||||
}, 600);
|
||||
});
|
||||
_resizeObs.observe(pd);
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -38,8 +38,8 @@ services:
|
||||
restart: unless-stopped
|
||||
command: ["-s"]
|
||||
ports:
|
||||
- "5201:5201/tcp"
|
||||
- "5201:5201/udp"
|
||||
- "5202:5201/tcp"
|
||||
- "5202:5201/udp"
|
||||
|
||||
volumes:
|
||||
nanometrics_data:
|
||||
|
||||
Reference in New Issue
Block a user