use crate::Collector;
use crate::filesystem::slurp;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::to_value;
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
#[derive(Debug, Deserialize)]
struct IPDevice {
ifname: String,
mtu: u32,
operstate: String,
link_type: String,
address: String,
addr_info: Vec<AddrInfo>,
}
#[derive(Debug, Deserialize)]
struct AddrInfo {
family: String,
local: String,
prefixlen: u32,
scope: String,
}
#[derive(Debug, Serialize)]
pub struct NetworkFacts {
pub hostname: String,
pub domain: Option<String>,
pub fqdn: Option<String>,
pub primary: Option<String>,
pub ip: Option<String>,
pub ip6: Option<String>,
pub mac: Option<String>,
pub mtu: Option<u32>,
pub interfaces: HashMap<String, Interface>,
}
#[derive(Serialize, Debug)]
pub struct Interface {
pub name: String,
pub ip: Option<String>,
pub prefix: Option<u32>,
pub ip6: Option<String>,
pub prefix6: Option<u32>,
pub mtu: Option<u32>,
pub mac: Option<String>,
pub operational_state: String,
pub link_type: String,
}
pub struct NetworkComponent;
impl NetworkComponent {
pub fn new() -> Self {
Self
}
}
impl Collector for NetworkComponent {
fn name(&self) -> &'static str {
"network"
}
fn collect(&self) -> Result<serde_json::Value> {
let hostname = get_hostname()?;
let domain = get_domain()?;
let fqdn = build_fqdn(&hostname, &domain);
let ip_devices_output = get_all_ip_devices_output()?;
let system_devices = parse_ip_devices_output(&ip_devices_output)?;
let mut interfaces: HashMap<String, Interface> = HashMap::new();
let mut primary_ifname = None;
let mut primary_ip = None;
let mut primary_ip6 = None;
let mut primary_mac = None;
let mut primary_mtu = None;
let mut primary_done = false;
for device in system_devices {
let mut ip = None;
let mut prefix = None;
let mut ip6 = None;
let mut prefix6 = None;
for addr_info in &device.addr_info {
if addr_info.scope == "link" {
continue;
}
if addr_info.family == "inet" {
ip = Some(addr_info.local.clone());
prefix = Some(addr_info.prefixlen);
}
if addr_info.family == "inet6" {
ip6 = Some(addr_info.local.clone());
prefix6 = Some(addr_info.prefixlen);
}
}
if !primary_done && device.link_type == "ether" {
primary_ifname = Some(device.ifname.clone());
primary_ip = ip.clone();
primary_ip6 = ip6.clone();
primary_mac = Some(device.address.clone());
primary_mtu = Some(device.mtu);
primary_done = true;
}
interfaces.insert(
device.ifname.clone(),
Interface {
name: device.ifname.clone(),
operational_state: device.operstate.clone(),
mtu: Some(device.mtu),
mac: Some(device.address.clone()),
link_type: device.link_type.clone(),
ip: ip,
prefix: prefix,
ip6: ip6,
prefix6: prefix6,
},
);
}
let facts = NetworkFacts {
hostname,
domain,
fqdn,
primary: primary_ifname,
ip: primary_ip,
ip6: primary_ip6,
mac: primary_mac,
mtu: primary_mtu,
interfaces,
};
let j = to_value(facts).context("serializing to json value")?;
Ok(j)
}
}
fn get_hostname() -> Result<String> {
slurp(Path::new("/proc/sys/kernel/hostname")).context("failed to read hostname")
}
fn get_domain() -> Result<Option<String>> {
let domain =
slurp(Path::new("/proc/sys/kernel/domainname")).context("failed to read domainname")?;
Ok(parse_domain(&domain))
}
fn parse_domain(s: &str) -> Option<String> {
if s.is_empty() || s == "(none)" {
return None;
}
Some(s.to_string())
}
fn build_fqdn(hostname: &str, domain: &Option<String>) -> Option<String> {
return match domain {
None => None,
Some(d) => Some(format!("{}.{}", hostname, d)),
};
}
fn parse_ip_devices_output(output: &str) -> Result<Vec<IPDevice>> {
let devices: Vec<IPDevice> = serde_json::from_str(&output)?;
Ok(devices)
}
fn get_all_ip_devices_output() -> Result<String> {
let output = Command::new("ip")
.arg("-j")
.arg("addr")
.arg("show")
.output()
.with_context(|| format!("running ip -j addr show"))?
.stdout;
let output = String::from_utf8(output)?;
Ok(output.trim_end().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_IP_OUTPUT: &str = r#"[
{
"ifindex": 1,
"ifname": "lo",
"flags": ["LOOPBACK", "UP", "LOWER_UP"],
"mtu": 65536,
"qdisc": "noqueue",
"operstate": "UNKNOWN",
"group": "default",
"txqlen": 1000,
"link_type": "loopback",
"address": "00:00:00:00:00:00",
"broadcast": "00:00:00:00:00:00",
"addr_info": [
{"family": "inet", "local": "127.0.0.1", "prefixlen": 8, "scope": "host",
"label": "lo", "valid_life_time": 4294967295, "preferred_life_time": 4294967295},
{"family": "inet6", "local": "::1", "prefixlen": 128, "scope": "host",
"noprefixroute": true, "valid_life_time": 4294967295, "preferred_life_time": 4294967295}
]
},
{
"ifindex": 2,
"ifname": "enp0s1",
"flags": ["BROADCAST", "MULTICAST", "UP", "LOWER_UP"],
"mtu": 1500,
"qdisc": "fq_codel",
"operstate": "UP",
"group": "default",
"txqlen": 1000,
"link_type": "ether",
"address": "56:a5:fa:dc:80:45",
"broadcast": "ff:ff:ff:ff:ff:ff",
"addr_info": [
{"family": "inet", "local": "192.168.64.8", "prefixlen": 24, "scope": "global",
"label": "enp0s1", "valid_life_time": 3132, "preferred_life_time": 3132},
{"family": "inet6", "local": "fd08:b294:739c:b65:54a5:faff:fedc:8045", "prefixlen": 64,
"scope": "global", "valid_life_time": 2591980, "preferred_life_time": 604780},
{"family": "inet6", "local": "fe80::54a5:faff:fedc:8045", "prefixlen": 64,
"scope": "link", "valid_life_time": 4294967295, "preferred_life_time": 4294967295}
]
}
]"#;
#[test]
fn test_parse_ip_devices_output() {
let devices = parse_ip_devices_output(SAMPLE_IP_OUTPUT).unwrap();
assert_eq!(devices.len(), 2);
let lo = &devices[0];
assert_eq!(lo.ifname, "lo");
assert_eq!(lo.mtu, 65536);
assert_eq!(lo.operstate, "UNKNOWN");
assert_eq!(lo.link_type, "loopback");
assert_eq!(lo.addr_info.len(), 2);
let eth = &devices[1];
assert_eq!(eth.ifname, "enp0s1");
assert_eq!(eth.mtu, 1500);
assert_eq!(eth.operstate, "UP");
assert_eq!(eth.link_type, "ether");
assert_eq!(eth.address, "56:a5:fa:dc:80:45");
assert_eq!(eth.addr_info.len(), 3);
assert_eq!(eth.addr_info[0].local, "192.168.64.8");
assert_eq!(eth.addr_info[0].prefixlen, 24);
}
const SAMPLE_IP_OUTPUT_NO_IPV6: &str = r#"[
{
"ifindex": 1,
"ifname": "lo",
"flags": ["LOOPBACK", "UP", "LOWER_UP"],
"mtu": 65536,
"qdisc": "noqueue",
"operstate": "UNKNOWN",
"group": "default",
"txqlen": 1000,
"link_type": "loopback",
"address": "00:00:00:00:00:00",
"broadcast": "00:00:00:00:00:00",
"addr_info": [
{"family": "inet", "local": "127.0.0.1", "prefixlen": 8, "scope": "host",
"label": "lo", "valid_life_time": 4294967295, "preferred_life_time": 4294967295}
]
},
{
"ifindex": 2,
"ifname": "enp0s1",
"flags": ["BROADCAST", "MULTICAST", "UP", "LOWER_UP"],
"mtu": 1500,
"qdisc": "fq_codel",
"operstate": "UP",
"group": "default",
"txqlen": 1000,
"link_type": "ether",
"address": "56:a5:fa:dc:80:45",
"broadcast": "ff:ff:ff:ff:ff:ff",
"addr_info": [
{"family": "inet", "local": "192.168.64.8", "prefixlen": 24, "scope": "global",
"label": "enp0s1", "valid_life_time": 3132, "preferred_life_time": 3132}
]
}
]"#;
#[test]
fn test_parse_ip_devices_output_no_ipv6() {
let devices = parse_ip_devices_output(SAMPLE_IP_OUTPUT_NO_IPV6).unwrap();
assert_eq!(devices.len(), 2);
let lo = &devices[0];
assert_eq!(lo.addr_info.len(), 1);
assert_eq!(lo.addr_info[0].family, "inet");
let eth = &devices[1];
assert_eq!(eth.addr_info.len(), 1);
assert_eq!(eth.addr_info[0].family, "inet");
assert_eq!(eth.addr_info[0].local, "192.168.64.8");
}
#[test]
fn test_parse_domain_none() {
assert!(parse_domain("(none)").is_none());
}
#[test]
fn test_parse_domain_empty() {
assert!(parse_domain("").is_none());
}
#[test]
fn test_parse_domain_valid() {
assert_eq!(parse_domain("example.com"), Some("example.com".to_string()));
}
#[test]
fn test_build_fqdn_with_domain() {
let fqdn = build_fqdn("web01", &Some("example.com".to_string()));
assert_eq!(fqdn, Some("web01.example.com".to_string()));
}
#[test]
fn test_build_fqdn_without_domain() {
let fqdn = build_fqdn("web01", &None);
assert_eq!(fqdn, None);
}
}