# frozen_string_literal: true # Copyright (c) [2024-2026] 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 "../agama/storage/config_context" require "y2storage" require "y2storage/agama_proposal" using Y2Storage::Refinements::SizeCasts # @param config [Agama::Storage::Configs::Drive, Agama::Storage::Configs::Partition] # @param name [String, nil] e.g., "/dev/vda" # @param filesystem [String, nil] e.g., "xfs" def block_device_config(config, name: nil, filesystem: nil) if name config.search = Agama::Storage::Configs::Search.new.tap do |search_config| search_config.condition = Agama::Storage::Configs::SearchConditions::Name.new(name) end end if filesystem config.filesystem = Agama::Storage::Configs::Filesystem.new.tap do |fs_config| fs_config.type = Agama::Storage::Configs::FilesystemType.new.tap do |type_config| type_config.fs_type = Y2Storage::Filesystems::Type.find(filesystem) end end end config end # @param name [String, nil] e.g., "/dev/vda" # @param filesystem [String, nil] e.g., "xfs" def drive_config(name: nil, filesystem: nil) config = Agama::Storage::Configs::Drive.new block_device_config(config, name: name, filesystem: filesystem) end # @param name [String, nil] e.g., "/dev/vda" # @param filesystem [String, nil] e.g., "xfs" # @param size [Y2Storage::DiskSize] def partition_config(name: nil, filesystem: nil, size: nil) config = Agama::Storage::Configs::Partition.new block_device_config(config, name: name, filesystem: filesystem) config.size = Agama::Storage::Configs::Size.new.tap do |size_config| size_config.min = size || Y2Storage::DiskSize.zero size_config.max = size || Y2Storage::DiskSize.unlimited end config end describe Y2Storage::AgamaProposal do include_context "config" subject(:proposal) do described_class.new( config, storage_system, product_config: product_config, bootloader_config: bootloader_config, issues_list: issues_list ) end let(:default_config) do Agama::Storage::Config.new.tap do |settings| settings.drives = drives end end let(:product_data) do { "storage" => { "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" => false, "snapshots_configurable" => true, "filesystems" => ["btrfs", "xfs", "ext3", "ext4"], "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, "filesystems" => ["xfs", "ext4"] } }, { "mount_path" => "swap", "filesystem" => "swap", "outline" => { "required" => false, "filesystems" => ["swap"] } }, { "mount_path" => "", "filesystem" => "ext4", "size" => { "min" => "100 MiB" }, "outline" => { "filesystems" => ["xfs", "ext4"] } } ] } } end let(:issues_list) { [] } let(:drives) { [drive0] } let(:drive0) { Agama::Storage::Configs::Drive.new.tap { |d| d.partitions = partitions0 } } let(:partitions0) { [root_partition] } let(:root_partition) do Agama::Storage::Configs::Partition.new.tap do |part| part.filesystem = Agama::Storage::Configs::Filesystem.new.tap do |fs| fs.path = "/" fs.type = Agama::Storage::Configs::FilesystemType.new.tap do |type| type.fs_type = Y2Storage::Filesystems::Type::BTRFS 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 end end let(:home_partition) do Agama::Storage::Configs::Partition.new.tap do |part| part.filesystem = Agama::Storage::Configs::Filesystem.new.tap do |fs| fs.path = "/home" fs.type = Agama::Storage::Configs::FilesystemType.new.tap do |type| type.fs_type = Y2Storage::Filesystems::Type::EXT4 end end part.size = Agama::Storage::Configs::Size.new.tap do |size| size.min = 10.GiB size.max = Y2Storage::DiskSize.unlimited end end end let(:scenario) { "empty-hd-50GiB.yaml" } describe "#propose" do context "when only the root partition is specified" do let(:config) { default_config } context "if no configuration about boot devices is specified" do it "proposes to create the root device and the boot-related partition" do proposal.propose partitions = proposal.devices.partitions expect(partitions.size).to eq 2 expect(partitions.first.id).to eq Y2Storage::PartitionId::BIOS_BOOT root_part = partitions.last expect(root_part.size).to be > 49.GiB root_fs = root_part.filesystem expect(root_fs.root?).to eq true expect(root_fs.type.is?(:btrfs)).to eq true end end context "if no boot devices should be created" do before do config.boot = Agama::Storage::Configs::Boot.new.tap { |b| b.configure = false } end it "proposes to create only the root device" do proposal.propose partitions = proposal.devices.partitions expect(partitions.size).to eq 1 root_part = partitions.first expect(root_part.id).to eq Y2Storage::PartitionId::LINUX expect(root_part.size).to be > 49.GiB root_fs = root_part.filesystem expect(root_fs.root?).to eq true expect(root_fs.type.is?(:btrfs)).to eq true end end end context "on an EFI system" do let(:config) { default_config } before do allow_any_instance_of(Y2Storage::Arch).to receive(:efiboot?).and_return(true) allow(Yast::Arch).to receive(:x86_64).and_return(true) allow(Yast::Arch).to receive(:i386).and_return(false) allow(Yast::Arch).to receive(:aarch64).and_return(false) allow(Yast::Arch).to receive(:arm).and_return(false) allow(Yast::Arch).to receive(:riscv64).and_return(false) end context "when systemd-boot is configured as bootloader" do before do allow(bootloader_config).to receive(:type) .and_return Y2Storage::BootloaderType::SYSTEMD_BOOT end it "proposes the corresponding boot partitions" do proposal.propose efi_partition = proposal.devices.partitions.find do |part| part.filesystem&.mount_path == "/boot" end expect(efi_partition).to_not be_nil expect(efi_partition.size).to eq(1.GiB) end end context "when Grub2 is configued as bootloader" do before do allow(bootloader_config).to receive(:type).and_return Y2Storage::BootloaderType::GRUB2 end it "proposes the corresponding boot partitions" do proposal.propose efi_partition = proposal.devices.partitions.find do |part| part.filesystem&.mount_path == "/boot/efi" end expect(efi_partition).to_not be_nil expect(efi_partition.size).to eq(512.MiB) end end end context "when only 'default' partitions is specified" do let(:scenario) { "empty-hd-50GiB.yaml" } let(:config_json) do { drives: [ { partitions: [ { generate: "default" } ] } ] } end it "proposes the expected devices" do devicegraph = proposal.propose expect(devicegraph.partitions.size).to eq(3) boot = devicegraph.find_by_name("/dev/sda1") expect(boot.id).to eq(Y2Storage::PartitionId::BIOS_BOOT) expect(boot.filesystem).to be_nil expect(boot.size).to eq(8.MiB) root = devicegraph.find_by_name("/dev/sda2") expect(root.filesystem.mount_path).to eq("/") expect(root.filesystem.type.is?(:btrfs)).to eq(true) expect(root.size).to be_between(44.99.GiB, 45.GiB) swap = devicegraph.find_by_name("/dev/sda3") expect(swap.filesystem.mount_path).to eq("swap") expect(swap.filesystem.type.is?(:swap)).to eq(true) expect(swap.size).to be_between(4.99.GiB, 5.GiB) end end context "when the config has 2 drives" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:drives) { [drive0, drive1] } let(:drive1) do Agama::Storage::Configs::Drive.new.tap { |d| d.partitions = [home_partition] } end it "proposes the expected devices" do devicegraph = proposal.propose root = devicegraph.find_by_name("/dev/vda4") expect(root.filesystem.mount_path).to eq("/") home = devicegraph.find_by_name("/dev/vdb1") expect(home.filesystem.mount_path).to eq("/home") end end context "when trying to reuse a file system from a drive" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:drives) { [drive] } let(:drive) do drive_config(name: name, filesystem: "ext3").tap { |c| c.filesystem.reuse = true } end before do config.boot.configure = false end context "if the drive is already formatted" do let(:name) { "/dev/vdc" } it "reuses the file system" do vdc = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vdc") fs_sid = vdc.filesystem.sid devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vdc").filesystem expect(filesystem.sid).to eq(fs_sid) end end context "if the drive is not formatted" do let(:name) { "/dev/vdb" } it "creates the file system" do devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vdb").filesystem expect(filesystem).to_not be_nil expect(filesystem.type).to eq(Y2Storage::Filesystems::Type::EXT3) end end end context "when a partition table type is specified for a drive" do let(:config) { default_config } let(:drive0) do Agama::Storage::Configs::Drive.new.tap do |drive| drive.partitions = partitions0 drive.ptable_type = Y2Storage::PartitionTables::Type::MSDOS end end it "tries to propose a partition table of the requested type" do proposal.propose ptable = proposal.devices.disks.first.partition_table expect(ptable.type).to eq Y2Storage::PartitionTables::Type::MSDOS end it "honors the partition table type if possible when calculating the boot partitions" do proposal.propose partitions = proposal.devices.partitions expect(partitions.map(&:id)).to_not include Y2Storage::PartitionId::BIOS_BOOT end end context "when encrypting some devices" do let(:config) { default_config } let(:partitions0) { [root_partition, home_partition] } let(:home_encryption) do Agama::Storage::Configs::Encryption.new.tap do |enc| enc.password = "notSecreT" enc.method = encryption_method end end let(:encryption_method) { Y2Storage::EncryptionMethod::LUKS2 } let(:available?) { true } before do home_partition.encryption = home_encryption # Mocking only the object at encryption_method introduces a problem with serialization allow_any_instance_of(Y2Storage::EncryptionMethod::Luks2) .to receive(:available?).and_return(available?) end context "if the encryption settings contain all the detailed information" do let(:home_encryption) do Agama::Storage::Configs::Encryption.new.tap do |enc| enc.password = "notSecreT" enc.method = encryption_method enc.pbkd_function = Y2Storage::PbkdFunction::ARGON2I enc.label = "luks_label" enc.cipher = "aes-xts-plain64" enc.key_size = 512 end end it "proposes the right encryption layer" do proposal.propose partition = proposal.devices.partitions.find do |part| part.blk_filesystem&.mount_path == "/home" end expect(partition.encrypted?).to eq true expect(partition.encryption).to have_attributes( method: Y2Storage::EncryptionMethod::LUKS2, password: "notSecreT", pbkdf: Y2Storage::PbkdFunction::ARGON2I, label: "luks_label", cipher: "aes-xts-plain64", # libstorage-ng uses bytes instead of bits to represent the key size, contrary to # all LUKS documentation and to cryptsetup key_size: 64 ) end end context "if the encryption settings contains pervasive method" do before do allow(Y2Storage::EncryptionProcesses::SecureKey).to receive(:all).and_return([]) allow(Y2Storage::EncryptionProcesses::Apqn).to receive(:all).and_return(apqns) end let(:apqns) { [apqn1, apqn2] } let(:apqn1) { Y2Storage::EncryptionProcesses::Apqn.new("01.0001", "", "", "") } let(:apqn2) { Y2Storage::EncryptionProcesses::Apqn.new("01.0002", "", "", "") } let(:home_encryption) do Agama::Storage::Configs::Encryption.new.tap do |enc| enc.method = Y2Storage::EncryptionMethod::PERVASIVE_LUKS2 enc.password = "notSecreT" enc.apqns = ["01.0001", "01.0002", "01.0003"] enc.pervasive_key_type = "CCA-AESCIPHER" end end it "proposes the right encryption layer" do proposal.propose partition = proposal.devices.partitions.find do |part| part.blk_filesystem&.mount_path == "/home" end expect(partition.encrypted?).to eq true expect(partition.encryption).to have_attributes( method: Y2Storage::EncryptionMethod::PERVASIVE_LUKS2, password: "notSecreT" ) expect(partition.encryption.encryption_process.apqns.map(&:name)) .to contain_exactly("01.0001", "01.0002") expect(partition.encryption.encryption_process.key_type).to eq("CCA-AESCIPHER") end end context "if the encryption method is not available for this system" do let(:available?) { false } it "aborts the proposal process" do proposal.propose expect(proposal.failed?).to eq true end it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( description: /Regular LUKS2 is not available/ ) end end context "if the encryption method is not suitable" do let(:encryption_method) { Y2Storage::EncryptionMethod::RANDOM_SWAP } it "aborts the proposal process" do proposal.propose expect(proposal.failed?).to eq true end it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( description: /Encryption with Volatile Random Key is not suitable/ ) end end context "if the method requires a password but none is provided" do let(:home_encryption) do Agama::Storage::Configs::Encryption.new.tap do |enc| enc.method = encryption_method end end it "aborts the proposal process" do proposal.propose expect(proposal.failed?).to eq true end it "reports the corresponding error" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( description: /No passphrase provided/ ) end end end context "when there are more drives than disks in the system" do let(:config) { default_config } let(:drives) { [drive0, drive1] } let(:drive1) do Agama::Storage::Configs::Drive.new.tap do |drive| drive.search = Agama::Storage::Configs::Search.new.tap do |search| search.if_not_found = if_not_found end end end context "if if_not_found is set to :skip for the surplus drive" do let(:if_not_found) { :skip } it "calculates a proposal if possible" do proposal.propose expect(proposal.failed?).to eq false end it "does not register any issue about missing disks" do proposal.propose expect(proposal.issues_list).to be_empty end end context "if if_not_found is set to :error for the surplus drive" do let(:if_not_found) { :error } it "aborts the proposal" do proposal.propose expect(proposal.failed?).to eq true end it "registers a critical issue" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( description: "Mandatory drive not found" ) end end end context "when searching for an existent drive" do let(:scenario) { "disks.yaml" } let(:config) { default_config } before do drive0.search.condition = Agama::Storage::Configs::SearchConditions::Name.new("/dev/vdb") end it "uses the drive" do proposal.propose root = proposal.devices.partitions.find do |part| part.filesystem&.mount_path == "/" end expect(root.disk.name).to eq("/dev/vdb") end end context "when searching for any drive" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:drives) { [drive0, drive1] } let(:drive0) do Agama::Storage::Configs::Drive.new.tap { |d| d.partitions = [root_partition] } end let(:drive1) do Agama::Storage::Configs::Drive.new.tap { |d| d.partitions = [home_partition] } end it "uses the first unassigned drive" do proposal.propose root = proposal.devices.partitions.find do |part| part.filesystem&.mount_path == "/" end home = proposal.devices.partitions.find do |part| part.filesystem&.mount_path == "/home" end expect(root.disk.name).to eq("/dev/vda") expect(home.disk.name).to eq("/dev/vdb") end end context "when searching for a missing partition" do let(:config) { default_config } let(:partitions0) { [root_partition, missing_partition] } let(:missing_partition) do Agama::Storage::Configs::Partition.new.tap do |part| part.search = Agama::Storage::Configs::Search.new.tap do |search| search.if_not_found = if_not_found end part.filesystem = Agama::Storage::Configs::Filesystem.new.tap do |fs| fs.path = "/foo" end end end context "if if_not_found is set to :skip" do let(:if_not_found) { :skip } it "calculates a proposal if possible" do proposal.propose expect(proposal.failed?).to eq false end it "does not register any issue about missing partitions" do proposal.propose expect(proposal.issues_list).to be_empty end end context "if if_not_found is set to :error" do let(:if_not_found) { :error } it "aborts the proposal" do proposal.propose expect(proposal.failed?).to eq true end it "registers a critical issue" do proposal.propose expect(proposal.issues_list).to include an_object_having_attributes( description: "Mandatory partition not found" ) end end context "if if_not_found is set to :create" do let(:if_not_found) { :create } it "calculates a proposal if possible" do proposal.propose expect(proposal.failed?).to eq false end it "does not register any issue about missing partitions" do proposal.propose expect(proposal.issues_list).to be_empty end it "creates the missing partition" do proposal.propose expect(proposal.devices.partitions.map(&:filesystem)) .to include an_object_having_attributes(mount_path: "/foo") end end end context "when searching for an existent partition" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:partitions0) { [root_partition, home_partition] } before do home_partition.search = Agama::Storage::Configs::Search.new.tap do |search| search.condition = Agama::Storage::Configs::SearchConditions::Name.new("/dev/vda3") end end it "reuses the partition" do vda3 = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3") proposal.propose partition = proposal.devices.find_by_name("/dev/vda3") expect(partition.sid).to eq(vda3.sid) expect(partition.filesystem.mount_path).to eq("/home") end end context "when searching for any partition" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:partitions0) { [root_partition, home_partition] } before do home_partition.search = Agama::Storage::Configs::Search.new.tap { |s| s.max = 1 } end # TODO: Is this correct? The first partition (boot partition) is reused for home. it "reuses the first unassigned partition" do vda1 = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda1") proposal.propose partition = proposal.devices.find_by_name("/dev/vda1") expect(partition.sid).to eq(vda1.sid) expect(partition.filesystem.mount_path).to eq("/home") end it "does not reuse the same partition twice" do vda1 = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda1") vda2 = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda2") root_partition.search = Agama::Storage::Configs::Search.new.tap { |s| s.max = 1 } proposal.propose root = proposal.devices.find_by_name("/dev/vda1") expect(root.sid).to eq(vda1.sid) expect(root.filesystem.mount_path).to eq("/") home = proposal.devices.find_by_name("/dev/vda2") expect(home.sid).to eq(vda2.sid) expect(home.filesystem.mount_path).to eq("/home") end end context "forcing to delete some partitions" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:partitions0) { [root_partition, vda2, vda3] } let(:vda2) do partition_config(name: "/dev/vda2").tap { |c| c.delete = true } end let(:vda3) do partition_config(name: "/dev/vda3").tap { |c| c.delete = true } end before do drive0.search.condition = Agama::Storage::Configs::SearchConditions::Name.new("/dev/vda") end it "deletes the partitions" do vda1_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda1").sid vda2_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda2").sid vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose expect(devicegraph.find_device(vda1_sid)).to_not be_nil expect(devicegraph.find_device(vda2_sid)).to be_nil expect(devicegraph.find_device(vda3_sid)).to be_nil root = devicegraph.find_by_name("/dev/vda2") expect(root.filesystem.mount_path).to eq("/") end end context "allowing to delete some partition" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:partitions0) { [root_partition, vda3] } let(:vda3) do partition_config(name: "/dev/vda3").tap { |c| c.delete_if_needed = true } end before do # vda has 18 GiB of free space. drive0.search.condition = Agama::Storage::Configs::SearchConditions::Name.new("/dev/vda") end context "if deleting the partition is not needed" do before do root_partition.size.min = 15.GiB end it "does not delete the partition" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose expect(devicegraph.find_device(vda3_sid)).to_not be_nil root = devicegraph.find_by_name("/dev/vda4") expect(root.filesystem.mount_path).to eq("/") end end context "if the partition has to be deleted" do before do root_partition.size.min = 20.GiB end it "deletes the partition" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose expect(devicegraph.find_device(vda3_sid)).to be_nil root = devicegraph.find_by_name("/dev/vda3") expect(root.filesystem.mount_path).to eq("/") end end end # Testing precedence. This configuration should not be possible. context "if the partition config indicates both force to delete and allow to delete" do let(:scenario) { "disks.yaml" } let(:config) { default_config } let(:partitions0) { [root_partition, vda3] } let(:vda3) do partition_config(name: "/dev/vda3").tap do |config| config.delete = true config.delete_if_needed = true end end before do drive0.search.condition = Agama::Storage::Configs::SearchConditions::Name.new("/dev/vda") end it "deletes the partition" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose expect(devicegraph.find_device(vda3_sid)).to be_nil root = devicegraph.find_by_name("/dev/vda3") expect(root.filesystem.mount_path).to eq("/") end end context "when reusing a partition" do let(:scenario) { "disks.yaml" } let(:config_json) do { drives: [ { partitions: [ { search: name, filesystem: { reuseIfPossible: reuse, path: "/", type: "ext3" }, size: size }, { filesystem: { path: "/home" } } ] } ] } end let(:reuse) { nil } let(:size) { nil } context "if trying to reuse the file system" do let(:reuse) { true } context "and the partition is already formatted" do let(:name) { "/dev/vda2" } it "reuses the file system" do vda2 = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda2") fs_sid = vda2.filesystem.sid devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vda2").filesystem expect(filesystem.sid).to eq(fs_sid) end end context "and the partition is not formatted" do let(:name) { "/dev/vda1" } it "creates the file system" do devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vda1").filesystem expect(filesystem).to_not be_nil expect(filesystem.type).to eq(Y2Storage::Filesystems::Type::EXT3) end end end context "if not trying to reuse the file system" do let(:reuse) { false } context "and the partition is already formatted" do let(:name) { "/dev/vda2" } it "creates the file system" do vda2 = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda2") fs_sid = vda2.filesystem.sid devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vda2").filesystem expect(filesystem.sid).to_not eq(fs_sid) expect(filesystem.type).to eq(Y2Storage::Filesystems::Type::EXT3) end end context "and the partition is not formatted" do let(:name) { "/dev/vda1" } it "creates the file system" do devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vda1").filesystem expect(filesystem).to_not be_nil expect(filesystem.type).to eq(Y2Storage::Filesystems::Type::EXT3) 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(:config_json) do { drives: [ { partitions: [ { filesystem: { reuseIfPossible: reuse, path: "/", type: "ext3" }, size: size }, { filesystem: { path: "/home" } } ] } ] } end let(:reuse) { nil } let(:size) { nil } context "if trying to reuse the file system" do let(:reuse) { true } it "creates the file system" do devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vda4").filesystem expect(filesystem).to_not be_nil expect(filesystem.type).to eq(Y2Storage::Filesystems::Type::EXT3) end end context "if not trying to reuse the file system" do let(:reuse) { false } it "creates the file system" do devicegraph = proposal.propose filesystem = devicegraph.find_by_name("/dev/vda4").filesystem expect(filesystem).to_not be_nil expect(filesystem.type).to eq(Y2Storage::Filesystems::Type::EXT3) end end context "if no size is indicated" do let(:size) { nil } 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 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 context "if the partition has to be enlarged according to RAM size" do let(:scenario) { "empty-hd-50GiB.yaml" } let(:product_data) do { "storage" => { "volume_templates" => [ { "mount_path" => "swap", "filesystem" => "swap", "size" => { "auto" => true }, "outline" => { "filesystems" => ["swap"], "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) config.boot.configure = false 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 # Regression test for https://github.com/agama-project/agama/issues/2985 context "when searching or creating a partition" do let(:scenario) { "disks.yaml" } let(:config_json) do { drives: [ { partitions: [ { search: { condition: { number: partition_nr }, ifNotFound: :create }, filesystem: { path: "/", type: "ext3" } } ] } ] } end context "if the partition already exists" do let(:partition_nr) { 3 } it "reuses the partition" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose vda3 = devicegraph.find_device(vda3_sid) expect(vda3).to_not be_nil expect(vda3.filesystem.mount_path).to eq("/") end end context "if the partition does not exist" do let(:partition_nr) { 5 } it "creates a new partition" do parts = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda").partitions.size devicegraph = proposal.propose vda = devicegraph.find_by_name("/dev/vda") expect(vda.partitions.size).to eq(parts + 1) expect(vda.partitions.map(&:filesystem)) .to include an_object_having_attributes(mount_path: "/") 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 let(:resize_info) do instance_double( Y2Storage::ResizeInfo, resize_ok?: true, min_size: Y2Storage::DiskSize::GiB(3), max_size: Y2Storage::DiskSize::GiB(35) ) end context "when the reused partition is expected to grow with no enforced limit" do 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 devicegraph = proposal.propose vda3 = devicegraph.find_device(vda3_sid) expect(vda3).to_not be_nil root = devicegraph.find_by_name("/dev/vda4") expect(root.filesystem.mount_path).to eq("/") expect(vda3.size).to be > 21.GiB gpt_adjustment = 1.MiB - 16.5.KiB expect(root.size).to eq(8.5.GiB + gpt_adjustment) end end context "when the reused partition is expected to grow up to a limit" do 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 devicegraph = proposal.propose vda3 = devicegraph.find_device(vda3_sid) expect(vda3).to_not be_nil root = devicegraph.find_by_name("/dev/vda4") expect(root.filesystem.mount_path).to eq("/") expect(vda3.size).to eq 15.GiB expect(root.size).to be > 10.GiB end end context "when the reused partition is expected to shrink as much as needed" do 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 vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose vda3 = devicegraph.find_device(vda3_sid) expect(vda3).to_not be_nil root = devicegraph.find_by_name("/dev/vda4") expect(root).to_not be_nil expect(vda3.size).to eq 10.GiB end end context "if the partition needs to be shrunk to allocate the new ones" do let(:root_size) { "24 GiB" } it "shrinks the partition as needed" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose vda3 = devicegraph.find_device(vda3_sid) expect(vda3).to_not be_nil root = devicegraph.find_by_name("/dev/vda4") expect(root).to_not be_nil expect(vda3.size).to be < 6.GiB gpt_adjustment = 1.MiB - 16.5.KiB expect(root.size).to eq(24.GiB + gpt_adjustment) end end end context "when the reused partition is expected to shrink in all cases" do 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 vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose vda3 = devicegraph.find_device(vda3_sid) expect(vda3).to_not be_nil root = devicegraph.find_by_name("/dev/vda4") expect(root).to_not be_nil expect(vda3.size).to eq 6.GiB end end context "if the partition needs to be shrunk to allocate the new ones" do let(:root_size) { "25 Gib" } it "shrinks the partition as needed" do vda3_sid = Y2Storage::StorageManager.instance.probed.find_by_name("/dev/vda3").sid devicegraph = proposal.propose vda3 = devicegraph.find_device(vda3_sid) expect(vda3).to_not be_nil root = devicegraph.find_by_name("/dev/vda4") expect(root).to_not be_nil expect(vda3.size).to be < 5.GiB gpt_adjustment = 1.MiB - 16.5.KiB expect(root.size).to eq(25.GiB + gpt_adjustment) end end end end end end