yast/yast-storage-ng

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

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2018-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 "y2storage/proposal_settings"
require "y2storage/proposal/autoinst_size_parser"
require "y2storage/volume_specification"

module Y2Storage
  module Proposal
    # This module offers a set of common methods that are used by AutoYaST planners.
    class AutoinstDrivePlanner
      # @!attribute [r] devicegraph
      #   @return [Devicegraph]
      # @!attribute [r] issues_list
      #
      attr_reader :devicegraph, :issues_list

      # Constructor
      #
      # @param devicegraph [Devicegraph] Devicegraph to be used as starting point
      # @param issues_list [AutoinstIssues::List] List of AutoYaST issues to register them
      def initialize(devicegraph, issues_list)
        @devicegraph = devicegraph
        @issues_list = issues_list
      end

      # Returns a planned volume group according to an AutoYaST specification
      #
      # @param _drive [AutoinstProfile::DriveSection] drive section
      # @return [Array] Array of planned devices
      def planned_devices(_drive)
        raise NotImplementedError
      end

      private

      # Set all the common attributes that are shared by any device defined by
      # a <partition> section of AutoYaST (i.e. a LV, MD or partition).
      #
      # @param device  [Planned::Device] Planned device
      # @param partition_section [AutoinstProfile::PartitionSection] AutoYaST
      #   specification of the concrete device
      # @param drive_section [AutoinstProfile::DriveSection] AutoYaST drive
      #   section containing the partition one
      def configure_device(device, partition_section, drive_section)
        configure_filesystem(device, partition_section, drive_section)
        configure_usage(device, partition_section)
        add_encryption_attrs(device, partition_section)
      end

      alias_method :device_config, :configure_device

      # Sets those attributes that determine how the device will be used (file system, RAID member, etc.)
      #
      # A device can be used as file system, a RAID member, an LVM PV, a Bcache backing/caching device
      # or as part of a Btrfs multi-device. And in the future, the list might grow.
      #
      # The logic to determine which device to honor is quite simple and it could be improved
      # in the future if needed.
      #
      # @param device  [Planned::Device] Planned device
      # @param partition_section [AutoinstProfile::PartitionSection] AutoYaST
      #   specification of the concrete device
      def configure_usage(device, partition_section)
        usage_attr, *ignored_attrs = usage_attrs_in(partition_section)
        return if usage_attr.nil?

        meth = USAGE_ATTRS_MAP[usage_attr] || usage_attr
        meth = "#{meth}="
        device.public_send(meth, partition_section.public_send(usage_attr)) if device.respond_to?(meth)
        return if ignored_attrs.empty?

        issues_list.add(Y2Storage::AutoinstIssues::ConflictingAttrs,
          partition_section, usage_attr, ignored_attrs)
      end

      # @return [Array<Symbol>] List of 'usage' attributes. The list is ordered by precedence.
      USAGE_ATTRS = [
        :mount, :raid_name, :lvm_group, :btrfs_name, :bcache_backing_for, :bcache_caching_for
      ]
      private_constant :USAGE_ATTRS

      # @return [Array<Symbol>] Map of 'usage' attributes to planned device methods. The map
      #   only contains those attributes which names does not match.
      USAGE_ATTRS_MAP = {
        lvm_group: :lvm_volume_group_name,
        mount:     :mount_point
      }
      private_constant :USAGE_ATTRS_MAP

      # Returns the attributes which determines how the device will be used
      #
      # @param partition_section [AutoinstProfile::PartitionSection] Partition specification
      # @return [Array<Symbol>] List of usage related attributes which are defined
      def usage_attrs_in(partition_section)
        USAGE_ATTRS.reject do |e|
          value = partition_section.public_send(e)
          value.nil? || (value.is_a?(Array) && value.empty?)
        end
      end

      # Sets all common filesystem attributes (e.g., label, uuid, mount point, etc)
      #
      # @param device  [Planned::Device]
      # @param partition_section [AutoinstProfile::PartitionSection]
      # @param drive_section [AutoinstProfile::DriveSection]
      def configure_filesystem(device, partition_section, drive_section)
        add_filesystem_attrs(device, partition_section)

        configure_snapshots(device, drive_section)
        configure_subvolumes(device, partition_section)
      end

      DEFAULT_ENCRYPTION_METHOD = EncryptionMethod.find(:luks1)
      private_constant :DEFAULT_ENCRYPTION_METHOD

      # Sets encryption attributes
      #
      # @param device [Planned::Device] Planned device
      # @param partition_section [AutoinstProfile::PartitionSection] AutoYaST specification
      def add_encryption_attrs(device, partition_section)
        return unless partition_section.crypt_fs || partition_section.crypt_method

        device.encryption_method =
          if partition_section.crypt_method
            find_encryption_method(device, partition_section)
          else
            DEFAULT_ENCRYPTION_METHOD
          end
        device.encryption_pbkdf = find_encryption_pbkdf(partition_section)
        device.encryption_label = partition_section.crypt_label
        device.encryption_cipher = partition_section.crypt_cipher
        device.encryption_key_size = encryption_key_size_for(partition_section)
        return unless device.encryption_method&.password_required?

        device.encryption_password = find_encryption_password(partition_section)
      end

      # Determines the encryption method for a partition section
      #
      # @param device [Planned::Device] Planned device
      # @param partition_section [AutoinstProfile::PartitionSection] AutoYaST specification
      # @return [EncryptionMethod,nil] Encryption method ID or nil if it could not be determined
      def find_encryption_method(device, partition_section)
        encryption_method = EncryptionMethod.find(partition_section.crypt_method)
        error =
          if encryption_method.nil?
            :unknown
          elsif !encryption_method.available?
            :unavailable
          elsif !device.supported_encryption_method?(encryption_method)
            :unsuitable
          end

        if error
          issues_list.add(Y2Storage::AutoinstIssues::InvalidEncryption, partition_section, error)
          return
        end

        encryption_method
      end

      # Extracts the encryption password for a partition section
      #
      # Additionally it registers an issue if it is not found.
      #
      # @return [String,nil]
      def find_encryption_password(partition_section)
        if partition_section.crypt_key.nil? || partition_section.crypt_key.empty?
          issues_list.add(Y2Storage::AutoinstIssues::MissingValue, partition_section, :crypt_key)
          return
        end
        partition_section.crypt_key
      end

      # Determines the encryption password-based key derivation function for a partition section
      #
      # Additionally it registers an issue if a value is specified but it does not correspond to
      # any function recognized by YaST.
      #
      # @param part_section [AutoinstProfile::PartitionSection] AutoYaST specification
      # @return [PbkdFunction,nil] nil if the field is omitted or the value is invalid
      def find_encryption_pbkdf(part_section)
        return unless part_section.crypt_pbkdf

        result = Y2Storage::PbkdFunction.find(part_section.crypt_pbkdf)
        if result.nil?
          # There is an InvalidEncryption kind of issue, but it looks oriented to report a wrong
          # encryption_method, which is a more critical error than this
          issues_list.add(Y2Storage::AutoinstIssues::InvalidValue, part_section, :crypt_pbkdf)
        end
        result
      end

      # Encryption key size in the given partition section
      #
      # Additionally it registers an issue if a value is specified but is not valid (is not
      # a multiple of 8).
      #
      # @param part_section [AutoinstProfile::PartitionSection] AutoYaST specification
      # @return [Integer,nil] nil if the field is omitted or the value is invalid
      def encryption_key_size_for(part_section)
        return unless part_section.crypt_key_size

        value = part_section.crypt_key_size.to_i
        if value % 8 != 0
          issues_list.add(Y2Storage::AutoinstIssues::InvalidValue, part_section, :crypt_key_size)
          return
        end

        value
      end

      # Sets common filesystem attributes
      #
      # @param device  [Planned::Device]
      # @param partition_section [AutoinstProfile::PartitionSection]
      def add_filesystem_attrs(device, partition_section)
        device.mount_point = partition_section.mount
        device.label = partition_section.label
        device.filesystem_type = filesystem_for(partition_section)
        device.mount_by = partition_section.type_for_mountby
        device.mkfs_options = partition_section.mkfs_options
        device.fstab_options = partition_section.fstab_options
        device.read_only = read_only?(partition_section.mount)
      end

      # Sets device attributes related to snapshots
      #
      # This method modifies the first argument
      #
      # @param device  [Planned::Device] Planned device
      # @param drive_section [AutoinstProfile::DriveSection] AutoYaST specification
      def configure_snapshots(device, drive_section)
        return unless device.respond_to?(:root?) && device.root?

        # Always try to enable snapshots if possible
        snapshots = true
        snapshots = false if drive_section.enable_snapshots == false

        device.snapshots = snapshots
      end

      # Sets devices attributes related to Btrfs subvolumes
      #
      # This method modifies the first argument setting default_subvolume and
      # subvolumes.
      #
      # @param device  [Planned::Device] Planned device
      # @param section [AutoinstProfile::PartitionSection] AutoYaST specification
      def configure_subvolumes(device, section)
        return unless device.btrfs?

        defaults = subvolume_attrs_for(device.mount_point)

        device.default_subvolume = section.subvolumes_prefix || defaults[:subvolumes_prefix]

        device.subvolumes =
          if section.create_subvolumes
            section.subvolumes || defaults[:subvolumes] || []
          else
            []
          end

        configure_btrfs_quotas(device, section)
      end

      # Sets the Btrfs quotas according to the section and the subvolumes
      #
      # If `section.quotas` is nil, it inspect whether quotas are needed for any
      # of the subvolumes. In that case, it sets `device.quota` to true.
      #
      # @param device  [Planned::Device] Planned device
      # @param section [AutoinstProfile::PartitionSection] AutoYaST specification
      def configure_btrfs_quotas(device, section)
        if !section.quotas.nil?
          device.quota = section.quotas
          return
        end

        subvols_with_quotas = device.subvolumes.select do |subvol|
          subvol.referenced_limit && !subvol.referenced_limit.unlimited?
        end
        return if subvols_with_quotas.empty?

        device.quota = true
        issues_list.add(
          Y2Storage::AutoinstIssues::MissingBtrfsQuotas, section, subvols_with_quotas
        )
      end

      # Return the default subvolume attributes for a given mount point
      #
      # @param mount [String] Mount point
      # @return [Hash]
      def subvolume_attrs_for(mount)
        return {} if mount.nil?

        spec = VolumeSpecification.for(mount)
        return {} if spec.nil?

        { subvolumes_prefix: spec.btrfs_default_subvolume, subvolumes: spec.subvolumes }
      end

      # Return the filesystem type for a given section
      #
      # @param partition_section [AutoinstProfile::PartitionSection] AutoYaST specification
      # @return [Filesystems::Type] Filesystem type
      def filesystem_for(partition_section)
        return partition_section.type_for_filesystem if partition_section.type_for_filesystem
        return nil unless partition_section.mount

        default_filesystem_for(partition_section)
      end

      # Return the default filesystem type for a given section
      #
      # @param section [AutoinstProfile::PartitionSection]
      # @return [Filesystems::Type] Filesystem type
      def default_filesystem_for(section)
        spec = VolumeSpecification.for(section.mount)
        return spec.fs_type if spec&.fs_type

        (section.mount == "swap") ? Filesystems::Type::SWAP : Filesystems::Type::BTRFS
      end

      # Determine whether the filesystem for the given mount point should be read-only
      #
      # @param mount_point [String] Filesystem mount point
      # @return [Boolean] true if it should be read-only; false otherwise.
      def read_only?(mount_point)
        return false unless mount_point

        spec = VolumeSpecification.for(mount_point)
        !!spec && spec.btrfs_read_only?
      end

      # @param planned_device [Planned::Device] Planned device
      # @param device         [Y2Storage::Device] Device to reuse
      # @param section        [AutoinstProfile::PartitionSection] AutoYaST specification
      def add_device_reuse(planned_device, device, section)
        planned_device.reuse_name = device.is_a?(LvmVg) ? device.volume_group_name : device.name
        planned_device.uuid = section.uuid
        planned_device.resize = !!section.resize if planned_device.respond_to?(:resize=)
        config_filesystem_reuse(planned_device, device, section)
      end

      # @param planned_device [Planned::Device] Planned device
      # @param device         [Y2Storage::Device] Device to reuse
      # @param section        [AutoinstProfile::PartitionSection,AutoinstProfile::Drive] relevant section
      #   of the AutoYaST specification
      def config_filesystem_reuse(planned_device, device, section)
        return unless section.is_a?(AutoinstProfile::PartitionSection)

        planned_device.reformat = !!section.format
        check_reusable_filesystem(planned_device, device, section) if device.respond_to?(:filesystem)
      end

      # @param planned_device [Planned::Partition,Planned::LvmLV,Planned::Md] Planned device
      # @param device         [Y2Storage::Device] Device to reuse
      # @param section        [AutoinstProfile::PartitionSection] AutoYaST specification
      def check_reusable_filesystem(planned_device, device, section)
        return if planned_device.reformat || device.filesystem || planned_device.component?
        # The device to be reused doesn't have filesystem... but maybe it's not
        # really needed, e.g. reusing a bios_boot partition (bsc#1134330)
        return if planned_device.mount_point.nil? && planned_device.filesystem_type.nil?

        issues_list.add(Y2Storage::AutoinstIssues::MissingReusableFilesystem, section)
      end

      # Parse the 'size' element
      #
      # @param section [AutoinstProfile::PartitionSection]
      # @param min     [DiskSize] Minimal size
      # @param max     [DiskSize] Maximal size
      # @see AutoinstSizeParser
      def parse_size(section, min, max)
        AutoinstSizeParser.new(proposal_settings).parse(section.size, section.mount, min, max)
      end

      # Instance of {ProposalSettings} based on the current product.
      #
      # Used to ensure consistency between the guided proposal and the AutoYaST
      # one when default values are used.
      #
      # @return [ProposalSettings]
      def proposal_settings
        @proposal_settings ||= ProposalSettings.new_for_current_product
      end

      # Set 'reusing' attributes for a partition
      #
      # This method modifies the first argument setting the values related to
      # reusing a partition (reuse and format).
      #
      # @param partition [Planned::Partition] Planned partition
      # @param section   [AutoinstProfile::PartitionSection] AutoYaST specification
      def add_partition_reuse(partition, section)
        partition_to_reuse = find_partition_to_reuse(partition, section)
        return unless partition_to_reuse

        partition.filesystem_type ||= partition_to_reuse.filesystem_type
        add_device_reuse(partition, partition_to_reuse, section)
      end

      # @param partition    [Planned::Partition] Planned partition
      # @param part_section [AutoinstProfile::PartitionSection] Partition specification
      #   from AutoYaST
      def find_partition_to_reuse(partition, part_section)
        disk = devicegraph.find_by_name(partition.disk)
        device =
          if part_section.partition_nr
            disk.partitions.find { |i| i.number == part_section.partition_nr }
          elsif part_section.uuid
            disk.partitions.find { |i| i.filesystem_uuid == part_section.uuid }
          elsif part_section.label
            disk.partitions.find { |i| i.filesystem_label == part_section.label }
          else
            issues_list.add(Y2Storage::AutoinstIssues::MissingReuseInfo, part_section)
            nil
          end

        issues_list.add(Y2Storage::AutoinstIssues::MissingReusableDevice, part_section) unless device
        device
      end

      # @return [DiskSize] Minimal partition size
      PARTITION_MIN_SIZE = DiskSize.B(1).freeze

      # @param container [Planned::Disk,Planned::Dasd,Planned::Md] Device to place the partitions on
      # @param drive [AutoinstProfile::DriveSection]
      # @param section [AutoinstProfile::PartitionSection]
      # @return [Planned::Partition,nil]
      def plan_partition(container, drive, section)
        partition = Y2Storage::Planned::Partition.new(nil, nil)

        return unless assign_size_to_partition(partition, section)

        partition.disk = container.name
        partition.partition_id = section.id_for_partition
        partition.primary = section.partition_type == "primary" if section.partition_type
        device_config(partition, section, drive)
        add_partition_reuse(partition, section) if section.create == false
        partition
      end

      # Assign disk size according to AutoYaSt section
      #
      # @param partition   [Planned::Partition] Partition to assign the size to
      # @param part_section   [AutoinstProfile::PartitionSection] Partition specification from AutoYaST
      def assign_size_to_partition(partition, part_section)
        size_info = parse_size(part_section, PARTITION_MIN_SIZE, DiskSize.unlimited)

        if size_info.nil?
          issues_list.add(Y2Storage::AutoinstIssues::InvalidValue, part_section, :size)
          return false
        end

        partition.percent_size = size_info.percentage
        partition.min_size = size_info.min
        partition.max_size = size_info.max
        partition.weight = 1 if size_info.unlimited?
        true
      end
    end
  end
end