yast/yast-storage-ng

View on GitHub
src/lib/y2storage/proposal/devicegraph_generator.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2016-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 "storage"
require "y2storage/proposal/space_maker"
require "y2storage/proposal/partition_creator"
require "y2storage/proposal/lvm_helper"
require "y2storage/planned"

module Y2Storage
  module Proposal
    # Class to create devicegraphs that can accommodate a given collection of
    # volumes
    class DevicegraphGenerator
      include Yast::Logger

      attr_accessor :settings

      def initialize(settings, disk_analyzer)
        @settings = settings
        @space_maker = Proposal::SpaceMaker.new(disk_analyzer, settings)
      end

      # Devicegraph including all the specified volumes
      #
      # @param planned_devices [Array<Planned::Device>] devices to accommodate
      # @param initial_graph [Devicegraph] initial devicegraph
      #           (typically the representation of the current system)
      #
      # @return [Devicegraph]
      # @raise [Error] if it was not possible to propose a devicegraph
      def devicegraph(planned_devices, initial_graph)
        # We are going to alter the volumes in several ways, so let's be a
        # good citizen and do it in our own copy
        planned_devices = planned_devices.map(&:dup)

        protect_sids(planned_devices)
        partitions = partitions_for(planned_devices)
        lvm_lvs = system_lvs(planned_devices)

        lvm_helper = LvmHelper.new(lvm_lvs, settings)
        space_result = provide_space(partitions, initial_graph, lvm_helper)

        refine_planned_partitions!(partitions, space_result[:deleted_partitions])
        creator_result = create_partitions(
          space_result[:partitions_distribution], space_result[:devicegraph]
        )
        reuse_devices(planned_devices, creator_result.devicegraph)

        graph = create_separate_vgs(planned_devices, creator_result).devicegraph

        if settings.use_lvm
          new_pvs = new_physical_volumes(space_result[:devicegraph], graph)
          graph = lvm_helper.create_volumes(graph, new_pvs)
        end

        graph.mount_points.each(&:adjust_mount_options)
        graph
      end

      # Devicegraph resulting of executing the mandatory actions to make space on all the relevant
      # disks
      #
      # @param planned_devices [Array<Planned::Device>] devices to accommodate
      # @param initial_graph [Devicegraph] initial devicegraph (typically the representation of the
      #   current system)
      #
      # @return [Devicegraph]
      def prepared(planned_devices, initial_graph)
        protect_sids(planned_devices)
        partitions = partitions_for(planned_devices)
        space_maker.prepare_devicegraph(initial_graph, partitions)
      end

      protected

      # @return [Proposal::SpaceMaker]
      attr_reader :space_maker

      # Planned partitions that will hold the given planned devices
      #
      # @see #planned_partition_for
      #
      # @param planned_devices [Array<Planned::Device>] list of planned devices
      # @return [Array<Planned::Partition>]
      def partitions_for(planned_devices)
        planned_devices.select { |d| plan_partition?(d) }.map { |d| planned_partition_for(d) }
      end

      # Whether the given planned device must result in the creation of a partition
      #
      # @param device [Planned::Device]
      # @return [Boolean]
      def plan_partition?(device)
        return false if device.reuse?

        device.is_a?(Planned::Partition) || device.is_a?(Planned::LvmVg)
      end

      # Planned partition that will hold the given planned device
      #
      # @param device [Planned::Device] a planned partition (so no transformation
      #   is required) or a planned VG (so a PV partition for the VG is calculated)
      # @return [Planned::Partition]
      def planned_partition_for(device)
        device.is_a?(Planned::Partition) ? device : device.single_pv_partition
      end

      # Configures SpaceMaker#protected_sids according to the given list of planned devices
      #
      # @param devices [Array<Planned::Device]
      def protect_sids(devices)
        space_maker.protected_sids = devices.select(&:reuse?).map(&:reuse_sid)
      end

      # Planned logical volumes that must be created at the system volume group
      #
      # @param devices [Array<Planned::Device>]
      # @return [Array<Planned::LvmLv>]
      def system_lvs(devices)
        devices.select { |dev| dev.is_a?(Planned::LvmLv) && !dev.reuse? }
      end

      # Creates the corresponding LVM devices for all the planned VGs in the
      # list of planned devices
      #
      # @param devices [Array<Planned::Device>] all planned devices
      # @param creator_result [Proposal::CreatorResult] result of the previous step,
      #     in which the partitions that must hold the PVs were created
      # @return [Proposal::CreatorResult]
      def create_separate_vgs(devices, creator_result)
        vgs = devices.select { |dev| dev.is_a?(Planned::LvmVg) }

        vgs.reduce(creator_result) do |result, vg|
          pvs = creator_result.created_names { |d| d.pv_for?(vg.volume_group_name) }
          creator = LvmCreator.new(result.devicegraph)
          result.merge(creator.create_volumes(vg, pvs))
        end
      end

      # Provides free disk space in the proposal devicegraph to fit the
      # planned partitions in.
      #
      # @raise [Error] if the goal is not reached
      #
      # @param planned_partitions [Array<Planned::Partition>] set partitions to
      #     make space for.
      #
      # @return [Devicegraph]
      def provide_space(planned_partitions, devicegraph, lvm_helper)
        if settings.use_lvm
          provide_space_lvm(planned_partitions, devicegraph, lvm_helper)
        else
          provide_space_no_lvm(planned_partitions, devicegraph, lvm_helper)
        end
      end

      # Variant of #provide_space when LVM is not involved
      # @see #provide_space
      def provide_space_no_lvm(planned_partitions, devicegraph, lvm_helper)
        result = space_maker.provide_space(devicegraph, planned_partitions, lvm_helper)
        log.info "Found enough space"
        result
      end

      # Variant of #provide_space when LVM is involved. It first tries to reuse
      # the existing volume groups (one at a time). If that fails, it tries to
      # create a new volume group from scratch.
      #
      # @see #provide_space
      def provide_space_lvm(planned_partitions, devicegraph, lvm_helper)
        lvm_helper.reused_volume_group = nil

        lvm_helper.reusable_volume_groups(devicegraph).each do |vg|

          lvm_helper.reused_volume_group = vg
          lvm_sids = lvm_helper.partitions_in_vg.map { |x| devicegraph.find_by_name(x) }.map(&:sid)
          original_sids = space_maker.protected_sids
          space_maker.protected_sids += lvm_sids

          result = space_maker.provide_space(devicegraph, planned_partitions, lvm_helper)
          log.info "Found enough space including LVM, reusing #{vg}"
          return result
        rescue Error
          space_maker.protected_sids = original_sids
          next

        end

        lvm_helper.reused_volume_group = nil
        result = space_maker.provide_space(devicegraph, planned_partitions, lvm_helper)
        log.info "Found enough space including LVM"

        result
      end

      # List of unused partitions with LVM id (i.e. potential physical volumes) that
      # are present in the new devicegraph but were not there in the old one.
      #
      # @param old_devicegraph [Devicegraph]
      # @param new_devicegraph [Devicegraph]
      # @return [Array<String>] device names of the partitions
      def new_physical_volumes(old_devicegraph, new_devicegraph)
        all_pvs = new_devicegraph.partitions.select do |part|
          part.id.is?(:lvm) && !part.part_of_lvm_or_md?
        end
        old_pv_sids = old_devicegraph.partitions.select { |p| p.id.is?(:lvm) }.map(&:sid)
        all_pvs.reject { |pv| old_pv_sids.include?(pv.sid) }.map(&:name)
      end

      # Adds some extra information to the planned partitions inferred from
      # the list of partitions deleted by the space maker.
      #
      # It enforces reuse of UUIDs and labels from the deleted swap
      # partitions.
      #
      # It modifies the passed volumes.
      #
      # @param planned_partitions [Array<Planned::Partition>] planned
      #     partitions to modify
      # @param deleted_partitions [Array<Partition>] partitions
      #     deleted from the initial devicegraph
      def refine_planned_partitions!(planned_partitions, deleted_partitions)
        deleted_swaps = deleted_partitions.select do |part|
          part.id.is?(:swap) && part.formatted_as?(:swap)
        end
        new_swap_volumes = planned_partitions.select do |vol|
          !vol.reuse_name && vol.mount_point == "swap"
        end

        new_swap_volumes.each_with_index do |swap_volume, idx|
          deleted_swap = deleted_swaps[idx]
          break unless deleted_swap

          swap_volume.uuid = deleted_swap.filesystem.uuid
          swap_volume.label = deleted_swap.filesystem.label
        end
      end

      # Creates partitions representing a set of volumes
      #
      # @param distribution [Planned::PartitionsDistribution]
      # @param initial_graph [Devicegraph] initial devicegraph
      #
      # @return [Proposal::CreatorResult]
      def create_partitions(distribution, initial_graph)
        partition_creator = PartitionCreator.new(initial_graph)
        partition_creator.create_partitions(distribution)
      end

      # Adjusts pre-existing (not created by us) devices assigning its
      # mount point and boot flag
      #
      # It works directly on the passed devicegraph
      #
      # @param planned_devices [Array<Planned::Device>]
      # @param graph [Devicegraph] devicegraph to modify
      def reuse_devices(planned_devices, graph)
        planned_devices.each do |planned|
          next unless planned.reuse?

          planned.reuse!(graph)
        end
      end
    end
  end
end