271 lines
9.5 KiB
Ruby
271 lines
9.5 KiB
Ruby
# 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<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, 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<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, 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<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
|