diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ef951836e9..e765d12347 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -208,6 +208,7 @@ dependencies = [ "agama-l10n", "agama-network", "agama-proxy", + "agama-s390", "agama-security", "agama-software", "agama-storage", @@ -274,6 +275,24 @@ dependencies = [ "url", ] +[[package]] +name = "agama-s390" +version = "0.1.0" +dependencies = [ + "agama-storage", + "agama-utils", + "async-trait", + "serde", + "serde_json", + "test-context", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-test", + "tracing", + "zbus", +] + [[package]] name = "agama-security" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 02a8ccc8c6..37869e86d5 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,6 +11,7 @@ members = [ "agama-manager", "agama-proxy", "agama-network", + "agama-s390", "agama-security", "agama-server", "agama-software", @@ -22,7 +23,6 @@ members = [ "suseconnect-agama/suseconnect-agama-sys", "xtask", "zypp-agama", - "zypp-agama/zypp-agama-sys", "zypp-agama/zypp-agama-sys" ] resolver = "2" diff --git a/rust/agama-iscsi/src/monitor.rs b/rust/agama-iscsi/src/monitor.rs index 8c43d4a00a..02ea533836 100644 --- a/rust/agama-iscsi/src/monitor.rs +++ b/rust/agama-iscsi/src/monitor.rs @@ -108,11 +108,11 @@ impl Monitor { continue; } if let Some(signal) = ProgressChanged::from_message(message.clone()) { - self.handle_progress_changed(signal)?; + self.handle_progress_changed(signal).await?; continue; } if let Some(signal) = ProgressFinished::from_message(message.clone()) { - self.handle_progress_finished(signal)?; + self.handle_progress_finished(signal).await?; continue; } tracing::warn!("Unmanaged iSCSI signal: {message:?}"); @@ -129,17 +129,19 @@ impl Monitor { Ok(()) } - fn handle_progress_changed(&self, signal: ProgressChanged) -> Result<(), Error> { + async fn handle_progress_changed(&self, signal: ProgressChanged) -> Result<(), Error> { let args = signal.args()?; let progress_data = serde_json::from_str::(args.progress)?; self.progress - .cast(progress::message::SetProgress::new(progress_data.into()))?; + .call(progress::message::SetProgress::new(progress_data.into())) + .await?; Ok(()) } - fn handle_progress_finished(&self, _signal: ProgressFinished) -> Result<(), Error> { + async fn handle_progress_finished(&self, _signal: ProgressFinished) -> Result<(), Error> { self.progress - .cast(progress::message::Finish::new(Scope::ISCSI))?; + .call(progress::message::Finish::new(Scope::ISCSI)) + .await?; Ok(()) } } diff --git a/rust/agama-iscsi/src/test_utils.rs b/rust/agama-iscsi/src/test_utils.rs index e9542e235c..0640606d3e 100644 --- a/rust/agama-iscsi/src/test_utils.rs +++ b/rust/agama-iscsi/src/test_utils.rs @@ -116,7 +116,7 @@ impl ISCSIClient for TestClient { } } -/// Starts a testing storage service. +/// Starts a testing iSCSI service. pub async fn start_service( storage: Handler, events: event::Sender, diff --git a/rust/agama-manager/Cargo.toml b/rust/agama-manager/Cargo.toml index 1531ac7d49..6aaf1bf003 100644 --- a/rust/agama-manager/Cargo.toml +++ b/rust/agama-manager/Cargo.toml @@ -11,6 +11,7 @@ agama-hostname = { path = "../agama-hostname" } agama-iscsi = { path = "../agama-iscsi" } agama-l10n = { path = "../agama-l10n" } agama-network = { path = "../agama-network" } +agama-s390 = { path = "../agama-s390" } agama-security = { path = "../agama-security" } agama-software = { path = "../agama-software" } agama-storage = { path = "../agama-storage" } diff --git a/rust/agama-manager/src/lib.rs b/rust/agama-manager/src/lib.rs index 001e1ef0a4..149e5f1eb3 100644 --- a/rust/agama-manager/src/lib.rs +++ b/rust/agama-manager/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2025] SUSE LLC +// Copyright (c) [2025-2026] SUSE LLC // // All Rights Reserved. // @@ -32,6 +32,7 @@ pub use agama_iscsi as iscsi; pub use agama_l10n as l10n; pub use agama_network as network; pub use agama_proxy as proxy; +pub use agama_s390 as s390; pub use agama_security as security; pub use agama_software as software; pub use agama_storage as storage; diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index b04b0596d1..60a944c436 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2025] SUSE LLC +// Copyright (c) [2025-2026] SUSE LLC // // All Rights Reserved. // @@ -19,8 +19,8 @@ // find current contact information at www.suse.com. use crate::{ - bootloader, checks, files, hardware, hostname, iscsi, l10n, message, network, proxy, security, - software, storage, tasks, users, + bootloader, checks, files, hardware, hostname, iscsi, l10n, message, network, proxy, s390, + security, software, storage, tasks, users, }; use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, @@ -30,6 +30,7 @@ use agama_utils::{ status::Stage, Action, Config, Event, FinishMethod, Issue, IssueMap, Proposal, Scope, Status, SystemInfo, }, + arch::Arch, issue, licenses, products::{self, ProductSpec}, progress, question, @@ -93,6 +94,8 @@ pub enum Error { PendingIssues { issues: HashMap> }, #[error(transparent)] Users(#[from] users::service::Error), + #[error(transparent)] + S390(#[from] s390::service::Error), } pub struct Starter { @@ -113,6 +116,7 @@ pub struct Starter { progress: Option>, hardware: Option, users: Option>, + s390: Option>, } impl Starter { @@ -139,6 +143,7 @@ impl Starter { progress: None, hardware: None, users: None, + s390: None, } } @@ -146,14 +151,17 @@ impl Starter { self.bootloader = Some(bootloader); self } + pub fn with_hostname(mut self, hostname: Handler) -> Self { self.hostname = Some(hostname); self } + pub fn with_iscsi(mut self, iscsi: Handler) -> Self { self.iscsi = Some(iscsi); self } + pub fn with_network(mut self, network: NetworkSystemClient) -> Self { self.network = Some(network); self @@ -193,6 +201,7 @@ impl Starter { self.proxy = Some(proxy); self } + pub fn with_progress(mut self, progress: Handler) -> Self { self.progress = Some(progress); self @@ -208,6 +217,11 @@ impl Starter { self } + pub fn with_s390(mut self, s390: Handler) -> Self { + self.s390 = Some(s390); + self + } + /// Starts the service and returns a handler to communicate with it. pub async fn start(self) -> Result, Error> { let issues = match self.issues { @@ -333,6 +347,25 @@ impl Starter { } }; + let s390 = match self.s390 { + Some(s390) => Some(s390), + None => { + if !Arch::is_s390() { + None + } else { + let s390 = s390::Service::starter( + storage.clone(), + self.events.clone(), + progress.clone(), + self.dbus.clone(), + ) + .start() + .await?; + Some(s390) + } + } + }; + let runner = tasks::TasksRunner { bootloader: bootloader.clone(), files: files.clone(), @@ -348,6 +381,7 @@ impl Starter { software: software.clone(), storage: storage.clone(), users: users.clone(), + s390: s390.clone(), }; let tasks = actor::spawn(runner); @@ -361,10 +395,8 @@ impl Starter { l10n, network, proxy, - security, software, storage, - files, products: products::Registry::default(), licenses: licenses::Registry::default(), hardware, @@ -373,6 +405,7 @@ impl Starter { product: None, users, tasks, + s390, }; service.setup().await?; @@ -386,11 +419,9 @@ pub struct Service { iscsi: Handler, proxy: Handler, l10n: Handler, - security: Handler, software: Handler, network: NetworkSystemClient, storage: Handler, - files: Handler, issues: Handler, progress: Handler, questions: Handler, @@ -401,6 +432,7 @@ pub struct Service { config: Config, system: manager::SystemInfo, users: Handler, + s390: Option>, tasks: Handler, } @@ -487,6 +519,13 @@ impl Service { Ok(()) } + async fn probe_dasd(&self) -> Result<(), Error> { + if let Some(s390) = &self.s390 { + s390.call(s390::message::ProbeDASD).await?; + } + Ok(()) + } + fn set_product(&mut self, config: &Config) -> Result<(), Error> { self.product = None; self.update_product(config) @@ -577,6 +616,12 @@ impl MessageHandler for Service { let iscsi = self.iscsi.call(iscsi::message::GetSystem).await?; let network = self.network.get_system().await?; + let s390 = if let Some(s390) = &self.s390 { + Some(s390.call(s390::message::GetSystem).await?) + } else { + None + }; + // If the software service is busy, it will not answer. let software = if self.is_software_available().await? { self.software.call(software::message::GetSystem).await? @@ -592,6 +637,7 @@ impl MessageHandler for Service { network, storage, iscsi, + s390, software, }) } @@ -621,6 +667,12 @@ impl MessageHandler for Service { let storage = self.storage.call(storage::message::GetConfig).await?; let users = self.users.call(users::message::GetConfig).await?; + let s390 = if let Some(s390) = &self.s390 { + Some(s390.call(s390::message::GetConfig).await?) + } else { + None + }; + // If the software service is busy, it will not answer. let software = if self.is_software_available().await? { Some(self.software.call(software::message::GetConfig).await?) @@ -641,6 +693,7 @@ impl MessageHandler for Service { storage, files: None, users: Some(users), + s390, }) } } @@ -745,6 +798,10 @@ impl MessageHandler for Service { checks::check_stage(&self.progress, Stage::Configuring).await?; self.probe_storage().await?; } + Action::ProbeDASD => { + checks::check_stage(&self.progress, Stage::Configuring).await?; + self.probe_dasd().await?; + } Action::Install => { self.tasks.cast(tasks::message::Install)?; } diff --git a/rust/agama-manager/src/tasks/runner.rs b/rust/agama-manager/src/tasks/runner.rs index 8da115586f..cb7e5dd513 100644 --- a/rust/agama-manager/src/tasks/runner.rs +++ b/rust/agama-manager/src/tasks/runner.rs @@ -21,8 +21,8 @@ use std::sync::Arc; use crate::{ - bootloader, checks, files, hostname, iscsi, l10n, proxy, security, service, software, storage, - tasks::message, users, + bootloader, checks, files, hostname, iscsi, l10n, proxy, s390, security, service, software, + storage, tasks::message, users, }; use agama_network::NetworkSystemClient; use agama_utils::{ @@ -57,6 +57,7 @@ pub struct TasksRunner { pub software: Handler, pub storage: Handler, pub users: Handler, + pub s390: Option>, } impl Actor for TasksRunner { @@ -116,6 +117,7 @@ impl MessageHandler for TasksRunner { software: self.software.clone(), storage: self.storage.clone(), users: self.users.clone(), + s390: self.s390.clone(), }; if let Err(error) = action.run(message.product, message.config).await { @@ -263,6 +265,7 @@ struct SetConfigAction { software: Handler, storage: Handler, users: Handler, + s390: Option>, } impl SetConfigAction { @@ -288,6 +291,10 @@ impl SetConfigAction { gettext("Configuring iSCSI devices"), ]; + if self.s390.is_some() { + steps.push(gettext("Configuring DASD devices")); + } + if config.network.is_some() { steps.push(gettext("Setting up the network")); } @@ -370,6 +377,14 @@ impl SetConfigAction { .call(iscsi::message::SetConfig::new(config.iscsi.clone())) .await?; + if let Some(s390) = &self.s390 { + self.progress + .call(progress::message::Next::new(Scope::Manager)) + .await?; + s390.call(s390::message::SetConfig::new(config.s390.clone())) + .await?; + } + if let Some(network) = config.network.clone() { self.progress .call(progress::message::Next::new(Scope::Manager)) diff --git a/rust/agama-manager/src/test_utils.rs b/rust/agama-manager/src/test_utils.rs index 56be95c333..757c6de551 100644 --- a/rust/agama-manager/src/test_utils.rs +++ b/rust/agama-manager/src/test_utils.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2025] SUSE LLC +// Copyright (c) [2025-2026] SUSE LLC // // All Rights Reserved. // @@ -27,6 +27,7 @@ use agama_iscsi::test_utils::start_service as start_iscsi_service; use agama_l10n::test_utils::start_service as start_l10n_service; use agama_network::test_utils::start_service as start_network_service; use agama_proxy::test_utils::start_service as start_proxy_service; +use agama_s390::test_utils::start_service as start_s390_service; use agama_security::test_utils::start_service as start_security_service; use agama_software::test_utils::start_service as start_software_service; use agama_storage::test_utils::start_service as start_storage_service; @@ -54,12 +55,20 @@ pub async fn start_service(events: event::Sender, dbus: zbus::Connection) -> Han dbus.clone(), ) .await; + let s390 = start_s390_service( + storage.clone(), + events.clone(), + progress.clone(), + dbus.clone(), + ) + .await; Service::starter(questions.clone(), events.clone(), dbus.clone()) .with_hostname(start_hostname_service(events.clone(), issues.clone()).await) .with_l10n(start_l10n_service(events.clone(), issues.clone()).await) .with_storage(storage) .with_iscsi(iscsi) + .with_s390(s390) .with_bootloader(start_bootloader_service(issues.clone(), dbus.clone()).await) .with_network(start_network_service(events.clone(), progress.clone()).await) .with_proxy(start_proxy_service(events.clone()).await) diff --git a/rust/agama-s390/Cargo.toml b/rust/agama-s390/Cargo.toml new file mode 100644 index 0000000000..09c963d541 --- /dev/null +++ b/rust/agama-s390/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "agama-s390" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true + +[dependencies] +agama-utils = { path = "../agama-utils" } +agama-storage = { path = "../agama-storage" } +thiserror = "2.0.16" +async-trait = "0.1.89" +zbus = "5.7.1" +tracing = "0.1.41" +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" + +[dev-dependencies] +test-context = "0.4.1" +tokio-test = "0.4.4" diff --git a/rust/agama-s390/src/dasd.rs b/rust/agama-s390/src/dasd.rs new file mode 100644 index 0000000000..aead70c64e --- /dev/null +++ b/rust/agama-s390/src/dasd.rs @@ -0,0 +1,27 @@ +// 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. + +pub mod client; +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 new file mode 100644 index 0000000000..3bf75f1adf --- /dev/null +++ b/rust/agama-s390/src/dasd/client.rs @@ -0,0 +1,88 @@ +// 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. + +//! Implements a client to access Agama's D-Bus API related to Bootloader management. + +use crate::dasd::dbus::DASDProxy; +use agama_utils::api::RawConfig; +use async_trait::async_trait; +use serde_json::Value; +use zbus::Connection; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + DBus(#[from] zbus::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +#[async_trait] +pub trait DASDClient { + async fn probe(&self) -> Result<(), Error>; + async fn get_system(&self) -> Result, Error>; + async fn get_config(&self) -> Result, Error>; + async fn set_config(&self, config: Option) -> Result<(), Error>; +} + +#[derive(Clone)] +pub struct Client<'a> { + proxy: DASDProxy<'a>, +} + +impl<'a> Client<'a> { + pub async fn new(connection: Connection) -> Result, Error> { + let proxy = DASDProxy::new(&connection).await?; + Ok(Self { proxy }) + } +} + +#[async_trait] +impl<'a> DASDClient for Client<'a> { + async fn probe(&self) -> Result<(), Error> { + self.proxy.probe().await?; + Ok(()) + } + + 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)), + } + } + + 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))), + } + } + + async fn set_config(&self, config: Option) -> Result<(), Error> { + self.proxy + .set_config(serde_json::to_string(&config)?.as_str()) + .await?; + Ok(()) + } +} diff --git a/rust/agama-s390/src/dasd/dbus.rs b/rust/agama-s390/src/dasd/dbus.rs new file mode 100644 index 0000000000..9eebf3a5c2 --- /dev/null +++ b/rust/agama-s390/src/dasd/dbus.rs @@ -0,0 +1,60 @@ +//! # 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 new file mode 100644 index 0000000000..6c34e844bb --- /dev/null +++ b/rust/agama-s390/src/dasd/monitor.rs @@ -0,0 +1,181 @@ +// 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 crate::{ + dasd::dbus::{ + DASDProxy, FormatChanged, FormatFinished, ProgressChanged, ProgressFinished, SystemChanged, + }, + storage, +}; +use agama_utils::{ + actor::Handler, + api::{ + event::{self, Event}, + s390::dasd::FormatSummary, + Progress, Scope, + }, + progress, +}; +use serde::Deserialize; +use serde_json; +use tokio::sync::broadcast; +use tokio_stream::StreamExt; +use zbus::{message, Connection, MatchRule, MessageStream}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Progress(#[from] progress::service::Error), + #[error(transparent)] + Event(#[from] broadcast::error::SendError), + #[error(transparent)] + DBus(#[from] zbus::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Storage(#[from] storage::service::Error), +} + +#[derive(Debug, Deserialize)] +struct ProgressData { + pub size: usize, + pub steps: Vec, + pub step: String, + pub index: usize, +} + +impl From for Progress { + fn from(data: ProgressData) -> Self { + Progress { + scope: Scope::DASD, + size: data.size, + steps: data.steps, + step: data.step, + index: data.index, + } + } +} + +pub struct Monitor { + storage: Handler, + progress: Handler, + events: event::Sender, + connection: Connection, +} + +impl Monitor { + pub fn new( + storage: Handler, + progress: Handler, + events: event::Sender, + connection: Connection, + ) -> Self { + Self { + storage, + progress, + events, + connection, + } + } + + async fn run(&self) -> Result<(), Error> { + let proxy = DASDProxy::new(&self.connection).await?; + let rule = MatchRule::builder() + .msg_type(message::Type::Signal) + .sender(proxy.inner().destination())? + .path(proxy.inner().path())? + .interface(proxy.inner().interface())? + .build(); + let mut stream = MessageStream::for_match_rule(rule, &self.connection, None).await?; + + while let Some(Ok(message)) = stream.next().await { + if let Some(signal) = SystemChanged::from_message(message.clone()) { + self.handle_system_changed(signal)?; + continue; + } + if let Some(signal) = ProgressChanged::from_message(message.clone()) { + self.handle_progress_changed(signal).await?; + continue; + } + if let Some(signal) = ProgressFinished::from_message(message.clone()) { + self.handle_progress_finished(signal).await?; + continue; + } + if let Some(signal) = FormatChanged::from_message(message.clone()) { + self.handle_format_changed(signal)?; + continue; + } + if let Some(signal) = FormatFinished::from_message(message.clone()) { + self.handle_format_finished(signal)?; + continue; + } + tracing::warn!("Unmanaged DASD signal: {message:?}"); + } + + Ok(()) + } + + fn handle_system_changed(&self, _signal: SystemChanged) -> Result<(), Error> { + self.events + .send(Event::SystemChanged { scope: Scope::DASD })?; + self.storage.cast(storage::message::Probe)?; + Ok(()) + } + + async fn handle_progress_changed(&self, signal: ProgressChanged) -> Result<(), Error> { + let args = signal.args()?; + let progress_data = serde_json::from_str::(args.progress)?; + self.progress + .call(progress::message::SetProgress::new(progress_data.into())) + .await?; + Ok(()) + } + + async fn handle_progress_finished(&self, _signal: ProgressFinished) -> Result<(), Error> { + self.progress + .call(progress::message::Finish::new(Scope::DASD)) + .await?; + Ok(()) + } + + fn handle_format_changed(&self, signal: FormatChanged) -> Result<(), Error> { + let args = signal.args()?; + let summary = serde_json::from_str::(args.summary)?; + self.events.send(Event::DASDFormatChanged { summary })?; + Ok(()) + } + + fn handle_format_finished(&self, _signal: FormatFinished) -> Result<(), Error> { + self.events.send(Event::DASDFormatFinished)?; + Ok(()) + } +} + +/// Spawns a Tokio task for the monitor. +/// +/// * `monitor`: monitor to spawn. +pub fn spawn(monitor: Monitor) -> Result<(), Error> { + tokio::spawn(async move { + if let Err(e) = monitor.run().await { + tracing::error!("Error running the DASD monitor: {e:?}"); + } + }); + Ok(()) +} diff --git a/rust/agama-s390/src/lib.rs b/rust/agama-s390/src/lib.rs new file mode 100644 index 0000000000..2ac1105c6b --- /dev/null +++ b/rust/agama-s390/src/lib.rs @@ -0,0 +1,117 @@ +// 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. + +pub mod service; +pub use service::{Service, Starter}; + +pub mod dasd; +pub mod message; +pub mod test_utils; + +use agama_storage as storage; + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::TestDASDClient; + use agama_utils::{ + actor::Handler, + api::{s390::Config, Event}, + issue, progress, test, + }; + use test_context::{test_context, AsyncTestContext}; + use tokio::sync::broadcast; + + struct Context { + handler: Handler, + dasd: TestDASDClient, + } + + impl AsyncTestContext for Context { + async fn setup() -> Context { + let (events, _) = broadcast::channel::(16); + let connection = test::dbus::connection().await.unwrap(); + let progress = progress::Service::starter(events.clone()).start(); + let issues = issue::Service::starter(events.clone()).start(); + let storage = storage::test_utils::start_service( + events.clone(), + issues.clone(), + progress.clone(), + connection.clone(), + ) + .await; + let dasd = TestDASDClient::new(); + let handler = Service::starter(storage, events, progress, connection) + .with_dasd(dasd.clone()) + .start() + .await + .expect("Could not start the DASD service"); + + Context { handler, dasd } + } + } + + #[test_context(Context)] + #[tokio::test] + async fn test_probe(ctx: &mut Context) -> Result<(), service::Error> { + ctx.handler.call(message::ProbeDASD).await?; + + let state = ctx.dasd.state().await; + assert!(state.probed); + + Ok(()) + } + + #[test_context(Context)] + #[tokio::test] + async fn test_get_system(ctx: &mut Context) -> Result<(), service::Error> { + let system = ctx.handler.call(message::GetSystem).await?; + assert!(system.dasd.is_some()); + + Ok(()) + } + + #[test_context(Context)] + #[tokio::test] + async fn test_set_config(ctx: &mut Context) -> Result<(), service::Error> { + let config: Config = serde_json::from_str( + r#" + { + "dasd": { + "devices": [ + { + "channel": "0.0.0100", + "active": true + } + ] + } + } + "#, + ) + .unwrap(); + let message = message::SetConfig::new(Some(config)); + ctx.handler.call(message).await?; + + let config = ctx.handler.call(message::GetConfig).await?; + assert!(config.dasd.is_some()); + + Ok(()) + } +} diff --git a/rust/agama-s390/src/message.rs b/rust/agama-s390/src/message.rs new file mode 100644 index 0000000000..d8063c1936 --- /dev/null +++ b/rust/agama-s390/src/message.rs @@ -0,0 +1,56 @@ +// 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::s390::{Config, SystemInfo}, +}; + +pub struct ProbeDASD; + +impl Message for ProbeDASD { + type Reply = (); +} + +pub struct GetSystem; + +impl Message for GetSystem { + type Reply = SystemInfo; +} + +pub struct GetConfig; + +impl Message for GetConfig { + type Reply = Config; +} + +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-s390/src/service.rs b/rust/agama-s390/src/service.rs new file mode 100644 index 0000000000..161ebb7fd8 --- /dev/null +++ b/rust/agama-s390/src/service.rs @@ -0,0 +1,140 @@ +// 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 crate::{ + dasd::{self, client::DASDClient}, + message, storage, +}; +use agama_utils::{ + actor::{self, Actor, Handler, MessageHandler}, + api::{ + event, + s390::{Config, SystemInfo}, + }, + progress, +}; +use async_trait::async_trait; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Actor(#[from] actor::Error), + #[error(transparent)] + DASDClient(#[from] dasd::client::Error), + #[error(transparent)] + DASDMonitor(#[from] dasd::monitor::Error), +} + +pub struct Starter { + storage: Handler, + events: event::Sender, + progress: Handler, + connection: zbus::Connection, + dasd: Option>, +} + +impl Starter { + pub fn new( + storage: Handler, + events: event::Sender, + progress: Handler, + connection: zbus::Connection, + ) -> Self { + Self { + storage, + events, + progress, + connection, + dasd: None, + } + } + + pub fn with_dasd(mut self, client: impl DASDClient + Send + 'static) -> Self { + self.dasd = Some(Box::new(client)); + self + } + + 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?), + }; + let service = Service { dasd: dasd_client }; + let handler = actor::spawn(service); + + let dasd_monitor = + dasd::Monitor::new(self.storage, self.progress, self.events, self.connection); + dasd::monitor::spawn(dasd_monitor)?; + + Ok(handler) + } +} + +pub struct Service { + dasd: Box, +} + +impl Service { + pub fn starter( + storage: Handler, + events: event::Sender, + progress: Handler, + connection: zbus::Connection, + ) -> Starter { + Starter::new(storage, events, progress, connection) + } +} + +impl Actor for Service { + type Error = Error; +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::ProbeDASD) -> Result<(), Error> { + self.dasd.probe().await?; + Ok(()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::GetSystem) -> Result { + let dasd = self.dasd.get_system().await?; + Ok(SystemInfo { dasd }) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::GetConfig) -> Result { + let dasd = self.dasd.get_config().await?; + Ok(Config { dasd }) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> { + let config = message.config.and_then(|c| c.dasd); + self.dasd.set_config(config).await?; + Ok(()) + } +} diff --git a/rust/agama-s390/src/test_utils.rs b/rust/agama-s390/src/test_utils.rs new file mode 100644 index 0000000000..2b103252d2 --- /dev/null +++ b/rust/agama-s390/src/test_utils.rs @@ -0,0 +1,137 @@ +// 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. + +//! This module implements a set of utilities for tests. + +use crate::{ + dasd::client::{DASDClient, Error}, + service::{Service, Starter}, + storage, +}; +use agama_utils::{ + actor::Handler, + api::{event, RawConfig}, + progress, +}; +use async_trait::async_trait; +use serde_json::Value; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Default, Clone)] +pub struct TestDASDClientState { + pub probed: bool, + pub config: Option, +} + +/// Test client for DASD. +/// +/// This client implements a dummy client to replace the original [DASDClient]. +/// +/// ``` +/// use agama_s390::{test_utils::TestDASDClient, dasd::client::DASDClient}; +/// +/// # tokio_test::block_on(async { +/// +/// // Assert whether the main methods were called. +/// let client = TestDASDClient::new(); +/// assert_eq!(client.state().await.probed, false); +/// +/// client.probe().await.unwrap(); +/// assert_eq!(client.state().await.probed, true); +/// # }); +/// ``` +#[derive(Clone)] +pub struct TestDASDClient { + state: Arc>, +} + +impl TestDASDClient { + pub fn new() -> Self { + let state = TestDASDClientState::default(); + Self { + state: Arc::new(Mutex::new(state)), + } + } + + pub async fn state(&self) -> TestDASDClientState { + self.state.lock().await.clone() + } +} + +#[async_trait] +impl DASDClient for TestDASDClient { + async fn probe(&self) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.probed = true; + Ok(()) + } + + async fn get_system(&self) -> Result, Error> { + let system: Value = serde_json::from_str( + r#" + { + "devices": [ + { + "channel": "0.0.0100", + "deviceName": "dasda", + "type": "ECKD", + "diag": false, + "accessType": "diag", + "partitionInfo": "1", + "status": "active", + "active": true, + "formatted": true + } + ] + } + "#, + ) + .unwrap(); + + Ok(Some(system)) + } + + async fn get_config(&self) -> Result, Error> { + let state = self.state.lock().await; + Ok(state.config.clone()) + } + + async fn set_config(&self, config: Option) -> Result<(), Error> { + let mut state = self.state.lock().await; + state.config = config; + Ok(()) + } +} + +/// Starts a testing DASD service. +pub async fn start_service( + storage: Handler, + events: event::Sender, + progress: Handler, + connection: zbus::Connection, +) -> Handler { + let dasd = TestDASDClient::new(); + Starter::new(storage, events, progress, connection) + .with_dasd(dasd) + .start() + .await + .expect("Could not start a testing s390 service") +} diff --git a/rust/agama-storage/src/monitor.rs b/rust/agama-storage/src/monitor.rs index 1b293bbece..d29ad3af78 100644 --- a/rust/agama-storage/src/monitor.rs +++ b/rust/agama-storage/src/monitor.rs @@ -153,8 +153,8 @@ impl Monitor { match signal { Signal::SystemChanged(signal) => self.handle_system_changed(signal)?, Signal::ProposalChanged(signal) => self.handle_proposal_changed(signal).await?, - Signal::ProgressChanged(signal) => self.handle_progress_changed(signal)?, - Signal::ProgressFinished(signal) => self.handle_progress_finished(signal)?, + Signal::ProgressChanged(signal) => self.handle_progress_changed(signal).await?, + Signal::ProgressFinished(signal) => self.handle_progress_finished(signal).await?, } Ok(()) } @@ -175,7 +175,7 @@ impl Monitor { self.update_issues().await } - fn handle_progress_changed(&self, signal: ProgressChanged) -> Result<(), Error> { + async fn handle_progress_changed(&self, signal: ProgressChanged) -> Result<(), Error> { let Ok(args) = signal.args() else { return Err(Error::ProgressChangedArgs); }; @@ -183,14 +183,16 @@ impl Monitor { return Err(Error::ProgressChangedData); }; self.progress - .cast(progress::message::SetProgress::new(progress_data.into()))?; + .call(progress::message::SetProgress::new(progress_data.into())) + .await?; Ok(()) } - fn handle_progress_finished(&self, _signal: ProgressFinished) -> Result<(), Error> { + async fn handle_progress_finished(&self, _signal: ProgressFinished) -> Result<(), Error> { self.progress - .cast(progress::message::Finish::new(Scope::Storage))?; + .call(progress::message::Finish::new(Scope::Storage)) + .await?; Ok(()) } diff --git a/rust/agama-utils/src/api.rs b/rust/agama-utils/src/api.rs index f66c1b8f80..194555cd33 100644 --- a/rust/agama-utils/src/api.rs +++ b/rust/agama-utils/src/api.rs @@ -42,6 +42,9 @@ pub use system_info::SystemInfo; pub mod config; pub use config::Config; +mod raw_config; +pub use raw_config::RawConfig; + pub mod patch; pub use patch::Patch; @@ -61,6 +64,7 @@ pub mod network; pub mod proxy; pub mod query; pub mod question; +pub mod s390; pub mod security; pub mod software; pub mod storage; diff --git a/rust/agama-utils/src/api/action.rs b/rust/agama-utils/src/api/action.rs index 30e8993627..8da323024f 100644 --- a/rust/agama-utils/src/api/action.rs +++ b/rust/agama-utils/src/api/action.rs @@ -30,6 +30,9 @@ pub enum Action { ActivateStorage, #[serde(rename = "probeStorage")] ProbeStorage, + /// Performs a DASD probing on demand. + #[serde(rename = "probeDASD")] + ProbeDASD, #[serde(rename = "configureL10n")] ConfigureL10n(l10n::SystemConfig), #[serde(rename = "install")] diff --git a/rust/agama-utils/src/api/config.rs b/rust/agama-utils/src/api/config.rs index 8f553ddb0b..f837ecbe5b 100644 --- a/rust/agama-utils/src/api/config.rs +++ b/rust/agama-utils/src/api/config.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2025] SUSE LLC +// Copyright (c) [2025-2026] SUSE LLC // // All Rights Reserved. // @@ -19,43 +19,39 @@ // find current contact information at www.suse.com. use crate::api::{ - bootloader, files, hostname, iscsi, l10n, network, proxy, question, security, + bootloader, files, hostname, iscsi, l10n, network, proxy, question, s390, security, software::{self, ProductConfig}, storage, users, }; use merge::Merge; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[skip_serializing_none] #[derive(Clone, Debug, Default, Deserialize, Serialize, Merge, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] #[merge(strategy = merge::option::recurse)] pub struct Config { - #[serde(skip_serializing_if = "Option::is_none")] pub bootloader: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub hostname: Option, - #[serde(skip_serializing_if = "Option::is_none")] #[serde(alias = "localization")] pub l10n: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub proxy: Option, - #[serde(flatten, skip_serializing_if = "Option::is_none")] + #[serde(flatten)] pub security: Option, - #[serde(flatten, skip_serializing_if = "Option::is_none")] + #[serde(flatten)] pub software: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub network: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub questions: Option, - #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] pub storage: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub iscsi: Option, - #[serde(flatten, skip_serializing_if = "Option::is_none")] + #[serde(flatten)] pub files: Option, - #[serde(flatten, skip_serializing_if = "Option::is_none")] + #[serde(flatten)] pub users: Option, + #[serde(flatten)] + pub s390: Option, } impl Config { diff --git a/rust/agama-utils/src/api/event.rs b/rust/agama-utils/src/api/event.rs index 69ac233182..05d62c998c 100644 --- a/rust/agama-utils/src/api/event.rs +++ b/rust/agama-utils/src/api/event.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::api::scope::Scope; -use crate::api::{progress::Progress, status::Stage}; +use crate::api::{progress::Progress, s390::dasd, scope::Scope, status::Stage}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; @@ -63,6 +62,12 @@ pub enum Event { QuestionAnswered { id: u32, }, + /// DASD format changed. + DASDFormatChanged { + summary: dasd::FormatSummary, + }, + /// DASD format finished (contains exit status of the format operation). + DASDFormatFinished, } pub type Sender = broadcast::Sender; diff --git a/rust/agama-utils/src/api/raw_config.rs b/rust/agama-utils/src/api/raw_config.rs new file mode 100644 index 0000000000..2d9570180b --- /dev/null +++ b/rust/agama-utils/src/api/raw_config.rs @@ -0,0 +1,155 @@ +// 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 merge::Merge; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, utoipa::ToSchema)] +#[serde(transparent)] +pub struct RawConfig(pub Value); + +impl Merge for RawConfig { + // Merge by using "ignore" strategy, that is, lhs takes precedence over rhs. + // Note that the new config is used as lhs, for example: new_config.merge(old_config). This + // implies we want to keep the current values of lhs and only the missing values are taken from + // rhs. + fn merge(&mut self, rhs: Self) { + let lhs_value = &mut self.0; + let rhs_value = rhs.0; + + let Value::Object(lhs_object) = lhs_value else { + return; + }; + + let Value::Object(rhs_object) = rhs_value else { + return; + }; + + for (k, v) in rhs_object { + lhs_object.entry(k).or_insert(v); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_merge_from_empty() { + let original: RawConfig = serde_json::from_str( + r#" + { + "field1": "value", + "field2": [ + { "foo": "bar" } + ] + } + "#, + ) + .unwrap(); + + let mut config: RawConfig = serde_json::from_str(r#"{}"#).unwrap(); + + config.merge(original.clone()); + assert_eq!(config, original); + } + + #[test] + fn test_merge_value() { + let original: RawConfig = serde_json::from_str( + r#" + { + "field1": "value", + "field2": [ + { "foo1": "bar1" } + ] + } + "#, + ) + .unwrap(); + + let mut config: RawConfig = serde_json::from_str( + r#" + { + "field2": [ + { "foo2": "bar2" } + ] + } + "#, + ) + .unwrap(); + + let result: RawConfig = serde_json::from_str( + r#" + { + "field1": "value", + "field2": [ + { "foo2": "bar2" } + ] + } + "#, + ) + .unwrap(); + + config.merge(original); + assert_eq!(config, result); + } + + #[test] + fn test_merge_list() { + let original: RawConfig = serde_json::from_str( + r#" + { + "field1": "value1", + "field2": [ + { "foo1": "bar1" } + ] + } + "#, + ) + .unwrap(); + + let mut config: RawConfig = serde_json::from_str( + r#" + { + "field1": "value2" + } + "#, + ) + .unwrap(); + + let result: RawConfig = serde_json::from_str( + r#" + { + "field1": "value2", + "field2": [ + { "foo1": "bar1" } + ] + } + "#, + ) + .unwrap(); + + config.merge(original); + assert_eq!(config, result); + } +} diff --git a/rust/agama-utils/src/api/s390.rs b/rust/agama-utils/src/api/s390.rs new file mode 100644 index 0000000000..c4c78a080c --- /dev/null +++ b/rust/agama-utils/src/api/s390.rs @@ -0,0 +1,27 @@ +// 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 config; +pub use config::Config; + +mod system_info; +pub use system_info::SystemInfo; + +pub mod dasd; diff --git a/rust/agama-utils/src/api/s390/config.rs b/rust/agama-utils/src/api/s390/config.rs new file mode 100644 index 0000000000..1362e2003e --- /dev/null +++ b/rust/agama-utils/src/api/s390/config.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. + +use crate::api::raw_config::RawConfig; +use merge::Merge; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(Clone, Debug, Default, Deserialize, Serialize, Merge, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +#[merge(strategy = merge::option::recurse)] +/// s390 configuration. +pub struct Config { + /// Configuration of the DASD devices. + pub dasd: Option, +} diff --git a/rust/agama-utils/src/api/s390/dasd.rs b/rust/agama-utils/src/api/s390/dasd.rs new file mode 100644 index 0000000000..0bdea8d738 --- /dev/null +++ b/rust/agama-utils/src/api/s390/dasd.rs @@ -0,0 +1,22 @@ +// 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 format_summary; +pub use format_summary::FormatSummary; diff --git a/rust/agama-utils/src/api/s390/dasd/format_summary.rs b/rust/agama-utils/src/api/s390/dasd/format_summary.rs new file mode 100644 index 0000000000..98ea18766f --- /dev/null +++ b/rust/agama-utils/src/api/s390/dasd/format_summary.rs @@ -0,0 +1,40 @@ +// 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. + +//! This module define types related to the progress report. + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(transparent)] +pub struct FormatSummary(Vec); + +#[derive(Clone, Debug, Deserialize, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +struct FormatInfo { + /// DASD channel. + channel: String, + /// Total number of cylinders to format. + total_cylinders: usize, + /// Number of formatted cylinders. + formatted_cylinders: usize, + /// Whether the format has finished. + finished: bool, +} diff --git a/rust/agama-utils/src/api/s390/system_info.rs b/rust/agama-utils/src/api/s390/system_info.rs new file mode 100644 index 0000000000..0cbb33e790 --- /dev/null +++ b/rust/agama-utils/src/api/s390/system_info.rs @@ -0,0 +1,30 @@ +// 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 serde::Serialize; +use serde_json::Value; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SystemInfo { + pub dasd: Option, +} diff --git a/rust/agama-utils/src/api/scope.rs b/rust/agama-utils/src/api/scope.rs index ec5c3b0e62..ce657c9ad0 100644 --- a/rust/agama-utils/src/api/scope.rs +++ b/rust/agama-utils/src/api/scope.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2025] SUSE LLC +// Copyright (c) [2025-2026] SUSE LLC // // All Rights Reserved. // @@ -48,5 +48,8 @@ pub enum Scope { Storage, Files, ISCSI, + #[strum(serialize = "dasd")] + #[serde(rename = "dasd")] + DASD, Users, } diff --git a/rust/agama-utils/src/api/system_info.rs b/rust/agama-utils/src/api/system_info.rs index aa16d83b46..32b2bd0d06 100644 --- a/rust/agama-utils/src/api/system_info.rs +++ b/rust/agama-utils/src/api/system_info.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2025] SUSE LLC +// Copyright (c) [2025-2026] SUSE LLC // // All Rights Reserved. // @@ -18,10 +18,12 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::api::{hostname, l10n, manager, network, proxy, software}; +use crate::api::{hostname, l10n, manager, network, proxy, s390, software}; use serde::Serialize; use serde_json::Value; +use serde_with::skip_serializing_none; +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct SystemInfo { @@ -31,9 +33,9 @@ pub struct SystemInfo { pub proxy: Option, pub l10n: l10n::SystemInfo, pub software: software::SystemInfo, - #[serde(skip_serializing_if = "Option::is_none")] pub storage: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub iscsi: Option, pub network: network::SystemInfo, + #[serde(flatten)] + pub s390: Option, } diff --git a/rust/agama-utils/src/arch.rs b/rust/agama-utils/src/arch.rs index aff1872ad3..80c1c4fa1a 100644 --- a/rust/agama-utils/src/arch.rs +++ b/rust/agama-utils/src/arch.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2025] SUSE LLC +// Copyright (c) [2025-2026] SUSE LLC // // All Rights Reserved. // @@ -58,6 +58,15 @@ impl Arch { Arch::X86_64 => "x86_64".to_string(), } } + + /// Whether the current architecture is s390. + pub fn is_s390() -> bool { + let Ok(arch) = Self::current() else { + tracing::error!("Failed to determine the architecture"); + return false; + }; + arch == Self::S390X + } } #[cfg(test)] @@ -111,4 +120,16 @@ mod tests { fn test_current_arch_x86_64() { assert_eq!(Arch::current().unwrap(), Arch::X86_64); } + + #[cfg(target_arch = "s390x")] + #[test] + fn test_arch_is_s390() { + assert!(Arch::is_s390()); + } + + #[cfg(not(target_arch = "s390x"))] + #[test] + fn test_arch_is_not_s390() { + assert!(!Arch::is_s390()); + } } diff --git a/rust/package/agama.changes b/rust/package/agama.changes index dd1a2ba5cf..47f8a1f8fb 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Feb 11 15:49:05 UTC 2026 - José Iván López González + +- Add agama-s390 service and integrate DASD into the new HTTP API + (gh#agama-project/agama#3128). + ------------------------------------------------------------------- Wed Feb 11 15:47:38 UTC 2026 - Imobach Gonzalez Sosa @@ -71,7 +77,7 @@ Fri Jan 30 16:27:35 UTC 2026 - Ancor Gonzalez Sosa ------------------------------------------------------------------- Fri Jan 30 11:00:08 UTC 2026 - Knut Anderssen -- Added proxy service allowing to configure the proxy through the +- Added proxy service allowing to configure the proxy through the kernel cmdline proxy argument and with HTTP API (gh#agama-project/agama#3069). diff --git a/service/lib/agama/dbus/storage/dasd.rb b/service/lib/agama/dbus/storage/dasd.rb index de418091bd..f588ba0fb5 100644 --- a/service/lib/agama/dbus/storage/dasd.rb +++ b/service/lib/agama/dbus/storage/dasd.rb @@ -173,7 +173,7 @@ def format_summary_json(format_statuses) { channel: format_status.dasd.id, totalCylinders: format_status.cylinders, - FormattedCylinders: format_status.progress, + formattedCylinders: format_status.progress, finished: format_status.done? } end diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index e26c372fa7..bdc7b0d77a 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Wed Feb 11 14:48:58 UTC 2026 - José Iván López González + +- Fix JSON of DASD format summary (gh#agama-project/agama#3128). + ------------------------------------------------------------------- Fri Feb 6 15:25:36 UTC 2026 - José Iván López González diff --git a/service/test/agama/dbus/storage/dasd_test.rb b/service/test/agama/dbus/storage/dasd_test.rb index df5df11957..2d723128ec 100644 --- a/service/test/agama/dbus/storage/dasd_test.rb +++ b/service/test/agama/dbus/storage/dasd_test.rb @@ -215,7 +215,7 @@ { channel: "0.0.0100", totalCylinders: 100, - FormattedCylinders: 10, + formattedCylinders: 10, finished: false } ]