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