diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index dbe12ab0bd..602e6ad93f 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -193,8 +193,8 @@ def default_volume(mount_path) end module ProposalStrategy - GUIDED = "guided".freeze - AUTOYAST = "autoyast".freeze + GUIDED = "guided" + AUTOYAST = "autoyast" end # Calculates a guided proposal. @@ -211,7 +211,7 @@ def calculate_guided_proposal(dbus_settings) "Agama settings: #{settings.inspect}" ) - success = proposal.calculate(settings) + success = proposal.calculate_guided(settings) success ? 0 : 1 end @@ -228,9 +228,7 @@ def calculate_autoyast_proposal(dbus_settings) "AutoYaST settings: #{settings.inspect}" ) - # @todo Call to expected backend method. - # success = autoyast_proposal.calculate(settings) - success = false + success = proposal.calculate_autoyast(settings) success ? 0 : 1 end @@ -241,12 +239,19 @@ def proposal_calculated? def proposal_result return {} unless proposal.calculated? - { - "success" => proposal.success?, - # @todo Return proper strategy. - "strategy" => ProposalStrategy::GUIDED, - "settings" => ProposalSettingsConversion.to_dbus(proposal.settings) - } + if proposal.strategy?(ProposalStrategy::GUIDED) + { + "success" => proposal.success?, + "strategy" => ProposalStrategy::GUIDED, + "settings" => ProposalSettingsConversion.to_dbus(proposal.settings) + } + else + { + "success" => proposal.success?, + "strategy" => ProposalStrategy::AUTOYAST, + "settings" => proposal.settings.to_json + } + end end dbus_interface PROPOSAL_CALCULATOR_INTERFACE do diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index aef32123e8..ee7531fed9 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -25,6 +25,7 @@ require "y2storage/clients/inst_prepdisk" require "agama/storage/actions" require "agama/storage/proposal" +require "agama/storage/autoyast_proposal" require "agama/storage/proposal_settings" require "agama/storage/callbacks" require "agama/storage/iscsi/manager" @@ -147,6 +148,13 @@ def proposal @proposal ||= Proposal.new(config, logger: logger) end + # Manager for the legacy AutoYaST storage proposal + # + # @return [Storage::AutoyastProposal] + def autoyast_proposal + @autoyast_proposal ||= AutoyastProposal.new(config, logger: logger) + end + # iSCSI manager # # @return [Storage::ISCSI::Manager] @@ -206,7 +214,7 @@ def probe_devices # Calculates the proposal using the settings from the config file. def calculate_proposal settings = ProposalSettingsReader.new(config).read - proposal.calculate(settings) + proposal.calculate_guided(settings) end # Adds the required packages to the list of resolvables to install diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb index 65f8d7e14e..c1a370d93a 100644 --- a/service/lib/agama/storage/proposal.rb +++ b/service/lib/agama/storage/proposal.rb @@ -21,8 +21,7 @@ require "agama/issue" require "agama/storage/actions" -require "agama/storage/device_settings" -require "agama/storage/proposal_settings_conversion" +require "agama/storage/proposal_strategies" require "yast" require "y2storage" @@ -32,14 +31,6 @@ module Storage class Proposal include Yast::I18n - # Settings used for calculating the proposal. - # - # @note Some values are recoverd from Y2Storage, see - # {ProposalSettingsConversion::FromY2Storage} - # - # @return [ProposalSettings, nil] nil if no proposal has been calculated yet. - attr_reader :settings - # @param config [Config] Agama config # @param logger [Logger] def initialize(config, logger: nil) @@ -47,9 +38,15 @@ def initialize(config, logger: nil) @config = config @logger = logger || Logger.new($stdout) + @issues = [] @on_calculate_callbacks = [] end + # List of issues. + # + # @return [Array] + attr_reader :issues + # Whether the proposal was already calculated. # # @return [Boolean] @@ -61,10 +58,10 @@ def calculated? # # @return [Boolean] def success? - calculated? && !proposal.failed? + calculated? && !proposal.failed? && issues.none?(&:error?) end - # Stores callbacks to be call after calculating a proposal. + # Stores callbacks to be called after calculating a proposal. def on_calculate(&block) @on_calculate_callbacks << block end @@ -76,21 +73,34 @@ def available_devices disk_analyzer&.candidate_disks || [] end - # Calculates a new proposal. + # Settings used to calculate the current proposal. # - # @param settings [Agamal::Storage::ProposalSettings] settings to calculate the proposal. - # @return [Boolean] whether the proposal was correctly calculated. - def calculate(settings) - return false unless storage_manager.probed? - - select_target_device(settings) if missing_target_device?(settings) + # The type depends on the kind of proposal, see {#calculate_guided} and {#calculate_autoyast}. + # + # @return [Agama::Storage::ProposalSettings, Array] + def settings + return unless calculated? - calculate_proposal(settings) + strategy_object.settings + end - @settings = ProposalSettingsConversion.from_y2storage(proposal.settings, settings) - @on_calculate_callbacks.each(&:call) + # Calculates a new guided proposal. + # + # @param settings [Agama::Storage::ProposalSettings] settings to calculate the proposal. + # @return [Boolean] whether the proposal was correctly calculated. + def calculate_guided(settings) + @strategy_object = ProposalStrategies::Guided.new(config, logger, settings) + calculate + end - success? + # Calculates a new legacy AutoYaST proposal. + # + # @param partitioning [Array] Hash-based representation of the section + # of the AutoYaST profile + # @return [Boolean] whether the proposal was correctly calculated. + def calculate_autoyast(partitioning) + @strategy_object = ProposalStrategies::Autoyast.new(config, logger, partitioning) + calculate end # Storage actions. @@ -102,17 +112,14 @@ def actions Actions.new(logger, proposal.devices.actiongraph).all end - # List of issues. + # Whether the current proposal was calculated the given strategy (:autoyast or :guided). # - # @return [Array] - def issues - return [] if !calculated? || success? + # @param id [#downcase] + # @return [Boolean] + def strategy?(id) + return false unless calculated? - [ - target_device_issue, - missing_devices_issue, - proposal_issue - ].compact + id.downcase.to_sym == strategy_object.id end private @@ -123,118 +130,78 @@ def issues # @return [Logger] attr_reader :logger - # @return [Y2Storage::MinGuidedProposal, nil] - def proposal - storage_manager.proposal - end + attr_reader :strategy_object - # Selects the first available device as target device for installation. + # Calculates a new proposal. # - # @param settings [ProposalSettings] - def select_target_device(settings) - device = available_devices.first&.name - return unless device - - case settings.device - when DeviceSettings::Disk - settings.device.name = device - when DeviceSettings::NewLvmVg - settings.device.candidate_pv_devices = [device] - when DeviceSettings::ReusedLvmVg - # TODO: select an existing VG? - end - end + # @return [Boolean] whether the proposal was correctly calculated. + def calculate + return false unless storage_manager.probed? - # Whether the given settings has no target device for the installation. - # - # @param settings [ProposalSettings] - # @return [Boolean] - def missing_target_device?(settings) - case settings.device - when DeviceSettings::Disk, DeviceSettings::ReusedLvmVg - settings.device.name.nil? - when DeviceSettings::NewLvmVg - settings.device.candidate_pv_devices.empty? + @issues = [] + + begin + strategy_object.calculate + @issues << failed_issue if proposal.failed? + rescue Y2Storage::Error => e + handle_exception(e) end + + @issues.concat(strategy_object.issues) + @on_calculate_callbacks.each(&:call) + success? end - # Instantiates and executes a Y2Storage proposal with the given settings - # - # @param settings [Y2Storage::ProposalSettings] - # @return [Y2Storage::GuidedProposal] - def calculate_proposal(settings) - proposal = Y2Storage::MinGuidedProposal.new( - settings: ProposalSettingsConversion.to_y2storage(settings, config: config), - devicegraph: probed_devicegraph, - disk_analyzer: disk_analyzer - ) - proposal.propose - storage_manager.proposal = proposal + # @return [Y2Storage::Proposal::Base, nil] + def proposal + storage_manager.proposal end # @return [Y2Storage::DiskAnalyzer, nil] nil if the system is not probed yet. def disk_analyzer - return nil unless storage_manager.probed? + return unless storage_manager.probed? storage_manager.probed_disk_analyzer end - # Devicegraph representing the system - # - # @return [Y2Storage::Devicegraph, nil] nil if the system is not probed yet. - def probed_devicegraph - return nil unless storage_manager.probed? - - storage_manager.probed - end - + # @return [Y2Storage::StorageManager] def storage_manager Y2Storage::StorageManager.instance end - # Returns an issue if there is no target device. - # - # @return [Issue, nil] - def target_device_issue - return unless missing_target_device?(settings) - - Issue.new(_("No device selected for installation"), - source: Issue::Source::CONFIG, - severity: Issue::Severity::ERROR) + # Handle Y2Storage exceptions + def handle_exception(error) + case error + when Y2Storage::NoDiskSpaceError + @issues << failed_issue + when Y2Storage::Error + @issues << exception_issue(error) + else + raise error + end end - # Returns an issue if any of the devices required for the proposal is not found + # Issue representing the proposal is not valid. # - # @return [Issue, nil] - def missing_devices_issue - available = available_devices.map(&:name) - missing = settings.installation_devices.reject { |d| available.include?(d) } - - return if missing.none? - + # @return [Issue] + def failed_issue Issue.new( - format( - n_( - "The following selected device is not found in the system: %{devices}", - "The following selected devices are not found in the system: %{devices}", - missing.size - ), - devices: missing.join(", ") - ), + _("Cannot accommodate the required file systems for installation"), source: Issue::Source::CONFIG, severity: Issue::Severity::ERROR ) end - # Returns an issue if the proposal is not valid. + # Issue to communicate a generic Y2Storage error. # - # @return [Issue, nil] - def proposal_issue - return if success? - - Issue.new(_("Cannot accommodate the required file systems for installation"), + # @return [Issue] + def exception_issue(error) + Issue.new( + _("A problem ocurred while calculating the storage setup"), + details: error.message, source: Issue::Source::CONFIG, - severity: Issue::Severity::ERROR) + severity: Issue::Severity::ERROR + ) end end end diff --git a/service/lib/agama/storage/proposal_strategies.rb b/service/lib/agama/storage/proposal_strategies.rb new file mode 100644 index 0000000000..c8efb53c08 --- /dev/null +++ b/service/lib/agama/storage/proposal_strategies.rb @@ -0,0 +1,31 @@ +# 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. + +module Agama + module Storage + # Namespace for the different strategies potentially used by Storage::Proposal. + module ProposalStrategies + end + end +end + +require "agama/storage/proposal_strategies/guided" +require "agama/storage/proposal_strategies/autoyast" diff --git a/service/lib/agama/storage/proposal_strategies/autoyast.rb b/service/lib/agama/storage/proposal_strategies/autoyast.rb new file mode 100644 index 0000000000..832e1b1eb2 --- /dev/null +++ b/service/lib/agama/storage/proposal_strategies/autoyast.rb @@ -0,0 +1,95 @@ +# 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/proposal_strategies/base" +require "agama/storage/proposal_settings" +require "agama/storage/proposal_settings_reader" +require "agama/storage/proposal_settings_conversion" + +module Agama + module Storage + module ProposalStrategies + # Strategy used by the Agama proposal for backwards compatibility with AutoYaST. + class Autoyast < Base + include Yast::I18n + + # @param config [Config] Agama config + # @param logger [Logger] + # @param partitioning [Array] + def initialize(config, logger, partitioning) + textdomain "agama" + + super(config, logger) + @partitioning = partitioning + end + + # Settings used for calculating the proposal. + # + # @return [Array] + attr_reader :partitioning + alias_method :settings, :partitioning + + # @see Base#calculate + def calculate + @ay_issues = ::Installation::AutoinstIssues::List.new + proposal = Y2Storage::AutoinstProposal.new( + partitioning: partitioning, + proposal_settings: proposal_settings, + devicegraph: probed_devicegraph, + disk_analyzer: disk_analyzer, + issues_list: ay_issues + ) + proposal.propose + ensure + storage_manager.proposal = proposal + end + + # @see Base#issues + def issues + ay_issues.map { |i| agama_issue(i) } + end + + private + + # Issues generated by the AutoYaST proposal + # @return [::Installation::AutoinstIssues::List] + attr_reader :ay_issues + + # Default proposal settings, potentially used to calculate omitted information + # + # @return [Y2Storage::ProposalSettings] + def proposal_settings + agama_default = ProposalSettingsReader.new(config).read + ProposalSettingsConversion.to_y2storage(agama_default, config: config) + end + + # Agama issue equivalent to the given AutoYaST issue + def agama_issue(ay_issue) + Issue.new( + ay_issue.message, + source: Issue::Source::CONFIG, + severity: ay_issue.warn? ? Issue::Severity::WARN : Issue::Severity::ERROR + ) + end + end + end + end +end diff --git a/service/lib/agama/storage/proposal_strategies/base.rb b/service/lib/agama/storage/proposal_strategies/base.rb new file mode 100644 index 0000000000..51f15ba528 --- /dev/null +++ b/service/lib/agama/storage/proposal_strategies/base.rb @@ -0,0 +1,103 @@ +# 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/issue" +require "yast" +require "y2storage" + +module Agama + module Storage + module ProposalStrategies + # Base class for the strategies used by the Agama proposal. + class Base + # @param config [Config] Agama config + # @param logger [Logger] + def initialize(config, logger) + @config = config + @logger = logger + end + + # Settings used for calculating the proposal. + def settings + raise NotImplementedError + end + + # Calculates a new proposal, storing the result at the storage manager. + # + # @raise [Y2Storage::NoDiskSpaceError] if it was not possible to calculate the proposal + # @raise [Y2Storage::Error] if something went wrong while calculating the proposal + def calculate + raise NotImplementedError + end + + # Identifier for the strategy. + # + # @return [Symbol] + def id + self.class.name.split("::").last.downcase.to_sym + end + + # List of issues. + # + # @return [Array] + def issues + [] + end + + private + + # @return [Config] + attr_reader :config + + # @return [Logger] + attr_reader :logger + + # @return [Y2Storage::DiskAnalyzer, nil] nil if the system is not probed yet. + def disk_analyzer + return nil unless storage_manager.probed? + + storage_manager.probed_disk_analyzer + end + + # Available devices for installation. + # + # @return [Array] + def available_devices + disk_analyzer&.candidate_disks || [] + end + + # Devicegraph representing the system + # + # @return [Y2Storage::Devicegraph, nil] nil if the system is not probed yet. + def probed_devicegraph + return nil unless storage_manager.probed? + + storage_manager.probed + end + + # @return [Y2Storage::StorageManager] + def storage_manager + Y2Storage::StorageManager.instance + end + end + end + end +end diff --git a/service/lib/agama/storage/proposal_strategies/guided.rb b/service/lib/agama/storage/proposal_strategies/guided.rb new file mode 100644 index 0000000000..105b86d46d --- /dev/null +++ b/service/lib/agama/storage/proposal_strategies/guided.rb @@ -0,0 +1,152 @@ +# 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/proposal_strategies/base" +require "agama/storage/device_settings" +require "agama/storage/proposal_settings_conversion" + +module Agama + module Storage + module ProposalStrategies + # Main strategy for the Agama proposal. + class Guided < Base + include Yast::I18n + + # @param config [Config] Agama config + # @param logger [Logger] + # @param input_settings [ProposalSettings] + def initialize(config, logger, input_settings) + textdomain "agama" + + super(config, logger) + @input_settings = input_settings + end + + # Settings used for calculating the proposal. + # + # @note Some values are recoverd from Y2Storage, see + # {ProposalSettingsConversion::FromY2Storage} + # + # @return [ProposalSettings] + attr_reader :settings + + # @see Base#calculate + def calculate + select_target_device(input_settings) if missing_target_device?(input_settings) + proposal = guided_proposal(input_settings) + proposal.propose + ensure + storage_manager.proposal = proposal + @settings = ProposalSettingsConversion.from_y2storage(proposal.settings, input_settings) + end + + # @see Base#issues + def issues + return [] unless storage_manager.proposal.failed? + + [target_device_issue, missing_devices_issue].compact + end + + private + + # Initial set of proposal settings + # @return [ProposalSettings] + attr_reader :input_settings + + # Selects the first available device as target device for installation. + # + # @param settings [ProposalSettings] + def select_target_device(settings) + device = available_devices.first&.name + return unless device + + case settings.device + when DeviceSettings::Disk + settings.device.name = device + when DeviceSettings::NewLvmVg + settings.device.candidate_pv_devices = [device] + when DeviceSettings::ReusedLvmVg + # TODO: select an existing VG? + end + end + + # Whether the given settings has no target device for the installation. + # + # @param settings [ProposalSettings] + # @return [Boolean] + def missing_target_device?(settings) + case settings.device + when DeviceSettings::Disk, DeviceSettings::ReusedLvmVg + settings.device.name.nil? + when DeviceSettings::NewLvmVg + settings.device.candidate_pv_devices.empty? + end + end + + # Instance of the Y2Storage proposal to be used to run the calculation. + # + # @param settings [Y2Storage::ProposalSettings] + # @return [Y2Storage::GuidedProposal] + def guided_proposal(settings) + Y2Storage::MinGuidedProposal.new( + settings: ProposalSettingsConversion.to_y2storage(settings, config: config), + devicegraph: probed_devicegraph, + disk_analyzer: disk_analyzer + ) + end + + # Returns an issue if there is no target device. + # + # @return [Issue, nil] + def target_device_issue + return unless missing_target_device?(settings) + + Issue.new(_("No device selected for installation"), + source: Issue::Source::CONFIG, + severity: Issue::Severity::ERROR) + end + + # Returns an issue if any of the devices required for the proposal is not found + # + # @return [Issue, nil] + def missing_devices_issue + available = available_devices.map(&:name) + missing = settings.installation_devices.reject { |d| available.include?(d) } + + return if missing.none? + + Issue.new( + format( + n_( + "The following selected device is not found in the system: %{devices}", + "The following selected devices are not found in the system: %{devices}", + missing.size + ), + devices: missing.join(", ") + ), + source: Issue::Source::CONFIG, + severity: Issue::Severity::ERROR + ) + end + end + end + end +end diff --git a/service/test/agama/dbus/storage/manager_test.rb b/service/test/agama/dbus/storage/manager_test.rb index 7470900614..87232073ed 100644 --- a/service/test/agama/dbus/storage/manager_test.rb +++ b/service/test/agama/dbus/storage/manager_test.rb @@ -316,7 +316,7 @@ end end - describe "#calculate_proposal" do + describe "#calculate_guided_proposal" do let(:dbus_settings) do { "Target" => "disk", @@ -335,7 +335,7 @@ end it "calculates a proposal with settings having values from D-Bus" do - expect(proposal).to receive(:calculate) do |settings| + expect(proposal).to receive(:calculate_guided) do |settings| expect(settings).to be_a(Agama::Storage::ProposalSettings) expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) expect(settings.device.name).to eq "/dev/vda" @@ -348,14 +348,14 @@ ) end - subject.calculate_proposal(dbus_settings) + subject.calculate_guided_proposal(dbus_settings) end context "when the D-Bus settings does not include some values" do let(:dbus_settings) { {} } it "calculates a proposal with default values for the missing settings" do - expect(proposal).to receive(:calculate) do |settings| + expect(proposal).to receive(:calculate_guided) do |settings| expect(settings).to be_a(Agama::Storage::ProposalSettings) expect(settings.device).to be_a(Agama::Storage::DeviceSettings::Disk) expect(settings.device.name).to be_nil @@ -365,7 +365,7 @@ expect(settings.volumes).to eq([]) end - subject.calculate_proposal(dbus_settings) + subject.calculate_guided_proposal(dbus_settings) end end @@ -374,11 +374,11 @@ # This is likely a temporary behavior it "calculates a proposal ignoring the unknown attributes" do - expect(proposal).to receive(:calculate) do |settings| + expect(proposal).to receive(:calculate_guided) do |settings| expect(settings).to be_a(Agama::Storage::ProposalSettings) end - subject.calculate_proposal(dbus_settings) + subject.calculate_guided_proposal(dbus_settings) end end @@ -412,7 +412,7 @@ end it "calculates a proposal with settings having a volume with values from D-Bus" do - expect(proposal).to receive(:calculate) do |settings| + expect(proposal).to receive(:calculate_guided) do |settings| volume = settings.volumes.first expect(volume.mount_path).to eq("/") @@ -422,7 +422,7 @@ expect(volume.btrfs.snapshots).to eq(true) end - subject.calculate_proposal(dbus_settings) + subject.calculate_guided_proposal(dbus_settings) end context "and the D-Bus volume does not include some values" do @@ -441,7 +441,7 @@ end it "calculates a proposal with a volume completed with its default settings" do - expect(proposal).to receive(:calculate) do |settings| + expect(proposal).to receive(:calculate_guided) do |settings| volume = settings.volumes.first expect(volume.mount_path).to eq("/") @@ -452,7 +452,7 @@ expect(volume.btrfs.snapshots).to eq(false) end - subject.calculate_proposal(dbus_settings) + subject.calculate_guided_proposal(dbus_settings) end end end diff --git a/service/test/agama/storage/autoyast_proposal_test.rb b/service/test/agama/storage/autoyast_proposal_test.rb new file mode 100644 index 0000000000..e68a3e10f0 --- /dev/null +++ b/service/test/agama/storage/autoyast_proposal_test.rb @@ -0,0 +1,546 @@ +# 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_relative "../../test_helper" +require_relative "storage_helpers" +require "agama/config" +require "agama/storage/proposal" +require "agama/issue" +require "y2storage" + +describe Agama::Storage::Proposal do + include Agama::RSpec::StorageHelpers + using Y2Storage::Refinements::SizeCasts + + subject(:proposal) { described_class.new(config, logger: logger) } + + let(:logger) { Logger.new($stdout, level: :warn) } + + before do + mock_storage(devicegraph: scenario) + allow(Y2Storage::StorageManager.instance).to receive(:arch).and_return(arch) + end + + let(:scenario) { "windows-linux-pc.yml" } + + let(:arch) do + instance_double(Y2Storage::Arch, efiboot?: true, ram_size: 4.GiB.to_i) + end + + let(:config_path) do + File.join(FIXTURES_PATH, "storage.yaml") + end + let(:config) { Agama::Config.from_file(config_path) } + + ROOT_PART = { + "filesystem" => :ext4, "mount" => "/", "size" => "25%", "label" => "new_root" + }.freeze + + let(:root) { ROOT_PART.merge("create" => true) } + let(:home) do + { "filesystem" => :xfs, "mount" => "/home", "size" => "50%", "create" => true } + end + + describe "#success?" do + it "returns false if no calculate has been called yet" do + expect(subject.success?).to eq(false) + end + + context "if calculate_autoyast was already called" do + let(:partitioning) do + [{ "device" => "/dev/#{disk}", "use" => "free", "partitions" => [root] }] + end + + before do + subject.calculate_autoyast(partitioning) + end + + context "and the proposal was successful" do + let(:disk) { "sdb" } + + it "returns true" do + expect(subject.success?).to eq(true) + end + end + + context "and the proposal failed" do + let(:disk) { "sda" } + + it "returns false" do + expect(subject.success?).to eq(false) + end + end + end + end + + describe "#calculate_autoyast" do + def staging + Y2Storage::StorageManager.instance.proposal.devices + end + + def root_filesystem(disk) + disk.partitions.map(&:filesystem).compact.find(&:root?) + end + + describe "when partitions are specified" do + context "if the requested layout is valid" do + let(:partitioning) do + [{ "device" => "/dev/sda", "use" => "all", "partitions" => [root, home] }] + end + + it "returns true and stores a successful proposal" do + expect(subject.calculate_autoyast(partitioning)).to eq true + expect(Y2Storage::StorageManager.instance.proposal.failed?).to eq false + end + + it "proposes a layout including specified partitions" do + subject.calculate_autoyast(partitioning) + + sda_partitions = staging.find_by_name("/dev/sda").partitions.sort_by(&:number) + expect(sda_partitions.size).to eq(3) + efi, root, home = sda_partitions + + expect(efi).to have_attributes( + filesystem_type: Y2Storage::Filesystems::Type::VFAT, + filesystem_mountpoint: "/boot/efi", + size: 512.MiB + ) + + expect(root).to have_attributes( + filesystem_type: Y2Storage::Filesystems::Type::EXT4, + filesystem_mountpoint: "/", + size: 125.GiB + ) + + expect(home).to have_attributes( + filesystem_type: Y2Storage::Filesystems::Type::XFS, + filesystem_mountpoint: "/home", + size: 250.GiB + ) + end + + it "registers no issues" do + subject.calculate_autoyast(partitioning) + expect(subject.issues).to be_empty + end + + it "runs all the callbacks" do + callback1 = proc {} + callback2 = proc {} + + subject.on_calculate(&callback1) + subject.on_calculate(&callback2) + + expect(callback1).to receive(:call) + expect(callback2).to receive(:call) + + subject.calculate_autoyast(partitioning) + end + end + + context "if no root is specified" do + let(:partitioning) do + [{ "device" => "/dev/sda", "use" => "all", "partitions" => [home] }] + end + + it "returns false and stores the resulting proposal" do + expect(subject.calculate_autoyast(partitioning)).to eq false + # From the AutoinstProposal POV, a proposal without root is not an error + # if root was not requested + expect(Y2Storage::StorageManager.instance.proposal.failed?).to eq false + end + + it "proposes a layout including only the specified partitions" do + subject.calculate_autoyast(partitioning) + + sda_partitions = staging.find_by_name("/dev/sda").partitions + # Only /boot/efi and /home, no root filesystem + expect(sda_partitions.size).to eq(2) + end + + it "registers a fatal issue due to the lack of root" do + subject.calculate_autoyast(partitioning) + expect(subject.issues).to include( + an_object_having_attributes( + description: /No root/, severity: Agama::Issue::Severity::ERROR + ) + ) + end + + it "runs all the callbacks" do + callback1 = proc {} + callback2 = proc {} + + subject.on_calculate(&callback1) + subject.on_calculate(&callback2) + + expect(callback1).to receive(:call) + expect(callback2).to receive(:call) + + subject.calculate_autoyast(partitioning) + end + end + end + + context "when the profile does not specify what to do with existing partitions" do + let(:partitioning) do + [{ "device" => "/dev/sda", "partitions" => [root] }] + end + + it "returns false and stores a failed proposal" do + expect(subject.calculate_autoyast(partitioning)).to eq false + expect(Y2Storage::StorageManager.instance.proposal.failed?).to eq true + end + + it "register a fatal issue about the missing 'use' element" do + subject.calculate_autoyast(partitioning) + expect(subject.issues).to include( + an_object_having_attributes( + description: /Missing element 'use'/, severity: Agama::Issue::Severity::ERROR + ) + ) + end + end + + describe "when existing partitions should be kept" do + let(:partitioning) do + [{ "device" => "/dev/#{disk}", "use" => "free", "partitions" => [root] }] + end + + context "if the requested partitions fit into the available space" do + let(:disk) { "sdb" } + + it "proposes a layout including previous and new partitions" do + subject.calculate_autoyast(partitioning) + sdb_partitions = staging.find_by_name("/dev/sdb").partitions + expect(sdb_partitions.size).to eq 3 + end + end + + context "if there is no available space" do + let(:disk) { "sda" } + + it "returns false and stores a failed proposal" do + expect(subject.calculate_autoyast(partitioning)).to eq false + expect(Y2Storage::StorageManager.instance.proposal.failed?).to eq true + end + + it "register issues" do + subject.calculate_autoyast(partitioning) + expect(subject.issues).to include( + an_object_having_attributes( + description: /Cannot accommodate/, severity: Agama::Issue::Severity::ERROR + ) + ) + end + end + end + + context "when only space from Linux partitions should be used" do + let(:partitioning) do + [{ "device" => "/dev/sda", "use" => "linux", "partitions" => [root] }] + end + + it "keeps all partitions except Linux ones" do + subject.calculate_autoyast(partitioning) + partitions = staging.find_by_name("/dev/sda").partitions + expect(partitions.map(&:filesystem_label)).to contain_exactly("windows", "", "new_root") + end + end + + describe "reusing partitions" do + let(:partitioning) do + [{ "device" => "/dev/sdb", "use" => "free", "partitions" => [root] }] + end + + let(:root) do + { "mount" => "/", "partition_nr" => 1, "create" => false } + end + + it "returns true and registers no issues" do + expect(subject.calculate_autoyast(partitioning)).to eq true + expect(subject.issues).to be_empty + end + + it "reuses the indicated partition" do + subject.calculate_autoyast(partitioning) + root, efi = staging.find_by_name("/dev/sdb").partitions.sort_by(&:number) + expect(root).to have_attributes( + filesystem_type: Y2Storage::Filesystems::Type::XFS, + filesystem_mountpoint: "/", + size: 113.GiB + ) + expect(efi).to have_attributes( + filesystem_type: Y2Storage::Filesystems::Type::VFAT, + filesystem_mountpoint: "/boot/efi", + id: Y2Storage::PartitionId::ESP, + size: 512.MiB + ) + end + + context "if the partitions needed for booting do not fit" do + let(:partitioning) do + [{ "device" => "/dev/sda", "use" => "free", "partitions" => [root] }] + end + + let(:root) do + { "mount" => "/", "partition_nr" => 3, "create" => false } + end + + it "returns true and stores a successful proposal" do + expect(subject.calculate_autoyast(partitioning)).to eq true + expect(Y2Storage::StorageManager.instance.proposal.failed?).to eq false + end + + it "does not create the boot partitions" do + subject.calculate_autoyast(partitioning) + partitions = staging.find_by_name("/dev/sda").partitions + expect(partitions.size).to eq 3 + expect(partitions.map(&:id)).to_not include Y2Storage::PartitionId::ESP + end + + it "register a non-fatal issue" do + subject.calculate_autoyast(partitioning) + expect(subject.issues).to include( + an_object_having_attributes( + description: /partitions recommended for booting/, + severity: Agama::Issue::Severity::WARN + ) + ) + end + end + end + + describe "skipping disks" do + let(:skip_list) do + [{ "skip_key" => "name", "skip_value" => skip_device }] + end + + let(:partitioning) do + [{ "use" => "all", "partitions" => [root, home], "skip_list" => skip_list }] + end + + context "when no disk is included in the skip_list" do + let(:skip_device) { "sdc" } + + it "does not skip any disk" do + subject.calculate_autoyast(partitioning) + sda = staging.find_by_name("/dev/sda") + expect(root_filesystem(sda)).to_not be_nil + end + end + + context "when a disk is included in the skip_list" do + let(:skip_device) { "sda" } + + it "skips the given disk" do + subject.calculate_autoyast(partitioning) + sda = staging.find_by_name("/dev/sda") + sdb = staging.find_by_name("/dev/sdb") + expect(root_filesystem(sda)).to be_nil + expect(root_filesystem(sdb)).to_not be_nil + end + end + + context "when all disks are skipped" do + let(:skip_list) do + [{ "skip_key" => "name", "skip_value" => "sda" }, + { "skip_key" => "name", "skip_value" => "sdb" }] + end + + it "returns false and stores a failed proposal" do + expect(subject.calculate_autoyast(partitioning)).to eq false + expect(Y2Storage::StorageManager.instance.proposal.failed?).to eq true + end + + it "register issues" do + subject.calculate_autoyast(partitioning) + expect(subject.issues).to include( + an_object_having_attributes( + description: /Cannot accommodate/, severity: Agama::Issue::Severity::ERROR + ) + ) + end + end + end + + describe "LVM on RAID" do + let(:partitioning) do + [ + { "device" => "/dev/sda", "use" => "all", "partitions" => [raid_spec] }, + { "device" => "/dev/sdb", "use" => "all", "partitions" => [raid_spec] }, + { "device" => "/dev/md", "partitions" => [md_spec] }, + { "device" => "/dev/vg0", "partitions" => [root_spec, home_spec], "type" => :CT_LVM } + ] + end + + let(:md_spec) do + { + "partition_nr" => 1, "raid_options" => raid_options, "lvm_group" => "vg0" + } + end + + let(:raid_options) do + { "raid_type" => "raid0" } + end + + let(:root_spec) do + { "mount" => "/", "filesystem" => :ext4, "lv_name" => "root", "size" => "5G" } + end + + let(:home_spec) do + { "mount" => "/home", "filesystem" => :xfs, "lv_name" => "home", "size" => "5G" } + end + + let(:raid_spec) do + { "raid_name" => "/dev/md1", "size" => "20GB", "partition_id" => 253 } + end + + it "returns true and registers no issues" do + expect(subject.calculate_autoyast(partitioning)).to eq true + expect(subject.issues).to be_empty + end + + it "creates the expected layout" do + subject.calculate_autoyast(partitioning) + expect(staging.md_raids).to contain_exactly( + an_object_having_attributes( + "number" => 1, + "md_level" => Y2Storage::MdLevel::RAID0 + ) + ) + raid = staging.md_raids.first + expect(raid.lvm_pv.lvm_vg.vg_name).to eq "vg0" + expect(staging.lvm_vgs).to contain_exactly( + an_object_having_attributes("vg_name" => "vg0") + ) + vg = staging.lvm_vgs.first.lvm_pvs.first + expect(vg.blk_device).to be_a(Y2Storage::Md) + expect(staging.lvm_lvs).to contain_exactly( + an_object_having_attributes("lv_name" => "root", "filesystem_mountpoint" => "/"), + an_object_having_attributes("lv_name" => "home", "filesystem_mountpoint" => "/home") + ) + end + end + + describe "using 'auto' for the size of some partitions" do + let(:partitioning) do + [{ "device" => "/dev/sda", "use" => "all", "partitions" => [root, swap] }] + end + let(:swap) do + { "filesystem" => :swap, "mount" => "swap", "size" => "auto" } + end + + it "returns true and stores a successful proposal" do + expect(subject.calculate_autoyast(partitioning)).to eq true + expect(Y2Storage::StorageManager.instance.proposal.failed?).to eq false + end + + # To prevent this fallback, we would need either to: + # - Fix AutoinstProposal to honor the passed ProposalSettings everywhere + # - Mock ProposalSettings.new_for_current_product to return settings obtained from Agama + it "fallbacks to legacy settings hardcoded at YaST" do + subject.calculate_autoyast(partitioning) + partitions = staging.find_by_name("/dev/sda").partitions.sort_by(&:number) + expect(partitions.size).to eq(3) + expect(partitions[0].id.is?(:esp)).to eq(true) + expect(partitions[1].filesystem.root?).to eq(true) + expect(partitions[2].filesystem.mount_path).to eq("swap") + expect(partitions[2].size).to eq 1.GiB + end + end + + describe "automatic partitioning" do + let(:partitioning) do + [ + { + "device" => "/dev/sda", + "use" => use, + "enable_snapshots" => snapshots + } + ] + end + + let(:use) { "all" } + let(:snapshots) { true } + + it "falls back to the initial guided proposal with the given disk" do + subject.calculate_autoyast(partitioning) + + partitions = staging.find_by_name("/dev/sda").partitions.sort_by(&:number) + expect(partitions.size).to eq(3) + expect(partitions[0].id.is?(:esp)).to eq(true) + expect(partitions[1].filesystem.root?).to eq(true) + expect(partitions[2].filesystem.mount_path).to eq("swap") + end + + context "when a subset of partitions should be used" do + let(:use) { "1" } + + # Since we use :bigger_resize, there is no compatibility with "use" + it "keeps partitions that should not be removed" do + subject.calculate_autoyast(partitioning) + + partitions = staging.find_by_name("/dev/sda").partitions + partitions.reject! { |p| p.type.is?(:extended) } + expect(partitions.size).to eq(5) + end + end + + context "when snapshots are enabled in the AutoYaST profile" do + let(:snapshots) { true } + + it "configures snapshots for root" do + subject.calculate_autoyast(partitioning) + + sda = staging.find_by_name("/dev/sda") + root = root_filesystem(sda) + expect(root.snapshots?).to eq(true) + end + end + + context "when snapshots are disabled in the AutoYaST profile" do + let(:snapshots) { false } + + it "does not configure snapshots for root" do + subject.calculate_autoyast(partitioning) + + sda = staging.find_by_name("/dev/sda") + root = root_filesystem(sda) + expect(root.snapshots?).to eq(false) + end + end + + it "runs all the callbacks" do + callback1 = proc {} + callback2 = proc {} + + subject.on_calculate(&callback1) + subject.on_calculate(&callback2) + + expect(callback1).to receive(:call) + expect(callback2).to receive(:call) + + subject.calculate_autoyast(partitioning) + end + end + end +end diff --git a/service/test/agama/storage/manager_test.rb b/service/test/agama/storage/manager_test.rb index 52025d1ed3..3a8299c994 100644 --- a/service/test/agama/storage/manager_test.rb +++ b/service/test/agama/storage/manager_test.rb @@ -150,7 +150,7 @@ allow(proposal).to receive(:issues).and_return(proposal_issues) allow(proposal).to receive(:available_devices).and_return(devices) allow(proposal).to receive(:settings).and_return(settings) - allow(proposal).to receive(:calculate) + allow(proposal).to receive(:calculate_guided) allow(config).to receive(:pick_product) allow(iscsi).to receive(:activate) @@ -192,7 +192,7 @@ end expect(iscsi).to receive(:probe) expect(y2storage_manager).to receive(:probe) - expect(proposal).to receive(:calculate).with(config_settings) + expect(proposal).to receive(:calculate_guided).with(config_settings) storage.probe end @@ -377,7 +377,7 @@ context "if a proposal was successfully calculated" do before do - subject.proposal.calculate(settings) + subject.proposal.calculate_guided(settings) end let(:settings) do diff --git a/service/test/agama/storage/proposal_test.rb b/service/test/agama/storage/proposal_test.rb index 87eb6f166b..2dbc51ae5a 100644 --- a/service/test/agama/storage/proposal_test.rb +++ b/service/test/agama/storage/proposal_test.rb @@ -57,13 +57,13 @@ end describe "#success?" do - it "returns false if calculate has not been called yet" do + it "returns false if no calculate has been called yet" do expect(subject.success?).to eq(false) end - context "if calculate was already called" do + context "if calculate_guided was already called" do before do - subject.calculate(settings) + subject.calculate_guided(settings) end context "and the proposal was successful" do @@ -84,11 +84,11 @@ end end - describe "#calculate" do + describe "#calculate_guided" do it "calculates a new proposal with the given settings" do expect(Y2Storage::StorageManager.instance.proposal).to be_nil - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) expect(Y2Storage::StorageManager.instance.proposal).to_not be_nil y2storage_settings = Y2Storage::StorageManager.instance.proposal.settings @@ -108,12 +108,12 @@ expect(callback1).to receive(:call) expect(callback2).to receive(:call) - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) end it "returns whether the proposal was successful" do - expect(subject.calculate(achievable_settings)).to eq(true) - expect(subject.calculate(impossible_settings)).to eq(false) + expect(subject.calculate_guided(achievable_settings)).to eq(true) + expect(subject.calculate_guided(impossible_settings)).to eq(false) end context "if the given device settings sets a disk as target" do @@ -127,7 +127,7 @@ end it "sets the first available device as target device for volumes" do - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) y2storage_settings = Y2Storage::StorageManager.instance.proposal.settings expect(y2storage_settings.volumes).to contain_exactly( @@ -148,7 +148,7 @@ end it "sets the first available device as candidate device" do - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) y2storage_settings = Y2Storage::StorageManager.instance.proposal.settings expect(y2storage_settings.candidate_devices).to contain_exactly("/dev/sda") @@ -162,7 +162,7 @@ end it "does not calculate a proposal" do - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) expect(Y2Storage::StorageManager.instance.proposal).to be_nil end @@ -176,11 +176,11 @@ expect(callback1).to_not receive(:call) expect(callback2).to_not receive(:call) - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) end it "returns false" do - expect(subject.calculate(achievable_settings)).to eq(false) + expect(subject.calculate_guided(achievable_settings)).to eq(false) end end end @@ -192,7 +192,7 @@ context "if the proposal was already calculated" do before do - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) end it "returns the settings used for calculating the proposal" do @@ -215,7 +215,7 @@ context "if the proposal failed" do before do - subject.calculate(impossible_settings) + subject.calculate_guided(impossible_settings) end it "returns an empty list" do @@ -225,7 +225,7 @@ context "if the proposal was successful" do before do - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) end it "returns the actions from the actiongraph" do @@ -242,7 +242,7 @@ end it "returns an empty list if the current proposal is successful" do - subject.calculate(achievable_settings) + subject.calculate_guided(achievable_settings) expect(subject.issues).to eq([]) end @@ -251,7 +251,7 @@ let(:settings) { impossible_settings } it "includes an error because the volumes cannot be accommodated" do - subject.calculate(settings) + subject.calculate_guided(settings) expect(subject.issues).to include( an_object_having_attributes(description: /Cannot accommodate/) @@ -261,13 +261,14 @@ context "and the settings does not indicate a target device" do before do # Avoid to automatically set the first device - allow(subject).to receive(:available_devices).and_return([]) + allow(Y2Storage::StorageManager.instance.probed_disk_analyzer) + .to receive(:candidate_disks).and_return([]) end let(:settings) { impossible_settings.tap { |s| s.device.name = nil } } it "includes an error because a device is not selected" do - subject.calculate(settings) + subject.calculate_guided(settings) expect(subject.issues).to include( an_object_having_attributes(description: /No device selected/) @@ -287,7 +288,7 @@ let(:settings) { impossible_settings.tap { |s| s.device.name = "/dev/vdz" } } it "includes an error because the device is not found" do - subject.calculate(settings) + subject.calculate_guided(settings) expect(subject.issues).to include( an_object_having_attributes(description: /is not found/) diff --git a/service/test/agama/storage/proposal_volumes_test.rb b/service/test/agama/storage/proposal_volumes_test.rb index d6f3867f6a..fa055227b6 100644 --- a/service/test/agama/storage/proposal_volumes_test.rb +++ b/service/test/agama/storage/proposal_volumes_test.rb @@ -149,13 +149,13 @@ def expect_proposal_with_specs(*specs) { mount_point: "swap", proposed: false }, { mount_point: "/two", proposed: false, fallback_for_min_size: "/" } ) - proposal.calculate(settings) + proposal.calculate_guided(settings) end end describe "#settings" do it "returns settings with a set of volumes with adjusted sizes" do - proposal.calculate(settings) + proposal.calculate_guided(settings) expect(proposal.settings.volumes).to contain_exactly( an_object_having_attributes( @@ -190,13 +190,13 @@ def expect_proposal_with_specs(*specs) { mount_point: "swap", proposed: false }, { mount_point: "/two", proposed: true, fallback_for_min_size: "/" } ) - proposal.calculate(settings) + proposal.calculate_guided(settings) end end describe "#settings" do it "returns settings with a set of volumes with adjusted sizes" do - proposal.calculate(settings) + proposal.calculate_guided(settings) expect(proposal.settings.volumes).to contain_exactly( an_object_having_attributes( @@ -232,13 +232,13 @@ def expect_proposal_with_specs(*specs) { mount_point: "swap", proposed: false }, { mount_point: "/two", proposed: false, fallback_for_min_size: "/" } ) - proposal.calculate(settings) + proposal.calculate_guided(settings) end end describe "#settings" do it "returns settings with a set of volumes with fixed limits and adjusted sizes" do - proposal.calculate(settings) + proposal.calculate_guided(settings) expect(proposal.settings.volumes).to contain_exactly( an_object_having_attributes( @@ -273,13 +273,13 @@ def expect_proposal_with_specs(*specs) min_size: Y2Storage::DiskSize.GiB(1) } ) - proposal.calculate(settings) + proposal.calculate_guided(settings) end end describe "#settings" do it "returns settings with a set of volumes with adjusted sizes" do - proposal.calculate(settings) + proposal.calculate_guided(settings) expect(proposal.settings.volumes).to contain_exactly( an_object_having_attributes(mount_path: "/", auto_size: true), @@ -322,13 +322,13 @@ def expect_proposal_with_specs(*specs) }, { mount_point: "/two", proposed: false, fallback_for_min_size: "/" } ) - proposal.calculate(settings) + proposal.calculate_guided(settings) end end describe "#settings" do it "returns settings with a set of volumes with fixed limits and adjusted sizes" do - proposal.calculate(settings) + proposal.calculate_guided(settings) expect(proposal.settings.volumes).to contain_exactly( an_object_having_attributes( diff --git a/service/test/fixtures/storage.yaml b/service/test/fixtures/storage.yaml new file mode 100644 index 0000000000..16f5a3a5c1 --- /dev/null +++ b/service/test/fixtures/storage.yaml @@ -0,0 +1,40 @@ +storage: + space_policy: delete + volumes: + - "/" + - "swap" + volume_templates: + - mount_path: "/" + filesystem: btrfs + btrfs: + snapshots: true + read_only: false + default_subvolume: "@" + subvolumes: + - path: home + - path: opt + - path: root + size: + auto: true + outline: + required: true + filesystems: + - btrfs + - ext4 + - xfs + auto_size: + base_min: 5 GiB + snapshots_increment: 250% + snapshots_configurable: true + - mount_path: "swap" + filesystem: swap + size: + auto: true + outline: + auto_size: + base_min: 1 GiB + base_max: 2 GiB + adjust_by_ram: true + required: false + filesystems: + - swap diff --git a/service/test/fixtures/windows-linux-pc.yml b/service/test/fixtures/windows-linux-pc.yml new file mode 100644 index 0000000000..f5f815a602 --- /dev/null +++ b/service/test/fixtures/windows-linux-pc.yml @@ -0,0 +1,40 @@ +--- +- disk: + name: /dev/sda + size: 500 GiB + partition_table: ms-dos + partitions: + + - partition: + size: 250 GiB + name: /dev/sda1 + id: 0x7 + file_system: ntfs + label: windows + + - partition: + size: 2 GiB + name: /dev/sda2 + id: swap + file_system: swap + label: swap + + - partition: + size: unlimited + name: /dev/sda3 + file_system: ext4 + label: root + uuid: sda3-uuid + +- disk: + name: /dev/sdb + size: 500 GiB + partition_table: gpt + partitions: + + - partition: + size: 113 GiB + name: /dev/sdb1 + file_system: xfs + label: data + uuid: sda1-uuid