Create dedicated network collector with physical/virtual interface grouping
All checks were successful
Build and Release / build-and-release (push) Successful in 1m43s
All checks were successful
Build and Release / build-and-release (push) Successful in 1m43s
Move network collection from NixOS collector to dedicated NetworkCollector. Add link status detection for physical interfaces (up/down). Group interfaces by physical/virtual, show status icons for physical NICs only. Down interfaces show as Inactive instead of Critical. Version bump to 0.1.165
This commit is contained in:
@@ -7,6 +7,7 @@ pub mod cpu;
|
||||
pub mod disk;
|
||||
pub mod error;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod nixos;
|
||||
pub mod systemd;
|
||||
|
||||
|
||||
137
agent/src/collectors/network.rs
Normal file
137
agent/src/collectors/network.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use async_trait::async_trait;
|
||||
use cm_dashboard_shared::{AgentData, NetworkInterfaceData, Status};
|
||||
use std::process::Command;
|
||||
use tracing::debug;
|
||||
|
||||
use super::{Collector, CollectorError};
|
||||
use crate::config::NetworkConfig;
|
||||
|
||||
/// Network interface collector with physical/virtual classification and link status
|
||||
pub struct NetworkCollector {
|
||||
_config: NetworkConfig,
|
||||
}
|
||||
|
||||
impl NetworkCollector {
|
||||
pub fn new(config: NetworkConfig) -> Self {
|
||||
Self { _config: config }
|
||||
}
|
||||
|
||||
/// Check if interface is physical (not virtual)
|
||||
fn is_physical_interface(name: &str) -> bool {
|
||||
// Physical interface patterns
|
||||
matches!(
|
||||
&name[..],
|
||||
s if s.starts_with("eth")
|
||||
|| s.starts_with("ens")
|
||||
|| s.starts_with("enp")
|
||||
|| s.starts_with("wlan")
|
||||
|| s.starts_with("wlp")
|
||||
|| s.starts_with("eno")
|
||||
|| s.starts_with("enx")
|
||||
)
|
||||
}
|
||||
|
||||
/// Get link status for an interface
|
||||
fn get_link_status(interface: &str) -> Status {
|
||||
let operstate_path = format!("/sys/class/net/{}/operstate", interface);
|
||||
|
||||
match std::fs::read_to_string(&operstate_path) {
|
||||
Ok(state) => {
|
||||
let state = state.trim();
|
||||
match state {
|
||||
"up" => Status::Ok,
|
||||
"down" => Status::Inactive,
|
||||
"unknown" => Status::Warning,
|
||||
_ => Status::Unknown,
|
||||
}
|
||||
}
|
||||
Err(_) => Status::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect network interfaces using ip command
|
||||
async fn collect_interfaces(&self) -> Vec<NetworkInterfaceData> {
|
||||
let mut interfaces = Vec::new();
|
||||
|
||||
match Command::new("ip").args(["-j", "addr"]).output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let json_str = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if let Ok(json_data) = serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
if let Some(ifaces) = json_data.as_array() {
|
||||
for iface in ifaces {
|
||||
let name = iface["ifname"].as_str().unwrap_or("").to_string();
|
||||
|
||||
// Skip loopback and empty names
|
||||
if name.is_empty() || name == "lo" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ipv4_addresses = Vec::new();
|
||||
let mut ipv6_addresses = Vec::new();
|
||||
|
||||
// Extract IP addresses
|
||||
if let Some(addr_info) = iface["addr_info"].as_array() {
|
||||
for addr in addr_info {
|
||||
if let Some(family) = addr["family"].as_str() {
|
||||
if let Some(local) = addr["local"].as_str() {
|
||||
match family {
|
||||
"inet" => ipv4_addresses.push(local.to_string()),
|
||||
"inet6" => {
|
||||
// Skip link-local IPv6 addresses (fe80::)
|
||||
if !local.starts_with("fe80:") {
|
||||
ipv6_addresses.push(local.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if physical and get status
|
||||
let is_physical = Self::is_physical_interface(&name);
|
||||
let link_status = if is_physical {
|
||||
Self::get_link_status(&name)
|
||||
} else {
|
||||
Status::Unknown // Virtual interfaces don't have meaningful link status
|
||||
};
|
||||
|
||||
interfaces.push(NetworkInterfaceData {
|
||||
name,
|
||||
ipv4_addresses,
|
||||
ipv6_addresses,
|
||||
is_physical,
|
||||
link_status,
|
||||
parent_interface: None, // TODO: Implement virtual interface parent detection
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to execute ip command: {}", e);
|
||||
}
|
||||
Ok(output) => {
|
||||
debug!("ip command failed with status: {}", output.status);
|
||||
}
|
||||
}
|
||||
|
||||
interfaces
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Collector for NetworkCollector {
|
||||
async fn collect_structured(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> {
|
||||
debug!("Collecting network interface data");
|
||||
|
||||
// Collect all network interfaces
|
||||
let interfaces = self.collect_interfaces().await;
|
||||
|
||||
agent_data.system.network.interfaces = interfaces;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use cm_dashboard_shared::{AgentData, NetworkInterfaceData};
|
||||
use cm_dashboard_shared::AgentData;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use tracing::debug;
|
||||
@@ -32,9 +32,6 @@ impl NixOSCollector {
|
||||
// Set NixOS build/generation information
|
||||
agent_data.build_version = self.get_nixos_generation().await;
|
||||
|
||||
// Collect network interfaces
|
||||
agent_data.system.network.interfaces = self.get_network_interfaces().await;
|
||||
|
||||
// Set current timestamp
|
||||
agent_data.timestamp = chrono::Utc::now().timestamp() as u64;
|
||||
|
||||
@@ -104,72 +101,6 @@ impl NixOSCollector {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get network interfaces and their IP addresses
|
||||
async fn get_network_interfaces(&self) -> Vec<NetworkInterfaceData> {
|
||||
let mut interfaces = Vec::new();
|
||||
|
||||
// Use ip command with JSON output for easier parsing
|
||||
match Command::new("ip").args(["-j", "addr"]).output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let json_str = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Parse JSON output
|
||||
if let Ok(json_data) = serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
if let Some(ifaces) = json_data.as_array() {
|
||||
for iface in ifaces {
|
||||
let name = iface["ifname"].as_str().unwrap_or("").to_string();
|
||||
|
||||
// Skip loopback and empty names
|
||||
if name.is_empty() || name == "lo" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ipv4_addresses = Vec::new();
|
||||
let mut ipv6_addresses = Vec::new();
|
||||
|
||||
// Extract IP addresses
|
||||
if let Some(addr_info) = iface["addr_info"].as_array() {
|
||||
for addr in addr_info {
|
||||
if let Some(family) = addr["family"].as_str() {
|
||||
if let Some(local) = addr["local"].as_str() {
|
||||
match family {
|
||||
"inet" => ipv4_addresses.push(local.to_string()),
|
||||
"inet6" => {
|
||||
// Skip link-local IPv6 addresses (fe80::)
|
||||
if !local.starts_with("fe80:") {
|
||||
ipv6_addresses.push(local.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only add interfaces that have at least one IP address
|
||||
if !ipv4_addresses.is_empty() || !ipv6_addresses.is_empty() {
|
||||
interfaces.push(NetworkInterfaceData {
|
||||
name,
|
||||
ipv4_addresses,
|
||||
ipv6_addresses,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to execute ip command: {}", e);
|
||||
}
|
||||
Ok(output) => {
|
||||
debug!("ip command failed with status: {}", output.status);
|
||||
}
|
||||
}
|
||||
|
||||
interfaces
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
Reference in New Issue
Block a user