From 8dd1ac73621860ca2b612fa651e7a222aaf1ed42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sun, 9 Nov 2025 09:41:19 +0000 Subject: [PATCH 01/10] Unify packages and patterns in SoftwareState * Introduce a new Resolvable type which contains a name and a type. --- rust/agama-software/src/lib.rs | 2 +- rust/agama-software/src/model.rs | 10 +- rust/agama-software/src/model/packages.rs | 16 +++ rust/agama-software/src/model/state.rs | 113 ++++++++++++++-------- rust/agama-software/src/zypp_server.rs | 15 ++- 5 files changed, 97 insertions(+), 59 deletions(-) diff --git a/rust/agama-software/src/lib.rs b/rust/agama-software/src/lib.rs index bdf7cf31ad..f38992faf5 100644 --- a/rust/agama-software/src/lib.rs +++ b/rust/agama-software/src/lib.rs @@ -42,7 +42,7 @@ pub mod service; pub use service::Service; mod model; -pub use model::{Model, ModelAdapter}; +pub use model::{Model, ModelAdapter, Resolvable, ResolvableType}; mod event; pub use event::Event; diff --git a/rust/agama-software/src/model.rs b/rust/agama-software/src/model.rs index 0ca05c9a1f..6975497a44 100644 --- a/rust/agama-software/src/model.rs +++ b/rust/agama-software/src/model.rs @@ -30,13 +30,7 @@ use agama_utils::{ use async_trait::async_trait; use tokio::sync::{mpsc, oneshot}; -use crate::{ - model::{ - packages::ResolvableType, software_selection::SoftwareSelection, state::SoftwareState, - }, - service, - zypp_server::SoftwareAction, -}; +use crate::{model::state::SoftwareState, service, zypp_server::SoftwareAction}; pub mod conflict; pub mod packages; @@ -44,6 +38,8 @@ pub mod registration; pub mod software_selection; pub mod state; +pub use packages::{Resolvable, ResolvableType}; + /// Abstract the software-related configuration from the underlying system. /// /// It offers an API to query and set different software and product elements of a diff --git a/rust/agama-software/src/model/packages.rs b/rust/agama-software/src/model/packages.rs index 1c2df708e7..67b7533a4a 100644 --- a/rust/agama-software/src/model/packages.rs +++ b/rust/agama-software/src/model/packages.rs @@ -20,6 +20,22 @@ use serde::{Deserialize, Serialize}; +/// Represents a software resolvable. +#[derive(Debug, Deserialize, PartialEq)] +pub struct Resolvable { + pub name: String, + pub r#type: ResolvableType, +} + +impl Resolvable { + pub fn new(name: &str, r#type: ResolvableType) -> Self { + Self { + name: name.to_string(), + r#type, + } + } +} + /// Software resolvable type (package or pattern). #[derive( Clone, Copy, Debug, Deserialize, Serialize, strum::Display, utoipa::ToSchema, PartialEq, diff --git a/rust/agama-software/src/model/state.rs b/rust/agama-software/src/model/state.rs index 513b41bcfd..def8ab9566 100644 --- a/rust/agama-software/src/model/state.rs +++ b/rust/agama-software/src/model/state.rs @@ -27,22 +27,30 @@ use agama_utils::{ products::{ProductSpec, UserPattern}, }; +use crate::{Resolvable, ResolvableType}; + /// Represents the wanted software configuration. /// /// It includes the list of repositories, selected resolvables, configuration /// options, etc. This configuration is later applied by a model adapter. +/// +/// The SoftwareState is built by the [SoftwareStateBuilder] using different +/// sources (the product specification, the user configuration, etc.). #[derive(Debug)] pub struct SoftwareState { pub product: String, pub repositories: Vec, - // TODO: consider implementing a list to make easier working with them. - pub patterns: Vec, - pub packages: Vec, + pub resolvables: Vec, pub options: SoftwareOptions, } -/// Builder to create a [SoftwareState] struct from the other sources like the -/// product specification, the user configuration, etc. +/// Builder to create a [SoftwareState] struct from different sources. +/// +/// At this point it uses the following sources: +/// +/// * [Product specification](ProductSpec). +/// * [Software user configuration](Config). +/// * [System information](agama_utils::api::software::SystemInfo). pub struct SoftwareStateBuilder<'a> { product: &'a ProductSpec, config: Option<&'a Config>, @@ -86,6 +94,10 @@ impl<'a> SoftwareStateBuilder<'a> { state } + /// Adds the elements from the underlying system. + /// + /// It searches for repositories in the underlying system. The idea is to + /// use the repositories for off-line installation. fn add_system_config(&self, state: &mut SoftwareState, system: &SystemInfo) { let repositories = system .repositories @@ -95,6 +107,7 @@ impl<'a> SoftwareStateBuilder<'a> { state.repositories.extend(repositories); } + /// Adds the elements from the user configuration. fn add_user_config(&self, state: &mut SoftwareState, config: &Config) { let Some(software) = &config.software else { return; @@ -108,24 +121,28 @@ impl<'a> SoftwareStateBuilder<'a> { if let Some(patterns) = &software.patterns { match patterns { PatternsConfig::PatternsList(list) => { - state.patterns.retain(|p| p.optional == false); - state - .patterns - .extend(list.iter().map(|n| ResolvableName::new(n, false))); + // Replaces the list, keeping only the non-optional elements. + state.resolvables.retain(|p| p.optional == false); + state.resolvables.extend( + list.iter() + .map(|n| ResolvableState::new(n, ResolvableType::Pattern, false)), + ); } PatternsConfig::PatternsMap(map) => { + // Adds or removes elements to the list if let Some(add) = &map.add { - state - .patterns - .extend(add.iter().map(|n| ResolvableName::new(n, false))); + state.resolvables.extend( + add.iter() + .map(|n| ResolvableState::new(n, ResolvableType::Pattern, false)), + ); } if let Some(remove) = &map.remove { // NOTE: should we notify when a user wants to remove a // pattern which is not optional? state - .patterns - .retain(|p| !(p.optional && remove.contains(&p.name))); + .resolvables + .retain(|p| !(p.optional && remove.contains(&p.resolvable.name))); } } } @@ -153,24 +170,28 @@ impl<'a> SoftwareStateBuilder<'a> { }) .collect(); - let mut patterns: Vec = software + let mut resolvables: Vec = software .mandatory_patterns .iter() - .map(|p| ResolvableName::new(p, false)) + .map(|p| ResolvableState::new(p, ResolvableType::Pattern, false)) .collect(); - patterns.extend( + resolvables.extend( software .optional_patterns .iter() - .map(|p| ResolvableName::new(p, true)), + .map(|p| ResolvableState::new(p, ResolvableType::Pattern, true)), ); - patterns.extend(software.user_patterns.iter().filter_map(|p| match p { + resolvables.extend(software.user_patterns.iter().filter_map(|p| match p { UserPattern::Plain(_) => None, UserPattern::Preselected(pattern) => { if pattern.selected { - Some(ResolvableName::new(&pattern.name, true)) + Some(ResolvableState::new( + &pattern.name, + ResolvableType::Pattern, + true, + )) } else { None } @@ -180,8 +201,7 @@ impl<'a> SoftwareStateBuilder<'a> { SoftwareState { product: software.base_product.clone(), repositories, - patterns, - packages: vec![], + resolvables, options: Default::default(), } } @@ -230,17 +250,17 @@ impl From<&agama_utils::api::software::Repository> for Repository { /// Defines a resolvable to be selected. #[derive(Debug, PartialEq)] -pub struct ResolvableName { +pub struct ResolvableState { /// Resolvable name. - pub name: String, + pub resolvable: Resolvable, /// Whether this resolvable is optional or not. pub optional: bool, } -impl ResolvableName { - pub fn new(name: &str, optional: bool) -> Self { +impl ResolvableState { + pub fn new(name: &str, r#type: ResolvableType, optional: bool) -> Self { Self { - name: name.to_string(), + resolvable: Resolvable::new(name, r#type), optional, } } @@ -265,7 +285,10 @@ mod tests { products::ProductSpec, }; - use crate::model::state::{ResolvableName, SoftwareStateBuilder}; + use crate::model::{ + packages::ResolvableType, + state::{ResolvableState, SoftwareStateBuilder}, + }; fn build_user_config(patterns: Option) -> Config { let repo = RepositoryConfig { @@ -318,10 +341,10 @@ mod tests { assert_eq!(state.product, "openSUSE".to_string()); assert_eq!( - state.patterns, + state.resolvables, vec![ - ResolvableName::new("enhanced_base", false), - ResolvableName::new("selinux", true), + ResolvableState::new("enhanced_base", ResolvableType::Pattern, false), + ResolvableState::new("selinux", ResolvableType::Pattern, true), ] ); } @@ -358,11 +381,11 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.patterns, + state.resolvables, vec![ - ResolvableName::new("enhanced_base", false), - ResolvableName::new("selinux", true), - ResolvableName::new("gnome", false) + ResolvableState::new("enhanced_base", ResolvableType::Pattern, false), + ResolvableState::new("selinux", ResolvableType::Pattern, true), + ResolvableState::new("gnome", ResolvableType::Pattern, false) ] ); } @@ -380,8 +403,12 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.patterns, - vec![ResolvableName::new("enhanced_base", false),] + state.resolvables, + vec![ResolvableState::new( + "enhanced_base", + ResolvableType::Pattern, + false + ),] ); } @@ -398,10 +425,10 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.patterns, + state.resolvables, vec![ - ResolvableName::new("enhanced_base", false), - ResolvableName::new("selinux", true) + ResolvableState::new("enhanced_base", ResolvableType::Pattern, false), + ResolvableState::new("selinux", ResolvableType::Pattern, true) ] ); } @@ -416,10 +443,10 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.patterns, + state.resolvables, vec![ - ResolvableName::new("enhanced_base", false), - ResolvableName::new("gnome", false) + ResolvableState::new("enhanced_base", ResolvableType::Pattern, false), + ResolvableState::new("gnome", ResolvableType::Pattern, false) ] ); } diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index ff741496e8..a704aab55c 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -252,8 +252,7 @@ impl ZyppServer { fn read(&self, zypp: &zypp_agama::Zypp) -> Result { let repositories = zypp - .list_repositories() - .unwrap() + .list_repositories()? .into_iter() .map(|repo| state::Repository { name: repo.user_name, @@ -267,8 +266,7 @@ impl ZyppServer { // FIXME: read the real product. product: "SLES".to_string(), repositories, - patterns: vec![], - packages: vec![], + resolvables: vec![], options: Default::default(), }; Ok(state) @@ -365,17 +363,18 @@ impl ZyppServer { } _ = progress.cast(progress::message::Next::new(Scope::Software)); - for pattern in &state.patterns { + for resolvable_state in &state.resolvables { + let resolvable = &resolvable_state.resolvable; // FIXME: we need to distinguish who is selecting the pattern. // and register an issue if it is not found and it was not optional. let result = zypp.select_resolvable( - &pattern.name, - zypp_agama::ResolvableKind::Pattern, + &resolvable.name, + resolvable.r#type.into(), zypp_agama::ResolvableSelected::Installation, ); if let Err(error) = result { - let message = format!("Could not select pattern '{}'", &pattern.name); + let message = format!("Could not select pattern '{}'", &resolvable.name); issues.push( Issue::new("software.select_pattern", &message, IssueSeverity::Error) .with_details(&error.to_string()), From 1697aae44a3ea770d8f90a026a2381a5cd535369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sun, 9 Nov 2025 12:33:37 +0000 Subject: [PATCH 02/10] Set the product on PATCH /config requests. --- rust/agama-manager/src/service.rs | 74 +++++++++++++++++++------------ 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 452be1de62..faf811f291 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -112,6 +112,7 @@ impl Service { /// If a default product is set, it asks the other services to initialize their configurations. pub async fn setup(&mut self) -> Result<(), Error> { self.read_registries().await?; + if let Some(product) = self.products.default_product() { let product = Arc::new(RwLock::new(product.clone())); _ = self.software.cast(software::message::SetConfig::new( @@ -119,9 +120,9 @@ impl Service { None, )); self.product = Some(product); - } else { - self.notify_no_product() } + + self.update_issues(); Ok(()) } @@ -173,15 +174,34 @@ impl Service { Ok(()) } - fn notify_no_product(&self) { - let issue = Issue::new( - "no_product", - "No product has been selected.", - IssueSeverity::Error, - ); - _ = self - .issues - .cast(issue::message::Set::new(Scope::Manager, vec![issue])); + fn set_product_from_config(&mut self, config: &Config) { + let product_id = config + .software + .as_ref() + .and_then(|s| s.product.as_ref()) + .and_then(|p| p.id.as_ref()); + + if let Some(id) = product_id { + if let Some(product_spec) = self.products.find(&id) { + let product = RwLock::new(product_spec.clone()); + self.product = Some(Arc::new(product)); + } + } + } + + fn update_issues(&self) { + if self.product.is_some() { + _ = self.issues.cast(issue::message::Clear::new(Scope::Manager)); + } else { + let issue = Issue::new( + "no_product", + "No product has been selected.", + IssueSeverity::Error, + ); + _ = self + .issues + .cast(issue::message::Set::new(Scope::Manager, vec![issue])); + } } } @@ -248,22 +268,8 @@ impl MessageHandler for Service { #[async_trait] impl MessageHandler for Service { /// Sets the user configuration with the given values. - /// Sets the config. async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> { - let product_id = message - .config - .software - .as_ref() - .and_then(|s| s.product.as_ref()) - .and_then(|p| p.id.as_ref()); - - if let Some(id) = product_id { - if let Some(product_spec) = self.products.find(&id) { - let product = RwLock::new(product_spec.clone()); - self.product = Some(Arc::new(product)); - _ = self.issues.cast(issue::message::Clear::new(Scope::Manager)); - } - } + self.set_product_from_config(&message.config); self.config = message.config.clone(); let config = message.config; @@ -279,8 +285,6 @@ impl MessageHandler for Service { config.software.clone(), )) .await?; - } else { - self.notify_no_product(); } self.l10n @@ -291,6 +295,7 @@ impl MessageHandler for Service { .call(storage::message::SetConfig::new(config.storage.clone())) .await?; + self.update_issues(); Ok(()) } } @@ -303,6 +308,7 @@ impl MessageHandler for Service { /// config, then it keeps the values from the current config. async fn handle(&mut self, message: message::UpdateConfig) -> Result<(), Error> { let config = merge(&self.config, &message.config).map_err(|_| Error::MergeConfig)?; + self.set_product_from_config(&config); if let Some(l10n) = &config.l10n { self.l10n @@ -322,7 +328,19 @@ impl MessageHandler for Service { .await?; } + if let Some(product) = &self.product { + if let Some(software) = &config.software { + self.software + .call(software::message::SetConfig::with( + Arc::clone(&product), + software.clone(), + )) + .await?; + } + } + self.config = config; + self.update_issues(); Ok(()) } } From 0786e4b8992a95dff528b5b53c4cea4626de6847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sun, 9 Nov 2025 12:35:17 +0000 Subject: [PATCH 03/10] Add support to specify a list of resolvables * It is an private API for other Agama services to ask for the installation of a given resolvable. --- rust/agama-manager/src/message.rs | 1 + rust/agama-manager/src/service.rs | 10 ++++ rust/agama-server/src/server/web.rs | 29 +++++++++- rust/agama-software/src/message.rs | 25 +++++++++ rust/agama-software/src/model.rs | 33 ------------ rust/agama-software/src/model/packages.rs | 3 +- .../src/model/software_selection.rs | 54 ++++++------------- rust/agama-software/src/model/state.rs | 39 ++++++++++++-- rust/agama-software/src/service.rs | 22 +++++++- 9 files changed, 136 insertions(+), 80 deletions(-) diff --git a/rust/agama-manager/src/message.rs b/rust/agama-manager/src/message.rs index 83676f56a6..2ee9bcecf1 100644 --- a/rust/agama-manager/src/message.rs +++ b/rust/agama-manager/src/message.rs @@ -18,6 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +use agama_software::Resolvable; use agama_utils::{ actor::Message, api::{Action, Config, IssueMap, Proposal, Status, SystemInfo}, diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index faf811f291..284c673e16 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -408,3 +408,13 @@ impl MessageHandler for Service { .await?) } } + +// FIXME: write a macro to forward a message. +#[async_trait] +impl MessageHandler for Service { + /// It sets the software resolvables. + async fn handle(&mut self, message: software::message::SetResolvables) -> Result<(), Error> { + self.software.call(message).await?; + Ok(()) + } +} diff --git a/rust/agama-server/src/server/web.rs b/rust/agama-server/src/server/web.rs index 389b686cc7..ada38182cc 100644 --- a/rust/agama-server/src/server/web.rs +++ b/rust/agama-server/src/server/web.rs @@ -23,6 +23,7 @@ use crate::server::config_schema; use agama_lib::error::ServiceError; use agama_manager::{self as manager, message}; +use agama_software::Resolvable; use agama_utils::{ actor::Handler, api::{ @@ -33,9 +34,9 @@ use agama_utils::{ question, }; use axum::{ - extract::State, + extract::{Path, State}, response::{IntoResponse, Response}, - routing::{get, post}, + routing::{get, post, put}, Json, Router, }; use hyper::StatusCode; @@ -109,6 +110,7 @@ pub async fn server_service( "/private/storage_model", get(get_storage_model).put(set_storage_model), ) + .route("/private/resolvables/:id", put(set_resolvables)) .with_state(state)) } @@ -378,6 +380,29 @@ async fn set_storage_model( Ok(()) } +#[utoipa::path( + put, + path = "/resolvables/:id", + context_path = "/api/v2", + responses( + (status = 200, description = "The resolvables list was updated.") + ) +)] +async fn set_resolvables( + State(state): State, + Path(id): Path, + Json(resolvables): Json>, +) -> ServerResult<()> { + state + .manager + .call(agama_software::message::SetResolvables::new( + id, + resolvables, + )) + .await?; + Ok(()) +} + fn to_option_response(value: Option) -> Response { match value { Some(inner) => Json(inner).into_response(), diff --git a/rust/agama-software/src/message.rs b/rust/agama-software/src/message.rs index 2d468bb389..fbe68f6521 100644 --- a/rust/agama-software/src/message.rs +++ b/rust/agama-software/src/message.rs @@ -26,6 +26,8 @@ use agama_utils::{ use std::sync::Arc; use tokio::sync::RwLock; +use crate::Resolvable; + #[derive(Clone)] pub struct GetSystem; @@ -66,6 +68,13 @@ impl SetConfig { pub fn new(product: Arc>, config: Option) -> Self { Self { config, product } } + + pub fn with(product: Arc>, config: T) -> Self { + Self { + config: Some(config), + product, + } + } } pub struct GetProposal; @@ -91,3 +100,19 @@ pub struct Finish; impl Message for Finish { type Reply = (); } + +// Sets a resolvables list +pub struct SetResolvables { + pub id: String, + pub resolvables: Vec, +} + +impl SetResolvables { + pub fn new(id: String, resolvables: Vec) -> Self { + Self { id, resolvables } + } +} + +impl Message for SetResolvables { + type Reply = (); +} diff --git a/rust/agama-software/src/model.rs b/rust/agama-software/src/model.rs index 6975497a44..386a55bc04 100644 --- a/rust/agama-software/src/model.rs +++ b/rust/agama-software/src/model.rs @@ -50,18 +50,6 @@ pub trait ModelAdapter: Send + Sync + 'static { /// List of available patterns. async fn patterns(&self) -> Result, service::Error>; - /// Gets resolvables set for given combination of id, type and optional flag - fn get_resolvables(&self, id: &str, r#type: ResolvableType, optional: bool) -> Vec; - - /// Sets resolvables set for given combination of id, type and optional flag - async fn set_resolvables( - &mut self, - id: &str, - r#type: ResolvableType, - resolvables: Vec, - optional: bool, - ) -> Result<(), service::Error>; - async fn compute_proposal(&self) -> Result; /// Refresh repositories information. @@ -91,7 +79,6 @@ pub struct Model { zypp_sender: mpsc::UnboundedSender, // FIXME: what about having a SoftwareServiceState to keep business logic state? selected_product: Option, - software_selection: SoftwareSelection, } impl Model { @@ -100,7 +87,6 @@ impl Model { Ok(Self { zypp_sender, selected_product: None, - software_selection: SoftwareSelection::default(), }) } } @@ -146,29 +132,10 @@ impl ModelAdapter for Model { Ok(rx.await??) } - fn get_resolvables(&self, id: &str, r#type: ResolvableType, optional: bool) -> Vec { - self.software_selection - .get(id, r#type, optional) - .unwrap_or_default() - } - async fn refresh(&mut self) -> Result<(), service::Error> { unimplemented!() } - async fn set_resolvables( - &mut self, - id: &str, - r#type: ResolvableType, - resolvables: Vec, - optional: bool, - ) -> Result<(), service::Error> { - self.software_selection - .set(&self.zypp_sender, id, r#type, optional, resolvables) - .await?; - Ok(()) - } - async fn finish(&self) -> Result<(), service::Error> { let (tx, rx) = oneshot::channel(); self.zypp_sender.send(SoftwareAction::Finish(tx))?; diff --git a/rust/agama-software/src/model/packages.rs b/rust/agama-software/src/model/packages.rs index 67b7533a4a..39b923b9cb 100644 --- a/rust/agama-software/src/model/packages.rs +++ b/rust/agama-software/src/model/packages.rs @@ -21,9 +21,10 @@ use serde::{Deserialize, Serialize}; /// Represents a software resolvable. -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq, utoipa::ToSchema)] pub struct Resolvable { pub name: String, + #[serde(rename = "type")] pub r#type: ResolvableType, } diff --git a/rust/agama-software/src/model/software_selection.rs b/rust/agama-software/src/model/software_selection.rs index 2913a2a1b1..224db13224 100644 --- a/rust/agama-software/src/model/software_selection.rs +++ b/rust/agama-software/src/model/software_selection.rs @@ -18,15 +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 tokio::sync::{mpsc, oneshot}; - -use crate::{model::packages::ResolvableType, service, zypp_server::SoftwareAction}; +use crate::{service, Resolvable}; pub struct ResolvablesSelection { id: String, optional: bool, - resolvables: Vec, - r#type: ResolvableType, + resolvables: Vec, } /// A selection of resolvables to be installed. @@ -41,39 +38,17 @@ pub struct SoftwareSelection { impl SoftwareSelection { /// Updates a set of resolvables. /// - /// * `zypp` - pointer to message bus to zypp thread to do real action /// * `id` - The id of the set. - /// * `r#type` - The type of the resolvables (patterns or packages). /// * `optional` - Whether the selection is optional or not. /// * `resolvables` - The resolvables included in the set. pub async fn set( &mut self, - zypp: &mpsc::UnboundedSender, id: &str, - r#type: ResolvableType, optional: bool, - resolvables: Vec, + resolvables: Vec, ) -> Result<(), service::Error> { - let list = self.find_or_create_selection(id, r#type, optional); - // FIXME: use reference counting here, if multiple ids require some package, to not unselect it - let (tx, rx) = oneshot::channel(); - zypp.send(SoftwareAction::UnsetResolvables { - tx, - resolvables: list.resolvables.clone(), - r#type: r#type.into(), - optional, - })?; - rx.await??; - + let list = self.find_or_create_selection(id, optional); list.resolvables = resolvables; - let (tx, rx) = oneshot::channel(); - zypp.send(SoftwareAction::UnsetResolvables { - tx, - resolvables: list.resolvables.clone(), - r#type: r#type.into(), - optional, - })?; - rx.await??; Ok(()) } @@ -82,30 +57,31 @@ impl SoftwareSelection { /// * `id` - The id of the set. /// * `r#type` - The type of the resolvables (patterns or packages). /// * `optional` - Whether the selection is optional or not. - pub fn get(&self, id: &str, r#type: ResolvableType, optional: bool) -> Option> { + pub fn get(&self, id: &str, optional: bool) -> Option> { self.selections .iter() - .find(|l| l.id == id && l.r#type == r#type && l.optional == optional) + .find(|l| l.id == id && l.optional == optional) .map(|l| l.resolvables.clone()) } - fn find_or_create_selection( - &mut self, - id: &str, - r#type: ResolvableType, - optional: bool, - ) -> &mut ResolvablesSelection { + pub fn resolvables<'a>(&'a self) -> impl Iterator + 'a { + self.selections + .iter() + .map(|s| s.resolvables.clone()) + .flatten() + } + + fn find_or_create_selection(&mut self, id: &str, optional: bool) -> &mut ResolvablesSelection { let found = self .selections .iter() - .position(|l| l.id == id && l.r#type == r#type && l.optional == optional); + .position(|l| l.id == id && l.optional == optional); if let Some(index) = found { &mut self.selections[index] } else { let selection = ResolvablesSelection { id: id.to_string(), - r#type, optional, resolvables: vec![], }; diff --git a/rust/agama-software/src/model/state.rs b/rust/agama-software/src/model/state.rs index def8ab9566..03022fa9a5 100644 --- a/rust/agama-software/src/model/state.rs +++ b/rust/agama-software/src/model/state.rs @@ -27,7 +27,7 @@ use agama_utils::{ products::{ProductSpec, UserPattern}, }; -use crate::{Resolvable, ResolvableType}; +use crate::{model::software_selection::SoftwareSelection, Resolvable, ResolvableType}; /// Represents the wanted software configuration. /// @@ -52,9 +52,14 @@ pub struct SoftwareState { /// * [Software user configuration](Config). /// * [System information](agama_utils::api::software::SystemInfo). pub struct SoftwareStateBuilder<'a> { + /// Product specification. product: &'a ProductSpec, + /// Configuration. config: Option<&'a Config>, + /// Information from the underlying system. system: Option<&'a SystemInfo>, + /// Agama's software selection. + selection: Option<&'a SoftwareSelection>, } impl<'a> SoftwareStateBuilder<'a> { @@ -64,6 +69,7 @@ impl<'a> SoftwareStateBuilder<'a> { product, config: None, system: None, + selection: None, } } @@ -78,6 +84,11 @@ impl<'a> SoftwareStateBuilder<'a> { self } + pub fn with_selection(mut self, selection: &'a SoftwareSelection) -> Self { + self.selection = Some(selection); + self + } + /// Builds the [SoftwareState] by merging the product specification and the /// user configuration. pub fn build(self) -> SoftwareState { @@ -91,6 +102,10 @@ impl<'a> SoftwareStateBuilder<'a> { self.add_user_config(&mut state, config); } + if let Some(selection) = self.selection { + self.add_selection(&mut state, selection); + } + state } @@ -153,6 +168,14 @@ impl<'a> SoftwareStateBuilder<'a> { } } + /// It adds the software selection from Agama modules. + fn add_selection(&self, state: &mut SoftwareState, selection: &SoftwareSelection) { + let resolvables = selection + .resolvables() + .map(|r| ResolvableState::new_with_resolvable(&r, false)); + state.resolvables.extend(resolvables) + } + fn from_product_spec(&self) -> SoftwareState { let software = &self.product.software; let repositories = software @@ -209,10 +232,16 @@ impl<'a> SoftwareStateBuilder<'a> { impl SoftwareState { // TODO: Add SoftwareSelection as additional argument. - pub fn build_from(product: &ProductSpec, config: &Config, system: &SystemInfo) -> Self { + pub fn build_from( + product: &ProductSpec, + config: &Config, + system: &SystemInfo, + selection: &SoftwareSelection, + ) -> Self { SoftwareStateBuilder::for_product(product) .with_config(config) .with_system(system) + .with_selection(selection) .build() } } @@ -259,8 +288,12 @@ pub struct ResolvableState { impl ResolvableState { pub fn new(name: &str, r#type: ResolvableType, optional: bool) -> Self { + Self::new_with_resolvable(&Resolvable::new(name, r#type), optional) + } + + pub fn new_with_resolvable(resolvable: &Resolvable, optional: bool) -> Self { Self { - resolvable: Resolvable::new(name, r#type), + resolvable: resolvable.clone(), optional, } } diff --git a/rust/agama-software/src/service.rs b/rust/agama-software/src/service.rs index 087fac9cf3..bc18f67e84 100644 --- a/rust/agama-software/src/service.rs +++ b/rust/agama-software/src/service.rs @@ -22,7 +22,7 @@ use std::{process::Command, sync::Arc}; use crate::{ message, - model::{state::SoftwareState, ModelAdapter}, + model::{software_selection::SoftwareSelection, state::SoftwareState, ModelAdapter}, zypp_server::{self, SoftwareAction}, }; use agama_utils::{ @@ -77,6 +77,7 @@ pub struct Service { progress: Handler, events: event::Sender, state: State, + selection: SoftwareSelection, } #[derive(Default)] @@ -99,6 +100,7 @@ impl Service { progress, events, state: Default::default(), + selection: Default::default(), } } @@ -148,7 +150,13 @@ impl MessageHandler> for Service { scope: Scope::Software, })?; - let software = SoftwareState::build_from(&product, &self.state.config, &self.state.system); + let software = SoftwareState::build_from( + &product, + &self.state.config, + &self.state.system, + &self.selection, + ); + tracing::info!("Wanted software state: {software:?}"); let model = self.model.clone(); let issues = self.issues.clone(); @@ -225,6 +233,16 @@ impl MessageHandler for Service { } } +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::SetResolvables) -> Result<(), Error> { + self.selection + .set(&message.id, false, message.resolvables) + .await?; + Ok(()) + } +} + const LIVE_REPO_DIR: &str = "/run/initramfs/live/install"; fn find_install_repository() -> Option { From 9ab57c149c07e59e4e7c0bdd6f245a4bca69b00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sun, 9 Nov 2025 14:01:17 +0000 Subject: [PATCH 04/10] Simplify the SoftwareSelection struct * Use a HashMap to keep the resolvables lists. * Remove the "optional" argument because it is not used. * Re-enable and fix the tests. --- .../src/model/software_selection.rs | 92 +++++++------------ rust/agama-software/src/service.rs | 4 +- 2 files changed, 32 insertions(+), 64 deletions(-) diff --git a/rust/agama-software/src/model/software_selection.rs b/rust/agama-software/src/model/software_selection.rs index 224db13224..218206f241 100644 --- a/rust/agama-software/src/model/software_selection.rs +++ b/rust/agama-software/src/model/software_selection.rs @@ -18,7 +18,9 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::{service, Resolvable}; +use std::collections::HashMap; + +use crate::Resolvable; pub struct ResolvablesSelection { id: String, @@ -31,9 +33,7 @@ pub struct ResolvablesSelection { /// It holds a selection of patterns and packages to be installed and whether they are optional or /// not. This class is similar to the `PackagesProposal` YaST module. #[derive(Default)] -pub struct SoftwareSelection { - selections: Vec, -} +pub struct SoftwareSelection(HashMap>); impl SoftwareSelection { /// Updates a set of resolvables. @@ -41,80 +41,50 @@ impl SoftwareSelection { /// * `id` - The id of the set. /// * `optional` - Whether the selection is optional or not. /// * `resolvables` - The resolvables included in the set. - pub async fn set( - &mut self, - id: &str, - optional: bool, - resolvables: Vec, - ) -> Result<(), service::Error> { - let list = self.find_or_create_selection(id, optional); - list.resolvables = resolvables; - Ok(()) + pub fn set(&mut self, id: &str, resolvables: Vec) { + self.0.insert(id.to_string(), resolvables); } - /// Returns a set of resolvables. - /// - /// * `id` - The id of the set. - /// * `r#type` - The type of the resolvables (patterns or packages). - /// * `optional` - Whether the selection is optional or not. - pub fn get(&self, id: &str, optional: bool) -> Option> { - self.selections - .iter() - .find(|l| l.id == id && l.optional == optional) - .map(|l| l.resolvables.clone()) + /// Remove the selection list with the given ID. + pub fn remove(&mut self, id: &str) { + self.0.remove(id); } + /// Returns all the resolvables. pub fn resolvables<'a>(&'a self) -> impl Iterator + 'a { - self.selections - .iter() - .map(|s| s.resolvables.clone()) - .flatten() - } - - fn find_or_create_selection(&mut self, id: &str, optional: bool) -> &mut ResolvablesSelection { - let found = self - .selections - .iter() - .position(|l| l.id == id && l.optional == optional); - - if let Some(index) = found { - &mut self.selections[index] - } else { - let selection = ResolvablesSelection { - id: id.to_string(), - optional, - resolvables: vec![], - }; - self.selections.push(selection); - self.selections.last_mut().unwrap() - } + self.0.values().flatten().cloned() } } -/* TODO: Fix tests with real mock of libzypp #[cfg(test)] mod tests { - use super::*; + use crate::ResolvableType; + + use super::{Resolvable, SoftwareSelection}; #[test] fn test_set_selection() { - let mut selection = SoftwareSelection::new(); - selection.add("agama", ResolvableType::Package, false, &["agama-scripts"]); - selection.set("agama", ResolvableType::Package, false, &["suse"]); + let mut selection = SoftwareSelection::default(); + let resolvable = Resolvable::new("agama-scripts", ResolvableType::Package); + selection.set("agama", vec![resolvable]); + let resolvable = Resolvable::new("btrfsprogs", ResolvableType::Pattern); + selection.set("software", vec![resolvable]); - let packages = selection - .get("agama", ResolvableType::Package, false) - .unwrap(); - assert_eq!(packages.len(), 1); + let all_resolvables: Vec<_> = selection.resolvables().collect(); + assert_eq!(all_resolvables.len(), 2); } #[test] fn test_remove_selection() { - let mut selection = SoftwareSelection::new(); - selection.add("agama", ResolvableType::Package, true, &["agama-scripts"]); - selection.remove("agama", ResolvableType::Package, true); - let packages = selection.get("agama", ResolvableType::Package, true); - assert_eq!(packages, None); + let mut selection = SoftwareSelection::default(); + let resolvable = Resolvable::new("agama-scripts", ResolvableType::Package); + selection.set("agama", vec![resolvable]); + + let all_resolvables: Vec<_> = selection.resolvables().collect(); + assert_eq!(all_resolvables.len(), 1); + + selection.remove("agama"); + let all_resolvables: Vec<_> = selection.resolvables().collect(); + assert!(all_resolvables.is_empty()); } } - */ diff --git a/rust/agama-software/src/service.rs b/rust/agama-software/src/service.rs index bc18f67e84..2cedf3c880 100644 --- a/rust/agama-software/src/service.rs +++ b/rust/agama-software/src/service.rs @@ -236,9 +236,7 @@ impl MessageHandler for Service { #[async_trait] impl MessageHandler for Service { async fn handle(&mut self, message: message::SetResolvables) -> Result<(), Error> { - self.selection - .set(&message.id, false, message.resolvables) - .await?; + self.selection.set(&message.id, message.resolvables); Ok(()) } } From 9f76751fd84ff08c6f1d5023c8207cf4eecf5f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sun, 9 Nov 2025 14:04:54 +0000 Subject: [PATCH 05/10] Drop unused code related to resolvables handling --- rust/agama-manager/src/message.rs | 1 - rust/agama-software/src/model/packages.rs | 11 ---- .../src/model/software_selection.rs | 6 -- rust/agama-software/src/zypp_server.rs | 57 +------------------ 4 files changed, 1 insertion(+), 74 deletions(-) diff --git a/rust/agama-manager/src/message.rs b/rust/agama-manager/src/message.rs index 2ee9bcecf1..83676f56a6 100644 --- a/rust/agama-manager/src/message.rs +++ b/rust/agama-manager/src/message.rs @@ -18,7 +18,6 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use agama_software::Resolvable; use agama_utils::{ actor::Message, api::{Action, Config, IssueMap, Proposal, Status, SystemInfo}, diff --git a/rust/agama-software/src/model/packages.rs b/rust/agama-software/src/model/packages.rs index 39b923b9cb..d04ff4c640 100644 --- a/rust/agama-software/src/model/packages.rs +++ b/rust/agama-software/src/model/packages.rs @@ -58,14 +58,3 @@ impl From for zypp_agama::ResolvableKind { } } } - -/// Resolvable list specification. -#[derive(Deserialize, Serialize, utoipa::ToSchema)] -pub struct ResolvableParams { - /// List of resolvables. - pub names: Vec, - /// Resolvable type. - pub r#type: ResolvableType, - /// Whether the resolvables are optional or not. - pub optional: bool, -} diff --git a/rust/agama-software/src/model/software_selection.rs b/rust/agama-software/src/model/software_selection.rs index 218206f241..2371dbd678 100644 --- a/rust/agama-software/src/model/software_selection.rs +++ b/rust/agama-software/src/model/software_selection.rs @@ -22,12 +22,6 @@ use std::collections::HashMap; use crate::Resolvable; -pub struct ResolvablesSelection { - id: String, - optional: bool, - resolvables: Vec, -} - /// A selection of resolvables to be installed. /// /// It holds a selection of patterns and packages to be installed and whether they are optional or diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index a704aab55c..63f7030828 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -34,10 +34,7 @@ use tokio::sync::{ }; use zypp_agama::ZyppError; -use crate::model::{ - packages::ResolvableType, - state::{self, SoftwareState}, -}; +use crate::model::state::{self, SoftwareState}; const TARGET_DIR: &str = "/run/agama/software_ng_zypp"; const GPG_KEYS: &str = "/usr/lib/rpm/gnupg/keys/gpg-*"; @@ -96,18 +93,6 @@ pub enum SoftwareAction { ProductSpec, oneshot::Sender>, ), - SetResolvables { - tx: oneshot::Sender>, - resolvables: Vec, - r#type: ResolvableType, - optional: bool, - }, - UnsetResolvables { - tx: oneshot::Sender>, - resolvables: Vec, - r#type: ResolvableType, - optional: bool, - }, Write { state: SoftwareState, progress: Handler, @@ -194,46 +179,6 @@ impl ZyppServer { SoftwareAction::Finish(tx) => { self.finish(zypp, tx).await?; } - SoftwareAction::SetResolvables { - tx, - r#type, - resolvables, - optional, - } => { - // TODO: support optional with check if resolvable is available - for res in resolvables { - let result = zypp.select_resolvable( - &res, - r#type.into(), - zypp_agama::ResolvableSelected::Installation, - ); - if let Err(e) = result { - tx.send(Err(e)) - .map_err(|_| ZyppDispatchError::ResponseChannelClosed)?; - break; - } - } - } - SoftwareAction::UnsetResolvables { - tx, - r#type, - resolvables, - optional, - } => { - // TODO: support optional with check if resolvable is available - for res in resolvables { - let result = zypp.unselect_resolvable( - &res, - r#type.into(), - zypp_agama::ResolvableSelected::Installation, - ); - if let Err(e) = result { - tx.send(Err(e)) - .map_err(|_| ZyppDispatchError::ResponseChannelClosed)?; - break; - } - } - } SoftwareAction::ComputeProposal(product_spec, sender) => { self.compute_proposal(product_spec, sender, zypp).await? } From 89a5b319c8537419e20d0664ef0b640cbe4f09d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sun, 9 Nov 2025 14:10:38 +0000 Subject: [PATCH 06/10] Remove the unused agama_software::events module --- rust/agama-software/src/event.rs | 40 -------------------------------- rust/agama-software/src/lib.rs | 3 --- 2 files changed, 43 deletions(-) delete mode 100644 rust/agama-software/src/event.rs diff --git a/rust/agama-software/src/event.rs b/rust/agama-software/src/event.rs deleted file mode 100644 index 7b750454b6..0000000000 --- a/rust/agama-software/src/event.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) [2025] SUSE LLC -// -// All Rights Reserved. -// -// This program is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by the Free -// Software Foundation; either version 2 of the License, or (at your option) -// any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -// more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, contact SUSE LLC. -// -// To contact SUSE LLC about this file by physical or electronic mail, you may -// find current contact information at www.suse.com. - -use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; - -/// Localization-related events. -// FIXME: is it really needed to implement Deserialize? -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "name")] -pub enum Event { - /// Proposal changed. - ProposalChanged, - /// The underlying system changed. - SystemChanged, - /// The use configuration changed. - ConfigChanged, -} - -/// Multi-producer single-consumer events sender. -pub type Sender = mpsc::UnboundedSender; -/// Multi-producer single-consumer events receiver. -pub type Receiver = mpsc::UnboundedReceiver; diff --git a/rust/agama-software/src/lib.rs b/rust/agama-software/src/lib.rs index f38992faf5..808d465c07 100644 --- a/rust/agama-software/src/lib.rs +++ b/rust/agama-software/src/lib.rs @@ -44,8 +44,5 @@ pub use service::Service; mod model; pub use model::{Model, ModelAdapter, Resolvable, ResolvableType}; -mod event; -pub use event::Event; - pub mod message; mod zypp_server; From 194a6717a3eea9c21f1fd0a25391988f8f310859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sun, 9 Nov 2025 14:32:10 +0000 Subject: [PATCH 07/10] Adapt PackagesProposal.SetResolvables to the new API --- .../y2dir/manager/modules/PackagesProposal.rb | 6 +-- service/lib/agama/http/clients/main.rb | 12 +++++ service/test/agama/http/clients/main_test.rb | 45 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 service/test/agama/http/clients/main_test.rb diff --git a/service/lib/agama/dbus/y2dir/manager/modules/PackagesProposal.rb b/service/lib/agama/dbus/y2dir/manager/modules/PackagesProposal.rb index ecb8d8a001..7d0ceeabce 100644 --- a/service/lib/agama/dbus/y2dir/manager/modules/PackagesProposal.rb +++ b/service/lib/agama/dbus/y2dir/manager/modules/PackagesProposal.rb @@ -18,7 +18,7 @@ # find current contact information at www.suse.com. require "yast" -require "agama/http/clients/software" +require "agama/http/clients/main" # :nodoc: module Yast @@ -26,7 +26,7 @@ module Yast class PackagesProposalClass < Module def main puts "Loading mocked module #{__FILE__}" - @client = Agama::HTTP::Clients::Software.new(::Logger.new($stdout)) + @client = Agama::HTTP::Clients::Main.new(::Logger.new($stdout)) end # @see https://github.com/yast/yast-yast2/blob/b8cd178b7f341f6e3438782cb703f4a3ab0529ed/library/general/src/modules/PackagesProposal.rb#L118 @@ -40,7 +40,7 @@ def AddResolvables(unique_id, type, resolvables, optional: false) # @see https://github.com/yast/yast-yast2/blob/b8cd178b7f341f6e3438782cb703f4a3ab0529ed/library/general/src/modules/PackagesProposal.rb#L145 def SetResolvables(unique_id, type, resolvables, optional: false) - client.set_resolvables(unique_id, type, resolvables || [], optional) + client.set_resolvables(unique_id, type, resolvables || []) true end diff --git a/service/lib/agama/http/clients/main.rb b/service/lib/agama/http/clients/main.rb index a6d70f5aec..600bef871c 100644 --- a/service/lib/agama/http/clients/main.rb +++ b/service/lib/agama/http/clients/main.rb @@ -29,6 +29,18 @@ class Main < Base def install post("v2/action", '"install"') end + + # Sets a list of resolvables for installation. + # + # @param unique_id [String] Unique ID to identify the list. + # @param type [String] Resolvable type (e.g., "package" or "pattern"). + # @param resolvables [Array] Resolvables names. + def set_resolvables(unique_id, type, resolvables) + data = resolvables.map do |name| + { "name" => name, "type" => type } + end + put("v2/private/resolvables/#{unique_id}", data) + end end end end diff --git a/service/test/agama/http/clients/main_test.rb b/service/test/agama/http/clients/main_test.rb new file mode 100644 index 0000000000..e1ddd668ca --- /dev/null +++ b/service/test/agama/http/clients/main_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../../test_helper" +require "agama/http/clients/main" + +describe Agama::HTTP::Clients::Main do + subject(:main) { described_class.new(Logger.new($stdout)) } + let(:response) { instance_double(Net::HTTPResponse, body: "") } + + before do + allow(File).to receive(:read).with("/run/agama/token") + .and_return("123456") + end + + describe "#set_resolvables" do + it "calls the end-point to set resolvables" do + url = URI("http://localhost/api/v2/private/resolvables/storage") + data = [{ "name" => "btrfsprogs", "type" => "package" }].to_json + expect(Net::HTTP).to receive(:put).with(url, data, { + "Content-Type": "application/json", + Authorization: "Bearer 123456" + }).and_return(response) + main.set_resolvables("storage", "package", ["btrfsprogs"]) + end + end +end From 0652b82b3d7cc7a28d7a36d51e4da34060c0aa9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 10 Nov 2025 07:14:42 +0000 Subject: [PATCH 08/10] Fix questions schema --- rust/agama-lib/share/profile.schema.json | 25 +++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 14ee57cfb8..071e1aac4b 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -1024,18 +1024,15 @@ "title": "Question class", "description": "Each question has a \"class\" which works as an identifier.", "type": "string", - "examples": ["storage.activate_multipath"] + "examples": [ + "storage.activate_multipath" + ] }, "text": { "title": "Question text", "description": "Question full text", "type": "string" }, - "answer": { - "title": "Question answer", - "description": "Answer to use for the question.", - "type": "string" - }, "password": { "title": "Password provided as response to a password-based question", "type": "string" @@ -1044,7 +1041,21 @@ "title": "Additional data for matching questions", "description": "Additional data for matching questions and answers", "type": "object", - "examples": [{ "device": "/dev/sda" }] + "examples": [ + { + "device": "/dev/sda" + } + ] + }, + "action": { + "title": "Predefined question action", + "description": "Action to use for the question.", + "type": "string" + }, + "value": { + "title": "Predefined question value", + "description": "Value to use for the question.", + "type": "string" } } }, From a096784b51bbbc933c2621597bf857cc0903abdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 10 Nov 2025 10:39:45 +0000 Subject: [PATCH 09/10] Log when the product is known --- rust/Cargo.lock | 1 + rust/agama-manager/Cargo.toml | 1 + rust/agama-manager/src/service.rs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a61972b45c..53011ea3ab 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -141,6 +141,7 @@ dependencies = [ "thiserror 2.0.16", "tokio", "tokio-test", + "tracing", "zbus", ] diff --git a/rust/agama-manager/Cargo.toml b/rust/agama-manager/Cargo.toml index 7900b723d9..61e507b0f0 100644 --- a/rust/agama-manager/Cargo.toml +++ b/rust/agama-manager/Cargo.toml @@ -15,6 +15,7 @@ async-trait = "0.1.83" zbus = { version = "5", default-features = false, features = ["tokio"] } merge-struct = "0.1.0" serde_json = "1.0.140" +tracing = "0.1.41" [dev-dependencies] tokio-test = "0.4.4" diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 284c673e16..aaa7524390 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -185,6 +185,8 @@ impl Service { if let Some(product_spec) = self.products.find(&id) { let product = RwLock::new(product_spec.clone()); self.product = Some(Arc::new(product)); + } else { + tracing::warn!("Unknown product '{id}'"); } } } From 073fcc97bb601397830305dbb1f61206b71422a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 10 Nov 2025 10:40:13 +0000 Subject: [PATCH 10/10] Update SoftwareStateBuilder documentation --- rust/agama-software/src/model/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/agama-software/src/model/state.rs b/rust/agama-software/src/model/state.rs index 03022fa9a5..f6a717661b 100644 --- a/rust/agama-software/src/model/state.rs +++ b/rust/agama-software/src/model/state.rs @@ -50,7 +50,8 @@ pub struct SoftwareState { /// /// * [Product specification](ProductSpec). /// * [Software user configuration](Config). -/// * [System information](agama_utils::api::software::SystemInfo). +/// * [System information](SystemInfo). +/// * [Agama software selection](SoftwareSelection). pub struct SoftwareStateBuilder<'a> { /// Product specification. product: &'a ProductSpec, @@ -231,7 +232,6 @@ impl<'a> SoftwareStateBuilder<'a> { } impl SoftwareState { - // TODO: Add SoftwareSelection as additional argument. pub fn build_from( product: &ProductSpec, config: &Config,