diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 37f34d5bbf..8bc36469c1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -44,10 +44,11 @@ dependencies = [ name = "agama-lib" version = "1.0.0" dependencies = [ + "agama-network", + "agama-utils", "anyhow", "async-trait", "chrono", - "cidr", "curl", "env_logger", "fluent-uri", @@ -88,17 +89,41 @@ dependencies = [ "utoipa", ] +[[package]] +name = "agama-network" +version = "0.1.0" +dependencies = [ + "agama-utils", + "anyhow", + "async-trait", + "cidr", + "futures-util", + "macaddr", + "pin-project", + "serde", + "serde_with", + "strum", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-test", + "tracing", + "utoipa", + "uuid", + "zbus", +] + [[package]] name = "agama-server" version = "0.1.0" dependencies = [ "agama-lib", "agama-locale-data", + "agama-utils", "anyhow", "async-trait", "axum", "axum-extra", - "cidr", "clap", "config", "futures-util", @@ -108,7 +133,6 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "libsystemd", - "macaddr", "openssl", "pam", "pin-project", @@ -137,6 +161,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "agama-utils" +version = "0.1.0" +dependencies = [ + "serde_json", + "utoipa", + "zvariant", +] + [[package]] name = "ahash" version = "0.8.11" @@ -5310,14 +5343,13 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.4.0" +version = "5.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" +checksum = "e34d60a2cff1bfddd0d4a73791fd0a4255a59e349aa0cec437cb0df7776f2450" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", "winnow", "zvariant_derive", "zvariant_utils", @@ -5325,9 +5357,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.4.0" +version = "5.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" +checksum = "0604376d86f838e0c3f4fe6d6ee1f861a04dac9015cbbb25c25d336996c91423" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 862bf98219..51549e628c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,10 +1,12 @@ [workspace] members = [ - "agama-cli", - "agama-server", - "agama-lib", - "agama-locale-data", - "xtask", + "agama-cli", + "agama-server", + "agama-lib", + "agama-locale-data", + "agama-network", + "agama-utils", + "xtask", ] resolver = "2" diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index aa2b9ac279..bc2aa734cc 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -7,8 +7,9 @@ edition = "2021" [dependencies] anyhow = "1.0" +agama-utils = { path = "../agama-utils" } +agama-network = { path = "../agama-network" } async-trait = "0.1.83" -cidr = { version = "0.3.1", features = ["serde"] } futures-util = "0.3.30" jsonschema = { version = "0.30.0", default-features = false, features = [ "resolve-file", diff --git a/rust/agama-lib/src/jobs.rs b/rust/agama-lib/src/jobs.rs index 6b6e9cfb08..5b6b85bc47 100644 --- a/rust/agama-lib/src/jobs.rs +++ b/rust/agama-lib/src/jobs.rs @@ -27,7 +27,8 @@ use std::collections::HashMap; use serde::Serialize; use zbus::zvariant::OwnedValue; -use crate::{dbus::get_property, error::ServiceError}; +use crate::error::ServiceError; +use agama_utils::dbus::get_property; pub mod client; diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 59924d4756..9719ac7d62 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -59,20 +59,18 @@ pub mod manager; pub mod network; pub mod product; pub mod profile; -pub mod software; -pub mod storage; -pub mod users; -// TODO: maybe expose only clients when we have it? -pub mod dbus; -pub mod openapi; pub mod progress; pub mod proxies; pub mod questions; pub mod scripts; pub mod security; +pub mod software; +pub mod storage; mod store; +pub mod users; pub use store::Store; pub mod utils; +pub use agama_utils::{dbus, openapi}; use crate::error::ServiceError; use zbus::conn::Builder; diff --git a/rust/agama-lib/src/network.rs b/rust/agama-lib/src/network.rs index fe08e823e5..41fa7fb7d9 100644 --- a/rust/agama-lib/src/network.rs +++ b/rust/agama-lib/src/network.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2024-2025] SUSE LLC // // All Rights Reserved. // @@ -21,10 +21,12 @@ //! Implements support for handling the network settings mod client; -pub mod settings; mod store; -pub mod types; +pub use agama_network::{ + error, model, settings, types, Action, Adapter, NetworkAdapterError, NetworkManagerAdapter, + NetworkSystem, NetworkSystemClient, NetworkSystemError, +}; pub use client::{NetworkClient, NetworkClientError}; pub use settings::NetworkSettings; pub use store::{NetworkStore, NetworkStoreError}; diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index a69f827043..81ee4b6a91 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -18,10 +18,10 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::dbus::{get_optional_property, get_property}; use crate::error::ServiceError; use crate::software::model::{AddonParams, AddonProperties}; use crate::software::proxies::SoftwareProductProxy; +use agama_utils::dbus::{get_optional_property, get_property}; use serde::Serialize; use std::collections::HashMap; use zbus::Connection; diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 68838ec235..886e2f0c88 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -27,8 +27,8 @@ use super::model::{ }; use super::proxies::{DevicesProxy, ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; use super::StorageSettings; -use crate::dbus::get_property; use crate::error::ServiceError; +use agama_utils::dbus::get_property; use serde_json::value::RawValue; use std::collections::HashMap; use zbus::fdo::ObjectManagerProxy; diff --git a/rust/agama-lib/src/storage/client/iscsi.rs b/rust/agama-lib/src/storage/client/iscsi.rs index ff7ee64185..4b0f1423b5 100644 --- a/rust/agama-lib/src/storage/client/iscsi.rs +++ b/rust/agama-lib/src/storage/client/iscsi.rs @@ -22,10 +22,10 @@ use core::fmt; use std::collections::HashMap; use crate::{ - dbus::{extract_id_from_path, get_property}, error::ServiceError, storage::proxies::iscsi::{ISCSIProxy, InitiatorProxy, NodeProxy}, }; +use agama_utils::dbus::{extract_id_from_path, get_property}; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use thiserror::Error; diff --git a/rust/agama-lib/src/storage/client/zfcp.rs b/rust/agama-lib/src/storage/client/zfcp.rs index 0826b2b4d3..4f5a6bc2da 100644 --- a/rust/agama-lib/src/storage/client/zfcp.rs +++ b/rust/agama-lib/src/storage/client/zfcp.rs @@ -30,13 +30,13 @@ use zbus::{ }; use crate::{ - dbus::{extract_id_from_path, get_property}, error::ServiceError, storage::{ model::zfcp::{ZFCPController, ZFCPDisk}, proxies::zfcp::{ControllerProxy, ManagerProxy}, }, }; +use agama_utils::dbus::{extract_id_from_path, get_property}; const ZFCP_CONTROLLER_PREFIX: &str = "/org/opensuse/Agama/Storage1/zfcp_controllers"; diff --git a/rust/agama-lib/src/storage/model.rs b/rust/agama-lib/src/storage/model.rs index 458343c776..ff456f1e5e 100644 --- a/rust/agama-lib/src/storage/model.rs +++ b/rust/agama-lib/src/storage/model.rs @@ -23,7 +23,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use zbus::zvariant::{OwnedValue, Value}; -use crate::dbus::{get_optional_property, get_property}; +use agama_utils::dbus::{get_optional_property, get_property}; pub mod dasd; pub mod zfcp; diff --git a/rust/agama-lib/src/storage/model/dasd.rs b/rust/agama-lib/src/storage/model/dasd.rs index 6e30b87a3e..02207e9d61 100644 --- a/rust/agama-lib/src/storage/model/dasd.rs +++ b/rust/agama-lib/src/storage/model/dasd.rs @@ -24,7 +24,8 @@ use std::collections::HashMap; use serde::Serialize; use zbus::zvariant::OwnedValue; -use crate::{dbus::get_property, error::ServiceError}; +use crate::error::ServiceError; +use agama_utils::dbus::get_property; /// Represents a DASD device (specific to s390x systems). #[derive(Clone, Debug, Serialize, Default, utoipa::ToSchema)] diff --git a/rust/agama-lib/src/storage/model/zfcp.rs b/rust/agama-lib/src/storage/model/zfcp.rs index 4419276ad6..e65f3cb1c8 100644 --- a/rust/agama-lib/src/storage/model/zfcp.rs +++ b/rust/agama-lib/src/storage/model/zfcp.rs @@ -24,7 +24,8 @@ use std::collections::HashMap; use serde::Serialize; use zbus::zvariant::OwnedValue; -use crate::{dbus::get_property, error::ServiceError}; +use crate::error::ServiceError; +use agama_utils::dbus::get_property; /// Represents a zFCP disk (specific to s390x systems). #[derive(Clone, Debug, Serialize, Default, utoipa::ToSchema)] diff --git a/rust/agama-network/Cargo.toml b/rust/agama-network/Cargo.toml new file mode 100644 index 0000000000..6b93d7307c --- /dev/null +++ b/rust/agama-network/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "agama-network" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true + +[dependencies] +agama-utils = { path = "../agama-utils" } +anyhow = "1.0.98" +async-trait = "0.1.88" +cidr = { version = "0.3.1", features = ["serde"] } +futures-util = { version = "0.3.30", default-features = false, features = [ + "alloc", +] } +macaddr = { version = "1.0.1", features = ["serde_std"] } +pin-project = "1.1.10" +serde = { version = "1.0.219", features = ["derive"] } +serde_with = "3.12.0" +strum = { version = "0.27.1", features = ["derive"] } +thiserror = "2.0.12" +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } +tokio-stream = "0.1.17" +tokio-test = "0.4.4" +tracing = "0.1.41" +utoipa = { version = "5.3.1", features = ["uuid"] } +uuid = { version = "1.16.0", features = ["v4"] } +zbus = { version = "5", default-features = false, features = ["tokio"] } diff --git a/rust/agama-server/src/network/action.rs b/rust/agama-network/src/action.rs similarity index 96% rename from rust/agama-server/src/network/action.rs rename to rust/agama-network/src/action.rs index 28437e2a36..d1d18a83ba 100644 --- a/rust/agama-server/src/network/action.rs +++ b/rust/agama-network/src/action.rs @@ -18,8 +18,8 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::network::model::{AccessPoint, Connection, Device}; -use agama_lib::network::types::{ConnectionState, DeviceType}; +use crate::model::{AccessPoint, Connection, Device}; +use crate::types::{ConnectionState, DeviceType}; use tokio::sync::oneshot; use uuid::Uuid; diff --git a/rust/agama-server/src/network/adapter.rs b/rust/agama-network/src/adapter.rs similarity index 88% rename from rust/agama-server/src/network/adapter.rs rename to rust/agama-network/src/adapter.rs index bbcde2460b..d61627223d 100644 --- a/rust/agama-server/src/network/adapter.rs +++ b/rust/agama-network/src/adapter.rs @@ -18,8 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::network::{model::StateConfig, Action, NetworkState}; -use agama_lib::error::ServiceError; +use crate::{model::StateConfig, Action, NetworkState}; use async_trait::async_trait; use thiserror::Error; use tokio::sync::mpsc::UnboundedSender; @@ -27,13 +26,13 @@ use tokio::sync::mpsc::UnboundedSender; #[derive(Error, Debug)] pub enum NetworkAdapterError { #[error("Could not read the network configuration: {0}")] - Read(ServiceError), + Read(anyhow::Error), #[error("Could not update the network configuration: {0}")] - Write(ServiceError), + Write(anyhow::Error), #[error("Checkpoint handling error: {0}")] - Checkpoint(ServiceError), // only relevant for adapters that implement a checkpoint mechanism + Checkpoint(anyhow::Error), // only relevant for adapters that implement a checkpoint mechanism #[error("The network watcher cannot run: {0}")] - Watcher(ServiceError), + Watcher(anyhow::Error), } /// A trait for the ability to read/write from/to a network service. diff --git a/rust/agama-server/src/network/error.rs b/rust/agama-network/src/error.rs similarity index 100% rename from rust/agama-server/src/network/error.rs rename to rust/agama-network/src/error.rs diff --git a/rust/agama-network/src/lib.rs b/rust/agama-network/src/lib.rs new file mode 100644 index 0000000000..01b992bc03 --- /dev/null +++ b/rust/agama-network/src/lib.rs @@ -0,0 +1,38 @@ +// Copyright (c) [2025] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +//! Network configuration service for Agama +//! +//! This library implements the network configuration service for Agama. + +pub mod action; +pub mod adapter; +pub mod error; +pub mod model; +mod nm; +pub mod settings; +mod system; +pub mod types; + +pub use action::Action; +pub use adapter::{Adapter, NetworkAdapterError}; +pub use model::NetworkState; +pub use nm::NetworkManagerAdapter; +pub use system::{NetworkSystem, NetworkSystemClient, NetworkSystemError}; diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-network/src/model.rs similarity index 99% rename from rust/agama-server/src/network/model.rs rename to rust/agama-network/src/model.rs index 4cfb06b53f..32ca60cdae 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-network/src/model.rs @@ -22,12 +22,10 @@ //! //! * This module contains the types that represent the network concepts. They are supposed to be //! agnostic from the real network service (e.g., NetworkManager). -use crate::network::error::NetworkStateError; -use agama_lib::network::settings::{ - BondSettings, IEEE8021XSettings, NetworkConnection, WirelessSettings, -}; -use agama_lib::network::types::{BondMode, ConnectionState, DeviceState, DeviceType, Status, SSID}; -use agama_lib::openapi::schemas; +use crate::error::NetworkStateError; +use crate::settings::{BondSettings, IEEE8021XSettings, NetworkConnection, WirelessSettings}; +use crate::types::{BondMode, ConnectionState, DeviceState, DeviceType, Status, SSID}; +use agama_utils::openapi::schemas; use cidr::IpInet; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; @@ -251,7 +249,7 @@ impl NetworkState { #[cfg(test)] mod tests { use super::*; - use crate::network::error::NetworkStateError; + use crate::error::NetworkStateError; use uuid::Uuid; #[test] @@ -1608,13 +1606,13 @@ impl fmt::Display for WirelessBand { } impl TryFrom<&str> for WirelessBand { - type Error = anyhow::Error; + type Error = NetworkStateError; fn try_from(value: &str) -> Result { match value { "a" => Ok(WirelessBand::A), "bg" => Ok(WirelessBand::BG), - _ => Err(anyhow::anyhow!("Invalid band: {}", value)), + _ => Err(NetworkStateError::InvalidWirelessBand(value.to_string())), } } } diff --git a/rust/agama-server/src/network/nm.rs b/rust/agama-network/src/nm.rs similarity index 100% rename from rust/agama-server/src/network/nm.rs rename to rust/agama-network/src/nm.rs diff --git a/rust/agama-server/src/network/nm/adapter.rs b/rust/agama-network/src/nm/adapter.rs similarity index 87% rename from rust/agama-server/src/network/nm/adapter.rs rename to rust/agama-network/src/nm/adapter.rs index c777c8fdd2..61ef9dabdf 100644 --- a/rust/agama-server/src/network/nm/adapter.rs +++ b/rust/agama-network/src/nm/adapter.rs @@ -18,17 +18,19 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::network::{ +use crate::{ adapter::Watcher, model::{Connection, NetworkState, StateConfig}, nm::{NetworkManagerClient, NetworkManagerWatcher}, Adapter, NetworkAdapterError, }; -use agama_lib::error::ServiceError; +use anyhow::anyhow; use async_trait::async_trait; use core::time; use std::thread; +use super::error::NmError; + /// An adapter for NetworkManager pub struct NetworkManagerAdapter<'a> { client: NetworkManagerClient<'a>, @@ -37,7 +39,7 @@ pub struct NetworkManagerAdapter<'a> { impl<'a> NetworkManagerAdapter<'a> { /// Returns the adapter for system's NetworkManager. - pub async fn from_system() -> Result, ServiceError> { + pub async fn from_system() -> Result, NmError> { let connection = zbus::Connection::system().await?; let client = NetworkManagerClient::new(connection.clone()).await?; Ok(Self { client, connection }) @@ -51,7 +53,7 @@ impl Adapter for NetworkManagerAdapter<'_> { .client .general_state() .await - .map_err(NetworkAdapterError::Read)?; + .map_err(|e| NetworkAdapterError::Read(anyhow!(e)))?; let mut state = NetworkState::default(); @@ -64,7 +66,7 @@ impl Adapter for NetworkManagerAdapter<'_> { .client .devices() .await - .map_err(NetworkAdapterError::Read)?; + .map_err(|e| NetworkAdapterError::Read(anyhow!(e)))?; } if config.connections { @@ -72,7 +74,7 @@ impl Adapter for NetworkManagerAdapter<'_> { .client .connections() .await - .map_err(NetworkAdapterError::Read)?; + .map_err(|e| NetworkAdapterError::Read(anyhow!(e)))?; } if config.access_points && general_state.wireless_enabled { @@ -80,14 +82,14 @@ impl Adapter for NetworkManagerAdapter<'_> { self.client .request_scan() .await - .map_err(NetworkAdapterError::Read)?; + .map_err(|e| NetworkAdapterError::Read(anyhow!(e)))?; thread::sleep(time::Duration::from_secs(1)); }; state.access_points = self .client .access_points() .await - .map_err(NetworkAdapterError::Read)?; + .map_err(|e| NetworkAdapterError::Read(anyhow!(e)))?; } Ok(state) @@ -106,7 +108,7 @@ impl Adapter for NetworkManagerAdapter<'_> { .client .create_checkpoint() .await - .map_err(NetworkAdapterError::Checkpoint)?; + .map_err(|e| NetworkAdapterError::Checkpoint(anyhow!(e)))?; tracing::info!("Updating the general state {:?}", &network.general_state); @@ -119,14 +121,14 @@ impl Adapter for NetworkManagerAdapter<'_> { self.client .rollback_checkpoint(&checkpoint.as_ref()) .await - .map_err(NetworkAdapterError::Checkpoint)?; + .map_err(|e| NetworkAdapterError::Checkpoint(anyhow!(e)))?; tracing::error!( "Could not update the general state {:?}: {}", &network.general_state, &e ); - return Err(NetworkAdapterError::Write(e)); + return Err(NetworkAdapterError::Write(anyhow!(e))); } for conn in ordered_connections(network) { @@ -157,16 +159,16 @@ impl Adapter for NetworkManagerAdapter<'_> { self.client .rollback_checkpoint(&checkpoint.as_ref()) .await - .map_err(NetworkAdapterError::Checkpoint)?; + .map_err(|e| NetworkAdapterError::Checkpoint(anyhow!(e)))?; tracing::error!("Could not process the connection {}: {}", conn.id, &e); - return Err(NetworkAdapterError::Write(e)); + return Err(NetworkAdapterError::Write(anyhow!(e))); } } self.client .destroy_checkpoint(&checkpoint.as_ref()) .await - .map_err(NetworkAdapterError::Checkpoint)?; + .map_err(|e| NetworkAdapterError::Checkpoint(anyhow!(e)))?; Ok(()) } diff --git a/rust/agama-server/src/network/nm/builder.rs b/rust/agama-network/src/nm/builder.rs similarity index 93% rename from rust/agama-server/src/network/nm/builder.rs rename to rust/agama-network/src/nm/builder.rs index ec8e272b24..3f79fa659e 100644 --- a/rust/agama-server/src/network/nm/builder.rs +++ b/rust/agama-network/src/nm/builder.rs @@ -20,22 +20,18 @@ //! Conversion mechanism between proxies and model structs. -use crate::network::{ +use crate::types::{DeviceState, DeviceType}; +use crate::{ model::{Device, IpConfig, IpRoute, MacAddress}, nm::{ model::NmDeviceType, proxies::{DeviceProxy, IP4ConfigProxy, IP6ConfigProxy}, }, }; -use agama_lib::{ - error::ServiceError, - network::types::{DeviceState, DeviceType}, -}; -use anyhow::Context; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr, str::FromStr}; -use super::model::NmDeviceState; +use super::{error::NmError, model::NmDeviceState}; /// Builder to create a [Device] from its corresponding NetworkManager D-Bus representation. pub struct DeviceFromProxyBuilder<'a> { @@ -52,12 +48,9 @@ impl<'a> DeviceFromProxyBuilder<'a> { } /// Creates a [Device] starting on the [DeviceProxy]. - pub async fn build(&self) -> Result { + pub async fn build(&self) -> Result { let device_type = NmDeviceType(self.proxy.device_type().await?); - // TODO: we need to check the errors hierarchy to not abuse ServiceError. - let type_: DeviceType = device_type - .try_into() - .context("Unsupported device type: {device_type}")?; + let type_: DeviceType = device_type.try_into()?; let mut device = Device { name: self.proxy.interface().await?, @@ -78,7 +71,7 @@ impl<'a> DeviceFromProxyBuilder<'a> { Ok(device) } - async fn build_ip_config(&self) -> Result, ServiceError> { + async fn build_ip_config(&self) -> Result, NmError> { let ip4_path = self.proxy.ip4_config().await?; let ip6_path = self.proxy.ip6_config().await?; @@ -111,7 +104,7 @@ impl<'a> DeviceFromProxyBuilder<'a> { &self, ip4_proxy: IP4ConfigProxy<'_>, ip6_proxy: IP6ConfigProxy<'_>, - ) -> Result { + ) -> Result { let address_data = ip4_proxy.address_data().await?; let nameserver_data = ip4_proxy.nameserver_data().await?; let mut addresses: Vec = vec![]; @@ -250,13 +243,11 @@ impl<'a> DeviceFromProxyBuilder<'a> { /// See https://www.networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState /// and https://www.networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceStateReason /// for further information. - async fn device_state_from_proxy(&self) -> Result { + async fn device_state_from_proxy(&self) -> Result { const USER_REQUESTED: u32 = 39; let (state, reason) = self.proxy.state_reason().await?; - let state: NmDeviceState = (state as u8) - .try_into() - .context("Unsupported device state: {state}")?; + let state: NmDeviceState = (state as u8).try_into()?; let device_state = match state { NmDeviceState::Unknown => DeviceState::Unknown, diff --git a/rust/agama-server/src/network/nm/client.rs b/rust/agama-network/src/nm/client.rs similarity index 91% rename from rust/agama-server/src/network/nm/client.rs rename to rust/agama-network/src/nm/client.rs index 2edb78555b..836c88765f 100644 --- a/rust/agama-server/src/network/nm/client.rs +++ b/rust/agama-network/src/nm/client.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2025] SUSE LLC // // All Rights Reserved. // @@ -26,17 +26,17 @@ use super::dbus::{ cleanup_dbus_connection, connection_from_dbus, connection_to_dbus, controller_from_dbus, merge_dbus_connections, }; +use super::error::NmError; use super::model::{NmConnectionState, NmDeviceType}; use super::proxies::{ AccessPointProxy, ActiveConnectionProxy, ConnectionProxy, DeviceProxy, NetworkManagerProxy, SettingsProxy, WirelessProxy, }; -use crate::network::model::{ +use crate::model::{ AccessPoint, Connection, ConnectionConfig, Device, GeneralState, SecurityProtocol, }; -use agama_lib::dbus::get_optional_property; -use agama_lib::error::ServiceError; -use agama_lib::network::types::{DeviceType, SSID}; +use crate::types::{DeviceType, SSID}; +use agama_utils::dbus::get_optional_property; use uuid::Uuid; use zbus; use zbus::zvariant::{ObjectPath, OwnedObjectPath}; @@ -54,16 +54,14 @@ impl<'a> NetworkManagerClient<'a> { /// Creates a NetworkManagerClient using the given D-Bus connection. /// /// * `connection`: D-Bus connection. - pub async fn new( - connection: zbus::Connection, - ) -> Result, ServiceError> { + pub async fn new(connection: zbus::Connection) -> Result, NmError> { Ok(Self { nm_proxy: NetworkManagerProxy::new(&connection).await?, connection, }) } /// Returns the general state - pub async fn general_state(&self) -> Result { + pub async fn general_state(&self) -> Result { let proxy = SettingsProxy::new(&self.connection).await?; let hostname = proxy.hostname().await?; let wireless_enabled = self.nm_proxy.wireless_enabled().await?; @@ -82,7 +80,7 @@ impl<'a> NetworkManagerClient<'a> { } /// Updates the general state - pub async fn update_general_state(&self, state: &GeneralState) -> Result<(), ServiceError> { + pub async fn update_general_state(&self, state: &GeneralState) -> Result<(), NmError> { let wireless_enabled = self.nm_proxy.wireless_enabled().await?; if wireless_enabled != state.wireless_enabled { @@ -95,7 +93,7 @@ impl<'a> NetworkManagerClient<'a> { } /// Returns the list of access points. - pub async fn request_scan(&self) -> Result<(), ServiceError> { + pub async fn request_scan(&self) -> Result<(), NmError> { for path in &self.nm_proxy.get_devices().await? { let proxy = DeviceProxy::builder(&self.connection) .path(path.as_str())? @@ -116,7 +114,7 @@ impl<'a> NetworkManagerClient<'a> { } /// Returns the list of access points. - pub async fn access_points(&self) -> Result, ServiceError> { + pub async fn access_points(&self) -> Result, NmError> { let mut points = vec![]; for path in &self.nm_proxy.get_devices().await? { let proxy = DeviceProxy::builder(&self.connection) @@ -160,7 +158,7 @@ impl<'a> NetworkManagerClient<'a> { } /// Returns the list of network devices. - pub async fn devices(&self) -> Result, ServiceError> { + pub async fn devices(&self) -> Result, NmError> { let mut devs = vec![]; for path in &self.nm_proxy.get_all_devices().await? { let proxy = DeviceProxy::builder(&self.connection) @@ -182,7 +180,7 @@ impl<'a> NetworkManagerClient<'a> { } /// Returns the list of network connections. - pub async fn connections(&self) -> Result, ServiceError> { + pub async fn connections(&self) -> Result, NmError> { let mut controlled_by: HashMap = HashMap::new(); let mut uuids_map: HashMap = HashMap::new(); @@ -251,7 +249,7 @@ impl<'a> NetworkManagerClient<'a> { Ok(connections) } - pub async fn connection_states(&self) -> Result, ServiceError> { + pub async fn connection_states(&self) -> Result, NmError> { let mut states = HashMap::new(); for active_path in &self.nm_proxy.active_connections().await? { @@ -272,7 +270,7 @@ impl<'a> NetworkManagerClient<'a> { &self, conn: &Connection, controller: Option<&Connection>, - ) -> Result<(), ServiceError> { + ) -> Result<(), NmError> { let mut new_conn = connection_to_dbus(conn, controller); let path = if let Ok(proxy) = self.get_connection_proxy(conn.uuid).await { @@ -295,14 +293,14 @@ impl<'a> NetworkManagerClient<'a> { } /// Removes a network connection. - pub async fn remove_connection(&self, uuid: Uuid) -> Result<(), ServiceError> { + pub async fn remove_connection(&self, uuid: Uuid) -> Result<(), NmError> { let proxy = self.get_connection_proxy(uuid).await?; proxy.delete().await?; Ok(()) } /// Creates a checkpoint. - pub async fn create_checkpoint(&self) -> Result { + pub async fn create_checkpoint(&self) -> Result { let path = self.nm_proxy.checkpoint_create(&[], 0, 0).await?; Ok(path) } @@ -310,10 +308,7 @@ impl<'a> NetworkManagerClient<'a> { /// Destroys a checkpoint. /// /// * `checkpoint`: checkpoint's D-Bus path. - pub async fn destroy_checkpoint( - &self, - checkpoint: &ObjectPath<'_>, - ) -> Result<(), ServiceError> { + pub async fn destroy_checkpoint(&self, checkpoint: &ObjectPath<'_>) -> Result<(), NmError> { self.nm_proxy.checkpoint_destroy(checkpoint).await?; Ok(()) } @@ -321,10 +316,7 @@ impl<'a> NetworkManagerClient<'a> { /// Rolls the configuration back to the given checkpoint. /// /// * `checkpoint`: checkpoint's D-Bus path. - pub async fn rollback_checkpoint( - &self, - checkpoint: &ObjectPath<'_>, - ) -> Result<(), ServiceError> { + pub async fn rollback_checkpoint(&self, checkpoint: &ObjectPath<'_>) -> Result<(), NmError> { self.nm_proxy.checkpoint_rollback(checkpoint).await?; Ok(()) } @@ -332,7 +324,7 @@ impl<'a> NetworkManagerClient<'a> { /// Activates a NetworkManager connection. /// /// * `path`: D-Bus patch of the connection. - async fn activate_connection(&self, path: OwnedObjectPath) -> Result<(), ServiceError> { + async fn activate_connection(&self, path: OwnedObjectPath) -> Result<(), NmError> { let proxy = NetworkManagerProxy::new(&self.connection).await?; let root = ObjectPath::try_from("/").unwrap(); proxy @@ -344,7 +336,7 @@ impl<'a> NetworkManagerClient<'a> { /// Deactivates a NetworkManager connection. /// /// * `path`: D-Bus patch of the connection. - async fn deactivate_connection(&self, path: OwnedObjectPath) -> Result<(), ServiceError> { + async fn deactivate_connection(&self, path: OwnedObjectPath) -> Result<(), NmError> { let proxy = NetworkManagerProxy::new(&self.connection).await?; if let Some(active_connection) = self.settings_active_connection(path.clone()).await? { @@ -354,14 +346,14 @@ impl<'a> NetworkManagerClient<'a> { { // Ignore ConnectionNotActive error since this just means the state is already correct if !e.to_string().contains("ConnectionNotActive") { - return Err(ServiceError::DBus(e)); + return Err(NmError::DBus(e)); } } } Ok(()) } - async fn get_connection_proxy(&self, uuid: Uuid) -> Result { + async fn get_connection_proxy(&self, uuid: Uuid) -> Result { let proxy = SettingsProxy::new(&self.connection).await?; let uuid_s = uuid.to_string(); let path = proxy.get_connection_by_uuid(uuid_s.as_str()).await?; @@ -375,7 +367,7 @@ impl<'a> NetworkManagerClient<'a> { async fn settings_active_connection( &self, path: OwnedObjectPath, - ) -> Result, ServiceError> { + ) -> Result, NmError> { for active_path in &self.nm_proxy.active_connections().await? { let proxy = ActiveConnectionProxy::builder(&self.connection) .path(active_path.as_str())? @@ -397,7 +389,7 @@ impl<'a> NetworkManagerClient<'a> { pub async fn add_secrets( config: &mut ConnectionConfig, proxy: &ConnectionProxy<'_>, - ) -> Result<(), ServiceError> { + ) -> Result<(), NmError> { let ConnectionConfig::Wireless(ref mut wireless) = config else { return Ok(()); }; diff --git a/rust/agama-server/src/network/nm/dbus.rs b/rust/agama-network/src/nm/dbus.rs similarity index 99% rename from rust/agama-server/src/network/nm/dbus.rs rename to rust/agama-network/src/nm/dbus.rs index 987ed8fabd..3c05395072 100644 --- a/rust/agama-server/src/network/nm/dbus.rs +++ b/rust/agama-network/src/nm/dbus.rs @@ -23,10 +23,10 @@ //! Working with hash maps coming from D-Bus is rather tedious and it is even worse when working //! with nested hash maps (see [NestedHash] and [OwnedNestedHash]). use super::{error::NmError, model::*}; -use crate::network::model::*; -use agama_lib::{ - dbus::{get_optional_property, get_property, to_owned_hash, NestedHash, OwnedNestedHash}, - network::types::{BondMode, SSID}, +use crate::model::*; +use crate::types::{BondMode, SSID}; +use agama_utils::dbus::{ + get_optional_property, get_property, to_owned_hash, NestedHash, OwnedNestedHash, }; use cidr::IpInet; use macaddr::MacAddr6; @@ -1298,14 +1298,14 @@ mod test { connection_from_dbus, connection_to_dbus, merge_dbus_connections, NestedHash, OwnedNestedHash, }; - use crate::network::{ + use crate::types::{BondMode, SSID}; + use crate::{ model::*, nm::{ dbus::{BOND_KEY, ETHERNET_KEY, INFINIBAND_KEY, WIRELESS_KEY, WIRELESS_SECURITY_KEY}, error::NmError, }, }; - use agama_lib::network::types::{BondMode, SSID}; use cidr::IpInet; use std::{collections::HashMap, net::IpAddr, str::FromStr}; use uuid::Uuid; diff --git a/rust/agama-server/src/network/nm/error.rs b/rust/agama-network/src/nm/error.rs similarity index 69% rename from rust/agama-server/src/network/nm/error.rs rename to rust/agama-network/src/nm/error.rs index cb5487874e..6032e79f60 100644 --- a/rust/agama-server/src/network/nm/error.rs +++ b/rust/agama-network/src/nm/error.rs @@ -19,13 +19,23 @@ // find current contact information at www.suse.com. //! NetworkManager error types -use crate::network::error::NetworkStateError; +use crate::error::NetworkStateError; use cidr::errors::NetworkLengthTooLongError; use std::net::AddrParseError; use thiserror::Error; +use super::model::InvalidNmDeviceState; + #[derive(Error, Debug)] pub enum NmError { + #[error("D-Bus service error: {0}")] + DBus(#[from] zbus::Error), + #[error("Could not connect to Agama bus at '{0}': {1}")] + DBusConnectionError(String, #[source] zbus::Error), + #[error("D-Bus protocol error: {0}")] + DBusProtocol(#[from] zbus::fdo::Error), + #[error("D-Bus message error")] + DBusMessage(#[from] zbus::zvariant::Error), #[error("Unsupported IP method: '{0}'")] UnsupportedIpMethod(String), #[error("Unsupported device type: '{0}'")] @@ -38,30 +48,32 @@ pub enum NmError { UnsupporedWirelessMode(String), #[error("Missing connection information")] MissingConnectionSection, - #[error("D-Bus message error")] - DBusMessage(#[from] zbus::zvariant::Error), #[error("Invalid network UUID")] InvalidNetworkUUID(#[from] uuid::Error), #[error("Connection type not supported for '{0}'")] UnsupportedConnectionType(String), + // FIXME: including the crate::model::InvalidEAPMethod looks wrong. + // Same for the rest. #[error("Invalid EAP method: '{0}'")] - InvalidEAPMethod(#[from] crate::network::model::InvalidEAPMethod), + InvalidEAPMethod(#[from] crate::model::InvalidEAPMethod), #[error("Invalid EAP phase2-auth method: '{0}'")] - InvalidPhase2AuthMethod(#[from] crate::network::model::InvalidPhase2AuthMethod), + InvalidPhase2AuthMethod(#[from] crate::model::InvalidPhase2AuthMethod), #[error("Invalid group algorithm: '{0}'")] - InvalidGroupAlgorithm(#[from] crate::network::model::InvalidGroupAlgorithm), + InvalidGroupAlgorithm(#[from] crate::model::InvalidGroupAlgorithm), #[error("Invalid pairwise algorithm: '{0}'")] - InvalidPairwiseAlgorithm(#[from] crate::network::model::InvalidPairwiseAlgorithm), + InvalidPairwiseAlgorithm(#[from] crate::model::InvalidPairwiseAlgorithm), #[error("Invalid WPA protocol version: '{0}'")] - InvalidWPAProtocolVersion(#[from] crate::network::model::InvalidWPAProtocolVersion), + InvalidWPAProtocolVersion(#[from] crate::model::InvalidWPAProtocolVersion), #[error("Invalid infiniband transport mode: '{0}'")] - InvalidInfinibandTranportMode(#[from] crate::network::model::InvalidInfinibandTransportMode), + InvalidInfinibandTranportMode(#[from] crate::model::InvalidInfinibandTransportMode), #[error("Invalid MAC address: '{0}'")] - InvalidMACAddress(#[from] crate::network::model::InvalidMacAddress), + InvalidMACAddress(#[from] crate::model::InvalidMacAddress), #[error("Invalid network prefix: '{0}'")] InvalidNetworkPrefix(#[from] NetworkLengthTooLongError), #[error("Invalid network address: '{0}'")] InvalidNetworkAddress(#[from] AddrParseError), + #[error("Invalid device state: '{0}'")] + InvalidDeviceState(#[from] InvalidNmDeviceState), } impl From for NetworkStateError { diff --git a/rust/agama-server/src/network/nm/model.rs b/rust/agama-network/src/nm/model.rs similarity index 99% rename from rust/agama-server/src/network/nm/model.rs rename to rust/agama-network/src/nm/model.rs index e41144fa29..5fe98ca32a 100644 --- a/rust/agama-server/src/network/nm/model.rs +++ b/rust/agama-network/src/nm/model.rs @@ -26,11 +26,11 @@ /// /// Using the newtype pattern around an String is enough. For proper support, we might replace this /// struct with an enum. -use crate::network::{ +use crate::{ model::{Ipv4Method, Ipv6Method, SecurityProtocol, WirelessMode}, nm::error::NmError, + types::{ConnectionState, DeviceType}, }; -use agama_lib::network::types::{ConnectionState, DeviceType}; use std::fmt; use std::str::FromStr; diff --git a/rust/agama-server/src/network/nm/proxies.rs b/rust/agama-network/src/nm/proxies.rs similarity index 99% rename from rust/agama-server/src/network/nm/proxies.rs rename to rust/agama-network/src/nm/proxies.rs index 6e9603b3cf..f17d492f06 100644 --- a/rust/agama-server/src/network/nm/proxies.rs +++ b/rust/agama-network/src/nm/proxies.rs @@ -32,7 +32,7 @@ //! …consequently `zbus-xmlgen` did not generate code for the above interfaces. //! Also some proxies can be used against multiple services when they share interface. -use agama_lib::dbus::OwnedNestedHash; +use agama_utils::dbus::OwnedNestedHash; use zbus::proxy; #[proxy( diff --git a/rust/agama-server/src/network/nm/streams.rs b/rust/agama-network/src/nm/streams.rs similarity index 100% rename from rust/agama-server/src/network/nm/streams.rs rename to rust/agama-network/src/nm/streams.rs diff --git a/rust/agama-server/src/network/nm/streams/common.rs b/rust/agama-network/src/nm/streams/common.rs similarity index 94% rename from rust/agama-server/src/network/nm/streams/common.rs rename to rust/agama-network/src/nm/streams/common.rs index 010d920903..8d1ba18189 100644 --- a/rust/agama-server/src/network/nm/streams/common.rs +++ b/rust/agama-network/src/nm/streams/common.rs @@ -18,9 +18,10 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use agama_lib::error::ServiceError; use zbus::{message::Type as MessageType, zvariant::OwnedObjectPath, MatchRule, MessageStream}; +use crate::nm::error::NmError; + #[derive(Debug, Clone)] pub enum NmChange { DeviceAdded(OwnedObjectPath), @@ -35,7 +36,7 @@ pub enum NmChange { pub async fn build_added_and_removed_stream( connection: &zbus::Connection, -) -> Result { +) -> Result { let rule = MatchRule::builder() .msg_type(MessageType::Signal) .path("/org/freedesktop")? @@ -50,7 +51,7 @@ pub async fn build_added_and_removed_stream( /// It listens for changes in several objects that are related to a network device. pub async fn build_properties_changed_stream( connection: &zbus::Connection, -) -> Result { +) -> Result { let rule = MatchRule::builder() .msg_type(MessageType::Signal) .interface("org.freedesktop.DBus.Properties")? diff --git a/rust/agama-server/src/network/nm/streams/connections.rs b/rust/agama-network/src/nm/streams/connections.rs similarity index 98% rename from rust/agama-server/src/network/nm/streams/connections.rs rename to rust/agama-network/src/nm/streams/connections.rs index 207228eeb3..39c7fac765 100644 --- a/rust/agama-server/src/network/nm/streams/connections.rs +++ b/rust/agama-network/src/nm/streams/connections.rs @@ -18,7 +18,6 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use agama_lib::error::ServiceError; use futures_util::ready; use pin_project::pin_project; use std::{ @@ -33,6 +32,8 @@ use zbus::{ Message, MessageStream, }; +use crate::nm::error::NmError; + use super::{ common::{build_added_and_removed_stream, build_properties_changed_stream}, NmChange, @@ -55,7 +56,7 @@ impl ActiveConnectionChangedStream { /// Builds a new stream using the given D-Bus connection. /// /// * `connection`: D-Bus connection. - pub async fn new(connection: &zbus::Connection) -> Result { + pub async fn new(connection: &zbus::Connection) -> Result { let connection = connection.clone(); let mut inner = StreamMap::new(); inner.insert( diff --git a/rust/agama-server/src/network/nm/streams/devices.rs b/rust/agama-network/src/nm/streams/devices.rs similarity index 99% rename from rust/agama-server/src/network/nm/streams/devices.rs rename to rust/agama-network/src/nm/streams/devices.rs index be031ee08f..0da8c2f8e9 100644 --- a/rust/agama-server/src/network/nm/streams/devices.rs +++ b/rust/agama-network/src/nm/streams/devices.rs @@ -18,7 +18,6 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use agama_lib::error::ServiceError; use futures_util::ready; use pin_project::pin_project; use std::{ @@ -35,6 +34,7 @@ use zbus::{ }; use super::common::{build_added_and_removed_stream, build_properties_changed_stream, NmChange}; +use crate::nm::error::NmError; /// Stream of device-related events. /// @@ -53,7 +53,7 @@ impl DeviceChangedStream { /// Builds a new stream using the given D-Bus connection. /// /// * `connection`: D-Bus connection. - pub async fn new(connection: &zbus::Connection) -> Result { + pub async fn new(connection: &zbus::Connection) -> Result { let connection = connection.clone(); let mut inner = StreamMap::new(); inner.insert( diff --git a/rust/agama-server/src/network/nm/watcher.rs b/rust/agama-network/src/nm/watcher.rs similarity index 94% rename from rust/agama-server/src/network/nm/watcher.rs rename to rust/agama-network/src/nm/watcher.rs index 065eca6550..2f446848fc 100644 --- a/rust/agama-server/src/network/nm/watcher.rs +++ b/rust/agama-network/src/nm/watcher.rs @@ -25,10 +25,10 @@ use std::collections::{hash_map::Entry, HashMap}; -use crate::network::{ +use crate::{ adapter::Watcher, model::Device, nm::proxies::DeviceProxy, Action, NetworkAdapterError, }; -use agama_lib::error::ServiceError; +use anyhow::anyhow; use async_trait::async_trait; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio_stream::StreamExt; @@ -36,6 +36,7 @@ use zbus::zvariant::OwnedObjectPath; use super::{ builder::DeviceFromProxyBuilder, + error::NmError, model::NmConnectionState, proxies::{ActiveConnectionProxy, NetworkManagerProxy}, streams::{ActiveConnectionChangedStream, DeviceChangedStream, NmChange}, @@ -108,7 +109,10 @@ impl Watcher for NetworkManagerWatcher { // Turn the changes into actions in a separate task. let connection = self.connection.clone(); let mut dispatcher = ActionDispatcher::new(connection, rx, actions); - dispatcher.run().await.map_err(NetworkAdapterError::Watcher) + dispatcher + .run() + .await + .map_err(|e| NetworkAdapterError::Watcher(anyhow!(e))) } } @@ -144,7 +148,7 @@ impl ActionDispatcher<'_> { /// Processes the updates. /// /// It runs until the updates channel is closed. - pub async fn run(&mut self) -> Result<(), ServiceError> { + pub async fn run(&mut self) -> Result<(), NmError> { self.read_devices().await?; while let Some(update) = self.updates_rx.recv().await { let result = match update { @@ -169,7 +173,7 @@ impl ActionDispatcher<'_> { } /// Reads the devices. - async fn read_devices(&mut self) -> Result<(), ServiceError> { + async fn read_devices(&mut self) -> Result<(), NmError> { let nm_proxy = NetworkManagerProxy::new(&self.connection).await?; for path in nm_proxy.get_devices().await? { self.proxies.find_or_add_device(&path).await?; @@ -180,7 +184,7 @@ impl ActionDispatcher<'_> { /// Handles the case where a new device appears. /// /// * `path`: D-Bus object path of the new device. - async fn handle_device_added(&mut self, path: OwnedObjectPath) -> Result<(), ServiceError> { + async fn handle_device_added(&mut self, path: OwnedObjectPath) -> Result<(), NmError> { let (_, proxy) = self.proxies.find_or_add_device(&path).await?; if let Ok(device) = Self::device_from_proxy(&self.connection, proxy.clone()).await { _ = self.actions_tx.send(Action::AddDevice(Box::new(device))); @@ -193,7 +197,7 @@ impl ActionDispatcher<'_> { /// Handles the case where a device is updated. /// /// * `path`: D-Bus object path of the updated device. - async fn handle_device_updated(&mut self, path: OwnedObjectPath) -> Result<(), ServiceError> { + async fn handle_device_updated(&mut self, path: OwnedObjectPath) -> Result<(), NmError> { let (old_name, proxy) = self.proxies.find_or_add_device(&path).await?; let device = Self::device_from_proxy(&self.connection, proxy.clone()).await?; let new_name = device.name.clone(); @@ -207,7 +211,7 @@ impl ActionDispatcher<'_> { /// Handles the case where a device is removed. /// /// * `path`: D-Bus object path of the removed device. - async fn handle_device_removed(&mut self, path: OwnedObjectPath) -> Result<(), ServiceError> { + async fn handle_device_removed(&mut self, path: OwnedObjectPath) -> Result<(), NmError> { if let Some((name, _)) = self.proxies.remove_device(&path) { _ = self.actions_tx.send(Action::RemoveDevice(name)); } @@ -217,10 +221,7 @@ impl ActionDispatcher<'_> { /// Handles the case where the IPv4 configuration changes. /// /// * `path`: D-Bus object path of the changed IP configuration. - async fn handle_ip4_config_changed( - &mut self, - path: OwnedObjectPath, - ) -> Result<(), ServiceError> { + async fn handle_ip4_config_changed(&mut self, path: OwnedObjectPath) -> Result<(), NmError> { if let Some((name, proxy)) = self.proxies.find_device_for_ip4(&path).await { let device = Self::device_from_proxy(&self.connection, proxy.clone()).await?; _ = self @@ -233,10 +234,7 @@ impl ActionDispatcher<'_> { /// Handles the case where the IPv6 configuration changes. /// /// * `path`: D-Bus object path of the changed IP configuration. - async fn handle_ip6_config_changed( - &mut self, - path: OwnedObjectPath, - ) -> Result<(), ServiceError> { + async fn handle_ip6_config_changed(&mut self, path: OwnedObjectPath) -> Result<(), NmError> { if let Some((name, proxy)) = self.proxies.find_device_for_ip6(&path).await { let device = Self::device_from_proxy(&self.connection, proxy.clone()).await?; _ = self @@ -252,7 +250,7 @@ impl ActionDispatcher<'_> { async fn handle_active_connection_updated( &mut self, path: OwnedObjectPath, - ) -> Result<(), ServiceError> { + ) -> Result<(), NmError> { let proxy = self.proxies.find_or_add_active_connection(&path).await?; let id = proxy.id().await?; let state = proxy.state().await.map(|s| NmConnectionState(s.clone()))?; @@ -272,7 +270,7 @@ impl ActionDispatcher<'_> { async fn handle_active_connection_removed( &mut self, path: OwnedObjectPath, - ) -> Result<(), ServiceError> { + ) -> Result<(), NmError> { if let Some(proxy) = self.proxies.remove_active_connection(&path) { let id = proxy.id().await?; let state = proxy.state().await.map(|s| NmConnectionState(s.clone()))?; @@ -289,7 +287,7 @@ impl ActionDispatcher<'_> { async fn device_from_proxy( connection: &zbus::Connection, proxy: DeviceProxy<'_>, - ) -> Result { + ) -> Result { let builder = DeviceFromProxyBuilder::new(connection, &proxy); builder.build().await } @@ -318,7 +316,7 @@ impl<'a> ProxiesRegistry<'a> { pub async fn find_or_add_device( &mut self, path: &OwnedObjectPath, - ) -> Result<&(String, DeviceProxy<'a>), ServiceError> { + ) -> Result<&(String, DeviceProxy<'a>), NmError> { // Cannot use entry(...).or_insert_with(...) because of the async call. match self.devices.entry(path.clone()) { Entry::Vacant(entry) => { @@ -340,7 +338,7 @@ impl<'a> ProxiesRegistry<'a> { pub async fn find_or_add_active_connection( &mut self, path: &OwnedObjectPath, - ) -> Result<&ActiveConnectionProxy<'a>, ServiceError> { + ) -> Result<&ActiveConnectionProxy<'a>, NmError> { // Cannot use entry(...).or_insert_with(...) because of the async call. match self.active_connections.entry(path.clone()) { Entry::Vacant(entry) => { diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-network/src/settings.rs similarity index 99% rename from rust/agama-lib/src/network/settings.rs rename to rust/agama-network/src/settings.rs index f329085d46..8ac8161420 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-network/src/settings.rs @@ -21,7 +21,7 @@ //! Representation of the network settings use super::types::{DeviceState, DeviceType, Status}; -use crate::openapi::schemas; +use agama_utils::openapi::schemas; use cidr::IpInet; use serde::{Deserialize, Serialize}; use std::default::Default; diff --git a/rust/agama-server/src/network/system.rs b/rust/agama-network/src/system.rs similarity index 96% rename from rust/agama-server/src/network/system.rs rename to rust/agama-network/src/system.rs index 0edd5f45ef..41f04f3e07 100644 --- a/rust/agama-server/src/network/system.rs +++ b/rust/agama-network/src/system.rs @@ -18,16 +18,15 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use super::{ +use crate::{ + action::Action, error::NetworkStateError, - model::{AccessPoint, Device, NetworkChange, StateConfig}, - NetworkAdapterError, + model::{ + AccessPoint, Connection, Device, GeneralState, NetworkChange, NetworkState, StateConfig, + }, + types::DeviceType, + Adapter, NetworkAdapterError, }; -use crate::network::{ - model::{Connection, GeneralState}, - Action, Adapter, NetworkState, -}; -use agama_lib::{error::ServiceError, network::types::DeviceType}; use std::error::Error; use tokio::sync::{ broadcast::{self, Receiver}, @@ -44,8 +43,6 @@ pub enum NetworkSystemError { InputError(#[from] SendError), #[error("Could not read an answer from the network system: {0}")] OutputError(#[from] RecvError), - #[error("D-Bus service error: {0}")] - ServiceError(#[from] ServiceError), #[error("Network backend error: {0}")] AdapterError(#[from] NetworkAdapterError), } @@ -56,8 +53,7 @@ pub enum NetworkSystemError { /// passing like the example below. /// /// ```no_run -/// # use agama_server::network::{Action, NetworkManagerAdapter, NetworkSystem}; -/// # use agama_lib::connection; +/// # use agama_network::{Action, NetworkManagerAdapter, NetworkSystem}; /// # use tokio::sync::oneshot; /// /// # tokio_test::block_on(async { diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-network/src/types.rs similarity index 100% rename from rust/agama-lib/src/network/types.rs rename to rust/agama-network/src/types.rs diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index a86bc872d6..a057126679 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -10,16 +10,15 @@ rust-version.workspace = true anyhow = "1.0" agama-locale-data = { path = "../agama-locale-data" } agama-lib = { path = "../agama-lib" } +agama-utils = { path = "../agama-utils" } zbus = { version = "5", default-features = false, features = ["tokio"] } uuid = { version = "1.10.0", features = ["v4"] } thiserror = "2.0.12" serde = { version = "1.0.210", features = ["derive"] } -cidr = { version = "0.3.1", features = ["serde"] } tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.16" gettext-rs = { version = "0.7.1", features = ["gettext-system"] } regex = "1.11.0" -macaddr = { version = "1.0", features = ["serde_std"] } async-trait = "0.1.83" axum = { version = "0.7.7", features = ["ws"] } serde_json = "1.0.128" diff --git a/rust/agama-server/src/dbus.rs b/rust/agama-server/src/dbus.rs index bb98881826..9cf9da92c0 100644 --- a/rust/agama-server/src/dbus.rs +++ b/rust/agama-server/src/dbus.rs @@ -27,7 +27,8 @@ use std::{ task::{Context, Poll}, }; -use agama_lib::{dbus::to_owned_hash, error::ServiceError}; +use agama_lib::error::ServiceError; +use agama_utils::dbus::to_owned_hash; use futures_util::{ready, Stream}; use pin_project::pin_project; use tokio_stream::StreamMap; diff --git a/rust/agama-server/src/network.rs b/rust/agama-server/src/network.rs index 3b0f5a89de..95e80f2639 100644 --- a/rust/agama-server/src/network.rs +++ b/rust/agama-server/src/network.rs @@ -18,56 +18,8 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -//! Network configuration service for Agama -//! -//! This library implements the network configuration service for Agama. -//! -//! ## Connections and devices -//! -//! The library is built around the concepts of network devices and connections, akin to -//! NetworkManager approach. -//! -//! Each network device is exposed as a D-Bus object using a path like -//! `/org/opensuse/Agama1/Network/devices/[0-9]+`. At this point, those objects expose a bit of -//! information about network devices. The entry point for the devices is the -//! `/org/opensuse/Agama1/Network/devices` object, that expose a `GetDevices` method that returns -//! the paths for the devices objects. -//! -//! The network configuration is exposed through the connections objects as -//! `/org/opensuse/Agama1/Network/connections/[0-9]+`. Those objects are composed of several -//! D-Bus interfaces depending on its type: -//! -//! * `org.opensuse.Agama1.Network.Connection` exposes common information across all connection -//! types. -//! * `org.opensuse.Agama1.Network.Connection.IPv4` includes IPv4 settings, like the configuration -//! method (DHCP, manual, etc.), IP addresses, name servers and so on. -//! * `org.opensuse.Agama1.Network.Connection.Wireless` exposes the configuration for wireless -//! connections. -//! -//! Analogous to the devices API, there is a special `/org/opensuse/Agama1/Network/connections` -//! object that implements a few methods that are related to the collection of connections like -//! `GetConnections`, `AddConnection` and `RemoveConnection`. Additionally, it implements an -//! `Apply` method to write the changes to the NetworkManager service. -//! -//! ## Limitations -//! -//! We expect to address the following problems as we evolve the API, but it is noteworthy to have -//! them in mind: -//! -//! * The devices list does not reflect the changes in the system. For instance, it is not updated -//! when a device is connected to the system. -//! * Many configuration types are still missing (bridges, bonding, etc.). - -mod action; -mod adapter; -pub mod error; -pub mod model; -mod nm; -pub mod system; pub mod web; -pub use action::Action; -pub use adapter::{Adapter, NetworkAdapterError}; -pub use model::NetworkState; -pub use nm::NetworkManagerAdapter; -pub use system::NetworkSystem; +pub use agama_lib::network::{ + model::NetworkState, Action, Adapter, NetworkAdapterError, NetworkManagerAdapter, NetworkSystem, +}; diff --git a/rust/agama-server/src/network/web.rs b/rust/agama-server/src/network/web.rs index a8f42db8f2..6a49670efa 100644 --- a/rust/agama-server/src/network/web.rs +++ b/rust/agama-server/src/network/web.rs @@ -33,17 +33,15 @@ use axum::{ Json, Router, }; -use super::{ - error::NetworkStateError, - model::{AccessPoint, GeneralState}, - system::{NetworkSystemClient, NetworkSystemError}, - Adapter, -}; - -use crate::network::{model::Connection, model::Device, NetworkSystem}; use agama_lib::{ error::ServiceError, - network::{settings::NetworkConnection, types::NetworkConnectionWithState}, + network::{ + error::NetworkStateError, + model::{AccessPoint, Connection, Device, GeneralState}, + settings::NetworkConnection, + types::NetworkConnectionWithState, + Adapter, NetworkSystem, NetworkSystemClient, NetworkSystemError, + }, }; use serde_json::json; diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index afebc222ea..e834c9d12f 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -27,11 +27,11 @@ use crate::{error::Error, web::Event}; use agama_lib::{ - dbus::{extract_id_from_path, get_property}, error::ServiceError, proxies::questions::{GenericQuestionProxy, QuestionWithPasswordProxy, QuestionsProxy}, questions::model::{Answer, GenericQuestion, PasswordAnswer, Question, QuestionWithPassword}, }; +use agama_utils::dbus::{extract_id_from_path, get_property}; use anyhow::Context; use axum::{ extract::{Path, State}, diff --git a/rust/agama-server/src/storage/web/dasd/stream.rs b/rust/agama-server/src/storage/web/dasd/stream.rs index 57c1e5783b..273a12c93d 100644 --- a/rust/agama-server/src/storage/web/dasd/stream.rs +++ b/rust/agama-server/src/storage/web/dasd/stream.rs @@ -23,14 +23,13 @@ use std::{collections::HashMap, task::Poll}; use agama_lib::{ - dbus::get_optional_property, error::ServiceError, - property_from_dbus, storage::{ client::dasd::DASDClient, model::dasd::{DASDDevice, DASDFormatSummary}, }, }; +use agama_utils::{dbus::get_optional_property, property_from_dbus}; use futures_util::{ready, Stream}; use pin_project::pin_project; use thiserror::Error; diff --git a/rust/agama-server/src/storage/web/iscsi.rs b/rust/agama-server/src/storage/web/iscsi.rs index 9de855ed23..b16ed1cc88 100644 --- a/rust/agama-server/src/storage/web/iscsi.rs +++ b/rust/agama-server/src/storage/web/iscsi.rs @@ -33,13 +33,13 @@ use crate::{ }, }; use agama_lib::{ - dbus::{get_optional_property, to_owned_hash}, error::ServiceError, storage::{ client::iscsi::{ISCSIAuth, ISCSIInitiator, ISCSINode, LoginResult}, ISCSIClient, }, }; +use agama_utils::dbus::{get_optional_property, to_owned_hash}; use axum::{ extract::{Path, State}, http::StatusCode, diff --git a/rust/agama-server/src/storage/web/iscsi/stream.rs b/rust/agama-server/src/storage/web/iscsi/stream.rs index 8e88c3f9e5..391ce36dbc 100644 --- a/rust/agama-server/src/storage/web/iscsi/stream.rs +++ b/rust/agama-server/src/storage/web/iscsi/stream.rs @@ -21,11 +21,13 @@ use std::{collections::HashMap, task::Poll}; use agama_lib::{ - dbus::{extract_id_from_path, get_optional_property}, error::ServiceError, - property_from_dbus, storage::{ISCSIClient, ISCSINode}, }; +use agama_utils::{ + dbus::{extract_id_from_path, get_optional_property}, + property_from_dbus, +}; use futures_util::{ready, Stream}; use pin_project::pin_project; use thiserror::Error; diff --git a/rust/agama-server/src/storage/web/zfcp/stream.rs b/rust/agama-server/src/storage/web/zfcp/stream.rs index 659b755a08..15722e1f3e 100644 --- a/rust/agama-server/src/storage/web/zfcp/stream.rs +++ b/rust/agama-server/src/storage/web/zfcp/stream.rs @@ -23,14 +23,13 @@ use std::{collections::HashMap, task::Poll}; use agama_lib::{ - dbus::get_optional_property, error::ServiceError, - property_from_dbus, storage::{ client::zfcp::ZFCPClient, model::zfcp::{ZFCPController, ZFCPDisk}, }, }; +use agama_utils::{dbus::get_optional_property, property_from_dbus}; use futures_util::{ready, Stream}; use pin_project::pin_project; use thiserror::Error; diff --git a/rust/agama-server/src/web/common/jobs.rs b/rust/agama-server/src/web/common/jobs.rs index c861c9bc39..9e13333029 100644 --- a/rust/agama-server/src/web/common/jobs.rs +++ b/rust/agama-server/src/web/common/jobs.rs @@ -21,11 +21,10 @@ use std::{collections::HashMap, pin::Pin, task::Poll}; use agama_lib::{ - dbus::get_optional_property, error::ServiceError, jobs::{client::JobsClient, Job}, - property_from_dbus, }; +use agama_utils::{dbus::get_optional_property, property_from_dbus}; use axum::{extract::State, routing::get, Json, Router}; use futures_util::{ready, Stream}; use pin_project::pin_project; diff --git a/rust/agama-server/src/web/docs/network.rs b/rust/agama-server/src/web/docs/network.rs index 604eac227a..e65a28f5f3 100644 --- a/rust/agama-server/src/web/docs/network.rs +++ b/rust/agama-server/src/web/docs/network.rs @@ -18,7 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use agama_lib::openapi::schemas; +use agama_utils::openapi::schemas; use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; @@ -62,46 +62,46 @@ impl ApiDocBuilder for NetworkApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() - .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema("IpAddr", schemas::ip_addr()) .schema("IpInet", schemas::ip_inet()) .schema("macaddr.MacAddr6", schemas::mac_addr6()) diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 75de687b1a..a33e3ed19b 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -18,7 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::network::model::NetworkChange; +use agama_lib::network::model::NetworkChange; use agama_lib::{ jobs::Job, localization::model::LocaleConfig, diff --git a/rust/agama-server/tests/network_service.rs b/rust/agama-server/tests/network_service.rs index 7bad291c96..c23a16c8aa 100644 --- a/rust/agama-server/tests/network_service.rs +++ b/rust/agama-server/tests/network_service.rs @@ -23,12 +23,11 @@ pub mod common; use agama_lib::error::ServiceError; use agama_lib::network::settings::{BondSettings, NetworkConnection}; use agama_lib::network::types::{DeviceType, SSID}; -use agama_server::network::web::network_service; -use agama_server::network::{ - self, - model::{self, AccessPoint, GeneralState, StateConfig}, - Adapter, NetworkAdapterError, NetworkState, +use agama_lib::network::{ + model::{self, AccessPoint, GeneralState, NetworkState, StateConfig}, + Adapter, NetworkAdapterError, }; +use agama_server::network::web::network_service; use async_trait::async_trait; use axum::http::header; @@ -62,15 +61,15 @@ async fn build_service(state: NetworkState) -> Result { } #[derive(Default)] -pub struct NetworkTestAdapter(network::NetworkState); +pub struct NetworkTestAdapter(NetworkState); #[async_trait] impl Adapter for NetworkTestAdapter { - async fn read(&self, _: StateConfig) -> Result { + async fn read(&self, _: StateConfig) -> Result { Ok(self.0.clone()) } - async fn write(&self, _network: &network::NetworkState) -> Result<(), NetworkAdapterError> { + async fn write(&self, _network: &NetworkState) -> Result<(), NetworkAdapterError> { unimplemented!("Not used in tests"); } } diff --git a/rust/agama-utils/Cargo.toml b/rust/agama-utils/Cargo.toml new file mode 100644 index 0000000000..88f2d6b43b --- /dev/null +++ b/rust/agama-utils/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "agama-utils" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true + +[dependencies] +serde_json = "1.0.140" +utoipa = "5.3.1" +zvariant = "5.5.2" diff --git a/rust/agama-lib/src/dbus.rs b/rust/agama-utils/src/dbus.rs similarity index 90% rename from rust/agama-lib/src/dbus.rs rename to rust/agama-utils/src/dbus.rs index 78984d5625..0e2c3d7d2f 100644 --- a/rust/agama-lib/src/dbus.rs +++ b/rust/agama-utils/src/dbus.rs @@ -19,7 +19,7 @@ // find current contact information at www.suse.com. use std::collections::HashMap; -use zbus::zvariant::{self, OwnedObjectPath, OwnedValue, Value}; +use zvariant::{self, OwnedObjectPath, OwnedValue, Value}; /// Nested hash to send to D-Bus. pub type NestedHash<'a> = HashMap<&'a str, HashMap<&'a str, zvariant::Value<'a>>>; @@ -30,14 +30,14 @@ pub type OwnedNestedHash = HashMap pub fn get_property<'a, T>( properties: &'a HashMap, name: &str, -) -> Result +) -> Result where T: TryFrom>, - >>::Error: Into, + >>::Error: Into, { let value: Value = properties .get(name) - .ok_or(zbus::zvariant::Error::Message(format!( + .ok_or(zvariant::Error::Message(format!( "Failed to find property '{}'", name )))? @@ -51,10 +51,10 @@ where pub fn get_optional_property<'a, T>( properties: &'a HashMap, name: &str, -) -> Result, zbus::zvariant::Error> +) -> Result, zvariant::Error> where T: TryFrom>, - >>::Error: Into, + >>::Error: Into, { if let Some(value) = properties.get(name) { let value: Value = value.try_into()?; @@ -78,7 +78,7 @@ macro_rules! property_from_dbus { /// NOTE: we could follow a different approach like building our own type (e.g. /// using the newtype idiom) and offering a better API. /// -/// * `source`: hash map containing non-onwed values ([enum@zbus::zvariant::Value]). +/// * `source`: hash map containing non-onwed values ([enum@zvariant::Value]). pub fn to_owned_hash( source: &HashMap>, ) -> Result, zvariant::Error> { @@ -105,7 +105,7 @@ pub fn extract_id_from_path(path: &OwnedObjectPath) -> Result