# 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 "agama/storage/config_checker" require "agama/storage/config_solver" require "agama/storage/bootloader_config" require "agama/storage/bootloader_config_solver" require "yast" require "y2storage/exceptions" require "y2storage/planned" require "y2storage/proposal" require "y2storage/proposal/agama_devices_creator" require "y2storage/proposal/agama_devices_planner" require "y2storage/proposal/boot_planner" require "y2storage/proposal/space_settings_builder" require "y2storage/proposal/planned_devices_handler" module Y2Storage # Class to calculate a storage proposal for Agama. # # @example Creating a proposal from the current Agama configuration # config = Agama::Storage::Config.new_from_json(config_json) # proposal = Y2Storage::AgamaProposal.new(config) # proposal.proposed? # => false # proposal.devices # => nil # proposal.planned_devices # => nil # # proposal.propose # Performs the calculation # # proposal.proposed? # => true # proposal.devices # => Proposed layout # class AgamaProposal < Proposal::Base include Proposal::PlannedDevicesHandler # @return [Agama::Storage::Config] attr_reader :config # @return [Array] List of found issues attr_reader :issues_list # @note The storage config (first param) is modified while solving it, see # {#calculate_proposal}. # # @param config [Agama::Storage::Config] # @param storage_system [Agama::Storage::System] # @param product_config [Agama::Config, nil] # @param issues_list [Array, nil] Stores issues found during the process. def initialize( config, storage_system, product_config: nil, bootloader_config: nil, issues_list: nil ) super(devicegraph: storage_system.devicegraph, disk_analyzer: storage_system.analyzer) @config = config @storage_system = storage_system @product_config = product_config || Agama::Config.new @bootloader_config = bootloader_config || Agama::Storage::BootloaderConfig.new @issues_list = issues_list || [] end private # @return [Agama::Storage::System] attr_reader :storage_system # @return [Agama::Config] attr_reader :product_config # @return [Proposal::AgamaSpaceMaker] attr_reader :space_maker # @return [Agama::Storage::BootloaderConfig] attr_reader :bootloader_config # Whether there is any issue. # # @return [Boolean] def issues? issues_list.any? end # Calculates the proposal # # @raise [NoDiskSpaceError] if there is no enough space to perform the installation def calculate_proposal Agama::Storage::BootloaderConfigSolver.new(product_config).solve(bootloader_config) Agama::Storage::ConfigSolver .new(product_config, bootloader_config, storage_system) .solve(config) issues = Agama::Storage::ConfigChecker .new(config, bootloader_config: bootloader_config, product_config: product_config) .issues issues_list.concat(issues) if issues? @devices = nil return @devices end space_settings = Proposal::SpaceSettingsBuilder.new(config).space_settings @space_maker = Proposal::SpaceMaker.new(disk_analyzer, space_settings) @devices = propose_devicegraph end # Proposes a devicegraph based on given configuration # # @return [Devicegraph, nil] Devicegraph containing the planned devices, nil if the proposal # failed def propose_devicegraph devicegraph = initial_devicegraph.dup calculate_initial_planned(devicegraph) return if issues? configure_ptable_types(devicegraph) devicegraph = clean_graph(devicegraph) return if issues? result = create_devices_with_boot(devicegraph) result.devicegraph end # Creates the devices doing several attempts, each one for a different set of boot partitions # # @raise [NoDiskSpaceError] if there is no enough space to perform the installation # @raise [NotBootableError] if adding partitions is not enough to make the system bootable def create_devices_with_boot(devicegraph) initial_devicegraph = devicegraph.dup possible_boot_sets = Proposal::BootPlanner.new(devicegraph, config, bootloader_config) .plans(planned_devices.mountable_devices) result = nil while !result && possible_boot_sets.any? begin boot_devices = possible_boot_sets.shift planned_with_boot = planned_devices.prepend(boot_devices) # Although this can affect the planned devices in the collection, that is not a problem # because the different "boot plans" are almost equal (only the sizes change) so there is # no difference in shadowing. remove_shadowed_subvols(planned_with_boot) result = create_devices(devicegraph, planned_with_boot) @planned_devices = planned_with_boot rescue NoDiskSpaceError raise if possible_boot_sets.empty? devicegraph = initial_devicegraph.dup end end result end # Fills the list of planned devices, excluding partitions from the boot requirements checker # # @return [Planned::DevicesCollection] def calculate_initial_planned(devicegraph) planner = Proposal::AgamaDevicesPlanner.new(devicegraph, issues_list) @planned_devices = planner.planned_devices(config) end # Performs the mandatory partition actions for making space on the given devicegraph. The # actions for making space in the volume groups are performed later by the devices creator. # # @param devicegraph [Devicegraph] the graph gets modified 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, devs_for_clean) end # Configures the disk devices on the given devicegraph to prefer the appropriate partition table # types, if any partition table needs to be created later during the proposal # # @param devicegraph [Devicegraph] the graph gets modified def configure_ptable_types(devicegraph) configured = config.drives.select(&:ptable_type) configured.each do |drive| dev = device_for(drive, devicegraph) next unless dev dev.forced_ptable_type = drive.ptable_type end end # Removes partition tables from candidate devices with empty partition table # # @param devicegraph [Devicegraph] the graph gets modified # @return [Array] sid of devices where partition table was deleted from def remove_empty_partition_tables(devicegraph) devices = drives_with_empty_partition_table(devicegraph) devices.each(&:delete_partition_table) devices.map(&:sid) end # All candidate devices with an empty partition table # # @param devicegraph [Y2Storage::Devicegraph] # @return [Array] def drives_with_empty_partition_table(devicegraph) devices = config.drives.map { |d| device_for(d, devicegraph) }.compact devices.select { |d| d.partition_table && d.partitions.empty? } end # Devices for which the mandatory actions must be executed # # @return [Array] names of partitionable devices def devs_for_clean (drives_names + raids_names + [boot_device_name]).compact.uniq end # Creates the planned devices on a given devicegraph # # @param devicegraph [Devicegraph] the graph gets modified def create_devices(devicegraph, all_devices) devices_creator = Proposal::AgamaDevicesCreator.new(devicegraph, issues_list) result = devices_creator.populated_devicegraph(all_devices, space_maker) end # Name of the boot device. # # @return [String, nil] def boot_device_name config.boot_device&.found_device&.name 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 # Names of all the devices that correspond to an MD RAID from the config # # @return [Array] def raids_names @raids_names ||= config.md_raids.map(&:found_device).compact.map(&:name) end # Equivalent device at the given devicegraph for the given configuration setting (eg. drive) # # @param drive [Agama::Storage::Configs::Drive] # @param devicegraph [Devicegraph] # @return [Device] def device_for(drive, devicegraph) return unless drive.found_device devicegraph.find_device(drive.found_device.sid) end end end