Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial API proposal for manager #42

Merged
merged 9 commits into from
Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
29 changes: 21 additions & 8 deletions service/lib/dinstaller/dbus/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,34 @@ def initialize(installer, logger)
@installer = installer
@logger = logger

# @available_base_products = installer.products

super(PATH)
end

dbus_interface MANAGER_INTERFACE do
dbus_method :probe, "out result:u" do
# TODO
0
dbus_method :probe, "" do
# TODO: do it assynchronous. How? ractors will have problem with sharing yast data.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np: asynchronous

We need to do some research here. By now, we have been using threads and, although it is far from ideal, it worked. But sure we need a better solution. Ideally, we could have an actors-like solution where every subsystem is represented by an actor and they share nothing. But that's not realistic at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sadly with way how we using yast I worry it will be very challenging

# For threads there are problem with race conditions. Like what yast will do if some variable change during installation?
end

dbus_method :commit, "out result:u" do
# TODO
0
dbus_method :commit, "" do
# TODO: do it assynchronous
end

# Enum list for statuses. Possible values:
# 0 : error ( it can be read from progress message )
# 1 : probing
# 2 : probed
# 3 : installing
# 4 : installed
dbus_reader :status, "u"

# Progress has struct with values:
# s message
# t total major steps to do
# t current major step
jreidinger marked this conversation as resolved.
Show resolved Hide resolved
# t total minor steps. Can be zero which means no minor steps
# t current minor step
dbus_reader :progress, "(stttt)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So a D-Bus client can poll the progress by reading the property, or listen for PropertiesChanged signals, right?

InstallationProgress has @dbus_obj&.Progress(...args...). How is that related? Which class (interface) is @dbus_obj?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. I see that pattern often.
InstallationProgress will be dropped. Plan is to use Progress.new in dbus Manager object and assign callback to it that raise signal. And attach progress to data structure read from that progress.object.

end
end
end
Expand Down
27 changes: 7 additions & 20 deletions service/lib/dinstaller/installer_status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,16 @@
module DInstaller
# This class represents the installer status
class InstallerStatus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe having a class is too much. Is this expected to grow?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea: What about having something like DInstaller::StatusManager class, which defines possible statuses, implement logic for changing status, for calling callbacks and so on? And DInstaller::Manager would simply instanciate a StatusManager and would request a status change when needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I am not sure. I try to not touch this part :) Just change it from string to unsigned.

class << self
# Returns all the possible installer statuses
#
# @return [Array<InstallerStatus>] Installer status
def all
@all ||= constants
.map { |c| InstallerStatus.const_get(c) }
.select { |c| c.is_a?(InstallerStatus) }
end
end

attr_reader :id, :name
attr_reader :id

def initialize(id, name)
def initialize(id)
@id = id
@name = name
end

IDLE = new(0, "Idle")
PROBING = new(1, "Probing")
PARTITIONING = new(2, "Partitioning")
INSTALLING = new(3, "Installing")
FINISHED = new(4, "Finished")
ERROR = new(5, "Error")
ERROR = new(0)
PROBING = new(1)
PROBED = new(2)
INSTALLING = new(3)
INSTALLED = new(4)
end
end
218 changes: 218 additions & 0 deletions service/lib/dinstaller/manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# 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 "y2storage"
require "dinstaller/installer_status"
require "dinstaller/progress"
require "dinstaller/software"
require "dinstaller/installation_progress"
require "bootloader/proposal_client"
require "bootloader/finish_client"
require "dbus"
require "forwardable"

Yast.import "Stage"

# YaST specific code lives under this namespace
module DInstaller
# This class represents top level installer manager.
#
# It is responsible for orchestrating the installation process. For module specific
# stuff it delegates it to module itself.
class Manager
class InvalidValue < StandardError; end

extend Forwardable

# TODO: move to own module classes
DEFAULT_LANGUAGE = "en_US"

# TODO: move to own module classes
attr_reader :disks, :languages
# TODO: move to own module classes
attr_reader :disk
attr_reader :logger
# TODO: move to own module classes
attr_reader :language

# TODO: software should use directly software module for getting and settings products
def_delegators :@software, :products, :product

attr_reader :status
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NP: Please, document its type.


# Returns a new instance of the Installer class
#
# @example Probe and run the installation
# installer = Installer.new
# installer.probe
# installer.install
#
# @example Reacting on status change
# installer = Installer.new
# installer.on_status_change do |status|
# log.info "Status changed: #{status}"
# end
#
# @param logger [Logger,nil] Logger to write messages to
def initialize(logger: nil)
Yast::Mode.SetUI("commandline")
Yast::Mode.SetMode("installation")
@disks = []
@languages = []
@products = []
@status = InstallerStatus::IDLE
@logger = logger || Logger.new($stdout)
@software = Software.new(@logger)
# Set stage to initial, so it will act as installer for some cases like
# proposing installer instead of reading current one
Yast::Stage.Set("initial")
end

def options
{ "disk" => disk, "product" => product, "language" => language }
end

# Starts the probing process
#
# At this point, it just initializes some YaST modules/subsystems:
#
# * Software management
# * Simplified storage probing
#
# The initialization of these subsystems should probably live in a different place.
#
# @return [Boolean] true if the probing process ended successfully; false otherwise.
def probe
change_status(InstallerStatus::PROBING)
probe_languages
probe_storage
@software.probe
change_status(InstallerStatus::PROBED)
rescue StandardError => e
change_status(InstallerStatus::ERROR)
logger.error "Probing error: #{e.inspect}"
false
ensure
end

def disk=(name)
raise InvalidValue unless propose_storage(name)

@disk = name
end

def product=(name)
@software.select_product(name)
rescue StandardError
raise InvalidValue
end

def language=(name)
raise InvalidValue unless languages.include?(name)

@language = name
end

def storage_proposal
storage_manager.proposal&.devices
end

def install
change_status(InstallerStatus::INSTALLING)
Yast::Installation.destdir = "/mnt"
# lets propose it here to be sure that software proposal reflects product selection
# FIXME: maybe repropose after product selection change?
# first make bootloader proposal to be sure that required packages is installed
proposal = ::Bootloader::ProposalClient.new.make_proposal({})
logger.info "Bootloader proposal #{proposal.inspect}"
@software.propose

progress = InstallationProgress.new(@dbus_obj, logger: logger)
progress.partitioning do |_|
Yast::WFM.CallFunction("inst_prepdisk", [])
end
progress.package_installation do |progr|
# call inst bootloader to get properly initialized bootloader
# sysconfig before package installation
Yast::WFM.CallFunction("inst_bootloader", [])
@software.install(progr)
end
progress.bootloader_installation do |_|
handle = Yast::WFM.SCROpen("chroot=#{Yast::Installation.destdir}:scr", false)
Yast::WFM.SCRSetDefault(handle)
::Bootloader::FinishClient.new.write
end
change_status(InstallerStatus::INSTALLED)
end

private

def change_status(new_status)
@status = new_status
end

# Returns the list of known languages
#
# @return [Hash]
def probe_languages
logger.info "Probing languages"
Yast.import "Language"
@languages = Yast::Language.GetLanguagesMap(true)
self.language = DEFAULT_LANGUAGE
end

def probe_storage
logger.info "Probing storage"
storage_manager.probe
@disks = storage_manager.probed.disks
self.disk = @disks.first&.name
end

# @return [Boolean] true if success; false if failed
def propose_storage(disk_name)
settings = Y2Storage::ProposalSettings.new_for_current_product
settings.candidate_devices = [disk_name]

# FIXME: clean up the disks
clean_probed = storage_probed.clone
clean_probed.disks.each(&:remove_descendants)

proposal = Y2Storage::GuidedProposal.initial(
devicegraph: clean_probed,
settings: settings
)
return false if proposal.failed?

storage_manager.proposal = proposal
true
end

def storage_probed
storage_manager.probed
end

def storage_manager
@storage_manager ||= Y2Storage::StorageManager.instance
end
end
end
Loading