From 834632066b99a6a130c0a96f5cfe23d9af95d8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 29 May 2024 16:30:27 +0100 Subject: [PATCH 1/9] feat(cli): drop config add/set --- rust/Cargo.lock | 19 -- rust/Cargo.toml | 2 - rust/agama-cli/Cargo.toml | 1 - rust/agama-cli/src/config.rs | 47 +-- rust/agama-derive/Cargo.toml | 13 - rust/agama-derive/src/lib.rs | 313 -------------------- rust/agama-lib/Cargo.toml | 1 - rust/agama-lib/src/install_settings.rs | 10 +- rust/agama-lib/src/localization/settings.rs | 3 +- rust/agama-lib/src/network/settings.rs | 92 +----- rust/agama-lib/src/product/settings.rs | 3 +- rust/agama-lib/src/software/settings.rs | 4 +- rust/agama-lib/src/storage/settings.rs | 3 +- rust/agama-lib/src/users/client.rs | 32 -- rust/agama-lib/src/users/settings.rs | 47 +-- rust/agama-settings/Cargo.toml | 10 - rust/agama-settings/src/error.rs | 27 -- rust/agama-settings/src/lib.rs | 63 ---- rust/agama-settings/src/settings.rs | 168 ----------- rust/agama-settings/tests/settings.rs | 106 ------- 20 files changed, 13 insertions(+), 951 deletions(-) delete mode 100644 rust/agama-derive/Cargo.toml delete mode 100644 rust/agama-derive/src/lib.rs delete mode 100644 rust/agama-settings/Cargo.toml delete mode 100644 rust/agama-settings/src/error.rs delete mode 100644 rust/agama-settings/src/lib.rs delete mode 100644 rust/agama-settings/src/settings.rs delete mode 100644 rust/agama-settings/tests/settings.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a506b415f3..e8d44152f2 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -22,7 +22,6 @@ name = "agama-cli" version = "1.0.0" dependencies = [ "agama-lib", - "agama-settings", "anyhow", "async-trait", "clap", @@ -45,20 +44,10 @@ dependencies = [ "zbus", ] -[[package]] -name = "agama-derive" -version = "1.0.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.53", -] - [[package]] name = "agama-lib" version = "1.0.0" dependencies = [ - "agama-settings", "anyhow", "async-trait", "chrono", @@ -146,14 +135,6 @@ dependencies = [ "zbus_macros", ] -[[package]] -name = "agama-settings" -version = "1.0.0" -dependencies = [ - "agama-derive", - "thiserror", -] - [[package]] name = "ahash" version = "0.8.11" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6885b0b788..991ffaae7f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,10 +2,8 @@ members = [ "agama-cli", "agama-server", - "agama-derive", "agama-lib", "agama-locale-data", - "agama-settings", ] resolver = "2" diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index 2e5007ded4..7ad277b7c6 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" clap = { version = "4.1.4", features = ["derive", "wrap_help"] } curl = { version = "0.4.44", features = ["protocol-ftp"] } agama-lib = { path="../agama-lib" } -agama-settings = { path="../agama-settings" } serde = { version = "1.0.152" } serde_json = "1.0.91" serde_yaml = "0.9.17" diff --git a/rust/agama-cli/src/config.rs b/rust/agama-cli/src/config.rs index eae517a449..83cdfb1832 100644 --- a/rust/agama-cli/src/config.rs +++ b/rust/agama-cli/src/config.rs @@ -8,29 +8,12 @@ use agama_lib::{ install_settings::{InstallSettings, Scope}, Store as SettingsStore, }; -use agama_settings::{settings::Settings, SettingObject, SettingValue}; use clap::Subcommand; use convert_case::{Case, Casing}; use std::{collections::HashMap, error::Error, io, str::FromStr}; #[derive(Subcommand, Debug)] pub enum ConfigCommands { - /// Add an element to a collection. - /// - /// In case of collections, this command allows adding a new element. For instance, let's add a - /// new item to the list of software patterns: - /// - /// $ agama config add software.patterns value=gnome - Add { key: String, values: Vec }, - - /// Set one or many installation settings - /// - /// For scalar values, this command allows setting a new value. For instance, let's change the - /// product to install: - /// - /// $ agama config set product.id=Tumbleweed - Set { values: Vec }, - /// Shows the value of the configuration settings. /// /// It is possible that many configuration settings do not have a value. Those settings @@ -47,8 +30,6 @@ pub enum ConfigCommands { } pub enum ConfigAction { - Add(String, HashMap), - Set(HashMap), Show, Load(String), } @@ -64,46 +45,26 @@ pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<( let command = parse_config_command(subcommand)?; match command { - ConfigAction::Set(changes) => { - let scopes = changes - .keys() - .filter_map(|k| key_to_scope(k).ok()) - .collect(); - let mut model = store.load(Some(scopes)).await?; - for (key, value) in changes { - model.set(&key.to_case(Case::Snake), SettingValue(value))?; - } - Ok(store.store(&model).await?) - } ConfigAction::Show => { let model = store.load(None).await?; print(model, io::stdout(), format)?; Ok(()) } - ConfigAction::Add(key, values) => { - let scope = key_to_scope(&key).unwrap(); - let mut model = store.load(Some(vec![scope])).await?; - model.add(&key.to_case(Case::Snake), SettingObject::from(values))?; - Ok(store.store(&model).await?) - } ConfigAction::Load(path) => { let contents = std::fs::read_to_string(path)?; let result: InstallSettings = serde_json::from_str(&contents)?; let scopes = result.defined_scopes(); - let mut model = store.load(Some(scopes)).await?; - model.merge(&result); - Ok(store.store(&model).await?) + // FIXME: merging should be implemented + // let mut model = store.load(Some(scopes)).await?; + // model.merge(&result); + Ok(store.store(&result).await?) } } } fn parse_config_command(subcommand: ConfigCommands) -> Result { match subcommand { - ConfigCommands::Add { key, values } => { - Ok(ConfigAction::Add(key, parse_keys_values(values)?)) - } ConfigCommands::Show => Ok(ConfigAction::Show), - ConfigCommands::Set { values } => Ok(ConfigAction::Set(parse_keys_values(values)?)), ConfigCommands::Load { path } => Ok(ConfigAction::Load(path)), } } diff --git a/rust/agama-derive/Cargo.toml b/rust/agama-derive/Cargo.toml deleted file mode 100644 index dcc7ab1160..0000000000 --- a/rust/agama-derive/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "agama-derive" -version = "1.0.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0.51" -quote = "1.0" -syn = "2.0" diff --git a/rust/agama-derive/src/lib.rs b/rust/agama-derive/src/lib.rs deleted file mode 100644 index ec72af3637..0000000000 --- a/rust/agama-derive/src/lib.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! Implements a derive macro to implement the Settings from the `agama_settings` crate. -//! -//! ```no_compile -//! use agama_settings::{Settings, settings::Settings}; -//! -//! #[derive(Default, Settings)] -//! struct UserSettings { -//! name: Option, -//! enabled: Option -//! } -//! -//! #[derive(Default, Settings)] -//! struct InstallSettings { -//! #[settings(nested, alias = "first_user")] -//! user: Option, -//! reboot: Option -//! product: Option, -//! #[settings(collection)] -//! packages: Vec -//! } -//! -//! ## Supported attributes -//! -//! * `nested`: the field is another struct that implements `Settings`. -//! * `collection`: the attribute is a vector of elements of type T. You might need to implement -//! `TryFrom for T` for your custom types. -//! * `flatten`: the field is flatten (in serde jargon). -//! * `alias`: and alternative name for the field. It can be specified several times. -//! ``` - -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::{parse_macro_input, DeriveInput, Fields, LitStr}; - -#[derive(Debug, Clone, Copy, PartialEq)] -enum SettingKind { - /// A single value; the default. - Scalar, - /// An array of scalars, use `#[settings(collection)]`. - Collection, - /// The value is another FooSettings, use `#[settings(nested)]`. - Nested, - Ignored, -} - -/// Represents a setting and its configuration -#[derive(Debug, Clone)] -struct SettingField { - /// Setting ident ("A word of Rust code, may be a keyword or variable name"). - pub ident: syn::Ident, - /// Setting kind (scalar, collection, struct). - pub kind: SettingKind, - /// Whether it is a flatten (in serde jargon) value. - pub flatten: bool, - /// Aliases for the field (especially useful for flatten fields). - pub aliases: Vec, -} - -impl SettingField { - pub fn new(ident: syn::Ident) -> Self { - Self { - ident, - kind: SettingKind::Scalar, - flatten: false, - aliases: vec![], - } - } -} - -/// List of setting fields -#[derive(Debug)] -struct SettingFieldsList(Vec); - -impl SettingFieldsList { - pub fn by_type(&self, kind: SettingKind) -> Vec<&SettingField> { - self.0.iter().filter(|f| f.kind == kind).collect() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - // TODO: implement Iterator? - pub fn all(&self) -> &Vec { - &self.0 - } -} - -/// Derive Settings, typically for a FooSettings struct. -/// (see the trait agama_settings::settings::Settings but I cannot link to it without a circular dependency) -#[proc_macro_derive(Settings, attributes(settings))] -pub fn agama_attributes_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let fields = match &input.data { - syn::Data::Struct(syn::DataStruct { - fields: Fields::Named(fields), - .. - }) => &fields.named, - _ => panic!("only structs are supported"), - }; - - let fields: Vec<&syn::Field> = fields.iter().collect(); - let settings = parse_setting_fields(fields); - - let set_fn = expand_set_fn(&settings); - let add_fn = expand_add_fn(&settings); - let merge_fn = expand_merge_fn(&settings); - - let name = input.ident; - let expanded = quote! { - impl agama_settings::settings::Settings for #name { - #set_fn - #add_fn - #merge_fn - } - }; - - expanded.into() -} - -fn expand_set_fn(settings: &SettingFieldsList) -> TokenStream2 { - let scalar_fields = settings.by_type(SettingKind::Scalar); - let nested_fields = settings.by_type(SettingKind::Nested); - if scalar_fields.is_empty() && nested_fields.is_empty() { - return quote! {}; - } - - let mut scalar_handling = quote! { Ok(()) }; - if !scalar_fields.is_empty() { - let field_name = scalar_fields.iter().map(|s| s.ident.clone()); - scalar_handling = quote! { - match attr { - #(stringify!(#field_name) => self.#field_name = value.try_into().map_err(|e| { - agama_settings::SettingsError::UpdateFailed(attr.to_string(), e) - })?,)* - _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) - } - Ok(()) - } - } - - let mut nested_handling = quote! {}; - if !nested_fields.is_empty() { - let field_name = nested_fields.iter().map(|s| s.ident.clone()); - let aliases = quote_fields_aliases(&nested_fields); - let attr = nested_fields - .iter() - .map(|s| if s.flatten { quote!(attr) } else { quote!(id) }); - nested_handling = quote! { - if let Some((ns, id)) = attr.split_once('.') { - match ns { - #(stringify!(#field_name) #aliases => { - let #field_name = self.#field_name.get_or_insert(Default::default()); - #field_name.set(#attr, value).map_err(|e| e.with_attr(attr))? - })* - _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) - } - return Ok(()) - } - } - } - - quote! { - fn set(&mut self, attr: &str, value: agama_settings::SettingValue) -> Result<(), agama_settings::SettingsError> { - #nested_handling - #scalar_handling - } - } -} - -fn expand_merge_fn(settings: &SettingFieldsList) -> TokenStream2 { - if settings.is_empty() { - return quote! {}; - } - - let arms = settings.all().iter().map(|s| { - let field_name = &s.ident; - match s.kind { - SettingKind::Scalar | SettingKind::Ignored => quote! { - if let Some(value) = &other.#field_name { - self.#field_name = Some(value.clone()) - } - }, - SettingKind::Nested => quote! { - if let Some(other_value) = &other.#field_name { - let nested = self.#field_name.get_or_insert(Default::default()); - nested.merge(other_value); - } - }, - SettingKind::Collection => quote! { - self.#field_name = other.#field_name.clone(); - }, - } - }); - - quote! { - fn merge(&mut self, other: &Self) - where - Self: Sized, - { - #(#arms)* - } - } -} - -fn expand_add_fn(settings: &SettingFieldsList) -> TokenStream2 { - let collection_fields = settings.by_type(SettingKind::Collection); - let nested_fields = settings.by_type(SettingKind::Nested); - if collection_fields.is_empty() && nested_fields.is_empty() { - return quote! {}; - } - - let mut collection_handling = quote! { Ok(()) }; - if !collection_fields.is_empty() { - let field_name = collection_fields.iter().map(|s| s.ident.clone()); - collection_handling = quote! { - match attr { - #(stringify!(#field_name) => { - let converted = value.try_into().map_err(|e| { - agama_settings::SettingsError::UpdateFailed(attr.to_string(), e) - })?; - self.#field_name.push(converted) - },)* - _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) - } - Ok(()) - }; - } - - let mut nested_handling = quote! {}; - if !nested_fields.is_empty() { - let field_name = nested_fields.iter().map(|s| s.ident.clone()); - nested_handling = quote! { - if let Some((ns, id)) = attr.split_once('.') { - match ns { - #(stringify!(#field_name) => { - let #field_name = self.#field_name.get_or_insert(Default::default()); - #field_name.add(id, value).map_err(|e| e.with_attr(attr))? - })* - _ => return Err(agama_settings::SettingsError::UnknownAttribute(attr.to_string())) - } - return Ok(()) - } - } - } - quote! { - fn add(&mut self, attr: &str, value: agama_settings::SettingObject) -> Result<(), agama_settings::SettingsError> { - #nested_handling - #collection_handling - } - } -} - -// Extracts information about the settings fields. -// -// syn::Field is "A field of a struct or enum variant.", -// has .ident .ty(pe) .mutability .vis(ibility)... -fn parse_setting_fields(fields: Vec<&syn::Field>) -> SettingFieldsList { - let mut settings = vec![]; - for field in fields { - let ident = field.ident.clone().expect("to find a field ident"); - let mut setting = SettingField::new(ident); - for attr in &field.attrs { - if !attr.path().is_ident("settings") { - continue; - } - - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("collection") { - setting.kind = SettingKind::Collection; - }; - - if meta.path.is_ident("nested") { - setting.kind = SettingKind::Nested; - } - - if meta.path.is_ident("ignored") { - setting.kind = SettingKind::Ignored; - } - - if meta.path.is_ident("flatten") { - setting.flatten = true; - } - - if meta.path.is_ident("alias") { - let value = meta.value()?; - let alias: LitStr = value.parse()?; - setting.aliases.push(alias.value()); - } - - Ok(()) - }) - .expect("settings arguments do not follow the expected structure"); - } - settings.push(setting); - } - SettingFieldsList(settings) -} - -fn quote_fields_aliases(nested_fields: &[&SettingField]) -> Vec { - nested_fields - .iter() - .map(|f| { - let aliases = f.aliases.clone(); - if aliases.is_empty() { - quote! {} - } else { - quote! { #(| #aliases)* } - } - }) - .collect() -} diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index 4dabd10eca..e6101ba679 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -agama-settings = { path="../agama-settings" } anyhow = "1.0" async-trait = "0.1.77" cidr = { version = "0.2.2", features = ["serde"] } diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index de4ddff75b..f1211878f9 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -5,7 +5,6 @@ use crate::{ localization::LocalizationSettings, network::NetworkSettings, product::ProductSettings, software::SoftwareSettings, storage::StorageSettings, users::UserSettings, }; -use agama_settings::Settings; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use std::default::Default; @@ -73,29 +72,22 @@ impl FromStr for Scope { /// /// This struct represents installation settings. It serves as an entry point and it is composed of /// other structs which hold the settings for each area ("users", "software", etc.). -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InstallSettings { #[serde(default, flatten)] - #[settings(nested, flatten, alias = "root")] pub user: Option, #[serde(default)] - #[settings(nested)] pub software: Option, #[serde(default)] - #[settings(nested)] pub product: Option, #[serde(default)] - #[settings(nested)] pub storage: Option, #[serde(default, rename = "legacyAutoyastStorage")] - #[settings(ignored)] pub storage_autoyast: Option>, #[serde(default)] - #[settings(nested)] pub network: Option, #[serde(default)] - #[settings(nested)] pub localization: Option, } diff --git a/rust/agama-lib/src/localization/settings.rs b/rust/agama-lib/src/localization/settings.rs index 7de43d26fd..a3692c8ddf 100644 --- a/rust/agama-lib/src/localization/settings.rs +++ b/rust/agama-lib/src/localization/settings.rs @@ -1,10 +1,9 @@ //! Representation of the localization settings -use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Localization settings for the system being installed (not the UI) -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LocalizationSettings { /// like "en_US.UTF-8" diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index b78934f40e..bb11184bc8 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -1,8 +1,6 @@ //! Representation of the network settings use super::types::{DeviceState, DeviceType, Status}; -use agama_settings::error::ConversionError; -use agama_settings::{SettingObject, SettingValue, Settings}; use cidr::IpInet; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -10,11 +8,10 @@ use std::default::Default; use std::net::IpAddr; /// Network settings for installation -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NetworkSettings { /// Connections to use in the installation - #[settings(collection)] pub connections: Vec, } @@ -126,90 +123,3 @@ impl NetworkConnection { } } } - -impl TryFrom for NetworkConnection { - type Error = ConversionError; - - fn try_from(value: SettingObject) -> Result { - let Some(id) = value.get("id") else { - return Err(ConversionError::MissingKey("id".to_string())); - }; - - let default_method = SettingValue("disabled".to_string()); - let method4 = value.get("method4").unwrap_or(&default_method); - let method6 = value.get("method6").unwrap_or(&default_method); - - let conn = NetworkConnection { - id: id.clone().try_into()?, - method4: method4.clone().try_into()?, - method6: method6.clone().try_into()?, - ..Default::default() - }; - - Ok(conn) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use agama_settings::{settings::Settings, SettingObject, SettingValue}; - use std::collections::HashMap; - - #[test] - fn test_device_type() { - let eth = NetworkConnection::default(); - assert_eq!(eth.device_type(), DeviceType::Ethernet); - - let wlan = NetworkConnection { - wireless: Some(WirelessSettings::default()), - ..Default::default() - }; - - let bond = NetworkConnection { - bond: Some(BondSettings::default()), - ..Default::default() - }; - - assert_eq!(wlan.device_type(), DeviceType::Wireless); - assert_eq!(bond.device_type(), DeviceType::Bond); - } - - #[test] - fn test_bonding_defaults() { - let bond = BondSettings::default(); - - assert_eq!(bond.mode, "balance-rr".to_string()); - assert_eq!(bond.ports.len(), 0); - assert_eq!(bond.options, None); - } - - #[test] - fn test_add_connection_to_setting() { - let name = SettingValue("Ethernet 1".to_string()); - let method = SettingValue("auto".to_string()); - let conn = HashMap::from([("id".to_string(), name), ("method".to_string(), method)]); - let conn = SettingObject(conn); - - let mut settings = NetworkSettings::default(); - settings.add("connections", conn).unwrap(); - assert_eq!(settings.connections.len(), 1); - } - - #[test] - fn test_setting_object_to_network_connection() { - let name = SettingValue("Ethernet 1".to_string()); - let method_auto = SettingValue("auto".to_string()); - let method_disabled = SettingValue("disabled".to_string()); - let settings = HashMap::from([ - ("id".to_string(), name), - ("method4".to_string(), method_auto), - ("method6".to_string(), method_disabled), - ]); - let settings = SettingObject(settings); - let conn: NetworkConnection = settings.try_into().unwrap(); - assert_eq!(conn.id, "Ethernet 1"); - assert_eq!(conn.method4, Some("auto".to_string())); - assert_eq!(conn.method6, Some("disabled".to_string())); - } -} diff --git a/rust/agama-lib/src/product/settings.rs b/rust/agama-lib/src/product/settings.rs index 647801470e..b4674a3d9c 100644 --- a/rust/agama-lib/src/product/settings.rs +++ b/rust/agama-lib/src/product/settings.rs @@ -1,10 +1,9 @@ //! Representation of the product settings -use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Software settings for installation -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProductSettings { /// ID of the product to install (e.g., "ALP", "Tumbleweed", etc.) diff --git a/rust/agama-lib/src/software/settings.rs b/rust/agama-lib/src/software/settings.rs index a2dba395ee..9601b96837 100644 --- a/rust/agama-lib/src/software/settings.rs +++ b/rust/agama-lib/src/software/settings.rs @@ -1,13 +1,11 @@ //! Representation of the software settings -use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Software settings for installation -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SoftwareSettings { /// List of patterns to install. If empty use default. - #[settings(collection)] pub patterns: Vec, } diff --git a/rust/agama-lib/src/storage/settings.rs b/rust/agama-lib/src/storage/settings.rs index cb786d651f..601a2ce80f 100644 --- a/rust/agama-lib/src/storage/settings.rs +++ b/rust/agama-lib/src/storage/settings.rs @@ -1,10 +1,9 @@ //! Representation of the storage settings -use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// Storage settings for installation -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StorageSettings { /// Whether LVM should be enabled diff --git a/rust/agama-lib/src/users/client.rs b/rust/agama-lib/src/users/client.rs index 979b788e3c..eeb0df73e6 100644 --- a/rust/agama-lib/src/users/client.rs +++ b/rust/agama-lib/src/users/client.rs @@ -2,7 +2,6 @@ use super::proxies::{FirstUser as FirstUserFromDBus, Users1Proxy}; use crate::error::ServiceError; -use agama_settings::{settings::Settings, SettingValue, SettingsError}; use serde::{Deserialize, Serialize}; use zbus::Connection; @@ -35,37 +34,6 @@ impl FirstUser { } } -// TODO: use the Settings macro (add support for ignoring fields to the macro and use Option for -// FirstUser fields) -impl Settings for FirstUser { - fn set(&mut self, attr: &str, value: SettingValue) -> Result<(), SettingsError> { - match attr { - "full_name" => { - self.full_name = value - .try_into() - .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? - } - "user_name" => { - self.user_name = value - .try_into() - .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? - } - "password" => { - self.full_name = value - .try_into() - .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? - } - "autologin" => { - self.full_name = value - .try_into() - .map_err(|e| SettingsError::UpdateFailed(attr.to_string(), e))? - } - _ => return Err(SettingsError::UnknownAttribute(attr.to_string())), - } - Ok(()) - } -} - /// D-Bus client for the users service #[derive(Clone)] pub struct UsersClient<'a> { diff --git a/rust/agama-lib/src/users/settings.rs b/rust/agama-lib/src/users/settings.rs index afc75ec317..c808e17d70 100644 --- a/rust/agama-lib/src/users/settings.rs +++ b/rust/agama-lib/src/users/settings.rs @@ -1,23 +1,20 @@ -use agama_settings::Settings; use serde::{Deserialize, Serialize}; /// User settings /// /// Holds the user settings for the installation. -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UserSettings { #[serde(rename = "user")] - #[settings(nested, alias = "user")] pub first_user: Option, - #[settings(nested)] pub root: Option, } /// First user settings /// /// Holds the settings for the first user. -#[derive(Clone, Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FirstUserSettings { /// First user's full name @@ -33,7 +30,7 @@ pub struct FirstUserSettings { /// Root user settings /// /// Holds the settings for the root user. -#[derive(Debug, Default, Settings, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RootUserSettings { /// Root's password (in clear text) @@ -42,41 +39,3 @@ pub struct RootUserSettings { /// Root SSH public key pub ssh_public_key: Option, } - -#[cfg(test)] -mod tests { - use super::*; - use agama_settings::settings::Settings; - - #[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") - } -} diff --git a/rust/agama-settings/Cargo.toml b/rust/agama-settings/Cargo.toml deleted file mode 100644 index 45ebf2bacc..0000000000 --- a/rust/agama-settings/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "agama-settings" -version = "1.0.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -agama-derive = { path="../agama-derive" } -thiserror = "1.0.43" diff --git a/rust/agama-settings/src/error.rs b/rust/agama-settings/src/error.rs deleted file mode 100644 index b796c5b467..0000000000 --- a/rust/agama-settings/src/error.rs +++ /dev/null @@ -1,27 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum SettingsError { - #[error("Unknown attribute '{0}'")] - UnknownAttribute(String), - #[error("Could not update '{0}': {1}")] - UpdateFailed(String, ConversionError), -} - -#[derive(Error, Debug)] -pub enum ConversionError { - #[error("Invalid value '{0}', expected a {1}")] - InvalidValue(String, String), - #[error("Missing key '{0}'")] - MissingKey(String), -} - -impl SettingsError { - /// Returns the an error with the updated attribute - pub fn with_attr(self, name: &str) -> Self { - match self { - Self::UnknownAttribute(_) => Self::UnknownAttribute(name.to_string()), - Self::UpdateFailed(_, source) => Self::UpdateFailed(name.to_string(), source), - } - } -} diff --git a/rust/agama-settings/src/lib.rs b/rust/agama-settings/src/lib.rs deleted file mode 100644 index 96a87bc940..0000000000 --- a/rust/agama-settings/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! This module offers a mechanism to easily map the values from the command -//! line to proper installation settings. -//! -//! In Agama, the installation settings are modeled using structs with optional fields and vectors. -//! To specify a value in the command line, the user needs to specify: -//! -//! * a setting ID (`"users.name"`, `"storage.lvm"`, and so on), that must be used to find the -//! setting. -//! * a value, which is captured as a string (`"Foo Bar"`, `"true"`, etc.) and it should be -//! converted to the proper type. -//! -//! Implementing the [Settings](crate::settings::Settings) trait adds support for setting the value -//! in an straightforward way, taking care of the conversions automatically. The newtype -//! [SettingValue] takes care of such a conversion. -//! -//! ## Example -//! -//! The best way to understand how it works is to see it in action. In the example below, there is -//! a simplified `InstallSettings` struct that is composed by the user settings, which is another -//! struct, and a boolean field. -//! -//! In this case, the trait is automatically derived, implementing a `set` method that allows -//! setting configuration value by specifying: -//! -//! * An ID, like `users.name`. -//! * A string-based value, which is automatically converted to the corresponding type in the -//! struct. -//! -//! ``` -//! use agama_settings::{Settings, settings::{SettingValue, Settings}}; -//! -//! #[derive(Default, Settings)] -//! struct UserSettings { -//! name: Option, -//! enabled: Option -//! } -//! -//! #[derive(Default, Settings)] -//! struct InstallSettings { -//! #[settings(nested)] -//! user: Option, -//! reboot: Option -//! } -//! -//! let user = UserSettings { name: Some(String::from("foo")), enabled: Some(false) }; -//! let mut settings = InstallSettings { user: Some(user), reboot: None }; -//! -//! settings.set("user.name", SettingValue("foo.bar".to_string())); -//! settings.set("user.enabled", SettingValue("true".to_string())); -//! settings.set("reboot", SettingValue("true".to_string())); -//! -//! let user = settings.user.unwrap(); -//! assert_eq!(user.name, Some("foo.bar".to_string())); -//! assert_eq!(user.enabled, Some(true)); -//! assert_eq!(settings.reboot, Some(true)); -//! ``` - -pub mod error; -pub mod settings; - -pub use self::error::SettingsError; -pub use self::settings::{SettingObject, SettingValue}; -pub use agama_derive::Settings; diff --git a/rust/agama-settings/src/settings.rs b/rust/agama-settings/src/settings.rs deleted file mode 100644 index 240a6cd854..0000000000 --- a/rust/agama-settings/src/settings.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::error::{ConversionError, SettingsError}; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::fmt::Display; - -/// Implements support for easily settings attributes values given an ID (`"users.name"`) and a -/// string value (`"Foo bar"`). -pub trait Settings { - /// Adds a new element to a collection. - /// - /// * `attr`: attribute name (e.g., `user.name`, `product`). - /// * `_value`: element to add to the collection. - fn add(&mut self, attr: &str, _value: SettingObject) -> Result<(), SettingsError> { - Err(SettingsError::UnknownAttribute(attr.to_string())) - } - - /// Sets an attribute's value. - /// - /// * `attr`: attribute name (e.g., `user.name`, `product`). - /// * `_value`: string-based value coming from the CLI. It will automatically - /// converted to the underlying type. - fn set(&mut self, attr: &str, _value: SettingValue) -> Result<(), SettingsError> { - Err(SettingsError::UnknownAttribute(attr.to_string())) - } - - /// Merges two settings structs. - /// - /// * `_other`: struct to copy the values from. - fn merge(&mut self, _other: &Self) - where - Self: Sized, - { - unimplemented!() - } -} - -/// Represents a string-based value and allows converting them to other types -/// -/// Supporting more conversions is a matter of implementing the [std::convert::TryFrom] trait for -/// more types. -/// -/// ``` -/// # use agama_settings::settings::SettingValue; -// -/// let value = SettingValue("true".to_string()); -/// let value: bool = value.try_into().expect("the conversion failed"); -/// assert_eq!(value, true); -/// ``` -#[derive(Clone, Debug)] -pub struct SettingValue(pub String); - -impl Display for SettingValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Represents a string-based collection and allows converting to other types -/// -/// It wraps a hash which uses String as key and SettingValue as value. -#[derive(Debug)] -pub struct SettingObject(pub HashMap); - -impl SettingObject { - /// Returns the value for the given key. - /// - /// * `key`: setting key. - pub fn get(&self, key: &str) -> Option<&SettingValue> { - self.0.get(key) - } -} - -impl From> for SettingObject { - fn from(value: HashMap) -> SettingObject { - let mut hash: HashMap = HashMap::new(); - for (k, v) in value { - hash.insert(k, SettingValue(v)); - } - SettingObject(hash) - } -} - -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; - - fn try_from(value: SettingValue) -> Result { - match value.0.to_lowercase().as_str() { - "true" | "yes" | "t" => Ok(true), - "false" | "no" | "f" => Ok(false), - _ => Err(ConversionError::InvalidValue( - value.to_string(), - "boolean".to_string(), - )), - } - } -} - -impl TryFrom for Option { - type Error = ConversionError; - - fn try_from(value: SettingValue) -> Result { - Ok(Some(value.try_into()?)) - } -} - -impl TryFrom for String { - type Error = ConversionError; - - fn try_from(value: SettingValue) -> Result { - Ok(value.0) - } -} - -impl TryFrom for Option { - type Error = ConversionError; - - fn try_from(value: SettingValue) -> Result { - Ok(Some(value.try_into()?)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_try_from_bool() { - let value = SettingValue("true".to_string()); - let value: bool = value.try_into().unwrap(); - assert!(value); - - let value = SettingValue("false".to_string()); - let value: bool = value.try_into().unwrap(); - assert!(!value); - - let value = SettingValue("fasle".to_string()); - let value: Result = value.try_into(); - let error = value.unwrap_err(); - assert_eq!( - error.to_string(), - "Invalid value 'fasle', expected a boolean" - ); - } - - #[test] - fn test_try_from_string() { - let value = SettingValue("some value".to_string()); - let value: String = value.try_into().unwrap(); - assert_eq!(value, "some value"); - } -} diff --git a/rust/agama-settings/tests/settings.rs b/rust/agama-settings/tests/settings.rs deleted file mode 100644 index 9ed945444b..0000000000 --- a/rust/agama-settings/tests/settings.rs +++ /dev/null @@ -1,106 +0,0 @@ -use agama_settings::{ - error::ConversionError, settings::Settings, SettingObject, SettingValue, Settings, -}; -use std::collections::HashMap; - -/// Main settings -#[derive(Debug, Default, Settings)] -pub struct Main { - product: Option, - #[settings(collection)] - patterns: Vec, - #[settings(nested)] - network: Option, -} - -/// Software patterns -#[derive(Debug, Clone)] -pub struct Pattern { - id: String, -} - -#[derive(Default, Debug, Settings)] -pub struct Network { - enabled: Option, - ssid: Option, -} - -/// TODO: deriving this trait could be easy. -impl TryFrom for Pattern { - type Error = ConversionError; - - fn try_from(value: SettingObject) -> Result { - match value.get("id") { - Some(id) => Ok(Pattern { - id: id.clone().to_string(), - }), - _ => Err(ConversionError::MissingKey("id".to_string())), - } - } -} - -#[test] -fn test_set() { - let mut main = Main::default(); - main.set("product", SettingValue("Tumbleweed".to_string())) - .unwrap(); - - assert_eq!(main.product, Some("Tumbleweed".to_string())); - main.set("network.enabled", SettingValue("true".to_string())) - .unwrap(); - let network = main.network.unwrap(); - assert_eq!(network.enabled, Some(true)); -} - -#[test] -fn test_set_unknown_attribute() { - let mut main = Main::default(); - let error = main - .set("missing", SettingValue("".to_string())) - .unwrap_err(); - assert_eq!(error.to_string(), "Unknown attribute 'missing'"); -} - -#[test] -fn test_invalid_set() { - let mut main = Main::default(); - - let error = main - .set("network.enabled", SettingValue("fasle".to_string())) - .unwrap_err(); - assert_eq!( - error.to_string(), - "Could not update 'network.enabled': Invalid value 'fasle', expected a boolean" - ); -} - -#[test] -fn test_add() { - let mut main = Main::default(); - let pattern = HashMap::from([("id".to_string(), SettingValue("base".to_string()))]); - main.add("patterns", SettingObject(pattern)).unwrap(); - - let pattern = main.patterns.first().unwrap(); - assert_eq!(pattern.id, "base"); -} - -#[test] -fn test_merge() { - let mut main0 = Main { - product: Some("Tumbleweed".to_string()), - ..Default::default() - }; - - let patterns = vec![Pattern { - id: "enhanced".to_string(), - }]; - let main1 = Main { - product: Some("ALP".to_string()), - patterns, - ..Default::default() - }; - - main0.merge(&main1); - assert_eq!(main0.product, Some("ALP".to_string())); - assert_eq!(main0.patterns.len(), 1); -} From 3080ab9062290027a134c63dbfdd9779b921e5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 4 Jun 2024 10:40:25 +0100 Subject: [PATCH 2/9] chore(cli): drop the convert_case dependency --- rust/Cargo.lock | 1 - rust/agama-cli/Cargo.toml | 1 - rust/agama-cli/src/config.rs | 1 - 3 files changed, 3 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e8d44152f2..4732b75113 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -26,7 +26,6 @@ dependencies = [ "async-trait", "clap", "console", - "convert_case", "curl", "fs_extra", "indicatif", diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index 7ad277b7c6..71d3629795 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -14,7 +14,6 @@ serde_json = "1.0.91" serde_yaml = "0.9.17" indicatif= "0.17.3" thiserror = "1.0.39" -convert_case = "0.6.0" console = "0.15.7" anyhow = "1.0.71" log = "0.4" diff --git a/rust/agama-cli/src/config.rs b/rust/agama-cli/src/config.rs index 83cdfb1832..4d61145c28 100644 --- a/rust/agama-cli/src/config.rs +++ b/rust/agama-cli/src/config.rs @@ -9,7 +9,6 @@ use agama_lib::{ Store as SettingsStore, }; use clap::Subcommand; -use convert_case::{Case, Casing}; use std::{collections::HashMap, error::Error, io, str::FromStr}; #[derive(Subcommand, Debug)] From 3e51ee68cee659793be02fd2b7bdb78de0afeddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 11 Jun 2024 22:04:58 +0100 Subject: [PATCH 3/9] feat(cli): "agama load" reads from stdin --- rust/agama-cli/src/config.rs | 77 ++++++------------------------------ 1 file changed, 11 insertions(+), 66 deletions(-) diff --git a/rust/agama-cli/src/config.rs b/rust/agama-cli/src/config.rs index 4d61145c28..91531f32fc 100644 --- a/rust/agama-cli/src/config.rs +++ b/rust/agama-cli/src/config.rs @@ -1,15 +1,13 @@ +use std::io::{self, Read}; + use crate::{ error::CliError, printers::{print, Format}, }; use agama_lib::{ - auth::AuthToken, - connection, - install_settings::{InstallSettings, Scope}, - Store as SettingsStore, + auth::AuthToken, connection, install_settings::InstallSettings, Store as SettingsStore, }; use clap::Subcommand; -use std::{collections::HashMap, error::Error, io, str::FromStr}; #[derive(Subcommand, Debug)] pub enum ConfigCommands { @@ -22,15 +20,12 @@ pub enum ConfigCommands { Show, /// Loads the configuration from a JSON file. - Load { - /// Local path to file with configuration. For schema see /usr/share/agama-cli/profile.json.schema - path: String, - }, + Load, } pub enum ConfigAction { Show, - Load(String), + Load, } pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<()> { @@ -46,16 +41,14 @@ pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<( match command { ConfigAction::Show => { let model = store.load(None).await?; - print(model, io::stdout(), format)?; + print(model, std::io::stdout(), format)?; Ok(()) } - ConfigAction::Load(path) => { - let contents = std::fs::read_to_string(path)?; + ConfigAction::Load => { + let mut stdin = io::stdin(); + let mut contents = String::new(); + stdin.read_to_string(&mut contents)?; let result: InstallSettings = serde_json::from_str(&contents)?; - let scopes = result.defined_scopes(); - // FIXME: merging should be implemented - // let mut model = store.load(Some(scopes)).await?; - // model.merge(&result); Ok(store.store(&result).await?) } } @@ -64,54 +57,6 @@ pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<( fn parse_config_command(subcommand: ConfigCommands) -> Result { match subcommand { ConfigCommands::Show => Ok(ConfigAction::Show), - ConfigCommands::Load { path } => Ok(ConfigAction::Load(path)), - } -} - -/// Split the elements on '=' to make a hash of them. -fn parse_keys_values(keys_values: Vec) -> Result, CliError> { - let mut changes = HashMap::new(); - for s in keys_values { - let Some((key, value)) = s.split_once('=') else { - return Err(CliError::MissingSeparator(s)); - }; - changes.insert(key.to_string(), value.to_string()); - } - Ok(changes) -} - -#[test] -fn test_parse_keys_values() { - // happy path, make a hash out of the vec - let happy_in = vec!["one=first".to_string(), "two=second".to_string()]; - let happy_out = HashMap::from([ - ("one".to_string(), "first".to_string()), - ("two".to_string(), "second".to_string()), - ]); - let r = parse_keys_values(happy_in); - assert!(r.is_ok()); - assert_eq!(r.unwrap(), happy_out); - - // an empty list is fine - let empty_vec = Vec::::new(); - let empty_hash = HashMap::::new(); - let r = parse_keys_values(empty_vec); - assert!(r.is_ok()); - assert_eq!(r.unwrap(), empty_hash); - - // an empty member fails - let empty_string = vec!["".to_string(), "two=second".to_string()]; - let r = parse_keys_values(empty_string); - assert!(r.is_err()); - assert_eq!( - format!("{}", r.unwrap_err()), - "Missing the '=' separator in ''" - ); -} - -fn key_to_scope(key: &str) -> Result> { - if let Some((name, _)) = key.split_once('.') { - return Ok(Scope::from_str(name)?); + ConfigCommands::Load => Ok(ConfigAction::Load), } - Err(Box::new(CliError::InvalidKeyName(key.to_string()))) } From 1ce99f57dabe89332366c19efb8d896ee69744f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 11 Jun 2024 22:05:18 +0100 Subject: [PATCH 4/9] chore(cli): remove unused errors --- rust/agama-cli/src/error.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rust/agama-cli/src/error.rs b/rust/agama-cli/src/error.rs index 235d5f7c92..2aa5985981 100644 --- a/rust/agama-cli/src/error.rs +++ b/rust/agama-cli/src/error.rs @@ -2,14 +2,10 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum CliError { - #[error("Invalid key name: '{0}'")] - InvalidKeyName(String), #[error("Cannot perform the installation as the settings are not valid")] ValidationError, #[error("Could not start the installation")] InstallationError, - #[error("Missing the '=' separator in '{0}'")] - MissingSeparator(String), #[error("Could not read the password: {0}")] MissingPassword(#[from] std::io::Error), } From 111302489bd74fe208fdf1bdf9c9c7c99802b29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 11 Jun 2024 22:06:17 +0100 Subject: [PATCH 5/9] refactor(cli): adapt internals of profile command to latest changes --- rust/agama-cli/src/profile.rs | 52 +++++++++++++------------- rust/agama-lib/src/install_settings.rs | 10 +++++ rust/agama-lib/src/network/settings.rs | 1 - 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/rust/agama-cli/src/profile.rs b/rust/agama-cli/src/profile.rs index c60101e949..c264e777d4 100644 --- a/rust/agama-cli/src/profile.rs +++ b/rust/agama-cli/src/profile.rs @@ -1,4 +1,10 @@ -use agama_lib::profile::{AutoyastProfile, ProfileEvaluator, ProfileValidator, ValidationResult}; +use agama_lib::{ + auth::AuthToken, + connection, + install_settings::InstallSettings, + profile::{AutoyastProfile, ProfileEvaluator, ProfileValidator, ValidationResult}, + Store as SettingsStore, +}; use anyhow::Context; use clap::Subcommand; use curl::easy::Easy; @@ -25,8 +31,8 @@ pub enum ProfileCommands { /// /// Schema is available at /usr/share/agama-cli/profile.schema.json Validate { - /// Local path to json file - path: String, + /// Local path to the JSON file to validate + path: PathBuf, }, /// Evaluate a profile, injecting the hardware information from D-Bus @@ -35,7 +41,7 @@ pub enum ProfileCommands { /// https://github.com/openSUSE/agama/blob/master/rust/agama-lib/share/examples/profile.jsonnet Evaluate { /// Path to jsonnet file. - path: String, + path: PathBuf, }, /// Process autoinstallation profile and loads it into agama @@ -66,9 +72,9 @@ pub fn download(url: &str, mut out_fd: impl Write) -> anyhow::Result<()> { Ok(()) } -fn validate(path: String) -> anyhow::Result<()> { +fn validate(path: &PathBuf) -> anyhow::Result<()> { let validator = ProfileValidator::default_schema()?; - let path = Path::new(&path); + // let path = Path::new(&path); let result = validator .validate_file(path) .context(format!("Could not validate the profile {:?}", path))?; @@ -86,10 +92,10 @@ fn validate(path: String) -> anyhow::Result<()> { Ok(()) } -fn evaluate(path: String) -> anyhow::Result<()> { +fn evaluate(path: &PathBuf) -> anyhow::Result<()> { let evaluator = ProfileEvaluator {}; evaluator - .evaluate(Path::new(&path), stdout()) + .evaluate(&path, stdout()) .context("Could not evaluate the profile".to_string())?; Ok(()) } @@ -134,22 +140,18 @@ async fn import(url_string: String, dir: Option) -> anyhow::Result<()> output_path = output_dir.join("profile.json"); } - let output_path_string = output_path - .to_str() - .context("Failed to get output path")? - .to_string(); - // Validate json profile - // TODO: optional skip of validation - validate(output_path_string.clone())?; - // load resulting json config - crate::config::run( - crate::config::ConfigCommands::Load { - path: output_path_string, - }, - crate::printers::Format::Json, - ) - .await?; + validate(&output_path)?; + store_settings(&output_path).await?; + + Ok(()) +} +async fn store_settings>(path: P) -> anyhow::Result<()> { + let token = AuthToken::find().context("You are not logged in")?; + let client = agama_lib::http_client(token.as_str())?; + let store = SettingsStore::new(connection().await?, client).await?; + let settings = InstallSettings::from_file(&path)?; + store.store(&settings).await?; Ok(()) } @@ -163,8 +165,8 @@ fn autoyast(url_string: String) -> anyhow::Result<()> { pub async fn run(subcommand: ProfileCommands) -> anyhow::Result<()> { match subcommand { ProfileCommands::Autoyast { url } => autoyast(url), - ProfileCommands::Validate { path } => validate(path), - ProfileCommands::Evaluate { path } => evaluate(path), + ProfileCommands::Validate { path } => validate(&path), + ProfileCommands::Evaluate { path } => evaluate(&path), ProfileCommands::Import { url, dir } => import(url, dir).await, } } diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index f1211878f9..3d21596c95 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -8,6 +8,9 @@ use crate::{ use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use std::default::Default; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; use std::str::FromStr; /// Settings scopes @@ -92,6 +95,13 @@ pub struct InstallSettings { } impl InstallSettings { + pub fn from_file>(path: P) -> Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let data = serde_json::from_reader(reader)?; + Ok(data) + } + pub fn defined_scopes(&self) -> Vec { let mut scopes = vec![]; if self.user.is_some() { diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index bb11184bc8..256ed0d055 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -3,7 +3,6 @@ use super::types::{DeviceState, DeviceType, Status}; use cidr::IpInet; use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; use std::default::Default; use std::net::IpAddr; From 4a65dde7f7e4dba2cacb84762467e31564480e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 11 Jun 2024 22:10:42 +0100 Subject: [PATCH 6/9] refactor(rust): drop the concept of Scope --- rust/agama-cli/src/config.rs | 2 +- rust/agama-lib/src/install_settings.rs | 85 -------------------------- rust/agama-lib/src/store.rs | 40 +++--------- 3 files changed, 10 insertions(+), 117 deletions(-) diff --git a/rust/agama-cli/src/config.rs b/rust/agama-cli/src/config.rs index 91531f32fc..d5bb8bb8e0 100644 --- a/rust/agama-cli/src/config.rs +++ b/rust/agama-cli/src/config.rs @@ -40,7 +40,7 @@ pub async fn run(subcommand: ConfigCommands, format: Format) -> anyhow::Result<( let command = parse_config_command(subcommand)?; match command { ConfigAction::Show => { - let model = store.load(None).await?; + let model = store.load().await?; print(model, std::io::stdout(), format)?; Ok(()) } diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index 3d21596c95..d0b4f03477 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -11,65 +11,6 @@ use std::default::Default; use std::fs::File; use std::io::BufReader; use std::path::Path; -use std::str::FromStr; - -/// Settings scopes -/// -/// They are used to limit the reading/writing of settings. For instance, if the Scope::Users is -/// given, only the data related to users (UsersStore) are read/written. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Scope { - /// User settings - Users, - /// Software settings - Software, - /// Storage settings - Storage, - /// Storage AutoYaST settings (for backward compatibility with AutoYaST profiles) - StorageAutoyast, - /// Network settings - Network, - /// Product settings - Product, - /// Localization settings - Localization, -} - -impl Scope { - /// Returns known scopes - /// - // TODO: we can rely on strum so we do not forget to add them - pub fn all() -> [Scope; 7] { - [ - Scope::Localization, - Scope::Network, - Scope::Product, - Scope::Software, - Scope::Storage, - Scope::StorageAutoyast, - Scope::Users, - ] - } -} - -impl FromStr for Scope { - type Err = &'static str; - - // Do not generate the StorageAutoyast scope. Note that storage AutoYaST settings will only be - // temporary available for importing an AutoYaST profile. But CLI should not allow modifying the - // storate AutoYaST settings. - fn from_str(s: &str) -> Result { - match s { - "users" => Ok(Self::Users), - "software" => Ok(Self::Software), - "storage" => Ok(Self::Storage), - "network" => Ok(Self::Network), - "product" => Ok(Self::Product), - "localization" => Ok(Self::Localization), - _ => Err("Unknown section"), - } - } -} /// Installation settings /// @@ -101,30 +42,4 @@ impl InstallSettings { let data = serde_json::from_reader(reader)?; Ok(data) } - - pub fn defined_scopes(&self) -> Vec { - let mut scopes = vec![]; - if self.user.is_some() { - scopes.push(Scope::Users); - } - if self.storage.is_some() { - scopes.push(Scope::Storage); - } - if self.storage_autoyast.is_some() { - scopes.push(Scope::StorageAutoyast); - } - if self.software.is_some() { - scopes.push(Scope::Software); - } - if self.network.is_some() { - scopes.push(Scope::Network); - } - if self.product.is_some() { - scopes.push(Scope::Product); - } - if self.localization.is_some() { - scopes.push(Scope::Localization); - } - scopes - } } diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index 89ac4b2f44..b17b848fdf 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -2,7 +2,7 @@ // TODO: quickly explain difference between FooSettings and FooStore, with an example use crate::error::ServiceError; -use crate::install_settings::{InstallSettings, Scope}; +use crate::install_settings::InstallSettings; use crate::{ localization::LocalizationStore, network::NetworkStore, product::ProductStore, software::SoftwareStore, storage::StorageAutoyastStore, storage::StorageStore, @@ -42,41 +42,19 @@ impl<'a> Store<'a> { }) } - /// Loads the installation settings from the D-Bus service. + /// Loads the installation settings from the HTTP interface. /// /// NOTE: The storage AutoYaST settings cannot be loaded because they cannot be modified. The /// ability of using the storage AutoYaST settings from a JSON config file is temporary and it /// will be removed in the future. - pub async fn load(&self, only: Option>) -> Result { - let scopes = match only { - Some(scopes) => scopes, - None => Scope::all().to_vec(), - }; - + pub async fn load(&self) -> Result { let mut settings: InstallSettings = Default::default(); - if scopes.contains(&Scope::Network) { - settings.network = Some(self.network.load().await?); - } - - if scopes.contains(&Scope::Storage) { - settings.storage = Some(self.storage.load().await?); - } - - if scopes.contains(&Scope::Software) { - settings.software = Some(self.software.load().await?); - } - - if scopes.contains(&Scope::Users) { - settings.user = Some(self.users.load().await?); - } - - if scopes.contains(&Scope::Product) { - settings.product = Some(self.product.load().await?); - } - - if scopes.contains(&Scope::Localization) { - settings.localization = Some(self.localization.load().await?); - } + settings.network = Some(self.network.load().await?); + settings.storage = Some(self.storage.load().await?); + settings.software = Some(self.software.load().await?); + settings.user = Some(self.users.load().await?); + settings.product = Some(self.product.load().await?); + settings.localization = Some(self.localization.load().await?); // TODO: use try_join here Ok(settings) From 05ef15e3e8da0aef535a67f117c54dfb1d8bed61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 5 Jun 2024 12:16:25 +0100 Subject: [PATCH 7/9] doc(cli): remove references to "config add/set" --- autoinstallation/README.md | 43 ++++---- doc/cli_guidelines.md | 202 ------------------------------------- 2 files changed, 17 insertions(+), 228 deletions(-) delete mode 100644 doc/cli_guidelines.md diff --git a/autoinstallation/README.md b/autoinstallation/README.md index 30b9216eb4..26f40363a4 100644 --- a/autoinstallation/README.md +++ b/autoinstallation/README.md @@ -7,10 +7,9 @@ provide a file, known as a "profile", that includes a description of the system approach might sound familiar to AutoYaST users. On the other hand, Agama can accept just a plain shell script, enabling custom pre-installation workflows. -If you are interested in using your AutoYaST profiles, Agama is not there yet. However, there are -plans to partially support them. - -By now, let's have a closer look at Agama's approaches. +Although Agama defines its own [profile format](../rust/agama-lib/share/profile.schema.json), it is +able to partially handle AutoYaST profiles. Please, check the [AutoYaST support document](../doc/ +autoyast.md) for further information. ## Profile-based installation @@ -65,23 +64,24 @@ Please, check [the example profile](../rust/agama-lib/share/examples/profile.jso information. > [!NOTE] -> You can inspect the available data by installing the `lshw` package and running the -> following command: `lshw -json`. +> You can inspect the available data by installing the `lshw` package and running the following +> command: `lshw -json`. ### Validating and evaluating a profile -Agama includes a handy command-line interface available in the `agama-cli` package. Among many other -things, it allows for downloading, validating and evaluating profiles. For instance, we could check -the result of the previous profile by running the following command: +Agama includes a handy command-line interface available in the `agama` package. Among many other +things, it allows downloading, validating and evaluating profiles. For instance, we could check the +result of the previous profile by running the following command: ``` $ sudo agama profile evaluate my-profile.jsonnet ``` -> [!WARNING] You need to use `sudo` to access the hardware information. +> [!WARNING] + You need to use `sudo` to access the hardware information. -Do you want to check whether your profile is valid? `agama-cli` have you covered. Bear in mind that -you can only validate JSON profiles (a Jsonnet profile must be evaluated first). +Do you want to check whether your profile is valid? `agama` have you covered. Bear in mind that you +can only validate JSON profiles (a Jsonnet profile must be evaluated first). ``` $ agama profile validate my-profile.json @@ -109,8 +109,7 @@ Below there is a minimal working example to install Tumbleweed: ```sh set -ex -/usr/bin/agama config set product.id=Tumbleweed -/usr/bin/agama config set user.userName=joe user.password=doe +/usr/bin/agama profile import ftp://my.server/profile.json /usr/bin/agama install ``` @@ -135,8 +134,7 @@ set -ex /usr/bin/agama download ftp://my.server/tricky_hardware_setup.sh > tricky_hardware_setup.sh sh tricky_hardware_setup.sh -/usr/bin/agama config set product.id=Tumbleweed -/usr/bin/agama config set user.userName=joe user.password=doe +/usr/bin/agama profile import ftp://my.server/profile.json /usr/bin/agama install ``` @@ -167,9 +165,7 @@ Agama and before installing RPMs, such as changing the fstab and mount an extra ```sh set -ex -/usr/bin/agama config set product.id=Tumbleweed -/usr/bin/agama config set user.userName=joe user.password=doe - +/usr/bin/agama profile import http://my.server/profile.json /usr/bin/agama install --until partitioning # install till the partitioning step # Place for specific changes to /dev @@ -190,10 +186,7 @@ software for internal network, then it must be modified before umount. set -ex /usr/bin/agama download ftp://my.server/velociraptor.config - -/usr/bin/agama config set product.id=Tumbleweed -/usr/bin/agama config set user.userName=joe user.password=doe - +/usr/bin/agama profile import http://my.server/profile.json /usr/bin/agama install --until deploy # do partitioning, rpm installation and configuration step # Example of enabling velociraptor @@ -216,9 +209,7 @@ some kernel tuning or adding some remote storage that needs to be mounted during ```sh set -ex -/usr/bin/agama config set product.id=Tumbleweed -/usr/bin/agama config set user.userName=joe user.password=doe - +/usr/bin/agama profile import http://my.server/profile.json /usr/bin/agama install --until deploy # do partitioning, rpm installation and configuration step # Do custom modification of /mnt including call to dracut diff --git a/doc/cli_guidelines.md b/doc/cli_guidelines.md deleted file mode 100644 index 4e244455c0..0000000000 --- a/doc/cli_guidelines.md +++ /dev/null @@ -1,202 +0,0 @@ -# Agama CLI Guidelines - -This document defines the syntax for the Agama CLI. For more CLI specific aspects like return values, flags, etc, please refer to [clig.dev](https://clig.dev/). Guidelines from clig.dev are agnostic about programming languages and tooling in general, and it can be perfectly used as reference for the Agama CLI. - -## CLI Syntax - -Note: The syntax presented here was previously discussed in this [document](https://gist.github.com/joseivanlopez/808c2be0cf668b4b457fc5d9ec20dc73). - -The installation settings are represented by a YAML structure, and the CLI is defined as a set of generic sub-commands and verbs that allow to edit any YAML value in a standard way. - -The CLI offers a `config` sub-command for editing the YAML config. The `config` sub-command has verbs for the following actions: - -* To load a YAML config file with the values for the installation. -* To edit any value of the config without loading a new complete file again. -* To show the current config for the installation. -* To check the current config. - -Moreover, the CLI also offers sub-commands for these actions: - -* To ask for the possible values that can be used for some setting (e.g., list of available products). -* To start and abort the installation. -* To see the installation status. -* To answers questions. - -### Sub-commands and Verbs - -In the following commands `` represents a YAML key from the config structure and `` is the value associated to the given key. Nested keys are dot-separated (e.g., `user.name`). - -This is the list of all sub-commands and verbs: - -~~~ -$ agama install -Starts the installation. - -$ agama abort -Aborts the installation. - -$ agama status -Prints the current status of the installation process and informs about pending actions (e.g., if there are questions waiting to be answered, if a product is not selected yet, etc). - -$ agama watch -Prints messages from the installation process (e.g., progress, questions, etc). - -$ agama config load -Loads installation config from a YAML file, keeping the rest of the config as it is. - -$ agama config show [] -Prints the current installation config in YAML format. If a is given, then it only prints the content for the given key. - -$ agama config set = ... -Sets a config value for the given key. - -$ agama config unset -Removes the current value for the given key. - -$ agama config reset [] -Sets the default value for the given . If no key is given, then the whole config is reset. - -$ agama config add [=] ... -Adds a new entry with all the given key-value pairs to a config list. The `=` part is omitted for a list of scalar values (e.g., languages). - -$ agama config delete [=] ... -Deletes any entry matching all the given key-value pairs from a config list. The `=` part is omitted for a list of scalar values. - -$ agama config check -Validates the config and prints errors - -$ agama config info [] -Prints info about the given key. If no value is given, then it prints what values are admitted by the given key. If a value is given, then it shows extra info about such a value. - -$ agama summary [
] -Prints a summary with the actions to perform in the system. If a section is given (e.g., storage, software, ...), then it only shows the section summary. - -$ agama questions -Prints questions and allows to answer them. - -~~~ - -### YAML Config - -The config settings are defined by this YAML structure: - -~~~ ---- -product: "Tumbleweed" - -languages: - - "es_ES" - - "en_US" - -user: - name: "test" - fullname: "User Test" - password: "12345" - autologin: true - -root: - ssh_key: "1234abcd" - password: "12345" - -storage: - candidate_devices: - - /dev/sda - lvm: true - encryption_password: 12345 - volumes: - - mountpoint: / - fstype: btrfs - - mountpoint: /home - fstype: ext4 - minsize: 10GiB -~~~ - -### Examples - -Let's see some examples: - -~~~ -# Set a product -$ agama config set product=Tumbleweed - -# Set user values -$ agama config set user.name=linux -$ agama config set user.fullname=linux -$ agama config set user.password=linux -$ agama config set user.name=linux user.fullname=linux user.password=12345 - -# Unset user -$ agama config unset user - -# Add and delete languages -$ agama config add languages en_US -$ agama config delete languages en_US - -# Set storage settings -$ agama config set storage.lvm=false -$ agama config set storage.encryption_password=12345 - -# Add and delete candidate devices -$ agama config add storage.candidate_devices /dev/sda -$ agama config delete storage.candidate_devices /dev/sdb - -# Add and delete storage volumes -$ agama config add storage.volumes mountpoint=/ minsize=10GiB -$ agama config delete storage.volumes mountpoint=/home - -# Reset storage config -$ agama config reset storage - -# Show some config values -$ agama config show storage.candidate_devices -$ agama config show user - -# Dump config into a file -$ agama config show > ~/config.yaml - -# Show info of a key -$ agama config info storage.candidate_devices -$ agama config info languages - -# Show info of a specific value -$ agama config info storage.candidate_devices /dev/sda - -# Show the storage actions to perform in the system -$ agama summary storage -~~~ - -## Product Selection - -Agama can automatically infers all the config values, but at least one product must be selected. Selecting a product implies some actions in the D-Bus services (e.g., storage devices are probed). And the D-Bus services might emit some questions if needed (e.g., asking to provide a LUKS password). Because of that, the command for selecting a product could ask questions to the user: - -~~~ -$ agama config set product=ALP -> The device /dev/sda is encrypted. Provide an encryption password if you want to open it (enter to skip): -~~~ - -If a product is not selected yet, then many commands cannot work. In that case, commands should inform about it: - -~~~ -$ agama config show -A product is not selected yet. Please, select a product first: agama config set product=. -~~~ - -## D-Bus Questions - -Sometimes answering pending questions is required before performing the requested command. For example, for single product live images the storage proposal is automatically done (the target product is already known). If some questions were emitted during the process, then they have to be answered before continuing using the CLI. Commands would show a warning to inform about the situation and how to proceed: - -~~~ -$ agama config show -There are pending questions. Please, answer questions first: agama questions. -~~~ - -## Non Interactive Mode - -There is a `questions` subcommand that support `mode` and `answers` subcommands. -The `mode` subcommand allows to set `interactive` and `non-interactive` modes. -The `answers` allows to pass a file with predefined answers. - -~~~ -agama questions mode non-interactive -agama questions answers /tmp/answers.json -~~~ From 720d5fbfd8099aea2658a5bd3cf0b91b7326e02b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 11 Jun 2024 22:33:17 +0100 Subject: [PATCH 8/9] feat(live): add the jq package to the image --- live/src/agama-live.kiwi | 1 + 1 file changed, 1 insertion(+) diff --git a/live/src/agama-live.kiwi b/live/src/agama-live.kiwi index 1f7dbfb51e..841330f111 100644 --- a/live/src/agama-live.kiwi +++ b/live/src/agama-live.kiwi @@ -109,6 +109,7 @@ + From 08516696d4b2be8daa3e4d1131cb75d71f786f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 5 Jun 2024 10:04:13 +0100 Subject: [PATCH 9/9] chore(cli): updates changes file --- live/src/agama-live.changes | 5 +++++ rust/package/agama.changes | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/live/src/agama-live.changes b/live/src/agama-live.changes index 639a67c273..2ea141ea76 100644 --- a/live/src/agama-live.changes +++ b/live/src/agama-live.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Tue Jun 11 21:39:51 UTC 2024 - Imobach Gonzalez Sosa + +- Add the jq package to the image (gh#openSUSE/agama#1314). + ------------------------------------------------------------------- Thu Jun 6 14:30:19 UTC 2024 - Ladislav Slezák diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 5717dd3367..c2cf33eb53 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Tue Jun 11 21:35:00 UTC 2024 - Imobach Gonzalez Sosa + +- CLI: use the master token /run/agama/token if available and + readable (gh#openSUSE/agama#1287). +- CLI: remove the "config add/set" subcommands + (gh#openSUSE/agama#1314). + ------------------------------------------------------------------- Mon Jun 10 14:24:33 UTC 2024 - Jorik Cronenberg