agama/service/lib/y2storage/agama_proposal.rb
2025-11-19 12:07:07 +00:00

254 lines
8.3 KiB
Ruby

# frozen_string_literal: true
# Copyright (c) [2024-2025] 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 "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/agama_space_maker"
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<Agama::Issue>] 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<Agama::Issue>, nil] Stores issues found during the process.
def initialize(config, storage_system, product_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
@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
# 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::ConfigSolver
.new(product_config, storage_system)
.solve(config)
issues = Agama::Storage::ConfigChecker
.new(config, product_config)
.issues
issues_list.concat(issues)
if issues?
@devices = nil
return @devices
end
@space_maker = Proposal::AgamaSpaceMaker.new(disk_analyzer, config)
@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)
complete_planned(devicegraph)
return if issues?
result = create_devices(devicegraph)
result.devicegraph
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 space-making actions on the given devicegraph
#
# @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
# Modifies the list of planned devices, removing shadowed subvolumes and adding any planned
# partition needed for booting the new target system
#
# @param devicegraph [Devicegraph]
def complete_planned(devicegraph)
if config.boot.configure?
@planned_devices = planned_devices.prepend(boot_partitions(devicegraph))
end
remove_shadowed_subvols(planned_devices)
end
# @see #complete_planned
def boot_partitions(devicegraph)
checker = BootRequirementsChecker.new(
devicegraph,
planned_devices: planned_devices.mountable_devices,
boot_disk_name: boot_device_name
)
# NOTE: Should we try with :desired first?
checker.needed_partitions(:min)
rescue BootRequirementsChecker::Error => e
raise NotBootableError, e.message
end
# Removes partition tables from candidate devices with empty partition table
#
# @param devicegraph [Devicegraph] the graph gets modified
# @return [Array<Integer>] 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<Y2Storage::BlkDevice>]
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<String>] 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)
devices_creator = Proposal::AgamaDevicesCreator.new(devicegraph, issues_list)
result = devices_creator.populated_devicegraph(planned_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<String>]
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<String>]
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