yast/yast-storage-ng

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

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2018-2022] 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 "pathname"
require "y2storage/blk_device"
require "y2storage/setup_error"
require "y2storage/boot_requirements_checker"
require "y2storage/proposal_settings"
require "y2storage/with_security_policies"

# This 'import' is necessary to load the control file (/etc/YaST/control.xml)
# when running in an installed system. During installation, this module
# is imported by WorkflowManager.
Yast.import "ProductControl"
Yast.import "Mode"

module Y2Storage
  # Class to check whether a setup (devicegraph) fulfills the storage requirements
  #
  # The storage requirements are the set of necessary volumes to properly boot and
  # run the system. Boot checkings are actually performed by class BootRequirementsChecker,
  # and needed volumes to run the system are read from control file (control.xml).
  #
  # @example
  #   checker = SetupChecker.new(devicegraph)
  #   checker.valid? #=> true
  class SetupChecker
    include Yast::I18n
    include Yast::Logger
    include WithSecurityPolicies

    # @return [Devicegraph]
    attr_reader :devicegraph

    # Constructor
    #
    # @param devicegraph [Devicegraph] setup to check
    def initialize(devicegraph)
      textdomain "storage"
      @devicegraph = devicegraph
    end

    # Whether the current setup fulfills the storage requirements
    #
    # @return [Boolean]
    def valid?
      errors.empty? && warnings.empty?
    end

    # Whether the system contain fatal errors preventing to continue
    #
    # @return [Array<SetupError>]
    def errors
      boot_requirements_checker.errors
    end

    # All storage warnings detected in the setup, for example, when a /boot/efi partition
    # is missing in a UEFI system, or a required product volume (e.g., swap) is missing.
    #
    # @see SetupError
    #
    # @return [Array<SetupError>]
    def warnings
      boot_warnings + product_warnings + mount_warnings + security_policy_warnings
    end

    # All boot errors detected in the setup
    #
    # @return [Array<SetupError>]
    #
    # @see BootRequirementsChecker#errors
    def boot_warnings
      @boot_warnings ||= boot_requirements_checker.warnings
    end

    # All product warnings detected in the setup
    #
    # This checks that all mandatory volumes specified in control file are present
    # in the system.
    #
    # @return [Array<SetupError>]
    def product_warnings
      @product_warnings ||= missing_product_volumes.map { |v| SetupError.new(missing_volume: v) }
    end

    # All warnings related to the definition of the mount points in the setup
    #
    # This checks for example whether the mount options make sense.
    #
    # @return [Array<SetupError>]
    def mount_warnings
      devicegraph.mount_points.map { |mp| mount_warning(mp) }.compact
    end

    # Security policy warnings detected in the setup
    #
    # @return [Array<SetupError>]
    def security_policy_warnings
      @security_policy_warnings ||= security_policy_failing_rules.map do |rule|
        SetupError.new(message: "#{rule.identifiers.first} #{rule.description}")
      end
    end

    # Currently enabled security policy
    #
    # @note yast2-security might not be available, see {#with_security_policies}.
    #
    # @return [Y2Security::SecurityPolicies::Policy, nil]
    def security_policy
      with_security_policies { Y2Security::SecurityPolicies::Manager.instance.enabled_policy }
    end

    # Failing rules from the enabled security policy
    #
    # @note yast2-security might not be available, see {#with_security_policies}.
    #
    # @return [Array<Y2Security::SecurityPolicies::Rule>]
    def security_policy_failing_rules
      return [] unless Yast::Mode.installation

      failing_rules = with_security_policies do
        policies_manager = Y2Security::SecurityPolicies::Manager.instance
        target_config = Y2Security::SecurityPolicies::TargetConfig.new.tap do |config|
          config.storage = devicegraph
        end

        policies_manager.failing_rules(target_config, scope: :storage)
      end

      failing_rules || []
    end

    private

    # Mandatory product volumes that are not present in the current setup
    #
    # @see #product_volumes
    #
    # @return [Array<VolumeSpecification>]
    def missing_product_volumes
      product_volumes.select { |v| missing?(v) }
    end

    # Mandatory product volumes for a valid storage setup
    #
    # @note This volumes are obtained from the control file (control.xml) and only
    #   mandatory volumes are taken into account.
    #
    # @see ProposalSettings#volumes
    #
    # @return [Array<VolumeSpecification>]
    def product_volumes
      # ProposalSettings#volumes is initialized to nil when using old settings format
      # because this attribute does not exist with old format
      volumes = ProposalSettings.new_for_current_product.volumes || []
      volumes.select { |v| mandatory?(v) }
    end

    # Whether a volume is mandatory, that is, the volume is proposed to be created
    # and cannot be deactivated.
    #
    # @param volume [VolumeSpecification]
    # @return [Boolean] true if the volume is mandatory; false otherwise.
    def mandatory?(volume)
      volume.proposed? && !volume.proposed_configurable?
    end

    # Whether a volume is missing in the current setup
    #
    # @see BlkDevice#match_volume?
    #
    # @param volume [VolumeSpecification]
    # @return [Boolean] true if the volume is missing; false otherwise.
    def missing?(volume)
      # Users mounting system volumes as NFS are supposed to know what they are doing
      return false if nfs?(volume)

      BlkDevice.all(devicegraph).none? { |d| d.match_volume?(volume) }
    end

    # Whether a volume is present in the current setup as an NFS mount
    #
    # @param volume [VolumeSpecification]
    # @return [Boolean]
    def nfs?(volume)
      return false unless volume.mount_point

      devicegraph.nfs_mounts.any? do |nfs|
        nfs.mount_point&.path?(volume.mount_point)
      end
    end

    # @see #mount_warnings
    #
    # @param mount_point [MountPoint]
    # @return [SetupError, nil]
    def mount_warning(mount_point)
      missing = mount_point.missing_mount_options
      return if missing.empty?

      # TRANSLATORS: do not translate %{opt} or %{path}. %{opt} is replaced by the name of a mount
      # option in the singular sentence or a list of options separated by commas in the plural one.
      # ${path} is replaced by the mount point.
      msg = n_(
        format(
          "The fstab option %{opt} may be needed to properly mount %{path}.",
          opt: missing.first, path: mount_point.path
        ),
        format(
          "The following fstab options may be needed to propely mount %{path}: %{opt}",
          opt: missing.join(","), path: mount_point.path
        ),
        missing.size
      )
      SetupError.new(message: msg)
    end

    # @return [BootRequirementsChecker] shortcut for boot requirements checker
    # with given device graph
    def boot_requirements_checker
      @boot_requirements_checker ||= BootRequirementsChecker.new(devicegraph)
    end
  end
end