Add VLAN ID display and smart parent assignment for virtual interfaces
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
Agent changes: - Parse /proc/net/vlan/config to extract VLAN IDs for interfaces - Detect primary physical interface via default route - Auto-assign primary interface as parent for virtual interfaces without explicit parent - Added vlan_id field to NetworkInterfaceData Dashboard changes: - Display VLAN ID in format "interface (vlan X): IP" - Show VLAN IDs for both nested and standalone virtual interfaces This ensures virtual interfaces (docker0, tailscale0, etc.) are properly nested under the primary physical NIC, and VLAN interfaces show their IDs. Updated to version 0.1.170
This commit is contained in:
parent
8aefab83ae
commit
937f4ad427
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.169"
|
version = "0.1.170"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.169"
|
version = "0.1.170"
|
||||||
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.169"
|
version = "0.1.170"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.169"
|
version = "0.1.170"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -49,10 +49,67 @@ impl NetworkCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the primary physical interface (the one with default route)
|
||||||
|
fn get_primary_physical_interface() -> Option<String> {
|
||||||
|
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<String, u16> {
|
||||||
|
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::<u16>() {
|
||||||
|
vlan_map.insert(interface_name.to_string(), vlan_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vlan_map
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect network interfaces using ip command
|
/// Collect network interfaces using ip command
|
||||||
async fn collect_interfaces(&self) -> Vec<NetworkInterfaceData> {
|
async fn collect_interfaces(&self) -> Vec<NetworkInterfaceData> {
|
||||||
let mut interfaces = Vec::new();
|
let mut interfaces = Vec::new();
|
||||||
|
|
||||||
|
// Parse VLAN configuration
|
||||||
|
let vlan_map = Self::parse_vlan_config();
|
||||||
|
|
||||||
match Command::new("ip").args(["-j", "addr"]).output() {
|
match Command::new("ip").args(["-j", "addr"]).output() {
|
||||||
Ok(output) if output.status.success() => {
|
Ok(output) if output.status.success() => {
|
||||||
let json_str = String::from_utf8_lossy(&output.stdout);
|
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
|
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 {
|
interfaces.push(NetworkInterfaceData {
|
||||||
name: interface_name,
|
name: interface_name,
|
||||||
ipv4_addresses,
|
ipv4_addresses,
|
||||||
@ -120,6 +180,7 @@ impl NetworkCollector {
|
|||||||
is_physical,
|
is_physical,
|
||||||
link_status,
|
link_status,
|
||||||
parent_interface,
|
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
|
interfaces
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.169"
|
version = "0.1.170"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -697,10 +697,19 @@ impl SystemWidget {
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let child_text = if !ip_text.is_empty() {
|
// 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 {
|
||||||
|
if !ip_text.is_empty() {
|
||||||
format!("{}: {}", child.name, ip_text)
|
format!("{}: {}", child.name, ip_text)
|
||||||
} else {
|
} else {
|
||||||
format!("{}:", child.name)
|
format!("{}:", child.name)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
@ -724,10 +733,19 @@ impl SystemWidget {
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let interface_text = if !ip_text.is_empty() {
|
// 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 {
|
||||||
|
if !ip_text.is_empty() {
|
||||||
format!("{}: {}", interface.name, ip_text)
|
format!("{}: {}", interface.name, ip_text)
|
||||||
} else {
|
} else {
|
||||||
format!("{}:", interface.name)
|
format!("{}:", interface.name)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.169"
|
version = "0.1.170"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -37,6 +37,7 @@ pub struct NetworkInterfaceData {
|
|||||||
pub is_physical: bool,
|
pub is_physical: bool,
|
||||||
pub link_status: Status,
|
pub link_status: Status,
|
||||||
pub parent_interface: Option<String>,
|
pub parent_interface: Option<String>,
|
||||||
|
pub vlan_id: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CPU monitoring data
|
/// CPU monitoring data
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user