Skip to content

Commit

Permalink
AgamaProposal: handle resize of partitions (#1599)
Browse files Browse the repository at this point in the history
joseivanlopez#1 documented the approach to
follow for resizing partitions at Agama, based on the partitioning
config (a.k.a. the "profile").

The current pull request implements the basis of the described
management.

It depends on the improvements introduced at yast2-storage-ng by
yast/yast-storage-ng#1388

### Pending

Some aspects of partition growing are not fully handled. That's planned
for a future iteration and documented at
https://trello.com/c/opInsicQ/531-storage-profile-partition-growing
  • Loading branch information
ancorgs authored Sep 20, 2024
2 parents 574a1a5 + ce5d46b commit 484fa9c
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,32 @@ def space_policy_conversion(target)

actions = case settings.space.policy
when :delete
all_devices.each_with_object({}) { |d, a| a[d] = :force_delete }
all_devices.map { |d| Y2Storage::SpaceActions::Delete.new(d, mandatory: true) }
when :resize
all_devices.each_with_object({}) { |d, a| a[d] = :resize }
all_devices.map { |d| Y2Storage::SpaceActions::Resize.new(d) }
when :keep
{}
when :custom
settings.space.actions
custom_space_actions
end

target.space_settings.actions = remove_unsupported_actions(actions)
end

# @see #space_policy_conversion
def custom_space_actions
settings.space.actions.map do |device, action|
case action
when :force_delete
Y2Storage::SpaceActions::Delete.new(device, mandatory: true)
when :delete
Y2Storage::SpaceActions::Delete.new(device)
when :resize
Y2Storage::SpaceActions::Resize.new(device)
end
end
end

# @param target [Y2Storage::ProposalSettings]
def volumes_conversion(target)
target.swap_reuse = :none
Expand Down Expand Up @@ -228,7 +242,7 @@ def partitions(device)
# @param actions [Hash]
# @return [Hash]
def remove_unsupported_actions(actions)
actions.reject { |d, a| a == :resize && !support_shrinking?(d) }
actions.reject { |a| a.is?(:resize) && !support_shrinking?(a.device) }
end

# Whether the device supports shrinking.
Expand Down
7 changes: 0 additions & 7 deletions service/lib/y2storage/agama_proposal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ def calculate_initial_planned(devicegraph)
# @param devicegraph [Devicegraph] the graph gets modified
def clean_graph(devicegraph)
remove_empty_partition_tables(devicegraph)
protect_sids
# {Proposal::SpaceMaker#prepare_devicegraph} returns a copy of the given devicegraph.
space_maker.prepare_devicegraph(devicegraph, partitions_for_clean)
end
Expand Down Expand Up @@ -234,18 +233,12 @@ def partitions_for_clean
planned_devices.partitions
end

# Configures SpaceMaker#protected_sids according to the given list of planned devices
def protect_sids
space_maker.protected_sids = planned_devices.all.select(&:reuse?).map(&:reuse_sid)
end

# Creates the planned devices on a given devicegraph
#
# @param devicegraph [Devicegraph] the graph gets modified
def create_devices(devicegraph)
devices_creator = Proposal::AgamaDevicesCreator.new(devicegraph, issues_list)
names = config.drives.map(&:found_device).compact.map(&:name)
protect_sids
result = devices_creator.populated_devicegraph(planned_devices, names, space_maker)
end

Expand Down
12 changes: 12 additions & 0 deletions service/lib/y2storage/proposal/agama_device_planner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def configure_reuse(planned, config)

planned.assign_reuse(device)
planned.reformat = reformat?(device, config)
planned.resize = grow?(device, config) if planned.respond_to?(:resize=)
end

# Whether to reformat the device.
Expand All @@ -75,6 +76,17 @@ def reformat?(device, config)
!config.filesystem&.reuse?
end

# Whether the device is a candidate to be resized (grown)
#
# @param device [Y2Storage::BlkDevice]
# @param config [Agama::Storage::Configs::Partition]
# @return [Boolean]
def grow?(device, config)
return false unless config.size

config.size.max.unlimited? || config.size.max > device.size
end

# @param planned [Planned::Disk, Planned::Partition]
# @param config [#encryption, #filesystem]
def configure_block_device(planned, config)
Expand Down
43 changes: 28 additions & 15 deletions service/lib/y2storage/proposal/agama_devices_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def populated_devicegraph(planned_devices, disk_names, space_maker)
attr_reader :creator_result

# @return [Devicegraph] Current devicegraph
attr_reader :devicegraph
attr_accessor :devicegraph

private

Expand Down Expand Up @@ -133,21 +133,11 @@ def process_existing_partitionables
# Check whether there is any chance of getting an unwanted order for the planned partitions
# within a disk
space_result = provide_space(partitions, original_graph, lvm_helper)
self.devicegraph = space_result[:devicegraph]
distribution = space_result[:partitions_distribution]

partition_creator = PartitionCreator.new(space_result[:devicegraph])
self.creator_result =
partition_creator.create_partitions(space_result[:partitions_distribution])

# This may be here or before create_partitions.
#
# What about resizing if needed?
# Likely shrinking is fine and should be always handled at the SpaceMaker.
# But I'm not so sure if growing is so fine (we may need to make some space first).
# I don't think we have the growing case covered by SpaceMaker, the distribution
# calculator, etc.
planned_devices
.select(&:reuse?)
.map { |d| d.reuse!(devicegraph) }
grow_and_reuse_devices(distribution)
self.creator_result = PartitionCreator.new(devicegraph).create_partitions(distribution)
end

# @see #process_devices
Expand Down Expand Up @@ -191,6 +181,29 @@ def provide_space(planned_partitions, devicegraph, lvm_helper)
result
end

# @see #process_existing_partitionables
def grow_and_reuse_devices(distribution)
planned_devices.select(&:reuse?).each do |planned|
space = assigned_space_next_to(planned, distribution)

planned.limit_grow(space.disposable_size, devicegraph) if space
planned.reuse!(devicegraph)
# TODO: Check if the final size is smaller than the min and... abort? register issue?
space&.update_disk_space
end
end

# Assigned space that is located next to the given planned device, only if that device
# points to an existing partition that needs to grow
#
# @return [Y2Storage::Planned::AssignedSpace]
def assigned_space_next_to(planned, distribution)
return unless planned.respond_to?(:subsequent_slot?)
return unless planned.resize?

distribution.spaces.find { |s| planned.subsequent_slot?(s) }
end

# @see #process_existing_partitionables
def partitions_for_existing(planned_devices)
# Maybe in the future this can include partitions on top of existing MDs
Expand Down
35 changes: 27 additions & 8 deletions service/lib/y2storage/proposal/agama_space_maker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,34 +57,53 @@ def guided_settings(config)
# @param config [Agama::Storage::Config]
# @return [Hash]
def space_actions(config)
force_delete_actions = force_delete_actions(config)
delete_actions = delete_actions(config)

force_delete_actions.merge(delete_actions)
actions = force_delete_actions(config)
actions.concat(delete_actions(config))
actions.concat(resize_actions(config))
end

# Space actions for devices that must be deleted.
#
# @param config [Agama::Storage::Config]
# @return [Hash]
# @return [Array<Y2Storage::SpaceActions::Delete>]
def force_delete_actions(config)
partition_configs = partitions(config).select(&:delete?)
partition_names = device_names(partition_configs)

partition_names.each_with_object({}) { |p, a| a[p] = :force_delete }
partition_names.map { |p| Y2Storage::SpaceActions::Delete.new(p, mandatory: true) }
end

# Space actions for devices that might be deleted.
#
# @note #delete? takes precedence over #delete_if_needed?.
#
# @param config [Agama::Storage::Config]
# @return [Hash]
# @return [Array<Y2Storage::SpaceActions::Delete>]
def delete_actions(config)
partition_configs = partitions(config).select(&:delete_if_needed?).reject(&:delete?)
partition_names = device_names(partition_configs)

partition_names.each_with_object({}) { |p, a| a[p] = :delete }
partition_names.map { |p| Y2Storage::SpaceActions::Delete.new(p) }
end

# Space actions for devices that might be resized
#
# @param config [Agama::Storage::Config]
# @return [Array<Y2Storage::SpaceActions::Resize>]
def resize_actions(config)
partition_configs = partitions(config).select(&:found_device).select(&:size)
partition_configs.map do |part|
# Resize actions contain information that is potentially useful for the SpaceMaker even
# when they are only about growing and not shrinking
min = current_size?(part, :min) ? nil : part.size.min
max = current_size?(part, :max) ? nil : part.size.max
Y2Storage::SpaceActions::Resize.new(part.found_device.name, min_size: min, max_size: max)
end.compact
end

# @see #resize_actions
def current_size?(part, attr)
part.found_device.size == part.size.public_send(attr)
end

# All partition configs from the given config.
Expand Down
2 changes: 1 addition & 1 deletion service/package/gem2rpm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
Requires: yast2-iscsi-client >= 4.5.7
Requires: yast2-network
Requires: yast2-proxy
Requires: yast2-storage-ng >= 5.0.17
Requires: yast2-storage-ng >= 5.0.18
Requires: yast2-users
%ifarch s390 s390x
Requires: yast2-s390 >= 4.6.4
Expand Down
6 changes: 6 additions & 0 deletions service/package/rubygem-agama-yast.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Fri Sep 20 13:09:47 UTC 2024 - Ancor Gonzalez Sosa <[email protected]>

- Storage: preliminary support for resizing partitions based on
limits specified at the config (gh#openSUSE/agama#1599).

-------------------------------------------------------------------
Fri Sep 20 11:40:53 UTC 2024 - Imobach Gonzalez Sosa <[email protected]>

Expand Down
5 changes: 5 additions & 0 deletions service/test/agama/dbus/storage/manager_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,13 @@ def serialize(value)
d.filesystem = filesystem
end

boot = Agama::Storage::Configs::Boot.new.tap do |b|
b.configure = false
end

Agama::Storage::Config.new.tap do |config|
config.drives = [drive]
config.boot = boot
end
end

Expand Down
8 changes: 8 additions & 0 deletions service/test/agama/storage/action_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
sda2.filesystem.delete_btrfs_subvolume("home")

# Resize sda2
allow(sda2).to receive(:resize_info).and_return(resize_info)
sda2.resize(Y2Storage::DiskSize.GiB(30))
end

Expand All @@ -59,6 +60,13 @@
described_class.new(action, system_graph)
end

let(:resize_info) do
instance_double(
Y2Storage::ResizeInfo, resize_ok?: true,
min_size: Y2Storage::DiskSize.GiB(20), max_size: Y2Storage::DiskSize.GiB(40)
)
end

describe "#device_sid" do
it "returns the SID of the affected device" do
sda2 = system_graph.find_by_name("/dev/sda2")
Expand Down
8 changes: 8 additions & 0 deletions service/test/agama/storage/actions_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
sda2.filesystem.delete_btrfs_subvolume("home")

# Resize sda2
allow(sda2).to receive(:resize_info).and_return(resize_info)
sda2.resize(Y2Storage::DiskSize.GiB(30))

# Create new partition
Expand All @@ -56,6 +57,13 @@
partition_table.delete_partition(sda3)
end

let(:resize_info) do
instance_double(
Y2Storage::ResizeInfo, resize_ok?: true,
min_size: Y2Storage::DiskSize.GiB(20), max_size: Y2Storage::DiskSize.GiB(40)
)
end

it "generates a sorted list of actions" do
actions = subject.generate
expect(actions.size).to eq(4)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@
expect(y2storage_settings.encryption_method).to eq(Y2Storage::EncryptionMethod::LUKS2)
expect(y2storage_settings.encryption_pbkdf).to eq(Y2Storage::PbkdFunction::ARGON2ID)
expect(y2storage_settings.space_settings.strategy).to eq(:bigger_resize)
expect(y2storage_settings.space_settings.actions).to eq({ "/dev/sda" => :force_delete })
expect(y2storage_settings.space_settings.actions).to eq(
[Y2Storage::SpaceActions::Delete.new("/dev/sda", mandatory: true)]
)
expect(y2storage_settings.volumes).to include(
an_object_having_attributes(
mount_point: "/test",
Expand Down Expand Up @@ -286,28 +288,33 @@

expect(y2storage_settings.space_settings).to have_attributes(
strategy: :bigger_resize,
actions: {
"/dev/sda1" => :force_delete,
"/dev/sda2" => :force_delete,
"/dev/sda3" => :force_delete
}
actions: [
Y2Storage::SpaceActions::Delete.new("/dev/sda1", mandatory: true),
Y2Storage::SpaceActions::Delete.new("/dev/sda2", mandatory: true),
Y2Storage::SpaceActions::Delete.new("/dev/sda3", mandatory: true)
]
)
end
end

context "when the space policy is set to :resize" do
before do
settings.space.policy = :resize

allow(Agama::Storage::DeviceShrinking).to receive(:new) do |dev|
dev.name == "/dev/sda2" ? shrink_true : shrink_false
end
end

let(:shrink_false) { instance_double(Agama::Storage::DeviceShrinking, supported?: false) }
let(:shrink_true) { instance_double(Agama::Storage::DeviceShrinking, supported?: true) }

it "generates resize actions for the partitions that support shrinking" do
y2storage_settings = subject.convert

expect(y2storage_settings.space_settings).to have_attributes(
strategy: :bigger_resize,
actions: {
"/dev/sda2" => :resize
}
actions: [Y2Storage::SpaceActions::Resize.new("/dev/sda2")]
)
end
end
Expand Down
1 change: 1 addition & 0 deletions service/test/agama/storage/proposal_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ def drive(partitions)
let(:config_json) do
{
storage: {
boot: { configure: false },
drives: [
{
filesystem: {
Expand Down
Loading

0 comments on commit 484fa9c

Please sign in to comment.