From 39bf93d992eea1533aeb08fc634c3dc88cbeb662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 11:14:51 +0100 Subject: [PATCH 1/5] chore(rust): update dependencies --- rust/Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 4732b75113..e16907f6d1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3042,18 +3042,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -3062,9 +3062,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", From 82d9fe2cbcf2e3c919545e600e0666d1e0537fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 11:16:01 +0100 Subject: [PATCH 2/5] feat(rust): set and get storage config --- rust/agama-lib/src/install_settings.rs | 6 +- rust/agama-lib/src/storage.rs | 2 - rust/agama-lib/src/storage/autoyast.rs | 2 - rust/agama-lib/src/storage/autoyast/store.rs | 26 ------- rust/agama-lib/src/storage/client.rs | 75 ++++---------------- rust/agama-lib/src/storage/proxies.rs | 9 ++- rust/agama-lib/src/storage/settings.rs | 23 ++++-- rust/agama-lib/src/storage/store.rs | 21 ++---- rust/agama-lib/src/store.rs | 26 +++---- rust/agama-server/src/storage/web.rs | 2 +- rust/package/agama.changes | 5 ++ 11 files changed, 61 insertions(+), 136 deletions(-) delete mode 100644 rust/agama-lib/src/storage/autoyast.rs delete mode 100644 rust/agama-lib/src/storage/autoyast/store.rs diff --git a/rust/agama-lib/src/install_settings.rs b/rust/agama-lib/src/install_settings.rs index d0b4f03477..6cb6b16caa 100644 --- a/rust/agama-lib/src/install_settings.rs +++ b/rust/agama-lib/src/install_settings.rs @@ -3,7 +3,7 @@ //! This module implements the mechanisms to load and store the installation settings. use crate::{ localization::LocalizationSettings, network::NetworkSettings, product::ProductSettings, - software::SoftwareSettings, storage::StorageSettings, users::UserSettings, + software::SoftwareSettings, users::UserSettings, }; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; @@ -26,8 +26,10 @@ pub struct InstallSettings { #[serde(default)] pub product: Option, #[serde(default)] - pub storage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub storage: Option>, #[serde(default, rename = "legacyAutoyastStorage")] + #[serde(skip_serializing_if = "Option::is_none")] pub storage_autoyast: Option>, #[serde(default)] pub network: Option, diff --git a/rust/agama-lib/src/storage.rs b/rust/agama-lib/src/storage.rs index 2df50cbae1..66940a3599 100644 --- a/rust/agama-lib/src/storage.rs +++ b/rust/agama-lib/src/storage.rs @@ -1,13 +1,11 @@ //! Implements support for handling the storage settings -mod autoyast; pub mod client; pub mod model; pub mod proxies; mod settings; mod store; -pub use autoyast::store::StorageAutoyastStore; pub use client::{ iscsi::{ISCSIAuth, ISCSIClient, ISCSIInitiator, ISCSINode}, StorageClient, diff --git a/rust/agama-lib/src/storage/autoyast.rs b/rust/agama-lib/src/storage/autoyast.rs deleted file mode 100644 index 4aefc745c8..0000000000 --- a/rust/agama-lib/src/storage/autoyast.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Implements support for handling the storage AutoYaST settings -pub mod store; diff --git a/rust/agama-lib/src/storage/autoyast/store.rs b/rust/agama-lib/src/storage/autoyast/store.rs deleted file mode 100644 index 576f744648..0000000000 --- a/rust/agama-lib/src/storage/autoyast/store.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Implements the store for the storage AutoYaST settings. - -use crate::error::ServiceError; -use crate::storage::StorageClient; -use zbus::Connection; - -/// Stores the storage AutoYaST settings to the D-Bus service. -/// -/// NOTE: The AutoYaST settings are not loaded from D-Bus because they cannot be modified. The only -/// way of using the storage AutoYaST settings is by loading a JSON config file. -pub struct StorageAutoyastStore<'a> { - storage_client: StorageClient<'a>, -} - -impl<'a> StorageAutoyastStore<'a> { - pub async fn new(connection: Connection) -> Result, ServiceError> { - Ok(Self { - storage_client: StorageClient::new(connection).await?, - }) - } - - pub async fn store(&self, settings: &str) -> Result<(), ServiceError> { - self.storage_client.calculate_autoyast(settings).await?; - Ok(()) - } -} diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index 00ee681442..6f5a8ef158 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -2,8 +2,8 @@ use super::model::{ Action, BlockDevice, Component, Device, DeviceInfo, DeviceSid, Drive, Filesystem, LvmLv, LvmVg, - Md, Multipath, Partition, PartitionTable, ProposalSettings, ProposalSettingsPatch, - ProposalTarget, Raid, Volume, + Md, Multipath, Partition, PartitionTable, ProposalSettings, ProposalSettingsPatch, Raid, + Volume, }; use super::proxies::{ProposalCalculatorProxy, ProposalProxy, Storage1Proxy}; use super::StorageSettings; @@ -105,73 +105,28 @@ impl<'a> StorageClient<'a> { Ok(self.proposal_proxy.settings().await?.try_into()?) } - /// Returns the boot device proposal setting - /// DEPRECATED, use proposal_settings instead - pub async fn boot_device(&self) -> Result, ServiceError> { - let settings = self.proposal_settings().await?; - let boot_device = settings.boot_device; - - if boot_device.is_empty() { - Ok(None) - } else { - Ok(Some(boot_device)) - } - } - - /// Returns the lvm proposal setting - /// DEPRECATED, use proposal_settings instead - pub async fn lvm(&self) -> Result, ServiceError> { - let settings = self.proposal_settings().await?; - Ok(Some(!matches!(settings.target, ProposalTarget::Disk))) - } - - /// Returns the encryption password proposal setting - /// DEPRECATED, use proposal_settings instead - pub async fn encryption_password(&self) -> Result, ServiceError> { - let settings = self.proposal_settings().await?; - let value = settings.encryption_password; - - if value.is_empty() { - Ok(None) - } else { - Ok(Some(value)) - } - } - /// Runs the probing process pub async fn probe(&self) -> Result<(), ServiceError> { Ok(self.storage_proxy.probe().await?) } - /// TODO: remove calculate when CLI will be adapted - pub async fn calculate2(&self, settings: ProposalSettingsPatch) -> Result { - Ok(self.calculator_proxy.calculate(settings.into()).await?) + /// Set the storage config according to the JSON schema + pub async fn set_config(&self, settings: StorageSettings) -> Result { + Ok(self + .storage_proxy + .set_config(serde_json::to_string(&settings).unwrap().as_str()) + .await?) } - pub async fn calculate_autoyast(&self, settings: &str) -> Result { - Ok(self.calculator_proxy.calculate_autoyast(settings).await?) + /// Get the storage config according to the JSON schema + pub async fn get_config(&self) -> Result { + let serialized_settings = self.storage_proxy.get_config().await?; + let settings = serde_json::from_str(serialized_settings.as_str()).unwrap(); + Ok(settings) } - /// Calculates a new proposal with the given settings. - pub async fn calculate(&self, settings: &StorageSettings) -> Result { - let mut dbus_settings: HashMap<&str, zbus::zvariant::Value<'_>> = HashMap::new(); - - if let Some(boot_device) = settings.boot_device.clone() { - dbus_settings.insert("BootDevice", zbus::zvariant::Value::new(boot_device)); - } - - if let Some(encryption_password) = settings.encryption_password.clone() { - dbus_settings.insert( - "EncryptionPassword", - zbus::zvariant::Value::new(encryption_password), - ); - } - - if let Some(lvm) = settings.lvm { - dbus_settings.insert("LVM", zbus::zvariant::Value::new(lvm)); - } - - Ok(self.calculator_proxy.calculate(dbus_settings).await?) + pub async fn calculate(&self, settings: ProposalSettingsPatch) -> Result { + Ok(self.calculator_proxy.calculate(settings.into()).await?) } /// Probed devices. diff --git a/rust/agama-lib/src/storage/proxies.rs b/rust/agama-lib/src/storage/proxies.rs index c64f87d02a..ebb9396ac7 100644 --- a/rust/agama-lib/src/storage/proxies.rs +++ b/rust/agama-lib/src/storage/proxies.rs @@ -18,6 +18,12 @@ trait Storage1 { /// Probe method fn probe(&self) -> zbus::Result<()>; + /// Set the storage config according to the JSON schema + fn set_config(&self, settings: &str) -> zbus::Result; + + /// Get the current storage config according to the JSON schema + fn get_config(&self) -> zbus::Result; + /// DeprecatedSystem property #[dbus_proxy(property)] fn deprecated_system(&self) -> zbus::Result; @@ -35,9 +41,6 @@ trait ProposalCalculator { settings: std::collections::HashMap<&str, zbus::zvariant::Value<'_>>, ) -> zbus::Result; - /// Calculate AutoYaST proposal - fn calculate_autoyast(&self, settings: &str) -> zbus::Result; - /// DefaultVolume method fn default_volume( &self, diff --git a/rust/agama-lib/src/storage/settings.rs b/rust/agama-lib/src/storage/settings.rs index 601a2ce80f..2d6f235d07 100644 --- a/rust/agama-lib/src/storage/settings.rs +++ b/rust/agama-lib/src/storage/settings.rs @@ -1,15 +1,26 @@ //! Representation of the storage settings +use crate::install_settings::InstallSettings; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; /// Storage settings for installation #[derive(Debug, Default, 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, - /// Boot device to use in the installation - pub boot_device: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub storage: Option>, + #[serde(default, rename = "legacyAutoyastStorage")] + #[serde(skip_serializing_if = "Option::is_none")] + pub storage_autoyast: Option>, +} + +impl From<&InstallSettings> for StorageSettings { + fn from(install_settings: &InstallSettings) -> Self { + StorageSettings { + storage: install_settings.storage.clone(), + storage_autoyast: install_settings.storage_autoyast.clone(), + } + } } diff --git a/rust/agama-lib/src/storage/store.rs b/rust/agama-lib/src/storage/store.rs index 199a724fe9..82b25a5c8f 100644 --- a/rust/agama-lib/src/storage/store.rs +++ b/rust/agama-lib/src/storage/store.rs @@ -1,6 +1,7 @@ //! Implements the store for the storage settings. -use super::{StorageClient, StorageSettings}; +use super::StorageClient; +use super::StorageSettings; use crate::error::ServiceError; use zbus::Connection; @@ -17,23 +18,11 @@ impl<'a> StorageStore<'a> { } pub async fn load(&self) -> Result { - // If it is not possible to get the settings (e.g., there are no settings yet), return - // the default. - let Ok(boot_device) = self.storage_client.boot_device().await else { - return Ok(StorageSettings::default()); - }; - let lvm = self.storage_client.lvm().await?; - let encryption_password = self.storage_client.encryption_password().await?; - - Ok(StorageSettings { - boot_device, - lvm, - encryption_password, - }) + Ok(self.storage_client.get_config().await?) } - pub async fn store(&self, settings: &StorageSettings) -> Result<(), ServiceError> { - self.storage_client.calculate(settings).await?; + pub async fn store(&self, settings: StorageSettings) -> Result<(), ServiceError> { + self.storage_client.set_config(settings).await?; Ok(()) } } diff --git a/rust/agama-lib/src/store.rs b/rust/agama-lib/src/store.rs index b17b848fdf..9add8349c6 100644 --- a/rust/agama-lib/src/store.rs +++ b/rust/agama-lib/src/store.rs @@ -5,8 +5,7 @@ use crate::error::ServiceError; use crate::install_settings::InstallSettings; use crate::{ localization::LocalizationStore, network::NetworkStore, product::ProductStore, - software::SoftwareStore, storage::StorageAutoyastStore, storage::StorageStore, - users::UsersStore, + software::SoftwareStore, storage::StorageStore, users::UsersStore, }; use zbus::Connection; @@ -22,7 +21,6 @@ pub struct Store<'a> { product: ProductStore<'a>, software: SoftwareStore<'a>, storage: StorageStore<'a>, - storage_autoyast: StorageAutoyastStore<'a>, localization: LocalizationStore<'a>, } @@ -37,25 +35,23 @@ impl<'a> Store<'a> { network: NetworkStore::new(http_client).await?, product: ProductStore::new(connection.clone()).await?, software: SoftwareStore::new(connection.clone()).await?, - storage: StorageStore::new(connection.clone()).await?, - storage_autoyast: StorageAutoyastStore::new(connection).await?, + storage: StorageStore::new(connection).await?, }) } /// 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) -> Result { let mut settings: InstallSettings = Default::default(); 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?); + let storage_settings = self.storage.load().await?; + settings.storage = storage_settings.storage.clone(); + settings.storage_autoyast = storage_settings.storage_autoyast.clone(); + // TODO: use try_join here Ok(settings) } @@ -80,14 +76,8 @@ impl<'a> Store<'a> { if let Some(user) = &settings.user { self.users.store(user).await?; } - if let Some(storage) = &settings.storage { - self.storage.store(storage).await?; - } - if let Some(storage_autoyast) = &settings.storage_autoyast { - // Storage scope has precedence. - if settings.storage.is_none() { - self.storage_autoyast.store(storage_autoyast.get()).await?; - } + if settings.storage.is_some() || settings.storage_autoyast.is_some() { + self.storage.store(settings.into()).await? } Ok(()) } diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index c0f4caf8ec..4f0de7fab4 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -280,6 +280,6 @@ async fn set_proposal_settings( State(state): State>, Json(config): Json, ) -> Result, Error> { - let result = state.client.calculate2(config).await?; + let result = state.client.calculate(config).await?; Ok(Json(result == 0)) } diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 1e106d521d..fa6a3bb7f6 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Wed Jun 26 10:29:05 UTC 2024 - José Iván López González + +- Set and get storage config (gh#openSUSE/agama#1293). + ------------------------------------------------------------------- Tue Jun 25 15:16:33 UTC 2024 - Imobach Gonzalez Sosa From a497d8b3b122694ea2ff509ee66036f6678e2eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 11:22:01 +0100 Subject: [PATCH 3/5] feat(storage): add conversions to and from JSON schema --- .../storage/proposal_settings_conversion.rb | 20 ++ .../from_schema.rb | 161 ++++++++++ .../proposal_settings_conversion/to_schema.rb | 108 +++++++ .../lib/agama/storage/volume_conversion.rb | 20 ++ .../storage/volume_conversion/from_schema.rb | 154 +++++++++ .../storage/volume_conversion/to_schema.rb | 100 ++++++ .../from_schema_test.rb | 293 ++++++++++++++++++ .../to_schema_test.rb | 121 ++++++++ .../proposal_settings_conversion_test.rb | 26 ++ .../volume_conversion/from_schema_test.rb | 279 +++++++++++++++++ .../volume_conversion/to_schema_test.rb | 95 ++++++ .../agama/storage/volume_conversion_test.rb | 26 ++ 12 files changed, 1403 insertions(+) create mode 100644 service/lib/agama/storage/proposal_settings_conversion/from_schema.rb create mode 100644 service/lib/agama/storage/proposal_settings_conversion/to_schema.rb create mode 100644 service/lib/agama/storage/volume_conversion/from_schema.rb create mode 100644 service/lib/agama/storage/volume_conversion/to_schema.rb create mode 100644 service/test/agama/storage/proposal_settings_conversion/from_schema_test.rb create mode 100644 service/test/agama/storage/proposal_settings_conversion/to_schema_test.rb create mode 100644 service/test/agama/storage/volume_conversion/from_schema_test.rb create mode 100644 service/test/agama/storage/volume_conversion/to_schema_test.rb diff --git a/service/lib/agama/storage/proposal_settings_conversion.rb b/service/lib/agama/storage/proposal_settings_conversion.rb index f8439c7531..ea0f28f161 100644 --- a/service/lib/agama/storage/proposal_settings_conversion.rb +++ b/service/lib/agama/storage/proposal_settings_conversion.rb @@ -19,7 +19,9 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/storage/proposal_settings_conversion/from_schema" require "agama/storage/proposal_settings_conversion/from_y2storage" +require "agama/storage/proposal_settings_conversion/to_schema" require "agama/storage/proposal_settings_conversion/to_y2storage" module Agama @@ -45,6 +47,24 @@ def self.from_y2storage(y2storage_settings, settings) def self.to_y2storage(settings, config:) ToY2Storage.new(settings, config: config).convert end + + # Performs conversion from Hash according to the JSON schema. + # + # @param schema_settings [Hash] + # @param config [Agama::Config] + # + # @return [Agama::Storage::ProposalSettings] + def self.from_schema(schema_settings, config:) + FromSchema.new(schema_settings, config: config).convert + end + + # Performs conversion according to the JSON schema. + # + # @param settings [Agama::Storage::ProposalSettings] + # @return [Hash] + def self.to_schema(settings) + ToSchema.new(settings).convert + end end end end diff --git a/service/lib/agama/storage/proposal_settings_conversion/from_schema.rb b/service/lib/agama/storage/proposal_settings_conversion/from_schema.rb new file mode 100644 index 0000000000..6db366f4d7 --- /dev/null +++ b/service/lib/agama/storage/proposal_settings_conversion/from_schema.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/boot_settings" +require "agama/storage/device_settings" +require "agama/storage/encryption_settings" +require "agama/storage/proposal_settings_reader" +require "agama/storage/space_settings" +require "agama/storage/volume_conversion" +require "y2storage/encryption_method" +require "y2storage/pbkd_function" + +module Agama + module Storage + module ProposalSettingsConversion + # Proposal settings conversion from Hash according to the JSON schema. + class FromSchema + # @param schema_settings [Hash] + # @param config [Config] + def initialize(schema_settings, config:) + # @todo Raise error if schema_settings does not match the JSON schema. + @schema_settings = schema_settings + @config = config + end + + # Performs the conversion from Hash according to the JSON schema. + # + # @return [ProposalSettings] + def convert + device_settings = target_conversion + boot_settings = boot_conversion + encryption_settings = encryption_conversion + space_settings = space_conversion + volumes = volumes_conversion + + Agama::Storage::ProposalSettingsReader.new(config).read.tap do |settings| + settings.device = device_settings if device_settings + settings.boot = boot_settings if boot_settings + settings.encryption = encryption_settings if encryption_settings + settings.space = space_settings if space_settings + settings.volumes = add_required_volumes(volumes, settings.volumes) if volumes.any? + end + end + + private + + # @return [Hash] + attr_reader :schema_settings + + # @return [Config] + attr_reader :config + + def target_conversion + target_schema = schema_settings[:target] + return unless target_schema + + if target_schema == "disk" + Agama::Storage::DeviceSettings::Disk.new + elsif target_schema == "newLvmVg" + Agama::Storage::DeviceSettings::NewLvmVg.new + elsif (device = target_schema[:disk]) + Agama::Storage::DeviceSettings::Disk.new(device) + elsif (devices = target_schema[:newLvmVg]) + Agama::Storage::DeviceSettings::NewLvmVg.new(devices) + end + end + + def boot_conversion + boot_schema = schema_settings[:boot] + return unless boot_schema + + Agama::Storage::BootSettings.new.tap do |boot_settings| + boot_settings.configure = boot_schema[:configure] + boot_settings.device = boot_schema[:device] + end + end + + def encryption_conversion + encryption_schema = schema_settings[:encryption] + return unless encryption_schema + + Agama::Storage::EncryptionSettings.new.tap do |encryption_settings| + encryption_settings.password = encryption_schema[:password] + + if (method_value = encryption_schema[:method]) + method = Y2Storage::EncryptionMethod.find(method_value.to_sym) + encryption_settings.method = method + end + + if (function_value = encryption_schema[:pbkdFunction]) + function = Y2Storage::PbkdFunction.find(function_value) + encryption_settings.pbkd_function = function + end + end + end + + def space_conversion + space_schema = schema_settings[:space] + return unless space_schema + + Agama::Storage::SpaceSettings.new.tap do |space_settings| + space_settings.policy = space_schema[:policy].to_sym + + actions_value = space_schema[:actions] || [] + space_settings.actions = actions_value.map { |a| action_conversion(a) }.inject(:merge) + end + end + + # @param action [Hash] + def action_conversion(action) + return action.invert unless action[:forceDelete] + + { action[:forceDelete] => :force_delete } + end + + def volumes_conversion + volumes_schema = schema_settings[:volumes] + return [] unless volumes_schema + + volumes_schema.map do |volume_schema| + VolumeConversion.from_schema(volume_schema, config: config) + end + end + + # Adds the missing required volumes to the list of volumes. + # + # @param volumes [Array] + # @param default_volumes [Array] Default volumes including the required ones. + # + # @return [Array] + def add_required_volumes(volumes, default_volumes) + mount_paths = volumes.map(&:mount_path) + + missing_required_volumes = default_volumes + .select { |v| v.outline.required? } + .reject { |v| mount_paths.include?(v.mount_path) } + + missing_required_volumes + volumes + end + end + end + end +end diff --git a/service/lib/agama/storage/proposal_settings_conversion/to_schema.rb b/service/lib/agama/storage/proposal_settings_conversion/to_schema.rb new file mode 100644 index 0000000000..071b1e424a --- /dev/null +++ b/service/lib/agama/storage/proposal_settings_conversion/to_schema.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/device_settings" +require "agama/storage/volume_conversion" + +module Agama + module Storage + module ProposalSettingsConversion + # Proposal settings conversion according to the JSON schema. + class ToSchema + # @param settings [ProposalSettings] + def initialize(settings) + @settings = settings + end + + # Performs the conversion according to the JSON schema. + # + # @return [Hash] + def convert + { + target: target_conversion, + boot: boot_conversion, + space: space_conversion, + volumes: volumes_conversion + }.tap do |schema| + encryption_schema = encryption_conversion + schema[:encryption] = encryption_schema if encryption_schema + end + end + + private + + # @return [ProposalSettings] + attr_reader :settings + + def target_conversion + device_settings = settings.device + + case device_settings + when Agama::Storage::DeviceSettings::Disk + device = device_settings.name + device ? { disk: device } : "disk" + when Agama::Storage::DeviceSettings::NewLvmVg + candidates = device_settings.candidate_pv_devices + candidates.any? ? { newLvmVg: candidates } : "newLvmVg" + end + end + + def boot_conversion + { + configure: settings.boot.configure? + }.tap do |schema| + device = settings.boot.device + schema[:device] = device if device + end + end + + def encryption_conversion + return unless settings.encryption.encrypt? + + { + password: settings.encryption.password, + method: settings.encryption.method.id.to_s + }.tap do |schema| + function = settings.encryption.pbkd_function + schema[:pbkdFunction] = function.value if function + end + end + + def space_conversion + { + policy: settings.space.policy.to_s, + actions: settings.space.actions.map { |d, a| { action_key(a) => d } } + } + end + + def action_key(action) + return action.to_sym if action.to_s != "force_delete" + + :forceDelete + end + + def volumes_conversion + settings.volumes.map { |v| VolumeConversion.to_schema(v) } + end + end + end + end +end diff --git a/service/lib/agama/storage/volume_conversion.rb b/service/lib/agama/storage/volume_conversion.rb index 432f39034f..9b393fa4e8 100644 --- a/service/lib/agama/storage/volume_conversion.rb +++ b/service/lib/agama/storage/volume_conversion.rb @@ -19,7 +19,9 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/storage/volume_conversion/from_schema" require "agama/storage/volume_conversion/from_y2storage" +require "agama/storage/volume_conversion/to_schema" require "agama/storage/volume_conversion/to_y2storage" module Agama @@ -41,6 +43,24 @@ def self.from_y2storage(volume) def self.to_y2storage(volume) ToY2Storage.new(volume).convert end + + # Performs conversion from Hash according to the JSON schema. + # + # @param volume_schema [Hash] + # @param config [Agama::Config] + # + # @return [Agama::Storage::Volume] + def self.from_schema(volume_schema, config:) + FromSchema.new(volume_schema, config: config).convert + end + + # Performs conversion according to the JSON schema. + # + # @param volume [Agama::Storage::Volume] + # @return [Hash] + def self.to_schema(volume) + ToSchema.new(volume).convert + end end end end diff --git a/service/lib/agama/storage/volume_conversion/from_schema.rb b/service/lib/agama/storage/volume_conversion/from_schema.rb new file mode 100644 index 0000000000..f792314576 --- /dev/null +++ b/service/lib/agama/storage/volume_conversion/from_schema.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/volume" +require "agama/storage/volume_location" +require "agama/storage/volume_templates_builder" +require "y2storage" + +module Agama + module Storage + module VolumeConversion + # Volume conversion from Hash according to the JSON schema. + class FromSchema + # @param volume_schema [Hash] + # @param config [Config] + def initialize(volume_schema, config:) + # @todo Raise error if volume_schema does not match the JSON schema. + @volume_schema = volume_schema + @config = config + end + + # Performs the conversion from Hash according to the JSON schema. + # + # @return [Volume] + def convert + default_volume.tap do |volume| + mount_conversion(volume) + filesystem_conversion(volume) + size_conversion(volume) + target_conversion(volume) + end + end + + private + + # @return [Hash] + attr_reader :volume_schema + + # @return [Agama::Config] + attr_reader :config + + # @param volume [Volume] + def mount_conversion(volume) + path_value = volume_schema.dig(:mount, :path) + options_value = volume_schema.dig(:mount, :options) + + volume.mount_path = path_value + volume.mount_options = options_value if options_value + end + + # @param volume [Volume] + def filesystem_conversion(volume) + filesystem_schema = volume_schema[:filesystem] + return unless filesystem_schema + + if filesystem_schema.is_a?(String) + filesystem_string_conversion(volume, filesystem_schema) + else + filesystem_hash_conversion(volume, filesystem_schema) + end + end + + # @param volume [Volume] + # @param filesystem [String] + def filesystem_string_conversion(volume, filesystem) + filesystems = volume.outline.filesystems + + fs_type = filesystems.find { |t| t.to_s == filesystem } + volume.fs_type = fs_type if fs_type + end + + # @param volume [Volume] + # @param filesystem [Hash] + def filesystem_hash_conversion(volume, filesystem) + filesystem_string_conversion(volume, "btrfs") + + snapshots_value = filesystem.dig(:btrfs, :snapshots) + return if !volume.outline.snapshots_configurable? || snapshots_value.nil? + + volume.btrfs.snapshots = snapshots_value + end + + # @todo Support array format ([min, max]) and string format ("2 GiB") + # @param volume [Volume] + def size_conversion(volume) + size_schema = volume_schema[:size] + return unless size_schema + + if size_schema == "auto" + volume.auto_size = true if volume.auto_size_supported? + else + volume.auto_size = false + + min_value = size_schema[:min] + max_value = size_schema[:max] + + volume.min_size = Y2Storage::DiskSize.new(min_value) + volume.max_size = if max_value + Y2Storage::DiskSize.new(max_value) + else + Y2Storage::DiskSize.unlimited + end + end + end + + def target_conversion(volume) + target_schema = volume_schema[:target] + return unless target_schema + + if target_schema == "default" + volume.location.target = :default + volume.location.device = nil + elsif (device = target_schema[:newPartition]) + volume.location.target = :new_partition + volume.location.device = device + elsif (device = target_schema[:newVg]) + volume.location.target = :new_vg + volume.location.device = device + elsif (device = target_schema[:device]) + volume.location.target = :device + volume.location.device = device + elsif (device = target_schema[:filesystem]) + volume.location.target = :filesystem + volume.location.device = device + end + end + + def default_volume + Agama::Storage::VolumeTemplatesBuilder + .new_from_config(config) + .for(volume_schema.dig(:mount, :path)) + end + end + end + end +end diff --git a/service/lib/agama/storage/volume_conversion/to_schema.rb b/service/lib/agama/storage/volume_conversion/to_schema.rb new file mode 100644 index 0000000000..0ba7e8b1ff --- /dev/null +++ b/service/lib/agama/storage/volume_conversion/to_schema.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "agama/storage/volume" +require "agama/storage/volume_location" +require "y2storage" + +module Agama + module Storage + module VolumeConversion + # Volume conversion according to the JSON schema. + class ToSchema + # @param volume [Volume] + def initialize(volume) + @volume = volume + end + + # Performs the conversion according to the JSON schema. + # + # @return [Hash] + def convert + { + mount: mount_conversion, + size: size_conversion, + target: target_conversion + }.tap do |schema| + filesystem_schema = filesystem_conversion + schema[:filesystem] = filesystem_schema if filesystem_schema + end + end + + private + + # @return [Volume] + attr_reader :volume + + def mount_conversion + { + path: volume.mount_path.to_s, + options: volume.mount_options + } + end + + def filesystem_conversion + return unless volume.fs_type + return volume.fs_type.to_s if volume.fs_type != Y2Storage::Filesystems::Type::BTRFS + + { + btrfs: { + snapshots: volume.btrfs.snapshots? + } + } + end + + def size_conversion + return "auto" if volume.auto_size? + + size = { min: volume.min_size.to_i } + size[:max] = volume.max_size.to_i if volume.max_size != Y2Storage::DiskSize.unlimited + size + end + + def target_conversion + location = volume.location + + case location.target + when :default + "default" + when :new_partition + { newPartition: location.device } + when :new_vg + { newVg: location.device } + when :device + { device: location.device } + when :filesystem + { filesystem: location.device } + end + end + end + end + end +end diff --git a/service/test/agama/storage/proposal_settings_conversion/from_schema_test.rb b/service/test/agama/storage/proposal_settings_conversion/from_schema_test.rb new file mode 100644 index 0000000000..14d7a89d91 --- /dev/null +++ b/service/test/agama/storage/proposal_settings_conversion/from_schema_test.rb @@ -0,0 +1,293 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "agama/storage/proposal_settings_conversion/from_schema" +require "agama/config" +require "agama/storage/device_settings" +require "agama/storage/proposal_settings" +require "y2storage/encryption_method" +require "y2storage/pbkd_function" + +describe Agama::Storage::ProposalSettingsConversion::FromSchema do + subject { described_class.new(settings_schema, config: config) } + + let(:config) { Agama::Config.new(config_data) } + + let(:config_data) do + { + "storage" => { + "lvm" => false, + "space_policy" => "delete", + "encryption" => { + "method" => "luks2", + "pbkd_function" => "argon2id" + }, + "volumes" => ["/", "swap"], + "volume_templates" => [ + { + "mount_path" => "/", + "outline" => { "required" => true } + }, + { + "mount_path" => "/home", + "outline" => { "required" => false } + }, + { + "mount_path" => "swap", + "outline" => { "required" => false } + } + ] + } + } + end + + before do + allow(Agama::Storage::EncryptionSettings) + .to receive(:available_methods).and_return( + [ + Y2Storage::EncryptionMethod::LUKS1, + Y2Storage::EncryptionMethod::LUKS2 + ] + ) + end + + describe "#convert" do + let(:settings_schema) do + { + target: { + disk: "/dev/sda" + }, + boot: { + configure: true, + device: "/dev/sdb" + }, + encryption: { + password: "notsecret", + method: "luks1", + pbkdFunction: "pbkdf2" + }, + space: { + policy: "custom", + actions: [ + { forceDelete: "/dev/sda" }, + { resize: "/dev/sdb1" } + ] + }, + volumes: [ + { + mount: { + path: "/" + } + }, + { + mount: { + path: "/test" + } + } + ] + } + end + + it "generates settings with the values provided from hash according to the JSON schema" do + settings = subject.convert + + expect(settings).to be_a(Agama::Storage::ProposalSettings) + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) + expect(settings.device.name).to eq("/dev/sda") + expect(settings.boot.configure?).to eq(true) + expect(settings.boot.device).to eq("/dev/sdb") + expect(settings.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS1) + expect(settings.encryption.pbkd_function).to eq(Y2Storage::PbkdFunction::PBKDF2) + expect(settings.space.policy).to eq(:custom) + expect(settings.space.actions).to eq({ + "/dev/sda" => :force_delete, "/dev/sdb1" => :resize + }) + expect(settings.volumes).to contain_exactly( + an_object_having_attributes(mount_path: "/"), + an_object_having_attributes(mount_path: "/test") + ) + end + + context "when the hash settings is missing some values" do + let(:settings_schema) { {} } + + it "completes the missing values with default values from the config" do + settings = subject.convert + + expect(settings).to be_a(Agama::Storage::ProposalSettings) + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) + expect(settings.device.name).to be_nil + expect(settings.boot.configure?).to eq(true) + expect(settings.boot.device).to be_nil + expect(settings.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS2) + expect(settings.encryption.pbkd_function).to eq(Y2Storage::PbkdFunction::ARGON2ID) + expect(settings.space.policy).to eq(:delete) + expect(settings.volumes).to contain_exactly( + an_object_having_attributes(mount_path: "/"), + an_object_having_attributes(mount_path: "swap") + ) + end + end + + context "when the hash settings does not indicate the target" do + let(:settings_schema) { {} } + + it "generates settings with disk target and without specific device" do + settings = subject.convert + + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) + expect(settings.device.name).to be_nil + end + end + + context "when the hash settings indicates disk target without device" do + let(:settings_schema) do + { + target: "disk" + } + end + + it "generates settings with disk target and without specific device" do + settings = subject.convert + + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) + expect(settings.device.name).to be_nil + end + end + + context "when the hash settings indicates disk target with a device" do + let(:settings_schema) do + { + target: { + disk: "/dev/vda" + } + } + end + + it "generates settings with disk target and with specific device" do + settings = subject.convert + + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) + expect(settings.device.name).to eq("/dev/vda") + end + end + + context "when the hash settings indicates newLvmVg target without devices" do + let(:settings_schema) do + { + target: "newLvmVg" + } + end + + it "generates settings with newLvmVg target and without specific devices" do + settings = subject.convert + + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::NewLvmVg) + expect(settings.device.candidate_pv_devices).to eq([]) + end + end + + context "when the hash settings indicates newLvmVg target with devices" do + let(:settings_schema) do + { + target: { + newLvmVg: ["/dev/vda", "/dev/vdb"] + } + } + end + + it "generates settings with newLvmVg target and with specific devices" do + settings = subject.convert + + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::NewLvmVg) + expect(settings.device.candidate_pv_devices).to contain_exactly("/dev/vda", "/dev/vdb") + end + end + + context "when the hash settings does not indicate volumes" do + let(:settings_schema) { { volumes: [] } } + + it "generates settings with the default volumes from config" do + settings = subject.convert + + expect(settings.volumes).to contain_exactly( + an_object_having_attributes(mount_path: "/"), + an_object_having_attributes(mount_path: "swap") + ) + end + + it "ignores templates of non-default volumes" do + settings = subject.convert + + expect(settings.volumes).to_not include( + an_object_having_attributes(mount_path: "/home") + ) + end + end + + context "when the hash settings does not contain a required volume" do + let(:settings_schema) do + { + volumes: [ + { + mount: { + path: "/test" + } + } + ] + } + end + + it "generates settings including the required volumes" do + settings = subject.convert + + expect(settings.volumes).to include( + an_object_having_attributes(mount_path: "/") + ) + end + + it "generates settings including the given volumes" do + settings = subject.convert + + expect(settings.volumes).to include( + an_object_having_attributes(mount_path: "/test") + ) + end + + it "ignores default volumes that are not required" do + settings = subject.convert + + expect(settings.volumes).to_not include( + an_object_having_attributes(mount_path: "swap") + ) + end + + it "ignores templates for excluded volumes" do + settings = subject.convert + + expect(settings.volumes).to_not include( + an_object_having_attributes(mount_path: "/home") + ) + end + end + end +end diff --git a/service/test/agama/storage/proposal_settings_conversion/to_schema_test.rb b/service/test/agama/storage/proposal_settings_conversion/to_schema_test.rb new file mode 100644 index 0000000000..8eba7d5a45 --- /dev/null +++ b/service/test/agama/storage/proposal_settings_conversion/to_schema_test.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "agama/storage/proposal_settings_conversion/to_schema" +require "agama/storage/device_settings" +require "agama/storage/proposal_settings" +require "agama/storage/volume" +require "y2storage/encryption_method" +require "y2storage/pbkd_function" + +describe Agama::Storage::ProposalSettingsConversion::ToSchema do + let(:default_settings) { Agama::Storage::ProposalSettings.new } + + let(:custom_settings) do + Agama::Storage::ProposalSettings.new.tap do |settings| + settings.device.name = "/dev/sda" + settings.boot.device = "/dev/sdb" + settings.encryption.password = "notsecret" + settings.encryption.method = Y2Storage::EncryptionMethod::LUKS2 + settings.encryption.pbkd_function = Y2Storage::PbkdFunction::ARGON2ID + settings.space.policy = :custom + settings.space.actions = { "/dev/sda" => :force_delete, "/dev/sdb1" => "resize" } + settings.volumes = [Agama::Storage::Volume.new("/test")] + end + end + + describe "#convert" do + it "converts the settings to the proper hash according to the JSON schema" do + # @todo Check whether the result matches the JSON schema. + + expect(described_class.new(default_settings).convert).to eq( + target: "disk", + boot: { + configure: true + }, + space: { + policy: "keep", + actions: [] + }, + volumes: [] + ) + + expect(described_class.new(custom_settings).convert).to eq( + target: { + disk: "/dev/sda" + }, + boot: { + configure: true, + device: "/dev/sdb" + }, + encryption: { + password: "notsecret", + method: "luks2", + pbkdFunction: "argon2id" + }, + space: { + policy: "custom", + actions: [ + { forceDelete: "/dev/sda" }, + { resize: "/dev/sdb1" } + ] + }, + volumes: [ + { + mount: { + path: "/test", + options: [] + }, + size: { + min: 0 + }, + target: "default" + } + ] + ) + end + + context "when the target is a new LVM volume group" do + let(:settings) do + Agama::Storage::ProposalSettings.new.tap do |settings| + settings.device = Agama::Storage::DeviceSettings::NewLvmVg.new(["/dev/vda"]) + end + end + + it "converts the settings to the proper hash according to the JSON schema" do + expect(described_class.new(settings).convert).to eq( + target: { + newLvmVg: ["/dev/vda"] + }, + boot: { + configure: true + }, + space: { + policy: "keep", + actions: [] + }, + volumes: [] + ) + end + end + end +end diff --git a/service/test/agama/storage/proposal_settings_conversion_test.rb b/service/test/agama/storage/proposal_settings_conversion_test.rb index 085e35f37b..e57a03e257 100644 --- a/service/test/agama/storage/proposal_settings_conversion_test.rb +++ b/service/test/agama/storage/proposal_settings_conversion_test.rb @@ -47,4 +47,30 @@ expect(result).to be_a(Y2Storage::ProposalSettings) end end + + describe "#from_schema" do + let(:config) { Agama::Config.new } + + let(:schema_settings) do + { + target: { + disk: "/dev/vda" + } + } + end + + it "generates proposal settings from settings according to the JSON schema" do + result = described_class.from_schema(schema_settings, config: config) + expect(result).to be_a(Agama::Storage::ProposalSettings) + end + end + + describe "#to_schema" do + let(:proposal_settings) { Agama::Storage::ProposalSettings.new } + + it "generates settings according to the JSON schema from the proposal settings" do + result = described_class.to_schema(proposal_settings) + expect(result).to be_a(Hash) + end + end end diff --git a/service/test/agama/storage/volume_conversion/from_schema_test.rb b/service/test/agama/storage/volume_conversion/from_schema_test.rb new file mode 100644 index 0000000000..0a009e1257 --- /dev/null +++ b/service/test/agama/storage/volume_conversion/from_schema_test.rb @@ -0,0 +1,279 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require_relative "../../rspec/matchers/storage" +require "agama/config" +require "agama/storage/volume" +require "agama/storage/volume_templates_builder" +require "agama/storage/volume_conversion/from_schema" +require "y2storage/disk_size" + +def default_volume(mount_path) + Agama::Storage::VolumeTemplatesBuilder.new_from_config(config).for(mount_path) +end + +describe Agama::Storage::VolumeConversion::FromSchema do + subject { described_class.new(volume_schema, config: config) } + + let(:config) { Agama::Config.new(config_data) } + + let(:config_data) do + { + "storage" => { + "volume_templates" => [ + { + "mount_path" => "/test", + "mount_options" => ["data=ordered"], + "filesystem" => "btrfs", + "size" => { + "auto" => false, + "min" => "5 GiB", + "max" => "10 GiB" + }, + "btrfs" => { + "snapshots" => false + }, + "outline" => outline + } + ] + } + } + end + + let(:outline) do + { + "filesystems" => ["xfs", "ext3", "ext4"], + "snapshots_configurable" => true + } + end + + describe "#convert" do + let(:volume_schema) do + { + mount: { + path: "/test", + options: ["rw", "default"] + }, + target: { + newVg: "/dev/sda" + }, + filesystem: "ext4", + size: { + min: 1024, + max: 2048 + } + } + end + + it "generates a volume with the expected outline from the config" do + volume = subject.convert + + expect(volume.outline).to eq_outline(default_volume("/test").outline) + end + + it "generates a volume with the values provided from hash according to the JSON schema" do + volume = subject.convert + + expect(volume).to be_a(Agama::Storage::Volume) + expect(volume.mount_path).to eq("/test") + expect(volume.mount_options).to contain_exactly("rw", "default") + expect(volume.location.device).to eq("/dev/sda") + expect(volume.location.target).to eq(:new_vg) + expect(volume.fs_type).to eq(Y2Storage::Filesystems::Type::EXT4) + expect(volume.auto_size?).to eq(false) + expect(volume.min_size.to_i).to eq(1024) + expect(volume.max_size.to_i).to eq(2048) + expect(volume.btrfs.snapshots).to eq(false) + end + + context "when the hash settings is missing some values" do + let(:volume_schema) do + { + mount: { + path: "/test" + } + } + end + + it "completes the missing values with default values from the config" do + volume = subject.convert + + expect(volume).to be_a(Agama::Storage::Volume) + expect(volume.mount_path).to eq("/test") + expect(volume.mount_options).to contain_exactly("data=ordered") + expect(volume.location.target).to eq :default + expect(volume.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS) + expect(volume.auto_size?).to eq(false) + expect(volume.min_size.to_i).to eq(5 * (1024**3)) + expect(volume.max_size.to_i).to eq(10 * (1024**3)) + expect(volume.btrfs.snapshots?).to eq(false) + end + end + + context "when the hash settings does not indicate max size" do + let(:volume_schema) do + { + mount: { + path: "/test" + }, + size: { + min: 1024 + } + } + end + + it "generates a volume with unlimited max size" do + volume = subject.convert + + expect(volume.max_size).to eq(Y2Storage::DiskSize.unlimited) + end + end + + context "when the hash settings indicates auto size for a supported volume" do + let(:outline) do + { + "auto_size" => { + "min_fallback_for" => ["/"] + } + } + end + + let(:volume_schema) do + { + mount: { + path: "/test" + }, + size: "auto" + } + end + + it "generates a volume with auto size" do + volume = subject.convert + + expect(volume.auto_size?).to eq(true) + end + end + + context "when the hash settings indicates auto size for an unsupported volume" do + let(:outline) { {} } + + let(:volume_schema) do + { + mount: { + path: "/test" + }, + size: "auto" + } + end + + it "ignores the auto size setting" do + volume = subject.convert + + expect(volume.auto_size?).to eq(false) + end + end + + context "when the hash settings indicates a filesystem included in the outline" do + let(:outline) { { "filesystems" => ["btrfs", "ext4"] } } + + let(:volume_schema) do + { + mount: { + path: "/test" + }, + filesystem: "ext4" + } + end + + it "generates a volume with the indicated filesystem" do + volume = subject.convert + + expect(volume.fs_type).to eq(Y2Storage::Filesystems::Type::EXT4) + end + end + + context "when the hash settings indicates a filesystem not included in the outline" do + let(:outline) { { "filesystems" => ["btrfs"] } } + + let(:volume_schema) do + { + mount: { + path: "/test" + }, + filesystem: "ext4" + } + end + + it "ignores the filesystem setting" do + volume = subject.convert + + expect(volume.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS) + end + end + + context "when the hash settings indicates snapshots for a supported volume" do + let(:outline) { { "snapshots_configurable" => true } } + + let(:volume_schema) do + { + mount: { + path: "/test" + }, + filesystem: { + btrfs: { + snapshots: true + } + } + } + end + + it "generates a volume with snapshots" do + volume = subject.convert + + expect(volume.btrfs.snapshots?).to eq(true) + end + end + + context "when the D-Bus settings provide Snapshots for an unsupported volume" do + let(:outline) { { "snapshots_configurable" => false } } + + let(:volume_schema) do + { + mount: { + path: "/test" + }, + filesystem: { + btrfs: { + snapshots: true + } + } + } + end + + it "ignores the snapshots setting" do + volume = subject.convert + + expect(volume.btrfs.snapshots?).to eq(false) + end + end + end +end diff --git a/service/test/agama/storage/volume_conversion/to_schema_test.rb b/service/test/agama/storage/volume_conversion/to_schema_test.rb new file mode 100644 index 0000000000..a3e757678e --- /dev/null +++ b/service/test/agama/storage/volume_conversion/to_schema_test.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "agama/storage/volume_conversion/to_schema" +require "agama/storage/volume" +require "y2storage/filesystems/type" +require "y2storage/disk_size" + +describe Agama::Storage::VolumeConversion::ToSchema do + let(:default_volume) { Agama::Storage::Volume.new("/test") } + + let(:custom_volume1) do + Agama::Storage::Volume.new("/test").tap do |volume| + volume.fs_type = Y2Storage::Filesystems::Type::XFS + volume.auto_size = true + end + end + + let(:custom_volume2) do + Agama::Storage::Volume.new("/test").tap do |volume| + volume.fs_type = Y2Storage::Filesystems::Type::BTRFS + volume.btrfs.snapshots = true + volume.mount_options = ["rw", "default"] + volume.location.device = "/dev/sda" + volume.location.target = :new_partition + volume.min_size = Y2Storage::DiskSize.new(1024) + volume.max_size = Y2Storage::DiskSize.new(2048) + end + end + + describe "#convert" do + it "converts the volume to the proper hash according to the JSON schema" do + # @todo Check whether the result matches the JSON schema. + + expect(described_class.new(default_volume).convert).to eq( + mount: { + path: "/test", + options: [] + }, + size: { + min: 0 + }, + target: "default" + ) + + expect(described_class.new(custom_volume1).convert).to eq( + mount: { + path: "/test", + options: [] + }, + filesystem: "xfs", + size: "auto", + target: "default" + ) + + expect(described_class.new(custom_volume2).convert).to eq( + mount: { + path: "/test", + options: ["rw", "default"] + }, + size: { + min: 1024, + max: 2048 + }, + target: { + newPartition: "/dev/sda" + }, + filesystem: { + btrfs: { + snapshots: true + } + } + ) + end + end +end diff --git a/service/test/agama/storage/volume_conversion_test.rb b/service/test/agama/storage/volume_conversion_test.rb index 141831b385..af2d9175bf 100644 --- a/service/test/agama/storage/volume_conversion_test.rb +++ b/service/test/agama/storage/volume_conversion_test.rb @@ -44,4 +44,30 @@ expect(result).to be_a(Y2Storage::VolumeSpecification) end end + + describe "#from_schema" do + let(:config) { Agama::Config.new } + + let(:volume_schema) do + { + mount: { + path: "/test" + } + } + end + + it "generates a volume from settings according to the JSON schema" do + result = described_class.from_schema(volume_schema, config: config) + expect(result).to be_a(Agama::Storage::Volume) + end + end + + describe "#to_schema" do + let(:volume) { Agama::Storage::Volume.new("/test") } + + it "generates volume settings according to the JSON schema from a volume" do + result = described_class.to_schema(volume) + expect(result).to be_a(Hash) + end + end end From 2e3278b0068a25aab536fe4dda8514c60b817e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 11:23:52 +0100 Subject: [PATCH 4/5] feat(dbus): add API to set and get storage config --- service/lib/agama/dbus/storage/manager.rb | 120 +++++-- .../proposal_settings_conversion/to_dbus.rb | 4 +- service/lib/agama/storage/boot_settings.rb | 2 +- service/package/rubygem-agama-yast.changes | 6 + .../test/agama/dbus/storage/manager_test.rb | 311 +++++++++++------- 5 files changed, 302 insertions(+), 141 deletions(-) diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 382899695b..9034066bd7 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -35,8 +35,9 @@ require "agama/dbus/storage/volume_conversion" require "agama/dbus/storage/with_iscsi_auth" require "agama/dbus/with_service_status" -require "agama/storage/volume_templates_builder" require "agama/storage/encryption_settings" +require "agama/storage/proposal_settings_conversion" +require "agama/storage/volume_templates_builder" Yast.import "Arch" @@ -90,6 +91,33 @@ def probe busy_while { backend.probe } end + # Sets the storage config and calculates a proposal (guided or AutoYaST). + # + # @raise If config is not valid. + # + # @param serialized_config [String] Serialized storage config. It can be storage or legacy + # AutoYaST settings: { "storage": ... } vs { "legacyAutoyastStorage": ... }. + def apply_storage_config(serialized_config) + @serialized_storage_config = serialized_config + storage_config = JSON.parse(serialized_config, symbolize_names: true) + + if (guided_settings = storage_config.dig(:storage, :guided)) + calculate_guided_proposal(guided_settings) + elsif (autoyast_settings = storage_config[:legacyAutoyastStorage]) + calculate_autoyast_proposal(autoyast_settings) + else + raise "Invalid config: #{serialized_config}" + end + end + + # Serialized storage config. It can contain storage or legacy AutoYaST settings: + # { "storage": ... } vs { "legacyAutoyastStorage": ... } + # + # @return [String] + def serialized_storage_config + @serialized_storage_config || generate_storage_config.to_json + end + def install busy_while { backend.install } end @@ -107,6 +135,10 @@ def deprecated_system dbus_interface STORAGE_INTERFACE do dbus_method(:Probe) { probe } + dbus_method(:SetConfig, "in serialized_config:s, out result:u") do |serialized_config| + busy_while { apply_storage_config(serialized_config) } + end + dbus_method(:GetConfig, "out config:s") { serialized_storage_config } dbus_method(:Install) { install } dbus_method(:Finish) { finish } dbus_reader(:deprecated_system, "b") @@ -195,10 +227,11 @@ module ProposalStrategy end # Calculates a guided proposal. + # @deprecated # # @param dbus_settings [Hash] # @return [Integer] 0 success; 1 error - def calculate_guided_proposal(dbus_settings) + def calculate_proposal(dbus_settings) settings = ProposalSettingsConversion.from_dbus(dbus_settings, config: config, logger: logger) @@ -212,23 +245,6 @@ def calculate_guided_proposal(dbus_settings) success ? 0 : 1 end - # Calculates an AutoYaST proposal. - # - # @param dbus_settings [String] - # @return [Integer] 0 success; 1 error - def calculate_autoyast_proposal(dbus_settings) - settings = JSON.parse(dbus_settings) - - logger.info( - "Calculating AutoYaST storage proposal from D-Bus.\n " \ - "D-Bus settings: #{dbus_settings}\n" \ - "AutoYaST settings: #{settings.inspect}" - ) - - success = proposal.calculate_autoyast(settings) - success ? 0 : 1 - end - # Whether a proposal was calculated. # # @return [Boolean] @@ -243,10 +259,11 @@ def proposal_result return {} unless proposal.calculated? if proposal.strategy?(ProposalStrategy::GUIDED) + settings = Agama::Storage::ProposalSettingsConversion.to_schema(proposal.settings) { "success" => proposal.success?, "strategy" => ProposalStrategy::GUIDED, - "settings" => ProposalSettingsConversion.to_dbus(proposal.settings) + "settings" => settings.to_json } else { @@ -273,12 +290,7 @@ def proposal_result # # result: 0 success; 1 error dbus_method(:Calculate, "in settings:a{sv}, out result:u") do |settings| - busy_while { calculate_guided_proposal(settings) } - end - - # result: 0 success; 1 error - dbus_method(:CalculateAutoyast, "in settings:s, out result:u") do |settings| - busy_while { calculate_autoyast_proposal(settings) } + busy_while { calculate_proposal(settings) } end dbus_reader :proposal_calculated?, "b", dbus_name: "Calculated" @@ -386,6 +398,62 @@ def proposal backend.proposal end + # Calculates a guided proposal. + # + # @param settings [Hash] Settings according to the JSON schema. + # @return [Integer] 0 success; 1 error + def calculate_guided_proposal(settings) + proposal_settings = Agama::Storage::ProposalSettingsConversion.from_schema( + settings, config: config + ) + + logger.info( + "Calculating guided storage proposal from D-Bus.\n" \ + "Input settings: #{settings}\n" \ + "Agama settings: #{proposal_settings.inspect}" + ) + + success = proposal.calculate_guided(proposal_settings) + success ? 0 : 1 + end + + # Calculates an AutoYaST proposal. + # + # @param settings [Hash] AutoYaST settings. + # @return [Integer] 0 success; 1 error + def calculate_autoyast_proposal(settings) + # Ensures keys are strings. + autoyast_settings = JSON.parse(settings.to_json) + + logger.info( + "Calculating AutoYaST storage proposal from D-Bus.\n" \ + "Input settings: #{settings}\n" \ + "AutoYaST settings: #{autoyast_settings}" + ) + + success = proposal.calculate_autoyast(autoyast_settings) + success ? 0 : 1 + end + + # Generates the storage config from the current proposal, if any. + # + # @return [Hash] Storage config according to the JSON schema. + def generate_storage_config + if proposal.strategy?(ProposalStrategy::GUIDED) + { + storage: { + guided: Agama::Storage::ProposalSettingsConversion.to_schema(proposal.settings) + } + } + elsif proposal.strategy?(ProposalStrategy::AUTOYAST) + { + autoyastLegacyStorage: proposal.settings + } + else + {} + end + end + def register_storage_callbacks backend.on_issues_change { issues_properties_changed } backend.on_deprecated_system_change { storage_properties_changed } diff --git a/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb b/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb index b1d725e69b..1fc3ed3f09 100644 --- a/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb +++ b/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb @@ -37,8 +37,8 @@ def initialize(settings) # # @return [Hash] # * "Target" [String] - # * "Device" [String] Optional - # * "CandidatePVDevices" [Array] Optional + # * "TargetDevice" [String] Optional + # * "TargetPVDevices" [Array] Optional # * "ConfigureBoot" [Boolean] # * "BootDevice" [String] # * "DefaultBootDevice" [String] diff --git a/service/lib/agama/storage/boot_settings.rb b/service/lib/agama/storage/boot_settings.rb index 4ef2e4dca8..228668280e 100644 --- a/service/lib/agama/storage/boot_settings.rb +++ b/service/lib/agama/storage/boot_settings.rb @@ -31,7 +31,7 @@ class BootSettings # Device to use for booting. # - # @return [String, nil] + # @return [String, nil] nil means use installation device. attr_accessor :device def initialize diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 3a3d2e3201..0d58f88f74 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jun 26 10:32:08 UTC 2024 - José Iván López González + +- Extend D-Bus storage API to set and get storage config using + settings according to the JSON schema (gh#openSUSE/agama#1293). + ------------------------------------------------------------------- Wed Jun 26 09:53:23 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index 32d52f59c9..b0d79d6ff3 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -20,12 +20,14 @@ # find current contact information at www.suse.com. require_relative "../../../test_helper" +require_relative "../../storage/storage_helpers" require "agama/dbus/storage/manager" require "agama/dbus/storage/proposal" require "agama/storage/device_settings" require "agama/storage/manager" require "agama/storage/proposal" require "agama/storage/proposal_settings" +require "agama/storage/proposal_settings_conversion" require "agama/storage/volume" require "agama/storage/iscsi/manager" require "agama/storage/dasd/manager" @@ -35,6 +37,8 @@ require "dbus" describe Agama::DBus::Storage::Manager do + include Agama::RSpec::StorageHelpers + subject(:manager) { described_class.new(backend, logger) } let(:logger) { Logger.new($stdout, level: :warn) } @@ -56,11 +60,7 @@ let(:config) { Agama::Config.new(config_data) } let(:config_data) { {} } - let(:proposal) do - instance_double(Agama::Storage::Proposal, on_calculate: nil, settings: settings) - end - - let(:settings) { nil } + let(:proposal) { Agama::Storage::Proposal.new(config) } let(:iscsi) do instance_double(Agama::Storage::ISCSI::Manager, @@ -75,6 +75,8 @@ before do allow(Yast::Arch).to receive(:s390).and_return false + allow(proposal).to receive(:on_calculate) + mock_storage(devicegraph: "empty-hd-50GiB.yaml") end describe "#deprecated_system" do @@ -336,157 +338,238 @@ end end - describe "#calculate_guided_proposal" do - let(:dbus_settings) do - { - "Target" => "disk", - "TargetDevice" => "/dev/vda", - "BootDevice" => "/dev/vdb", - "EncryptionPassword" => "n0ts3cr3t", - "Volumes" => dbus_volumes - } - end - - let(:dbus_volumes) do - [ - { "MountPath" => "/" }, - { "MountPath" => "swap" } - ] - end - - it "calculates a proposal with settings having values from D-Bus" do - expect(proposal).to receive(:calculate_guided) do |settings| - expect(settings).to be_a(Agama::Storage::ProposalSettings) - expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) - expect(settings.device.name).to eq "/dev/vda" - expect(settings.boot.device).to eq "/dev/vdb" - expect(settings.encryption).to be_a(Agama::Storage::EncryptionSettings) - expect(settings.encryption.password).to eq("n0ts3cr3t") - expect(settings.volumes).to contain_exactly( - an_object_having_attributes(mount_path: "/"), - an_object_having_attributes(mount_path: "swap") - ) + describe "#apply_storage_config" do + context "if the serialized config contains guided proposal settings" do + let(:storage_config) do + { + storage: { + guided: { + target: { + disk: "/dev/vda" + }, + boot: { + device: "/dev/vdb" + }, + encryption: { + password: "notsecret" + }, + volumes: volumes_settings + } + } + } end - subject.calculate_guided_proposal(dbus_settings) - end - - context "when the D-Bus settings does not include some values" do - let(:dbus_settings) { {} } + let(:volumes_settings) do + [ + { + mount: { + path: "/" + } + }, + { + mount: { + path: "swap" + } + } + ] + end - it "calculates a proposal with default values for the missing settings" do + it "calculates a guided proposal with the given settings" do expect(proposal).to receive(:calculate_guided) do |settings| expect(settings).to be_a(Agama::Storage::ProposalSettings) expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) - expect(settings.device.name).to be_nil - expect(settings.boot.device).to be_nil + expect(settings.device.name).to eq "/dev/vda" + expect(settings.boot.device).to eq "/dev/vdb" expect(settings.encryption).to be_a(Agama::Storage::EncryptionSettings) - expect(settings.encryption.password).to be_nil - expect(settings.volumes).to eq([]) + expect(settings.encryption.password).to eq("notsecret") + expect(settings.volumes).to contain_exactly( + an_object_having_attributes(mount_path: "/"), + an_object_having_attributes(mount_path: "swap") + ) end - subject.calculate_guided_proposal(dbus_settings) + subject.apply_storage_config(storage_config.to_json) end - end - - context "when the D-Bus settings include some unexpected attribute" do - let(:dbus_settings) { { "CandidateDevices" => ["/dev/vda"] } } - # This is likely a temporary behavior - it "calculates a proposal ignoring the unknown attributes" do - expect(proposal).to receive(:calculate_guided) do |settings| - expect(settings).to be_a(Agama::Storage::ProposalSettings) + context "when the serialized config omits some settings" do + let(:storage_config) do + { + storage: { + guided: {} + } + } end - subject.calculate_guided_proposal(dbus_settings) - end - end - - context "when the D-Bus settings includes a volume" do - let(:dbus_volumes) { [dbus_volume1] } + it "calculates a proposal with default values for the missing settings" do + expect(proposal).to receive(:calculate_guided) do |settings| + expect(settings).to be_a(Agama::Storage::ProposalSettings) + expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) + expect(settings.device.name).to be_nil + expect(settings.boot.device).to be_nil + expect(settings.encryption).to be_a(Agama::Storage::EncryptionSettings) + expect(settings.encryption.password).to be_nil + expect(settings.volumes).to eq([]) + end - let(:dbus_volume1) do - { - "MountPath" => "/", - "AutoSize" => false, - "MinSize" => 1024, - "MaxSize" => 2048, - "FsType" => "Btrfs", - "Snapshots" => true - } + subject.apply_storage_config(storage_config.to_json) + end end - let(:config_data) do - { "storage" => { "volumes" => [], "volume_templates" => cfg_templates } } - end + context "when the serialized config includes a volume" do + let(:volumes_settings) { [volume1_settings] } - let(:cfg_templates) do - [ + let(:volume1_settings) do { - "mount_path" => "/", - "outline" => { - "snapshots_configurable" => true + mount: { + path: "/" + }, + size: { + min: 1024, + max: 2048 + }, + filesystem: { + btrfs: { + snapshots: true + } } } - ] - end - - it "calculates a proposal with settings having a volume with values from D-Bus" do - expect(proposal).to receive(:calculate_guided) do |settings| - volume = settings.volumes.first - - expect(volume.mount_path).to eq("/") - expect(volume.auto_size).to eq(false) - expect(volume.min_size.to_i).to eq(1024) - expect(volume.max_size.to_i).to eq(2048) - expect(volume.btrfs.snapshots).to eq(true) end - subject.calculate_guided_proposal(dbus_settings) - end - - context "and the D-Bus volume does not include some values" do - let(:dbus_volume1) { { "MountPath" => "/" } } + let(:config_data) do + { "storage" => { "volumes" => [], "volume_templates" => cfg_templates } } + end let(:cfg_templates) do [ { - "mount_path" => "/", "filesystem" => "btrfs", - "size" => { "auto" => false, "min" => "5 GiB", "max" => "20 GiB" }, - "outline" => { - "filesystems" => ["btrfs"] + "mount_path" => "/", + "outline" => { + "snapshots_configurable" => true } } ] end - it "calculates a proposal with a volume completed with its default settings" do + it "calculates a proposal with the given volume settings" do expect(proposal).to receive(:calculate_guided) do |settings| volume = settings.volumes.first expect(volume.mount_path).to eq("/") expect(volume.auto_size).to eq(false) - expect(volume.min_size.to_i).to eq(5 * (1024**3)) - # missing maximum value means unlimited size - expect(volume.max_size.to_i).to eq(-1) - expect(volume.btrfs.snapshots).to eq(false) + expect(volume.min_size.to_i).to eq(1024) + expect(volume.max_size.to_i).to eq(2048) + expect(volume.btrfs.snapshots).to eq(true) end - subject.calculate_guided_proposal(dbus_settings) + subject.apply_storage_config(storage_config.to_json) end + + context "and the volume settings omits some values" do + let(:volume1_settings) do + { + mount: { + path: "/" + } + } + end + + let(:cfg_templates) do + [ + { + "mount_path" => "/", "filesystem" => "btrfs", + "size" => { "auto" => false, "min" => "5 GiB", "max" => "20 GiB" }, + "outline" => { + "filesystems" => ["btrfs"] + } + } + ] + end + + it "calculates a proposal with default settings for the missing volume settings" do + expect(proposal).to receive(:calculate_guided) do |settings| + volume = settings.volumes.first + + expect(volume.mount_path).to eq("/") + expect(volume.auto_size).to eq(false) + expect(volume.min_size.to_i).to eq(5 * (1024**3)) + expect(volume.max_size.to_i).to eq(20 * (1024**3)) + expect(volume.btrfs.snapshots).to eq(false) + end + + subject.apply_storage_config(storage_config.to_json) + end + end + end + end + + context "if the serialized config contains legacy AutoYaST settings" do + let(:storage_config) do + { + legacyAutoyastStorage: [ + { device: "/dev/vda" } + ] + } + end + + it "calculates an AutoYaST proposal with the given settings" do + expect(proposal).to receive(:calculate_autoyast) do |settings| + expect(settings).to eq([{ "device" => "/dev/vda" }]) + end + + subject.apply_storage_config(storage_config.to_json) end end end - describe "#calculate_autoyast_proposal" do - let(:dbus_settings) { '[{ "device": "/dev/vda" }]' } + describe "#serialized_storage_config" do + context "if the storage config has not been set yet" do + context "and a proposal has not been calculated" do + it "returns serialized empty storage config" do + expect(subject.serialized_storage_config).to eq({}.to_json) + end + end + + context "and a proposal has been calculated" do + before do + proposal.calculate_guided(settings) + end + + let(:settings) do + Agama::Storage::ProposalSettings.new.tap do |settings| + settings.device = Agama::Storage::DeviceSettings::Disk.new("/dev/vda") + end + end + + it "returns serialized storage config including guided proposal settings" do + expected_config = { + storage: { + guided: Agama::Storage::ProposalSettingsConversion.to_schema(settings) + } + } - it "calculates an AutoYaST proposal with the settings from D-Bus" do - expect(proposal).to receive(:calculate_autoyast) do |settings| - expect(settings).to eq([{ "device" => "/dev/vda" }]) + expect(subject.serialized_storage_config).to eq(expected_config.to_json) + end + end + end + + context "if the storage config has been set" do + before do + subject.apply_storage_config(storage_config.to_json) end - subject.calculate_autoyast_proposal(dbus_settings) + let(:storage_config) do + { + storage: { + guided: { + disk: "/dev/vdc" + } + } + } + end + + it "returns the serialized storage config" do + expect(subject.serialized_storage_config).to eq(storage_config.to_json) + end end end @@ -539,11 +622,14 @@ it "returns a Hash with success, strategy and settings" do result = subject.proposal_result + serialized_settings = Agama::Storage::ProposalSettingsConversion + .to_schema(proposal.settings) + .to_json expect(result.keys).to contain_exactly("success", "strategy", "settings") expect(result["success"]).to eq(true) expect(result["strategy"]).to eq(guided) - expect(result["settings"]).to be_a(Hash) + expect(result["settings"]).to eq(serialized_settings) end end @@ -556,11 +642,12 @@ it "returns a Hash with success, strategy and settings" do result = subject.proposal_result + serialized_settings = proposal.settings.to_json expect(result.keys).to contain_exactly("success", "strategy", "settings") expect(result["success"]).to eq(true) expect(result["strategy"]).to eq(autoyast) - expect(result["settings"]).to be_a(String) + expect(result["settings"]).to eq(serialized_settings) end end end From 0059e2ff93e2ede8f4e1311afec3baf9c14dbfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 26 Jun 2024 12:42:27 +0100 Subject: [PATCH 5/5] fix(rust): import using a single line --- rust/agama-lib/src/storage/store.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/agama-lib/src/storage/store.rs b/rust/agama-lib/src/storage/store.rs index 82b25a5c8f..ae5b68cc49 100644 --- a/rust/agama-lib/src/storage/store.rs +++ b/rust/agama-lib/src/storage/store.rs @@ -1,7 +1,6 @@ //! Implements the store for the storage settings. -use super::StorageClient; -use super::StorageSettings; +use super::{StorageClient, StorageSettings}; use crate::error::ServiceError; use zbus::Connection;