diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml deleted file mode 120000 index 7b1c091d22..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml +++ /dev/null @@ -1 +0,0 @@ -org.opensuse.Agama.Network1.Connection.bus.xml \ No newline at end of file diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml new file mode 100644 index 0000000000..f51ea03ee2 --- /dev/null +++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.IPv4.bus.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml deleted file mode 120000 index 7b1c091d22..0000000000 --- a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml +++ /dev/null @@ -1 +0,0 @@ -org.opensuse.Agama.Network1.Connection.bus.xml \ No newline at end of file diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml new file mode 100644 index 0000000000..f51ea03ee2 --- /dev/null +++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.Wireless.bus.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.bus.xml index 91545a74fe..f51ea03ee2 100644 --- a/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.bus.xml +++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connection.bus.xml @@ -2,14 +2,79 @@ + + + + + + + + + + + + + + + + + + + - + + + + - + + - - + + + + @@ -37,22 +102,5 @@ - - - - - - - - - - - - - - - - - diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Connections.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Connections.bus.xml index 1987d9850c..3c840bdea8 100644 --- a/doc/dbus/bus/org.opensuse.Agama.Network1.Connections.bus.xml +++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Connections.bus.xml @@ -47,16 +47,25 @@ * `ty`: connection type (see [crate::model::DeviceType]). --> - + + + + + + - + + diff --git a/doc/dbus/bus/org.opensuse.Agama.Network1.Devices.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Network1.Devices.bus.xml index eff3ad0179..0b1f278bff 100644 --- a/doc/dbus/bus/org.opensuse.Agama.Network1.Devices.bus.xml +++ b/doc/dbus/bus/org.opensuse.Agama.Network1.Devices.bus.xml @@ -17,6 +17,11 @@ + + + + + @@ -41,10 +46,5 @@ - - - - - diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connection.IPv4.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connection.IPv4.doc.xml index c7d36731e2..2f9a6a011e 100644 --- a/doc/dbus/org.opensuse.Agama.Network1.Connection.IPv4.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Network1.Connection.IPv4.doc.xml @@ -3,9 +3,30 @@ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> - + + + - + + + diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connection.Wireless.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connection.Wireless.doc.xml index bad0fb7f53..184008a809 100644 --- a/doc/dbus/org.opensuse.Agama.Network1.Connection.Wireless.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Network1.Connection.Wireless.doc.xml @@ -3,10 +3,31 @@ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> - + + + + - + + diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connection.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connection.doc.xml index 20a7bf0f71..730f07eb20 100644 --- a/doc/dbus/org.opensuse.Agama.Network1.Connection.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Network1.Connection.doc.xml @@ -3,8 +3,14 @@ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> + - diff --git a/doc/dbus/org.opensuse.Agama.Network1.Connections.doc.xml b/doc/dbus/org.opensuse.Agama.Network1.Connections.doc.xml index 3ea5d4387f..8d229b98e8 100644 --- a/doc/dbus/org.opensuse.Agama.Network1.Connections.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Network1.Connections.doc.xml @@ -14,14 +14,23 @@ AddConnection: Adds a new network connection. - * `name`: connection name. - + * `id`: connection name. * `ty`: connection type (see [crate::model::DeviceType]). --> - + + + + + + - + + diff --git a/rust/agama-dbus-server/src/network/action.rs b/rust/agama-dbus-server/src/network/action.rs index 8c7b93945d..e0592e7fa4 100644 --- a/rust/agama-dbus-server/src/network/action.rs +++ b/rust/agama-dbus-server/src/network/action.rs @@ -1,5 +1,5 @@ -use crate::network::model::{Connection, DeviceType}; -use uuid::Uuid; +use crate::network::model::Connection; +use agama_lib::network::types::DeviceType; /// Networking actions, like adding, updating or removing connections. /// @@ -12,7 +12,7 @@ pub enum Action { /// Update a connection (replacing the old one). UpdateConnection(Connection), /// Remove the connection with the given Uuid. - RemoveConnection(Uuid), + RemoveConnection(String), /// Apply the current configuration. Apply, } diff --git a/rust/agama-dbus-server/src/network/dbus/interfaces.rs b/rust/agama-dbus-server/src/network/dbus/interfaces.rs index ff91881da1..6ed155d555 100644 --- a/rust/agama-dbus-server/src/network/dbus/interfaces.rs +++ b/rust/agama-dbus-server/src/network/dbus/interfaces.rs @@ -6,8 +6,11 @@ use super::ObjectsRegistry; use crate::network::{ action::Action, error::NetworkStateError, - model::{Connection as NetworkConnection, Device as NetworkDevice, WirelessConnection}, + model::{ + Connection as NetworkConnection, Device as NetworkDevice, IpAddress, WirelessConnection, + }, }; +use log; use agama_lib::network::types::SSID; use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; @@ -15,8 +18,10 @@ use std::{ net::{AddrParseError, Ipv4Addr}, sync::{mpsc::Sender, Arc}, }; -use uuid::Uuid; -use zbus::{dbus_interface, zvariant::ObjectPath}; +use zbus::{ + dbus_interface, + zvariant::{ObjectPath, OwnedObjectPath}, +}; /// D-Bus interface for the network devices collection /// @@ -65,11 +70,19 @@ impl Device { #[dbus_interface(name = "org.opensuse.Agama.Network1.Device")] impl Device { + /// Device name. + /// + /// Kernel device name, e.g., eth0, enp1s0, etc. #[dbus_interface(property)] pub fn name(&self) -> &str { &self.device.name } + /// Device type. + /// + /// Possible values: 0 = loopback, 1 = ethernet, 2 = wireless. + /// + /// See [crate::model::DeviceType]. #[dbus_interface(property, name = "Type")] pub fn device_type(&self) -> u8 { self.device.type_ as u8 @@ -110,24 +123,35 @@ impl Connections { /// Adds a new network connection. /// - /// * `name`: connection name. + /// * `id`: connection name. /// * `ty`: connection type (see [crate::model::DeviceType]). - pub async fn add_connection(&mut self, name: String, ty: u8) -> zbus::fdo::Result<()> { + pub async fn add_connection(&mut self, id: String, ty: u8) -> zbus::fdo::Result<()> { let actions = self.actions.lock(); actions - .send(Action::AddConnection(name, ty.try_into()?)) + .send(Action::AddConnection(id, ty.try_into()?)) .unwrap(); Ok(()) } + /// Returns the D-Bus path of the network connection. + /// + /// * `id`: connection ID. + pub async fn get_connection(&self, id: &str) -> zbus::fdo::Result { + let objects = self.objects.lock(); + match objects.connection_path(&id) { + Some(path) => Ok(path.into()), + None => Err(NetworkStateError::UnknownConnection(id.to_string()).into()), + } + } + /// Removes a network connection. /// /// * `uuid`: connection UUID.. - pub async fn remove_connection(&mut self, uuid: &str) -> zbus::fdo::Result<()> { + pub async fn remove_connection(&mut self, id: &str) -> zbus::fdo::Result<()> { let actions = self.actions.lock(); - let uuid = - Uuid::parse_str(uuid).map_err(|_| NetworkStateError::InvalidUuid(uuid.to_string()))?; - actions.send(Action::RemoveConnection(uuid)).unwrap(); + actions + .send(Action::RemoveConnection(id.to_string())) + .unwrap(); Ok(()) } @@ -164,15 +188,15 @@ impl Connection { #[dbus_interface(name = "org.opensuse.Agama.Network1.Connection")] impl Connection { + /// Connection ID. + /// + /// Unique identifier of the network connection. It may or not be the same that the used by the + /// backend. For instance, when using NetworkManager (which is the only supported backend by + /// now), it uses the original ID but appending a number in case the ID is duplicated. #[dbus_interface(property)] pub fn id(&self) -> String { self.get_connection().id().to_string() } - - #[dbus_interface(property, name = "UUID")] - pub fn uuid(&self) -> String { - self.get_connection().uuid().to_string() - } } /// D-Bus interface for IPv4 settings @@ -215,42 +239,56 @@ impl Ipv4 { #[dbus_interface(name = "org.opensuse.Agama.Network1.Connection.IPv4")] impl Ipv4 { + /// List of IP addresses. + /// + /// When the method is 'auto', these addresses are used as additional addresses. #[dbus_interface(property)] - pub fn addresses(&self) -> Vec<(String, u32)> { + pub fn addresses(&self) -> Vec { let connection = self.get_connection(); connection .ipv4() .addresses .iter() - .map(|(addr, prefix)| (addr.to_string(), *prefix)) + .map(|ip| ip.to_string()) .collect() } #[dbus_interface(property)] - pub fn set_addresses(&mut self, addresses: Vec<(String, u32)>) -> zbus::fdo::Result<()> { + pub fn set_addresses(&mut self, addresses: Vec) -> zbus::fdo::Result<()> { let mut connection = self.get_connection(); - addresses - .iter() - .map(|(addr, prefix)| addr.parse::().map(|a| (a, *prefix))) - .collect::, AddrParseError>>() - .and_then(|parsed| Ok(connection.ipv4_mut().addresses = parsed)) - .map_err(|err| NetworkStateError::from(err))?; + let parsed: Vec = addresses + .into_iter() + .filter_map(|ip| match ip.parse::() { + Ok(address) => Some(address), + Err(error) => { + log::error!("Ignoring the invalid IPv4 address: {} ({})", ip, error); + None + } + }) + .collect(); + connection.ipv4_mut().addresses = parsed; self.update_connection(connection) } + /// IP configuration method. + /// + /// Possible values: "disabled", "auto", "manual" or "link-local". + /// + /// See [crate::model::IpMethod]. #[dbus_interface(property)] - pub fn method(&self) -> u8 { + pub fn method(&self) -> String { let connection = self.get_connection(); - connection.ipv4().method as u8 + connection.ipv4().method.to_string() } #[dbus_interface(property)] - pub fn set_method(&mut self, method: u8) -> zbus::fdo::Result<()> { + pub fn set_method(&mut self, method: &str) -> zbus::fdo::Result<()> { let mut connection = self.get_connection(); - connection.ipv4_mut().method = method.try_into()?; + connection.ipv4_mut().method = method.parse()?; self.update_connection(connection) } + /// Name server addresses. #[dbus_interface(property)] pub fn nameservers(&self) -> Vec { let connection = self.get_connection(); @@ -275,6 +313,10 @@ impl Ipv4 { self.update_connection(connection) } + /// Network gateway. + /// + /// An empty string removes the current value. It is not possible to set a gateway if the + /// addresses property is empty. #[dbus_interface(property)] pub fn gateway(&self) -> String { let connection = self.get_connection(); @@ -343,6 +385,7 @@ impl Wireless { #[dbus_interface(name = "org.opensuse.Agama.Network1.Connection.Wireless")] impl Wireless { + /// Network SSID. #[dbus_interface(property, name = "SSID")] pub fn ssid(&self) -> Vec { let connection = self.get_wireless(); @@ -356,6 +399,11 @@ impl Wireless { self.update_connection(connection) } + /// Wireless connection mode. + /// + /// Possible values: "unknown", "adhoc", "infrastructure", "ap" or "mesh". + /// + /// See [crate::model::WirelessMode]. #[dbus_interface(property)] pub fn mode(&self) -> String { let connection = self.get_wireless(); @@ -369,6 +417,7 @@ impl Wireless { self.update_connection(connection) } + /// Password to connect to the wireless network. #[dbus_interface(property)] pub fn password(&self) -> String { let connection = self.get_wireless(); @@ -390,6 +439,12 @@ impl Wireless { self.update_connection(connection) } + /// Wireless security protocol. + /// + /// Possible values: "none", "owe", "ieee8021x", "wpa-psk", "sae", "wpa-eap", + /// "wpa-eap-suite-b192". + /// + /// See [crate::model::WirelessMode]. #[dbus_interface(property)] pub fn security(&self) -> String { let connection = self.get_wireless(); diff --git a/rust/agama-dbus-server/src/network/dbus/tree.rs b/rust/agama-dbus-server/src/network/dbus/tree.rs index 71e740e7ea..a34adea32e 100644 --- a/rust/agama-dbus-server/src/network/dbus/tree.rs +++ b/rust/agama-dbus-server/src/network/dbus/tree.rs @@ -1,8 +1,9 @@ use agama_lib::error::ServiceError; use parking_lot::Mutex; -use uuid::Uuid; +use zbus::zvariant::{ObjectPath, OwnedObjectPath}; use crate::network::{action::Action, dbus::interfaces, model::*}; +use log; use std::collections::HashMap; use std::sync::mpsc::Sender; use std::sync::Arc; @@ -36,7 +37,10 @@ impl Tree { /// adding/removing interfaces. We should add/update/delete objects as needed. /// /// * `connections`: list of connections. - pub async fn set_connections(&self, connections: &Vec) -> Result<(), ServiceError> { + pub async fn set_connections( + &self, + connections: &mut Vec, + ) -> Result<(), ServiceError> { self.remove_connections().await?; self.add_connections(connections).await?; Ok(()) @@ -57,10 +61,11 @@ impl Tree { pub async fn add_devices(&mut self, devices: &Vec) -> Result<(), ServiceError> { for (i, dev) in devices.iter().enumerate() { let path = format!("{}/{}", DEVICES_PATH, i); + let path = ObjectPath::try_from(path.as_str()).unwrap(); self.add_interface(&path, interfaces::Device::new(dev.clone())) .await?; let mut objects = self.objects.lock(); - objects.register_device(&dev.name, &path); + objects.register_device(&dev.name, path); } self.add_interface( @@ -75,10 +80,15 @@ impl Tree { /// Adds a connection to the D-Bus tree. /// /// * `connection`: connection to add. - pub async fn add_connection(&self, conn: &Connection) -> Result<(), ServiceError> { + pub async fn add_connection(&self, conn: &mut Connection) -> Result<(), ServiceError> { let mut objects = self.objects.lock(); - let path = format!("{}/{}", CONNECTIONS_PATH, objects.connections.len()); + let (id, path) = objects.register_connection(&conn); + if id != conn.id() { + conn.set_id(&id) + } + log::info!("Publishing network connection '{}'", id); + let cloned = Arc::new(Mutex::new(conn.clone())); self.add_interface(&path, interfaces::Connection::new(Arc::clone(&cloned))) .await?; @@ -97,28 +107,27 @@ impl Tree { .await?; } - objects.register_connection(conn.uuid(), &path); Ok(()) } /// Removes a connection from the tree /// - /// * `uuid`: UUID of the connection to remove. - pub async fn remove_connection(&mut self, uuid: Uuid) -> Result<(), ServiceError> { + /// * `id`: connection ID. + pub async fn remove_connection(&mut self, id: &str) -> Result<(), ServiceError> { let mut objects = self.objects.lock(); - let Some(path) = objects.connection_path(uuid) else { + let Some(path) = objects.connection_path(id) else { return Ok(()) }; - self.remove_connection_on(path).await?; - objects.deregister_connection(uuid).unwrap(); + self.remove_connection_on(path.as_str()).await?; + objects.deregister_connection(id).unwrap(); Ok(()) } /// Adds connections to the D-Bus tree. /// /// * `connections`: list of connections. - async fn add_connections(&self, connections: &Vec) -> Result<(), ServiceError> { - for conn in connections.iter() { + async fn add_connections(&self, connections: &mut Vec) -> Result<(), ServiceError> { + for conn in connections.iter_mut() { self.add_connection(conn).await?; } @@ -178,44 +187,53 @@ impl Tree { /// Objects paths for known devices and connections /// -/// TODO: use zvariant::OwnedObjectPath instead of String as values. +/// Connections are indexed by its Id, which is expected to be unique. #[derive(Debug, Default)] pub struct ObjectsRegistry { /// device_name (eth0) -> object_path - pub devices: HashMap, - /// uuid -> object_path - pub connections: HashMap, + devices: HashMap, + /// id -> object_path + connections: HashMap, } impl ObjectsRegistry { /// Registers a network device. /// - /// * `name`: device name. + /// * `id`: device name. /// * `path`: object path. - pub fn register_device(&mut self, name: &str, path: &str) { - self.devices.insert(name.to_string(), path.to_string()); + pub fn register_device(&mut self, id: &str, path: ObjectPath) { + self.devices.insert(id.to_string(), path.into()); } - /// Registers a network connection. + /// Registers a network connection and returns its D-Bus path. /// - /// * `uuid`: connection UUID. - /// * `path`: object path. - pub fn register_connection(&mut self, uuid: Uuid, path: &str) { - self.connections.insert(uuid, path.to_string()); + /// It returns the connection Id and the D-Bus path. Bear in mind that the Id can be different + /// in case the original one already existed. + /// + /// * `conn`: network connection. + pub fn register_connection(&mut self, conn: &Connection) -> (String, ObjectPath) { + let path = format!("{}/{}", CONNECTIONS_PATH, self.connections.len()); + let path = ObjectPath::try_from(path).unwrap(); + let mut id = conn.id().to_string(); + if self.connection_path(&id).is_some() { + id = self.propose_id(&id); + }; + self.connections.insert(id.clone(), path.clone().into()); + (id, path) } /// Returns the path for a connection. /// - /// * `uuid`: connection UUID. - pub fn connection_path(&self, uuid: Uuid) -> Option<&str> { - self.connections.get(&uuid).map(|p| p.as_str()) + /// * `id`: connection ID. + pub fn connection_path(&self, id: &str) -> Option { + self.connections.get(id).map(|p| p.as_ref()) } /// Deregisters a network connection. /// - /// * `uuid`: connection UUID. - pub fn deregister_connection(&mut self, uuid: Uuid) -> Option { - self.connections.remove(&uuid) + /// * `id`: connection ID. + pub fn deregister_connection(&mut self, id: &str) -> Option { + self.connections.remove(id) } /// Returns all devices paths. @@ -227,4 +245,18 @@ impl ObjectsRegistry { pub fn connections_paths(&self) -> Vec { self.connections.values().map(|p| p.to_string()).collect() } + + /// Proposes a connection ID. + /// + /// * `id`: original connection ID. + fn propose_id(&self, id: &str) -> String { + let prefix = format!("{}-", id); + let filtered: Vec<_> = self + .connections + .keys() + .filter_map(|i| i.strip_prefix(&prefix).and_then(|n| n.parse::().ok())) + .collect(); + let index = filtered.into_iter().max().unwrap_or(0); + format!("{}{}", prefix, index + 1) + } } diff --git a/rust/agama-dbus-server/src/network/error.rs b/rust/agama-dbus-server/src/network/error.rs index c20dd0918d..5eec005909 100644 --- a/rust/agama-dbus-server/src/network/error.rs +++ b/rust/agama-dbus-server/src/network/error.rs @@ -6,8 +6,8 @@ use uuid::Uuid; /// Errors that are related to the network configuration. #[derive(Error, Debug)] pub enum NetworkStateError { - #[error("Invalid connection name: '{0}'")] - UnknownConnection(Uuid), + #[error("Unknown connection with ID: '{0}'")] + UnknownConnection(String), #[error("Invalid connection UUID: '{0}'")] InvalidUuid(String), #[error("Invalid IP address")] @@ -18,8 +18,6 @@ pub enum NetworkStateError { InvalidWirelessMode(String), #[error("Connection '{0}' already exists")] ConnectionExists(Uuid), - #[error("Invalid device type: '{0}'")] - InvalidDeviceType(u8), #[error("Invalid security wireless protocol: '{0}'")] InvalidSecurityProtocol(String), #[error("Adapter error: '{0}'")] diff --git a/rust/agama-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 186d990274..d8a2fcb860 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -5,8 +5,13 @@ use uuid::Uuid; use crate::network::error::NetworkStateError; -use agama_lib::network::types::SSID; -use std::{fmt, net::Ipv4Addr, str}; +use agama_lib::network::types::{DeviceType, SSID}; +use std::{ + fmt, + net::{AddrParseError, Ipv4Addr}, + str::{self, FromStr}, +}; +use thiserror::Error; #[derive(Default)] pub struct NetworkState { @@ -36,22 +41,22 @@ impl NetworkState { /// Get connection by UUID /// /// * `uuid`: connection UUID - pub fn get_connection(&self, uuid: Uuid) -> Option<&Connection> { - self.connections.iter().find(|c| c.uuid() == uuid) + pub fn get_connection(&self, id: &str) -> Option<&Connection> { + self.connections.iter().find(|c| c.id() == id) } /// Get connection by UUID as mutable /// /// * `uuid`: connection UUID - pub fn get_connection_mut(&mut self, uuid: Uuid) -> Option<&mut Connection> { - self.connections.iter_mut().find(|c| c.uuid() == uuid) + pub fn get_connection_mut(&mut self, id: &str) -> Option<&mut Connection> { + self.connections.iter_mut().find(|c| c.id() == id) } /// Adds a new connection. /// /// It uses the `id` to decide whether the connection already exists. pub fn add_connection(&mut self, conn: Connection) -> Result<(), NetworkStateError> { - if let Some(_) = self.get_connection(conn.uuid()) { + if let Some(_) = self.get_connection(conn.id()) { return Err(NetworkStateError::ConnectionExists(conn.uuid())); } @@ -65,8 +70,8 @@ impl NetworkState { /// /// Additionally, it registers the connection to be removed when the changes are applied. pub fn update_connection(&mut self, conn: Connection) -> Result<(), NetworkStateError> { - let Some(old_conn) = self.get_connection_mut(conn.uuid()) else { - return Err(NetworkStateError::UnknownConnection(conn.uuid())); + let Some(old_conn) = self.get_connection_mut(conn.id()) else { + return Err(NetworkStateError::UnknownConnection(conn.id().to_string())); }; *old_conn = conn; @@ -76,9 +81,9 @@ impl NetworkState { /// Removes a connection from the state. /// /// Additionally, it registers the connection to be removed when the changes are applied. - pub fn remove_connection(&mut self, uuid: Uuid) -> Result<(), NetworkStateError> { - let Some(conn) = self.get_connection_mut(uuid) else { - return Err(NetworkStateError::UnknownConnection(uuid)); + pub fn remove_connection(&mut self, id: &str) -> Result<(), NetworkStateError> { + let Some(conn) = self.get_connection_mut(id) else { + return Err(NetworkStateError::UnknownConnection(id.to_string())); }; conn.remove(); @@ -98,12 +103,13 @@ mod tests { let mut state = NetworkState::default(); let uuid = Uuid::new_v4(); let base = BaseConnection { + id: "eth0".to_string(), uuid, ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base }); state.add_connection(conn0).unwrap(); - let found = state.get_connection(uuid).unwrap(); + let found = state.get_connection("eth0").unwrap(); assert_eq!(found.uuid(), uuid); } @@ -124,21 +130,23 @@ mod tests { #[test] fn test_update_connection() { let mut state = NetworkState::default(); - let uuid = Uuid::new_v4(); let base0 = BaseConnection { - uuid, + id: "eth0".to_string(), + uuid: Uuid::new_v4(), ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base: base0 }); state.add_connection(conn0).unwrap(); + let uuid = Uuid::new_v4(); let base1 = BaseConnection { + id: "eth0".to_string(), uuid, ..Default::default() }; let conn2 = Connection::Ethernet(EthernetConnection { base: base1 }); state.update_connection(conn2).unwrap(); - let found = state.get_connection(uuid).unwrap(); + let found = state.get_connection("eth0").unwrap(); assert_eq!(found.uuid(), uuid); } @@ -158,22 +166,24 @@ mod tests { #[test] fn test_remove_connection() { let mut state = NetworkState::default(); + let id = "eth0".to_string(); let uuid = Uuid::new_v4(); let base0 = BaseConnection { + id, uuid, ..Default::default() }; let conn0 = Connection::Ethernet(EthernetConnection { base: base0 }); state.add_connection(conn0).unwrap(); - state.remove_connection(uuid).unwrap(); - let found = state.get_connection(uuid).unwrap(); + state.remove_connection("eth0").unwrap(); + let found = state.get_connection("eth0").unwrap(); assert!(found.is_removed()); } #[test] fn test_remove_unknown_connection() { let mut state = NetworkState::default(); - let error = state.remove_connection(Uuid::new_v4()).unwrap_err(); + let error = state.remove_connection("eth0").unwrap_err(); assert!(matches!(error, NetworkStateError::UnknownConnection(_))); } } @@ -185,26 +195,6 @@ pub struct Device { pub type_: DeviceType, } -#[derive(Debug, PartialEq, Copy, Clone)] -pub enum DeviceType { - Loopback = 0, - Ethernet = 1, - Wireless = 2, -} - -impl TryFrom for DeviceType { - type Error = NetworkStateError; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(DeviceType::Loopback), - 1 => Ok(DeviceType::Ethernet), - 2 => Ok(DeviceType::Wireless), - _ => Err(NetworkStateError::InvalidDeviceType(value)), - } - } -} - /// Represents an available network connection #[derive(Debug, PartialEq, Clone)] pub enum Connection { @@ -251,6 +241,10 @@ impl Connection { self.base().id.as_str() } + pub fn set_id(&mut self, id: &str) { + self.base_mut().id = id.to_string() + } + pub fn uuid(&self) -> Uuid { self.base().uuid } @@ -296,11 +290,15 @@ pub enum Status { #[derive(Debug, Default, PartialEq, Clone)] pub struct Ipv4Config { pub method: IpMethod, - pub addresses: Vec<(Ipv4Addr, u32)>, + pub addresses: Vec, pub nameservers: Vec, pub gateway: Option, } +#[derive(Debug, Error)] +#[error("Unknown IP configuration method name: {0}")] +pub struct UnknownIpMethod(String); + #[derive(Debug, Default, Copy, Clone, PartialEq)] pub enum IpMethod { #[default] @@ -321,21 +319,26 @@ impl fmt::Display for IpMethod { } } -// NOTE: we could use num-derive. -impl TryFrom for IpMethod { - type Error = NetworkStateError; +impl FromStr for IpMethod { + type Err = UnknownIpMethod; - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(IpMethod::Disabled), - 1 => Ok(IpMethod::Auto), - 2 => Ok(IpMethod::Manual), - 3 => Ok(IpMethod::LinkLocal), - _ => Err(NetworkStateError::InvalidIpMethod(value)), + fn from_str(s: &str) -> Result { + match s { + "disabled" => Ok(IpMethod::Disabled), + "auto" => Ok(IpMethod::Auto), + "manual" => Ok(IpMethod::Manual), + "link-local" => Ok(IpMethod::LinkLocal), + _ => Err(UnknownIpMethod(s.to_string())), } } } +impl From for zbus::fdo::Error { + fn from(value: UnknownIpMethod) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(value.to_string()) + } +} + #[derive(Debug, Default, PartialEq, Clone)] pub struct EthernetConnection { pub base: BaseConnection, @@ -443,3 +446,69 @@ impl TryFrom<&str> for SecurityProtocol { } } } + +/// Represents an IPv4 address with a prefix. +#[derive(Debug, Clone, PartialEq)] +pub struct IpAddress(Ipv4Addr, u32); + +#[derive(Error, Debug)] +pub enum ParseIpAddressError { + #[error("Missing prefix")] + MissingPrefix, + #[error("Invalid prefix part '{0}'")] + InvalidPrefix(String), + #[error("Invalid address part: {0}")] + InvalidAddr(AddrParseError), +} + +impl IpAddress { + /// Returns an new IpAddress object + /// + /// * `addr`: IPv4 address. + /// * `prefix`: IPv4 address prefix. + pub fn new(addr: Ipv4Addr, prefix: u32) -> Self { + IpAddress(addr, prefix) + } + + /// Returns the IPv4 address. + pub fn addr(&self) -> &Ipv4Addr { + &self.0 + } + + /// Returns the prefix. + pub fn prefix(&self) -> u32 { + self.1 + } +} + +impl From for (String, u32) { + fn from(value: IpAddress) -> Self { + (value.0.to_string(), value.1) + } +} + +impl FromStr for IpAddress { + type Err = ParseIpAddressError; + + fn from_str(s: &str) -> Result { + let Some((address, prefix)) = s.split_once("/") else { + return Err(ParseIpAddressError::MissingPrefix); + }; + + let address: Ipv4Addr = address + .parse() + .map_err(|e| ParseIpAddressError::InvalidAddr(e))?; + + let prefix: u32 = prefix + .parse() + .map_err(|_| ParseIpAddressError::InvalidPrefix(prefix.to_string()))?; + + Ok(IpAddress(address, prefix)) + } +} + +impl fmt::Display for IpAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.0.to_string(), self.1) + } +} diff --git a/rust/agama-dbus-server/src/network/nm/adapter.rs b/rust/agama-dbus-server/src/network/nm/adapter.rs index 2f7c8654e7..261b547c54 100644 --- a/rust/agama-dbus-server/src/network/nm/adapter.rs +++ b/rust/agama-dbus-server/src/network/nm/adapter.rs @@ -4,6 +4,7 @@ use crate::network::{ Adapter, }; use agama_lib::error::ServiceError; +use log; /// An adapter for NetworkManager pub struct NetworkManagerAdapter<'a> { @@ -48,11 +49,11 @@ impl<'a> Adapter for NetworkManagerAdapter<'a> { } if conn.is_removed() { if let Err(e) = self.client.remove_connection(conn.uuid()).await { - eprintln!("Could not remove the connection {}: {}", conn.uuid(), e); + log::error!("Could not remove the connection {}: {}", conn.uuid(), e); } } else { if let Err(e) = self.client.add_or_update_connection(conn).await { - eprintln!("Could not add/update the connection {}: {}", conn.uuid(), e); + log::error!("Could not add/update the connection {}: {}", conn.uuid(), e); } } } diff --git a/rust/agama-dbus-server/src/network/nm/client.rs b/rust/agama-dbus-server/src/network/nm/client.rs index 3f5926f0f7..dbcf7c47a4 100644 --- a/rust/agama-dbus-server/src/network/nm/client.rs +++ b/rust/agama-dbus-server/src/network/nm/client.rs @@ -4,8 +4,10 @@ use super::model::NmDeviceType; use super::proxies::{ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy}; use crate::network::model::{Connection, Device}; use agama_lib::error::ServiceError; +use log; use uuid::Uuid; use zbus; +use zbus::zvariant::{ObjectPath, OwnedObjectPath}; /// Simplified NetworkManager D-Bus client. /// @@ -53,9 +55,10 @@ impl<'a> NetworkManagerClient<'a> { }); } else { // TODO: use a logger - eprintln!( - "Unsupported device type {:?} for {}", - &device_type, &device_name + log::warn!( + "Ignoring network device '{}' (unsupported type '{}')", + &device_name, + &device_type ); } } @@ -87,14 +90,16 @@ impl<'a> NetworkManagerClient<'a> { /// * `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); - if let Ok(proxy) = self.get_connection_proxy(conn.uuid()).await { + 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); proxy.update(merged).await?; + OwnedObjectPath::from(proxy.path().to_owned()) } else { let proxy = SettingsProxy::new(&self.connection).await?; - proxy.add_connection(new_conn).await?; - } + proxy.add_connection(new_conn).await? + }; + self.activate_connection(path).await?; Ok(()) } @@ -105,6 +110,18 @@ impl<'a> NetworkManagerClient<'a> { Ok(()) } + /// Activates a NetworkManager connection. + /// + /// * `path`: D-Bus patch of the connection. + async fn activate_connection(&self, path: OwnedObjectPath) -> Result<(), ServiceError> { + let proxy = NetworkManagerProxy::new(&self.connection).await?; + let root = ObjectPath::try_from("/").unwrap(); + proxy + .activate_connection(&path.as_ref(), &root, &root) + .await?; + Ok(()) + } + async fn get_connection_proxy(&self, uuid: Uuid) -> Result { let proxy = SettingsProxy::new(&self.connection).await?; let uuid_s = uuid.to_string(); diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index a0ef6458b3..a52c3ee0c7 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -9,7 +9,6 @@ use agama_lib::{ network::types::SSID, }; use std::collections::HashMap; -use std::net::Ipv4Addr; use uuid::Uuid; use zbus::zvariant::{self, Value}; @@ -99,10 +98,12 @@ pub fn merge_dbus_connections<'a>( fn cleanup_dbus_connection<'a>(conn: &'a mut NestedHash) { if let Some(ipv4) = conn.get_mut("ipv4") { ipv4.remove("addresses"); + ipv4.remove("dns"); } if let Some(ipv6) = conn.get_mut("ipv6") { ipv6.remove("addresses"); + ipv6.remove("dns"); } } @@ -110,10 +111,10 @@ fn ipv4_to_dbus(ipv4: &Ipv4Config) -> HashMap<&str, zvariant::Value> { let addresses: Vec> = ipv4 .addresses .iter() - .map(|(addr, prefix)| { + .map(|ip| { HashMap::from([ - ("address", Value::new(addr.to_string())), - ("prefix", Value::new(prefix)), + ("address", Value::new(ip.addr().to_string())), + ("prefix", Value::new(ip.prefix())), ]) }) .collect(); @@ -183,13 +184,13 @@ fn ipv4_config_from_dbus(ipv4: &HashMap) -> Option let method: &str = ipv4.get("method")?.downcast_ref()?; let address_data = ipv4.get("address-data")?; let address_data = address_data.downcast_ref::()?; - let mut addresses: Vec<(Ipv4Addr, u32)> = vec![]; + let mut addresses: Vec = vec![]; for addr in address_data.get() { let dict = addr.downcast_ref::()?; let map = >>::try_from(dict.clone()).unwrap(); let addr_str: &str = map.get("address")?.downcast_ref()?; let prefix: &u32 = map.get("prefix")?.downcast_ref()?; - addresses.push((addr_str.parse().unwrap(), *prefix)) + addresses.push(IpAddress::new(addr_str.parse().unwrap(), *prefix)) } let mut ipv4_config = Ipv4Config { method: NmMethod(method.to_string()).try_into().ok()?, @@ -288,7 +289,7 @@ mod test { assert_eq!(connection.id(), "eth0"); let ipv4 = connection.ipv4(); - assert_eq!(ipv4.addresses, vec![(Ipv4Addr::new(192, 168, 0, 10), 24)]); + assert_eq!(ipv4.addresses, vec!["192.168.0.10/24".parse().unwrap()]); assert_eq!(ipv4.nameservers, vec![Ipv4Addr::new(192, 168, 0, 2)]); assert_eq!(ipv4.method, IpMethod::Auto); } @@ -436,7 +437,7 @@ mod test { } fn build_base_connection() -> BaseConnection { - let addresses = vec![(Ipv4Addr::new(192, 168, 0, 2), 24)]; + let addresses = vec!["192.168.0.2/24".parse().unwrap()]; let ipv4 = Ipv4Config { addresses, gateway: Some(Ipv4Addr::new(192, 168, 0, 1)), diff --git a/rust/agama-dbus-server/src/network/nm/model.rs b/rust/agama-dbus-server/src/network/nm/model.rs index 2b7378d851..cfffdf0cb5 100644 --- a/rust/agama-dbus-server/src/network/nm/model.rs +++ b/rust/agama-dbus-server/src/network/nm/model.rs @@ -7,9 +7,10 @@ /// Using the newtype pattern around an String is enough. For proper support, we might replace this /// struct with an enum. use crate::network::{ - model::{DeviceType, IpMethod, SecurityProtocol, WirelessMode}, + model::{IpMethod, SecurityProtocol, WirelessMode}, nm::error::NmError, }; +use agama_lib::network::types::DeviceType; use std::fmt; #[derive(Debug, PartialEq)] @@ -67,6 +68,12 @@ impl From for u32 { } } +impl fmt::Display for NmDeviceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + impl Default for NmDeviceType { fn default() -> Self { NmDeviceType(0) diff --git a/rust/agama-dbus-server/src/network/system.rs b/rust/agama-dbus-server/src/network/system.rs index b529ac7f23..8ff9d80d04 100644 --- a/rust/agama-dbus-server/src/network/system.rs +++ b/rust/agama-dbus-server/src/network/system.rs @@ -59,7 +59,9 @@ impl NetworkSystem { /// Populates the D-Bus tree with the known devices and connections. pub async fn setup(&mut self) -> Result<(), ServiceError> { - self.tree.set_connections(&self.state.connections).await?; + self.tree + .set_connections(&mut self.state.connections) + .await?; self.tree.set_devices(&self.state.devices).await?; Ok(()) } @@ -79,22 +81,24 @@ impl NetworkSystem { pub async fn dispatch_action(&mut self, action: Action) -> Result<(), Box> { match action { Action::AddConnection(name, ty) => { - let conn = Connection::new(name, ty); - self.tree.add_connection(&conn).await?; + let mut conn = Connection::new(name, ty); + self.tree.add_connection(&mut conn).await?; self.state.add_connection(conn)?; } Action::UpdateConnection(conn) => { self.state.update_connection(conn)?; } - Action::RemoveConnection(uuid) => { - self.tree.remove_connection(uuid).await?; - self.state.remove_connection(uuid)?; + Action::RemoveConnection(id) => { + self.tree.remove_connection(&id).await?; + self.state.remove_connection(&id)?; } Action::Apply => { self.to_network_manager().await?; // TODO: re-creating the tree is kind of brute-force and it sends signals about // adding/removing interfaces. We should add/update/delete objects as needed. - self.tree.set_connections(&self.state.connections).await?; + self.tree + .set_connections(&mut self.state.connections) + .await?; } } diff --git a/rust/agama-lib/share/examples/profile.json b/rust/agama-lib/share/examples/profile.json index 82ec4cb0db..12dad32cde 100644 --- a/rust/agama-lib/share/examples/profile.json +++ b/rust/agama-lib/share/examples/profile.json @@ -25,7 +25,7 @@ "network": { "connections": [ { - "name": "Ethernet network device 1", + "id": "Ethernet network device 1", "method": "manual", "addresses": [ "192.168.122.100/24" diff --git a/rust/agama-lib/share/examples/profile.jsonnet b/rust/agama-lib/share/examples/profile.jsonnet index b862c4435b..3f2dfc0b77 100644 --- a/rust/agama-lib/share/examples/profile.jsonnet +++ b/rust/agama-lib/share/examples/profile.jsonnet @@ -32,7 +32,7 @@ local findBiggestDisk(disks) = network: { connections: [ { - name: 'AgamaNetwork', + id: 'AgamaNetwork', wireless: { password: 'agama.test', security: 'wpa-psk', @@ -40,7 +40,7 @@ local findBiggestDisk(disks) = } }, { - name: 'Etherned device 1', + id: 'Etherned device 1', method: 'manual', gateway: '192.168.122.1', addresses: [ @@ -49,7 +49,7 @@ local findBiggestDisk(disks) = nameservers: [ '1.2.3.4' ] - ] - } - ] + } + ] + } } diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index b4ba50babc..e49ea4e43f 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -19,6 +19,7 @@ "network": { "description": "Network settings", "type": "object", + "additionalProperties": false, "properties": { "connections": { "description": "Network connections to be defined", @@ -27,7 +28,7 @@ "type": "object", "additionalProperties": false, "properties": { - "name": { + "id": { "description": "Connection ID", "type": "string" }, @@ -88,7 +89,7 @@ } }, "required": [ - "name" + "id" ] } } diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index 6ddd48a2b7..7f788900a3 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -2,10 +2,8 @@ use super::settings::{NetworkConnection, WirelessSettings}; use super::types::SSID; use crate::error::ServiceError; -use super::proxies::ConnectionProxy; -use super::proxies::ConnectionsProxy; -use super::proxies::IPv4Proxy; -use super::proxies::WirelessProxy; +use super::proxies::{ConnectionProxy, ConnectionsProxy, IPv4Proxy, WirelessProxy}; +use zbus::zvariant::OwnedObjectPath; use zbus::Connection; /// D-BUS client for the network service @@ -40,6 +38,12 @@ impl<'a> NetworkClient<'a> { Ok(connections) } + /// Applies the network configuration. + pub async fn apply(&self) -> Result<(), ServiceError> { + self.connections_proxy.apply().await?; + Ok(()) + } + /// Returns the NetworkConnection for the given connection path /// /// * `path`: the connections path to get the config from @@ -48,35 +52,24 @@ impl<'a> NetworkClient<'a> { .path(path)? .build() .await?; - let name = connection_proxy.id().await?; + let id = connection_proxy.id().await?; let ipv4_proxy = IPv4Proxy::builder(&self.connection) .path(path)? .build() .await?; - /// TODO: consider using the `IPMethod` struct from `agama-network`. - let method = match ipv4_proxy.method().await? { - 0 => "auto", - 1 => "manual", - 2 => "link-local", - 3 => "disable", - _ => "auto", - }; + let method = ipv4_proxy.method().await?; let gateway = match ipv4_proxy.gateway().await?.as_str() { "" => None, value => Some(value.to_string()), }; let nameservers = ipv4_proxy.nameservers().await?; let addresses = ipv4_proxy.addresses().await?; - let addresses = addresses - .into_iter() - .map(|(ip, prefix)| format!("{ip}/{prefix}")) - .collect(); Ok(NetworkConnection { - name, - method: method.to_string(), + id, + method: Some(method.to_string()), gateway, addresses, nameservers, @@ -101,4 +94,91 @@ impl<'a> NetworkClient<'a> { Ok(wireless) } + + /// Adds or updates a network connection. + /// + /// If a network connection with the same name exists, it updates its settings. Otherwise, it + /// adds a new connection. + /// + /// * `conn`: settings of the network connection to add/update. + pub async fn add_or_update_connection( + &self, + conn: &NetworkConnection, + ) -> Result<(), ServiceError> { + let path = match self.connections_proxy.get_connection(&conn.id).await { + Ok(path) => path, + Err(_) => self.add_connection(&conn).await?, + }; + self.update_connection(&path, &conn).await?; + Ok(()) + } + + /// Adds a network connection. + /// + /// * `conn`: settings of the network connection to add. + async fn add_connection( + &self, + conn: &NetworkConnection, + ) -> Result { + self.connections_proxy + .add_connection(&conn.id, conn.device_type() as u8) + .await?; + Ok(self.connections_proxy.get_connection(&conn.id).await?) + } + + /// Updates a network connection. + /// + /// * `path`: connection D-Bus path. + /// * `conn`: settings of the network connection. + async fn update_connection( + &self, + path: &OwnedObjectPath, + conn: &NetworkConnection, + ) -> Result<(), ServiceError> { + let proxy = IPv4Proxy::builder(&self.connection) + .path(path)? + .build() + .await?; + + if let Some(ref method) = conn.method { + proxy.set_method(method.as_str()).await?; + } + + let addresses: Vec<_> = conn.addresses.iter().map(String::as_ref).collect(); + proxy.set_addresses(addresses.as_slice()).await?; + + let nameservers: Vec<_> = conn.nameservers.iter().map(String::as_ref).collect(); + proxy.set_nameservers(nameservers.as_slice()).await?; + + let gateway = conn.gateway.as_ref().map(|g| g.as_str()).unwrap_or(""); + proxy.set_gateway(gateway).await?; + + if let Some(ref wireless) = conn.wireless { + self.update_wireless_settings(&path, wireless).await?; + } + Ok(()) + } + + /// Updates the wireless settings for network connection. + /// + /// * `path`: connection D-Bus path. + /// * `wireless`: wireless settings of the network connection. + async fn update_wireless_settings( + &self, + path: &OwnedObjectPath, + wireless: &WirelessSettings, + ) -> Result<(), ServiceError> { + let proxy = WirelessProxy::builder(&self.connection) + .path(path)? + .build() + .await?; + + proxy.set_ssid(wireless.ssid.as_bytes()).await?; + proxy.set_mode(wireless.mode.to_string().as_str()).await?; + proxy + .set_security(wireless.security.to_string().as_str()) + .await?; + proxy.set_password(&wireless.password).await?; + Ok(()) + } } diff --git a/rust/agama-lib/src/network/proxies.rs b/rust/agama-lib/src/network/proxies.rs index 1f975393a7..5b787ca677 100644 --- a/rust/agama-lib/src/network/proxies.rs +++ b/rust/agama-lib/src/network/proxies.rs @@ -18,6 +18,11 @@ trait Connections { /// Apply method fn apply(&self) -> zbus::Result<()>; + /// Gets a connection D-Bus path by its ID + /// + /// * `id`: connection ID. + fn get_connection(&self, id: &str) -> zbus::Result; + /// GetConnections method fn get_connections(&self) -> zbus::Result>; @@ -36,17 +41,20 @@ trait Wireless { /// Possible values are 'unknown', 'adhoc', 'infrastructure', 'ap' or 'mesh' #[dbus_proxy(property)] fn mode(&self) -> zbus::Result; - fn set_mode(&self, value: String) -> zbus::Result<()>; + #[dbus_proxy(property)] + fn set_mode(&self, value: &str) -> zbus::Result<()>; /// Password property #[dbus_proxy(property)] fn password(&self) -> zbus::Result; + #[dbus_proxy(property)] fn set_password(&self, value: &str) -> zbus::Result<()>; /// SSID property #[dbus_proxy(property, name = "SSID")] fn ssid(&self) -> zbus::Result>; - fn set_ssid(&self, value: Vec) -> zbus::Result<()>; + #[dbus_proxy(property, name = "SSID")] + fn set_ssid(&self, value: &[u8]) -> zbus::Result<()>; /// Wireless Security property /// @@ -54,7 +62,8 @@ trait Wireless { /// 'wpa-eap', 'wpa-eap-suite-b192' #[dbus_proxy(property)] fn security(&self) -> zbus::Result; - fn set_security(&self, value: String) -> zbus::Result<()>; + #[dbus_proxy(property)] + fn set_security(&self, value: &str) -> zbus::Result<()>; } #[dbus_proxy( @@ -66,10 +75,6 @@ trait Connection { /// Id property #[dbus_proxy(property)] fn id(&self) -> zbus::Result; - - /// UUID property - #[dbus_proxy(property, name = "UUID")] - fn uuid(&self) -> zbus::Result; } #[dbus_proxy( @@ -82,23 +87,27 @@ trait IPv4 { /// /// By now just an array of IPv4 addresses in string format #[dbus_proxy(property)] - fn addresses(&self) -> zbus::Result>; - fn set_addresses(&self, value: &[(&str, u32)]) -> zbus::Result<()>; + fn addresses(&self) -> zbus::Result>; + #[dbus_proxy(property)] + fn set_addresses(&self, value: &[&str]) -> zbus::Result<()>; /// Gateway property #[dbus_proxy(property)] fn gateway(&self) -> zbus::Result; + #[dbus_proxy(property)] fn set_gateway(&self, value: &str) -> zbus::Result<()>; /// Method property #[dbus_proxy(property)] - fn method(&self) -> zbus::Result; - fn set_method(&self, value: u8) -> zbus::Result<()>; + fn method(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_method(&self, value: &str) -> zbus::Result<()>; /// Nameservers property /// /// By now just an array of IPv4 addresses in string format #[dbus_proxy(property)] fn nameservers(&self) -> zbus::Result>; + #[dbus_proxy(property)] fn set_nameservers(&self, value: &[&str]) -> zbus::Result<()>; } diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index aea162f770..69a615bc95 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -1,21 +1,37 @@ //! Representation of the network settings -use crate::settings::{SettingObject, Settings}; -use agama_derive::Settings; +use super::types::DeviceType; +use crate::settings::{SettingObject, SettingValue, Settings}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::default::Default; /// Network settings for installation -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NetworkSettings { /// Connections to use in the installation - #[collection_setting] pub connections: Vec, } -#[derive(Debug, Default, Serialize, Deserialize)] +impl Settings for NetworkSettings { + fn add(&mut self, attr: &str, value: SettingObject) -> Result<(), &'static str> { + match attr { + "connections" => self.connections.push(value.try_into()?), + _ => return Err("unknown attribute"), + }; + Ok(()) + } + + fn merge(&mut self, other: &Self) + where + Self: Sized, + { + self.connections = other.connections.clone(); + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct WirelessSettings { #[serde(skip_serializing_if = "String::is_empty")] pub password: String, @@ -24,30 +40,94 @@ pub struct WirelessSettings { pub mode: String, } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct NetworkConnection { - pub name: String, - pub method: String, + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub method: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gateway: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(skip_serializing_if = "Vec::is_empty", default)] pub addresses: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(skip_serializing_if = "Vec::is_empty", default)] pub nameservers: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub wireless: Option, } +impl NetworkConnection { + /// Device type expected for the network connection. + /// + /// Which device type to use is inferred from the included settings. For instance, if it has + /// wireless settings, it should be applied to a wireless device. + pub fn device_type(&self) -> DeviceType { + if self.wireless.is_some() { + DeviceType::Wireless + } else { + DeviceType::Ethernet + } + } +} + impl TryFrom for NetworkConnection { type Error = &'static str; fn try_from(value: SettingObject) -> Result { - match value.0.get("name") { - Some(name) => Ok(NetworkConnection { - name: name.clone().try_into()?, - ..Default::default() - }), - None => Err("'name' key not found"), - } + let Some(id) = value.get("id") else { + return Err("The 'id' key is missing"); + }; + + let default_method = SettingValue("disabled".to_string()); + let method = value.get("method").unwrap_or(&default_method); + + let conn = NetworkConnection { + id: id.clone().try_into()?, + method: method.clone().try_into()?, + ..Default::default() + }; + + Ok(conn) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::settings::{SettingObject, SettingValue}; + use std::collections::HashMap; + + #[test] + fn test_device_type() { + let eth = NetworkConnection::default(); + assert_eq!(eth.device_type(), DeviceType::Ethernet); + + let wlan = NetworkConnection { + wireless: Some(WirelessSettings::default()), + ..Default::default() + }; + assert_eq!(wlan.device_type(), DeviceType::Wireless); + } + + #[test] + fn test_add_connection_to_setting() { + let name = SettingValue("Ethernet 1".to_string()); + let method = SettingValue("auto".to_string()); + let conn = HashMap::from([("id".to_string(), name), ("method".to_string(), method)]); + let conn = SettingObject(conn); + + let mut settings = NetworkSettings::default(); + settings.add("connections", conn).unwrap(); + assert_eq!(settings.connections.len(), 1); + } + + #[test] + fn test_setting_object_to_network_connection() { + let name = SettingValue("Ethernet 1".to_string()); + let method = SettingValue("auto".to_string()); + let settings = HashMap::from([("id".to_string(), name), ("method".to_string(), method)]); + let settings = SettingObject(settings); + let conn: NetworkConnection = settings.try_into().unwrap(); + assert_eq!(conn.id, "Ethernet 1"); + assert_eq!(conn.method, Some("auto".to_string())); } } diff --git a/rust/agama-lib/src/network/store.rs b/rust/agama-lib/src/network/store.rs index 6853976a25..3315eb05b9 100644 --- a/rust/agama-lib/src/network/store.rs +++ b/rust/agama-lib/src/network/store.rs @@ -19,10 +19,18 @@ impl<'a> NetworkStore<'a> { pub async fn load(&self) -> Result> { let connections = self.network_client.connections().await?; - Ok(NetworkSettings { connections }) + Ok(NetworkSettings { + connections, + ..Default::default() + }) } pub async fn store(&self, settings: &NetworkSettings) -> Result<(), Box> { + for conn in &settings.connections { + self.network_client.add_or_update_connection(&conn).await?; + } + self.network_client.apply().await?; + Ok(()) } } diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index 537427838b..5c3ea6cf2f 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -1,6 +1,7 @@ -use std::{fmt, str}; - use serde::{Deserialize, Serialize}; +use std::{fmt, str}; +use thiserror::Error; +use zbus; #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] pub struct SSID(pub Vec); @@ -22,3 +23,60 @@ impl From for Vec { value.0.clone() } } + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum DeviceType { + Loopback = 0, + Ethernet = 1, + Wireless = 2, +} + +#[derive(Debug, Error, PartialEq)] +#[error("Invalid device type: {0}")] +pub struct InvalidDeviceType(u8); + +impl TryFrom for DeviceType { + type Error = InvalidDeviceType; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(DeviceType::Loopback), + 1 => Ok(DeviceType::Ethernet), + 2 => Ok(DeviceType::Wireless), + _ => Err(InvalidDeviceType(value)), + } + } +} + +impl From for zbus::fdo::Error { + fn from(value: InvalidDeviceType) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(format!("Network error: {}", value.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_ssid() { + let ssid = SSID(vec![97, 103, 97, 109, 97]); + assert_eq!(format!("{}", ssid), "agama"); + } + + #[test] + fn test_ssid_to_vec() { + let vec = vec![97, 103, 97, 109, 97]; + let ssid = SSID(vec.clone()); + assert_eq!(ssid.to_vec(), &vec); + } + + #[test] + fn test_device_type_from_u8() { + let dtype = DeviceType::try_from(0); + assert_eq!(dtype, Ok(DeviceType::Loopback)); + + let dtype = DeviceType::try_from(128); + assert_eq!(dtype, Err(InvalidDeviceType(128))); + } +} diff --git a/rust/agama-lib/src/settings.rs b/rust/agama-lib/src/settings.rs index 3f2d56bb47..e1c03e1e86 100644 --- a/rust/agama-lib/src/settings.rs +++ b/rust/agama-lib/src/settings.rs @@ -86,14 +86,24 @@ pub trait Settings { /// let value: bool = value.try_into().expect("the conversion failed"); /// assert_eq!(value, true); /// ``` -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct SettingValue(pub String); /// Represents a string-based collection and allows converting to other types /// /// It wraps a hash which uses String as key and SettingValue as value. +#[derive(Debug)] pub struct SettingObject(pub HashMap); +impl SettingObject { + /// Returns the value for the given key. + /// + /// * `key`: setting key. + pub fn get(&self, key: &str) -> Option<&SettingValue> { + self.0.get(key) + } +} + impl From> for SettingObject { fn from(value: HashMap) -> SettingObject { let mut hash: HashMap = HashMap::new(); diff --git a/rust/agama-lib/src/storage/settings.rs b/rust/agama-lib/src/storage/settings.rs index a17cad4327..1e208be46b 100644 --- a/rust/agama-lib/src/storage/settings.rs +++ b/rust/agama-lib/src/storage/settings.rs @@ -29,7 +29,7 @@ impl TryFrom for Device { type Error = &'static str; fn try_from(value: SettingObject) -> Result { - match value.0.get("name") { + match value.get("name") { Some(name) => Ok(Device { name: name.clone().try_into()?, }),