d-installer 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +75 -0
- data/bin/d-installer +74 -0
- data/etc/d-installer.yaml +284 -0
- data/lib/dinstaller/can_ask_question.rb +48 -0
- data/lib/dinstaller/cmdline_args.rb +80 -0
- data/lib/dinstaller/cockpit_manager.rb +176 -0
- data/lib/dinstaller/config.rb +128 -0
- data/lib/dinstaller/config_reader.rb +164 -0
- data/lib/dinstaller/dbus/base_object.rb +58 -0
- data/lib/dinstaller/dbus/clients/base.rb +71 -0
- data/lib/dinstaller/dbus/clients/language.rb +86 -0
- data/lib/dinstaller/dbus/clients/manager.rb +76 -0
- data/lib/dinstaller/dbus/clients/software.rb +185 -0
- data/lib/dinstaller/dbus/clients/users.rb +112 -0
- data/lib/dinstaller/dbus/clients/with_progress.rb +56 -0
- data/lib/dinstaller/dbus/clients/with_service_status.rb +75 -0
- data/lib/dinstaller/dbus/clients.rb +34 -0
- data/lib/dinstaller/dbus/interfaces/progress.rb +113 -0
- data/lib/dinstaller/dbus/interfaces/service_status.rb +89 -0
- data/lib/dinstaller/dbus/language.rb +93 -0
- data/lib/dinstaller/dbus/language_service.rb +92 -0
- data/lib/dinstaller/dbus/manager.rb +147 -0
- data/lib/dinstaller/dbus/manager_service.rb +132 -0
- data/lib/dinstaller/dbus/question.rb +176 -0
- data/lib/dinstaller/dbus/questions.rb +124 -0
- data/lib/dinstaller/dbus/service_runner.rb +97 -0
- data/lib/dinstaller/dbus/service_status.rb +87 -0
- data/lib/dinstaller/dbus/software/manager.rb +131 -0
- data/lib/dinstaller/dbus/software/proposal.rb +82 -0
- data/lib/dinstaller/dbus/software.rb +31 -0
- data/lib/dinstaller/dbus/software_service.rb +86 -0
- data/lib/dinstaller/dbus/storage/proposal.rb +170 -0
- data/lib/dinstaller/dbus/storage.rb +30 -0
- data/lib/dinstaller/dbus/users.rb +132 -0
- data/lib/dinstaller/dbus/users_service.rb +92 -0
- data/lib/dinstaller/dbus/with_service_status.rb +48 -0
- data/lib/dinstaller/dbus/y2dir/manager/modules/Package.rb +51 -0
- data/lib/dinstaller/dbus/y2dir/manager/modules/PackagesProposal.rb +62 -0
- data/lib/dinstaller/dbus/y2dir/modules/Autologin.rb +214 -0
- data/lib/dinstaller/dbus/y2dir/software/modules/SpaceCalculation.rb +44 -0
- data/lib/dinstaller/dbus.rb +11 -0
- data/lib/dinstaller/errors.rb +28 -0
- data/lib/dinstaller/installation_phase.rb +106 -0
- data/lib/dinstaller/language.rb +73 -0
- data/lib/dinstaller/luks_activation_question.rb +92 -0
- data/lib/dinstaller/manager.rb +261 -0
- data/lib/dinstaller/network.rb +53 -0
- data/lib/dinstaller/package_callbacks.rb +69 -0
- data/lib/dinstaller/progress.rb +149 -0
- data/lib/dinstaller/question.rb +103 -0
- data/lib/dinstaller/questions_manager.rb +145 -0
- data/lib/dinstaller/security.rb +96 -0
- data/lib/dinstaller/service_status_recorder.rb +58 -0
- data/lib/dinstaller/software.rb +232 -0
- data/lib/dinstaller/storage/actions.rb +90 -0
- data/lib/dinstaller/storage/callbacks/activate.rb +93 -0
- data/lib/dinstaller/storage/callbacks/activate_luks.rb +93 -0
- data/lib/dinstaller/storage/callbacks/activate_multipath.rb +77 -0
- data/lib/dinstaller/storage/callbacks.rb +30 -0
- data/lib/dinstaller/storage/manager.rb +104 -0
- data/lib/dinstaller/storage/proposal.rb +197 -0
- data/lib/dinstaller/storage.rb +30 -0
- data/lib/dinstaller/users.rb +156 -0
- data/lib/dinstaller/with_progress.rb +63 -0
- data/lib/dinstaller.rb +5 -0
- data/share/dbus.conf +38 -0
- data/share/dbus.service +5 -0
- data/share/systemd.service +14 -0
- metadata +295 -0
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) [2022] SUSE LLC
|
4
|
+
#
|
5
|
+
# All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This program is free software; you can redistribute it and/or modify it
|
8
|
+
# under the terms of version 2 of the GNU General Public License as published
|
9
|
+
# by the Free Software Foundation.
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
13
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
14
|
+
# more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License along
|
17
|
+
# with this program; if not, contact SUSE LLC.
|
18
|
+
#
|
19
|
+
# To contact SUSE LLC about this file by physical or electronic mail, you may
|
20
|
+
# find current contact information at www.suse.com.
|
21
|
+
|
22
|
+
require "yast"
|
23
|
+
require "bootloader/proposal_client"
|
24
|
+
require "bootloader/finish_client"
|
25
|
+
require "dinstaller/config"
|
26
|
+
require "dinstaller/network"
|
27
|
+
require "dinstaller/security"
|
28
|
+
require "dinstaller/storage"
|
29
|
+
require "dinstaller/questions_manager"
|
30
|
+
require "dinstaller/with_progress"
|
31
|
+
require "dinstaller/installation_phase"
|
32
|
+
require "dinstaller/service_status_recorder"
|
33
|
+
require "dinstaller/dbus/clients/language"
|
34
|
+
require "dinstaller/dbus/clients/software"
|
35
|
+
require "dinstaller/dbus/clients/users"
|
36
|
+
|
37
|
+
Yast.import "Stage"
|
38
|
+
|
39
|
+
module DInstaller
|
40
|
+
# This class represents the top level installer manager.
|
41
|
+
#
|
42
|
+
# It is responsible for orchestrating the installation process. For module
|
43
|
+
# specific stuff it delegates it to the corresponding module class (e.g.,
|
44
|
+
# {DInstaller::Network}, {DInstaller::Storage::Proposal}, etc.) or asks
|
45
|
+
# other services via D-Bus (e.g., `org.opensuse.DInstaller.Software`).
|
46
|
+
class Manager
|
47
|
+
include WithProgress
|
48
|
+
|
49
|
+
# @return [Logger]
|
50
|
+
attr_reader :logger
|
51
|
+
|
52
|
+
# @return [QuestionsManager]
|
53
|
+
attr_reader :questions_manager
|
54
|
+
|
55
|
+
# @return [InstallationPhase]
|
56
|
+
attr_reader :installation_phase
|
57
|
+
|
58
|
+
# Constructor
|
59
|
+
#
|
60
|
+
# @param logger [Logger]
|
61
|
+
def initialize(config, logger)
|
62
|
+
@config = config
|
63
|
+
@logger = logger
|
64
|
+
@questions_manager = QuestionsManager.new(logger)
|
65
|
+
@installation_phase = InstallationPhase.new
|
66
|
+
@service_status_recorder = ServiceStatusRecorder.new
|
67
|
+
end
|
68
|
+
|
69
|
+
# Runs the startup phase
|
70
|
+
def startup_phase
|
71
|
+
installation_phase.startup
|
72
|
+
|
73
|
+
probe_single_product unless config.multi_product?
|
74
|
+
|
75
|
+
logger.info("Startup phase done")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Runs the config phase
|
79
|
+
def config_phase
|
80
|
+
installation_phase.config
|
81
|
+
|
82
|
+
storage.probe(questions_manager)
|
83
|
+
security.probe
|
84
|
+
network.probe
|
85
|
+
|
86
|
+
logger.info("Config phase done")
|
87
|
+
rescue StandardError => e
|
88
|
+
logger.error "Startup error: #{e.inspect}. Backtrace: #{e.backtrace}"
|
89
|
+
# TODO: report errors
|
90
|
+
end
|
91
|
+
|
92
|
+
# Runs the install phase
|
93
|
+
# rubocop:disable Metrics/AbcSize
|
94
|
+
def install_phase
|
95
|
+
installation_phase.install
|
96
|
+
|
97
|
+
start_progress(9)
|
98
|
+
|
99
|
+
progress.step("Reading software repositories") do
|
100
|
+
software.probe
|
101
|
+
Yast::Installation.destdir = "/mnt"
|
102
|
+
end
|
103
|
+
|
104
|
+
progress.step("Partitioning") do
|
105
|
+
# lets propose it here to be sure that software proposal reflects product selection
|
106
|
+
# FIXME: maybe repropose after product selection change?
|
107
|
+
# first make bootloader proposal to be sure that required packages are installed
|
108
|
+
proposal = ::Bootloader::ProposalClient.new.make_proposal({})
|
109
|
+
logger.info "Bootloader proposal #{proposal.inspect}"
|
110
|
+
storage.install
|
111
|
+
# propose software after /mnt is already separated, so it uses proper
|
112
|
+
# target
|
113
|
+
software.propose
|
114
|
+
|
115
|
+
# call inst bootloader to get properly initialized bootloader
|
116
|
+
# sysconfig before package installation
|
117
|
+
Yast::WFM.CallFunction("inst_bootloader", [])
|
118
|
+
end
|
119
|
+
|
120
|
+
progress.step("Installing Software") { software.install }
|
121
|
+
|
122
|
+
on_target do
|
123
|
+
progress.step("Writing Users") { users.write }
|
124
|
+
|
125
|
+
progress.step("Writing Network Configuration") { network.install }
|
126
|
+
|
127
|
+
progress.step("Installing Bootloader") do
|
128
|
+
security.write
|
129
|
+
::Bootloader::FinishClient.new.write
|
130
|
+
end
|
131
|
+
|
132
|
+
progress.step("Saving Language Settings") { language.finish }
|
133
|
+
|
134
|
+
progress.step("Writing repositories information") { software.finish }
|
135
|
+
|
136
|
+
progress.step("Finishing installation") { finish_installation }
|
137
|
+
end
|
138
|
+
|
139
|
+
logger.info("Install phase done")
|
140
|
+
end
|
141
|
+
# rubocop:enable Metrics/AbcSize
|
142
|
+
|
143
|
+
# Software client
|
144
|
+
#
|
145
|
+
# @return [DBus::Clients::Software]
|
146
|
+
def software
|
147
|
+
@software ||= DBus::Clients::Software.new.tap do |client|
|
148
|
+
client.on_service_status_change do |status|
|
149
|
+
service_status_recorder.save(client.service.name, status)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Language manager
|
155
|
+
#
|
156
|
+
# @return [DBus::Clients::Language]
|
157
|
+
def language
|
158
|
+
@language ||= DBus::Clients::Language.new
|
159
|
+
end
|
160
|
+
|
161
|
+
# Users client
|
162
|
+
#
|
163
|
+
# @return [DBus::Clients::Users]
|
164
|
+
def users
|
165
|
+
@users ||= DBus::Clients::Users.new.tap do |client|
|
166
|
+
client.on_service_status_change do |status|
|
167
|
+
service_status_recorder.save(client.service.name, status)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Network manager
|
173
|
+
#
|
174
|
+
# @return [Network]
|
175
|
+
def network
|
176
|
+
@network ||= Network.new(logger)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Storage manager
|
180
|
+
#
|
181
|
+
# @return [Storage::Manager]
|
182
|
+
def storage
|
183
|
+
@storage ||= Storage::Manager.new(logger, config)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Security manager
|
187
|
+
#
|
188
|
+
# @return [Security]
|
189
|
+
def security
|
190
|
+
@security ||= Security.new(logger, config)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Actions to perform when a product is selected
|
194
|
+
#
|
195
|
+
# @note The config phase is executed.
|
196
|
+
def select_product(product)
|
197
|
+
config.pick_product(product)
|
198
|
+
config_phase
|
199
|
+
end
|
200
|
+
|
201
|
+
# Name of busy services
|
202
|
+
#
|
203
|
+
# @see ServiceStatusRecorder
|
204
|
+
#
|
205
|
+
# @return [Array<String>]
|
206
|
+
def busy_services
|
207
|
+
service_status_recorder.busy_services
|
208
|
+
end
|
209
|
+
|
210
|
+
# Registers a callback to be called when the status of a service changes
|
211
|
+
#
|
212
|
+
# @see ServiceStatusRecorder
|
213
|
+
def on_services_status_change(&block)
|
214
|
+
service_status_recorder.on_service_status_change(&block)
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
attr_reader :config
|
220
|
+
|
221
|
+
# @return [ServiceStatusRecorder]
|
222
|
+
attr_reader :service_status_recorder
|
223
|
+
|
224
|
+
# Performs required steps after installing the system
|
225
|
+
#
|
226
|
+
# For now, this only unmounts the installed system and copies installation logs. Note that YaST
|
227
|
+
# performs many more steps like copying configuration files, creating snapshots, etc. Adding
|
228
|
+
# more features to D-Installer could require to recover some of that YaST logic.
|
229
|
+
def finish_installation
|
230
|
+
Yast::WFM.CallFunction("copy_logs_finish", ["Write"])
|
231
|
+
storage.finish
|
232
|
+
end
|
233
|
+
|
234
|
+
# Runs a block in the target system
|
235
|
+
def on_target(&block)
|
236
|
+
old_handle = Yast::WFM.SCRGetDefault
|
237
|
+
handle = Yast::WFM.SCROpen("chroot=#{Yast::Installation.destdir}:scr", false)
|
238
|
+
Yast::WFM.SCRSetDefault(handle)
|
239
|
+
|
240
|
+
begin
|
241
|
+
block.call
|
242
|
+
rescue StandardError => e
|
243
|
+
logger.error "Error while running on target tasks: #{e.inspect}"
|
244
|
+
ensure
|
245
|
+
Yast::WFM.SCRSetDefault(old_handle)
|
246
|
+
Yast::WFM.SCRClose(handle)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Runs the config phase for the first product found
|
251
|
+
#
|
252
|
+
# Adjust the configuration and run the config phase.
|
253
|
+
#
|
254
|
+
# This method is expected to be used on single-product scenarios.
|
255
|
+
def probe_single_product
|
256
|
+
selected = config.data["products"].keys.first
|
257
|
+
config.pick_product(selected)
|
258
|
+
config_phase
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) [2022] SUSE LLC
|
4
|
+
#
|
5
|
+
# All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This program is free software; you can redistribute it and/or modify it
|
8
|
+
# under the terms of version 2 of the GNU General Public License as published
|
9
|
+
# by the Free Software Foundation.
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
13
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
14
|
+
# more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License along
|
17
|
+
# with this program; if not, contact SUSE LLC.
|
18
|
+
#
|
19
|
+
# To contact SUSE LLC about this file by physical or electronic mail, you may
|
20
|
+
# find current contact information at www.suse.com.
|
21
|
+
|
22
|
+
require "singleton"
|
23
|
+
require "yast"
|
24
|
+
require "y2network/proposal_settings"
|
25
|
+
Yast.import "Lan"
|
26
|
+
|
27
|
+
module DInstaller
|
28
|
+
# Backend class to handle network configuration
|
29
|
+
class Network
|
30
|
+
def initialize(logger)
|
31
|
+
@logger = logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# Probes the network configuration
|
35
|
+
def probe
|
36
|
+
logger.info "Probing network"
|
37
|
+
Yast::Lan.read_config
|
38
|
+
settings = Y2Network::ProposalSettings.instance
|
39
|
+
settings.refresh_packages
|
40
|
+
settings.apply_defaults
|
41
|
+
end
|
42
|
+
|
43
|
+
# Writes the network configuration to the installed system
|
44
|
+
def install
|
45
|
+
Yast::WFM.CallFunction("save_network", [])
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# @return [Logger]
|
51
|
+
attr_reader :logger
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) [2021] SUSE LLC
|
4
|
+
#
|
5
|
+
# All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This program is free software; you can redistribute it and/or modify it
|
8
|
+
# under the terms of version 2 of the GNU General Public License as published
|
9
|
+
# by the Free Software Foundation.
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
13
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
14
|
+
# more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License along
|
17
|
+
# with this program; if not, contact SUSE LLC.
|
18
|
+
#
|
19
|
+
# To contact SUSE LLC about this file by physical or electronic mail, you may
|
20
|
+
# find current contact information at www.suse.com.
|
21
|
+
|
22
|
+
require "yast"
|
23
|
+
|
24
|
+
Yast.import "Pkg"
|
25
|
+
|
26
|
+
# YaST specific code lives under this namespace
|
27
|
+
module DInstaller
|
28
|
+
# This class represents the installer status
|
29
|
+
class PackageCallbacks
|
30
|
+
class << self
|
31
|
+
def setup(pkg_count, progress)
|
32
|
+
new(pkg_count, progress).setup
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(pkg_count, progress)
|
37
|
+
@total = pkg_count
|
38
|
+
@installed = 0
|
39
|
+
@progress = progress
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup
|
43
|
+
Yast::Pkg.CallbackDonePackage(
|
44
|
+
fun_ref(method(:package_installed), "string (integer, string)")
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# @return [DInstaller::Progress]
|
51
|
+
attr_reader :progress
|
52
|
+
|
53
|
+
def fun_ref(method, signature)
|
54
|
+
Yast::FunRef.new(method, signature)
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO: error handling
|
58
|
+
def package_installed(_error, _reason)
|
59
|
+
@installed += 1
|
60
|
+
progress.step(msg)
|
61
|
+
|
62
|
+
""
|
63
|
+
end
|
64
|
+
|
65
|
+
def msg
|
66
|
+
"Installing packages (#{@total - @installed} remains)"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) [2022] SUSE LLC
|
4
|
+
#
|
5
|
+
# All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This program is free software; you can redistribute it and/or modify it
|
8
|
+
# under the terms of version 2 of the GNU General Public License as published
|
9
|
+
# by the Free Software Foundation.
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
13
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
14
|
+
# more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License along
|
17
|
+
# with this program; if not, contact SUSE LLC.
|
18
|
+
#
|
19
|
+
# To contact SUSE LLC about this file by physical or electronic mail, you may
|
20
|
+
# find current contact information at www.suse.com.
|
21
|
+
|
22
|
+
module DInstaller
|
23
|
+
# Class to manage progress
|
24
|
+
#
|
25
|
+
# It allows to configure callbacks to be called on each step and also when the progress finishes.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
#
|
29
|
+
# progress = Progress.new(3) # 3 steps
|
30
|
+
# progress.on_change { puts progress.message } # configures callbacks
|
31
|
+
# progress.on_finish { puts "finished" } # configures callbacks
|
32
|
+
#
|
33
|
+
# progress.step("Doing step1") { step1 } # calls on_change callbacks and executes step1
|
34
|
+
# progress.step("Doing step2") { step2 } # calls on_change callbacks and executes step2
|
35
|
+
#
|
36
|
+
# progress.current_step #=> <Step>
|
37
|
+
# progress.current_step.id #=> 2
|
38
|
+
# progress.current_step.description #=> "Doing step2"
|
39
|
+
#
|
40
|
+
# progress.finished? #=> false
|
41
|
+
|
42
|
+
# progress.step("Doing step3") do # calls on_change callbacks, executes the given
|
43
|
+
# progress.current_step.description # block and calls on_finish callbacks
|
44
|
+
# end #=> "Doing step3"
|
45
|
+
#
|
46
|
+
# progress.finished? #=> true
|
47
|
+
# progress.current_step #=> nil
|
48
|
+
class Progress
|
49
|
+
# Step of the progress
|
50
|
+
class Step
|
51
|
+
# Id of the step
|
52
|
+
#
|
53
|
+
# @return [Integer]
|
54
|
+
attr_reader :id
|
55
|
+
|
56
|
+
# Description of the step
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
attr_reader :description
|
60
|
+
|
61
|
+
# Constructor
|
62
|
+
#
|
63
|
+
# @param id [Integer]
|
64
|
+
# @param description [String]
|
65
|
+
def initialize(id, description)
|
66
|
+
@id = id
|
67
|
+
@description = description
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Total number of steps
|
72
|
+
#
|
73
|
+
# @return [Integer]
|
74
|
+
attr_reader :total_steps
|
75
|
+
|
76
|
+
# Constructor
|
77
|
+
#
|
78
|
+
# @param toal_steps [Integer] total number of steps
|
79
|
+
def initialize(total_steps)
|
80
|
+
@total_steps = total_steps
|
81
|
+
@current_step = nil
|
82
|
+
@counter = 0
|
83
|
+
@finished = false
|
84
|
+
@on_change_callbacks = []
|
85
|
+
@on_finish_callbacks = []
|
86
|
+
end
|
87
|
+
|
88
|
+
# Current progress step, if any
|
89
|
+
#
|
90
|
+
# @return [Step, nil] nil if the progress is already finished or not stated yet.
|
91
|
+
def current_step
|
92
|
+
return nil if finished?
|
93
|
+
|
94
|
+
@current_step
|
95
|
+
end
|
96
|
+
|
97
|
+
# Runs a progress step
|
98
|
+
#
|
99
|
+
# It calls the `on_change` callbacks and then runs the given block, if any. It also calls
|
100
|
+
# `on_finish` callbacks after the last step.
|
101
|
+
#
|
102
|
+
# @param description [String] description of the step
|
103
|
+
# @param block [Proc]
|
104
|
+
#
|
105
|
+
# @return [Object, nil] result of the given block or nil if no block is given
|
106
|
+
def step(description, &block)
|
107
|
+
return if finished?
|
108
|
+
|
109
|
+
@counter += 1
|
110
|
+
@current_step = Step.new(@counter, description)
|
111
|
+
@on_change_callbacks.each(&:call)
|
112
|
+
|
113
|
+
result = block_given? ? block.call : nil
|
114
|
+
|
115
|
+
finish if @counter == total_steps
|
116
|
+
|
117
|
+
result
|
118
|
+
end
|
119
|
+
|
120
|
+
# Whether the last step was already done
|
121
|
+
#
|
122
|
+
# @return [Boolean]
|
123
|
+
def finished?
|
124
|
+
total_steps == 0 || @finished
|
125
|
+
end
|
126
|
+
|
127
|
+
# Finishes the progress and runs the callbacks
|
128
|
+
#
|
129
|
+
# This method can be called to force the progress to finish before of running all the steps.
|
130
|
+
def finish
|
131
|
+
@finished = true
|
132
|
+
@on_finish_callbacks.each(&:call)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Adds a callback to be called when progress changes
|
136
|
+
#
|
137
|
+
# @param block [Proc]
|
138
|
+
def on_change(&block)
|
139
|
+
@on_change_callbacks << block
|
140
|
+
end
|
141
|
+
|
142
|
+
# Adds a callback to be called when progress finishes
|
143
|
+
#
|
144
|
+
# @param block [Proc]
|
145
|
+
def on_finish(&block)
|
146
|
+
@on_finish_callbacks << block
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) [2022] SUSE LLC
|
4
|
+
#
|
5
|
+
# All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This program is free software; you can redistribute it and/or modify it
|
8
|
+
# under the terms of version 2 of the GNU General Public License as published
|
9
|
+
# by the Free Software Foundation.
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
13
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
14
|
+
# more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License along
|
17
|
+
# with this program; if not, contact SUSE LLC.
|
18
|
+
#
|
19
|
+
# To contact SUSE LLC about this file by physical or electronic mail, you may
|
20
|
+
# find current contact information at www.suse.com.
|
21
|
+
|
22
|
+
module DInstaller
|
23
|
+
# This class represents a question
|
24
|
+
#
|
25
|
+
# Questions are used when some information needs to be asked. For example, a question could be
|
26
|
+
# created for asking whether to continue or not when an error is detected.
|
27
|
+
#
|
28
|
+
# Questions are managed by a questions manager, see {QuestionsManager}.
|
29
|
+
class Question
|
30
|
+
# Each question is identified by an unique id
|
31
|
+
#
|
32
|
+
# @return [Integer]
|
33
|
+
attr_reader :id
|
34
|
+
|
35
|
+
# Text of the question
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
attr_reader :text
|
39
|
+
|
40
|
+
# Options the question offers
|
41
|
+
#
|
42
|
+
# The question must be answered with one of that options.
|
43
|
+
#
|
44
|
+
# @return [Array<Symbol>]
|
45
|
+
attr_reader :options
|
46
|
+
|
47
|
+
# Default option to use as answer
|
48
|
+
#
|
49
|
+
# @return [Symbol, nil]
|
50
|
+
attr_reader :default_option
|
51
|
+
|
52
|
+
# Answer of the question
|
53
|
+
#
|
54
|
+
# @return [Symbol, nil] nil if the question is not answered yet
|
55
|
+
attr_reader :answer
|
56
|
+
|
57
|
+
def initialize(text, options: [], default_option: nil)
|
58
|
+
@id = IdGenerator.next
|
59
|
+
@text = text
|
60
|
+
@options = options
|
61
|
+
@default_option = default_option
|
62
|
+
end
|
63
|
+
|
64
|
+
# Answers the question with an option
|
65
|
+
#
|
66
|
+
# @raise [ArgumentError] if the given value is not a valid answer.
|
67
|
+
#
|
68
|
+
# @param value [Symbol]
|
69
|
+
def answer=(value)
|
70
|
+
raise ArgumentError, "Invalid answer. Options: #{options}" unless valid_answer?(value)
|
71
|
+
|
72
|
+
@answer = value
|
73
|
+
end
|
74
|
+
|
75
|
+
# Whether the question is already answered
|
76
|
+
#
|
77
|
+
# @return [Boolean]
|
78
|
+
def answered?
|
79
|
+
!answer.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Checks whether the given value is a valid answer
|
85
|
+
#
|
86
|
+
# @param value [Symbol]
|
87
|
+
# @return [Boolean]
|
88
|
+
def valid_answer?(value)
|
89
|
+
options.include?(value)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Helper class for generating unique ids
|
93
|
+
class IdGenerator
|
94
|
+
# Generates the next id to be used
|
95
|
+
#
|
96
|
+
# @return [Integer]
|
97
|
+
def self.next
|
98
|
+
@last_id ||= 0
|
99
|
+
@last_id += 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|