diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ea440e35db..37af36ad03 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -27,6 +27,7 @@ dependencies = [ name = "agama-bootloader" version = "0.1.0" dependencies = [ + "agama-storage-client", "agama-utils", "anyhow", "async-trait", @@ -108,6 +109,7 @@ name = "agama-iscsi" version = "0.1.0" dependencies = [ "agama-storage", + "agama-storage-client", "agama-utils", "async-trait", "serde", @@ -280,6 +282,7 @@ name = "agama-s390" version = "0.1.0" dependencies = [ "agama-storage", + "agama-storage-client", "agama-utils", "async-trait", "serde", @@ -403,6 +406,7 @@ dependencies = [ name = "agama-storage" version = "0.1.0" dependencies = [ + "agama-storage-client", "agama-utils", "async-trait", "serde", @@ -412,6 +416,22 @@ dependencies = [ "tokio", "tokio-stream", "tokio-test", + "tracing", + "zbus", +] + +[[package]] +name = "agama-storage-client" +version = "0.1.0" +dependencies = [ + "agama-utils", + "async-trait", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", "zbus", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 11ce98cef5..956ea3c27a 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,6 +16,7 @@ members = [ "agama-server", "agama-software", "agama-storage", + "agama-storage-client", "agama-transfer", "agama-utils", "agama-users", diff --git a/rust/agama-bootloader/Cargo.toml b/rust/agama-bootloader/Cargo.toml index e11def782e..9ec00a2521 100644 --- a/rust/agama-bootloader/Cargo.toml +++ b/rust/agama-bootloader/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] agama-utils = { path = "../agama-utils" } +agama-storage-client = { path = "../agama-storage-client" } anyhow = "1.0.99" async-trait = "0.1.89" gettext-rs = { version = "0.7.2", features = ["gettext-system"] } diff --git a/rust/agama-bootloader/src/client.rs b/rust/agama-bootloader/src/client.rs index a238b2b494..0025d21930 100644 --- a/rust/agama-bootloader/src/client.rs +++ b/rust/agama-bootloader/src/client.rs @@ -22,12 +22,10 @@ use std::collections::HashMap; -use agama_utils::api::bootloader::Config; +use agama_storage_client::message; +use agama_utils::{actor::Handler, api::bootloader::Config}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use zbus::Connection; - -use crate::dbus::BootloaderProxy; /// Errors that can occur when using the Bootloader client. #[derive(thiserror::Error, Debug)] @@ -38,6 +36,8 @@ pub enum Error { /// Error parsing or generating JSON data. #[error("Passed json data is not correct: {0}")] InvalidJson(#[from] serde_json::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } /// Trait defining the interface for the Bootloader client. @@ -71,28 +71,27 @@ pub type ClientResult = Result; /// Client to connect to Agama's D-Bus API for Bootloader management. #[derive(Clone)] -pub struct Client<'a> { - bootloader_proxy: BootloaderProxy<'a>, +pub struct Client { + storage_dbus: Handler, kernel_args: HashMap, } -impl<'a> Client<'a> { - pub async fn new(connection: Connection) -> ClientResult> { - let bootloader_proxy = BootloaderProxy::new(&connection).await?; - +impl Client { + pub async fn new(storage_dbus: Handler) -> ClientResult { Ok(Self { - bootloader_proxy, + storage_dbus, kernel_args: HashMap::new(), }) } } #[async_trait] -impl<'a> BootloaderClient for Client<'a> { +impl BootloaderClient for Client { async fn get_config(&self) -> ClientResult { - let serialized_string = self.bootloader_proxy.get_config().await?; - let settings = serde_json::from_str(serialized_string.as_str())?; - Ok(settings) + Ok(self + .storage_dbus + .call(message::bootloader::GetConfig) + .await?) } async fn set_config(&self, config: &Config) -> ClientResult<()> { @@ -103,8 +102,9 @@ impl<'a> BootloaderClient for Client<'a> { tracing::info!("sending bootloader config {:?}", full_config); // ignore return value as currently it does not fail and who knows what future brings // but it should not be part of result and instead transformed to Issue - self.bootloader_proxy - .set_config(serde_json::to_string(&full_config)?.as_str()) + let value = serde_json::to_value(&full_config)?; + self.storage_dbus + .call(message::bootloader::SetConfig::new(value)) .await?; Ok(()) } diff --git a/rust/agama-bootloader/src/lib.rs b/rust/agama-bootloader/src/lib.rs index 151674e0de..64b793a854 100644 --- a/rust/agama-bootloader/src/lib.rs +++ b/rust/agama-bootloader/src/lib.rs @@ -39,6 +39,5 @@ pub mod service; pub use service::{Service, Starter}; pub mod client; -mod dbus; pub mod message; pub mod test_utils; diff --git a/rust/agama-bootloader/src/service.rs b/rust/agama-bootloader/src/service.rs index 35a0c84546..b6c3f721a7 100644 --- a/rust/agama-bootloader/src/service.rs +++ b/rust/agama-bootloader/src/service.rs @@ -35,6 +35,8 @@ pub enum Error { Actor(#[from] actor::Error), #[error(transparent)] Client(#[from] client::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } /// Builds and spawns the bootloader service. @@ -67,7 +69,13 @@ impl Starter { pub async fn start(self) -> Result, Error> { let client = match self.client { Some(client) => client, - None => Box::new(Client::new(self.connection.clone()).await?), + None => { + let storage_dbus = + agama_storage_client::service::Starter::new(self.connection.clone()) + .start() + .await?; + Box::new(Client::new(storage_dbus).await?) + } }; let service = Service { client }; let handler = actor::spawn(service); diff --git a/rust/agama-iscsi/Cargo.toml b/rust/agama-iscsi/Cargo.toml index 25920d183e..652e6a0f39 100644 --- a/rust/agama-iscsi/Cargo.toml +++ b/rust/agama-iscsi/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true [dependencies] agama-utils = { path = "../agama-utils" } agama-storage = { path = "../agama-storage" } +agama-storage-client = { path = "../agama-storage-client" } thiserror = "2.0.16" async-trait = "0.1.89" zbus = "5.7.1" diff --git a/rust/agama-iscsi/src/client.rs b/rust/agama-iscsi/src/client.rs index 63c93f3ba5..8f4c513b0e 100644 --- a/rust/agama-iscsi/src/client.rs +++ b/rust/agama-iscsi/src/client.rs @@ -18,14 +18,14 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -//! Implements a client to access Agama's D-Bus API related to Bootloader management. +//! Implements a client to access Agama's D-Bus API related to iSCSI management. -use crate::dbus::ISCSIProxy; +use agama_storage_client::message; +use agama_utils::actor::Handler; use agama_utils::api::iscsi::Config; use agama_utils::api::iscsi::DiscoverConfig; use async_trait::async_trait; use serde_json::Value; -use zbus::Connection; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -33,6 +33,8 @@ pub enum Error { DBus(#[from] zbus::Error), #[error(transparent)] Json(#[from] serde_json::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } pub enum DiscoverResult { @@ -49,23 +51,24 @@ pub trait ISCSIClient { } #[derive(Clone)] -pub struct Client<'a> { - proxy: ISCSIProxy<'a>, +pub struct Client { + storage_dbus: Handler, } -impl<'a> Client<'a> { - pub async fn new(connection: Connection) -> Result, Error> { - let proxy = ISCSIProxy::new(&connection).await?; - Ok(Self { proxy }) +impl Client { + pub async fn new( + storage_dbus: Handler, + ) -> Result { + Ok(Self { storage_dbus }) } } #[async_trait] -impl<'a> ISCSIClient for Client<'a> { +impl ISCSIClient for Client { async fn discover(&self, config: DiscoverConfig) -> Result { let result = self - .proxy - .discover(serde_json::to_string(&config)?.as_str()) + .storage_dbus + .call(message::iscsi::Discover::new(config)) .await?; match result { 0 => Ok(DiscoverResult::Success), @@ -74,26 +77,16 @@ impl<'a> ISCSIClient for Client<'a> { } async fn get_system(&self) -> Result, Error> { - let serialized_system = self.proxy.get_system().await?; - let system: Value = serde_json::from_str(serialized_system.as_str())?; - match system { - Value::Null => Ok(None), - _ => Ok(Some(system)), - } + Ok(self.storage_dbus.call(message::iscsi::GetSystem).await?) } async fn get_config(&self) -> Result, Error> { - let serialized_config = self.proxy.get_config().await?; - let value: Value = serde_json::from_str(serialized_config.as_str())?; - match value { - Value::Null => Ok(None), - _ => Ok(Some(Config(value))), - } + Ok(self.storage_dbus.call(message::iscsi::GetConfig).await?) } async fn set_config(&self, config: Option) -> Result<(), Error> { - self.proxy - .set_config(serde_json::to_string(&config)?.as_str()) + self.storage_dbus + .call(message::iscsi::SetConfig::new(config)) .await?; Ok(()) } diff --git a/rust/agama-iscsi/src/lib.rs b/rust/agama-iscsi/src/lib.rs index 219af4c87c..367d7b9a2c 100644 --- a/rust/agama-iscsi/src/lib.rs +++ b/rust/agama-iscsi/src/lib.rs @@ -25,7 +25,6 @@ pub mod client; pub mod message; pub mod test_utils; -mod dbus; mod monitor; use agama_storage as storage; diff --git a/rust/agama-iscsi/src/monitor.rs b/rust/agama-iscsi/src/monitor.rs index 1f07985251..ab8d4ba339 100644 --- a/rust/agama-iscsi/src/monitor.rs +++ b/rust/agama-iscsi/src/monitor.rs @@ -18,10 +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::{ - dbus::{ISCSIProxy, ProgressChanged, ProgressFinished, SystemChanged}, - storage, -}; +use crate::storage; +use agama_storage_client::proxies::{ISCSIProxy, ProgressChanged, ProgressFinished, SystemChanged}; use agama_utils::{ actor::Handler, api::{ @@ -130,7 +128,7 @@ impl Monitor { async fn handle_progress_changed(&self, signal: ProgressChanged) -> Result<(), Error> { let args = signal.args()?; - let progress_data = serde_json::from_str::(args.progress)?; + let progress_data = serde_json::from_str::(args.serialized_progress)?; self.progress .call(progress::message::SetProgress::new(progress_data.into())) .await?; diff --git a/rust/agama-iscsi/src/service.rs b/rust/agama-iscsi/src/service.rs index 8ec851f94c..723aab9d57 100644 --- a/rust/agama-iscsi/src/service.rs +++ b/rust/agama-iscsi/src/service.rs @@ -40,6 +40,8 @@ pub enum Error { Client(#[from] client::Error), #[error(transparent)] Monitor(#[from] monitor::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } pub struct Starter { @@ -74,7 +76,13 @@ impl Starter { pub async fn start(self) -> Result, Error> { let client = match self.client { Some(client) => client, - None => Box::new(Client::new(self.connection.clone()).await?), + None => { + let storage_dbus = + agama_storage_client::service::Starter::new(self.connection.clone()) + .start() + .await?; + Box::new(Client::new(storage_dbus).await?) + } }; let service = Service { client }; let handler = actor::spawn(service); diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index b6c1a9f2c3..0e042bc976 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -722,6 +722,7 @@ impl MessageHandler for Service { /// Sets the user configuration with the given values. async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> { checks::check_stage(&self.progress, Stage::Configuring).await?; + tracing::debug!("DEBUG: SetConfig handler (calling set_config)"); self.set_config(message.config).await } } diff --git a/rust/agama-manager/src/tasks/runner.rs b/rust/agama-manager/src/tasks/runner.rs index 7bb9835a2f..1b3b08c08e 100644 --- a/rust/agama-manager/src/tasks/runner.rs +++ b/rust/agama-manager/src/tasks/runner.rs @@ -410,12 +410,14 @@ impl SetConfigAction { self.progress .call(progress::message::Next::new(Scope::Manager)) .await?; - self.storage + let future = self + .storage .call(storage::message::SetConfig::new( Arc::clone(product), config.storage.clone(), )) .await?; + let _ = future.await; // call bootloader always after storage to ensure that bootloader reflect new storage settings self.progress diff --git a/rust/agama-s390/Cargo.toml b/rust/agama-s390/Cargo.toml index 09c963d541..e8f842d344 100644 --- a/rust/agama-s390/Cargo.toml +++ b/rust/agama-s390/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true [dependencies] agama-utils = { path = "../agama-utils" } agama-storage = { path = "../agama-storage" } +agama-storage-client = { path = "../agama-storage-client" } thiserror = "2.0.16" async-trait = "0.1.89" zbus = "5.7.1" diff --git a/rust/agama-s390/src/dasd.rs b/rust/agama-s390/src/dasd.rs index aead70c64e..112c34f4eb 100644 --- a/rust/agama-s390/src/dasd.rs +++ b/rust/agama-s390/src/dasd.rs @@ -23,5 +23,3 @@ pub use client::Client; pub mod monitor; pub use monitor::Monitor; - -mod dbus; diff --git a/rust/agama-s390/src/dasd/client.rs b/rust/agama-s390/src/dasd/client.rs index 3bf75f1adf..58ccc859ec 100644 --- a/rust/agama-s390/src/dasd/client.rs +++ b/rust/agama-s390/src/dasd/client.rs @@ -18,13 +18,13 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -//! Implements a client to access Agama's D-Bus API related to Bootloader management. +//! Implements a client to access Agama's D-Bus API related to DASD management. -use crate::dasd::dbus::DASDProxy; +use agama_storage_client::message; +use agama_utils::actor::Handler; use agama_utils::api::RawConfig; use async_trait::async_trait; use serde_json::Value; -use zbus::Connection; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -32,6 +32,8 @@ pub enum Error { DBus(#[from] zbus::Error), #[error(transparent)] Json(#[from] serde_json::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } #[async_trait] @@ -43,45 +45,35 @@ pub trait DASDClient { } #[derive(Clone)] -pub struct Client<'a> { - proxy: DASDProxy<'a>, +pub struct Client { + storage_dbus: Handler, } -impl<'a> Client<'a> { - pub async fn new(connection: Connection) -> Result, Error> { - let proxy = DASDProxy::new(&connection).await?; - Ok(Self { proxy }) +impl Client { + pub async fn new( + storage_dbus: Handler, + ) -> Result { + Ok(Self { storage_dbus }) } } #[async_trait] -impl<'a> DASDClient for Client<'a> { +impl DASDClient for Client { async fn probe(&self) -> Result<(), Error> { - self.proxy.probe().await?; - Ok(()) + Ok(self.storage_dbus.call(message::dasd::Probe).await?) } async fn get_system(&self) -> Result, Error> { - let serialized_system = self.proxy.get_system().await?; - let system: Value = serde_json::from_str(serialized_system.as_str())?; - match system { - Value::Null => Ok(None), - _ => Ok(Some(system)), - } + Ok(self.storage_dbus.call(message::dasd::GetSystem).await?) } async fn get_config(&self) -> Result, Error> { - let serialized_config = self.proxy.get_config().await?; - let value: Value = serde_json::from_str(serialized_config.as_str())?; - match value { - Value::Null => Ok(None), - _ => Ok(Some(RawConfig(value))), - } + Ok(self.storage_dbus.call(message::dasd::GetConfig).await?) } async fn set_config(&self, config: Option) -> Result<(), Error> { - self.proxy - .set_config(serde_json::to_string(&config)?.as_str()) + self.storage_dbus + .call(message::dasd::SetConfig::new(config)) .await?; Ok(()) } diff --git a/rust/agama-s390/src/dasd/dbus.rs b/rust/agama-s390/src/dasd/dbus.rs deleted file mode 100644 index 9eebf3a5c2..0000000000 --- a/rust/agama-s390/src/dasd/dbus.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! # D-Bus interface proxy for: `org.opensuse.Agama.Storage1.DASD` -//! -//! This code was generated by `zbus-xmlgen` `5.2.0` from D-Bus introspection data. -//! Source: `iscsi.xml`. -//! -//! You may prefer to adapt it, instead of using it verbatim. -//! -//! More information can be found in the [Writing a client proxy] section of the zbus -//! documentation. -//! -//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the -//! following zbus API can be used: -//! -//! * [`zbus::fdo::PropertiesProxy`] -//! * [`zbus::fdo::IntrospectableProxy`] -//! -//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. -//! -//! [Writing a client proxy]: https://z-galaxy.github.io/zbus/client.html -//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, -use zbus::proxy; -#[proxy( - default_service = "org.opensuse.Agama.Storage1", - default_path = "/org/opensuse/Agama/Storage1/DASD", - interface = "org.opensuse.Agama.Storage1.DASD", - assume_defaults = true -)] -pub trait DASD { - /// Probe method - fn probe(&self) -> zbus::Result<()>; - - /// GetConfig method - fn get_config(&self) -> zbus::Result; - - /// GetSystem method - fn get_system(&self) -> zbus::Result; - - /// SetConfig method - fn set_config(&self, serialized_config: &str) -> zbus::Result<()>; - - /// FormatChanged signal - #[zbus(signal)] - fn format_changed(&self, summary: &str) -> zbus::Result<()>; - - /// FormatFinished signal - #[zbus(signal)] - fn format_finished(&self, status: &str) -> zbus::Result<()>; - - /// SystemChanged signal - #[zbus(signal)] - fn system_changed(&self, system: &str) -> zbus::Result<()>; - - /// ProgressChanged signal - #[zbus(signal)] - fn progress_changed(&self, progress: &str) -> zbus::Result<()>; - - /// ProgressFinished signal - #[zbus(signal)] - fn progress_finished(&self) -> zbus::Result<()>; -} diff --git a/rust/agama-s390/src/dasd/monitor.rs b/rust/agama-s390/src/dasd/monitor.rs index 6c34e844bb..6c713f0a1e 100644 --- a/rust/agama-s390/src/dasd/monitor.rs +++ b/rust/agama-s390/src/dasd/monitor.rs @@ -18,11 +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::{ - dasd::dbus::{ - DASDProxy, FormatChanged, FormatFinished, ProgressChanged, ProgressFinished, SystemChanged, - }, - storage, +use crate::storage; +use agama_storage_client::proxies::{ + DASDProgressChanged as ProgressChanged, DASDProgressFinished as ProgressFinished, DASDProxy, + DASDSystemChanged as SystemChanged, FormatChanged, FormatFinished, }; use agama_utils::{ actor::Handler, diff --git a/rust/agama-s390/src/service.rs b/rust/agama-s390/src/service.rs index 161ebb7fd8..1c332778cb 100644 --- a/rust/agama-s390/src/service.rs +++ b/rust/agama-s390/src/service.rs @@ -40,6 +40,8 @@ pub enum Error { DASDClient(#[from] dasd::client::Error), #[error(transparent)] DASDMonitor(#[from] dasd::monitor::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } pub struct Starter { @@ -74,7 +76,13 @@ impl Starter { pub async fn start(self) -> Result, Error> { let dasd_client = match self.dasd { Some(client) => client, - None => Box::new(dasd::Client::new(self.connection.clone()).await?), + None => { + let storage_dbus = + agama_storage_client::service::Starter::new(self.connection.clone()) + .start() + .await?; + Box::new(dasd::Client::new(storage_dbus).await?) + } }; let service = Service { dasd: dasd_client }; let handler = actor::spawn(service); diff --git a/rust/agama-storage-client/Cargo.toml b/rust/agama-storage-client/Cargo.toml new file mode 100644 index 0000000000..ee74da750d --- /dev/null +++ b/rust/agama-storage-client/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "agama-storage-client" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true + +[dependencies] +agama-utils = { path = "../agama-utils" } +thiserror = "2.0.16" +async-trait = "0.1.89" +zbus = "5.7.1" +tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "sync"] } +tokio-stream = "0.1.16" +serde = { version = "1.0.228" } +serde_json = "1.0.140" +tracing = "0.1.44" + diff --git a/rust/agama-storage-client/src/lib.rs b/rust/agama-storage-client/src/lib.rs new file mode 100644 index 0000000000..43425cd508 --- /dev/null +++ b/rust/agama-storage-client/src/lib.rs @@ -0,0 +1,31 @@ +// Copyright (c) [2026] 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. + +//! Service to interact with the storage D-Bus interface. +//! +//! This service tries to simplify the interaction with the storage D-Bus service. It implements an +//! actor that offers a set of operations to read and set the configuration. + +pub mod service; +pub use service::{Error, Service}; + +pub mod message; + +pub mod proxies; diff --git a/rust/agama-storage-client/src/message.rs b/rust/agama-storage-client/src/message.rs new file mode 100644 index 0000000000..385e1ecfd1 --- /dev/null +++ b/rust/agama-storage-client/src/message.rs @@ -0,0 +1,138 @@ +// Copyright (c) [2026] 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. + +use std::sync::Arc; + +use agama_utils::{ + actor::Message, + api::{storage::Config, Issue}, + products::ProductSpec, + BoxFuture, +}; +use tokio::sync::RwLock; + +use crate::service; + +pub mod bootloader; +pub mod dasd; +pub mod iscsi; + +pub struct CallAction { + pub action: String, +} + +impl CallAction { + pub fn new(action: &str) -> Self { + Self { + action: action.to_string(), + } + } +} + +impl Message for CallAction { + type Reply = (); +} + +pub struct SetStorageConfig { + pub product: Arc>, + pub config: Option, +} + +impl SetStorageConfig { + pub fn new(product: Arc>, config: Option) -> Self { + Self { product, config } + } +} + +impl Message for SetStorageConfig { + type Reply = BoxFuture>; +} + +pub struct GetStorageConfig; + +impl Message for GetStorageConfig { + type Reply = Option; +} + +pub struct GetSystem; + +impl Message for GetSystem { + type Reply = Option; +} + +pub struct GetProposal; + +impl Message for GetProposal { + type Reply = Option; +} + +pub struct GetIssues; + +impl Message for GetIssues { + type Reply = Vec; +} + +pub struct GetConfigFromModel { + pub model: serde_json::Value, +} + +impl GetConfigFromModel { + pub fn new(model: serde_json::Value) -> Self { + Self { model } + } +} + +impl Message for GetConfigFromModel { + type Reply = Option; +} + +pub struct GetConfigModel; + +impl Message for GetConfigModel { + type Reply = Option; +} + +pub struct SolveConfigModel { + pub model: serde_json::Value, +} + +impl SolveConfigModel { + pub fn new(model: serde_json::Value) -> Self { + Self { model } + } +} + +impl Message for SolveConfigModel { + type Reply = Option; +} + +pub struct SetLocale { + pub locale: String, +} + +impl SetLocale { + pub fn new(locale: String) -> Self { + Self { locale } + } +} + +impl Message for SetLocale { + type Reply = (); +} diff --git a/rust/agama-storage-client/src/message/bootloader.rs b/rust/agama-storage-client/src/message/bootloader.rs new file mode 100644 index 0000000000..0e2bbf4f0a --- /dev/null +++ b/rust/agama-storage-client/src/message/bootloader.rs @@ -0,0 +1,44 @@ +// Copyright (c) [2026] 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. + +use agama_utils::{actor::Message, api::bootloader}; + +pub struct GetConfig; + +impl Message for GetConfig { + type Reply = bootloader::Config; +} + +pub struct SetConfig { + pub config: serde_json::Value, +} + +impl SetConfig { + // FIXME: To be consistent, this action should receive + // the bootloader configuration. However, it uses an internal + // FullConfig struct defined in the agama-bootloader::client module. + pub fn new(config: serde_json::Value) -> Self { + Self { config } + } +} + +impl Message for SetConfig { + type Reply = (); +} diff --git a/rust/agama-storage-client/src/message/dasd.rs b/rust/agama-storage-client/src/message/dasd.rs new file mode 100644 index 0000000000..39194c822c --- /dev/null +++ b/rust/agama-storage-client/src/message/dasd.rs @@ -0,0 +1,53 @@ +// Copyright (c) [2026] 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. + +use agama_utils::actor::Message; + +pub struct Probe; + +impl Message for Probe { + type Reply = (); +} + +pub struct GetSystem; + +impl Message for GetSystem { + type Reply = Option; +} + +pub struct GetConfig; + +impl Message for GetConfig { + type Reply = Option; +} + +pub struct SetConfig { + pub config: Option, +} + +impl SetConfig { + pub fn new(config: Option) -> Self { + Self { config } + } +} + +impl Message for SetConfig { + type Reply = (); +} diff --git a/rust/agama-storage-client/src/message/iscsi.rs b/rust/agama-storage-client/src/message/iscsi.rs new file mode 100644 index 0000000000..bae5f5d7dc --- /dev/null +++ b/rust/agama-storage-client/src/message/iscsi.rs @@ -0,0 +1,61 @@ +// Copyright (c) [2026] 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. + +use agama_utils::{actor::Message, api::iscsi}; + +pub struct Discover { + pub config: iscsi::DiscoverConfig, +} + +impl Discover { + pub fn new(config: iscsi::DiscoverConfig) -> Self { + Self { config } + } +} + +impl Message for Discover { + type Reply = u32; +} + +pub struct GetSystem; + +impl Message for GetSystem { + type Reply = Option; +} + +pub struct GetConfig; + +impl Message for GetConfig { + type Reply = Option; +} + +pub struct SetConfig { + pub config: Option, +} + +impl SetConfig { + pub fn new(config: Option) -> Self { + Self { config } + } +} + +impl Message for SetConfig { + type Reply = (); +} diff --git a/rust/agama-storage-client/src/proxies.rs b/rust/agama-storage-client/src/proxies.rs new file mode 100644 index 0000000000..2680c389d5 --- /dev/null +++ b/rust/agama-storage-client/src/proxies.rs @@ -0,0 +1,34 @@ +// Copyright (c) [2026] 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. + +mod storage1; +pub use storage1::Storage1Proxy; + +mod bootloader; +pub use bootloader::BootloaderProxy; + +mod iscsi; +pub use iscsi::{ISCSIProxy, ProgressChanged, ProgressFinished, SystemChanged}; + +mod dasd; +pub use dasd::{ + DASDProxy, FormatChanged, FormatFinished, ProgressChanged as DASDProgressChanged, + ProgressFinished as DASDProgressFinished, SystemChanged as DASDSystemChanged, +}; diff --git a/rust/agama-bootloader/src/dbus.rs b/rust/agama-storage-client/src/proxies/bootloader.rs similarity index 80% rename from rust/agama-bootloader/src/dbus.rs rename to rust/agama-storage-client/src/proxies/bootloader.rs index 0998e49a70..aea9f058f2 100644 --- a/rust/agama-bootloader/src/dbus.rs +++ b/rust/agama-storage-client/src/proxies/bootloader.rs @@ -1,7 +1,7 @@ -//! # D-Bus interface proxy for: `org.freedesktop.locale1` +//! # D-Bus interface proxy for: `org.opensuse.Agama.Storage1.Bootloader` //! //! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data. -//! Source: `Interface '/org/freedesktop/locale1' from service 'org.freedesktop.locale1' on system bus`. +//! Source: `storage.xml`. //! //! You may prefer to adapt it, instead of using it verbatim. //! @@ -11,9 +11,8 @@ //! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the //! following zbus API can be used: //! -//! * [`zbus::fdo::PeerProxy`] -//! * [`zbus::fdo::IntrospectableProxy`] //! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] //! //! Consequently `zbus-xmlgen` did not generate code for the above interfaces. //! @@ -27,9 +26,10 @@ use zbus::proxy; assume_defaults = true )] pub trait Bootloader { - /// GetConfig method - fn get_config(&self) -> zbus::Result; - /// SetConfig method fn set_config(&self, serialized_config: &str) -> zbus::Result; + + /// Config property + #[zbus(property)] + fn config(&self) -> zbus::Result; } diff --git a/rust/agama-storage-client/src/proxies/dasd.rs b/rust/agama-storage-client/src/proxies/dasd.rs new file mode 100644 index 0000000000..aae0b2101a --- /dev/null +++ b/rust/agama-storage-client/src/proxies/dasd.rs @@ -0,0 +1,66 @@ +// Copyright (c) [2026] 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. + +//! # D-Bus interface proxy for: `org.opensuse.Agama.Storage1.DASD` + +use zbus::proxy; + +#[proxy( + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1/DASD", + interface = "org.opensuse.Agama.Storage1.DASD", + assume_defaults = true +)] +pub trait DASD { + /// Probe method + fn probe(&self) -> zbus::Result<()>; + + /// Config property + #[zbus(property)] + fn config(&self) -> zbus::Result; + + /// System property + /// Temporary rename to avoid clashing with the system_changed signal. + #[zbus(property, name = "System")] + fn dasd_system(&self) -> zbus::Result; + + /// SetConfig method + fn set_config(&self, serialized_config: &str) -> zbus::Result<()>; + + /// FormatChanged signal + #[zbus(signal)] + fn format_changed(&self, summary: &str) -> zbus::Result<()>; + + /// FormatFinished signal + #[zbus(signal)] + fn format_finished(&self, status: &str) -> zbus::Result<()>; + + /// SystemChanged signal + #[zbus(signal)] + fn system_changed(&self, system: &str) -> zbus::Result<()>; + + /// ProgressChanged signal + #[zbus(signal)] + fn progress_changed(&self, progress: &str) -> zbus::Result<()>; + + /// ProgressFinished signal + #[zbus(signal)] + fn progress_finished(&self, status: &str) -> zbus::Result<()>; +} diff --git a/rust/agama-iscsi/src/dbus.rs b/rust/agama-storage-client/src/proxies/iscsi.rs similarity index 70% rename from rust/agama-iscsi/src/dbus.rs rename to rust/agama-storage-client/src/proxies/iscsi.rs index 019baf071c..893c16b735 100644 --- a/rust/agama-iscsi/src/dbus.rs +++ b/rust/agama-storage-client/src/proxies/iscsi.rs @@ -1,6 +1,6 @@ //! # D-Bus interface proxy for: `org.opensuse.Agama.Storage1.ISCSI` //! -//! This code was generated by `zbus-xmlgen` `5.2.0` from D-Bus introspection data. +//! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data. //! Source: `iscsi.xml`. //! //! You may prefer to adapt it, instead of using it verbatim. @@ -16,7 +16,7 @@ //! //! Consequently `zbus-xmlgen` did not generate code for the above interfaces. //! -//! [Writing a client proxy]: https://z-galaxy.github.io/zbus/client.html +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html //! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, use zbus::proxy; #[proxy( @@ -29,18 +29,12 @@ pub trait ISCSI { /// Discover method fn discover(&self, serialized_options: &str) -> zbus::Result; - /// GetConfig method - fn get_config(&self) -> zbus::Result; - - /// GetSystem method - fn get_system(&self) -> zbus::Result; - /// SetConfig method fn set_config(&self, serialized_config: &str) -> zbus::Result<()>; /// ProgressChanged signal #[zbus(signal)] - fn progress_changed(&self, progress: &str) -> zbus::Result<()>; + fn progress_changed(&self, serialized_progress: &str) -> zbus::Result<()>; /// ProgressFinished signal #[zbus(signal)] @@ -48,5 +42,14 @@ pub trait ISCSI { /// SystemChanged signal #[zbus(signal)] - fn system_changed(&self, system: &str) -> zbus::Result<()>; + fn system_changed(&self, serialized_system: &str) -> zbus::Result<()>; + + /// Config property + #[zbus(property)] + fn config(&self) -> zbus::Result; + + /// System property + /// Temporary rename to avoid clashing with the system_changed signal. + #[zbus(property, name = "System")] + fn iscsi_system(&self) -> zbus::Result; } diff --git a/rust/agama-storage-client/src/proxies/storage1.rs b/rust/agama-storage-client/src/proxies/storage1.rs new file mode 100644 index 0000000000..57ce0902cf --- /dev/null +++ b/rust/agama-storage-client/src/proxies/storage1.rs @@ -0,0 +1,83 @@ +//! # D-Bus interface proxy for: `org.opensuse.Agama.Storage1` +//! +//! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data. +//! Source: `storage.xml`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy( + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1", + interface = "org.opensuse.Agama.Storage1", + assume_defaults = true +)] +pub trait Storage1 { + /// Activate method + fn activate(&self) -> zbus::Result<()>; + + /// Finish method + fn finish(&self) -> zbus::Result<()>; + + /// GetConfigFromModel method + fn get_config_from_model(&self, model: &str) -> zbus::Result; + + /// Install method + fn install(&self) -> zbus::Result<()>; + + /// Probe method + fn probe(&self) -> zbus::Result<()>; + + /// SetConfig method + fn set_config(&self, product: &str, config: &str) -> zbus::Result<()>; + + /// SetLocale method + fn set_locale(&self, locale: &str) -> zbus::Result<()>; + + /// SolveConfigModel method + fn solve_config_model(&self, model: &str) -> zbus::Result; + + /// Umount method + fn umount(&self) -> zbus::Result<()>; + + /// ProgressChanged signal + #[zbus(signal)] + fn progress_changed(&self, progress: &str) -> zbus::Result<()>; + + /// ProgressFinished signal + #[zbus(signal)] + fn progress_finished(&self) -> zbus::Result<()>; + + /// Config property + #[zbus(property)] + fn config(&self) -> zbus::Result; + + /// ConfigModel property + #[zbus(property)] + fn config_model(&self) -> zbus::Result; + + /// Issues property + #[zbus(property)] + fn issues(&self) -> zbus::Result; + + /// Proposal property + #[zbus(property)] + fn proposal(&self) -> zbus::Result; + + /// System property + #[zbus(property)] + fn system(&self) -> zbus::Result; +} diff --git a/rust/agama-storage-client/src/service.rs b/rust/agama-storage-client/src/service.rs new file mode 100644 index 0000000000..6f7dfe6655 --- /dev/null +++ b/rust/agama-storage-client/src/service.rs @@ -0,0 +1,362 @@ +// Copyright (c) [2026] 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. + +use std::future::Future; + +use agama_utils::{ + actor::{self, Actor, Handler, MessageHandler}, + api::{bootloader, iscsi, storage::Config, Issue}, + arch::Arch, + BoxFuture, +}; +use async_trait::async_trait; +use tokio::sync::oneshot; + +use crate::{ + message, + proxies::{self, BootloaderProxy, DASDProxy, ISCSIProxy, Storage1Proxy}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Actor(#[from] actor::Error), + #[error(transparent)] + DBus(#[from] zbus::Error), + #[error(transparent)] + JSON(#[from] serde_json::Error), + #[error("Method not found: {0}")] + MethodNotFound(String), +} + +/// Builds and starts the service. +pub struct Starter { + dbus: zbus::Connection, +} + +impl Starter { + pub fn new(dbus: zbus::Connection) -> Self { + Self { dbus } + } + + /// Starts the service. + /// + /// As part of the initialization process, it reads the information + /// from each proxy to cache the current values. + pub async fn start(self) -> Result, Error> { + let storage_proxy = proxies::Storage1Proxy::new(&self.dbus).await?; + storage_proxy.config().await?; + + let bootloader_proxy = proxies::BootloaderProxy::new(&self.dbus).await?; + bootloader_proxy.config().await?; + + let iscsi_proxy = proxies::ISCSIProxy::new(&self.dbus).await?; + iscsi_proxy.config().await?; + + let dasd_proxy = if Arch::is_s390() { + let proxy = proxies::DASDProxy::new(&self.dbus).await?; + proxy.config().await?; + Some(proxy) + } else { + None + }; + + let service = Service { + storage_proxy, + bootloader_proxy, + iscsi_proxy, + dasd_proxy, + }; + let handler = actor::spawn(service); + Ok(handler) + } +} + +pub struct Service { + storage_proxy: Storage1Proxy<'static>, + bootloader_proxy: BootloaderProxy<'static>, + iscsi_proxy: ISCSIProxy<'static>, + dasd_proxy: Option>, +} + +impl Actor for Service { + type Error = Error; +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::CallAction) -> Result<(), Error> { + match message.action.as_str() { + "Activate" => self.storage_proxy.activate().await?, + "Probe" => self.storage_proxy.probe().await?, + "Install" => self.storage_proxy.install().await?, + "Finish" => self.storage_proxy.finish().await?, + "Umount" => self.storage_proxy.umount().await?, + _ => { + return Err(Error::MethodNotFound(message.action)); + } + } + Ok(()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::GetStorageConfig, + ) -> Result, Error> { + let raw_json = self.storage_proxy.config().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::GetSystem, + ) -> Result, Error> { + let raw_json = self.storage_proxy.system().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::GetProposal, + ) -> Result, Error> { + let raw_json = self.storage_proxy.proposal().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::GetIssues) -> Result, Error> { + let raw_json = self.storage_proxy.issues().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + message: message::GetConfigFromModel, + ) -> Result, Error> { + let raw_json = self + .storage_proxy + .get_config_from_model(&message.model.to_string()) + .await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::GetConfigModel, + ) -> Result, Error> { + let raw_json = self.storage_proxy.config_model().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + message: message::SolveConfigModel, + ) -> Result, Error> { + let raw_json = self + .storage_proxy + .solve_config_model(&message.model.to_string()) + .await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::SetLocale) -> Result<(), Error> { + Ok(self.storage_proxy.set_locale(&message.locale).await?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + message: message::SetStorageConfig, + ) -> Result>, Error> { + let proxy = self.storage_proxy.clone(); + let result = run_in_background(async move { + let product = message.product.read().await; + let product_json = serde_json::to_string(&*product)?; + let config = message.config.filter(|c| c.has_value()); + let config_json = serde_json::to_string(&config)?; + proxy.set_config(&product_json, &config_json).await?; + Ok(()) + }); + Ok(Box::pin(async move { + result + .await + .map_err(|_| Error::Actor(actor::Error::Response(Self::name())))? + })) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::bootloader::GetConfig, + ) -> Result { + let raw_json = self.bootloader_proxy.config().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::bootloader::SetConfig) -> Result<(), Error> { + self.bootloader_proxy + .set_config(&message.config.to_string()) + .await?; + Ok(()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::iscsi::Discover) -> Result { + let options = serde_json::to_string(&message.config)?; + Ok(self.iscsi_proxy.discover(&options).await?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::iscsi::GetSystem, + ) -> Result, Error> { + let raw_json = self.iscsi_proxy.iscsi_system().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::iscsi::GetConfig, + ) -> Result, Error> { + let raw_json = self.iscsi_proxy.config().await?; + Ok(try_from_string(&raw_json)?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::iscsi::SetConfig) -> Result<(), Error> { + let config = serde_json::to_string(&message.config)?; + Ok(self.iscsi_proxy.set_config(&config).await?) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::dasd::Probe) -> Result<(), Error> { + if let Some(proxy) = &self.dasd_proxy { + proxy.probe().await?; + } + Ok(()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::dasd::GetSystem, + ) -> Result, Error> { + if let Some(proxy) = &self.dasd_proxy { + let raw_json = proxy.dasd_system().await?; + Ok(try_from_string(&raw_json)?) + } else { + Ok(None) + } + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle( + &mut self, + _message: message::dasd::GetConfig, + ) -> Result, Error> { + if let Some(proxy) = &self.dasd_proxy { + let raw_json = proxy.config().await?; + Ok(try_from_string(&raw_json)?) + } else { + Ok(None) + } + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::dasd::SetConfig) -> Result<(), Error> { + if let Some(proxy) = &self.dasd_proxy { + let config = serde_json::to_string(&message.config)?; + proxy.set_config(&config).await?; + } + Ok(()) + } +} + +fn run_in_background(func: F) -> oneshot::Receiver> +where + F: Future> + Send + 'static, +{ + let (tx, rx) = oneshot::channel::>(); + tokio::spawn(async move { + let result = func.await; + _ = tx.send(result); + }); + rx +} + +/// Converts a string into a Value. +/// +/// If the string is "null", return the default value. +fn try_from_string(raw_json: &str) -> Result { + let json: serde_json::Value = serde_json::from_str(raw_json)?; + if json.is_null() { + return Ok(T::default()); + } + let value = serde_json::from_value(json)?; + Ok(value) +} diff --git a/rust/agama-storage/Cargo.toml b/rust/agama-storage/Cargo.toml index 6d1ade1f75..ccf287534f 100644 --- a/rust/agama-storage/Cargo.toml +++ b/rust/agama-storage/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] agama-utils = { path = "../agama-utils" } +agama-storage-client = { path = "../agama-storage-client" } thiserror = "2.0.16" async-trait = "0.1.89" zbus = "5.7.1" @@ -13,6 +14,7 @@ tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "sync"] } tokio-stream = "0.1.16" serde = { version = "1.0.228" } serde_json = "1.0.140" +tracing = "0.1.44" [dev-dependencies] test-context = "0.4.1" diff --git a/rust/agama-storage/src/client.rs b/rust/agama-storage/src/client.rs index de67d37714..03c8e4301f 100644 --- a/rust/agama-storage/src/client.rs +++ b/rust/agama-storage/src/client.rs @@ -20,19 +20,17 @@ //! Implements a client to access Agama's storage service. +use agama_storage_client::message; use agama_utils::{ + actor::{self, Handler}, api::{storage::Config, Issue}, products::ProductSpec, + BoxFuture, }; use async_trait::async_trait; use serde_json::Value; use std::sync::Arc; use tokio::sync::RwLock; -use zbus::{names::BusName, zvariant::OwnedObjectPath, Connection, Message}; - -const SERVICE_NAME: &str = "org.opensuse.Agama.Storage1"; -const OBJECT_PATH: &str = "/org/opensuse/Agama/Storage1"; -const INTERFACE: &str = "org.opensuse.Agama.Storage1"; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -44,6 +42,10 @@ pub enum Error { DBusVariant(#[from] zbus::zvariant::Error), #[error(transparent)] Json(#[from] serde_json::Error), + #[error(transparent)] + Actor(#[from] actor::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } #[async_trait] @@ -63,7 +65,7 @@ pub trait StorageClient { &self, product: Arc>, config: Option, - ) -> Result<(), Error>; + ) -> Result>, Error>; async fn solve_config_model(&self, model: Value) -> Result, Error>; async fn set_locale(&self, locale: String) -> Result<(), Error>; } @@ -71,119 +73,95 @@ pub trait StorageClient { /// D-Bus client for the storage service #[derive(Clone)] pub struct Client { - connection: Connection, + storage_dbus: Handler, } impl Client { - pub fn new(connection: Connection) -> Self { - Self { connection } + pub fn new(storage_dbus: Handler) -> Self { + Self { storage_dbus } } - async fn call( - &self, - method: &str, - body: &T, - ) -> Result { - let bus = BusName::try_from(SERVICE_NAME.to_string())?; - let path = OwnedObjectPath::try_from(OBJECT_PATH)?; - self.connection - .call_method(Some(&bus), &path, Some(INTERFACE), method, body) - .await - .map_err(|e| e.into()) + async fn call_action(&self, action: &str) -> Result<(), Error> { + self.storage_dbus + .call(message::CallAction::new(action)) + .await?; + Ok(()) } } #[async_trait] impl StorageClient for Client { async fn activate(&self) -> Result<(), Error> { - self.call("Activate", &()).await?; - Ok(()) + self.call_action("Activate").await } async fn probe(&self) -> Result<(), Error> { - self.call("Probe", &()).await?; - Ok(()) + self.call_action("Probe").await } async fn install(&self) -> Result<(), Error> { - self.call("Install", &()).await?; - Ok(()) + self.call_action("Install").await } async fn finish(&self) -> Result<(), Error> { - self.call("Finish", &()).await?; - Ok(()) + self.call_action("Finish").await } async fn umount(&self) -> Result<(), Error> { - self.call("Umount", &()).await?; - Ok(()) + self.call_action("Umount").await } async fn get_system(&self) -> Result, Error> { - let message = self.call("GetSystem", &()).await?; - try_from_message(message) + let value = self.storage_dbus.call(message::GetSystem).await?; + Ok(value) } async fn get_config(&self) -> Result, Error> { - let message = self.call("GetConfig", &()).await?; - try_from_message(message) + let value = self.storage_dbus.call(message::GetStorageConfig).await?; + Ok(value) } async fn get_config_from_model(&self, model: Value) -> Result, Error> { - let message = self - .call("GetConfigFromModel", &(model.to_string())) - .await?; - try_from_message(message) + let message = message::GetConfigFromModel::new(model); + let value = self.storage_dbus.call(message).await?; + Ok(value) } async fn get_config_model(&self) -> Result, Error> { - let message = self.call("GetConfigModel", &()).await?; - try_from_message(message) + let value = self.storage_dbus.call(message::GetConfigModel).await?; + Ok(value) } async fn get_proposal(&self) -> Result, Error> { - let message = self.call("GetProposal", &()).await?; - try_from_message(message) + let value = self.storage_dbus.call(message::GetProposal).await?; + Ok(value) } async fn get_issues(&self) -> Result, Error> { - let message = self.call("GetIssues", &()).await?; - try_from_message(message) + let value = self.storage_dbus.call(message::GetIssues).await?; + Ok(value) } async fn set_config( &self, product: Arc>, config: Option, - ) -> Result<(), Error> { - let product_guard = product.read().await; - let product_json = serde_json::to_string(&*product_guard)?; - let config = config.filter(|c| c.has_value()); - let config_json = serde_json::to_string(&config)?; - self.call("SetConfig", &(product_json, config_json)).await?; - Ok(()) + ) -> Result>, Error> { + let message = message::SetStorageConfig::new(product.clone(), config); + let rx = self.storage_dbus.call(message).await?; + Ok(Box::pin(async move { rx.await.map_err(Error::from) })) } async fn solve_config_model(&self, model: Value) -> Result, Error> { - let message = self.call("SolveConfigModel", &(model.to_string())).await?; - try_from_message(message) + let message = message::SolveConfigModel::new(model); + let value = self.storage_dbus.call(message).await?; + Ok(value) } async fn set_locale(&self, locale: String) -> Result<(), Error> { - self.call("SetLocale", &(locale)).await?; + self.storage_dbus + .call(message::SetLocale::new(locale)) + .await?; Ok(()) } } - -fn try_from_message( - message: Message, -) -> Result { - let raw_json: String = message.body().deserialize()?; - let json: Value = serde_json::from_str(&raw_json)?; - if json.is_null() { - return Ok(T::default()); - } - let value = serde_json::from_value(json)?; - Ok(value) -} diff --git a/rust/agama-storage/src/message.rs b/rust/agama-storage/src/message.rs index 362d12974e..5fe752ce8a 100644 --- a/rust/agama-storage/src/message.rs +++ b/rust/agama-storage/src/message.rs @@ -18,11 +18,18 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use agama_utils::{actor::Message, api::storage::Config, products::ProductSpec}; +use agama_utils::{ + actor::Message, + api::{storage::Config, Issue}, + products::ProductSpec, + BoxFuture, +}; use serde_json::Value; use std::sync::Arc; use tokio::sync::RwLock; +use crate::client; + #[derive(Clone)] pub struct Activate; @@ -101,6 +108,13 @@ impl Message for GetProposal { type Reply = Option; } +#[derive(Clone)] +pub struct GetIssues; + +impl Message for GetIssues { + type Reply = Vec; +} + #[derive(Clone)] pub struct SetConfig { pub product: Arc>, @@ -121,7 +135,7 @@ impl SetConfig { } impl Message for SetConfig { - type Reply = (); + type Reply = BoxFuture>; } #[derive(Clone)] diff --git a/rust/agama-storage/src/monitor.rs b/rust/agama-storage/src/monitor.rs index 7b98fcea09..4ecf97b23d 100644 --- a/rust/agama-storage/src/monitor.rs +++ b/rust/agama-storage/src/monitor.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 crate::client::{self, Client, StorageClient}; use agama_utils::{ actor::Handler, api::{ @@ -47,8 +46,8 @@ pub enum Error { DBus(#[from] zbus::Error), #[error(transparent)] Event(#[from] broadcast::error::SendError), - #[error(transparent)] - Client(#[from] client::Error), + #[error("Storage D-Bus server error: {0}")] + DBusClient(#[from] agama_storage_client::Error), } #[proxy( @@ -104,22 +103,23 @@ pub struct Monitor { issues: Handler, events: event::Sender, connection: Connection, - client: Client, + storage_dbus: Handler, } impl Monitor { - pub fn new( + pub async fn new( progress: Handler, issues: Handler, events: event::Sender, connection: Connection, + storage_dbus: Handler, ) -> Self { Self { progress, issues, events, connection: connection.clone(), - client: Client::new(connection), + storage_dbus, } } @@ -142,7 +142,10 @@ impl Monitor { } async fn update_issues(&self) -> Result<(), Error> { - let issues = self.client.get_issues().await?; + let issues = self + .storage_dbus + .call(agama_storage_client::message::GetIssues) + .await?; self.issues .cast(issue::message::Set::new(Scope::Storage, issues))?; Ok(()) diff --git a/rust/agama-storage/src/service.rs b/rust/agama-storage/src/service.rs index b79ed0ebd5..de1b5584cc 100644 --- a/rust/agama-storage/src/service.rs +++ b/rust/agama-storage/src/service.rs @@ -26,7 +26,7 @@ use crate::{ use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, api::{event, storage::Config}, - issue, progress, + issue, progress, BoxFuture, }; use async_trait::async_trait; use serde_json::Value; @@ -73,16 +73,35 @@ impl Starter { /// Starts the service and returns a handler to communicate with it. pub async fn start(self) -> Result, Error> { - let client = match self.client { - Some(client) => client, - None => Box::new(Client::new(self.dbus.clone())), + let (client, storage_dbus) = match self.client { + Some(client) => (client, None), + None => { + let storage_dbus = agama_storage_client::service::Starter::new(self.dbus.clone()) + .start() + .await + .map_err(client::Error::from)?; + ( + Box::new(Client::new(storage_dbus.clone())) + as Box, + Some(storage_dbus), + ) + } }; let service = Service { client }; let handler = actor::spawn(service); - let monitor = Monitor::new(self.progress, self.issues, self.events, self.dbus); - monitor::spawn(monitor)?; + if let Some(storage_dbus) = storage_dbus { + let monitor = Monitor::new( + self.progress, + self.issues, + self.events, + self.dbus, + storage_dbus, + ) + .await; + monitor::spawn(monitor)?; + } Ok(handler) } } @@ -190,11 +209,15 @@ impl MessageHandler for Service { #[async_trait] impl MessageHandler for Service { - async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> { - self.client + async fn handle( + &mut self, + message: message::SetConfig, + ) -> Result>, Error> { + let rx = self + .client .set_config(message.product, message.config) .await?; - Ok(()) + Ok(rx) } } diff --git a/rust/agama-storage/src/test_utils.rs b/rust/agama-storage/src/test_utils.rs index 5f3859ab3d..c23f762992 100644 --- a/rust/agama-storage/src/test_utils.rs +++ b/rust/agama-storage/src/test_utils.rs @@ -27,7 +27,7 @@ use agama_utils::{ api::{event, storage::Config, Issue}, issue, products::ProductSpec, - progress, + progress, BoxFuture, }; use async_trait::async_trait; use serde_json::Value; @@ -149,10 +149,13 @@ impl StorageClient for TestClient { &self, _product: Arc>, config: Option, - ) -> Result<(), Error> { - let mut state = self.state.lock().await; - state.config = config; - Ok(()) + ) -> Result>, Error> { + let state = self.state.clone(); + Ok(Box::pin(async move { + let mut state = state.lock().await; + state.config = config; + Ok(()) + })) } async fn solve_config_model(&self, _model: Value) -> Result, Error> { diff --git a/rust/agama-utils/src/lib.rs b/rust/agama-utils/src/lib.rs index fd33eac4fa..60569c6df1 100644 --- a/rust/agama-utils/src/lib.rs +++ b/rust/agama-utils/src/lib.rs @@ -35,6 +35,12 @@ pub mod progress; pub mod question; pub mod test; +use std::future::Future; +use std::pin::Pin; + +/// A pinned, boxed, and sendable future. +pub type BoxFuture = Pin + Send + 'static>>; + /// Does nothing at runtime, marking the text for translation. /// /// This is useful when you need both the untranslated id diff --git a/rust/package/agama.changes b/rust/package/agama.changes index e963a3838c..6ba9a269c7 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Tue Mar 3 10:08:58 UTC 2026 - Imobach Gonzalez Sosa + +- Cache storage data using the new attributes-based API (bsc#1258466). + ------------------------------------------------------------------- Mon Mar 2 12:20:15 UTC 2026 - Michal Filka