Add network interface collection and display
Some checks failed
Build and Release / build-and-release (push) Failing after 1m32s

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
This commit is contained in:
Christoffer Martinsson 2025-11-26 17:41:35 +01:00
parent 3858309a5d
commit b7ffeaced5
7 changed files with 152 additions and 21 deletions

6
Cargo.lock generated
View File

@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.160" version = "0.1.161"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -301,7 +301,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.160" version = "0.1.161"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -324,7 +324,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.160" version = "0.1.161"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.160" version = "0.1.161"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -1,5 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use cm_dashboard_shared::AgentData; use cm_dashboard_shared::{AgentData, NetworkInterfaceData};
use std::fs; use std::fs;
use std::process::Command; use std::process::Command;
use tracing::debug; use tracing::debug;
@ -32,6 +32,9 @@ impl NixOSCollector {
// Set NixOS build/generation information // Set NixOS build/generation information
agent_data.build_version = self.get_nixos_generation().await; 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 // Set current timestamp
agent_data.timestamp = chrono::Utc::now().timestamp() as u64; 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<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] #[async_trait]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.160" version = "0.1.161"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -8,13 +8,16 @@ use ratatui::{
use crate::ui::theme::{StatusIcons, Typography}; 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)] #[derive(Clone)]
pub struct SystemWidget { pub struct SystemWidget {
// NixOS information // NixOS information
nixos_build: Option<String>, nixos_build: Option<String>,
agent_hash: Option<String>, agent_hash: Option<String>,
// Network interfaces
network_interfaces: Vec<cm_dashboard_shared::NetworkInterfaceData>,
// CPU metrics // CPU metrics
cpu_load_1min: Option<f32>, cpu_load_1min: Option<f32>,
cpu_load_5min: Option<f32>, cpu_load_5min: Option<f32>,
@ -89,6 +92,7 @@ impl SystemWidget {
Self { Self {
nixos_build: None, nixos_build: None,
agent_hash: None, agent_hash: None,
network_interfaces: Vec::new(),
cpu_load_1min: None, cpu_load_1min: None,
cpu_load_5min: None, cpu_load_5min: None,
cpu_load_15min: None, cpu_load_15min: None,
@ -164,6 +168,9 @@ impl Widget for SystemWidget {
// Extract build version // Extract build version
self.nixos_build = agent_data.build_version.clone(); self.nixos_build = agent_data.build_version.clone();
// Extract network interfaces
self.network_interfaces = agent_data.system.network.interfaces.clone();
// Extract CPU data directly // Extract CPU data directly
let cpu = &agent_data.system.cpu; let cpu = &agent_data.system.cpu;
self.cpu_load_1min = Some(cpu.load_1min); self.cpu_load_1min = Some(cpu.load_1min);
@ -573,8 +580,46 @@ impl SystemWidget {
lines lines
} }
/// Render network section for display
fn render_network(&self) -> Vec<Line<'_>> {
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 /// Render system widget
pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, config: Option<&crate::config::DashboardConfig>) { pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, _config: Option<&crate::config::DashboardConfig>) {
let mut lines = Vec::new(); let mut lines = Vec::new();
// NixOS section // NixOS section
@ -592,16 +637,15 @@ impl SystemWidget {
Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary()) Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary())
])); ]));
// Display detected connection IP // Network section
if let Some(config) = config { if !self.network_interfaces.is_empty() {
if let Some(host_details) = config.hosts.get(hostname) { lines.push(Line::from(vec![
let detected_ip = host_details.get_connection_ip(hostname); Span::styled("Network:", Typography::widget_title())
lines.push(Line::from(vec![ ]));
Span::styled(format!("IP: {}", detected_ip), Typography::secondary())
]));
}
}
let network_lines = self.render_network();
lines.extend(network_lines);
}
// CPU section // CPU section
lines.push(Line::from(vec![ lines.push(Line::from(vec![

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.160" version = "0.1.161"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -16,11 +16,26 @@ pub struct AgentData {
/// System-level monitoring data /// System-level monitoring data
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemData { pub struct SystemData {
pub network: NetworkData,
pub cpu: CpuData, pub cpu: CpuData,
pub memory: MemoryData, pub memory: MemoryData,
pub storage: StorageData, pub storage: StorageData,
} }
/// Network interface monitoring data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkData {
pub interfaces: Vec<NetworkInterfaceData>,
}
/// Individual network interface data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInterfaceData {
pub name: String,
pub ipv4_addresses: Vec<String>,
pub ipv6_addresses: Vec<String>,
}
/// CPU monitoring data /// CPU monitoring data
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpuData { pub struct CpuData {
@ -171,6 +186,9 @@ impl AgentData {
build_version: None, build_version: None,
timestamp: chrono::Utc::now().timestamp() as u64, timestamp: chrono::Utc::now().timestamp() as u64,
system: SystemData { system: SystemData {
network: NetworkData {
interfaces: Vec::new(),
},
cpu: CpuData { cpu: CpuData {
load_1min: 0.0, load_1min: 0.0,
load_5min: 0.0, load_5min: 0.0,