diff --git a/package/yast2-storage-ng.changes b/package/yast2-storage-ng.changes index 82e62c204..ddc0bb82f 100644 --- a/package/yast2-storage-ng.changes +++ b/package/yast2-storage-ng.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Fri Mar 6 10:50:26 UTC 2026 - Ancor Gonzalez Sosa + +- Added the needed infrastructure to specify what to do with the + existing volumes in a reused LVM volume group (related to + jsc#PED-15104, bsc#1254718 and gh#agama-project/agama#3171). +- Fixed creation of thin pools within pre-existing thin pools. +- 5.0.41 + ------------------------------------------------------------------- Thu Jan 29 09:00:40 UTC 2026 - Ancor Gonzalez Sosa diff --git a/package/yast2-storage-ng.spec b/package/yast2-storage-ng.spec index 0cd1caa4b..47c0e2c50 100644 --- a/package/yast2-storage-ng.spec +++ b/package/yast2-storage-ng.spec @@ -16,7 +16,7 @@ # Name: yast2-storage-ng -Version: 5.0.40 +Version: 5.0.41 Release: 0 Summary: YaST2 - Storage Configuration License: GPL-2.0-only OR GPL-3.0-only diff --git a/src/lib/y2storage/planned/lvm_vg.rb b/src/lib/y2storage/planned/lvm_vg.rb index 9c87da484..55ab18b72 100644 --- a/src/lib/y2storage/planned/lvm_vg.rb +++ b/src/lib/y2storage/planned/lvm_vg.rb @@ -135,6 +135,20 @@ def initialize_from_real_vg(real_vg) self.reuse_name = real_vg.vg_name end + # Redefines the corresponding method from the base class + # + # @see Device#assign_reuse + # + # For some reason (maybe just a historical mistake), the usage of #reuse_name is inconsistent + # in this class compared to the rest. Instead of using the device name, it uses the volume + # group name. + # + # @param device [Y2Storage::LvmVg] + def assign_reuse(device) + super(device) + @reuse_name = device.vg_name + end + # Min size that a partition (or any other block device) must have to be useful as PV # # @return [DiskSize] diff --git a/src/lib/y2storage/proposal/lvm_creator.rb b/src/lib/y2storage/proposal/lvm_creator.rb index 8cb48fbb8..3d1294b42 100644 --- a/src/lib/y2storage/proposal/lvm_creator.rb +++ b/src/lib/y2storage/proposal/lvm_creator.rb @@ -20,6 +20,7 @@ require "y2storage/planned" require "y2storage/disk_size" require "y2storage/proposal/creator_result" +require "y2storage/proposal/lvm_space_maker" module Y2Storage module Proposal @@ -39,8 +40,12 @@ class LvmCreator # Constructor # # @param original_devicegraph [Devicegraph] Initial devicegraph - def initialize(original_devicegraph) + # @param space_settings [ProposalSpaceSettings, nil] Optional settings to customize what to do + # with every existing logical volume. If omitted, the traditional YaST approach is used + # (ie. the strategy "auto" is used, see {LvmSpaceStrategies::Auto}). + def initialize(original_devicegraph, space_settings = nil) @original_devicegraph = original_devicegraph + @space_settings = space_settings end # Returns a copy of the original devicegraph in which the volume @@ -129,61 +134,13 @@ def assign_physical_volumes(volume_group, part_names, devicegraph) # Makes space for planned logical volumes # - # When making free space, three different policies can be followed: - # - # * :needed: remove logical volumes until there's enough space for - # planned ones. - # * :remove: remove all logical volumes. - # * :keep: keep all logical volumes. - # # This method modifies the volume group received as first argument. # # @param volume_group [LvmVg] volume group to clean-up # @param planned_vg [Planned::LvmVg] planned logical volume def make_space(volume_group, planned_vg) - return if planned_vg.make_space_policy == :keep - - case planned_vg.make_space_policy - when :needed - make_space_until_fit(volume_group, planned_vg.lvs) - when :remove - lvs_to_keep = planned_vg.all_lvs.select(&:reuse?).map(&:reuse_name) - remove_logical_volumes(volume_group, lvs_to_keep) - end - end - - # Makes sure the given volume group has enough free extends to allocate - # all the planned volumes, by deleting the existing logical volumes. - # - # This method modifies the volume group received as first argument. - # - # FIXME: the current implementation does not guarantee than the freed - # space is the minimum valid one. - # - # @param volume_group [LvmVg] volume group to modify - def make_space_until_fit(volume_group, planned_lvs) - space_size = DiskSize.sum(planned_lvs.map(&:min_size)) - missing = missing_vg_space(volume_group, space_size) - while missing > DiskSize.zero - lv_to_delete = delete_candidate(volume_group, missing) - if lv_to_delete.nil? - error_msg = "The volume group #{volume_group.vg_name} is not big enough" - raise NoDiskSpaceError, error_msg - end - volume_group.delete_lvm_lv(lv_to_delete) - missing = missing_vg_space(volume_group, space_size) - end - end - - # Remove all logical volumes from a volume group - # - # This method modifies the volume group received as a first argument. - # - # @param volume_group [LvmVg] volume group to remove logical volumes from - # @param lvs_to_keep [Array] name of logical volumes to keep - def remove_logical_volumes(volume_group, lvs_to_keep) - lvs_to_remove = volume_group.all_lvm_lvs.reject { |v| lvs_to_keep.include?(v.name) } - lvs_to_remove.each { |v| volume_group.delete_lvm_lv(v) } + space_maker = LvmSpaceMaker.new(volume_group, planned_vg, @space_settings) + space_maker.provide_space end # Creates a logical volume for each planned volume. @@ -196,14 +153,17 @@ def remove_logical_volumes(volume_group, lvs_to_keep) # @return [Hash{String => Planned::LvmLv}] planned LVs indexed by the # device name of the real LV devices that were created def create_logical_volumes(volume_group, planned_lvs) - adjusted_lvs = planned_lvs_in_vg(planned_lvs, volume_group) + adjusted_lvs = planned_lvs_in_vg(planned_lvs, volume_group).reject(&:reuse?) vg_size = volume_group.available_space lvs = Planned::LvmLv.distribute_space(adjusted_lvs, vg_size, rounding: volume_group.extent_size) - all_lvs = lvs + lvs.map(&:thin_lvs).flatten + all_lvs = lvs + lvs.map(&:thin_lvs).flatten + thin_lvs_from_reused_pools(planned_lvs) all_lvs.reject(&:reuse?).each_with_object({}) do |planned_lv, devices_map| new_lv = create_logical_volume(volume_group, planned_lv) devices_map[new_lv.name] = planned_lv end + rescue RuntimeError => e + log.info "The logical volumes do not fit into the volume group: #{e}" + raise NoDiskSpaceError end # Creates a logical volume in a volume group @@ -229,32 +189,6 @@ def create_logical_volume(volume_group, planned_lv) new_lv end - # Best logical volume to delete next while trying to make space for the - # planned volumes. It returns the smallest logical volume that would - # fulfill the goal. If no LV is big enough, it returns the biggest one. - def delete_candidate(volume_group, target_space) - lvs = volume_group.lvm_lvs - big_lvs = lvs.select { |lv| lv.size >= target_space } - if big_lvs.empty? - lvs.max_by(&:size) - else - big_lvs.min_by(&:size) - end - end - - # Missing space in the volume group to fullfil a target - # - # @param volume_group [LvmVg] Volume group - # @param target_space [DiskSize] Required space - def missing_vg_space(volume_group, target_space) - available = volume_group.available_space - if available > target_space - DiskSize.zero - else - target_space - available - end - end - # Returns the name that is available taking original_name as a base. If # the name is already taken, the returned name will have a number # appended. @@ -310,6 +244,15 @@ def planned_lvs_in_vg(lvs, vg) end end + # Returns a list of planned logical thin volumes that should be created in thin pools + # that already exist. + # + # @param lvs [Array] List of planned logical volumes + # @return [Array DiskSize.zero + lv_to_delete = delete_candidate(volume_group.lvm_lvs) + if lv_to_delete.nil? + error_msg = "The volume group #{volume_group.vg_name} is not big enough" + raise NoDiskSpaceError, error_msg + end + volume_group.delete_lvm_lv(lv_to_delete) + end + end + + # Remove all logical volumes from a volume group + # + # This method modifies the volume group received as a first argument. + # + # @param lvs_to_keep [Array] name of logical volumes to keep + def remove_logical_volumes(lvs_to_keep) + lvs_to_remove = volume_group.all_lvm_lvs.reject { |v| lvs_to_keep.include?(v.name) } + lvs_to_remove.each { |v| volume_group.delete_lvm_lv(v) } + end + end + end + end +end diff --git a/src/lib/y2storage/proposal/lvm_space_strategies/base.rb b/src/lib/y2storage/proposal/lvm_space_strategies/base.rb new file mode 100644 index 000000000..8a89919f6 --- /dev/null +++ b/src/lib/y2storage/proposal/lvm_space_strategies/base.rb @@ -0,0 +1,105 @@ +# Copyright (c) [2017-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 "y2storage/disk_size" + +module Y2Storage + module Proposal + module LvmSpaceStrategies + # Base class for the LVM space strategies + class Base + include Yast::Logger + + # Constructor + # + # @param volume_group [LvmVg] volume group to clean-up + # @param planned_vg [Planned::LvmVg] planned logical volume + # @param space_settings [ProposalSpaceSettings, nil] Optional settings. See + # {LvmCreator#initialize}. + def initialize(volume_group, planned_vg, space_settings) + @volume_group = volume_group + @planned_vg = planned_vg + @space_settings = space_settings + end + + # Makes space for planned logical volumes + # + # This method modifies the volume group assigned to the strategy object. + # + def provide_space + raise NotImplementedError + end + + private + + # @return [LvmVg] volume group to clean-up + attr_reader :volume_group + + # @return [Planned::LvmVg] planned logical volume + attr_reader :planned_vg + + # @return [ProposalSpaceSettings] settings to make space + attr_reader :space_settings + + # Space that needs to be available in order to be able to create the volumes + # + # @return [DiskSize] + def target_space + @target_space ||= DiskSize.sum( + relevant_planned_lvs.map(&:min_size), + rounding: volume_group.extent_size + ) + end + + # Planned logical volumes to take into account when calculating the target space + # + # @see #target_space + # + # @return [Array] + def relevant_planned_lvs + planned_vg.lvs.reject(&:reuse?).reject { |v| v.lv_type.is?(:thin) } + end + + # Missing space in the volume group to fullfil the target + # + # @return [DiskSize] + def missing_vg_space + available = volume_group.available_space + if available > target_space + DiskSize.zero + else + target_space - available + end + end + + # Best logical volume to delete next while trying to make space for the + # planned volumes. It returns the smallest logical volume that would + # fulfill the goal. If no LV is big enough, it returns the biggest one. + def delete_candidate(lvs) + big_lvs = lvs.select { |lv| lv.size >= missing_vg_space } + if big_lvs.empty? + lvs.max_by(&:size) + else + big_lvs.min_by(&:size) + end + end + end + end + end +end diff --git a/src/lib/y2storage/proposal/lvm_space_strategies/bigger_resize.rb b/src/lib/y2storage/proposal/lvm_space_strategies/bigger_resize.rb new file mode 100644 index 000000000..e0baccbe1 --- /dev/null +++ b/src/lib/y2storage/proposal/lvm_space_strategies/bigger_resize.rb @@ -0,0 +1,195 @@ +# 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 "y2storage/proposal/lvm_space_strategies/base" + +module Y2Storage + module Proposal + module LvmSpaceStrategies + # Strategy used by the Agama proposal to make space in a existing LVM volume group + # + # This used the same rationale and general rules than the corresponding SpaceMaker strategy. + class BiggerResize < Base + # Makes space for planned logical volumes + # + # @see Base#provide_space + def provide_space + delete_mandatory + shrink_mandatory + return if done? + + shrink_optional + return if done? + + delete_optional + return if done? + + error_msg = "The volume group #{volume_group.vg_name} is not big enough" + raise NoDiskSpaceError, error_msg + end + + private + + # Whether the goal has been achieved + # + # @return [Boolean] + def done? + missing_vg_space <= DiskSize.zero + end + + # Executes the mandatory delete actions + def delete_mandatory + each_action(:delete) do |action, lv| + next unless action.mandatory + + volume_group.delete_lvm_lv(lv) + end + end + + # Executes the mandatory shrink actions + def shrink_mandatory + each_action(:resize) do |action, lv| + next unless action.max_size + next if lv.size <= action.max_size + + lv.resize(action.max_size) + end + end + + # Shrinks logical volumes as needed to make enough space for the new volumes + def shrink_optional + candidates = calculate_optional_shrinks + until done? || candidates.empty? + shrink = candidates.pop + recover = [missing_vg_space, shrink[:recoverable]].min + shrink[:lv].resize(shrink[:lv].size - recover) + end + end + + # Deletes logical volumes as needed to make enough space for the new volumes + def delete_optional + candidates = calculate_optional_delete_lvs + until done? || candidates.empty? + lv = delete_candidate(candidates) + candidates.delete(lv) + volume_group.delete_lvm_lv(lv) + end + end + + # @see #shrink_optional + def calculate_optional_shrinks + shrinks = [] + each_action(:resize) do |action, lv| + next if min_for(action) && min_for(action) > lv.size + next if max_for(action) && max_for(action) < lv.size + # Resizing a thin volume would not get us any closer to our goal + next if lv.lv_type.is?(:thin) + + shrinks << { + lv: lv, + recoverable: recoverable_size(lv, action) + } + end + + shrinks.sort { |a, b| preferred_shrink(a, b) } + end + + # Compares two shrinking operations to decide which one should be executed first + # + # @return [Integer] -1, 0, or 1 just like the <=> ruby operator + def preferred_shrink(shrink1, shrink2) + result = shrink1[:recoverable] <=> shrink2[:recoverable] + return result unless result.zero? + + # Just to ensure stable sorting between different executions in case of draw + shrink1[:lv].name <=> shrink2[:lv].name + end + + # Max space that can be recovered from the given volume, having into account the + # restrictions imposed by its Resize action + # + # @see #shrink_optional + # + # @return [DiskSize] + def recoverable_size(lv, resize) + min = min_for(resize) + recoverable = lv.recoverable_size.floor(volume_group.extent_size) + return recoverable if min.nil? || min > lv.size + + [recoverable, lv.size - min].min + end + + # @see #delete_optional + # + # @return [Array] + def calculate_optional_delete_lvs + lvs = [] + each_action(:delete) do |action, lv| + next if action.mandatory + # Deleting a thin volume does not recover any useful space + next if lv.lv_type.is?(:thin) + + lvs << lv + end + lvs + end + + # Iterates over the actions of the given type + # + # @param type [:delete, :resize] + def each_action(type) + space_settings.public_send(:"#{type}_actions").each do |action| + lv = lv_for(action) + next unless lv + + yield(action, lv) + end + end + + # Logical volume associated to a given space action + # + # @param action [SpaceActions::Base] + # @return [LvmLv] + def lv_for(action) + volume_group.all_lvm_lvs.find { |v| v.name == action.device } + end + + # Rounded min size for the resize action, if any + # + # Used to ensure all operations fit the extent size of the volume group + # + # @param action [SpaceActions::Resize] + # @return [DiskSize, nil] + def min_for(action) + action.min_size ? action.min_size.ceil(volume_group.extent_size) : nil + end + + # Rounded max size for the resize action, if any + # + # Used to ensure all operations fit the extent size of the volume group + # + # @param action [SpaceActions::Resize] + # @return [DiskSize, nil] + def max_for(action) + action.max_size ? action.max_size.floor(volume_group.extent_size) : nil + end + end + end + end +end diff --git a/src/lib/y2storage/proposal/space_maker_actions/bigger_resize_strategy.rb b/src/lib/y2storage/proposal/space_maker_actions/bigger_resize_strategy.rb index c0f1da2e5..83a5f00c8 100644 --- a/src/lib/y2storage/proposal/space_maker_actions/bigger_resize_strategy.rb +++ b/src/lib/y2storage/proposal/space_maker_actions/bigger_resize_strategy.rb @@ -251,12 +251,12 @@ def resize_to_shrink(partition, resize) # All delete actions from the settings def delete_actions - settings.actions.select { |a| a.is?(:delete) } + settings.delete_actions end # All resize actions from the settings def resize_actions - settings.actions.select { |a| a.is?(:resize) } + settings.resize_actions end end end diff --git a/src/lib/y2storage/proposal_space_settings.rb b/src/lib/y2storage/proposal_space_settings.rb index 5987b599d..bdaf0a2c0 100644 --- a/src/lib/y2storage/proposal_space_settings.rb +++ b/src/lib/y2storage/proposal_space_settings.rb @@ -128,5 +128,23 @@ def delete_forced(type) end alias_method :delete_forced?, :delete_forced + + # All delete actions + # + # @see #actions + # + # @return [Array] + def delete_actions + actions.select { |a| a.is?(:delete) } + end + + # All resize actions + # + # @see #actions + # + # @return [Array] + def resize_actions + actions.select { |a| a.is?(:resize) } + end end end diff --git a/test/y2storage/autoinst_proposal_test.rb b/test/y2storage/autoinst_proposal_test.rb index 7d53b4273..4a4aef3ff 100755 --- a/test/y2storage/autoinst_proposal_test.rb +++ b/test/y2storage/autoinst_proposal_test.rb @@ -1228,6 +1228,33 @@ def root_filesystem(disk) expect(issue).to_not be_nil end end + + context "creating a new thin volume in the existing pool" do + let(:pool_spec) do + { "lv_name" => "pool0", "size" => "200GiB", "pool" => true, "create" => false } + end + + let(:home_spec) do + { + "mount" => "/home", "filesystem" => "ext4", "lv_name" => "home", "size" => "100GiB", + "used_pool" => "pool0" + } + end + + let(:lvs) { [pool_spec, root_spec, home_spec] } + + it "reuses the thin pool and create the new thin volumes" do + proposal.propose + devicegraph = proposal.devices + thin_vols = devicegraph.lvm_lvs.find { |v| v.lv_name == "pool0" }.lvm_lvs + expect(thin_vols.size).to eq 2 + filesystems = thin_vols.map(&:filesystem) + expect(filesystems.map(&:mount_path)).to contain_exactly("/", "/home") + root_fs = filesystems.find { |f| f.mount_path == "/" } + # keep the same filesystem type + expect(root_fs.type).to eq(Y2Storage::Filesystems::Type::EXT4) + end + end end end diff --git a/test/y2storage/proposal/lvm_creator_bigger_resize_test.rb b/test/y2storage/proposal/lvm_creator_bigger_resize_test.rb new file mode 100755 index 000000000..d9a9315d3 --- /dev/null +++ b/test/y2storage/proposal/lvm_creator_bigger_resize_test.rb @@ -0,0 +1,245 @@ +#!/usr/bin/env rspec + +# 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 "../spec_helper" +require "y2storage" + +describe Y2Storage::Proposal::LvmCreator do + using Y2Storage::Refinements::SizeCasts + + subject(:creator) { described_class.new(fake_devicegraph, space_settings) } + + before { fake_scenario(scenario) } + + let(:scenario) { "lvm-new-pvs" } + let(:space_settings) do + Y2Storage::ProposalSpaceSettings.new.tap do |settings| + settings.strategy = :bigger_resize + settings.actions = settings_actions + end + end + let(:settings_actions) { [] } + let(:delete) { Y2Storage::SpaceActions::Delete } + let(:resize) { Y2Storage::SpaceActions::Resize } + + describe "#create_volumes" do + before { vg.reuse_name = reused_vg.vg_name } + + let(:reused_vg) { fake_devicegraph.lvm_vgs.first } + let(:vg) { planned_vg(lvs: volumes) } + let(:pv_partitions) { [] } + let(:ext4) { Y2Storage::Filesystems::Type::EXT4 } + + context "if there is enough space for the new LVs" do + let(:volumes) do + [ + planned_lv(mount_point: "/1", type: :ext4, logical_volume_name: "one", min: 5.GiB), + planned_lv(mount_point: "/2", type: :ext4, logical_volume_name: "two", min: 5.GiB) + ] + end + + context "and there are no mandatory actions" do + let(:settings_actions) do + [ + delete.new("/dev/vg0/lv1", mandatory: false), + resize.new("/dev/vg0/lv1", min_size: 0.GiB) + ] + end + + it "creates the new LVs" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + vg = devicegraph.lvm_vgs.first + expect(vg.lvm_lvs.map(&:lv_name)).to include "one", "two" + end + + it "does not modify the pre-existing LVs" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + lvs = devicegraph.lvm_vgs.first.lvm_lvs + expect(lvs).to include( + an_object_having_attributes(lv_name: "lv1", size: 10.GiB), + an_object_having_attributes(lv_name: "lv2", size: 8.GiB) + ) + end + end + + context "and there are mandatory actions to delete logical volumes" do + let(:settings_actions) { [delete.new("/dev/vg0/lv1", mandatory: true)] } + + it "deletes the designated pre-existing LVs and creates the new ones" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + lvs = devicegraph.lvm_vgs.first.lvm_lvs + expect(lvs.map(&:lv_name)).to contain_exactly "lv2", "one", "two" + end + end + + context "and there are mandatory actions to resize logical volumes" do + let(:settings_actions) { [resize.new("/dev/vg0/lv1", max_size: 8.GiB)] } + + let(:resize_info) do + instance_double("ResizeInfo", resize_ok?: true, min_size: 1.GiB, max_size: 30.GiB) + end + + before do + allow_any_instance_of(Y2Storage::LvmLv) + .to receive(:detect_resize_info).and_return(resize_info) + end + + it "creates the new LVs" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + vg = devicegraph.lvm_vgs.first + expect(vg.lvm_lvs.map(&:lv_name)).to include "one", "two" + end + + it "resizes the pre-existing LVs according to the action" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + lvs = devicegraph.lvm_vgs.first.lvm_lvs + expect(lvs).to include( + an_object_having_attributes(lv_name: "lv1", size: 8.GiB), + an_object_having_attributes(lv_name: "lv2", size: 8.GiB) + ) + end + end + end + + context "if there is no enough space for the new LVs" do + let(:volumes) do + [ + planned_lv(mount_point: "/1", type: :ext4, logical_volume_name: "one", min: 10.GiB), + planned_lv(mount_point: "/2", type: :ext4, logical_volume_name: "two", min: 5.GiB) + ] + end + + context "and no actions are allowed" do + it "raises a NoDiskSpace exception" do + expect { creator.create_volumes(vg, pv_partitions) } + .to raise_error Y2Storage::NoDiskSpaceError + end + end + + context "and delete and resizing are allowed" do + let(:settings_actions) do + [ + delete.new("/dev/vg0/lv1", mandatory: false), + resize.new("/dev/vg0/lv1", min_size: 0.GiB) + ] + end + + before do + allow_any_instance_of(Y2Storage::LvmLv) + .to receive(:detect_resize_info).and_return(resize_info) + end + + context "and resizing is enough" do + let(:resize_info) do + instance_double("ResizeInfo", resize_ok?: true, min_size: 1.GiB, max_size: 30.GiB) + end + + it "creates the new LVs" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + vg = devicegraph.lvm_vgs.first + expect(vg.lvm_lvs.map(&:lv_name)).to include "one", "two" + end + + it "resizes the pre-existing LVs as needed" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + lvs = devicegraph.lvm_vgs.first.lvm_lvs + expect(lvs).to include( + an_object_having_attributes(lv_name: "lv1", size: 7.GiB - 4.MiB), + an_object_having_attributes(lv_name: "lv2", size: 8.GiB) + ) + end + end + + context "and resizing is not enough" do + let(:resize_info) do + instance_double("ResizeInfo", resize_ok?: true, min_size: 8.GiB, max_size: 30.GiB) + end + + it "deletes the pre-existing LVs as needed to create the new ones" do + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + lvs = devicegraph.lvm_vgs.first.lvm_lvs + expect(lvs.map(&:lv_name)).to contain_exactly "lv2", "one", "two" + end + end + end + end + + context "when creating new thin volumes in an existing pool" do + let(:scenario) { "lvm_with_nested_thin_lvs.xml" } + let(:reused_vg) { fake_devicegraph.find_by_name("/dev/vg_a") } + let(:reused_pool) { fake_devicegraph.find_by_name("/dev/vg_a/lvt_01") } + + let(:volumes) { [planned_lv(logical_volume_name: reused_pool.lv_name)] } + let(:thin_volumes) do + [ + planned_lv( + mount_point: "/1", type: :ext4, logical_volume_name: "one", min: 100.GiB, + lv_type: Y2Storage::LvType::THIN + ), + planned_lv( + mount_point: "/2", type: :ext4, logical_volume_name: "two", min: 100.GiB, + lv_type: Y2Storage::LvType::THIN + ) + ] + end + + let(:settings_actions) do + [ + delete.new("/dev/vg_a/lv_01", mandatory: false), + resize.new("/dev/vg_a/lv_01", min_size: 0.GiB), + delete.new("/dev/vg_a/lv_02", mandatory: false), + resize.new("/dev/vg_a/lv_02", min_size: 0.GiB), + delete.new("/dev/vg_a/tv_01", mandatory: false), + resize.new("/dev/vg_a/tv_01", min_size: 0.GiB) + ] + end + + before do + volumes.first.assign_reuse(reused_pool) + thin_volumes.each { |v| volumes.first.add_thin_lv(v) } + allow_any_instance_of(Y2Storage::LvmLv) + .to receive(:detect_resize_info).and_return(resize_info) + end + + let(:resize_info) do + instance_double("ResizeInfo", resize_ok?: true, min_size: 1.GiB, max_size: 30.GiB) + end + + it "does not delete or resize any other logical volume" do + initial_lvs = reused_vg.lvm_lvs + initial_thin_lvs = reused_pool.lvm_lvs + + devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph + vg = devicegraph.find_by_name("/dev/vg_a") + pool = devicegraph.find_by_name("/dev/vg_a/lvt_01") + + expect(vg.lvm_lvs.size).to eq initial_lvs.size + expect(Y2Storage::DiskSize.sum(vg.lvm_lvs.map(&:size))) + .to eq Y2Storage::DiskSize.sum(initial_lvs.map(&:size)) + + expect(pool.lvm_lvs.size).to eq initial_thin_lvs.size + 2 + # The LvmCreator uses the total size of the thin pool as maximum size for new thin vols + expect(Y2Storage::DiskSize.sum(pool.lvm_lvs.map(&:size))) + .to eq Y2Storage::DiskSize.sum(initial_thin_lvs.map(&:size)) + 20.GiB + end + end + end +end diff --git a/test/y2storage/proposal/lvm_creator_test.rb b/test/y2storage/proposal/lvm_creator_test.rb index 048286538..6f7d60627 100755 --- a/test/y2storage/proposal/lvm_creator_test.rb +++ b/test/y2storage/proposal/lvm_creator_test.rb @@ -52,6 +52,20 @@ let(:vg) { planned_vg(volume_group_name: "system", lvs: volumes) } + context "if a non-valid strategy is configured at the proposal space settings" do + subject(:creator) { described_class.new(fake_devicegraph, space_settings) } + + let(:space_settings) do + Y2Storage::ProposalSpaceSettings.new.tap do |settings| + settings.strategy = :invented + end + end + + it "raises an exception" do + expect { creator.create_volumes(vg, pv_partitions) }.to raise_exception(ArgumentError) + end + end + context "if no volume group is reused" do it "creates a new volume group" do devicegraph = creator.create_volumes(vg, pv_partitions).devicegraph