From b7ffeaced58fd85329125fdb82b867b0514cc3f2 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Wed, 26 Nov 2025 17:41:35 +0100 Subject: [PATCH] Add network interface collection and display Extend NixOS collector to gather network interfaces using ip command JSON output. Display all interfaces with IPv4 and IPv6 addresses in Network section above CPU metrics. Filters out loopback and link-local addresses. Version bump to 0.1.161 --- Cargo.lock | 6 +-- agent/Cargo.toml | 2 +- agent/src/collectors/nixos.rs | 71 ++++++++++++++++++++++++++++- dashboard/Cargo.toml | 2 +- dashboard/src/ui/widgets/system.rs | 72 ++++++++++++++++++++++++------ shared/Cargo.toml | 2 +- shared/src/agent_data.rs | 18 ++++++++ 7 files changed, 152 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5b7e28..ca0f5b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.160" +version = "0.1.161" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.160" +version = "0.1.161" dependencies = [ "anyhow", "async-trait", @@ -324,7 +324,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.160" +version = "0.1.161" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index e0b23ec..5cd9b8e 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.160" +version = "0.1.161" edition = "2021" [dependencies] diff --git a/agent/src/collectors/nixos.rs b/agent/src/collectors/nixos.rs index ec9d246..183811e 100644 --- a/agent/src/collectors/nixos.rs +++ b/agent/src/collectors/nixos.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use cm_dashboard_shared::AgentData; +use cm_dashboard_shared::{AgentData, NetworkInterfaceData}; use std::fs; use std::process::Command; use tracing::debug; @@ -32,6 +32,9 @@ 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; @@ -101,6 +104,72 @@ impl NixOSCollector { } } } + + /// Get network interfaces and their IP addresses + async fn get_network_interfaces(&self) -> Vec { + 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::(&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] diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index dd3e9c5..278b36a 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.160" +version = "0.1.161" edition = "2021" [dependencies] diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 32ab695..9e46bf5 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -8,13 +8,16 @@ use ratatui::{ use crate::ui::theme::{StatusIcons, Typography}; -/// System widget displaying NixOS info, CPU, RAM, and Storage in unified layout +/// System widget displaying NixOS info, Network, CPU, RAM, and Storage in unified layout #[derive(Clone)] pub struct SystemWidget { // NixOS information nixos_build: Option, agent_hash: Option, - + + // Network interfaces + network_interfaces: Vec, + // CPU metrics cpu_load_1min: Option, cpu_load_5min: Option, @@ -89,6 +92,7 @@ impl SystemWidget { Self { nixos_build: None, agent_hash: None, + network_interfaces: Vec::new(), cpu_load_1min: None, cpu_load_5min: None, cpu_load_15min: None, @@ -164,6 +168,9 @@ impl Widget for SystemWidget { // Extract build version self.nixos_build = agent_data.build_version.clone(); + // Extract network interfaces + self.network_interfaces = agent_data.system.network.interfaces.clone(); + // Extract CPU data directly let cpu = &agent_data.system.cpu; self.cpu_load_1min = Some(cpu.load_1min); @@ -573,8 +580,46 @@ impl SystemWidget { lines } - /// Render system widget - pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, config: Option<&crate::config::DashboardConfig>) { + /// Render network section for display + fn render_network(&self) -> Vec> { + let mut lines = Vec::new(); + + if self.network_interfaces.is_empty() { + return lines; + } + + for (i, interface) in self.network_interfaces.iter().enumerate() { + let is_last = i == self.network_interfaces.len() - 1; + let tree_symbol = if is_last { " └─ " } else { " ├─ " }; + + // Show interface name + let mut interface_text = format!("{}: ", interface.name); + + // Add IPv4 addresses + if !interface.ipv4_addresses.is_empty() { + interface_text.push_str(&interface.ipv4_addresses.join(", ")); + } + + // Add IPv6 addresses + if !interface.ipv6_addresses.is_empty() { + if !interface.ipv4_addresses.is_empty() { + interface_text.push_str(", "); + } + interface_text.push_str(&interface.ipv6_addresses.join(", ")); + } + + let mut spans = vec![ + Span::styled(tree_symbol, Typography::tree()), + ]; + spans.extend(StatusIcons::create_status_spans(Status::Ok, &interface_text)); + lines.push(Line::from(spans)); + } + + lines + } + + /// Render system widget + pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, _config: Option<&crate::config::DashboardConfig>) { let mut lines = Vec::new(); // NixOS section @@ -591,17 +636,16 @@ impl SystemWidget { lines.push(Line::from(vec![ Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary()) ])); - - // Display detected connection IP - if let Some(config) = config { - if let Some(host_details) = config.hosts.get(hostname) { - let detected_ip = host_details.get_connection_ip(hostname); - lines.push(Line::from(vec![ - Span::styled(format!("IP: {}", detected_ip), Typography::secondary()) - ])); - } + + // Network section + if !self.network_interfaces.is_empty() { + lines.push(Line::from(vec![ + Span::styled("Network:", Typography::widget_title()) + ])); + + let network_lines = self.render_network(); + lines.extend(network_lines); } - // CPU section lines.push(Line::from(vec![ diff --git a/shared/Cargo.toml b/shared/Cargo.toml index f4f425d..42a9d79 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.160" +version = "0.1.161" edition = "2021" [dependencies] diff --git a/shared/src/agent_data.rs b/shared/src/agent_data.rs index 58b560e..c93dc32 100644 --- a/shared/src/agent_data.rs +++ b/shared/src/agent_data.rs @@ -16,11 +16,26 @@ pub struct AgentData { /// System-level monitoring data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SystemData { + pub network: NetworkData, pub cpu: CpuData, pub memory: MemoryData, pub storage: StorageData, } +/// Network interface monitoring data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkData { + pub interfaces: Vec, +} + +/// Individual network interface data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkInterfaceData { + pub name: String, + pub ipv4_addresses: Vec, + pub ipv6_addresses: Vec, +} + /// CPU monitoring data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CpuData { @@ -171,6 +186,9 @@ impl AgentData { build_version: None, timestamp: chrono::Utc::now().timestamp() as u64, system: SystemData { + network: NetworkData { + interfaces: Vec::new(), + }, cpu: CpuData { load_1min: 0.0, load_5min: 0.0,