diff --git a/rust/agama-lib/share/storage.schema.json b/rust/agama-lib/share/storage.schema.json index c01588d992..a95fa83dbb 100644 --- a/rust/agama-lib/share/storage.schema.json +++ b/rust/agama-lib/share/storage.schema.json @@ -242,6 +242,7 @@ "required": ["name"], "properties": { "name": { "$ref": "#/$defs/baseName" }, + "search": { "$ref": "#/$defs/volumeGroupSearch" }, "extentSize": { "$ref": "#/$defs/sizeValue" }, "physicalVolumes": { "description": "Devices to use as physical volumes.", @@ -328,6 +329,7 @@ "additionalProperties": false, "properties": { "name": { "$ref": "#/$defs/baseName" }, + "search": { "$ref": "#/$defs/logicalVolumeSearch" }, "size": { "$ref": "#/$defs/size" }, "stripes": { "$ref": "#/$defs/logicalVolumeStripes" }, "stripeSize": { "$ref": "#/$defs/sizeValue" }, @@ -476,6 +478,57 @@ "minProperties": 1, "maxProperties": 1 }, + "volumeGroupSearch": { + "anyOf": [ + { "$ref": "#/$defs/searchAll" }, + { "$ref": "#/$defs/searchName" }, + { "$ref": "#/$defs/volumeGroupAdvancedSearch" } + ] + }, + "volumeGroupAdvancedSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "condition": { "$ref": "#/$defs/volumeGroupSearchCondition" }, + "sort": { "$ref": "#/$defs/volumeGroupSearchSort" }, + "max": { "$ref": "#/$defs/searchMax" }, + "ifNotFound": { "$ref": "#/$defs/searchCreatableActions" } + } + }, + "volumeGroupSearchCondition": { + "anyOf": [ + { "$ref": "#/$defs/searchConditionName" }, + { "$ref": "#/$defs/searchConditionSize" } + ] + }, + "volumeGroupSearchSort": { + "anyOf": [ + { "$ref": "#/$defs/volumeGroupSearchSortCriterion" }, + { + "type": "array", + "items": { "$ref": "#/$defs/volumeGroupSearchSortCriterion" } + } + ] + }, + "volumeGroupSearchSortCriterion": { + "anyOf": [ + { "$ref": "#/$defs/volumeGroupSearchSortCriterionShort" }, + { "$ref": "#/$defs/volumeGroupSearchSortCriterionFull" } + ] + }, + "volumeGroupSearchSortCriterionShort": { + "enum": ["name", "size"] + }, + "volumeGroupSearchSortCriterionFull": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/searchSortCriterionOrder" }, + "size": { "$ref": "#/$defs/searchSortCriterionOrder" } + }, + "minProperties": 1, + "maxProperties": 1 + }, "partitionSearch": { "anyOf": [ { "$ref": "#/$defs/searchAll" }, @@ -517,6 +570,29 @@ { "$ref": "#/$defs/searchConditionPartitionNumber" } ] }, + "logicalVolumeSearch": { + "anyOf": [ + { "$ref": "#/$defs/searchAll" }, + { "$ref": "#/$defs/searchName" }, + { "$ref": "#/$defs/logicalVolumeAdvancedSearch" } + ] + }, + "logicalVolumeAdvancedSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "condition": { "$ref": "#/$defs/logicalVolumeSearchCondition" }, + "sort": { "$ref": "#/$defs/logicalVolumeSearchSort" }, + "max": { "$ref": "#/$defs/searchMax" }, + "ifNotFound": { "$ref": "#/$defs/searchCreatableActions" } + } + }, + "logicalVolumeSearchCondition": { + "anyOf": [ + { "$ref": "#/$defs/searchConditionName" }, + { "$ref": "#/$defs/searchConditionSize" } + ] + }, "searchConditionName": { "type": "object", "additionalProperties": false, @@ -605,6 +681,34 @@ "minProperties": 1, "maxProperties": 1 }, + "logicalVolumeSearchSort": { + "anyOf": [ + { "$ref": "#/$defs/logicalVolumeSearchSortCriterion" }, + { + "type": "array", + "items": { "$ref": "#/$defs/logicalVolumeSearchSortCriterion" } + } + ] + }, + "logicalVolumeSearchSortCriterion": { + "anyOf": [ + { "$ref": "#/$defs/logicalVolumeSearchSortCriterionShort" }, + { "$ref": "#/$defs/logicalVolumeSearchSortCriterionFull" } + ] + }, + "logicalVolumeSearchSortCriterionShort": { + "enum": ["name", "size", "number"] + }, + "logicalVolumeSearchSortCriterionFull": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/searchSortCriterionOrder" }, + "size": { "$ref": "#/$defs/searchSortCriterionOrder" } + }, + "minProperties": 1, + "maxProperties": 1 + }, "searchMax": { "description": "Maximum devices to match.", "type": "integer", diff --git a/service/lib/agama/storage/config_checkers/volume_group.rb b/service/lib/agama/storage/config_checkers/volume_group.rb index 65ab6ef145..89de8178eb 100644 --- a/service/lib/agama/storage/config_checkers/volume_group.rb +++ b/service/lib/agama/storage/config_checkers/volume_group.rb @@ -71,7 +71,7 @@ def issues # # @return [Issue, nil] def name_issue - return if config.name && !config.name.empty? + return if config.vg_name && !config.vg_name.empty? error( _("There is a volume group without name"), 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 ea0b2be263..4d2edc64fa 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2024] SUSE LLC +# Copyright (c) [2024-2026] SUSE LLC # # All Rights Reserved. # @@ -23,6 +23,7 @@ require "agama/storage/config_conversions/from_json_conversions/with_encryption" require "agama/storage/config_conversions/from_json_conversions/with_filesystem" require "agama/storage/config_conversions/from_json_conversions/with_size" +require "agama/storage/config_conversions/from_json_conversions/with_search" require "agama/storage/configs/logical_volume" require "y2storage/disk_size" @@ -37,6 +38,7 @@ class LogicalVolume < Base include WithEncryption include WithFilesystem include WithSize + include WithSearch # @see Base # @return [Configs::LogicalVolume] @@ -50,15 +52,18 @@ def default_config # @return [Hash] def conversions { - alias: logical_volume_json[:alias], - encryption: convert_encryption, - filesystem: convert_filesystem, - size: convert_size, - name: logical_volume_json[:name], - stripes: logical_volume_json[:stripes], - stripe_size: convert_stripe_size, - pool: logical_volume_json[:pool], - used_pool: logical_volume_json[:usedPool] + alias: logical_volume_json[:alias], + search: convert_search, + encryption: convert_encryption, + filesystem: convert_filesystem, + size: convert_size, + name: logical_volume_json[:name], + stripes: logical_volume_json[:stripes], + stripe_size: convert_stripe_size, + pool: logical_volume_json[:pool], + used_pool: logical_volume_json[:usedPool], + delete: logical_volume_json[:delete], + delete_if_needed: logical_volume_json[:deleteIfNeeded] } end 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 3a7688ab97..d8567f88f9 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2024-2025] SUSE LLC +# Copyright (c) [2024-2026] SUSE LLC # # All Rights Reserved. # @@ -22,6 +22,7 @@ require "agama/storage/config_conversions/from_json_conversions/base" require "agama/storage/config_conversions/from_json_conversions/encryption" require "agama/storage/config_conversions/from_json_conversions/logical_volume" +require "agama/storage/config_conversions/from_json_conversions/with_search" require "agama/storage/configs/volume_group" require "y2storage/disk_size" @@ -33,6 +34,8 @@ module FromJSONConversions class VolumeGroup < Base private + include WithSearch + alias_method :volume_group_json, :config_json # @see Base @@ -46,6 +49,7 @@ def default_config def conversions { name: volume_group_json[:name], + search: convert_search, extent_size: convert_extent_size, physical_volumes_devices: convert_physical_volumes_devices, physical_volumes_encryption: convert_physical_volumes_encryption, diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/logical_volume.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/logical_volume.rb index fd040277ef..e22f85d7c1 100644 --- a/service/lib/agama/storage/config_conversions/from_model_conversions/logical_volume.rb +++ b/service/lib/agama/storage/config_conversions/from_model_conversions/logical_volume.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2025] SUSE LLC +# Copyright (c) [2025-2026] SUSE LLC # # All Rights Reserved. # @@ -22,6 +22,7 @@ require "agama/storage/config_conversions/from_model_conversions/base" require "agama/storage/config_conversions/from_model_conversions/with_filesystem" require "agama/storage/config_conversions/from_model_conversions/with_size" +require "agama/storage/config_conversions/from_model_conversions/with_search" require "agama/storage/configs/logical_volume" require "y2storage/disk_size" @@ -33,6 +34,7 @@ module FromModelConversions class LogicalVolume < Base include WithFilesystem include WithSize + include WithSearch private @@ -49,6 +51,7 @@ def default_config def conversions { name: logical_volume_model[:lvName], + search: convert_search, filesystem: convert_filesystem, size: convert_size, stripes: logical_volume_model[:stripes], diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/volume_group.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/volume_group.rb index 70042f8542..6f00248906 100644 --- a/service/lib/agama/storage/config_conversions/from_model_conversions/volume_group.rb +++ b/service/lib/agama/storage/config_conversions/from_model_conversions/volume_group.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2025] SUSE LLC +# Copyright (c) [2025-2026] SUSE LLC # # All Rights Reserved. # @@ -22,6 +22,7 @@ require "agama/storage/config_conversions/from_model_conversions/base" require "agama/storage/config_conversions/from_model_conversions/logical_volume" require "agama/storage/config_conversions/from_model_conversions/encryption" +require "agama/storage/config_conversions/from_model_conversions/with_search" require "agama/storage/configs/volume_group" require "y2storage/disk_size" @@ -31,6 +32,8 @@ module ConfigConversions module FromModelConversions # Volume group conversion from model according to the JSON schema. class VolumeGroup < Base + include WithSearch + # @param model_json [Hash] # @param targets [Array] # @param encryption_model [Hash, nil] @@ -61,6 +64,7 @@ def default_config def conversions { name: volume_group_model[:vgName], + search: convert_search, extent_size: convert_extent_size, physical_volumes_devices: convert_physical_volumes_devices, physical_volumes_encryption: convert_physical_volumes_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 b025ae0abf..1a9143ccb2 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2024] SUSE LLC +# Copyright (c) [2024-2026] SUSE LLC # # All Rights Reserved. # @@ -23,6 +23,7 @@ require "agama/storage/config_conversions/to_json_conversions/with_encryption" require "agama/storage/config_conversions/to_json_conversions/with_filesystem" require "agama/storage/config_conversions/to_json_conversions/with_size" +require "agama/storage/config_conversions/to_json_conversions/with_search" module Agama module Storage @@ -33,6 +34,8 @@ class LogicalVolume < Base include WithEncryption include WithFilesystem include WithSize + include WithSearch + include WithDelete # @param config [Configs::LogicalVolume] def initialize(config) @@ -44,8 +47,11 @@ def initialize(config) # @see Base#conversions def conversions + return convert_delete if convert_delete? + { alias: config.alias, + search: convert_search, encryption: convert_encryption, filesystem: convert_filesystem, size: convert_size, 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 99bda1c7ce..b86476e146 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/partition.rb @@ -24,6 +24,7 @@ require "agama/storage/config_conversions/to_json_conversions/with_filesystem" require "agama/storage/config_conversions/to_json_conversions/with_search" require "agama/storage/config_conversions/to_json_conversions/with_size" +require "agama/storage/config_conversions/to_json_conversions/with_delete" module Agama module Storage @@ -35,6 +36,7 @@ class Partition < Base include WithEncryption include WithFilesystem include WithSize + include WithDelete # @param config [Configs::Partition] def initialize(config) @@ -46,9 +48,7 @@ def initialize(config) # @see Base#conversions def conversions - return convert_delete if config.delete? - - return convert_delete_if_needed if config.delete_if_needed? + return convert_delete if convert_delete? { search: convert_search, @@ -59,23 +59,6 @@ def conversions id: config.id&.to_s } end - - # @return [Hash] - def convert_delete - { - search: convert_search, - delete: true - } - end - - # @return [Hash] - def convert_delete_if_needed - { - search: convert_search, - size: convert_size, - deleteIfNeeded: true - } - end end end end 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 76ccc7eb00..7418520ca4 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2024] SUSE LLC +# Copyright (c) [2024-2026] SUSE LLC # # All Rights Reserved. # @@ -22,6 +22,7 @@ require "agama/storage/config_conversions/to_json_conversions/base" require "agama/storage/config_conversions/to_json_conversions/encryption" require "agama/storage/config_conversions/to_json_conversions/logical_volume" +require "agama/storage/config_conversions/to_json_conversions/with_search" module Agama module Storage @@ -29,6 +30,8 @@ module ConfigConversions module ToJSONConversions # Volume group conversion to JSON hash according to schema. class VolumeGroup < Base + include WithSearch + # @param config [Configs::VolumeGroup] def initialize(config) super() @@ -41,6 +44,7 @@ def initialize(config) def conversions { name: config.name, + search: convert_search, extentSize: config.extent_size&.to_i, physicalVolumes: convert_physical_volumes, logicalVolumes: convert_logical_volumes diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/with_delete.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/with_delete.rb new file mode 100644 index 0000000000..a710e342f7 --- /dev/null +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/with_delete.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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_json_conversions/search" + +module Agama + module Storage + module ConfigConversions + module ToJSONConversions + # Mixin for delete conversion to JSON. + # + # Including this mixin also includes WithSearch and WithSize. + module WithDelete + include WithSize + include WithSearch + + # @return [Hash, nil] + def convert_delete + return unless convert_delete? + + config.delete? ? convert_mandatory_delete : convert_optional_delete + end + + def convert_delete? + config.delete? || config.delete_if_needed? + end + + # @return [Hash] + def convert_mandatory_delete + { + search: convert_search, + delete: true + } + end + + # @return [Hash] + def convert_optional_delete + { + search: convert_search, + size: convert_size, + deleteIfNeeded: true + } + end + end + end + end + end +end diff --git a/service/lib/agama/storage/config_solver.rb b/service/lib/agama/storage/config_solver.rb index e959a9d23b..cda0b0ccc4 100644 --- a/service/lib/agama/storage/config_solver.rb +++ b/service/lib/agama/storage/config_solver.rb @@ -49,6 +49,7 @@ def solve(config) ConfigSolvers::Filesystem.new(product_config).solve(config) ConfigSolvers::DrivesSearch.new(storage_system).solve(config) ConfigSolvers::MdRaidsSearch.new(storage_system).solve(config) + ConfigSolvers::VolumeGroupsSearch.new(storage_system).solve(config) # Sizes and boot must be solved once the searches are solved. ConfigSolvers::Boot.new(product_config, storage_system).solve(config) ConfigSolvers::Size.new(product_config).solve(config) diff --git a/service/lib/agama/storage/config_solvers.rb b/service/lib/agama/storage/config_solvers.rb index 5c19a76f80..2215b3f072 100644 --- a/service/lib/agama/storage/config_solvers.rb +++ b/service/lib/agama/storage/config_solvers.rb @@ -25,6 +25,8 @@ require "agama/storage/config_solvers/filesystem" require "agama/storage/config_solvers/md_raids_search" require "agama/storage/config_solvers/partitions_search" +require "agama/storage/config_solvers/volume_groups_search" +require "agama/storage/config_solvers/logical_volumes_search" require "agama/storage/config_solvers/size" module Agama diff --git a/service/lib/agama/storage/config_solvers/logical_volumes_search.rb b/service/lib/agama/storage/config_solvers/logical_volumes_search.rb new file mode 100644 index 0000000000..643d32eb5b --- /dev/null +++ b/service/lib/agama/storage/config_solvers/logical_volumes_search.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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_solvers/devices_search" +require "agama/storage/config_solvers/search_matchers" + +module Agama + module Storage + module ConfigSolvers + # Solver for the search of the logical volume configs. + class LogicalVolumesSearch < DevicesSearch + include SearchMatchers + + # Solves the search of the logical volume configs. + # + # @note The config object is modified. + # + # @param config [Configs::VolumeGroup] + # @return [Configs::VolumeGroup] + def solve(config) + candidate_lvs = config.found_device&.all_lvm_lvs || [] + config.logical_volumes = super(config.logical_volumes, candidate_lvs) + config + end + + private + + # @see DevicesSearch#match_condition? + # @param lv_config [Configs::LogicalVolume] + # @param lvm_lv [Y2Storage::LvmLv] + # + # @return [Boolean] + def match_condition?(lv_config, lvm_lv) + match_name?(lv_config, lvm_lv) && match_size?(lv_config, lvm_lv) + end + + # @see DevicesSearch#solve_with_device + def solve_with_device(device_config, device) + result = super + result.pool = result.found_device.lv_type.is?(:thin_pool) + result.name = result.found_device.lv_name + result + end + end + end + end +end diff --git a/service/lib/agama/storage/config_solvers/volume_groups_search.rb b/service/lib/agama/storage/config_solvers/volume_groups_search.rb new file mode 100644 index 0000000000..4156e2d2c7 --- /dev/null +++ b/service/lib/agama/storage/config_solvers/volume_groups_search.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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_solvers/devices_search" +require "agama/storage/config_solvers/logical_volumes_search" +require "agama/storage/config_solvers/search_matchers" + +module Agama + module Storage + module ConfigSolvers + # Solver for the search of the volume group configs. + class VolumeGroupsSearch < DevicesSearch + include SearchMatchers + + # @param storage_system [Storage::System] + def initialize(storage_system) + super() + @storage_system = storage_system + end + + # Solves the search of the volume group configs and solves the searches of their + # logical volumes. + # + # @note The config object is modified. + # + # @param config [Storage::Config] + # @return [Storage::Config] + def solve(config) + config.volume_groups = super(config.volume_groups, storage_system.devicegraph.lvm_vgs) + config.volume_groups.each { |vg| LogicalVolumesSearch.new.solve(vg) } + config + end + + private + + # @return [Storage::System] + attr_reader :storage_system + + # @see DevicesSearch#match_condition? + # @param volume_group_config [Configs::VolumeGroup] + # @param volume_group [Y2Storage::LvmVg] + # + # @return [Boolean] + def match_condition?(volume_group_config, volume_group) + match_name?(volume_group_config, volume_group) && + match_size?(volume_group_config, volume_group) + end + end + end + end +end diff --git a/service/lib/agama/storage/configs/logical_volume.rb b/service/lib/agama/storage/configs/logical_volume.rb index 65120bf269..4c24965458 100644 --- a/service/lib/agama/storage/configs/logical_volume.rb +++ b/service/lib/agama/storage/configs/logical_volume.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2024-2025] SUSE LLC +# Copyright (c) [2024-2026] SUSE LLC # # All Rights Reserved. # @@ -22,6 +22,8 @@ require "agama/storage/configs/size" require "agama/storage/configs/with_alias" require "agama/storage/configs/with_filesystem" +require "agama/storage/configs/with_search" +require "agama/storage/configs/with_delete" module Agama module Storage @@ -30,6 +32,8 @@ module Configs class LogicalVolume include WithAlias include WithFilesystem + include WithSearch + include WithDelete # @return [String, nil] attr_accessor :name @@ -54,6 +58,7 @@ class LogicalVolume attr_accessor :encryption def initialize + initialize_delete @size = Size.new @pool = false end diff --git a/service/lib/agama/storage/configs/partition.rb b/service/lib/agama/storage/configs/partition.rb index babbfd0bf7..834a51cefa 100644 --- a/service/lib/agama/storage/configs/partition.rb +++ b/service/lib/agama/storage/configs/partition.rb @@ -23,12 +23,15 @@ require "agama/storage/configs/with_alias" require "agama/storage/configs/with_filesystem" require "agama/storage/configs/with_search" +require "agama/storage/configs/with_delete" module Agama module Storage module Configs # Section of the configuration representing a partition class Partition + include WithDelete + # Partition config meaning "delete all partitions". # # @return [Configs::Partition] @@ -53,14 +56,6 @@ def self.new_for_shrink_any_if_needed include WithFilesystem include WithSearch - # @return [Boolean] - attr_accessor :delete - alias_method :delete?, :delete - - # @return [Boolean] - attr_accessor :delete_if_needed - alias_method :delete_if_needed?, :delete_if_needed - # @return [Y2Storage::PartitionId, nil] attr_accessor :id @@ -71,9 +66,8 @@ def self.new_for_shrink_any_if_needed attr_accessor :encryption def initialize + initialize_delete @size = Size.new - @delete = false - @delete_if_needed = false end end end diff --git a/service/lib/agama/storage/configs/volume_group.rb b/service/lib/agama/storage/configs/volume_group.rb index ae225f45aa..311d811a4b 100644 --- a/service/lib/agama/storage/configs/volume_group.rb +++ b/service/lib/agama/storage/configs/volume_group.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2024] SUSE LLC +# Copyright (c) [2024-2026] SUSE LLC # # All Rights Reserved. # @@ -19,13 +19,19 @@ # 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_search" + module Agama module Storage module Configs # Section of the configuration representing a LVM volume group. class VolumeGroup + include WithSearch + # Base name. # + # @see #vg_name + # # @return [String, nil] e.g., "system". attr_accessor :name @@ -59,6 +65,15 @@ def initialize def logical_volume?(device_alias) logical_volumes.find { |l| l.alias?(device_alias) } end + + # Name to be used for the volume group + # + # For reused devices it comes from the real device, for new devices is set by {#name}. + # + # @return [String] + def vg_name + found_device ? found_device.vg_name : name + end end end end diff --git a/service/lib/agama/storage/configs/with_delete.rb b/service/lib/agama/storage/configs/with_delete.rb new file mode 100644 index 0000000000..5a62b9a633 --- /dev/null +++ b/service/lib/agama/storage/configs/with_delete.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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 attributes to delete pre-existing devices. + module WithDelete + # @return [Boolean] + attr_accessor :delete + alias_method :delete?, :delete + + # @return [Boolean] + attr_accessor :delete_if_needed + alias_method :delete_if_needed?, :delete_if_needed + + # Sets initial value for attributes related to deleting. + def initialize_delete + @delete = false + @delete_if_needed = false + end + end + end + end +end diff --git a/service/lib/y2storage/agama_proposal.rb b/service/lib/y2storage/agama_proposal.rb index e6be3b93f9..931438fc87 100644 --- a/service/lib/y2storage/agama_proposal.rb +++ b/service/lib/y2storage/agama_proposal.rb @@ -27,7 +27,7 @@ require "y2storage/proposal" require "y2storage/proposal/agama_devices_creator" require "y2storage/proposal/agama_devices_planner" -require "y2storage/proposal/agama_space_maker" +require "y2storage/proposal/space_settings_builder" require "y2storage/proposal/planned_devices_handler" module Y2Storage @@ -106,7 +106,8 @@ def calculate_proposal return @devices end - @space_maker = Proposal::AgamaSpaceMaker.new(disk_analyzer, config) + space_settings = Proposal::SpaceSettingsBuilder.new(config).space_settings + @space_maker = Proposal::SpaceMaker.new(disk_analyzer, space_settings) @devices = propose_devicegraph end diff --git a/service/lib/y2storage/proposal/agama_device_planner.rb b/service/lib/y2storage/proposal/agama_device_planner.rb index 1fb571faaf..fcc18781c0 100644 --- a/service/lib/y2storage/proposal/agama_device_planner.rb +++ b/service/lib/y2storage/proposal/agama_device_planner.rb @@ -70,7 +70,7 @@ def configure_reuse(planned, config) return unless device planned.assign_reuse(device) - planned.reformat = reformat?(device, config) + planned.reformat = reformat?(device, config) if planned.respond_to?(:reformat=) planned.resize = grow?(device, config) if planned.respond_to?(:resize=) end @@ -194,7 +194,7 @@ def configure_pv(planned, device_config, config) vg = config.volume_groups.find { |v| v.physical_volumes.include?(device_config.alias) } return unless vg - planned.lvm_volume_group_name = vg.name + planned.lvm_volume_group_name = vg.vg_name end # @param planned [Planned::Disk, Planned::Partition] diff --git a/service/lib/y2storage/proposal/agama_devices_creator.rb b/service/lib/y2storage/proposal/agama_devices_creator.rb index 75570e9a54..001617deb2 100644 --- a/service/lib/y2storage/proposal/agama_devices_creator.rb +++ b/service/lib/y2storage/proposal/agama_devices_creator.rb @@ -173,7 +173,6 @@ def process_new_partitionables(devices) # @see #process_devices def process_volume_groups - # TODO: Reuse volume groups. planned_devices.vgs.map { |v| create_volume_group(v) } end @@ -201,10 +200,10 @@ def automatic_vgs_for_existing # @param planned [Planned::LvmVg] def create_volume_group(planned) pv_names = physical_volumes_for(planned.volume_group_name) - # TODO: Generate issue if there are no physical volumes. - return if pv_names.empty? + # TODO: Generate issue if there are no physical volumes for a new VG. + return if pv_names.empty? && !planned.reuse? - creator = Proposal::LvmCreator.new(creator_result.devicegraph) + creator = Proposal::LvmCreator.new(creator_result.devicegraph, space_maker.settings) new_result = creator.create_volumes(planned, pv_names) self.creator_result = creator_result.merge(new_result) end diff --git a/service/lib/y2storage/proposal/agama_vg_planner.rb b/service/lib/y2storage/proposal/agama_vg_planner.rb index 497fc78d29..e993bc0b6e 100644 --- a/service/lib/y2storage/proposal/agama_vg_planner.rb +++ b/service/lib/y2storage/proposal/agama_vg_planner.rb @@ -46,12 +46,13 @@ def planned_vg(vg_config) # automatically generated if missing? # # @see AgamaDevicePlanner#configure_pv - Y2Storage::Planned::LvmVg.new(volume_group_name: vg_config.name).tap do |planned| + Y2Storage::Planned::LvmVg.new(volume_group_name: vg_config.vg_name).tap do |planned| planned.extent_size = vg_config.extent_size planned.lvs = planned_lvs(vg_config) planned.size_strategy = :use_needed planned.pvs_candidate_devices = devices_for_pvs(vg_config) configure_pvs_encryption(planned, vg_config) + configure_reuse(planned, vg_config) end end @@ -143,6 +144,7 @@ def planned_lv(config, type) planned.stripe_size = config.stripe_size configure_block_device(planned, config) configure_size(planned, config.size) + configure_reuse(planned, config) end end end diff --git a/service/lib/y2storage/proposal/agama_space_maker.rb b/service/lib/y2storage/proposal/space_settings_builder.rb similarity index 61% rename from service/lib/y2storage/proposal/agama_space_maker.rb rename to service/lib/y2storage/proposal/space_settings_builder.rb index b30d3359ea..8fdaf720ed 100644 --- a/service/lib/y2storage/proposal/agama_space_maker.rb +++ b/service/lib/y2storage/proposal/space_settings_builder.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2024] SUSE LLC +# Copyright (c) [2024-2026] SUSE LLC # # All Rights Reserved. # @@ -19,35 +19,32 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "y2storage/proposal/space_maker" require "y2storage/proposal_settings" module Y2Storage module Proposal - # Space maker for Agama. - # - # FIXME: this class must dissappear. It does not implement any own logic compared to the - # original SpaceMaker. It simply encapsulates the conversion from Agama config to - # ProposalSpaceSettings. - class AgamaSpaceMaker < SpaceMaker - # @param disk_analyzer [DiskAnalyzer] + # Class to encapsulate the conversion from Agama config to ProposalSpaceSettings + class SpaceSettingsBuilder # @param config [Agama::Storage::Config] - def initialize(disk_analyzer, config) - super(disk_analyzer, space_settings(config)) + def initialize(config) + @config = config end - private - - # Method used by the constructor to convert the Agama config to ProposalSpaceSettings + # ProposalSpaceSettings corresponding to the Agama configuration # - # @param config [Agama::Storage::Config] - def space_settings(config) + # @return [ProposalSpaceSettings] + def space_settings Y2Storage::ProposalSpaceSettings.new.tap do |target| target.strategy = :bigger_resize target.actions = space_actions(config) end end + private + + # @return [Agama::Storage::Config] + attr_reader :config + # Space actions from the given config. # # @param config [Agama::Storage::Config] @@ -63,10 +60,10 @@ def space_actions(config) # @param config [Agama::Storage::Config] # @return [Array] def force_delete_actions(config) - partition_configs = partitions(config).select(&:delete?) - partition_names = device_names(partition_configs) + configs = config_devices(config).select(&:delete?) + names = device_names(configs) - partition_names.map { |p| Y2Storage::SpaceActions::Delete.new(p, mandatory: true) } + names.map { |d| Y2Storage::SpaceActions::Delete.new(d, mandatory: true) } end # Space actions for devices that might be deleted. @@ -76,10 +73,10 @@ def force_delete_actions(config) # @param config [Agama::Storage::Config] # @return [Array] def delete_actions(config) - partition_configs = partitions(config).select(&:delete_if_needed?).reject(&:delete?) - partition_names = device_names(partition_configs) + configs = config_devices(config).select(&:delete_if_needed?).reject(&:delete?) + names = device_names(configs) - partition_names.map { |p| Y2Storage::SpaceActions::Delete.new(p) } + names.map { |d| Y2Storage::SpaceActions::Delete.new(d) } end # Space actions for devices that might be resized @@ -87,36 +84,36 @@ def delete_actions(config) # @param config [Agama::Storage::Config] # @return [Array] def resize_actions(config) - partition_configs = partitions(config).select(&:found_device).select(&:size) + configs = config_devices(config).select(&:found_device).select(&:size) # Resize actions contain information that is potentially useful for the SpaceMaker even # when they are only about growing and not shrinking - partition_configs.map { |p| resize_action(p) }.compact + configs.map { |c| resize_action(c) }.compact end # @see #resize_actions # - # @param part [Agama::Storage::Configs::Partition] + # @param dev [Agama::Storage::Configs::Partition, Agama::Storage::Configs::LogicalVolume] # @return [Y2Storage::SpaceActions::Resize, nil] - def resize_action(part) - min = current_size?(part, :min) ? nil : part.size.min - max = current_size?(part, :max) ? nil : part.size.max + def resize_action(dev) + min = current_size?(dev, :min) ? nil : dev.size.min + max = current_size?(dev, :max) ? nil : dev.size.max # If both min and max are equal to the current device size, there is nothing to do return unless min || max - Y2Storage::SpaceActions::Resize.new(part.found_device.name, min_size: min, max_size: max) + Y2Storage::SpaceActions::Resize.new(dev.found_device.name, min_size: min, max_size: max) end # @see #resize_actions - def current_size?(part, attr) - part.found_device.size == part.size.public_send(attr) + def current_size?(dev, attr) + dev.found_device.size == dev.size.public_send(attr) end - # All partition configs from the given config. + # All partition and logical volume configs from the given config. # # @param config [Agama::Storage::Config] - # @return [Array] - def partitions(config) - config.partitions + # @return [Array] + def config_devices(config) + config.partitions + config.logical_volumes end # Device names from the given configs. diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 3e71b9bd23..eaac2f5fec 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Fri Mar 6 13:03:21 UTC 2026 - Ancor Gonzalez Sosa + +- Allow to reuse LVM volume groups, volumes and thin pools + (jsc#PED-15104, bsc#1254718 and gh#agama-project/agama#3171). + ------------------------------------------------------------------- Tue Mar 3 12:35:55 UTC 2026 - José Iván López González diff --git a/service/test/agama/storage/config_solver_test.rb b/service/test/agama/storage/config_solver_test.rb index 2e16fb4c32..40c0d449c8 100644 --- a/service/test/agama/storage/config_solver_test.rb +++ b/service/test/agama/storage/config_solver_test.rb @@ -577,5 +577,58 @@ include_examples "new volume size", logical_volumes_proc end end + + context "for a reused volume group config" do + let(:scenario) { "several_vgs.yaml" } + + let(:config_json) do + { + volumeGroups: [ + { + search: { size: { greater: "40 GiB" } }, + logicalVolumes: logical_volumes + } + ] + } + end + + let(:logical_volumes) { nil } + + it "solves the search" do + subject.solve(config) + vg = config.volume_groups.first + expect(vg.search.solved?).to eq(true) + expect(vg.search.device.name).to eq("/dev/data") + end + + context "for a new logical volume config" do + let(:logical_volumes) do + [ + { + encryption: encryption, + filesystem: filesystem + } + ] + end + + let(:encryption) { nil } + let(:filesystem) { nil } + + logical_volume_proc = proc { |c| c.volume_groups.first.logical_volumes.first } + include_examples "block device", logical_volume_proc + end + + context "for a reused logical volume" do + let(:logical_volumes) { [{ search: "*" }] } + + it "solves the search" do + subject.solve(config) + vg = config.volume_groups.first + lv = vg.logical_volumes.first + expect(lv.search.solved?).to eq(true) + expect(lv.search.device.name).to eq("/dev/data/home") + end + end + end end end diff --git a/service/test/agama/storage/config_solvers/logical_volumes_search_test.rb b/service/test/agama/storage/config_solvers/logical_volumes_search_test.rb new file mode 100644 index 0000000000..bd5ac0d2c2 --- /dev/null +++ b/service/test/agama/storage/config_solvers/logical_volumes_search_test.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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 "agama/storage/config_conversions/from_json" +require "agama/storage/config_solvers/logical_volumes_search" +require "y2storage" + +describe Agama::Storage::ConfigSolvers::LogicalVolumesSearch do + include Agama::RSpec::StorageHelpers + + subject { described_class.new } + + let(:devicegraph) { Y2Storage::StorageManager.instance.probed } + + before do + mock_storage(devicegraph: scenario) + end + + describe "#solve" do + let(:scenario) { "several_vgs.yaml" } + + let(:config_json) do + { + volumeGroups: [ + { + search: "/dev/system", + logicalVolumes: logical_volumes + } + ] + } + end + + let(:config) do + Agama::Storage::ConfigConversions::FromJSON + .new(config_json) + .convert + end + + let(:volume_group) { config.volume_groups.first } + + context "if the volume group config is not solved yet" do + let(:logical_volumes) do + [ + { search: {} }, + { search: {} } + ] + end + + it "does not set LVs to the logical volume configs" do + subject.solve(volume_group) + lv1, lv2 = volume_group.logical_volumes + expect(lv1.search.solved?).to eq(true) + expect(lv1.search.device).to be_nil + expect(lv2.search.solved?).to eq(true) + expect(lv2.search.device).to be_nil + end + end + + context "if the volume_group config is solved" do + before do + volume_group.search.solve(vg) + end + + let(:vg) { devicegraph.find_by_name("/dev/system") } + + context "if a logical volume config has a search without condition and without max" do + let(:logical_volumes) do + [ + { search: {} } + ] + end + + it "expands the number of logical volume configs to match all the existing LVs" do + subject.solve(volume_group) + logical_volumes = volume_group.logical_volumes + expect(logical_volumes.size).to eq(2) + + lv1, lv2 = logical_volumes + expect(lv1.search.solved?).to eq(true) + expect(lv1.search.device.name).to eq("/dev/system/root") + expect(lv2.search.solved?).to eq(true) + expect(lv2.search.device.name).to eq("/dev/system/swap") + end + + context "and there are more logical volume searches without name" do + let(:logical_volumes) do + [ + { search: {} }, + { search: {} }, + { search: "*" } + ] + end + + it "does not set a device to the surpluss configs" do + subject.solve(volume_group) + logical_volumes = volume_group.logical_volumes + expect(logical_volumes.size).to eq(4) + + _, _, lv3, lv4 = logical_volumes + expect(lv3.search.solved?).to eq(true) + expect(lv3.search.device).to be_nil + expect(lv4.search.solved?).to eq(true) + expect(lv4.search.device).to be_nil + end + end + end + end + end +end diff --git a/service/test/agama/storage/config_solvers/volume_groups_search_test.rb b/service/test/agama/storage/config_solvers/volume_groups_search_test.rb new file mode 100644 index 0000000000..b7555d76ce --- /dev/null +++ b/service/test/agama/storage/config_solvers/volume_groups_search_test.rb @@ -0,0 +1,353 @@ +# frozen_string_literal: true + +# Copyright (c) [2026] 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 "agama/storage/config_conversions/from_json" +require "agama/storage/config_solvers/volume_groups_search" +require "agama/storage/system" +require "y2storage" + +describe Agama::Storage::ConfigSolvers::VolumeGroupsSearch do + include Agama::RSpec::StorageHelpers + + subject { described_class.new(storage_system) } + + let(:storage_system) { Agama::Storage::System.new } + + before do + mock_storage(devicegraph: scenario) + end + + describe "#solve" do + let(:scenario) { "several_vgs.yaml" } + + let(:config_json) { { volumeGroups: volume_groups } } + + let(:config) do + Agama::Storage::ConfigConversions::FromJSON + .new(config_json) + .convert + end + + context "if a volume group config has a search for any device" do + let(:volume_groups) do + [ + { search: { max: 1 } }, + { search: { max: 1 } } + ] + end + + it "sets the first unassigned device to the volume group config" do + subject.solve(config) + expect(config.volume_groups.size).to eq(2) + + vg1, vg2 = config.volume_groups + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/data") + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device.name).to eq("/dev/extra") + end + end + + context "if a volume group config contains a search without condition and without max" do + let(:volume_groups) do + [ + { search: {} } + ] + end + + it "expands the number of volume group configs to match all the existing VGs" do + subject.solve(config) + expect(config.volume_groups.size).to eq(3) + + vg1, vg2, vg3 = config.volume_groups + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/data") + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device.name).to eq("/dev/extra") + expect(vg3.search.solved?).to eq(true) + expect(vg3.search.device.name).to eq("/dev/system") + end + + context "but ordering by descending device name" do + let(:volume_groups) do + [ + { search: { sort: { name: "desc" } } } + ] + end + + it "expands the number of volume group configs to match all the existing VGs in order" do + subject.solve(config) + expect(config.volume_groups.size).to eq(3) + + vg1, vg2, vg3 = config.volume_groups + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/system") + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device.name).to eq("/dev/extra") + expect(vg3.search.solved?).to eq(true) + expect(vg3.search.device.name).to eq("/dev/data") + end + end + end + + context "if a volume group config contains a search without conditions but with a max" do + let(:volume_groups) do + [ + { search: { max: max } } + ] + end + + context "and the max is equal or smaller than the number of VGs" do + let(:max) { 2 } + + it "expands the number of volume group configs to match the max" do + subject.solve(config) + expect(config.volume_groups.size).to eq(2) + + vg1, vg2 = config.volume_groups + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/data") + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device.name).to eq("/dev/extra") + end + + context "but ordering by descending device name" do + let(:volume_groups) do + [ + { search: { sort: { name: "desc" }, max: max } } + ] + end + + it "expands the number of volume group configs to match the max considering the order" do + subject.solve(config) + expect(config.volume_groups.size).to eq(2) + + vg1, vg2 = config.volume_groups + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/system") + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device.name).to eq("/dev/extra") + end + end + end + + context "and the max is bigger than the number of VGs" do + let(:max) { 20 } + + it "expands the number of volume_group configs to match all the existing VGs" do + subject.solve(config) + expect(config.volume_groups.size).to eq(3) + + vg1, vg2, vg3 = config.volume_groups + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/data") + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device.name).to eq("/dev/extra") + expect(vg3.search.solved?).to eq(true) + expect(vg3.search.device.name).to eq("/dev/system") + end + end + end + + context "if a volume group config has a search with condition" do + let(:volume_groups) do + [ + { search: search } + ] + end + + context "and the device was already assigned" do + let(:volume_groups) do + [ + { search: { max: 1 } }, + { search: "/dev/data" } + ] + end + + it "does not set a device to the vg RAID config" do + subject.solve(config) + expect(config.volume_groups.size).to eq(2) + + _, vg2 = config.volume_groups + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device).to be_nil + end + end + + context "and there is other volume group config with the same device" do + let(:volume_groups) do + [ + { search: "/dev/data" }, + { search: "/dev/data" } + ] + end + + it "only sets the device to the first volume group config" do + subject.solve(config) + expect(config.volume_groups.size).to eq(2) + + vg1, vg2 = config.volume_groups + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/data") + expect(vg2.search.solved?).to eq(true) + expect(vg2.search.device).to be_nil + end + end + end + + context "if a volume group config has a search with a device name" do + let(:volume_groups) do + [ + { search: search } + ] + end + + context "and the device is found" do + let(:search) { "/dev/extra" } + + it "sets the device to the volume group config" do + subject.solve(config) + expect(config.volume_groups.size).to eq(1) + + vg1 = config.volume_groups.first + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq("/dev/extra") + end + end + + context "and the device is not found" do + let(:search) { "/dev/missing" } + + # Speed-up fallback search (and make sure it fails) + before { allow(Y2Storage::BlkDevice).to receive(:find_by_any_name) } + + it "does not set a device to the vg RAID config" do + subject.solve(config) + expect(config.volume_groups.size).to eq(1) + + vg1 = config.volume_groups.first + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device).to be_nil + end + end + end + + context "if a volume group config has a search with a size" do + let(:volume_groups) do + [ + { + search: { + condition: { size: size } + } + } + ] + end + + shared_examples "find device" do |device| + it "sets the device to the volume_group config" do + subject.solve(config) + expect(config.volume_groups.size).to eq(1) + + vg1 = config.volume_groups.first + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device.name).to eq(device) + end + end + + shared_examples "do not find device" do + it "does not set a device to the volume group config" do + subject.solve(config) + expect(config.volume_groups.size).to eq(1) + + vg1 = config.volume_groups.first + expect(vg1.search.solved?).to eq(true) + expect(vg1.search.device).to be_nil + end + end + + context "and the operator is :equal" do + let(:size) { { equal: value } } + + context "and there is a VG with equal size" do + let(:value) { storage_system.devicegraph.find_by_name("/dev/extra").size } + include_examples "find device", "/dev/extra" + end + + context "and there is no VG with equal size" do + let(:size) { "20 GiB" } + include_examples "do not find device" + end + end + + context "and the operator is :greater" do + let(:size) { { greater: value } } + + context "and there is a VG with greater size" do + let(:value) { "40 GiB" } + include_examples "find device", "/dev/data" + end + + context "and there is no VG with greater size" do + let(:value) { "200 GiB" } + include_examples "do not find device" + end + end + + context "and the operator is :less" do + let(:size) { { less: value } } + + context "and there is a vg RAID with less size" do + let(:value) { "6 GiB" } + include_examples "find device", "/dev/extra" + end + + context "and there is no vg RAID with less size" do + let(:value) { "4 GiB" } + include_examples "do not find device" + end + end + end + + context "if a volume group config has logical volumes with search" do + let(:volume_groups) do + [ + { + search: "/dev/system", + logicalVolumes: [{ search: {} }] + } + ] + end + + it "solves the search of the logical volumes" do + subject.solve(config) + logical_volumes = config.volume_groups.first.logical_volumes + expect(logical_volumes.size).to eq(2) + + lv1, lv2 = logical_volumes + expect(lv1.search.solved?).to eq(true) + expect(lv1.search.device.name).to eq("/dev/system/root") + expect(lv2.search.solved?).to eq(true) + expect(lv2.search.device.name).to eq("/dev/system/swap") + end + end + end +end diff --git a/service/test/fixtures/lvm_with_nested_thin_lvs.xml b/service/test/fixtures/lvm_with_nested_thin_lvs.xml new file mode 100644 index 0000000000..b4e8874a44 --- /dev/null +++ b/service/test/fixtures/lvm_with_nested_thin_lvs.xml @@ -0,0 +1,803 @@ + + + + + + 42 + /dev/sdd + sdd + /devices/pci0000:00/0000:00:02.0/virtio0/host0/target0:0:3/0:0:3:0/block/sdd + + 125829120 + 512 + + pci-0000:00:02.0-scsi-0:0:3:0 + scsi-0QEMU_QEMU_HARDDISK_393233db30f7899195cf + scsi-3393233db30f78991 + scsi-SQEMU_QEMU_HARDDISK_393233db30f7899195cf71da771418d0 + wwn-0x393233db30f78991 + 256 + true + + + 43 + /dev/sdb + sdb + /devices/pci0000:00/0000:00:02.0/virtio0/host0/target0:0:1/0:0:1:0/block/sdb + + 125829120 + 512 + + pci-0000:00:02.0-scsi-0:0:1:0 + scsi-0QEMU_QEMU_HARDDISK_6961bb051436935764ee + scsi-36961bb0514369357 + scsi-SQEMU_QEMU_HARDDISK_6961bb051436935764ee2103efaa17b5 + wwn-0x6961bb0514369357 + 256 + true + + + 44 + /dev/sdc + sdc + /devices/pci0000:00/0000:00:02.0/virtio0/host0/target0:0:2/0:0:2:0/block/sdc + + 125829120 + 512 + + pci-0000:00:02.0-scsi-0:0:2:0 + scsi-0QEMU_QEMU_HARDDISK_a378925617ff920fdd29 + scsi-3a378925617ff920f + scsi-SQEMU_QEMU_HARDDISK_a378925617ff920fdd2963d87b22033a + wwn-0xa378925617ff920f + 256 + true + + + 45 + /dev/sda + sda + /devices/pci0000:00/0000:00:02.0/virtio0/host0/target0:0:0/0:0:0:0/block/sda + + 125829120 + 512 + + pci-0000:00:02.0-scsi-0:0:0:0 + scsi-0QEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9 + scsi-31c3e3f19205d5c0f + scsi-SQEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9d811889ca12d + wwn-0x1c3e3f19205d5c0f + 256 + true + + + 46 + vg_a + HuCUYd-K2nP-RMBS-S5d0-DUuG-Jq6o-E7AwFP + + 15359 + 4194304 + + 13 + + + 47 + vg_b + B7CTzP-fF1L-Uzl7-zHa6-U3Wv-mFM0-I2XVcd + + 30718 + 4194304 + + 3332 + + + 48 + 3Qfhft-rHtR-dHHA-eBee-wYAy-D9YT-BvG9L3 + 1048576 + + + 49 + hC03MB-V706-Dk7D-ageX-PuB2-hj44-8cHpHg + 1048576 + + + 50 + dXhRSQ-bTvM-H7WF-wFxJ-6KxD-m1fS-dJFxfs + 1048576 + + + 51 + /dev/vg_a/lv_01 + dm-28 + /devices/virtual/block/dm-28 + + 512 + 4194304 + + vg_a-lv_01 + lv_01 + normal + zJS2uS-vMn6-yYx0-7Dk7-tU9h-ZCMK-4PvhZx + 512 + 1 + + + 52 + /dev/vg_a/lv_02 + dm-23 + /devices/virtual/block/dm-23 + + 768 + 4194304 + + vg_a-lv_02 + lv_02 + normal + 64zXyb-0t0z-u15O-9U3x-fbY4-wJCA-dcyN5w + 768 + 1 + + + 53 + /dev/vg_a/lv_02_snap + dm-25 + /devices/virtual/block/dm-25 + + 768 + 4194304 + + vg_a-lv_02_snap + lv_02_snap + snapshot + rBU4Ag-5e4T-GXgf-Fmio-WzwD-srRR-csJccI + 388 + 1 + + + 54 + /dev/vg_a/lvt_01 + false + + 2560 + 4194304 + + vg_a-lvt_01 + lvt_01 + thin-pool + Ep6lA0-EeOf-EHNx-P5lf-KB6V-yvZs-8yzAVf + 2560 + 1 + 65536 + + + 55 + /dev/vg_a/lvt_02 + false + + 5120 + 4194304 + + vg_a-lvt_02 + lvt_02 + thin-pool + mXcVfR-pgWz-TSCd-4TJC-y6cd-YSk6-cndxo0 + 5120 + 1 + 65536 + + + 56 + /dev/vg_b/tpool + false + + 2560 + 4194304 + + vg_b-tpool + tpool + thin-pool + lFT5qw-3uZ8-LBGC-3UeW-qn5p-FDB9-lMoUnl + 2560 + 2 + 65536 + + + 57 + /dev/vg_a/tv_01 + dm-21 + /devices/virtual/block/dm-21 + + 7680 + 4194304 + + vg_a-tv_01 + tv_01 + thin + SR0zKd-IlMI-W3AH-mFOq-4vpE-pXHi-tr1ZnC + + + 58 + /dev/vg_b/tv_01 + dm-26 + /devices/virtual/block/dm-26 + + 25600 + 4194304 + + vg_b-tv_01 + tv_01 + thin + 2eFcGJ-SuXN-Xe6b-JoNe-fQ3B-dqEv-TLpruh + + + 59 + /dev/vg_a/tv_02 + dm-13 + /devices/virtual/block/dm-13 + + 10240 + 4194304 + + vg_a-tv_02 + tv_02 + thin + bTtQ2s-Hs7L-exC7-2uZ7-tsIy-sCah-l8YWqq + + + 60 + /dev/vg_b/tv_02 + dm-27 + /devices/virtual/block/dm-27 + + 25600 + 4194304 + + vg_b-tv_02 + tv_02 + thin + XQ0kqe-dQrh-K48e-WnwA-bUVm-4872-1K4Kqa + + + 61 + /dev/vg_b/tv_02_snap + false + + 25600 + 4194304 + + vg_b-tv_02_snap + tv_02_snap + thin + ZeSEb8-RdK4-zrgH-Rq6a-jTFF-VuGp-AVLkJK + + + 62 + /dev/vg_b/tv_02_snap2 + false + + 25600 + 4194304 + + vg_b-tv_02_snap2 + tv_02_snap2 + thin + T1GbZb-dQWC-IfNj-2FNu-Ct4v-zeZD-cXyAsr + + + 63 + true + + + 64 + /dev/sda1 + sda1 + /devices/pci0000:00/0000:00:02.0/virtio0/host0/target0:0:0/0:0:0:0/block/sda/sda1 + + 2048 + 16384 + 512 + + pci-0000:00:02.0-scsi-0:0:0:0-part1 + scsi-0QEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9-part1 + scsi-31c3e3f19205d5c0f-part1 + scsi-SQEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9d811889ca12d-part1 + wwn-0x1c3e3f19205d5c0f-part1 + primary + 257 + 368487ba-8530-4e18-8f4a-ad3763353d79 + + + 65 + /dev/sda2 + sda2 + /devices/pci0000:00/0000:00:02.0/virtio0/host0/target0:0:0/0:0:0:0/block/sda/sda2 + + 18432 + 121614336 + 512 + + pci-0000:00:02.0-scsi-0:0:0:0-part2 + scsi-0QEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9-part2 + scsi-31c3e3f19205d5c0f-part2 + scsi-SQEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9d811889ca12d-part2 + wwn-0x1c3e3f19205d5c0f-part2 + primary + 131 + true + c0937424-7805-4cc7-99d7-7c5becbd0a11 + + + 66 + /dev/sda3 + sda3 + /devices/pci0000:00/0000:00:02.0/virtio0/host0/target0:0:0/0:0:0:0/block/sda/sda3 + + 121632768 + 4196319 + 512 + + pci-0000:00:02.0-scsi-0:0:0:0-part3 + scsi-0QEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9-part3 + scsi-31c3e3f19205d5c0f-part3 + scsi-SQEMU_QEMU_HARDDISK_1c3e3f19205d5c0fa6b9d811889ca12d-part3 + wwn-0x1c3e3f19205d5c0f-part3 + primary + 130 + d175601f-7cae-48a2-94e6-e124d4cc7a61 + + + 67 + 27cf240e-d534-455b-820a-bfbf286c7aca + + + 68 + swap + true + uuid + swap + true + true + 0 + 0 + + + 69 + e413b0f4-6a32-4c9b-99d4-b4b2d5ebc86b + DUP + SINGLE + + + 70 + 5 + + + + 71 + 256 + @ + true + + + 72 + 258 + @/var + true + + + 73 + 259 + @/usr/local + + + 74 + 260 + @/tmp + + + 75 + 261 + @/srv + + + 76 + 262 + @/root + + + 77 + 263 + @/opt + + + 78 + 264 + @/home + + + 79 + 265 + @/boot/grub2/x86_64-efi + + + 80 + 266 + @/boot/grub2/i386-pc + + + 81 + / + true + uuid + btrfs + true + true + 0 + 0 + + + 82 + /var + true + uuid + subvol=/@/var + btrfs + true + true + 0 + 0 + + + 83 + /usr/local + true + uuid + subvol=/@/usr/local + btrfs + true + true + 0 + 0 + + + 84 + /tmp + true + uuid + subvol=/@/tmp + btrfs + true + true + 0 + 0 + + + 85 + /srv + true + uuid + subvol=/@/srv + btrfs + true + true + 0 + 0 + + + 86 + /root + true + uuid + subvol=/@/root + btrfs + true + true + 0 + 0 + + + 87 + /opt + true + uuid + subvol=/@/opt + btrfs + true + true + 0 + 0 + + + 88 + /home + true + uuid + subvol=/@/home + btrfs + true + true + 0 + 0 + + + 89 + /boot/grub2/x86_64-efi + true + uuid + subvol=/@/boot/grub2/x86_64-efi + btrfs + true + true + 0 + 0 + + + 90 + /boot/grub2/i386-pc + true + uuid + subvol=/@/boot/grub2/i386-pc + btrfs + true + true + 0 + 0 + + + 91 + + d1551576-0a83-4eee-8e44-506e11306829 + DUP + SINGLE + + + 92 + 5 + + true + + + 93 + 256 + subz1 + + + 94 + 257 + subz1/subza + + + 95 + 258 + subz2 + + + 96 + 259 + subz1/subza/subzX + + + + + 48 + 46 + + + 49 + 47 + + + 50 + 47 + + + 46 + 51 + + + 46 + 52 + + + 46 + 53 + + + 46 + 54 + + + 46 + 55 + + + 47 + 56 + + + 54 + 57 + + + 56 + 58 + + + 55 + 59 + + + 56 + 60 + + + 56 + 61 + + + 56 + 62 + + + 52 + 53 + + + 60 + 61 + + + 61 + 62 + + + 43 + 48 + + + 44 + 49 + + + 42 + 50 + + + 45 + 63 + + + 63 + 64 + + + 63 + 65 + + + 63 + 66 + + + 66 + 67 + + + 67 + 68 + + + 69 + 70 + + + 65 + 69 + 1 + + + 70 + 71 + + + 71 + 72 + + + 71 + 73 + + + 71 + 74 + + + 71 + 75 + + + 71 + 76 + + + 71 + 77 + + + 71 + 78 + + + 71 + 79 + + + 71 + 80 + + + 69 + 81 + + + 72 + 82 + + + 73 + 83 + + + 74 + 84 + + + 75 + 85 + + + 76 + 86 + + + 77 + 87 + + + 78 + 88 + + + 79 + 89 + + + 80 + 90 + + + 91 + 92 + + + 52 + 91 + 1 + + + 92 + 93 + + + 93 + 94 + + + 92 + 95 + + + 94 + 96 + + + diff --git a/service/test/fixtures/several_vgs.yaml b/service/test/fixtures/several_vgs.yaml new file mode 100644 index 0000000000..82f10f9cc3 --- /dev/null +++ b/service/test/fixtures/several_vgs.yaml @@ -0,0 +1,72 @@ +- disk: + name: "/dev/sda" + size: 60 GiB + partition_table: gpt + partitions: + - partition: + size: 8 MiB + name: "/dev/sda1" + id: bios_boot + - partition: + size: 27649 MiB (27.00 GiB) + name: "/dev/sda2" + id: lvm + - partition: + size: 34592751.5 KiB (32.99 GiB) + name: "/dev/sda3" + id: lvm +- disk: + name: "/dev/sdb" + size: 10 GiB + partition_table: gpt + partitions: + - partition: + size: 10484719.5 KiB (10.00 GiB) + name: "/dev/sdb1" + id: lvm +- disk: + name: "/dev/sdc" + size: 80 GiB + partition_table: gpt + partitions: + - partition: + size: 5 GiB + name: "/dev/sdc1" + id: lvm +- lvm_vg: + vg_name: system + extent_size: 4 MiB + lvm_lvs: + - lvm_lv: + lv_name: root + size: 25 GiB + stripes: 1 + file_system: btrfs + - lvm_lv: + lv_name: swap + size: 2 GiB + stripes: 1 + file_system: swap + lvm_pvs: + - lvm_pv: + blk_device: "/dev/sda2" +- lvm_vg: + vg_name: data + extent_size: 4 MiB + lvm_lvs: + - lvm_lv: + lv_name: home + size: 30 GiB + stripes: 1 + file_system: xfs + lvm_pvs: + - lvm_pv: + blk_device: "/dev/sda3" + - lvm_pv: + blk_device: "/dev/sdb1" +- lvm_vg: + vg_name: extra + extent_size: 4 MiB + lvm_pvs: + - lvm_pv: + blk_device: "/dev/sdc1" diff --git a/service/test/y2storage/agama_proposal_lvm_test.rb b/service/test/y2storage/agama_proposal_lvm_test.rb index 6fe158d56e..ae5ec88034 100644 --- a/service/test/y2storage/agama_proposal_lvm_test.rb +++ b/service/test/y2storage/agama_proposal_lvm_test.rb @@ -491,5 +491,298 @@ expect(pv_vg1.blk_device.type.is?(:luks1)).to eq true end end + + context "when creating new volumes in an existing volume group" do + let(:scenario) { "several_vgs.yaml" } + + let(:config_json) do + { + boot: { configure: false }, + volumeGroups: [ + { + search: "/dev/data", + logicalVolumes: previous_lvs + new_lvs + } + ] + } + end + + let(:previous_lvs) { [] } + let(:new_lvs) do + [ + { + name: "root", + size: "5 GiB", + filesystem: { + path: "/", + type: "btrfs" + } + }, + { + name: "home", + size: { min: home_min }, + filesystem: { + path: "/home", + type: "xfs" + } + } + ] + end + + context "if the LVs fit into the available space at the VG" do + let(:home_min) { "5 GiB" } + + it "adds the new volumes to the volume group keeping the previous ones" do + vg_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/data").sid + proposal.propose + + vg = proposal.devices.find_by_name("/dev/data") + expect(vg.sid).to eq vg_sid + # The previous one plus the two new ones + expect(vg.lvm_lvs.size).to eq 3 + paths = vg.lvm_lvs.map(&:filesystem).map(&:mount_path) + expect(paths).to contain_exactly(nil, "/", "/home") + end + end + + context "if the LVs fit into the total size of the VG but not in the available space" do + let(:home_min) { "15 GiB" } + + context "and nothing is configured to make space in the VG" do + it "raises an error" do + expect { proposal.propose }.to raise_error(Y2Storage::NoDiskSpaceError) + end + end + + context "and all previous LVs should be deleted " do + let(:previous_lvs) do + [ + { search: "*", delete: true } + ] + end + + it "adds the new volumes to the volume group deleting the previous ones" do + vg_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/data").sid + proposal.propose + + vg = proposal.devices.find_by_name("/dev/data") + expect(vg.sid).to eq vg_sid + expect(vg.lvm_lvs.size).to eq 2 + paths = vg.lvm_lvs.map(&:filesystem).map(&:mount_path) + expect(paths).to contain_exactly("/", "/home") + end + end + end + + context "if the LVs do not fit into the total size of the VG" do + let(:home_min) { "150 GiB" } + + context "and nothing is configured to make space in the VG" do + it "raises an error" do + expect { proposal.propose }.to raise_error(Y2Storage::NoDiskSpaceError) + end + end + + context "and all previous LVs should be deleted " do + let(:previous_lvs) do + [ + { search: "*", delete: true } + ] + end + + it "raises an error" do + expect { proposal.propose }.to raise_error(Y2Storage::NoDiskSpaceError) + end + end + end + end + + context "when using existing logical volumes" do + let(:scenario) { "several_vgs.yaml" } + + let(:config_json) do + { + boot: { configure: false }, + volumeGroups: [ + { + search: "/dev/data", + logicalVolumes: [ + { + name: "root", + size: { min: "5 GiB" }, + filesystem: { + path: "/", + type: "btrfs" + } + }, + { + search: { max: 1 }, + filesystem: { + path: "/home", + reuseIfPossible: true + }, + size: home_size + } + ] + } + ] + } + end + + let(:home_size) { nil } + + it "uses the volume group and the logical volume" do + initial_vg = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/data") + vg_sid = initial_vg.sid + fs_sid = initial_vg.lvm_lvs.first.filesystem.sid + proposal.propose + + vg = proposal.devices.find_by_name("/dev/data") + expect(vg.sid).to eq vg_sid + expect(vg.lvm_lvs.size).to eq 2 + filesystems = vg.lvm_lvs.map(&:filesystem) + expect(filesystems.map(&:mount_path)).to contain_exactly("/", "/home") + expect(filesystems.map(&:sid)).to include fs_sid + end + + it "correctly distributes the available space" do + proposal.propose + + vg = proposal.devices.find_by_name("/dev/data") + root = vg.lvm_lvs.find { |v| v.filesystem.mount_path == "/" } + home = vg.lvm_lvs.find { |v| v.filesystem.mount_path == "/home" } + expect(vg.available_space).to be_zero + expect(root.size).to be > 12.GiB + expect(home.size).to eq 30.GiB + end + + context "if some existing logical volume is chosen for growing" do + let(:home_size) { { min: "30 GiB", max: "35 GiB" } } + + it "grows the LV and distributes the remaining space" do + proposal.propose + + vg = proposal.devices.find_by_name("/dev/data") + root = vg.lvm_lvs.find { |v| v.filesystem.mount_path == "/" } + home = vg.lvm_lvs.find { |v| v.filesystem.mount_path == "/home" } + expect(vg.available_space).to be_zero + expect(home.size).to eq 35.GiB + expect(root.size).to be > 7.GiB + expect(root.size).to be < 8.GiB + end + end + end + + context "when using an existing thin pool and existing logical thin volume" do + let(:scenario) { "lvm_with_nested_thin_lvs.xml" } + + let(:config_json) do + { + boot: { configure: false }, + volumeGroups: [ + { + search: "/dev/vg_b", + logicalVolumes: [ + { + search: "/dev/vg_b/tv_01", + filesystem: { path: "/" } + }, + { + search: "/dev/vg_b/tpool", + alias: "pool" + }, + { + name: "home", + usedPool: "pool", + size: "5 GiB", + filesystem: { path: "/home" } + } + ] + } + ] + } + end + + it "uses the volume group and the logical volumes" do + vg_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vg_b").sid + lv_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vg_b/tv_01").sid + pool = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vg_b/tpool") + pool_sid = pool.sid + pool_size = pool.lvm_lvs.size + proposal.propose + + vg = proposal.devices.find_by_name("/dev/vg_b") + expect(vg.sid).to eq vg_sid + expect(vg.thin_pool_lvm_lvs.size).to eq 1 + pool = vg.thin_pool_lvm_lvs.first + expect(pool.sid).to eq pool_sid + thin_vols = pool.lvm_lvs + expect(thin_vols.size).to eq(pool_size + 1) + expect(thin_vols.map(&:sid)).to include lv_sid + filesystems = thin_vols.map(&:filesystem).compact + expect(filesystems.map(&:mount_path)).to contain_exactly("/", "/home") + end + end + + context "when adding more PVs to an existing volume group" do + let(:scenario) { "several_vgs.yaml" } + + let(:config_json) do + { + boot: { configure: false }, + drives: [ + { + search: "/dev/sdc", + partitions: [ + { + size: "40 GiB", + alias: "pv1" + } + ] + } + ], + volumeGroups: [ + { + search: "/dev/data", + logicalVolumes: [ + { + name: "root", + size: { min: "5 GiB" }, + filesystem: { + path: "/", + type: "btrfs" + } + } + ], + physicalVolumes: ["pv1"] + } + ] + } + end + + it "uses the volume group" do + vg_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/data").sid + proposal.propose + + vg = proposal.devices.find_by_name("/dev/data") + expect(vg.sid).to eq vg_sid + expect(vg.lvm_lvs.map(&:filesystem).map(&:mount_path)).to include "/" + end + + it "grows the volume group" do + proposal.propose + + vg = proposal.devices.find_by_name("/dev/data") + expect(vg.lvm_pvs.size).to eq 3 + expect(vg.size).to be > Y2Storage::DiskSize.GiB(80) + end + + it "uses the new volume group space to allocate the logical volumes" do + proposal.propose + + lv = proposal.devices.find_by_name("/dev/data/root") + expect(lv.size).to be > Y2Storage::DiskSize.GiB(50) + end + end end end