yast/yast-storage-ng

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

Summary

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

module Y2Partitioner
  module Actions
    module Controllers
      # This class stores information about an LVM volume group being created or
      # modified and takes care of updating the devicegraph when needed.
      class LvmVg < Base
        include Yast::I18n

        include SizeParser

        include AvailableDevices

        # @return [String] given volume group name
        attr_accessor :vg_name

        # @return [Y2Storage::DiskSize] given extent size
        attr_reader :extent_size

        # @return [Y2Storage::LvmVg] volume group to work over
        attr_reader :vg

        DEFAULT_VG_NAME = "".freeze
        DEFAULT_EXTENT_SIZE = Y2Storage::DiskSize.MiB(4).freeze

        ALLOWED_NAME_CHARS =
          "0123456789" \
          "abcdefghijklmnopqrstuvwxyz" \
          "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
          "._-+".freeze

        # Constructor
        #
        # @note When the volume group is not given, a new LvmVg object will be created in
        #   the devicegraph right away.
        #
        # @see #initialize_action
        #
        # @param vg [Y2Storage::LvmVg] a volume group to be modified
        def initialize(vg: nil)
          super()
          textdomain "storage"

          initialize_action(vg)
        end

        # Title to display in the dialogs during the process
        #
        # @note The returned title depends on the action to perform (see {#initialize_action})
        #
        # @return [String]
        def wizard_title
          case action
          when :add
            # TRANSLATORS: dialog title when creating an LVM volume group
            _("Add Volume Group")
          when :resize
            # TRANSLATORS: dialog title when resizing an LVM volume group, where %s is replaced
            # by a device name (e.g., /dev/vg0)
            _("Resize Volume Group %s") % vg.name
          end
        end

        # Stores the given extent size
        # @note The value is internally stored as DiskSize when it can be parsed;
        #   nil otherwise.
        #
        # @param value [String]
        def extent_size=(value)
          @extent_size = parse_user_size(value)
        end

        # Effective size of the resulting LVM volume group
        #
        # @return [Y2Storage::DiskSize]
        def vg_size
          vg.size
        end

        # Size of the logical volumes belonging to the current volume group
        #
        # @return [Y2Storage::DiskSize]
        def lvs_size
          Y2Storage::DiskSize.sum(vg.lvm_lvs.map(&:size))
        end

        # Applies given values (i.e., volume group name and extent size) to the
        # volume group
        def apply_values
          vg.vg_name = vg_name
          vg.extent_size = extent_size
        end

        # Error messages for the volume group name
        #
        # @note When there is no error, an empty list is returned.
        #
        # @return [Array<String>] list of errors
        def vg_name_errors
          errors = []
          errors << empty_vg_name_message if empty_vg_name?
          errors << illegal_vg_name_message if vg_name && illegal_vg_name?
          errors << duplicated_vg_name_message if vg_name && duplicated_vg_name?
          errors
        end

        # Error messages for the extent size
        #
        # @note When there is no error, an empty list is returned.
        #
        # @return [Array<String>] list of errors
        def extent_size_errors
          errors = []
          errors << invalid_extent_size_message if invalid_extent_size?
          errors
        end

        # Devices that can be selected to become physical volume of a volume group
        #
        # @note A physical volume could be created using a partition, disk, multipath,
        #   DM Raid or MD Raid. Dasd devices cannot be used.
        #
        # @return [Array<Y2Storage::BlkDevice>]
        def available_devices
          super(working_graph) { |d| valid_device?(d) }
        end

        # Devices that are already used as physical volume by the volume group
        #
        # @return [Array<Y2Storage::BlkDevice>]
        def devices_in_vg
          vg.lvm_pvs.map(&:plain_blk_device).sort { |a, b| a.compare_by_name(b) }
        end

        # Devices used by committed physical volumes of the current volume group
        #
        # @return [Array<Y2Storage::BlkDevice>]
        def committed_devices
          return [] unless probed_vg?

          probed_vg.lvm_pvs.map(&:plain_blk_device)
        end

        # Adds a device as physical volume of the volume group
        #
        # It removes any previous children (like filesystems) from the device and
        # adapts the partition id if possible.
        #
        # @raise [ArgumentError] if the device is already an physcial volume of the
        #   volume group.
        #
        # @param device [Y2Storage::BlkDevice]
        def add_device(device)
          if devices_in_vg.include?(device)
            raise ArgumentError,
              "The device #{device} is already a physical volume of the volume group #{vg_name}"
          end

          device.adapted_id = Y2Storage::PartitionId::LVM if device.is?(:partition)
          device = device.encryption if device.encryption
          device.remove_descendants
          vg.add_lvm_pv(device)
        end

        # Removes a device from the physical volumes of the volume group
        #
        # @raise [ArgumentError] if the device is not a physical volume of the volume group
        #
        # @param device [Y2Storage::BlkDevice]
        def remove_device(device)
          if !devices_in_vg.include?(device)
            raise ArgumentError,
              "The device #{device} is not used as physical volumen by the volume group #{vg_name}"
          end

          device = device.encryption if device.encryption
          vg.remove_lvm_pv(device)
          BlkDeviceRestorer.new(device.plain_device).restore_from_checkpoint
        end

        # Whether the volume group can allocate each striped logical volume.
        #
        # Note that this method only checks that the volume group has the necessary physical volumes to
        # allocate each striped volume individually. Checking that the volume group can allocate all the
        # striped volumes at the same time would be very tricky. Such a check requires to know how the
        # volumes are distributed in the physical volumes.
        #
        # @return [Boolean]
        def size_for_striped_lvs?
          striped_lvs.all? { |l| vg.size_for_striped_lv?(l.size, l.stripes) }
        end

        # Maximum number of stripes used by the striped logical volumes of the volume group
        #
        # @return [Integer] 0 if no striped volumes
        def lvs_stripes
          return 0 if striped_lvs.none?

          striped_lvs.map(&:stripes).max
        end

        private

        # Current action to perform
        # @return [Symbol] :add, :resize
        attr_reader :action

        # Sets the action to perform and initializes necessary data
        #
        # @param current_vg [Y2Storage::LvmVg, nil] nil if the volume group is
        #   going to be created.
        def initialize_action(current_vg)
          detect_action(current_vg)

          case action
          when :add
            initialize_for_add
          when :resize
            initialize_for_resize(current_vg)
          end
        end

        # Detects current action
        #
        # @note When no volume group is given, the action is set to :add. Otherwise,
        #   the action is set to :resize.
        def detect_action(vg)
          # A volume group is given when it is going to be resized
          @action = vg.nil? ? :add : :resize
        end

        # Initializes internal values for add action
        def initialize_for_add
          @vg = create_vg
          @extent_size = DEFAULT_EXTENT_SIZE
          @vg_name = DEFAULT_VG_NAME
        end

        # Initializes internal values for resize action
        def initialize_for_resize(vg)
          @vg = vg
        end

        # Creates a new volume group
        #
        # @return [Y2Storage::LvmVg]
        def create_vg
          Y2Storage::LvmVg.create(working_graph, "")
        end

        # Probed version of the current volume group
        #
        # @note It returns nil if the volume group does not exist in probed devicegraph.
        #
        # @return [Y2Storage::LvmVg, nil]
        def probed_vg
          system_device(vg)
        end

        # Whether the current volume group exists in the probed devicegraph
        #
        # @return [Boolean] true if the volume group exists in probed; false otherwise.
        def probed_vg?
          !probed_vg.nil?
        end

        # Checks whether the given volume group name is empty
        #
        # @return [Boolean] true if volume group name is "" or nil; false otherwise
        def empty_vg_name?
          vg_name.nil? || vg_name.empty?
        end

        # Error message to show when the given volume group name is empty
        #
        # @return [String]
        def empty_vg_name_message
          _("Enter a name for the volume group.")
        end

        # Checks whether the given volume group name has illegal characters
        #
        # @see ALLOWED_NAME_CHARS
        #
        # @return [Boolean] true if volume group name has illegal characters;
        #   false otherwise
        def illegal_vg_name?
          vg_name.chars.any? { |c| !ALLOWED_NAME_CHARS.include?(c) }
        end

        # Error message to show when the given volume group name contains illegal
        # characters
        #
        # @return [String]
        def illegal_vg_name_message
          _("The name for the volume group contains illegal characters. Allowed\n" \
            "are alphanumeric characters, \".\", \"_\", \"-\" and \"+\"")
        end

        # Checks whether it exists another device with the given volume group name
        #
        # @return [Boolean] true if there is another device with the same name;
        #   false otherwise
        def duplicated_vg_name?
          # libstorage has the logic to generate a volume group name, so the name could
          # assigned to the volume group, then check the duplicity of the name and restore
          # previous name if the given one is duplicated. Instead of that, the logic to
          # generate the volume group name has been duplicated here due to its simplicity.
          name = File.join("/dev", vg_name)
          !working_graph.find_by_name(name).nil?
        end

        # Error message to show when the given volume group name is duplicated
        #
        # @return [String]
        def duplicated_vg_name_message
          # TRANSLATORS: %{vg_name} is replaced by the name of the volume group
          format(
            _("The volume group name \"%{vg_name}\" conflicts\n" \
              "with another entry in the /dev directory."),
            vg_name: vg_name
          )
        end

        # Checks whether the given extent size is not valid
        #
        # @note Extent size is valid if it is bigger than 1 KiB, multiple of 128 KiB
        #   and power of 2.
        #
        # @return [Boolean] true if the extent size is not valid; false otherwise
        def invalid_extent_size?
          !(extent_size &&
            extent_size > Y2Storage::DiskSize.KiB(1) &&
            extent_size % Y2Storage::DiskSize.KiB(128) == Y2Storage::DiskSize.zero &&
            extent_size.power_of?(2))
        end

        # Error message to show when the given extent size is not valid
        #
        # @see #invalid_extent_size?
        #
        # @return [String]
        def invalid_extent_size_message
          _("The data entered is invalid. Insert a physical extent size larger than 1 KiB\n" \
            "in powers of 2 and multiple of 128 KiB, for example, \"512 KiB\" or \"4 MiB\"")
        end

        # Checks whether an available device can be used as physical volume
        #
        # @param device [Y2Storage::BlkDevice]
        # @return [Boolean]
        def valid_device?(device)
          if device.is?(:partition)
            valid_partition_for_vg?(device)
          else
            valid_device_for_vg?(device)
          end
        end

        # Checks whether an available partition can be used as physical volume
        #
        # The partition can be used if its id is linux, lvm, swap or raid.
        #
        # @param partition [Y2Storage::Partition]
        # @return [Boolean]
        def valid_partition_for_vg?(partition)
          partition.id.is?(:linux_system)
        end

        # Checks whether an available device can be used as physical volume
        #
        # The device can be used if its has a proper type.
        #
        # @param device [Y2Storage::BlkDevice]
        # @return [Boolean]
        def valid_device_for_vg?(device)
          device.is?(:disk, :multipath, :bios_raid, :md)
        end

        # Striped logical volumes from the volume group
        #
        # @return [Array<Y2Storage::LvmLv>]
        def striped_lvs
          vg.lvm_lvs.select(&:striped?)
        end
      end
    end
  end
end