yast/yast-installation

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

Summary

Maintainability
D
1 day
Test Coverage
# ------------------------------------------------------------------------------
# Copyright (c) 2006-2012 Novell, Inc. 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 Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail, you may find
# current contact information at www.novell.com.
# ------------------------------------------------------------------------------

require "installation/minimal_installation"

require "shellwords"

Yast.import "UI"
Yast.import "Pkg"

Yast.import "AddOnProduct"
Yast.import "WorkflowManager"
Yast.import "Installation"
Yast.import "Linuxrc"
Yast.import "Misc"
Yast.import "Mode"
Yast.import "Stage"
Yast.import "Popup"
Yast.import "ProductControl"
Yast.import "Progress"
Yast.import "Report"
Yast.import "Wizard"
Yast.import "String"
Yast.import "GetInstArgs"
Yast.import "ProductFeatures"
Yast.import "SlideShow"
Yast.import "InstError"
Yast.import "PackageCallbacks"
Yast.import "Hooks"

# added for fate# 303395
Yast.import "Directory"

module Yast
  class InstFinishClient < Client
    include Yast::Logger

    def main
      textdomain "installation"

      return :auto if GetInstArgs.going_back

      setup_wizard
      setup_slide_show

      init_packager

      aborted = !write

      finish_slide_show

      if aborted
        Builtins.y2milestone("inst_finish aborted")
        return :abort
      end

      report_hooks

      report_messages
      handle_kexec

      :next
    end

  private

    def write
      stages.each_with_index do |stage, index|
        current_stage_percent = 100 * index / stages.size
        SlideShow.StageProgress(
          current_stage_percent,
          stage["label"] || ""
        )
        SlideShow.AppendMessageToInstLog(stage["label"] || "")
        steps_nr = stage["steps"].size
        stage["steps"].each_with_index do |step, step_index|
          # a fallback busy message
          fallback_msg = Builtins.sformat(
            _("Calling step %1..."),
            step["client"]
          )
          SlideShow.SubProgress(
            100 * step_index / steps_nr,
            step["title"] || fallback_msg
          )
          SlideShow.StageProgress(
            current_stage_percent + ((100 / stages.size) * step_index / steps_nr),
            nil
          )
          # use as ' * %1' -> ' * One of the finish steps...' in the SlideShow log
          SlideShow.AppendMessageToInstLog(
            Builtins.sformat(
              _(" * %1"),
              step["title"] || fallback_msg
            )
          )
          orig = Progress.set(false)

          Hooks.run "before_#{step["client"]}"

          WFM.CallFunction(step["client"], ["Write"])

          Hooks.run "after_#{step["client"]}"

          Progress.set(orig)
          # Handle user input during client run
          user_ret = UI.PollInput
          # Aborting...?
          if user_ret == :abort
            return false if Popup.ConfirmAbort(:incomplete)
          # Anything else
          else
            SlideShow.HandleInput(user_ret)
          end
        end
        SlideShow.SubProgress(100, nil)
      end

      true
    end

    def report_messages
      return if Misc.boot_msg.empty?
      return if Mode.autoinst

      # --------------------------------------------------------------
      # Check if there is a message left to display
      # and display it, if necessary

      # Do not call any SCR, it's already closed!
      # bugzilla #245742, #160301
      if Linuxrc.reboot_timeout
        Report.DisplayMessages(true, Linuxrc.reboot_timeout)
      else
        # Display the message and wait for user to accept it
        # also live installation - bzilla #297691
        Report.DisplayMessages(true,
          ((Linuxrc.usessh && !Linuxrc.vnc) || Mode.live_installation) ? 0 : 10)
      end
      Report.LongMessage(Misc.boot_msg)
      Misc.boot_msg = ""
    end

    def handle_kexec
      # fate #303395: Use kexec to avoid booting between first and second stage
      # run new kernel via kexec instead of reboot

      # command for reading kernel_params
      cmd = "/usr/bin/ls #{Directory.vardir.shellescape}/kexec_done | /usr/bin/tr -d '\n'"
      log.info "Checking flag of successful loading kernel via command #{cmd}"

      out = WFM.Execute(path(".local.bash_output"), cmd)

      expected_output = "#{Directory.vardir}/kexec_done"

      # check output
      if out["stdout"] != expected_output
        log.info "File kexec_done was not found, output: #{out}"
        return
      end

      # HACK: using kexec switch to console 1
      cmd = "/usr/bin/chvt 1"
      log.info "Switch to console 1 via command: #{cmd}"
      # switch to console 1
      out = WFM.Execute(path(".local.bash_output"), cmd)
      # check output
      if out["exit"] != 0
        log.error "Switching failed, output: #{out}"
        return
      end

      # waiting s for switching...
      sleep(1)
    end

    def report_hooks
      used_hooks = Hooks.all.select(&:used?)
      failed_hooks = used_hooks.select(&:failed?)

      if !failed_hooks.empty?
        log.error "#{failed_hooks.size} failed hooks found: " \
                  "#{failed_hooks.map(&:name).join(", ")}"
      end

      log.info "Hook summary:" unless used_hooks.empty?

      used_hooks.each do |hook|
        log.info "Hook name: #{hook.name}"
        log.info "Hook result: #{hook.succeeded? ? "success" : "failure"}"
        hook.files.each do |file|
          log.info "Hook file: #{file.path}"
          log.info "Hook output: #{file.output}"
        end
      end

      show_used_hooks(used_hooks) unless failed_hooks.empty?
    end

    def show_used_hooks(hooks)
      content = Table(
        Id(:hooks_table),
        Opt(:notify),
        Header("Hook name", "Result", "Output"),
        hooks.map do |hook|
          Item(
            Id(:hook),
            hook.name,
            hook.failed? ? "failure" : "success",
            hook.files.map(&:output).reject(&:empty?).join
          )
        end
      )
      Builtins.y2milestone "Showing the hooks results in UI"
      Popup.LongText(
        "Hooks results",
        content,
        # the width and hight numbers reflect subjective visual appearance of the popup
        80, 5 + hooks.size
      )
    end
    # --> Functions

    def ReportClientError(client_error_text)
      # get the latest errors
      cmd = Convert.to_map(
        WFM.Execute(
          path(".local.bash_output"),
          "/usr/bin/tail -n 200 /var/log/YaST2/y2log | /usr/bin/grep ' <\\(3\\|5\\)> '"
        )
      )

      text = cmd["stdout"] if cmd["exit"] == 0 && !cmd["stdout"].empty?
      InstError.ShowErrorPopUp(
        _("Installation Error"),
        client_error_text,
        text
      )

      nil
    end

    def setup_wizard
      Wizard.DisableBackButton
      Wizard.DisableNextButton
    end

    def setup_slide_show
      # Adjust a SlideShow dialog if not configured
      if [nil, {}].include?(SlideShow.GetSetup)
        Builtins.y2milestone("No SlideShow setup has been set, adjusting")
        SlideShow.Setup(
          [
            {
              "name"        => "finish",
              "description" => _("Finishing Basic Installation"),
              # fixed value
              "value"       => 100,
              "units"       => :sec
            }
          ]
        )
      end

      # Do not open a new SlideShow widget, reuse the old one instead
      # variable used later to close that dialog (if needed)
      @required_to_open_sl_dialog = !SlideShow.HaveSlideWidget

      if @required_to_open_sl_dialog
        Builtins.y2milestone("SlideShow dialog not yet created")
        SlideShow.OpenDialog
      end

      # Might be left from the previous stage
      SlideShow.HideTable

      SlideShow.MoveToStage("finish")

      log = _("Creating list of finish scripts to call...")
      SlideShow.SubProgress(0, "")
      SlideShow.StageProgress(0, log)
      SlideShow.AppendMessageToInstLog(log)
    end

    def finish_slide_show
      SlideShow.StageProgress(100, nil)
      SlideShow.AppendMessageToInstLog(_("Finished"))

      return unless @required_to_open_sl_dialog

      log.info "Closing previously opened SlideShow dialog"
      SlideShow.CloseDialog
    end

    def init_packager
      # Used later in 'stages' definition
      # Using empty callbacks that don't break the UI
      PackageCallbacks.RegisterEmptyProgressCallbacks
      Pkg.TargetInitialize(Installation.destdir)
      Pkg.TargetLoad
      PackageCallbacks.RestorePreviousProgressCallbacks
    end

    COPY_FILES_STEPS =
      [
        "autoinst_scripts1",
        "copy_files",
        "live_copy_files",
        "switch_scr"
      ].freeze

    def copy_files_steps
      COPY_FILES_STEPS
    end

    SAVE_CONFIG_STEPS_MINIMAL =
      [
        "save_config",
        "live_save_config",
        "storage",
        "kernel"
      ].freeze

    SAVE_CONFIG_STEPS_FULL =
      [
        "ldconfig",
        "save_config",
        "live_save_config",
        "default_target",
        "desktop",
        "storage",
        "iscsi-client",
        "fcoe-client",
        "kernel",
        "x11",
        "proxy",
        "scc",
        "driver_update1",
        # bnc #340733
        "system_settings"
      ].freeze

    def save_config_steps
      if ::Installation::MinimalInstallation.instance.enabled?
        SAVE_CONFIG_STEPS_MINIMAL
      else
        SAVE_CONFIG_STEPS_FULL
      end
    end

    SAVE_SETTINGS_STEPS_MINIMAL =
      [
        "yast_inf",
        "autoinst_scripts2",
        "installation_settings"
      ].freeze

    SAVE_SETTINGS_STEPS_FULL =
      [
        "yast_inf",
        "network",
        "security",
        "ntp-client",
        "ssh_settings",
        "remote",
        "save_hw_status",
        "users",
        "autoinst_files",
        "autoinst_scripts2",
        "installation_settings",
        "roles",
        "services",
        "services-manager",
        "pkg", # Some _finish clients might still need Pkg calls (e.g. users) (bsc#1128385)
        # *.repo files must be written to the installed system (bsc#1177522)
        "configuration_management"
      ].freeze

    def save_settings_steps
      if ::Installation::MinimalInstallation.instance.enabled?
        SAVE_SETTINGS_STEPS_MINIMAL
      else
        SAVE_SETTINGS_STEPS_FULL
      end
    end

    def install_bootloader_steps
      if ::Installation::MinimalInstallation.instance.enabled?
        ["bootloader"]
      else
        [
          "cio_ignore", # needs to be run before initrd is created (bsc#933177)
          (ProductFeatures.GetBooleanFeature("globals", "enable_kdump") == true) ? "kdump" : "",
          "bootloader"
        ]
      end
    end

    def control_stages
      log.info "Using inst_finish steps definition from control file"
      stages = deep_copy(ProductControl.inst_finish)

      # Inst-finish need to be translated (#343783)
      textdom = Ops.get_string(
        ProductControl.productControl,
        "textdomain",
        "control"
      )

      log.info "Inst finish stages before: #{stages}"

      stages.each do |stage|
        label = stage["label"]
        next if label.nil? || label == ""

        loc_label = Builtins.dgettext(textdom, label)
        # if translated
        stage["label"] = loc_label if !loc_label.nil? && loc_label != "" && loc_label != label
      end

      log.info "Inst finish stages after: #{stages}"

      stages
    end

    def predefined_stages
      log.info "inst_finish steps definition not found in control file"

      [
        {
          "id"    => "copy_files",
          # progress stage
          "label" => _("Copy files to installed system"),
          "steps" => copy_files_steps
        },
        {
          "id"    => "save_config",
          # progress stage
          "label" => _("Save configuration"),
          "steps" => save_config_steps
        },
        {
          "id"    => "save_settings",
          # progress stage
          "label" => _("Save installation settings"),
          "steps" => save_settings_steps
        },
        # bnc#860089: Save bootloader as late as possible
        # all different (config) files need to be written and copied first
        {
          "id"    => "install_bootloader",
          # progress stage
          "label" => _("Install boot manager"),
          "steps" => install_bootloader_steps
        },
        {
          "id"    => "prepare_for_reboot",
          # progress stage
          "label" => _("Prepare system for initial boot"),
          "steps" => [
            # For live installer only
            Mode.live_installation ? "live_runme_at_boot" : "",
            # vm_finish called only if yast2-vm is installed
            # Can't use PackageSystem::Installed as the current SCR is attached to inst-sys
            # instead of the installed system
            Pkg.PkgInstalled("yast2-vm") ? "vm" : "",
            # copy logs just before 'umount'
            # keeps maximum logs available after reboot
            "copy_logs",
            # no second stage if possible
            "pre_umount",
            "snapshots",
            "driver_update2",
            "umount"
          ]
        }
      ]
    end

    def merge_addon_steps(stages)
      # merge steps from add-on products
      # bnc #438678
      stages[0]["steps"] =
        WorkflowManager.GetAdditionalFinishSteps("before_chroot") + stages[0]["steps"]
      stages[1]["steps"] =
        WorkflowManager.GetAdditionalFinishSteps("after_chroot") + stages[1]["steps"]
      stages[3]["steps"].concat(WorkflowManager.GetAdditionalFinishSteps("after_chroot"))
    end

    def run_type
      return @run_type if @run_type

      @run_type = if Mode.update
        :update
      elsif Mode.autoinst
        :autoinst
      elsif Mode.live_installation
        :live_installation
      else
        :installation
      end

      @run_type
    end

    def keep_only_valid_steps(stage)
      steps = stage["steps"].map do |s|
        # some steps are called in live installer only
        next nil if s == "" || s.nil?

        s += "_finish"
        if !WFM.ClientExists(s)
          log.warn "Missing YaST client: #{s}"
          next nil
        end
        log.info "Calling inst_finish script: #{s} (Info)"
        orig = Progress.set(false)
        info = WFM.CallFunction(s, ["Info"])
        Progress.set(orig)
        if info.nil?
          log.error "Client #{s} returned invalid data"
          ReportClientError(
            Builtins.sformat(_("Client %1 returned invalid data."), s)
          )
          next nil
        end
        if info["when"] && !info["when"].include?(run_type) &&
            # special hack for autoupgrade - should be as regular upgrade as possible,
            # scripts are the only exception
            !(Mode.autoupgrade && info["when"].include?(:autoupg))
          next nil
        end

        log.info "inst_finish client #{s} will be called"
        info["client"] = s

        info
      end
      stage["steps"] = steps.compact
    end

    def stages
      return @stages if @stages

      # FIXME: looks like product specific finish steps are not used at all
      stages = if ProductControl.inst_finish.empty?
        predefined_stages
      else
        control_stages
      end

      merge_addon_steps(stages)

      stages.each_with_index do |stage, index|
        SlideShow.SubProgress(
          100 * (index + 1) / stages.size,
          Builtins.sformat(_("Checking stage: %1..."), stage["label"] || stage["id"] || "")
        )
        keep_only_valid_steps(stage)
      end

      log.info "These inst_finish stages will be called:"
      stages.each { |stage| log.info "Stage: #{stage}" }

      stages.delete_if { |s| s["steps"].empty? }

      @stages = stages
    end
  end
end