yast/yast-storage-ng

View on GitHub
src/lib/y2storage/autoinst_proposal.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2017] 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 "yast"
require "y2storage/proposal_settings"
require "y2storage/exceptions"
require "y2storage/planned"
require "y2storage/proposal"
require "y2storage/guided_proposal"

module Y2Storage
  # Class to calculate a storage proposal for autoinstallation
  #
  # @example Creating a proposal from the current AutoYaST profile
  #   partitioning = Yast::Profile.current["partitioning"]
  #   proposal = Y2Storage::AutoinstProposal.new(partitioning: partitioning)
  #   proposal.proposed?            # => false
  #   proposal.devices              # => nil
  #   proposal.planned_devices      # => nil
  #
  #   proposal.propose              # Performs the calculation
  #
  #   proposal.proposed?            # => true
  #   proposal.devices              # => Proposed layout
  #
  class AutoinstProposal < Proposal::Base
    # @return [AutoinstProfile::PartitioningSection] Partitioning layout from an AutoYaST profile
    attr_reader :partitioning

    # @return [AutoinstIssues::List] List of found AutoYaST issues
    attr_reader :issues_list

    # @return [DiskSize] Missing space for the originally planned devices
    attr_reader :missing_space

    # Constructor
    #
    # @param partitioning [Array<Hash>] Partitioning schema from an AutoYaST profile
    # @param proposal_settings [Y2Storage::ProposalSettings] Guided proposal settings
    # @param devicegraph  [Devicegraph] starting point. If nil, then probed devicegraph
    #   will be used
    # @param disk_analyzer [DiskAnalyzer] by default, the method will create a new one
    #   based on the initial devicegraph or will use the one in {StorageManager} if
    #   starting from probed (i.e. 'devicegraph' argument is also missing)
    # @param issues_list [AutoinstIssues::List] List of AutoYaST issues to register them
    def initialize(partitioning: [], proposal_settings: nil, devicegraph: nil, disk_analyzer: nil,
      issues_list: nil)
      super(devicegraph: devicegraph, disk_analyzer: disk_analyzer)
      @issues_list = issues_list || ::Installation::AutoinstIssues::List.new
      @proposal_settings = proposal_settings
      @partitioning = AutoinstProfile::PartitioningSection.new_from_hashes(partitioning)
    end

    private

    # Calculates the proposal
    #
    # @raise [NoDiskSpaceError] if there is no enough space to perform the installation
    def calculate_proposal
      drives = Proposal::AutoinstDrivesMap.new(initial_devicegraph, partitioning, issues_list)
      if issues_list.fatal?
        @devices = nil
        return @devices
      end

      @devices = propose_devicegraph(initial_devicegraph, drives)
    end

    # Proposes a devicegraph based on given drives map
    #
    # This method falls back to #proposed_guided_devicegraph when the device map
    # does not contain any partition.
    #
    # @param devicegraph [Devicegraph]                 Starting point
    # @param drives      [Proposal::AutoinstDrivesMap] Devices map from an AutoYaST profile
    # @return [Devicegraph] Devicegraph containing the planned devices
    def propose_devicegraph(devicegraph, drives)
      if drives.partitions?
        @planned_devices = plan_devices(devicegraph, drives)

        devicegraph = clean_graph(devicegraph, drives, @planned_devices)
        add_partition_tables(devicegraph, drives)

        result = create_devices(devicegraph, @planned_devices, drives.disk_names)
        add_reduced_devices_issues(result)
        @missing_space = result.missing_space
        result.devicegraph
      else
        log.info "No partitions were specified. Falling back to guided setup planning."
        propose_guided_devicegraph(devicegraph, drives)
      end
    end

    # Add partition tables
    #
    # This method create/change partitions tables according to information
    # specified in the profile. Disks containing any partition will be ignored.
    #
    # The devicegraph which is passed as first argument will be modified.
    #
    # @param devicegraph [Devicegraph]                 Starting point
    # @param drives      [Proposal::AutoinstDrivesMap] Devices map from an AutoYaST profile
    def add_partition_tables(devicegraph, drives)
      drives.each do |disk_name, drive_spec|
        next if drive_spec.unwanted_partitions?

        disk = devicegraph.disk_devices.find { |d| d.name == disk_name }
        update_partition_table(disk, suitable_ptable_type(disk, drive_spec)) if disk
      end
    end

    # Determines which partition table type should be used
    #
    # @param disk        [Y2Storage::Disk] Disk to set the partition table on
    # @param drive_spec  [Y2Storage::AutoinstProfile::DriveSection] Drive section from the profile
    # @return [Y2Storage::PartitionTables::Type] Partition table type
    def suitable_ptable_type(disk, drive_spec)
      ptable_type = nil
      ptable_type = Y2Storage::PartitionTables::Type.find(drive_spec.disklabel) if drive_spec.disklabel

      disk_ptable_type = disk.partition_table ? disk.partition_table.type : nil
      ptable_type || disk_ptable_type || disk.preferred_ptable_type
    end

    # Update partition table
    #
    # It does nothing if current partition table type and wanted one are the same.
    # The disk object is modified.
    #
    # @param disk        [Y2Storage::Disk] Disk to set the partition table on
    # @param ptable_type [Y2Storage::PartitionTables::Type] Partition table type
    def update_partition_table(disk, ptable_type)
      return unless update_partition_table?(disk, ptable_type)

      disk.remove_descendants if disk.partition_table
      disk.create_partition_table(ptable_type)
    end

    # Determines whether the partition table must be updated
    #
    # @param disk        [Y2Storage::Disk] Disk to set the partition table on
    # @param ptable_type [Y2Storage::PartitionTables::Type] Partition table type
    def update_partition_table?(disk, ptable_type)
      disk.partitions.empty? &&
        (disk.partition_table.nil? || disk.partition_table.type != ptable_type)
    end

    # Add devices to make the system bootable
    #
    # The devicegraph which is passed as first argument will be modified.
    #
    # @param devicegraph [Devicegraph]         Starting point
    # @return [Array<Planned::DevicesCollection>] List of required planned devices to boot
    def boot_devices(devicegraph, devices)
      return unless root?(devices.mountable_devices)

      checker = BootRequirementsChecker.new(devicegraph, planned_devices: devices.mountable_devices)
      begin
        result = checker.needed_partitions
      rescue BootRequirementsChecker::Error => e
        issues_list.add(Y2Storage::AutoinstIssues::CouldNotCalculateBoot)
        log.error e.message
        result = []
      end
      result
    end

    # Determines whether the list of devices includes a root partition
    #
    # @param  devices [Array<Planned:Device>] List of planned devices
    # @return [Boolean] true if there is a root partition; false otherwise.
    def root?(devices)
      return true if devices.any? { |d| d.respond_to?(:mount_point) && d.mount_point == "/" }

      issues_list.add(Y2Storage::AutoinstIssues::MissingRoot)
    end

    # Finds a suitable devicegraph using the initial guided proposal approach
    #
    # @see Y2Storage::GuidedProposal.initial
    #
    # @raise [Error] No suitable devicegraph was found
    #
    # @param devicegraph [Devicegraph] Starting point
    # @param drives [AutoinstDrivesMap] Devices map from an AutoYaST profile
    #
    # @return [Devicegraph] Proposed devicegraph using the guided proposal approach
    def propose_guided_devicegraph(devicegraph, drives)
      devicegraph = clean_graph(devicegraph, drives, [])

      settings = proposal_settings_for_disks(drives)

      proposal = GuidedProposal.initial(devicegraph: devicegraph, settings: settings)

      proposal.devices
    end

    # Calculates list of planned devices
    #
    # If the list does not contain any partition, then it follows the same
    # approach as the guided proposal
    #
    # @param devicegraph [Devicegraph]                 Starting point
    # @param drives      [Proposal::AutoinstDrivesMap] Devices map from an AutoYaST profile
    # @return [Planned::DevicesCollection] Devices to add
    def plan_devices(devicegraph, drives)
      planner = Proposal::AutoinstDevicesPlanner.new(devicegraph, issues_list)
      planner.planned_devices(drives)
    end

    # Clean a devicegraph according to an AutoYaST drives map
    #
    # @param devicegraph     [Devicegraph]       Starting point
    # @param drives          [AutoinstDrivesMap] Devices map from an AutoYaST profile
    # @param planned_devices [<Planned::DevicesCollection>] Planned devices
    # @return [Devicegraph] Clean devicegraph
    #
    # @see Y2Storage::Proposal::AutoinstSpaceMaker
    def clean_graph(devicegraph, drives, planned_devices)
      space_maker = Proposal::AutoinstSpaceMaker.new(disk_analyzer, issues_list)
      space_maker.cleaned_devicegraph(devicegraph, drives, planned_devices)
    end

    # Creates planned devices on a given devicegraph
    #
    # If adding boot devices makes impossible to create the rest of devices,
    # it will try again without them. In such a case, it will register an
    # issue.
    #
    # As a side effect, it updates the planned devices list if needed.
    #
    # @param devicegraph     [Devicegraph]                Starting point
    # @param planned_devices [Planned::DevicesCollection] Devices to add
    # @param disk_names      [Array<String>]              Names of the disks to consider
    # @return [Devicegraph] Copy of devicegraph containing the planned devices
    def create_devices(devicegraph, planned_devices, disk_names)
      boot_parts = boot_devices(devicegraph, @planned_devices)
      devices_creator = Proposal::AutoinstDevicesCreator.new(devicegraph, issues_list)
      begin
        planned_with_boot = planned_devices.prepend(boot_parts)
        result = devices_creator.populated_devicegraph(planned_with_boot, disk_names)
        @planned_devices = planned_with_boot
      rescue Y2Storage::NoDiskSpaceError
        raise if boot_parts.empty?

        result = devices_creator.populated_devicegraph(planned_devices, disk_names)
        issues_list.add(Y2Storage::AutoinstIssues::CouldNotCreateBoot, boot_parts)
      end
      result
    end

    # Add shrinked devices to the issues list
    #
    # @param result [Proposal::CreatorResult] Result after creating the planned devices
    def add_reduced_devices_issues(result)
      if !result.shrinked_partitions.empty?
        issues_list.add(Y2Storage::AutoinstIssues::ShrinkedPlannedDevices,
          result.shrinked_partitions)
      end
      if !result.shrinked_lvs.empty?
        issues_list.add(Y2Storage::AutoinstIssues::ShrinkedPlannedDevices,
          result.shrinked_lvs)
      end
      nil
    end

    # Returns the product's proposal settings for a given set of disks
    #
    # @param drives [Proposal::AutoinstDrivesMap] Devices map from an AutoYaST profile
    # @return [ProposalSettings] Proposal settings considering only the given disks
    #
    # @see Y2Storage::BlkDevice#name
    def proposal_settings_for_disks(drives)
      settings = @proposal_settings || ProposalSettings.new_for_current_product
      drives.use_snapshots? ? settings.force_enable_snapshots : settings.force_disable_snapshots
      settings.candidate_devices = drives.disk_names
      settings
    end
  end
end