From 524833b51da7cb75877c0060865c380bb0e1f3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 23 Sep 2024 15:39:02 +0100 Subject: [PATCH] storage: add size solver - New config solver for sizes (current, defaults, etc). - Move logic related to fallback sizes to ConfigBuilder. --- service/lib/agama/storage/config.rb | 98 +--- service/lib/agama/storage/config_builder.rb | 102 ++++ .../storage/config_conversions/from_json.rb | 18 +- .../lib/agama/storage/config_size_solver.rb | 206 +++++++ service/lib/agama/storage/config_solver.rb | 13 +- service/lib/agama/storage/configs/search.rb | 7 + service/lib/agama/storage/configs/size.rb | 2 +- .../storage/proposal_strategies/agama.rb | 7 +- service/lib/y2storage/agama_proposal.rb | 16 +- .../config_conversions/from_json_test.rb | 526 ++++++++++-------- service/test/agama/storage/proposal_test.rb | 5 +- service/test/y2storage/agama_proposal_test.rb | 459 ++++++++++++--- 12 files changed, 1008 insertions(+), 451 deletions(-) create mode 100644 service/lib/agama/storage/config_size_solver.rb diff --git a/service/lib/agama/storage/config.rb b/service/lib/agama/storage/config.rb index 7384c447c8..05e532675a 100644 --- a/service/lib/agama/storage/config.rb +++ b/service/lib/agama/storage/config.rb @@ -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 @@ -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] - def filesystems - (drives + partitions + logical_volumes).map(&:filesystem).compact - end - # return [Array] def partitions drives.flat_map(&:partitions) @@ -119,83 +102,6 @@ def partitions def logical_volumes volume_groups.flat_map(&:logical_volumes) end - - # return [Array] - 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 diff --git a/service/lib/agama/storage/config_builder.rb b/service/lib/agama/storage/config_builder.rb index 7a5738858b..d7b1dd339c 100644 --- a/service/lib/agama/storage/config_builder.rb +++ b/service/lib/agama/storage/config_builder.rb @@ -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 @@ -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] @@ -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] + # @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] + # @param paths_to_remove [Array] + # + # @return [Array] + 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 diff --git a/service/lib/agama/storage/config_conversions/from_json.rb b/service/lib/agama/storage/config_conversions/from_json.rb index 846a8b7154..6dbc12f933 100644 --- a/service/lib/agama/storage/config_conversions/from_json.rb +++ b/service/lib/agama/storage/config_conversions/from_json.rb @@ -19,9 +19,9 @@ # 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 @@ -29,11 +29,11 @@ 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. @@ -41,12 +41,9 @@ def initialize(config_json, product_config:) # @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 @@ -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 diff --git a/service/lib/agama/storage/config_size_solver.rb b/service/lib/agama/storage/config_size_solver.rb new file mode 100644 index 0000000000..1098c0d618 --- /dev/null +++ b/service/lib/agama/storage/config_size_solver.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +# Copyright (c) [2024] 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/configs/size" +require "agama/storage/config_builder" + +module Agama + module Storage + # Solver for the size configs. + # + # It assigns proper size values according to the product and the system. + class ConfigSizeSolver + # @param devicegraph [Y2Storage::Devicegraph] + # @param product_config [Agama::Config] + def initialize(devicegraph, product_config) + @devicegraph = devicegraph + @product_config = product_config + end + + # Solves all the size configs within a given config. + # + # @note The config object is modified. + # + # @param config [Config] + def solve(config) + @config = config + + solve_default_sizes + solve_current_sizes + end + + private + + # @return [Y2Storage::Devicegraph] + attr_reader :devicegraph + + # @return [Agama::Config] + attr_reader :product_config + + # @return [Config] + attr_reader :config + + def solve_default_sizes + configs_with_default_product_size.each { |c| solve_default_product_size(c) } + configs_with_default_device_size.each { |c| solve_default_device_size(c) } + end + + def solve_current_sizes + configs_with_valid_current_size.each { |c| solve_current_size(c) } + configs_with_invalid_current_size.each { |c| solve_default_product_size(c) } + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + def solve_default_product_size(config) + config.size = size_from_product(config) + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + def solve_default_device_size(config) + config.size = size_from_device(config.found_device) + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + def solve_current_size(config) + min = config.size.min + max = config.size.max + size = size_from_device(config.found_device) + size.min = min if min + size.max = max if max + config.size = size + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + # @return [Configs::Size] + def size_from_product(config) + path = config.filesystem&.path + snapshots = config.filesystem&.btrfs_snapshots? + + paths = configs_with_filesystem + .map(&:filesystem) + .compact + .map(&:path) + .compact + + config_builder.default_size(path, having_paths: paths, with_snapshots: snapshots) + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + # @return [Configs::Size] + def size_from_device(device) + Configs::Size.new.tap do |config| + config.default = false + config.min = device.size + config.max = device.size + end + end + + # @return [Array] + def configs_with_size + configs = config.partitions + config.logical_volumes + configs.select { |c| valid?(c) } + end + + # @return [Array] + def configs_with_filesystem + configs = config.drives + config.partitions + config.logical_volumes + configs.select { |c| valid?(c) } + end + + # @return [Array] + def configs_with_default_product_size + configs_with_size.select { |c| with_default_product_size?(c) } + end + + # @return [Array] + def configs_with_default_device_size + configs_with_size.select { |c| with_default_device_size?(c) } + end + + # @return [Array] + def configs_with_valid_current_size + configs_with_size.select { |c| with_valid_current_size?(c) } + end + + # @return [Array] + def configs_with_invalid_current_size + configs_with_size.select { |c| with_invalid_current_size?(c) } + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + # @return [Boolean] + def with_default_product_size?(config) + config.size.default? && create_device?(config) + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + # @return [Boolean] + def with_default_device_size?(config) + config.size.default? && reuse_device?(config) + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + # @return [Boolean] + def with_valid_current_size?(config) + with_current_size?(config) && reuse_device?(config) + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + # @return [Boolean] + def with_invalid_current_size?(config) + with_current_size?(config) && create_device?(config) + end + + # @param config [Configs::Partition, Configs::LogicalVolume] + # @return [Boolean] + def with_current_size?(config) + !config.size.default? && (config.size.min.nil? || config.size.max.nil?) + end + + # @param config [Object] Any config from {Configs}. + # @return [Boolean] + def valid?(config) + create_device?(config) || reuse_device?(config) + end + + # @param config [Object] Any config from {Configs}. + # @return [Boolean] + def create_device?(config) + return true unless config.respond_to?(:search) + + config.search.nil? || config.search.create_device? + end + + # @param config [Object] Any config from {Configs}. + # @return [Boolean] + def reuse_device?(config) + return false unless config.respond_to?(:found_device) + + !config.found_device.nil? + end + + # @return [ConfigBuilder] + def config_builder + @config_builder ||= ConfigBuilder.new(product_config) + end + end + end +end diff --git a/service/lib/agama/storage/config_solver.rb b/service/lib/agama/storage/config_solver.rb index e03989d5a3..505dc06a81 100644 --- a/service/lib/agama/storage/config_solver.rb +++ b/service/lib/agama/storage/config_solver.rb @@ -20,29 +20,38 @@ # find current contact information at www.suse.com. require "agama/storage/config_search_solver" +require "agama/storage/config_size_solver" module Agama module Storage # Class for solving a storage config. + # + # It assigns proper devices and size values according to the product and the system. class ConfigSolver # @param devicegraph [Y2Storage::Devicegraph] - def initialize(devicegraph) + # @param product_config [Agama::Config] + def initialize(devicegraph, product_config) @devicegraph = devicegraph + @product_config = product_config end - # Solves the given config with information from the devicegrah. + # Solves all the search and size configs within a given config. # # @note The config object is modified. # # @param config [Config] def solve(config) ConfigSearchSolver.new(devicegraph).solve(config) + ConfigSizeSolver.new(devicegraph, product_config).solve(config) end private # @return [Y2Storage::Devicegraph] attr_reader :devicegraph + + # @return [Agama::Config] + attr_reader :product_config end end end diff --git a/service/lib/agama/storage/configs/search.rb b/service/lib/agama/storage/configs/search.rb index 207a48a289..ecb39c2b0f 100644 --- a/service/lib/agama/storage/configs/search.rb +++ b/service/lib/agama/storage/configs/search.rb @@ -71,6 +71,13 @@ def any_device? def skip_device? solved? && device.nil? && if_not_found == :skip end + + # Whether the device is not found and it has to be created. + # + # @return [Boolean] + def create_device? + solved? && device.nil? && if_not_found == :create + end end end end diff --git a/service/lib/agama/storage/configs/size.rb b/service/lib/agama/storage/configs/size.rb index 5c3d408d84..63353fe46a 100644 --- a/service/lib/agama/storage/configs/size.rb +++ b/service/lib/agama/storage/configs/size.rb @@ -40,7 +40,7 @@ def initialize # @return [Boolean] def default? - !!@default + @default end end end diff --git a/service/lib/agama/storage/proposal_strategies/agama.rb b/service/lib/agama/storage/proposal_strategies/agama.rb index c34e134f7c..1ae18619cb 100644 --- a/service/lib/agama/storage/proposal_strategies/agama.rb +++ b/service/lib/agama/storage/proposal_strategies/agama.rb @@ -68,9 +68,10 @@ def issues # @return [Y2Storage::AgamaProposal] def agama_proposal Y2Storage::AgamaProposal.new(storage_config, - issues_list: [], - devicegraph: probed_devicegraph, - disk_analyzer: disk_analyzer) + product_config: config, + devicegraph: probed_devicegraph, + disk_analyzer: disk_analyzer, + issues_list: []) end end end diff --git a/service/lib/y2storage/agama_proposal.rb b/service/lib/y2storage/agama_proposal.rb index 5dca7796db..a386da7e4f 100644 --- a/service/lib/y2storage/agama_proposal.rb +++ b/service/lib/y2storage/agama_proposal.rb @@ -56,21 +56,28 @@ class AgamaProposal < Proposal::Base # @note The storage config (first param) is modified in several ways: # * The search configs are solved. + # * The sizes are solved (setting the size of the selected device, assigning fallbacks, etc). # # @param config [Agama::Storage::Config] + # @param product_config [Agama::Config] # @param devicegraph [Devicegraph] Starting point. If nil, then probed devicegraph will be used. # @param disk_analyzer [DiskAnalyzer] By default, the method will create a new one based on the # initial devicegraph or will use the one from the StorageManager if starting from probed # (i.e. 'devicegraph' argument is also missing). # @param issues_list [Array { + "lvm" => false, + "space_policy" => "delete", + "encryption" => { + "method" => "luks2" + }, + "volumes" => ["/", "swap"], + "volume_templates" => [ + { + "mount_path" => "/", "filesystem" => "btrfs", "size" => { "auto" => true }, + "btrfs" => { + "snapshots" => true, "default_subvolume" => "@", + "subvolumes" => ["home", "opt", "root", "srv"] + }, + "outline" => { + "required" => true, "snapshots_configurable" => true, + "auto_size" => { + "base_min" => "5 GiB", "base_max" => "10 GiB", + "min_fallback_for" => ["/home"], "max_fallback_for" => ["/home"], + "snapshots_increment" => "300%" + } + } + }, + { + "mount_path" => "/home", "size" => { "auto" => false, "min" => "5 GiB" }, + "filesystem" => "xfs", "outline" => { "required" => false } + }, + { + "mount_path" => "swap", "filesystem" => "swap", + "outline" => { "required" => false } + }, + { "mount_path" => "", "filesystem" => "ext4", + "size" => { "min" => "100 MiB" } } + ] + } + } + end + let(:issues_list) { [] } let(:drives) { [drive0] } @@ -98,6 +154,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end end part.size = Agama::Storage::Configs::Size.new.tap do |size| + size.default = false size.min = 8.5.GiB size.max = Y2Storage::DiskSize.unlimited end @@ -121,10 +178,8 @@ def partition_config(name: nil, filesystem: nil, size: nil) before do mock_storage(devicegraph: scenario) - end - - subject(:proposal) do - described_class.new(initial_config, issues_list: issues_list) + # To speed-up the tests + allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true) end let(:scenario) { "empty-hd-50GiB.yaml" } @@ -147,7 +202,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) context "if no boot devices should be created" do before do - initial_config.boot = Agama::Storage::Configs::Boot.new.tap { |b| b.configure = false } + config.boot = Agama::Storage::Configs::Boot.new.tap { |b| b.configure = false } end it "proposes to create only the root device" do @@ -650,18 +705,37 @@ def partition_config(name: nil, filesystem: nil, size: nil) context "when reusing a partition" do let(:scenario) { "disks.yaml" } - let(:drives) { [drive] } - - let(:drive) do - drive_config.tap { |c| c.partitions = [partition] } + let(:config_json) do + { + drives: [ + { + partitions: [ + { + search: name, + filesystem: { + reuseIfPossible: reuse, + path: "/", + type: "ext3" + }, + size: size + }, + { + filesystem: { + path: "/home" + } + } + ] + } + ] + } end - let(:partition) { partition_config(name: name, filesystem: "ext3", size: 20.GiB) } + let(:reuse) { nil } + + let(:size) { nil } context "if trying to reuse the file system" do - before do - partition.filesystem.reuse = true - end + let(:reuse) { true } context "and the partition is already formatted" do let(:name) { "/dev/vda2" } @@ -691,9 +765,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "if not trying to reuse the file system" do - before do - partition.filesystem.reuse = false - end + let(:reuse) { false } context "and the partition is already formatted" do let(:name) { "/dev/vda2" } @@ -722,23 +794,54 @@ def partition_config(name: nil, filesystem: nil, size: nil) end end end + + context "if no size is indicated" do + let(:name) { "/dev/vda2" } + + let(:size) { nil } + + it "does not resize the partition" do + devicegraph = proposal.propose + + vda2 = devicegraph.find_by_name("/dev/vda2") + expect(vda2.size).to eq(20.GiB) + end + end end context "when creating a new partition" do let(:scenario) { "disks.yaml" } - let(:drives) { [drive] } - - let(:drive) do - drive_config.tap { |c| c.partitions = [partition] } + let(:config_json) do + { + drives: [ + { + partitions: [ + { + filesystem: { + reuseIfPossible: reuse, + path: "/", + type: "ext3" + }, + size: size + }, + { + filesystem: { + path: "/home" + } + } + ] + } + ] + } end - let(:partition) { partition_config(filesystem: "ext3", size: 1.GiB) } + let(:reuse) { nil } + + let(:size) { nil } context "if trying to reuse the file system" do - before do - partition.filesystem.reuse = true - end + let(:reuse) { true } it "creates the file system" do devicegraph = proposal.propose @@ -750,9 +853,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "if not trying to reuse the file system" do - before do - partition.filesystem.reuse = false - end + let(:reuse) { false } it "creates the file system" do devicegraph = proposal.propose @@ -762,27 +863,250 @@ def partition_config(name: nil, filesystem: nil, size: nil) expect(filesystem.type).to eq(Y2Storage::Filesystems::Type::EXT3) end end - end - context "resizing an existing partition" do - let(:scenario) { "disks.yaml" } + context "if no size is indicated" do + let(:size) { nil } - let(:partitions0) { [root_partition, vda3] } + it "creates the partition according to the size from the product definition" do + devicegraph = proposal.propose - let(:vda3) do - Agama::Storage::Configs::Partition.new.tap do |config| - block_device_config(config, name: "/dev/vda3") - config.size = Agama::Storage::Configs::Size.new.tap do |size_config| - size_config.min = vda3_min - size_config.max = vda3_max + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "/"), + size: 10.GiB - 1.MiB + ) + ) + end + end + + context "if a size is indicated" do + let(:size) { "5 GiB" } + + it "creates the partition according to the given size" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "/"), + size: 5.GiB + ) + ) + end + end + + context "if 'current' size is indicated" do + let(:size) { { min: "current" } } + + it "creates the partition according to the size from the product definition" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "/"), + size: 10.GiB - 1.MiB + ) + ) + end + end + + context "if the size is not indicated for some partition with fallbacks" do + let(:scenario) { "empty-hd-50GiB.yaml" } + + let(:config_json) do + { + drives: [ + { + partitions: [ + { + filesystem: { + path: "/", + type: { + btrfs: { snapshots: snapshots } + } + } + }, + { + filesystem: { path: other_path } + } + ] + } + ] + } + end + + context "and the other partitions are omitted" do + let(:other_path) { nil } + let(:snapshots) { false } + + it "creates the partition adding the fallback sizes" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "/"), + size: 29.95.GiB - 2.80.MiB + ) + ) + end + + context "and snapshots are enabled" do + let(:snapshots) { true } + + it "creates the partition adding the fallback and snapshots sizes" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "/"), + size: 44.95.GiB - 2.80.MiB + ) + ) + end + end + end + + context "and the other partitions are present" do + let(:other_path) { "/home" } + let(:snapshots) { false } + + it "creates the partition ignoring the fallback sizes" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "/"), + size: 10.GiB + ) + ) + end + + context "and snapshots are enabled" do + let(:snapshots) { true } + + it "creates the partition adding the snapshots sizes" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "/"), + size: 32.50.GiB - 4.MiB + ) + ) + end end end end - before do - drive0.search.name = "/dev/vda" + context "if the partition has to be enlarged according to RAM size" do + let(:scenario) { "empty-hd-50GiB.yaml" } - allow_any_instance_of(Y2Storage::Partition).to receive(:detect_resize_info) + let(:product_data) do + { + "storage" => { + "volume_templates" => [ + { + "mount_path" => "swap", + "filesystem" => "swap", + "size" => { "auto" => true }, + "outline" => { + "auto_size" => { + "adjust_by_ram" => true, + "base_min" => "2 GiB", + "base_max" => "4 GiB" + } + } + } + ] + } + } + end + + let(:config_json) do + { + drives: [ + { + partitions: [ + { + filesystem: { + path: "swap" + }, + size: size + } + ] + } + ] + } + end + + before do + allow_any_instance_of(Y2Storage::Arch).to receive(:ram_size).and_return(8.GiB) + end + + context "and the partition size is not indicated" do + let(:size) { nil } + + it "creates the partition as big as the RAM" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "swap"), + size: 8.GiB + ) + ) + end + end + + context "and the partition size is indicated" do + let(:size) { "2 GiB" } + + it "creates the partition with the given size" do + devicegraph = proposal.propose + + expect(devicegraph.partitions).to include( + an_object_having_attributes( + filesystem: an_object_having_attributes(mount_path: "swap"), + size: 2.GiB + ) + ) + end + end + end + end + + context "resizing an existing partition" do + let(:scenario) { "disks.yaml" } + + let(:config_json) do + { + drives: [ + { + search: "/dev/vda", + partitions: [ + { + filesystem: { + type: "btrfs", + path: "/" + }, + size: root_size + }, + { + search: "/dev/vda3", + size: vda3_size + } + ] + } + ] + } + end + + let(:root_size) { ["8.5 GiB"] } + + let(:vda3_size) { nil } + + before do + allow_any_instance_of(Y2Storage::Partition) + .to(receive(:detect_resize_info)) .and_return(resize_info) end @@ -794,9 +1118,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "when the reused partition is expected to grow with no enforced limit" do - # Initial size, so no shrinking - let(:vda3_min) { 10.GiB } - let(:vda3_max) { Y2Storage::DiskSize.unlimited } + let(:vda3_size) { ["current"] } it "grows the device as much as allowed by the min size of the new partitions" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid @@ -815,9 +1137,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "when the reused partition is expected to grow up to a limit" do - # Initial size, so no shrinking - let(:vda3_min) { 10.GiB } - let(:vda3_max) { 15.GiB } + let(:vda3_size) { ["10 GiB", "15 GiB"] } it "grows the device up to the limit so the new partitions can exceed their mins" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid @@ -835,9 +1155,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "when the reused partition is expected to shrink as much as needed" do - let(:vda3_min) { 0.KiB } - # Initial size, so no growing - let(:vda3_max) { 10.GiB } + let(:vda3_size) { ["0 KiB", "current"] } context "if there is no need to shrink the partition" do it "does not modify the size of the partition" do @@ -855,9 +1173,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "if the partition needs to be shrunk to allocate the new ones" do - before do - root_partition.size.min = 24.GiB - end + let(:root_size) { "24 GiB" } it "shrinks the partition as needed" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid @@ -877,8 +1193,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "when the reused partition is expected to shrink in all cases" do - let(:vda3_min) { 0.KiB } - let(:vda3_max) { 6.GiB } + let(:vda3_size) { ["0 KiB", "6 GiB"] } context "if there is no need to shrink the partition" do it "shrinks the partition to the specified max size" do @@ -896,9 +1211,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "if the partition needs to be shrunk to allocate the new ones" do - before do - root_partition.size.min = 25.GiB - end + let(:root_size) { "25 Gib" } it "shrinks the partition as needed" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid @@ -921,14 +1234,6 @@ def partition_config(name: nil, filesystem: nil, size: nil) context "when the config has LVM volume groups" do let(:scenario) { "empty-hd-50GiB.yaml" } - let(:initial_config) do - Agama::Storage::ConfigConversions::FromJSON - .new(config_json, product_config: product_config) - .convert - end - - let(:product_config) { Agama::Config.new } - let(:config_json) do { drives: [ @@ -987,7 +1292,8 @@ def partition_config(name: nil, filesystem: nil, size: nil) filesystem: { path: "/home", type: "xfs" - } + }, + size: "2 GiB" } ] } @@ -1012,6 +1318,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) system_vg = devicegraph.find_by_name("/dev/system") system_pvs = system_vg.lvm_pvs.map(&:plain_blk_device) system_lvs = system_vg.lvm_lvs + expect(system_pvs).to contain_exactly( an_object_having_attributes(name: "/dev/sda2", size: 40.GiB) ) @@ -1060,7 +1367,7 @@ def partition_config(name: nil, filesystem: nil, size: nil) an_object_having_attributes( lv_name: "home", lv_type: Y2Storage::LvType::NORMAL, - size: 5.GiB - 4.MiB, + size: 2.GiB, filesystem: an_object_having_attributes( type: Y2Storage::Filesystems::Type::XFS, mount_path: "/home" @@ -1071,14 +1378,6 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "when a LVM physical volume is not found" do - let(:initial_config) do - Agama::Storage::ConfigConversions::FromJSON - .new(config_json, product_config: product_config) - .convert - end - - let(:product_config) { Agama::Config.new } - let(:config_json) do { drives: [ @@ -1127,14 +1426,6 @@ def partition_config(name: nil, filesystem: nil, size: nil) end context "when a LVM thin pool volume is not found" do - let(:initial_config) do - Agama::Storage::ConfigConversions::FromJSON - .new(config_json, product_config: product_config) - .convert - end - - let(:product_config) { Agama::Config.new } - let(:config_json) do { drives: [