yast/yast-installation

View on GitHub
src/lib/installation/clients/copy_files_finish.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# Copyright (c) [2006-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 "fileutils"
require "installation/ssh_importer"
require "installation/finish_client"

require "shellwords"

Yast.import "Pkg"
Yast.import "Arch"
Yast.import "Linuxrc"
Yast.import "Installation"
Yast.import "Directory"
Yast.import "Mode"
Yast.import "Packages"
Yast.import "ProductControl"
Yast.import "String"
Yast.import "WorkflowManager"
Yast.import "SystemFilesCopy"
Yast.import "InstFunctions"

module Yast
  # Step of base installation finish including mainly files to copy from insts-sys to target system
  # @note This client expect that SCR is not yet switched.
  #   Expect the hell to be frozen if you call it with switched SCR.
  class CopyFilesFinishClient < ::Installation::FinishClient
    include Yast::I18n

    def initialize
      super
      textdomain "installation"
    end

    def modes
      [:installation, :update, :autoinst]
    end

    def title
      _("Copying files to installed system...")
    end

    # @raise RuntimeError if called with switched SCR to have defined behavior and prevent
    #   accidental overwrite and data loss
    def write
      if Yast::WFM.scr_chrooted?
        raise "Calling CopyFilesFinish client with SCR switched to #{Yast::WFM.scr_root}"
      end

      # bugzilla #221815 and #485980
      # Adding blacklisted modules into the /etc/modprobe.d/50-blacklist.conf
      # This should run before the SCR::switch function
      adjust_modprobe_blacklist

      copy_hardware_status
      copy_vnc
      copy_multipath
      copy_nvme
      # Copy cio_ignore whitelist (bsc#1095033)
      copy_active_devices

      handle_second_stage

      # Copy control.xml so it can be read once again during continue mode
      # FIXME: probably won't work as expected with new multimedia layout.
      #   Ideally final modified control.xml should be saved.
      copy_control_file

      # Copy /media.1/media to the installed system (fate#311377)
      # Formerly /media.1/build (bsc#1062297)
      copy_media_file

      # List of files used as additional workflow definitions
      # TODO check if it is still needed
      copy_all_workflow_files

      # Copy files from inst-sys to the just installed system
      # FATE #301937, items are defined in the control file
      SystemFilesCopy.SaveInstSysContent

      # bugzila #328126
      # Copy 70-persistent-cd.rules ... if not updating
      copy_hardware_udev_rules

      # fate#319624
      copy_ssh_files
      # Do not rely on copy_ssh_files returned value (bsc#1099104)
      nil
    end

  private

    def copy_hardware_status
      log.info "Copying hardware information"
      destdir = ::File.join(installation_destination, "/var/lib")
      ::FileUtils.mkdir_p(destdir)
      WFM.Execute(
        path(".local.bash"),
        # BNC #596938: Files / dirs might be symlinks
        "/usr/bin/cp -a --recursive --dereference /var/lib/hardware #{destdir.shellescape}"
      )
    end

    def copy_all_workflow_files
      control_files = WorkflowManager.GetAllUsedControlFiles
      if !control_files || control_files.empty?
        log.info "No additional workflows"
        return
      end

      log.info "Copying additional control files #{control_files.inspect}"
      workflows_list = control_files.map do |one_filename|
        ::File.basename(one_filename)
      end

      # Remove the directory with all additional control files (if exists)
      # and create it again (empty). BNC #471454
      control_files_directory = ::File.join(installation_destination, Directory.etcdir,
        "control_files")
      ::FileUtils.rm_rf(control_files_directory)
      ::FileUtils.mkdir_p(control_files_directory)

      # BNC #475516: Writing the control-files-order index 'after' removing the directory
      # Control files need to follow the exact order, only those liseted here are used
      order_file = ::File.join(control_files_directory, "order.ycp")
      Yast::SCR.Write(
        path(".target.ycp"),
        order_file,
        workflows_list
      )
      ::FileUtils.chmod(0o644, ::File.join(order_file))

      # Now copy all the additional control files to the just installed system
      control_files.each do |file|
        ::FileUtils.cp(file, control_files_directory)
        ::FileUtils.chmod(0o644, ::File.join(control_files_directory, ::File.basename(file)))
      end
    end

    UDEV_RULES_DIR = "/etc/udev/rules.d".freeze

    # see bugzilla #328126
    def copy_hardware_udev_rules
      return if Mode.update

      udev_rules_destdir = ::File.join(installation_destination, UDEV_RULES_DIR)
      ::FileUtils.mkdir_p(udev_rules_destdir)

      # Copy all udev files, but do not overwrite those that already exist
      # on the system bnc#860089
      # They are (also) needed for initrd bnc#666079
      cmd = "/usr/bin/cp -avr --no-clobber #{UDEV_RULES_DIR}/. #{udev_rules_destdir.shellescape}"
      log.info "Copying all udev rules from #{UDEV_RULES_DIR} to #{udev_rules_destdir}"
      cmd_out = WFM.Execute(path(".local.bash_output"), cmd)

      log.error "Error copying udev rules with #{cmd_out.inspect}" if cmd_out["exit"] != 0
    end

    def copy_ssh_files
      log.info "Copying SSH keys and config files"
      ::Installation::SshImporter.instance.write(installation_destination)
    end

    # Function appends blacklisted modules to the /etc/modprobe.d/50-blacklist.conf
    # file.
    #
    # More information in bugzilla #221815 and #485980
    def adjust_modprobe_blacklist
      # check whether we need to run it
      brokenmodules = Linuxrc.InstallInf("BrokenModules")
      if !brokenmodules || brokenmodules.empty?
        log.info "No BrokenModules in install.inf, skipping..."
        return
      end

      # comma-separated list of modules
      blacklisted_modules = brokenmodules.split(", ")

      # run before SCR switch
      blacklist_file = ::File.join(
        installation_destination,
        "/etc/modprobe.d/50-blacklist.conf"
      )

      # read the content
      content = ""
      if ::File.exist?(blacklist_file)
        content = ::File.read(blacklist_file)
      else
        log.warn "File #{blacklist_file} does not exist, installation will create new one"
      end

      # creating new entries with comment
      blacklist_file_append = "# Note: Entries added during installation/update (Bug 221815)\n"
      blacklisted_modules.each do |blacklisted_module|
        blacklist_file_append << "blacklist #{blacklisted_module}\n"
      end

      # newline if the file is not empty
      content << "\n\n" unless content.empty?
      content << blacklist_file_append

      log.info "Blacklisting modules: #{blacklisted_modules} in #{blacklist_file}"
      ::File.write(blacklist_file, content)
    end

    def installation_destination
      ::Yast::Installation.destdir
    end

    def copy_vnc
      # if VNC, copy setup data
      return unless Linuxrc.vnc

      log.info "Copying VNC settings"
      WFM.Execute(
        path(".local.bash"),
        "/bin/cp -a /root/.vnc #{installation_destination.shellescape}/root/"
      )
    end

    # See {https://www.suse.com/support/kb/doc/?id=7001133}
    MULTIPATH_CONFIG_FILES = [
      "/etc/multipath.conf",
      "/etc/multipath/bindings",
      "/etc/multipath/wwids"
    ].freeze

    private_constant :MULTIPATH_CONFIG_FILES

    def copy_multipath
      # Only in install, as update should keep its old config
      return unless Mode.installation

      # Copy multipath stuff (bnc#885628, bsc#1133045)
      MULTIPATH_CONFIG_FILES.each do |config_file|
        next unless File.exist?(config_file)

        log.info "Copying multipath config file: '#{config_file}'"
        target_path = File.join(Installation.destdir, config_file)
        target_dir = File.dirname(target_path)
        ::FileUtils.mkdir_p(target_dir) unless File.exist?(target_dir)
        ::FileUtils.cp(config_file, target_path)
      end
    end

    NVME_CONFIG_FILES = [
      "/etc/nvme/hostnqn",
      "/etc/nvme/hostid"
    ].freeze

    private_constant :NVME_CONFIG_FILES

    def copy_nvme
      # Only in install, as update should keep its config
      return unless Mode.installation

      # Copy NVMe config files (bsc #1172853)
      NVME_CONFIG_FILES.each do |config_file|
        next unless File.exist?(config_file)

        log.info "Copying NVMe config file: '#{config_file}'"
        target_path = File.join(Installation.destdir, config_file)
        target_dir = File.dirname(target_path)
        ::FileUtils.mkdir_p(target_dir) unless File.exist?(target_dir)
        ::FileUtils.cp(config_file, target_path)
      end
    end

    def copy_active_devices
      # Only in install, as update should keep its old config
      return unless Mode.installation
      return unless Arch.s390

      path = "/boot/zipl/active_devices.txt"
      return unless File.exist?(path)

      log.info "Copying zipl active devices '#{path}'"
      target_path = File.join(Installation.destdir, path)
      ::FileUtils.mkdir_p(File.dirname(target_path))
      ::FileUtils.cp(path, target_path)
    end

    def handle_second_stage
      # Copy /etc/install.inf into built system so that the
      # second phase of the installation can find it.
      Linuxrc.SaveInstallInf(Installation.destdir) if InstFunctions.second_stage_required?
    end

    def copy_control_file
      log.info "Copying YaST control file"
      destination = File.join(Installation.destdir, Directory.etcdir, "control.xml")
      ::FileUtils.mkdir_p(File.join(Installation.destdir, Directory.etcdir))
      ::FileUtils.cp(ProductControl.current_control_file, destination)
      ::FileUtils.chmod(0o644, destination)
    end

    def copy_media_file
      media_file = Pkg.SourceProvideOptionalFile(Packages.GetBaseSourceID, 1, "/media.1/media")

      if media_file
        log.info("Copying /media.1/media file to /media")
        media_destination = File.join(Installation.destdir, Directory.etcdir, "media")
        ::FileUtils.cp(media_file, media_destination)
        ::FileUtils.chmod(0o644, media_destination)

        # FIXME: `/build` is deprecated, only kept for backward compatibility. It must be removed
        # as soon it will not longer needed.
        log.info("Copying /media.1/media file to /build (backward compatibility)")
        build_destination = File.join(Installation.destdir, Directory.etcdir, "build")
        ::FileUtils.cp(media_file, build_destination)
        ::FileUtils.chmod(0o644, build_destination)
      else
        log.error("Cannot find /media.1/media file")
      end
    end
  end
end