Skip to content

Commit

Permalink
storage: add size solver
Browse files Browse the repository at this point in the history
- New config solver for sizes (current, defaults, etc).
- Move logic related to fallback sizes to ConfigBuilder.
  • Loading branch information
joseivanlopez committed Sep 23, 2024
1 parent 966eb34 commit 524833b
Show file tree
Hide file tree
Showing 12 changed files with 1,008 additions and 451 deletions.
98 changes: 2 additions & 96 deletions service/lib/agama/storage/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
# 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"
require "agama/storage/configs/boot"
require "agama/storage/config_conversions/from_json"

module Agama
module Storage
Expand Down Expand Up @@ -92,24 +93,6 @@ def implicit_boot_device
root_drive&.found_device&.name
end

# Sets min and max sizes for all partitions and logical volumes with default size
#
# @param volume_builder [VolumeTemplatesBuilder] used to check the configuration of the
# product volume templates
def calculate_default_sizes(volume_builder)
default_size_devices.each do |dev|
dev.size.min = default_size(dev, :min, volume_builder)
dev.size.max = default_size(dev, :max, volume_builder)
end
end

private

# return [Array<Configs::Filesystem>]
def filesystems
(drives + partitions + logical_volumes).map(&:filesystem).compact
end

# return [Array<Configs::Partition>]
def partitions
drives.flat_map(&:partitions)
Expand All @@ -119,83 +102,6 @@ def partitions
def logical_volumes
volume_groups.flat_map(&:logical_volumes)
end

# return [Array<Configs::Partition, Configs::LogicalVolume>]
def default_size_devices
(partitions + logical_volumes).select { |p| p.size&.default? }
end

# Min or max size that should be used for the given partition or logical volume
#
# @param device [Configs::Partition] device configured to have a default size
# @param attr [Symbol] :min or :max
# @param builder [VolumeTemplatesBuilder] see {#calculate_default_sizes}
def default_size(device, attr, builder)
path = device.filesystem&.path || ""
vol = builder.for(path)
return fallback_size(attr) unless vol

# Theoretically, neither Volume#min_size or Volume#max_size can be nil
# At most they will be zero or unlimited, respectively
return vol.send(:"#{attr}_size") unless vol.auto_size?

outline = vol.outline
size = size_with_fallbacks(outline, attr, builder)
size = size_with_ram(size, outline)
size_with_snapshots(size, device, outline)
end

# TODO: these are the fallbacks used when constructing volumes, not sure if repeating them
# here is right
def fallback_size(attr)
return Y2Storage::DiskSize.zero if attr == :min

Y2Storage::DiskSize.unlimited
end

# @see #default_size
def size_with_fallbacks(outline, attr, builder)
fallback_paths = outline.send(:"#{attr}_size_fallback_for")
missing_paths = fallback_paths.reject { |p| proposed_path?(p) }

size = outline.send(:"base_#{attr}_size")
missing_paths.inject(size) { |total, p| total + builder.for(p).send(:"#{attr}_size") }
end

# @see #default_size
def size_with_ram(initial_size, outline)
return initial_size unless outline.adjust_by_ram?

[initial_size, ram_size].max
end

# @see #default_size
def size_with_snapshots(initial_size, device, outline)
return initial_size unless device.filesystem.btrfs_snapshots?
return initial_size unless outline.snapshots_affect_sizes?

if outline.snapshots_size && outline.snapshots_size > DiskSize.zero
initial_size + outline.snapshots_size
else
multiplicator = 1.0 + (outline.snapshots_percentage / 100.0)
initial_size * multiplicator
end
end

# Whether there is a separate filesystem configured for the given path
#
# @param path [String, Pathname]
# @return [Boolean]
def proposed_path?(path)
filesystems.any? { |fs| fs.path?(path) }
end

# Return the total amount of RAM as DiskSize
#
# @return [DiskSize] current RAM size
def ram_size
@ram_size ||= Y2Storage::DiskSize.new(Y2Storage::StorageManager.instance.arch.ram_size)
end
end
end
end
102 changes: 102 additions & 0 deletions service/lib/agama/storage/config_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
require "agama/storage/configs"
require "agama/storage/proposal_settings_reader"
require "agama/storage/volume_templates_builder"
require "pathname"
require "y2storage/disk_size"
require "y2storage/storage_manager"

module Agama
module Storage
Expand Down Expand Up @@ -55,6 +58,23 @@ def default_filesystem(path = nil)
end
end

# Default size config from the product definition.
#
# @param path [String, nil]
# @return [Configs::Size]
def default_size(path = nil, having_paths: [], with_snapshots: true)
volume = volume_builder.for(path || "")

return unlimited_size unless volume

return auto_size(volume.outline, having_paths, with_snapshots) if volume.auto_size?

Configs::Size.new.tap do |config|
config.min = volume.min_size
config.max = volume.max_size
end
end

private

# @return [Agama::Config]
Expand All @@ -73,6 +93,88 @@ def default_fstype(path = nil)
end
end

# @return [Configs::Size]
def unlimited_size
Configs::Size.new.tap do |config|
config.min = Y2Storage::DiskSize.zero
config.max = Y2Storage::DiskSize.unlimited
end
end

# @see #default_size
#
# @param outline [VolumeOutline]
# @param paths [Array<String>]
# @param snapshots [Boolean]
#
# @return [Configs::Size]
def auto_size(outline, paths, snapshots)
min_fallbacks = remove_paths(outline.min_size_fallback_for, paths)
min_size_fallbacks = min_fallbacks.map { |p| volume_builder.for(p).min_size }
min = min_size_fallbacks.reduce(outline.base_min_size, &:+)

max_fallbacks = remove_paths(outline.max_size_fallback_for, paths)
max_size_fallbacks = max_fallbacks.map { |p| volume_builder.for(p).max_size }
max = max_size_fallbacks.reduce(outline.base_max_size, &:+)

if outline.adjust_by_ram?
min = size_with_ram(min)
max = size_with_ram(max)
end

if snapshots
min = size_with_snapshots(min, outline)
max = size_with_snapshots(max, outline)
end

Configs::Size.new.tap do |config|
config.min = min
config.max = max
end
end

# @see #default_size
#
# @param size [Y2Storage::DiskSize]
# @return [Y2Storage::DiskSize]
def size_with_ram(size)
[size, ram_size].max
end

# @see #default_size
#
# @param size [Y2Storage::DiskSize]
# @param outline [VolumeOutline]
#
# @return [Y2Storage::DiskSize]
def size_with_snapshots(size, outline)
return size unless outline.snapshots_affect_sizes?

if outline.snapshots_size && outline.snapshots_size > Y2Storage::DiskSize.zero
size + outline.snapshots_size
else
multiplicator = 1.0 + (outline.snapshots_percentage / 100.0)
size * multiplicator
end
end

# @param paths [Array<String>]
# @param paths_to_remove [Array<String>]
#
# @return [Array<String>]
def remove_paths(paths, paths_to_remove)
paths.reject do |path|
paths_to_remove.any? { |p| Pathname.new(p).cleanpath == Pathname.new(path).cleanpath }
end
end

# Total amount of RAM.
#
# @return [DiskSize]
def ram_size
@ram_size ||= Y2Storage::DiskSize.new(Y2Storage::StorageManager.instance.arch.ram_size)
end

# @return [ProposalSettings]
def settings
@settings ||= ProposalSettingsReader.new(product_config).read
Expand Down
18 changes: 5 additions & 13 deletions service/lib/agama/storage/config_conversions/from_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,31 @@
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require "agama/config"
require "agama/storage/config_builder"
require "agama/storage/config_conversions/from_json_conversions/config"
require "agama/storage/volume_templates_builder"

module Agama
module Storage
module ConfigConversions
# Config conversion from JSON hash according to schema.
class FromJSON
# @param config_json [Hash]
# @param product_config [Agama::Config]
def initialize(config_json, product_config:)
# @param product_config [Agama::Config, nil]
def initialize(config_json, product_config: nil)
# TODO: Replace product_config param by a ProductDefinition.
@config_json = config_json
@product_config = product_config
@product_config = product_config || Agama::Config.new
end

# Performs the conversion from Hash according to the JSON schema.
#
# @return [Storage::Config]
def convert
# TODO: Raise error if config_json does not match the JSON schema.
config = FromJSONConversions::Config
FromJSONConversions::Config
.new(config_json, config_builder: config_builder)
.convert

config.calculate_default_sizes(volume_builder)
config
end

private
Expand All @@ -61,11 +58,6 @@ def convert
def config_builder
@config_builder ||= ConfigBuilder.new(product_config)
end

# @return [VolumeTemplatesBuilder]
def volume_builder
@volume_builder ||= VolumeTemplatesBuilder.new_from_config(product_config)
end
end
end
end
Expand Down
Loading

0 comments on commit 524833b

Please sign in to comment.