diff --git a/service/lib/dinstaller/dbus/software_service.rb b/service/lib/dinstaller/dbus/software_service.rb index e2c443b58c..7d25398ad8 100644 --- a/service/lib/dinstaller/dbus/software_service.rb +++ b/service/lib/dinstaller/dbus/software_service.rb @@ -43,7 +43,7 @@ class SoftwareService def initialize(config, logger = nil) @logger = logger || Logger.new($stdout) @bus = ::DBus::SystemBus.instance - @backend = DInstaller::Software.new(config, logger) + @backend = DInstaller::Software::Manager.new(config, logger) @backend.on_progress_change { dispatch } end diff --git a/service/lib/dinstaller/dbus/y2dir/software/modules/PackageCallbacks.rb b/service/lib/dinstaller/dbus/y2dir/software/modules/PackageCallbacks.rb new file mode 100644 index 0000000000..f4a57a5692 --- /dev/null +++ b/service/lib/dinstaller/dbus/y2dir/software/modules/PackageCallbacks.rb @@ -0,0 +1,52 @@ +# Copyright (c) [2022] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "logger" +require "dinstaller/software/callbacks" +require "dinstaller/dbus/clients/questions_manager" + +# :nodoc: +module Yast + # Replacement for the Yast::PackageCallbacks module. + class PackageCallbacksClass < Module + def main + puts "Loading mocked module #{__FILE__}" + end + + # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L183 + def InitPackageCallbacks(logger = ::Logger.new($stdout)) + DInstaller::Software::Callbacks::Signature.new( + questions_manager, logger + ).setup + + DInstaller::Software::Callbacks::Media.new( + questions_manager, logger + ).setup + end + + def questions_manager + @questions_manager ||= DInstaller::DBus::Clients::QuestionsManager.new + end + end + + PackageCallbacks = PackageCallbacksClass.new + PackageCallbacks.main +end + diff --git a/service/lib/dinstaller/package_callbacks.rb b/service/lib/dinstaller/package_callbacks.rb deleted file mode 100644 index 73bdf142e4..0000000000 --- a/service/lib/dinstaller/package_callbacks.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2021] 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 LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "yast" - -Yast.import "Pkg" - -# YaST specific code lives under this namespace -module DInstaller - # This class represents the installer status - class PackageCallbacks - class << self - def setup(pkg_count, progress) - new(pkg_count, progress).setup - end - end - - def initialize(pkg_count, progress) - @total = pkg_count - @installed = 0 - @progress = progress - end - - def setup - Yast::Pkg.CallbackDonePackage( - fun_ref(method(:package_installed), "string (integer, string)") - ) - end - - private - - # @return [DInstaller::Progress] - attr_reader :progress - - def fun_ref(method, signature) - Yast::FunRef.new(method, signature) - end - - # TODO: error handling - def package_installed(_error, _reason) - @installed += 1 - progress.step(msg) - - "" - end - - def msg - "Installing packages (#{@total - @installed} remains)" - end - end -end diff --git a/service/lib/dinstaller/software.rb b/service/lib/dinstaller/software.rb index 119705f1ad..628e2bfb00 100644 --- a/service/lib/dinstaller/software.rb +++ b/service/lib/dinstaller/software.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2021] SUSE LLC +# Copyright (c) [2022] SUSE LLC # # All Rights Reserved. # @@ -19,238 +19,10 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "yast" -require "fileutils" -require "dinstaller/package_callbacks" -require "dinstaller/config" -require "dinstaller/with_progress" -require "y2packager/product" -require "yast2/arch_filter" - -Yast.import "Package" -Yast.import "Packages" -Yast.import "Pkg" -Yast.import "Stage" - -# YaST specific code lives under this namespace module DInstaller - # This class is responsible for software handling - class Software - include WithProgress - - GPG_KEYS_GLOB = "/usr/lib/rpm/gnupg/keys/gpg-*" - private_constant :GPG_KEYS_GLOB - - attr_reader :product - - DEFAULT_LANGUAGES = ["en_US"].freeze - private_constant :DEFAULT_LANGUAGES - - attr_accessor :languages - - # FIXME: what about defining a Product class? - # @return [Array>] An array containing the product ID and - # additional information in a hash - attr_reader :products - - def initialize(config, logger) - @config = config - @logger = logger - @languages = DEFAULT_LANGUAGES - @products = @config.products - if @config.multi_product? - @product = nil - else - @product = @products.keys.first # use the available product as default - @config.pick_product(@product) - end - end - - def select_product(name) - return if name == @product - raise ArgumentError unless @products[name] - - @config.pick_product(name) - @product = name - end - - def probe - logger.info "Probing software" - - store_original_repos - Yast::Pkg.SetSolverFlags("ignoreAlreadyRecommended" => false, "onlyRequires" => true) - - # as we use liveDVD with normal like ENV, lets temporary switch to normal to use its repos - Yast::Stage.Set("normal") - - start_progress(3) - progress.step("Initialize target repositories") { initialize_target_repos } - progress.step("Initialize sources") { add_base_repo } - progress.step("Making the initial proposal") do - proposal = Yast::Packages.Proposal(force_reset = true, reinit = false, _simple = true) - logger.info "proposal #{proposal["raw_proposal"]}" - end - - Yast::Stage.Set("initial") - end - - def initialize_target_repos - Yast::Pkg.TargetInitialize("/") - import_gpg_keys - end - - def propose - Yast::Pkg.TargetFinish # ensure that previous target is closed - Yast::Pkg.TargetInitialize(Yast::Installation.destdir) - Yast::Pkg.TargetLoad - Yast::Pkg.SetAdditionalLocales(languages) - select_base_product(@config.data["software"]["base_product"]) - - add_resolvables - proposal = Yast::Packages.Proposal(force_reset = false, reinit = false, _simple = true) - logger.info "proposal #{proposal["raw_proposal"]}" - - solve_dependencies - - # do not return proposal hash, so intentional nil here - nil - end - - def install - start_progress(count_packages) - PackageCallbacks.setup(count_packages, progress) - - # TODO: error handling - commit_result = Yast::Pkg.Commit({}) - - if commit_result.nil? || commit_result.empty? - logger.error("Commit failed") - raise Yast::Pkg.LastError - end - - logger.info "Commit result #{commit_result}" - end - - # Writes the repositories information to the installed system - def finish - start_progress(2) - progress.step("Writing repositories to the target system") do - Yast::Pkg.SourceSaveAll - Yast::Pkg.TargetFinish - Yast::Pkg.SourceCacheCopyTo(Yast::Installation.destdir) - end - progress.step("Restoring original repositories") { restore_original_repos } - end - - # Determine whether the given tag is provided by the selected packages - # - # @param tag [String] Tag to search for (package names, requires/provides, or file - # names) - # @return [Boolean] true if it is provided; false otherwise - def provision_selected?(tag) - Yast::Pkg.IsSelected(tag) || Yast::Pkg.IsProvided(tag) - end - - # Determines whether a package is installed - # - # @param name [String] Package name - # @return [Boolean] true if it is installed; false otherwise - def package_installed?(name) - Yast::Package.Installed(name, target: :system) - end - - private - - # adds resolvables from yaml config for given product - def add_resolvables - mandatory_patterns = @config.data["software"]["mandatory_patterns"] || [] - Yast::PackagesProposal.SetResolvables("d-installer", :pattern, mandatory_patterns) - - optional_patterns = @config.data["software"]["optional_patterns"] || [] - Yast::PackagesProposal.SetResolvables("d-installer", :pattern, optional_patterns, - optional: true) - - mandatory_packages = @config.data["software"]["mandatory_packages"] || [] - Yast::PackagesProposal.SetResolvables("d-installer", :package, mandatory_packages) - - optional_packages = @config.data["software"]["optional_packages"] || [] - Yast::PackagesProposal.SetResolvables("d-installer", :package, optional_packages, - optional: true) - end - - # call solver to satisfy dependency or log error - def solve_dependencies - res = Yast::Pkg.PkgSolve(unused = true) - logger.info "solver run #{res.inspect}" - - return if res - - logger.error "Solver failed: #{Yast::Pkg.LastError}" - logger.error "Details: #{Yast::Pkg.LastErrorDetails}" - logger.error "Solving issues: #{Yast::Pkg.PkgSolveErrors}" - end - - # @return [Logger] - attr_reader :logger - - def count_packages - Yast::Pkg.PkgMediaCount.reduce(0) { |sum, res| sum + res.reduce(0, :+) } - end - - def import_gpg_keys - gpg_keys = Dir.glob(GPG_KEYS_GLOB).map(&:to_s) - logger.info "Importing GPG keys: #{gpg_keys}" - gpg_keys.each do |path| - Yast::Pkg.ImportGPGKey(path, true) - end - end - - def add_base_repo - @config.data["software"]["installation_repositories"].each do |repo| - if repo.is_a?(Hash) - url = repo["url"] - # skip if repo is not for current arch - next if repo["archs"] && !Yast2::ArchFilter.from_string(repo["archs"]).match? - else - url = repo - end - Yast::Pkg.SourceCreate(url, "/") # TODO: having that dir also in config? - end - - Yast::Pkg.SourceSaveAll - end - - def select_base_product(name) - base_product = Y2Packager::Product.available_base_products.find do |product| - product.name == name - end - logger.info "Base product to select: #{base_product&.name}" - base_product&.select - end - - REPOS_BACKUP = "/etc/zypp/repos.d.dinstaller.backup" - private_constant :REPOS_BACKUP - - REPOS_DIR = "/etc/zypp/repos.d" - private_constant :REPOS_DIR - - # ensure that repos backup is there and repos.d is empty - def store_original_repos - # Backup was already created, so just remove all repos - if File.directory?(REPOS_BACKUP) - logger.info "removing #{REPOS_DIR}" - FileUtils.rm_rf(REPOS_DIR) - else # move repos to backup - logger.info "moving #{REPOS_DIR} to #{REPOS_BACKUP}" - FileUtils.mv(REPOS_DIR, REPOS_BACKUP) - end - end - - def restore_original_repos - logger.info "removing #{REPOS_DIR}" - FileUtils.rm_rf(REPOS_DIR) - logger.info "moving #{REPOS_BACKUP} to #{REPOS_DIR}" - FileUtils.mv(REPOS_BACKUP, REPOS_DIR) - end + # Namespace for software backend + module Software end end + +require "dinstaller/software/manager" diff --git a/service/lib/dinstaller/software/callbacks.rb b/service/lib/dinstaller/software/callbacks.rb new file mode 100644 index 0000000000..267aa283df --- /dev/null +++ b/service/lib/dinstaller/software/callbacks.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Copyright (c) [2022] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +module DInstaller + module Software + # Namespace for software callbacks + module Callbacks + end + end +end + +require "dinstaller/software/callbacks/media" +require "dinstaller/software/callbacks/progress" +require "dinstaller/software/callbacks/signature" diff --git a/service/lib/dinstaller/software/callbacks/media.rb b/service/lib/dinstaller/software/callbacks/media.rb new file mode 100644 index 0000000000..046daaecc2 --- /dev/null +++ b/service/lib/dinstaller/software/callbacks/media.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# Copyright (c) [2021] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "dinstaller/can_ask_question" +require "dinstaller/question" + +Yast.import "Pkg" + +module DInstaller + module Software + module Callbacks + # Callbacks related to media handling + class Media + include CanAskQuestion + + # @param questions_manager [DBus::Clients::QuestionsManager] + # @param logger [Logger] + def initialize(questions_manager, logger) + @questions_manager = questions_manager + @logger = logger + end + + # Register the callbacks + def setup + Yast::Pkg.CallbackMediaChange( + Yast::FunRef.new( + method(:media_change), + "string (string, string, string, string, integer, string, integer, string, " \ + "boolean, list , integer)" + ) + ) + end + + # Media change callback + # + # @return [String] + # @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620 + # rubocop:disable Metrics/ParameterLists + def media_change(_error_code, error, _url, _product, _current, _current_label, _wanted, + _wanted_label, _double_sided, _devices, _current_device) + question = DInstaller::Question.new( + error, options: [:Retry, :Skip], default_option: :Retry + ) + ask(question) do |q| + logger.info "#{q.text}: #{q.answer}" + + (q.answer == :Retry) ? "" : "S" + end + end + # rubocop:enable Metrics/ParameterLists + + private + + # @return [DBus::Clients::QuestionsManager] + attr_reader :questions_manager + + # @return [Logger] + attr_reader :logger + end + end + end +end diff --git a/service/lib/dinstaller/software/callbacks/progress.rb b/service/lib/dinstaller/software/callbacks/progress.rb new file mode 100644 index 0000000000..9d0de5d9c6 --- /dev/null +++ b/service/lib/dinstaller/software/callbacks/progress.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Copyright (c) [2021] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" + +Yast.import "Pkg" + +module DInstaller + module Software + module Callbacks + # This class represents the installer status + class Progress + class << self + def setup(pkg_count, progress) + new(pkg_count, progress).setup + end + end + + def initialize(pkg_count, progress) + @total = pkg_count + @installed = 0 + @progress = progress + end + + def setup + Yast::Pkg.CallbackDonePackage( + fun_ref(method(:package_installed), "string (integer, string)") + ) + end + + private + + # @return [DInstaller::Progress] + attr_reader :progress + + def fun_ref(method, signature) + Yast::FunRef.new(method, signature) + end + + # TODO: error handling + def package_installed(_error, _reason) + @installed += 1 + progress.step(msg) + + "" + end + + def msg + "Installing packages (#{@total - @installed} remains)" + end + end + end + end +end diff --git a/service/lib/dinstaller/software/callbacks/signature.rb b/service/lib/dinstaller/software/callbacks/signature.rb new file mode 100644 index 0000000000..02f76261f3 --- /dev/null +++ b/service/lib/dinstaller/software/callbacks/signature.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Copyright (c) [2022] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "dinstaller/can_ask_question" +require "dinstaller/question" + +Yast.import "Pkg" + +module DInstaller + module Software + module Callbacks + # Callbacks related to signatures handling + class Signature + include CanAskQuestion + + # @param questions_manager [DBus::Clients::QuestionsManager] + # @param logger [Logger] + def initialize(questions_manager, logger) + @questions_manager = questions_manager + @logger = logger + end + + # Register the callbacks + def setup + Yast::Pkg.CallbackAcceptUnsignedFile( + Yast::FunRef.new(method(:accept_unsigned_file), "boolean (string, integer)") + ) + end + + # Callback to handle unsigned files + # + # @param filename [String] File name + # @param repo_id [Integer] Repository ID. It might be -1 if there is not an associated repo. + def accept_unsigned_file(filename, repo_id) + repo = Yast::Pkg.SourceGeneralData(repo_id) + source = if repo + format( + "The file %{filename} from repository %{repo_name} (%{repo_url})", + filename: filename, repo_name: repo["name"], repo_url: repo["url"] + ) + else + format("The file %{filename}", filename: filename) + end + + message = format( + "%{source} is not digitally signed. The origin and integrity of the file cannot be "\ + "verified. Use it anyway?", source: source + ) + + question = DInstaller::Question.new( + message, options: [:Yes, :No], default_option: :No + ) + ask(question) do |q| + logger.info "#{q.text} #{q.answer}" + q.answer == :Yes + end + end + + private + + # @return [DBus::Clients::QuestionsManager] + attr_reader :questions_manager + + # @return [Logger] + attr_reader :logger + end + end + end +end diff --git a/service/lib/dinstaller/software/manager.rb b/service/lib/dinstaller/software/manager.rb new file mode 100644 index 0000000000..1bb62c4c56 --- /dev/null +++ b/service/lib/dinstaller/software/manager.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +# Copyright (c) [2021] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "fileutils" +require "dinstaller/config" +require "dinstaller/with_progress" +require "y2packager/product" +require "yast2/arch_filter" +require "dinstaller/software/callbacks" + +Yast.import "Package" +Yast.import "Packages" +Yast.import "PackageCallbacks" +Yast.import "Pkg" +Yast.import "Stage" + +module DInstaller + module Software + # This class is responsible for software handling + class Manager + include WithProgress + + GPG_KEYS_GLOB = "/usr/lib/rpm/gnupg/keys/gpg-*" + private_constant :GPG_KEYS_GLOB + + attr_reader :product + + DEFAULT_LANGUAGES = ["en_US"].freeze + private_constant :DEFAULT_LANGUAGES + + attr_accessor :languages + + # FIXME: what about defining a Product class? + # @return [Array>] An array containing the product ID and + # additional information in a hash + attr_reader :products + + def initialize(config, logger) + @config = config + @logger = logger + @languages = DEFAULT_LANGUAGES + @products = @config.products + if @config.multi_product? + @product = nil + else + @product = @products.keys.first # use the available product as default + @config.pick_product(@product) + end + end + + def select_product(name) + return if name == @product + raise ArgumentError unless @products[name] + + @config.pick_product(name) + @product = name + end + + def probe + logger.info "Probing software" + + store_original_repos + Yast::Pkg.SetSolverFlags("ignoreAlreadyRecommended" => false, "onlyRequires" => true) + + # as we use liveDVD with normal like ENV, lets temporary switch to normal to use its repos + Yast::Stage.Set("normal") + + start_progress(3) + progress.step("Initialize target repositories") { initialize_target_repos } + progress.step("Initialize sources") { add_base_repo } + progress.step("Making the initial proposal") do + proposal = Yast::Packages.Proposal(force_reset = true, reinit = false, _simple = true) + logger.info "proposal #{proposal["raw_proposal"]}" + end + + Yast::Stage.Set("initial") + end + + def initialize_target_repos + Yast::PackageCallbacks.InitPackageCallbacks(logger) + Yast::Pkg.TargetInitialize("/") + import_gpg_keys + end + + def propose + Yast::Pkg.TargetFinish # ensure that previous target is closed + Yast::Pkg.TargetInitialize(Yast::Installation.destdir) + Yast::Pkg.TargetLoad + Yast::Pkg.SetAdditionalLocales(languages) + select_base_product(@config.data["software"]["base_product"]) + + add_resolvables + proposal = Yast::Packages.Proposal(force_reset = false, reinit = false, _simple = true) + logger.info "proposal #{proposal["raw_proposal"]}" + + solve_dependencies + + # do not return proposal hash, so intentional nil here + nil + end + + def install + start_progress(count_packages) + Callbacks::Progress.setup(count_packages, progress) + + # TODO: error handling + commit_result = Yast::Pkg.Commit({}) + + if commit_result.nil? || commit_result.empty? + logger.error("Commit failed") + raise Yast::Pkg.LastError + end + + logger.info "Commit result #{commit_result}" + rescue DInstaller::WithProgress::NotFinishedProgress => e + logger.error "There is an unfinished progress: #{e.inspect}" + finish_progress + end + + # Writes the repositories information to the installed system + def finish + start_progress(2) + progress.step("Writing repositories to the target system") do + Yast::Pkg.SourceSaveAll + Yast::Pkg.TargetFinish + Yast::Pkg.SourceCacheCopyTo(Yast::Installation.destdir) + end + progress.step("Restoring original repositories") { restore_original_repos } + end + + # Determine whether the given tag is provided by the selected packages + # + # @param tag [String] Tag to search for (package names, requires/provides, or file + # names) + # @return [Boolean] true if it is provided; false otherwise + def provision_selected?(tag) + Yast::Pkg.IsSelected(tag) || Yast::Pkg.IsProvided(tag) + end + + # Determines whether a package is installed + # + # @param name [String] Package name + # @return [Boolean] true if it is installed; false otherwise + def package_installed?(name) + Yast::Package.Installed(name, target: :system) + end + + private + + # adds resolvables from yaml config for given product + def add_resolvables + mandatory_patterns = @config.data["software"]["mandatory_patterns"] || [] + Yast::PackagesProposal.SetResolvables("d-installer", :pattern, mandatory_patterns) + + optional_patterns = @config.data["software"]["optional_patterns"] || [] + Yast::PackagesProposal.SetResolvables("d-installer", :pattern, optional_patterns, + optional: true) + + mandatory_packages = @config.data["software"]["mandatory_packages"] || [] + Yast::PackagesProposal.SetResolvables("d-installer", :package, mandatory_packages) + + optional_packages = @config.data["software"]["optional_packages"] || [] + Yast::PackagesProposal.SetResolvables("d-installer", :package, optional_packages, + optional: true) + end + + # call solver to satisfy dependency or log error + def solve_dependencies + res = Yast::Pkg.PkgSolve(unused = true) + logger.info "solver run #{res.inspect}" + + return if res + + logger.error "Solver failed: #{Yast::Pkg.LastError}" + logger.error "Details: #{Yast::Pkg.LastErrorDetails}" + logger.error "Solving issues: #{Yast::Pkg.PkgSolveErrors}" + end + + # @return [Logger] + attr_reader :logger + + def count_packages + Yast::Pkg.PkgMediaCount.reduce(0) { |sum, res| sum + res.reduce(0, :+) } + end + + def import_gpg_keys + gpg_keys = Dir.glob(GPG_KEYS_GLOB).map(&:to_s) + logger.info "Importing GPG keys: #{gpg_keys}" + gpg_keys.each do |path| + Yast::Pkg.ImportGPGKey(path, true) + end + end + + def add_base_repo + @config.data["software"]["installation_repositories"].each do |repo| + if repo.is_a?(Hash) + url = repo["url"] + # skip if repo is not for current arch + next if repo["archs"] && !Yast2::ArchFilter.from_string(repo["archs"]).match? + else + url = repo + end + Yast::Pkg.SourceCreate(url, "/") # TODO: having that dir also in config? + end + + Yast::Pkg.SourceSaveAll + end + + def select_base_product(name) + base_product = Y2Packager::Product.available_base_products.find do |product| + product.name == name + end + logger.info "Base product to select: #{base_product&.name}" + base_product&.select + end + + REPOS_BACKUP = "/etc/zypp/repos.d.dinstaller.backup" + private_constant :REPOS_BACKUP + + REPOS_DIR = "/etc/zypp/repos.d" + private_constant :REPOS_DIR + + # ensure that repos backup is there and repos.d is empty + def store_original_repos + # Backup was already created, so just remove all repos + if File.directory?(REPOS_BACKUP) + logger.info "removing #{REPOS_DIR}" + FileUtils.rm_rf(REPOS_DIR) + else # move repos to backup + logger.info "moving #{REPOS_DIR} to #{REPOS_BACKUP}" + FileUtils.mv(REPOS_DIR, REPOS_BACKUP) + end + end + + def restore_original_repos + logger.info "removing #{REPOS_DIR}" + FileUtils.rm_rf(REPOS_DIR) + logger.info "moving #{REPOS_BACKUP} to #{REPOS_DIR}" + FileUtils.mv(REPOS_BACKUP, REPOS_DIR) + end + end + end +end diff --git a/service/lib/dinstaller/with_progress.rb b/service/lib/dinstaller/with_progress.rb index ab6ab17902..8896040a08 100644 --- a/service/lib/dinstaller/with_progress.rb +++ b/service/lib/dinstaller/with_progress.rb @@ -24,6 +24,9 @@ module DInstaller # Mixin that allows to start a progress and configure callbacks module WithProgress + # There is an unfinished progress + class NotFinishedProgress < StandardError; end + # @return [Progress, nil] attr_reader :progress @@ -33,7 +36,7 @@ module WithProgress # # @param total_steps [Integer] total number of the steps for the progress. def start_progress(total_steps) - raise "There already is an unfinished progress" if progress && !progress.finished? + raise NotFinishedProgress if progress && !progress.finished? on_change_callbacks = @on_progress_change_callbacks || [] on_finish_callbacks = @on_progress_finish_callbacks || [] @@ -44,6 +47,13 @@ def start_progress(total_steps) end end + # Finishes the current progress + def finish_progress + return if progress.nil? || progress.finished? + + progress.finish + end + # Registers an on_change callback to be added to the progress # # @param block [Proc] diff --git a/service/package/rubygem-d-installer.changes b/service/package/rubygem-d-installer.changes index 4b0991a095..4183e68317 100644 --- a/service/package/rubygem-d-installer.changes +++ b/service/package/rubygem-d-installer.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Dec 14 22:38:24 UTC 2022 - Imobach Gonzalez Sosa + +- Implement AcceptUnsignedFile and MediaChange libzypp callbacks + (gh#yast/d-installer#369). + ------------------------------------------------------------------- Wed Dec 14 15:29:12 UTC 2022 - Imobach Gonzalez Sosa diff --git a/service/test/dinstaller/dbus/software/manager_test.rb b/service/test/dinstaller/dbus/software/manager_test.rb index ee5aa15fe2..861a5fca19 100644 --- a/service/test/dinstaller/dbus/software/manager_test.rb +++ b/service/test/dinstaller/dbus/software/manager_test.rb @@ -30,7 +30,7 @@ let(:logger) { Logger.new($stdout, level: :warn) } - let(:backend) { instance_double(DInstaller::Software) } + let(:backend) { instance_double(DInstaller::Software::Manager) } let(:progress_interface) { DInstaller::DBus::Interfaces::Progress::PROGRESS_INTERFACE } diff --git a/service/test/dinstaller/software/callbacks/media_test.rb b/service/test/dinstaller/software/callbacks/media_test.rb new file mode 100644 index 0000000000..ff491a54be --- /dev/null +++ b/service/test/dinstaller/software/callbacks/media_test.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Copyright (c) [2022] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "dinstaller/software/callbacks/media" +require "dinstaller/dbus/clients/questions_manager" +require "dinstaller/question" + +describe DInstaller::Software::Callbacks::Media do + subject { described_class.new(questions_manager, logger) } + + let(:questions_manager) do + instance_double(DInstaller::DBus::Clients::QuestionsManager) + end + + let(:logger) { Logger.new($stdout) } + + describe "#media_changed" do + let(:asked_question) do + instance_double(DInstaller::Question, text: "Better safe than sorry", answer: answer) + end + let(:answer) { :Retry } + + before do + allow(subject).to receive(:ask).and_yield(asked_question) + end + + context "when the user answers :Retry" do + it "returns ''" do + ret = subject.media_change( + "NOT_FOUND", "Package not found", "", "", 0, "", 0, "", true, [], 0 + ) + expect(ret).to eq("") + end + end + + context "when the user answers :Skip" do + let(:answer) { :Skip } + + it "returns 'S'" do + ret = subject.media_change( + "NOT_FOUND", "Package not found", "", "", 0, "", 0, "", true, [], 0 + ) + expect(ret).to eq("S") + end + end + end +end diff --git a/service/test/dinstaller/software/callbacks/signature_test.rb b/service/test/dinstaller/software/callbacks/signature_test.rb new file mode 100644 index 0000000000..f680f79a4e --- /dev/null +++ b/service/test/dinstaller/software/callbacks/signature_test.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Copyright (c) [2022] 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 LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../../../test_helper" +require "dinstaller/software/callbacks/signature" +require "dinstaller/dbus/clients/questions_manager" +require "dinstaller/question" + +describe DInstaller::Software::Callbacks::Signature do + subject { described_class.new(questions_manager, logger) } + + let(:questions_manager) do + instance_double(DInstaller::DBus::Clients::QuestionsManager) + end + + let(:logger) { Logger.new($stdout) } + + describe "#accept_unsigned_file" do + let(:asked_question) do + instance_double(DInstaller::Question, text: "Better safe than sorry", answer: answer) + end + let(:answer) { :Yes } + + before do + allow(subject).to receive(:ask).and_yield(asked_question) + end + + context "when the user answers :Yes" do + it "returns true" do + expect(subject.accept_unsigned_file("repomd.xml", -1)).to eq(true) + end + end + + context "when the user answers :No" do + let(:answer) { :No } + + it "returns false" do + expect(subject.accept_unsigned_file("repomd.xml", -1)).to eq(false) + end + end + + context "when the repo information is available" do + before do + allow(Yast::Pkg).to receive(:SourceGeneralData).with(1) + .and_return("name" => "OSS", "url" => "http://localhost/repo") + end + + it "includes the name and the URL in the question" do + expect(subject).to receive(:ask) do |question| + expect(question.text).to include("OSS (http://localhost/repo)") + end + expect(subject.accept_unsigned_file("repomd.xml", 1)) + end + end + + context "when the repo information is not available" + it "includes a generic message containing the filename" do + expect(subject).to receive(:ask) do |question| + expect(question.text).to include("repomd.xml") + end + expect(subject.accept_unsigned_file("repomd.xml", 1)) + end + end +end diff --git a/service/test/dinstaller/software_test.rb b/service/test/dinstaller/software/manager_test.rb similarity index 89% rename from service/test/dinstaller/software_test.rb rename to service/test/dinstaller/software/manager_test.rb index 2764c79135..59bac4b4de 100644 --- a/service/test/dinstaller/software_test.rb +++ b/service/test/dinstaller/software/manager_test.rb @@ -19,11 +19,14 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../test_helper" +require_relative "../../test_helper" +require_relative File.join( + SRC_PATH, "dinstaller", "dbus", "y2dir", "software", "modules", "PackageCallbacks.rb" +) require "dinstaller/config" -require "dinstaller/software" +require "dinstaller/software/manager" -describe DInstaller::Software do +describe DInstaller::Software::Manager do subject { described_class.new(config, logger) } let(:logger) { Logger.new($stdout, level: :warn) } @@ -39,6 +42,10 @@ DInstaller::Config.new(YAML.safe_load(File.read(config_path))) end + let(:questions_manager) do + instance_double(DInstaller::DBus::Clients::QuestionsManager) + end + before do allow(Yast::Pkg).to receive(:TargetInitialize) allow(Yast::Pkg).to receive(:ImportGPGKey) @@ -48,6 +55,8 @@ .and_return(base_url) allow(Yast::Pkg).to receive(:SourceCreate) allow(Yast::Installation).to receive(:destdir).and_return(destdir) + allow(DInstaller::DBus::Clients::QuestionsManager).to receive(:new) + .and_return(questions_manager) end describe "#probe" do @@ -56,8 +65,8 @@ let(:backup_repos_dir) { File.join(rootdir, "etc", "zypp", "repos.d.backup") } before do - stub_const("DInstaller::Software::REPOS_DIR", repos_dir) - stub_const("DInstaller::Software::REPOS_BACKUP", backup_repos_dir) + stub_const("DInstaller::Software::Manager::REPOS_DIR", repos_dir) + stub_const("DInstaller::Software::Manager::REPOS_BACKUP", backup_repos_dir) FileUtils.mkdir_p(repos_dir) end @@ -166,7 +175,7 @@ end it "sets up the package callbacks" do - expect(DInstaller::PackageCallbacks).to receive(:setup) + expect(DInstaller::Software::Callbacks::Progress).to receive(:setup) subject.install end @@ -185,8 +194,8 @@ let(:backup_repos_dir) { File.join(rootdir, "etc", "zypp", "repos.d.backup") } before do - stub_const("DInstaller::Software::REPOS_DIR", repos_dir) - stub_const("DInstaller::Software::REPOS_BACKUP", backup_repos_dir) + stub_const("DInstaller::Software::Manager::REPOS_DIR", repos_dir) + stub_const("DInstaller::Software::Manager::REPOS_BACKUP", backup_repos_dir) FileUtils.mkdir_p(repos_dir) FileUtils.mkdir_p(backup_repos_dir) FileUtils.touch(File.join(backup_repos_dir, "example.repo")) diff --git a/service/test/dinstaller/with_progress_test.rb b/service/test/dinstaller/with_progress_test.rb index 9a3f288fe9..c86d2885e3 100644 --- a/service/test/dinstaller/with_progress_test.rb +++ b/service/test/dinstaller/with_progress_test.rb @@ -54,7 +54,8 @@ class WithProgressTest end it "raises an error" do - expect { subject.start_progress(1) }.to raise_error(/unfinished progress/) + expect { subject.start_progress(1) } + .to raise_error(DInstaller::WithProgress::NotFinishedProgress) end end @@ -90,4 +91,36 @@ class WithProgressTest end end end + + describe "#finish" do + context "when the current progress is not finished" do + before do + subject.start_progress(1) + end + + it "finishes the current progress" do + expect { subject.finish_progress } + .to change { subject.progress.finished? } + .from(false).to(true) + end + end + + context "when the current progress is already finished" do + before do + subject.start_progress(1) + subject.progress.step("") { nil } + end + + it "does not crash" do + expect(subject.progress).to_not receive(:finish) + subject.finish_progress + end + end + + context "when there is no progress" do + it "does not crash" do + subject.finish_progress + end + end + end end diff --git a/web/package/cockpit-d-installer.changes b/web/package/cockpit-d-installer.changes index e0a1b3bb6c..02a45f4bdc 100644 --- a/web/package/cockpit-d-installer.changes +++ b/web/package/cockpit-d-installer.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Thu Dec 15 08:55:02 UTC 2022 - Imobach Gonzalez Sosa + +- Display questions during the software installation (related to + gh#yast/d-installer#369). +- Update to version 0.6.1 + ------------------------------------------------------------------- Mon Dec 5 13:18:37 UTC 2022 - Imobach Gonzalez Sosa diff --git a/web/src/components/core/InstallationProgress.jsx b/web/src/components/core/InstallationProgress.jsx index 86a41ab27b..26803e6d46 100644 --- a/web/src/components/core/InstallationProgress.jsx +++ b/web/src/components/core/InstallationProgress.jsx @@ -23,6 +23,7 @@ import React from "react"; import ProgressReport from "./ProgressReport"; import { Center, Title, PageIcon } from "@components/layout"; +import { Questions } from "@components/questions"; import { EOS_DOWNLOADING as Icon } from "eos-icons-react"; @@ -32,6 +33,7 @@ function InstallationProgress() { Installing
+ ); }