diff --git a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml index 9535bb3c0f..525ce9d6fb 100644 --- a/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml +++ b/doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml @@ -35,12 +35,15 @@ + + + diff --git a/doc/dbus/org.opensuse.Agama.Software1.doc.xml b/doc/dbus/org.opensuse.Agama.Software1.doc.xml index bacbe87d94..ed2dca4c0f 100644 --- a/doc/dbus/org.opensuse.Agama.Software1.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Software1.doc.xml @@ -4,18 +4,68 @@ + + + + + + + + + + + + + + + diff --git a/rust/agama-cli/src/logs.rs b/rust/agama-cli/src/logs.rs index f6b6fcf404..9b94adb8e3 100644 --- a/rust/agama-cli/src/logs.rs +++ b/rust/agama-cli/src/logs.rs @@ -258,7 +258,7 @@ impl LogItem for LogCmd { }; file_name.retain(|c| c != ' '); - self.dst_path.as_path().join(format!("{}", file_name)) + self.dst_path.as_path().join(&file_name) } fn store(&self) -> Result<(), io::Error> { @@ -420,7 +420,7 @@ fn store(options: LogOptions) -> Result<(), io::Error> { Err(_e) => "[Failed]", }; - showln(verbose, format!("{}", res).as_str()); + showln(verbose, res.to_string().as_str()); } compress_logs(&tmp_dir, &result) diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index 866e4275d2..52a3fa4a89 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -25,7 +25,6 @@ use std::{ thread::sleep, time::Duration, }; -use tokio; #[derive(Parser)] #[command(name = "agama", version, about, long_about = None)] diff --git a/rust/agama-dbus-server/src/locale/helpers.rs b/rust/agama-dbus-server/src/locale/helpers.rs index d9e65b6de7..490203d5b1 100644 --- a/rust/agama-dbus-server/src/locale/helpers.rs +++ b/rust/agama-dbus-server/src/locale/helpers.rs @@ -23,7 +23,7 @@ pub fn init_locale() -> Result> { /// pub fn set_service_locale(locale: &LocaleCode) { // Let's force the encoding to be 'UTF-8'. - let locale = format!("{}.UTF-8", locale.to_string()); + let locale = format!("{}.UTF-8", locale); if setlocale(LocaleCategory::LcAll, locale).is_none() { log::warn!("Could not set the locale"); } diff --git a/rust/agama-dbus-server/src/locale/locale.rs b/rust/agama-dbus-server/src/locale/locale.rs index 88a6b4c95e..a56919ea0a 100644 --- a/rust/agama-dbus-server/src/locale/locale.rs +++ b/rust/agama-dbus-server/src/locale/locale.rs @@ -45,7 +45,7 @@ impl LocalesDatabase { .lines() .filter_map(|line| TryInto::::try_into(line).ok()) .collect(); - self.locales = self.get_locales(&ui_language)?; + self.locales = self.get_locales(ui_language)?; Ok(()) } @@ -82,7 +82,7 @@ impl LocalesDatabase { let names = &language.names; let language_label = names - .name_for(&ui_language) + .name_for(ui_language) .or_else(|| names.name_for(DEFAULT_LANG)) .unwrap_or(language.id.to_string()); @@ -92,7 +92,7 @@ impl LocalesDatabase { let names = &territory.names; let territory_label = names - .name_for(&ui_language) + .name_for(ui_language) .or_else(|| names.name_for(DEFAULT_LANG)) .unwrap_or(territory.id.to_string()); @@ -120,10 +120,7 @@ mod tests { db.read("de").unwrap(); let found_locales = db.entries(); let spanish: LocaleCode = "es_ES".try_into().unwrap(); - let found = found_locales - .into_iter() - .find(|l| l.code == spanish) - .unwrap(); + let found = found_locales.iter().find(|l| l.code == spanish).unwrap(); assert_eq!(&found.language, "Spanisch"); assert_eq!(&found.territory, "Spanien"); } diff --git a/rust/agama-dbus-server/src/locale/timezone.rs b/rust/agama-dbus-server/src/locale/timezone.rs index 8b60197a45..f26917b6af 100644 --- a/rust/agama-dbus-server/src/locale/timezone.rs +++ b/rust/agama-dbus-server/src/locale/timezone.rs @@ -52,7 +52,7 @@ impl TimezonesDatabase { let ret = timezones .into_iter() .map(|tz| { - let parts = translate_parts(&tz, &ui_language, &tz_parts); + let parts = translate_parts(&tz, ui_language, &tz_parts); TimezoneEntry { code: tz, parts } }) .collect(); @@ -62,10 +62,10 @@ impl TimezonesDatabase { fn translate_parts(timezone: &str, ui_language: &str, tz_parts: &TimezoneIdParts) -> Vec { timezone - .split("/") + .split('/') .map(|part| { tz_parts - .localize_part(part, &ui_language) + .localize_part(part, ui_language) .unwrap_or(part.to_owned()) }) .collect() @@ -82,7 +82,7 @@ mod tests { let found_timezones = db.entries(); dbg!(&found_timezones); let found = found_timezones - .into_iter() + .iter() .find(|tz| tz.code == "Europe/Berlin") .unwrap(); assert_eq!(&found.code, "Europe/Berlin"); diff --git a/rust/agama-dbus-server/src/main.rs b/rust/agama-dbus-server/src/main.rs index 33c0c41db8..c890f5db71 100644 --- a/rust/agama-dbus-server/src/main.rs +++ b/rust/agama-dbus-server/src/main.rs @@ -4,7 +4,6 @@ use agama_lib::connection_to; use anyhow::Context; use log::{self, LevelFilter}; use std::future::pending; -use tokio; const ADDRESS: &str = "unix:path=/run/agama/bus"; const SERVICE_NAME: &str = "org.opensuse.Agama1"; diff --git a/rust/agama-dbus-server/src/network.rs b/rust/agama-dbus-server/src/network.rs index edf2e82e7b..35158a10a8 100644 --- a/rust/agama-dbus-server/src/network.rs +++ b/rust/agama-dbus-server/src/network.rs @@ -60,5 +60,5 @@ pub async fn export_dbus_objects( let adapter = NetworkManagerAdapter::from_system() .await .expect("Could not connect to NetworkManager to read the configuration."); - NetworkService::start(&connection, adapter).await + NetworkService::start(connection, adapter).await } diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index 11cc97c19a..f19100065b 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -117,10 +117,7 @@ pub fn merge_dbus_connections<'a>( /// * `conn`: connection represented as a NestedHash. fn cleanup_dbus_connection(conn: &mut NestedHash) { if let Some(connection) = conn.get_mut("connection") { - if connection - .get("interface-name") - .is_some_and(|v| is_empty_value(&v)) - { + if connection.get("interface-name").is_some_and(is_empty_value) { connection.remove("interface-name"); } } @@ -254,28 +251,13 @@ fn wireless_config_to_dbus(conn: &WirelessConnection) -> NestedHash { /// /// * `match_config`: MatchConfig to convert. fn match_config_to_dbus(match_config: &MatchConfig) -> HashMap<&str, zvariant::Value> { - let drivers: Value = match_config - .driver - .iter() - .cloned() - .collect::>() - .into(); + let drivers: Value = match_config.driver.to_vec().into(); - let kernels: Value = match_config - .kernel - .iter() - .cloned() - .collect::>() - .into(); + let kernels: Value = match_config.kernel.to_vec().into(); - let paths: Value = match_config.path.iter().cloned().collect::>().into(); + let paths: Value = match_config.path.to_vec().into(); - let interfaces: Value = match_config - .interface - .iter() - .cloned() - .collect::>() - .into(); + let interfaces: Value = match_config.interface.to_vec().into(); HashMap::from([ ("driver", drivers), @@ -314,7 +296,7 @@ fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option { base_connection.mac_address = mac_address_from_dbus(wireless_config)?; } - base_connection.ip_config = ip_config_from_dbus(&conn)?; + base_connection.ip_config = ip_config_from_dbus(conn)?; Some(base_connection) } @@ -381,7 +363,7 @@ fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option { ip_config.method4 = NmMethod(method4.to_string()).try_into().ok()?; let address_data = ipv4.get("address-data")?; - let mut addresses = addresses_with_prefix_from_dbus(&address_data)?; + let mut addresses = addresses_with_prefix_from_dbus(address_data)?; ip_config.addresses.append(&mut addresses); @@ -405,7 +387,7 @@ fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option { ip_config.method6 = NmMethod(method6.to_string()).try_into().ok()?; let address_data = ipv6.get("address-data")?; - let mut addresses = addresses_with_prefix_from_dbus(&address_data)?; + let mut addresses = addresses_with_prefix_from_dbus(address_data)?; ip_config.addresses.append(&mut addresses); diff --git a/rust/agama-dbus-server/src/questions.rs b/rust/agama-dbus-server/src/questions.rs index 2f45d69442..fda860bf6d 100644 --- a/rust/agama-dbus-server/src/questions.rs +++ b/rust/agama-dbus-server/src/questions.rs @@ -334,7 +334,7 @@ pub async fn export_dbus_objects( const PATH: &str = "/org/opensuse/Agama1/Questions"; // When serving, request the service name _after_ exposing the main object - let questions = Questions::new(&connection); + let questions = Questions::new(connection); connection.object_server().at(PATH, questions).await?; connection.object_server().at(PATH, ObjectManager).await?; diff --git a/rust/agama-dbus-server/tests/common/mod.rs b/rust/agama-dbus-server/tests/common/mod.rs index 5c7cd9e6ba..47841518e8 100644 --- a/rust/agama-dbus-server/tests/common/mod.rs +++ b/rust/agama-dbus-server/tests/common/mod.rs @@ -5,7 +5,7 @@ use std::{ process::{Child, Command}, time::Duration, }; -use tokio; + use tokio_stream::StreamExt; use uuid::Uuid; use zbus::{MatchRule, MessageStream, MessageType}; @@ -101,7 +101,7 @@ impl NameOwnerChangedStream { .sender("org.freedesktop.DBus")? .member("NameOwnerChanged")? .build(); - let stream = MessageStream::for_match_rule(rule, &connection, None).await?; + let stream = MessageStream::for_match_rule(rule, connection, None).await?; Ok(Self(stream)) } @@ -137,7 +137,7 @@ where if retry > RETRIES { return Err(error); } - retry = retry + 1; + retry += 1; let wait_time = Duration::from_millis(INTERVAL); tokio::time::sleep(wait_time).await; } diff --git a/rust/agama-dbus-server/tests/network.rs b/rust/agama-dbus-server/tests/network.rs index e4f8d4862e..4462164764 100644 --- a/rust/agama-dbus-server/tests/network.rs +++ b/rust/agama-dbus-server/tests/network.rs @@ -36,7 +36,7 @@ async fn test_read_connections() -> Result<(), Box> { let state = NetworkState::new(vec![device], vec![eth0]); let adapter = NetworkTestAdapter(state); - let _service = NetworkService::start(&server.connection(), adapter).await?; + NetworkService::start(&server.connection(), adapter).await?; server.request_name().await?; let client = NetworkClient::new(server.connection()).await?; @@ -54,7 +54,7 @@ async fn test_add_connection() -> Result<(), Box> { let adapter = NetworkTestAdapter(NetworkState::default()); - let _service = NetworkService::start(&server.connection(), adapter).await?; + NetworkService::start(&server.connection(), adapter).await?; server.request_name().await?; let client = NetworkClient::new(server.connection().clone()).await?; @@ -103,7 +103,7 @@ async fn test_update_connection() -> Result<(), Box> { let state = NetworkState::new(vec![device], vec![eth0]); let adapter = NetworkTestAdapter(state); - let _service = NetworkService::start(&server.connection(), adapter).await?; + NetworkService::start(&server.connection(), adapter).await?; server.request_name().await?; let client = NetworkClient::new(server.connection()).await?; diff --git a/rust/agama-derive/src/lib.rs b/rust/agama-derive/src/lib.rs index 7f5a1a2d91..020d4d461f 100644 --- a/rust/agama-derive/src/lib.rs +++ b/rust/agama-derive/src/lib.rs @@ -293,7 +293,7 @@ fn parse_setting_fields(fields: Vec<&syn::Field>) -> SettingFieldsList { SettingFieldsList(settings) } -fn quote_fields_aliases(nested_fields: &Vec<&SettingField>) -> Vec { +fn quote_fields_aliases(nested_fields: &[&SettingField]) -> Vec { nested_fields .iter() .map(|f| { diff --git a/rust/agama-lib/share/examples/profile_Dolomite.json b/rust/agama-lib/share/examples/profile_Dolomite.json new file mode 100644 index 0000000000..16ba127267 --- /dev/null +++ b/rust/agama-lib/share/examples/profile_Dolomite.json @@ -0,0 +1,43 @@ +{ + "localization": { + "keyboard": "en_US", + "language": "en_US" + }, + "product": { + "id": "ALP-Dolomite", + "registrationCode": "FILL IT UP", + "registrationEmail": "jreidinger@suse.com" + }, + "storage": { + "bootDevice": "/dev/dm-1" + }, + "user": { + "fullName": "Jane Doe", + "password": "123456", + "userName": "jane.doe" + }, + "root": { + "password": "nots3cr3t", + "sshKey": "..." + }, + "network": { + "connections": [ + { + "id": "Ethernet network device 1", + "method4": "manual", + "method6": "manual", + "interface": "eth0", + "addresses": [ + "192.168.122.100/24", + "::ffff:c0a8:7ac7/64" + ], + "gateway4": "192.168.122.1", + "gateway6": "::ffff:c0a8:7a01", + "nameservers": [ + "192.168.122.1", + "2001:4860:4860::8888" + ] + } + ] + } +} diff --git a/rust/agama-lib/share/examples/profile.json b/rust/agama-lib/share/examples/profile_tw.json similarity index 90% rename from rust/agama-lib/share/examples/profile.json rename to rust/agama-lib/share/examples/profile_tw.json index 0aae970862..b55d82423a 100644 --- a/rust/agama-lib/share/examples/profile.json +++ b/rust/agama-lib/share/examples/profile_tw.json @@ -4,7 +4,12 @@ "language": "en_US" }, "software": { - "product": "ALP-Dolomite" + "patterns": [ + "gnome" + ] + }, + "product": { + "id": "Tumbleweed" }, "storage": { "bootDevice": "/dev/dm-1" diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 97e2a29293..eb916132cf 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -10,9 +10,27 @@ "description": "Software settings (e.g., product to install)", "type": "object", "properties": { - "product": { + "patterns": { + "description": "List of patterns to install", + "type": "array" + } + } + }, + "product": { + "description": "Software settings (e.g., product to install)", + "type": "object", + "properties": { + "id": { "description": "Product identifier", "type": "string" + }, + "registrationCode": { + "description": "Product registration code", + "type": "string" + }, + "registrationEmail": { + "description": "Product registration email", + "type": "string" } } }, diff --git a/rust/agama-lib/src/error.rs b/rust/agama-lib/src/error.rs index 7124e00cbb..b1fc9ba579 100644 --- a/rust/agama-lib/src/error.rs +++ b/rust/agama-lib/src/error.rs @@ -16,6 +16,10 @@ pub enum ServiceError { Anyhow(#[from] anyhow::Error), #[error("Wrong user parameters: '{0:?}'")] WrongUser(Vec), + #[error("Registration failed: '{0}'")] + FailedRegistration(String), + #[error("Failed to find these patterns: {0:?}")] + UnknownPatterns(Vec), #[error("Error: {0}")] UnsuccessfulAction(String), } diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index 1584385f3d..b1b9a398ab 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -2,8 +2,8 @@ //! //! This module implements the mechanisms to load and store the installation settings. use crate::{ - network::NetworkSettings, software::SoftwareSettings, storage::StorageSettings, - users::UserSettings, + network::NetworkSettings, product::ProductSettings, software::SoftwareSettings, + storage::StorageSettings, users::UserSettings, }; use agama_settings::Settings; use serde::{Deserialize, Serialize}; @@ -24,15 +24,18 @@ pub enum Scope { Storage, /// Network settings Network, + /// Product settings + Product, } impl Scope { /// Returns known scopes /// // TODO: we can rely on strum so we do not forget to add them - pub fn all() -> [Scope; 4] { + pub fn all() -> [Scope; 5] { [ Scope::Network, + Scope::Product, Scope::Software, Scope::Storage, Scope::Users, @@ -49,6 +52,7 @@ impl FromStr for Scope { "software" => Ok(Self::Software), "storage" => Ok(Self::Storage), "network" => Ok(Self::Network), + "product" => Ok(Self::Product), _ => Err("Unknown section"), } } @@ -69,6 +73,9 @@ pub struct InstallSettings { pub software: Option, #[serde(default)] #[settings(nested)] + pub product: Option, + #[serde(default)] + #[settings(nested)] pub storage: Option, #[serde(default)] #[settings(nested)] @@ -92,6 +99,9 @@ impl InstallSettings { if self.network.is_some() { scopes.push(Scope::Network); } + if self.product.is_some() { + scopes.push(Scope::Product); + } scopes } } diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index a3c055b55a..e71c9ff6e4 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -27,6 +27,7 @@ pub mod error; pub mod install_settings; pub mod manager; pub mod network; +pub mod product; pub mod profile; pub mod software; pub mod storage; diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index d8e3077216..f0a46823ad 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -22,7 +22,7 @@ impl<'a> NetworkClient<'a> { pub async fn get_connection(&self, id: &str) -> Result { let path = self.connections_proxy.get_connection(id).await?; - Ok(self.connection_from(path.as_str()).await?) + self.connection_from(path.as_str()).await } /// Returns an array of network connections diff --git a/rust/agama-lib/src/product.rs b/rust/agama-lib/src/product.rs new file mode 100644 index 0000000000..e85173f6c9 --- /dev/null +++ b/rust/agama-lib/src/product.rs @@ -0,0 +1,10 @@ +//! Implements support for handling the product settings + +mod client; +mod proxies; +mod settings; +mod store; + +pub use client::ProductClient; +pub use settings::ProductSettings; +pub use store::ProductStore; diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs new file mode 100644 index 0000000000..106166b621 --- /dev/null +++ b/rust/agama-lib/src/product/client.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; + +use crate::error::ServiceError; +use crate::software::proxies::SoftwareProductProxy; +use serde::Serialize; +use zbus::Connection; + +use super::proxies::RegistrationProxy; + +/// Represents a software product +#[derive(Debug, Serialize)] +pub struct Product { + /// Product ID (eg., "ALP", "Tumbleweed", etc.) + pub id: String, + /// Product name (e.g., "openSUSE Tumbleweed") + pub name: String, + /// Product description + pub description: String, +} + +/// D-Bus client for the software service +pub struct ProductClient<'a> { + product_proxy: SoftwareProductProxy<'a>, + registration_proxy: RegistrationProxy<'a>, +} + +impl<'a> ProductClient<'a> { + pub async fn new(connection: Connection) -> Result, ServiceError> { + Ok(Self { + product_proxy: SoftwareProductProxy::new(&connection).await?, + registration_proxy: RegistrationProxy::new(&connection).await?, + }) + } + + /// Returns the available products + pub async fn products(&self) -> Result, ServiceError> { + let products: Vec = self + .product_proxy + .available_products() + .await? + .into_iter() + .map(|(id, name, data)| { + let description = match data.get("description") { + Some(value) => value.try_into().unwrap(), + None => "", + }; + Product { + id, + name, + description: description.to_string(), + } + }) + .collect(); + Ok(products) + } + + /// Returns the id of the selected product to install + pub async fn product(&self) -> Result { + Ok(self.product_proxy.selected_product().await?) + } + + /// Selects the product to install + pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> { + let result = self.product_proxy.select_product(product_id).await?; + + match result { + (0, _) => Ok(()), + (3, description) => { + let products = self.products().await?; + let ids: Vec = products.into_iter().map(|p| p.id).collect(); + let error = format!("{0}. Available products: '{1:?}'", description, ids); + Err(ServiceError::UnsuccessfulAction(error)) + } + (_, description) => Err(ServiceError::UnsuccessfulAction(description)), + } + } + + /// registration code used to register product + pub async fn registration_code(&self) -> Result { + Ok(self.registration_proxy.reg_code().await?) + } + + /// email used to register product + pub async fn email(&self) -> Result { + Ok(self.registration_proxy.email().await?) + } + + /// register product + pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { + let mut options: HashMap<&str, zbus::zvariant::Value> = HashMap::new(); + if !email.is_empty() { + options.insert("Email", zbus::zvariant::Value::new(email)); + } + Ok(self.registration_proxy.register(code, options).await?) + } +} diff --git a/rust/agama-lib/src/product/proxies.rs b/rust/agama-lib/src/product/proxies.rs new file mode 100644 index 0000000000..1fd258f2fa --- /dev/null +++ b/rust/agama-lib/src/product/proxies.rs @@ -0,0 +1,33 @@ +//! # DBus interface proxy for: `org.opensuse.Agama1.Registration` +//! +//! This code was generated by `zbus-xmlgen` `3.1.1` from DBus introspection data. +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.opensuse.Agama1.Registration", + default_service = "org.opensuse.Agama.Software1", + default_path = "/org/opensuse/Agama/Software1/Product" +)] +trait Registration { + /// Deregister method + fn deregister(&self) -> zbus::Result<(u32, String)>; + + /// Register method + fn register( + &self, + reg_code: &str, + options: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, + ) -> zbus::Result<(u32, String)>; + + /// Email property + #[dbus_proxy(property)] + fn email(&self) -> zbus::Result; + + /// RegCode property + #[dbus_proxy(property)] + fn reg_code(&self) -> zbus::Result; + + /// Requirement property + #[dbus_proxy(property)] + fn requirement(&self) -> zbus::Result; +} diff --git a/rust/agama-lib/src/product/settings.rs b/rust/agama-lib/src/product/settings.rs new file mode 100644 index 0000000000..647801470e --- /dev/null +++ b/rust/agama-lib/src/product/settings.rs @@ -0,0 +1,14 @@ +//! Representation of the product settings + +use agama_settings::Settings; +use serde::{Deserialize, Serialize}; + +/// Software settings for installation +#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProductSettings { + /// ID of the product to install (e.g., "ALP", "Tumbleweed", etc.) + pub id: Option, + pub registration_code: Option, + pub registration_email: Option, +} diff --git a/rust/agama-lib/src/product/store.rs b/rust/agama-lib/src/product/store.rs new file mode 100644 index 0000000000..5ed3e72745 --- /dev/null +++ b/rust/agama-lib/src/product/store.rs @@ -0,0 +1,65 @@ +//! Implements the store for the product settings. + +use super::{ProductClient, ProductSettings}; +use crate::error::ServiceError; +use crate::manager::ManagerClient; +use zbus::Connection; + +/// Loads and stores the product settings from/to the D-Bus service. +pub struct ProductStore<'a> { + product_client: ProductClient<'a>, + manager_client: ManagerClient<'a>, +} + +impl<'a> ProductStore<'a> { + pub async fn new(connection: Connection) -> Result, ServiceError> { + Ok(Self { + product_client: ProductClient::new(connection.clone()).await?, + manager_client: ManagerClient::new(connection).await?, + }) + } + + pub async fn load(&self) -> Result { + let product = self.product_client.product().await?; + let registration_code = self.product_client.registration_code().await?; + let email = self.product_client.email().await?; + + Ok(ProductSettings { + id: Some(product), + registration_code: Some(registration_code), + registration_email: Some(email), + }) + } + + pub async fn store(&self, settings: &ProductSettings) -> Result<(), ServiceError> { + let mut probe = false; + if let Some(product) = &settings.id { + let existing_product = self.product_client.product().await?; + if *product != existing_product { + // avoid selecting same product and unnecessary probe + self.product_client.select_product(product).await?; + probe = true; + } + } + if let Some(reg_code) = &settings.registration_code { + let (result, message); + if let Some(email) = &settings.registration_email { + (result, message) = self.product_client.register(reg_code, email).await?; + } else { + (result, message) = self.product_client.register(reg_code, "").await?; + } + // FIXME: name the magic numbers. 3 is Registration not required + // FIXME: well don't register when not required (no regcode in profile) + if result != 0 && result != 3 { + return Err(ServiceError::FailedRegistration(message)); + } + probe = true; + } + + if probe { + self.manager_client.probe().await?; + } + + Ok(()) + } +} diff --git a/rust/agama-lib/src/software.rs b/rust/agama-lib/src/software.rs index b494e4f26c..53f2d6bf51 100644 --- a/rust/agama-lib/src/software.rs +++ b/rust/agama-lib/src/software.rs @@ -1,7 +1,7 @@ //! Implements support for handling the software settings mod client; -mod proxies; +pub mod proxies; mod settings; mod store; diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index 2d6ae30c39..0670467b02 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -1,71 +1,83 @@ -use super::proxies::SoftwareProductProxy; +use super::proxies::Software1Proxy; use crate::error::ServiceError; use serde::Serialize; use zbus::Connection; /// Represents a software product #[derive(Debug, Serialize)] -pub struct Product { - /// Product ID (eg., "ALP", "Tumbleweed", etc.) +pub struct Pattern { + /// Pattern ID (eg., "aaa_base", "gnome") pub id: String, - /// Product name (e.g., "openSUSE Tumbleweed") - pub name: String, - /// Product description + /// Pattern category (e.g., "Production") + pub category: String, + /// Pattern icon path locally on system + pub icon: String, + /// Pattern description pub description: String, + /// Pattern summary + pub summary: String, + /// Pattern order + pub order: String, } /// D-Bus client for the software service pub struct SoftwareClient<'a> { - product_proxy: SoftwareProductProxy<'a>, + software_proxy: Software1Proxy<'a>, } impl<'a> SoftwareClient<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { - product_proxy: SoftwareProductProxy::new(&connection).await?, + software_proxy: Software1Proxy::new(&connection).await?, }) } - /// Returns the available products - pub async fn products(&self) -> Result, ServiceError> { - let products: Vec = self - .product_proxy - .available_products() + /// Returns the available patterns + pub async fn patterns(&self, filtered: bool) -> Result, ServiceError> { + let patterns: Vec = self + .software_proxy + .list_patterns(filtered) .await? .into_iter() - .map(|(id, name, data)| { - let description = match data.get("description") { - Some(value) => value.try_into().unwrap(), - None => "", - }; - Product { + .map( + |(id, (category, description, icon, summary, order))| Pattern { id, - name, - description: description.to_string(), - } - }) + category, + icon, + description, + summary, + order, + }, + ) .collect(); - Ok(products) + Ok(patterns) } - /// Returns the selected product to install - pub async fn product(&self) -> Result { - Ok(self.product_proxy.selected_product().await?) + /// Returns the ids of patterns selected by user + pub async fn user_selected_patterns(&self) -> Result, ServiceError> { + const USER_SELECTED: u8 = 0; + let patterns: Vec = self + .software_proxy + .selected_patterns() + .await? + .into_iter() + .filter(|(_id, reason)| *reason == USER_SELECTED) + .map(|(id, _reason)| id) + .collect(); + Ok(patterns) } - /// Selects the product to install - pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> { - let result = self.product_proxy.select_product(product_id).await?; - - match result { - (0, _) => Ok(()), - (3, description) => { - let products = self.products().await?; - let ids: Vec = products.into_iter().map(|p| p.id).collect(); - let error = format!("{0}. Available products: '{1:?}'", description, ids); - Err(ServiceError::UnsuccessfulAction(error)) - } - (_, description) => Err(ServiceError::UnsuccessfulAction(description)), + /// Selects patterns by user + pub async fn select_patterns(&self, patterns: &[String]) -> Result<(), ServiceError> { + let patterns: Vec<&str> = patterns.iter().map(AsRef::as_ref).collect(); + let wrong_patterns = self + .software_proxy + .set_user_patterns(patterns.as_slice()) + .await?; + if !wrong_patterns.is_empty() { + Err(ServiceError::UnknownPatterns(wrong_patterns)) + } else { + Ok(()) } } } diff --git a/rust/agama-lib/src/software/proxies.rs b/rust/agama-lib/src/software/proxies.rs index 8fc081b3c6..a37cc1b341 100644 --- a/rust/agama-lib/src/software/proxies.rs +++ b/rust/agama-lib/src/software/proxies.rs @@ -10,7 +10,7 @@ use zbus::dbus_proxy; )] trait Software1 { /// AddPattern method - fn add_pattern(&self, id: &str) -> zbus::Result<()>; + fn add_pattern(&self, id: &str) -> zbus::Result; /// Finish method fn finish(&self) -> zbus::Result<()>; @@ -37,10 +37,10 @@ trait Software1 { fn provisions_selected(&self, provisions: &[&str]) -> zbus::Result>; /// RemovePattern method - fn remove_pattern(&self, id: &str) -> zbus::Result<()>; + fn remove_pattern(&self, id: &str) -> zbus::Result; /// SetUserPatterns method - fn set_user_patterns(&self, ids: &[&str]) -> zbus::Result<()>; + fn set_user_patterns(&self, ids: &[&str]) -> zbus::Result>; /// UsedDiskSpace method fn used_disk_space(&self) -> zbus::Result; diff --git a/rust/agama-lib/src/software/settings.rs b/rust/agama-lib/src/software/settings.rs index 9458b964d3..a2dba395ee 100644 --- a/rust/agama-lib/src/software/settings.rs +++ b/rust/agama-lib/src/software/settings.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Settings, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SoftwareSettings { - /// ID of the product to install (e.g., "ALP", "Tumbleweed", etc.) - pub product: Option, + /// List of patterns to install. If empty use default. + #[settings(collection)] + pub patterns: Vec, } diff --git a/rust/agama-lib/src/software/store.rs b/rust/agama-lib/src/software/store.rs index 9a8a5fb365..66b6f6b092 100644 --- a/rust/agama-lib/src/software/store.rs +++ b/rust/agama-lib/src/software/store.rs @@ -2,36 +2,29 @@ use super::{SoftwareClient, SoftwareSettings}; use crate::error::ServiceError; -use crate::manager::ManagerClient; use zbus::Connection; /// Loads and stores the software settings from/to the D-Bus service. pub struct SoftwareStore<'a> { software_client: SoftwareClient<'a>, - manager_client: ManagerClient<'a>, } impl<'a> SoftwareStore<'a> { pub async fn new(connection: Connection) -> Result, ServiceError> { Ok(Self { software_client: SoftwareClient::new(connection.clone()).await?, - manager_client: ManagerClient::new(connection).await?, }) } pub async fn load(&self) -> Result { - let product = self.software_client.product().await?; - - Ok(SoftwareSettings { - product: Some(product), - }) + let patterns = self.software_client.user_selected_patterns().await?; + Ok(SoftwareSettings { patterns }) } pub async fn store(&self, settings: &SoftwareSettings) -> Result<(), ServiceError> { - if let Some(product) = &settings.product { - self.software_client.select_product(product).await?; - self.manager_client.probe().await?; - } + self.software_client + .select_patterns(&settings.patterns) + .await?; Ok(()) } diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 7cdbb0e37e..06455992b8 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -52,7 +52,7 @@ impl<'a> StorageClient<'a> { .map(|path| self.storage_device(path)) .collect(); - return join_all(devices).await.into_iter().collect(); + join_all(devices).await.into_iter().collect() } /// Returns the storage device for the given D-Bus path diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index ae3ee8f082..1f0d4e5c15 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -3,7 +3,8 @@ use crate::error::ServiceError; use crate::install_settings::{InstallSettings, Scope}; use crate::{ - network::NetworkStore, software::SoftwareStore, storage::StorageStore, users::UsersStore, + network::NetworkStore, product::ProductStore, software::SoftwareStore, storage::StorageStore, + users::UsersStore, }; use zbus::Connection; @@ -16,6 +17,7 @@ use zbus::Connection; pub struct Store<'a> { users: UsersStore<'a>, network: NetworkStore<'a>, + product: ProductStore<'a>, software: SoftwareStore<'a>, storage: StorageStore<'a>, } @@ -25,6 +27,7 @@ impl<'a> Store<'a> { Ok(Self { users: UsersStore::new(connection.clone()).await?, network: NetworkStore::new(connection.clone()).await?, + product: ProductStore::new(connection.clone()).await?, software: SoftwareStore::new(connection.clone()).await?, storage: StorageStore::new(connection).await?, }) @@ -53,6 +56,10 @@ impl<'a> Store<'a> { settings.user = Some(self.users.load().await?); } + if scopes.contains(&Scope::Product) { + settings.product = Some(self.product.load().await?); + } + // TODO: use try_join here Ok(settings) } @@ -62,6 +69,11 @@ impl<'a> Store<'a> { if let Some(network) = &settings.network { self.network.store(network).await?; } + // order is important here as network can be critical for connection + // to registration server and selecting product is important for rest + if let Some(product) = &settings.product { + self.product.store(product).await?; + } if let Some(software) = &settings.software { self.software.store(software).await?; } diff --git a/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs b/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs index a1bdd07cb6..a462f5cffe 100644 --- a/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs +++ b/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs @@ -6,7 +6,7 @@ use quick_xml::de::from_str; use serde::Deserialize; use std::{error::Error, fs}; -const DB_PATH: &'static str = "/usr/share/X11/xkb/rules/base.xml"; +const DB_PATH: &str = "/usr/share/X11/xkb/rules/base.xml"; /// X Keyboard Configuration Database #[derive(Deserialize, Debug)] @@ -20,7 +20,7 @@ impl XkbConfigRegistry { /// /// - `path`: database path. pub fn from(path: &str) -> Result> { - let contents = fs::read_to_string(&path)?; + let contents = fs::read_to_string(path)?; Ok(from_str(&contents)?) } diff --git a/rust/agama-locale-data/src/lib.rs b/rust/agama-locale-data/src/lib.rs index e1a48914d0..8b9dfcee4f 100644 --- a/rust/agama-locale-data/src/lib.rs +++ b/rust/agama-locale-data/src/lib.rs @@ -150,7 +150,6 @@ mod tests { let localized = get_timezone_parts() .unwrap() .localize_timezones("de", &timezones); - let _res: Vec<(String, String)> = - timezones.into_iter().zip(localized.into_iter()).collect(); + let _res: Vec<(String, String)> = timezones.into_iter().zip(localized).collect(); } } diff --git a/rust/agama-settings/src/settings.rs b/rust/agama-settings/src/settings.rs index 0692d9ec78..240a6cd854 100644 --- a/rust/agama-settings/src/settings.rs +++ b/rust/agama-settings/src/settings.rs @@ -80,6 +80,23 @@ impl From> for SettingObject { } } +impl From for SettingObject { + fn from(value: String) -> SettingObject { + SettingObject(HashMap::from([("value".to_string(), SettingValue(value))])) + } +} + +impl TryFrom for String { + type Error = ConversionError; + + fn try_from(value: SettingObject) -> Result { + if let Some(v) = value.get("value") { + return Ok(v.to_string()); + } + Err(ConversionError::MissingKey("value".to_string())) + } +} + impl TryFrom for bool { type Error = ConversionError; diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes index 059d13682e..708fba8e0e 100644 --- a/rust/package/agama-cli.changes +++ b/rust/package/agama-cli.changes @@ -1,3 +1,13 @@ +------------------------------------------------------------------- +Fri Dec 8 09:23:09 UTC 2023 - Josef Reidinger + +- Change the config in a way that: (gh#openSUSE/agama#919) + 1. product is moved to own section and is now under product.id + 2. in product section is now also registrationCode and registrationEmail + 3. in software section is now patterns to select patterns to install +- adapt profile.schema according to above changes +- org.opensuse.Agama.Software1 API changed to report missing patterns + ------------------------------------------------------------------- Tue Dec 5 11:18:41 UTC 2023 - Jorik Cronenberg diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb index a22729c3eb..9ca3ea8eb2 100644 --- a/service/lib/agama/dbus/software/manager.rb +++ b/service/lib/agama/dbus/software/manager.rb @@ -87,9 +87,9 @@ def issues # 1 for auto selected. Can be extended in future e.g. for mandatory patterns dbus_reader_attr_accessor :selected_patterns, "a{sy}" - dbus_method(:AddPattern, "in id:s") { |p| backend.add_pattern(p) } - dbus_method(:RemovePattern, "in id:s") { |p| backend.remove_pattern(p) } - dbus_method(:SetUserPatterns, "in ids:as") { |ids| backend.user_patterns = ids } + dbus_method(:AddPattern, "in id:s, out result:b") { |p| backend.add_pattern(p) } + dbus_method(:RemovePattern, "in id:s, out result:b") { |p| backend.remove_pattern(p) } + dbus_method(:SetUserPatterns, "in ids:as, out wrong:as") { |ids| [backend.assign_patterns(ids)] } dbus_method :ProvisionsSelected, "in Provisions:as, out Result:ab" do |provisions| [provisions.map { |p| backend.provision_selected?(p) }] diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index 6c94418a9d..ba983232ec 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -283,7 +283,7 @@ def connect_result(first_error_code: 1, &block) def connect_result_from_error(error, error_code, details = nil) logger.error("Error connecting to registration server: #{error}") - description = "Connection to registration server failed" + description = "Connection to registration server failed: #{error}" description += " (#{details})" if details [error_code, description] diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index 9c2aeb8cf9..a06e780b14 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -223,7 +223,8 @@ def patterns(filtered) end def add_pattern(id) - # TODO: error handling + return false unless pattern_exist?(id) + res = Yast::Pkg.ResolvableInstall(id, :pattern) logger.info "Adding pattern #{res.inspect}" Yast::PackagesProposal.AddResolvables(PROPOSAL_ID, :pattern, [id]) @@ -231,10 +232,13 @@ def add_pattern(id) res = Yast::Pkg.PkgSolve(unused = true) logger.info "Solver run #{res.inspect}" selected_patterns_changed + + true end def remove_pattern(id) - # TODO: error handling + return false unless pattern_exist?(id) + res = Yast::Pkg.ResolvableNeutral(id, :pattern, force = false) logger.info "Removing pattern #{res.inspect}" Yast::PackagesProposal.RemoveResolvables(PROPOSAL_ID, :pattern, [id]) @@ -242,18 +246,25 @@ def remove_pattern(id) res = Yast::Pkg.PkgSolve(unused = true) logger.info "Solver run #{res.inspect}" selected_patterns_changed + + true end - def user_patterns=(ids) + def assign_patterns(ids) + wrong_patterns = ids.reject { |p| pattern_exist?(p) } + return wrong_patterns unless wrong_patterns.empty? + user_patterns = Yast::PackagesProposal.GetResolvables(PROPOSAL_ID, :pattern) user_patterns.each { |p| Yast::Pkg.ResolvableNeutral(p, :pattern, force = false) } Yast::PackagesProposal.SetResolvables(PROPOSAL_ID, :pattern, ids) ids.each { |p| Yast::Pkg.ResolvableInstall(p, :pattern) } - logger.info "Setting patterns to #{res.inspect}" + logger.info "Setting patterns to #{ids.inspect}" res = Yast::Pkg.PkgSolve(unused = true) logger.info "Solver run #{res.inspect}" selected_patterns_changed + + [] end # @return [Array,Array] returns pair of arrays where the first one @@ -479,6 +490,10 @@ def missing_registration? registration.reg_code.nil? && registration.requirement == Agama::Registration::Requirement::MANDATORY end + + def pattern_exist?(pattern_name) + !Y2Packager::Resolvable.find(kind: :pattern, name: pattern_name).empty? + end end end end