Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c632eb6
[service] Extract the software proposal logic to a separate class
imobachgs Jan 25, 2023
8b27616
[service] Extract the logic to handle repositories from the proposal
imobachgs Jan 26, 2023
7955fc8
[service] Move proposal size calculation to the Proposal class
imobachgs Jan 27, 2023
1bfe6ac
[service] Improve feedback while reading software repositories
imobachgs Feb 7, 2023
8586cee
[web] Fix some linter issues
imobachgs Feb 7, 2023
e58046b
[web] Add support to refresh the repositories
imobachgs Feb 8, 2023
f90787e
[service] Reprobe software if the connection is back
imobachgs Feb 8, 2023
0187a56
[service] Add a Software::Proposal#valid? method
imobachgs Feb 8, 2023
ca6d2cc
[service] Pass the network state to the on_connection_changed callback
imobachgs Feb 8, 2023
3cba515
[service] Dispatch messages in the system bus
imobachgs Feb 8, 2023
89cf82c
[service] Do not report UsedSize unless the proposal is valid
imobachgs Feb 8, 2023
7bcf540
[web] Fix software section presentation
imobachgs Feb 8, 2023
6d2aaaf
[service] Do not report Pkg.LastError if PkgSolveErrors.zero?
imobachgs Feb 8, 2023
7f4c9d9
[service] Improve repository probing
imobachgs Feb 8, 2023
c9e81dd
[service] Do not dump the software proposal to the logs
imobachgs Feb 8, 2023
18215c3
Update the changes files
imobachgs Feb 8, 2023
85ddbec
[service] Documentation and logging improvements
imobachgs Feb 9, 2023
2f0a5f5
[service] Fix the name of the RepositoriesManager tests
imobachgs Feb 9, 2023
dcd55a1
[service] Fix software manager and proposal tests
imobachgs Feb 9, 2023
93960d3
[web] Use the loading icon in the software section
imobachgs Feb 9, 2023
866fc90
[service] Add a test for SoftwareProposal#set_resolvables
imobachgs Feb 9, 2023
c4a2e33
[web] Display the 'refresh repos' button only when there are errors
imobachgs Feb 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions service/lib/dinstaller/dbus/clients/network.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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 "dinstaller/dbus/clients/base"

module DInstaller
module DBus
module Clients
# D-Bus client for the network service
#
# This client is intended to be used to watch for changes in
# NetworkManager because D-Installer does not implement its own network
# service. The configuration is done directly in the UI or through
# `nmcli`.
class Network < Base
def initialize
super

@dbus_object = service["/org/freedesktop/NetworkManager"]
@dbus_object.introspect
@nm_iface = @dbus_object["org.freedesktop.NetworkManager"]
end

def service_name
@service_name ||= "org.freedesktop.NetworkManager"
end

CONNECTED_NM_STATE = 70
private_constant :CONNECTED_NM_STATE

# Registers a callback to call when connectivity state changes
#
# The block receives a boolean argument which is true when the network
# connection is working or false otherwise.
#
# @param [Proc] block
def on_connection_changed(&block)
@nm_iface.on_signal("StateChanged") do |nm_state|
block.call(nm_state == CONNECTED_NM_STATE)
end
end

private

def bus
@bus ||= ::DBus::SystemBus.instance
end
end
end
end
end
6 changes: 5 additions & 1 deletion service/lib/dinstaller/dbus/service_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

require "eventmachine"
require "logger"
require "dbus"

module DInstaller
module DBus
Expand Down Expand Up @@ -51,7 +52,10 @@ def run
# which is equivalent to #export in most cases.
service.respond_to?(:start) ? service.start : service.export
EventMachine.run do
EventMachine::PeriodicTimer.new(0.1) { service.dispatch }
EventMachine::PeriodicTimer.new(0.1) do
service.dispatch
::DBus::SystemBus.instance.dispatch_message_queue
end
end
end

Expand Down
10 changes: 8 additions & 2 deletions service/lib/dinstaller/dbus/software/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
require "dinstaller/dbus/base_object"
require "dinstaller/dbus/with_service_status"
require "dinstaller/dbus/clients/language"
require "dinstaller/dbus/clients/network"
require "dinstaller/dbus/interfaces/progress"
require "dinstaller/dbus/interfaces/service_status"
require "dinstaller/dbus/interfaces/validation"
Expand Down Expand Up @@ -134,10 +135,15 @@ def finish

# Registers callback to be called
def register_callbacks
client = DInstaller::DBus::Clients::Language.new
client.on_language_selected do |language_ids|
lang_client = DInstaller::DBus::Clients::Language.new
lang_client.on_language_selected do |language_ids|
backend.languages = language_ids
end

nm_client = DInstaller::DBus::Clients::Network.new
nm_client.on_connection_changed do |connected|
probe if connected
end
end
end
end
Expand Down
166 changes: 63 additions & 103 deletions service/lib/dinstaller/software/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
require "y2packager/product"
require "yast2/arch_filter"
require "dinstaller/software/callbacks"
require "dinstaller/software/proposal"
require "dinstaller/software/repositories_manager"

Yast.import "Package"
Yast.import "Packages"
Expand Down Expand Up @@ -57,6 +59,8 @@ class Manager
# additional information in a hash
attr_reader :products

attr_reader :repositories

def initialize(config, logger)
@config = config
@probed = false
Expand All @@ -69,6 +73,7 @@ def initialize(config, logger)
@product = @products.keys.first # use the available product as default
@config.pick_product(@product)
end
@repositories = RepositoriesManager.new
end

def select_product(name)
Expand All @@ -83,22 +88,22 @@ def select_product(name)
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)
Yast::PackageCallbacks.InitPackageCallbacks(logger)
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"]}"
if repositories.empty?
start_progress(4)
store_original_repos
Yast::PackageCallbacks.InitPackageCallbacks(logger)
progress.step("Initializing target repositories") { initialize_target_repos }
progress.step("Initializing sources") { add_base_repos }
else
start_progress(2)
end

@probed = true
progress.step("Refreshing repositories metadata") { repositories.load }
progress.step("Calculating the software proposal") { propose }

Yast::Stage.Set("initial")
end

Expand All @@ -107,33 +112,36 @@ def initialize_target_repos
import_gpg_keys
end

# Updates the software proposal
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"]}"

deps_result = solve_dependencies

proposal_result(proposal, deps_result)
proposal.base_product = @product
proposal.languages = languages
select_resolvables
result = proposal.calculate
logger.info "Proposal result: #{result.inspect}"
result
end

# Returns the errors related to the software proposal
#
# * Repositories that could not be probed are reported as errors.
# * If none of the repositories could be probed, do not report missing
# patterns and/or packages. Those issues does not make any sense if there
# are no repositories to install from.
def validate
# validation without probing does not make sense and produces false errors
return [] unless @probed
errors = repositories.disabled.map do |repo|
ValidationError.new("Could not read the repository #{repo.name}")
end
return errors if repositories.enabled.empty?

msgs = propose
msgs.map { |m| ValidationError.new(m) }
errors + proposal.errors
end

# Installs the packages to the target system
def install
start_progress(count_packages)
Callbacks::Progress.setup(count_packages, progress)
steps = proposal.packages_count
start_progress(steps)
Callbacks::Progress.setup(steps, progress)

# TODO: error handling
commit_result = Yast::Pkg.Commit({})
Expand Down Expand Up @@ -180,67 +188,23 @@ def package_installed?(name)
# Counts how much disk space installation will use.
# @return [String]
# @note Reimplementation of Yast::Package.CountSizeToBeInstalled
# @todo move to Software::Proposal
def used_disk_space
return "" unless @probed

size = Yast::Pkg.PkgMediaSizes.reduce(0) do |res, media_size|
media_size.reduce(res, :+)
end
return "" unless proposal.valid?

# FormatSizeWithPrecision(bytes, precision, omit_zeroes)
Yast::String.FormatSizeWithPrecision(size, 1, true)
Yast::String.FormatSizeWithPrecision(proposal.packages_size, 1, true)
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 true if res

logger.error "Solver failed: #{Yast::Pkg.LastError}"
logger.error "Details: #{Yast::Pkg.LastErrorDetails}"
logger.error "Solving issues: #{Yast::Pkg.PkgSolveErrors}"

false
end

# messages with reason why solver failed
def solve_messages
last_error = Yast::Pkg.LastError
solve_errors = Yast::Pkg.PkgSolveErrors
res = []
res << last_error unless last_error.empty?
res << "Found #{solve_errors} dependency issues." if solve_errors > 0
res
def proposal
@proposal ||= Proposal.new
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}"
Expand All @@ -249,27 +213,21 @@ def import_gpg_keys
end
end

def add_base_repo
@config.data["software"]["installation_repositories"].each do |repo|
def installation_repositories
@config.data["software"]["installation_repositories"]
end

def add_base_repos
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?
repositories.add(url)
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"
Expand Down Expand Up @@ -297,19 +255,21 @@ def restore_original_repos
FileUtils.mv(REPOS_BACKUP, REPOS_DIR)
end

def proposal_result(proposal, deps_result)
result = []
# TODO: find if there is a better way to get proposal issue as list
result.concat(process_warnings(proposal)) if proposal["warning_level"] == :blocker
result.concat(solve_messages) unless deps_result
# adds resolvables from yaml config for given product
def select_resolvables
mandatory_patterns = @config.data["software"]["mandatory_patterns"] || []
proposal.set_resolvables("d-installer", :pattern, mandatory_patterns)

result
end
optional_patterns = @config.data["software"]["optional_patterns"] || []
proposal.set_resolvables("d-installer", :pattern, optional_patterns,
optional: true)

mandatory_packages = @config.data["software"]["mandatory_packages"] || []
proposal.set_resolvables("d-installer", :package, mandatory_packages)

def process_warnings(proposal)
proposal["warning"]
.split("<br>")
.grep_v(/Please manually select .*/)
optional_packages = @config.data["software"]["optional_packages"] || []
proposal.set_resolvables("d-installer", :package, optional_packages,
optional: true)
end
end
end
Expand Down
Loading