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]]
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",

View File

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

View File

@ -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<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]

View File

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

View File

@ -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<String>,
agent_hash: Option<String>,
// Network interfaces
network_interfaces: Vec<cm_dashboard_shared::NetworkInterfaceData>,
// CPU metrics
cpu_load_1min: Option<f32>,
cpu_load_5min: Option<f32>,
@ -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 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
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();
// NixOS section
@ -592,16 +637,15 @@ impl SystemWidget {
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![

View File

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

View File

@ -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<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
#[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,