Skip to content

Commit

Permalink
Added ProductUpgrade class (bsc#1086259)
Browse files Browse the repository at this point in the history
It finds the base product for upgrade.
  • Loading branch information
lslezak committed Mar 28, 2018
1 parent 2b19183 commit 0371869
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 6 deletions.
1 change: 1 addition & 0 deletions library/packages/src/Makefile.am
Expand Up @@ -35,6 +35,7 @@ y2packager_DATA = \
lib/y2packager/product.rb \
lib/y2packager/product_reader.rb \
lib/y2packager/product_sorter.rb \
lib/y2packager/product_upgrade.rb \
lib/y2packager/release_notes.rb \
lib/y2packager/release_notes_content_prefs.rb \
lib/y2packager/release_notes_reader.rb \
Expand Down
29 changes: 27 additions & 2 deletions library/packages/src/lib/y2packager/product.rb
Expand Up @@ -42,9 +42,20 @@ class Product
attr_reader :installation_package

class << self
# Return all known products
# Create a product from pkg-bindings hash data.
# @param p [Hash] the pkg-binindgs product hash
# @return [Y2Packager::Product] converted product
def from_h(p)
Y2Packager::Product.new(
name: p["name"], short_name: p["short_name"], display_name: p["display_name"],
version: p["version"], arch: p["arch"], category: p["category"],
vendor: p["vendor"]
)
end

# Return all known available products
#
# @return [Array<Product>] Known products
# @return [Array<Product>] Known available products
def all
Y2Packager::ProductReader.new.all_products
end
Expand All @@ -56,6 +67,20 @@ def available_base_products
Y2Packager::ProductReader.new.available_base_products
end

# Return the installed base product
#
# @return [Product,nil] Installed base product or nil if not found
def installed_base_product
Y2Packager::ProductReader.new.installed_base_product
end

# Return all installed products (including the base product)
#
# @return [Product,nil] Installed products
def installed_products
Y2Packager::ProductReader.new.all_installed_products
end

# Returns the selected base product
#
# It assumes that at most 1 base product can be selected.
Expand Down
52 changes: 48 additions & 4 deletions library/packages/src/lib/y2packager/product_reader.rb
Expand Up @@ -94,6 +94,21 @@ def available_base_products
products
end

# Read the installed base product
# @return [Y2Packager::Product,nil] the installed base product or nil if not found
def installed_base_product
base = base_product
return nil unless base

Y2Packager::Product.from_h(base)
end

# All installed products
# @return [Array<Y2Packager::Product>] the product list
def all_installed_products
installed_products.map { |p| Y2Packager::Product.from_h(p) }
end

def product_package(name, repo_id)
return nil unless name
Yast::Pkg.ResolvableDependencies(name, :package, "").find do |prod|
Expand All @@ -105,12 +120,9 @@ def product_package(name, repo_id)

# read the available products, remove potential duplicates
# @return [Array<Hash>] pkg-bindings data structure
def available_products
def zypp_products
products = Yast::Pkg.ResolvableProperties("", :product, "")

# remove e.g. installed products
products.select! { |p| p["status"] == :available || p["status"] == :selected }

# remove duplicates, there migth be different flavors ("DVD"/"POOL")
# or archs (x86_64/i586), when selecting the product to install later
# libzypp will select the correct arch automatically
Expand All @@ -120,6 +132,38 @@ def available_products
products
end

# read the available products, remove potential duplicates
# @return [Array<Hash>] pkg-bindings data structures
def available_products
# remove e.g. installed products
zypp_products.select { |p| p["status"] == :available || p["status"] == :selected }
end

# read the installed products
# @return [Array<Hash>] pkg-bindings data structures
def installed_products
# remove e.g. available or removed products
zypp_products.select { |p| p["status"] == :installed || p["status"] == :removed }
end

# find the installed base product
# @return[Hash,nil] the pkg-bindings product structure or nil if not found
def base_product
products = Yast::Pkg.ResolvableProperties("", :product, "")

# The base product is identified by the /etc/products.d/baseproduct symlink
# and because a symlink can point only to one file there can be only one base product.
# The "status" conditition is actually not required because that symlink is created
# only for installed products. (Just make sure it still works in case the libzypp
# internal implementation is changed.)
base = products.find do |p|
p["type"] == "base" && (p["status"] == :installed || p["status"] == :removed)
end

log.info("Found installed base product: #{base}")
base
end

def installation_package_mapping
self.class.installation_package_mapping
end
Expand Down
171 changes: 171 additions & 0 deletions library/packages/src/lib/y2packager/product_upgrade.rb
@@ -0,0 +1,171 @@
# ------------------------------------------------------------------------------
# Copyright (c) 2018 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.
# ------------------------------------------------------------------------------

require "y2packager/product"

module Y2Packager
# Evaluate the installed and available products and find the new upgraded
# product tp install.
class ProductUpgrade
include Yast::Logger

Yast.import "Pkg"

# fallback mapping with upgraded products to handle some corner cases,
# maps installed products to a new base product
MAPPING = {
# SLES12 + HPC module => SLESHPC15
# (a bit tricky, the module became a new base product!)
["SLES", "sle-module-hpc"] => "SLES_HPC",
# this is an internal product so far...
["SLE-HPC"] => "SLES_HPC",
# SLES11 => SLES15
["SUSE_SLES"] => "SLES",
# SLED11 => SLED15
["SUSE_SLED"] => "SLED",
# SLES4SAP11 => SLES4SAP15
["SUSE_SLES_SAP"] => "SLES_SAP"
}.freeze

class << self
# Find a new available base product which upgrades the installed base product.
#
# The workflow to find the new base product is:
#
# 1) If there is only one available base product then just use it,
# there are no other options than to upgrade to this product.
#
# 2) Let the solver to evaluate the product versions, their dependencies,
# Obsoletes/Provides, ... and find the correct upgrade candidate.
#
# However, this step is quite fragile as the solver evaluates *all*
# packages, not just the products. That means the solver might fail
# because of some unrelated package dependency issue and cannot
# find the correct upgrade candidate. That's more likely when using
# custom or 3rd party packages.
#
# If the solver fails then we try some fallback mechanisms for finding
# the new product.
#
# 3) Use a harcoded fallback mapping with the list of installed products
# mapped to a new base product product. The static mapping is needed to
# handle some corner cases properly. This includes product renames or
# changing a module to a base product.
#
# 4) As the last attempt try to find the installed base product in the
# available base products. It is very likely that SLES will be upgraded
# to SLES, SLED to SLED and so on.
#
# If no candidate product is found then it returns nil. That should happen
# only when using completely incompatible products.
#
# @return [Y2Packager::Product,nil] the new upgraded product
def new_base_product
available = Y2Packager::Product.available_base_products
return nil if available.empty?

# just one product?
product = find_by_count(available)
return product if product

# found by solver?
product = find_by_solver
return product if product

# found by hardcoded mapping?
product = find_by_mapping(available)
return product if product

# just 1:1 product upgrade?
find_by_name(available)
end

private

# check the count of the new base products
# @param available [Array<Y2Packager::Product>] the available base products
# @return [Y2Packager::Product,nil] the new upgraded product
def find_by_count(available)
return nil unless available.size == 1

# only one base product available, we can upgrade only to this product
log.info("Only one base product available: #{available.first}")
available.first
end

# We do not know which available product might upgrade the installed product
# if the installation medium contains several products.
# Temporarily turn on the update mode to let the solver select the product for upgrade,
# this will correctly handle possible product renames specified via Obsoletes/Provides.
# @return [Y2Packager::Product,nil] the new upgraded product
def find_by_solver
# store the current resolvable states
Yast::Pkg.SaveState

# run the solver in the upgrade mode
Yast::Pkg.PkgUpdateAll({})
log_products

product = Y2Packager::Product.selected_base
# save the solver test case for easier debugging if no product upgrade was found
Yast::Pkg.CreateSolverTestCase("/var/log/YaST2/solver-product-upgrade") unless product

# restore the original resolvable states
Yast::Pkg.RestoreState
log_products

log.info("Upgraded base product found by solver: #{product.inspect}")
product
end

# find the upgrade product from the fallback mapping
# @param available [Array<Y2Packager::Product>] the available base products
# @return [Y2Packager::Product,nil] the new upgraded product
def find_by_mapping(available)
installed = Y2Packager::Product.installed_products

# sort the keys by length, try more products first
upgrade = MAPPING.keys.sort_by(&:size).find do |keys|
keys.all? { |name| installed.any? { |p| p.name == name } }
end

log.info("Found fallback upgrade for products: #{upgrade.inspect}")
return nil unless upgrade

name = MAPPING[upgrade]
product = available.find { |p| p.name == name }
log.info("New product: #{product}")
product
end

# find the upgrade product with the same identifier as the installed product
# @param available [Array<Y2Packager::Product>] the available base products
# @return [Y2Packager::Product,nil] the new upgraded product
def find_by_name(available)
installed_base = Y2Packager::Product.installed_base_product
return nil unless installed_base

product = available.find { |a| a.name == installed_base.name }
log.info("New product: #{product}")
product
end

# just a helper for logging the details for easier debugging
def log_products
products = Yast::Pkg.ResolvableProperties("", :product, "")
log.debug("All products: #{products.inspect}")
names = products.select { |p| p["status"] == :selected }.map { |p| p["name"] }
log.info("Selected products: #{names.inspect}")
end
end
end
end
17 changes: 17 additions & 0 deletions library/packages/test/y2packager/product_reader_test.rb
Expand Up @@ -87,6 +87,23 @@
end
end

describe "#installed_base_product" do
let(:base_prod) do
# reuse the available SLES15 product, just change some attributes
base = products.first.dup
base["name"] = "base_product"
base["type"] = "base"
base["status"] = :installed
base
end

it "returns the installed base product" do
expect(Yast::Pkg).to receive(:ResolvableProperties).with("", :product, "")
.and_return(products + [base_prod])
expect(subject.installed_base_product.name).to eq("base_product")
end
end

describe "#products" do
before do
allow(Yast::Pkg).to receive(:ResolvableProperties).with("", :product, "")
Expand Down

0 comments on commit 0371869

Please sign in to comment.