diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 36b07ed3a3..b201f0e9ae 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -284,6 +284,7 @@ dependencies = [ "serde_with", "serde_yaml", "strum", + "test-context", "thiserror 2.0.16", "tokio", "tokio-stream", diff --git a/rust/agama-l10n/src/lib.rs b/rust/agama-l10n/src/lib.rs index 25111d6fa2..c8885f69c0 100644 --- a/rust/agama-l10n/src/lib.rs +++ b/rust/agama-l10n/src/lib.rs @@ -60,7 +60,7 @@ mod tests { use agama_utils::{ actor::Handler, api::{self, event::Event, scope::Scope}, - issue, test, + issue, }; use test_context::{test_context, AsyncTestContext}; use tokio::sync::broadcast; @@ -74,8 +74,7 @@ mod tests { impl AsyncTestContext for Context { async fn setup() -> Context { let (events_tx, events_rx) = broadcast::channel::(16); - let dbus = test::dbus::connection().await.unwrap(); - let issues = issue::start(events_tx.clone(), dbus).await.unwrap(); + let issues = issue::Service::starter(events_tx.clone()).start(); let model = TestModel::with_sample_data(); let handler = Service::starter(events_tx, issues.clone()) diff --git a/rust/agama-l10n/src/service.rs b/rust/agama-l10n/src/service.rs index 663501e0c3..e81c1624f5 100644 --- a/rust/agama-l10n/src/service.rs +++ b/rust/agama-l10n/src/service.rs @@ -29,7 +29,7 @@ use agama_utils::{ self, event::{self, Event}, l10n::{Proposal, SystemConfig, SystemInfo}, - Issue, IssueSeverity, IssueSource, Scope, + Issue, Scope, }, issue, }; @@ -53,8 +53,6 @@ pub enum Error { #[error(transparent)] Event(#[from] broadcast::error::SendError), #[error(transparent)] - Issue(#[from] api::issue::Error), - #[error(transparent)] IssueService(#[from] issue::service::Error), #[error(transparent)] Actor(#[from] actor::Error), @@ -186,33 +184,24 @@ impl Service { let config = &self.config; let mut issues = vec![]; if !self.model.locales_db().exists(&config.locale) { - issues.push(Issue { - description: format!("Locale '{}' is unknown", &config.locale), - details: None, - source: IssueSource::Config, - severity: IssueSeverity::Error, - class: "unknown_locale".to_string(), - }); + issues.push(Issue::new( + "unknown_locale", + &format!("Locale '{}' is unknown", config.locale), + )); } if !self.model.keymaps_db().exists(&config.keymap) { - issues.push(Issue { - description: format!("Keymap '{}' is unknown", &config.keymap), - details: None, - source: IssueSource::Config, - severity: IssueSeverity::Error, - class: "unknown_keymap".to_string(), - }); + issues.push(Issue::new( + "unknown_keymap", + &format!("Keymap '{}' is unknown", config.keymap), + )); } if !self.model.timezones_db().exists(&config.timezone) { - issues.push(Issue { - description: format!("Timezone '{}' is unknown", &config.timezone), - details: None, - source: IssueSource::Config, - severity: IssueSeverity::Error, - class: "unknown_timezone".to_string(), - }); + issues.push(Issue::new( + "unknown_timezone", + &format!("Timezone '{}' is unknown", config.timezone), + )); } issues diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 7c830978c7..9b016de8a1 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -25,7 +25,7 @@ use agama_utils::{ self, event, manager::{self, LicenseContent}, status::State, - Action, Config, Event, Issue, IssueMap, IssueSeverity, Proposal, Scope, Status, SystemInfo, + Action, Config, Event, Issue, IssueMap, Proposal, Scope, Status, SystemInfo, }, issue, licenses, products::{self, ProductSpec}, @@ -137,7 +137,7 @@ impl Starter { pub async fn start(self) -> Result, Error> { let issues = match self.issues { Some(issues) => issues, - None => issue::start(self.events.clone(), self.dbus.clone()).await?, + None => issue::Service::starter(self.events.clone()).start(), }; let progress = match self.progress { @@ -245,7 +245,9 @@ impl Service { if let Some(product) = self.products.default_product() { let config = Config::with_product(product.id.clone()); self.set_config(config).await?; - } + } else { + self.update_issues()?; + }; Ok(()) } @@ -412,11 +414,7 @@ impl Service { self.issues .cast(issue::message::Clear::new(Scope::Manager))?; } else { - let issue = Issue::new( - "no_product", - "No product has been selected.", - IssueSeverity::Error, - ); + let issue = Issue::new("no_product", "No product has been selected."); self.issues .cast(issue::message::Set::new(Scope::Manager, vec![issue]))?; } diff --git a/rust/agama-manager/src/start.rs b/rust/agama-manager/src/start.rs deleted file mode 100644 index 631159939c..0000000000 --- a/rust/agama-manager/src/start.rs +++ /dev/null @@ -1,172 +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 crate::{l10n, network, service::Service, software, storage}; -use agama_utils::{ - actor::{self, Handler}, - api::event, - issue, progress, question, -}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Progress(#[from] progress::start::Error), - #[error(transparent)] - L10n(#[from] l10n::start::Error), - #[error(transparent)] - Manager(#[from] crate::service::Error), - #[error(transparent)] - Network(#[from] network::start::Error), - #[error(transparent)] - Software(#[from] software::start::Error), - #[error(transparent)] - Storage(#[from] storage::start::Error), - #[error(transparent)] - Issues(#[from] issue::start::Error), -} - -/// Starts the manager service. -/// -/// * `events`: channel to emit the [events](agama_utils::Event). -/// * `dbus`: connection to Agama's D-Bus server. If it is not given, those features -/// that require to connect to the Agama's D-Bus server won't work. -pub async fn start( - questions: Handler, - events: event::Sender, - dbus: zbus::Connection, -) -> Result, Error> { - let issues = issue::start(events.clone(), dbus.clone()).await?; - let progress = progress::start(events.clone()).await?; - let l10n = l10n::start(issues.clone(), events.clone()).await?; - let network = network::start().await?; - let software = software::start(issues.clone(), &progress, &questions, events.clone()).await?; - let storage = storage::start(progress.clone(), issues.clone(), events.clone(), dbus).await?; - - let mut service = Service::new( - l10n, - network, - software, - storage, - issues, - progress, - questions, - events.clone(), - ); - service.setup().await?; - - let handler = actor::spawn(service); - Ok(handler) -} - -#[cfg(test)] -mod test { - use crate::{self as manager, message, service::Service}; - use agama_utils::{ - actor::Handler, - api::{l10n, Config, Event}, - question, test, - }; - use std::path::PathBuf; - use tokio::sync::broadcast; - - async fn start_service() -> Handler { - let (events_sender, mut events_receiver) = broadcast::channel::(16); - let dbus = test::dbus::connection().await.unwrap(); - - tokio::spawn(async move { - while let Ok(event) = events_receiver.recv().await { - println!("{:?}", event); - } - }); - - let questions = question::start(events_sender.clone()).await.unwrap(); - manager::start(questions, events_sender, dbus) - .await - .unwrap() - } - - #[tokio::test] - #[cfg(not(ci))] - async fn test_update_config() -> Result<(), Box> { - let share_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../test/share"); - std::env::set_var("AGAMA_SHARE_DIR", share_dir.display().to_string()); - - let handler = start_service().await; - - let input_config = Config { - l10n: Some(l10n::Config { - locale: Some("es_ES.UTF-8".to_string()), - keymap: Some("es".to_string()), - timezone: Some("Atlantic/Canary".to_string()), - }), - ..Default::default() - }; - - handler - .call(message::SetConfig::new(input_config.clone())) - .await?; - - let config = handler.call(message::GetConfig).await?; - - assert_eq!(input_config.l10n.unwrap(), config.l10n.unwrap()); - - Ok(()) - } - - #[tokio::test] - #[cfg(not(ci))] - async fn test_patch_config() -> Result<(), Box> { - let handler = start_service().await; - - // Ensure the keymap is different to the system one. - let config = handler.call(message::GetExtendedConfig).await?; - let keymap = if config.l10n.unwrap().keymap.unwrap() == "es" { - "en" - } else { - "es" - }; - - let input_config = Config { - l10n: Some(l10n::Config { - keymap: Some(keymap.to_string()), - ..Default::default() - }), - ..Default::default() - }; - - handler - .call(message::UpdateConfig::new(input_config.clone())) - .await?; - - let config = handler.call(message::GetConfig).await?; - - assert_eq!(input_config.l10n.unwrap(), config.l10n.unwrap()); - - let extended_config = handler.call(message::GetExtendedConfig).await?; - let l10n_config = extended_config.l10n.unwrap(); - - assert!(l10n_config.locale.is_some()); - assert!(l10n_config.keymap.is_some()); - assert!(l10n_config.timezone.is_some()); - - Ok(()) - } -} diff --git a/rust/agama-manager/src/test_utils.rs b/rust/agama-manager/src/test_utils.rs index 09e795fc46..8080eb3818 100644 --- a/rust/agama-manager/src/test_utils.rs +++ b/rust/agama-manager/src/test_utils.rs @@ -29,7 +29,7 @@ use crate::Service; /// Starts a testing manager service. pub async fn start_service(events: event::Sender, dbus: zbus::Connection) -> Handler { - let issues = issue::start(events.clone(), dbus.clone()).await.unwrap(); + let issues = issue::Service::starter(events.clone()).start(); let questions = question::start(events.clone()).await.unwrap(); let progress = progress::Service::starter(events.clone()).start(); diff --git a/rust/agama-server/src/server/web.rs b/rust/agama-server/src/server/web.rs index 8fa9e5cc47..c9201b70cd 100644 --- a/rust/agama-server/src/server/web.rs +++ b/rust/agama-server/src/server/web.rs @@ -31,7 +31,7 @@ use agama_utils::{ manager::LicenseContent, query, question::{Question, QuestionSpec, UpdateQuestion}, - Action, Config, IssueMap, Patch, Status, SystemInfo, + Action, Config, IssueWithScope, Patch, Status, SystemInfo, }, question, }; @@ -252,18 +252,29 @@ async fn get_proposal(State(state): State) -> ServerResult), (status = 400, description = "Not possible to retrieve the issues") ) )] -async fn get_issues(State(state): State) -> ServerResult> { - let issues = state.manager.call(message::GetIssues).await?; +async fn get_issues(State(state): State) -> ServerResult>> { + let issue_groups = state.manager.call(message::GetIssues).await?; + + let issues = issue_groups + .into_iter() + .flat_map(|(scope, issues)| -> Vec { + issues + .into_iter() + .map(|issue| IssueWithScope { scope, issue }) + .collect() + }) + .collect(); + Ok(Json(issues)) } diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index fee20113b8..98bc110bbe 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -124,9 +124,8 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema_from::() - .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-software/src/service.rs b/rust/agama-software/src/service.rs index 5b58c0a366..9d33c25721 100644 --- a/rust/agama-software/src/service.rs +++ b/rust/agama-software/src/service.rs @@ -18,8 +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 std::{process::Command, sync::Arc}; - use crate::{ message, model::{software_selection::SoftwareSelection, state::SoftwareState, ModelAdapter}, @@ -31,13 +29,14 @@ use agama_utils::{ api::{ event::{self, Event}, software::{Config, Proposal, Repository, SoftwareProposal, SystemInfo}, - Issue, IssueSeverity, Scope, + Issue, Scope, }, issue, products::ProductSpec, progress, question, }; use async_trait::async_trait; +use std::{process::Command, sync::Arc}; use tokio::sync::{broadcast, Mutex, RwLock}; #[derive(thiserror::Error, Debug)] @@ -236,7 +235,6 @@ impl MessageHandler> for Service { let new_issue = Issue::new( "software.proposal_failed", "It was not possible to create a software proposal", - IssueSeverity::Error, ) .with_details(&error.to_string()); let mut state = state.write().await; diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index b3ed776166..38ee2ccf11 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -22,7 +22,7 @@ use agama_utils::{ actor::Handler, api::{ software::{Pattern, SelectedBy, SoftwareProposal}, - Issue, IssueSeverity, Scope, + Issue, Scope, }, products::ProductSpec, progress, question, @@ -283,8 +283,7 @@ impl ZyppServer { if let Err(error) = result { let message = format!("Could not add the repository {}", repo.alias); issues.push( - Issue::new("software.add_repo", &message, IssueSeverity::Error) - .with_details(&error.to_string()), + Issue::new("software.add_repo", &message).with_details(&error.to_string()), ); } // Add an issue if it was not possible to add the repository. @@ -299,8 +298,7 @@ impl ZyppServer { if let Err(error) = result { let message = format!("Could not remove the repository {}", repo.alias); issues.push( - Issue::new("software.remove_repo", &message, IssueSeverity::Error) - .with_details(&error.to_string()), + Issue::new("software.remove_repo", &message).with_details(&error.to_string()), ); } } @@ -315,8 +313,7 @@ impl ZyppServer { if let Err(error) = result { let message = format!("Could not read the repositories"); issues.push( - Issue::new("software.load_source", &message, IssueSeverity::Error) - .with_details(&error.to_string()), + Issue::new("software.load_source", &message).with_details(&error.to_string()), ); } } @@ -335,7 +332,7 @@ impl ZyppServer { if let Err(error) = result { let message = format!("Could not select pattern '{}'", &resolvable.name); issues.push( - Issue::new("software.select_pattern", &message, IssueSeverity::Error) + Issue::new("software.select_pattern", &message) .with_details(&error.to_string()), ); } diff --git a/rust/agama-storage/Cargo.toml b/rust/agama-storage/Cargo.toml index 0ac5bf61a7..6d1ade1f75 100644 --- a/rust/agama-storage/Cargo.toml +++ b/rust/agama-storage/Cargo.toml @@ -13,7 +13,7 @@ tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "sync"] } tokio-stream = "0.1.16" serde = { version = "1.0.228" } serde_json = "1.0.140" -test-context = "0.4.1" [dev-dependencies] +test-context = "0.4.1" tokio-test = "0.4.4" diff --git a/rust/agama-storage/src/client.rs b/rust/agama-storage/src/client.rs index 6343d7afe1..7d929b85a0 100644 --- a/rust/agama-storage/src/client.rs +++ b/rust/agama-storage/src/client.rs @@ -57,7 +57,6 @@ pub trait StorageClient { async fn get_config_model(&self) -> Result, Error>; async fn get_proposal(&self) -> Result, Error>; async fn get_issues(&self) -> Result, Error>; - async fn set_product(&self, id: String) -> Result<(), Error>; async fn set_config( &self, product: Arc>, @@ -140,12 +139,6 @@ impl StorageClient for Client { try_from_message(message) } - //TODO: send a product config instead of an id. - async fn set_product(&self, id: String) -> Result<(), Error> { - self.call("SetProduct", &(id)).await?; - Ok(()) - } - async fn set_config( &self, product: Arc>, diff --git a/rust/agama-storage/src/lib.rs b/rust/agama-storage/src/lib.rs index 6e3869a5b5..7e9daad3b1 100644 --- a/rust/agama-storage/src/lib.rs +++ b/rust/agama-storage/src/lib.rs @@ -47,7 +47,7 @@ mod tests { async fn setup() -> Context { let (events_tx, _events_rx) = broadcast::channel::(16); let dbus = test::dbus::connection().await.unwrap(); - let issues = issue::start(events_tx.clone(), dbus.clone()).await.unwrap(); + let issues = issue::Service::starter(events_tx.clone()).start(); let progress = progress::Service::starter(events_tx.clone()).start(); let client = TestClient::new(); diff --git a/rust/agama-storage/src/message.rs b/rust/agama-storage/src/message.rs index 47203ad4e6..31d995772f 100644 --- a/rust/agama-storage/src/message.rs +++ b/rust/agama-storage/src/message.rs @@ -18,11 +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_utils::{ - actor::Message, - api::{storage::Config, Issue}, - products::ProductSpec, -}; +use agama_utils::{actor::Message, api::storage::Config, products::ProductSpec}; use serde_json::Value; use std::sync::Arc; use tokio::sync::RwLock; @@ -83,21 +79,6 @@ impl Message for GetProposal { type Reply = Option; } -#[derive(Clone)] -pub struct SetProduct { - pub id: String, -} - -impl SetProduct { - pub fn new(id: &str) -> Self { - Self { id: id.to_string() } - } -} - -impl Message for SetProduct { - type Reply = (); -} - #[derive(Clone)] pub struct SetConfig { pub product: Arc>, diff --git a/rust/agama-storage/src/monitor.rs b/rust/agama-storage/src/monitor.rs index 21af46bdc0..5a5b623e14 100644 --- a/rust/agama-storage/src/monitor.rs +++ b/rust/agama-storage/src/monitor.rs @@ -18,7 +18,7 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::client::{self, StorageClient}; +use crate::client::{self, Client, StorageClient}; use agama_utils::{ actor::Handler, api::{ @@ -105,7 +105,7 @@ pub struct Monitor { issues: Handler, events: event::Sender, connection: Connection, - client: client::Client, + client: Client, } impl Monitor { @@ -114,14 +114,13 @@ impl Monitor { issues: Handler, events: event::Sender, connection: Connection, - client: client::Client, ) -> Self { Self { progress, issues, events, - connection, - client, + connection: connection.clone(), + client: Client::new(connection), } } @@ -134,6 +133,8 @@ impl Monitor { tokio::pin!(streams); + self.update_issues().await?; + while let Some((_, signal)) = streams.next().await { self.handle_signal(signal).await?; } @@ -141,6 +142,13 @@ impl Monitor { Ok(()) } + async fn update_issues(&self) -> Result<(), Error> { + let issues = self.client.get_issues().await?; + self.issues + .cast(issue::message::Set::new(Scope::Storage, issues))?; + Ok(()) + } + async fn handle_signal(&self, signal: Signal) -> Result<(), Error> { match signal { Signal::SystemChanged(signal) => self.handle_system_changed(signal)?, @@ -159,18 +167,12 @@ impl Monitor { Ok(()) } - // TODO: add proposal to the event. async fn handle_proposal_changed(&self, _signal: ProposalChanged) -> Result<(), Error> { self.events.send(Event::ProposalChanged { scope: Scope::Storage, })?; - let issues = self.client.get_issues().await?; - self.issues - .call(issue::message::Set::new(Scope::Storage, issues)) - .await?; - - Ok(()) + self.update_issues().await } fn handle_progress_changed(&self, signal: ProgressChanged) -> Result<(), Error> { diff --git a/rust/agama-storage/src/service.rs b/rust/agama-storage/src/service.rs index 7465f5a1c0..23123d41b9 100644 --- a/rust/agama-storage/src/service.rs +++ b/rust/agama-storage/src/service.rs @@ -25,7 +25,7 @@ use crate::{ }; use agama_utils::{ actor::{self, Actor, Handler, MessageHandler}, - api::{event, storage::Config, Scope}, + api::{event, storage::Config}, issue, progress, }; use async_trait::async_trait; @@ -38,8 +38,6 @@ pub enum Error { #[error(transparent)] Client(#[from] client::Error), #[error(transparent)] - Issue(#[from] issue::service::Error), - #[error(transparent)] Monitor(#[from] monitor::Error), } @@ -80,20 +78,10 @@ impl Starter { None => Box::new(Client::new(self.dbus.clone())), }; - let service = Service { - issues: self.issues.clone(), - client, - }; + let service = Service { client }; let handler = actor::spawn(service); - let monitor_client = Client::new(self.dbus.clone()); - let monitor = Monitor::new( - self.progress, - self.issues, - self.events, - self.dbus, - monitor_client, - ); + let monitor = Monitor::new(self.progress, self.issues, self.events, self.dbus); monitor::spawn(monitor)?; Ok(handler) } @@ -101,7 +89,6 @@ impl Starter { /// Storage service. pub struct Service { - issues: Handler, client: Box, } @@ -114,14 +101,6 @@ impl Service { ) -> Starter { Starter::new(events, issues, progress, dbus) } - - pub async fn setup(self) -> Result { - let issues = self.client.get_issues().await?; - self.issues - .call(issue::message::Set::new(Scope::Storage, issues)) - .await?; - Ok(self) - } } impl Actor for Service { @@ -188,14 +167,6 @@ impl MessageHandler for Service { } } -#[async_trait] -impl MessageHandler for Service { - async fn handle(&mut self, message: message::SetProduct) -> Result<(), Error> { - self.client.set_product(message.id).await?; - Ok(()) - } -} - #[async_trait] impl MessageHandler for Service { async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> { diff --git a/rust/agama-storage/src/test_utils.rs b/rust/agama-storage/src/test_utils.rs index 47c635ca8d..68cff198bd 100644 --- a/rust/agama-storage/src/test_utils.rs +++ b/rust/agama-storage/src/test_utils.rs @@ -126,9 +126,7 @@ impl StorageClient for TestClient { async fn get_issues(&self) -> Result, Error> { Ok(vec![]) } - async fn set_product(&self, _id: String) -> Result<(), Error> { - Ok(()) - } + async fn set_config( &self, _product: Arc>, diff --git a/rust/agama-utils/Cargo.toml b/rust/agama-utils/Cargo.toml index f07725ce66..ff75fb9350 100644 --- a/rust/agama-utils/Cargo.toml +++ b/rust/agama-utils/Cargo.toml @@ -26,4 +26,5 @@ cidr = { version = "0.3.1", features = ["serde"] } macaddr = { version = "1.0.1", features = ["serde_std"] } [dev-dependencies] +test-context = "0.4.1" tokio-test = "0.4.4" diff --git a/rust/agama-utils/src/api.rs b/rust/agama-utils/src/api.rs index 55d8f7e776..015447dfe5 100644 --- a/rust/agama-utils/src/api.rs +++ b/rust/agama-utils/src/api.rs @@ -34,7 +34,7 @@ pub mod status; pub use status::Status; pub mod issue; -pub use issue::{Issue, IssueMap, IssueSeverity, IssueSource}; +pub use issue::{Issue, IssueMap, IssueWithScope}; mod system_info; pub use system_info::SystemInfo; diff --git a/rust/agama-utils/src/api/issue.rs b/rust/agama-utils/src/api/issue.rs index a3036d14c4..dd074e6877 100644 --- a/rust/agama-utils/src/api/issue.rs +++ b/rust/agama-utils/src/api/issue.rs @@ -21,40 +21,34 @@ use crate::api::scope::Scope; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use strum::FromRepr; -pub type IssueMap = HashMap>; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("D-Bus conversion error")] - DBus(#[from] zbus::zvariant::Error), - #[error("Unknown issue source: {0}")] - UnknownSource(u8), - #[error("Unknown issue severity: {0}")] - UnknownSeverity(u8), +#[derive(Clone, Debug, Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssueWithScope { + pub scope: Scope, + #[serde(flatten)] + pub issue: Issue, } +pub type IssueMap = HashMap>; + // NOTE: in order to compare two issues, it should be enough to compare the description // and the details. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Issue { + pub class: String, pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, - pub source: IssueSource, - pub severity: IssueSeverity, - pub class: String, } impl Issue { /// Creates a new issue. - pub fn new(class: &str, description: &str, severity: IssueSeverity) -> Self { + pub fn new(class: &str, description: &str) -> Self { Self { description: description.to_string(), class: class.to_string(), - source: IssueSource::Config, - severity, details: None, } } @@ -64,123 +58,4 @@ impl Issue { self.details = Some(details.to_string()); self } - - /// Sets the source for the issue. - pub fn with_source(mut self, source: IssueSource) -> Self { - self.source = source; - self - } -} - -#[derive( - Clone, Copy, Debug, Deserialize, Serialize, FromRepr, PartialEq, Eq, Hash, utoipa::ToSchema, -)] -#[repr(u8)] -#[serde(rename_all = "camelCase")] -pub enum IssueSource { - Unknown = 0, - System = 1, - Config = 2, -} - -#[derive( - Clone, Copy, Debug, Deserialize, Serialize, FromRepr, PartialEq, Eq, Hash, utoipa::ToSchema, -)] -#[repr(u8)] -#[serde(rename_all = "camelCase")] -pub enum IssueSeverity { - Warn = 0, - Error = 1, -} - -impl TryFrom<&zbus::zvariant::Value<'_>> for Issue { - type Error = Error; - - fn try_from(value: &zbus::zvariant::Value<'_>) -> Result { - let value = value.downcast_ref::()?; - let fields = value.fields(); - - let Some([description, class, details, source, severity]) = fields.get(0..5) else { - return Err(zbus::zvariant::Error::Message( - "Not enough elements for building an Issue.".to_string(), - ))?; - }; - - let description: String = description.try_into()?; - let class: String = class.try_into()?; - let details: String = details.try_into()?; - let source: u32 = source.try_into()?; - let source = source as u8; - let source = IssueSource::from_repr(source).ok_or(Error::UnknownSource(source))?; - - let severity: u32 = severity.try_into()?; - let severity = severity as u8; - let severity = - IssueSeverity::from_repr(severity).ok_or(Error::UnknownSeverity(severity))?; - - Ok(Issue { - description, - class, - details: if details.is_empty() { - None - } else { - Some(details.to_string()) - }, - source, - severity, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use zbus::zvariant; - use zvariant::{Structure, Value}; - - #[test] - fn test_issue_from_dbus() { - let dbus_issue = Structure::from(( - "Product not selected", - "missing_product", - "A product is required.", - 1 as u32, - 0 as u32, - )); - - let issue = Issue::try_from(&Value::Structure(dbus_issue)).unwrap(); - assert_eq!(&issue.description, "Product not selected"); - assert_eq!(&issue.class, "missing_product"); - assert_eq!(issue.details, Some("A product is required.".to_string())); - assert_eq!(issue.source, IssueSource::System); - assert_eq!(issue.severity, IssueSeverity::Warn); - } - - #[test] - fn test_unknown_issue_source() { - let dbus_issue = Structure::from(( - "Product not selected", - "missing_product", - "A product is required.", - 5 as u32, - 0 as u32, - )); - - let issue = Issue::try_from(&Value::Structure(dbus_issue)); - assert!(matches!(issue, Err(Error::UnknownSource(5)))); - } - - #[test] - fn test_unknown_issue_severity() { - let dbus_issue = Structure::from(( - "Product not selected", - "missing_product", - "A product is required.", - 0 as u32, - 5 as u32, - )); - - let issue = Issue::try_from(&Value::Structure(dbus_issue)); - assert!(matches!(issue, Err(Error::UnknownSeverity(5)))); - } } diff --git a/rust/agama-utils/src/issue.rs b/rust/agama-utils/src/issue.rs index 35abeae891..ef4ff5d5ac 100644 --- a/rust/agama-utils/src/issue.rs +++ b/rust/agama-utils/src/issue.rs @@ -32,7 +32,89 @@ pub use service::Service; pub mod message; -pub mod start; -pub use start::start; +#[cfg(test)] +mod tests { + use crate::{ + actor::Handler, + api::{ + event::{Event, Receiver}, + issue::Issue, + scope::Scope, + }, + issue::{ + message, + service::{Error, Service}, + }, + }; + use test_context::{test_context, AsyncTestContext}; + use tokio::sync::broadcast::{self, error::TryRecvError}; -mod monitor; + fn build_issue() -> Issue { + Issue { + description: "Product not selected".to_string(), + class: "missing_product".to_string(), + details: Some("A product is required.".to_string()), + } + } + + struct Context { + handler: Handler, + receiver: Receiver, + } + + impl AsyncTestContext for Context { + async fn setup() -> Context { + let (sender, receiver) = broadcast::channel::(16); + let handler = Service::starter(sender).start(); + Self { handler, receiver } + } + } + + #[test_context(Context)] + #[tokio::test] + async fn test_get_and_update_issues(ctx: &mut Context) -> Result<(), Error> { + let issues = ctx.handler.call(message::Get).await.unwrap(); + assert!(issues.is_empty()); + + let issue = build_issue(); + ctx.handler + .cast(message::Set::new(Scope::Manager, vec![issue]))?; + + let issues = ctx.handler.call(message::Get).await?; + assert_eq!(issues.len(), 1); + + assert!(ctx.receiver.recv().await.is_ok()); + Ok(()) + } + + #[test_context(Context)] + #[tokio::test] + async fn test_update_without_event(ctx: &mut Context) -> Result<(), Error> { + let issues = ctx.handler.call(message::Get).await?; + assert!(issues.is_empty()); + + let issue = build_issue(); + let update = message::Set::new(Scope::Manager, vec![issue]).notify(false); + ctx.handler.cast(update)?; + + let issues = ctx.handler.call(message::Get).await?; + assert_eq!(issues.len(), 1); + + assert!(matches!(ctx.receiver.try_recv(), Err(TryRecvError::Empty))); + Ok(()) + } + + #[test_context(Context)] + #[tokio::test] + async fn test_update_without_change(ctx: &mut Context) -> Result<(), Error> { + let issue = build_issue(); + let update = message::Set::new(Scope::Manager, vec![issue.clone()]); + ctx.handler.call(update).await?; + assert!(ctx.receiver.try_recv().is_ok()); + + let update = message::Set::new(Scope::Manager, vec![issue]); + ctx.handler.call(update).await?; + assert!(matches!(ctx.receiver.try_recv(), Err(TryRecvError::Empty))); + Ok(()) + } +} diff --git a/rust/agama-utils/src/issue/monitor.rs b/rust/agama-utils/src/issue/monitor.rs deleted file mode 100644 index c95a4d4546..0000000000 --- a/rust/agama-utils/src/issue/monitor.rs +++ /dev/null @@ -1,195 +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 crate::actor::Handler; -use crate::api::issue; -use crate::api::issue::Issue; -use crate::api::scope::Scope; -use crate::dbus::build_properties_changed_stream; -use crate::issue::message; -use crate::issue::service; -use crate::issue::Service; -use tokio_stream::StreamExt; -use zbus::fdo::PropertiesChanged; -use zbus::names::BusName; -use zbus::zvariant::Array; -use zvariant::OwnedObjectPath; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Error parsing issues from D-Bus: {0}")] - InvalidIssue(#[from] zbus::zvariant::Error), - #[error("Invalid D-Bus name")] - InvalidDBusName(#[from] zbus::names::Error), - #[error(transparent)] - DBus(#[from] zbus::Error), - #[error(transparent)] - Service(#[from] service::Error), - #[error(transparent)] - Issue(#[from] issue::Error), -} - -/// Listens the D-Bus server and updates the list of issues. -/// -/// It retrieves and keeps up-to-date the list of issues for the Agama services -/// that offers a D-Bus API. -pub struct Monitor { - handler: Handler, - dbus: zbus::Connection, -} - -const MANAGER_SERVICE: &str = "org.opensuse.Agama.Manager1"; -const SOFTWARE_SERVICE: &str = "org.opensuse.Agama.Software1"; -const STORAGE_SERVICE: &str = "org.opensuse.Agama.Storage1"; - -const ISCSI_PATH: &str = "/org/opensuse/Agama/Storage1/ISCSI"; -const PRODUCT_PATH: &str = "/org/opensuse/Agama/Software1/Product"; -const SOFTWARE_PATH: &str = "/org/opensuse/Agama/Software1"; -const USERS_PATH: &str = "/org/opensuse/Agama/Users1"; - -impl Monitor { - pub fn new(handler: Handler, dbus: zbus::Connection) -> Self { - Self { handler, dbus } - } - - /// Run the monitor on a separate Tokio task. - async fn run(&self) -> Result<(), Error> { - let mut messages = build_properties_changed_stream(&self.dbus).await?; - - self.initialize_issues(MANAGER_SERVICE, USERS_PATH).await?; - self.initialize_issues(SOFTWARE_SERVICE, SOFTWARE_PATH) - .await?; - self.initialize_issues(SOFTWARE_SERVICE, PRODUCT_PATH) - .await?; - self.initialize_issues(STORAGE_SERVICE, ISCSI_PATH).await?; - - while let Some(Ok(message)) = messages.next().await { - if let Some(changed) = PropertiesChanged::from_message(message) { - if let Err(e) = self.handle_property_changed(changed) { - println!("Could not handle issues change: {:?}", e); - } - } - } - - Ok(()) - } - - /// Handles PropertiesChanged events. - /// - /// It reports an error if something went work. If the message was processed or skipped - /// it returns Ok(()). - fn handle_property_changed(&self, message: PropertiesChanged) -> Result<(), Error> { - let args = message.args()?; - let inner = message.message(); - let header = inner.header(); - - // We are neither interested on this message... - let Some(path) = header.path() else { - return Ok(()); - }; - - // nor on this... - if args.interface_name.as_str() != "org.opensuse.Agama1.Issues" { - return Ok(()); - } - - // nor on this one. - let Some(all) = args.changed_properties().get("All") else { - return Ok(()); - }; - - let all = all.downcast_ref::<&Array>()?; - let issues = all - .into_iter() - .map(Issue::try_from) - .collect::, _>>()?; - - self.update_issues(path.as_str(), issues, true)?; - - Ok(()) - } - - /// Initializes the list of issues reading the list from D-Bus. - /// - /// * `service`: service name. - /// * `path`: path of the object implementing issues interface. - async fn initialize_issues(&self, service: &str, path: &str) -> Result<(), Error> { - let bus = BusName::try_from(service.to_string())?; - let dbus_path = OwnedObjectPath::try_from(path)?; - let output = self - .dbus - .call_method( - Some(&bus), - &dbus_path, - Some("org.freedesktop.DBus.Properties"), - "Get", - &("org.opensuse.Agama1.Issues", "All"), - ) - .await?; - - let body = output.body(); - let body: zbus::zvariant::Value = body.deserialize()?; - let body = body.downcast_ref::<&Array>()?; - - let issues = body - .into_iter() - .map(Issue::try_from) - .collect::, _>>()?; - self.update_issues(path, issues, false)?; - - Ok(()) - } - - /// Updates the list of issues. - fn update_issues(&self, path: &str, issues: Vec, notify: bool) -> Result<(), Error> { - match Self::scope_from_path(path) { - Some(scope) => { - self.handler - .cast(message::Set::new(scope, issues).notify(notify))?; - } - None => { - eprintln!("Unknown issues object {}", path); - } - } - Ok(()) - } - - /// Turns the D-Bus path into a scope. - fn scope_from_path(path: &str) -> Option { - match path { - SOFTWARE_PATH => Some(Scope::Software), - PRODUCT_PATH => Some(Scope::Product), - USERS_PATH => Some(Scope::Users), - ISCSI_PATH => Some(Scope::Iscsi), - _ => None, - } - } -} - -/// Spawns a Tokio task for the monitor. -/// -/// * `monitor`: monitor to spawn. -pub fn spawn(monitor: Monitor) { - tokio::spawn(async move { - if let Err(e) = monitor.run().await { - println!("Error running the issues monitor: {e:?}"); - } - }); -} diff --git a/rust/agama-utils/src/issue/service.rs b/rust/agama-utils/src/issue/service.rs index da4414f790..a49061499c 100644 --- a/rust/agama-utils/src/issue/service.rs +++ b/rust/agama-utils/src/issue/service.rs @@ -18,11 +18,11 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. -use crate::actor::{self, Actor, MessageHandler}; -use crate::api::event; -use crate::api::event::Event; -use crate::api::issue; -use crate::api::issue::IssueMap; +use crate::actor::{self, Actor, Handler, MessageHandler}; +use crate::api::{ + event::{self, Event}, + issue::IssueMap, +}; use crate::issue::message; use async_trait::async_trait; use std::collections::HashSet; @@ -34,8 +34,21 @@ pub enum Error { Event(#[from] broadcast::error::SendError), #[error(transparent)] Actor(#[from] actor::Error), - #[error(transparent)] - Issue(#[from] issue::Error), +} + +pub struct Starter { + events: event::Sender, +} + +impl Starter { + pub fn new(events: event::Sender) -> Self { + Self { events } + } + + pub fn start(self) -> Handler { + let service = Service::new(self.events); + actor::spawn(service) + } } pub struct Service { @@ -44,7 +57,11 @@ pub struct Service { } impl Service { - pub fn new(events: event::Sender) -> Self { + pub fn starter(events: event::Sender) -> Starter { + Starter::new(events) + } + + fn new(events: event::Sender) -> Self { Self { issues: IssueMap::new(), events, diff --git a/rust/agama-utils/src/issue/start.rs b/rust/agama-utils/src/issue/start.rs deleted file mode 100644 index 4e53fe799c..0000000000 --- a/rust/agama-utils/src/issue/start.rs +++ /dev/null @@ -1,123 +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 crate::{ - actor::{self, Handler}, - api::event, - issue::{ - monitor::{self, Monitor}, - service::{self, Service}, - }, -}; - -// FIXME: replace this start function with a service builder. -pub async fn start( - events: event::Sender, - dbus: zbus::Connection, -) -> Result, service::Error> { - let service = Service::new(events); - let handler = actor::spawn(service); - - let dbus_monitor = Monitor::new(handler.clone(), dbus); - monitor::spawn(dbus_monitor); - - Ok(handler) -} - -#[cfg(test)] -mod tests { - use crate::{ - api::{ - event::Event, - issue::{Issue, IssueSeverity, IssueSource}, - scope::Scope, - }, - issue::{self, message}, - test, - }; - use tokio::sync::broadcast::{self, error::TryRecvError}; - - fn build_issue() -> Issue { - Issue { - description: "Product not selected".to_string(), - class: "missing_product".to_string(), - details: Some("A product is required.".to_string()), - source: IssueSource::Config, - severity: IssueSeverity::Error, - } - } - - #[tokio::test] - async fn test_get_and_update_issues() -> Result<(), Box> { - let (events_tx, mut events_rx) = broadcast::channel::(16); - let dbus = test::dbus::connection().await.unwrap(); - let issues = issue::start(events_tx, dbus).await.unwrap(); - let issues_list = issues.call(message::Get).await.unwrap(); - assert!(issues_list.is_empty()); - - let issue = build_issue(); - _ = issues - .cast(message::Set::new(Scope::Manager, vec![issue])) - .unwrap(); - - let issues_list = issues.call(message::Get).await.unwrap(); - assert_eq!(issues_list.len(), 1); - - assert!(events_rx.recv().await.is_ok()); - Ok(()) - } - - #[tokio::test] - async fn test_update_without_event() -> Result<(), Box> { - let (events_tx, mut events_rx) = broadcast::channel::(16); - let dbus = test::dbus::connection().await.unwrap(); - let issues = issue::start(events_tx, dbus).await.unwrap(); - - let issues_list = issues.call(message::Get).await.unwrap(); - assert!(issues_list.is_empty()); - - let issue = build_issue(); - let update = message::Set::new(Scope::Manager, vec![issue]).notify(false); - _ = issues.cast(update).unwrap(); - - let issues_list = issues.call(message::Get).await.unwrap(); - assert_eq!(issues_list.len(), 1); - - assert!(matches!(events_rx.try_recv(), Err(TryRecvError::Empty))); - Ok(()) - } - - #[tokio::test] - async fn test_update_without_change() -> Result<(), Box> { - let (events_tx, mut events_rx) = broadcast::channel::(16); - let dbus = test::dbus::connection().await.unwrap(); - let issues = issue::start(events_tx, dbus).await.unwrap(); - - let issue = build_issue(); - let update = message::Set::new(Scope::Manager, vec![issue.clone()]); - issues.call(update).await.unwrap(); - assert!(events_rx.try_recv().is_ok()); - - let update = message::Set::new(Scope::Manager, vec![issue]); - issues.call(update).await.unwrap(); - assert!(matches!(events_rx.try_recv(), Err(TryRecvError::Empty))); - Ok(()) - } -} diff --git a/rust/share/system.storage.schema.json b/rust/share/system.storage.schema.json index 4382a24ca7..17d1c9cbd8 100644 --- a/rust/share/system.storage.schema.json +++ b/rust/share/system.storage.schema.json @@ -104,14 +104,8 @@ "properties": { "description": { "type": "string" }, "class": { "type": "string" }, - "details": { "type": "string" }, - "source": { "$ref": "#/$defs/issueSource" }, - "severity": { "enum": ["warn", "error"] } + "details": { "type": "string" } } - }, - "issueSource": { - "title": "System issue source", - "enum": ["config", "system"] } } } diff --git a/service/lib/agama/config.rb b/service/lib/agama/config.rb index dfb1e395c1..641bcaee81 100644 --- a/service/lib/agama/config.rb +++ b/service/lib/agama/config.rb @@ -20,6 +20,7 @@ # find current contact information at www.suse.com. require "agama/copyable" +require "yast2/equatable" module Agama # Class representing the current product configuration. diff --git a/service/lib/agama/dbus/clients/base.rb b/service/lib/agama/dbus/clients/base.rb deleted file mode 100644 index 30a9fdd17f..0000000000 --- a/service/lib/agama/dbus/clients/base.rb +++ /dev/null @@ -1,111 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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 "dbus" -require "agama/dbus/bus" -require "abstract_method" - -module Agama - module DBus - module Clients - # Base class for D-Bus clients - # - # The clients should be singleton because the #on_properties_change method - # will not work properly with several instances. Given a - # ::DBus::BusConnection object, ruby-dbus does not allow to register more - # than one callback for the same object. - # - # It causes the last instance to overwrite the callbacks from previous ones. - # - # @example Creating a new client - # require "singleton" - # - # class Locale < Base - # include Singleton - # - # # client methods - # end - class Base - # @!method service_name - # Name of the D-Bus service - # @return [String] - abstract_method :service_name - - # Constructor - # - # @param logger [Logger, nil] - def initialize(logger: nil) - @logger = logger || Logger.new($stdout) - end - - # D-Bus service - # - # @return [::DBus::ObjectServer] - def service - @service ||= bus.service(service_name) - end - - private - - # @return [Logger] - attr_reader :logger - - # Registers a callback to be called when the properties of the given object changes. - # - # @param dbus_object [::DBus::Object] - # @param block [Proc] - def on_properties_change(dbus_object, &block) - subscribe(dbus_object, "org.freedesktop.DBus.Properties", "PropertiesChanged", &block) - end - - # Subscribes to a D-Bus signal. - # - # @note Signal subscription is done only once. Otherwise, the latest subscription overrides - # the previous one. - # - # @param dbus_object [::DBus::Object] - # @param interface [String] - # @param signal [String] - # @param block [Proc] - def subscribe(dbus_object, interface, signal, &block) - @signal_handlers ||= {} - @signal_handlers[dbus_object.path] ||= {} - @signal_handlers[dbus_object.path][interface] ||= {} - @signal_handlers[dbus_object.path][interface][signal] ||= [] - @signal_handlers[dbus_object.path][interface][signal] << block - - callbacks = @signal_handlers.dig(dbus_object.path, interface, signal) - - return if callbacks.size > 1 - - interface_proxy = dbus_object[interface] - interface_proxy.on_signal(signal) do |*args| - callbacks.each { |c| c.call(*args) } - end - end - - def bus - @bus ||= Bus.current - end - end - end - end -end diff --git a/service/lib/agama/dbus/clients/storage.rb b/service/lib/agama/dbus/clients/storage.rb deleted file mode 100644 index f5fe91c34b..0000000000 --- a/service/lib/agama/dbus/clients/storage.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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 "agama/dbus/clients/base" -require "agama/dbus/clients/with_locale" -require "json" - -module Agama - module DBus - module Clients - # D-Bus client for storage configuration - class Storage < Base - include WithLocale - - STORAGE_IFACE = "org.opensuse.Agama.Storage1" - private_constant :STORAGE_IFACE - - def service_name - @service_name ||= "org.opensuse.Agama.Storage1" - end - - def product=(id) - dbus_object.SetProduct(id) - end - - # Starts the probing process - # - # If a block is given, the method returns immediately and the probing is performed in an - # asynchronous way. - # - # @param done [Proc] Block to execute once the probing is done - def probe(&done) - dbus_object.Probe(&done) - end - - # Performs the packages installation - def install - dbus_object.Install - end - - # Cleans-up the storage stuff after installation - def finish - dbus_object.Finish - end - - # Gets the current storage config. - # - # @return [Hash, nil] nil if there is no config yet. - def config - # Use storage iface to avoid collision with bootloader iface - serialized_config = dbus_object[STORAGE_IFACE].GetConfig - JSON.parse(serialized_config, symbolize_names: true) - end - - # Sets the storage config. - # - # @param config [Hash] - def config=(config) - serialized_config = JSON.pretty_generate(config) - # Use storage iface to avoid collision with bootloader iface - dbus_object[STORAGE_IFACE].SetConfig(serialized_config) - end - - private - - # @return [::DBus::Object] - def dbus_object - @dbus_object ||= service["/org/opensuse/Agama/Storage1"].tap(&:introspect) - end - end - end - end -end diff --git a/service/lib/agama/dbus/clients/with_issues.rb b/service/lib/agama/dbus/clients/with_issues.rb deleted file mode 100644 index c4243dd80e..0000000000 --- a/service/lib/agama/dbus/clients/with_issues.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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 "agama/issue" - -module Agama - module DBus - # Mixin to include in the clients of services that implement the Issues interface - module WithIssues - ISSUES_IFACE = "org.opensuse.Agama1.Issues" - private_constant :ISSUES_IFACE - - # Returns issues from all objects that implement the issues interface. - # - # @return [Array] - def issues - objects_with_issues_interface.map { |o| issues_from(o) }.flatten - end - - # Determines whether there are errors - # - # @return [Boolean] - def errors? - issues.any?(&:error?) - end - - def objects_with_issues_interface - service.root.descendant_objects.select { |o| o.interfaces.include?(ISSUES_IFACE) } - end - - def issues_from(dbus_object) - sources = [nil, Issue::Source::SYSTEM, Issue::Source::CONFIG] - severities = [Issue::Severity::WARN, Issue::Severity::ERROR] - - dbus_object[ISSUES_IFACE]["All"].map do |dbus_issue| - Issue.new(dbus_issue[0], - kind: dbus_issue[1].to_sym, - details: dbus_issue[2], - source: sources[dbus_issue[3]], - severity: severities[dbus_issue[4]]) - end - end - end - end -end diff --git a/service/lib/agama/dbus/clients/with_locale.rb b/service/lib/agama/dbus/clients/with_locale.rb deleted file mode 100644 index 08d9fd208a..0000000000 --- a/service/lib/agama/dbus/clients/with_locale.rb +++ /dev/null @@ -1,42 +0,0 @@ -# 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. - -module Agama - module DBus - module Clients - # Mixin for clients of services that define the Locale D-Bus interface - # - # Provides a method to interact with the API of the Locale interface. - # - # @note This mixin is expected to be included in a class inherited from {Clients::Base} and - # it requires a #dbus_object method that returns a {::DBus::Object} implementing the - # Locale interface. - module WithLocale - # Changes the service locale - # - # @param locale [String] new locale - def locale=(locale) - dbus_object.SetLocale(locale) - end - end - end - end -end diff --git a/service/lib/agama/dbus/clients/with_progress.rb b/service/lib/agama/dbus/clients/with_progress.rb deleted file mode 100644 index b3ef577e32..0000000000 --- a/service/lib/agama/dbus/clients/with_progress.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "agama/dbus/interfaces/progress" - -module Agama - module DBus - module Clients - # Mixin for clients of services that define the Progress D-Bus interface - # - # Provides methods to interact with the API of the Progress interface. - # - # @note This mixin is expected to be included in a class inherited from {Clients::Base} and - # it requires a #dbus_object method that returns a {::DBus::Object} implementing the - # Progress interface. - module WithProgress - # Registers a callback to run when the progress changes - # - # @param block [Proc] - # @yieldparam total_steps [Integer] - # @yieldparam current_step [Integer] - # @yieldparam message [String] - # @yieldparam finished [Boolean] - def on_progress_change(&block) - on_properties_change(dbus_object) do |interface, changes, _| - if interface == Interfaces::Progress::PROGRESS_INTERFACE - total_steps = changes["TotalSteps"] - current_step = changes["CurrentStep"][0] - message = changes["CurrentStep"][1] - finished = changes["Finished"] - block.call(total_steps, current_step, message, finished) - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/clients/with_service_status.rb b/service/lib/agama/dbus/clients/with_service_status.rb deleted file mode 100644 index 1181393292..0000000000 --- a/service/lib/agama/dbus/clients/with_service_status.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "dbus" -require "agama/dbus/service_status" -require "agama/dbus/interfaces/service_status" - -module Agama - module DBus - module Clients - # Mixin for clients of services that define the ServiceStatus D-Bus interface - # - # Provides methods to interact with the API of the ServiceStatus interface. - # - # @note This mixin is expected to be included in a class inherited from {Clients::Base} and - # it requires a #dbus_object method that returns a {::DBus::Object} implementing the - # ServiceStatus interface. - module WithServiceStatus - # Current value of the service status - # - # @see Interfaces::ServiceStatus - # - # @return [ServiceStatus::IDLE, ServiceStatus::BUSY] - def service_status - dbus_status = dbus_object[Interfaces::ServiceStatus::SERVICE_STATUS_INTERFACE]["Current"] - to_service_status(dbus_status) - end - - # Registers a callback to run when the current status property changes - # - # @param block [Proc] - # @yieldparam service_status [ServiceStatus::IDLE, ServiceStatus::BUSY] - def on_service_status_change(&block) - on_properties_change(dbus_object) do |interface, changes, _| - if interface == Interfaces::ServiceStatus::SERVICE_STATUS_INTERFACE - service_status = to_service_status(changes["Current"]) - block.call(service_status) - end - end - end - - # Converts the D-Bus status value to the equivalent service status value - # - # @param dbus_status [Integer] - # @return [ServiceStatus::IDLE, ServiceStatus::BUSY] - def to_service_status(dbus_status) - case dbus_status - when Interfaces::ServiceStatus::SERVICE_STATUS_IDLE - ServiceStatus::IDLE - when Interfaces::ServiceStatus::SERVICE_STATUS_BUSY - ServiceStatus::BUSY - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/hash_validator.rb b/service/lib/agama/dbus/hash_validator.rb deleted file mode 100644 index a7c7a4c3e9..0000000000 --- a/service/lib/agama/dbus/hash_validator.rb +++ /dev/null @@ -1,154 +0,0 @@ -# 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 "agama/dbus/types" - -module Agama - module DBus - # Validates a Hash (dictionary) from D-Bus according to an scheme. - # - # This validation is useful to check the expected types of a D-Bus call when some parameter is - # a dictionary with variant types. - # - # @example - # # Let's say there is a D-Bus method with the following signature: - # - # dbus_method :Calculate, "in settings:a{sv}" - # - # # The settings parameter will be transformed to a ruby Hash and this class allows to - # # validate the types of the hash values. - # - # scheme = { - # "ID" => Integer, - # "Name" => String, - # "Children" => Agama::DBus::Types.Array.new(Integer) - # } - # - # value1 = { "ID" => 10, "Name" => "Foo", "Color" => "red" } - # validator = HashValidator.new(value1, scheme: scheme) - # validator.valid? #=> true - # validator.missing_keys #=> ["Children"] - # validator.extra_keys #=> ["Color"] - # validator.issues #=> ["Unknown D-Bus property Color"] - # - # value2 = { "ID" => 10, "Name" => 33 } - # validator = HashValidator.new(value2, scheme: scheme) - # validator.valid? #=> false - # validator.missing_keys #=> ["Children"] - # validator.wrong_type_keys #=> ["Name"] - # validator.issues.size #=> 1 - class HashValidator - # @param value [Hash{String => Object}] Hash to validate. - # @param scheme [Hash{String => Class, Types::BOOL, Types::Array, Types::Hash}] Scheme - # for validating the hash. - def initialize(value, scheme:) - @value = value - @scheme = scheme - end - - # Whether the hash is valid. - # - # The hash is consider as valid if there is no key with wrong type. Missing and extra keys are - # not validated. - # - # @return [Boolean] - def valid? - wrong_type_keys.none? - end - - # Keys with correct type. - # - # Missing and extra keys are ignored. - # - # @return [Array] - def valid_keys - value.keys.select { |k| valid_key?(k) } - end - - # Keys with incorrect type. - # - # Missing and extra keys are ignored. - # - # @return [Array] - def wrong_type_keys - value.keys.select { |k| !extra_key?(k) && wrong_type_key?(k) } - end - - # Keys not included in the scheme. - # - # @return [Array] - def extra_keys - value.keys.select { |k| extra_key?(k) } - end - - # Keys included in the scheme but missing in the hash value. - # - # @return [Array] - def missing_keys - scheme.keys - value.keys - end - - # List of issues. - # - # There is an issue for each extra key and for each key with wrong type. - # - # @return [Array] - def issues - issues = [] - - extra_keys.map do |key| - issues << "Unknown D-Bus property #{key}" - end - - wrong_type_keys.map do |key| - type = scheme[key] - value = self.value[key] - - issues << "D-Bus property #{key} must be #{type}: #{value} (#{value.class})" - end - - issues - end - - private - - # @return [Hash{String => Object}] - attr_reader :value - - # @return [Hash{String => Class, Types::BOOL, Types::Array, Types::Hash}] - attr_reader :scheme - - def valid_key?(key) - !(extra_key?(key) || wrong_type_key?(key)) - end - - def extra_key?(key) - !scheme.keys.include?(key) - end - - def wrong_type_key?(key) - type = scheme[key] - checker = Types::Checker.new(type) - !checker.match?(value[key]) - end - end - end -end diff --git a/service/lib/agama/dbus/interfaces.rb b/service/lib/agama/dbus/interfaces.rb index a439d3f505..905d2fce4d 100644 --- a/service/lib/agama/dbus/interfaces.rb +++ b/service/lib/agama/dbus/interfaces.rb @@ -28,6 +28,3 @@ module Interfaces end require "agama/dbus/interfaces/issues" -require "agama/dbus/interfaces/progress" -require "agama/dbus/interfaces/service_status" -require "agama/dbus/interfaces/validation" diff --git a/service/lib/agama/dbus/interfaces/issues.rb b/service/lib/agama/dbus/interfaces/issues.rb index 9a8cae97f9..5cf0e25f31 100644 --- a/service/lib/agama/dbus/interfaces/issues.rb +++ b/service/lib/agama/dbus/interfaces/issues.rb @@ -34,23 +34,10 @@ module Issues # Issues with the D-Bus format # - # @return [Array] The description, details, source - # and severity of each issue. - # Source: 1 for system, 2 for config and 0 for unknown. - # Severity: 0 for warn and 1 for error. + # @return [Array] The description, kind and details of each issue. def dbus_issues issues.map do |issue| - source = case issue.source - when Agama::Issue::Source::SYSTEM - 1 - when Agama::Issue::Source::CONFIG - 2 - else - 0 - end - severity = (issue.severity == Agama::Issue::Severity::WARN) ? 0 : 1 - - [issue.description, issue.kind.to_s, issue.details.to_s, source, severity] + [issue.description, issue.kind.to_s, issue.details.to_s] end end @@ -64,7 +51,7 @@ def self.included(base) base.class_eval do dbus_interface ISSUES_INTERFACE do # @see {#dbus_issues} - dbus_reader :dbus_issues, "a(sssuu)", dbus_name: "All" + dbus_reader :dbus_issues, "a(sss)", dbus_name: "All" end end end diff --git a/service/lib/agama/dbus/interfaces/locale.rb b/service/lib/agama/dbus/interfaces/locale.rb deleted file mode 100644 index 6a9b358ee6..0000000000 --- a/service/lib/agama/dbus/interfaces/locale.rb +++ /dev/null @@ -1,53 +0,0 @@ -# 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 "yast" -require "dbus" - -Yast.import "WFM" - -module Agama - module DBus - module Interfaces - # Mixin to define the Locale interface. - # - # @note This mixin is expected to be included in a class that inherits from {DBus::BaseObject} - # and it requires a #locale= method that sets the service's locale. - module Locale - include Yast::I18n - include Yast::Logger - - LOCALE_INTERFACE = "org.opensuse.Agama1.LocaleMixin" - - def self.included(base) - base.class_eval do - dbus_interface LOCALE_INTERFACE do - # It expects a locale (en_US.UTF-8) as argument. - dbus_method :SetLocale, "in locale:s" do |locale| - self.locale = locale - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/interfaces/progress.rb b/service/lib/agama/dbus/interfaces/progress.rb deleted file mode 100644 index e5e69d719a..0000000000 --- a/service/lib/agama/dbus/interfaces/progress.rb +++ /dev/null @@ -1,123 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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 "dbus" - -module Agama - module DBus - module Interfaces - # Mixin to define the Progress D-Bus interface - # - # @note This mixin is expected to be included by a class which inherits from - # {DBus::BaseObject} and that includes the {Agama::DBus::WithProgress} mixin. - # - # @example - # class Demo < Agama::DBus::BaseObject - # include Agama::DBus::WithProgress - # include Agama::DBus::Interfaces::Progress - # - # def initialize - # super("org.test.Demo") - # register_progress_callbacks - # end - # end - module Progress - PROGRESS_INTERFACE = "org.opensuse.Agama1.Progress" - - # Total number of steps of the progress - # - # @return [Integer] 0 if no progress defined - def progress_total_steps - return 0 unless progress - - progress.total_steps - end - - # Current step data - # - # @return [Array(Number,String)] Step id and description - def progress_current_step - current_step = progress&.current_step - return [0, ""] unless current_step - - [current_step.id, current_step.description] - end - - # Whether the progress has finished - # - # @return [Boolean] - def progress_finished - return true unless progress - - progress.finished? - end - - # Returns the known step descriptions - # - # @return [Array] - def progress_steps - return [] unless progress - - progress.descriptions - end - - # D-Bus properties of the Progress interface - # - # @return [Hash] - def progress_properties - interfaces_and_properties[PROGRESS_INTERFACE] - end - - # Registers callbacks to be called when the progress changes or finishes - # - # @note This method is expected to be called in the constructor. - def register_progress_callbacks - progress_manager.on_change do - dbus_properties_changed(PROGRESS_INTERFACE, progress_properties, []) - ProgressChanged( - progress_total_steps, progress_current_step, progress_finished, progress_steps - ) - end - - progress_manager.on_finish do - dbus_properties_changed(PROGRESS_INTERFACE, progress_properties, []) - ProgressChanged( - progress_total_steps, progress_current_step, progress_finished, progress_steps - ) - end - end - - def self.included(base) - base.class_eval do - dbus_interface PROGRESS_INTERFACE do - dbus_reader :progress_total_steps, "u", dbus_name: "TotalSteps" - dbus_reader :progress_current_step, "(us)", dbus_name: "CurrentStep" - dbus_reader :progress_finished, "b", dbus_name: "Finished" - dbus_reader :progress_steps, "as", dbus_name: "Steps" - dbus_signal :ProgressChanged, - "total_steps:u, current_step:(us), finished:b, steps:as" - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/interfaces/service_status.rb b/service/lib/agama/dbus/interfaces/service_status.rb deleted file mode 100644 index 838087eb6b..0000000000 --- a/service/lib/agama/dbus/interfaces/service_status.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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 "dbus" - -module Agama - module DBus - module Interfaces - # Mixin to define the ServiceStatus D-Bus interface - # - # @note This mixin is expected to be included by a class which inherits from - # {DBus::BaseObject} and that includes the {Agama::DBus::WithServiceStatus} mixin. - # - # @example - # class Demo < ::DBus::Object - # include Agama::DBus::WithServiceStatus - # include Agama::DBus::Interfaces::ServiceStatus - # - # def initialize - # super("org.test.Demo") - # register_service_status_callbacks - # end - # end - module ServiceStatus - SERVICE_STATUS_INTERFACE = "org.opensuse.Agama1.ServiceStatus" - - SERVICE_STATUS_IDLE = 0 - SERVICE_STATUS_BUSY = 1 - - # Description of all possible service status values - # - # @return [Array] - def service_status_all - [ - { "id" => SERVICE_STATUS_IDLE, "label" => "idle" }, - { "id" => SERVICE_STATUS_BUSY, "label" => "busy" } - ] - end - - # Current value of the service status - # - # @return [Integer] - def service_status_current - service_status.busy? ? SERVICE_STATUS_BUSY : SERVICE_STATUS_IDLE - end - - # Registers callbacks to be called when the value of the service status changes - # - # @note This method is expected to be called in the constructor. - def register_service_status_callbacks - service_status.on_change do - dbus_properties_changed(SERVICE_STATUS_INTERFACE, - { "Current" => service_status_current }, []) - end - end - - def self.included(base) - base.class_eval do - dbus_interface SERVICE_STATUS_INTERFACE do - dbus_reader :service_status_all, "aa{sv}", dbus_name: "All" - dbus_reader :service_status_current, "u", dbus_name: "Current" - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index a99f91b731..ad39ce3373 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -21,7 +21,6 @@ require "y2storage/storage_manager" require "agama/dbus/base_object" -require "agama/dbus/interfaces/issues" require "agama/dbus/storage/iscsi_nodes_tree" require "agama/storage/config_conversions" require "agama/storage/encryption_settings" @@ -429,9 +428,7 @@ def json_issue(issue) { description: issue.description, class: issue.kind&.to_s, - details: issue.details&.to_s, - source: issue.source&.to_s, - severity: issue.severity&.to_s + details: issue.details&.to_s }.compact end diff --git a/service/lib/agama/dbus/types.rb b/service/lib/agama/dbus/types.rb deleted file mode 100644 index 0109dd5e40..0000000000 --- a/service/lib/agama/dbus/types.rb +++ /dev/null @@ -1,159 +0,0 @@ -# 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. - -module Agama - module DBus - # This module offers classes to help to validate the expected types from a D-Bus call when - # variant types are involved, typically using dictionaries. - # - # @example - # # Let's say there is a D-Bus method with the following signature: - # - # dbus_method :Calculate, "in settings:a{sv}" - # - # # The settings parameter will be transformed to a ruby Hash. This module provides a - # # {Checker} class that helps to validate the expected types. - # - # # Checks whether the value is a String - # checker = Types::Checker.new(String) - # checker.match?(settings["Foo"]) - # - # # Checks whether the value is bool - # checker = Types::Checker.new(Types::BOOL) - # checker.match?(settings["Foo"]) - # - # # Checks whether the value is an Array of String - # checker = Types::Checker.new(Types::Array.new(String)) - # checker.match?(settings["Foo"]) - # - # # Checks whether the value is a Hash of String keys and Integer values - # checker = Types::Checker.new(Types::Hash.new(k: String, v: Integer)) - # checker.match?(settings["Foo"]) - # - # See {HashValidator} for validating hashes coming from D-Bus according to a scheme. - module Types - # Type representing a boolean (true or false). - BOOL = :bool - - # Type representing an array of values. - class Array - # @return [Class, BOOL, Array, Hash, nil] - attr_reader :elements_type - - # @param elements_type [Class, BOOL, Array, Hash, nil] The type of the elements in the - # array. If nil, the type of the elements is not checked. - def initialize(elements_type = nil) - @elements_type = elements_type - end - end - - # Type representing a hash. - class Hash - # @return [Class, BOOL, Array, Hash, nil] - attr_reader :keys_type - - # @return [Class, BOOL, Array, Hash, nil] - attr_reader :values_type - - # @param key [Class, BOOL, Array, Hash, nil] The type of keys. If nil, the type of the keys - # is not checked. - # @param value [Class, BOOL, Array, Hash, nil] The type of values. If nil, the type of the - # values is not checked. - def initialize(key: nil, value: nil) - @keys_type = key - @values_type = value - end - end - - # Checks whether a value matches a type. - class Checker - # @param type [Class, BOOL, Array, Hash] The type to check. - def initialize(type) - @type = type - end - - # Whether the given value matches the type. - # - # @param value [Object] - # @return [Boolean] - def match?(value) - case type - when BOOL - match_bool?(value) - when Agama::DBus::Types::Array - match_array?(value) - when Agama::DBus::Types::Hash - match_hash?(value) - when Class, Module - value.is_a?(type) - else - false - end - end - - private - - # @return [Class, BOOL, Array, Hash] - attr_reader :type - - # Whether the value matches with {BOOL} type. - # - # @return [Boolean] - def match_bool?(value) - value.is_a?(TrueClass) || value.is_a?(FalseClass) - end - - # Whether the value matches with {Array} type. - # - # @return [Boolean] - def match_array?(value) - return false unless value.is_a?(::Array) - - if type.elements_type - checker = Checker.new(type.elements_type) - return value.all? { |v| checker.match?(v) } - end - - true - end - - # Whether the value matches with {Hash} type. - # - # @return [Boolean] - def match_hash?(value) - return false unless value.is_a?(::Hash) - - if type.keys_type - checker = Checker.new(Types::Array.new(type.keys_type)) - return false unless checker.match?(value.keys) - end - - if type.values_type - checker = Checker.new(Types::Array.new(type.values_type)) - return false unless checker.match?(value.values) - end - - true - end - end - end - end -end diff --git a/service/lib/agama/dbus/with_progress.rb b/service/lib/agama/dbus/with_progress.rb deleted file mode 100644 index 552de83822..0000000000 --- a/service/lib/agama/dbus/with_progress.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# 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 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. - -module Agama - module DBus - # Mixin to be included by D-Bus objects that need to register progress. - module WithProgress - # TODO: make D-Bus objects to own a ProgressManager instance instead of getting it from the - # backend instance. - # - # @return [Agama::ProgressManager] - def progress_manager - backend.progress_manager - end - - # @return [Agama::Progress, nil] - def progress - progress_manager.progress - end - end - end -end diff --git a/service/lib/agama/installation_phase.rb b/service/lib/agama/installation_phase.rb deleted file mode 100644 index 0bc4c9a1c4..0000000000 --- a/service/lib/agama/installation_phase.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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. - -module Agama - # Represents the installation phase of the manager service and allows to configure callbacks to be - # called when the installation phase value changes - class InstallationPhase - # Possible installation phase values - STARTUP = "startup" - CONFIG = "config" - INSTALL = "install" - FINISH = "finish" - - def initialize - @value = STARTUP - @on_change_callbacks = [] - end - - # Whether the current installation phase value is startup - # - # @return [Boolean] - def startup? - value == STARTUP - end - - # Whether the current installation phase value is config - # - # @return [Boolean] - def config? - value == CONFIG - end - - # Whether the current installation phase value is install - # - # @return [Boolean] - def install? - value == INSTALL - end - - # Whether the current installation phase value is finish - # - # @return [Boolean] - def finish? - value == FINISH - end - - # Sets the installation phase value to startup - # - # @note Callbacks are called. - # - # @return [self] - def startup - change_to(STARTUP) - self - end - - # Sets the installation phase value to config - # - # @note Callbacks are called. - # - # @return [self] - def config - change_to(CONFIG) - self - end - - # Sets the installation phase value to install - # - # @note Callbacks are called. - # - # @return [self] - def install - change_to(INSTALL) - self - end - - # Sets the installation phase value to finish - # - # @note Callbacks are called. - # - # @return [self] - def finish - change_to(FINISH) - self - end - - # Registers callbacks to be called when the installation phase value changes - # - # @param block [Proc] - def on_change(&block) - @on_change_callbacks << block - end - - private - - # @return [STARTUP, CONFIG, INSTALL] - attr_reader :value - - # Changes the installation phase value and runs the callbacks - # - # @param value [STARTUP, CONFIG, INSTALL] - def change_to(value) - @value = value - @on_change_callbacks.each(&:call) - end - end -end diff --git a/service/lib/agama/issue.rb b/service/lib/agama/issue.rb index 0e698c0dae..ea1e7404f1 100644 --- a/service/lib/agama/issue.rb +++ b/service/lib/agama/issue.rb @@ -45,53 +45,20 @@ class Issue # @return [String, nil] attr_reader :details - # Source of the issue, see {Source} - # - # @return [Symbol, nil] - attr_reader :source - - # Severity of the issue, see {Severity} - # - # @return [Symbol] - attr_reader :severity - # Kind of error # # It helps to identify the error without having to rely on the message # @return [Symbol] attr_reader :kind - # Defines possible sources - module Source - SYSTEM = :system - CONFIG = :config - end - - # Defines different severity levels - module Severity - WARN = :warn - ERROR = :error - end - # Constructor # # @param description [String] # @param details [String, nil] - # @param source [symbol, nil] - # @param severity [symbol] - def initialize(description, kind: :generic, details: "", source: nil, severity: Severity::WARN) + def initialize(description, kind: :generic, details: "") @description = description @kind = kind @details = details - @source = source - @severity = severity - end - - # Whether the issue has error severity - # - # @return [Boolean] - def error? - severity == Severity::ERROR end end end diff --git a/service/lib/agama/registered_addon.rb b/service/lib/agama/registered_addon.rb deleted file mode 100644 index 65622b1552..0000000000 --- a/service/lib/agama/registered_addon.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -# 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 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. - -module Agama - # Handles everything related to registration of system to SCC, RMT or similar. - class RegisteredAddon - # Name (id) of the addon product, e.g. "sle-ha" - # - # @return [String] - attr_reader :name - - # Version of the addon, e.g. "16.0" - # - # @return [String] - attr_reader :version - - # The addon version was explicitly specified by the user or it was autodetected. - # - # @return [Boolean] true if explicitly specified by user, false when autodetected - attr_reader :required_version - - # Code used for registering the addon. - # - # @return [String] empty string if the registration code is not required - attr_reader :reg_code - - def initialize(name, version, required_version, reg_code = "") - @name = name - @version = version - @required_version = required_version - @reg_code = reg_code - end - end -end diff --git a/service/lib/agama/service_status_recorder.rb b/service/lib/agama/service_status_recorder.rb deleted file mode 100644 index f0151b92af..0000000000 --- a/service/lib/agama/service_status_recorder.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "agama/dbus/service_status" - -module Agama - # Allows to record the status of services and to register callbacks to be called when a service - # status changes its value - class ServiceStatusRecorder - def initialize - @statuses = {} - @on_service_status_change_callbacks = [] - end - - # Saves the status of the given service and runs the callbacks if the status has changed - # - # @see ServiceStatus - # - # @param service_name [String] - # @param status [String] - def save(service_name, status) - return if @statuses[service_name] == status - - @statuses[service_name] = status - @on_service_status_change_callbacks.each(&:call) - end - - # Name of services with busy status - # - # @return [Array] - def busy_services - @statuses.select { |_service, status| status == DBus::ServiceStatus::BUSY }.keys - end - - # Registers callbacks to be called when saving a new status - # - # @param block [Proc] - def on_service_status_change(&block) - @on_service_status_change_callbacks << block - end - end -end diff --git a/service/lib/agama/storage/config_checkers/base.rb b/service/lib/agama/storage/config_checkers/base.rb index 66672c3957..e49faa7fb3 100644 --- a/service/lib/agama/storage/config_checkers/base.rb +++ b/service/lib/agama/storage/config_checkers/base.rb @@ -35,19 +35,13 @@ def issues private - # Creates an error issue. + # Creates an issue. # # @param message [String] # @param kind [Symbol, nil] if nil or ommited, default value defined by Agama::Issue # @return [Issue] def error(message, kind: nil) - issue_args = { - source: Agama::Issue::Source::CONFIG, - severity: Agama::Issue::Severity::ERROR - } - issue_args[:kind] = kind if kind - - Agama::Issue.new(message, **issue_args) + Agama::Issue.new(message, kind: kind) end end end diff --git a/service/lib/agama/storage/iscsi/manager.rb b/service/lib/agama/storage/iscsi/manager.rb index 616127def3..88b8977d84 100644 --- a/service/lib/agama/storage/iscsi/manager.rb +++ b/service/lib/agama/storage/iscsi/manager.rb @@ -363,9 +363,7 @@ def apply_target_config(target) # @param target [ISCSI::Configs::Target] # @return [Issue] def login_issue(target) - Issue.new(format(_("Cannot login to iSCSI target %s"), target.name), - source: Issue::Source::CONFIG, - severity: Issue::Severity::ERROR) + Issue.new(format(_("Cannot login to iSCSI target %s"), target.name)) end end end diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index 4c6bdf6fa2..ddb8ec968a 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -28,7 +28,6 @@ require "agama/storage/finisher" require "agama/storage/iscsi/manager" require "agama/storage/proposal" -require "agama/storage/proposal_settings" require "agama/with_issues" require "agama/with_locale" require "agama/with_progress_manager" @@ -203,10 +202,7 @@ def probing_issues return [] if y2storage_issues.nil? y2storage_issues.map do |y2storage_issue| - Issue.new(y2storage_issue.message, - details: y2storage_issue.details, - source: Issue::Source::SYSTEM, - severity: Issue::Severity::WARN) + Issue.new(y2storage_issue.message, details: y2storage_issue.details) end end @@ -216,9 +212,7 @@ def probing_issues def candidate_devices_issue return if proposal.storage_system.candidate_devices.any? - Issue.new("There is no suitable device for installation", - source: Issue::Source::SYSTEM, - severity: Issue::Severity::ERROR) + Issue.new("There is no suitable device for installation") end # Returns the client to ask questions diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb index d21a2b199d..c052dd577b 100644 --- a/service/lib/agama/storage/proposal.rb +++ b/service/lib/agama/storage/proposal.rb @@ -58,7 +58,7 @@ def calculated? # # @return [Boolean] def success? - calculated? && !proposal.failed? && issues.none?(&:error?) + calculated? && !proposal.failed? && issues.none? end # Default storage config according to the JSON schema. @@ -302,8 +302,7 @@ def storage_manager def failed_issue Issue.new( _("Cannot calculate a valid storage setup with the current configuration"), - source: Issue::Source::SYSTEM, - severity: Issue::Severity::ERROR + kind: :proposal ) end @@ -313,9 +312,8 @@ def failed_issue def exception_issue(error) Issue.new( _("A problem ocurred while calculating the storage setup"), - details: error.message, - source: Issue::Source::SYSTEM, - severity: Issue::Severity::ERROR + kind: :proposal, + details: error.message ) end end diff --git a/service/lib/agama/storage/proposal_strategies/autoyast.rb b/service/lib/agama/storage/proposal_strategies/autoyast.rb index 3f8b3ff392..be2e6b5428 100644 --- a/service/lib/agama/storage/proposal_strategies/autoyast.rb +++ b/service/lib/agama/storage/proposal_strategies/autoyast.rb @@ -58,13 +58,14 @@ def calculate issues_list: ay_issues ) proposal.propose + log_warnings ensure storage_manager.proposal = proposal end # @see Base#issues def issues - ay_issues.map { |i| agama_issue(i) } + ay_errors.map { |i| agama_issue(i) } end private @@ -81,13 +82,28 @@ def proposal_settings agama_default.to_y2storage(config: product_config) end + # Logs AutoYaST warnings + def log_warnings + ay_warnings.each { |w| logger.warn(w) } + end + + # AutoYaST warnings + # + # @return [Array<::Installation::AutoinstIssues::Issue> + def ay_warnings + ay_issues.select(&:warn?) + end + + # AutoYaST errors + # + # @return [Array<::Installation::AutoinstIssues::Issue> + def ay_errors + ay_issues.reject(&:warn?) + end + # Agama issue equivalent to the given AutoYaST issue def agama_issue(ay_issue) - Issue.new( - ay_issue.message, - source: Issue::Source::CONFIG, - severity: ay_issue.warn? ? Issue::Severity::WARN : Issue::Severity::ERROR - ) + Issue.new(ay_issue.message) end end end diff --git a/service/lib/agama/with_issues.rb b/service/lib/agama/with_issues.rb index dd45a4cf68..950ddce6bb 100644 --- a/service/lib/agama/with_issues.rb +++ b/service/lib/agama/with_issues.rb @@ -29,13 +29,6 @@ def issues @issues || [] end - # Whether there are errors - # - # @return [Boolean] - def errors? - issues.any?(&:error?) - end - # Sets the list of current issues # # @param issues [Array] diff --git a/service/lib/y2storage/agama_proposal.rb b/service/lib/y2storage/agama_proposal.rb index c7bbeae2e5..e6be3b93f9 100644 --- a/service/lib/y2storage/agama_proposal.rb +++ b/service/lib/y2storage/agama_proposal.rb @@ -80,11 +80,11 @@ def initialize(config, storage_system, product_config: nil, issues_list: nil) # @return [Proposal::AgamaSpaceMaker] attr_reader :space_maker - # Whether the list of issues generated so far already includes any serious error + # Whether there is any issue. # # @return [Boolean] - def fatal_error? - issues_list.any?(&:error?) + def issues? + issues_list.any? end # Calculates the proposal @@ -101,7 +101,7 @@ def calculate_proposal issues_list.concat(issues) - if fatal_error? + if issues? @devices = nil return @devices end @@ -118,12 +118,12 @@ def propose_devicegraph devicegraph = initial_devicegraph.dup calculate_initial_planned(devicegraph) - return if fatal_error? + return if issues? configure_ptable_types(devicegraph) devicegraph = clean_graph(devicegraph) complete_planned(devicegraph) - return if fatal_error? + return if issues? result = create_devices(devicegraph) result.devicegraph diff --git a/service/test/agama/dbus/clients/storage_test.rb b/service/test/agama/dbus/clients/storage_test.rb deleted file mode 100644 index 856f51f6e2..0000000000 --- a/service/test/agama/dbus/clients/storage_test.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-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/dbus/clients/storage" -require "dbus" - -describe Agama::DBus::Clients::Storage do - before do - allow(Agama::DBus::Bus).to receive(:current).and_return(bus) - allow(bus).to receive(:service).with("org.opensuse.Agama.Storage1").and_return(service) - allow(service).to receive(:[]).with("/org/opensuse/Agama/Storage1").and_return(dbus_object) - allow(dbus_object).to receive(:introspect) - end - - let(:bus) { instance_double(Agama::DBus::Bus) } - let(:service) { instance_double(::DBus::ProxyService) } - let(:dbus_object) { instance_double(::DBus::ProxyObject) } - - subject { described_class.new } - - describe "#probe" do - let(:dbus_object) { double(::DBus::ProxyObject, Probe: nil) } - - it "calls the D-Bus Probe method" do - expect(dbus_object).to receive(:Probe) - - subject.probe - end - - context "when a block is given" do - it "passes the block to the Probe method (async)" do - callback = proc {} - expect(dbus_object).to receive(:Probe) do |&block| - expect(block).to be(callback) - end - - subject.probe(&callback) - end - end - end - - describe "#install" do - let(:dbus_object) { double(::DBus::ProxyObject, Install: nil) } - - it "calls the D-Bus Install method" do - expect(dbus_object).to receive(:Install) - - subject.install - end - end - - describe "#finish" do - let(:dbus_object) { double(::DBus::ProxyObject, Finish: nil) } - - it "calls the D-Bus Install method" do - expect(dbus_object).to receive(:Finish) - - subject.finish - end - end -end diff --git a/service/test/agama/dbus/clients/with_issues_examples.rb b/service/test/agama/dbus/clients/with_issues_examples.rb deleted file mode 100644 index 508de6425a..0000000000 --- a/service/test/agama/dbus/clients/with_issues_examples.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2025] 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/issue" - -shared_examples "issues" do - before do - allow(service).to receive(:root).and_return(root_node) - - allow(dbus_object1).to receive(:[]).with("org.opensuse.Agama1.Issues") - .and_return(issues_interface1) - - allow(dbus_object3).to receive(:[]).with("org.opensuse.Agama1.Issues") - .and_return(issues_interface3) - - allow(issues_interface1).to receive(:[]).with("All").and_return(issues1) - allow(issues_interface3).to receive(:[]).with("All").and_return(issues3) - end - - let(:root_node) do - instance_double(DBus::Node, descendant_objects: [dbus_object1, dbus_object2, dbus_object3]) - end - - let(:dbus_object1) do - instance_double(DBus::ProxyObject, - interfaces: ["org.opensuse.Agama1.Test", "org.opensuse.Agama1.Issues"]) - end - - let(:dbus_object2) do - instance_double(DBus::ProxyObject, interfaces: ["org.opensuse.Agama1.Test"]) - end - - let(:dbus_object3) do - instance_double(DBus::ProxyObject, interfaces: ["org.opensuse.Agama1.Issues"]) - end - - let(:issues_interface1) { instance_double(DBus::ProxyObjectInterface) } - - let(:issues_interface3) { instance_double(DBus::ProxyObjectInterface) } - - let(:issues1) do - [ - ["Issue 1", "generic", "Details 1", 1, 0], - ["Issue 2", "generic", "Details 2", 2, 1] - ] - end - - let(:issues3) do - [ - ["Issue 3", "generic", "Details 3", 1, 0] - ] - end - - describe "#issues" do - it "returns the list of issues from all objects" do - expect(subject.issues).to all(be_a(Agama::Issue)) - - expect(subject.issues).to contain_exactly( - an_object_having_attributes( - description: "Issue 1", - details: "Details 1", - source: Agama::Issue::Source::SYSTEM, - severity: Agama::Issue::Severity::WARN - ), - an_object_having_attributes( - description: "Issue 2", - details: "Details 2", - source: Agama::Issue::Source::CONFIG, - severity: Agama::Issue::Severity::ERROR - ), - an_object_having_attributes( - description: "Issue 3", - details: "Details 3", - source: Agama::Issue::Source::SYSTEM, - severity: Agama::Issue::Severity::WARN - ) - ) - end - end - - describe "#errors?" do - context "if there is any error" do - it "returns true" do - expect(subject.errors?).to eq(true) - end - end - - context "if there is no error" do - let(:issues1) { [] } - - it "returns false" do - expect(subject.errors?).to eq(false) - end - end - end -end diff --git a/service/test/agama/dbus/clients/with_progress_examples.rb b/service/test/agama/dbus/clients/with_progress_examples.rb deleted file mode 100644 index 19093c841a..0000000000 --- a/service/test/agama/dbus/clients/with_progress_examples.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "dbus" - -shared_examples "progress" do - describe "#on_progress_change" do - before do - allow(dbus_object).to receive(:path).and_return("/org/opensuse/Agama/Test") - allow(dbus_object).to receive(:[]).with("org.freedesktop.DBus.Properties") - .and_return(properties_iface) - allow(properties_iface).to receive(:on_signal) - end - - let(:properties_iface) { instance_double(::DBus::ProxyObjectInterface) } - - context "if there are no callbacks for changes in properties" do - it "subscribes to properties change signal" do - expect(properties_iface).to receive(:on_signal) - subject.on_progress_change { "test" } - end - end - - context "if there already are callbacks for changes in properties" do - before do - subject.on_progress_change { "test" } - end - - it "does not subscribe to properties change signal again" do - expect(properties_iface).to_not receive(:on_signal) - subject.on_progress_change { "test" } - end - end - end -end diff --git a/service/test/agama/dbus/clients/with_service_status_examples.rb b/service/test/agama/dbus/clients/with_service_status_examples.rb deleted file mode 100644 index 3358fc41a3..0000000000 --- a/service/test/agama/dbus/clients/with_service_status_examples.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "dbus" -require "agama/dbus/service_status" -require "agama/dbus/interfaces/service_status" - -shared_examples "service status" do - before do - allow(dbus_object).to receive(:[]).with("org.opensuse.Agama1.ServiceStatus") - .and_return(service_status_iface) - end - - let(:service_status_iface) { instance_double(::DBus::ProxyObjectInterface) } - - describe "#service_status" do - before do - allow(service_status_iface).to receive(:[]).with("Current") - .and_return(Agama::DBus::Interfaces::ServiceStatus::SERVICE_STATUS_BUSY) - end - - it "returns the value of the service status" do - expect(subject.service_status).to eq(Agama::DBus::ServiceStatus::BUSY) - end - end - - describe "#on_service_status_change" do - before do - allow(dbus_object).to receive(:path).and_return("/org/opensuse/Agama/Test") - allow(dbus_object).to receive(:[]).with("org.freedesktop.DBus.Properties") - .and_return(properties_iface) - allow(properties_iface).to receive(:on_signal) - end - - let(:properties_iface) { instance_double(::DBus::ProxyObjectInterface) } - - context "if there are no callbacks for changes in properties" do - it "subscribes to properties change signal" do - expect(properties_iface).to receive(:on_signal) - subject.on_service_status_change { "test" } - end - end - - context "if there already are callbacks for changes in properties" do - before do - subject.on_service_status_change { "test" } - end - - it "does not subscribe to properties change signal again" do - expect(properties_iface).to_not receive(:on_signal) - subject.on_service_status_change { "test" } - end - end - end -end diff --git a/service/test/agama/dbus/hash_validator_test.rb b/service/test/agama/dbus/hash_validator_test.rb deleted file mode 100644 index 69b995af31..0000000000 --- a/service/test/agama/dbus/hash_validator_test.rb +++ /dev/null @@ -1,197 +0,0 @@ -# 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/dbus/hash_validator" -require "agama/dbus/types" - -describe Agama::DBus::HashValidator do - subject { described_class.new(value, scheme: scheme) } - - let(:scheme) do - { - "Name" => String, - "Surname" => Agama::DBus::Types::Array.new(String), - "Age" => Integer, - "Height" => Integer, - "Children" => Array - } - end - - describe "#valid?" do - context "if there is any key with unexpected type" do - let(:value) do - { - "Gender" => "Male", - "Height" => true - } - end - - it "returns false" do - expect(subject.valid?).to eq(false) - end - end - - context "if there is no key with unexpected type" do - let(:value) do - { - "Name" => "John", - "Gender" => "Male", - "Height" => 175 - } - end - - it "returns true" do - expect(subject.valid?).to eq(true) - end - end - end - - describe "#valid_keys" do - let(:value) do - { - "Name" => "John", - "Gender" => "Male", - "Age" => 45, - "Height" => true, - "Children" => ["Mark", "Zara"] - } - end - - it "returns the hash keys defined in the scheme and with the type indicated in the scheme" do - expect(subject.valid_keys).to eq(["Name", "Age", "Children"]) - end - end - - describe "#wrong_type_keys" do - context "if the hash contains the same types as the scheme" do - let(:value) do - { - "Name" => "John", - "Age" => 45, - "Children" => ["Mark", "Zara"] - } - end - - it "returns an empty list" do - expect(subject.wrong_type_keys).to eq([]) - end - end - - context "if the hash contains types different to the scheme" do - let(:value) do - { - "Name" => true, - "Age" => 45, - "Children" => "none" - } - end - - it "returns the keys with wrong type" do - expect(subject.wrong_type_keys).to eq(["Name", "Children"]) - end - end - end - - describe "#extra_keys" do - context "if the hash does not contain keys that are not included in the scheme" do - let(:value) do - { - "Name" => "Jhon", - "Children" => [] - } - end - - it "returns an empty list" do - expect(subject.extra_keys).to eq([]) - end - end - - context "if the hash contains some keys that are not included in the scheme" do - let(:value) do - { - "Name" => "Jhon", - "Gender" => "Male", - "Birthday" => nil, - "Children" => [] - } - end - - it "returns a list with the extra keys" do - expect(subject.extra_keys).to eq(["Gender", "Birthday"]) - end - end - end - - describe "#missing_keys" do - context "if the hash contains all the keys defined in the scheme" do - let(:value) do - { - "Name" => "Jhon", - "Surname" => [], - "Age" => 45, - "Height" => 176, - "Children" => [] - } - end - - it "returns an empty list" do - expect(subject.missing_keys).to eq([]) - end - end - - context "if the hash does not contain any of the keys defined in the scheme" do - let(:value) do - { - "Surname" => [], - "Age" => 45, - "Height" => 176 - } - end - - it "returns a list with the missing keys" do - expect(subject.missing_keys).to eq(["Name", "Children"]) - end - end - end - - describe "#issues" do - let(:value) do - { - "Name" => "John", - "Age" => 45, - "Gender" => "Male", - "Birthday" => nil, - "Height" => "175", - "Children" => {} - } - end - - it "generates an issue for each extra key and for each wrong type" do - expect(subject.issues).to contain_exactly( - /Unknown .* Gender/, - /Unknown .* Birthday/, - /Height must be/, - /Children must be/ - ) - end - end -end diff --git a/service/test/agama/dbus/interfaces/issues_test.rb b/service/test/agama/dbus/interfaces/issues_test.rb index 6e10c3620d..b767699c57 100644 --- a/service/test/agama/dbus/interfaces/issues_test.rb +++ b/service/test/agama/dbus/interfaces/issues_test.rb @@ -33,15 +33,8 @@ def initialize def issues [ - Agama::Issue.new("Issue 1", - details: "Details 1", - source: Agama::Issue::Source::SYSTEM, - severity: Agama::Issue::Severity::WARN), - Agama::Issue.new("Issue 2", - details: "Details 2", - source: Agama::Issue::Source::CONFIG, - severity: Agama::Issue::Severity::ERROR, - kind: :missing_product) + Agama::Issue.new("Issue 1", details: "Details 1"), + Agama::Issue.new("Issue 2", details: "Details 2", kind: :missing_product) ] end end @@ -60,8 +53,8 @@ def issues result = subject.dbus_issues expect(result).to contain_exactly( - ["Issue 1", "generic", "Details 1", 1, 0], - ["Issue 2", "missing_product", "Details 2", 2, 1] + ["Issue 1", "generic", "Details 1"], + ["Issue 2", "missing_product", "Details 2"] ) end end diff --git a/service/test/agama/dbus/interfaces/progress_test.rb b/service/test/agama/dbus/interfaces/progress_test.rb deleted file mode 100644 index 1c8047ece8..0000000000 --- a/service/test/agama/dbus/interfaces/progress_test.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022-2025] 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/dbus/base_object" -require "agama/dbus/interfaces/progress" -require "agama/dbus/with_progress" -require "agama/with_progress_manager" - -class DBusObjectWithProgressInterface < Agama::DBus::BaseObject - include Agama::DBus::WithProgress - include Agama::DBus::Interfaces::Progress - - def initialize - super("org.opensuse.Agama.UnitTests") - end - - def backend - @backend ||= Backend.new - end - - class Backend - include Agama::WithProgressManager - end -end - -describe DBusObjectWithProgressInterface do - let(:progress) { subject.backend.progress } - - let(:progress_interface) { Agama::DBus::Interfaces::Progress::PROGRESS_INTERFACE } - - it "defines Progress D-Bus interface" do - expect(subject.intfs.keys).to include(progress_interface) - end - - describe "#progress_total_steps" do - context "if there is no progress" do - it "returns 0" do - expect(subject.progress_total_steps).to eq(0) - end - end - - context " if there is a progress" do - before do - subject.backend.start_progress_with_size(2) - end - - it "returns the total number of steps of the progress" do - expect(subject.progress_total_steps).to eq(2) - end - end - end - - describe "#progress_current_step" do - context "if there is no progress" do - it "returns id 0 and empty descreption" do - expect(subject.progress_current_step).to eq([0, ""]) - end - end - - context " if there is a progress" do - before do - subject.backend.start_progress_with_size(2) - end - - before do - progress.step("test") - end - - it "returns the id and description of the current step" do - expect(subject.progress_current_step).to eq([1, "test"]) - end - end - end - - describe "#progress_finished" do - context "if there is no progress" do - it "returns true" do - expect(subject.progress_finished).to eq(true) - end - end - - context " if there is a progress" do - before do - subject.backend.start_progress_with_size(2) - end - - context "and the progress is not started" do - it "returns false" do - expect(subject.progress_finished).to eq(false) - end - end - - context "and the progress is started but not finished yet" do - before do - progress.step("step 1") - end - - it "returns false" do - expect(subject.progress_finished).to eq(false) - end - end - - context "and the progress is finished" do - before do - progress.step("step 1") - progress.step("step 2") - end - - it "returns true" do - expect(subject.progress_finished).to eq(true) - end - end - end - end - - describe "#progress_properties" do - context "when steps are not known in advance" do - before do - subject.backend.start_progress_with_size(2) - progress.step("step 1") - end - - it "returns de D-Bus properties of the progress interface" do - expected_properties = { - "TotalSteps" => 2, - "CurrentStep" => [1, "step 1"], - "Finished" => false, - "Steps" => [] - } - expect(subject.progress_properties).to eq(expected_properties) - end - end - - context "when steps are known in advance" do - before do - subject.backend.start_progress_with_descriptions("step 1", "step 2") - progress.step - end - - it "includes the steps" do - expected_properties = { - "TotalSteps" => 2, - "CurrentStep" => [1, "step 1"], - "Finished" => false, - "Steps" => ["step 1", "step 2"] - } - expect(subject.progress_properties).to eq(expected_properties) - end - end - end - - describe "#register_progress_callbacks" do - it "register callbacks to be called when the progress changes" do - subject.register_progress_callbacks - subject.backend.start_progress_with_size(2) - - expect(subject).to receive(:dbus_properties_changed) - .with(progress_interface, anything, anything) - expect(subject).to receive(:ProgressChanged) - .with(2, [1, "step 1"], false, []) - - progress.step("step 1") - end - - it "register callbacks to be called when the progress finishes" do - subject.register_progress_callbacks - subject.backend.start_progress_with_size(2) - - expect(subject).to receive(:dbus_properties_changed) - .with(progress_interface, anything, anything) - expect(subject).to receive(:ProgressChanged) - .with(2, [0, ""], true, []) - - progress.finish - end - end -end diff --git a/service/test/agama/dbus/interfaces/service_status_test.rb b/service/test/agama/dbus/interfaces/service_status_test.rb deleted file mode 100644 index 5756704e59..0000000000 --- a/service/test/agama/dbus/interfaces/service_status_test.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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 "dbus" -require "agama/dbus/interfaces/service_status" -require "agama/dbus/with_service_status" - -class DBusObjectWithServiceStatusInterface < ::DBus::Object - include Agama::DBus::WithServiceStatus - include Agama::DBus::Interfaces::ServiceStatus - - def initialize - super("org.opensuse.Agama.UnitTests") - end -end - -describe DBusObjectWithServiceStatusInterface do - let(:service_status_interface) do - Agama::DBus::Interfaces::ServiceStatus::SERVICE_STATUS_INTERFACE - end - - it "defines ServiceStatus D-Bus interface" do - expect(subject.intfs.keys).to include(service_status_interface) - end - - describe "#service_status_all" do - it "includes all possible values for the service status" do - labels = subject.service_status_all.map { |i| i["label"] } - expect(labels).to contain_exactly("idle", "busy") - end - - it "associates 'idle' with the id 0" do - idle = subject.service_status_all.find { |i| i["label"] == "idle" } - expect(idle["id"]).to eq(0) - end - - it "associates 'busy' with the id 1" do - busy = subject.service_status_all.find { |i| i["label"] == "busy" } - expect(busy["id"]).to eq(1) - end - end - - describe "#service_status_current" do - context "when the current service status is idle" do - before do - subject.service_status.idle - end - - it "returns 0" do - expect(subject.service_status_current).to eq(0) - end - end - - context "when the current service status is busy" do - before do - subject.service_status.busy - end - - it "returns 1" do - expect(subject.service_status_current).to eq(1) - end - end - end - - describe "#register_service_status_callbacks" do - it "register callbacks to be called when the service status changes" do - subject.register_service_status_callbacks - - expect(subject).to receive(:dbus_properties_changed) - .with(service_status_interface, anything, anything) - - subject.service_status.busy - end - end -end diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index 7a5f301f9f..1c195c0c7e 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -69,6 +69,8 @@ def parse(string) allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true) # Speed up tests by avoiding looking up by name in the system allow(Y2Storage::BlkDevice).to receive(:find_by_any_name) + allow(Y2Storage::BootRequirementsStrategies::Analyzer) + .to receive(:bls_bootloader_proposed?).and_return(false) allow(Yast::Arch).to receive(:s390).and_return false allow(backend).to receive(:on_configure) @@ -1166,10 +1168,10 @@ def parse(string) result = parse(subject.recover_issues) expect(result).to include( a_hash_including( - description: /cannot calculate a valid storage setup/i, severity: "error" + description: /cannot calculate a valid storage setup/i ), a_hash_including( - description: /boot device cannot be automatically/i, severity: "error" + description: /boot device cannot be automatically/i ) ) end diff --git a/service/test/agama/dbus/types_test.rb b/service/test/agama/dbus/types_test.rb deleted file mode 100644 index 6661508be7..0000000000 --- a/service/test/agama/dbus/types_test.rb +++ /dev/null @@ -1,167 +0,0 @@ -# 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/dbus/types" - -describe Agama::DBus::Types::Checker do - describe "#match?" do - describe "for Bool type" do - subject { described_class.new(Agama::DBus::Types::BOOL) } - - it "returns true if the given value is true or false" do - expect(subject.match?(true)).to eq(true) - expect(subject.match?(false)).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?(nil)).to eq(false) - expect(subject.match?("foo")).to eq(false) - expect(subject.match?(10)).to eq(false) - expect(subject.match?([])).to eq(false) - expect(subject.match?({})).to eq(false) - end - end - - describe "for Array type" do - subject { described_class.new(array_type) } - - let(:array_type) { Agama::DBus::Types::Array.new } - - it "returns true if the given value is an array" do - expect(subject.match?([])).to eq(true) - expect(subject.match?([1, 2])).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?(nil)).to eq(false) - expect(subject.match?("foo")).to eq(false) - expect(subject.match?(10)).to eq(false) - expect(subject.match?(true)).to eq(false) - expect(subject.match?({})).to eq(false) - end - - context "if the elements of the array have to be of a specific type" do - let(:array_type) { Agama::DBus::Types::Array.new(String) } - - it "returns true if all the elements match the given type" do - expect(subject.match?([])).to eq(true) - expect(subject.match?(["foo", "bar"])).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?([nil])).to eq(false) - expect(subject.match?([10])).to eq(false) - expect(subject.match?([true])).to eq(false) - expect(subject.match?([[]])).to eq(false) - expect(subject.match?([{}])).to eq(false) - end - end - end - - describe "for Hash type" do - subject { described_class.new(hash_type) } - - let(:hash_type) { Agama::DBus::Types::Hash.new } - - it "returns true if the given value is an hash" do - expect(subject.match?({})).to eq(true) - expect(subject.match?({ foo: "", bar: 1 })).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?(nil)).to eq(false) - expect(subject.match?("foo")).to eq(false) - expect(subject.match?(10)).to eq(false) - expect(subject.match?(true)).to eq(false) - expect(subject.match?([])).to eq(false) - end - - context "if the keys of the hash have to be of a specific type" do - let(:hash_type) { Agama::DBus::Types::Hash.new(key: String) } - - it "returns true if all the keys match the given type" do - expect(subject.match?({})).to eq(true) - expect(subject.match?({ "foo" => "", "bar" => 1 })).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?({ nil: 1 })).to eq(false) - expect(subject.match?({ a: 1 })).to eq(false) - expect(subject.match?({ 10 => 1 })).to eq(false) - expect(subject.match?({ true => 1 })).to eq(false) - expect(subject.match?({ [] => 1 })).to eq(false) - expect(subject.match?({ {} => 1 })).to eq(false) - end - end - - context "if the values of the hash have to be of a specific type" do - let(:hash_type) { Agama::DBus::Types::Hash.new(value: Integer) } - - it "returns true if all the values match the given type" do - expect(subject.match?({})).to eq(true) - expect(subject.match?({ "foo" => 1, bar: 2 })).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?({ foo: nil })).to eq(false) - expect(subject.match?({ foo: 1.0 })).to eq(false) - expect(subject.match?({ foo: "" })).to eq(false) - expect(subject.match?({ foo: [] })).to eq(false) - expect(subject.match?({ foo: {} })).to eq(false) - end - end - - context "if the keys and the values of the hash have to be of a specific type" do - let(:hash_type) do - Agama::DBus::Types::Hash.new(key: String, value: Agama::DBus::Types::BOOL) - end - - it "returns true if all the keys and values match the given types" do - expect(subject.match?({})).to eq(true) - expect(subject.match?({ "foo" => true, "bar" => false })).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?({ "foo" => nil })).to eq(false) - expect(subject.match?({ foo: true })).to eq(false) - end - end - end - - describe "for other types" do - subject { described_class.new(Array) } - - it "returns true if the given value is an instance of the given type" do - expect(subject.match?([])).to eq(true) - expect(subject.match?([1, 2])).to eq(true) - end - - it "returns false otherwise" do - expect(subject.match?(nil)).to eq(false) - expect(subject.match?("foo")).to eq(false) - expect(subject.match?(10)).to eq(false) - expect(subject.match?(true)).to eq(false) - expect(subject.match?({})).to eq(false) - end - end - end -end diff --git a/service/test/agama/installation_phase_test.rb b/service/test/agama/installation_phase_test.rb deleted file mode 100644 index 8eb08c427e..0000000000 --- a/service/test/agama/installation_phase_test.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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/installation_phase" - -describe Agama::InstallationPhase do - let(:logger) { Logger.new($stdout, level: :warn) } - - before do - subject.on_change { logger.info("change phase") } - end - - describe "startup?" do - context "if the installation phase is startup" do - before do - subject.startup - end - - it "returns true" do - expect(subject.startup?).to eq(true) - end - end - - context "if the installation phase is not startup" do - before do - subject.config - end - - it "returns false" do - expect(subject.startup?).to eq(false) - end - end - end - - describe "config?" do - context "if the installation phase is config" do - before do - subject.config - end - - it "returns true" do - expect(subject.config?).to eq(true) - end - end - - context "if the installation phase is not config" do - before do - subject.startup - end - - it "returns false" do - expect(subject.config?).to eq(false) - end - end - end - - describe "install?" do - context "if the installation phase is install" do - before do - subject.install - end - - it "returns true" do - expect(subject.install?).to eq(true) - end - end - - context "if the installation phase is not install" do - before do - subject.config - end - - it "returns false" do - expect(subject.install?).to eq(false) - end - end - end - - describe "finish?" do - context "if the installation phase is finish" do - before do - subject.finish - end - - it "returns true" do - expect(subject.finish?).to eq(true) - end - end - - context "if the installation phase is not finish" do - before do - subject.config - end - - it "returns false" do - expect(subject.finish?).to eq(false) - end - end - end - - describe "#startup" do - it "sets the installation phase to startup" do - subject.startup - expect(subject.startup?).to eq(true) - end - - it "runs the 'on_change' callbacks" do - expect(logger).to receive(:info).with(/change phase/) - subject.startup - end - end - - describe "#config" do - it "sets the installation phase to config" do - subject.config - expect(subject.config?).to eq(true) - end - - it "runs the 'on_change' callbacks" do - expect(logger).to receive(:info).with(/change phase/) - subject.config - end - end - - describe "#install" do - it "sets the installation phase to install" do - subject.install - expect(subject.install?).to eq(true) - end - - it "runs the 'on_change' callbacks" do - expect(logger).to receive(:info).with(/change phase/) - subject.install - end - end -end diff --git a/service/test/agama/issue_test.rb b/service/test/agama/issue_test.rb deleted file mode 100644 index ae2a00d858..0000000000 --- a/service/test/agama/issue_test.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023] 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/issue" - -describe Agama::Issue do - subject { described_class.new("Issue test", severity: severity) } - - describe "#error?" do - context "if the issue has warn severity" do - let(:severity) { Agama::Issue::Severity::WARN } - - it "returns false" do - expect(subject.error?).to eq(false) - end - end - - context "if the issue has error severity" do - let(:severity) { Agama::Issue::Severity::ERROR } - - it "returns true" do - expect(subject.error?).to eq(true) - end - end - end -end diff --git a/service/test/agama/service_status_recorder_test.rb b/service/test/agama/service_status_recorder_test.rb deleted file mode 100644 index bb2f4e7372..0000000000 --- a/service/test/agama/service_status_recorder_test.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2022] 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/service_status_recorder" -require "agama/dbus/service_status" - -describe Agama::ServiceStatusRecorder do - let(:logger) { Logger.new($stdout, level: :warn) } - - let(:idle) { Agama::DBus::ServiceStatus::IDLE } - let(:busy) { Agama::DBus::ServiceStatus::BUSY } - - before do - subject.on_service_status_change { logger.info("change status") } - end - - describe "#save" do - it "stores the status of a service" do - subject.save("org.opensuse.Agama.Test", busy) - expect(subject.busy_services).to include("org.opensuse.Agama.Test") - end - - context "when the given service status is different to the stored one" do - before do - subject.save("org.opensuse.Agama.Test", busy) - end - - it "stores the new status" do - subject.save("org.opensuse.Agama.Test", idle) - expect(subject.busy_services).to_not include("org.opensuse.Agama.Test") - end - - it "runs the callbacks" do - expect(logger).to receive(:info).with(/change status/) - subject.save("org.opensuse.Agama.Test", idle) - end - end - - context "when the given service status is the same as the stored one" do - before do - subject.save("org.opensuse.Agama.Test", busy) - end - - it "does not run the callbacks" do - expect(logger).to_not receive(:info).with(/change status/) - subject.save("org.opensuse.Agama.Test", busy) - end - end - end - - describe "#busy_services" do - before do - subject.save("org.opensuse.Agama.Test1", busy) - subject.save("org.opensuse.Agama.Test2", idle) - subject.save("org.opensuse.Agama.Test3", busy) - end - - it "returns the name of the busy services" do - expect(subject.busy_services).to contain_exactly( - "org.opensuse.Agama.Test1", - "org.opensuse.Agama.Test3" - ) - end - end -end diff --git a/service/test/agama/storage/autoyast_proposal_test.rb b/service/test/agama/storage/autoyast_proposal_test.rb index 1e0b21e1b1..c9a539746b 100644 --- a/service/test/agama/storage/autoyast_proposal_test.rb +++ b/service/test/agama/storage/autoyast_proposal_test.rb @@ -169,9 +169,7 @@ def root_filesystem(disk) it "registers a fatal issue due to the lack of root" do subject.calculate_autoyast(partitioning) expect(subject.issues).to include( - an_object_having_attributes( - description: /No root/, severity: Agama::Issue::Severity::ERROR - ) + an_object_having_attributes(description: /No root/) ) end end @@ -191,7 +189,7 @@ def root_filesystem(disk) subject.calculate_autoyast(partitioning) expect(subject.issues).to include( an_object_having_attributes( - description: /Missing element 'use'/, severity: Agama::Issue::Severity::ERROR + description: /Missing element 'use'/ ) ) end @@ -224,7 +222,7 @@ def root_filesystem(disk) subject.calculate_autoyast(partitioning) expect(subject.issues).to include( an_object_having_attributes( - description: /Cannot calculate/, severity: Agama::Issue::Severity::ERROR + description: /Cannot calculate/ ) ) end @@ -294,15 +292,22 @@ def root_filesystem(disk) expect(partitions.map(&:id)).to_not include Y2Storage::PartitionId::ESP end - it "register a non-fatal issue" do + it "does not register an issue" do subject.calculate_autoyast(partitioning) - expect(subject.issues).to include( + expect(subject.issues).to_not include( an_object_having_attributes( - description: /partitions recommended for booting/, - severity: Agama::Issue::Severity::WARN + description: /partitions recommended for booting/ ) ) end + + it "logs a warning" do + allow(logger).to receive(:info) + expect(logger).to receive(:warn) do |issue| + expect(issue.message).to match(/partitions recommended for booting/) + end + subject.calculate_autoyast(partitioning) + end end end @@ -352,7 +357,7 @@ def root_filesystem(disk) subject.calculate_autoyast(partitioning) expect(subject.issues).to include( an_object_having_attributes( - description: /Cannot calculate/, severity: Agama::Issue::Severity::ERROR + description: /Cannot calculate/ ) ) end diff --git a/service/test/agama/storage/config_checker_test.rb b/service/test/agama/storage/config_checker_test.rb index 48705d37e7..14fd203055 100644 --- a/service/test/agama/storage/config_checker_test.rb +++ b/service/test/agama/storage/config_checker_test.rb @@ -53,7 +53,6 @@ it "includes the boot issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_root, description: "The boot device cannot be automatically selected" ) @@ -75,7 +74,6 @@ it "includes the drive issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :search, description: "Mandatory device /dev/vda not found" ) @@ -96,7 +94,6 @@ it "includes the partition issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :filesystem, description: "Missing file system type for '/'" ) @@ -113,7 +110,6 @@ it "includes the MD RAID issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_such_alias, description: /no MD RAID member device with alias 'disk1'/ ) @@ -134,7 +130,6 @@ it "includes the partition issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :filesystem, description: "Missing file system type for '/'" ) @@ -151,7 +146,6 @@ it "includes the volume group issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, description: /without name/ ) end @@ -171,7 +165,6 @@ it "includes the logical volume issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :filesystem, description: "Missing file system type for '/'" ) @@ -210,7 +203,6 @@ it "includes an issue for the missing mount path" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :required_filesystems, description: /file system for \/ is/ ) @@ -273,7 +265,6 @@ it "includes the expected issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :vg_target_devices, description: /The device 'disk1' is used several times/ ) diff --git a/service/test/agama/storage/config_checkers/alias_test.rb b/service/test/agama/storage/config_checkers/alias_test.rb index 5a496ead67..9d93cc0d42 100644 --- a/service/test/agama/storage/config_checkers/alias_test.rb +++ b/service/test/agama/storage/config_checkers/alias_test.rb @@ -26,7 +26,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :overused_alias, description: /alias '#{device_alias}' is used by more than one/ ) @@ -146,7 +145,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :formatted_with_user, description: /alias '#{device_alias}' cannot be formatted because it is used/ ) @@ -222,7 +220,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :partitioned_with_user, description: /alias '#{device_alias}' cannot be partitioned because it is used/ ) diff --git a/service/test/agama/storage/config_checkers/boot_test.rb b/service/test/agama/storage/config_checkers/boot_test.rb index a59c348f62..673e4eef57 100644 --- a/service/test/agama/storage/config_checkers/boot_test.rb +++ b/service/test/agama/storage/config_checkers/boot_test.rb @@ -51,7 +51,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_such_alias, description: /There is no boot device with alias '.*'/ ) @@ -68,7 +67,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_root, description: /The boot device cannot be automatically selected/ ) diff --git a/service/test/agama/storage/config_checkers/encryption_test.rb b/service/test/agama/storage/config_checkers/encryption_test.rb index 8ba1101e61..7beb4151d2 100644 --- a/service/test/agama/storage/config_checkers/encryption_test.rb +++ b/service/test/agama/storage/config_checkers/encryption_test.rb @@ -54,7 +54,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /No passphrase/ ) @@ -79,7 +78,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /'Pervasive Volume Encryption' is not available/ ) @@ -104,7 +102,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /'TPM-Based Full Disk Encrytion' is not available/ ) @@ -124,7 +121,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /'Encryption with Volatile Protected Key' is not a suitable/ ) diff --git a/service/test/agama/storage/config_checkers/examples.rb b/service/test/agama/storage/config_checkers/examples.rb index a5bf1d6ab8..4e4cd96356 100644 --- a/service/test/agama/storage/config_checkers/examples.rb +++ b/service/test/agama/storage/config_checkers/examples.rb @@ -31,7 +31,6 @@ it "includes the search issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :search, description: "Mandatory device /test not found" ) @@ -48,7 +47,6 @@ it "includes the filesystem issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :filesystem, description: "Missing file system type for '/'" ) @@ -65,7 +63,6 @@ it "includes the encryption issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /No passphrase .*/ ) @@ -88,7 +85,6 @@ it "includes the partition issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :search, description: "Mandatory partition not found" ) @@ -110,7 +106,6 @@ it "includes the alias issues" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :overused_alias, description: /alias '#{device_alias}' is used by more than one/ ) diff --git a/service/test/agama/storage/config_checkers/filesystem_test.rb b/service/test/agama/storage/config_checkers/filesystem_test.rb index d84af9a3db..e510cca31a 100644 --- a/service/test/agama/storage/config_checkers/filesystem_test.rb +++ b/service/test/agama/storage/config_checkers/filesystem_test.rb @@ -53,7 +53,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :filesystem, description: /type 'FAT' is not suitable for '\/'/ ) @@ -96,7 +95,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :filesystem, description: /Missing file system type for '\/'/ ) diff --git a/service/test/agama/storage/config_checkers/logical_volume_test.rb b/service/test/agama/storage/config_checkers/logical_volume_test.rb index c28b6986b0..72aa09ec1c 100644 --- a/service/test/agama/storage/config_checkers/logical_volume_test.rb +++ b/service/test/agama/storage/config_checkers/logical_volume_test.rb @@ -64,7 +64,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_such_alias, description: /no LVM thin pool/ ) @@ -77,7 +76,6 @@ it "does not include an issue" do issues = subject.issues expect(issues).to_not include an_object_having_attributes( - error?: true, kind: :no_such_alias, description: /no LVM thin pool/ ) diff --git a/service/test/agama/storage/config_checkers/md_raid_test.rb b/service/test/agama/storage/config_checkers/md_raid_test.rb index a9848c69e5..45b36b5d4b 100644 --- a/service/test/agama/storage/config_checkers/md_raid_test.rb +++ b/service/test/agama/storage/config_checkers/md_raid_test.rb @@ -76,7 +76,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :md_raid, description: /MD RAID without level/ ) @@ -107,7 +106,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :md_raid, description: "At least 2 devices are required for raid0" ) @@ -120,7 +118,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_such_alias, description: /no MD RAID member device with alias 'disk2'/ ) @@ -154,7 +151,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :reused_md_member, description: /.*vda.*cannot be formatted.*part of.*md0/ ) @@ -173,7 +169,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :reused_md_member, description: /.*vda.*cannot be partitioned.*part of.*md0/ ) @@ -192,7 +187,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :reused_md_member, description: /.*vda.*cannot be used.*part of.*md0/ ) @@ -219,7 +213,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :reused_md_member, description: /.*vda1.*cannot be deleted.*part of.*md0/ ) @@ -246,7 +239,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :reused_md_member, description: /.*vda1.*cannot be resized.*part of.*md0/ ) @@ -269,7 +261,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :reused_md_member, description: /.*vda.*cannot be formatted.*part of.*md0/ ) diff --git a/service/test/agama/storage/config_checkers/search_test.rb b/service/test/agama/storage/config_checkers/search_test.rb index 25e5182d4f..35ba40c7fe 100644 --- a/service/test/agama/storage/config_checkers/search_test.rb +++ b/service/test/agama/storage/config_checkers/search_test.rb @@ -52,7 +52,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :search, description: "Mandatory drive not found" ) diff --git a/service/test/agama/storage/config_checkers/volume_group_test.rb b/service/test/agama/storage/config_checkers/volume_group_test.rb index 30e3655ea2..212c5292a8 100644 --- a/service/test/agama/storage/config_checkers/volume_group_test.rb +++ b/service/test/agama/storage/config_checkers/volume_group_test.rb @@ -53,7 +53,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, description: /without name/ ) end @@ -65,7 +64,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_such_alias, description: /no LVM physical volume with alias 'pv1'/ ) @@ -86,7 +84,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :no_such_alias, description: /no target device for LVM physical volumes with alias 'second-disk'/ ) @@ -113,7 +110,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /No passphrase/ ) @@ -138,7 +134,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /'Regular LUKS2' is not available/ ) @@ -151,7 +146,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :encryption, description: /'Encryption with Volatile Random Key' is not a suitable method/ ) @@ -225,7 +219,6 @@ it "includes the expected issue" do issues = subject.issues expect(issues).to include an_object_having_attributes( - error?: true, kind: :incompatible_pv_targets, description: /'system' is mixing reused devices and new devices/ ) diff --git a/service/test/agama/storage/configurator_test.rb b/service/test/agama/storage/configurator_test.rb index e4258b816d..cb48186200 100644 --- a/service/test/agama/storage/configurator_test.rb +++ b/service/test/agama/storage/configurator_test.rb @@ -60,6 +60,9 @@ before do mock_storage(devicegraph: scenario) + # To speed-up the tests + allow(Y2Storage::BootRequirementsStrategies::Analyzer) + .to receive(:bls_bootloader_proposed?).and_return(false) end describe "#configure" do diff --git a/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb index 87b686b046..24d9d02dcf 100644 --- a/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb +++ b/service/test/agama/storage/proposal_settings_conversions/to_y2storage_test.rb @@ -34,6 +34,12 @@ let(:config) { Agama::Config.new } + before do + # To speed-up the tests + allow(Y2Storage::BootRequirementsStrategies::Analyzer) + .to receive(:bls_bootloader_proposed?).and_return(false) + end + describe "#convert" do let(:settings) do Agama::Storage::ProposalSettings.new.tap do |settings| diff --git a/service/test/agama/storage/proposal_test.rb b/service/test/agama/storage/proposal_test.rb index e45bf8d8f6..100c976443 100644 --- a/service/test/agama/storage/proposal_test.rb +++ b/service/test/agama/storage/proposal_test.rb @@ -70,6 +70,9 @@ def drive(partitions) before do mock_storage(devicegraph: "empty-hd-50GiB.yaml") + # To speed-up the tests + allow(Y2Storage::BootRequirementsStrategies::Analyzer) + .to receive(:bls_bootloader_proposed?).and_return(false) end let(:achivable_config) do diff --git a/service/test/y2storage/agama_proposal_lvm_test.rb b/service/test/y2storage/agama_proposal_lvm_test.rb index d6e4ca84ed..6fe158d56e 100644 --- a/service/test/y2storage/agama_proposal_lvm_test.rb +++ b/service/test/y2storage/agama_proposal_lvm_test.rb @@ -51,6 +51,8 @@ mock_storage(devicegraph: scenario) # To speed-up the tests allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true) + allow(Y2Storage::BootRequirementsStrategies::Analyzer) + .to receive(:bls_bootloader_proposed?).and_return(false) end let(:scenario) { "empty-hd-50GiB.yaml" } @@ -244,8 +246,7 @@ it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( - description: /no LVM physical volume with alias 'pv2'/, - severity: Agama::Issue::Severity::ERROR + description: /no LVM physical volume with alias 'pv2'/ ) end end @@ -296,8 +297,7 @@ it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( - description: /no LVM thin pool volume with alias 'pool'/, - severity: Agama::Issue::Severity::ERROR + description: /no LVM thin pool volume with alias 'pool'/ ) end end diff --git a/service/test/y2storage/agama_proposal_search_test.rb b/service/test/y2storage/agama_proposal_search_test.rb index f6c359b59b..0c109d0f09 100644 --- a/service/test/y2storage/agama_proposal_search_test.rb +++ b/service/test/y2storage/agama_proposal_search_test.rb @@ -51,6 +51,8 @@ mock_storage(devicegraph: scenario) # To speed-up the tests allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true) + allow(Y2Storage::BootRequirementsStrategies::Analyzer) + .to receive(:bls_bootloader_proposed?).and_return(false) end let(:scenario) { "disks.yaml" } @@ -185,8 +187,7 @@ it "register an error and returns nil" do expect(proposal.propose).to be_nil expect(proposal.issues_list).to include an_object_having_attributes( - description: "Mandatory partition not found", - severity: Agama::Issue::Severity::ERROR + description: "Mandatory partition not found" ) end end diff --git a/service/test/y2storage/agama_proposal_test.rb b/service/test/y2storage/agama_proposal_test.rb index 32a5a3d23b..65c861ddf1 100644 --- a/service/test/y2storage/agama_proposal_test.rb +++ b/service/test/y2storage/agama_proposal_test.rb @@ -189,6 +189,12 @@ def partition_config(name: nil, filesystem: nil, size: nil) let(:scenario) { "empty-hd-50GiB.yaml" } + before do + # To speed-up the tests + allow(Y2Storage::BootRequirementsStrategies::Analyzer) + .to receive(:bls_bootloader_proposed?).and_return(false) + end + describe "#propose" do context "when only the root partition is specified" do let(:config) { default_config } @@ -412,8 +418,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( - description: /method 'Regular LUKS2' is not available/, - severity: Agama::Issue::Severity::ERROR + description: /method 'Regular LUKS2' is not available/ ) end end @@ -429,8 +434,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( - description: /'Encryption with Volatile Random Key' is not a suitable method/, - severity: Agama::Issue::Severity::ERROR + description: /'Encryption with Volatile Random Key' is not a suitable method/ ) end end @@ -450,8 +454,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( - description: /No passphrase provided/, - severity: Agama::Issue::Severity::ERROR + description: /No passphrase provided/ ) end end @@ -494,8 +497,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) it "registers a critical issue" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( - description: "Mandatory drive not found", - severity: Agama::Issue::Severity::ERROR + description: "Mandatory drive not found" ) end end @@ -588,8 +590,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) it "registers a critical issue" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( - description: "Mandatory partition not found", - severity: Agama::Issue::Severity::ERROR + description: "Mandatory partition not found" ) end end diff --git a/web/src/api.ts b/web/src/api.ts index 9787c969c2..928c3118a1 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -23,7 +23,7 @@ import { get, patch, post, put } from "~/http"; import { apiModel } from "~/api/storage"; import { Config } from "~/api/config"; -import { IssuesMap } from "~/api/issue"; +import { Issue } from "~/api/issue"; import { Proposal } from "~/api/proposal"; import { Question } from "~/api/question"; import { Status } from "~/api/status"; @@ -50,7 +50,7 @@ const getSystem = (): Promise => get("/api/v2/system"); const getProposal = (): Promise => get("/api/v2/proposal"); -const getIssues = (): Promise => get("/api/v2/issues"); +const getIssues = (): Promise => get("/api/v2/issues"); const getQuestions = (): Promise => get("/api/v2/questions"); @@ -112,3 +112,4 @@ export type { Response, System, Config, Proposal }; export * as system from "~/api/system"; export * as config from "~/api/config"; export * as proposal from "~/api/proposal"; +export * as issue from "~/api/issue"; diff --git a/web/src/api/issue.ts b/web/src/api/issue.ts index 5efb26ecd9..a504c79ce2 100644 --- a/web/src/api/issue.ts +++ b/web/src/api/issue.ts @@ -20,76 +20,13 @@ * find current contact information at www.suse.com. */ -/** - * Known scopes for issues. - */ -type IssuesScope = "localization" | "product" | "software" | "storage" | "users" | "iscsi"; - -/** - * Source of the issue - * - * Which is the origin of the issue (the system, the configuration or unknown). - */ -enum IssueSource { - /** Unknown source (it is kind of a fallback value) */ - Unknown = "unknown", - /** An unexpected situation in the system (e.g., missing device). */ - System = "system", - /** Wrong or incomplete configuration (e.g., an authentication mechanism is not set) */ - Config = "config", -} +type Scope = "localization" | "product" | "software" | "storage" | "users" | "iscsi"; -/** - * Issue severity - * - * It indicates how severe the problem is. - */ -enum IssueSeverity { - /** Just a warning, the installation can start */ - Warn = "warn", - /** An important problem that makes the installation not possible */ - Error = "error", -} - -/** - * Pre-installation issue as they come from the API. - */ -type ApiIssue = { - /** Issue description */ +type Issue = { + scope: Scope; description: string; - /** Issue kind **/ kind: string; - /** Issue details */ details?: string; - /** Where the issue comes from */ - source: IssueSource; - /** How severe is the issue */ - severity: IssueSeverity; -}; - -/** - * Issues grouped by scope as they come from the API. - */ -type IssuesMap = { - localization?: ApiIssue[]; - software?: ApiIssue[]; - product?: ApiIssue[]; - storage?: ApiIssue[]; - iscsi?: ApiIssue[]; - users?: ApiIssue[]; -}; - -/** - * Pre-installation issue augmented with the scope. - */ -type Issue = ApiIssue & { scope: IssuesScope }; - -/** - * Validation error - */ -type ValidationError = { - message: string; }; -export { IssueSource, IssueSeverity }; -export type { ApiIssue, IssuesMap, IssuesScope, Issue, ValidationError }; +export type { Issue, Scope }; diff --git a/web/src/api/storage/system.ts b/web/src/api/storage/system.ts index 82aedb2de0..842fbdf3f2 100644 --- a/web/src/api/storage/system.ts +++ b/web/src/api/storage/system.ts @@ -33,7 +33,6 @@ export type EncryptionMethod = | "protectedSwap" | "secureSwap" | "randomSwap"; -export type SystemIssueSource = "config" | "system"; /** * API description of the system @@ -174,6 +173,4 @@ export interface Issue { description: string; class?: string; details?: string; - source?: SystemIssueSource; - severity?: "warn" | "error"; } diff --git a/web/src/components/core/ChangeProductOption.test.tsx b/web/src/components/core/ChangeProductOption.test.tsx index 9cee0d2205..2feea19871 100644 --- a/web/src/components/core/ChangeProductOption.test.tsx +++ b/web/src/components/core/ChangeProductOption.test.tsx @@ -25,7 +25,7 @@ import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { useSystem } from "~/hooks/api"; import { PRODUCT as PATHS } from "~/routes/paths"; -import { Product, RegistrationInfo } from "~/types/software"; +import { Product } from "~/types/software"; import ChangeProductOption from "./ChangeProductOption"; const tumbleweed: Product = { @@ -43,7 +43,7 @@ const microos: Product = { registration: false, }; -let registrationInfoMock: RegistrationInfo; +// let registrationInfoMock: RegistrationInfo; const mockSystemProducts: jest.Mock = jest.fn(); jest.mock("~/hooks/api", () => ({ @@ -67,14 +67,14 @@ describe("ChangeProductOption", () => { // FIXME: activate it again when registration is ready in api v2 describe.skip("but a product is registered", () => { - beforeEach(() => { - registrationInfoMock = { - registered: true, - key: "INTERNAL-USE-ONLY-1234-5678", - email: "", - url: "", - }; - }); + // beforeEach(() => { + // registrationInfoMock = { + // registered: true, + // key: "INTERNAL-USE-ONLY-1234-5678", + // email: "", + // url: "", + // }; + // }); it("renders nothing", () => { const { container } = installerRender(); diff --git a/web/src/components/core/IssuesDrawer.tsx b/web/src/components/core/IssuesDrawer.tsx index ee495ff09d..594b0649bf 100644 --- a/web/src/components/core/IssuesDrawer.tsx +++ b/web/src/components/core/IssuesDrawer.tsx @@ -32,7 +32,6 @@ import { import Link from "~/components/core/Link"; import { useIssues } from "~/hooks/api"; import { useInstallerStatus } from "~/queries/status"; -import { IssueSeverity } from "~/api/issue"; import { InstallationPhase } from "~/types/status"; import { _ } from "~/i18n"; @@ -40,7 +39,7 @@ import { _ } from "~/i18n"; * Drawer for displaying installation issues */ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => { - const issues = useIssues().filter((i) => i.severity === IssueSeverity.Error); + const issues = useIssues(); const { phase } = useInstallerStatus({ suspense: true }); // FIXME: share below headers with navigation menu @@ -83,13 +82,10 @@ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => {
    {scopeIssues.map((issue, subIdx) => { - const variant = issue.severity === IssueSeverity.Error ? "warning" : "info"; - return (
  • - {/** @ts-expect-error TS complain about variant, let's fix it after PF6 migration */} - + {issue.description} diff --git a/web/src/components/overview/L10nSection.tsx b/web/src/components/overview/L10nSection.tsx index 044921d0d5..3f15c9cc15 100644 --- a/web/src/components/overview/L10nSection.tsx +++ b/web/src/components/overview/L10nSection.tsx @@ -41,9 +41,7 @@ export default function L10nSection() { {_("Localization")} - - {locale ? `${msg1}${locale.name} (${locale.territory})${msg2}` : _("Not selected yet")} - + {locale ? `${msg1}${locale.id} (${locale.territory})${msg2}` : _("Not selected yet")} ); diff --git a/web/src/components/overview/SoftwareSection.test.tsx b/web/src/components/overview/SoftwareSection.test.tsx index 4a9e85629c..e4e1aba3c5 100644 --- a/web/src/components/overview/SoftwareSection.test.tsx +++ b/web/src/components/overview/SoftwareSection.test.tsx @@ -23,18 +23,18 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import testingProposal from "~/components/software/proposal.test.json"; +// import testingProposal from "~/components/software/proposal.test.json"; import SoftwareSection from "~/components/overview/SoftwareSection"; -import { SoftwareProposal } from "~/types/software"; +// import { SoftwareProposal } from "~/types/software"; -let mockTestingProposal: SoftwareProposal; +// let mockTestingProposal: SoftwareProposal; // FIXME: redo this tests once new overview is done after api v2 describe.skip("SoftwareSection", () => { describe("when the proposal does not have patterns to select", () => { - beforeEach(() => { - mockTestingProposal = { patterns: {}, size: "" }; - }); + // beforeEach(() => { + // mockTestingProposal = { patterns: {}, size: "" }; + // }); it("renders nothing", () => { const { container } = installerRender(); @@ -43,9 +43,9 @@ describe.skip("SoftwareSection", () => { }); describe("when the proposal has patterns to select", () => { - beforeEach(() => { - mockTestingProposal = testingProposal; - }); + // beforeEach(() => { + // mockTestingProposal = testingProposal; + // }); it("renders the required space and the selected patterns", () => { installerRender(); diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index aa06e36c36..cabbd966a4 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -23,7 +23,6 @@ import React from "react"; import { Alert, Content } from "@patternfly/react-core"; import { useStorageModel, useScopeIssues } from "~/hooks/api"; -import { useConfigIssues } from "~/hooks/storage/issues"; import * as partitionUtils from "~/components/storage/utils/partition"; import { _, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; @@ -87,11 +86,8 @@ const Description = () => { * - The generated proposal contains no errors. */ export default function ProposalFailedInfo() { - const configErrors = useConfigIssues(); - const errors = useScopeIssues("storage"); - - if (configErrors.length !== 0) return; - if (errors.length === 0) return; + const issues = useScopeIssues("storage"); + if (issues.length === 0) return; return ( diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index 6a52c3e06f..e92b7be888 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -46,18 +46,19 @@ import ProposalResultSection from "./ProposalResultSection"; import ProposalTransactionalInfo from "./ProposalTransactionalInfo"; import UnsupportedModelInfo from "./UnsupportedModelInfo"; import { useAvailableDevices } from "~/hooks/storage/system"; +import { useScopeIssues } from "~/hooks/api"; import { useResetConfig } from "~/hooks/storage/config"; +import { useProposal } from "~/hooks/storage/proposal"; import { useConfigModel } from "~/queries/storage/config-model"; import { useZFCPSupported } from "~/queries/storage/zfcp"; import { useDASDSupported } from "~/queries/storage/dasd"; -import { useSystemIssues, useConfigIssues } from "~/hooks/storage/issues"; import { STORAGE as PATHS } from "~/routes/paths"; import { _, n_ } from "~/i18n"; import { useProgress, useProgressChanges } from "~/queries/progress"; import { useNavigate } from "react-router"; function InvalidConfigEmptyState(): React.ReactNode { - const errors = useConfigIssues(); + const errors = useScopeIssues("storage"); const reset = useResetConfig(); return ( @@ -175,8 +176,7 @@ function ProposalEmptyState(): React.ReactNode { function ProposalSections(): React.ReactNode { const model = useConfigModel({ suspense: true }); - const systemErrors = useSystemIssues(); - const hasResult = !systemErrors.length; + const proposal = useProposal(); return ( @@ -211,7 +211,7 @@ function ProposalSections(): React.ReactNode { )} - {hasResult && } + {proposal && } ); } @@ -223,8 +223,8 @@ function ProposalSections(): React.ReactNode { export default function ProposalPage(): React.ReactNode { const model = useConfigModel({ suspense: true }); const availableDevices = useAvailableDevices(); - const systemErrors = useSystemIssues(); - const configErrors = useConfigIssues(); + const proposal = useProposal(); + const issues = useScopeIssues("storage"); const progress = useProgress("storage"); const navigate = useNavigate(); @@ -235,10 +235,10 @@ export default function ProposalPage(): React.ReactNode { }, [progress, navigate]); const fixable = ["no_root", "required_filesystems", "vg_target_devices", "reused_md_member"]; - const unfixableErrors = configErrors.filter((e) => !fixable.includes(e.kind)); + const unfixableErrors = issues.filter((e) => !fixable.includes(e.kind)); const isModelEditable = model && !unfixableErrors.length; const hasDevices = !!availableDevices.length; - const hasResult = !systemErrors.length; + const hasResult = proposal !== null; const showSections = hasDevices && (isModelEditable || hasResult); return ( diff --git a/web/src/hooks/api.ts b/web/src/hooks/api.ts index 0e6024cb72..ed7c9c0b3c 100644 --- a/web/src/hooks/api.ts +++ b/web/src/hooks/api.ts @@ -31,6 +31,7 @@ import { getQuestions, getIssues, getStatus, + issue, } from "~/api"; import { useInstallerClient } from "~/context/installer"; import { System } from "~/api/system"; @@ -39,7 +40,7 @@ import { Status } from "~/api/status"; import { Config } from "~/api/config"; import { apiModel } from "~/api/storage"; import { Question } from "~/api/question"; -import { IssuesScope, Issue, IssuesMap } from "~/api/issue"; +import { Issue } from "~/api/issue"; import { QueryHookOptions } from "~/types/queries"; const statusQuery = () => ({ @@ -199,22 +200,9 @@ const issuesQuery = () => { }; }; -const selectIssues = (data: IssuesMap | null): Issue[] => { - if (!data) return []; - - return Object.keys(data).reduce((all: Issue[], key: IssuesScope) => { - const scoped = data[key].map((i) => ({ ...i, scope: key })); - return all.concat(scoped); - }, []); -}; - function useIssues(options?: QueryHookOptions): Issue[] { const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func({ - ...issuesQuery(), - select: selectIssues, - }); - return data; + return func(issuesQuery())?.data; } const useIssuesChanges = () => { @@ -232,13 +220,12 @@ const useIssuesChanges = () => { }, [client, queryClient]); }; -function useScopeIssues(scope: IssuesScope, options?: QueryHookOptions): Issue[] { +function useScopeIssues(scope: issue.Scope, options?: QueryHookOptions): Issue[] { const func = options?.suspense ? useSuspenseQuery : useQuery; const { data } = func({ ...issuesQuery(), select: useCallback( - (data: IssuesMap | null): Issue[] => - selectIssues(data).filter((i: Issue) => i.scope === scope), + (data: Issue[]): Issue[] => data.filter((i: Issue) => i.scope === scope), [scope], ), }); @@ -251,7 +238,6 @@ export { extendedConfigQuery, storageModelQuery, issuesQuery, - selectIssues, useSystem, useStatus, useSystemChanges, diff --git a/web/src/hooks/storage/issues.ts b/web/src/hooks/storage/issues.ts deleted file mode 100644 index 565bbc0577..0000000000 --- a/web/src/hooks/storage/issues.ts +++ /dev/null @@ -1,52 +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. - */ - -import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import { QueryHookOptions } from "~/types/queries"; -import { IssueSource, Issue, IssuesMap } from "~/api/issue"; -import { issuesQuery, selectIssues } from "~/hooks/api"; - -const selectSystemIssues = (data: IssuesMap | null) => - selectIssues(data).filter((i) => i.scope === "storage" && i.source === IssueSource.System); - -function useSystemIssues(options?: QueryHookOptions): Issue[] { - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func({ - ...issuesQuery(), - select: selectSystemIssues, - }); - return data; -} - -const selectConfigIssues = (data: IssuesMap | null) => - selectIssues(data).filter((i) => i.scope === "storage" && i.source === IssueSource.Config); - -function useConfigIssues(options?: QueryHookOptions): Issue[] { - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func({ - ...issuesQuery(), - select: selectConfigIssues, - }); - return data; -} - -export { useSystemIssues, useConfigIssues }; diff --git a/web/src/hooks/storage/proposal.ts b/web/src/hooks/storage/proposal.ts index 4bdf4d7d07..92d1b9cf61 100644 --- a/web/src/hooks/storage/proposal.ts +++ b/web/src/hooks/storage/proposal.ts @@ -25,6 +25,17 @@ import { Proposal, storage } from "~/api/proposal"; import { QueryHookOptions } from "~/types/queries"; import { proposalQuery } from "~/hooks/api"; +const selectProposal = (data: Proposal | null): storage.Proposal | null => data?.storage; + +function useProposal(options?: QueryHookOptions): storage.Proposal | null { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...proposalQuery(), + select: selectProposal, + }); + return data; +} + const selectDevices = (data: Proposal | null): storage.Device[] => data?.storage?.devices || []; function useDevices(options?: QueryHookOptions): storage.Device[] { @@ -47,4 +58,4 @@ function useActions(options?: QueryHookOptions): storage.Action[] { return data; } -export { useDevices, useActions }; +export { useProposal, useDevices, useActions };