d-installer 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +29 -0
  3. data/Gemfile.lock +75 -0
  4. data/bin/d-installer +74 -0
  5. data/etc/d-installer.yaml +284 -0
  6. data/lib/dinstaller/can_ask_question.rb +48 -0
  7. data/lib/dinstaller/cmdline_args.rb +80 -0
  8. data/lib/dinstaller/cockpit_manager.rb +176 -0
  9. data/lib/dinstaller/config.rb +128 -0
  10. data/lib/dinstaller/config_reader.rb +164 -0
  11. data/lib/dinstaller/dbus/base_object.rb +58 -0
  12. data/lib/dinstaller/dbus/clients/base.rb +71 -0
  13. data/lib/dinstaller/dbus/clients/language.rb +86 -0
  14. data/lib/dinstaller/dbus/clients/manager.rb +76 -0
  15. data/lib/dinstaller/dbus/clients/software.rb +185 -0
  16. data/lib/dinstaller/dbus/clients/users.rb +112 -0
  17. data/lib/dinstaller/dbus/clients/with_progress.rb +56 -0
  18. data/lib/dinstaller/dbus/clients/with_service_status.rb +75 -0
  19. data/lib/dinstaller/dbus/clients.rb +34 -0
  20. data/lib/dinstaller/dbus/interfaces/progress.rb +113 -0
  21. data/lib/dinstaller/dbus/interfaces/service_status.rb +89 -0
  22. data/lib/dinstaller/dbus/language.rb +93 -0
  23. data/lib/dinstaller/dbus/language_service.rb +92 -0
  24. data/lib/dinstaller/dbus/manager.rb +147 -0
  25. data/lib/dinstaller/dbus/manager_service.rb +132 -0
  26. data/lib/dinstaller/dbus/question.rb +176 -0
  27. data/lib/dinstaller/dbus/questions.rb +124 -0
  28. data/lib/dinstaller/dbus/service_runner.rb +97 -0
  29. data/lib/dinstaller/dbus/service_status.rb +87 -0
  30. data/lib/dinstaller/dbus/software/manager.rb +131 -0
  31. data/lib/dinstaller/dbus/software/proposal.rb +82 -0
  32. data/lib/dinstaller/dbus/software.rb +31 -0
  33. data/lib/dinstaller/dbus/software_service.rb +86 -0
  34. data/lib/dinstaller/dbus/storage/proposal.rb +170 -0
  35. data/lib/dinstaller/dbus/storage.rb +30 -0
  36. data/lib/dinstaller/dbus/users.rb +132 -0
  37. data/lib/dinstaller/dbus/users_service.rb +92 -0
  38. data/lib/dinstaller/dbus/with_service_status.rb +48 -0
  39. data/lib/dinstaller/dbus/y2dir/manager/modules/Package.rb +51 -0
  40. data/lib/dinstaller/dbus/y2dir/manager/modules/PackagesProposal.rb +62 -0
  41. data/lib/dinstaller/dbus/y2dir/modules/Autologin.rb +214 -0
  42. data/lib/dinstaller/dbus/y2dir/software/modules/SpaceCalculation.rb +44 -0
  43. data/lib/dinstaller/dbus.rb +11 -0
  44. data/lib/dinstaller/errors.rb +28 -0
  45. data/lib/dinstaller/installation_phase.rb +106 -0
  46. data/lib/dinstaller/language.rb +73 -0
  47. data/lib/dinstaller/luks_activation_question.rb +92 -0
  48. data/lib/dinstaller/manager.rb +261 -0
  49. data/lib/dinstaller/network.rb +53 -0
  50. data/lib/dinstaller/package_callbacks.rb +69 -0
  51. data/lib/dinstaller/progress.rb +149 -0
  52. data/lib/dinstaller/question.rb +103 -0
  53. data/lib/dinstaller/questions_manager.rb +145 -0
  54. data/lib/dinstaller/security.rb +96 -0
  55. data/lib/dinstaller/service_status_recorder.rb +58 -0
  56. data/lib/dinstaller/software.rb +232 -0
  57. data/lib/dinstaller/storage/actions.rb +90 -0
  58. data/lib/dinstaller/storage/callbacks/activate.rb +93 -0
  59. data/lib/dinstaller/storage/callbacks/activate_luks.rb +93 -0
  60. data/lib/dinstaller/storage/callbacks/activate_multipath.rb +77 -0
  61. data/lib/dinstaller/storage/callbacks.rb +30 -0
  62. data/lib/dinstaller/storage/manager.rb +104 -0
  63. data/lib/dinstaller/storage/proposal.rb +197 -0
  64. data/lib/dinstaller/storage.rb +30 -0
  65. data/lib/dinstaller/users.rb +156 -0
  66. data/lib/dinstaller/with_progress.rb +63 -0
  67. data/lib/dinstaller.rb +5 -0
  68. data/share/dbus.conf +38 -0
  69. data/share/dbus.service +5 -0
  70. data/share/systemd.service +14 -0
  71. 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