yast/yast-yast2

View on GitHub
library/general/src/lib/cfa/sysctl_config.rb

Summary

Maintainability
A
1 hr
Test Coverage
# Copyright (c) [2019-2020] 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 "yast2/execute"
require "cfa/sysctl"
require "cfa/conflict_report"

Yast.import "FileUtils"

module CFA
  # CFA based API to adjust the sysctl tool configuration
  #
  # This class does not modify the running kernel configuration. It just writes
  # the desired values into the configuration file ({PATH}).
  # Despite the class Sysctl this class also takes care about entries in
  #   /run/sysctl.d,
  #   /etc/sysctl.d
  #   /usr/local/lib/sysctl.d
  #   /usr/lib/sysctl.d
  #   /lib/sysctl.d
  #   /etc/sysctl.conf
  # and inform the user if his settings will be overruled by setting in
  # other files.
  #
  # @example Enabling IPv4 forwarding
  #   sysctl = SysctlConfig.new
  #   sysctl.forward_ipv4 = true
  #   sysctl.save
  class SysctlConfig
    include Yast::Logger

    PATHS = [
      "/run/sysctl.d",
      "/etc/sysctl.d",
      "/usr/local/lib/sysctl.d",
      "/usr/lib/sysctl.d",
      "/lib/sysctl.d",
      "/etc/sysctl.conf"
    ].freeze
    private_constant :PATHS

    YAST_CONFIG_PATH = Sysctl::PATH
    private_constant :YAST_CONFIG_PATH

    class << self
      def define_attr(attr)
        define_method attr do
          file = files.reverse.find do |f|
            f.present?(attr)
          end
          return file.public_send(attr) if file

          yast_config_file.public_send(attr)
        end

        define_method "#{attr}=" do |value|
          yast_config_file.public_send("#{attr}=", value)
        end
      end
    end

    Sysctl.known_attributes.each { |a| define_attr(a) }

    def load
      files.each(&:load)
    end

    # Saving all sysctl settings
    #
    def save
      yast_config_file&.save
    end

    # Whether there is a conflict with given attributes
    #
    # @param only [Array<Symbol>] attributes to check
    # @param show_information [Boolean] showing a popup if it is conflicting
    # @return [Boolean] true if any conflict is found; false otherwise
    def conflict?(only: [], show_information: true)
      return false if yast_config_file.empty?

      conflicting_attrs = Sysctl::ATTRIBUTES.keys
      conflicting_attrs &= only unless only.empty?
      conflicts = {}
      higher_precedence_files.each do |file|
        # Checking all "higher" files if their values overrule the current
        # YAST settings.
        conflict_values = yast_config_file.conflicts(file) & conflicting_attrs
        conflicts[file.file_path] = conflict_values unless conflict_values.empty?
      end

      # Transform into real tags
      conflicts.each do |file, tags|
        conflicts[file] = tags.map { |t| Sysctl::ATTRIBUTES[t.to_sym] }
      end

      if !conflicts.empty?
        log.warn("It could be that #{YAST_CONFIG_PATH} will not be written.")
        log.warn("There are conflicts in sysctl files: #{conflicts}.")
        ConfictReport.report(conflicts) if show_information
      end

      !conflicts.empty?
    end

    def files
      @files ||= config_paths.map { |file| Sysctl.new(file_path: file) }
    end

  private

    def yast_config_file
      @yast_config_file ||= files.find { |f| f.file_path == YAST_CONFIG_PATH }
    end

    def lower_precedence_files
      @lower_precedence_files ||= files[0...yast_config_file_idx]
    end

    def higher_precedence_files
      @higher_precedence_files ||= files[(yast_config_file_idx + 1)..-1]
    end

    def config_paths
      paths = PATHS.each_with_object([YAST_CONFIG_PATH]) do |path, all|
        all.concat(file_paths_in(path))
      end

      paths.uniq! { |f| File.basename(f) }
      # Sort files lexicographic
      paths.sort_by! { |f| File.basename(f) }

      # Prepend the kernel configuration file
      paths.unshift(boot_config_path) unless boot_config_path.empty?

      paths
    end

    def file_paths_in(path)
      if Yast::FileUtils.IsFile(path)
        [path]
      elsif Yast::FileUtils.IsDirectory(path)
        Yast::SCR.Read(Yast::Path.new(".target.dir"), path)
          .select { |f| f.end_with?(".conf") } # according to 'sysctl.conf' manpage, only .conf files are considered
          .map { |f| File.join(path, f) }
      else
        log.debug("Ignoring not valid path: #{path}")

        []
      end
    end

    def boot_config_path
      return @boot_config_path if @boot_config_path

      @boot_config_path = if kernel_version.empty?
        ""
      else
        "/boot/sysctl.conf-#{kernel_version}"
      end
    end

    def boot_config_file
      @boot_config_file ||= files.find { |f| f.file_path == boot_config_path }
    end

    def kernel_version
      @kernel_version ||= Yast::Execute.on_target.stdout("/usr/bin/uname", "-r").to_s.chomp
    end

    def yast_config_file_idx
      @yast_config_file_idx ||= files.find_index { |f| f == yast_config_file }
    end
  end
end