diff --git a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
index f5c91d7edb..cc0e530113 100644
--- a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
+++ b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
@@ -52,6 +52,10 @@
+
+
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Storage1.doc.xml b/doc/dbus/org.opensuse.Agama.Storage1.doc.xml
index a848f9f8c0..a4ac2393f4 100644
--- a/doc/dbus/org.opensuse.Agama.Storage1.doc.xml
+++ b/doc/dbus/org.opensuse.Agama.Storage1.doc.xml
@@ -55,6 +55,31 @@
-->
+
+
+
+
+
+
+
diff --git a/doc/storage_proposal_from_profile.md b/doc/storage_proposal_from_profile.md
new file mode 100644
index 0000000000..600abc9d7c
--- /dev/null
+++ b/doc/storage_proposal_from_profile.md
@@ -0,0 +1,267 @@
+# Calculating a proposal from a profile
+
+The Agama proposal can be calculated either from a very detailed JSON profile or from a "sparse
+profile". The information not provided by the profile is automatically inferred (solved) by Agama.
+Several layers are involved in the process of obtaining the final storage config used by the Agama
+proposal, as shown in the following diagram:
+
+```
+JSON profile ------------> JSON profile (solved) ------------> Storage::Config ------------> Storage::Config (solved)
+ | | |
+ (JSON solver) (config conversion) (config solver)
+```
+
+## JSON solver
+
+The JSON profile provides the *generator* concept. A *generator* allows indicating what volumes to
+create without explicitly defining them. The JSON solver (`Agama::Storage::JSONConfigSolver` class)
+takes care of replacing the volume generator by the corresponding JSON volumes according to the
+product.
+
+For example, a JSON profile like this:
+
+~~~json
+{
+ "drives": [
+ {
+ "partitions": [ { "generate": "default" } ]
+ }
+ ]
+}
+~~~
+
+would be solved to something like:
+
+~~~json
+{
+ "drives": [
+ {
+ "partitions": [
+ { "filesystem": { "path": "/" } },
+ { "filesystem": { "path": "swap" } }
+ ]
+ }
+ ]
+}
+~~~
+
+The volumes are solved with their very minimum information (i.e., a mount path). The resulting
+solved JSON is used for getting the storage config object.
+
+## Config conversion
+
+The class `Agama::Storage::ConfigConversions::FromJSON` takes a solved JSON profile and generates a
+`Agama::Storage::Config` object. The resulting config only contains the information provided by the
+profile. For example, if the profile does not specify a file system type for a partition, then the
+config would not have any information about the file system to use for such a partition.
+
+If something is not provided by the profile (e.g., "boot", "size", "filesystem"), then the config
+marks that values as default ones. For example:
+
+```json
+{
+ "drives": [
+ {
+ "partitions": [
+ { "filesystem": { "path": "/" } }
+ ]
+ }
+ ]
+}
+```
+
+generates a config with these default values:
+
+```ruby
+config.boot.device.default? #=> true
+
+partition = config.drives.first.partitions.first
+partition.size.default? #=> true
+partition.filesystem.type.default? #=> true
+```
+
+The configs set as default and any other missing value have to be solved to a value provided by the
+product definition.
+
+## Config solver
+
+The config solver (`Agama::Storage::ConfigSolver` class) assigns a value to all the unknown
+properties of a config object. As result, the config object is totally complete and ready to be used
+by the agama proposal.
+
+### How sizes are solved
+
+A volume size in the profile:
+
+* Can be totally omitted.
+* Can omit the max size.
+* Can use "current" as value for min and/or max.
+
+Let's see each case.
+
+#### Omitting size completely
+
+```json
+"partitions": [
+ { "filesystem": { "path": "/" } }
+]
+```
+
+In this case, the config conversion would generate something like:
+
+```ruby
+partition.size.default? #=> true
+partition.size.min #=> nil
+partition.size.max #=> nil
+```
+
+If the size is default, then the config solver always assigns a value for `#min` and `#max`
+according to the product definition and ignoring the current values assigned to `#min` and `#max`.
+The solver takes into account the mount path, the fallback devices and swap config in order to set
+the proper sizes.
+
+If the size is default and the volume already exists, then the solver sets the current size of the
+volume to both `#min` and `#max` sizes.
+
+#### Omitting the max size
+
+```json
+"partitions": [
+ {
+ "size": { "min": "10 GiB" },
+ "filesystem": { "path": "/" }
+ }
+]
+```
+
+The config conversion generates:
+
+```ruby
+partition.size.default? #=> false
+partition.size.min #=> Y2Storage::DiskSize.GiB(10)
+partition.size.max #=> Y2Storage::DiskSize.Unlimited
+```
+
+Note that `#max` is set to unlimited when it does not appear in the profile. In this case, nothing
+has to be solved because both `#min` and `#max` have a value.
+
+#### Using "current"
+
+Both *min* and *max* sizes admit "current" as a valid size value in the JSON profile. The "current"
+value stands for the current size of the volume. Using "current" is useful for growing or shrinking
+a device.
+
+The config conversion knows nothing about the current size of a volume, so it simply replaces
+"current" values by `nil`.
+
+For example, in this case:
+
+```json
+"partitions": [
+ {
+ "search": "/dev/vda1",
+ "size": { "min": "current" },
+ "filesystem": { "path": "/" }
+ }
+]
+```
+
+the config conversion generates a size with `nil` for `#min`:
+
+```ruby
+partition.size.default? #=> false
+partition.size.min #=> nil
+partition.size.max #=> Y2Storage::DiskSize.Unlimited
+```
+
+The config solver replaces the `nil` sizes by the device size. In the example before, let's say that
+/dev/vda1 has 10 GiB, so the resulting config would be:
+
+```ruby
+partition.size.default? #=> false
+partition.size.min #=> Y2Storage::DiskSize.GiB(10)
+partition.size.max #=> Y2Storage::DiskSize.Unlimited
+```
+
+##### Use case: growing a device
+
+```json
+"partitions": [
+ {
+ "search": "/dev/vda1",
+ "size": { "min": "current" },
+ "filesystem": { "path": "/" }
+ }
+]
+```
+
+```ruby
+partition.size.default? #=> false
+partition.size.min #=> Y2Storage::DiskSize.GiB(10)
+partition.size.max #=> Y2Storage::DiskSize.Unlimited
+```
+
+##### Use case: shrinking a device
+
+```json
+"partitions": [
+ {
+ "search": "/dev/vda1",
+ "size": { "min": 0, "max": "current" },
+ "filesystem": { "path": "/" }
+ }
+]
+```
+
+```ruby
+partition.size.default? #=> false
+partition.size.min #=> 0
+partition.size.max #=> Y2Storage::DiskSize.GiB(10)
+```
+
+##### Use case: keeping a device size
+
+Note that this is equivalent to omitting the size.
+
+```json
+"partitions": [
+ {
+ "search": "/dev/vda1",
+ "size": { "min": "current", "max": "current" },
+ "filesystem": { "path": "/" }
+ }
+]
+```
+
+```ruby
+partition.size.default? #=> false
+partition.size.min #=> Y2Storage::DiskSize.GiB(10)
+partition.size.max #=> Y2Storage::DiskSize.GiB(10)
+```
+
+##### Use case: fallback for not found devices
+
+A profile can specify an "advanced search" to indicate that a volume has to be created if it is not
+found in the system.
+
+```json
+"partitions": [
+ {
+ "search": {
+ "condition": { "name": "/dev/vda1" },
+ "ifNotFound": "create"
+ },
+ "size": { "min": "current" },
+ "filesystem": { "path": "/" }
+ }
+]
+```
+
+If the device does not exist, then "current" cannot be replaced by any device size. In this case,
+the config solver uses the default size defined by the product as fallback for "current".
+
+```ruby
+partition.size.default? #=> false
+partition.size.min #=> Y2Storage::DiskSize.GiB(15)
+partition.size.max #=> Y2Storage::DiskSize.Unlimited
+```
diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs
index 14af4ae594..cfbf228609 100644
--- a/rust/agama-lib/src/storage/client.rs
+++ b/rust/agama-lib/src/storage/client.rs
@@ -154,6 +154,14 @@ impl<'a> StorageClient<'a> {
Ok(settings)
}
+ /// Set the storage config according to the JSON schema
+ pub async fn set_config_model(&self, model: Box) -> Result {
+ Ok(self
+ .storage_proxy
+ .set_config_model(serde_json::to_string(&model).unwrap().as_str())
+ .await?)
+ }
+
/// Get the storage config model according to the JSON schema
pub async fn get_config_model(&self) -> Result, ServiceError> {
let serialized_config_model = self.storage_proxy.get_config_model().await?;
diff --git a/rust/agama-lib/src/storage/proxies/storage1.rs b/rust/agama-lib/src/storage/proxies/storage1.rs
index 052e91ebf3..67e31c247d 100644
--- a/rust/agama-lib/src/storage/proxies/storage1.rs
+++ b/rust/agama-lib/src/storage/proxies/storage1.rs
@@ -62,6 +62,9 @@ pub trait Storage1 {
/// Get the current storage config according to the JSON schema
fn get_config(&self) -> zbus::Result;
+ /// Set the storage config model according to the JSON schema
+ fn set_config_model(&self, model: &str) -> zbus::Result;
+
/// Get the storage config model according to the JSON schema
fn get_config_model(&self) -> zbus::Result;
diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs
index 442f4002e5..26c0b60bef 100644
--- a/rust/agama-server/src/storage/web.rs
+++ b/rust/agama-server/src/storage/web.rs
@@ -114,7 +114,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result>,
+ Json(settings): Json,
+) -> Result, Error> {
+ let _status: u32 = state
+ .client
+ .set_config(settings)
+ .await
+ .map_err(Error::Service)?;
+ Ok(Json(()))
+}
+
/// Returns the storage config model.
///
/// * `state` : service state.
@@ -181,27 +207,28 @@ async fn get_config_model(
Ok(Json(config_model))
}
-/// Sets the storage configuration.
+/// Sets the storage config model.
///
/// * `state`: service state.
-/// * `config`: storage configuration.
+/// * `config_model`: storage config model.
#[utoipa::path(
put,
- path = "/config",
+ request_body = String,
+ path = "/config_model",
context_path = "/api/storage",
- operation_id = "set_storage_config",
+ operation_id = "set_storage_config_model",
responses(
- (status = 200, description = "Set the storage configuration"),
+ (status = 200, description = "Set the storage config model"),
(status = 400, description = "The D-Bus service could not perform the action")
)
)]
-async fn set_config(
+async fn set_config_model(
State(state): State>,
- Json(settings): Json,
+ Json(model): Json>,
) -> Result, Error> {
let _status: u32 = state
.client
- .set_config(settings)
+ .set_config_model(model)
.await
.map_err(Error::Service)?;
Ok(Json(()))
diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb
index c6742718eb..ccc8616691 100644
--- a/service/lib/agama/dbus/storage/manager.rb
+++ b/service/lib/agama/dbus/storage/manager.rb
@@ -119,6 +119,18 @@ def recover_config
JSON.pretty_generate(json)
end
+ # Applies the given serialized config model according to the JSON schema.
+ #
+ # @param serialized_model [String] Serialized storage config model.
+ # @return [Integer] 0 success; 1 error
+ def apply_config_model(serialized_model)
+ logger.info("Setting storage config model from D-Bus: #{serialized_model}")
+
+ model_json = JSON.parse(serialized_model, symbolize_names: true)
+ proposal.calculate_from_model(model_json)
+ proposal.success? ? 0 : 1
+ end
+
# Gets and serializes the storage config model.
#
# @return [String]
@@ -148,6 +160,9 @@ def deprecated_system
busy_while { apply_config(serialized_config) }
end
dbus_method(:GetConfig, "out serialized_config:s") { recover_config }
+ dbus_method(:SetConfigModel, "in serialized_model:s, out result:u") do |serialized_model|
+ busy_while { apply_config_model(serialized_model) }
+ end
dbus_method(:GetConfigModel, "out serialized_model:s") { recover_model }
dbus_method(:Install) { install }
dbus_method(:Finish) { finish }
diff --git a/service/lib/agama/storage/config.rb b/service/lib/agama/storage/config.rb
index 928c787b30..dbc249223d 100644
--- a/service/lib/agama/storage/config.rb
+++ b/service/lib/agama/storage/config.rb
@@ -25,7 +25,10 @@
module Agama
module Storage
- # Settings used to calculate an storage proposal.
+ # Config used to calculate an storage proposal.
+ #
+ # See doc/storage_proposal_from_profile.md for a complete description of how the config is
+ # generated from a profile.
class Config
include Copyable
diff --git a/service/lib/agama/storage/config_conversions.rb b/service/lib/agama/storage/config_conversions.rb
index 1bcc863703..65233dde94 100644
--- a/service/lib/agama/storage/config_conversions.rb
+++ b/service/lib/agama/storage/config_conversions.rb
@@ -20,6 +20,7 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/from_json"
+require "agama/storage/config_conversions/from_model"
require "agama/storage/config_conversions/to_json"
require "agama/storage/config_conversions/to_model"
diff --git a/service/lib/agama/storage/config_conversions/from_json.rb b/service/lib/agama/storage/config_conversions/from_json.rb
index 86cddab38a..61c1f66127 100644
--- a/service/lib/agama/storage/config_conversions/from_json.rb
+++ b/service/lib/agama/storage/config_conversions/from_json.rb
@@ -27,6 +27,9 @@ module Agama
module Storage
module ConfigConversions
# Config conversion from JSON hash according to schema.
+ #
+ # See doc/storage_proposal_from_profile.md for a complete description of how the config is
+ # generated from a profile.
class FromJSON
# @param config_json [Hash]
# @param default_paths [Array] Default paths of the product.
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/base.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/base.rb
index e3e449f0f6..63c2076bc5 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/base.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/base.rb
@@ -25,15 +25,17 @@ module ConfigConversions
module FromJSONConversions
# Base class for conversions from JSON hash according to schema.
class Base
+ # @param config_json [Hash]
def initialize(config_json)
@config_json = config_json
end
# Performs the conversion from Hash according to the JSON schema.
#
- # @param config [Object] A {Config} or any of its configs from {Storage::Configs}.
# @return [Object] A {Config} or any its configs from {Storage::Configs}.
- def convert(config)
+ def convert
+ config = default_config
+
conversions.each do |property, value|
next if value.nil?
@@ -45,8 +47,16 @@ def convert(config)
private
+ # @return [Hash]
attr_reader :config_json
+ # Default config object (defined by derived classes).
+ #
+ # @return [Object]
+ def default_config
+ raise "Undefined default config"
+ end
+
# Values to apply to the config.
#
# @return [Hash] e.g., { name: "/dev/vda" }.
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/boot.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/boot.rb
index e2d2096abf..fa120ab880 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/boot.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/boot.rb
@@ -28,16 +28,16 @@ module ConfigConversions
module FromJSONConversions
# Boot conversion from JSON hash according to schema.
class Boot < Base
- # @see Base#convert
- # @return [Configs::Boot]
- def convert
- super(Configs::Boot.new)
- end
-
private
alias_method :boot_json, :config_json
+ # @see Base
+ # @return [Configs::Boot]
+ def default_config
+ Configs::Boot.new
+ end
+
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/btrfs.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/btrfs.rb
index f658287b74..df02e04f8b 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/btrfs.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/btrfs.rb
@@ -28,16 +28,16 @@ module ConfigConversions
module FromJSONConversions
# Btrfs conversion from JSON hash according to schema.
class Btrfs < Base
- # @see Base#convert
- # @return [Configs::Btrfs]
- def convert
- super(Configs::Btrfs.new)
- end
-
private
alias_method :btrfs_json, :config_json
+ # @see Base
+ # @return [Configs::Btrfs]
+ def default_config
+ Configs::Btrfs.new
+ end
+
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/config.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/config.rb
index 4e86fef679..46b0ea71fd 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/config.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/config.rb
@@ -31,14 +31,14 @@ module ConfigConversions
module FromJSONConversions
# Config conversion from JSON hash according to schema.
class Config < Base
- # @see Base#convert
+ private
+
+ # @see Base
# @return [Config]
- def convert
- super(Storage::Config.new)
+ def default_config
+ Storage::Config.new
end
- private
-
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb
index 8fd661dd6c..dacb84bc2b 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb
@@ -33,22 +33,22 @@ module ConfigConversions
module FromJSONConversions
# Drive conversion from JSON hash according to schema.
class Drive < Base
+ private
+
include WithSearch
include WithEncryption
include WithFilesystem
include WithPtableType
include WithPartitions
- # @see Base#convert
+ alias_method :drive_json, :config_json
+
+ # @see Base
# @return [Configs::Drive]
- def convert
- super(Configs::Drive.new)
+ def default_config
+ Configs::Drive.new
end
- private
-
- alias_method :drive_json, :config_json
-
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb
index 9f75003ee7..e059b32478 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb
@@ -30,16 +30,16 @@ module ConfigConversions
module FromJSONConversions
# Encryption conversion from JSON hash according to schema.
class Encryption < Base
- # @see Base#convert
- # @return [Configs::Encryption]
- def convert
- super(Configs::Encryption.new)
- end
-
private
alias_method :encryption_json, :config_json
+ # @see Base
+ # @return [Configs::Encryption]
+ def default_config
+ Configs::Encryption.new
+ end
+
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem.rb
index b5aac14056..537416374a 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem.rb
@@ -30,16 +30,16 @@ module ConfigConversions
module FromJSONConversions
# Filesystem conversion from JSON hash according to schema.
class Filesystem < Base
- # @see Base#convert
- # @return [Configs::Filesystem]
- def convert
- super(Configs::Filesystem.new)
- end
-
private
alias_method :filesystem_json, :config_json
+ # @see Base
+ # @return [Configs::Filesystem]
+ def default_config
+ Configs::Filesystem.new
+ end
+
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem_type.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem_type.rb
index 995604a9bc..4872c48678 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem_type.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/filesystem_type.rb
@@ -31,16 +31,16 @@ module ConfigConversions
module FromJSONConversions
# Filesystem type conversion from JSON hash according to schema.
class FilesystemType < Base
- # @see Base#convert
- # @return [Configs::FilesystemType]
- def convert
- super(Configs::FilesystemType.new)
- end
-
private
alias_method :filesystem_type_json, :config_json
+ # @see Base
+ # @return [Configs::FilesystemType]
+ def default_config
+ Configs::FilesystemType.new
+ end
+
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb
index cbd98c3e34..ea0b2be263 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb
@@ -32,18 +32,18 @@ module ConfigConversions
module FromJSONConversions
# Logical volume conversion from JSON hash according to schema.
class LogicalVolume < Base
+ private
+
include WithEncryption
include WithFilesystem
include WithSize
- # @see Base#convert
+ # @see Base
# @return [Configs::LogicalVolume]
- def convert
- super(Configs::LogicalVolume.new)
+ def default_config
+ Configs::LogicalVolume.new
end
- private
-
alias_method :logical_volume_json, :config_json
# @see Base#conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb
index 00823a0c45..5da159eb46 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb
@@ -33,19 +33,19 @@ module ConfigConversions
module FromJSONConversions
# Partition conversion from JSON hash according to schema.
class Partition < Base
+ private
+
include WithSearch
include WithEncryption
include WithFilesystem
include WithSize
- # @see Base#convert
+ # @see Base
# @return [Configs::Partition]
- def convert
- super(Configs::Partition.new)
+ def default_config
+ Configs::Partition.new
end
- private
-
alias_method :partition_json, :config_json
# @see Base#conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/search.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/search.rb
index e742de66a2..c221fd8f77 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/search.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/search.rb
@@ -28,14 +28,14 @@ module ConfigConversions
module FromJSONConversions
# Search conversion from JSON hash according to schema.
class Search < Base
- # @see Base#convert
+ private
+
+ # @see Base
# @return [Configs::Search]
- def convert
- super(Configs::Search.new)
+ def default_config
+ Configs::Search.new
end
- private
-
# Reserved search value meaning 'match all devices or ignore the section'.
#
# { search: "*" } is a shortcut for { search: { ifNotFound: "skip" } }.
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/size.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/size.rb
index c1cf93881b..9291a93546 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/size.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/size.rb
@@ -29,16 +29,16 @@ module ConfigConversions
module FromJSONConversions
# Size conversion from JSON hash according to schema.
class Size < Base
- # @see Base#convert
- # @return [Configs::Size]
- def convert
- super(Configs::Size.new)
- end
-
private
alias_method :size_json, :config_json
+ # @see Base
+ # @return [Configs::Size]
+ def default_config
+ Configs::Size.new
+ end
+
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb
index f1516456b7..8770d41a06 100644
--- a/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb
+++ b/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb
@@ -30,16 +30,16 @@ module ConfigConversions
module FromJSONConversions
# Volume group conversion from JSON hash according to schema.
class VolumeGroup < Base
- # @see Base#convert
- # @return [Configs::VolumeGroup]
- def convert
- super(Configs::VolumeGroup.new)
- end
-
private
alias_method :volume_group_json, :config_json
+ # @see Base
+ # @return [Configs::VolumeGroup]
+ def default_config
+ Configs::VolumeGroup.new
+ end
+
# @see Base#conversions
# @return [Hash]
def conversions
diff --git a/service/lib/agama/storage/config_conversions/from_model.rb b/service/lib/agama/storage/config_conversions/from_model.rb
new file mode 100644
index 0000000000..e48bc8bfa7
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model.rb
@@ -0,0 +1,50 @@
+# 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/config"
+require "agama/storage/config_conversions/from_model_conversions/config"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ # Config conversion from model according to the JSON schema.
+ class FromModel
+ # @param model_json [Hash]
+ def initialize(model_json)
+ @model_json = model_json
+ end
+
+ # Performs the conversion from model according to the JSON schema.
+ #
+ # @return [Storage::Config]
+ def convert
+ # TODO: Raise error if model_json does not match the JSON schema.
+ FromModelConversions::Config.new(model_json).convert
+ end
+
+ private
+
+ # @return [Hash]
+ attr_reader :model_json
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions.rb b/service/lib/agama/storage/config_conversions/from_model_conversions.rb
new file mode 100644
index 0000000000..267a567fd5
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions.rb
@@ -0,0 +1,38 @@
+# 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/config_conversions/from_model_conversions/config"
+require "agama/storage/config_conversions/from_model_conversions/drive"
+require "agama/storage/config_conversions/from_model_conversions/filesystem"
+require "agama/storage/config_conversions/from_model_conversions/filesystem_type"
+require "agama/storage/config_conversions/from_model_conversions/partition"
+require "agama/storage/config_conversions/from_model_conversions/search"
+require "agama/storage/config_conversions/from_model_conversions/size"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ # Conversions from model according to the JSON schema.
+ module FromModelConversions
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/base.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/base.rb
new file mode 100644
index 0000000000..9b7fe83747
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/base.rb
@@ -0,0 +1,70 @@
+# 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.
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Base class for conversions from model according to the JSON schema.
+ class Base
+ # @param model_json [Hash]
+ def initialize(model_json)
+ @model_json = model_json
+ end
+
+ # Performs the conversion from model according to the JSON schema.
+ #
+ # @return [Object] A {Config} or any its configs from {Storage::Configs}.
+ def convert
+ config = default_config
+
+ conversions.each do |property, value|
+ next if value.nil?
+
+ config.public_send("#{property}=", value)
+ end
+
+ config
+ end
+
+ private
+
+ # @return [Hash]
+ attr_reader :model_json
+
+ # Default config object (defined by derived classes).
+ #
+ # @return [Object]
+ def default_config
+ raise "Undefined default config"
+ end
+
+ # Values to apply to the config.
+ #
+ # @return [Hash] e.g., { name: "/dev/vda" }.
+ def conversions
+ {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/config.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/config.rb
new file mode 100644
index 0000000000..29a0af95cd
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/config.rb
@@ -0,0 +1,65 @@
+# 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/config_conversions/from_model_conversions/base"
+require "agama/storage/config_conversions/from_model_conversions/drive"
+require "agama/storage/config"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Config conversion from model according to the JSON schema.
+ class Config < Base
+ private
+
+ # @see Base
+ # @return [Storage::Config]
+ def default_config
+ Storage::Config.new
+ end
+
+ # @see Base#conversions
+ # @return [Hash]
+ def conversions
+ {
+ drives: convert_drives
+ }
+ end
+
+ # @return [Array, nil]
+ def convert_drives
+ drive_models = model_json[:drives]
+ return unless drive_models
+
+ drive_models.map { |d| convert_drive(d) }
+ end
+
+ # @param drive_model [Hash]
+ # @return [Configs::Drive]
+ def convert_drive(drive_model)
+ FromModelConversions::Drive.new(drive_model).convert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/drive.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/drive.rb
new file mode 100644
index 0000000000..97bedf6cc0
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/drive.rb
@@ -0,0 +1,65 @@
+# 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/config_conversions/from_model_conversions/base"
+require "agama/storage/config_conversions/from_model_conversions/with_filesystem"
+require "agama/storage/config_conversions/from_model_conversions/with_partitions"
+require "agama/storage/config_conversions/from_model_conversions/with_ptable_type"
+require "agama/storage/config_conversions/from_model_conversions/with_search"
+require "agama/storage/configs/drive"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Drive conversion from model according to the JSON schema.
+ class Drive < Base
+ private
+
+ include WithFilesystem
+ include WithPtableType
+ include WithPartitions
+ include WithSearch
+
+ alias_method :drive_model, :model_json
+
+ # @see Base
+ # @return [Configs::Drive]
+ def default_config
+ Configs::Drive.new
+ end
+
+ # @see Base#conversions
+ # @return [Hash]
+ def conversions
+ {
+ search: convert_search,
+ alias: drive_model[:alias],
+ filesystem: convert_filesystem,
+ ptable_type: convert_ptable_type,
+ partitions: convert_partitions
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem.rb
new file mode 100644
index 0000000000..d30091a985
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem.rb
@@ -0,0 +1,60 @@
+# 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/config_conversions/from_model_conversions/base"
+require "agama/storage/config_conversions/from_model_conversions/filesystem_type"
+require "agama/storage/configs/filesystem"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Filesystem conversion from model according to the JSON schema.
+ class Filesystem < Base
+ private
+
+ # @see Base
+ # @return [Configs::Filesystem]
+ def default_config
+ Configs::Filesystem.new
+ end
+
+ # @see Base#conversions
+ # @return [Hash]
+ def conversions
+ {
+ path: model_json[:mountPath],
+ type: convert_type
+ }
+ end
+
+ # @return [Configs::FilesystemType, nil]
+ def convert_type
+ filesystem_model = model_json[:filesystem]
+ return if filesystem_model.nil?
+
+ FromModelConversions::FilesystemType.new(filesystem_model).convert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem_type.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem_type.rb
new file mode 100644
index 0000000000..04a7565985
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem_type.rb
@@ -0,0 +1,71 @@
+# 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/config_conversions/from_model_conversions/base"
+require "agama/storage/configs/btrfs"
+require "agama/storage/configs/filesystem_type"
+require "y2storage/filesystems/type"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Filesystem type conversion from model according to the JSON schema.
+ class FilesystemType < Base
+ private
+
+ alias_method :filesystem_model, :model_json
+
+ # @see Base
+ # @return [Configs::FilesystemType]
+ def default_config
+ Configs::FilesystemType.new
+ end
+
+ # @see Base#conversions
+ # @return [Hash]
+ def conversions
+ {
+ default: filesystem_model[:default],
+ fs_type: convert_type,
+ btrfs: convert_btrfs
+ }
+ end
+
+ # @return [Y2Storage::Filesystems::Type, nil]
+ def convert_type
+ value = filesystem_model[:type]
+ return unless value
+
+ Y2Storage::Filesystems::Type.find(value.to_sym)
+ end
+
+ # @return [Configs::Btrfs, nil]
+ def convert_btrfs
+ return unless filesystem_model[:type] == "btrfs"
+
+ Configs::Btrfs.new.tap { |c| c.snapshots = filesystem_model[:snapshots] }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/partition.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/partition.rb
new file mode 100644
index 0000000000..bbf1325d40
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/partition.rb
@@ -0,0 +1,74 @@
+# 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/config_conversions/from_model_conversions/base"
+require "agama/storage/config_conversions/from_model_conversions/with_filesystem"
+require "agama/storage/config_conversions/from_model_conversions/with_search"
+require "agama/storage/config_conversions/from_model_conversions/with_size"
+require "agama/storage/configs/partition"
+require "y2storage/partition_id"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Partition conversion from model according to the JSON schema.
+ class Partition < Base
+ private
+
+ include WithSearch
+ include WithFilesystem
+ include WithSize
+
+ # @see Base
+ # @return [Configs::Partition]
+ def default_config
+ Configs::Partition.new
+ end
+
+ alias_method :partition_model, :model_json
+
+ # @see Base#conversions
+ # @return [Hash]
+ def conversions
+ {
+ search: convert_search,
+ alias: partition_model[:alias],
+ filesystem: convert_filesystem,
+ size: convert_size,
+ id: convert_id,
+ delete: partition_model[:delete],
+ delete_if_needed: partition_model[:deleteIfNeeded]
+ }
+ end
+
+ # @return [Y2Storage::PartitionId, nil]
+ def convert_id
+ value = partition_model[:id]
+ return unless value
+
+ Y2Storage::PartitionId.find(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/size.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/size.rb
new file mode 100644
index 0000000000..9805acb0bf
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/size.rb
@@ -0,0 +1,77 @@
+# 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/config_conversions/from_model_conversions/base"
+require "agama/storage/configs/size"
+require "y2storage/disk_size"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Size conversion from model according to the JSON schema.
+ class Size < Base
+ private
+
+ alias_method :size_model, :model_json
+
+ # @see Base
+ # @return [Configs::Size]
+ def default_config
+ Configs::Size.new
+ end
+
+ # @see Base#conversions
+ # @return [Hash]
+ def conversions
+ {
+ default: size_model[:default],
+ min: convert_min_size,
+ max: convert_max_size
+ }
+ end
+
+ # @return [Y2Storage::DiskSize, nil]
+ def convert_min_size
+ value = size_model[:min]
+ return unless value
+
+ disk_size(value)
+ end
+
+ # @return [Y2Storage::DiskSize]
+ def convert_max_size
+ value = size_model[:max]
+ return Y2Storage::DiskSize.unlimited unless value
+
+ disk_size(value)
+ end
+
+ # @param value [Integer]
+ # @return [Y2Storage::DiskSize]
+ def disk_size(value)
+ Y2Storage::DiskSize.new(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/with_filesystem.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/with_filesystem.rb
new file mode 100644
index 0000000000..46853c16e1
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/with_filesystem.rb
@@ -0,0 +1,40 @@
+# 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/config_conversions/from_model_conversions/filesystem"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Mixin for filesystem conversion.
+ module WithFilesystem
+ # @return [Configs::Filesystem, nil]
+ def convert_filesystem
+ return unless model_json[:mountPath] || model_json[:filesystem]
+
+ FromModelConversions::Filesystem.new(model_json).convert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/with_partitions.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/with_partitions.rb
new file mode 100644
index 0000000000..da8c4bd5f7
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/with_partitions.rb
@@ -0,0 +1,114 @@
+# 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/config_conversions/from_model_conversions/partition"
+require "agama/storage/configs/partition"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Mixin for partitions conversion.
+ module WithPartitions
+ # @return [Array]
+ def convert_partitions
+ space_policy = model_json[:spacePolicy]
+
+ case space_policy
+ when "keep"
+ used_partition_configs
+ when "delete"
+ [used_partition_configs, delete_all_partition_config].flatten
+ when "resize"
+ [used_partition_configs, resize_all_partition_config].flatten
+ else
+ partition_configs
+ end
+ end
+
+ # @param partition_model [Hash]
+ # @return [Configs::Partition]
+ def convert_partition(partition_model)
+ FromModelConversions::Partition.new(partition_model).convert
+ end
+
+ # @return [Array]
+ def partition_configs
+ partitions.map { |p| convert_partition(p) }
+ end
+
+ # @return [Array]
+ def used_partition_configs
+ used_partitions.map { |p| convert_partition(p) }
+ end
+
+ # @return [Array]
+ def partitions
+ model_json[:partitions] || []
+ end
+
+ # @return [Array]
+ def used_partitions
+ partitions.select { |p| used_partition?(p) }
+ end
+
+ # @param partition_model [Hash]
+ # @return [Boolean]
+ def used_partition?(partition_model)
+ new_partition?(partition_model) || reused_partition?(partition_model)
+ end
+
+ # @param partition_model [Hash]
+ # @return [Boolean]
+ def new_partition?(partition_model)
+ partition_model[:name].nil? &&
+ !partition_model[:delete] &&
+ !partition_model[:deleteIfNeeded]
+ end
+
+ # @param partition_model [Hash]
+ # @return [Boolean]
+ def reused_partition?(partition_model)
+ # TODO: improve check by ensuring the alias is referenced by other device.
+ any_usage = partition_model[:mountPath] ||
+ partition_model[:filesystem] ||
+ partition_model[:alias]
+
+ any_usage &&
+ partition_model[:name] &&
+ !partition_model[:delete] &&
+ !partition_model[:deleteIfNeeded]
+ end
+
+ # @return [Configs::Partition]
+ def delete_all_partition_config
+ Configs::Partition.new_for_delete_all
+ end
+
+ # @return [Configs::Partition]
+ def resize_all_partition_config
+ Configs::Partition.new_for_shrink_any_if_needed
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/with_ptable_type.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/with_ptable_type.rb
new file mode 100644
index 0000000000..7789a75280
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/with_ptable_type.rb
@@ -0,0 +1,41 @@
+# 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 "y2storage/partition_tables/type"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Mixin for partition table type conversion.
+ module WithPtableType
+ # @return [Y2Storage::PartitionTables::Type, nil]
+ def convert_ptable_type
+ value = model_json[:ptableType]
+ return unless value
+
+ Y2Storage::PartitionTables::Type.find(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/with_search.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/with_search.rb
new file mode 100644
index 0000000000..a8d29f09be
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/with_search.rb
@@ -0,0 +1,41 @@
+# 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/configs/search"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Mixin for search conversion.
+ module WithSearch
+ # @return [Configs::Search, nil]
+ def convert_search
+ name = model_json[:name]
+ return unless name
+
+ Configs::Search.new.tap { |c| c.name = name }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/with_size.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/with_size.rb
new file mode 100644
index 0000000000..23e7a3686c
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/from_model_conversions/with_size.rb
@@ -0,0 +1,44 @@
+# 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/config_conversions/from_model_conversions/size"
+require "agama/storage/configs/size"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module FromModelConversions
+ # Mixin for size conversion.
+ module WithSize
+ # @return [Configs::Size, nil]
+ def convert_size
+ return Configs::Size.new_for_shrink_if_needed if model_json[:resizeIfNeeded]
+
+ size_model = model_json[:size]
+ return if size_model.nil?
+
+ FromModelConversions::Size.new(size_model).convert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/base.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/base.rb
index e96efd3891..32f68e4855 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/base.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/base.rb
@@ -25,22 +25,6 @@ module ConfigConversions
module ToJSONConversions
# Base class for conversions to JSON hash according to schema.
class Base
- # Defines the expected config type to perform the conversion.
- #
- # @raise If a subclass does not defines a type.
- # @return [Class]
- def self.config_type
- raise "Undefined config type"
- end
-
- # @param config [Object] The config type is provided by the {.config_type} method.
- def initialize(config)
- type = self.class.config_type
- raise "Invalid config (#{type} expected): #{config}" unless config.is_a?(type)
-
- @config = config
- end
-
# Performs the conversion to Hash according to the JSON schema.
#
# @return [Hash, nil]
@@ -58,7 +42,7 @@ def convert
private
- # @return [Object] The config type is provided by the {.config_type} method.
+ # @return [Object] See {#initialize}.
attr_reader :config
# Values to generate the JSON.
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/boot.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/boot.rb
index 88a96e3716..698f3f09fe 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/boot.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/boot.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/to_json_conversions/base"
-require "agama/storage/configs/logical_volume"
module Agama
module Storage
@@ -28,9 +27,10 @@ module ConfigConversions
module ToJSONConversions
# Boot conversion to JSON hash according to schema.
class Boot < Base
- # @see Base
- def self.config_type
- Configs::Boot
+ # @param config [Configs::Boot]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/config.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/config.rb
index 2eb2017e7c..21114a2d2c 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/config.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/config.rb
@@ -19,7 +19,6 @@
# 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/config"
require "agama/storage/config_conversions/to_json_conversions/base"
require "agama/storage/config_conversions/to_json_conversions/boot"
require "agama/storage/config_conversions/to_json_conversions/drive"
@@ -31,9 +30,10 @@ module ConfigConversions
module ToJSONConversions
# Config conversion to JSON hash according to schema.
class Config < Base
- # @see Base
- def self.config_type
- Storage::Config
+ # @param config [Storage::Config]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb
index 5a6f9bf1b1..3ba6c139e4 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb
@@ -25,7 +25,6 @@
require "agama/storage/config_conversions/to_json_conversions/with_partitions"
require "agama/storage/config_conversions/to_json_conversions/with_ptable_type"
require "agama/storage/config_conversions/to_json_conversions/with_search"
-require "agama/storage/configs/drive"
module Agama
module Storage
@@ -39,9 +38,10 @@ class Drive < Base
include WithPtableType
include WithPartitions
- # @see Base
- def self.config_type
- Configs::Drive
+ # @param config [Configs::Drive]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb
index d12692a8f8..bf4d26a310 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb
@@ -21,7 +21,6 @@
require "agama/storage/config_conversions/to_json_conversions/base"
require "agama/storage/config_conversions/to_json_conversions/encryption_properties"
-require "agama/storage/configs/encryption"
module Agama
module Storage
@@ -29,9 +28,10 @@ module ConfigConversions
module ToJSONConversions
# Encryption conversion to JSON hash according to schema.
class Encryption < Base
- # @see Base
- def self.config_type
- Configs::Encryption
+ # @param config [Configs::Encryption]
+ def initialize(config)
+ super()
+ @config = config
end
# @see Base#convert
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/encryption_properties.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption_properties.rb
index 44ef0cfd21..66f1c8c2c1 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/encryption_properties.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption_properties.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/to_json_conversions/base"
-require "agama/storage/configs/encryption"
module Agama
module Storage
@@ -28,9 +27,10 @@ module ConfigConversions
module ToJSONConversions
# Encryption properties conversion to JSON hash according to schema.
class EncryptionProperties < Base
- # @see Base
- def self.config_type
- Configs::Encryption
+ # @param config [Configs::Encryption]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/filesystem.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/filesystem.rb
index 45f67d7d18..9cfaaf0628 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/filesystem.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/filesystem.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/to_json_conversions/base"
-require "agama/storage/configs/filesystem"
module Agama
module Storage
@@ -28,9 +27,10 @@ module ConfigConversions
module ToJSONConversions
# Filesystem conversion to JSON hash according to schema.
class Filesystem < Base
- # @see Base
- def self.config_type
- Configs::Filesystem
+ # @param config [Configs::Filesystem]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb
index 531bcd2054..bb3c5ce60c 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb
@@ -23,7 +23,6 @@
require "agama/storage/config_conversions/to_json_conversions/with_encryption"
require "agama/storage/config_conversions/to_json_conversions/with_filesystem"
require "agama/storage/config_conversions/to_json_conversions/with_size"
-require "agama/storage/configs/logical_volume"
module Agama
module Storage
@@ -35,9 +34,10 @@ class LogicalVolume < Base
include WithFilesystem
include WithSize
- # @see Base
- def self.config_type
- Configs::LogicalVolume
+ # @param config [Configs::LogicalVolume]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb
index 9155ebe2aa..99bda1c7ce 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb
@@ -24,7 +24,6 @@
require "agama/storage/config_conversions/to_json_conversions/with_filesystem"
require "agama/storage/config_conversions/to_json_conversions/with_search"
require "agama/storage/config_conversions/to_json_conversions/with_size"
-require "agama/storage/configs/partition"
module Agama
module Storage
@@ -37,9 +36,10 @@ class Partition < Base
include WithFilesystem
include WithSize
- # @see Base
- def self.config_type
- Configs::Partition
+ # @param config [Configs::Partition]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/search.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/search.rb
index 957392e25b..a6eda81759 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/search.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/search.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/to_json_conversions/base"
-require "agama/storage/configs/search"
module Agama
module Storage
@@ -28,9 +27,10 @@ module ConfigConversions
module ToJSONConversions
# Search conversion to JSON hash according to schema.
class Search < Base
- # @see Base
- def self.config_type
- Configs::Search
+ # @param config [Configs::Search]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/size.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/size.rb
index f57c2ba55c..f43d1c44b6 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/size.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/size.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/to_json_conversions/base"
-require "agama/storage/configs/size"
module Agama
module Storage
@@ -28,9 +27,10 @@ module ConfigConversions
module ToJSONConversions
# Size conversion to JSON hash according to schema.
class Size < Base
- # @see Base
- def self.config_type
- Configs::Size
+ # @param config [Configs::Size]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb
index ffe0be78a4..217d88ded0 100644
--- a/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb
+++ b/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb
@@ -22,7 +22,6 @@
require "agama/storage/config_conversions/to_json_conversions/base"
require "agama/storage/config_conversions/to_json_conversions/encryption"
require "agama/storage/config_conversions/to_json_conversions/logical_volume"
-require "agama/storage/configs/volume_group"
module Agama
module Storage
@@ -30,9 +29,10 @@ module ConfigConversions
module ToJSONConversions
# Volume group conversion to JSON hash according to schema.
class VolumeGroup < Base
- # @see Base
- def self.config_type
- Configs::VolumeGroup
+ # @param config [Configs::VolumeGroup]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/base.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/base.rb
index e447dd1e11..9178e770b4 100644
--- a/service/lib/agama/storage/config_conversions/to_model_conversions/base.rb
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/base.rb
@@ -25,22 +25,6 @@ module ConfigConversions
module ToModelConversions
# Base class for conversions to model according to the JSON schema.
class Base
- # Defines the expected config type to perform the conversion.
- #
- # @raise If a subclass does not defines a type.
- # @return [Class]
- def self.config_type
- raise "Undefined config type"
- end
-
- # @param config [Object] The config type is provided by the {.config_type} method.
- def initialize(config)
- type = self.class.config_type
- raise "Invalid config (#{type} expected): #{config}" unless config.is_a?(type)
-
- @config = config
- end
-
# Performs the conversion to model according to the JSON schema.
#
# @return [Hash, nil]
@@ -58,7 +42,7 @@ def convert
private
- # @return [Object] The config type is provided by the {.config_type} method.
+ # @return [Object] See {#initialize}.
attr_reader :config
# Values to generate the model.
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb
index 94b65aeeab..40e3da02e7 100644
--- a/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb
@@ -19,7 +19,6 @@
# 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/config"
require "agama/storage/config_conversions/to_model_conversions/base"
require "agama/storage/config_conversions/to_model_conversions/drive"
@@ -29,9 +28,10 @@ module ConfigConversions
module ToModelConversions
# Config conversion to model according to the JSON schema.
class Config < Base
- # @see Base
- def self.config_type
- Storage::Config
+ # @param config [Storage::Config]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb
index 8bd54b4e62..0542d7f4fe 100644
--- a/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb
@@ -23,7 +23,6 @@
require "agama/storage/config_conversions/to_model_conversions/with_filesystem"
require "agama/storage/config_conversions/to_model_conversions/with_partitions"
require "agama/storage/config_conversions/to_model_conversions/with_space_policy"
-require "agama/storage/configs/drive"
module Agama
module Storage
@@ -35,9 +34,10 @@ class Drive < Base
include WithPartitions
include WithSpacePolicy
- # @see Base
- def self.config_type
- Configs::Drive
+ # @param config [Configs::Drive]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb
index c8b6b04f24..eb3e02b448 100644
--- a/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/to_model_conversions/base"
-require "agama/storage/configs/filesystem"
module Agama
module Storage
@@ -28,9 +27,10 @@ module ConfigConversions
module ToModelConversions
# Drive conversion to model according to the JSON schema.
class Filesystem < Base
- # @see Base
- def self.config_type
- Configs::Filesystem
+ # @param config [Configs::Filesystem]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb
index f8ac908ba0..dd1944db26 100644
--- a/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb
@@ -22,7 +22,6 @@
require "agama/storage/config_conversions/to_model_conversions/base"
require "agama/storage/config_conversions/to_model_conversions/with_filesystem"
require "agama/storage/config_conversions/to_model_conversions/with_size"
-require "agama/storage/configs/partition"
module Agama
module Storage
@@ -33,9 +32,10 @@ class Partition < Base
include WithFilesystem
include WithSize
- # @see Base
- def self.config_type
- Configs::Partition
+ # @param config [Configs::Partition]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/size.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/size.rb
index 23e8217f73..13ab91151a 100644
--- a/service/lib/agama/storage/config_conversions/to_model_conversions/size.rb
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/size.rb
@@ -20,7 +20,6 @@
# find current contact information at www.suse.com.
require "agama/storage/config_conversions/to_model_conversions/base"
-require "agama/storage/configs/size"
module Agama
module Storage
@@ -28,9 +27,10 @@ module ConfigConversions
module ToModelConversions
# Size conversion to model according to the JSON schema.
class Size < Base
- # @see Base
- def self.config_type
- Configs::Size
+ # @param config [Configs::Size]
+ def initialize(config)
+ super()
+ @config = config
end
private
diff --git a/service/lib/agama/storage/config_json_solver.rb b/service/lib/agama/storage/config_json_solver.rb
index 3f05dda4af..06e8ba4663 100644
--- a/service/lib/agama/storage/config_json_solver.rb
+++ b/service/lib/agama/storage/config_json_solver.rb
@@ -57,6 +57,9 @@ module Storage
# }
# ]
# }
+ #
+ # See doc/storage_proposal_from_profile.md for a complete description of how the config is
+ # generated from a profile.
class ConfigJSONSolver
# @param default_paths [Array] Default paths of the product.
# @param mandatory_paths [Array] Mandatory paths of the product.
diff --git a/service/lib/agama/storage/config_solver.rb b/service/lib/agama/storage/config_solver.rb
index 3ec4084c5c..1fbff4e1fe 100644
--- a/service/lib/agama/storage/config_solver.rb
+++ b/service/lib/agama/storage/config_solver.rb
@@ -31,6 +31,9 @@ module Storage
# Solving a config means to assign proper values according to the product and the system. For
# example, the sizes of a partition config taking into account its fallbacks, assigning a
# specific device when a config has a search, etc.
+ #
+ # See doc/storage_proposal_from_profile.md for a complete description of how the config is
+ # generated from a profile.
class ConfigSolver
# @param devicegraph [Y2Storage::Devicegraph] initial layout of the system
# @param product_config [Agama::Config] configuration of the product to install
diff --git a/service/lib/agama/storage/configs/partition.rb b/service/lib/agama/storage/configs/partition.rb
index 7d8a853e47..d615ade999 100644
--- a/service/lib/agama/storage/configs/partition.rb
+++ b/service/lib/agama/storage/configs/partition.rb
@@ -28,6 +28,26 @@ module Storage
module Configs
# Section of the configuration representing a partition
class Partition
+ # Partition config meaning "delete all partitions".
+ #
+ # @return [Configs::Partition]
+ def self.new_for_delete_all
+ new.tap do |config|
+ config.search = Configs::Search.new_for_search_all
+ config.delete = true
+ end
+ end
+
+ # Partition config meaning "shrink any partitions if needed".
+ #
+ # @return [Configs::Partition]
+ def self.new_for_shrink_any_if_needed
+ new.tap do |config|
+ config.search = Configs::Search.new_for_search_all
+ config.size = Configs::Size.new_for_shrink_if_needed
+ end
+ end
+
include WithAlias
include WithSearch
diff --git a/service/lib/agama/storage/configs/search.rb b/service/lib/agama/storage/configs/search.rb
index abc826b9da..b0af036151 100644
--- a/service/lib/agama/storage/configs/search.rb
+++ b/service/lib/agama/storage/configs/search.rb
@@ -25,6 +25,13 @@ module Configs
# Configuration used to match drives, partitions and other device definition with devices
# from the initial devicegraph
class Search
+ # Search config meaning "search all".
+ #
+ # @return [Configs::Search]
+ def self.new_for_search_all
+ new.tap { |c| c.if_not_found = :skip }
+ end
+
# Found device, if any
# @return [Y2Storage::Device, nil]
attr_reader :device
diff --git a/service/lib/agama/storage/configs/size.rb b/service/lib/agama/storage/configs/size.rb
index a84a774154..424ae12917 100644
--- a/service/lib/agama/storage/configs/size.rb
+++ b/service/lib/agama/storage/configs/size.rb
@@ -19,11 +19,25 @@
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
+require "y2storage/disk_size"
+
module Agama
module Storage
module Configs
# Size configuration.
class Size
+ # Size config meaning "shrink if needed".
+ #
+ # @return [Configs::Size]
+ def self.new_for_shrink_if_needed
+ new.tap do |config|
+ config.default = false
+ config.min = Y2Storage::DiskSize.zero
+ end
+ end
+
+ # Whether the size is the default size for the volume.
+ #
# @return [Boolean]
attr_accessor :default
alias_method :default?, :default
diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb
index 74531aaf1e..fff8bd40ad 100644
--- a/service/lib/agama/storage/proposal.rb
+++ b/service/lib/agama/storage/proposal.rb
@@ -21,9 +21,7 @@
require "agama/issue"
require "agama/storage/actions_generator"
-require "agama/storage/config_conversions/from_json"
-require "agama/storage/config_conversions/to_json"
-require "agama/storage/config_conversions/to_model"
+require "agama/storage/config_conversions"
require "agama/storage/proposal_settings"
require "agama/storage/proposal_strategies"
require "json"
@@ -129,6 +127,15 @@ def calculate_from_json(source_json)
success?
end
+ # Calculates a new proposal from a config model.
+ #
+ # @param model_json [Hash] Source config model according to the JSON schema.
+ # @return [Boolean] Whether the proposal successes.
+ def calculate_from_model(model_json)
+ config = ConfigConversions::FromModel.new(model_json).convert
+ calculate_agama(config)
+ end
+
# Calculates a new proposal using the guided strategy.
#
# @param settings [Agama::Storage::ProposalSettings]
diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb
index 7381474efb..8a423168e3 100644
--- a/service/test/agama/dbus/storage/manager_test.rb
+++ b/service/test/agama/dbus/storage/manager_test.rb
@@ -557,6 +557,37 @@
end
end
+ describe "#apply_config_model" do
+ let(:serialized_model) { model_json.to_json }
+
+ let(:model_json) do
+ {
+ drives: [
+ name: "/dev/vda",
+ partitions: [
+ { mountPath: "/" }
+ ]
+ ]
+ }
+ end
+
+ it "calculates an agama proposal with the given config" do
+ expect(proposal).to receive(:calculate_agama) do |config|
+ expect(config).to be_a(Agama::Storage::Config)
+ expect(config.drives.size).to eq(1)
+
+ drive = config.drives.first
+ expect(drive.search.name).to eq("/dev/vda")
+ expect(drive.partitions.size).to eq(1)
+
+ partition = drive.partitions.first
+ expect(partition.filesystem.path).to eq("/")
+ end
+
+ subject.apply_config_model(serialized_model)
+ end
+ end
+
describe "#recover_config" do
def serialize(value)
JSON.pretty_generate(value)
diff --git a/service/test/agama/storage/config_conversions/from_model_test.rb b/service/test/agama/storage/config_conversions/from_model_test.rb
new file mode 100644
index 0000000000..31ec4226e5
--- /dev/null
+++ b/service/test/agama/storage/config_conversions/from_model_test.rb
@@ -0,0 +1,924 @@
+# 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/config"
+require "agama/storage/config_conversions"
+require "y2storage/encryption_method"
+require "y2storage/filesystems/mount_by_type"
+require "y2storage/filesystems/type"
+require "y2storage/pbkd_function"
+require "y2storage/refinements"
+
+using Y2Storage::Refinements::SizeCasts
+
+shared_examples "without alias" do |config_proc|
+ it "does not set #alias" do
+ config = config_proc.call(subject.convert)
+ expect(config.alias).to be_nil
+ end
+end
+
+shared_examples "without filesystem" do |config_proc|
+ it "does not set #filesystem" do
+ config = config_proc.call(subject.convert)
+ expect(config.filesystem).to be_nil
+ end
+end
+
+shared_examples "without ptableType" do |config_proc|
+ it "does not set #ptable_type" do
+ config = config_proc.call(subject.convert)
+ expect(config.ptable_type).to be_nil
+ end
+end
+
+shared_examples "without partitions" do |config_proc|
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ expect(config.partitions).to eq([])
+ end
+end
+
+shared_examples "without size" do |config_proc|
+ it "sets #size to default size" do
+ config = config_proc.call(subject.convert)
+ expect(config.size.default?).to eq(true)
+ expect(config.size.min).to be_nil
+ expect(config.size.max).to be_nil
+ end
+end
+
+shared_examples "without delete" do |config_proc|
+ it "sets #delete to false" do
+ config = config_proc.call(subject.convert)
+ expect(config.delete?).to eq(false)
+ end
+end
+
+shared_examples "without deleteIfNeeded" do |config_proc|
+ it "sets #delete_if_needed to false" do
+ config = config_proc.call(subject.convert)
+ expect(config.delete_if_needed?).to eq(false)
+ end
+end
+
+shared_examples "with name" do |config_proc|
+ let(:name) { "/dev/vda" }
+
+ it "sets #search to the expected value" do
+ config = config_proc.call(subject.convert)
+ expect(config.search).to be_a(Agama::Storage::Configs::Search)
+ expect(config.search.name).to eq("/dev/vda")
+ expect(config.search.max).to be_nil
+ expect(config.search.if_not_found).to eq(:error)
+ end
+end
+
+shared_examples "with alias" do |config_proc|
+ let(:device_alias) { "test" }
+
+ it "sets #alias to the expected value" do
+ config = config_proc.call(subject.convert)
+ expect(config.alias).to eq("test")
+ end
+end
+
+shared_examples "with mountPath" do |config_proc|
+ let(:mountPath) { "/test" }
+
+ it "sets #filesystem to the expected value" do
+ config = config_proc.call(subject.convert)
+ filesystem = config.filesystem
+ expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
+ expect(filesystem.reuse?).to eq(false)
+ expect(filesystem.type).to be_nil
+ expect(filesystem.label).to be_nil
+ expect(filesystem.path).to eq("/test")
+ expect(filesystem.mount_by).to be_nil
+ expect(filesystem.mkfs_options).to be_empty
+ expect(filesystem.mount_options).to be_empty
+ end
+end
+
+shared_examples "with filesystem" do |config_proc|
+ context "if the filesystem is default" do
+ let(:filesystem) do
+ {
+ default: true,
+ type: type,
+ snapshots: true
+ }
+ end
+
+ context "and the type is 'btrfs'" do
+ let(:type) { "btrfs" }
+
+ it "sets #filesystem to the expected value" do
+ config = config_proc.call(subject.convert)
+ filesystem = config.filesystem
+ expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
+ expect(filesystem.reuse?).to eq(false)
+ expect(filesystem.type.default?).to eq(true)
+ expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS)
+ expect(filesystem.type.btrfs).to be_a(Agama::Storage::Configs::Btrfs)
+ expect(filesystem.type.btrfs.snapshots?).to eq(true)
+ expect(filesystem.label).to be_nil
+ expect(filesystem.path).to be_nil
+ expect(filesystem.mount_by).to be_nil
+ expect(filesystem.mkfs_options).to be_empty
+ expect(filesystem.mount_options).to be_empty
+ end
+ end
+
+ context "and the type is not 'btrfs'" do
+ let(:type) { "xfs" }
+
+ it "sets #filesystem to the expected value" do
+ config = config_proc.call(subject.convert)
+ filesystem = config.filesystem
+ expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
+ expect(filesystem.reuse?).to eq(false)
+ expect(filesystem.type.default?).to eq(true)
+ expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::XFS)
+ expect(filesystem.type.btrfs).to be_nil
+ expect(filesystem.label).to be_nil
+ expect(filesystem.path).to be_nil
+ expect(filesystem.mount_by).to be_nil
+ expect(filesystem.mkfs_options).to be_empty
+ expect(filesystem.mount_options).to be_empty
+ end
+ end
+ end
+
+ context "if the filesystem is not default" do
+ let(:filesystem) do
+ {
+ default: false,
+ type: type,
+ snapshots: true
+ }
+ end
+
+ context "and the type is 'btrfs'" do
+ let(:type) { "btrfs" }
+
+ it "sets #filesystem to the expected value" do
+ config = config_proc.call(subject.convert)
+ filesystem = config.filesystem
+ expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
+ expect(filesystem.reuse?).to eq(false)
+ expect(filesystem.type.default?).to eq(false)
+ expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS)
+ expect(filesystem.type.btrfs).to be_a(Agama::Storage::Configs::Btrfs)
+ expect(filesystem.type.btrfs.snapshots?).to eq(true)
+ expect(filesystem.label).to be_nil
+ expect(filesystem.path).to be_nil
+ expect(filesystem.mount_by).to be_nil
+ expect(filesystem.mkfs_options).to be_empty
+ expect(filesystem.mount_options).to be_empty
+ end
+ end
+
+ context "and the type is not 'btrfs'" do
+ let(:type) { "xfs" }
+
+ it "sets #filesystem to the expected value" do
+ config = config_proc.call(subject.convert)
+ filesystem = config.filesystem
+ expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
+ expect(filesystem.reuse?).to eq(false)
+ expect(filesystem.type.default?).to eq(false)
+ expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::XFS)
+ expect(filesystem.type.btrfs).to be_nil
+ expect(filesystem.label).to be_nil
+ expect(filesystem.path).to be_nil
+ expect(filesystem.mount_by).to be_nil
+ expect(filesystem.mkfs_options).to be_empty
+ expect(filesystem.mount_options).to be_empty
+ end
+ end
+ end
+
+ context "if the filesystem does not specify 'type'" do
+ let(:filesystem) { { default: false } }
+
+ it "sets #filesystem to the expected value" do
+ config = config_proc.call(subject.convert)
+ filesystem = config.filesystem
+ expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
+ expect(filesystem.reuse?).to eq(false)
+ expect(filesystem.type.default?).to eq(false)
+ expect(filesystem.type.fs_type).to be_nil
+ expect(filesystem.type.btrfs).to be_nil
+ expect(filesystem.label).to be_nil
+ expect(filesystem.path).to be_nil
+ expect(filesystem.mount_by).to be_nil
+ expect(filesystem.mkfs_options).to eq([])
+ expect(filesystem.mount_options).to eq([])
+ end
+ end
+end
+
+shared_examples "with mountPath and filesystem" do |config_proc|
+ let(:mountPath) { "/test" }
+
+ let(:filesystem) do
+ {
+ default: false,
+ type: "btrfs",
+ snapshots: true
+ }
+ end
+
+ it "sets #filesystem to the expected value" do
+ config = config_proc.call(subject.convert)
+ filesystem = config.filesystem
+ expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
+ expect(filesystem.reuse?).to eq(false)
+ expect(filesystem.type.default?).to eq(false)
+ expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS)
+ expect(filesystem.type.btrfs).to be_a(Agama::Storage::Configs::Btrfs)
+ expect(filesystem.type.btrfs.snapshots?).to eq(true)
+ expect(filesystem.label).to be_nil
+ expect(filesystem.path).to eq("/test")
+ expect(filesystem.mount_by).to be_nil
+ expect(filesystem.mkfs_options).to be_empty
+ expect(filesystem.mount_options).to be_empty
+ end
+end
+
+shared_examples "with ptableType" do |config_proc|
+ let(:ptableType) { "gpt" }
+
+ it "sets #ptable_type to the expected value" do
+ config = config_proc.call(subject.convert)
+ expect(config.ptable_type).to eq(Y2Storage::PartitionTables::Type::GPT)
+ end
+end
+
+shared_examples "with size" do |config_proc|
+ context "if the size is default" do
+ let(:size) do
+ {
+ default: true,
+ min: 1.GiB.to_i,
+ max: 10.GiB.to_i
+ }
+ end
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(true)
+ expect(size.min).to eq(1.GiB)
+ expect(size.max).to eq(10.GiB)
+ end
+ end
+
+ context "if the size is not default" do
+ let(:size) do
+ {
+ default: false,
+ min: 1.GiB.to_i,
+ max: 10.GiB.to_i
+ }
+ end
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(false)
+ expect(size.min).to eq(1.GiB)
+ expect(size.max).to eq(10.GiB)
+ end
+ end
+
+ context "if the size does not spicify 'max'" do
+ let(:size) do
+ {
+ default: false,
+ min: 1.GiB.to_i
+ }
+ end
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(false)
+ expect(size.min).to eq(1.GiB)
+ expect(size.max).to eq(Y2Storage::DiskSize.unlimited)
+ end
+ end
+end
+
+shared_examples "with resizeIfNeeded" do |config_proc|
+ context "if 'resizeIfNeeded' is true" do
+ let(:resizeIfNeeded) { true }
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(false)
+ expect(size.min).to eq(Y2Storage::DiskSize.zero)
+ expect(size.max).to be_nil
+ end
+ end
+
+ context "if 'resizeIfNeeded' is false" do
+ let(:resizeIfNeeded) { false }
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(true)
+ expect(size.min).to be_nil
+ expect(size.max).to be_nil
+ end
+ end
+end
+
+shared_examples "with size and resizeIfNeeded" do |config_proc|
+ let(:size) do
+ {
+ default: true,
+ min: 1.GiB.to_i,
+ max: 10.GiB.to_i
+ }
+ end
+
+ context "if 'resizeIfNeeded' is true" do
+ let(:resizeIfNeeded) { true }
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(false)
+ expect(size.min).to eq(Y2Storage::DiskSize.zero)
+ expect(size.max).to be_nil
+ end
+ end
+
+ context "if 'resizeIfNeeded' is false" do
+ let(:resizeIfNeeded) { false }
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(true)
+ expect(size.min).to eq(1.GiB)
+ expect(size.max).to eq(10.GiB)
+ end
+ end
+end
+
+shared_examples "with size and resize" do |config_proc|
+ let(:size) do
+ {
+ default: true,
+ min: 1.GiB.to_i,
+ max: 10.GiB.to_i
+ }
+ end
+
+ context "if 'resize' is true" do
+ let(:resize) { true }
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(true)
+ expect(size.min).to eq(1.GiB)
+ expect(size.max).to eq(10.GiB)
+ end
+ end
+
+ context "if 'size' is false" do
+ let(:resize) { false }
+
+ it "sets #size to the expected value" do
+ config = config_proc.call(subject.convert)
+ size = config.size
+ expect(size).to be_a(Agama::Storage::Configs::Size)
+ expect(size.default?).to eq(true)
+ expect(size.min).to eq(1.GiB)
+ expect(size.max).to eq(10.GiB)
+ end
+ end
+end
+
+shared_examples "with delete" do |config_proc|
+ it "sets #delete to true" do
+ config = config_proc.call(subject.convert)
+ expect(config.delete?).to eq(true)
+ end
+end
+
+shared_examples "with deleteIfNeeded" do |config_proc|
+ it "sets #delete_if_needed to true" do
+ config = config_proc.call(subject.convert)
+ expect(config.delete_if_needed?).to eq(true)
+ end
+end
+
+shared_examples "with partitions" do |config_proc|
+ let(:partitions) do
+ [
+ partition,
+ { mountPath: "/test" }
+ ]
+ end
+
+ let(:partition) { { mountPath: "/" } }
+
+ context "with an empty list" do
+ let(:partitions) { [] }
+
+ it "sets #partitions to empty" do
+ config = config_proc.call(subject.convert)
+ expect(config.partitions).to eq([])
+ end
+ end
+
+ context "with a list of partitions" do
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions.size).to eq(2)
+
+ partition1, partition2 = partitions
+ expect(partition1).to be_a(Agama::Storage::Configs::Partition)
+ expect(partition1.filesystem.path).to eq("/")
+ expect(partition2).to be_a(Agama::Storage::Configs::Partition)
+ expect(partition2.filesystem.path).to eq("/test")
+ end
+ end
+
+ partition_proc = proc { |c| config_proc.call(c).partitions.first }
+
+ context "if a partition does not specify 'name'" do
+ let(:partition) { {} }
+
+ it "does not set #search" do
+ partition = partition_proc.call(subject.convert)
+ expect(partition.search).to be_nil
+ end
+ end
+
+ context "if a partition does not spicify 'alias'" do
+ let(:partition) { {} }
+ include_examples "without alias", partition_proc
+ end
+
+ context "if a partition does not spicify 'id'" do
+ let(:partition) { {} }
+
+ it "does not set #id" do
+ partition = partition_proc.call(subject.convert)
+ expect(partition.id).to be_nil
+ end
+ end
+
+ context "if a partition does not spicify 'size'" do
+ let(:partition) { {} }
+ include_examples "without size", partition_proc
+ end
+
+ context "if a partition does not spicify neither 'mountPath' nor 'filesystem'" do
+ let(:partition) { {} }
+ include_examples "without filesystem", partition_proc
+ end
+
+ context "if a partition does not spicify 'delete'" do
+ let(:partition) { {} }
+ include_examples "without delete", partition_proc
+ end
+
+ context "if a partition does not spicify 'deleteIfNeeded'" do
+ let(:partition) { {} }
+ include_examples "without deleteIfNeeded", partition_proc
+ end
+
+ context "if a partition specifies 'name'" do
+ let(:partition) { { name: name } }
+ include_examples "with name", partition_proc
+ end
+
+ context "if a partition specifies 'alias'" do
+ let(:partition) { { alias: device_alias } }
+ include_examples "with alias", partition_proc
+ end
+
+ context "if a partition spicifies 'id'" do
+ let(:partition) { { id: "esp" } }
+
+ it "sets #id to the expected value" do
+ partition = partition_proc.call(subject.convert)
+ expect(partition.id).to eq(Y2Storage::PartitionId::ESP)
+ end
+ end
+
+ context "if a partition specifies 'mountPath'" do
+ let(:partition) { { mountPath: mountPath } }
+ include_examples "with mountPath", partition_proc
+ end
+
+ context "if a partition specifies 'filesystem'" do
+ let(:partition) { { filesystem: filesystem } }
+ include_examples "with filesystem", partition_proc
+ end
+
+ context "if a partition specifies both 'mountPath' and 'filesystem'" do
+ let(:partition) { { mountPath: mountPath, filesystem: filesystem } }
+ include_examples "with mountPath and filesystem", partition_proc
+ end
+
+ context "if a partition spicifies 'size'" do
+ let(:partition) { { size: size } }
+ include_examples "with size", partition_proc
+ end
+
+ context "if a partition spicifies 'resizeIfNeeded'" do
+ let(:partition) { { resizeIfNeeded: resizeIfNeeded } }
+ include_examples "with resizeIfNeeded", partition_proc
+ end
+
+ context "if a partition spicifies both 'size' and 'resizeIfNeeded'" do
+ let(:partition) { { size: size, resizeIfNeeded: resizeIfNeeded } }
+ include_examples "with size and resizeIfNeeded", partition_proc
+ end
+
+ context "if a partition spicifies both 'size' and 'resize'" do
+ let(:partition) { { size: size, resize: resize } }
+ include_examples "with size and resize", partition_proc
+ end
+
+ context "if a partition specifies 'delete'" do
+ let(:partition) { { delete: true } }
+ include_examples "with delete", partition_proc
+ end
+
+ context "if a partition specifies 'deleteIfNeeded'" do
+ let(:partition) { { deleteIfNeeded: true } }
+ include_examples "with deleteIfNeeded", partition_proc
+ end
+end
+
+shared_examples "with spacePolicy" do |config_proc|
+ context "if space policy is 'keep'" do
+ let(:spacePolicy) { "keep" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions).to be_empty
+ end
+ end
+
+ context "if space policy is 'delete'" do
+ let(:spacePolicy) { "delete" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions.size).to eq(1)
+
+ partition = partitions.first
+ expect(partition.search.name).to be_nil
+ expect(partition.search.if_not_found).to eq(:skip)
+ expect(partition.search.max).to be_nil
+ expect(partition.delete?).to eq(true)
+ end
+ end
+
+ context "if space policy is 'resize'" do
+ let(:spacePolicy) { "resize" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions.size).to eq(1)
+
+ partition = partitions.first
+ expect(partition.search.name).to be_nil
+ expect(partition.search.if_not_found).to eq(:skip)
+ expect(partition.search.max).to be_nil
+ expect(partition.delete?).to eq(false)
+ expect(partition.size.default?).to eq(false)
+ expect(partition.size.min).to eq(Y2Storage::DiskSize.zero)
+ expect(partition.size.max).to be_nil
+ end
+ end
+
+ context "if space policy is 'custom'" do
+ let(:spacePolicy) { "custom" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions).to be_empty
+ end
+ end
+end
+
+shared_examples "with spacePolicy and partitions" do |config_proc|
+ let(:partitions) do
+ [
+ {
+ name: "/dev/vda1",
+ mountPath: "/data"
+ },
+ {
+ name: "/dev/vda2",
+ mountPath: "swap",
+ filesystem: { type: "swap" }
+ },
+ {
+ name: "/dev/vda3",
+ mountPath: "/home",
+ size: { default: false, min: 1.GiB.to_i, max: 10.GiB.to_i }
+ },
+ {
+ name: "/dev/vda4",
+ resizeIfNeeded: true
+ },
+ {
+ name: "/dev/vda5",
+ deleteIfNeeded: true
+ },
+ {
+ name: "/dev/vda6",
+ size: { default: false, min: 5.GiB }
+ },
+ {
+ name: "/dev/vda7",
+ delete: true
+ },
+ {
+ mountPath: "/",
+ filesystem: { type: "btrfs" }
+ }
+ ]
+ end
+
+ context "if space policy is 'keep'" do
+ let(:spacePolicy) { "keep" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions.size).to eq(4)
+ expect(partitions[0].search.name).to eq("/dev/vda1")
+ expect(partitions[1].search.name).to eq("/dev/vda2")
+ expect(partitions[2].search.name).to eq("/dev/vda3")
+ expect(partitions[2].size.default?).to eq(false)
+ expect(partitions[2].size.min).to eq(1.GiB)
+ expect(partitions[2].size.max).to eq(10.GiB)
+ expect(partitions[3].filesystem.path).to eq("/")
+ end
+ end
+
+ context "if space policy is 'delete'" do
+ let(:spacePolicy) { "delete" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions.size).to eq(5)
+ expect(partitions[0].search.name).to eq("/dev/vda1")
+ expect(partitions[1].search.name).to eq("/dev/vda2")
+ expect(partitions[2].search.name).to eq("/dev/vda3")
+ expect(partitions[2].size.default?).to eq(false)
+ expect(partitions[2].size.min).to eq(1.GiB)
+ expect(partitions[2].size.max).to eq(10.GiB)
+ expect(partitions[3].filesystem.path).to eq("/")
+ expect(partitions[4].search.name).to be_nil
+ expect(partitions[4].search.max).to be_nil
+ expect(partitions[4].delete).to eq(true)
+ end
+ end
+
+ context "if space policy is 'resize'" do
+ let(:spacePolicy) { "resize" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions.size).to eq(5)
+ expect(partitions[0].search.name).to eq("/dev/vda1")
+ expect(partitions[1].search.name).to eq("/dev/vda2")
+ expect(partitions[2].search.name).to eq("/dev/vda3")
+ expect(partitions[2].size.default?).to eq(false)
+ expect(partitions[2].size.min).to eq(1.GiB)
+ expect(partitions[2].size.max).to eq(10.GiB)
+ expect(partitions[3].filesystem.path).to eq("/")
+ expect(partitions[4].search.name).to be_nil
+ expect(partitions[4].search.max).to be_nil
+ expect(partitions[4].size.default?).to eq(false)
+ expect(partitions[4].size.min).to eq(Y2Storage::DiskSize.zero)
+ expect(partitions[4].size.max).to be_nil
+ end
+ end
+
+ context "if space policy is 'custom'" do
+ let(:spacePolicy) { "custom" }
+
+ it "sets #partitions to the expected value" do
+ config = config_proc.call(subject.convert)
+ partitions = config.partitions
+ expect(partitions.size).to eq(8)
+ expect(partitions[0].search.name).to eq("/dev/vda1")
+ expect(partitions[1].search.name).to eq("/dev/vda2")
+ expect(partitions[2].search.name).to eq("/dev/vda3")
+ expect(partitions[2].size.default?).to eq(false)
+ expect(partitions[2].size.min).to eq(1.GiB)
+ expect(partitions[2].size.max).to eq(10.GiB)
+ expect(partitions[3].search.name).to eq("/dev/vda4")
+ expect(partitions[3].size.default?).to eq(false)
+ expect(partitions[3].size.min).to eq(Y2Storage::DiskSize.zero)
+ expect(partitions[3].size.max).to be_nil
+ expect(partitions[4].search.name).to eq("/dev/vda5")
+ expect(partitions[4].delete_if_needed?).to eq(true)
+ expect(partitions[5].search.name).to eq("/dev/vda6")
+ expect(partitions[5].size.default?).to eq(false)
+ expect(partitions[5].size.min).to eq(5.GiB)
+ expect(partitions[5].size.max).to eq(Y2Storage::DiskSize.unlimited)
+ expect(partitions[6].search.name).to eq("/dev/vda7")
+ expect(partitions[6].delete?).to eq(true)
+ expect(partitions[7].filesystem.path).to eq("/")
+ end
+ end
+end
+
+describe Agama::Storage::ConfigConversions::FromModel do
+ subject do
+ described_class.new(model_json)
+ end
+
+ before do
+ # Speed up tests by avoding real check of TPM presence.
+ allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true)
+ end
+
+ describe "#convert" do
+ let(:model_json) { {} }
+
+ it "returns a storage config" do
+ config = subject.convert
+ expect(config).to be_a(Agama::Storage::Config)
+ end
+
+ context "with an empty JSON" do
+ let(:model_json) { {} }
+
+ it "sets #drives to the expected value" do
+ config = subject.convert
+ expect(config.drives).to be_empty
+ end
+ end
+
+ context "with a JSON specifying 'drives'" do
+ let(:model_json) do
+ { drives: drives }
+ end
+
+ let(:drives) do
+ [
+ drive,
+ { name: "/dev/vdb" }
+ ]
+ end
+
+ let(:drive) do
+ { name: "/dev/vda" }
+ end
+
+ context "with an empty list" do
+ let(:drives) { [] }
+
+ it "sets #drives to the expected value" do
+ config = subject.convert
+ expect(config.drives).to eq([])
+ end
+ end
+
+ context "with a list of drives" do
+ it "sets #drives to the expected value" do
+ config = subject.convert
+ expect(config.drives.size).to eq(2)
+ expect(config.drives).to all(be_a(Agama::Storage::Configs::Drive))
+
+ drive1, drive2 = config.drives
+ expect(drive1.search.name).to eq("/dev/vda")
+ expect(drive1.partitions).to eq([])
+ expect(drive2.search.name).to eq("/dev/vdb")
+ expect(drive2.partitions).to eq([])
+ end
+ end
+
+ drive_proc = proc { |c| c.drives.first }
+
+ context "if a drive does not specify 'name'" do
+ let(:drive) { {} }
+
+ it "sets #search to the expected value" do
+ drive = drive_proc.call(subject.convert)
+ expect(drive.search).to be_a(Agama::Storage::Configs::Search)
+ expect(drive.search.name).to be_nil
+ expect(drive.search.if_not_found).to eq(:error)
+ end
+ end
+
+ context "if a drive does not spicify 'alias'" do
+ let(:drive) { {} }
+ include_examples "without alias", drive_proc
+ end
+
+ context "if a drive does not spicify neither 'mountPath' nor 'filesystem'" do
+ let(:drive) { {} }
+ include_examples "without filesystem", drive_proc
+ end
+
+ context "if a drive does not spicify 'ptableType'" do
+ let(:drive) { {} }
+ include_examples "without ptableType", drive_proc
+ end
+
+ context "if a drive does not spicify neither 'spacePolicy' nor 'partitions'" do
+ let(:drive) { {} }
+ include_examples "without partitions", drive_proc
+ end
+
+ context "if a drive specifies 'name'" do
+ let(:drive) { { name: name } }
+ include_examples "with name", drive_proc
+ end
+
+ context "if a drive specifies 'alias'" do
+ let(:drive) { { alias: device_alias } }
+ include_examples "with alias", drive_proc
+ end
+
+ context "if a drive specifies 'mountPath'" do
+ let(:drive) { { mountPath: mountPath } }
+ include_examples "with mountPath", drive_proc
+ end
+
+ context "if a drive specifies 'filesystem'" do
+ let(:drive) { { filesystem: filesystem } }
+ include_examples "with filesystem", drive_proc
+ end
+
+ context "if a drive specifies both 'mountPath' and 'filesystem'" do
+ let(:drive) { { mountPath: mountPath, filesystem: filesystem } }
+ include_examples "with mountPath and filesystem", drive_proc
+ end
+
+ context "if a drive specifies 'ptableType'" do
+ let(:drive) { { ptableType: ptableType } }
+ include_examples "with ptableType", drive_proc
+ end
+
+ context "if a drive specifies 'partitions'" do
+ let(:drive) { { partitions: partitions } }
+ include_examples "with partitions", drive_proc
+ end
+
+ context "if a drive specifies 'spacePolicy'" do
+ let(:drive) { { spacePolicy: spacePolicy } }
+ include_examples "with spacePolicy", drive_proc
+ end
+
+ context "if a drive specifies both 'spacePolicy' and 'partitions'" do
+ let(:drive) { { spacePolicy: spacePolicy, partitions: partitions } }
+ include_examples "with spacePolicy and partitions", drive_proc
+ end
+ end
+ end
+end
diff --git a/service/test/agama/storage/config_solver_test.rb b/service/test/agama/storage/config_solver_test.rb
index d6504126c5..8b3c1253ec 100644
--- a/service/test/agama/storage/config_solver_test.rb
+++ b/service/test/agama/storage/config_solver_test.rb
@@ -550,6 +550,52 @@
end
end
end
+
+ context "if a config does not specify max size" do
+ let(:config_json) do
+ {
+ drives: [
+ {
+ partitions: [
+ {
+ search: search,
+ filesystem: { path: "/" },
+ size: {
+ min: "10 GiB"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ end
+
+ context "and there is no device assigned" do
+ let(:search) { nil }
+
+ it "sets max size to unlimited" do
+ subject.solve(config)
+ partition = partition_proc.call(config)
+ expect(partition.size.default?).to eq(false)
+ expect(partition.size.min).to eq(10.GiB)
+ expect(partition.size.max).to eq(Y2Storage::DiskSize.unlimited)
+ end
+ end
+
+ context "and there is a device assigned" do
+ let(:scenario) { "disks.yaml" }
+
+ let(:search) { "/dev/vda2" }
+
+ it "sets max size to unlimited" do
+ subject.solve(config)
+ partition = partition_proc.call(config)
+ expect(partition.size.default?).to eq(false)
+ expect(partition.size.min).to eq(10.GiB)
+ expect(partition.size.max).to eq(Y2Storage::DiskSize.unlimited)
+ end
+ end
+ end
end
context "if a drive omits the search" do
diff --git a/service/test/agama/storage/proposal_test.rb b/service/test/agama/storage/proposal_test.rb
index 97c13326af..a5dda91ee3 100644
--- a/service/test/agama/storage/proposal_test.rb
+++ b/service/test/agama/storage/proposal_test.rb
@@ -692,6 +692,34 @@ def drive(partitions)
end
end
+ describe "#calculate_from_model" do
+ let(:model_json) do
+ {
+ drives: [
+ {
+ name: "/dev/vda",
+ filesystem: {
+ type: "xfs"
+ }
+ }
+ ]
+ }
+ end
+
+ it "calculates a proposal with the agama strategy and with the expected config" do
+ expect(subject).to receive(:calculate_agama) do |config|
+ expect(config).to be_a(Agama::Storage::Config)
+ expect(config.drives.size).to eq(1)
+
+ drive = config.drives.first
+ expect(drive.search.name).to eq("/dev/vda")
+ expect(drive.filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::XFS)
+ end
+
+ subject.calculate_from_model(model_json)
+ end
+ end
+
describe "#actions" do
it "returns an empty list if calculate has not been called yet" do
expect(subject.actions).to eq([])
diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts
index 9913e89d39..2c8ebedd13 100644
--- a/web/src/api/storage.ts
+++ b/web/src/api/storage.ts
@@ -38,7 +38,9 @@ const fetchConfig = (): Promise =>
const fetchConfigModel = (): Promise =>
get("/api/storage/config_model");
-const setConfig = (config: config.Config) => put("/api/storage/config", config);
+const setConfig = (config: config.Config) => put("/api/storage/config", { storage: config });
+
+const setConfigModel = (model: configModel.Config) => put("/api/storage/config_model", model);
/**
* Returns the list of jobs
@@ -69,6 +71,7 @@ export {
fetchConfig,
fetchConfigModel,
setConfig,
+ setConfigModel,
fetchStorageJobs,
findStorageJob,
refresh,
diff --git a/web/src/api/storage/types/config-model.ts b/web/src/api/storage/types/config-model.ts
index b27f2421a3..7951ae948b 100644
--- a/web/src/api/storage/types/config-model.ts
+++ b/web/src/api/storage/types/config-model.ts
@@ -39,6 +39,8 @@ export interface Drive {
spacePolicy?: SpacePolicy;
ptableType?: PtableType;
partitions?: Partition[];
+ boot?: string;
+ volumeGroups: string[];
}
export interface Filesystem {
default: boolean;
@@ -53,7 +55,9 @@ export interface Partition {
filesystem?: Filesystem;
size?: Size;
delete?: boolean;
+ // TODO: ignore
deleteIfNeeded?: boolean;
+ // TODO: ignore
resize?: boolean;
resizeIfNeeded?: boolean;
}
diff --git a/web/src/components/overview/StorageSection.test.tsx b/web/src/components/overview/StorageSection.test.tsx
index bc85cd1fdd..811f83d0e2 100644
--- a/web/src/components/overview/StorageSection.test.tsx
+++ b/web/src/components/overview/StorageSection.test.tsx
@@ -24,24 +24,25 @@ import React from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { StorageSection } from "~/components/overview";
-import * as ConfigModel from "~/storage/model/config";
+import * as ConfigModel from "~/api/storage/types/config-model";
const mockDevices = [
- { name: "/dev/sda", size: 536870912000 },
- { name: "/dev/sdb", size: 697932185600 },
+ { name: "/dev/sda", size: 536870912000, volumeGroups: [] },
+ { name: "/dev/sdb", size: 697932185600, volumeGroups: [] },
];
-const mockConfig = { devices: [] as ConfigModel.Device[] };
+const mockConfig = { drives: [] as ConfigModel.Drive[] };
jest.mock("~/queries/storage", () => ({
...jest.requireActual("~/queries/storage"),
+ useConfigModel: () => mockConfig,
useDevices: () => mockDevices,
- useConfigDevices: () => mockConfig.devices,
+ useConfigDevices: () => mockConfig.drives,
}));
describe("when the configuration does not include any device", () => {
beforeEach(() => {
- mockConfig.devices = [];
+ mockConfig.drives = [];
});
it("indicates that a device is not selected", async () => {
@@ -53,7 +54,7 @@ describe("when the configuration does not include any device", () => {
describe("when the configuration contains one drive", () => {
beforeEach(() => {
- mockConfig.devices = [{ name: "/dev/sda", spacePolicy: "delete" }];
+ mockConfig.drives = [{ name: "/dev/sda", spacePolicy: "delete", volumeGroups: [] }];
});
it("renders the proposal summary", async () => {
@@ -66,7 +67,7 @@ describe("when the configuration contains one drive", () => {
describe("and the space policy is set to 'resize'", () => {
beforeEach(() => {
- mockConfig.devices[0].spacePolicy = "resize";
+ mockConfig.drives[0].spacePolicy = "resize";
});
it("indicates that partitions may be shrunk", async () => {
@@ -78,7 +79,7 @@ describe("when the configuration contains one drive", () => {
describe("and the space policy is set to 'keep'", () => {
beforeEach(() => {
- mockConfig.devices[0].spacePolicy = "keep";
+ mockConfig.drives[0].spacePolicy = "keep";
});
it("indicates that partitions will be kept", async () => {
@@ -90,7 +91,7 @@ describe("when the configuration contains one drive", () => {
describe("and the drive matches no disk", () => {
beforeEach(() => {
- mockConfig.devices[0].name = null;
+ mockConfig.drives[0].name = null;
});
it("indicates that a device is not selected", async () => {
@@ -103,9 +104,9 @@ describe("when the configuration contains one drive", () => {
describe("when the configuration contains several drives", () => {
beforeEach(() => {
- mockConfig.devices = [
- { name: "/dev/sda", spacePolicy: "delete" },
- { name: "/dev/sdb", spacePolicy: "delete" },
+ mockConfig.drives = [
+ { name: "/dev/sda", spacePolicy: "delete", volumeGroups: [] },
+ { name: "/dev/sdb", spacePolicy: "delete", volumeGroups: [] },
];
});
diff --git a/web/src/components/overview/StorageSection.tsx b/web/src/components/overview/StorageSection.tsx
index b0c0b6082a..7c02aae9ca 100644
--- a/web/src/components/overview/StorageSection.tsx
+++ b/web/src/components/overview/StorageSection.tsx
@@ -89,25 +89,33 @@ export default function StorageSection() {
return _("Install using several devices with a custom strategy to find the needed space.");
};
- if (drives.length === 0) return {_("No device selected yet")};
+ const existDevice = (name) => devices.some((d) => d.name === name);
+ const noDrive = drives.length === 0 || drives.some((d) => !existDevice(d.name));
- if (drives.length > 1) {
+ if (noDrive)
return (
- {msgMultipleDisks(drives)}
+ {_("No device selected yet")}
);
- } else {
- const [msg1, msg2] = msgSingleDisk(drives[0]).split("%s");
+ if (drives.length > 1) {
return (
-
- {msg1}
- {label(drives[0])}
- {msg2}
-
+ {msgMultipleDisks(drives)}
);
}
+
+ const [msg1, msg2] = msgSingleDisk(drives[0]).split("%s");
+
+ return (
+
+
+ {msg1}
+ {label(drives[0])}
+ {msg2}
+
+
+ );
}
diff --git a/web/src/components/storage/DeviceSelectorTable.tsx b/web/src/components/storage/DeviceSelectorTable.tsx
index bed25f5c15..3dfe18517c 100644
--- a/web/src/components/storage/DeviceSelectorTable.tsx
+++ b/web/src/components/storage/DeviceSelectorTable.tsx
@@ -37,6 +37,7 @@ import { sprintf } from "sprintf-js";
import { deviceBaseName } from "~/components/storage/utils";
import { PartitionSlot, StorageDevice } from "~/types/storage";
import { ExpandableSelectorColumn, ExpandableSelectorProps } from "../core/ExpandableSelector";
+import { typeDescription, contentDescription } from "./utils/device";
/**
* @component
diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx
index b206327957..cbcbcae82d 100644
--- a/web/src/components/storage/DriveEditor.tsx
+++ b/web/src/components/storage/DriveEditor.tsx
@@ -21,13 +21,15 @@
*/
import React, { useRef, useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, generatePath } from "react-router-dom";
import { _, formatList } from "~/i18n";
import { sprintf } from "sprintf-js";
import { baseName, deviceLabel, formattedPath, SPACE_POLICIES } from "~/components/storage/utils";
import { useAvailableDevices } from "~/queries/storage";
-import { config as type } from "~/api/storage/types";
+import { configModel } from "~/api/storage/types";
import { StorageDevice } from "~/types/storage";
+import { STORAGE as PATHS } from "~/routes/paths";
+import { useChangeDrive, useSetSpacePolicy } from "~/queries/storage";
import * as driveUtils from "~/components/storage/utils/drive";
import { typeDescription, contentDescription } from "~/components/storage/utils/device";
import { Icon } from "../layout";
@@ -50,7 +52,7 @@ import {
MenuToggle,
} from "@patternfly/react-core";
-type DriveEditorProps = { drive: type.DriveElement; driveDevice: StorageDevice };
+type DriveEditorProps = { drive: configModel.Drive; driveDevice: StorageDevice };
// FIXME: Presentation is quite poor
const SpacePolicySelectorIntro = ({ device }) => {
@@ -82,11 +84,21 @@ const SpacePolicySelectorIntro = ({ device }) => {
);
};
-const SpacePolicySelector = ({ drive, driveDevice }) => {
+const SpacePolicySelector = ({ drive, driveDevice }: DriveEditorProps) => {
const menuRef = useRef();
const toggleMenuRef = useRef();
const [isOpen, setIsOpen] = useState(false);
+ const navigate = useNavigate();
+ const setSpacePolicy = useSetSpacePolicy();
const onToggle = () => setIsOpen(!isOpen);
+ const onSpacePolicyChange = (spacePolicy: configModel.SpacePolicy) => {
+ if (spacePolicy === "custom") {
+ return navigate(generatePath(PATHS.spacePolicy, { id: baseName(drive.name) }));
+ } else {
+ setSpacePolicy(drive.name, spacePolicy);
+ setIsOpen(false);
+ }
+ };
const currentPolicy = driveUtils.spacePolicyEntry(drive);
@@ -96,7 +108,12 @@ const SpacePolicySelector = ({ drive, driveDevice }) => {
const Name = () => (isSelected ? {policy.label} : policy.label);
return (
-