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()?,
}),