Skip to content

Commit

Permalink
feature(storage): config support for current size (#1617)
Browse files Browse the repository at this point in the history
Add support to the storage config for resizing a partition using its
current size as minimum or maximim limit, see
[auto_storage.md](https://github.com/openSUSE/agama/blob/master/doc/auto_storage.md#specifying-the-size-of-a-device)
document.

For growing a partition as much as possible:

~~~json
{
  "partitions": [
    {
      "search": "/dev/vda1",
      "size": {
        "min": "current"
      }
    }
  ]
}
~~~ 

For shrinking a partition as much as possible:

~~~json
{
  "partitions": [
    {
      "search": "/dev/vda1",
      "size": {
        "min": 0,
        "max": "current"
      }
    }
  ]
}
~~~ 

For omittied size, the default size indicated by the product is used for
a new partition. If the partition already exists (has a *search*), then
the current device size is used as default (i.e., the device is not
resized at all).
  • Loading branch information
joseivanlopez authored Sep 24, 2024
2 parents 64351dc + aa00a4f commit 1e736e3
Show file tree
Hide file tree
Showing 21 changed files with 1,281 additions and 613 deletions.
62 changes: 62 additions & 0 deletions rust/agama-lib/share/examples/storage_sizes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"storage": {
"drives": [
{
"partitions": [
{
"size": 2048
},
{
"size": "10 GiB"
},
{
"size": ["1 GiB"]
},
{
"size": [1024, "50 GiB"]
},
{
"size": {
"min": "1 GiB"
}
},
{
"size": {
"min": 1024,
"max": "50 GiB"
}
},
{
"search": {},
"size": ["current"]
},
{
"search": {},
"size": [0, "current"]
},
{
"search": {},
"size": ["current", "10 GiB"]
},
{
"size": {
"min": "current"
}
},
{
"size": {
"min": 0,
"max": "current"
}
},
{
"size": {
"min": "current",
"max": "10 GiB"
}
}
]
}
]
}
}
23 changes: 14 additions & 9 deletions rust/agama-lib/share/profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -920,14 +920,19 @@
{ "$ref": "#/$defs/sizeInteger" }
]
},
"sizeValueWithCurrent": {
"anyOf": [
{ "$ref": "#/$defs/sizeValue" },
{
"title": "Current size",
"description": "The current size of the device.",
"const": "current"
}
]
},
"size": {
"title": "Size options",
"anyOf": [
{
"title": "Automatic size",
"description": "The size is auto calculated according to the product.",
"const": "auto"
},
{
"$ref": "#/$defs/sizeValue"
},
Expand All @@ -936,11 +941,11 @@
"description": "Lower size limit and optionally upper size limit.",
"type": "array",
"items": {
"$ref": "#/$defs/sizeValue"
"$ref": "#/$defs/sizeValueWithCurrent"
},
"minItems": 1,
"maxItems": 2,
"examples": [[1024, 2048], ["1 GiB", "5 GiB"], [1024, "2 GiB"], ["2 GiB"]]
"examples": [[1024, "current"], ["1 GiB", "5 GiB"], [1024, "2 GiB"], ["2 GiB"]]
},
{
"title": "Size range",
Expand All @@ -950,11 +955,11 @@
"properties": {
"min": {
"title": "Mandatory lower size limit",
"$ref": "#/$defs/sizeValue"
"$ref": "#/$defs/sizeValueWithCurrent"
},
"max": {
"title": "Optional upper size limit",
"$ref": "#/$defs/sizeValue"
"$ref": "#/$defs/sizeValueWithCurrent"
}
}
}
Expand Down
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
116 changes: 116 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,37 @@ def default_filesystem(path = nil)
end
end

# Default size config from the product definition.
#
# The size defined by the product depends on the mount path of the device. That size can be
# increased because some reasons:
# * Fallback sizes: the size of other path is added to the volume. For example, if /home is
# not present, then the root volume increases its min and max limits by adding the min and
# max limits from the missing /home. The having_paths parameter is used to indicate what
# paths are present. The product defines the fallback paths.
# * Snapshots size: a device can be configured to use snapshots. The default size limits
# could be increased if snapshots are used. The with_snapshots parameter indicates whether
# to add the snapshots size. The product defines the snapshots size.
# * RAM size: the product defines whether the volume for a specific path should be as big as
# the RAM size.
#
# @param path [String, nil] Mount path of the device.
# @param having_paths [Array<String>] Paths where other devices are mounted.
# @param with_snapshots [Boolean] Whether to add the Btrfs snapshots size.
# @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 +107,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
Loading

0 comments on commit 1e736e3

Please sign in to comment.