src/lib/registration/addon.rb
# encoding: utf-8
# ------------------------------------------------------------------------------
# Copyright (c) 2014 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.
#
# 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 "forwardable"
require "set"
require "registration/sw_mgmt"
require "y2packager/resolvable"
module Registration
# A wrapper class around SUSE::Connect::Product object,
# https://rubydoc.info/github/SUSE/connect/SUSE/Connect/Product
class Addon
extend Yast::I18n
include Yast::I18n
# The list of addons has not been loaded
class AddonsNotLoaded < StandardError; end
class << self
# read the remote add-on from the registration server
# @param registration [Registration::Registration] use this object for
# reading the remote add-ons
# @return [Array<Addon>]
def find_all(registration)
return @cached_addons if @cached_addons
@cached_addons = load_addons(registration)
dump_addons
@cached_addons
end
# Find an addon using its ID
#
# @note This method needs #find_all to be call previously.
#
# @param id [Integer] Addon's ID
# @return [Addon,nil] The addon with the given ID or nil if it was not found
def find_by_id(id)
raise AddonsNotLoaded unless @cached_addons
@cached_addons.find { |a| a.id == id }
end
def reset!
@cached_addons = nil
@registered = nil
@selected = nil
@auto_selected = nil
end
# list of registered add-ons
# @return [Array<Addon>] registered add-ons
def registered
@registered ||= []
end
# list of selected add-ons
# @return [Array<Addon>] selected add-ons
def selected
@selected ||= []
end
# invalidates automatically selected addons. Resulting in recalculating it.
def reset_auto_selected
@auto_selected = nil
end
# @return [Array<Addon>] auto selected add-ons
def auto_selected
@auto_selected ||= detect_auto_selection
end
# return add-ons which are registered but not installed in the system
# and are available to install
# @return [Array<Addon>] the list of add-ons
def registered_not_installed
registered.select do |addon|
installed = SwMgmt.installed_products.find do |product|
product["name"] == addon.identifier &&
product["version_version"] == addon.version &&
product["arch"] == addon.arch
end
available = Y2Packager::Resolvable.any?(kind: :product,
name: addon.identifier, status: :available)
!installed && available
end
end
# Returns passed addons sorted with all dependencies ordered that it can be
# registered from first to last ( so no dependencies for first ).
# @param list [Array<Addon>] of addons
def registration_order(list)
to_process = list.dup
result = []
loop do
break if to_process.empty?
next_addon = to_process.find do |addon|
addon.depends_on.nil? || !to_process.include?(addon.depends_on)
end
raise "circular dependencies found in addons #{to_process.inspect}" unless next_addon
result << next_addon
to_process.delete(next_addon)
end
result
end
private
# create an Addon from a SUSE::Connect::Product
# @param root [SUSE::Connect::Product] the root add-on object
# @return [Array<Addon>] list of addons, where the first one is
# the one based on root and rest is its children
def create_addon_with_deps(root)
# to_process is array of pairs, where first is pure addon to process and second is
# its dependency. Currently SUSE::Connect structure have only one dependency.
to_process = [[root, nil]]
processed = Set.new
result = []
to_process.each do |(pure, dependency)|
# this avoid endless loop if there is circular dependency.
next if processed.include?(pure)
processed << pure
addon = Addon.new(pure)
result << addon
addon.depends_on = dependency
(pure.extensions || []).each do |ext|
to_process << [ext, addon]
end
end
result
end
# @return [Array<Addon>]
def load_addons(registration)
pure_addons = registration.get_addon_list
# get IDs of the already activated addons
activated_addon_ids = registration.activated_products.map(&:id)
@cached_addons = pure_addons.reduce([]) do |res, addon|
yast_addons = create_addon_with_deps(addon)
# mark as registered if found in the status call
yast_addons.each do |yast_addon|
yast_addon.registered if activated_addon_ids.include?(yast_addon.id)
end
res.concat(yast_addons)
end
end
# @return [Array<Addon>]
def detect_auto_selection
required = selected + registered
# here we use sets as for bigger dependencies this can be quite slow
# how it works? it fills set with selected and registered items and it will
# adds recursive all its children and then subtract that manually selected
# or registered.
already_processed = Set.new(required)
to_process = required.dup
to_process.each do |addon|
already_processed << addon
# prepared when depends_on support multiple addons
dependencies = addon.depends_on ? [addon.depends_on] : []
new_addons = dependencies.reject { |c| already_processed.include?(c) }
to_process.concat(new_addons)
end
to_process - required
end
end
extend Forwardable
include Yast::Logger
attr_accessor :depends_on, :regcode
# delegate methods to underlaying suse connect object
def_delegators :@pure_addon,
:arch,
:description,
:eula_url,
:free,
:friendly_name,
:id,
:identifier,
:name,
:product_type,
:recommended,
:release_stage,
:release_type,
:repositories,
:version
# the constructor
# @param pure_addon [SUSE::Connect::Product] a pure add-on from the registration server
def initialize(pure_addon)
textdomain "registration"
@pure_addon = pure_addon
end
# is the add-on selected
# @return [Boolean] true if the add-on is selected
def selected?
Addon.selected.include?(self)
end
# is the add-on auto_selected
# @return [Boolean] true if the add-on is auto_selected
def auto_selected?
Addon.auto_selected.include?(self)
end
# select the add-on
def selected
return if selected?
Addon.selected << self
Addon.reset_auto_selected
end
# unselect the add-on
def unselected
return unless selected?
Addon.selected.delete(self)
Addon.reset_auto_selected
end
# returns status of addon. Potential statuses are :registered, :selected, :auto_selected,
# :available and :unknown.
# @return [Symbol]
def status
return :registered if registered?
return :selected if selected?
return :auto_selected if auto_selected?
return :available if available?
log.warn "unknown state for #{inspect}"
:unknown
end
# toggle the selection state of the add-on
def toggle_selected
if selected?
unselected
else
selected
end
Addon.reset_auto_selected
end
# has been the add-on registered?
# @return [Boolean] true if the add-on has been registered
def registered?
Addon.registered.include?(self)
end
# mark the add-on as registered
def registered
Addon.registered << self unless registered?
unselected # if register then mark as no longer selected as register is different state
end
# just internally mark the addon as NOT registered, not a real unregistration
def unregistered
Addon.registered.delete(self) if registered?
end
DEVELOPMENT_STAGES = ["alpha", "beta"].freeze
def released?
release_stage == "released"
end
# get a product printable name (long name if present, fallbacks to the short name)
# @return [String] label usable in UI
def label
(friendly_name && !friendly_name.empty?) ? friendly_name : name
end
# can be the addon selected in UI or should it be disabled?
# return [Boolean] true if it should be enabled
def selectable?
# Do not support unregister
return false if registered?
# Do not select not available addons
return false if !available?
true
end
# Convert to a Hash, exports only the basic Addon properties
# @param [Boolean] release_type_string if true the "release_type" atribute
# will be always a String (nil will be converted to "nil")
# @return [Hash] Hash with basic Addon properties
def to_h(release_type_string: false)
{
"name" => identifier,
"arch" => arch,
"version" => version,
"release_type" => (release_type.nil? && release_type_string) ? "nil" : release_type
}
end
# is the addon available? SMT/RMT may have mirrored only some extensions,
# the not mirrored extensions are marked as not available
# @return [Boolean] true if the addon is available to register
def available?
# explicitly check for false, undefined (nil) means it is available,
# it's only reported by SMT/RMT
@pure_addon.available != false
end
# Checks whether this addon updates an old addon
# @param [Hash] old_addon addon Hash received from pkg-bindings
# @return [Boolean] true if it updates the old addon, false otherwise
def updates_addon?(old_addon)
old_addon["name"] == identifier || old_addon["name"] == @pure_addon.former_identifier
end
def matches_remote_product?(remote_product)
[:arch, :identifier, :version, :release_type].all? do |attr|
send(attr) == remote_product.send(attr)
end
end
# Whether the EULA acceptance is required
#
# @return [Boolean] true if a not empty EULA url is present; false otherwise
def eula_acceptance_needed?
!eula_url.to_s.strip.empty?
end
# Returns all the dependencies
#
# Includes all dependencies in a recursive way.
#
# @return [Array<Addon>]
def dependencies
return [] if depends_on.nil?
[depends_on] + depends_on.dependencies
end
def self.dump_addons
# dump the downloaded data to a file for easier debugging,
# avoid write failures when running as an unprivileged user (rspec tests)
return unless File.writable?("/var/log/YaST2")
require "yaml"
header = "# see " \
"https://github.com/yast/yast-registration/tree/master/devel/dump_reader.rb\n" \
"# for an example how to read this dump file\n"
File.write("/var/log/YaST2/registration_addons.yml",
header + @cached_addons.to_yaml)
end
end
end