use super::model::{ActiveConnection, IpConfig, Ipv6Config, NetworkDevice, NetworkDeviceDetail};
use super::{
ACTIVE_IFACE, DEVICE_IFACE, DHCP4_IFACE, IP4_IFACE, IP6_IFACE, NM_MGR_IFACE, NM_MGR_PATH,
PROPS_IFACE,
};
use crate::capabilities::{CapabilityContext, View};
use crate::error::{FezError, Result};
use crate::protocol::client::BridgeClient;
use serde_json::{json, Value};
fn get_all(client: &mut BridgeClient, channel: &str, path: &str, iface: &str) -> Result<Value> {
let out = client.dbus_call(channel, path, PROPS_IFACE, "GetAll", json!([iface]))?;
out.get(0)
.cloned()
.ok_or_else(|| FezError::Problem(format!("GetAll({iface}) returned no value")))
}
fn load_ip4_config(
client: &mut BridgeClient,
channel: &str,
path: Option<&str>,
) -> Result<IpConfig> {
match path {
Some(path) => IpConfig::from_value(get_all(client, channel, path, IP4_IFACE)?),
None => Ok(IpConfig::default()),
}
}
fn load_ip6_config(
client: &mut BridgeClient,
channel: &str,
path: Option<&str>,
) -> Result<Ipv6Config> {
match path {
Some(path) => Ipv6Config::from_value(get_all(client, channel, path, IP6_IFACE)?),
None => Ok(Ipv6Config::default()),
}
}
fn load_active_connection(
client: &mut BridgeClient,
channel: &str,
path: Option<&str>,
) -> Result<Option<ActiveConnection>> {
match path {
Some(path) => {
let val = get_all(client, channel, path, ACTIVE_IFACE)?;
Ok(Some(ActiveConnection::from_value(val)?))
}
None => Ok(None),
}
}
fn load_dhcp4_options(
client: &mut BridgeClient,
channel: &str,
path: Option<&str>,
) -> Result<Option<Value>> {
match path {
Some(path) => {
let val = get_all(client, channel, path, DHCP4_IFACE)?;
Ok(val
.get("Options")
.map(|v| v.get("v").unwrap_or(v))
.and_then(flatten_options))
}
None => Ok(None),
}
}
fn device_paths(client: &mut BridgeClient, channel: &str) -> Result<Vec<String>> {
let out = client.dbus_call(channel, NM_MGR_PATH, NM_MGR_IFACE, "GetDevices", json!([]))?;
let arr = out
.get(0)
.and_then(Value::as_array)
.ok_or_else(|| FezError::Problem("GetDevices returned a non-array response".into()))?;
Ok(arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect())
}
pub(super) fn list(ctx: &mut CapabilityContext<'_>, all: bool) -> Result<View> {
let paths = device_paths(ctx.client, ctx.channel)?;
let mut devices = Vec::new();
for path in &paths {
let device =
NetworkDevice::from_value(get_all(ctx.client, ctx.channel, path, DEVICE_IFACE)?)?;
if !device.should_list(all) {
continue;
}
let ip4 = load_ip4_config(ctx.client, ctx.channel, device.ip4_config.as_deref())?;
let ip6 = load_ip6_config(ctx.client, ctx.channel, device.ip6_config.as_deref())?;
devices.push((device, ip4, ip6));
}
let mut human = format!(
"{:<14} {:<10} {:<13} {:<20} {}\n",
"DEVICE", "TYPE", "STATE", "IPV4", "MAC"
);
for d in &devices {
human.push_str(&format!(
"{:<14} {:<10} {:<13} {:<20} {}\n",
d.0.interface,
d.0.type_name(),
d.0.state_name(),
d.1.primary_address(),
d.0.mac,
));
}
let columns = ["interface", "type", "state", "ip4", "ip6", "mac"];
let rows: Vec<Value> = devices
.iter()
.map(|(device, ip4, ip6)| {
json!([
device.interface,
device.type_name(),
device.state_name(),
ip4.primary_address(),
ip6.primary_address(),
device.mac,
])
})
.collect();
Ok(View::new(
"NetworkDeviceList",
ctx.host,
crate::envelope::table_data(&columns, rows),
human,
))
}
pub(super) fn show(ctx: &mut CapabilityContext<'_>, device: &str) -> Result<View> {
let paths = device_paths(ctx.client, ctx.channel)?;
let mut found: Option<NetworkDevice> = None;
for path in &paths {
let candidate =
NetworkDevice::from_value(get_all(ctx.client, ctx.channel, path, DEVICE_IFACE)?)?;
if candidate.interface == device {
found = Some(candidate);
break;
}
}
let device = found.ok_or_else(|| FezError::NotFound(format!("network device {device}")))?;
let ipv4 = load_ip4_config(ctx.client, ctx.channel, device.ip4_config.as_deref())?;
let ipv6 = load_ip6_config(ctx.client, ctx.channel, device.ip6_config.as_deref())?;
let connection =
load_active_connection(ctx.client, ctx.channel, device.active_connection.as_deref())?;
let dhcp4 = load_dhcp4_options(ctx.client, ctx.channel, device.dhcp4_config.as_deref())?;
let device_type = device.type_name();
let state = device.state_name();
let detail = NetworkDeviceDetail {
interface: device.interface,
device_type,
state,
mac: device.mac,
mtu: device.mtu,
ipv4,
ipv6,
connection,
dhcp4,
};
let human = render_show_human(&detail);
let data = serde_json::to_value(detail).map_err(FezError::Decode)?;
Ok(View::new("NetworkDeviceDetail", ctx.host, data, human))
}
fn flatten_options(opts: &Value) -> Option<Value> {
let obj = opts.as_object()?;
let flat: serde_json::Map<String, Value> = obj
.iter()
.map(|(k, v)| (k.clone(), v.get("v").unwrap_or(v).clone()))
.collect();
Some(Value::Object(flat))
}
fn render_show_human(d: &NetworkDeviceDetail) -> String {
let mut out = String::new();
out.push_str(&format!("Device: {}\n", d.interface));
out.push_str(&format!("Type: {}\n", d.device_type));
out.push_str(&format!("State: {}\n", d.state));
out.push_str(&format!("MAC: {}\n", d.mac));
out.push_str(&format!("MTU: {}\n", d.mtu));
out.push_str(&format!("IPv4: {}\n", d.ipv4.addresses.join(", ")));
out.push_str(&format!("Gateway: {}\n", d.ipv4.gateway));
out.push_str(&format!("DNS: {}\n", d.ipv4.dns.join(", ")));
out.push_str(&format!("Domains: {}\n", d.ipv4.domains.join(", ")));
out.push_str(&format!("IPv6: {}\n", d.ipv6.addresses.join(", ")));
if let Some(conn) = &d.connection {
out.push_str(&format!(
"Connection: {} ({})\n",
conn.id, conn.connection_type
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flatten_options_unwraps_variant_values() {
let opts = json!({
"routers": {"t":"s","v":"192.168.10.1"},
"lease_time": {"t":"s","v":"3600"},
});
let flat = flatten_options(&opts).unwrap();
assert_eq!(flat["routers"], json!("192.168.10.1"));
assert_eq!(flat["lease_time"], json!("3600"));
assert_eq!(flatten_options(&json!("x")), None);
}
#[test]
fn show_human_renders_typed_detail() {
let detail = NetworkDeviceDetail {
interface: "eth0".into(),
device_type: "ethernet".into(),
state: "activated".into(),
mac: "52:54:00:00:00:01".into(),
mtu: 1500,
ipv4: IpConfig {
addresses: vec!["192.0.2.10/24".into()],
gateway: "192.0.2.1".into(),
dns: vec!["1.1.1.1".into()],
domains: vec!["example.test".into()],
},
ipv6: Ipv6Config {
addresses: vec!["2001:db8::10/64".into()],
},
connection: Some(ActiveConnection {
id: "Wired".into(),
connection_type: "802-3-ethernet".into(),
default: true,
}),
dhcp4: None,
};
let human = render_show_human(&detail);
assert!(human.contains("Device: eth0"));
assert!(human.contains("IPv4: 192.0.2.10/24"));
assert!(human.contains("DNS: 1.1.1.1"));
assert!(human.contains("Connection: Wired (802-3-ethernet)"));
}
}