diff --git a/rust/agama-dbus-server/src/network/error.rs b/rust/agama-dbus-server/src/network/error.rs index 8c62c05f24..89f35d2ca0 100644 --- a/rust/agama-dbus-server/src/network/error.rs +++ b/rust/agama-dbus-server/src/network/error.rs @@ -15,6 +15,8 @@ pub enum NetworkStateError { InvalidIpMethod(u8), #[error("Invalid wireless mode: '{0}'")] InvalidWirelessMode(String), + #[error("Unknown parent kind '{0}'")] + UnknownParentKind(String), #[error("Connection '{0}' already exists")] ConnectionExists(Uuid), #[error("Invalid security wireless protocol: '{0}'")] diff --git a/rust/agama-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 1b6b49f76f..e6526b4c4d 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -6,6 +6,7 @@ use crate::network::error::NetworkStateError; use agama_lib::network::types::{DeviceType, SSID}; use cidr::IpInet; use std::{ + collections::HashMap, default::Default, fmt, net::IpAddr, @@ -219,6 +220,7 @@ pub enum Connection { Ethernet(EthernetConnection), Wireless(WirelessConnection), Loopback(LoopbackConnection), + Bond(BondConnection), } impl Connection { @@ -234,6 +236,10 @@ impl Connection { }), DeviceType::Loopback => Connection::Loopback(LoopbackConnection { base }), DeviceType::Ethernet => Connection::Ethernet(EthernetConnection { base }), + DeviceType::Bond => Connection::Bond(BondConnection { + base, + ..Default::default() + }), } } @@ -244,6 +250,7 @@ impl Connection { Connection::Ethernet(conn) => &conn.base, Connection::Wireless(conn) => &conn.base, Connection::Loopback(conn) => &conn.base, + Connection::Bond(conn) => &conn.base, } } @@ -252,6 +259,7 @@ impl Connection { Connection::Ethernet(conn) => &mut conn.base, Connection::Wireless(conn) => &mut conn.base, Connection::Loopback(conn) => &mut conn.base, + Connection::Bond(conn) => &mut conn.base, } } @@ -306,6 +314,37 @@ impl Connection { } } +#[derive(Debug, PartialEq, Clone)] +pub enum ParentKind { + Bond, +} + +impl fmt::Display for ParentKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + ParentKind::Bond => "bond", + }; + write!(f, "{}", name) + } +} + +impl FromStr for ParentKind { + type Err = NetworkStateError; + + fn from_str(s: &str) -> Result { + match s { + "bond" => Ok(ParentKind::Bond), + _ => Err(NetworkStateError::UnknownParentKind(s.to_string())), + } + } +} + +#[derive(Debug, Clone)] +pub struct Parent { + pub interface: String, + pub kind: ParentKind, +} + #[derive(Debug, Default, Clone)] pub struct BaseConnection { pub id: String, @@ -314,6 +353,7 @@ pub struct BaseConnection { pub status: Status, pub interface: String, pub match_config: MatchConfig, + pub parent: Option, } impl PartialEq for BaseConnection { @@ -408,6 +448,17 @@ pub struct LoopbackConnection { } #[derive(Debug, Default, PartialEq, Clone)] +pub struct BondConnection { + pub base: BaseConnection, + pub bond: BondConfig, +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct BondConfig { + pub options: HashMap, +} + +#[derive(Debug, Default, Clone, PartialEq)] pub struct WirelessConfig { pub mode: WirelessMode, pub ssid: SSID, diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index cb873b2267..dbe1b4a0c1 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -14,6 +14,7 @@ use uuid::Uuid; use zbus::zvariant::{self, OwnedValue, Value}; const ETHERNET_KEY: &str = "802-3-ethernet"; +const BOND_KEY: &str = "bond"; const WIRELESS_KEY: &str = "802-11-wireless"; const WIRELESS_SECURITY_KEY: &str = "802-11-wireless-security"; const LOOPBACK_KEY: &str = "loopback"; @@ -28,18 +29,30 @@ pub fn connection_to_dbus(conn: &Connection) -> NestedHash { ("type", ETHERNET_KEY.into()), ("interface-name", conn.interface().into()), ]); + if let Some(parent) = &conn.base().parent { + connection_dbus.insert("master", parent.interface.clone().into()); + connection_dbus.insert("slave-type", parent.kind.to_string().into()); + } result.insert("ipv4", ip_config_to_ipv4_dbus(conn.ip_config())); result.insert("ipv6", ip_config_to_ipv6_dbus(conn.ip_config())); result.insert("match", match_config_to_dbus(conn.match_config())); if let Connection::Wireless(wireless) = conn { - connection_dbus.insert("type", "802-11-wireless".into()); + connection_dbus.insert("type", WIRELESS_KEY.into()); let wireless_dbus = wireless_config_to_dbus(wireless); for (k, v) in wireless_dbus { result.insert(k, v); } } + if let Connection::Bond(bond) = conn { + connection_dbus.insert("type", BOND_KEY.into()); + let bond_dbus = bond_config_to_dbus(bond); + for (k, v) in bond_dbus { + result.insert(k, v); + } + } + result.insert("connection", connection_dbus); result } @@ -57,6 +70,13 @@ pub fn connection_from_dbus(conn: OwnedNestedHash) -> Option { })); } + if let Some(bond_config) = bond_config_from_dbus(&conn) { + return Some(Connection::Bond(BondConnection { + base, + bond: bond_config, + })); + } + if conn.get(LOOPBACK_KEY).is_some() { return Some(Connection::Loopback(LoopbackConnection { base })); }; @@ -211,6 +231,14 @@ fn wireless_config_to_dbus(conn: &WirelessConnection) -> NestedHash { ]) } +fn bond_config_to_dbus(conn: &BondConnection) -> NestedHash { + let config = &conn.bond; + let bond: HashMap<&str, zvariant::Value> = + HashMap::from([("options", Value::new(config.options.clone()))]); + + NestedHash::from([("bond", bond)]) +} + /// Converts a MatchConfig struct into a HashMap that can be sent over D-Bus. /// /// * `match_config`: MatchConfig to convert. @@ -412,6 +440,21 @@ fn wireless_config_from_dbus(conn: &OwnedNestedHash) -> Option { Some(wireless_config) } +fn bond_config_from_dbus(conn: &OwnedNestedHash) -> Option { + let Some(bond) = conn.get(BOND_KEY) else { + return None; + }; + + if let Some(dict) = bond.get("options") { + let dict: zvariant::Dict = dict.downcast_ref::()?.try_into().unwrap(); + if dict.full_signature() == "a{aa}" { + let options: HashMap = dict.try_into().unwrap(); + return Some(BondConfig { options }); + } + } + None +} + /// Determines whether a value is empty. /// /// TODO: Generalize for other kind of values, like dicts or arrays. diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index 5aa56c5145..729f68bc10 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -29,6 +29,7 @@ pub enum DeviceType { Loopback = 0, Ethernet = 1, Wireless = 2, + Bond = 3, } #[derive(Debug, Error, PartialEq)] @@ -43,6 +44,7 @@ impl TryFrom for DeviceType { 0 => Ok(DeviceType::Loopback), 1 => Ok(DeviceType::Ethernet), 2 => Ok(DeviceType::Wireless), + 3 => Ok(DeviceType::Bond), _ => Err(InvalidDeviceType(value)), } } diff --git a/rust/agama-migrate-wicked/src/interface.rs b/rust/agama-migrate-wicked/src/interface.rs index e8439bb68c..c22c10908f 100644 --- a/rust/agama-migrate-wicked/src/interface.rs +++ b/rust/agama-migrate-wicked/src/interface.rs @@ -1,8 +1,7 @@ -use agama_dbus_server::network::model::{self, IpConfig, IpMethod}; -use agama_lib::network::types::DeviceType; +use agama_dbus_server::network::model::{self, IpConfig, IpMethod, Parent}; use cidr::IpInet; use serde::{Deserialize, Deserializer, Serialize}; -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] @@ -21,6 +20,13 @@ pub struct Interface { pub ipv6_dhcp: Option, #[serde(rename = "ipv6-auto", skip_serializing_if = "Option::is_none")] pub ipv6_auto: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bond: Option, +} + +#[derive(Debug, PartialEq, Serialize, Clone, Deserialize)] +pub enum ParentKind { + Bond, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] @@ -35,7 +41,12 @@ pub struct Firewall {} #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] -pub struct Link {} +pub struct Link { + #[serde(rename = "master", skip_serializing_if = "Option::is_none")] + pub parent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, +} #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] #[serde(default)] @@ -113,13 +124,134 @@ where .collect()) } +#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct Bond { + pub mode: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub miimon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub arpmon: Option, + #[serde(deserialize_with = "unwrap_slaves")] + pub slaves: Vec, +} + +impl Bond { + pub fn primary(self: &Bond) -> Option<&String> { + for s in self.slaves.iter() { + if s.primary.is_some() && s.primary.unwrap() { + return Some(&s.device); + } + } + None + } +} + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct Slave { + pub device: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub primary: Option, +} + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct Miimon { + pub frequency: u32, + #[serde(rename = "carrier-detect")] + pub carrier_detect: String, +} + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ArpMon { + pub interval: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub validate: Option, + #[serde(rename = "validate-target")] + pub validate_target: Option, + pub targets: Vec, +} + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct ArpMonTargetAddressV4 { + #[serde(rename = "ipv4-address")] + pub ipv4_address: String, +} + +fn unwrap_slaves<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] + struct Slaves { + slave: Vec, + } + Ok(Slaves::deserialize(deserializer)?.slave) +} + +impl From for HashMap { + fn from(bond: Bond) -> HashMap { + let mut h: HashMap = HashMap::new(); + + h.insert(String::from("mode"), bond.mode.clone()); + if let Some(p) = bond.primary() { + h.insert(String::from("primary"), p.clone()); + } + + if let Some(m) = &bond.miimon { + h.insert(String::from("miimon"), format!("{}", m.frequency)); + } + + if let Some(a) = &bond.arpmon { + h.insert(String::from("arp_interval"), format!("{}", a.interval)); + if let Some(v) = &a.validate { + h.insert(String::from("arp_validate"), v.clone()); + } + + if !a.targets.is_empty() { + let sv = a + .targets + .iter() + .map(|c| c.ipv4_address.as_ref()) + .collect::>() + .join(","); + h.insert(String::from("arp_ip_target"), sv); + } + + if let Some(v) = &a.validate_target { + h.insert(String::from("arp_all_targets"), v.clone()); + } + } + h + } +} + impl From for model::Connection { - fn from(val: Interface) -> Self { - let mut con = model::Connection::new(val.name.clone(), DeviceType::Ethernet); - let base_connection = con.base_mut(); - base_connection.interface = val.name.clone(); - base_connection.ip_config = (&val).into(); - con + fn from(ifc: Interface) -> model::Connection { + let mut base = model::BaseConnection { + id: ifc.name.clone(), + interface: ifc.name.clone(), + ip_config: (&ifc).into(), + ..Default::default() + }; + + if ifc.link.kind.is_some() && ifc.link.parent.is_some() { + let interface = ifc.link.parent.clone().unwrap(); + let kind = match ifc.link.kind { + Some(p) => match &p { + ParentKind::Bond => model::ParentKind::Bond, + }, + None => panic!("Missing ParentType"), + }; + base.parent = Some(Parent { interface, kind }); + } + + if let Some(b) = ifc.bond { + model::Connection::Bond(model::BondConnection { + base, + bond: model::BondConfig { options: b.into() }, + }) + } else { + model::Connection::Ethernet(model::EthernetConnection { base }) + } } } diff --git a/rust/agama-migrate-wicked/src/reader.rs b/rust/agama-migrate-wicked/src/reader.rs index 98f645047f..549f5b813d 100644 --- a/rust/agama-migrate-wicked/src/reader.rs +++ b/rust/agama-migrate-wicked/src/reader.rs @@ -1,6 +1,7 @@ -use crate::interface::Interface; +use crate::interface::{Interface, ParentKind}; use quick_xml::de::from_str; use regex::Regex; +use std::collections::HashMap; use std::fs; use std::io; use std::path::PathBuf; @@ -19,13 +20,33 @@ fn replace_colons(colon_string: String) -> String { replaced } +pub fn post_process_interface(interfaces: &mut [Interface]) { + let mut helper = HashMap::new(); + for (idx, i) in interfaces.iter().enumerate() { + if let Some(parent) = &i.link.parent { + for j in interfaces.iter() { + if j.name == *parent && j.bond.is_some() { + helper.insert(idx, Some(ParentKind::Bond)); + } + } + } + } + for (_, (k, v)) in helper.iter().enumerate() { + if let Some(ifc) = interfaces.get_mut(*k) { + ifc.link.kind = v.clone(); + } + } +} + pub async fn read(path: PathBuf) -> Result, io::Error> { - let interfaces: Vec = if path.is_dir() { + let mut interfaces: Vec = if path.is_dir() { fs::read_dir(path)? + .filter(|r| !r.as_ref().unwrap().path().is_dir()) .flat_map(|res| res.map(|e| read_xml(e.path()).unwrap()).unwrap()) .collect::>() } else { read_xml(path).unwrap() }; + post_process_interface(&mut interfaces); Ok(interfaces) } diff --git a/rust/agama-migrate-wicked/tests/bond_active-backup.xml b/rust/agama-migrate-wicked/tests/bond_active-backup.xml new file mode 100644 index 0000000000..49390563b1 --- /dev/null +++ b/rust/agama-migrate-wicked/tests/bond_active-backup.xml @@ -0,0 +1,84 @@ + + bond0 + + boot + + + public + + + active-backup + + 100 + netif + + + + en0 + true + + + en1 + + + + + + true + true + + + true + group + default-route,hostname,dns,nis,ntp,smb,nds,mtu,tz,boot + 15 + true + false + + + true + prefer-public + false + + + true + group + dns,nis,ntp,tz,boot + auto + true + 15 + true + false + false + + + + en0 + + hotplug + + + bond0 + + + false + + + false + + + + en1 + + hotplug + + + bond0 + + + false + + + false + +