src/lib/registration/ui/migration_repos_workflow.rb
# ------------------------------------------------------------------------------
# Copyright (c) 2015 SUSE LLC
#
# 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.
#
# ------------------------------------------------------------------------------
#
require "pp"
require "yast"
require "yast2/popup"
require "y2packager/medium_type"
require "y2packager/product_spec"
require "y2packager/product_upgrade"
require "y2packager/resolvable"
require "y2packager/repo_product_spec"
require "registration/registration"
require "registration/registration_ui"
require "registration/migration_repositories"
require "registration/connect_helpers"
require "registration/releasever"
require "registration/sw_mgmt"
require "registration/url_helpers"
require "registration/ui/wizard_client"
require "registration/ui/migration_selection_dialog"
require "registration/ui/migration_repos_selection_dialog"
require "registration/ui/not_installed_products_dialog"
require "registration/rollback_script"
require "registration/storage"
module Registration
module UI
# This class handles workflow for adding migration services
class MigrationReposWorkflow < WizardClient
include Yast::UIShortcuts
Yast.import "Sequencer"
Yast.import "Mode"
Yast.import "Stage"
Yast.import "SourceDialogs"
Yast.import "Linuxrc"
Yast.import "Installation"
Yast.import "AddOnProduct"
Yast.import "InstURL"
Yast.import "Packages"
Yast.import "Pkg"
Yast.import "URL"
Yast.import "WorkflowManager"
# the constructor
def initialize
textdomain "registration"
url = UrlHelpers.registration_url
self.registration = Registration.new(url)
self.registration_ui = RegistrationUI.new(registration)
self.registered_services = []
end
# The repositories migration workflow is:
#
# - if the system is not registered ask the user to register it first
# (otherwise abort the online migration)
# - check registered but not installed products allowing the user to
# syncronize them (skipped in case of not found)
# - find all installed products
# - query registered addons from the server
# - ask the registration server for the available product migrations
# (for both installed and registered products)
# - user selects the migration target
# - the registered products are upgraded and new services/repositories
# are added to the system
# - (optionally) user manually selects the migration repositories
# - user is asked to install also the latest updates (or to migrate to the GA version)
# - "Update from ALL" is set in libzypp (uses all enabled repositories)
# - (optional step) user can manually set the migration repositories,
# the selected repositories are enabled or disabled
# - return the user input symbol (:next or :abort) to the caller
# @return [Symbol] the UI symbol
#
def run_sequence
log.info "Starting migration repositories sequence"
aliases = {
"registration_check" => [->() { registration_check }, true],
"not_installed_products_check" => [->() { not_installed_products_check }, true],
"find_products" => [->() { find_products }, true],
"load_migration_products" => [->() { load_migration_products }, true],
"select_migration_products" => ->() { select_migration_products },
"update_releasever" => ->() { update_releasever },
"register_migration_products" => [->() { register_migration_products }, true],
"activate_migration_repos" => [->() { activate_migration_repos }, true],
"select_migration_repos" => ->() { select_migration_repos },
"select_products" => ->() { select_products },
"store_repos_state" => ->() { store_repos_state }
}
ui = Yast::Sequencer.Run(aliases, WORKFLOW_SEQUENCE)
log.info "User input: #{ui}"
ui
end
private
attr_accessor :products, :migrations, :registration,
:registration_ui, :selected_migration, :registered_services,
:manual_repo_selection
WORKFLOW_SEQUENCE = {
"ws_start" => "registration_check",
"registration_check" => {
abort: :abort,
skip: :next,
next: "not_installed_products_check"
},
"not_installed_products_check" => {
abort: :abort,
cancel: :abort,
next: "find_products"
},
"find_products" => {
abort: :abort,
cancel: :abort,
next: "load_migration_products"
},
"load_migration_products" => {
abort: :abort,
cancel: :abort,
empty: :back,
next: "select_migration_products"
},
"select_migration_products" => {
abort: :abort,
cancel: :abort,
next: "update_releasever"
},
"update_releasever" => {
next: "register_migration_products"
},
"register_migration_products" => {
abort: :rollback,
cancel: :rollback,
next: "activate_migration_repos"
},
"activate_migration_repos" => {
abort: :rollback,
cancel: :rollback,
repo_selection: "select_migration_repos",
next: "select_products"
},
"select_migration_repos" => {
abort: :rollback,
cancel: :rollback,
next: "select_products"
},
"select_products" => {
next: "store_repos_state"
},
"store_repos_state" => {
next: :next
}
}.freeze
# check whether the system is registered, ask the user to register it
# if the system is not registered
# @return [Symbol] workflow symbol, :next if registered, :abort when not
def registration_check
ret = registration_check_at_installation
return ret if ret
return :next if Registration.is_registered?
# TRANSLATORS: a popup message with [Continue] [Cancel] buttons,
# pressing [Continue] starts the registration module, [Cancel] aborts
# the online migration
register = Yast::Popup.ContinueCancel(_("The system is not registered,\n" \
"to run the online migration you need\n" \
"to register the system first."))
return :abort unless register
register_system
end
# check the current registration status
# @return [nil, Symbol] the workflow symbol (:next, :skip) or nil if not
# in an installation
def registration_check_at_installation
# handle system upgrade (fate#323163)
return nil unless Yast::Stage.initial
# test autoupgrade first, Mode.update covers the autoupgrade as well
return registration_check_at_autoupgrade if Yast::Mode.autoupgrade
return system_upgrade_check if Yast::Mode.update
nil
end
def registration_check_at_autoupgrade
Registration.is_registered? ? :next : :skip
end
# run the registration module to register the system
# @return [Symbol] the registration result
def register_system
# temporarily switch back to the normal mode so the registration behaves as expected
mode = Yast::Mode.mode
log.info "Setting 'normal' mode"
Yast::Mode.SetMode("normal")
ret = Yast::WFM.call("inst_scc")
log.info "Registration result: #{ret.inspect}"
log.info "Restoring #{mode.inspect} mode"
Yast::Mode.SetMode(mode)
ret
end
def not_installed_products_check
# FIXME: do the check also at offline upgrade?
# Currently it reads the addons for the new SLES15 which is not
# registered yet and fails.
return :next if Yast::Stage.initial
SwMgmt.init(true)
Addon.find_all(registration)
UI::NotInstalledProductsDialog.run
end
# find all installed products
# @return [Symbol] workflow symbol (:next or :abort)
def find_products
log.info "Loading installed products"
self.products = ::Registration::SwMgmt.installed_products.map do |product|
# report the installed products without the version release (bsc#1079051#c11)
::Registration::SwMgmt.remote_product(product, version_release: false)
end
if products.empty?
# TRANSLATORS: Error message
Yast::Report.Error(_("No installed product found."))
return :abort
end
# do not read addons when upgrading from an NCC based system (SLE11)
ncc_file = File.join(Yast::Installation.destdir,
SUSE::Connect::YaST::DEFAULT_CREDENTIALS_DIR, "NCCcredentials")
merge_registered_addons unless File.exist?(ncc_file)
log.info "Products to migrate: #{products}"
:next
end
# ask the user about adding not installed addons to the current products
#
# @return [Array<Hash>] installed products and addons selected to be installed
def merge_registered_addons
return unless load_addons
# TRANSLATORS: Popup question, merge this addon that are registered but not
# installed to the current migration products list.
# %s is an addon friendly name, e.g 'SUSE Enterprise Storage 2 x86_64'
msg = _("The '%s' extension is registered but not installed.\n" \
"If you accept it will be added for be installed, in other case " \
"it will be unregistered at the end of the migration.\n\n" \
"Do you want to add it?")
addons =
Addon.registered_not_installed.each_with_object([]) do |addon, result|
next unless Yast::Mode.auto || Yast::Popup.YesNoHeadline(
addon.friendly_name, (msg % addon.friendly_name)
)
result << SwMgmt.remote_product(addon.to_h)
end
products.concat(addons)
end
# Tries to load the registered addons
#
# Registration errors are shown to the user, allowing to retry in some cases.
#
# @return [Boolean] true on success
def load_addons
ConnectHelpers.catch_registration_errors do
# load the extensions to merge the registered but not installed extensions
Addon.find_all(registration)
end
end
# load migration products for the installed products from the registration server,
# loads online or offline migrations depending on the system state
# @return [Symbol] workflow symbol (:next or :abort)
def load_migration_products
# just for easier debugging log the currently registered products on the server
activated = registration_ui.activated_products
log.info("Activated products on this system: #{activated.map(&:friendly_name).inspect}")
log.info("Activated: #{activated.inspect}")
if Yast::Stage.initial
load_migration_products_offline(activated)
else
load_migration_products_online
end
end
# load migration products for the installed products from the registration server
# for the currently running system (online migration)
# @return [Symbol] workflow symbol (:next or :abort)
def load_migration_products_online
log.info "Loading online migration products from the server..."
self.migrations = registration_ui.migration_products(products)
if migrations.empty?
# TRANSLATORS: Error message
Yast::Report.Error(_("No migration product found."))
return :abort
end
:next
end
# load migration products for the installed products from the registration server
# on a system that is not running (offline migration)
# @return [Symbol] workflow symbol (:next or :abort)
def load_migration_products_offline(activations)
base_product = Y2Packager::ProductUpgrade.new_base_product
if !base_product
# TRANSLATORS: Error message
Yast::Report.Error(_("Cannot find a base product to upgrade."))
return :empty
end
return :empty unless migration_confirmed?(base_product, activations)
remote_product = OpenStruct.new(
arch: base_product.arch.to_s,
identifier: base_product.name,
version: SwMgmt.version_without_release(base_product),
# FIXME: not supported by Y2Packager::Product yet
release_type: nil
)
load_migrations_for_products(products, remote_product)
end
def load_migrations_for_products(products, remote_product)
log.info "Loading offline migrations for target product: #{remote_product.inspect}"
log.info "Installed products: #{products.inspect}"
self.migrations = registration_ui.offline_migration_products(products, remote_product)
if migrations.empty?
msg = [
# TRANSLATORS. Error message
_("No migration product found."),
# TRANSLATORS: Help message, %{product} is the product name
_("Please, boot the original system and make sure " \
"that all registerable products are correctly registered.\n" \
"Also check that the installed system is supported for upgrade to \n" \
"%{product}.") % { product: Y2Packager::ProductUpgrade.new_base_product.display_name }
]
Yast::Report.Error(msg.join("\n\n"))
return Yast::Mode.auto ? :abort : :empty
end
:next
end
# check if the target producte has been already activated, if yes
# require user confirmation for the migration (though it will very likely fail)
# @param base_product [Y2Packager::Product] the base product to which migrate to
# @param activations [Array<SUSE::Connect::Remote::Product>] already activated
# products on the server
# @return [Boolean] true = continue with the migration, false = stop
def migration_confirmed?(base_product, activations)
# split the release version, e.g. "15-0" => ["15", "0"]
base_product_version, _release = base_product.version.split("-", 2)
activated_base = activations.find do |a|
a.identifier == base_product.name && a.version == base_product_version
end
return true unless activated_base
log.warn("Found already activated base product: #{activated_base.inspect}")
# TRANSLATORS: Continue/Cancel popup question, confirm the migration by user,
# %s is a product name, e.g. "SUSE Linux Enterprise Server 15"
message = _("The product %s\nis already activated on this system.\n\n" \
"Migrating again to the same product might not work properly.") % base_product.label
ui = Yast2::Popup.show(message, buttons: :continue_cancel, focus: :cancel)
log.info("Use input: #{ui.inspect}")
ui == :continue
end
# run the migration target selection dialog
# @return [Symbol] workflow symbol (:next or :abort)
def select_migration_products
return select_migration_products_autoyast if Yast::Mode.auto
log.info "Displaying migration target selection dialog"
dialog = MigrationSelectionDialog.new(migrations, SwMgmt.installed_products)
ret = dialog.run
if ret == :next
self.selected_migration = dialog.selected_migration
self.manual_repo_selection = dialog.manual_repo_selection
log.info "Selected migration: #{selected_migration}"
end
ret
end
# Select the migration product in the AutoYaST mode
# @return [Symbol] workflow symbol (:next)
def select_migration_products_autoyast
# TODO: for now simply select the first found migration (bsc#1087206#c7)
# later we can improve this (either choose the migration with higher product versions
# or allow selecting the migration in the AY profile)
self.selected_migration = migrations.first
self.manual_repo_selection = false
log.warn "More than one migration available, using the first one" if migrations.size > 1
log.info "Selected migration: #{selected_migration}"
:next
end
# collect products to migrate
# @return [Array<Hash>] installed or registered products
def products_to_migrate
installed_products = SwMgmt.installed_products
log.info "installed_products: #{installed_products}"
registered_products = Addon.registered_not_installed.map do |addon|
ret = addon.to_h
ret["display_name"] = addon.friendly_name
ret
end
installed_products.concat(registered_products)
end
# upgrade the services to the new version
# @return [Symbol] workflow symbol (:next)
def register_migration_products
migration_progress
install_rollback_script if Yast::Stage.initial
begin
log.info "Registering the migration target products"
if !selected_migration.all? { |product| register_migration_product(product) }
return :abort
end
ensure
Yast::Wizard.EnableNextButton
Yast::Wizard.EnableBackButton
end
# synchronize the changes done by modifying the services,
# reinitialize the repositories
Yast::Pkg.SourceFinishAll
Yast::Pkg.TargetFinish
SwMgmt.init
# check the repositories (and possibly disable the invalid repositories)
return :abort unless SwMgmt.check_repositories
# reload the available packages
Yast::Pkg.SourceLoad
log.info "Registered services: #{registered_services}"
:next
ensure
# clear the progress message
Yast::Wizard.ClearContents
end
# just set an empty Wizard dialog to replace the current one after
# pressing "Next"
def migration_progress
Yast::Wizard.SetContents(
_("Registration"),
# TRANSLATORS: progress message
Label(_("Preparing Migration Repositories...")),
"",
false,
false
)
end
# register a migration product
# @return [Boolean] true on success
def register_migration_product(product)
log.info "Registering migration product #{product}"
ret = nil
Yast::Popup.Feedback(RegistrationUI::CONTACTING_MESSAGE,
# TRANSLATORS: Progress label
_("Updating to %s ...") % product.friendly_name) do
ret = ConnectHelpers.catch_registration_errors do
registered_services << registration.upgrade_product(product)
end
end
ret
end
# activate the added migration repos (set the DUP property)
# @return [Symbol] the UI symbol (:abort, :next or :repo_selection)
def activate_migration_repos
log.info "Activating the migration repositories"
migration_repos = ::Registration::MigrationRepositories.new
registered_services.each do |service|
migration_repos.services << service
end
if Yast::Mode.auto
migration_repos.install_updates = ::Registration::Storage::Config.instance.install_updates
elsif migration_repos.service_with_update_repo?
migration_repos.install_updates = registration_ui.install_updates?
end
migration_repos.activate_services
manual_repo_selection ? :repo_selection : :next
end
# run the manual migration repository selection dialog
# @return [Symbol] the UI symbol (:abort, :next)
def select_migration_repos
UI::MigrationReposSelectionDialog.run
end
# Select products for migration
#
# It causes the *-release packages to be installed (see bsc#1086818 for
# further details).
def select_products
SwMgmt.select_addon_products(registered_services)
:next
end
def store_repos_state
RepoStateStorage.instance.write
:next
end
# update the $releasever
def update_releasever
new_base = selected_migration.find(&:isbase)
if new_base
log.info "Activating new $releasever for base product: #{new_base}"
releasever = Releasever.new(new_base.version)
releasever.activate
else
log.info "The base system is not updated, skipping $releasever update"
end
:next
end
# check the system status at upgrade and return the symbol for the next step
# @return [Symbol] workflow symbol, :skip => do not use the SCC/SMT/RMT upgrade
# (unregistered system or explicitly requested by user), :next =>
# continue with the SCC/SMT/RMT based upgrade
def system_upgrade_check
log.info "System upgrade mode detected"
# media based upgrade requested by user
if Yast::Linuxrc.InstallInf("MediaUpgrade") == "1"
# we cannot do medium based upgrade using the Online medium,
# it does not contain any packages
if Y2Packager::MediumType.online?
log.warn "The Online medium cannot be used with the media_upgrade=1 boot option"
Yast::Popup.LongMessage(online_media_upgrade)
return :abort
end
# add the Full medium base product repository
add_offline_base_product
explicit_media_upgrade
return :skip
# the system is registered, continue with the SCC/SMT/RMT based upgrade
elsif Registration.is_registered?
log.info "The system is registered, using the registration server for upgrade"
# act during upgrade for registered system like online even when medium is Full
if Yast::Stage.initial && Y2Packager::MediumType.offline?
Y2Packager::MediumType.type = :online
end
return :next
elsif Y2Packager::MediumType.online?
log.warn "The system is NOT registered for online medium. Stopping"
Yast::Popup.LongMessage(unregistered_online_message)
return :abort
else
# the system is unregistered we can only upgrade via media
unregistered_media_upgrade
return :skip
end
end
# explicit media upgrade, requested via boot option
def explicit_media_upgrade
log.info "Skipping SCC upgrade, media based upgrade requested"
if Registration.is_registered?
Yast::Popup.LongMessageGeometry(media_upgrade(true), 60, 15)
else
Yast::Popup.LongMessage(media_upgrade(false))
end
prepare_media_upgrade
end
# implicit media upgrade for an unregistered system
def unregistered_media_upgrade
log.info "The system is NOT registered, activating the media based upgrade"
if Y2Packager::MediumType.offline?
# Add the Full medium base product repository
# TODO: move somewhere else?
add_offline_base_product
else
# FIXME: can be removed when old media format removed from SLE as opensuse
# does not have registration and online is handled above
# we do not support registering the old system at upgrade, that must
# be done before the upgrade, skip registration in that case
Yast::Popup.LongMessage(unregistered_message)
prepare_media_upgrade
end
end
def prepare_media_upgrade
# do not display the "I would like to install an additional Add On Product"
# check box, allow adding the upgrade media directly
Yast::SourceDialogs.display_addon_checkbox = false
# preselect the DVD repository type
Yast::SourceDialogs.SetURL("dvd://")
end
# Add and initialize the respective base product repository from the Full medium
# for upgrade, select the new base product to install.
def add_offline_base_product
# find the installed base product
installed = Y2Packager::Resolvable.find(kind: :product, status: :installed)
# Resolvable.find cannot filter by product type, run the filter here
installed_base = installed.find { |p| p.type == "base" }
if !installed_base
log.error("Installed base product not found")
return
end
new_base = full_medium_upgrade_product(installed_base)
if !new_base
log.error("New base product not found")
return
end
log.info("Upgrading product #{installed_base.name.inspect} to #{new_base.inspect}")
add_full_medium_repository(new_base.name, new_base.dir)
end
#
# Scan the installation repository and return the found products
#
# This method is called during offline installation, so it is expected to return
# RepoProductSpec objects only.
#
# @return [Array<Y2Packager::RepoProductSpec>] the products found in the offline medium
def full_medium_products
products = Y2Packager::ProductSpec.base_products
.select { |p| p.is_a?(Y2Packager::RepoProductSpec) }
log.info("Found base products on the offline medium: #{products.pretty_inspect}")
# in SP6+ always use the new product mapping
new_migration = true
log.info "Using SP6+ product upgrade mapping: #{new_migration}"
Y2Packager::ProductUpgrade.new_renames = new_migration
Yast::AddOnProduct.new_renames = new_migration
products
end
#
# Find the upgrade base product from the Full medium for the installed
# base product.
#
# @param installed_base [<Y2Packager::Resolvable>] the installed base product
# @return [Y2Packager::ProductSpec, nil] The location of the new base product on
# the Full installation medium, nil if no suitable upgrade product is found
def full_medium_upgrade_product(installed_base)
new_base = nil
base_products = full_medium_products
installed_names = Y2Packager::Resolvable.find(kind: :product, status: :installed)
.map(&:name)
# first check if there is a product rename defined for the installed products
# and the new renamed base product is available
# key in the mapping: list of installed products, value: the new base product
Y2Packager::ProductUpgrade.mapping.each do |k, v|
if (k - installed_names).empty?
new_base = base_products.find { |p| p.name == v }
end
end
# if no product rename was found then use the 1:1 upgrade
new_base ||= base_products.find do |p|
p.name == installed_base.name
end
log.info("Found upgrade product on the full medium: #{new_base}")
new_base
end
#
# Initialize the base product repository and select the base product
# to install.
#
# @param new_product [String] Name of the product to install
# @param dir [String] Directory on the medium with the selected base product repository
def add_full_medium_repository(new_product, dir)
# FIXME: this is the same as in installation/widgets/product_selector.rb and other places
show_popup = true
url = Yast::InstURL.installInf2Url("")
log_url = Yast::URL.HidePassword(url)
Yast::Packages.Initialize_StageInitial(show_popup, url, log_url, dir)
# select the product to install
Yast::Pkg.ResolvableInstall(new_product, :product, "") if new_product
# initialize addons and the workflow manager
Yast::AddOnProduct.SetBaseProductURL(url)
Yast::WorkflowManager.SetBaseWorkflow(false)
end
# Informative message
# @return [String] translated message
def unregistered_message
# TRANSLATORS: Unregistered system message (1/3)
# Message displayed during upgrade for unregistered systems.
# The user can either boot the old system and register it or use the
# DVD media for upgrade. Use the RichText format.
_("<h2>Unregistered System</h2><p>The system is not registered, that means " \
"the installer cannot add the new software repositories required for migration " \
"automatically.</p>") +
# TRANSLATORS: Unregistered system message (2/3)
_("<p>Please add the installation media manually in the next step.</p>") +
# TRANSLATORS: Unregistered system message (3/3)
_("<p>If you cannot provide the installation media you can abort the migration " \
"and boot the original system to register it. Then start the migration again.</p>")
end
# Informative message
# @return [String] translated message
def unregistered_online_message
# TRANSLATORS: Unregistered system message (1/3)
# Message displayed during upgrade for unregistered systems.
# The user can either boot the old system and register it or use the
# Full media for upgrade. Use the RichText format.
_("<h2>Unregistered System</h2><p>The system is not registered, that means " \
"the installer cannot add the new software repositories required for migration " \
"automatically.</p>") +
# TRANSLATORS: Unregistered system message (2/3)
_("<p>Please use Full media instead of Online one.</p>") +
# TRANSLATORS: Unregistered system message (3/3)
_("<p>Another option is booting the original system and registering it.</p>")
end
# Informative message
# @return [String] translated message
def online_media_upgrade
# TRANSLATORS: Media upgrade forced but the Online medium was detected (1/2)
# Message displayed during upgrade when the Online medium is used
# with the "media_upgrade=1" boot option. The Online medium does not
# contain any packages and it cannot be used in this case, Full medium
# is required. Use the RichText format.
_("<h2>Online Medium</h2><p>The media based upgrade was requested, "\
"but you are using the Online medium which does not provide any packages "\
"to install.</p>") +
# TRANSLATORS: Force media upgrade, Online medium detected (2/2)
_("<p>Please use the Full medium instead of the Online.</p>")
end
# Informative message
# @param registered [Boolean] is the system registered?
# @return [String] translated message
def media_upgrade(registered)
# TRANSLATORS: Media based upgrade requested by user (1/3)
# User requested media based upgrade which does not use SCC/SMT/RMT
# but the downloaded media (physical DVD or shared repo on a local server).
ret = _("<h2>Media Based Upgrade</h2><p>The media based upgrade is requested. " \
"In this mode YaST will not contact the registration server to obtain " \
"the new software repositories required for migration.</p>") +
# TRANSLATORS: Media based upgrade requested by user (2/3)
_("<p>Please add the installation media manually in the next step.</p>")
return ret unless registered
# TRANSLATORS: a warning message, upgrading the registered systems
# using media is not supported
ret + _("<h2>Warning!</h2><p><b>The media based upgrade for registered " \
"systems is not supported!<b></p>") +
_("<p>If you upgrade the system using media the registration status " \
"will not be updated and the system will be still registered " \
"using the previous product. The packages from the registration " \
"repositories can conflict with the new packages.</p>")
end
# install the rollback script so the registration is rolled back
# when the upgrade is aborted or YaST crashes
def install_rollback_script
rollback = RollbackScript.new(root: Yast::Installation.destdir)
if rollback.applicable?
rollback.create
Storage::Cache.instance.rollback = rollback
else
log.info("The rollback script is not applicable")
end
end
end
end
end