diff --git a/rust/agama-derive/src/lib.rs b/rust/agama-derive/src/lib.rs index 8689671cf4..fa41a4bc81 100644 --- a/rust/agama-derive/src/lib.rs +++ b/rust/agama-derive/src/lib.rs @@ -52,7 +52,7 @@ fn expand_set_fn(field_name: &Vec) -> TokenStream2 { } quote! { - fn set(&mut self, attr: &str, value: SettingValue) -> Result<(), &'static str> { + fn set(&mut self, attr: &str, value: crate::settings::SettingValue) -> Result<(), &'static str> { match attr { #(stringify!(#field_name) => self.#field_name = value.try_into()?,)* _ => return Err("unknown attribute") diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index c343e28a38..203bfd6e14 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -1,11 +1,14 @@ //! Configuration settings handling //! //! This module implements the mechanisms to load and store the installation settings. -use crate::network::NetworkSettings; +use crate::{ + network::NetworkSettings, + storage::StorageSettings, + software::SoftwareSettings, + users::UserSettings +}; use crate::settings::{SettingObject, SettingValue, Settings}; -use agama_derive::Settings; use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; use std::default::Default; use std::str::FromStr; @@ -169,154 +172,4 @@ impl Settings for InstallSettings { storage.merge(other_storage); } } -} - -/// User settings -/// -/// Holds the user settings for the installation. -#[derive(Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserSettings { - #[serde(rename = "user")] - pub first_user: Option, - pub root: Option, -} - -impl Settings for UserSettings { - fn set(&mut self, attr: &str, value: SettingValue) -> Result<(), &'static str> { - if let Some((ns, id)) = attr.split_once('.') { - match ns { - "user" => { - let first_user = self.first_user.get_or_insert(Default::default()); - first_user.set(id, value)? - } - "root" => { - let root_user = self.root.get_or_insert(Default::default()); - root_user.set(id, value)? - } - _ => return Err("unknown attribute"), - } - } - Ok(()) - } - - fn merge(&mut self, other: &Self) { - if let Some(other_first_user) = &other.first_user { - let first_user = self.first_user.get_or_insert(Default::default()); - first_user.merge(other_first_user); - } - - if let Some(other_root) = &other.root { - let root = self.root.get_or_insert(Default::default()); - root.merge(other_root); - } - } -} - -/// First user settings -/// -/// Holds the settings for the first user. -#[derive(Debug, Default, Settings, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FirstUserSettings { - /// First user's full name - pub full_name: Option, - /// First user's username - pub user_name: Option, - /// First user's password (in clear text) - pub password: Option, - /// Whether auto-login should enabled or not - pub autologin: Option, -} - -/// Root user settings -/// -/// Holds the settings for the root user. -#[derive(Debug, Default, Settings, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RootUserSettings { - /// Root's password (in clear text) - #[serde(skip_serializing)] - pub password: Option, - /// Root SSH public key - pub ssh_public_key: Option, -} - -/// Storage settings for installation -#[derive(Debug, Default, Settings, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StorageSettings { - /// Whether LVM should be enabled - pub lvm: Option, - /// Encryption password for the storage devices (in clear text) - pub encryption_password: Option, - /// Devices to use in the installation - #[collection_setting] - pub devices: Vec, -} - -/// Device to use in the installation -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Device { - /// Device name (e.g., "/dev/sda") - pub name: String, -} - -impl TryFrom for Device { - type Error = &'static str; - - fn try_from(value: SettingObject) -> Result { - match value.0.get("name") { - Some(name) => Ok(Device { - name: name.clone().try_into()?, - }), - None => Err("'name' key not found"), - } - } -} - -/// Software settings for installation -#[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, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_user_settings_merge() { - let mut user1 = UserSettings::default(); - let user2 = UserSettings { - first_user: Some(FirstUserSettings { - full_name: Some("Jane Doe".to_string()), - ..Default::default() - }), - root: Some(RootUserSettings { - password: Some("nots3cr3t".to_string()), - ..Default::default() - }), - }; - user1.merge(&user2); - let first_user = user1.first_user.unwrap(); - assert_eq!(first_user.full_name, Some("Jane Doe".to_string())); - let root_user = user1.root.unwrap(); - assert_eq!(root_user.password, Some("nots3cr3t".to_string())); - } - - #[test] - fn test_merge() { - let mut user1 = FirstUserSettings::default(); - let user2 = FirstUserSettings { - full_name: Some("Jane Doe".to_owned()), - autologin: Some(true), - ..Default::default() - }; - user1.merge(&user2); - assert_eq!(user1.full_name.unwrap(), "Jane Doe") - } -} +} \ No newline at end of file diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 4ae2bd00d2..916fc0778b 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -1,3 +1,28 @@ +//! # Interacting with Agama +//! +//! This library offers an API to interact with Agama services. At this point, the library allows: +//! +//! * Reading and writing [installation settings](install_settings::InstallSettings). +//! * Monitoring the [progress](progress). +//! * Triggering actions through the [manager] (e.g., starting installation). +//! +//! ## Handling installation settings +//! +//! Let's have a look to the components that are involved when dealing with the installation +//! settings, as it is the most complex part of the library. The code is organized in a set of +//! modules, one for each topic, like [network], [software], and so on. +//! +//! Each of those modules contains, at least: +//! +//! * A settings model: it is a representation of the installation settings for the given topic. It +//! is expected to implement the [serde::Serialize], [serde::Deserialize] and [settings::Settings] +//! traits. +//! * A store: it is the responsible for reading/writing the settings to the service. Usually, it +//! relies on a D-Bus client for communicating with the service, although it could implement that +//! logic itself. Note: we are considering defining a trait for stores too. +//! +//! As said, those modules might implement additional stuff, like specific types, clients, etc. + pub mod error; pub mod install_settings; pub mod manager; diff --git a/rust/agama-lib/src/network.rs b/rust/agama-lib/src/network.rs index 523d7b820d..c63c7c6096 100644 --- a/rust/agama-lib/src/network.rs +++ b/rust/agama-lib/src/network.rs @@ -1,9 +1,11 @@ +//! Implements support for handling the network settings + mod client; -mod model; +mod settings; mod proxies; mod store; pub mod types; pub use client::NetworkClient; -pub use model::NetworkSettings; +pub use settings::NetworkSettings; pub use store::NetworkStore; diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index 848bc76e7a..1c691efd02 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -1,4 +1,4 @@ -use super::model::{NetworkConnection, WirelessSettings}; +use super::settings::{NetworkConnection, WirelessSettings}; use super::types::SSID; use crate::error::ServiceError; diff --git a/rust/agama-lib/src/network/model.rs b/rust/agama-lib/src/network/settings.rs similarity index 92% rename from rust/agama-lib/src/network/model.rs rename to rust/agama-lib/src/network/settings.rs index c98317ec6a..aea162f770 100644 --- a/rust/agama-lib/src/network/model.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -1,6 +1,5 @@ -//! Configuration network settings handling -//! -//! This module implements the mechanisms to load and store the installation settings. +//! Representation of the network settings + use crate::settings::{SettingObject, Settings}; use agama_derive::Settings; use serde::{Deserialize, Serialize}; diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index 4cd58408f3..5340e494d7 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -137,245 +137,4 @@ trait Questions1 { size: &str, attempt: u8, ) -> zbus::Result; -} - -#[dbus_proxy( - interface = "org.opensuse.Agama.Software1", - default_service = "org.opensuse.Agama.Software1", - default_path = "/org/opensuse/Agama/Software1" -)] -trait Software1 { - /// Finish method - fn finish(&self) -> zbus::Result<()>; - - /// Install method - fn install(&self) -> zbus::Result<()>; - - /// IsPackageInstalled method - fn is_package_installed(&self, name: &str) -> zbus::Result; - - /// Probe method - fn probe(&self) -> zbus::Result<()>; - - /// Propose method - fn propose(&self) -> zbus::Result<()>; - - /// ProvisionSelected method - fn provision_selected(&self, provision: &str) -> zbus::Result; - - /// ProvisionsSelected method - fn provisions_selected(&self, provisions: &[&str]) -> zbus::Result>; - - /// SelectProduct method - fn select_product(&self, product_id: &str) -> zbus::Result<()>; - - /// UsedDiskSpace method - fn used_disk_space(&self) -> zbus::Result; - - /// AvailableBaseProducts property - #[dbus_proxy(property)] - fn available_base_products( - &self, - ) -> zbus::Result< - Vec<( - String, - String, - std::collections::HashMap, - )>, - >; - - /// SelectedBaseProduct property - #[dbus_proxy(property)] - fn selected_base_product(&self) -> zbus::Result; -} - -#[dbus_proxy( - interface = "org.opensuse.Agama.Software1.Proposal", - default_service = "org.opensuse.Agama.Software1", - default_path = "/org/opensuse/Agama/Software1/Proposal" -)] -trait SoftwareProposal { - /// AddResolvables method - fn add_resolvables( - &self, - id: &str, - r#type: u8, - resolvables: &[&str], - optional: bool, - ) -> zbus::Result<()>; - - /// GetResolvables method - fn get_resolvables(&self, id: &str, r#type: u8, optional: bool) -> zbus::Result>; - - /// RemoveResolvables method - fn remove_resolvables( - &self, - id: &str, - r#type: u8, - resolvables: &[&str], - optional: bool, - ) -> zbus::Result<()>; - - /// SetResolvables method - fn set_resolvables( - &self, - id: &str, - r#type: u8, - resolvables: &[&str], - optional: bool, - ) -> zbus::Result<()>; -} - -#[dbus_proxy( - interface = "org.opensuse.Agama1.Validation", - default_service = "org.opensuse.Agama.Storage1", - default_path = "/org/opensuse/Agama/Storage1" -)] -trait Validation { - /// Errors property - #[dbus_proxy(property)] - fn errors(&self) -> zbus::Result>; - - /// Valid property - #[dbus_proxy(property)] - fn valid(&self) -> zbus::Result; -} - -#[dbus_proxy( - interface = "org.opensuse.Agama.Storage1", - default_service = "org.opensuse.Agama.Storage1", - default_path = "/org/opensuse/Agama/Storage1" -)] -trait Storage1 { - /// Finish method - fn finish(&self) -> zbus::Result<()>; - - /// Install method - fn install(&self) -> zbus::Result<()>; - - /// Probe method - fn probe(&self) -> zbus::Result<()>; - - /// DeprecatedSystem property - #[dbus_proxy(property)] - fn deprecated_system(&self) -> zbus::Result; -} - -#[dbus_proxy( - interface = "org.opensuse.Agama.Storage1.Proposal.Calculator", - default_service = "org.opensuse.Agama.Storage1", - default_path = "/org/opensuse/Agama/Storage1" -)] -trait Calculator { - /// Calculate method - fn calculate( - &self, - settings: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, - ) -> zbus::Result; - - /// AvailableDevices property - #[dbus_proxy(property)] - fn available_devices( - &self, - ) -> zbus::Result< - Vec<( - String, - String, - std::collections::HashMap, - )>, - >; - - /// Result property - #[dbus_proxy(property)] - fn result(&self) -> zbus::Result; - - /// VolumeTemplates property - #[dbus_proxy(property)] - fn volume_templates( - &self, - ) -> zbus::Result>>; -} - -#[dbus_proxy( - interface = "org.opensuse.Agama.Storage1.Proposal", - default_service = "org.opensuse.Agama.Storage1", - default_path = "/org/opensuse/Agama/Storage1/Proposal" -)] -trait StorageProposal { - /// Actions property - #[dbus_proxy(property)] - fn actions( - &self, - ) -> zbus::Result>>; - - /// CandidateDevices property - #[dbus_proxy(property)] - fn candidate_devices(&self) -> zbus::Result>; - - /// EncryptionPassword property - #[dbus_proxy(property)] - fn encryption_password(&self) -> zbus::Result; - - /// LVM property - #[dbus_proxy(property, name = "LVM")] - fn lvm(&self) -> zbus::Result; - - /// Volumes property - #[dbus_proxy(property)] - fn volumes( - &self, - ) -> zbus::Result>>; -} - -#[dbus_proxy( - interface = "org.opensuse.Agama.Users1", - default_service = "org.opensuse.Agama.Users1", - default_path = "/org/opensuse/Agama/Users1" -)] -trait Users1 { - /// RemoveFirstUser method - fn remove_first_user(&self) -> zbus::Result; - - /// RemoveRootPassword method - fn remove_root_password(&self) -> zbus::Result; - - /// SetFirstUser method - fn set_first_user( - &self, - full_name: &str, - user_name: &str, - password: &str, - auto_login: bool, - data: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, - ) -> zbus::Result<(bool, Vec)>; - - /// SetRootPassword method - fn set_root_password(&self, value: &str, encrypted: bool) -> zbus::Result; - - /// SetRootSSHKey method - #[dbus_proxy(name = "SetRootSSHKey")] - fn set_root_sshkey(&self, value: &str) -> zbus::Result; - - /// Write method - fn write(&self) -> zbus::Result; - - /// FirstUser property - #[dbus_proxy(property)] - fn first_user( - &self, - ) -> zbus::Result<( - String, - String, - String, - bool, - std::collections::HashMap, - )>; - - /// RootPasswordSet property - #[dbus_proxy(property)] - fn root_password_set(&self) -> zbus::Result; - - /// RootSSHKey property - #[dbus_proxy(property, name = "RootSSHKey")] - fn root_sshkey(&self) -> zbus::Result; -} +} \ No newline at end of file diff --git a/rust/agama-lib/src/settings.rs b/rust/agama-lib/src/settings.rs index 7c11548b9b..3f2d56bb47 100644 --- a/rust/agama-lib/src/settings.rs +++ b/rust/agama-lib/src/settings.rs @@ -24,9 +24,9 @@ use std::convert::TryFrom; /// In the example below, the trait is manually implemented for `InstallSettings` and derived for /// `UserSettings`. /// -/// ``` -/// # use agama_derive::Settings; +/// ```no_compile /// # use agama_lib::settings::{Settings, SettingValue}; +/// # use agama_derive::Settings; /// /// #[derive(Settings)] /// struct UserSettings { diff --git a/rust/agama-lib/src/software.rs b/rust/agama-lib/src/software.rs index 1a56faf9da..6b557e32c0 100644 --- a/rust/agama-lib/src/software.rs +++ b/rust/agama-lib/src/software.rs @@ -1,60 +1,10 @@ -use super::proxies::Software1Proxy; -use crate::error::ServiceError; -use serde::Serialize; -use zbus::Connection; +//! Implements support for handling the software settings -/// 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, -} +mod client; +mod proxies; +mod settings; +mod store; -/// D-Bus client for the software service -pub struct SoftwareClient<'a> { - software_proxy: Software1Proxy<'a>, -} - -impl<'a> SoftwareClient<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { - Ok(Self { - software_proxy: Software1Proxy::new(&connection).await?, - }) - } - - /// Returns the available products - pub async fn products(&self) -> Result, ServiceError> { - let products: Vec = self - .software_proxy - .available_base_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 selected product to install - pub async fn product(&self) -> Result { - Ok(self.software_proxy.selected_base_product().await?) - } - - /// Selects the product to install - pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> { - Ok(self.software_proxy.select_product(product_id).await?) - } -} +pub use client::SoftwareClient; +pub use settings::SoftwareSettings; +pub use store::SoftwareStore; \ No newline at end of file diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs new file mode 100644 index 0000000000..1a56faf9da --- /dev/null +++ b/rust/agama-lib/src/software/client.rs @@ -0,0 +1,60 @@ +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 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 SoftwareClient<'a> { + software_proxy: Software1Proxy<'a>, +} + +impl<'a> SoftwareClient<'a> { + pub async fn new(connection: Connection) -> Result, ServiceError> { + Ok(Self { + software_proxy: Software1Proxy::new(&connection).await?, + }) + } + + /// Returns the available products + pub async fn products(&self) -> Result, ServiceError> { + let products: Vec = self + .software_proxy + .available_base_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 selected product to install + pub async fn product(&self) -> Result { + Ok(self.software_proxy.selected_base_product().await?) + } + + /// Selects the product to install + pub async fn select_product(&self, product_id: &str) -> Result<(), ServiceError> { + Ok(self.software_proxy.select_product(product_id).await?) + } +} diff --git a/rust/agama-lib/src/software/proxies.rs b/rust/agama-lib/src/software/proxies.rs new file mode 100644 index 0000000000..e76907008c --- /dev/null +++ b/rust/agama-lib/src/software/proxies.rs @@ -0,0 +1,91 @@ +//! D-Bus interface proxies for: `org.opensuse.Agama.Software1.*` +//! +//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.opensuse.Agama.Software1", + default_service = "org.opensuse.Agama.Software1", + default_path = "/org/opensuse/Agama/Software1" +)] +trait Software1 { + /// Finish method + fn finish(&self) -> zbus::Result<()>; + + /// Install method + fn install(&self) -> zbus::Result<()>; + + /// IsPackageInstalled method + fn is_package_installed(&self, name: &str) -> zbus::Result; + + /// Probe method + fn probe(&self) -> zbus::Result<()>; + + /// Propose method + fn propose(&self) -> zbus::Result<()>; + + /// ProvisionSelected method + fn provision_selected(&self, provision: &str) -> zbus::Result; + + /// ProvisionsSelected method + fn provisions_selected(&self, provisions: &[&str]) -> zbus::Result>; + + /// SelectProduct method + fn select_product(&self, product_id: &str) -> zbus::Result<()>; + + /// UsedDiskSpace method + fn used_disk_space(&self) -> zbus::Result; + + /// AvailableBaseProducts property + #[dbus_proxy(property)] + fn available_base_products( + &self, + ) -> zbus::Result< + Vec<( + String, + String, + std::collections::HashMap, + )>, + >; + + /// SelectedBaseProduct property + #[dbus_proxy(property)] + fn selected_base_product(&self) -> zbus::Result; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama.Software1.Proposal", + default_service = "org.opensuse.Agama.Software1", + default_path = "/org/opensuse/Agama/Software1/Proposal" +)] +trait SoftwareProposal { + /// AddResolvables method + fn add_resolvables( + &self, + id: &str, + r#type: u8, + resolvables: &[&str], + optional: bool, + ) -> zbus::Result<()>; + + /// GetResolvables method + fn get_resolvables(&self, id: &str, r#type: u8, optional: bool) -> zbus::Result>; + + /// RemoveResolvables method + fn remove_resolvables( + &self, + id: &str, + r#type: u8, + resolvables: &[&str], + optional: bool, + ) -> zbus::Result<()>; + + /// SetResolvables method + fn set_resolvables( + &self, + id: &str, + r#type: u8, + resolvables: &[&str], + optional: bool, + ) -> zbus::Result<()>; +} diff --git a/rust/agama-lib/src/software/settings.rs b/rust/agama-lib/src/software/settings.rs new file mode 100644 index 0000000000..776e414312 --- /dev/null +++ b/rust/agama-lib/src/software/settings.rs @@ -0,0 +1,13 @@ +//! Representation of the software settings + +use crate::settings::Settings; +use agama_derive::Settings; +use serde::{Deserialize, Serialize}; + +/// Software settings for installation +#[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, +} diff --git a/rust/agama-lib/src/store/software.rs b/rust/agama-lib/src/software/store.rs similarity index 92% rename from rust/agama-lib/src/store/software.rs rename to rust/agama-lib/src/software/store.rs index f446f972e4..0e39487163 100644 --- a/rust/agama-lib/src/store/software.rs +++ b/rust/agama-lib/src/software/store.rs @@ -1,6 +1,7 @@ +//! Implements the store for the storage settings. + use crate::error::{ServiceError, WrongParameter}; -use crate::install_settings::SoftwareSettings; -use crate::software::SoftwareClient; +use super::{SoftwareSettings, SoftwareClient}; use std::error::Error; use zbus::Connection; diff --git a/rust/agama-lib/src/storage.rs b/rust/agama-lib/src/storage.rs index c948394de9..f203b7b1ed 100644 --- a/rust/agama-lib/src/storage.rs +++ b/rust/agama-lib/src/storage.rs @@ -1,80 +1,10 @@ -use super::proxies::{CalculatorProxy, Storage1Proxy, StorageProposalProxy}; -use crate::error::ServiceError; -use serde::Serialize; -use std::collections::HashMap; -use zbus::Connection; +//! Implements support for handling the storage settings -/// Represents a storage device -#[derive(Serialize, Debug)] -pub struct StorageDevice { - name: String, - description: String, -} +mod client; +mod proxies; +mod settings; +mod store; -/// D-Bus client for the storage service -pub struct StorageClient<'a> { - pub connection: Connection, - calculator_proxy: CalculatorProxy<'a>, - storage_proxy: Storage1Proxy<'a>, -} - -impl<'a> StorageClient<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { - Ok(Self { - calculator_proxy: CalculatorProxy::new(&connection).await?, - storage_proxy: Storage1Proxy::new(&connection).await?, - connection, - }) - } - - /// Returns the proposal proxy - /// - /// The proposal might not exist. - // NOTE: should we implement some kind of memoization? - async fn proposal_proxy(&self) -> Result, ServiceError> { - Ok(StorageProposalProxy::new(&self.connection).await?) - } - - /// Returns the available devices - /// - /// These devices can be used for installing the system. - pub async fn available_devices(&self) -> Result, ServiceError> { - let devices: Vec<_> = self - .calculator_proxy - .available_devices() - .await? - .into_iter() - .map(|(name, description, _)| StorageDevice { name, description }) - .collect(); - Ok(devices) - } - - /// Returns the candidate devices for the proposal - pub async fn candidate_devices(&self) -> Result, ServiceError> { - Ok(self.proposal_proxy().await?.candidate_devices().await?) - } - - /// Runs the probing process - pub async fn probe(&self) -> Result<(), ServiceError> { - Ok(self.storage_proxy.probe().await?) - } - - pub async fn calculate( - &self, - candidate_devices: Vec, - encryption_password: String, - lvm: bool, - ) -> Result { - let mut settings: HashMap<&str, zbus::zvariant::Value<'_>> = HashMap::new(); - settings.insert( - "CandidateDevices", - zbus::zvariant::Value::new(candidate_devices), - ); - settings.insert( - "EncryptionPassword", - zbus::zvariant::Value::new(encryption_password), - ); - settings.insert("LVM", zbus::zvariant::Value::new(lvm)); - Ok(self.calculator_proxy.calculate(settings).await?) - } -} +pub use client::StorageClient; +pub use settings::StorageSettings; +pub use store::StorageStore; \ No newline at end of file diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs new file mode 100644 index 0000000000..c3861983db --- /dev/null +++ b/rust/agama-lib/src/storage/client.rs @@ -0,0 +1,82 @@ +//! Implements a client to access Agama's storage service. + +use super::proxies::{CalculatorProxy, Storage1Proxy, StorageProposalProxy}; +use crate::error::ServiceError; +use serde::Serialize; +use std::collections::HashMap; +use zbus::Connection; + +/// Represents a storage device +#[derive(Serialize, Debug)] +pub struct StorageDevice { + name: String, + description: String, +} + +/// D-Bus client for the storage service +pub struct StorageClient<'a> { + pub connection: Connection, + calculator_proxy: CalculatorProxy<'a>, + storage_proxy: Storage1Proxy<'a>, +} + +impl<'a> StorageClient<'a> { + pub async fn new(connection: Connection) -> Result, ServiceError> { + Ok(Self { + calculator_proxy: CalculatorProxy::new(&connection).await?, + storage_proxy: Storage1Proxy::new(&connection).await?, + connection, + }) + } + + /// Returns the proposal proxy + /// + /// The proposal might not exist. + // NOTE: should we implement some kind of memoization? + async fn proposal_proxy(&self) -> Result, ServiceError> { + Ok(StorageProposalProxy::new(&self.connection).await?) + } + + /// Returns the available devices + /// + /// These devices can be used for installing the system. + pub async fn available_devices(&self) -> Result, ServiceError> { + let devices: Vec<_> = self + .calculator_proxy + .available_devices() + .await? + .into_iter() + .map(|(name, description, _)| StorageDevice { name, description }) + .collect(); + Ok(devices) + } + + /// Returns the candidate devices for the proposal + pub async fn candidate_devices(&self) -> Result, ServiceError> { + Ok(self.proposal_proxy().await?.candidate_devices().await?) + } + + /// Runs the probing process + pub async fn probe(&self) -> Result<(), ServiceError> { + Ok(self.storage_proxy.probe().await?) + } + + pub async fn calculate( + &self, + candidate_devices: Vec, + encryption_password: String, + lvm: bool, + ) -> Result { + let mut settings: HashMap<&str, zbus::zvariant::Value<'_>> = HashMap::new(); + settings.insert( + "CandidateDevices", + zbus::zvariant::Value::new(candidate_devices), + ); + settings.insert( + "EncryptionPassword", + zbus::zvariant::Value::new(encryption_password), + ); + settings.insert("LVM", zbus::zvariant::Value::new(lvm)); + Ok(self.calculator_proxy.calculate(settings).await?) + } +} diff --git a/rust/agama-lib/src/storage/proxies.rs b/rust/agama-lib/src/storage/proxies.rs new file mode 100644 index 0000000000..19ba25ff50 --- /dev/null +++ b/rust/agama-lib/src/storage/proxies.rs @@ -0,0 +1,106 @@ +//! D-Bus interface proxies for: `org.opensuse.Agama.Storage1.*` +//! +//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.opensuse.Agama1.Validation", + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" +)] +trait Validation { + /// Errors property + #[dbus_proxy(property)] + fn errors(&self) -> zbus::Result>; + + /// Valid property + #[dbus_proxy(property)] + fn valid(&self) -> zbus::Result; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama.Storage1", + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" +)] +trait Storage1 { + /// Finish method + fn finish(&self) -> zbus::Result<()>; + + /// Install method + fn install(&self) -> zbus::Result<()>; + + /// Probe method + fn probe(&self) -> zbus::Result<()>; + + /// DeprecatedSystem property + #[dbus_proxy(property)] + fn deprecated_system(&self) -> zbus::Result; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama.Storage1.Proposal.Calculator", + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1" +)] +trait Calculator { + /// Calculate method + fn calculate( + &self, + settings: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, + ) -> zbus::Result; + + /// AvailableDevices property + #[dbus_proxy(property)] + fn available_devices( + &self, + ) -> zbus::Result< + Vec<( + String, + String, + std::collections::HashMap, + )>, + >; + + /// Result property + #[dbus_proxy(property)] + fn result(&self) -> zbus::Result; + + /// VolumeTemplates property + #[dbus_proxy(property)] + fn volume_templates( + &self, + ) -> zbus::Result>>; +} + +#[dbus_proxy( + interface = "org.opensuse.Agama.Storage1.Proposal", + default_service = "org.opensuse.Agama.Storage1", + default_path = "/org/opensuse/Agama/Storage1/Proposal" +)] +trait StorageProposal { + /// Actions property + #[dbus_proxy(property)] + fn actions( + &self, + ) -> zbus::Result>>; + + /// CandidateDevices property + #[dbus_proxy(property)] + fn candidate_devices(&self) -> zbus::Result>; + + /// EncryptionPassword property + #[dbus_proxy(property)] + fn encryption_password(&self) -> zbus::Result; + + /// LVM property + #[dbus_proxy(property, name = "LVM")] + fn lvm(&self) -> zbus::Result; + + /// Volumes property + #[dbus_proxy(property)] + fn volumes( + &self, + ) -> zbus::Result>>; +} + diff --git a/rust/agama-lib/src/storage/settings.rs b/rust/agama-lib/src/storage/settings.rs new file mode 100644 index 0000000000..a17cad4327 --- /dev/null +++ b/rust/agama-lib/src/storage/settings.rs @@ -0,0 +1,39 @@ +//! Representation of the storage settings + +use crate::settings::{SettingObject, Settings}; +use agama_derive::Settings; +use serde::{Deserialize, Serialize}; + +/// Storage settings for installation +#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageSettings { + /// Whether LVM should be enabled + pub lvm: Option, + /// Encryption password for the storage devices (in clear text) + pub encryption_password: Option, + /// Devices to use in the installation + #[collection_setting] + pub devices: Vec, +} + +/// Device to use in the installation +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Device { + /// Device name (e.g., "/dev/sda") + pub name: String, +} + +impl TryFrom for Device { + type Error = &'static str; + + fn try_from(value: SettingObject) -> Result { + match value.0.get("name") { + Some(name) => Ok(Device { + name: name.clone().try_into()?, + }), + None => Err("'name' key not found"), + } + } +} diff --git a/rust/agama-lib/src/store/storage.rs b/rust/agama-lib/src/storage/store.rs similarity index 91% rename from rust/agama-lib/src/store/storage.rs rename to rust/agama-lib/src/storage/store.rs index add7fef705..22b873c356 100644 --- a/rust/agama-lib/src/store/storage.rs +++ b/rust/agama-lib/src/storage/store.rs @@ -1,6 +1,7 @@ +//! Implements the store for the storage settings. + use crate::error::ServiceError; -use crate::install_settings::StorageSettings; -use crate::storage::StorageClient; +use super::{StorageClient, StorageSettings}; use std::default::Default; use std::error::Error; use zbus::Connection; diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 04a69fc9bf..65ebef8c09 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -1,17 +1,17 @@ -mod software; -mod storage; -mod users; +//! Load/store the settings from/to the D-Bus services. use crate::error::ServiceError; use crate::install_settings::{InstallSettings, Scope}; -use crate::network::NetworkStore; -use crate::store::software::SoftwareStore; -use crate::store::storage::StorageStore; -use crate::store::users::UsersStore; +use crate::{ + network::NetworkStore, software::SoftwareStore, storage::StorageStore, users::UsersStore, +}; use std::error::Error; use zbus::Connection; -/// Loading and storing the settings in the D-Bus service +/// Struct that loads/stores the settings from/to the D-Bus services. +/// +/// It is composed by a set of "stores" that are able to load/store the +/// settings for each service. /// /// This struct uses the default connection built by [connection function](super::connection). pub struct Store<'a> { diff --git a/rust/agama-lib/src/users.rs b/rust/agama-lib/src/users.rs index d88ab3a76a..5e816f0a02 100644 --- a/rust/agama-lib/src/users.rs +++ b/rust/agama-lib/src/users.rs @@ -1,114 +1,10 @@ -//! Users configuration support +//! Implements support for handling the users settings -use super::proxies::Users1Proxy; -use crate::error::ServiceError; -use crate::settings::{SettingValue, Settings}; -use serde::Serialize; -use zbus::Connection; +mod client; +mod settings; +mod proxies; +mod store; -/// Represents the settings for the first user -#[derive(Serialize, Debug, Default)] -pub struct FirstUser { - /// First user's full name - pub full_name: String, - /// First user's username - pub user_name: String, - /// First user's password (in clear text) - pub password: String, - /// Whether auto-login should enabled or not - pub autologin: bool, - /// Additional data coming from the D-Bus service - pub data: std::collections::HashMap, -} - -impl FirstUser { - pub fn from_dbus( - dbus_data: zbus::Result<( - String, - String, - String, - bool, - std::collections::HashMap, - )>, - ) -> zbus::Result { - let data = dbus_data?; - Ok(Self { - full_name: data.0, - user_name: data.1, - password: data.2, - autologin: data.3, - data: data.4, - }) - } -} - -impl Settings for FirstUser { - fn set(&mut self, attr: &str, value: SettingValue) -> Result<(), &'static str> { - match attr { - "full_name" => self.full_name = value.try_into()?, - "user_name" => self.user_name = value.try_into()?, - "password" => self.password = value.try_into()?, - "autologin" => self.autologin = value.try_into()?, - _ => return Err("unknown attribute"), - } - Ok(()) - } -} - -/// D-Bus client for the users service -pub struct UsersClient<'a> { - users_proxy: Users1Proxy<'a>, -} - -impl<'a> UsersClient<'a> { - pub async fn new(connection: Connection) -> zbus::Result> { - Ok(Self { - users_proxy: Users1Proxy::new(&connection).await?, - }) - } - - /// Returns the settings for first non admin user - pub async fn first_user(&self) -> zbus::Result { - FirstUser::from_dbus(self.users_proxy.first_user().await) - } - - /// SetRootPassword method - pub async fn set_root_password( - &self, - value: &str, - encrypted: bool, - ) -> Result { - Ok(self.users_proxy.set_root_password(value, encrypted).await?) - } - - /// Whether the root password is set or not - pub async fn is_root_password(&self) -> Result { - Ok(self.users_proxy.root_password_set().await?) - } - - /// Returns the SSH key for the root user - pub async fn root_ssh_key(&self) -> zbus::Result { - self.users_proxy.root_sshkey().await - } - - /// SetRootSSHKey method - pub async fn set_root_sshkey(&self, value: &str) -> Result { - Ok(self.users_proxy.set_root_sshkey(value).await?) - } - - /// Set the configuration for the first user - pub async fn set_first_user( - &self, - first_user: &FirstUser, - ) -> zbus::Result<(bool, Vec)> { - self.users_proxy - .set_first_user( - &first_user.full_name, - &first_user.user_name, - &first_user.password, - first_user.autologin, - std::collections::HashMap::new(), - ) - .await - } -} +pub use client::{UsersClient, FirstUser}; +pub use settings::{FirstUserSettings, RootUserSettings, UserSettings}; +pub use store::UsersStore; \ No newline at end of file diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs new file mode 100644 index 0000000000..c81ccdeae3 --- /dev/null +++ b/rust/agama-lib/src/users/client.rs @@ -0,0 +1,114 @@ +//! Implements a client to access Agama's users service. + +use super::proxies::Users1Proxy; +use crate::error::ServiceError; +use crate::settings::{SettingValue, Settings}; +use serde::Serialize; +use zbus::Connection; + +/// Represents the settings for the first user +#[derive(Serialize, Debug, Default)] +pub struct FirstUser { + /// First user's full name + pub full_name: String, + /// First user's username + pub user_name: String, + /// First user's password (in clear text) + pub password: String, + /// Whether auto-login should enabled or not + pub autologin: bool, + /// Additional data coming from the D-Bus service + pub data: std::collections::HashMap, +} + +impl FirstUser { + pub fn from_dbus( + dbus_data: zbus::Result<( + String, + String, + String, + bool, + std::collections::HashMap, + )>, + ) -> zbus::Result { + let data = dbus_data?; + Ok(Self { + full_name: data.0, + user_name: data.1, + password: data.2, + autologin: data.3, + data: data.4, + }) + } +} + +impl Settings for FirstUser { + fn set(&mut self, attr: &str, value: SettingValue) -> Result<(), &'static str> { + match attr { + "full_name" => self.full_name = value.try_into()?, + "user_name" => self.user_name = value.try_into()?, + "password" => self.password = value.try_into()?, + "autologin" => self.autologin = value.try_into()?, + _ => return Err("unknown attribute"), + } + Ok(()) + } +} + +/// D-Bus client for the users service +pub struct UsersClient<'a> { + users_proxy: Users1Proxy<'a>, +} + +impl<'a> UsersClient<'a> { + pub async fn new(connection: Connection) -> zbus::Result> { + Ok(Self { + users_proxy: Users1Proxy::new(&connection).await?, + }) + } + + /// Returns the settings for first non admin user + pub async fn first_user(&self) -> zbus::Result { + FirstUser::from_dbus(self.users_proxy.first_user().await) + } + + /// SetRootPassword method + pub async fn set_root_password( + &self, + value: &str, + encrypted: bool, + ) -> Result { + Ok(self.users_proxy.set_root_password(value, encrypted).await?) + } + + /// Whether the root password is set or not + pub async fn is_root_password(&self) -> Result { + Ok(self.users_proxy.root_password_set().await?) + } + + /// Returns the SSH key for the root user + pub async fn root_ssh_key(&self) -> zbus::Result { + self.users_proxy.root_sshkey().await + } + + /// SetRootSSHKey method + pub async fn set_root_sshkey(&self, value: &str) -> Result { + Ok(self.users_proxy.set_root_sshkey(value).await?) + } + + /// Set the configuration for the first user + pub async fn set_first_user( + &self, + first_user: &FirstUser, + ) -> zbus::Result<(bool, Vec)> { + self.users_proxy + .set_first_user( + &first_user.full_name, + &first_user.user_name, + &first_user.password, + first_user.autologin, + std::collections::HashMap::new(), + ) + .await + } +} \ No newline at end of file diff --git a/rust/agama-lib/src/users/proxies.rs b/rust/agama-lib/src/users/proxies.rs new file mode 100644 index 0000000000..4a2850a8f3 --- /dev/null +++ b/rust/agama-lib/src/users/proxies.rs @@ -0,0 +1,57 @@ +//! D-Bus interface proxies for: `org.opensuse.Agama.Users1.*` +//! +//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data.`. +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.opensuse.Agama.Users1", + default_service = "org.opensuse.Agama.Users1", + default_path = "/org/opensuse/Agama/Users1" +)] +trait Users1 { + /// RemoveFirstUser method + fn remove_first_user(&self) -> zbus::Result; + + /// RemoveRootPassword method + fn remove_root_password(&self) -> zbus::Result; + + /// SetFirstUser method + fn set_first_user( + &self, + full_name: &str, + user_name: &str, + password: &str, + auto_login: bool, + data: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, + ) -> zbus::Result<(bool, Vec)>; + + /// SetRootPassword method + fn set_root_password(&self, value: &str, encrypted: bool) -> zbus::Result; + + /// SetRootSSHKey method + #[dbus_proxy(name = "SetRootSSHKey")] + fn set_root_sshkey(&self, value: &str) -> zbus::Result; + + /// Write method + fn write(&self) -> zbus::Result; + + /// FirstUser property + #[dbus_proxy(property)] + fn first_user( + &self, + ) -> zbus::Result<( + String, + String, + String, + bool, + std::collections::HashMap, + )>; + + /// RootPasswordSet property + #[dbus_proxy(property)] + fn root_password_set(&self) -> zbus::Result; + + /// RootSSHKey property + #[dbus_proxy(property, name = "RootSSHKey")] + fn root_sshkey(&self) -> zbus::Result; +} diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs new file mode 100644 index 0000000000..a7efc1de10 --- /dev/null +++ b/rust/agama-lib/src/users/settings.rs @@ -0,0 +1,111 @@ +use crate::settings::{SettingValue, Settings}; +use agama_derive::Settings; +use serde::{Deserialize, Serialize}; + +/// User settings +/// +/// Holds the user settings for the installation. +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserSettings { + #[serde(rename = "user")] + pub first_user: Option, + pub root: Option, +} + +impl Settings for UserSettings { + fn set(&mut self, attr: &str, value: SettingValue) -> Result<(), &'static str> { + if let Some((ns, id)) = attr.split_once('.') { + match ns { + "user" => { + let first_user = self.first_user.get_or_insert(Default::default()); + first_user.set(id, value)? + } + "root" => { + let root_user = self.root.get_or_insert(Default::default()); + root_user.set(id, value)? + } + _ => return Err("unknown attribute"), + } + } + Ok(()) + } + + fn merge(&mut self, other: &Self) { + if let Some(other_first_user) = &other.first_user { + let first_user = self.first_user.get_or_insert(Default::default()); + first_user.merge(other_first_user); + } + + if let Some(other_root) = &other.root { + let root = self.root.get_or_insert(Default::default()); + root.merge(other_root); + } + } +} + +/// First user settings +/// +/// Holds the settings for the first user. +#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FirstUserSettings { + /// First user's full name + pub full_name: Option, + /// First user's username + pub user_name: Option, + /// First user's password (in clear text) + pub password: Option, + /// Whether auto-login should enabled or not + pub autologin: Option, +} + +/// Root user settings +/// +/// Holds the settings for the root user. +#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RootUserSettings { + /// Root's password (in clear text) + #[serde(skip_serializing)] + pub password: Option, + /// Root SSH public key + pub ssh_public_key: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_user_settings_merge() { + let mut user1 = UserSettings::default(); + let user2 = UserSettings { + first_user: Some(FirstUserSettings { + full_name: Some("Jane Doe".to_string()), + ..Default::default() + }), + root: Some(RootUserSettings { + password: Some("nots3cr3t".to_string()), + ..Default::default() + }), + }; + user1.merge(&user2); + let first_user = user1.first_user.unwrap(); + assert_eq!(first_user.full_name, Some("Jane Doe".to_string())); + let root_user = user1.root.unwrap(); + assert_eq!(root_user.password, Some("nots3cr3t".to_string())); + } + + #[test] + fn test_merge() { + let mut user1 = FirstUserSettings::default(); + let user2 = FirstUserSettings { + full_name: Some("Jane Doe".to_owned()), + autologin: Some(true), + ..Default::default() + }; + user1.merge(&user2); + assert_eq!(user1.full_name.unwrap(), "Jane Doe") + } +} \ No newline at end of file diff --git a/rust/agama-lib/src/store/users.rs b/rust/agama-lib/src/users/store.rs similarity index 95% rename from rust/agama-lib/src/store/users.rs rename to rust/agama-lib/src/users/store.rs index a4dfa841d1..397051b854 100644 --- a/rust/agama-lib/src/store/users.rs +++ b/rust/agama-lib/src/users/store.rs @@ -1,6 +1,5 @@ use crate::error::WrongParameter; -use crate::install_settings::{FirstUserSettings, RootUserSettings, UserSettings}; -use crate::users::{FirstUser, UsersClient}; +use super::{FirstUserSettings, RootUserSettings, UserSettings, UsersClient, FirstUser}; use std::error::Error; use zbus::Connection;