src/lib/registration/sw_mgmt.rb
# encoding: utf-8
# ------------------------------------------------------------------------------
# Copyright (c) 2014 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 "yast"
require "tmpdir"
require "fileutils"
require "ostruct"
require "registration/exceptions"
require "registration/helpers"
require "registration/url_helpers"
require "registration/repo_state"
require "registration/storage"
require "packager/product_patterns"
require "y2packager/medium_type"
require "y2packager/product_spec"
require "y2packager/product_reader"
require "yast2/execute"
require "y2packager/resolvable"
module Registration
Yast.import "Arch"
Yast.import "AddOnProduct"
Yast.import "Mode"
Yast.import "Stage"
Yast.import "Pkg"
Yast.import "Report"
Yast.import "PackageLock"
Yast.import "Installation"
Yast.import "PackageCallbacks"
Yast.import "OSRelease"
Yast.import "Popup"
Yast.import "Product"
class SwMgmt
include Yast
include Yast::Logger
extend Yast::I18n
textdomain "registration"
ZYPP_DIR = "/etc/zypp".freeze
FAKE_BASE_PRODUCT = {
"arch" => "x86_64",
"display_name" => "SUSE Linux Enterprise Desktop 15 SP4",
"flavor" => "",
"name" => "SLED",
"product_line" => "sled",
"register_release" => "",
"register_target" => "sle-15-x86_64",
"version" => "15.4-0",
"version_version" => "15.4"
}.freeze
OEM_DIR = "/var/lib/suseRegister/OEM".freeze
# initialize the package management
# @param [Boolean] load_packages load also the available packages from the repositories
def self.init(load_packages = false)
# false = do not allow continuing without the libzypp lock
lock = PackageLock.Connect(false)
# User would like to abort
raise_pkg_exception(PkgAborted) if lock["aborted"]
# locking has failed
raise_pkg_exception unless lock["connected"]
# display progress when refreshing repositories
PackageCallbacks.InitPackageCallbacks
raise_pkg_exception unless init_target(Installation.destdir)
raise_pkg_exception unless Pkg.TargetLoad
raise_pkg_exception(SourceRestoreError) unless Pkg.SourceRestore
raise_pkg_exception if load_packages && !Pkg.SourceLoad
end
# try refreshing all enabled repositories with autorefresh enabled
# and report repositories which fail, ask the user to disable them or to abort
# @return [Boolean] true = migration can continue, false = abort migration
def self.check_repositories
# only enabled repositories
repos = Pkg.SourceGetCurrent(true)
repos.each do |repo|
data = Pkg.SourceGeneralData(repo)
# skip repositories which have autorefresh disabled
next unless data["autorefresh"]
log.info "Refreshing repository #{data["alias"].inspect}"
next if Pkg.SourceRefreshNow(repo)
# TRANSLATORS: error popup, %s is a repository name, the popup is displayed
# when a migration repository cannot be accessed, there are [Skip]
# and [Abort] buttons displayed below the question
question = _("Repository '%s'\ncannot be loaded.\n\n"\
"Skip the repository or abort?") % data["name"]
ret = Popup.ErrorAnyQuestion(Label.ErrorMsg, question, Label.SkipButton,
Label.AbortButton, :focus_yes)
log.info "Abort online migration: #{ret}"
return false unless ret
# disable the repository
log.info "Disabling repository #{data["alias"].inspect}"
Pkg.SourceSetEnabled(repo, false)
# make sure the repository is enabled again after migration
RepoStateStorage.instance.add(repo, true)
end
true
end
# Prepare a pkg-binding product hash using the requested name and version,
# if the version is empty the fallback from the /etc/os-release file is used.
#
# @param self_update_id [String] product name to be used for get the installer updates
# @return [Hash,nil] with pkg-binding format; return nil if the
# given self_update_id is empty
def self.installer_update_base_product(self_update_id, version = "")
return if self_update_id.empty?
product_info = {
"name" => self_update_id,
"arch" => Yast::Arch.rpm_arch,
"version" => version,
"release_type" => nil
}
log.info("Base product for installer update: #{product_info}")
product_info
end
# Converts Y2Packager::Resolvable product to hash
# @param [Y2Packager::Resolvable] product
# @return [Hash] The product hash
def self.resolvable_to_h(product)
{ "name" => product.name,
"version_version" => product.version_version,
"version" => product.version,
"arch" => product.arch,
"display_name" => product.display_name,
"register_target" => product.register_target,
"register_release" => product.register_release,
"product_line" => product.product_line,
"flavor" => product.flavor }
end
# Product to register for the online installation medium
# @return [Hash] The product
def self.online_base_product
if Mode.update
prods = Y2Packager::ProductSpec.base_products
installed_names = installed_products.map { |p| p["name"] }
prod = prods.find { |p| installed_names.include?(p.name) }
log.info "selecting product from control #{prod}"
raise "No base product selected from control.xml matching installed products!" unless prod
else
prod = Y2Packager::ProductSpec.selected_base
raise "No base product selected from control.xml!" unless prod
end
{ "name" => prod.name,
"version_version" => prod.version,
"arch" => prod.arch,
"display_name" => prod.label,
"register_target" => prod.register_target }
end
# Product to register for the online installation medium
# @return [Hash] base product
def self.find_base_product
# FIXME: refactor the code to use Y2Packager::Product
# just for debugging:
return FAKE_BASE_PRODUCT if ENV["FAKE_BASE_PRODUCT"]
return online_base_product if Stage.initial && Y2Packager::MediumType.online?
yaml_product = Storage::InstallationOptions.instance.yaml_product
return yaml_product if yaml_product
# use the selected product if a product has been already selected
selected = product_selected? if Stage.initial
installed = product_installed? if Stage.initial
# list of products defined by the "system-installation()" provides
system_products = Y2Packager::ProductReader.installation_package_mapping.keys
log.info("Found system-installation() products: #{system_products.inspect}")
# during installation the products are :selected,
# on a running system the products are :installed
# during upgrade use the newer selected product (same as in installation)
# The base product must be marked by the "system-installation()" provides
# by some package.
products = Y2Packager::Resolvable.find(kind: :product).find_all do |p|
if Stage.initial && !system_products.include?(p.name)
log.info("Skipping product #{p.name}, no system-installation() provides")
next false
end
evaluate_product(p, selected, installed)
end
log.debug "Found base products: #{products}"
log.info "Found base products: #{products.map(&:name)}"
log.warn "More than one base product found!" if products.size > 1
products.first ? resolvable_to_h(products.first) : {}
end
def self.installed_base_product
reader = Y2Packager::ProductReader.new
reader.installed_base_product.resolvable_properties
end
# Evaluate the product if it is a base product depending on the current
# system status.
# @param p [Y2Packager::Resolvable] the product from pkg-bindings
# @param selected [Boolean,nil] is any product selected?
# @param installed [Boolean,nil] is any product istalled?
# @return [Boolean] true if it is a base product
def self.evaluate_product(p, selected, installed)
if Stage.initial && Mode.auto
Yast.import "AutoinstFunctions"
# note: AutoinstFunctions.selected_product should never be nil when
# AY let it pass here
return false unless AutoinstFunctions.selected_product
p.name == AutoinstFunctions.selected_product.name
elsif Stage.initial && !Mode.update
# during installation the ["type"] value is not valid yet yet
# (the base product is determined by /etc/products.d/baseproduct symlink)
# use the selected or available product
p.status == (selected ? :selected : :available)
elsif Stage.initial
# during upgrade it depends on whether target is already initialized,
# use the product from the medium for the self-update step
# (during upgrade the installed product might me already selected for removal)
if installed
(p.status == :installed || p.status == :removed) && p.type == "base"
elsif selected
p.status == :selected
else
p.status == :available
end
else
# in installed system or at upgrade the base product has valid type
(p.status == :installed || p.status == :removed) && p.type == "base"
end
end
private_class_method :evaluate_product
# Any product selected to install?
# @return [Boolean] true if at least one product is selected to install
def self.product_selected?
Y2Packager::Resolvable.any?(kind: :product, status: :selected)
end
# Any product installed? (e.g. during upgrade)
# @return [Boolean] true if at least one product is installed
def self.product_installed?
Y2Packager::Resolvable.any?(kind: :product, status: :installed) ||
Y2Packager::Resolvable.any?(kind: :product, status: :removed)
end
# Evaluates all installed products
# @return [Array<Hash>] product Hash
def self.installed_products
# just for testing/debugging
return [FAKE_BASE_PRODUCT] if ENV["FAKE_BASE_PRODUCT"]
all_products = Y2Packager::Resolvable.find(kind: :product)
log.info("Evaluating products: #{all_products.map(&:name)}")
products = all_products.select do |p|
# installed or installed marked for removal (at upgrade)
p.status == :installed || p.status == :removed
end
log.info "Found installed products: #{products.map(&:name)}"
products.map { |p| resolvable_to_h(p) }
end
# convert a libzypp Product Hash to a SUSE::Connect::Remote::Product object
# @param product [Hash] product Hash obtained from pkg-bindings
# @return [SUSE::Connect::Remote::Product] the remote product
def self.remote_product(product, version_release: true)
# default value if it does not exist
product["version_version"] ||= product["version"]
OpenStruct.new(
arch: product["arch"],
identifier: product["name"],
# the "version_version" key does not contain the release number,
# e.g. if "version" is "11.4-1.109" then "version_version" is just "11.4"
version: version_release ? product["version"] : product["version_version"],
release_type: product["release_type"]
)
end
# remove relase string from version. E.g.: "15.3-0" --> "15.3"
# @param product [Y2Packager::ProductSpec] Product specification
# @return [String] version withouth the "release" part
def self.version_without_release(product)
pkg_product = Y2Packager::Resolvable.find(kind: :product,
name: product.name, version: product.version).first
pkg_product ? pkg_product.version_version : product.version
end
# create UI label for a base product
# @param base_product [Hash] Product (hash from pkg-bindings)
# @return [String] UI Label
def self.product_label(base_product)
base_product["display_name"] ||
base_product["short_name"] ||
base_product["name"] ||
_("Unknown product")
end
# Find base product to register
# @return [Hash] base product or nil if not found
def self.base_product_to_register
base_product = find_base_product
return if !base_product || base_product.empty?
# filter out not needed data
product_info = {
"name" => base_product["name"],
"arch" => base_product["arch"],
"version" => base_product["version_version"],
"release_type" => get_release_type(base_product)
}
log.info("Base product to register: #{product_info}")
product_info
end
# add the services to libzypp and load (refresh) them
def self.add_service(product_service, credentials)
# save repositories before refreshing added services (otherwise
# pkg-bindings will treat them as removed by the service refresh and
# unload them)
if !Pkg.SourceSaveAll
# error message
raise ::Registration::PkgError, N_("Saving repository configuration failed.")
end
# services for registered products
log.info "Adding service #{product_service.name.inspect} (#{product_service.url})"
credentials_file = UrlHelpers.credentials_from_url(product_service.url)
if credentials_file
if Mode.update || Yast::WFM.scr_chrooted?
# at update libzypp is already switched to /mnt target,
# update the path accordingly
credentials_file = File.join(Installation.destdir,
::SUSE::Connect::YaST::DEFAULT_CREDENTIALS_DIR,
credentials_file)
log.info "Using #{credentials_file} credentials path"
end
# SCC uses the same credentials for all services, just save them to
# a different file
SUSE::Connect::YaST.create_credentials_file(credentials.username,
credentials.password, credentials_file)
end
service_name = product_service.name
# add a new service or update the existing service
if Pkg.ServiceAliases.include?(service_name)
log.info "Updating existing service: #{service_name}"
if !Pkg.ServiceSet(service_name,
"alias" => service_name,
"name" => service_name,
"url" => product_service.url.to_s,
"enabled" => true,
"autorefresh" => true)
## error message
raise ::Registration::ServiceError.new(N_("Updating service '%s' failed."), service_name)
end
else
log.info "Adding new service: #{service_name}"
if !Pkg.ServiceAdd(service_name, product_service.url.to_s)
# error message
raise ::Registration::ServiceError.new(N_("Adding service '%s' failed."), service_name)
end
if !Pkg.ServiceSet(service_name, "autorefresh" => true)
# error message
raise ::Registration::ServiceError.new(N_("Updating service '%s' failed."), service_name)
end
end
# refresh works only for saved services
if !Pkg.ServiceSave(service_name)
# error message
raise ::Registration::ServiceError.new(N_("Saving service '%s' failed."), service_name)
end
# Force refreshing due timing issues (bnc#967828)
if !Pkg.ServiceForceRefresh(service_name)
# error message
raise ::Registration::ServiceError.new(N_("Refreshing service '%s' failed."), service_name)
end
ensure
Pkg.SourceSaveAll
end
# remove a libzypp service and save the repository configuration
# @param [String] name name of the service to remove
def self.remove_service(name)
log.info "Removing service #{name}"
if Pkg.ServiceDelete(name) && !Pkg.SourceSaveAll
# error message
raise ::Registration::PkgError, N_("Saving repository configuration failed.")
end
end
# Check if a libzypp service is installed
# @param [String] name name of the service
def self.service_installed?(name)
ret = Pkg.ServiceAliases.include?(name)
log.info "Service #{name} is installed: #{ret}"
ret
end
# get list of repositories belonging to registered services
# @param product_service [SUSE::Connect::Remote::Service] added service
# @param only_updates [Boolean] return only update repositories
# @return [Array<Hash>] list of repositories
def self.service_repos(product_service, only_updates: false)
repo_data = Pkg.SourceGetCurrent(false).map { |repo| repository_data(repo) }
service_name = product_service.name
log.info "Service name: #{service_name.inspect}"
# select only repositories belonging to the product services
repos = repo_data.select { |repo| service_name == repo["service"] }
log.info "Service repositories: #{repos}"
if only_updates
# leave only update repositories
repos.select! { |repo| repo["is_update_repo"] }
log.info "Found update repositories: #{repos}"
end
repos
end
# get repository data
# @param [Fixnum] repo repository ID
# @return [Hash] repository properties, including the repository ID ("SrcId" key)
def self.repository_data(repo)
data = Pkg.SourceGeneralData(repo)
data["SrcId"] = repo
data
end
# Set repository state (enabled/disabled)
# The original repository state is saved to RepoStateStorage to restore
# the original state later.
# @param repos [Array<Hash>] list of repositories
# @param enabled [Boolean] true = enable, false = disable, nil = no change
# @return [void]
def self.set_repos_state(repos, enabled)
# keep the defaults when not defined
return if enabled.nil?
repos.each do |repo|
next if repo["enabled"] == enabled
# remember the original state
RepoStateStorage.instance.add(repo["SrcId"], repo["enabled"])
log.info "Changing repository state: #{repo["name"]} enabled: #{enabled}"
Pkg.SourceSetEnabled(repo["SrcId"], enabled)
end
end
# copy old NCC/SCC credentials from the old installation to new SCC credentials
# the files are copied to the root of the current system (/), at installation
# the credentials are copied to the target (/mnt) at the beginning of the
# installation (in the inst_kickoff.rb client)
def self.copy_old_credentials(source_dir)
log.info "Searching registration credentials in #{source_dir}..."
dir = SUSE::Connect::YaST::DEFAULT_CREDENTIALS_DIR
# create the target directory if missing
if !File.exist?(dir)
log.info "Creating directory #{dir}"
::FileUtils.mkdir_p(dir)
end
# if the system contains both NCC and SCC credentials then the SCC ones
# should be preferred (bsc#1096813)
# take advantage that "NCCcredentials" is alphabetically before
# "SCCcredentials" so it is enough to just sort the files and then the
# SCC credentials will simply overwrite the NCC credentials
Dir[File.join(source_dir, dir, "*")].sort.each do |path|
# skip non-files
next unless File.file?(path)
# check for the NCC credentials, we need to save them as the SCC credentials
new_path = if File.basename(path) == "NCCcredentials"
SUSE::Connect::YaST::GLOBAL_CREDENTIALS_FILE
else
File.join(dir, File.basename(path))
end
copy_old_credentials_file(path, new_path)
end
end
private_class_method def self.copy_old_credentials_file(file, new_file)
log.info "Copying the old credentials from previous installation"
log.info "Copying #{file} to #{new_file}"
# SMT/RMT uses extra ACL permissions, make sure they are kept in the copied file,
# (use "cp -a ", ::FileUtils.cp(..., preserve: true) cannot be used as it preserves only
# the traditional Unix file permissions, the extended ACLs are NOT copied!)
Yast::Execute.locally!("cp", "-a", file, new_file)
use_credentials(new_file)
rescue Cheetah::ExecutionFailed => error
log.warn "Cannot copy the old credentials file #{file} to #{new_file}: #{error.message}"
end
# Use credentials from a file
#
# @param filename [String] credentials filename.
# @return [Boolean] true if credentials can be used; false otherwise.
def self.use_credentials(filename)
credentials = SUSE::Connect::YaST.credentials(filename)
log.info "Using previous credentials (username): #{credentials.username}"
true
rescue SUSE::Connect::MalformedSccCredentialsFile => e
log.warn "Cannot parse the credentials file: #{e.inspect}"
false
end
private_class_method :use_credentials
def self.find_addon_updates(addons)
log.info "Available addons: #{addons.map(&:identifier)}"
products = Y2Packager::Resolvable.find(kind: :product)
installed_addons = products.select do |product|
(product.status == :installed || product.status == :removed) &&
product.type != "base"
end
product_names = installed_addons.map { |a| "#{a.name}-#{a.version}" }
log.info "Installed addons: #{product_names}"
ret = addons.select do |addon|
installed_addons.any? do |installed_addon|
addon.updates_addon?("name" => installed_addon.name)
end
end
log.info "Found addons to update: #{ret.map(&:identifier)}"
ret
end
# update the static defaults in AddOnProduct module
def self.update_product_renames(renames)
renames.each do |old_name, new_name|
AddOnProduct.add_rename(old_name, new_name)
end
end
# a helper method for iterating over repositories
# @param repo_aliases [Array<String>] list of repository aliases
# @param block block evaluated for each found repository
private_class_method def self.each_repo(repo_aliases, &block)
all_repos = Pkg.SourceGetCurrent(false)
repo_aliases.each do |repo_alias|
# find the repository with the alias
repository = all_repos.find do |repo|
Pkg.SourceGeneralData(repo)["alias"] == repo_alias
end
if repository
block.call(repository)
else
log.warn "Repository '#{repo_alias}' was not found, skipping"
end
end
end
# select products for new added extensions/modules
#
# @param addon_services [Array<SUSE::Connect::Remote::Service] List of services
# If it is not specified, it falls back to {Registration::Storage::Cache#addon_services}.
# @return [Boolean] true on success
def self.select_addon_products(addon_services = nil)
addon_services ||= ::Registration::Storage::Cache.instance.addon_services
log.info "New addon services: #{addon_services}"
new_repos = addon_services.reduce([]) do |acc, service|
acc.concat(::Registration::SwMgmt.service_repos(service))
end
return true if new_repos.empty?
products = Y2Packager::Resolvable.find(kind: :product)
products.select! do |product|
product.status == :available &&
new_repos.any? { |new_repo| product.source == new_repo["SrcId"] }
end
products.map!(&:name)
log.info "Products to install: #{products}"
ret = products.all? { |product| Pkg.ResolvableInstall(product, :product) }
select_default_product_patterns unless Mode.update
# run the solver to recalculate the product statuses
Pkg.PkgSolve(false)
ret
end
# Select default product patterns
def self.select_default_product_patterns
# preselect the default product patterns (FATE#320199)
# note: must be called *after* selecting the products
product_patterns = ProductPatterns.new
log.info "Selecting the default product patterns: #{product_patterns.names}"
product_patterns.select
end
# select remote addons matching the product resolvables
# @param products [Array<Hash>] products
# @param addons [Array<Addon>] addons
def self.select_product_addons(products, addons)
addons.each do |addon|
log.info "Found remote addon: #{addon.identifier}-#{addon.version}-#{addon.arch}"
end
# select a remote addon for each product
products.each do |product|
remote_addon = addons.find do |addon|
product["name"] == addon.identifier &&
product["version_version"] == addon.version &&
product["arch"] == addon.arch
end
if remote_addon
remote_addon.selected
else
product_label = "#{product["display_name"]} (#{product["name"]}" \
"-#{product["version_version"]}-#{product["arch"]})"
# TRANSLATORS: %s is a product name
Report.Error(_("Cannot find remote product %s.\n" \
"The product cannot be registered.") % product_label)
end
end
end
# find the product resolvables from the specified repository
# @return [Array<Hash>] products in Hash format
def self.products_from_repo(repo_id)
# TODO: only installed products??
products = Y2Packager::Resolvable.find(kind: :product, source: repo_id)
products.map { |p| resolvable_to_h(p) }
end
# Return release type
# @param product [Hash] Product (hash from pkg-bindings)
# @return [String] release type
def self.get_release_type(product)
if product["product_line"] && !product["product_line"].empty?
oem_file = File.join(OEM_DIR, product["product_line"])
if File.exist?(oem_file)
# read only the first line
line = File.open(oem_file, &:readline)
return line.chomp if line
end
end
product["register_release"]
end
def self.raise_pkg_exception(klass = PkgError)
raise klass, Pkg.LastError
end
# initialize the libzypp target
# @param destdir [String] the target directory
# @return [Boolean] true on sucess, false otherwise
def self.init_target(destdir)
if Stage.initial && Mode.update
# at upgrade we need to override the target_distro otherwise libzypp
# will use the old value from the upgraded system which might not
# match the new target_distro from the media and might result in ignoring
# service repositories (bsc#1094865)
options = { "target_distro" => target_distribution(destdir) }
Pkg.TargetInitializeOptions(destdir, options)
else
Pkg.TargetInitialize(destdir)
end
end
# get the target distribution for the new base product
# @param destdir [String] the target directory
# @return [String] target distribution name or empty string if not found
def self.target_distribution(destdir)
if Y2Packager::MediumType.online?
control_products = Y2Packager::ProductSpec.base_products
target_distro = if control_products.empty?
""
else
# curently all products have the same "register_target" value
control_products.first.register_target || ""
end
else
# ensure the target is initialized
Pkg.TargetInitialize(destdir)
# the sources are initialized by the Product.FindBaseProducts call internally
base_products = Product.FindBaseProducts
# empty target distribution disables service compatibility check in case
# the base product cannot be found
target_distro = base_products ? base_products.first["register_target"] : ""
# Save the current repositories so they are not lost
Pkg.SourceSaveAll
# close both target and sources to fully reinitialize later
Pkg.SourceFinishAll
Pkg.TargetFinish
end
log.info "Base product target distribution: #{target_distro.inspect}"
target_distro
end
private_class_method :init_target, :target_distribution
end
end