diff --git a/rust/agama-lib/share/storage.schema.json b/rust/agama-lib/share/storage.schema.json index 18d3265729..17fea9d42d 100644 --- a/rust/agama-lib/share/storage.schema.json +++ b/rust/agama-lib/share/storage.schema.json @@ -50,6 +50,7 @@ "required": ["filesystem"], "properties": { "search": { "$ref": "#/$defs/searchElement" }, + "index": { "$ref": "#/$defs/index" }, "alias": { "$ref": "#/$defs/alias" }, "encryption": { "$ref": "#/$defs/encryption" }, "filesystem": { "$ref": "#/$defs/filesystem" } @@ -60,6 +61,7 @@ "additionalProperties": false, "properties": { "search": { "$ref": "#/$defs/searchElement" }, + "index": { "$ref": "#/$defs/index" }, "alias": { "$ref": "#/$defs/alias" }, "ptableType": { "$ref": "#/$defs/ptableType" }, "partitions": { @@ -117,6 +119,7 @@ "additionalProperties": false, "properties": { "search": { "$ref": "#/$defs/searchElement" }, + "index": { "$ref": "#/$defs/index" }, "alias": { "$ref": "#/$defs/alias" }, "id": { "title": "Partition id", @@ -141,6 +144,7 @@ "required": ["delete", "search"], "properties": { "search": { "$ref": "#/$defs/searchElement" }, + "index": { "$ref": "#/$defs/index" }, "delete": { "description": "Delete the partition.", "const": true @@ -153,6 +157,7 @@ "required": ["deleteIfNeeded", "search"], "properties": { "search": { "$ref": "#/$defs/searchElement" }, + "index": { "$ref": "#/$defs/index" }, "deleteIfNeeded": { "description": "Delete the partition if needed to make space.", "const": true @@ -165,6 +170,7 @@ "type": "object", "additionalProperties": false, "properties": { + "index": { "$ref": "#/$defs/index" }, "name": { "description": "Volume group name.", "type": "string", @@ -255,6 +261,7 @@ "type": "object", "additionalProperties": false, "properties": { + "index": { "$ref": "#/$defs/index" }, "name": { "description": "Logical volume name.", "type": "string", @@ -272,6 +279,7 @@ "additionalProperties": false, "required": ["pool"], "properties": { + "index": { "$ref": "#/$defs/index" }, "pool": { "description": "LVM thin pool.", "const": true @@ -293,6 +301,7 @@ "additionalProperties": false, "required": ["usedPool"], "properties": { + "index": { "$ref": "#/$defs/index" }, "name": { "description": "Thin logical volume name.", "type": "string", @@ -310,6 +319,11 @@ "minimum": 1, "maximum": 128 }, + "index": { + "description": "Autogenerated index used for matching solved and unsolved configs.", + "type": "integer", + "minimum": 0 + }, "searchElement": { "anyOf": [ { "$ref": "#/$defs/simpleSearchAll" }, 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..d4e775eb46 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -458,7 +458,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/from_json_conversions/drive.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb index 8fd661dd6c..c1c0b56b79 100644 --- a/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/drive.rb @@ -53,6 +53,7 @@ def convert # @return [Hash] def conversions { + index: drive_json[:index], search: convert_search, alias: drive_json[:alias], encryption: convert_encryption, diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb index cbd98c3e34..557236a2ab 100644 --- a/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/logical_volume.rb @@ -50,6 +50,7 @@ def convert # @return [Hash] def conversions { + index: logical_volume_json[:index], alias: logical_volume_json[:alias], encryption: convert_encryption, filesystem: convert_filesystem, diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb index 00823a0c45..f66eee3ab4 100644 --- a/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/partition.rb @@ -52,6 +52,7 @@ def convert # @return [Hash] def conversions { + index: partition_json[:index], search: convert_search, alias: partition_json[:alias], encryption: convert_encryption, diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb index f1516456b7..f560e87cc4 100644 --- a/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/volume_group.rb @@ -44,6 +44,7 @@ def convert # @return [Hash] def conversions { + index: volume_group_json[:index], name: volume_group_json[:name], extent_size: convert_extent_size, physical_volumes_devices: convert_physical_volumes_devices, diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb index 5a6f9bf1b1..fce72a52ff 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/drive.rb @@ -49,6 +49,7 @@ def self.config_type # @see Base#conversions def conversions { + index: config.index, search: convert_search, alias: config.alias, encryption: convert_encryption, diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb index 531bcd2054..41a0c7318f 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/logical_volume.rb @@ -45,6 +45,7 @@ def self.config_type # @see Base#conversions def conversions { + index: config.index, alias: config.alias, encryption: convert_encryption, filesystem: convert_filesystem, diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb index 9155ebe2aa..84f1caf2fd 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb @@ -51,6 +51,7 @@ def conversions return convert_delete_if_needed if config.delete_if_needed? { + index: config.index, search: convert_search, alias: config.alias, encryption: convert_encryption, @@ -63,6 +64,7 @@ def conversions # @return [Hash] def convert_delete { + index: config.index, search: convert_search, delete: true } @@ -71,6 +73,7 @@ def convert_delete # @return [Hash] def convert_delete_if_needed { + index: config.index, search: convert_search, size: convert_size, deleteIfNeeded: true diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb index ffe0be78a4..edc31dd968 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/volume_group.rb @@ -40,6 +40,7 @@ def self.config_type # @see Base#conversions def conversions { + index: config.index, name: config.name, extentSize: config.extent_size&.to_i, physicalVolumes: convert_physical_volumes, 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_json_solver.rb b/service/lib/agama/storage/config_json_solver.rb index 3f05dda4af..9105047116 100644 --- a/service/lib/agama/storage/config_json_solver.rb +++ b/service/lib/agama/storage/config_json_solver.rb @@ -73,6 +73,7 @@ def initialize(default_paths: [], mandatory_paths: []) def solve(config_json) @config_json = config_json + solve_indexes solve_generate end @@ -87,6 +88,20 @@ def solve(config_json) # @return [Hash] attr_reader :config_json + def solve_indexes + drives = config_json[:drives] || [] + assign_indexes(drives) + drives.each { |d| assign_indexes(d[:partitions] || []) } + + volume_groups = config_json[:volumeGroups] || [] + assign_indexes(volume_groups) + volume_groups.each { |d| assign_indexes(d[:logicalVolumes] || []) } + end + + def assign_indexes(configs) + configs.each_with_index { |c, i| c[:index] = i } + end + def solve_generate configs = configs_with_generate return unless configs.any? @@ -98,12 +113,16 @@ def solve_generate # @param config [Hash] Drive or volume group config (e.g., { partitions: [...] }). def expand_generate(config) configs = volume_configs(config) - index = configs.index { |v| with_generate?(v) } + generate_config = configs.find { |c| with_generate?(c) } + + return unless generate_config + + current_index = configs.index(generate_config) + index = generate_config[:index] || current_index - return unless index + configs[current_index] = volumes_from_generate(generate_config) + .each { |c| c[:index] = index } - generate_config = configs[index] - configs[index] = volumes_from_generate(generate_config) configs.flatten! end diff --git a/service/lib/agama/storage/configs/drive.rb b/service/lib/agama/storage/configs/drive.rb index 8c6c3a7bcf..1c2c37f704 100644 --- a/service/lib/agama/storage/configs/drive.rb +++ b/service/lib/agama/storage/configs/drive.rb @@ -21,6 +21,7 @@ require "agama/storage/configs/search" require "agama/storage/configs/with_alias" +require "agama/storage/configs/with_index" require "agama/storage/configs/with_search" module Agama @@ -29,6 +30,7 @@ module Configs # Section of the configuration representing a device that is expected to exist in the target # system and that can be used as a regular disk. class Drive + include WithIndex include WithAlias include WithSearch diff --git a/service/lib/agama/storage/configs/logical_volume.rb b/service/lib/agama/storage/configs/logical_volume.rb index 1f1696a5a1..7a201067f5 100644 --- a/service/lib/agama/storage/configs/logical_volume.rb +++ b/service/lib/agama/storage/configs/logical_volume.rb @@ -21,12 +21,14 @@ require "agama/storage/configs/size" require "agama/storage/configs/with_alias" +require "agama/storage/configs/with_index" module Agama module Storage module Configs # Section of the configuration representing a LVM logical volume. class LogicalVolume + include WithIndex include WithAlias # @return [String, nil] diff --git a/service/lib/agama/storage/configs/partition.rb b/service/lib/agama/storage/configs/partition.rb index 7d8a853e47..ff5e42e096 100644 --- a/service/lib/agama/storage/configs/partition.rb +++ b/service/lib/agama/storage/configs/partition.rb @@ -20,14 +20,16 @@ # find current contact information at www.suse.com. require "agama/storage/configs/size" -require "agama/storage/configs/with_search" require "agama/storage/configs/with_alias" +require "agama/storage/configs/with_index" +require "agama/storage/configs/with_search" module Agama module Storage module Configs # Section of the configuration representing a partition class Partition + include WithIndex include WithAlias include WithSearch diff --git a/service/lib/agama/storage/configs/volume_group.rb b/service/lib/agama/storage/configs/volume_group.rb index c595b6e531..a11ef3248c 100644 --- a/service/lib/agama/storage/configs/volume_group.rb +++ b/service/lib/agama/storage/configs/volume_group.rb @@ -19,11 +19,15 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. +require "agama/storage/configs/with_index" + module Agama module Storage module Configs # Section of the configuration representing a LVM volume group. class VolumeGroup + include WithIndex + # @return [String, nil] attr_accessor :name diff --git a/service/lib/agama/storage/configs/with_index.rb b/service/lib/agama/storage/configs/with_index.rb new file mode 100644 index 0000000000..351cdf5ffb --- /dev/null +++ b/service/lib/agama/storage/configs/with_index.rb @@ -0,0 +1,34 @@ +# 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 Configs + # Mixin for configs with index. + # + # The index is used for matching with the correponding element from an unsolved config. + module WithIndex + # @return [Integer, nil] + attr_accessor :index + 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/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index 53f312caf3..996dcc3696 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, @@ -658,6 +658,7 @@ def serialize(value) }, drives: [ { + index: 0, search: { condition: { name: "/dev/sda" }, ifNotFound: "error", @@ -665,6 +666,7 @@ def serialize(value) }, partitions: [ { + index: 0, filesystem: { reuseIfPossible: false, path: "/", 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..55f10e2447 100644 --- a/service/test/agama/storage/config_conversions/from_json_test.rb +++ b/service/test/agama/storage/config_conversions/from_json_test.rb @@ -545,8 +545,10 @@ partition1, partition2 = partitions expect(partition1).to be_a(Agama::Storage::Configs::Partition) + expect(partition1.index).to eq(0) expect(partition1.filesystem.path).to eq("/") expect(partition2).to be_a(Agama::Storage::Configs::Partition) + expect(partition2.index).to eq(1) expect(partition2.filesystem.path).to eq("/test") end end @@ -668,14 +670,17 @@ test_part = partitions.find { |p| p.filesystem.path == "/test" } expect(root_part).to_not be_nil + expect(root_part.index).to eq(0) expect(root_part.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS2) expect(root_part.encryption.password).to eq("12345") expect(swap_part).to_not be_nil + expect(swap_part.index).to eq(0) expect(swap_part.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS2) expect(swap_part.encryption.password).to eq("12345") expect(test_part).to_not be_nil + expect(test_part.index).to eq(1) expect(test_part.encryption).to be_nil end end @@ -693,10 +698,12 @@ default1 = configs.find { |c| c.filesystem.path == "/default1" } expect(default1).to_not be_nil + expect(default1.index).to eq(0) expect(default1.encryption).to be_nil default2 = configs.find { |c| c.filesystem.path == "/default2" } expect(default2).to_not be_nil + expect(default2.index).to eq(0) expect(default2.encryption).to be_nil end end @@ -711,6 +718,7 @@ mandatory1 = configs.find { |c| c.filesystem.path == "/mandatory1" } expect(mandatory1).to_not be_nil + expect(mandatory1.index).to eq(0) expect(mandatory1.encryption).to be_nil end end @@ -812,8 +820,10 @@ drive1, drive2 = config.drives expect(drive1.alias).to eq("first-disk") expect(drive1.partitions).to eq([]) + expect(drive1.index).to eq(0) expect(drive2.alias).to eq("second-disk") expect(drive2.partitions).to eq([]) + expect(drive2.index).to eq(1) end end @@ -917,8 +927,10 @@ volume_group1, volume_group2 = config.volume_groups expect(volume_group1.name).to eq("vg1") + expect(volume_group1.index).to eq(0) expect(volume_group1.logical_volumes).to eq([]) expect(volume_group2.name).to eq("vg2") + expect(volume_group2.index).to eq(1) expect(volume_group2.logical_volumes).to eq([]) end end @@ -1160,8 +1172,10 @@ lv1, lv2 = lvs expect(lv1).to be_a(Agama::Storage::Configs::LogicalVolume) expect(lv1.name).to eq("root") + expect(lv1.index).to eq(0) expect(lv2).to be_a(Agama::Storage::Configs::LogicalVolume) expect(lv2.name).to eq("test") + expect(lv2.index).to eq(1) end end @@ -1330,14 +1344,17 @@ test_lv = lvs.find { |v| v.name == "test" } expect(root_lv).to_not be_nil + expect(root_lv.index).to eq(0) expect(root_lv.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS2) expect(root_lv.encryption.password).to eq("12345") expect(swap_lv).to_not be_nil + expect(swap_lv.index).to eq(0) expect(swap_lv.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS2) expect(swap_lv.encryption.password).to eq("12345") expect(test_lv).to_not be_nil + expect(test_lv.index).to eq(1) expect(test_lv.encryption).to be_nil end end @@ -1382,8 +1399,11 @@ swap_part = partitions.find { |p| p.filesystem.path == "swap" } home_part = partitions.find { |p| p.filesystem.path == "/home" } expect(root_part).to_not be_nil + expect(root_part.index).to eq(0) expect(swap_part).to_not be_nil + expect(swap_part.index).to eq(0) expect(home_part).to_not be_nil + expect(home_part.index).to eq(1) end end @@ -1466,7 +1486,9 @@ root_part = partitions.find { |p| p.filesystem.path == "/" } swap_part = partitions.find { |p| p.filesystem.path == "swap" } expect(root_part).to_not be_nil + expect(root_part.index).to eq(0) expect(swap_part).to_not be_nil + expect(swap_part.index).to eq(0) end end @@ -1532,8 +1554,11 @@ swap_lv = lvs.find { |v| v.filesystem.path == "swap" } home_lv = lvs.find { |v| v.filesystem.path == "/home" } expect(root_lv).to_not be_nil + expect(root_lv.index).to eq(0) expect(swap_lv).to_not be_nil + expect(swap_lv.index).to eq(0) expect(home_lv).to_not be_nil + expect(home_lv.index).to eq(1) end end @@ -1616,7 +1641,9 @@ root_lv = lvs.find { |v| v.filesystem.path == "/" } swap_lv = lvs.find { |v| v.filesystem.path == "swap" } expect(root_lv).to_not be_nil + expect(root_lv.index).to eq(0) expect(swap_lv).to_not be_nil + expect(swap_lv.index).to eq(0) end end diff --git a/service/test/agama/storage/config_conversions/to_json_test.rb b/service/test/agama/storage/config_conversions/to_json_test.rb index 4b6b9d1062..6efb40ed68 100644 --- a/service/test/agama/storage/config_conversions/to_json_test.rb +++ b/service/test/agama/storage/config_conversions/to_json_test.rb @@ -464,6 +464,7 @@ expect(partitions_json).to eq( [ { + index: 0, search: { condition: { name: "/dev/vda1" }, ifNotFound: "error" @@ -471,6 +472,7 @@ alias: "vda1" }, { + index: 1, search: { condition: { name: "/dev/vda2" }, ifNotFound: "error" @@ -648,15 +650,18 @@ it "generates the expected JSON for 'drives'" do drives_json = subject.convert[:drives] - default_drive_json = { - search: { ifNotFound: "error", max: 1 }, - partitions: [] - } - expect(drives_json).to eq( [ - default_drive_json, - default_drive_json + { + index: 0, + search: { ifNotFound: "error", max: 1 }, + partitions: [] + }, + { + index: 1, + search: { ifNotFound: "error", max: 1 }, + partitions: [] + } ] ) end @@ -752,11 +757,13 @@ expect(volume_groups_json).to eq( [ { + index: 0, name: "vg1", physicalVolumes: [], logicalVolumes: [] }, { + index: 1, name: "vg2", physicalVolumes: [], logicalVolumes: [] @@ -912,12 +919,14 @@ expect(config_json[:logicalVolumes]).to eq( [ { - name: "lv1", - pool: false + index: 0, + name: "lv1", + pool: false }, { - name: "lv2", - pool: false + index: 1, + name: "lv2", + pool: false } ] ) 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..dacf57849d 100644 --- a/service/test/agama/storage/config_solver_test.rb +++ b/service/test/agama/storage/config_solver_test.rb @@ -607,6 +607,7 @@ it "expands the number of drives to match all the existing disks" do subject.solve(config) expect(config.drives.size).to eq 3 + expect(config.drives.map(&:index)).to all(eq(0)) search1, search2, search3 = config.drives.map(&:search) expect(search1.solved?).to eq(true) expect(search1.device.name).to eq("/dev/vda") @@ -631,6 +632,7 @@ it "expands the number of drives to match all the existing disks" do subject.solve(config) expect(config.drives.size).to eq 3 + expect(config.drives.map(&:index)).to all(eq(0)) expect(config.drives.map(&:search).map(&:solved?)).to all(eq(true)) expect(config.drives.map(&:search).map(&:device).map(&:name)) .to eq ["/dev/vda", "/dev/vdb", "/dev/vdc"] @@ -654,6 +656,7 @@ it "expands the number of drives to match the max" do subject.solve(config) expect(config.drives.size).to eq 2 + expect(config.drives.map(&:index)).to all(eq(0)) expect(config.drives.map(&:search).map(&:solved?)).to all(eq(true)) expect(config.drives.map(&:search).map(&:device).map(&:name)) .to eq ["/dev/vda", "/dev/vdb"] @@ -666,6 +669,7 @@ it "expands the number of drives to match all the existing disks" do subject.solve(config) expect(config.drives.size).to eq 3 + expect(config.drives.map(&:index)).to all(eq(0)) expect(config.drives.map(&:search).map(&:solved?)).to all(eq(true)) expect(config.drives.map(&:search).map(&:device).map(&:name)) .to eq ["/dev/vda", "/dev/vdb", "/dev/vdc"] @@ -762,6 +766,7 @@ it "expands the number of partition configs to match all the existing partitions" do subject.solve(config) drive_partitions = config.drives.first.partitions + expect(drive_partitions.map(&:index)).to all(eq(0)) expect(drive_partitions.size).to eq 3 search1, search2, search3 = drive_partitions.map(&:search) expect(search1.solved?).to eq(true) @@ -785,6 +790,11 @@ subject.solve(config) drive_partitions = config.drives.first.partitions expect(drive_partitions.size).to eq 5 + expect(drive_partitions[0].index).to eq(0) + expect(drive_partitions[1].index).to eq(0) + expect(drive_partitions[2].index).to eq(0) + expect(drive_partitions[3].index).to eq(1) + expect(drive_partitions[4].index).to eq(2) searches = drive_partitions[3..-1].map(&:search) expect(searches.map(&:solved?)).to eq [true, true] expect(searches.map(&:device)).to eq [nil, nil] @@ -805,6 +815,7 @@ subject.solve(config) drive_partitions = config.drives.first.partitions expect(drive_partitions.size).to eq 3 + expect(drive_partitions.map(&:index)).to all(eq(0)) expect(drive_partitions.map(&:search).map(&:solved?)).to all(eq(true)) expect(drive_partitions.map(&:search).map(&:device).map(&:name)) .to eq ["/dev/vda1", "/dev/vda2", "/dev/vda3"] @@ -863,9 +874,11 @@ it "does not set a partition to the config" do subject.solve(config) - search = config.drives.first.partitions.last.search - expect(search.solved?).to eq(true) - expect(search.device).to be_nil + *partitions, last_partition = config.drives.first.partitions + expect(partitions.map(&:index)).to all(eq(0)) + expect(last_partition.index).to eq(1) + expect(last_partition.search.solved?).to eq(true) + expect(last_partition.search.device).to be_nil end end @@ -904,6 +917,7 @@ subject.solve(config) drive_partitions = config.drives.first.partitions expect(drive_partitions.size).to eq 2 + expect(drive_partitions.map(&:index)).to all(eq(0)) expect(drive_partitions.map(&:search).map(&:solved?)).to all(eq(true)) expect(drive_partitions.map(&:search).map(&:device).map(&:name)) .to eq ["/dev/vda1", "/dev/vda2"] @@ -917,6 +931,7 @@ subject.solve(config) drive_partitions = config.drives.first.partitions expect(drive_partitions.size).to eq 3 + expect(drive_partitions.map(&:index)).to all(eq(0)) expect(drive_partitions.map(&:search).map(&:solved?)).to all(eq(true)) expect(drive_partitions.map(&:search).map(&:device).map(&:name)) .to eq ["/dev/vda1", "/dev/vda2", "/dev/vda3"] 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..39b778229e 100644 --- a/service/test/agama/storage/proposal_test.rb +++ b/service/test/agama/storage/proposal_test.rb @@ -325,6 +325,7 @@ def drive(partitions) boot: { configure: false }, drives: [ { + index: 0, search: { condition: { name: "/dev/sda" }, ifNotFound: "error", diff --git a/web/src/api/storage/types/config.ts b/web/src/api/storage/types/config.ts index 6245c83964..0cc47675f3 100644 --- a/web/src/api/storage/types/config.ts +++ b/web/src/api/storage/types/config.ts @@ -16,6 +16,10 @@ export type SimpleSearchByName = string; * How to handle the section if the device is not found. */ export type SearchAction = "skip" | "error"; +/** + * Autogenerated index used for matching solved and unsolved configs. + */ +export type Index = number; /** * Alias used to reference a device. */ @@ -137,6 +141,7 @@ export interface Boot { */ export interface FormattedDrive { search?: SearchElement; + index?: Index; alias?: Alias; encryption?: Encryption; filesystem: Filesystem; @@ -233,6 +238,7 @@ export interface FilesystemTypeBtrfs { } export interface PartitionedDrive { search?: SearchElement; + index?: Index; alias?: Alias; ptableType?: PtableType; partitions?: PartitionElement[]; @@ -254,6 +260,7 @@ export interface AdvancedPartitionsGenerator { } export interface RegularPartition { search?: SearchElement; + index?: Index; alias?: Alias; id?: PartitionId; size?: Size; @@ -269,6 +276,7 @@ export interface SizeRange { } export interface PartitionToDelete { search: SearchElement; + index?: Index; /** * Delete the partition. */ @@ -276,6 +284,7 @@ export interface PartitionToDelete { } export interface PartitionToDeleteIfNeeded { search: SearchElement; + index?: Index; /** * Delete the partition if needed to make space. */ @@ -286,6 +295,7 @@ export interface PartitionToDeleteIfNeeded { * LVM volume group. */ export interface VolumeGroup { + index?: Index; /** * Volume group name. */ @@ -324,6 +334,7 @@ export interface AdvancedLogicalVolumesGenerator { }; } export interface LogicalVolume { + index?: Index; /** * Logical volume name. */ @@ -335,6 +346,7 @@ export interface LogicalVolume { filesystem?: Filesystem; } export interface ThinPoolLogicalVolume { + index?: Index; /** * LVM thin pool. */ @@ -350,6 +362,7 @@ export interface ThinPoolLogicalVolume { encryption?: Encryption; } export interface ThinLogicalVolume { + index?: Index; /** * Thin logical volume name. */ diff --git a/web/src/components/storage/ConfigEditor.tsx b/web/src/components/storage/ConfigEditor.tsx new file mode 100644 index 0000000000..e31b3f011f --- /dev/null +++ b/web/src/components/storage/ConfigEditor.tsx @@ -0,0 +1,45 @@ +/* + * 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 React from "react"; +import { _ } from "~/i18n"; +import { useConfig, useSolvedConfig, useConfigDevices } from "~/queries/storage"; + +export default function ConfigEditor() { + const config = useConfig(); + const solvedConfig = useSolvedConfig(); + const configDevices = useConfigDevices(); + + if (!solvedConfig) return null; + + console.log("config: ", config); + console.log("solved config: ", solvedConfig); + console.log("devices: ", configDevices); + + return ( + + ); +} diff --git a/web/src/storage/model/config.test.ts b/web/src/storage/model/config.test.ts index eed3735cef..d4484354fc 100644 --- a/web/src/storage/model/config.test.ts +++ b/web/src/storage/model/config.test.ts @@ -23,7 +23,7 @@ import * as model from "~/storage/model/config"; describe("#generate", () => { - it("returns a list of devices", () => { + it("returns the expected list of devices from a config", () => { expect( model.generate( { @@ -35,16 +35,19 @@ describe("#generate", () => { { drives: [ { + index: 0, search: "/dev/vda", partitions: [ - { search: "/dev/vda1", delete: true }, - { search: "/dev/vda2", delete: true }, + { index: 0, search: "/dev/vda1", delete: true }, + { index: 0, search: "/dev/vda2", delete: true }, ], }, { + index: 1, search: "/dev/vdb", partitions: [ { + index: 0, filesystem: { type: "xfs", path: "/test" }, size: { min: 1024, max: 2048 }, }, @@ -55,33 +58,39 @@ describe("#generate", () => { ), ).toEqual([ { + index: 0, name: "/dev/vda", alias: undefined, spacePolicy: "delete", partitions: [ { + index: 0, name: "/dev/vda1", delete: true, }, { + index: 0, name: "/dev/vda2", delete: true, }, ], }, { + index: 1, name: "/dev/vdb", alias: undefined, spacePolicy: "keep", partitions: [ { + index: 0, name: undefined, alias: undefined, - resizeIfNeeded: true, + resize: undefined, + resizeIfNeeded: undefined, filesystem: "xfs", mountPath: "/test", snapshots: undefined, - size: { min: 1024, max: 2048 }, + size: { auto: true, min: 1024, max: 2048 }, }, ], }, diff --git a/web/src/storage/model/config.ts b/web/src/storage/model/config.ts index fc3a121b88..eff9d6ab4b 100644 --- a/web/src/storage/model/config.ts +++ b/web/src/storage/model/config.ts @@ -40,12 +40,11 @@ class ConfigDevicesGenerator { private generateDrives(): Drive[] { const solvedDriveConfigs = this.solvedConfig.drives || []; - return solvedDriveConfigs.map((c, i) => this.generateDrive(c, i)); + return solvedDriveConfigs.map((c) => this.generateDrive(c)); } - 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]; + private generateDrive(solvedDriveConfig: config.DriveElement): Drive { + const driveConfig = (this.config.drives || [])[solvedDriveConfig.index]; return generateDrive(driveConfig, solvedDriveConfig); } } diff --git a/web/src/storage/model/config/drive.test.ts b/web/src/storage/model/config/drive.test.ts index 82f1c9ed55..347550bc81 100644 --- a/web/src/storage/model/config/drive.test.ts +++ b/web/src/storage/model/config/drive.test.ts @@ -23,7 +23,7 @@ import * as model from "~/storage/model/config/drive"; describe("#generate", () => { - it("returns a drive object from a drive section", () => { + it("returns the expected drive object from a drive section", () => { expect( model.generate(undefined, { search: "/dev/vda", @@ -34,6 +34,7 @@ describe("#generate", () => { }, }), ).toEqual({ + index: undefined, name: "/dev/vda", alias: "test", filesystem: "xfs", @@ -45,9 +46,11 @@ describe("#generate", () => { expect( model.generate( { - partitions: [{ search: "*", delete: true }], + alias: "test", + filesystem: { path: "/test" }, }, { + index: 0, search: "/dev/vda", alias: "test", filesystem: { @@ -59,6 +62,7 @@ describe("#generate", () => { }, ), ).toEqual({ + index: 0, name: "/dev/vda", alias: "test", filesystem: "btrfs", @@ -73,26 +77,43 @@ describe("#generate", () => { partitions: [{ search: "*", delete: true }, { generate: "default" }], }, { + index: 0, search: "/dev/vda", - partitions: [{ search: "/dev/vda1", delete: true }, { filesystem: { path: "/" } }], + partitions: [ + { + index: 0, + search: "/dev/vda1", + delete: true, + }, + { + index: 1, + filesystem: { path: "/", type: "ext4" }, + size: { min: 1024, max: 2048 }, + }, + ], }, ), ).toEqual({ + index: 0, name: "/dev/vda", alias: undefined, spacePolicy: "delete", partitions: [ { + index: 0, name: "/dev/vda1", delete: true, }, { + index: 1, name: undefined, alias: undefined, - filesystem: undefined, + resize: undefined, + resizeIfNeeded: undefined, + filesystem: "ext4", mountPath: "/", snapshots: undefined, - size: undefined, + size: { auto: true, min: 1024, max: 2048 }, }, ], }); diff --git a/web/src/storage/model/config/drive.ts b/web/src/storage/model/config/drive.ts index 7d6aa54115..8db4ada38a 100644 --- a/web/src/storage/model/config/drive.ts +++ b/web/src/storage/model/config/drive.ts @@ -24,6 +24,7 @@ import { config } from "~/api/storage/types"; import * as checks from "~/api/storage/types/checks"; import { Partition, + PartitionConfig, isPartitionConfig, generate as generatePartition, } from "~/storage/model/config/partition"; @@ -31,6 +32,7 @@ import { SpacePolicy, generate as generateSpacePolicy } from "~/storage/model/co import { generateName, generateFilesystem, generateSnapshots } from "~/storage/model/config/common"; export type Drive = { + index?: number; name?: string; alias?: string; filesystem?: string; @@ -62,6 +64,7 @@ class DriveGenerator { private fromFormattedDrive(solvedDriveConfig: config.FormattedDrive): Drive { return { + index: solvedDriveConfig.index, name: generateName(solvedDriveConfig), alias: solvedDriveConfig.alias, spacePolicy: this.generateSpacePolicy(), @@ -73,6 +76,7 @@ class DriveGenerator { private fromPartitionedDrive(solvedDriveConfig: config.PartitionedDrive): Drive { return { + index: solvedDriveConfig.index, name: generateName(solvedDriveConfig), alias: solvedDriveConfig.alias, spacePolicy: this.generateSpacePolicy(), @@ -88,11 +92,25 @@ class DriveGenerator { private generatePartitions(solvedDriveConfig: config.PartitionedDrive): Partition[] { const solvedPartitionConfigs = solvedDriveConfig.partitions || []; - return solvedPartitionConfigs.filter(isPartitionConfig).map(generatePartition); + return solvedPartitionConfigs.filter(isPartitionConfig).map((c) => this.generatePartition(c)); + } + + private generatePartition(solvedPartitionConfig: PartitionConfig): Partition { + let partitionConfig: config.PartitionElement | undefined; + + if (this.driveConfig !== undefined && checks.isPartitionedDrive(this.driveConfig)) { + const partitionConfigs = this.driveConfig.partitions || []; + partitionConfig = partitionConfigs[solvedPartitionConfig.index]; + } + + return generatePartition(partitionConfig, solvedPartitionConfig); } } -export function generate(config: config.DriveElement, solvedConfig: config.DriveElement): Drive { - const generator = new DriveGenerator(config, solvedConfig); +export function generate( + driveConfig: config.DriveElement | undefined, + solvedDriveConfig: config.DriveElement, +): Drive { + const generator = new DriveGenerator(driveConfig, solvedDriveConfig); return generator.generate(); } diff --git a/web/src/storage/model/config/partition.test.ts b/web/src/storage/model/config/partition.test.ts index 310a101fb6..8d764b7cf1 100644 --- a/web/src/storage/model/config/partition.test.ts +++ b/web/src/storage/model/config/partition.test.ts @@ -23,66 +23,151 @@ import * as model from "~/storage/model/config/partition"; describe("#generate", () => { - it("returns a partition object from a partition section", () => { + it("returns the expeced partition object from a partition section", () => { expect( - model.generate({ - search: "/dev/vda1", - delete: true, - }), + model.generate( + { + search: "*", + delete: true, + }, + { + index: 1, + search: "/dev/vda1", + delete: true, + }, + ), ).toEqual({ + index: 1, name: "/dev/vda1", delete: true, }); expect( - model.generate({ - search: "/dev/vda1", - deleteIfNeeded: true, - size: 1024, - }), + model.generate( + { + search: "*", + deleteIfNeeded: true, + }, + { + index: 0, + search: "/dev/vda1", + deleteIfNeeded: true, + }, + ), + ).toEqual({ + index: 0, + name: "/dev/vda1", + deleteIfNeeded: true, + resizeIfNeeded: false, + resize: false, + size: undefined, + }); + + expect( + model.generate( + { + search: "*", + deleteIfNeeded: true, + size: 1024, + }, + { + index: 0, + search: "/dev/vda1", + deleteIfNeeded: true, + size: { min: 1024, max: 1024 }, + }, + ), ).toEqual({ + index: 0, name: "/dev/vda1", deleteIfNeeded: true, resizeIfNeeded: false, - size: { min: 1024, max: 1024 }, + resize: true, + size: { auto: false, min: 1024, max: 1024 }, }); expect( - model.generate({ - search: "/dev/vda1", - alias: "test", - filesystem: { - path: "/test", - type: { - btrfs: { snapshots: true }, + model.generate( + { + search: "*", + deleteIfNeeded: true, + size: { min: 1024 }, + }, + { + index: 0, + search: "/dev/vda1", + deleteIfNeeded: true, + size: { min: 1024 }, + }, + ), + ).toEqual({ + index: 0, + name: "/dev/vda1", + deleteIfNeeded: true, + resizeIfNeeded: true, + resize: false, + size: { auto: false, min: 1024 }, + }); + + expect( + model.generate( + { + search: "/dev/vda1", + alias: "test", + filesystem: { path: "/test" }, + }, + { + index: 0, + search: "/dev/vda1", + alias: "test", + filesystem: { + path: "/test", + type: { + btrfs: { snapshots: true }, + }, }, + size: { min: 0, max: 2048 }, }, - size: { min: 0, max: 2048 }, - }), + ), ).toEqual({ + index: 0, name: "/dev/vda1", alias: "test", - resizeIfNeeded: true, + resizeIfNeeded: false, + resize: false, filesystem: "btrfs", mountPath: "/test", snapshots: true, - size: { min: 0, max: 2048 }, + size: { auto: true, min: 0, max: 2048 }, }); expect( - model.generate({ - filesystem: { - path: "/test", + model.generate( + { + filesystem: { path: "/test" }, + size: 1024, + }, + { + index: 0, + filesystem: { + path: "/test", + type: { + btrfs: { snapshots: true }, + }, + }, + size: { min: 1024, max: 1024 }, }, - }), + ), ).toEqual({ + index: 0, name: undefined, alias: undefined, resizeIfNeeded: undefined, - filesystem: undefined, + resize: undefined, + filesystem: "btrfs", mountPath: "/test", - snapshots: undefined, - size: undefined, + snapshots: true, + size: { auto: false, min: 1024, max: 1024 }, }); }); }); diff --git a/web/src/storage/model/config/partition.ts b/web/src/storage/model/config/partition.ts index 6f0025d143..719af89553 100644 --- a/web/src/storage/model/config/partition.ts +++ b/web/src/storage/model/config/partition.ts @@ -22,14 +22,16 @@ 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 { Size, generate as generateSize } from "~/storage/model/config/size"; import { generateName, generateFilesystem, generateSnapshots } from "~/storage/model/config/common"; export type Partition = { + index?: number; name?: string; alias?: string; delete?: boolean; deleteIfNeeded?: boolean; + resize?: boolean; resizeIfNeeded?: boolean; filesystem?: string; mountPath?: string; @@ -52,64 +54,97 @@ export function isPartitionConfig( ); } +type PartitionWithSizeConfig = config.RegularPartition | config.PartitionToDeleteIfNeeded; + +function isPartitionWithSizeConfig( + partition: config.PartitionElement, +): partition is PartitionWithSizeConfig { + return checks.isRegularPartition(partition) || checks.isPartitionToDeleteIfNeeded(partition); +} + class PartitionGenerator { - private partitionConfig: PartitionConfig; + private partitionConfig: config.PartitionElement | undefined; + private solvedPartitionConfig: PartitionConfig; - constructor(partitionConfig: PartitionConfig) { + constructor( + partitionConfig: config.PartitionElement | undefined, + solvedPartitionConfig: PartitionConfig, + ) { this.partitionConfig = partitionConfig; + this.solvedPartitionConfig = solvedPartitionConfig; } 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); + if (checks.isRegularPartition(this.solvedPartitionConfig)) { + return this.fromRegularPartition(this.solvedPartitionConfig); + } else if (checks.isPartitionToDelete(this.solvedPartitionConfig)) { + return this.fromPartitionToDelete(this.solvedPartitionConfig); + } else if (checks.isPartitionToDeleteIfNeeded(this.solvedPartitionConfig)) { + return this.fromPartitionToDeleteIfNeeded(this.solvedPartitionConfig); } } - private fromRegularPartition(partitionConfig: config.RegularPartition): Partition { + private fromRegularPartition(solvedPartitionConfig: 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), + index: this.solvedPartitionConfig.index, + name: generateName(solvedPartitionConfig), + alias: solvedPartitionConfig.alias, + resize: this.generateResize(), + resizeIfNeeded: this.generateResizeIfNeeded(), + filesystem: generateFilesystem(solvedPartitionConfig), + mountPath: solvedPartitionConfig.filesystem?.path, + snapshots: generateSnapshots(solvedPartitionConfig), + size: this.generateSize(), }; } - private fromPartitionToDelete(partitionConfig: config.PartitionToDelete): Partition { + private fromPartitionToDelete(solvedPartitionConfig: config.PartitionToDelete): Partition { return { - name: generateName(partitionConfig), + index: this.solvedPartitionConfig.index, + name: generateName(solvedPartitionConfig), delete: true, }; } private fromPartitionToDeleteIfNeeded( - partitionConfig: config.PartitionToDeleteIfNeeded, + solvedPartitionConfig: config.PartitionToDeleteIfNeeded, ): Partition { return { - name: generateName(partitionConfig), + index: this.solvedPartitionConfig.index, + name: generateName(solvedPartitionConfig), deleteIfNeeded: true, - resizeIfNeeded: this.generateResizeIfNeeded(partitionConfig), - size: generateSize(partitionConfig), + resize: this.generateResize(), + resizeIfNeeded: this.generateResizeIfNeeded(), + size: this.generateSize(), }; } - private generateResizeIfNeeded( - partitionConfig: TypeWithSize, - ): boolean | undefined { - if (!partitionConfig.size) return; + private generateSize(): Size | undefined { + if (!isPartitionWithSizeConfig(this.solvedPartitionConfig)) return; + + return generateSize(this.partitionConfig, this.solvedPartitionConfig); + } + + // TODO: return false if the size is equal to the size of the system device. + private generateResize(): boolean | undefined { + if (this.solvedPartitionConfig.search === undefined) return; + + const size = this.generateSize(); + return size !== undefined && !size.auto && size.min !== undefined && size.min === size.max; + } + + private generateResizeIfNeeded(): boolean | undefined { + if (this.solvedPartitionConfig.search === undefined) return; - const size = generateSize(partitionConfig); - return size.min !== undefined && size.min !== size.max; + const size = this.generateSize(); + return size !== undefined && !size.auto && size.min !== size.max; } } -export function generate(config: PartitionConfig): Partition { - const generator = new PartitionGenerator(config); +export function generate( + partitionConfig: config.PartitionElement | undefined, + solvedPartitionConfig: PartitionConfig, +): Partition { + const generator = new PartitionGenerator(partitionConfig, solvedPartitionConfig); return generator.generate(); } diff --git a/web/src/storage/model/config/size.test.ts b/web/src/storage/model/config/size.test.ts index 8cb49a866d..b180c9ca1d 100644 --- a/web/src/storage/model/config/size.test.ts +++ b/web/src/storage/model/config/size.test.ts @@ -23,99 +23,112 @@ import * as model from "~/storage/model/config/size"; describe("#generate", () => { - it("returns the size in bytes from the size section", () => { + it("returns the expected size object from a size section", () => { expect( - model.generate({ - size: 1024, - }), - ).toEqual({ min: 1024, max: 1024 }); + model.generate( + {}, + { + size: "1 KiB", + }, + ), + ).toEqual({ auto: true, min: 1024, max: 1024 }); expect( - model.generate({ - size: "1 KiB", - }), - ).toEqual({ min: 1024, max: 1024 }); + model.generate( + {}, + { + size: "1KiB", + }, + ), + ).toEqual({ auto: true, min: 1024, max: 1024 }); expect( - model.generate({ - size: "1KiB", - }), - ).toEqual({ min: 1024, max: 1024 }); + model.generate( + {}, + { + size: "1kb", + }, + ), + ).toEqual({ auto: true, min: 1000, max: 1000 }); expect( - model.generate({ - size: "1kb", - }), - ).toEqual({ min: 1000, max: 1000 }); + model.generate( + {}, + { + size: "1k", + }, + ), + ).toEqual({ auto: true, min: 1000, max: 1000 }); expect( - model.generate({ - size: "1k", - }), - ).toEqual({ min: 1000, max: 1000 }); + model.generate( + {}, + { + size: "665.284 TiB", + }, + ), + ).toEqual({ auto: true, min: 731487493773328, max: 731487493773328 }); - expect( - model.generate({ - size: "665.284 TiB", - }), - ).toEqual({ min: 731487493773328, max: 731487493773328 }); + expect(model.generate({ size: 1024 }, { size: { min: 1024, max: 1024 } })).toEqual({ + auto: false, + min: 1024, + max: 1024, + }); - expect( - model.generate({ - size: [1024], - }), - ).toEqual({ min: 1024 }); + expect(model.generate({}, { size: { min: 1024, max: 1024 } })).toEqual({ + auto: true, + min: 1024, + max: 1024, + }); - expect( - model.generate({ - size: [1024, 2048], - }), - ).toEqual({ min: 1024, max: 2048 }); + expect(model.generate({}, { size: [1024] })).toEqual({ auto: true, min: 1024 }); - expect( - model.generate({ - size: ["1 kib", "2 KIB"], - }), - ).toEqual({ min: 1024, max: 2048 }); + expect(model.generate({}, { size: [1024, 2048] })).toEqual({ + auto: true, + min: 1024, + max: 2048, + }); expect( - model.generate({ - size: { - min: 1024, + model.generate( + {}, + { + size: ["1 kib", "2 KIB"], }, - }), - ).toEqual({ min: 1024 }); + ), + ).toEqual({ auto: true, min: 1024, max: 2048 }); - expect( - model.generate({ - size: { - min: 1024, - max: 2048, - }, - }), - ).toEqual({ min: 1024, max: 2048 }); + expect(model.generate({}, { size: { min: 1024 } })).toEqual({ auto: true, min: 1024 }); + + expect(model.generate({}, { size: { min: 1024, max: 2048 } })).toEqual({ + auto: true, + min: 1024, + max: 2048, + }); expect( - model.generate({ - size: { - min: "1 kib", - max: "2 KiB", + model.generate( + {}, + { + size: { + min: "1 kib", + max: "2 KiB", + }, }, - }), - ).toEqual({ min: 1024, max: 2048 }); + ), + ).toEqual({ auto: true, min: 1024, max: 2048 }); }); + + // This scenario should not happen because the solved config does not return "custom" value. it("returns undefined for 'custom' value", () => { - expect( - model.generate({ - size: { - min: "custom", - max: 2048, - }, - }), - ).toEqual({ min: undefined, max: 2048 }); + expect(model.generate({}, { size: { min: "custom", max: 2048 } })).toEqual({ + auto: true, + min: undefined, + max: 2048, + }); }); it("returns undefined if there is no size section", () => { - expect(model.generate({})).toBeUndefined; + expect(model.generate({}, {})).toBeUndefined; }); }); diff --git a/web/src/storage/model/config/size.ts b/web/src/storage/model/config/size.ts index 657aef034d..01b1b32326 100644 --- a/web/src/storage/model/config/size.ts +++ b/web/src/storage/model/config/size.ts @@ -25,24 +25,28 @@ import * as checks from "~/api/storage/types/checks"; import xbytes from "xbytes"; export type Size = { + auto: boolean; min?: number; max?: number; }; -export interface WithSize { +interface WithSize { size?: config.Size; } +type AnyConfig = object | undefined; + class SizeGenerator { - private config: TypeWithSize; + private config: AnyConfig; + private solvedConfig: TypeWithSize; - constructor(config: TypeWithSize) { + constructor(config: AnyConfig, solvedConfig: TypeWithSize) { this.config = config; + this.solvedConfig = solvedConfig; } - // TODO: detect auto size by checking the unsolved config. generate(): Size | undefined { - const size = this.config.size; + const size = this.solvedConfig.size; if (!size) return; if (checks.isSizeValue(size)) return this.fromSizeValue(size); @@ -52,23 +56,50 @@ class SizeGenerator { private fromSizeValue(value: config.SizeValue): Size { const bytes = this.bytes(value); - return { min: bytes, max: bytes }; + + return { + auto: this.generateAuto(), + 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]); + const size: Size = { + auto: this.generateAuto(), + 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); + const size: Size = { + auto: this.generateAuto(), + min: this.bytes(sizeRange.min), + }; + + if (sizeRange.max) { + size.max = this.bytes(sizeRange.max); + } return size; } + private generateAuto(): boolean { + return this.config === undefined || !("size" in this.config); + } + + 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; + } + 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(); @@ -77,14 +108,11 @@ class SizeGenerator { 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(); +export function generate( + config: AnyConfig, + solvedConfig: TypeWithSize, +): Size | undefined { + return new SizeGenerator(config, solvedConfig).generate(); } diff --git a/web/src/storage/model/config/space-policy.test.ts b/web/src/storage/model/config/space-policy.test.ts index 80a8ad66c3..d8fdd318ce 100644 --- a/web/src/storage/model/config/space-policy.test.ts +++ b/web/src/storage/model/config/space-policy.test.ts @@ -24,24 +24,14 @@ 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"); + 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: "*", delete: true }] })).toEqual("delete"); expect( - model.generate({ - partitions: [{ search: { ifNotFound: "skip" }, delete: true }], - }), + model.generate({ partitions: [{ search: { ifNotFound: "skip" }, delete: true }] }), ).toEqual("delete"); expect( @@ -53,16 +43,12 @@ describe("#generate", () => { it("returns 'resize' if there is a 'shrink all' partition", () => { expect( - model.generate({ - partitions: [{ search: "*", size: { min: 0, max: "current" } }], - }), + 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: "*", size: [0, "current"] }] })).toEqual( + "resize", + ); expect( model.generate({ @@ -78,52 +64,34 @@ describe("#generate", () => { }); 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: "/dev/vda", delete: true }] })).toEqual( + "custom", + ); expect( - model.generate({ - partitions: [{ search: { max: 2, ifNotFound: "skip" }, delete: true }], - }), + 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 }] })).toEqual( + "custom", + ); expect( - model.generate({ - partitions: [{ search: "*", deleteIfNeeded: true, size: [0, "current"] }], - }), + 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 } }] })).toEqual("custom"); - expect( - model.generate({ - partitions: [{ search: "*", size: { min: 0, max: 1024 } }], - }), - ).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", delete: true }] })).toEqual( + "custom", + ); expect( - model.generate({ - partitions: [{ search: "/dev/vda", size: { min: 0, max: "current" } }], - }), + model.generate({ partitions: [{ search: "/dev/vda", size: { min: 0, max: "current" } }] }), ).toEqual("custom"); expect( @@ -134,24 +102,16 @@ describe("#generate", () => { }); 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: [{ search: "*", filesystem: { type: "xfs" } }] })).toEqual( + "keep", + ); expect( - model.generate({ - partitions: [{ generate: "default" }, { filesystem: { path: "/home" } }], - }), + 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"); + expect(model.generate({ search: "/dev/vda" })).toEqual("keep"); }); });