diff --git a/Cargo.lock b/Cargo.lock index 994842e..91e475f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.169" +version = "0.1.170" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.169" +version = "0.1.170" dependencies = [ "anyhow", "async-trait", @@ -324,7 +324,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.169" +version = "0.1.170" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index f41240f..f37f266 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.169" +version = "0.1.170" edition = "2021" [dependencies] diff --git a/agent/src/collectors/network.rs b/agent/src/collectors/network.rs index f35b360..dc8d8a6 100644 --- a/agent/src/collectors/network.rs +++ b/agent/src/collectors/network.rs @@ -49,10 +49,67 @@ impl NetworkCollector { } } + /// Get the primary physical interface (the one with default route) + fn get_primary_physical_interface() -> Option { + match Command::new("ip").args(["route", "show", "default"]).output() { + Ok(output) if output.status.success() => { + let output_str = String::from_utf8_lossy(&output.stdout); + // Parse: "default via 192.168.1.1 dev eno1 ..." + for line in output_str.lines() { + if line.starts_with("default") { + if let Some(dev_pos) = line.find(" dev ") { + let after_dev = &line[dev_pos + 5..]; + if let Some(space_pos) = after_dev.find(' ') { + let interface = &after_dev[..space_pos]; + // Only return if it's a physical interface + if Self::is_physical_interface(interface) { + return Some(interface.to_string()); + } + } else { + // No space after interface name (end of line) + let interface = after_dev.trim(); + if Self::is_physical_interface(interface) { + return Some(interface.to_string()); + } + } + } + } + } + None + } + _ => None, + } + } + + /// Parse VLAN configuration from /proc/net/vlan/config + /// Returns a map of interface name -> VLAN ID + fn parse_vlan_config() -> std::collections::HashMap { + let mut vlan_map = std::collections::HashMap::new(); + + if let Ok(contents) = std::fs::read_to_string("/proc/net/vlan/config") { + for line in contents.lines().skip(2) { // Skip header lines + let parts: Vec<&str> = line.split('|').collect(); + if parts.len() >= 2 { + let interface_name = parts[0].trim(); + let vlan_id_str = parts[1].trim(); + + if let Ok(vlan_id) = vlan_id_str.parse::() { + vlan_map.insert(interface_name.to_string(), vlan_id); + } + } + } + } + + vlan_map + } + /// Collect network interfaces using ip command async fn collect_interfaces(&self) -> Vec { let mut interfaces = Vec::new(); + // Parse VLAN configuration + let vlan_map = Self::parse_vlan_config(); + match Command::new("ip").args(["-j", "addr"]).output() { Ok(output) if output.status.success() => { let json_str = String::from_utf8_lossy(&output.stdout); @@ -113,6 +170,9 @@ impl NetworkCollector { Status::Unknown // Virtual interfaces don't have meaningful link status }; + // Look up VLAN ID from the map (use original name before @ parsing) + let vlan_id = vlan_map.get(&name).copied(); + interfaces.push(NetworkInterfaceData { name: interface_name, ipv4_addresses, @@ -120,6 +180,7 @@ impl NetworkCollector { is_physical, link_status, parent_interface, + vlan_id, }); } } @@ -133,6 +194,17 @@ impl NetworkCollector { } } + // Assign primary physical interface as parent to virtual interfaces without explicit parent + let primary_interface = Self::get_primary_physical_interface(); + if let Some(primary) = primary_interface { + for interface in interfaces.iter_mut() { + // Only assign parent to virtual interfaces that don't already have one + if !interface.is_physical && interface.parent_interface.is_none() { + interface.parent_interface = Some(primary.clone()); + } + } + } + interfaces } } diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 2e7f300..cda1f6d 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.169" +version = "0.1.170" edition = "2021" [dependencies] diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 89d6d1f..2009219 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -697,10 +697,19 @@ impl SystemWidget { String::new() }; - let child_text = if !ip_text.is_empty() { - format!("{}: {}", child.name, ip_text) + // Format: "name (vlan X): IP" or "name: IP" + let child_text = if let Some(vlan_id) = child.vlan_id { + if !ip_text.is_empty() { + format!("{} (vlan {}): {}", child.name, vlan_id, ip_text) + } else { + format!("{} (vlan {}):", child.name, vlan_id) + } } else { - format!("{}:", child.name) + if !ip_text.is_empty() { + format!("{}: {}", child.name, ip_text) + } else { + format!("{}:", child.name) + } }; lines.push(Line::from(vec![ @@ -724,10 +733,19 @@ impl SystemWidget { String::new() }; - let interface_text = if !ip_text.is_empty() { - format!("{}: {}", interface.name, ip_text) + // Format: "name (vlan X): IP" or "name: IP" + let interface_text = if let Some(vlan_id) = interface.vlan_id { + if !ip_text.is_empty() { + format!("{} (vlan {}): {}", interface.name, vlan_id, ip_text) + } else { + format!("{} (vlan {}):", interface.name, vlan_id) + } } else { - format!("{}:", interface.name) + if !ip_text.is_empty() { + format!("{}: {}", interface.name, ip_text) + } else { + format!("{}:", interface.name) + } }; lines.push(Line::from(vec![ diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 7d65ed4..8404be2 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.169" +version = "0.1.170" edition = "2021" [dependencies] diff --git a/shared/src/agent_data.rs b/shared/src/agent_data.rs index d93e7b2..0d76e28 100644 --- a/shared/src/agent_data.rs +++ b/shared/src/agent_data.rs @@ -37,6 +37,7 @@ pub struct NetworkInterfaceData { pub is_physical: bool, pub link_status: Status, pub parent_interface: Option, + pub vlan_id: Option, } /// CPU monitoring data