diff --git a/rust/agama-dbus-server/src/network/action.rs b/rust/agama-dbus-server/src/network/action.rs index 9f935f70f1..3ddf6687d9 100644 --- a/rust/agama-dbus-server/src/network/action.rs +++ b/rust/agama-dbus-server/src/network/action.rs @@ -1,5 +1,12 @@ use crate::network::model::Connection; use agama_lib::network::types::DeviceType; +use tokio::sync::oneshot; +use uuid::Uuid; + +use super::error::NetworkStateError; + +pub type Responder = oneshot::Sender; +pub type ControllerConnection = (Connection, Vec); /// Networking actions, like adding, updating or removing connections. /// @@ -9,6 +16,16 @@ use agama_lib::network::types::DeviceType; pub enum Action { /// Add a new connection with the given name and type. AddConnection(String, DeviceType), + /// Gets a connection + GetConnection(Uuid, Responder>), + /// Gets a controller connection + GetController( + Uuid, + Responder>, + ), + /// Sets a controller's ports. It uses the Uuid of the controller and the IDs or interface names + /// of the ports. + SetPorts(Uuid, Vec, Responder>), /// Update a connection (replacing the old one). UpdateConnection(Connection), /// Remove the connection with the given Uuid. diff --git a/rust/agama-dbus-server/src/network/dbus/interfaces.rs b/rust/agama-dbus-server/src/network/dbus/interfaces.rs index 23ebdbb784..0467efc8a6 100644 --- a/rust/agama-dbus-server/src/network/dbus/interfaces.rs +++ b/rust/agama-dbus-server/src/network/dbus/interfaces.rs @@ -2,19 +2,22 @@ //! //! This module contains the set of D-Bus interfaces that are exposed by [D-Bus network //! service](crate::NetworkService). + use super::ObjectsRegistry; use crate::network::{ action::Action, error::NetworkStateError, model::{ - Connection as NetworkConnection, Device as NetworkDevice, MacAddress, WirelessConnection, + BondConnection, Connection as NetworkConnection, Device as NetworkDevice, MacAddress, + WirelessConnection, }, }; use agama_lib::network::types::SSID; use std::{str::FromStr, sync::Arc}; -use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::{mpsc::UnboundedSender, oneshot}; use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard}; +use uuid::Uuid; use zbus::{ dbus_interface, zvariant::{ObjectPath, OwnedObjectPath}, @@ -229,9 +232,31 @@ impl Connection { self.get_connection().await.id().to_string() } + /// Connection UUID. + /// + /// Unique identifier of the network connection. It may or not be the same that the used by the + /// backend. + #[dbus_interface(property)] + pub async fn uuid(&self) -> String { + self.get_connection().await.uuid().to_string() + } + + #[dbus_interface(property)] + pub async fn controller(&self) -> String { + let connection = self.get_connection().await; + match connection.controller() { + Some(uuid) => uuid.to_string(), + None => "".to_string(), + } + } + #[dbus_interface(property)] pub async fn interface(&self) -> String { - self.get_connection().await.interface().to_string() + self.get_connection() + .await + .interface() + .unwrap_or("") + .to_string() } #[dbus_interface(property)] @@ -296,6 +321,106 @@ impl Match { } } +/// D-Bus interface for Bond settings. +pub struct Bond { + actions: Arc>>, + uuid: Uuid, +} + +impl Bond { + /// Creates a Bond interface object. + /// + /// * `actions`: sending-half of a channel to send actions. + /// * `uuid`: connection UUID. + pub fn new(actions: UnboundedSender, uuid: Uuid) -> Self { + Self { + actions: Arc::new(Mutex::new(actions)), + uuid, + } + } + + /// Gets the bond connection. + /// + /// Beware that it crashes when it is not a bond connection. + async fn get_bond(&self) -> BondConnection { + let actions = self.actions.lock().await; + let (tx, rx) = oneshot::channel(); + actions.send(Action::GetConnection(self.uuid, tx)).unwrap(); + let connection = rx.await.unwrap(); + + match connection { + Some(NetworkConnection::Bond(config)) => config, + _ => panic!("Not a bond connection. This is most probably a bug."), + } + } + + /// Updates the connection data in the NetworkSystem. + /// + /// * `connection`: Updated connection. + async fn update_connection<'a>(&self, connection: BondConnection) -> zbus::fdo::Result<()> { + let actions = self.actions.lock().await; + let connection = NetworkConnection::Bond(connection.clone()); + actions.send(Action::UpdateConnection(connection)).unwrap(); + Ok(()) + } +} + +#[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.Bond")] +impl Bond { + /// Bonding mode. + #[dbus_interface(property)] + pub async fn mode(&self) -> String { + let connection = self.get_bond().await; + connection.bond.mode.to_string() + } + + #[dbus_interface(property)] + pub async fn set_mode(&mut self, mode: &str) -> zbus::fdo::Result<()> { + let mut connection = self.get_bond().await; + connection.set_mode(mode.try_into()?); + self.update_connection(connection).await + } + + /// List of bonding options. + #[dbus_interface(property)] + pub async fn options(&self) -> String { + let connection = self.get_bond().await; + connection.bond.options.to_string() + } + + #[dbus_interface(property)] + pub async fn set_options(&mut self, opts: &str) -> zbus::fdo::Result<()> { + let mut connection = self.get_bond().await; + connection.set_options(opts.try_into()?); + self.update_connection(connection).await + } + + /// List of bond ports. + /// + /// For the port names, it uses the interface name (preferred) or, as a fallback, + /// the connection ID of the port. + #[dbus_interface(property)] + pub async fn ports(&self) -> zbus::fdo::Result> { + let actions = self.actions.lock().await; + let (tx, rx) = oneshot::channel(); + actions.send(Action::GetController(self.uuid, tx)).unwrap(); + + let (_, ports) = rx.await.unwrap()?; + Ok(ports) + } + + #[dbus_interface(property)] + pub async fn set_ports(&mut self, ports: Vec) -> zbus::fdo::Result<()> { + let actions = self.actions.lock().await; + let (tx, rx) = oneshot::channel(); + actions + .send(Action::SetPorts(self.uuid, ports, tx)) + .unwrap(); + let result = rx.await.unwrap(); + Ok(result?) + } +} + #[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.Match")] impl Match { /// List of driver names to match. @@ -477,7 +602,6 @@ impl Wireless { connection.wireless.security = security .try_into() .map_err(|_| NetworkStateError::InvalidSecurityProtocol(security.to_string()))?; - self.update_connection(connection).await?; - Ok(()) + self.update_connection(connection).await } } diff --git a/rust/agama-dbus-server/src/network/dbus/tree.rs b/rust/agama-dbus-server/src/network/dbus/tree.rs index 28a9e34053..f2e9f0263e 100644 --- a/rust/agama-dbus-server/src/network/dbus/tree.rs +++ b/rust/agama-dbus-server/src/network/dbus/tree.rs @@ -88,6 +88,7 @@ impl Tree { let mut objects = self.objects.lock().await; let orig_id = conn.id().to_owned(); + let uuid = conn.uuid(); let (id, path) = objects.register_connection(conn); if id != conn.id() { conn.set_id(&id) @@ -112,6 +113,12 @@ impl Tree { interfaces::Match::new(self.actions.clone(), Arc::clone(&cloned)), ) .await?; + + if let Connection::Bond(_) = conn { + self.add_interface(&path, interfaces::Bond::new(self.actions.clone(), uuid)) + .await?; + } + if let Connection::Wireless(_) = conn { self.add_interface( &path, @@ -185,6 +192,7 @@ impl Tree { /// * `path`: connection D-Bus path. async fn remove_connection_on(&self, path: &str) -> Result<(), ServiceError> { let object_server = self.connection.object_server(); + _ = object_server.remove::(path).await; _ = object_server.remove::(path).await; object_server.remove::(path).await?; object_server diff --git a/rust/agama-dbus-server/src/network/error.rs b/rust/agama-dbus-server/src/network/error.rs index 8c62c05f24..e3873eaa27 100644 --- a/rust/agama-dbus-server/src/network/error.rs +++ b/rust/agama-dbus-server/src/network/error.rs @@ -5,7 +5,7 @@ use uuid::Uuid; /// Errors that are related to the network configuration. #[derive(Error, Debug)] pub enum NetworkStateError { - #[error("Unknown connection with ID: '{0}'")] + #[error("Unknown connection '{0}'")] UnknownConnection(String), #[error("Invalid connection UUID: '{0}'")] InvalidUuid(String), @@ -21,6 +21,12 @@ pub enum NetworkStateError { InvalidSecurityProtocol(String), #[error("Adapter error: '{0}'")] AdapterError(String), + #[error("Invalid bond mode '{0}'")] + InvalidBondMode(String), + #[error("Invalid bond options")] + InvalidBondOptions, + #[error("Not a controller connection: '{0}'")] + NotControllerConnection(String), } impl From for zbus::fdo::Error { diff --git a/rust/agama-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 9d48127a19..7091e746d3 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -3,7 +3,7 @@ //! * This module contains the types that represent the network concepts. They are supposed to be //! agnostic from the real network service (e.g., NetworkManager). use crate::network::error::NetworkStateError; -use agama_lib::network::types::{DeviceType, SSID}; +use agama_lib::network::types::{BondMode, DeviceType, SSID}; use cidr::IpInet; use std::{ collections::HashMap, @@ -15,8 +15,10 @@ use std::{ use thiserror::Error; use uuid::Uuid; use zbus::zvariant::Value; +mod builder; +pub use builder::ConnectionBuilder; -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct NetworkState { pub devices: Vec, pub connections: Vec, @@ -43,18 +45,41 @@ impl NetworkState { /// Get connection by UUID /// - /// * `uuid`: connection UUID + /// * `id`: connection UUID + pub fn get_connection_by_uuid(&self, uuid: Uuid) -> Option<&Connection> { + self.connections.iter().find(|c| c.uuid() == uuid) + } + + /// Get connection by interface + /// + /// * `name`: connection interface name + pub fn get_connection_by_interface(&self, name: &str) -> Option<&Connection> { + let interface = Some(name); + self.connections.iter().find(|c| c.interface() == interface) + } + + /// Get connection by ID + /// + /// * `id`: connection ID pub fn get_connection(&self, id: &str) -> Option<&Connection> { self.connections.iter().find(|c| c.id() == id) } - /// Get connection by UUID as mutable + /// Get connection by ID as mutable /// - /// * `uuid`: connection UUID + /// * `id`: connection ID pub fn get_connection_mut(&mut self, id: &str) -> Option<&mut Connection> { self.connections.iter_mut().find(|c| c.id() == id) } + pub fn get_controlled_by(&mut self, uuid: Uuid) -> Vec<&Connection> { + let uuid = Some(uuid); + self.connections + .iter() + .filter(|c| c.controller() == uuid) + .collect() + } + /// Adds a new connection. /// /// It uses the `id` to decide whether the connection already exists. @@ -62,8 +87,8 @@ impl NetworkState { if self.get_connection(conn.id()).is_some() { return Err(NetworkStateError::ConnectionExists(conn.uuid())); } - self.connections.push(conn); + Ok(()) } @@ -76,8 +101,8 @@ impl NetworkState { let Some(old_conn) = self.get_connection_mut(conn.id()) else { return Err(NetworkStateError::UnknownConnection(conn.id().to_string())); }; - *old_conn = conn; + Ok(()) } @@ -92,14 +117,50 @@ impl NetworkState { conn.remove(); Ok(()) } + + /// Sets a controller's ports. + /// + /// If the connection is not a controller, returns an error. + /// + /// * `controller`: controller to set ports on. + /// * `ports`: list of port names (using the connection ID or the interface name). + pub fn set_ports( + &mut self, + controller: &Connection, + ports: Vec, + ) -> Result<(), NetworkStateError> { + if let Connection::Bond(_) = &controller { + let mut controlled = vec![]; + for port in ports { + let connection = self + .get_connection_by_interface(&port) + .or_else(|| self.get_connection(&port)) + .ok_or(NetworkStateError::UnknownConnection(port))?; + controlled.push(connection.uuid()); + } + + for conn in self.connections.iter_mut() { + if controlled.contains(&conn.uuid()) { + conn.set_controller(controller.uuid()); + } else if conn.controller() == Some(controller.uuid()) { + conn.unset_controller(); + } + } + Ok(()) + } else { + Err(NetworkStateError::NotControllerConnection( + controller.id().to_owned(), + )) + } + } } #[cfg(test)] mod tests { - use uuid::Uuid; - + use super::builder::ConnectionBuilder; use super::*; use crate::network::error::NetworkStateError; + use uuid::Uuid; #[test] fn test_add_connection() { @@ -206,6 +267,61 @@ mod tests { let conn = Connection::Loopback(LoopbackConnection { base }); assert!(conn.is_loopback()); } + + #[test] + fn test_set_bonding_ports() { + let mut state = NetworkState::default(); + let eth0 = ConnectionBuilder::new("eth0") + .with_interface("eth0") + .build(); + let eth1 = ConnectionBuilder::new("eth1") + .with_interface("eth1") + .build(); + let bond0 = ConnectionBuilder::new("bond0") + .with_type(DeviceType::Bond) + .build(); + + state.add_connection(eth0).unwrap(); + state.add_connection(eth1).unwrap(); + state.add_connection(bond0.clone()).unwrap(); + + state.set_ports(&bond0, vec!["eth1".to_string()]).unwrap(); + + let eth1_found = state.get_connection("eth1").unwrap(); + assert_eq!(eth1_found.controller(), Some(bond0.uuid())); + let eth0_found = state.get_connection("eth0").unwrap(); + assert_eq!(eth0_found.controller(), None); + } + + #[test] + fn test_set_bonding_missing_port() { + let mut state = NetworkState::default(); + let bond0 = ConnectionBuilder::new("bond0") + .with_type(DeviceType::Bond) + .build(); + state.add_connection(bond0.clone()).unwrap(); + + let error = state + .set_ports(&bond0, vec!["eth0".to_string()]) + .unwrap_err(); + dbg!(&error); + assert!(matches!(error, NetworkStateError::UnknownConnection(_))); + } + + #[test] + fn test_set_non_controller_ports() { + let mut state = NetworkState::default(); + let eth0 = ConnectionBuilder::new("eth0").build(); + state.add_connection(eth0.clone()).unwrap(); + + let error = state + .set_ports(ð0, vec!["eth1".to_string()]) + .unwrap_err(); + assert!(matches!( + error, + NetworkStateError::NotControllerConnection(_), + )); + } } /// Network device @@ -222,12 +338,14 @@ pub enum Connection { Wireless(WirelessConnection), Loopback(LoopbackConnection), Dummy(DummyConnection), + Bond(BondConnection), } impl Connection { pub fn new(id: String, device_type: DeviceType) -> Self { let base = BaseConnection { id, + uuid: Uuid::new_v4(), ..Default::default() }; match device_type { @@ -238,6 +356,10 @@ impl Connection { DeviceType::Loopback => Connection::Loopback(LoopbackConnection { base }), DeviceType::Ethernet => Connection::Ethernet(EthernetConnection { base }), DeviceType::Dummy => Connection::Dummy(DummyConnection { base }), + DeviceType::Bond => Connection::Bond(BondConnection { + base, + ..Default::default() + }), } } @@ -249,6 +371,7 @@ impl Connection { Connection::Wireless(conn) => &conn.base, Connection::Loopback(conn) => &conn.base, Connection::Dummy(conn) => &conn.base, + Connection::Bond(conn) => &conn.base, } } @@ -258,6 +381,7 @@ impl Connection { Connection::Wireless(conn) => &mut conn.base, Connection::Loopback(conn) => &mut conn.base, Connection::Dummy(conn) => &mut conn.base, + Connection::Bond(conn) => &mut conn.base, } } @@ -269,19 +393,34 @@ impl Connection { self.base_mut().id = id.to_string() } - pub fn interface(&self) -> &str { - self.base().interface.as_str() + pub fn interface(&self) -> Option<&str> { + self.base().interface.as_deref() } pub fn set_interface(&mut self, interface: &str) { - self.base_mut().interface = interface.to_string() + self.base_mut().interface = Some(interface.to_string()) + } + + /// A port's controller name, e.g.: bond0, br0 + pub fn controller(&self) -> Option { + self.base().controller + } + + /// Sets the port's controller. + /// + /// `controller`: Uuid of the controller (Bridge, Bond, Team), e.g.: bond0. + pub fn set_controller(&mut self, controller: Uuid) { + self.base_mut().controller = Some(controller) + } + + pub fn unset_controller(&mut self) { + self.base_mut().controller = None; } pub fn uuid(&self) -> Uuid { self.base().uuid } - /// FIXME: rename to ip_config pub fn ip_config(&self) -> &IpConfig { &self.base().ip_config } @@ -315,6 +454,7 @@ impl Connection { matches!(self, Connection::Loopback(_)) || matches!(self, Connection::Ethernet(_)) || matches!(self, Connection::Dummy(_)) + || matches!(self, Connection::Bond(_)) } pub fn mac_address(&self) -> String { @@ -333,7 +473,8 @@ pub struct BaseConnection { pub mac_address: MacAddress, pub ip_config: IpConfig, pub status: Status, - pub interface: String, + pub interface: Option, + pub controller: Option, pub match_config: MatchConfig, } @@ -556,6 +697,61 @@ pub struct DummyConnection { pub base: BaseConnection, } +#[derive(Debug, Default, PartialEq, Clone)] +pub struct BondConnection { + pub base: BaseConnection, + pub bond: BondConfig, +} + +impl BondConnection { + pub fn set_mode(&mut self, mode: BondMode) { + self.bond.mode = mode; + } + + pub fn set_options(&mut self, options: BondOptions) { + self.bond.options = options; + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct BondOptions(pub HashMap); + +impl TryFrom<&str> for BondOptions { + type Error = NetworkStateError; + + fn try_from(value: &str) -> Result { + let mut options = HashMap::new(); + + for opt in value.split_whitespace() { + let (key, value) = opt + .trim() + .split_once('=') + .ok_or(NetworkStateError::InvalidBondOptions)?; + options.insert(key.to_string(), value.to_string()); + } + + Ok(BondOptions(options)) + } +} + +impl fmt::Display for BondOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let opts = &self + .0 + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>(); + + write!(f, "{}", opts.join(" ")) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct BondConfig { + pub mode: BondMode, + pub options: BondOptions, +} + #[derive(Debug, Default, PartialEq, Clone)] pub struct WirelessConfig { pub mode: WirelessMode, diff --git a/rust/agama-dbus-server/src/network/model/builder.rs b/rust/agama-dbus-server/src/network/model/builder.rs new file mode 100644 index 0000000000..758dd52be6 --- /dev/null +++ b/rust/agama-dbus-server/src/network/model/builder.rs @@ -0,0 +1,48 @@ +use super::{Connection, DeviceType}; +use uuid::Uuid; + +#[derive(Debug, Default)] +pub struct ConnectionBuilder { + id: String, + interface: Option, + controller: Option, + type_: Option, +} + +impl ConnectionBuilder { + pub fn new(id: &str) -> Self { + Self { + id: id.to_string(), + ..Default::default() + } + } + + pub fn with_interface(mut self, interface: &str) -> Self { + self.interface = Some(interface.to_string()); + self + } + + pub fn with_controller(mut self, controller: Uuid) -> Self { + self.controller = Some(controller); + self + } + + pub fn with_type(mut self, type_: DeviceType) -> Self { + self.type_ = Some(type_); + self + } + + pub fn build(self) -> Connection { + let mut conn = Connection::new(self.id, self.type_.unwrap_or(DeviceType::Ethernet)); + + if let Some(interface) = self.interface { + conn.set_interface(&interface); + } + + if let Some(controller) = self.controller { + conn.set_controller(controller); + } + + conn + } +} diff --git a/rust/agama-dbus-server/src/network/nm/adapter.rs b/rust/agama-dbus-server/src/network/nm/adapter.rs index a5d3c4d811..26a7f75b1e 100644 --- a/rust/agama-dbus-server/src/network/nm/adapter.rs +++ b/rust/agama-dbus-server/src/network/nm/adapter.rs @@ -39,21 +39,33 @@ impl<'a> Adapter for NetworkManagerAdapter<'a> { }) } + /// Writes the connections to NetworkManager. + /// + /// Internally, it creates an ordered list of connections before processing them. The reason is + /// that using async recursive functions is giving us some troubles, so we decided to go with a + /// simpler approach. + /// + /// * `network`: network model. fn write(&self, network: &NetworkState) -> Result<(), Box> { // By now, traits do not support async functions. Using `task::block_on` allows // to use 'await'. task::block_in_place(|| { Handle::current().block_on(async { - for conn in &network.connections { + for conn in ordered_connections(network) { if !Self::is_writable(conn) { continue; } - if conn.is_removed() { - if let Err(e) = self.client.remove_connection(conn.uuid()).await { - log::error!("Could not remove the connection {}: {}", conn.id(), e); - } - } else if let Err(e) = self.client.add_or_update_connection(conn).await { - log::error!("Could not add/update the connection {}: {}", conn.id(), e); + let result = if conn.is_removed() { + self.client.remove_connection(conn.uuid()).await + } else { + let ctrl = conn + .controller() + .and_then(|uuid| network.get_connection_by_uuid(uuid)); + self.client.add_or_update_connection(conn, ctrl).await + }; + + if let Err(e) = result { + log::error!("Could not process the connection {}: {}", conn.id(), e); } } }) @@ -62,3 +74,29 @@ impl<'a> Adapter for NetworkManagerAdapter<'a> { Ok(()) } } + +/// Returns the connections in the order they should be processed. +/// +/// * `network`: network model. +fn ordered_connections(network: &NetworkState) -> Vec<&Connection> { + let mut conns: Vec<&Connection> = vec![]; + for conn in &network.connections { + add_ordered_connections(conn, network, &mut conns); + } + conns +} + +fn add_ordered_connections<'b>( + conn: &'b Connection, + network: &'b NetworkState, + conns: &mut Vec<&'b Connection>, +) { + if let Some(uuid) = conn.controller() { + let controller = network.get_connection_by_uuid(uuid).unwrap(); + add_ordered_connections(controller, network, conns); + } + + if !conns.contains(&conn) { + conns.push(conn); + } +} diff --git a/rust/agama-dbus-server/src/network/nm/client.rs b/rust/agama-dbus-server/src/network/nm/client.rs index 9c859b586d..a37486eafa 100644 --- a/rust/agama-dbus-server/src/network/nm/client.rs +++ b/rust/agama-dbus-server/src/network/nm/client.rs @@ -1,5 +1,10 @@ //! NetworkManager client. -use super::dbus::{connection_from_dbus, connection_to_dbus, merge_dbus_connections}; +use std::collections::HashMap; + +use super::dbus::{ + cleanup_dbus_connection, connection_from_dbus, connection_to_dbus, controller_from_dbus, + merge_dbus_connections, +}; use super::model::NmDeviceType; use super::proxies::{ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy}; use crate::network::model::{Connection, Device}; @@ -68,6 +73,9 @@ impl<'a> NetworkManagerClient<'a> { /// Returns the list of network connections. pub async fn connections(&self) -> Result, ServiceError> { + let mut controlled_by: HashMap = HashMap::new(); + let mut uuids_map: HashMap = HashMap::new(); + let proxy = SettingsProxy::new(&self.connection).await?; let paths = proxy.list_connections().await?; let mut connections: Vec = Vec::with_capacity(paths.len()); @@ -77,19 +85,47 @@ impl<'a> NetworkManagerClient<'a> { .build() .await?; let settings = proxy.get_settings().await?; - // TODO: log an error if a connection is not found - if let Some(connection) = connection_from_dbus(settings) { + + if let Some(connection) = connection_from_dbus(settings.clone()) { + if let Some(controller) = controller_from_dbus(&settings) { + controlled_by.insert(connection.uuid(), controller.to_string()); + } + if let Some(iname) = connection.interface() { + uuids_map.insert(iname.to_string(), connection.uuid()); + } connections.push(connection); } } + + for conn in connections.iter_mut() { + let Some(interface_name) = controlled_by.get(&conn.uuid()) else { + continue; + }; + + if let Some(uuid) = uuids_map.get(interface_name) { + conn.set_controller(*uuid); + } else { + log::warn!( + "Could not found a connection for the interface '{}' (required by connection '{}')", + interface_name, + conn.id() + ); + } + } + Ok(connections) } /// Adds or updates a connection if it already exists. /// /// * `conn`: connection to add or update. - pub async fn add_or_update_connection(&self, conn: &Connection) -> Result<(), ServiceError> { - let new_conn = connection_to_dbus(conn); + pub async fn add_or_update_connection( + &self, + conn: &Connection, + controller: Option<&Connection>, + ) -> Result<(), ServiceError> { + let mut new_conn = connection_to_dbus(conn, controller); + let path = if let Ok(proxy) = self.get_connection_proxy(conn.uuid()).await { let original = proxy.get_settings().await?; let merged = merge_dbus_connections(&original, &new_conn); @@ -97,8 +133,10 @@ impl<'a> NetworkManagerClient<'a> { OwnedObjectPath::from(proxy.path().to_owned()) } else { let proxy = SettingsProxy::new(&self.connection).await?; + cleanup_dbus_connection(&mut new_conn); proxy.add_connection(new_conn).await? }; + self.activate_connection(path).await?; Ok(()) } diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index f19100065b..de16523628 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -6,7 +6,7 @@ use super::model::*; use crate::network::model::*; use agama_lib::{ dbus::{NestedHash, OwnedNestedHash}, - network::types::SSID, + network::types::{BondMode, SSID}, }; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr, str::FromStr}; @@ -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"; @@ -22,13 +23,27 @@ const DUMMY_KEY: &str = "dummy"; /// Converts a connection struct into a HashMap that can be sent over D-Bus. /// /// * `conn`: Connection to convert. -pub fn connection_to_dbus(conn: &Connection) -> NestedHash { +pub fn connection_to_dbus<'a>( + conn: &'a Connection, + controller: Option<&'a Connection>, +) -> NestedHash<'a> { let mut result = NestedHash::new(); - let mut connection_dbus = HashMap::from([ - ("id", conn.id().into()), - ("type", ETHERNET_KEY.into()), - ("interface-name", conn.interface().into()), - ]); + let mut connection_dbus = + HashMap::from([("id", conn.id().into()), ("type", ETHERNET_KEY.into())]); + + if let Some(interface) = &conn.interface() { + connection_dbus.insert("interface-name", interface.to_owned().into()); + } + + if let Some(controller) = controller { + connection_dbus.insert("slave-type", "bond".into()); // TODO: only 'bond' is supported + let master = controller.interface().unwrap_or(controller.id()); + connection_dbus.insert("master", master.into()); + } else { + connection_dbus.insert("slave-type", "".into()); // TODO: only 'bond' is supported + connection_dbus.insert("master", "".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())); @@ -37,16 +52,27 @@ pub fn connection_to_dbus(conn: &Connection) -> NestedHash { let ethernet_config = HashMap::from([("assigned-mac-address", Value::new(conn.mac_address()))]); result.insert(ETHERNET_KEY, ethernet_config); - } else if let Connection::Wireless(wireless) = conn { - 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::Dummy(_) = conn { - connection_dbus.insert("type", DUMMY_KEY.into()); + match &conn { + Connection::Wireless(wireless) => { + 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); + } + } + Connection::Bond(bond) => { + connection_dbus.insert("type", BOND_KEY.into()); + if !connection_dbus.contains_key("interface-name") { + connection_dbus.insert("interface-name", conn.id().into()); + } + result.insert("bond", bond_config_to_dbus(bond)); + } + Connection::Dummy(_) => { + connection_dbus.insert("type", DUMMY_KEY.into()); + } + _ => {} } result.insert("connection", connection_dbus); @@ -66,6 +92,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(DUMMY_KEY).is_some() { return Some(Connection::Dummy(DummyConnection { base })); }; @@ -115,11 +148,19 @@ pub fn merge_dbus_connections<'a>( /// replaced with "address-data". However, if "addresses" is present, it takes precedence. /// /// * `conn`: connection represented as a NestedHash. -fn cleanup_dbus_connection(conn: &mut NestedHash) { +pub fn cleanup_dbus_connection(conn: &mut NestedHash) { if let Some(connection) = conn.get_mut("connection") { if connection.get("interface-name").is_some_and(is_empty_value) { connection.remove("interface-name"); } + + if connection.get("master").is_some_and(is_empty_value) { + connection.remove("master"); + } + + if connection.get("slave-type").is_some_and(is_empty_value) { + connection.remove("slave-type"); + } } if let Some(ipv4) = conn.get_mut("ipv4") { @@ -133,6 +174,16 @@ fn cleanup_dbus_connection(conn: &mut NestedHash) { } } +/// Ancillary function to get the controller for a given interface. +pub fn controller_from_dbus(conn: &OwnedNestedHash) -> Option { + let Some(connection) = conn.get("connection") else { + return None; + }; + + let master: &str = connection.get("master")?.downcast_ref()?; + Some(master.to_string()) +} + fn ip_config_to_ipv4_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value> { let addresses: Vec> = ip_config .addresses @@ -241,10 +292,16 @@ fn wireless_config_to_dbus(conn: &WirelessConnection) -> NestedHash { security.insert("psk", password.to_string().into()); } - NestedHash::from([ - ("802-11-wireless", wireless), - ("802-11-wireless-security", security), - ]) + NestedHash::from([(WIRELESS_KEY, wireless), (WIRELESS_SECURITY_KEY, security)]) +} + +fn bond_config_to_dbus(conn: &BondConnection) -> HashMap<&str, zvariant::Value> { + let config = &conn.bond; + + let mut options = config.options.0.clone(); + options.insert("mode".to_string(), config.mode.to_string()); + + HashMap::from([("options", Value::new(options))]) } /// Converts a MatchConfig struct into a HashMap that can be sent over D-Bus. @@ -275,6 +332,7 @@ fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option { let id: &str = connection.get("id")?.downcast_ref()?; let uuid: &str = connection.get("uuid")?.downcast_ref()?; let uuid: Uuid = uuid.try_into().ok()?; + let mut base_connection = BaseConnection { id: id.to_string(), uuid, @@ -283,7 +341,7 @@ fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option { if let Some(interface) = connection.get("interface-name") { let interface: &str = interface.downcast_ref()?; - base_connection.interface = interface.to_string(); + base_connection.interface = Some(interface.to_string()); } if let Some(match_config) = conn.get("match") { @@ -489,6 +547,28 @@ 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; + }; + + let dict: &zvariant::Dict = bond.get("options")?.downcast_ref()?; + + let mut options = >::try_from(dict.clone()).unwrap(); + let mode = options.remove("mode"); + + let mut bond = BondConfig { + options: BondOptions(options), + ..Default::default() + }; + + if let Some(mode) = mode { + bond.mode = BondMode::try_from(mode.as_str()).unwrap_or_default(); + } + + Some(bond) +} + /// Determines whether a value is empty. /// /// TODO: Generalize for other kind of values, like dicts or arrays. @@ -508,8 +588,11 @@ mod test { connection_from_dbus, connection_to_dbus, merge_dbus_connections, NestedHash, OwnedNestedHash, }; - use crate::network::{model::*, nm::dbus::ETHERNET_KEY}; - use agama_lib::network::types::SSID; + use crate::network::{ + model::*, + nm::dbus::{BOND_KEY, ETHERNET_KEY, WIRELESS_KEY, WIRELESS_SECURITY_KEY}, + }; + use agama_lib::network::types::{BondMode, SSID}; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr, str::FromStr}; use uuid::Uuid; @@ -672,8 +755,8 @@ mod test { let dbus_conn = HashMap::from([ ("connection".to_string(), connection_section), - ("802-11-wireless".to_string(), wireless_section), - ("802-11-wireless-security".to_string(), security_section), + (WIRELESS_KEY.to_string(), wireless_section), + (WIRELESS_SECURITY_KEY.to_string(), security_section), ]); let connection = connection_from_dbus(dbus_conn).unwrap(); @@ -686,6 +769,34 @@ mod test { } } + #[test] + fn test_connection_from_dbus_bonding() { + let uuid = Uuid::new_v4().to_string(); + let connection_section = HashMap::from([ + ("id".to_string(), Value::new("bond0").to_owned()), + ("uuid".to_string(), Value::new(uuid).to_owned()), + ]); + + let bond_options = Value::new(HashMap::from([( + "options".to_string(), + HashMap::from([("mode".to_string(), Value::new("active-backup").to_owned())]), + )])); + + let dbus_conn = HashMap::from([ + ( + "connection".to_string(), + connection_section.try_into().unwrap(), + ), + (BOND_KEY.to_string(), bond_options.try_into().unwrap()), + ]); + + let connection = connection_from_dbus(dbus_conn).unwrap(); + assert!(matches!(connection, Connection::Bond(_))); + if let Connection::Bond(connection) = connection { + assert_eq!(connection.bond.mode, BondMode::ActiveBackup); + } + } + #[test] fn test_dbus_from_wireless_connection() { let config = WirelessConfig { @@ -700,9 +811,9 @@ mod test { ..Default::default() }; let wireless = Connection::Wireless(wireless); - let wireless_dbus = connection_to_dbus(&wireless); + let wireless_dbus = connection_to_dbus(&wireless, None); - let wireless = wireless_dbus.get("802-11-wireless").unwrap(); + let wireless = wireless_dbus.get(WIRELESS_KEY).unwrap(); let mode: &str = wireless.get("mode").unwrap().downcast_ref().unwrap(); assert_eq!(mode, "infrastructure"); let mac_address: &str = wireless @@ -720,7 +831,7 @@ mod test { .collect(); assert_eq!(ssid, "agama".as_bytes()); - let security = wireless_dbus.get("802-11-wireless-security").unwrap(); + let security = wireless_dbus.get(WIRELESS_SECURITY_KEY).unwrap(); let key_mgmt: &str = security.get("key-mgmt").unwrap().downcast_ref().unwrap(); assert_eq!(key_mgmt, "wpa-psk"); } @@ -728,7 +839,7 @@ mod test { #[test] fn test_dbus_from_ethernet_connection() { let ethernet = build_ethernet_connection(); - let ethernet_dbus = connection_to_dbus(ðernet); + let ethernet_dbus = connection_to_dbus(ðernet, None); check_dbus_base_connection(ðernet_dbus); } @@ -778,7 +889,7 @@ mod test { let base = BaseConnection { id: "agama".to_string(), - interface: "eth0".to_string(), + interface: Some("eth0".to_string()), ..Default::default() }; let ethernet = EthernetConnection { @@ -786,7 +897,7 @@ mod test { ..Default::default() }; let updated = Connection::Ethernet(ethernet); - let updated = connection_to_dbus(&updated); + let updated = connection_to_dbus(&updated, None); let merged = merge_dbus_connections(&original, &updated); let connection = merged.get("connection").unwrap(); @@ -846,7 +957,7 @@ mod test { let mut updated = Connection::Ethernet(EthernetConnection::default()); updated.set_interface(""); updated.set_mac_address(MacAddress::Unset); - let updated = connection_to_dbus(&updated); + let updated = connection_to_dbus(&updated, None); let merged = merge_dbus_connections(&original, &updated); let connection = merged.get("connection").unwrap(); diff --git a/rust/agama-dbus-server/src/network/nm/model.rs b/rust/agama-dbus-server/src/network/nm/model.rs index 9175b8b342..1f8e967aff 100644 --- a/rust/agama-dbus-server/src/network/nm/model.rs +++ b/rust/agama-dbus-server/src/network/nm/model.rs @@ -84,6 +84,7 @@ impl TryFrom for DeviceType { NmDeviceType(1) => Ok(DeviceType::Ethernet), NmDeviceType(2) => Ok(DeviceType::Wireless), NmDeviceType(3) => Ok(DeviceType::Dummy), + NmDeviceType(10) => Ok(DeviceType::Bond), NmDeviceType(_) => Err(NmError::UnsupportedDeviceType(value.into())), } } diff --git a/rust/agama-dbus-server/src/network/system.rs b/rust/agama-dbus-server/src/network/system.rs index fcfb3e170e..fa17121d8b 100644 --- a/rust/agama-dbus-server/src/network/system.rs +++ b/rust/agama-dbus-server/src/network/system.rs @@ -1,6 +1,8 @@ +use super::error::NetworkStateError; use crate::network::{dbus::Tree, model::Connection, Action, Adapter, NetworkState}; use std::error::Error; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use uuid::Uuid; /// Represents the network system using holding the state and setting up the D-Bus tree. pub struct NetworkSystem { @@ -70,6 +72,18 @@ impl NetworkSystem { self.tree.add_connection(&mut conn, true).await?; self.state.add_connection(conn)?; } + Action::GetConnection(uuid, rx) => { + let conn = self.state.get_connection_by_uuid(uuid); + rx.send(conn.cloned()).unwrap(); + } + Action::GetController(uuid, rx) => { + let result = self.get_controller_action(uuid); + rx.send(result).unwrap() + } + Action::SetPorts(uuid, ports, rx) => { + let result = self.set_ports_action(uuid, ports); + rx.send(result).unwrap(); + } Action::UpdateConnection(conn) => { self.state.update_connection(conn)?; } @@ -89,4 +103,36 @@ impl NetworkSystem { Ok(()) } + + fn set_ports_action( + &mut self, + uuid: Uuid, + ports: Vec, + ) -> Result<(), NetworkStateError> { + let conn = self + .state + .get_connection_by_uuid(uuid) + .ok_or(NetworkStateError::UnknownConnection(uuid.to_string()))?; + self.state.set_ports(&conn.clone(), ports) + } + + fn get_controller_action( + &mut self, + uuid: Uuid, + ) -> Result<(Connection, Vec), NetworkStateError> { + let conn = self + .state + .get_connection_by_uuid(uuid) + .ok_or(NetworkStateError::UnknownConnection(uuid.to_string()))?; + let conn = conn.clone(); + + let controlled = self + .state + .get_controlled_by(uuid) + .iter() + .map(|c| c.interface().unwrap_or(c.id()).to_string()) + .collect::>(); + + Ok((conn, controlled)) + } } diff --git a/rust/agama-dbus-server/tests/network.rs b/rust/agama-dbus-server/tests/network.rs index 4462164764..87b1aaa475 100644 --- a/rust/agama-dbus-server/tests/network.rs +++ b/rust/agama-dbus-server/tests/network.rs @@ -6,7 +6,11 @@ use agama_dbus_server::network::{ model::{self, Ipv4Method, Ipv6Method}, Adapter, NetworkService, NetworkState, }; -use agama_lib::network::{settings, types::DeviceType, NetworkClient}; +use agama_lib::network::{ + settings::{self}, + types::DeviceType, + NetworkClient, +}; use cidr::IpInet; use std::error::Error; use tokio::test; @@ -88,6 +92,48 @@ async fn test_add_connection() -> Result<(), Box> { assert_eq!(method4, &Ipv4Method::Auto.to_string()); let method6 = conn.method6.as_ref().unwrap(); assert_eq!(method6, &Ipv6Method::Disabled.to_string()); + + Ok(()) +} + +#[test] +async fn test_add_bond_connection() -> Result<(), Box> { + let mut server = DBusServer::new().start().await?; + + let adapter = NetworkTestAdapter(NetworkState::default()); + + let _service = NetworkService::start(&server.connection(), adapter).await?; + server.request_name().await?; + + let client = NetworkClient::new(server.connection().clone()).await?; + let eth0 = settings::NetworkConnection { + id: "eth0".to_string(), + ..Default::default() + }; + let bond0 = settings::NetworkConnection { + id: "bond0".to_string(), + method4: Some("auto".to_string()), + method6: Some("disabled".to_string()), + interface: Some("bond0".to_string()), + bond: Some(settings::BondSettings { + mode: "active-backup".to_string(), + ports: vec!["eth0".to_string()], + options: Some("primary=eth1".to_string()), + }), + ..Default::default() + }; + + client.add_or_update_connection(ð0).await?; + client.add_or_update_connection(&bond0).await?; + let conns = async_retry(|| client.connections()).await?; + assert_eq!(conns.len(), 2); + + let conn = conns.iter().find(|c| c.id == "bond0".to_string()).unwrap(); + assert_eq!(conn.id, "bond0"); + assert_eq!(conn.device_type(), DeviceType::Bond); + let bond = conn.bond.clone().unwrap(); + assert_eq!(bond.mode, "active-backup"); + Ok(()) } diff --git a/rust/agama-lib/share/examples/profile.jsonnet b/rust/agama-lib/share/examples/profile.jsonnet index cd6b63f5bf..b37e986809 100644 --- a/rust/agama-lib/share/examples/profile.jsonnet +++ b/rust/agama-lib/share/examples/profile.jsonnet @@ -47,7 +47,8 @@ local findBiggestDisk(disks) = wireless: { password: 'agama.test', security: 'wpa-psk', - ssid: 'AgamaNetwork' + ssid: 'AgamaNetwork', + mode: 'infrastructure' } }, { @@ -63,6 +64,15 @@ local findBiggestDisk(disks) = match: { path: ["pci-0000:00:19.0"] } + }, + { + id: 'bond0', + bond: { + ports: ['eth0', 'eth1'], + mode: 'active-backup', + options: "primary=eth1" + + } } ] } diff --git a/rust/agama-lib/share/examples/profile_tw.json b/rust/agama-lib/share/examples/profile_tw.json index b55d82423a..0032bcfb99 100644 --- a/rust/agama-lib/share/examples/profile_tw.json +++ b/rust/agama-lib/share/examples/profile_tw.json @@ -39,7 +39,7 @@ "nameservers": [ "192.168.122.1", "2001:4860:4860::8888" - ] + ], } ] } diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index eb916132cf..5e6e2a6d16 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -127,6 +127,27 @@ } } }, + "bond": { + "type": "object", + "description": "Bonding configuration", + "additionalProperties": false, + "properties": { + "mode": { + "type": "string" + }, + "options": { + "type": "string" + }, + "ports": { + "type": "array", + "items": { + "description": "A list of the interfaces or connections to be bonded", + "type": "string", + "additionalProperties": false + } + } + } + }, "match": { "type": "object", "description": "Match settings", diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index f0a46823ad..79d9e4466b 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -1,6 +1,9 @@ -use super::proxies::{ConnectionProxy, ConnectionsProxy, IPProxy, MatchProxy, WirelessProxy}; -use super::settings::{MatchSettings, NetworkConnection, WirelessSettings}; -use super::types::SSID; +use super::proxies::{ + BondProxy, ConnectionProxy, ConnectionsProxy, DeviceProxy, DevicesProxy, IPProxy, MatchProxy, + WirelessProxy, +}; +use super::settings::{BondSettings, MatchSettings, NetworkConnection, WirelessSettings}; +use super::types::{Device, DeviceType, SSID}; use crate::error::ServiceError; use tokio_stream::StreamExt; use zbus::zvariant::OwnedObjectPath; @@ -10,12 +13,14 @@ use zbus::Connection; pub struct NetworkClient<'a> { pub connection: Connection, connections_proxy: ConnectionsProxy<'a>, + devices_proxy: DevicesProxy<'a>, } impl<'a> NetworkClient<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { connections_proxy: ConnectionsProxy::new(&connection).await?, + devices_proxy: DevicesProxy::new(&connection).await?, connection, }) } @@ -25,6 +30,19 @@ impl<'a> NetworkClient<'a> { self.connection_from(path.as_str()).await } + pub async fn available_devices(&self) -> Result, ServiceError> { + let devices_paths = self.devices_proxy.get_devices().await?; + let mut devices = vec![]; + + for path in devices_paths { + let device = self.device_from(path.as_str()).await?; + + devices.push(device); + } + + Ok(devices) + } + /// Returns an array of network connections pub async fn connections(&self) -> Result, ServiceError> { let connection_paths = self.connections_proxy.get_connections().await?; @@ -33,6 +51,10 @@ impl<'a> NetworkClient<'a> { for path in connection_paths { let mut connection = self.connection_from(path.as_str()).await?; + if let Ok(bond) = self.bond_from(path.as_str()).await { + connection.bond = Some(bond); + } + if let Ok(wireless) = self.wireless_from(path.as_str()).await { connection.wireless = Some(wireless); } @@ -54,6 +76,23 @@ impl<'a> NetworkClient<'a> { Ok(()) } + /// Returns the NetworkDevice for the given device path + /// + /// * `path`: the connections path to get the config from + async fn device_from(&self, path: &str) -> Result { + let device_proxy = DeviceProxy::builder(&self.connection) + .path(path)? + .build() + .await?; + let name = device_proxy.name().await?; + let device_type = device_proxy.type_().await?; + + Ok(Device { + name, + type_: DeviceType::try_from(device_type).unwrap(), + }) + } + /// Returns the NetworkConnection for the given connection path /// /// * `path`: the connections path to get the config from @@ -100,6 +139,22 @@ impl<'a> NetworkClient<'a> { }) } + /// Returns the [bond settings][BondSettings] for the given connection + /// + /// * `path`: the connection's path to get the wireless config from + async fn bond_from(&self, path: &str) -> Result { + let bond_proxy = BondProxy::builder(&self.connection) + .path(path)? + .build() + .await?; + let bond = BondSettings { + mode: bond_proxy.mode().await?, + options: Some(bond_proxy.options().await?), + ports: bond_proxy.ports().await?, + }; + + Ok(bond) + } /// Returns the [wireless settings][WirelessSettings] for the given connection /// /// * `path`: the connections path to get the wireless config from @@ -149,6 +204,7 @@ impl<'a> NetworkClient<'a> { Ok(path) => path, Err(_) => self.add_connection(conn).await?, }; + self.update_connection(&path, conn).await?; Ok(()) } @@ -191,14 +247,19 @@ impl<'a> NetworkClient<'a> { .build() .await?; - let interface = conn.interface.as_deref().unwrap_or(""); - proxy.set_interface(interface).await?; + if let Some(ref interface) = conn.interface { + proxy.set_interface(interface).await?; + } let mac_address = conn.mac_address.as_deref().unwrap_or(""); proxy.set_mac_address(mac_address).await?; self.update_ip_settings(path, conn).await?; + if let Some(ref bond) = conn.bond { + self.update_bond_settings(path, bond).await?; + } + if let Some(ref wireless) = conn.wireless { self.update_wireless_settings(path, wireless).await?; } @@ -249,6 +310,29 @@ impl<'a> NetworkClient<'a> { Ok(()) } + /// Updates the bond settings for a network connection. + /// + /// * `path`: connection D-Bus path. + /// * `bond`: bond settings of the network connection. + async fn update_bond_settings( + &self, + path: &OwnedObjectPath, + bond: &BondSettings, + ) -> Result<(), ServiceError> { + let proxy = BondProxy::builder(&self.connection) + .path(path)? + .build() + .await?; + + let ports: Vec<_> = bond.ports.iter().map(String::as_ref).collect(); + proxy.set_ports(ports.as_slice()).await?; + if let Some(ref options) = bond.options { + proxy.set_options(options.to_string().as_str()).await?; + } + proxy.set_mode(bond.mode.as_str()).await?; + + Ok(()) + } /// Updates the wireless settings for network connection. /// /// * `path`: connection D-Bus path. diff --git a/rust/agama-lib/src/network/proxies.rs b/rust/agama-lib/src/network/proxies.rs index 314e0c5b23..a3b1916abf 100644 --- a/rust/agama-lib/src/network/proxies.rs +++ b/rust/agama-lib/src/network/proxies.rs @@ -3,6 +3,30 @@ //! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. use zbus::dbus_proxy; +#[dbus_proxy( + interface = "org.opensuse.Agama1.Network.Devices", + default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Network/devices" +)] +trait Devices { + /// GetDevices method + fn get_devices(&self) -> zbus::Result>; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama1.Network.Device", + default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Network/devices/1" +)] +trait Device { + /// Name property + #[dbus_proxy(property)] + fn name(&self) -> zbus::Result; + /// Type property + #[dbus_proxy(property)] + fn type_(&self) -> zbus::Result; +} + #[dbus_proxy( interface = "org.opensuse.Agama1.Network.Connections", default_service = "org.opensuse.Agama1", @@ -141,10 +165,12 @@ trait Match { /// Driver property #[dbus_proxy(property)] fn driver(&self) -> zbus::Result>; + fn set_driver(&self, value: &[&str]) -> zbus::Result<()>; /// Interface property #[dbus_proxy(property)] fn interface(&self) -> zbus::Result>; + fn set_interface(&self, value: &[&str]) -> zbus::Result<()>; /// Path property #[dbus_proxy(property)] @@ -155,4 +181,30 @@ trait Match { /// Path property #[dbus_proxy(property)] fn kernel(&self) -> zbus::Result>; + fn set_kernel(&self, value: &[&str]) -> zbus::Result<()>; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama1.Network.Connection.Bond", + default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Network" +)] +trait Bond { + /// Mode property + #[dbus_proxy(property)] + fn mode(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_mode(&self, value: &str) -> zbus::Result<()>; + + /// Ports property + #[dbus_proxy(property)] + fn options(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_options(&self, value: &str) -> zbus::Result<()>; + + /// Ports property + #[dbus_proxy(property)] + fn ports(&self) -> zbus::Result>; + #[dbus_proxy(property)] + fn set_ports(&self, value: &[&str]) -> zbus::Result<()>; } diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 34be2f4090..db0321f74c 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -48,6 +48,31 @@ pub struct WirelessSettings { pub mode: String, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BondSettings { + pub mode: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub ports: Vec, +} + +impl Default for BondSettings { + fn default() -> Self { + Self { + mode: "balance-rr".to_string(), + options: None, + ports: vec![], + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NetworkDevice { + pub id: String, + pub type_: DeviceType, +} + #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct NetworkConnection { pub id: String, @@ -69,6 +94,10 @@ pub struct NetworkConnection { pub interface: Option, #[serde(skip_serializing_if = "Option::is_none")] pub match_settings: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bond: Option, #[serde(rename = "mac-address", skip_serializing_if = "Option::is_none")] pub mac_address: Option, } @@ -81,6 +110,8 @@ impl NetworkConnection { pub fn device_type(&self) -> DeviceType { if self.wireless.is_some() { DeviceType::Wireless + } else if self.bond.is_some() { + DeviceType::Bond } else { DeviceType::Ethernet } @@ -125,7 +156,23 @@ mod tests { wireless: Some(WirelessSettings::default()), ..Default::default() }; + + let bond = NetworkConnection { + bond: Some(BondSettings::default()), + ..Default::default() + }; + assert_eq!(wlan.device_type(), DeviceType::Wireless); + assert_eq!(bond.device_type(), DeviceType::Bond); + } + + #[test] + fn test_bonding_defaults() { + let bond = BondSettings::default(); + + assert_eq!(bond.mode, "balance-rr".to_string()); + assert_eq!(bond.ports.len(), 0); + assert_eq!(bond.options, None); } #[test] diff --git a/rust/agama-lib/src/network/store.rs b/rust/agama-lib/src/network/store.rs index b4711274a7..7ed70c332e 100644 --- a/rust/agama-lib/src/network/store.rs +++ b/rust/agama-lib/src/network/store.rs @@ -1,3 +1,4 @@ +use super::settings::NetworkConnection; use crate::error::ServiceError; use crate::network::{NetworkClient, NetworkSettings}; use zbus::Connection; @@ -22,7 +23,10 @@ impl<'a> NetworkStore<'a> { } pub async fn store(&self, settings: &NetworkSettings) -> Result<(), ServiceError> { - for conn in &settings.connections { + for id in ordered_connections(&settings.connections) { + let id = id.as_str(); + let fallback = default_connection(id); + let conn = find_connection(id, &settings.connections).unwrap_or(&fallback); self.network_client.add_or_update_connection(conn).await?; } self.network_client.apply().await?; @@ -30,3 +34,101 @@ impl<'a> NetworkStore<'a> { Ok(()) } } + +/// Returns the list of connections in the order they should be written to the D-Bus service. +/// +/// * `conns`: connections to write. +fn ordered_connections(conns: &Vec) -> Vec { + let mut ordered: Vec = Vec::with_capacity(conns.len()); + for conn in conns { + add_ordered_connection(conn, conns, &mut ordered); + } + ordered +} + +/// Adds a connections and its dependencies to the list. +/// +/// * `conn`: connection to add. +/// * `conns`: existing connections. +/// * `ordered`: ordered list of connections. +fn add_ordered_connection( + conn: &NetworkConnection, + conns: &Vec, + ordered: &mut Vec, +) { + if let Some(bond) = &conn.bond { + for port in &bond.ports { + if let Some(conn) = find_connection(port, conns) { + add_ordered_connection(conn, conns, ordered); + } else if !ordered.contains(&conn.id) { + ordered.push(port.clone()); + } + } + } + + if !ordered.contains(&conn.id) { + ordered.push(conn.id.to_owned()) + } +} + +/// Finds a connection by id in the list. +/// +/// * `id`: connection ID. +fn find_connection<'a>(id: &str, conns: &'a [NetworkConnection]) -> Option<&'a NetworkConnection> { + conns + .iter() + .find(|c| c.id == id || c.interface == Some(id.to_string())) +} + +fn default_connection(id: &str) -> NetworkConnection { + NetworkConnection { + id: id.to_string(), + interface: Some(id.to_string()), + ..Default::default() + } +} + +#[cfg(test)] +mod tests { + use super::ordered_connections; + use crate::network::settings::{BondSettings, NetworkConnection}; + + #[test] + fn test_ordered_connections() { + let bond = NetworkConnection { + id: "bond0".to_string(), + bond: Some(BondSettings { + ports: vec!["eth0".to_string(), "eth1".to_string(), "eth3".to_string()], + ..Default::default() + }), + ..Default::default() + }; + + let eth0 = NetworkConnection { + id: "eth0".to_string(), + ..Default::default() + }; + let eth1 = NetworkConnection { + id: "Wired connection".to_string(), + interface: Some("eth1".to_string()), + ..Default::default() + }; + let eth2 = NetworkConnection { + id: "eth2".to_string(), + ..Default::default() + }; + + let conns = vec![bond, eth0, eth1, eth2]; + let ordered = ordered_connections(&conns); + assert_eq!( + ordered, + vec![ + "eth0".to_string(), + "Wired connection".to_string(), + "eth3".to_string(), + "bond0".to_string(), + "eth2".to_string() + ] + ) + } +} diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index 10daa400c1..5a14fe238e 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -3,6 +3,13 @@ use std::{fmt, str}; use thiserror::Error; use zbus; +/// Network device +#[derive(Debug, Clone)] +pub struct Device { + pub name: String, + pub type_: DeviceType, +} + #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] pub struct SSID(pub Vec); @@ -24,12 +31,98 @@ impl From for Vec { } } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum DeviceType { Loopback = 0, Ethernet = 1, Wireless = 2, Dummy = 3, + Bond = 4, +} + +/// Bond mode +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub enum BondMode { + #[serde(rename = "balance-rr")] + RoundRobin = 0, + #[serde(rename = "active-backup")] + ActiveBackup = 1, + #[serde(rename = "balance-xor")] + BalanceXOR = 2, + #[serde(rename = "broadcast")] + Broadcast = 3, + #[serde(rename = "802.3ad")] + LACP = 4, + #[serde(rename = "balance-tlb")] + BalanceTLB = 5, + #[serde(rename = "balance-alb")] + BalanceALB = 6, +} +impl Default for BondMode { + fn default() -> Self { + Self::RoundRobin + } +} + +impl std::fmt::Display for BondMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + BondMode::RoundRobin => "balance-rr", + BondMode::ActiveBackup => "active-backup", + BondMode::BalanceXOR => "balance-xor", + BondMode::Broadcast => "broadcast", + BondMode::LACP => "802.3ad", + BondMode::BalanceTLB => "balance-tlb", + BondMode::BalanceALB => "balance-alb", + } + ) + } +} + +#[derive(Debug, Error, PartialEq)] +#[error("Invalid bond mode: {0}")] +pub struct InvalidBondMode(String); + +impl TryFrom<&str> for BondMode { + type Error = InvalidBondMode; + + fn try_from(value: &str) -> Result { + match value { + "balance-rr" => Ok(BondMode::RoundRobin), + "active-backup" => Ok(BondMode::ActiveBackup), + "balance-xor" => Ok(BondMode::BalanceXOR), + "broadcast" => Ok(BondMode::Broadcast), + "802.3ad" => Ok(BondMode::LACP), + "balance-tlb" => Ok(BondMode::BalanceTLB), + "balance-alb" => Ok(BondMode::BalanceALB), + _ => Err(InvalidBondMode(value.to_string())), + } + } +} +impl TryFrom for BondMode { + type Error = InvalidBondMode; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(BondMode::RoundRobin), + 1 => Ok(BondMode::ActiveBackup), + 2 => Ok(BondMode::BalanceXOR), + 3 => Ok(BondMode::Broadcast), + 4 => Ok(BondMode::LACP), + 5 => Ok(BondMode::BalanceTLB), + 6 => Ok(BondMode::BalanceALB), + _ => Err(InvalidBondMode(value.to_string())), + } + } +} + +impl From for zbus::fdo::Error { + fn from(value: InvalidBondMode) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(format!("Network error: {value}")) + } } #[derive(Debug, Error, PartialEq)] @@ -45,6 +138,7 @@ impl TryFrom for DeviceType { 1 => Ok(DeviceType::Ethernet), 2 => Ok(DeviceType::Wireless), 3 => Ok(DeviceType::Dummy), + 4 => Ok(DeviceType::Bond), _ => Err(InvalidDeviceType(value)), } } @@ -81,4 +175,10 @@ mod tests { let dtype = DeviceType::try_from(128); assert_eq!(dtype, Err(InvalidDeviceType(128))); } + + #[test] + fn test_display_bond_mode() { + let mode = BondMode::try_from(1).unwrap(); + assert_eq!(format!("{}", mode), "active-backup"); + } } diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes index 708fba8e0e..90d92bb8fa 100644 --- a/rust/package/agama-cli.changes +++ b/rust/package/agama-cli.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Wed Dec 13 22:41:34 UTC 2023 - Knut Anderssen + +- Add support for bonding connections (gh#openSUSE/agama#885). + ------------------------------------------------------------------- Fri Dec 8 09:23:09 UTC 2023 - Josef Reidinger