yast/yast-storage-ng

View on GitHub
src/lib/y2partitioner/actions/controllers/md.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2017-2019] 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"
require "y2partitioner/actions/controllers/base"
require "y2partitioner/blk_device_restorer"
require "y2partitioner/actions/controllers/available_devices"

module Y2Partitioner
  module Actions
    module Controllers
      # This class stores information about an MD RAID being created or modified
      # and takes care of updating the devicegraph when needed, so the different
      # dialogs can always work directly on a real Md object in the devicegraph.
      class Md < Base
        include Yast::I18n

        include AvailableDevices

        extend Forwardable

        def_delegators :md, :md_level, :md_level=, :md_name,
          :chunk_size_supported?, :chunk_size, :chunk_size=,
          :md_parity, :md_parity=

        # Constructor
        #
        # @note When the device is not given, a new Md object will be created in
        #   the devicegraph right away.
        #
        # @param md [Y2Storage::Md] a MD RAID to work on
        def initialize(md: nil)
          super()
          textdomain "storage"

          # A MD RAID is given to modify its used devices
          @action = md.nil? ? :add : :edit_devices

          md ||= new_md

          @md_sid = md.sid
          @initial_name = md.name
        end

        # MD RAID being modified
        #
        # @return [Y2Storage::Md]
        def md
          working_graph.find_device(@md_sid)
        end

        # Devices that can be selected to become part of the MD array
        #
        # @return [Array<Y2Storage::BlkDevice>]
        def available_devices
          devices = super(working_graph) { |d| valid_device?(d) }

          devices.sort { |a, b| a.compare_by_name(b) }
        end

        # Partitions that are already part of the MD array, sorted by position
        #
        # @return [Array<Y2Storage::Partition>]
        def devices_in_md
          md.sorted_plain_devices
        end

        # Adds a device at the end of the Md array
        #
        # It removes any previous children (like filesystems) from the device and
        # adapts the partition id if possible.
        #
        # @raise [ArgumentError] if the device is already in the RAID
        #
        # @param device [Y2Storage::BlkDevice]
        def add_device(device)
          if md.devices.include?(device)
            raise ArgumentError, "The device #{device} is already part of the Md #{md}"
          end

          # When adding a whole disk (or other partitionable device) we need to
          # ensure the partition table will be not affected (i.e. restored) if
          # the users change their mind during the process.
          BlkDeviceRestorer.new(device).update_checkpoint if device.respond_to?(:partition_table)

          device.adapted_id = Y2Storage::PartitionId::RAID if device.is?(:partition)
          device.remove_descendants
          md.push_device(device)
        end

        # Removes a device from the Md array
        #
        # @raise [ArgumentError] if the device is not in the RAID
        #
        # @param device [Y2Storage::BlkDevice]
        def remove_device(device)
          if !md.devices.include?(device)
            raise ArgumentError, "The device #{device} is not part of the Md #{md}"
          end

          md.remove_device(device)
          BlkDeviceRestorer.new(device).restore_from_checkpoint
        end

        # Modifies the position of some devices in the MD RAID, moving them one
        # step up (before in the list) or down (later in the list)
        #
        # @param sids [Array<Integer>] sids of the devices to move. They all must
        #   be part of {#devices_in_md}
        # @param up [Symbol] true to move devices up, false to move them down
        def devices_one_step(sids, up: true)
          devices = md.sorted_devices
          to_move = indexes_of_devices(sids, devices)
          new_order = move_indexes(to_move, devices.size, up)

          md.sorted_devices = new_order.map { |idx| devices[idx] }
        end

        # Modifies the position of some devices in the MD RAID, moving them to
        # the beginning of the list.
        #
        # @param sids [Array<Integer>] sids of the devices to move. They all must
        #   be part of {#devices_in_md}
        def devices_to_top(sids)
          selected, others = md.sorted_devices.partition { |dev| sids.include?(dev.plain_device.sid) }
          md.sorted_devices = selected + others
        end

        # Modifies the position of some devices in the MD RAID, moving them to
        # the end of the list.
        #
        # @param sids [Array<Integer>] sids of the devices to move. They all must
        #   be part of {#devices_in_md}
        def devices_to_bottom(sids)
          selected, others = md.sorted_devices.partition { |dev| sids.include?(dev.plain_device.sid) }
          md.sorted_devices = others + selected
        end

        # Effective size of the resulting Md device
        #
        # @return [Y2Storage::DiskSize]
        def md_size
          md.size
        end

        # Allowed partity algorithms
        #
        # Allowed parities depends on the RAID type and the number of devices in the Md RAID
        #
        # @return [Array<Y2Storage::MdParity>]
        def md_parities
          md.allowed_md_parities
        end

        # Sets the name of the Md device
        #
        # Unlike {Y2Storage::Md#md_name=}, setting the value to nil or empty will
        # effectively turn the Md device back into a numeric one.
        #
        # @param name [String, nil]
        def md_name=(name)
          if name.nil? || name.empty?
            md.name = @initial_name
          else
            md.md_name = name
          end
        end

        # Title to display in the dialogs during the process
        #
        # @note The returned title depends on the action to perform (see {#initialize})
        #
        # @return [String]
        def wizard_title
          case @action
          when :add
            # TRANSLATORS: dialog title when creating a MD RAID.
            # %s is a device name like /dev/md0
            _("Add RAID %s") % md.name
          when :edit_devices
            # TRANSLATORS: dialog title when editing the devices of a Software RAID.
            # %s is a device name (e.g., /dev/md0)
            _("Edit devices of RAID %s") % md.name
          end
        end

        # Sets default values for the chunk size and parity algorithm of the Md device
        #
        # @note Md level must be previously set.
        # @see #default_chunk_size
        # @see #default_md_parity
        def apply_default_options
          md.chunk_size = default_chunk_size if chunk_size_supported?
          md.md_parity = default_md_parity if parity_supported?
        end

        # Whether is possible to set the parity configuration for the current Md device
        #
        # @note Parity algorithm only makes sense for Raid5, Raid6 and Raid10, see
        #   {Y2Storage::Md#allowed_md_parities}
        #
        # @return [Boolean]
        def parity_supported?
          md.allowed_md_parities.any?
        end

        # Minimal number of devices required for the Md object.
        #
        # @see Y2Storage::Md#minimal_number_of_devices
        #
        # @return [Integer]
        def min_devices
          md.minimal_number_of_devices
        end

        # Possible chunk sizes for the Md object depending on its md_level.
        #
        # @return [Array<Y2Storage::DiskSize>]
        def chunk_sizes
          sizes = []
          size = min_chunk_size

          while size <= max_chunk_size
            sizes << Y2Storage::DiskSize.new(size)
            size *= 2
          end
          sizes
        end

        private

        # Creates a new MD RAID
        #
        # @return [Y2Storage::Md]
        def new_md
          name = Y2Storage::Md.find_free_numeric_name(working_graph)
          md = Y2Storage::Md.create(working_graph, name)
          md.md_level = Y2Storage::MdLevel::RAID0 if md.md_level.is?(:unknown)
          md
        end

        # Whether an available device is valid to be used in a MD RAID
        #
        # @param device [Y2Storage::BlkDevice]
        # @return [Boolean]
        def valid_device?(device)
          if device.is?(:partition)
            valid_partition_for_md?(device)
          else
            valid_device_for_md?(device)
          end
        end

        # Whether an available partition is valid to be used in a MD RAID
        #
        # The partition can be used if its id is linux, lvm, swap or raid, and it is not created over a
        # RAID.
        #
        # The reasons to filter RAID partitions out (basically possible complications with booting and/or
        # auto-assembling) are explained in doc/sle15_features_in_partitioner.md.
        #
        # @param partition [Y2Storage::Partition]
        # @return [Boolean]
        def valid_partition_for_md?(partition)
          partition.id.is?(:linux_system) && !partition.partitionable.is?(:raid)
        end

        # Whether an available device is valid to be used in a MD RAID
        #
        # The device can be used if its has a proper type.
        #
        # @param device [Y2Storage::BlkDevice]
        # @return [Boolean]
        def valid_device_for_md?(device)
          # StrayBlkDevice are not offered. They are used for very concrete
          # use cases in which RAID makes probably no sense.
          device.is?(:disk, :multipath)
        end

        def min_chunk_size
          case md.md_level.to_sym
          when :raid0
            [block_size, Y2Storage::DiskSize.KiB(4)].max # bsc#1200018
          when :raid10
            [block_size, page_size, Y2Storage::DiskSize.KiB(4)].max # bsc#1200018
          else # including raid5 and raid6
            [default_chunk_size, Y2Storage::DiskSize.KiB(64)].min
          end
        end

        def max_chunk_size
          Y2Storage::DiskSize.MiB(64)
        end

        def default_chunk_size
          case md.md_level.to_sym
          when :raid5, :raid6
            Y2Storage::DiskSize.KiB(128)
          else # including raid0 and raid10
            Y2Storage::DiskSize.KiB(64)
          end
        end

        def block_size
          md.block_size
        end

        def page_size
          Y2Storage::DiskSize.B(Y2Storage::StorageManager.instance.arch.page_size)
        end

        def default_md_parity
          Y2Storage::MdParity.find(:default)
        end

        # Indexes of the given sids within a full list of devices
        #
        # It locates devices by its sid or by the sid of its plain device
        # (useful if some encrypted devices that are part of the MD).
        def indexes_of_devices(sids, list)
          sids.map { |sid| list.index { |dev| dev.plain_device.sid == sid } }.compact
        end

        def move_indexes(idxs_to_move, list_size, up)
          indexes = Array(0..(list_size - 1))
          # When moving down, we must iterate the list in inverse order for the
          # algorithm to work.
          indexes.reverse! unless up

          consecutive = 0
          indexes.sort_by do |idx|
            if idxs_to_move.include?(idx)
              # This logic is better explained with an example. Let's say we want
              # the third, fourth and fifth devices in the list to go up and, thus,
              # be placed before the current second one. These will be the results:
              #
              #   third  -> 0.9   (reduced enough to be before 1)
              #   fourth -> 0.99  (extra decimal, stable sorting among the moving devices)
              #   fifth  -> 0.999 (same here)
              #   second -> 1     (original index, the other branch of the 'if')
              consecutive += 1
              delta = consecutive + (0.1**consecutive)
              up ? idx - delta : idx + delta
            else
              consecutive = 0
              idx
            end
          end
        end
      end
    end
  end
end