diff --git a/service/lib/agama/storage/config.rb b/service/lib/agama/storage/config.rb index ab8166010e..f8d9a1dd18 100644 --- a/service/lib/agama/storage/config.rb +++ b/service/lib/agama/storage/config.rb @@ -62,6 +62,18 @@ def boot_device explicit_boot_device || implicit_boot_device end + # return [Array] + def partitions + drives.flat_map(&:partitions) + end + + # return [Array] + def logical_volumes + volume_groups.flat_map(&:logical_volumes) + end + + private + # Device used for booting the target system # # @return [String, nil] nil if no disk is explicitly chosen @@ -73,9 +85,15 @@ def explicit_boot_device # Device that seems to be expected to be used for booting, according to the drive definitions # - # @return [String, nil] nil if the information cannot be inferred from the list of drives + # @return [String, nil] nil if the information cannot be inferred from the config def implicit_boot_device - # NOTE: preliminary implementation with very simplistic checks + implicit_drive_boot_device || implicit_lvm_boot_device + end + + # @see #implicit_boot_device + # + # @return [String, nil] nil if the information cannot be inferred from the list of drives + def implicit_drive_boot_device root_drive = drives.find do |drive| drive.partitions.any? { |p| p.filesystem&.root? } end @@ -83,14 +101,37 @@ def implicit_boot_device root_drive&.found_device&.name end - # return [Array] - def partitions - drives.flat_map(&:partitions) + # @see #implicit_boot_device + # + # @return [String, nil] nil if the information cannot be inferred from the list of LVM VGs + def implicit_lvm_boot_device + root_vg = root_volume_group + return nil unless root_vg + + root_drives = drives.select { |d| drive_for_vg?(d, root_vg) } + names = root_drives.map { |d| d.found_device&.name }.compact + # Return the first name in alphabetical order + names.min end - # return [Array] - def logical_volumes - volume_groups.flat_map(&:logical_volumes) + # @see #implicit_lvm_boot_device + # + # @return [Configs::VolumeGroup, nil] + def root_volume_group + volume_groups.find do |vg| + vg.logical_volumes.any? { |lv| lv.filesystem&.root? } + end + end + + # @see #implicit_lvm_boot_device + # + # @return [Boolean] + def drive_for_vg?(drive, volume_group) + return true if volume_group.physical_volumes_devices.any? { |d| drive.alias?(d) } + + volume_group.physical_volumes.any? do |pv| + drive.partitions.any? { |p| p.alias?(pv) } + end end end end diff --git a/service/lib/y2storage/agama_proposal.rb b/service/lib/y2storage/agama_proposal.rb index 13675f089b..e9f42e78d1 100644 --- a/service/lib/y2storage/agama_proposal.rb +++ b/service/lib/y2storage/agama_proposal.rb @@ -145,7 +145,7 @@ def calculate_initial_planned(devicegraph) def clean_graph(devicegraph) remove_empty_partition_tables(devicegraph) # {Proposal::SpaceMaker#prepare_devicegraph} returns a copy of the given devicegraph. - space_maker.prepare_devicegraph(devicegraph, partitions_for_clean) + space_maker.prepare_devicegraph(devicegraph, disks_for_clean) end # Configures the disk devices on the given devicegraph to prefer the appropriate partition table @@ -206,14 +206,11 @@ def drives_with_empty_partition_table(devicegraph) devices.select { |d| d.partition_table && d.partitions.empty? } end - # Planned partitions that will hold the given planned devices + # Devices for which the mandatory actions must be executed # - # @return [Array] - def partitions_for_clean - # The current logic is quite trivial, but this is implemented as a separate method because - # some extra logic is expected in the future (eg. considering partitions on pre-existing - # RAIDs and more stuff). See the equivalent method at DevicegraphGenerator. - planned_devices.partitions + # @return [Array] names of partitionable devices + def disks_for_clean + (drives_names + [config.boot_device]).compact.uniq end # Creates the planned devices on a given devicegraph @@ -221,8 +218,14 @@ def partitions_for_clean # @param devicegraph [Devicegraph] the graph gets modified def create_devices(devicegraph) devices_creator = Proposal::AgamaDevicesCreator.new(devicegraph, issues_list) - names = config.drives.map(&:found_device).compact.map(&:name) - result = devices_creator.populated_devicegraph(planned_devices, names, space_maker) + result = devices_creator.populated_devicegraph(planned_devices, drives_names, space_maker) + end + + # Names of all the devices that correspond to a drive from the config + # + # @return [Array] + def drives_names + @drives_names ||= config.drives.map(&:found_device).compact.map(&:name) end # Equivalent device at the given devicegraph for the given configuration setting (eg. drive) diff --git a/service/lib/y2storage/proposal/agama_devices_creator.rb b/service/lib/y2storage/proposal/agama_devices_creator.rb index 8d5400b36d..98d5b4163f 100644 --- a/service/lib/y2storage/proposal/agama_devices_creator.rb +++ b/service/lib/y2storage/proposal/agama_devices_creator.rb @@ -20,7 +20,6 @@ # find current contact information at www.suse.com. require "y2storage/exceptions" -require "y2storage/proposal/agama_lvm_helper" require "y2storage/proposal/lvm_creator" require "y2storage/proposal/partition_creator" @@ -126,13 +125,11 @@ def process_devices def process_existing_partitionables partitions = partitions_for_existing(planned_devices) - # lvm_lvs = system_lvm_over_existing? ? system_lvs(planned_devices) : [] - lvm_lvs = [] - lvm_helper = AgamaLvmHelper.new(lvm_lvs) - # Check whether there is any chance of getting an unwanted order for the planned partitions # within a disk - space_result = provide_space(partitions, original_graph, lvm_helper) + space_result = space_maker.provide_space( + original_graph, partitions: partitions, volume_groups: automatic_vgs + ) self.devicegraph = space_result[:devicegraph] distribution = space_result[:partitions_distribution] @@ -146,6 +143,16 @@ def process_volume_groups planned_devices.vgs.map { |v| create_volume_group(v) } end + # Planned volume groups for which the proposal must automatically create the needed physical + # volumes + # + # @return [Array] + def automatic_vgs + planned_devices.select do |dev| + dev.is_a?(Planned::LvmVg) && dev.pvs_candidate_devices.any? + end + end + # Creates a volume group for the the given planned device. # # @param planned [Planned::LvmVg] @@ -174,13 +181,6 @@ def physical_volumes_for(vg_name) new_pvs + reused_pvs end - # @see #process_existing_partitionables - def provide_space(planned_partitions, devicegraph, lvm_helper) - result = space_maker.provide_space(devicegraph, planned_partitions, lvm_helper) - log.info "Found enough space" - result - end - # @see #process_existing_partitionables def grow_and_reuse_devices(distribution) planned_devices.select(&:reuse?).each do |planned| diff --git a/service/lib/y2storage/proposal/agama_devices_planner.rb b/service/lib/y2storage/proposal/agama_devices_planner.rb index a8788e0402..79b81bb3ff 100644 --- a/service/lib/y2storage/proposal/agama_devices_planner.rb +++ b/service/lib/y2storage/proposal/agama_devices_planner.rb @@ -75,7 +75,7 @@ def planned_drives(config) def planned_vgs(config) config.volume_groups.flat_map do |vg| planner = AgamaVgPlanner.new(devicegraph, issues_list) - planner.planned_devices(vg) + planner.planned_devices(vg, config) end end end diff --git a/service/lib/y2storage/proposal/agama_lvm_helper.rb b/service/lib/y2storage/proposal/agama_lvm_helper.rb deleted file mode 100644 index 80784a13b4..0000000000 --- a/service/lib/y2storage/proposal/agama_lvm_helper.rb +++ /dev/null @@ -1,52 +0,0 @@ -# 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 "y2storage/proposal/lvm_helper" -require "y2storage/proposal_settings" - -module Y2Storage - module Proposal - # LVM helper for Agama. - class AgamaLvmHelper < LvmHelper - # Constructor - def initialize(lvm_lvs) - super(lvm_lvs, guided_settings) - end - - private - - # Method used by the constructor to somehow simulate a typical Guided Proposal - def guided_settings - # Despite the "current_product" part in the name of the constructor, it only applies - # generic default values that are independent of the product (there is no YaST - # ProductFeatures mechanism in place). - Y2Storage::ProposalSettings.new_for_current_product.tap do |target| - target.lvm_vg_strategy = :use_needed - target.lvm_vg_reuse = false - # TODO: Add encryption options. - target.encryption_password = nil - # target.encryption_pbkdf - # target.encryption_method - end - end - end - end -end diff --git a/service/lib/y2storage/proposal/agama_space_maker.rb b/service/lib/y2storage/proposal/agama_space_maker.rb index 69984ddf85..7fc75f8229 100644 --- a/service/lib/y2storage/proposal/agama_space_maker.rb +++ b/service/lib/y2storage/proposal/agama_space_maker.rb @@ -25,30 +25,26 @@ module Y2Storage module Proposal # Space maker for Agama. + # + # FIXME: this class must dissappear. It does not implement any own logic compared to the + # original SpaceMaker. It simply encapsulates the conversion from Agama config to + # ProposalSpaceSettings. class AgamaSpaceMaker < SpaceMaker # @param disk_analyzer [DiskAnalyzer] # @param config [Agama::Storage::Config] def initialize(disk_analyzer, config) - super(disk_analyzer, guided_settings(config)) + super(disk_analyzer, space_settings(config)) end private - # Method used by the constructor to somehow simulate a typical Guided Proposal + # Method used by the constructor to convert the Agama config to ProposalSpaceSettings # # @param config [Agama::Storage::Config] - def guided_settings(config) - # Despite the "current_product" part in the name of the constructor, it only applies - # generic default values that are independent of the product (there is no YaST - # ProductFeatures mechanism in place). - Y2Storage::ProposalSettings.new_for_current_product.tap do |target| - target.space_settings.strategy = :bigger_resize - target.space_settings.actions = space_actions(config) - - boot_device = config.boot_device - - target.root_device = boot_device - target.candidate_devices = [boot_device].compact + def space_settings(config) + Y2Storage::ProposalSpaceSettings.new.tap do |target| + target.strategy = :bigger_resize + target.actions = space_actions(config) end end diff --git a/service/lib/y2storage/proposal/agama_vg_planner.rb b/service/lib/y2storage/proposal/agama_vg_planner.rb index e510abad4f..e56ffcdf1d 100644 --- a/service/lib/y2storage/proposal/agama_vg_planner.rb +++ b/service/lib/y2storage/proposal/agama_vg_planner.rb @@ -28,26 +28,57 @@ module Proposal class AgamaVgPlanner < AgamaDevicePlanner # @param config [Agama::Storage::Configs::VolumeGroup] # @return [Array] - def planned_devices(config) - [planned_vg(config)] + def planned_devices(vg_config, config) + [planned_vg(vg_config, config)] end private - # @param config [Agama::Storage::Configs::VolumeGroup] + # @param vg_config [Agama::Storage::Configs::VolumeGroup] + # @param config [Agama::Storage::Config] # @return [Planned::LvmVg] - def planned_vg(config) + def planned_vg(vg_config, config) # TODO: A volume group name is expected. Otherwise, the planned physical volumes cannot # be associated to the planned volume group. Should the volume group name be # automatically generated if missing? # # @see AgamaDevicePlanner#configure_pv - Y2Storage::Planned::LvmVg.new(volume_group_name: config.name).tap do |planned| - planned.extent_size = config.extent_size - planned.lvs = planned_lvs(config) + Y2Storage::Planned::LvmVg.new(volume_group_name: vg_config.name).tap do |planned| + planned.extent_size = vg_config.extent_size + planned.lvs = planned_lvs(vg_config) + planned.size_strategy = :use_needed + planned.pvs_candidate_devices = devices_for_pvs(vg_config, config) + configure_pvs_encryption(planned, vg_config) end end + # Names of the devices that must be used to calculate automatic physical volumes + # for the given volume group + # + # @param vg_config [Agama::Storage::Configs::VolumeGroup] + # @param config [Agama::Storage::Config] + # @return [Array] + def devices_for_pvs(vg_config, config) + drives = vg_config.physical_volumes_devices.map do |dev_alias| + config.drives.find { |d| d.alias?(dev_alias) } + end.compact + + drives.map { |d| d.found_device.name } + end + + # Configures the encryption-related fields of the given planned volume group + # + # @param planned [Planned::LvmVg] + # @param config [Agama::Storage::Configs::VolumeGroup] + def configure_pvs_encryption(planned, config) + enc = config.physical_volumes_encryption + return unless enc + + planned.pvs_encryption_method = enc.method + planned.pvs_encryption_password = enc.password + planned.pvs_encryption_pbkdf = enc.pbkd_function + end + # @param config [Agama::Storage::Configs::VolumeGroup] # @return [Array] def planned_lvs(config) diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml index e3225ad522..bdd561fc91 100644 --- a/service/package/gem2rpm.yml +++ b/service/package/gem2rpm.yml @@ -39,7 +39,7 @@ Requires: yast2-iscsi-client >= 4.5.7 Requires: yast2-network Requires: yast2-proxy - Requires: yast2-storage-ng >= 5.0.18 + Requires: yast2-storage-ng >= 5.0.20 Requires: yast2-users %ifarch s390 s390x Requires: yast2-s390 >= 4.6.4 diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index cfd7f956ca..ad56095053 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Oct 8 12:25:08 UTC 2024 - Ancor Gonzalez Sosa + +- Storage: added support for automatic creation of physical volumes + (gh#agama-project/agama#1655). + ------------------------------------------------------------------- Mon Oct 7 06:58:48 UTC 2024 - José Iván López González diff --git a/service/test/fixtures/disks.yaml b/service/test/fixtures/disks.yaml index 38dfaf5ffa..cd45024504 100644 --- a/service/test/fixtures/disks.yaml +++ b/service/test/fixtures/disks.yaml @@ -12,11 +12,13 @@ size: 20 GiB name: "/dev/vda2" id: linux + label: "previous_root" file_system: btrfs - partition: size: 10 GiB name: "/dev/vda3" id: linux + label: "previous_home" file_system: xfs - disk: name: "/dev/vdb" diff --git a/service/test/y2storage/agama_proposal_lvm_test.rb b/service/test/y2storage/agama_proposal_lvm_test.rb new file mode 100644 index 0000000000..0c7130eaf2 --- /dev/null +++ b/service/test/y2storage/agama_proposal_lvm_test.rb @@ -0,0 +1,492 @@ +# 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 "../agama/storage/storage_helpers" +require "agama/config" +require "agama/storage/config" +require "agama/storage/config_conversions/from_json" +require "y2storage" +require "y2storage/agama_proposal" + +describe Y2Storage::AgamaProposal do + using Y2Storage::Refinements::SizeCasts + include Agama::RSpec::StorageHelpers + + subject(:proposal) do + described_class.new(config, issues_list: issues_list) + end + + let(:config) { config_from_json } + + let(:config_from_json) do + Agama::Storage::ConfigConversions::FromJSON + .new(config_json) + .convert + end + + let(:issues_list) { [] } + + before do + mock_storage(devicegraph: scenario) + # To speed-up the tests + allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:possible?).and_return(true) + end + + let(:scenario) { "empty-hd-50GiB.yaml" } + + describe "#propose" do + context "when the config has LVM volume groups" do + let(:scenario) { "empty-hd-50GiB.yaml" } + + let(:config_json) do + { + drives: [ + { + partitions: [ + { + alias: "system-pv", + size: "40 GiB" + }, + { + alias: "vg1-pv", + size: "5 GiB" + } + ] + } + ], + volumeGroups: [ + { + name: "system", + extentSize: "2 MiB", + physicalVolumes: ["system-pv"], + logicalVolumes: [ + { + name: "root", + size: "10 GiB", + filesystem: { + path: "/", + type: "btrfs" + }, + encryption: { + luks2: { password: "12345" } + } + }, + { + alias: "system-pool", + name: "pool", + pool: true, + size: "20 GiB", + stripes: 10, + stripeSize: "4 KiB" + }, + { + name: "data", + size: "50 GiB", + usedPool: "system-pool", + filesystem: { type: "xfs" } + } + ] + }, + { + name: "vg1", + physicalVolumes: ["vg1-pv"], + logicalVolumes: [ + { + name: "home", + filesystem: { + path: "/home", + type: "xfs" + }, + size: "2 GiB" + } + ] + } + ] + } + end + + it "proposes the expected devices" do + devicegraph = proposal.propose + + expect(devicegraph.lvm_vgs).to contain_exactly( + an_object_having_attributes( + vg_name: "system", + extent_size: 2.MiB + ), + an_object_having_attributes( + vg_name: "vg1", + extent_size: 4.MiB + ) + ) + + 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) + ) + expect(system_lvs).to contain_exactly( + an_object_having_attributes( + lv_name: "root", + lv_type: Y2Storage::LvType::NORMAL, + size: 10.GiB, + filesystem: an_object_having_attributes( + type: Y2Storage::Filesystems::Type::BTRFS, + mount_path: "/" + ), + encryption: an_object_having_attributes( + type: Y2Storage::EncryptionType::LUKS2, + password: "12345" + ) + ), + an_object_having_attributes( + lv_name: "pool", + lv_type: Y2Storage::LvType::THIN_POOL, + size: 20.GiB, + filesystem: be_nil, + encryption: be_nil, + stripes: 10, + stripe_size: 4.KiB, + lvm_lvs: contain_exactly( + an_object_having_attributes( + lv_name: "data", + lv_type: Y2Storage::LvType::THIN, + size: 50.GiB, + filesystem: an_object_having_attributes( + type: Y2Storage::Filesystems::Type::XFS + ) + ) + ) + ) + ) + + vg1 = devicegraph.find_by_name("/dev/vg1") + vg1_pvs = vg1.lvm_pvs.map(&:plain_blk_device) + vg1_lvs = vg1.lvm_lvs + expect(vg1_pvs).to contain_exactly( + an_object_having_attributes(name: "/dev/sda3", size: 5.GiB) + ) + expect(vg1_lvs).to contain_exactly( + an_object_having_attributes( + lv_name: "home", + lv_type: Y2Storage::LvType::NORMAL, + size: 2.GiB, + filesystem: an_object_having_attributes( + type: Y2Storage::Filesystems::Type::XFS, + mount_path: "/home" + ) + ) + ) + end + end + + context "when a LVM physical volume is not found" do + let(:config_json) do + { + drives: [ + { + partitions: [ + { + size: "40 GiB" + }, + { + alias: "pv1", + size: "5 GiB" + } + ] + } + ], + volumeGroups: [ + { + name: "system", + extentSize: "2 MiB", + physicalVolumes: ["pv1", "pv2"], + logicalVolumes: [ + { + name: "root", + filesystem: { + path: "/" + } + } + ] + } + ] + } + 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 LVM physical volume with alias 'pv2'/, + severity: Agama::Issue::Severity::ERROR + ) + end + end + + context "when a LVM thin pool volume is not found" do + let(:config_json) do + { + drives: [ + { + partitions: [ + { + size: "40 GiB" + }, + { + alias: "pv1", + size: "5 GiB" + } + ] + } + ], + volumeGroups: [ + { + name: "system", + extentSize: "2 MiB", + physicalVolumes: ["pv1"], + logicalVolumes: [ + { + pool: true + }, + { + name: "root", + filesystem: { + path: "/" + }, + usedPool: "pool" + } + ] + } + ] + } + 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 LVM thin pool volume with alias 'pool'/, + severity: Agama::Issue::Severity::ERROR + ) + end + end + + context "when the config has LVM volume groups with generated physical volumes" do + let(:scenario) { "disks.yaml" } + + let(:config_json) do + { + drives: [ + { + alias: "vda", + partitions: [ + { + search: "/dev/vda2", + size: { min: "0", max: "current" } + }, + { + size: { min: "4 GiB" }, + filesystem: { path: "/foo" } + } + ] + }, + { + alias: "vdb" + } + ], + volumeGroups: [ + { + name: "system", + physicalVolumes: [ + { generate: { targetDevices: ["vda"] } } + ], + logicalVolumes: [ + { + name: "root", + size: "10 GiB", + filesystem: { + path: "/", + type: "btrfs" + } + }, + { + name: "data", + size: "10 GiB", + filesystem: { type: "xfs" } + } + ] + }, + { + name: "vg1", + physicalVolumes: [ + { + generate: { + targetDevices: ["vdb"], + encryption: { luks2: { password: "s3cr3t" } } + } + } + ], + logicalVolumes: [ + { + name: "home", + filesystem: { + path: "/home", + type: "xfs" + }, + size: "20 GiB" + } + ] + } + ] + } + end + + 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 + + it "proposes the expected devices" do + devicegraph = proposal.propose + + resized = devicegraph.find_by_name("/dev/vda2") + expect(resized.filesystem.label).to eq("previous_root") + expect(resized.size).to be > 15.GiB + expect(resized.size).to be < 16.GiB + + foo = devicegraph.find_by_name("/dev/vda4") + expect(foo.filesystem.mount_path).to eq("/foo") + expect(foo.size).to be > 4.GiB + expect(foo.size).to be < 5.GiB + + system = devicegraph.find_by_name("/dev/system") + expect(system.lvm_lvs.size).to eq 2 + expect(Y2Storage::DiskSize.sum(system.lvm_lvs.map(&:size))).to eq 20.GiB + expect(system.lvm_pvs.size).to eq 2 + + vg1 = devicegraph.find_by_name("/dev/vg1") + expect(vg1.lvm_lvs.size).to eq 1 + expect(vg1.lvm_lvs.first.size).to eq 20.GiB + expect(vg1.lvm_pvs.size).to eq 1 + + pv_vg1 = vg1.lvm_pvs.first + expect(pv_vg1.blk_device.is?(:encryption)).to eq true + expect(pv_vg1.blk_device.type.is?(:luks2)).to eq true + end + end + + context "when two volume groups with generated physical volumes share a disk" do + let(:scenario) { "disks.yaml" } + + let(:config_json) do + { + drives: [ + { alias: "vda" }, + { alias: "vdb" }, + { alias: "vdc" } + ], + volumeGroups: [ + { + name: "system", + physicalVolumes: [ + { generate: { targetDevices: ["vdb", "vdc"] } } + ], + logicalVolumes: [ + { + name: "root", + size: "10 GiB", + filesystem: { + path: "/", + type: "btrfs" + } + }, + { + name: "home", + size: { + min: "30 GiB" + }, + filesystem: { + path: "/home", + type: "xfs" + } + } + ] + }, + { + name: "vg1", + physicalVolumes: [ + { + generate: { + targetDevices: ["vdc"], + encryption: { luks1: { password: "s3cr3t" } } + } + } + ], + logicalVolumes: [ + { + name: "data", + filesystem: { type: "xfs" }, + size: { min: "30 GiB", max: "40 GiB" } + } + ] + } + ] + } + end + + it "proposes the expected devices" do + devicegraph = proposal.propose + + system = devicegraph.find_by_name("/dev/system") + expect(system.lvm_lvs.map { |lv| lv.mount_point.path }).to contain_exactly("/", "/home") + expect(system.lvm_pvs.map { |pv| pv.blk_device.partitionable.name }) + .to contain_exactly("/dev/vdb", "/dev/vdc") + + vg1 = devicegraph.find_by_name("/dev/vg1") + expect(vg1.lvm_lvs.map(&:lv_name)).to contain_exactly("data") + expect(vg1.lvm_pvs.map { |pv| pv.plain_blk_device.partitionable.name }) + .to contain_exactly("/dev/vdc") + + pv_vg1 = vg1.lvm_pvs.first + expect(pv_vg1.blk_device.is?(:encryption)).to eq true + expect(pv_vg1.blk_device.type.is?(:luks1)).to eq true + end + end + end +end diff --git a/service/test/y2storage/agama_proposal_test.rb b/service/test/y2storage/agama_proposal_test.rb index 33783ab5c7..fbc0924d01 100644 --- a/service/test/y2storage/agama_proposal_test.rb +++ b/service/test/y2storage/agama_proposal_test.rb @@ -1293,251 +1293,5 @@ def partition_config(name: nil, filesystem: nil, size: nil) end end end - - context "when the config has LVM volume groups" do - let(:scenario) { "empty-hd-50GiB.yaml" } - - let(:config_json) do - { - drives: [ - { - partitions: [ - { - alias: "system-pv", - size: "40 GiB" - }, - { - alias: "vg1-pv", - size: "5 GiB" - } - ] - } - ], - volumeGroups: [ - { - name: "system", - extentSize: "2 MiB", - physicalVolumes: ["system-pv"], - logicalVolumes: [ - { - name: "root", - size: "10 GiB", - filesystem: { - path: "/", - type: "btrfs" - }, - encryption: { - luks2: { password: "12345" } - } - }, - { - alias: "system-pool", - name: "pool", - pool: true, - size: "20 GiB", - stripes: 10, - stripeSize: "4 KiB" - }, - { - name: "data", - size: "50 GiB", - usedPool: "system-pool", - filesystem: { type: "xfs" } - } - ] - }, - { - name: "vg1", - physicalVolumes: ["vg1-pv"], - logicalVolumes: [ - { - name: "home", - filesystem: { - path: "/home", - type: "xfs" - }, - size: "2 GiB" - } - ] - } - ] - } - end - - it "proposes the expected devices" do - devicegraph = proposal.propose - - expect(devicegraph.lvm_vgs).to contain_exactly( - an_object_having_attributes( - vg_name: "system", - extent_size: 2.MiB - ), - an_object_having_attributes( - vg_name: "vg1", - extent_size: 4.MiB - ) - ) - - 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) - ) - expect(system_lvs).to contain_exactly( - an_object_having_attributes( - lv_name: "root", - lv_type: Y2Storage::LvType::NORMAL, - size: 10.GiB, - filesystem: an_object_having_attributes( - type: Y2Storage::Filesystems::Type::BTRFS, - mount_path: "/" - ), - encryption: an_object_having_attributes( - type: Y2Storage::EncryptionType::LUKS2, - password: "12345" - ) - ), - an_object_having_attributes( - lv_name: "pool", - lv_type: Y2Storage::LvType::THIN_POOL, - size: 20.GiB, - filesystem: be_nil, - encryption: be_nil, - stripes: 10, - stripe_size: 4.KiB, - lvm_lvs: contain_exactly( - an_object_having_attributes( - lv_name: "data", - lv_type: Y2Storage::LvType::THIN, - size: 50.GiB, - filesystem: an_object_having_attributes( - type: Y2Storage::Filesystems::Type::XFS - ) - ) - ) - ) - ) - - vg1 = devicegraph.find_by_name("/dev/vg1") - vg1_pvs = vg1.lvm_pvs.map(&:plain_blk_device) - vg1_lvs = vg1.lvm_lvs - expect(vg1_pvs).to contain_exactly( - an_object_having_attributes(name: "/dev/sda3", size: 5.GiB) - ) - expect(vg1_lvs).to contain_exactly( - an_object_having_attributes( - lv_name: "home", - lv_type: Y2Storage::LvType::NORMAL, - size: 2.GiB, - filesystem: an_object_having_attributes( - type: Y2Storage::Filesystems::Type::XFS, - mount_path: "/home" - ) - ) - ) - end - end - - context "when a LVM physical volume is not found" do - let(:config_json) do - { - drives: [ - { - partitions: [ - { - size: "40 GiB" - }, - { - alias: "pv1", - size: "5 GiB" - } - ] - } - ], - volumeGroups: [ - { - name: "system", - extentSize: "2 MiB", - physicalVolumes: ["pv1", "pv2"], - logicalVolumes: [ - { - name: "root", - filesystem: { - path: "/" - } - } - ] - } - ] - } - 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 LVM physical volume with alias 'pv2'/, - severity: Agama::Issue::Severity::ERROR - ) - end - end - - context "when a LVM thin pool volume is not found" do - let(:config_json) do - { - drives: [ - { - partitions: [ - { - size: "40 GiB" - }, - { - alias: "pv1", - size: "5 GiB" - } - ] - } - ], - volumeGroups: [ - { - name: "system", - extentSize: "2 MiB", - physicalVolumes: ["pv1"], - logicalVolumes: [ - { - pool: true - }, - { - name: "root", - filesystem: { - path: "/" - }, - usedPool: "pool" - } - ] - } - ] - } - 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 LVM thin pool volume with alias 'pool'/, - severity: Agama::Issue::Severity::ERROR - ) - end - end end end