diff --git a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
index aabc7084ba..f5c91d7edb 100644
--- a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
+++ b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml
@@ -52,8 +52,8 @@
-
-
+
+
diff --git a/doc/dbus/org.opensuse.Agama.Storage1.doc.xml b/doc/dbus/org.opensuse.Agama.Storage1.doc.xml
index f7f0272a34..a848f9f8c0 100644
--- a/doc/dbus/org.opensuse.Agama.Storage1.doc.xml
+++ b/doc/dbus/org.opensuse.Agama.Storage1.doc.xml
@@ -56,34 +56,35 @@
-
+
-
+
diff --git a/rust/agama-lib/share/examples/storage/model.json b/rust/agama-lib/share/examples/storage/model.json
new file mode 100644
index 0000000000..a90734569a
--- /dev/null
+++ b/rust/agama-lib/share/examples/storage/model.json
@@ -0,0 +1,69 @@
+{
+ "drives": [
+ {
+ "name": "/dev/vda",
+ "alias": "root",
+ "mountPath": "/",
+ "filesystem": {
+ "default": true,
+ "type": "btrfs",
+ "snapshots": true
+ },
+ "spacePolicy": "delete"
+ },
+ {
+ "name": "/dev/vdb",
+ "spacePolicy": "custom",
+ "ptableType": "gpt",
+ "partitions": [
+ {
+ "name": "/dev/vda1",
+ "size": {
+ "default": false,
+ "min": 0,
+ "max": 1234567
+ },
+ "deleteIfNeeded": true,
+ "resizeIfNeeded": true
+ },
+ {
+ "name": "/dev/vda2",
+ "size": {
+ "default": false,
+ "min": 1234567,
+ "max": 1234567
+ },
+ "resize": true
+ },
+ {
+ "name": "/dev/vdb3",
+ "delete": true
+ },
+ {
+ "size": {
+ "default": true,
+ "min": 4444,
+ "max": 8888
+ },
+ "mountPath": "swap",
+ "filesystem": {
+ "default": true,
+ "type": "swap"
+ }
+ },
+ {
+ "size": {
+ "default": false,
+ "min": 100000,
+ "max": 100000
+ },
+ "mountPath": "/home",
+ "filesystem": {
+ "default": false,
+ "type": "xfs"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/rust/agama-lib/share/storage.model.schema.json b/rust/agama-lib/share/storage.model.schema.json
new file mode 100644
index 0000000000..e069a3fb70
--- /dev/null
+++ b/rust/agama-lib/share/storage.model.schema.json
@@ -0,0 +1,96 @@
+{
+ "title": "Config",
+ "description": "Config model",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "drives": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/drive" }
+ }
+ },
+ "$defs": {
+ "drive": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": { "type": "string" },
+ "alias": { "type": "string" },
+ "mountPath": { "type": "string" },
+ "filesystem": { "$ref": "#/$defs/filesystem" },
+ "spacePolicy": { "$ref": "#/$defs/spacePolicy" },
+ "ptableType": { "$ref": "#/$defs/ptableType" },
+ "partitions": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/partition" }
+ }
+ }
+ },
+ "partition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": { "type": "string" },
+ "alias": { "type": "string" },
+ "id": { "$ref": "#/$defs/partitionId" },
+ "mountPath": { "type": "string" },
+ "filesystem": { "$ref": "#/$defs/filesystem" },
+ "size": { "$ref": "#/$defs/size" },
+ "delete": { "type": "boolean" },
+ "deleteIfNeeded": { "type": "boolean" },
+ "resize": { "type": "boolean" },
+ "resizeIfNeeded": { "type": "boolean" }
+ }
+ },
+ "spacePolicy": {
+ "enum": ["delete", "resize", "keep", "custom"]
+ },
+ "ptableType": {
+ "enum": ["gpt", "msdos", "dasd"]
+ },
+ "partitionId": {
+ "enum": ["linux", "swap", "lvm", "raid", "esp", "prep", "bios_boot"]
+ },
+ "filesystem": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["default"],
+ "properties": {
+ "default": { "type": "boolean" },
+ "type": { "$ref": "#/$defs/filesystemType" },
+ "snapshots": { "type": "boolean" }
+ }
+ },
+ "filesystemType": {
+ "enum": [
+ "bcachefs",
+ "btrfs",
+ "exfat",
+ "ext2",
+ "ext3",
+ "ext4",
+ "f2fs",
+ "jfs",
+ "nfs",
+ "nilfs2",
+ "ntfs",
+ "reiserfs",
+ "swap",
+ "tmpfs",
+ "vfat",
+ "xfs"
+ ]
+ },
+ "size": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["default", "min"],
+ "properties": {
+ "default": { "type": "boolean" },
+ "min": { "type": "integer", "minimum": 0 },
+ "max": { "type": "integer", "minimum": 0 }
+ }
+ }
+ }
+}
diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs
index 4125188233..d145d76309 100644
--- a/rust/agama-lib/src/storage/client.rs
+++ b/rust/agama-lib/src/storage/client.rs
@@ -29,6 +29,7 @@ use super::proxies::{DevicesProxy, ProposalCalculatorProxy, ProposalProxy, Stora
use super::StorageSettings;
use crate::dbus::get_property;
use crate::error::ServiceError;
+use serde_json::value::RawValue;
use std::collections::HashMap;
use zbus::fdo::ObjectManagerProxy;
use zbus::names::{InterfaceName, OwnedInterfaceName};
@@ -153,11 +154,11 @@ impl<'a> StorageClient<'a> {
Ok(settings)
}
- /// Get the storage solved config according to the JSON schema
- pub async fn get_solved_config(&self) -> Result {
- let serialized_settings = self.storage_proxy.get_solved_config().await?;
- let settings = serde_json::from_str(serialized_settings.as_str()).unwrap();
- Ok(settings)
+ /// 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?;
+ let config_model = serde_json::from_str(serialized_config_model.as_str()).unwrap();
+ Ok(config_model)
}
pub async fn calculate(&self, settings: ProposalSettingsPatch) -> Result {
diff --git a/rust/agama-lib/src/storage/proxies.rs b/rust/agama-lib/src/storage/proxies.rs
index f83ee7af09..cde0e5491e 100644
--- a/rust/agama-lib/src/storage/proxies.rs
+++ b/rust/agama-lib/src/storage/proxies.rs
@@ -44,8 +44,8 @@ trait Storage1 {
/// Get the current storage config according to the JSON schema
fn get_config(&self) -> zbus::Result;
- /// Get the current storage solved config according to the JSON schema
- fn get_solved_config(&self) -> zbus::Result;
+ /// Get the storage config model according to the JSON schema
+ fn get_config_model(&self) -> zbus::Result;
/// DeprecatedSystem property
#[dbus_proxy(property)]
diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs
index bfb909e055..259e82acb0 100644
--- a/rust/agama-server/src/storage/web.rs
+++ b/rust/agama-server/src/storage/web.rs
@@ -39,6 +39,7 @@ use axum::{
Json, Router,
};
use serde::{Deserialize, Serialize};
+use serde_json::value::RawValue;
use tokio_stream::{Stream, StreamExt};
use zfcp::{zfcp_service, zfcp_stream};
@@ -113,7 +114,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result),
(status = 400, description = "The D-Bus service could not perform the action")
)
)]
-async fn get_solved_config(
+async fn get_config_model(
State(state): State>,
-) -> Result, Error> {
- // StorageSettings is just a wrapper over serde_json::value::RawValue
- let settings = state
+) -> Result>, Error> {
+ let config_model = state
.client
- .get_solved_config()
+ .get_config_model()
.await
.map_err(Error::Service)?;
- Ok(Json(settings))
+ Ok(Json(config_model))
}
/// Sets the storage configuration.
diff --git a/service/lib/agama/config.rb b/service/lib/agama/config.rb
index 67b0999908..8c51602c4f 100644
--- a/service/lib/agama/config.rb
+++ b/service/lib/agama/config.rb
@@ -203,6 +203,20 @@ def mandatory_paths
default_paths.select { |p| mandatory_path?(p) }
end
+ # Default policy to make space.
+ #
+ # @return [String]
+ def space_policy
+ data.dig("storage", "space_policy") || "keep"
+ end
+
+ # Whether LVM must be used by default.
+ #
+ # @return [Boolean]
+ def lvm?
+ data.dig("storage", "lvm") || false
+ end
+
private
def mandatory_path?(path)
diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb
index 3194ed5157..c6742718eb 100644
--- a/service/lib/agama/dbus/storage/manager.rb
+++ b/service/lib/agama/dbus/storage/manager.rb
@@ -45,7 +45,7 @@ module Agama
module DBus
module Storage
# D-Bus object to manage storage installation
- class Manager < BaseObject
+ class Manager < BaseObject # rubocop:disable Metrics/ClassLength
include WithISCSIAuth
include WithServiceStatus
include ::DBus::ObjectManager
@@ -111,12 +111,19 @@ def apply_config(serialized_config)
proposal.success? ? 0 : 1
end
- # Gets and serializes the storage config used to calculate the current proposal.
+ # Gets and serializes the storage config used for calculating the current proposal.
#
- # @param solved [Boolean] Whether to recover the solved config.
- # @return [String] Serialized config according to the JSON schema.
- def recover_config(solved: false)
- json = proposal.storage_json(solved: solved)
+ # @return [String]
+ def recover_config
+ json = proposal.storage_json
+ JSON.pretty_generate(json)
+ end
+
+ # Gets and serializes the storage config model.
+ #
+ # @return [String]
+ def recover_model
+ json = proposal.model_json
JSON.pretty_generate(json)
end
@@ -141,7 +148,7 @@ def deprecated_system
busy_while { apply_config(serialized_config) }
end
dbus_method(:GetConfig, "out serialized_config:s") { recover_config }
- dbus_method(:GetSolvedConfig, "out serialized_config:s") { recover_config(solved: true) }
+ dbus_method(:GetConfigModel, "out serialized_model:s") { recover_model }
dbus_method(:Install) { install }
dbus_method(:Finish) { finish }
dbus_reader(:deprecated_system, "b")
@@ -458,7 +465,7 @@ def tree_path(tree_root)
# @return [Agama::Config]
def config
- backend.config
+ backend.product_config
end
# @return [Agama::VolumeTemplatesBuilder]
diff --git a/service/lib/agama/storage/config_conversions.rb b/service/lib/agama/storage/config_conversions.rb
index e8f49d38f5..1bcc863703 100644
--- a/service/lib/agama/storage/config_conversions.rb
+++ b/service/lib/agama/storage/config_conversions.rb
@@ -21,6 +21,7 @@
require "agama/storage/config_conversions/from_json"
require "agama/storage/config_conversions/to_json"
+require "agama/storage/config_conversions/to_model"
module Agama
module Storage
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 14e032d280..995604a9bc 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
@@ -45,6 +45,7 @@ def convert
# @return [Hash]
def conversions
{
+ default: false,
fs_type: convert_type,
btrfs: convert_btrfs
}
diff --git a/service/lib/agama/storage/config_conversions/to_model.rb b/service/lib/agama/storage/config_conversions/to_model.rb
new file mode 100644
index 0000000000..c59ad9f12d
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model.rb
@@ -0,0 +1,48 @@
+# 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/to_model_conversions/config"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ # Config conversion to model according to the JSON schema.
+ class ToModel
+ # @param config [Storage::Config]
+ def initialize(config)
+ @config = config
+ end
+
+ # Performs the conversion to config model according to the JSON schema.
+ #
+ # @return [Hash]
+ def convert
+ ToModelConversions::Config.new(config).convert
+ end
+
+ private
+
+ # @return [Storage::Config]
+ attr_reader :config
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions.rb b/service/lib/agama/storage/config_conversions/to_model_conversions.rb
new file mode 100644
index 0000000000..87dede1eda
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_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/to_model_conversions/base"
+require "agama/storage/config_conversions/to_model_conversions/config"
+require "agama/storage/config_conversions/to_model_conversions/drive"
+require "agama/storage/config_conversions/to_model_conversions/filesystem"
+require "agama/storage/config_conversions/to_model_conversions/partition"
+require "agama/storage/config_conversions/to_model_conversions/size"
+require "agama/storage/config_conversions/to_model_conversions/space_policy"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ # Conversions to model according to the JSON schema.
+ module ToModelConversions
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..e447dd1e11
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/base.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.
+
+module Agama
+ module Storage
+ 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]
+ def convert
+ model_json = {}
+
+ conversions.each do |property, value|
+ next if value.nil?
+
+ model_json[property] = value
+ end
+
+ model_json.empty? ? nil : model_json
+ end
+
+ private
+
+ # @return [Object] The config type is provided by the {.config_type} method.
+ attr_reader :config
+
+ # Values to generate the model.
+ #
+ # @return [Hash] e.g., { name: "/dev/vda" }.
+ def conversions
+ {}
+ end
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..94b65aeeab
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb
@@ -0,0 +1,57 @@
+# 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"
+require "agama/storage/config_conversions/to_model_conversions/base"
+require "agama/storage/config_conversions/to_model_conversions/drive"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module ToModelConversions
+ # Config conversion to model according to the JSON schema.
+ class Config < Base
+ # @see Base
+ def self.config_type
+ Storage::Config
+ end
+
+ private
+
+ # @see Base#conversions
+ def conversions
+ { drives: convert_drives }
+ end
+
+ # @return [Array]
+ def convert_drives
+ valid_drives.map { |d| ToModelConversions::Drive.new(d).convert }
+ end
+
+ # @return [Array]
+ def valid_drives
+ config.drives.select(&:found_device)
+ end
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..8bd54b4e62
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb
@@ -0,0 +1,61 @@
+# 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/to_model_conversions/base"
+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
+ module ConfigConversions
+ module ToModelConversions
+ # Drive conversion to model according to the JSON schema.
+ class Drive < Base
+ include WithFilesystem
+ include WithPartitions
+ include WithSpacePolicy
+
+ # @see Base
+ def self.config_type
+ Configs::Drive
+ end
+
+ private
+
+ # @see Base#conversions
+ def conversions
+ {
+ name: config.found_device&.name,
+ alias: config.alias,
+ mountPath: config.filesystem&.path,
+ filesystem: convert_filesystem,
+ spacePolicy: convert_space_policy,
+ ptableType: config.ptable_type&.to_s,
+ partitions: convert_partitions
+ }
+ end
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..c8b6b04f24
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.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/to_model_conversions/base"
+require "agama/storage/configs/filesystem"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module ToModelConversions
+ # Drive conversion to model according to the JSON schema.
+ class Filesystem < Base
+ # @see Base
+ def self.config_type
+ Configs::Filesystem
+ end
+
+ private
+
+ # @see Base#conversions
+ def conversions
+ {
+ default: convert_default,
+ type: convert_type,
+ snapshots: convert_snapshots
+ }
+ end
+
+ # @return [Boolean, nil]
+ def convert_default
+ return unless config.type
+
+ config.type.default?
+ end
+
+ # @return [String, nil]
+ def convert_type
+ return unless config.type&.fs_type
+
+ config.type.fs_type.to_s
+ end
+
+ # @return [Boolean, nil]
+ def convert_snapshots
+ return unless config.type&.fs_type&.is?(:btrfs)
+
+ config.type.btrfs&.snapshots?
+ end
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..f8ac908ba0
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb
@@ -0,0 +1,76 @@
+# 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/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
+ module ConfigConversions
+ module ToModelConversions
+ # Partition conversion to model according to the JSON schema.
+ class Partition < Base
+ include WithFilesystem
+ include WithSize
+
+ # @see Base
+ def self.config_type
+ Configs::Partition
+ end
+
+ private
+
+ # @see Base#conversions
+ def conversions
+ {
+ name: config.found_device&.name,
+ alias: config.alias,
+ id: config.id&.to_s,
+ mountPath: config.filesystem&.path,
+ filesystem: convert_filesystem,
+ size: convert_size,
+ delete: config.delete?,
+ deleteIfNeeded: config.delete_if_needed?,
+ resize: convert_resize,
+ resizeIfNeeded: convert_resize_if_needed
+ }
+ end
+
+ # @return [Booelan]
+ def convert_resize
+ size = config.size
+
+ !size.nil? && !size.default? && size.min == size.max
+ end
+
+ # @return [Booelan]
+ def convert_resize_if_needed
+ size = config.size
+
+ !size.nil? && !size.default? && size.min != size.max
+ end
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000..23e8217f73
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/size.rb
@@ -0,0 +1,58 @@
+# 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/to_model_conversions/base"
+require "agama/storage/configs/size"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module ToModelConversions
+ # Size conversion to model according to the JSON schema.
+ class Size < Base
+ # @see Base
+ def self.config_type
+ Configs::Size
+ end
+
+ private
+
+ # @see Base#conversions
+ def conversions
+ {
+ default: config.default?,
+ min: config.min&.to_i,
+ max: convert_max_size
+ }
+ end
+
+ # @return [Integer, nil]
+ def convert_max_size
+ max = config.max
+ return if max.nil? || max.unlimited?
+
+ max.to_i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/space_policy.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/space_policy.rb
new file mode 100644
index 0000000000..6b028b7614
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/space_policy.rb
@@ -0,0 +1,99 @@
+# 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 ToModelConversions
+ # Space policy conversion to model according to the JSON schema.
+ class SpacePolicy
+ # TODO: make it work with volume groups and raids too.
+ #
+ # @param config [Configs::Drive]
+ def initialize(config)
+ @config = config
+ end
+
+ # @return [String]
+ def convert
+ return "delete" if config.filesystem || delete_all_partition?
+ return "resize" if shrink_all_partition?
+ return "custom" if delete_partition? || resize_partition?
+
+ "keep"
+ end
+
+ private
+
+ # @return [Configs::Drive]
+ attr_reader :config
+
+ # @return [Boolean]
+ def delete_all_partition?
+ config.partitions.any? { |p| delete_all?(p) }
+ end
+
+ # @return [Boolean]
+ def shrink_all_partition?
+ config.partitions.any? { |p| shrink_all?(p) }
+ end
+
+ # @return [Boolean]
+ def delete_partition?
+ config.partitions
+ .select(&:found_device)
+ .any? { |p| p.delete? || p.delete_if_needed? }
+ end
+
+ # @return [Boolean]
+ def resize_partition?
+ config.partitions
+ .select(&:found_device)
+ .any? { |p| !p.size.default? }
+ end
+
+ # @param partition_config [Configs::Partition]
+ # @return [Boolean]
+ def delete_all?(partition_config)
+ search_all?(partition_config) && partition_config.delete?
+ end
+
+ # @param partition_config [Configs::Partition]
+ # @return [Boolean]
+ def shrink_all?(partition_config)
+ search_all?(partition_config) &&
+ !partition_config.size.nil? &&
+ !partition_config.size.min.nil? &&
+ partition_config.size.min.to_i == 0
+ end
+
+ # @param partition_config [Configs::Partition]
+ # @return [Boolean]
+ def search_all?(partition_config)
+ !partition_config.search.nil? &&
+ partition_config.search.always_match? &&
+ partition_config.search.max.nil?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/with_filesystem.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/with_filesystem.rb
new file mode 100644
index 0000000000..903d6609d5
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/with_filesystem.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/config_conversions/to_model_conversions/filesystem"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module ToModelConversions
+ # Mixin for filesystem conversion to model according to the JSON schema.
+ module WithFilesystem
+ # @return [Hash, nil]
+ def convert_filesystem
+ filesystem = config.filesystem
+ return unless filesystem
+
+ ToModelConversions::Filesystem.new(filesystem).convert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/with_partitions.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/with_partitions.rb
new file mode 100644
index 0000000000..d13152bb1e
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/with_partitions.rb
@@ -0,0 +1,66 @@
+# 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/to_model_conversions/partition"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module ToModelConversions
+ # Mixin for partitions conversion to model according to the JSON schema.
+ module WithPartitions
+ # @return [Array]
+ def convert_partitions
+ valid_partitions
+ .map { |p| ToModelConversions::Partition.new(p).convert }
+ .compact
+ end
+
+ # @return [Array]
+ def valid_partitions
+ config.partitions.select { |p| valid_partition?(p) }
+ end
+
+ # @param partition_config [Configs::Partition]
+ # @return [Boolean]
+ def valid_partition?(partition_config)
+ valid_new_partition(partition_config) || valid_existing_partition(partition_config)
+ end
+
+ # @param partition_config [Configs::Partition]
+ # @return [Boolean]
+ def valid_new_partition(partition_config)
+ delete = partition_config.delete? || partition_config.delete_if_needed?
+ return false if delete
+
+ partition_config.search.nil? || partition_config.search.create_device?
+ end
+
+ # @param partition_config [Configs::Partition]
+ # @return [Boolean]
+ def valid_existing_partition(partition_config)
+ !partition_config.found_device.nil?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/with_size.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/with_size.rb
new file mode 100644
index 0000000000..6844076b21
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/with_size.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/config_conversions/to_model_conversions/size"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module ToModelConversions
+ # Mixin for size conversion to model according to the JSON schema.
+ module WithSize
+ # @return [Hash, nil]
+ def convert_size
+ size = config.size
+ return unless size
+
+ ToModelConversions::Size.new(size).convert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/with_space_policy.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/with_space_policy.rb
new file mode 100644
index 0000000000..d87ae591c7
--- /dev/null
+++ b/service/lib/agama/storage/config_conversions/to_model_conversions/with_space_policy.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/to_model_conversions/space_policy"
+
+module Agama
+ module Storage
+ module ConfigConversions
+ module ToModelConversions
+ # Mixin for space policy conversion to model according to the JSON schema.
+ module WithSpacePolicy
+ # @return [String, nil]
+ def convert_space_policy
+ return unless config.respond_to?(:partitions)
+
+ ToModelConversions::SpacePolicy.new(config).convert
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/service/lib/agama/storage/config_reader.rb b/service/lib/agama/storage/config_json_reader.rb
similarity index 58%
rename from service/lib/agama/storage/config_reader.rb
rename to service/lib/agama/storage/config_json_reader.rb
index 64d05af17d..b17cb983d3 100644
--- a/service/lib/agama/storage/config_reader.rb
+++ b/service/lib/agama/storage/config_json_reader.rb
@@ -19,61 +19,30 @@
# 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"
-
module Agama
module Storage
- # Reader for the initial storage config
- class ConfigReader
- # @param agama_config [Agama::Config]
- def initialize(agama_config)
- @agama_config = agama_config
+ # Reader for the initial JSON config.
+ class ConfigJSONReader
+ # @param product_config [Agama::Config]
+ def initialize(product_config)
+ @product_config = product_config
end
- # Generates a storage config from the Agama control file.
+ # Generates a JSON config from the product config.
#
- # @return [Storage::Config]
+ # @return [Hash]
def read
- ConfigConversions::FromJSON.new(json, default_paths: default_paths).convert
+ json = product_config.lvm? ? json_for_lvm : json_for_disk
+
+ { storage: json }
end
private
# @return [Agama::Config]
- attr_reader :agama_config
-
- # Default filesystem paths from the Agama control file
- #
- # @return [Array]
- def default_paths
- @default_paths ||= agama_config.default_paths
- end
+ attr_reader :product_config
- # Default policy to make space from the Agama control file
- #
- # @return [String]
- def space_policy
- @space_policy ||= agama_config.data.dig("storage", "space_policy")
- end
-
- # Whether the Agama control file specifies that LVM must be used by default
- #
- # @return [Boolean]
- def lvm?
- return @lvm unless @lvm.nil?
-
- @lvm = !!agama_config.data.dig("storage", "lvm")
- end
-
- # JSON representation of the initial storage config
- #
- # @return [Hash]
- def json
- lvm? ? json_for_lvm : json_for_disk
- end
-
- # @see #json
- #
+ # @see #read
# @return [Hash]
def json_for_disk
{
@@ -83,17 +52,16 @@ def json_for_disk
}
end
- # @see #json
- #
+ # @see #read
# @return [Hash]
def json_for_lvm
+ partition = partition_for_existing
+
+ drive = { alias: "target" }
+ drive[:partitions] = [partition] if partition
+
{
- drives: [
- {
- alias: "target",
- partitions: [partition_for_existing].compact
- }
- ],
+ drives: [drive],
volumeGroups: [
{
name: "system",
@@ -104,17 +72,18 @@ def json_for_lvm
}
end
- # JSON piece to generate default filesystems as partitions or logical volumes
+ # JSON piece to generate default filesystems as partitions or logical volumes.
#
# @return [Hash]
def volumes_generator
{ generate: "default" }
end
- # JSON piece to specify what to do with existing partitions
+ # JSON piece to specify what to do with existing partitions.
#
- # @return [Hash, nil] nil if no actions are to be performed
+ # @return [Hash, nil] nil if no actions are to be performed.
def partition_for_existing
+ space_policy = product_config.space_policy
return unless ["delete", "resize"].include?(space_policy)
partition = { search: "*" }
diff --git a/service/lib/agama/storage/config_size_solver.rb b/service/lib/agama/storage/config_size_solver.rb
index bb8ffbc5b0..3785b96da3 100644
--- a/service/lib/agama/storage/config_size_solver.rb
+++ b/service/lib/agama/storage/config_size_solver.rb
@@ -80,12 +80,9 @@ def solve_default_device_size(config)
# @param config [Configs::Partition, Configs::LogicalVolume]
def solve_current_size(config)
- min = config.size.min
- max = config.size.max
size = size_from_device(config.found_device)
- size.min = min if min
- size.max = max if max
- config.size = size
+ config.size.min ||= size.min
+ config.size.max ||= size.max
end
# @param config [Configs::Partition, Configs::LogicalVolume]
@@ -107,7 +104,6 @@ def size_from_product(config)
# @return [Configs::Size]
def size_from_device(device)
Configs::Size.new.tap do |config|
- config.default = false
config.min = device.size
config.max = device.size
end
diff --git a/service/lib/agama/storage/configs/filesystem_type.rb b/service/lib/agama/storage/configs/filesystem_type.rb
index d9ef3cddf1..1b9cc04d19 100644
--- a/service/lib/agama/storage/configs/filesystem_type.rb
+++ b/service/lib/agama/storage/configs/filesystem_type.rb
@@ -22,12 +22,21 @@
module Agama
module Storage
module Configs
+ # Config for a filesystem type.
class FilesystemType
+ # @return [Boolean]
+ attr_accessor :default
+ alias_method :default?, :default
+
# @return [Y2Storage::Filesystems::Type, nil]
attr_accessor :fs_type
# @return [Configs::Btrfs, nil]
attr_accessor :btrfs
+
+ def initialize
+ @default = true
+ end
end
end
end
diff --git a/service/lib/agama/storage/configs/size.rb b/service/lib/agama/storage/configs/size.rb
index 63353fe46a..a84a774154 100644
--- a/service/lib/agama/storage/configs/size.rb
+++ b/service/lib/agama/storage/configs/size.rb
@@ -26,6 +26,7 @@ module Configs
class Size
# @return [Boolean]
attr_accessor :default
+ alias_method :default?, :default
# @return [Y2Storage::DiskSize, nil]
attr_accessor :min
@@ -37,11 +38,6 @@ class Size
def initialize
@default = true
end
-
- # @return [Boolean]
- def default?
- @default
- end
end
end
end
diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb
index be84d91fea..9689523d9e 100644
--- a/service/lib/agama/storage/manager.rb
+++ b/service/lib/agama/storage/manager.rb
@@ -29,7 +29,7 @@
require "agama/storage/callbacks"
require "agama/storage/iscsi/manager"
require "agama/storage/finisher"
-require "agama/storage/config_reader"
+require "agama/storage/config_json_reader"
require "agama/issue"
require "agama/with_locale"
require "agama/with_issues"
@@ -49,17 +49,17 @@ class Manager
include WithProgress
include Yast::I18n
- # @return [Config]
- attr_reader :config
+ # @return [Agama::Config]
+ attr_reader :product_config
# Constructor
#
- # @param config [Config]
+ # @param product_config [Agama::Config]
# @param logger [Logger]
- def initialize(config, logger)
+ def initialize(product_config, logger)
textdomain "agama"
- @config = config
+ @product_config = product_config
@logger = logger
register_proposal_callbacks
on_progress_change { logger.info progress.to_s }
@@ -110,7 +110,7 @@ def on_probe(&block)
# Probes storage devices and performs an initial proposal
def probe
start_progress_with_size(4)
- config.pick_product(software.selected_product)
+ product_config.pick_product(software.selected_product)
check_multipath
progress.step(_("Activating storage devices")) { activate_devices }
progress.step(_("Probing storage devices")) { probe_devices }
@@ -139,14 +139,14 @@ def install
# Performs the final steps on the target file system(s)
def finish
- Finisher.new(logger, config, security).run
+ Finisher.new(logger, product_config, security).run
end
# Storage proposal manager
#
# @return [Storage::Proposal]
def proposal
- @proposal ||= Proposal.new(config, logger: logger)
+ @proposal ||= Proposal.new(product_config, logger: logger)
end
# iSCSI manager
@@ -214,10 +214,10 @@ def probe_devices
self.deprecated_system = false
end
- # Calculates the proposal using the settings from the config file.
+ # Calculates the proposal using the storage config from the product.
def calculate_proposal
- settings = ConfigReader.new(config).read
- proposal.calculate_agama(settings)
+ config_json = ConfigJSONReader.new(product_config).read
+ proposal.calculate_from_json(config_json)
end
# Adds the required packages to the list of resolvables to install
@@ -297,7 +297,7 @@ def available_devices_issue
#
# @return [Security]
def security
- @security ||= Security.new(logger, config)
+ @security ||= Security.new(logger, product_config)
end
# Returns the client to ask questions
diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb
index 7f179692b3..74531aaf1e 100644
--- a/service/lib/agama/storage/proposal.rb
+++ b/service/lib/agama/storage/proposal.rb
@@ -23,6 +23,7 @@
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/proposal_settings"
require "agama/storage/proposal_strategies"
require "json"
@@ -71,13 +72,10 @@ def available_devices
disk_analyzer&.candidate_disks || []
end
- # Storage JSON config from the current proposal, if any.
+ # Storage config according to the JSON schema from the current proposal.
#
- # @param solved [Boolean] Whether to get the solved config.
- # @return [Hash] JSON config according to the JSON schema.
- def storage_json(solved: false)
- return source_json if !solved && source_json
-
+ # @return [Hash, nil] nil if there is no proposal yet.
+ def storage_json
case strategy
when ProposalStrategies::Guided
{
@@ -86,17 +84,24 @@ def storage_json(solved: false)
}
}
when ProposalStrategies::Agama
- config = config(solved: solved)
- {
- storage: ConfigConversions::ToJSON.new(config).convert
- }
+ source_json || { storage: ConfigConversions::ToJSON.new(config).convert }
when ProposalStrategies::Autoyast
- strategy.settings
- else
- {}
+ source_json || {
+ legacyAutoyastStorage: JSON.parse(strategy.settings.to_json, symbolize_names: true)
+ }
end
end
+ # Config model according to the JSON schema.
+ #
+ # @return [Hash, nil] nil if there is no config.
+ def model_json
+ config = config(solved: true)
+ return unless config
+
+ ConfigConversions::ToModel.new(config).convert
+ end
+
# Calculates a new proposal using the given JSON.
#
# @raise If the JSON is not valid.
diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb
index 53f312caf3..7381474efb 100644
--- a/service/test/agama/dbus/storage/manager_test.rb
+++ b/service/test/agama/dbus/storage/manager_test.rb
@@ -49,7 +49,7 @@
proposal: proposal,
iscsi: iscsi,
software: software,
- config: config,
+ product_config: product_config,
on_probe: nil,
on_progress_change: nil,
on_progress_finish: nil,
@@ -57,10 +57,10 @@
on_deprecated_system_change: nil)
end
- let(:config) { Agama::Config.new(config_data) }
+ let(:product_config) { Agama::Config.new(config_data) }
let(:config_data) { {} }
- let(:proposal) { Agama::Storage::Proposal.new(config) }
+ let(:proposal) { Agama::Storage::Proposal.new(product_config) }
let(:iscsi) do
instance_double(Agama::Storage::ISCSI::Manager,
@@ -563,8 +563,8 @@ def serialize(value)
end
context "if a proposal has not been calculated" do
- it "returns serialized empty storage config" do
- expect(subject.recover_config).to eq(serialize({}))
+ it "returns 'null'" do
+ expect(subject.recover_config).to eq("null")
end
end
@@ -583,37 +583,25 @@ def serialize(value)
}
end
- context "and unsolved config is requested" do
- let(:solved) { false }
-
- it "returns serialized unsolved guided storage config" do
- expect(subject.recover_config(solved: solved)).to eq(serialize(settings_json))
- end
- end
-
- context "and solved config is requested" do
- let(:solved) { true }
-
- it "returns serialized solved guided storage config" do
- expect(subject.recover_config(solved: solved)).to eq(
- serialize({
- storage: {
- guided: {
- target: {
- disk: "/dev/vda"
- },
- boot: {
- configure: true
- },
- space: {
- policy: "keep"
- },
- volumes: []
- }
+ it "returns serialized solved guided storage config" do
+ expect(subject.recover_config).to eq(
+ serialize({
+ storage: {
+ guided: {
+ target: {
+ disk: "/dev/vda"
+ },
+ boot: {
+ configure: true
+ },
+ space: {
+ policy: "keep"
+ },
+ volumes: []
}
- })
- )
- end
+ }
+ })
+ )
end
end
@@ -638,52 +626,110 @@ def serialize(value)
}
end
- context "and unsolved config is requested" do
- let(:solved) { false }
+ it "returns serialized storage config" do
+ expect(subject.recover_config).to eq(serialize(config_json))
+ end
+ end
- it "returns serialized unsolved storage config" do
- expect(subject.recover_config(solved: solved)).to eq(serialize(config_json))
- end
+ context "if an AutoYaST proposal has been calculated" do
+ before do
+ proposal.calculate_from_json(autoyast_json)
+ end
+
+ let(:autoyast_json) do
+ {
+ legacyAutoyastStorage: [
+ { device: "/dev/vda" }
+ ]
+ }
end
- context "and solved config is requested" do
- let(:solved) { true }
+ it "returns the serialized AutoYaST config" do
+ expect(subject.recover_config).to eq(serialize(autoyast_json))
+ end
+ end
+ end
- it "returns serialized solved guided storage config" do
- expect(subject.recover_config(solved: solved)).to eq(
- serialize({
- storage: {
- boot: {
- configure: true
- },
- drives: [
+ describe "#recover_model" do
+ def serialize(value)
+ JSON.pretty_generate(value)
+ end
+
+ context "if a proposal has not been calculated" do
+ it "returns 'null'" do
+ expect(subject.recover_model).to eq("null")
+ end
+ end
+
+ context "if a guided proposal has been calculated" do
+ before do
+ proposal.calculate_from_json(settings_json)
+ end
+
+ let(:settings_json) do
+ {
+ storage: {
+ guided: {
+ target: { disk: "/dev/vda" }
+ }
+ }
+ }
+ end
+
+ it "returns 'null'" do
+ expect(subject.recover_model).to eq("null")
+ end
+ end
+
+ context "if an agama proposal has been calculated" do
+ before do
+ proposal.calculate_from_json(config_json)
+ end
+
+ let(:config_json) do
+ {
+ storage: {
+ drives: [
+ {
+ partitions: [
{
- search: {
- condition: { name: "/dev/sda" },
- ifNotFound: "error",
- max: 1
+ filesystem: { path: "/" }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ end
+
+ it "returns the serialized config model" do
+ expect(subject.recover_model).to eq(
+ serialize({
+ drives: [
+ {
+ name: "/dev/sda",
+ spacePolicy: "keep",
+ partitions: [
+ {
+ mountPath: "/",
+ filesystem: {
+ default: true,
+ type: "ext4"
},
- partitions: [
- {
- filesystem: {
- reuseIfPossible: false,
- path: "/",
- mountOptions: [],
- mkfsOptions: [],
- type: "ext4"
- },
- size: {
- min: 0
- }
- }
- ]
+ size: {
+ default: true,
+ min: 0
+ },
+ delete: false,
+ deleteIfNeeded: false,
+ resize: false,
+ resizeIfNeeded: false
}
- ],
- volumeGroups: []
+ ]
}
- })
- )
- end
+ ]
+ })
+ )
end
end
@@ -700,8 +746,8 @@ def serialize(value)
}
end
- it "returns the serialized AutoYaST config" do
- expect(subject.recover_config).to eq(serialize(autoyast_json))
+ it "returns 'null'" do
+ expect(subject.recover_model).to eq("null")
end
end
end
diff --git a/service/test/agama/storage/config_conversions/from_json_test.rb b/service/test/agama/storage/config_conversions/from_json_test.rb
index 2f46c8e134..62aa519836 100644
--- a/service/test/agama/storage/config_conversions/from_json_test.rb
+++ b/service/test/agama/storage/config_conversions/from_json_test.rb
@@ -273,6 +273,7 @@
filesystem = config.filesystem
expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem)
expect(filesystem.reuse?).to eq(true)
+ 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 eq("test")
@@ -298,6 +299,7 @@
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.snapshots?).to eq(true)
expect(filesystem.label).to be_nil
diff --git a/service/test/agama/storage/config_conversions/to_model_test.rb b/service/test/agama/storage/config_conversions/to_model_test.rb
new file mode 100644
index 0000000000..fed7db4b8f
--- /dev/null
+++ b/service/test/agama/storage/config_conversions/to_model_test.rb
@@ -0,0 +1,736 @@
+# 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 "../storage_helpers"
+require_relative "../../../test_helper"
+require "agama/storage/config_conversions"
+require "agama/storage/config_solver"
+require "y2storage/refinements"
+
+using Y2Storage::Refinements::SizeCasts
+
+shared_examples "without name" do |result_scope|
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json.keys).to_not include(:name)
+ end
+end
+
+shared_examples "without alias" do |result_scope|
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json.keys).to_not include(:alias)
+ end
+end
+
+shared_examples "without filesystem" do |result_scope|
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json.keys).to_not include(:mountPath)
+ expect(model_json.keys).to_not include(:filesystem)
+ end
+end
+
+shared_examples "without ptable_type" do |result_scope|
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json.keys).to_not include(:ptableType)
+ end
+end
+
+shared_examples "without partitions" do |result_scope|
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("keep")
+ expect(model_json[:partitions]).to eq([])
+ end
+end
+
+shared_examples "with name" do |result_scope, device_scope|
+ let(:search) { device.name }
+ let(:device) { device_scope.call(devicegraph) }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:name]).to eq(device.name)
+ end
+end
+
+shared_examples "with alias" do |result_scope|
+ let(:device_alias) { "test" }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:alias]).to eq("test")
+ end
+end
+
+shared_examples "with filesystem" do |result_scope|
+ let(:filesystem) do
+ {
+ reuseIfPossible: true,
+ type: "xfs",
+ label: "test",
+ path: "/test",
+ mountBy: "device",
+ mkfsOptions: ["version=2"],
+ mountOptions: ["rw"]
+ }
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:mountPath]).to eq("/test")
+ expect(model_json[:filesystem]).to eq(
+ {
+ default: false,
+ type: "xfs"
+ }
+ )
+ end
+
+ context "with a default filesystem" do
+ let(:filesystem) { { path: "/" } }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:mountPath]).to eq("/")
+ expect(model_json[:filesystem]).to eq(
+ {
+ default: true,
+ type: "btrfs",
+ snapshots: true
+ }
+ )
+ end
+ end
+end
+
+shared_examples "with ptable_type" do |result_scope|
+ let(:ptableType) { "gpt" }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:ptableType]).to eq("gpt")
+ end
+end
+
+shared_examples "with partitions" do |result_scope, device_scope|
+ let(:partitions) do
+ [
+ partition,
+ {}
+ ]
+ end
+
+ let(:partition) { {} }
+
+ let(:default_partition_json) do
+ {
+ delete: false,
+ deleteIfNeeded: false,
+ resize: false,
+ resizeIfNeeded: false,
+ size: { default: true, min: 100.MiB.to_i }
+ }
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:partitions]).to eq(
+ [
+ default_partition_json,
+ default_partition_json
+ ]
+ )
+ end
+
+ context "if a partition is set to delete without device" do
+ let(:partition) { { delete: true } }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:partitions]).to eq(
+ [
+ default_partition_json
+ ]
+ )
+ end
+ end
+
+ context "if a partition is set to delete if needed without device" do
+ let(:partition) { { deleteIfNeeded: true } }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:partitions]).to eq(
+ [
+ default_partition_json
+ ]
+ )
+ end
+ end
+
+ context "if a device is not found for a partition" do
+ let(:partition) { { search: "/not/found" } }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:partitions]).to eq(
+ [
+ default_partition_json
+ ]
+ )
+ end
+ end
+
+ context "if a partition should be created if not found" do
+ let(:partition) do
+ {
+ search: {
+ condition: { name: "/not/found" },
+ ifNotFound: "create"
+ }
+ }
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:partitions]).to eq(
+ [
+ default_partition_json,
+ default_partition_json
+ ]
+ )
+ end
+ end
+
+ context "if a device is found for a partition" do
+ # The device should have at least one partition.
+ let(:partition_device) { device_scope.call(devicegraph).partitions.first }
+
+ let(:partition) { { search: partition_device.name } }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:partitions]).to eq(
+ [
+ {
+ name: partition_device.name,
+ delete: false,
+ deleteIfNeeded: false,
+ resize: false,
+ resizeIfNeeded: false,
+ size: {
+ default: true,
+ min: partition_device.size.to_i,
+ max: partition_device.size.to_i
+ }
+ },
+ default_partition_json
+ ]
+ )
+ end
+ end
+
+ partition_result_scope = proc { |c| result_scope.call(c)[:partitions].first }
+ partition_scope = proc { |c| device_scope.call(c).partitions.first }
+
+ context "if a device is not assigned to a partition" do
+ let(:partition) { {} }
+ include_examples "without name", partition_result_scope
+ end
+
+ context "if #alias is not configured for a partition" do
+ let(:partition) { {} }
+ include_examples "without alias", partition_result_scope
+ end
+
+ context "if #id is not configured for a partition" do
+ let(:partition) { {} }
+
+ it "generates the expected JSON" do
+ model_json = partition_result_scope.call(subject.convert)
+ expect(model_json.keys).to_not include(:id)
+ end
+ end
+
+ context "if #filesystem is not configured for a partition" do
+ let(:partition) { {} }
+ include_examples "without filesystem", partition_result_scope
+ end
+
+ context "if a device is assigned to a partition" do
+ let(:partition) { { search: search } }
+ include_examples "with name", partition_result_scope, partition_scope
+ end
+
+ context "if #alias is configured for a partition" do
+ let(:partition) { { alias: device_alias } }
+ include_examples "with alias", partition_result_scope
+ end
+
+ context "if #id is configured for a partition" do
+ let(:partition) { { id: "esp" } }
+
+ it "generates the expected JSON" do
+ model_json = partition_result_scope.call(subject.convert)
+ expect(model_json[:id]).to eq("esp")
+ end
+ end
+
+ context "if #filesystem is configured for a partition" do
+ let(:partition) { { filesystem: filesystem } }
+ include_examples "with filesystem", partition_result_scope
+ end
+
+ context "for the #size property" do
+ let(:partition) { { search: search, size: size } }
+ include_examples "#size property", partition_result_scope, partition_scope
+ end
+
+ context "for the #delete property" do
+ let(:partition) { { search: device.name, delete: delete } }
+ let(:device) { partition_scope.call(devicegraph) }
+ include_examples "#delete property", partition_result_scope
+ end
+
+ context "for the #deleteIfNeeded property" do
+ let(:partition) { { search: device.name, deleteIfNeeded: delete_if_needed } }
+ let(:device) { partition_scope.call(devicegraph) }
+ include_examples "#deleteIfNeeded property", partition_result_scope
+ end
+end
+
+shared_examples "#spacePolicy property" do |result_scope|
+ context "if there is a 'delete all' partition" do
+ let(:partitions) do
+ [
+ { search: "*", delete: true },
+ { size: "2 GiB" }
+ ]
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("delete")
+ end
+ end
+
+ context "if there is a 'resize all' partition" do
+ let(:partitions) do
+ [
+ { search: "*", size: { min: 0, max: "current" } },
+ { size: "2 GiB" }
+ ]
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("resize")
+ end
+ end
+
+ context "if there is a 'delete' partition" do
+ let(:partitions) do
+ [
+ { search: { max: 1 }, delete: true },
+ { filesystem: { path: "/" } }
+ ]
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("custom")
+ end
+ end
+
+ context "if there is a 'delete if needed' partition" do
+ let(:partitions) do
+ [
+ { search: { max: 1 }, deleteIfNeeded: true },
+ { filesystem: { path: "/" } }
+ ]
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("custom")
+ end
+ end
+
+ context "if there is a 'resize' partition" do
+ let(:partitions) do
+ [
+ { search: { max: 1 }, size: "1 GiB" },
+ { filesystem: { path: "/" } }
+ ]
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("custom")
+ end
+ end
+
+ context "if there is a 'resize if needed' partition" do
+ let(:partitions) do
+ [
+ { search: { max: 1 }, size: { min: 0, max: "1 GiB" } },
+ { filesystem: { path: "/" } }
+ ]
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("custom")
+ end
+ end
+
+ context "if there is neither 'delete' nor 'resize' partition" do
+ let(:partitions) do
+ [
+ { size: { min: "1 GiB" } },
+ { filesystem: { path: "/" } }
+ ]
+ end
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:spacePolicy]).to eq("keep")
+ end
+ end
+end
+
+shared_examples "#size property" do |result_scope, partition_scope|
+ context "if there is not device assigned to the config" do
+ let(:search) { nil }
+
+ context "if the config contains the default product size" do
+ let(:size) { nil }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:size]).to eq(
+ {
+ default: true,
+ min: 100.MiB.to_i
+ }
+ )
+ end
+ end
+
+ context "if the config contains a specific size" do
+ let(:size) { "10 GiB" }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:size]).to eq(
+ {
+ default: false,
+ min: 10.GiB.to_i,
+ max: 10.GiB.to_i
+ }
+ )
+ end
+ end
+ end
+
+ context "if there is a device assigned to the config" do
+ let(:search) { device.name }
+ let(:device) { partition_scope.call(devicegraph) }
+
+ context "if the config contains the default product size" do
+ let(:size) { nil }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:size]).to eq(
+ {
+ default: true,
+ min: device.size.to_i,
+ max: device.size.to_i
+ }
+ )
+ end
+ end
+
+ context "if the config contains a specific size" do
+ let(:size) { "10 GiB" }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:size]).to eq(
+ {
+ default: false,
+ min: 10.GiB.to_i,
+ max: 10.GiB.to_i
+ }
+ )
+ end
+ end
+ end
+end
+
+shared_examples "#delete property" do |result_scope|
+ context "if #delete is set to false" do
+ let(:delete) { false }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:delete]).to eq(false)
+ end
+ end
+
+ context "if #delete is set to true" do
+ let(:delete) { true }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:delete]).to eq(true)
+ end
+ end
+end
+
+shared_examples "#deleteIfNeeded property" do |result_scope|
+ context "if #delete_if_needed is set to false" do
+ let(:delete_if_needed) { false }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:deleteIfNeeded]).to eq(false)
+ end
+ end
+
+ context "if #delete_if_needed is set to true" do
+ let(:delete_if_needed) { true }
+
+ it "generates the expected JSON" do
+ model_json = result_scope.call(subject.convert)
+ expect(model_json[:deleteIfNeeded]).to eq(true)
+ end
+ end
+end
+
+describe Agama::Storage::ConfigConversions::ToModel do
+ include Agama::RSpec::StorageHelpers
+
+ let(:product_data) do
+ {
+ "storage" => {
+ "volumes" => ["/", "swap"],
+ "volume_templates" => [
+ {
+ "mount_path" => "/",
+ "filesystem" => "btrfs",
+ "size" => {
+ "auto" => true,
+ "min" => "5 GiB",
+ "max" => "10 GiB"
+ },
+ "btrfs" => {
+ "snapshots" => true
+ },
+ "outline" => {
+ "required" => true,
+ "snapshots_configurable" => true,
+ "auto_size" => {
+ "base_min" => "5 GiB",
+ "base_max" => "10 GiB"
+ }
+ }
+ },
+ {
+ "mount_path" => "/home",
+ "filesystem" => "xfs",
+ "size" => {
+ "auto" => false,
+ "min" => "5 GiB"
+ },
+ "outline" => {
+ "required" => false
+ }
+ },
+ {
+ "mount_path" => "swap",
+ "filesystem" => "swap",
+ "size" => {
+ "auto" => true
+ },
+ "outline" => {
+ "auto_size" => {
+ "base_min" => "2 GiB",
+ "base_max" => "4 GiB"
+ }
+ }
+ },
+ {
+ "mount_path" => "",
+ "filesystem" => "ext4",
+ "size" => {
+ "min" => "100 MiB"
+ }
+ }
+ ]
+ }
+ }
+ end
+
+ let(:product_config) { Agama::Config.new(product_data) }
+
+ let(:devicegraph) { Y2Storage::StorageManager.instance.probed }
+
+ let(:config) do
+ Agama::Storage::ConfigConversions::FromJSON
+ .new(config_json)
+ .convert
+ .tap { |c| Agama::Storage::ConfigSolver.new(devicegraph, product_config).solve(c) }
+ end
+
+ before do
+ mock_storage(devicegraph: scenario)
+ # To speed-up the tests
+ allow(Y2Storage::EncryptionMethod::TPM_FDE)
+ .to(receive(:possible?))
+ .and_return(true)
+ end
+
+ subject { described_class.new(config) }
+
+ describe "#convert" do
+ let(:scenario) { "disks.yaml" }
+
+ context "with the default config" do
+ let(:config_json) { {} }
+
+ it "generates the expected JSON" do
+ expect(subject.convert).to eq(
+ {
+ drives: []
+ }
+ )
+ end
+ end
+
+ context "if #drives is configured" do
+ let(:config_json) do
+ { drives: drives }
+ end
+
+ let(:drives) do
+ [
+ drive,
+ {}
+ ]
+ end
+
+ let(:drive) { {} }
+
+ it "generates the expected JSON for 'drives'" do
+ drives_json = subject.convert[:drives]
+
+ expect(drives_json).to eq(
+ [
+ { name: "/dev/vda", spacePolicy: "keep", partitions: [] },
+ { name: "/dev/vdb", spacePolicy: "keep", partitions: [] }
+ ]
+ )
+ end
+
+ context "if a device is not found for a drive" do
+ let(:drive) { { search: "/dev/vdd" } }
+
+ it "generates the expected JSON for 'drives'" do
+ drives_json = subject.convert[:drives]
+
+ expect(drives_json).to eq(
+ [
+ { name: "/dev/vda", spacePolicy: "keep", partitions: [] }
+ ]
+ )
+ end
+ end
+
+ context "if a device is found for a drive" do
+ let(:drive) { { search: "/dev/vda" } }
+
+ it "generates the expected JSON for 'drives'" do
+ drives_json = subject.convert[:drives]
+
+ expect(drives_json).to eq(
+ [
+ { name: "/dev/vda", spacePolicy: "keep", partitions: [] },
+ { name: "/dev/vdb", spacePolicy: "keep", partitions: [] }
+ ]
+ )
+ end
+ end
+
+ drive_result_scope = proc { |c| c[:drives].first }
+ drive_scope = proc { |d| d.find_by_name("/dev/vda") }
+
+ context "if #alias is not configured for a drive" do
+ let(:drive) { {} }
+ include_examples "without alias", drive_result_scope
+ end
+
+ context "if #filesystem is not configured for a drive" do
+ let(:drive) { {} }
+ include_examples "without filesystem", drive_result_scope
+ end
+
+ context "if #ptable_type is not configured for a drive" do
+ let(:drive) { {} }
+ include_examples "without ptable_type", drive_result_scope
+ end
+
+ context "if #partitions is not configured for a drive" do
+ let(:drive) { {} }
+ include_examples "without partitions", drive_result_scope
+ end
+
+ context "if #alias is configured for a drive" do
+ let(:drive) { { alias: device_alias } }
+ include_examples "with alias", drive_result_scope
+ end
+
+ context "if #filesystem is configured for a drive" do
+ let(:drive) { { filesystem: filesystem } }
+ include_examples "with filesystem", drive_result_scope
+ end
+
+ context "if #ptable_type is configured for a drive" do
+ let(:drive) { { ptableType: ptableType } }
+ include_examples "with ptable_type", drive_result_scope
+ end
+
+ context "if #partitions is configured for a drive" do
+ let(:drive) { { partitions: partitions } }
+ include_examples "with partitions", drive_result_scope, drive_scope
+ end
+
+ context "for the #spacePolicy property" do
+ let(:drive) { { partitions: partitions } }
+ include_examples "#spacePolicy property", drive_result_scope
+ end
+ end
+ end
+end
diff --git a/service/test/agama/storage/config_json_reader_test.rb b/service/test/agama/storage/config_json_reader_test.rb
new file mode 100644
index 0000000000..6aa6c3aa77
--- /dev/null
+++ b/service/test/agama/storage/config_json_reader_test.rb
@@ -0,0 +1,252 @@
+# 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_json_reader"
+
+describe Agama::Storage::ConfigJSONReader do
+ let(:product_config) { Agama::Config.new(config_data) }
+
+ subject { described_class.new(product_config) }
+
+ describe "#read" do
+ let(:config_data) do
+ {
+ "storage" => {
+ "lvm" => lvm,
+ "space_policy" => space_policy,
+ "encryption" => {
+ "method" => "luks2",
+ "pbkd_function" => "argon2id"
+ },
+ "volumes" => ["/", "swap"],
+ "volume_templates" => [
+ {
+ "mount_path" => "/",
+ "outline" => { "required" => true }
+ },
+ {
+ "mount_path" => "/home",
+ "outline" => { "required" => false }
+ },
+ {
+ "mount_path" => "swap",
+ "outline" => { "required" => false }
+ }
+ ]
+ }
+ }
+ end
+
+ context "if lvm is disabled" do
+ let(:lvm) { false }
+
+ context "and the space policy is 'delete'" do
+ let(:space_policy) { "delete" }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ {
+ partitions: [
+ { search: "*", delete: true },
+ { generate: "default" }
+ ]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context "and the space policy is 'resize'" do
+ let(:space_policy) { "resize" }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ {
+ partitions: [
+ { search: "*", size: { min: 0, max: "current" } },
+ { generate: "default" }
+ ]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context "and the space policy is 'keep'" do
+ let(:space_policy) { "keep" }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ {
+ partitions: [
+ { generate: "default" }
+ ]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context "and the space policy is unknown" do
+ let(:space_policy) { nil }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ {
+ partitions: [
+ { generate: "default" }
+ ]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+ end
+
+ context "if lvm is enabled" do
+ let(:lvm) { true }
+
+ context "and the space policy is 'delete'" do
+ let(:space_policy) { "delete" }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ {
+ alias: "target",
+ partitions: [
+ { search: "*", delete: true }
+ ]
+ }
+ ],
+ volumeGroups: [
+ {
+ name: "system",
+ physicalVolumes: [{ generate: ["target"] }],
+ logicalVolumes: [{ generate: "default" }]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context "and the space policy is 'resize'" do
+ let(:space_policy) { "resize" }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ {
+ alias: "target",
+ partitions: [
+ { search: "*", size: { min: 0, max: "current" } }
+ ]
+ }
+ ],
+ volumeGroups: [
+ {
+ name: "system",
+ physicalVolumes: [{ generate: ["target"] }],
+ logicalVolumes: [{ generate: "default" }]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context "and the space policy is 'keep'" do
+ let(:space_policy) { "keep" }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ { alias: "target" }
+ ],
+ volumeGroups: [
+ {
+ name: "system",
+ physicalVolumes: [{ generate: ["target"] }],
+ logicalVolumes: [{ generate: "default" }]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context "and the space policy is 'keep'" do
+ let(:space_policy) { nil }
+
+ it "generates the expected JSON" do
+ expect(subject.read).to eq(
+ {
+ storage: {
+ drives: [
+ { alias: "target" }
+ ],
+ volumeGroups: [
+ {
+ name: "system",
+ physicalVolumes: [{ generate: ["target"] }],
+ logicalVolumes: [{ generate: "default" }]
+ }
+ ]
+ }
+ }
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/service/test/agama/storage/config_reader_test.rb b/service/test/agama/storage/config_reader_test.rb
deleted file mode 100644
index 4e021a9a04..0000000000
--- a/service/test/agama/storage/config_reader_test.rb
+++ /dev/null
@@ -1,180 +0,0 @@
-# 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/device_settings"
-require "agama/storage/config_reader"
-require "y2storage"
-
-describe Agama::Storage::ConfigReader do
- let(:agama_config) { Agama::Config.new(config_data) }
-
- subject { described_class.new(agama_config) }
-
- describe "#read" do
- let(:lvm) { false }
- let(:space_policy) { "delete" }
- let(:config_data) do
- {
- "storage" => {
- "lvm" => lvm,
- "space_policy" => space_policy,
- "encryption" => {
- "method" => "luks2",
- "pbkd_function" => "argon2id"
- },
- "volumes" => ["/", "swap"],
- "volume_templates" => [
- {
- "mount_path" => "/",
- "outline" => { "required" => true }
- },
- {
- "mount_path" => "/home",
- "outline" => { "required" => false }
- },
- {
- "mount_path" => "swap",
- "outline" => { "required" => false }
- }
- ]
- }
- }
- end
-
- it "generates the corresponding storage configuration" do
- config = subject.read
- expect(config).to be_a(Agama::Storage::Config)
- expect(config.drives.size).to eq 1
- end
-
- context "if lvm is disabled" do
- let(:lvm) { false }
-
- it "applies the space policy to the first drive and places the default volumes there" do
- config = subject.read
- expect(config.drives.size).to eq 1
-
- partitions = config.drives.first.partitions
- expect(partitions).to contain_exactly(
- an_object_having_attributes(
- search: an_instance_of(Agama::Storage::Configs::Search), filesystem: nil
- ),
- an_object_having_attributes(
- search: nil, filesystem: an_object_having_attributes(path: "/")
- ),
- an_object_having_attributes(
- search: nil, filesystem: an_object_having_attributes(path: "swap")
- )
- )
- end
- end
-
- context "if lvm is enabled" do
- let(:lvm) { true }
-
- it "applies the space policy to the first drive" do
- config = subject.read
- expect(config.drives.size).to eq 1
-
- partitions = config.drives.first.partitions
- expect(partitions.size).to eq 1
- partition = partitions.first
- expect(partition.search).to be_a Agama::Storage::Configs::Search
- end
-
- it "places the default volumes at a new LVM over the first disk" do
- config = subject.read
- expect(config.volume_groups.size).to eq 1
- vg = config.volume_groups.first
- disk_alias = config.drives.first.alias
- expect(vg.physical_volumes_devices).to contain_exactly disk_alias
-
- expect(vg.logical_volumes).to contain_exactly(
- an_object_having_attributes(filesystem: an_object_having_attributes(path: "/")),
- an_object_having_attributes(filesystem: an_object_having_attributes(path: "swap"))
- )
- end
- end
-
- context "if the space policy is unknown" do
- let(:space_policy) { nil }
-
- it "generates no partitition config to match existing partitions" do
- config = subject.read
- partitions = config.drives.first.partitions
- expect(partitions).to_not include(
- an_object_having_attributes(search: an_instance_of(Agama::Storage::Configs::Search))
- )
- end
- end
-
- context "if the space policy is 'keep'" do
- let(:space_policy) { "keep" }
-
- it "generates no partitition config to match existing partitions" do
- config = subject.read
- partitions = config.drives.first.partitions
- expect(partitions).to_not include(
- an_object_having_attributes(search: an_instance_of(Agama::Storage::Configs::Search))
- )
- end
- end
-
- context "if the space policy is 'delete'" do
- let(:space_policy) { "delete" }
-
- it "generates a partitition config to delete existing partitions" do
- config = subject.read
- partitions = config.drives.first.partitions
- expect(partitions).to include(
- an_object_having_attributes(search: an_instance_of(Agama::Storage::Configs::Search))
- )
-
- search_part = partitions.find(&:search)
- expect(search_part.delete).to eq true
- expect(search_part.search.name).to be_nil
- expect(search_part.search.if_not_found).to eq :skip
- end
- end
-
- context "if the space policy is 'resize'" do
- let(:space_policy) { "resize" }
-
- it "generates a partitition config to shrink existing partitions" do
- config = subject.read
- partitions = config.drives.first.partitions
- expect(partitions).to include(
- an_object_having_attributes(search: an_instance_of(Agama::Storage::Configs::Search))
- )
-
- search_part = partitions.find(&:search)
- expect(search_part.delete).to eq false
- expect(search_part.size).to have_attributes(
- default: false, min: Y2Storage::DiskSize.zero, max: nil
- )
- expect(search_part.search.name).to be_nil
- expect(search_part.search.if_not_found).to eq :skip
- 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 eb0cc66949..1ef682034f 100644
--- a/service/test/agama/storage/config_solver_test.rb
+++ b/service/test/agama/storage/config_solver_test.rb
@@ -185,6 +185,7 @@
drive = config.drives.first
filesystem = drive.filesystem
expect(filesystem.type).to be_a(Agama::Storage::Configs::FilesystemType)
+ 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)
@@ -288,7 +289,7 @@
it "sets the device size" do
subject.solve(config)
partition = partition_proc.call(config)
- expect(partition.size.default?).to eq(false)
+ expect(partition.size.default?).to eq(true)
expect(partition.size.min).to eq(20.GiB)
expect(partition.size.max).to eq(20.GiB)
end
diff --git a/service/test/agama/storage/manager_test.rb b/service/test/agama/storage/manager_test.rb
index ce1fcbf211..c942dc7fc7 100644
--- a/service/test/agama/storage/manager_test.rb
+++ b/service/test/agama/storage/manager_test.rb
@@ -22,16 +22,17 @@
require_relative "../../test_helper"
require_relative "../with_progress_examples"
require_relative "../with_issues_examples"
-require_relative "storage_helpers"
+require_relative "./storage_helpers"
+require "agama/dbus/clients/questions"
+require "agama/config"
+require "agama/http"
+require "agama/issue"
+require "agama/storage/config_json_reader"
+require "agama/storage/iscsi/manager"
require "agama/storage/manager"
require "agama/storage/proposal"
require "agama/storage/proposal_settings"
-require "agama/storage/iscsi/manager"
require "agama/storage/volume"
-require "agama/config"
-require "agama/issue"
-require "agama/dbus/clients/questions"
-require "agama/http"
require "y2storage/issue"
Yast.import "Installation"
@@ -163,16 +164,13 @@
allow(proposal).to receive(:issues).and_return(proposal_issues)
allow(proposal).to receive(:available_devices).and_return(devices)
- allow(proposal).to receive(:calculate_agama)
+ allow(proposal).to receive(:calculate_from_json)
allow(config).to receive(:pick_product)
allow(iscsi).to receive(:activate)
allow(y2storage_manager).to receive(:activate)
allow(iscsi).to receive(:probe)
allow(y2storage_manager).to receive(:probe)
-
- allow_any_instance_of(Agama::Storage::ConfigReader).to receive(:read)
- .and_return(storage_config)
end
let(:raw_devicegraph) do
@@ -185,8 +183,6 @@
let(:devices) { [disk1, disk2] }
- let(:storage_config) { Agama::Storage::Config.new }
-
let(:disk1) { instance_double(Y2Storage::Disk, name: "/dev/vda") }
let(:disk2) { instance_double(Y2Storage::Disk, name: "/dev/vdb") }
@@ -204,7 +200,7 @@
end
expect(iscsi).to receive(:probe)
expect(y2storage_manager).to receive(:probe)
- expect(proposal).to receive(:calculate_agama).with(storage_config)
+ expect(proposal).to receive(:calculate_from_json)
storage.probe
end
diff --git a/service/test/agama/storage/proposal_test.rb b/service/test/agama/storage/proposal_test.rb
index 5933a5e89f..ae0c78f936 100644
--- a/service/test/agama/storage/proposal_test.rb
+++ b/service/test/agama/storage/proposal_test.rb
@@ -118,9 +118,9 @@ def drive(partitions)
describe "#storage_json" do
context "if no proposal has been calculated yet" do
- it "returns an empty hash" do
+ it "returns nil" do
expect(subject.calculated?).to eq(false)
- expect(proposal.storage_json).to eq({})
+ expect(proposal.storage_json).to be_nil
end
end
@@ -156,83 +156,65 @@ def drive(partitions)
subject.calculate_agama(achivable_config)
end
- context "and unsolved config is requested" do
- let(:solved) { false }
-
- it "returns the unsolved JSON config" do
- expect(subject.storage_json(solved: solved)).to eq(
- {
- storage: {
- boot: { configure: true },
- drives: [
- {
- search: {
- ifNotFound: "error",
- max: 1
- },
- partitions: [
- {
- filesystem: {
- reuseIfPossible: false,
- path: "/",
- type: "btrfs",
- mkfsOptions: [],
- mountOptions: []
- },
- size: {
- min: 10.GiB.to_i,
- max: 10.GiB.to_i
- }
+ it "returns the unsolved JSON config" do
+ expect(subject.storage_json).to eq(
+ {
+ storage: {
+ boot: { configure: true },
+ drives: [
+ {
+ search: {
+ ifNotFound: "error",
+ max: 1
+ },
+ partitions: [
+ {
+ filesystem: {
+ reuseIfPossible: false,
+ path: "/",
+ type: "btrfs",
+ mkfsOptions: [],
+ mountOptions: []
+ },
+ size: {
+ min: 10.GiB.to_i,
+ max: 10.GiB.to_i
}
- ]
- }
- ],
- volumeGroups: []
- }
+ }
+ ]
+ }
+ ],
+ volumeGroups: []
}
- )
- end
+ }
+ )
end
+ end
- context "and solved config is requested" do
- let(:solved) { true }
+ context "if a proposal was calculated with the autoyast strategy" do
+ before do
+ subject.calculate_autoyast(partitioning)
+ end
- it "returns the solved JSON config" do
- expect(subject.storage_json(solved: solved)).to eq(
- {
- storage: {
- boot: { configure: true },
- drives: [
- {
- search: {
- condition: { name: "/dev/sda" },
- ifNotFound: "error",
- max: 1
- },
- partitions: [
- {
- filesystem: {
- reuseIfPossible: false,
- path: "/",
- type: {
- btrfs: { snapshots: false }
- },
- mkfsOptions: [],
- mountOptions: []
- },
- size: {
- min: 10.GiB.to_i,
- max: 10.GiB.to_i
- }
- }
- ]
- }
- ],
- volumeGroups: []
+ let(:partitioning) do
+ [
+ {
+ partitions: [
+ {
+ mount: "/",
+ size: "10 GiB"
}
- }
- )
- end
+ ]
+ }
+ ]
+ end
+
+ it "returns the unsolved JSON config" do
+ expect(subject.storage_json).to eq(
+ {
+ legacyAutoyastStorage: partitioning
+ }
+ )
end
end
@@ -253,37 +235,25 @@ def drive(partitions)
}
end
- context "and unsolved config is requested" do
- let(:solved) { false }
-
- it "returns the given guided JSON config" do
- expect(subject.storage_json(solved: solved)).to eq(config_json)
- end
- end
-
- context "and solved config is requested" do
- let(:solved) { true }
-
- it "returns the solved guided JSON config" do
- expected_json = {
- storage: {
- guided: {
- boot: {
- configure: true
- },
- space: {
- policy: "keep"
- },
- target: {
- disk: "/dev/vda"
- },
- volumes: []
- }
+ it "returns the solved guided JSON config" do
+ expected_json = {
+ storage: {
+ guided: {
+ boot: {
+ configure: true
+ },
+ space: {
+ policy: "keep"
+ },
+ target: {
+ disk: "/dev/vda"
+ },
+ volumes: []
}
}
+ }
- expect(subject.storage_json(solved: solved)).to eq(expected_json)
- end
+ expect(subject.storage_json).to eq(expected_json)
end
end
@@ -307,45 +277,8 @@ def drive(partitions)
}
end
- context "and unsolved config is requested" do
- let(:solved) { false }
-
- it "returns the given JSON config" do
- expect(subject.storage_json(solved: solved)).to eq(config_json)
- end
- end
-
- context "and solved config is requested" do
- let(:solved) { true }
-
- it "returns the solved JSON config" do
- expect(subject.storage_json(solved: solved)).to eq(
- {
- storage: {
- boot: { configure: false },
- drives: [
- {
- search: {
- condition: { name: "/dev/sda" },
- ifNotFound: "error",
- max: 1
- },
- filesystem: {
- mkfsOptions: [],
- mountOptions: [],
- reuseIfPossible: false,
- type: {
- btrfs: { snapshots: false }
- }
- },
- partitions: []
- }
- ],
- volumeGroups: []
- }
- }
- )
- end
+ it "returns the given JSON config" do
+ expect(subject.storage_json).to eq(config_json)
end
end
@@ -375,6 +308,104 @@ def drive(partitions)
end
end
+ describe "#model_json" do
+ context "if no proposal has been calculated yet" do
+ it "returns nil" do
+ expect(subject.model_json).to be_nil
+ end
+ end
+
+ context "if a guided proposal has been calculated" do
+ before do
+ subject.calculate_from_json(settings_json)
+ end
+
+ let(:settings_json) do
+ {
+ storage: {
+ guided: {
+ target: { disk: "/dev/vda" }
+ }
+ }
+ }
+ end
+
+ it "returns nil" do
+ expect(subject.model_json).to be_nil
+ end
+ end
+
+ context "if an agama proposal has been calculated" do
+ before do
+ subject.calculate_from_json(config_json)
+ end
+
+ let(:config_json) do
+ {
+ storage: {
+ drives: [
+ {
+ partitions: [
+ {
+ filesystem: { path: "/" }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ end
+
+ it "returns the config model" do
+ expect(subject.model_json).to eq(
+ {
+ drives: [
+ {
+ name: "/dev/sda",
+ spacePolicy: "keep",
+ partitions: [
+ {
+ mountPath: "/",
+ filesystem: {
+ default: true,
+ type: "ext4"
+ },
+ size: {
+ default: true,
+ min: 0
+ },
+ delete: false,
+ deleteIfNeeded: false,
+ resize: false,
+ resizeIfNeeded: false
+ }
+ ]
+ }
+ ]
+ }
+ )
+ end
+ end
+
+ context "if an AutoYaST proposal has been calculated" do
+ before do
+ subject.calculate_from_json(autoyast_json)
+ end
+
+ let(:autoyast_json) do
+ {
+ legacyAutoyastStorage: [
+ { device: "/dev/vda" }
+ ]
+ }
+ end
+
+ it "returns nil" do
+ expect(subject.model_json).to be_nil
+ end
+ end
+ end
+
shared_examples "check proposal callbacks" do |action, settings|
it "runs all the callbacks" do
callback1 = proc {}
diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts
index 2f3608427d..9913e89d39 100644
--- a/web/src/api/storage.ts
+++ b/web/src/api/storage.ts
@@ -23,7 +23,7 @@
import { get, post, put } from "~/api/http";
import { Job } from "~/types/job";
import { calculate, fetchSettings } from "~/api/storage/proposal";
-import { config } from "~/api/storage/types";
+import { config, configModel } from "~/api/storage/types";
/**
* Starts the storage probing process.
@@ -35,8 +35,8 @@ const probe = (): Promise => post("/api/storage/probe");
const fetchConfig = (): Promise =>
get("/api/storage/config").then((config) => config.storage);
-const fetchSolvedConfig = (): Promise =>
- get("/api/storage/solved_config").then((config) => config.storage);
+const fetchConfigModel = (): Promise =>
+ get("/api/storage/config_model");
const setConfig = (config: config.Config) => put("/api/storage/config", config);
@@ -67,7 +67,7 @@ const refresh = async (): Promise => {
export {
probe,
fetchConfig,
- fetchSolvedConfig,
+ fetchConfigModel,
setConfig,
fetchStorageJobs,
findStorageJob,
diff --git a/web/src/api/storage/types.ts b/web/src/api/storage/types.ts
index d295e566ae..216d0eda10 100644
--- a/web/src/api/storage/types.ts
+++ b/web/src/api/storage/types.ts
@@ -21,6 +21,7 @@
*/
import * as config from "./types/config";
+import * as configModel from "./types/config-model";
export * from "./types/openapi";
-export { config };
+export { config, configModel };
diff --git a/web/src/api/storage/types/config-model.ts b/web/src/api/storage/types/config-model.ts
new file mode 100644
index 0000000000..4d09b437b4
--- /dev/null
+++ b/web/src/api/storage/types/config-model.ts
@@ -0,0 +1,65 @@
+/* eslint-disable */
+/**
+ * This file was automatically generated by json-schema-to-typescript.
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+ * and run json-schema-to-typescript to regenerate this file.
+ */
+
+export type FilesystemType =
+ | "bcachefs"
+ | "btrfs"
+ | "exfat"
+ | "ext2"
+ | "ext3"
+ | "ext4"
+ | "f2fs"
+ | "jfs"
+ | "nfs"
+ | "nilfs2"
+ | "ntfs"
+ | "reiserfs"
+ | "swap"
+ | "tmpfs"
+ | "vfat"
+ | "xfs";
+export type SpacePolicy = "delete" | "resize" | "keep" | "custom";
+export type PtableType = "gpt" | "msdos" | "dasd";
+export type PartitionId = "linux" | "swap" | "lvm" | "raid" | "esp" | "prep" | "bios_boot";
+
+/**
+ * Config model
+ */
+export interface Config {
+ drives?: Drive[];
+}
+export interface Drive {
+ name: string;
+ alias?: string;
+ mountPath?: string;
+ filesystem?: Filesystem;
+ spacePolicy?: SpacePolicy;
+ ptableType?: PtableType;
+ partitions?: Partition[];
+}
+export interface Filesystem {
+ default: boolean;
+ type?: FilesystemType;
+ snapshots?: boolean;
+}
+export interface Partition {
+ name?: string;
+ alias?: string;
+ id?: PartitionId;
+ mountPath?: string;
+ filesystem?: Filesystem;
+ size?: Size;
+ delete?: boolean;
+ deleteIfNeeded?: boolean;
+ resize?: boolean;
+ resizeIfNeeded?: boolean;
+}
+export interface Size {
+ default: boolean;
+ min: number;
+ max?: number;
+}
diff --git a/web/src/queries/storage.ts b/web/src/queries/storage.ts
index a6a53b7a48..05385f6cf1 100644
--- a/web/src/queries/storage.ts
+++ b/web/src/queries/storage.ts
@@ -28,7 +28,7 @@ import {
useSuspenseQuery,
} from "@tanstack/react-query";
import React from "react";
-import { fetchConfig, fetchSolvedConfig, setConfig } from "~/api/storage";
+import { fetchConfig, fetchConfigModel, setConfig } from "~/api/storage";
import { fetchDevices, fetchDevicesDirty } from "~/api/storage/devices";
import {
calculate,
@@ -40,6 +40,7 @@ import {
import { useInstallerClient } from "~/context/installer";
import {
config,
+ configModel,
ProductParams,
Volume as APIVolume,
ProposalSettingsPatch,
@@ -51,7 +52,6 @@ import {
Volume,
VolumeTarget,
} from "~/types/storage";
-import * as ConfigModel from "~/storage/model/config";
import { QueryHookOptions } from "~/types/queries";
@@ -61,9 +61,9 @@ const configQuery = {
staleTime: Infinity,
};
-const solvedConfigQuery = {
- queryKey: ["storage", "solvedConfig"],
- queryFn: fetchSolvedConfig,
+const configModelQuery = {
+ queryKey: ["storage", "configModel"],
+ queryFn: fetchConfigModel,
staleTime: Infinity,
};
@@ -128,10 +128,10 @@ const useConfig = (options?: QueryHookOptions): config.Config => {
};
/**
- * Hook that returns the solved config.
+ * Hook that returns the config model.
*/
-const useSolvedConfig = (options?: QueryHookOptions): config.Config => {
- const query = solvedConfigQuery;
+const useConfigModel = (options?: QueryHookOptions): configModel.Config => {
+ const query = configModelQuery;
const func = options?.suspense ? useSuspenseQuery : useQuery;
const { data } = func(query);
return data;
@@ -150,18 +150,6 @@ const useConfigMutation = () => {
return useMutation(query);
};
-/**
- * Hook that returns the config devices.
- */
-const useConfigDevices = (options?: QueryHookOptions): ConfigModel.Device[] => {
- const config = useConfig(options);
- const solvedConfig = useSolvedConfig(options);
-
- if (!config || !solvedConfig) return [];
-
- return ConfigModel.generate(config, solvedConfig);
-};
-
/**
* Hook that returns the list of storage devices for the given scope.
*
@@ -345,9 +333,8 @@ const useDeprecatedChanges = () => {
export {
useConfig,
- useSolvedConfig,
useConfigMutation,
- useConfigDevices,
+ useConfigModel,
useDevices,
useAvailableDevices,
useProductParams,
diff --git a/web/src/storage/model.ts b/web/src/storage/model.ts
deleted file mode 100644
index ecbb67c203..0000000000
--- a/web/src/storage/model.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import * as config from "~/storage/model/config";
-
-export { config };
diff --git a/web/src/storage/model/config.test.ts b/web/src/storage/model/config.test.ts
deleted file mode 100644
index eed3735cef..0000000000
--- a/web/src/storage/model/config.test.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import * as model from "~/storage/model/config";
-
-describe("#generate", () => {
- it("returns a list of devices", () => {
- expect(
- model.generate(
- {
- drives: [
- { partitions: [{ search: "*", delete: true }] },
- { partitions: [{ filesystem: { path: "/test" } }] },
- ],
- },
- {
- drives: [
- {
- search: "/dev/vda",
- partitions: [
- { search: "/dev/vda1", delete: true },
- { search: "/dev/vda2", delete: true },
- ],
- },
- {
- search: "/dev/vdb",
- partitions: [
- {
- filesystem: { type: "xfs", path: "/test" },
- size: { min: 1024, max: 2048 },
- },
- ],
- },
- ],
- },
- ),
- ).toEqual([
- {
- name: "/dev/vda",
- alias: undefined,
- spacePolicy: "delete",
- partitions: [
- {
- name: "/dev/vda1",
- delete: true,
- },
- {
- name: "/dev/vda2",
- delete: true,
- },
- ],
- },
- {
- name: "/dev/vdb",
- alias: undefined,
- spacePolicy: "keep",
- partitions: [
- {
- name: undefined,
- alias: undefined,
- resizeIfNeeded: true,
- filesystem: "xfs",
- mountPath: "/test",
- snapshots: undefined,
- size: { min: 1024, max: 2048 },
- },
- ],
- },
- ]);
- });
-});
diff --git a/web/src/storage/model/config.ts b/web/src/storage/model/config.ts
deleted file mode 100644
index fc3a121b88..0000000000
--- a/web/src/storage/model/config.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import { config } from "~/api/storage/types";
-import { Drive, generate as generateDrive } from "~/storage/model/config/drive";
-
-export type Device = Drive;
-
-class ConfigDevicesGenerator {
- private config: config.Config;
- private solvedConfig: config.Config;
-
- constructor(config: config.Config, solvedConfig: config.Config) {
- this.config = config;
- this.solvedConfig = solvedConfig;
- }
-
- generate(): Device[] {
- return this.generateDrives();
- }
-
- private generateDrives(): Drive[] {
- const solvedDriveConfigs = this.solvedConfig.drives || [];
- return solvedDriveConfigs.map((c, i) => this.generateDrive(c, i));
- }
-
- private generateDrive(solvedDriveConfig: config.DriveElement, id: number): Drive {
- // TODO: Use an index to associate a drive config with an unsolved drive config.
- const driveConfig = (this.config.drives || [])[id];
- return generateDrive(driveConfig, solvedDriveConfig);
- }
-}
-
-export function generate(config: config.Config, solvedConfig: config.Config): Device[] {
- const generator = new ConfigDevicesGenerator(config, solvedConfig);
- return generator.generate();
-}
diff --git a/web/src/storage/model/config/common.test.ts b/web/src/storage/model/config/common.test.ts
deleted file mode 100644
index a63d417b84..0000000000
--- a/web/src/storage/model/config/common.test.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import * as model from "~/storage/model/config/common";
-
-describe("#generateName", () => {
- it("returns the device name from the search section", () => {
- expect(
- model.generateName({
- search: "/dev/vda",
- }),
- ).toEqual("/dev/vda");
-
- expect(
- model.generateName({
- search: {
- condition: {
- name: "/dev/vda",
- },
- },
- }),
- ).toEqual("/dev/vda");
- });
-
- it("returns undefined if the search section has no name", () => {
- expect(
- model.generateName({
- search: "*",
- }),
- ).toBeUndefined;
-
- expect(
- model.generateName({
- search: {
- max: 5,
- ifNotFound: "skip",
- },
- }),
- ).toBeUndefined;
- });
-
- it("returns undefined if there is no search section", () => {
- expect(model.generateName({})).toBeUndefined;
- });
-});
-
-describe("#generateFilesystem", () => {
- it("returns the file system type from the filesystem section", () => {
- expect(
- model.generateFilesystem({
- filesystem: {
- type: "xfs",
- },
- }),
- ).toEqual("xfs");
-
- expect(
- model.generateFilesystem({
- filesystem: {
- type: {
- btrfs: {
- snapshots: true,
- },
- },
- },
- }),
- ).toEqual("btrfs");
- });
-
- it("returns undefined if the filesystem section has no type", () => {
- expect(
- model.generateFilesystem({
- filesystem: {
- path: "/",
- },
- }),
- ).toBeUndefined;
- });
-
- it("returns undefined if there is no filesystem section", () => {
- expect(model.generateFilesystem({})).toBeUndefined;
- });
-});
-
-describe("#generateSnapshots", () => {
- it("returns the snapshots value from the filesystem section", () => {
- expect(
- model.generateSnapshots({
- filesystem: {
- type: {
- btrfs: {
- snapshots: true,
- },
- },
- },
- }),
- ).toEqual(true);
-
- expect(
- model.generateSnapshots({
- filesystem: {
- type: {
- btrfs: {
- snapshots: false,
- },
- },
- },
- }),
- ).toEqual(false);
- });
-
- it("returns undefined if the filesystem section has no snapshots", () => {
- expect(
- model.generateSnapshots({
- filesystem: {
- type: "btrfs",
- },
- }),
- ).toBeUndefined;
-
- expect(
- model.generateSnapshots({
- filesystem: {
- type: {
- btrfs: {},
- },
- },
- }),
- ).toBeUndefined;
- });
-
- it("returns undefined if there is no filesystem section", () => {
- expect(model.generateSnapshots({})).toBeUndefined;
- });
-});
diff --git a/web/src/storage/model/config/common.ts b/web/src/storage/model/config/common.ts
deleted file mode 100644
index 3dc8a9c37d..0000000000
--- a/web/src/storage/model/config/common.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import { config } from "~/api/storage/types";
-import * as checks from "~/api/storage/types/checks";
-
-interface WithFilesystem {
- filesystem?: config.Filesystem;
-}
-
-interface WithSearch {
- search?: config.SearchElement;
-}
-
-export function generateName(config: Type): string | undefined {
- const search = config.search;
-
- if (!search) return;
- if (checks.isSimpleSearchAll(search)) return;
- if (checks.isSimpleSearchByName(search)) return search;
- if (checks.isAdvancedSearch(search)) return search.condition?.name;
-}
-
-export function generateFilesystem(config: Type): string | undefined {
- const fstype = config.filesystem?.type;
-
- if (!fstype) return;
- if (checks.isFilesystemTypeBtrfs(fstype)) return "btrfs";
- if (checks.isFilesystemTypeAny(fstype)) return fstype;
-}
-
-export function generateSnapshots(config: Type): boolean | undefined {
- const fstype = config.filesystem?.type;
-
- if (!fstype) return;
- if (checks.isFilesystemTypeAny(fstype)) return;
- if (checks.isFilesystemTypeBtrfs(fstype)) return fstype.btrfs.snapshots;
-}
diff --git a/web/src/storage/model/config/drive.test.ts b/web/src/storage/model/config/drive.test.ts
deleted file mode 100644
index 82f1c9ed55..0000000000
--- a/web/src/storage/model/config/drive.test.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import * as model from "~/storage/model/config/drive";
-
-describe("#generate", () => {
- it("returns a drive object from a drive section", () => {
- expect(
- model.generate(undefined, {
- search: "/dev/vda",
- alias: "test",
- filesystem: {
- type: "xfs",
- path: "/test",
- },
- }),
- ).toEqual({
- name: "/dev/vda",
- alias: "test",
- filesystem: "xfs",
- mountPath: "/test",
- snapshots: undefined,
- spacePolicy: undefined,
- });
-
- expect(
- model.generate(
- {
- partitions: [{ search: "*", delete: true }],
- },
- {
- search: "/dev/vda",
- alias: "test",
- filesystem: {
- type: {
- btrfs: { snapshots: false },
- },
- path: "/test",
- },
- },
- ),
- ).toEqual({
- name: "/dev/vda",
- alias: "test",
- filesystem: "btrfs",
- mountPath: "/test",
- snapshots: false,
- spacePolicy: "delete",
- });
-
- expect(
- model.generate(
- {
- partitions: [{ search: "*", delete: true }, { generate: "default" }],
- },
- {
- search: "/dev/vda",
- partitions: [{ search: "/dev/vda1", delete: true }, { filesystem: { path: "/" } }],
- },
- ),
- ).toEqual({
- name: "/dev/vda",
- alias: undefined,
- spacePolicy: "delete",
- partitions: [
- {
- name: "/dev/vda1",
- delete: true,
- },
- {
- name: undefined,
- alias: undefined,
- filesystem: undefined,
- mountPath: "/",
- snapshots: undefined,
- size: undefined,
- },
- ],
- });
- });
-});
diff --git a/web/src/storage/model/config/drive.ts b/web/src/storage/model/config/drive.ts
deleted file mode 100644
index 7d6aa54115..0000000000
--- a/web/src/storage/model/config/drive.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import { config } from "~/api/storage/types";
-import * as checks from "~/api/storage/types/checks";
-import {
- Partition,
- isPartitionConfig,
- generate as generatePartition,
-} from "~/storage/model/config/partition";
-import { SpacePolicy, generate as generateSpacePolicy } from "~/storage/model/config/space-policy";
-import { generateName, generateFilesystem, generateSnapshots } from "~/storage/model/config/common";
-
-export type Drive = {
- name?: string;
- alias?: string;
- filesystem?: string;
- mountPath?: string;
- snapshots?: boolean;
- spacePolicy?: SpacePolicy;
- partitions?: Partition[];
-};
-
-class DriveGenerator {
- private driveConfig: config.DriveElement | undefined;
- private solvedDriveConfig: config.DriveElement;
-
- constructor(
- driveConfig: config.DriveElement | undefined,
- solvedDriveConfig: config.DriveElement,
- ) {
- this.driveConfig = driveConfig;
- this.solvedDriveConfig = solvedDriveConfig;
- }
-
- generate(): Drive {
- if (checks.isFormattedDrive(this.solvedDriveConfig)) {
- return this.fromFormattedDrive(this.solvedDriveConfig);
- } else if (checks.isPartitionedDrive(this.solvedDriveConfig)) {
- return this.fromPartitionedDrive(this.solvedDriveConfig);
- }
- }
-
- private fromFormattedDrive(solvedDriveConfig: config.FormattedDrive): Drive {
- return {
- name: generateName(solvedDriveConfig),
- alias: solvedDriveConfig.alias,
- spacePolicy: this.generateSpacePolicy(),
- filesystem: generateFilesystem(solvedDriveConfig),
- mountPath: solvedDriveConfig.filesystem?.path,
- snapshots: generateSnapshots(solvedDriveConfig),
- };
- }
-
- private fromPartitionedDrive(solvedDriveConfig: config.PartitionedDrive): Drive {
- return {
- name: generateName(solvedDriveConfig),
- alias: solvedDriveConfig.alias,
- spacePolicy: this.generateSpacePolicy(),
- partitions: this.generatePartitions(solvedDriveConfig),
- };
- }
-
- private generateSpacePolicy(): SpacePolicy {
- if (this.driveConfig === undefined) return;
-
- return generateSpacePolicy(this.driveConfig);
- }
-
- private generatePartitions(solvedDriveConfig: config.PartitionedDrive): Partition[] {
- const solvedPartitionConfigs = solvedDriveConfig.partitions || [];
- return solvedPartitionConfigs.filter(isPartitionConfig).map(generatePartition);
- }
-}
-
-export function generate(config: config.DriveElement, solvedConfig: config.DriveElement): Drive {
- const generator = new DriveGenerator(config, solvedConfig);
- return generator.generate();
-}
diff --git a/web/src/storage/model/config/partition.test.ts b/web/src/storage/model/config/partition.test.ts
deleted file mode 100644
index 310a101fb6..0000000000
--- a/web/src/storage/model/config/partition.test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import * as model from "~/storage/model/config/partition";
-
-describe("#generate", () => {
- it("returns a partition object from a partition section", () => {
- expect(
- model.generate({
- search: "/dev/vda1",
- delete: true,
- }),
- ).toEqual({
- name: "/dev/vda1",
- delete: true,
- });
-
- expect(
- model.generate({
- search: "/dev/vda1",
- deleteIfNeeded: true,
- size: 1024,
- }),
- ).toEqual({
- name: "/dev/vda1",
- deleteIfNeeded: true,
- resizeIfNeeded: false,
- size: { min: 1024, max: 1024 },
- });
-
- expect(
- model.generate({
- search: "/dev/vda1",
- alias: "test",
- filesystem: {
- path: "/test",
- type: {
- btrfs: { snapshots: true },
- },
- },
- size: { min: 0, max: 2048 },
- }),
- ).toEqual({
- name: "/dev/vda1",
- alias: "test",
- resizeIfNeeded: true,
- filesystem: "btrfs",
- mountPath: "/test",
- snapshots: true,
- size: { min: 0, max: 2048 },
- });
-
- expect(
- model.generate({
- filesystem: {
- path: "/test",
- },
- }),
- ).toEqual({
- name: undefined,
- alias: undefined,
- resizeIfNeeded: undefined,
- filesystem: undefined,
- mountPath: "/test",
- snapshots: undefined,
- size: undefined,
- });
- });
-});
diff --git a/web/src/storage/model/config/partition.ts b/web/src/storage/model/config/partition.ts
deleted file mode 100644
index 6f0025d143..0000000000
--- a/web/src/storage/model/config/partition.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import { config } from "~/api/storage/types";
-import * as checks from "~/api/storage/types/checks";
-import { Size, WithSize, generate as generateSize } from "~/storage/model/config/size";
-import { generateName, generateFilesystem, generateSnapshots } from "~/storage/model/config/common";
-
-export type Partition = {
- name?: string;
- alias?: string;
- delete?: boolean;
- deleteIfNeeded?: boolean;
- resizeIfNeeded?: boolean;
- filesystem?: string;
- mountPath?: string;
- snapshots?: boolean;
- size?: Size;
-};
-
-export type PartitionConfig =
- | config.RegularPartition
- | config.PartitionToDelete
- | config.PartitionToDeleteIfNeeded;
-
-export function isPartitionConfig(
- partition: config.PartitionElement,
-): partition is PartitionConfig {
- return (
- checks.isRegularPartition(partition) ||
- checks.isPartitionToDelete(partition) ||
- checks.isPartitionToDeleteIfNeeded(partition)
- );
-}
-
-class PartitionGenerator {
- private partitionConfig: PartitionConfig;
-
- constructor(partitionConfig: PartitionConfig) {
- this.partitionConfig = partitionConfig;
- }
-
- generate(): Partition {
- if (checks.isRegularPartition(this.partitionConfig)) {
- return this.fromRegularPartition(this.partitionConfig);
- } else if (checks.isPartitionToDelete(this.partitionConfig)) {
- return this.fromPartitionToDelete(this.partitionConfig);
- } else if (checks.isPartitionToDeleteIfNeeded(this.partitionConfig)) {
- return this.fromPartitionToDeleteIfNeeded(this.partitionConfig);
- }
- }
-
- private fromRegularPartition(partitionConfig: config.RegularPartition): Partition {
- return {
- name: generateName(partitionConfig),
- alias: partitionConfig.alias,
- resizeIfNeeded: this.generateResizeIfNeeded(partitionConfig),
- filesystem: generateFilesystem(partitionConfig),
- mountPath: partitionConfig.filesystem?.path,
- snapshots: generateSnapshots(partitionConfig),
- size: generateSize(partitionConfig),
- };
- }
-
- private fromPartitionToDelete(partitionConfig: config.PartitionToDelete): Partition {
- return {
- name: generateName(partitionConfig),
- delete: true,
- };
- }
-
- private fromPartitionToDeleteIfNeeded(
- partitionConfig: config.PartitionToDeleteIfNeeded,
- ): Partition {
- return {
- name: generateName(partitionConfig),
- deleteIfNeeded: true,
- resizeIfNeeded: this.generateResizeIfNeeded(partitionConfig),
- size: generateSize(partitionConfig),
- };
- }
-
- private generateResizeIfNeeded(
- partitionConfig: TypeWithSize,
- ): boolean | undefined {
- if (!partitionConfig.size) return;
-
- const size = generateSize(partitionConfig);
- return size.min !== undefined && size.min !== size.max;
- }
-}
-
-export function generate(config: PartitionConfig): Partition {
- const generator = new PartitionGenerator(config);
- return generator.generate();
-}
diff --git a/web/src/storage/model/config/size.test.ts b/web/src/storage/model/config/size.test.ts
deleted file mode 100644
index 8cb49a866d..0000000000
--- a/web/src/storage/model/config/size.test.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import * as model from "~/storage/model/config/size";
-
-describe("#generate", () => {
- it("returns the size in bytes from the size section", () => {
- expect(
- model.generate({
- size: 1024,
- }),
- ).toEqual({ min: 1024, max: 1024 });
-
- expect(
- model.generate({
- size: "1 KiB",
- }),
- ).toEqual({ min: 1024, max: 1024 });
-
- expect(
- model.generate({
- size: "1KiB",
- }),
- ).toEqual({ min: 1024, max: 1024 });
-
- expect(
- model.generate({
- size: "1kb",
- }),
- ).toEqual({ min: 1000, max: 1000 });
-
- expect(
- model.generate({
- size: "1k",
- }),
- ).toEqual({ min: 1000, max: 1000 });
-
- expect(
- model.generate({
- size: "665.284 TiB",
- }),
- ).toEqual({ min: 731487493773328, max: 731487493773328 });
-
- expect(
- model.generate({
- size: [1024],
- }),
- ).toEqual({ min: 1024 });
-
- expect(
- model.generate({
- size: [1024, 2048],
- }),
- ).toEqual({ min: 1024, max: 2048 });
-
- expect(
- model.generate({
- size: ["1 kib", "2 KIB"],
- }),
- ).toEqual({ min: 1024, max: 2048 });
-
- expect(
- model.generate({
- size: {
- min: 1024,
- },
- }),
- ).toEqual({ min: 1024 });
-
- expect(
- model.generate({
- size: {
- min: 1024,
- max: 2048,
- },
- }),
- ).toEqual({ min: 1024, max: 2048 });
-
- expect(
- model.generate({
- size: {
- min: "1 kib",
- max: "2 KiB",
- },
- }),
- ).toEqual({ min: 1024, max: 2048 });
- });
- it("returns undefined for 'custom' value", () => {
- expect(
- model.generate({
- size: {
- min: "custom",
- max: 2048,
- },
- }),
- ).toEqual({ min: undefined, max: 2048 });
- });
-
- it("returns undefined if there is no size section", () => {
- expect(model.generate({})).toBeUndefined;
- });
-});
diff --git a/web/src/storage/model/config/size.ts b/web/src/storage/model/config/size.ts
deleted file mode 100644
index 657aef034d..0000000000
--- a/web/src/storage/model/config/size.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import { config } from "~/api/storage/types";
-import * as checks from "~/api/storage/types/checks";
-import xbytes from "xbytes";
-
-export type Size = {
- min?: number;
- max?: number;
-};
-
-export interface WithSize {
- size?: config.Size;
-}
-
-class SizeGenerator {
- private config: TypeWithSize;
-
- constructor(config: TypeWithSize) {
- this.config = config;
- }
-
- // TODO: detect auto size by checking the unsolved config.
- generate(): Size | undefined {
- const size = this.config.size;
-
- if (!size) return;
- if (checks.isSizeValue(size)) return this.fromSizeValue(size);
- if (checks.isSizeTuple(size)) return this.fromSizeTuple(size);
- if (checks.isSizeRange(size)) return this.fromSizeRange(size);
- }
-
- private fromSizeValue(value: config.SizeValue): Size {
- const bytes = this.bytes(value);
- return { min: bytes, max: bytes };
- }
-
- private fromSizeTuple(sizeTuple: config.SizeTuple): Size {
- const size: Size = { min: this.bytes(sizeTuple[0]) };
- if (sizeTuple.length === 2) size.max = this.bytes(sizeTuple[1]);
-
- return size;
- }
-
- private fromSizeRange(sizeRange: config.SizeRange): Size {
- const size: Size = { min: this.bytes(sizeRange.min) };
- if (sizeRange.max) size.max = this.bytes(sizeRange.max);
-
- return size;
- }
-
- private parseSizeString(value: string): number | undefined {
- // xbytes.parseSize will not work with a string like '10k', the unit must end with 'b' or 'B'
- let adapted = value.trim();
- if (!adapted.match(/b$/i)) adapted = adapted + "b";
-
- const parsed = xbytes.parseSize(adapted, { bits: false }) || parseInt(adapted);
- if (parsed) return Math.trunc(parsed);
- }
-
- private bytes(value: config.SizeValueWithCurrent): number | undefined {
- if (checks.isSizeCurrent(value)) return;
- if (checks.isSizeString(value)) return this.parseSizeString(value);
- if (checks.isSizeBytes(value)) return value;
- }
-}
-
-export function generate(config: TypeWithSize): Size | undefined {
- return new SizeGenerator(config).generate();
-}
diff --git a/web/src/storage/model/config/space-policy.test.ts b/web/src/storage/model/config/space-policy.test.ts
deleted file mode 100644
index 80a8ad66c3..0000000000
--- a/web/src/storage/model/config/space-policy.test.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import * as model from "~/storage/model/config/space-policy";
-
-describe("#generate", () => {
- it("returns 'delete' if there is a file system", () => {
- expect(
- model.generate({
- filesystem: { type: "xfs" },
- }),
- ).toEqual("delete");
- });
-
- it("returns 'delete' if there is a 'delete all' partition", () => {
- expect(
- model.generate({
- partitions: [{ search: "*", delete: true }],
- }),
- ).toEqual("delete");
-
- expect(
- model.generate({
- partitions: [{ search: { ifNotFound: "skip" }, delete: true }],
- }),
- ).toEqual("delete");
-
- expect(
- model.generate({
- partitions: [{ search: "*", delete: true }, { filesystem: { path: "/" } }],
- }),
- ).toEqual("delete");
- });
-
- it("returns 'resize' if there is a 'shrink all' partition", () => {
- expect(
- model.generate({
- partitions: [{ search: "*", size: { min: 0, max: "current" } }],
- }),
- ).toEqual("resize");
-
- expect(
- model.generate({
- partitions: [{ search: "*", size: [0, "current"] }],
- }),
- ).toEqual("resize");
-
- expect(
- model.generate({
- partitions: [{ search: { ifNotFound: "skip" }, size: { min: 0, max: "current" } }],
- }),
- ).toEqual("resize");
-
- expect(
- model.generate({
- partitions: [{ search: "*", size: { min: 0, max: "current" } }, { generate: "default" }],
- }),
- ).toEqual("resize");
- });
-
- it("returns 'custom' if there is a 'delete' or 'resize' partition", () => {
- expect(
- model.generate({
- partitions: [{ search: "/dev/vda", delete: true }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: { max: 2, ifNotFound: "skip" }, delete: true }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: "*", deleteIfNeeded: true }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: "*", deleteIfNeeded: true, size: [0, "current"] }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: "*", size: { min: 0 } }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: "*", size: { min: 0, max: 1024 } }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: "/dev/vda", delete: true }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: "/dev/vda", size: { min: 0, max: "current" } }],
- }),
- ).toEqual("custom");
-
- expect(
- model.generate({
- partitions: [{ search: "/dev/vda", delete: true }, { filesystem: { path: "/" } }],
- }),
- ).toEqual("custom");
- });
-
- it("returns 'keep' if there is neither 'delete' nor 'resize' partition", () => {
- expect(
- model.generate({
- partitions: [{ search: "*", filesystem: { type: "xfs" } }],
- }),
- ).toEqual("keep");
-
- expect(
- model.generate({
- partitions: [{ generate: "default" }, { filesystem: { path: "/home" } }],
- }),
- ).toEqual("keep");
- });
-
- it("returns 'keep' if there are not partitions", () => {
- expect(
- model.generate({
- search: "/dev/vda",
- }),
- ).toEqual("keep");
- });
-});
diff --git a/web/src/storage/model/config/space-policy.ts b/web/src/storage/model/config/space-policy.ts
deleted file mode 100644
index 86e726b516..0000000000
--- a/web/src/storage/model/config/space-policy.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * 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 the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * 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.
- */
-
-import { config } from "~/api/storage/types";
-import * as checks from "~/api/storage/types/checks";
-
-export type SpacePolicy = "keep" | "resize" | "delete" | "custom";
-
-class SpacePolicyGenerator {
- private driveConfig: config.DriveElement;
-
- constructor(driveConfig: config.DriveElement) {
- this.driveConfig = driveConfig;
- }
-
- generate(): SpacePolicy {
- if (this.isDeletePolicy()) return "delete";
- if (this.isResizePolicy()) return "resize";
- if (this.isCustomPolicy()) return "custom";
-
- return "keep";
- }
-
- private isDeletePolicy(): boolean {
- return checks.isFormattedDrive(this.driveConfig) || this.hasDeleteAllPartition();
- }
-
- private isResizePolicy(): boolean {
- return this.hasResizeAllPartition();
- }
-
- private isCustomPolicy(): boolean {
- return this.hasDeletePartition() || this.hasResizePartition();
- }
-
- private hasDeleteAllPartition(): boolean {
- const deleteAllPartition = this.partitionConfigs().find((c) => this.isDeleteAllPartition(c));
- return deleteAllPartition !== undefined;
- }
-
- private hasDeletePartition(): boolean {
- const deletePartition = this.partitionConfigs().find((c) => this.isDeletePartition(c));
- return deletePartition !== undefined;
- }
-
- private hasResizeAllPartition(): boolean {
- const resizeAllPartition = this.partitionConfigs().find((c) => this.isResizeAllPartition(c));
- return resizeAllPartition !== undefined;
- }
-
- private hasResizePartition(): boolean {
- const resizePartition = this.partitionConfigs().find((c) => this.isResizePartition(c));
- return resizePartition !== undefined;
- }
-
- private isDeleteAllPartition(partitionConfig: config.PartitionElement): boolean {
- return checks.isPartitionToDelete(partitionConfig) && this.isSearchAll(partitionConfig.search);
- }
-
- private isDeletePartition(partitionConfig: config.PartitionElement): boolean {
- const isDelete =
- checks.isPartitionToDelete(partitionConfig) ||
- checks.isPartitionToDeleteIfNeeded(partitionConfig);
-
- return isDelete && !this.isDeleteAllPartition(partitionConfig);
- }
-
- private isResizeAllPartition(partitionConfig: config.PartitionElement): boolean {
- return (
- checks.isRegularPartition(partitionConfig) &&
- partitionConfig.search &&
- this.isSearchAll(partitionConfig.search) &&
- partitionConfig.size &&
- this.isShrinkAllSize(partitionConfig.size)
- );
- }
-
- // TODO: if the size is not a range, then detect resize partitions by comparing with the size of
- // the system device.
- private isResizePartition(partitionConfig: config.PartitionElement): boolean {
- if (!checks.isRegularPartition(partitionConfig)) return false;
-
- return (
- !this.isResizeAllPartition(partitionConfig) &&
- partitionConfig.search &&
- partitionConfig.size &&
- this.isResizeSize(partitionConfig.size)
- );
- }
-
- private isSearchAll(searchConfig: config.SearchElement): boolean {
- const isAdvancedSearchAll =
- checks.isAdvancedSearch(searchConfig) &&
- !searchConfig.condition &&
- !searchConfig.max &&
- searchConfig.ifNotFound === "skip";
-
- return isAdvancedSearchAll || checks.isSimpleSearchAll(searchConfig);
- }
-
- private isShrinkAllSize(sizeConfig: config.Size): boolean {
- if (!this.isResizeSize(sizeConfig)) return false;
-
- return (
- (checks.isSizeTuple(sizeConfig) &&
- sizeConfig[0] === 0 &&
- sizeConfig[1] &&
- checks.isSizeCurrent(sizeConfig[1])) ||
- (checks.isSizeRange(sizeConfig) &&
- sizeConfig.min === 0 &&
- sizeConfig.max &&
- checks.isSizeCurrent(sizeConfig.max))
- );
- }
-
- private isResizeSize(sizeConfig: config.Size): boolean {
- if (checks.isSizeBytes(sizeConfig)) return false;
- if (checks.isSizeString(sizeConfig)) return false;
- if (checks.isSizeTuple(sizeConfig)) return sizeConfig[0] !== sizeConfig[1];
- if (checks.isSizeRange(sizeConfig)) return sizeConfig.min !== sizeConfig.max;
- }
-
- private partitionConfigs(): config.PartitionElement[] {
- if (checks.isFormattedDrive(this.driveConfig)) return [];
-
- return this.driveConfig.partitions || [];
- }
-}
-
-export function generate(config: config.DriveElement): SpacePolicy {
- const generator = new SpacePolicyGenerator(config);
- return generator.generate();
-}