yast/yast-migration

View on GitHub
src/lib/migration/main_workflow.rb

Summary

Maintainability
B
5 hrs
Test Coverage
# ------------------------------------------------------------------------------
# Copyright (c) 2015-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 about this file by physical or electronic mail, you may find
# current contact information at www.suse.com.
# ------------------------------------------------------------------------------

require "yast"

Yast.import "Mode"
Yast.import "Sequencer"
Yast.import "Update"
Yast.import "Report"
Yast.import "Pkg"
Yast.import "Installation"
Yast.import "OSRelease"
Yast.import "PackageCallbacks"

require "migration/finish_dialog"
require "migration/restarter"
require "migration/patches"

module Migration
  # The goal of the class is to provide main single entry point to start
  # migration work-flow. It is UI oriented sequence.
  #
  class MainWorkflow
    include Yast::Logger
    include Yast::I18n

    FIND_CONFIG_CMD = "/usr/bin/snapper --no-dbus --csvout list-configs " \
    "--columns config,subvolume | grep \"^root,\" >/dev/null".freeze

    CREATE_SNAPSHOT_CMD = "/usr/bin/snapper create --type=%{snapshot_type} " \
      "--cleanup-algorithm=number --print-number --userdata important=yes " \
      "--description=\"%{description}\"".freeze

    # a temporary vendor configuration file
    VENDOR_FILE = "/etc/zypp/vendors.d/YaST_openSUSE_migration.conf".freeze

    # content of the vendor configuation file,
    # make the "openSUSE" and "SUSE" vendors equal, allow to change
    # the vendor from "openSUSE" to "SUSE"
    VENDOR_CONTENT = "[main]\nvendors=openSUSE,SUSE\n".freeze

    def self.run
      workflow = new
      workflow.run
    end

    def run
      textdomain "migration"
      Yast::Mode.SetMode("update")

      begin
        Yast::Wizard.CreateDialog
        Yast::Sequencer.Run(aliases, WORKFLOW_SEQUENCE)
      ensure
        vendor_cleanup
        Yast::Wizard.CloseDialog
      end
    end

  private

    # remember the "pre" snapshot id (needed for the "post" snapshot)
    attr_accessor :pre_snapshot

    WORKFLOW_SEQUENCE = {
      "ws_start"                => "start",
      "start"                   => {
        start:                   "create_pre_snapshot",
        restart_after_update:    "online_update",
        restart_after_migration: "migration_finish"
      },
      "create_pre_snapshot"     => {
        next: "create_backup"
      },
      "create_backup"           => {
        next: "online_update"
      },
      "online_update"           => {
        abort:   :abort,
        restart: "restart_after_update",
        next:    "repositories"
      },
      "restart_after_update"    => {
        restart: :restart
      },
      "repositories"            => {
        abort:    :abort,
        rollback: "rollback",
        next:     "license"
      },
      "license"                 => {
        abort: "rollback",
        next:  "proposals"
      },
      "proposals"               => {
        abort: "rollback",
        next:  "perform_migration"
      },
      "rollback"                => {
        abort: :abort,
        next:  :next
      },
      "perform_migration"       => {
        abort: :abort,
        next:  "restart_after_migration"
      },
      "restart_after_migration" => {
        restart: :restart
      },
      # note: the steps after the YaST restart use the new code from
      # the updated (migrated) yast2-migration package!!
      "migration_finish"        => {
        abort: :abort,
        next:  "create_post_snapshot"
      },
      "create_post_snapshot"    => {
        next: "finish_dialog"
      },
      "finish_dialog"           => {
        abort: :abort,
        next:  :next
      }
    }.freeze

    def aliases
      {
        "start"                   => ->() { start },
        "create_pre_snapshot"     => ->() { create_pre_snapshot },
        "create_backup"           => ->() { create_backup },
        "online_update"           => ->() { online_update },
        "restart_after_update"    => ->() { restart_yast(:restart_after_update) },
        "rollback"                => ->() { rollback },
        "perform_migration"       => ->() { perform_migration },
        "proposals"               => ->() { proposals },
        "repositories"            => ->() { repositories },
        "license"                 => ->() { license },
        "restart_after_migration" => ->() { restart_yast(:restart_after_migration) },
        # note: the steps after the YaST restart use the new code from
        # the updated (migrated) yast2-migration package!!
        "migration_finish"        => ->() { migration_finish },
        "create_post_snapshot"    => ->() { create_post_snapshot },
        "finish_dialog"           => ->() { finish_dialog }
      }
    end

    def create_backup
      Yast::Update.clean_backup
      Yast::Update.create_backup(
        "repos",
        [
          "/etc/zypp/repos.d",
          "/etc/zypp/credentials.d",
          "/etc/zypp/services.d"
        ]
      )

      :next
    end

    def online_update
      return :abort unless init_pkg_mgmt

      patches = Patches.new
      return :next unless patches.available?

      # TRANSLATORS: popup question, confirm installing the available updates now
      question = _("There are some online updates available to installation,\n" \
          "it is recommended to install all updates before proceeding.\n\n" \
          "Would you like to install the updates now?")
      return :next unless Yast::Popup.YesNo(question)

      ui = patches.install
      # user canceled installing patches, continue without them...
      return :next if ui == :cancel || ui == :abort

      :restart
    end

    def rollback
      Yast::WFM.CallFunction("registration_sync")
      :abort
    end

    def repositories
      prepare_repos
      Yast::WFM.CallFunction("migration_repos", [{ "enable_back" => false }])
    end

    # display license for the new base product
    def license
      Yast::WFM.CallFunction("inst_product_upgrade_license", [{ "enable_back" => false }])
    end

    def proposals
      Yast::WFM.CallFunction("migration_proposals", [{ "hide_export" => true }])
    end

    def perform_migration
      # disable the default snapshots created by the zypp plugin
      ENV["DISABLE_SNAPPER_ZYPP_PLUGIN"] = "1"

      # this client is located in the yast2-installation package
      Yast::WFM.CallFunction("inst_prepareprogress")
      # this client is located in the yast2-packager package
      Yast::WFM.CallFunction("inst_kickoff")
      # this client is located in the yast2-packager package
      ret = Yast::WFM.CallFunction("inst_rpmcopy")
      log.info "inst_rpmcopy result: #{ret.inspect}"

      display_abort_message if ret == :abort || ret == :cancel

      ret
    end

    def display_abort_message
      Yast::Report.Error(
        # TRANSLATORS: an error message, the migration to a new service pack failed
        # or was aborted, the system is in partly migrated state and should be restored
        # from a snapshot or backup
        _("The migration to the new service pack has failed. The system is most\n" \
            "likely in an inconsistent state.\n" \
            "\n" \
            "We strongly recommend to rollback to a snapshot created before the\n" \
            "migration was started (via selecting the snapshot in the boot menu\n" \
            "if you use snapper) or restore the system from a backup.")
      )
    end

    def create_pre_snapshot
      if snapper_configured?
        self.pre_snapshot = perform_snapshot(:pre, "before online migration")
      end

      :next
    end

    def create_post_snapshot
      if snapper_configured? && pre_snapshot
        perform_snapshot(:post, "after online migration", pre_snapshot)
      end

      :next
    end

    def migration_finish
      # this client is located in the yast2-registration package
      Yast::WFM.CallFunction("migration_finish")
    end

    # check whether snapper is configured
    # @return [Boolean] true if snapper is configured
    def snapper_configured?
      out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"),
        FIND_CONFIG_CMD)

      log.debug "Checking snapper config: '#{FIND_CONFIG_CMD}'"
      log.info "Found snapper config: #{out}"

      out["exit"] == 0
    end

    # create a filesystem snapshot
    # @param [Symbol, String] type the type of the snapshot (:single, :pre or :post)
    # @param [String] desc description of the snapshot for users
    # @param [Fixnum] pre_id id of the respective "pre" snapshot (needed
    #   only for "post" type snapshots)
    # @return [Fixnum,nil] id of the created snapshot (nil if failed)
    def perform_snapshot(type, desc, pre_id = nil)
      cmd = format(CREATE_SNAPSHOT_CMD, snapshot_type: type, description: desc)
      cmd << " --pre-number=#{pre_id}" if pre_id

      log.info "Creating snapshot: #{cmd}"
      out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd)

      if out["exit"] == 0
        ret = out["stdout"].to_i
        log.info "Created snapshot: #{ret}"
        return ret
      end

      log.error "Snapshot could not be created: #{out}"
      Yast::Report.Error(_("Failed to create a filesystem snapshot."))
      nil
    end

    # display the finish dialog and optionally reboot the system
    # @return [Symbol] UI user input
    def finish_dialog
      dialog = Migration::FinishDialog.new
      ret = dialog.run

      if ret == :next && dialog.reboot
        log.info "Preparing the system for reboot..."
        Restarter.instance.reboot
      end

      ret
    end

    # evaluate the starting point for the workflow, start from the beginning
    # or continue after restarting the YaST
    # return [Symbol] workflow symbol
    def start
      return :start unless Restarter.instance.restarted

      # reload the stored snapshot id (from the previous run)
      if Restarter.instance.data.is_a?(Hash)
        self.pre_snapshot = Restarter.instance.data[:pre_snapshot]
        step = Restarter.instance.data[:step]
        return step if step
      end

      log.warn "No saved step found, starting from the beginning"
      :start
    end

    # schedule YaST restart
    # @param [String] step current step in the workflow
    # @return [Symbol] workflow symbol (always :restart)
    def restart_yast(step)
      # save the snapshot for later (after restart)
      Restarter.instance.restart_yast(pre_snapshot: pre_snapshot, step: step)
      :restart
    end

    # Initialize the package manager (libzypp)
    def init_pkg_mgmt
      # the vendor configuration file must be created *before* initializing libzypp
      vendor_init

      # display progress when refreshing repositories
      Yast::PackageCallbacks.InitPackageCallbacks

      ret = Yast::Pkg.TargetInitialize(Yast::Installation.destdir) &&
        Yast::Pkg.TargetLoad &&
        Yast::Pkg.SourceRestore &&
        Yast::Pkg.SourceLoad

      Yast::Report.Error(Yast::Pkg.LastError) unless ret

      ret
    end

    # Enable the openSUSE => SUSE vendor change when migrating from
    # an openSUSE distribution to SLE.
    def vendor_init
      File.write(VENDOR_FILE, VENDOR_CONTENT) if opensuse?
    end

    # Remove the vendor configuration file
    def vendor_cleanup
      File.delete(VENDOR_FILE) if File.exist?(VENDOR_FILE)
    end

    # Prepare the repositories for the online migration.
    # This mainly activates some workarounds in the openSUSE => SLE migration
    def prepare_repos
      return unless opensuse?

      # disable all enabled repositories, the migration only removes (upgrades)
      # the repositories from the registration server (SCC), all other repositories
      # (like the default OSS and non-OSS repos) would collide with the new SLES repos
      Yast::Pkg.SourceGetCurrent(true).each { |r| Yast::Pkg.SourceSetEnabled(r, false) }
    end

    # Running in an openSUSE distribution?
    #
    # @return [Boolean] True if running in an openSUSE distribution, false otherwise
    def opensuse?
      Yast::OSRelease.id.match?(/opensuse/i)
    end
  end
end