• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yast / yast-bootloader / 5901045604

18 Aug 2023 09:27AM UTC coverage: 87.999% (+0.1%) from 87.881%
5901045604

Pull #686

github

schubi2
fixed typo; added docu
Pull Request #686: Systemd boot feature

735 of 735 new or added lines in 15 files covered. (100.0%)

3175 of 3608 relevant lines covered (88.0%)

13.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.53
/src/lib/bootloader/proposal_client.rb
1
# frozen_string_literal: true
2

3
require "installation/proposal_client"
1✔
4
require "bootloader/exceptions"
1✔
5
require "bootloader/main_dialog"
1✔
6
require "bootloader/bootloader_factory"
1✔
7
require "bootloader/systeminfo"
1✔
8
require "yast2/popup"
1✔
9

10
Yast.import "BootArch"
1✔
11

12
module Bootloader
1✔
13
  # Proposal client for bootloader configuration
14
  # rubocop:disable Metrics/ClassLength
15
  class ProposalClient < ::Installation::ProposalClient
1✔
16
    # Error when during update media is booted by different technology than target system.
17
    class MismatchBootloader < RuntimeError
1✔
18
      include Yast::I18n
1✔
19

20
      def initialize(old_bootloader, new_bootloader)
1✔
21
        @old_bootloader = old_bootloader
1✔
22
        @new_bootloader = new_bootloader
1✔
23

24
        raise "Invalid old bootloader #{old_bootloader}" unless boot_map[old_bootloader]
1✔
25
        raise "Invalid new bootloader #{new_bootloader}" unless boot_map[new_bootloader]
1✔
26

27
        super("Mismatching bootloaders")
1✔
28
      end
29

30
      def boot_map
1✔
31
        textdomain "bootloader"
4✔
32

33
        {
34
          # TRANSLATORS: kind of boot. It is term for way how x86_64 can boot
35
          "grub2"        => _("Legacy BIOS boot"),
4✔
36
          # TRANSLATORS: kind of boot. It is term for way how x86_64 can boot
37
          "grub2-efi"    => _("EFI boot"),
38
          # TRANSLATORS: kind of boot. It is term for way how can boot.
39
          "systemd-boot" => _("Systemd boot")
40
        }
41
      end
42

43
      def user_message
1✔
44
        textdomain "bootloader"
1✔
45

46
        # TRANSLATORS: keep %{} intact. It will be replaced by kind of boot
47
        format(_(
1✔
48
                 "Cannot upgrade the bootloader because of a mismatch of the boot technology. " \
49
                 "The upgraded system uses <i>%{old_boot}</i> while the installation medium " \
50
                 "has been booted using <i>%{new_boot}</i>.<br><br>" \
51
                 "This scenario is not supported, the upgraded system may not boot " \
52
                 "or the upgrade process can fail later."
53
               ),
54
          old_boot: boot_map[@old_bootloader], new_boot: boot_map[@new_bootloader])
55
      end
56
    end
57

58
    include Yast::I18n
1✔
59
    include Yast::Logger
1✔
60

61
    def initialize
1✔
62
      textdomain "bootloader"
28✔
63

64
      super
28✔
65

66
      Yast.import "UI"
28✔
67
      Yast.import "Arch"
28✔
68
      Yast.import "BootStorage"
28✔
69
      Yast.import "Bootloader"
28✔
70
      Yast.import "Installation"
28✔
71
      Yast.import "Mode"
28✔
72
      Yast.import "BootSupportCheck"
28✔
73
      Yast.import "Product"
28✔
74
      Yast.import "PackagesProposal"
28✔
75
    end
76

77
    PROPOSAL_LINKS = [
1✔
78
      "enable_boot_mbr",
79
      "disable_boot_mbr",
80
      "enable_boot_boot",
81
      "disable_boot_boot",
82
      "enable_boot_extended",
83
      "disable_boot_extended",
84
      "enable_secure_boot",
85
      "disable_secure_boot",
86
      "enable_update_nvram",
87
      "disable_update_nvram",
88
      "enable_trusted_boot",
89
      "disable_trusted_boot"
90
    ].freeze
91

92
    def make_proposal(attrs)
1✔
93
      make_proposal_raising(attrs)
19✔
94
    rescue ::Bootloader::NoRoot
95
      no_root_proposal
1✔
96
    rescue MismatchBootloader => e
97
      mismatch_bootloader_proposal(e)
1✔
98
    rescue ::Bootloader::BrokenConfiguration => e
99
      broken_configuration_proposal(e)
1✔
100
    end
101

102
    def ask_user(param)
1✔
103
      chosen_id = param["chosen_id"]
10✔
104
      result = :next
10✔
105
      log.info "ask user called with #{chosen_id}"
10✔
106

107
      # enable boot from MBR
108
      case chosen_id
10✔
109
      when *PROPOSAL_LINKS
110
        value = (chosen_id =~ /enable/) ? true : false
5✔
111
        option = chosen_id[/(enable|disable)_(.*)/, 2]
5✔
112
        single_click_action(option, value)
5✔
113
      else
114
        settings = export_settings
5✔
115
        result = ::Bootloader::MainDialog.new.run_auto
5✔
116
        if result == :next
5✔
117
          Yast::Bootloader.proposed_cfg_changed = true
3✔
118
        elsif settings
2✔
119
          Yast::Bootloader.Import(settings)
1✔
120
        end
121
      end
122
      # Fill return map
123
      { "workflow_sequence" => result }
10✔
124
    end
125

126
    def description
1✔
127
      {
128
        # proposal part - bootloader label
129
        "rich_text_title" => _("Booting"),
1✔
130
        # menubutton entry
131
        "menu_title"      => _("&Booting"),
132
        "id"              => "bootloader_stuff"
133
      }
134
    end
135

136
  private
1✔
137

138
    # make proposal without handling of exceptions
139
    def make_proposal_raising(attrs)
1✔
140
      force_reset = attrs["force_reset"]
19✔
141
      storage_read = Yast::BootStorage.storage_read?
19✔
142
      # This must be checked at the beginning because the call to BootStorage.boot_filesystem
143
      # below can trigger a re-read and change the result of BootStorage.storage_changed?
144
      # See bsc#1180218 and bsc#1180976.
145
      storage_changed = Yast::BootStorage.storage_changed?
19✔
146

147
      if Yast::BootStorage.boot_filesystem.is?(:nfs)
19✔
148
        ::Bootloader::BootloaderFactory.current_name = "none"
1✔
149
        return construct_proposal_map
1✔
150
      end
151

152
      log.info "Storage changed: #{storage_changed} force_reset #{force_reset}."
17✔
153
      log.info "Storage read previously #{storage_read.inspect}"
17✔
154
      # clear storage-ng devices cache otherwise it crashes (bsc#1071931)
155
      Yast::BootStorage.reset_disks if storage_changed
17✔
156

157
      if reset_needed?(force_reset, storage_changed && storage_read)
17✔
158
        # force re-calculation of bootloader proposal
159
        # this deletes any internally cached values, a new proposal will
160
        # not be partially based on old data now any more
161
        log.info "Recalculation of bootloader configuration"
9✔
162
        Yast::Bootloader.Reset
9✔
163
      end
164

165
      if Yast::Mode.update
17✔
166
        return { "raw_proposal" => [_("do not change")] } unless propose_for_update(force_reset)
3✔
167
      elsif Yast::Bootloader.proposed_cfg_changed
14✔
168
        # do nothing as user already modify it
169
      else
170
        # in installation always propose missing stuff
171
        # current below use proposed value if not already set
172
        # If set, then use same bootloader, but propose it again
173
        bl = ::Bootloader::BootloaderFactory.current
7✔
174
        bl.propose
7✔
175
      end
176

177
      update_required_packages
15✔
178

179
      construct_proposal_map
15✔
180
    end
181

182
    # returns if proposal should be reseted
183
    # logic in this condition:
184
    # when reset is forced or user do not modify proposal, reset proposal,
185
    # but only when not using auto_mode
186
    # But if storage changed, always repropose as it can be very wrong.
187
    def reset_needed?(force_reset, storage_changed)
1✔
188
      log.info "reset_needed? force_reset: #{force_reset} storage_changed: #{storage_changed}" \
17✔
189
               "auto mode: #{Yast::Mode.auto} cfg_changed #{Yast::Bootloader.proposed_cfg_changed}"
190
      return true if storage_changed
17✔
191
      return false if Yast::Mode.autoinst || Yast::Mode.autoupgrade
16✔
192
      return true if force_reset
15✔
193
      # reset when user does not do any change and not in update
194
      return true if !Yast::Mode.update && !Yast::Bootloader.proposed_cfg_changed
14✔
195

196
      false
7✔
197
    end
198

199
    BOOT_SYSCONFIG_PATH = "/etc/sysconfig/bootloader"
1✔
200
    # read bootloader from /mnt as SCR is not yet switched in proposal
201
    # phase of update (bnc#874646)
202
    def old_bootloader
1✔
203
      target_boot_sysconfig_path = ::File.join(Yast::Installation.destdir, BOOT_SYSCONFIG_PATH)
×
204
      return nil unless ::File.exist? target_boot_sysconfig_path
×
205

206
      boot_sysconfig = ::File.read target_boot_sysconfig_path
×
207
      old_bootloader = boot_sysconfig.lines.grep(/^\s*LOADER_TYPE/)
×
208
      log.info "bootloader entry #{old_bootloader.inspect}"
×
209
      return nil if old_bootloader.empty?
×
210

211
      # get value from entry
212
      old_bootloader.last.chomp.sub(/^.*=\s*(\S*).*/, "\\1").delete('"\'')
×
213
    end
214

215
    def propose_for_update(force_reset)
1✔
216
      current_bl = ::Bootloader::BootloaderFactory.current
3✔
217

218
      if grub2_update?(current_bl)
3✔
219
        log.info "update of grub2, do not repropose"
×
220
        Yast::Bootloader.ReadOrProposeIfNeeded
×
221
      elsif old_bootloader == "none"
3✔
222
        log.info "Bootloader not configured, do not repropose"
1✔
223
        # blRead just exits for none bootloader
224
        ::Bootloader::BootloaderFactory.current_name = "none"
1✔
225
        ::Bootloader::BootloaderFactory.current.read
1✔
226

227
        return false
1✔
228
      # old one is grub2, but mismatch of EFI and non-EFI (bsc#1081355)
229
      elsif old_bootloader =~ /grub2/ && old_bootloader != current_bl.name
2✔
230
        raise MismatchBootloader.new(old_bootloader, current_bl.name)
1✔
231
      elsif !current_bl.proposed? || force_reset
1✔
232
        # Repropose the type. A regular Reset/Propose is not enough.
233
        # For more details see bnc#872081
234
        Yast::Bootloader.Reset
1✔
235
        ::Bootloader::BootloaderFactory.clear_cache
1✔
236
        proposed = ::Bootloader::BootloaderFactory.proposed
1✔
237
        proposed.propose
1✔
238
        ::Bootloader::BootloaderFactory.current = proposed
1✔
239
      end
240

241
      true
1✔
242
    end
243

244
    def grub2_update?(current_bl)
1✔
245
      [current_bl.name].include?(old_bootloader) &&
3✔
246
        !current_bl.proposed? &&
247
        !Yast::Bootloader.proposed_cfg_changed
248
    end
249

250
    def construct_proposal_map
1✔
251
      ret = {}
16✔
252

253
      ret["links"] = PROPOSAL_LINKS # use always possible links even if it maybe not used
16✔
254
      ret["raw_proposal"] = Yast::Bootloader.Summary
16✔
255
      ret["label_proposal"] = Yast::Bootloader.Summary(simple_mode: true)
15✔
256

257
      # support diskless client (FATE#300779)
258
      if Yast::BootStorage.boot_filesystem.is?(:nfs)
15✔
259
        log.info "Boot partition is nfs type, bootloader will not be installed."
1✔
260
        return ret
1✔
261
      end
262

263
      handle_errors(ret)
14✔
264

265
      ret
14✔
266
    end
267

268
    # Result of {#make_proposal} if a {Bootloader::NoRoot} exception is raised
269
    # while calculating the proposal
270
    #
271
    # @return [Hash]
272
    def no_root_proposal
1✔
273
      {
274
        "label_proposal" => [],
1✔
275
        "warning_level"  => :fatal,
276
        "warning"        => _("Cannot detect device mounted as root. Please check partitioning.")
277
      }
278
    end
279

280
    # Result of {#make_proposal} if a {Bootloader::BrokenConfiguration} exception
281
    # is raised while calculating the proposal
282
    #
283
    # @param err [Bootloader::BrokenConfiguration] raised exception
284
    # @return [Hash]
285
    def broken_configuration_proposal(err)
1✔
286
      {
287
        "label_proposal" => [],
1✔
288
        "warning_level"  => :error,
289
        # TRANSLATORS: %s is a string containing the technical details of the error
290
        "warning"        => _(
291
          "Error reading the bootloader configuration files. " \
292
          "Please open the booting options to fix it. Details: %s"
293
        ) % err.reason
294
      }
295
    end
296

297
    # Result of {#make_proposal} if a {Bootloader::MismatchBootloader} exception
298
    # is raised while calculating the proposal
299
    #
300
    # @param err [Bootloader::MismatchBootloader] raised exception
301
    # @return [Hash]
302
    def mismatch_bootloader_proposal(err)
1✔
303
      {
304
        "label_proposal" => [],
1✔
305
        "warning_level"  => :warning,
306
        "warning"        => err.user_message
307
      }
308
    end
309

310
    # Settings from the system being upgraded
311
    #
312
    # If the configuration is broken, this method simply returns nil without
313
    # user interaction. The circumstance has already been notified via the
314
    # standard warning mechanism of the proposal and the user will be asked
315
    # about what to do when opening the main configuration dialog.
316
    #
317
    # We don't need to handle the same problem for a third time.
318
    #
319
    # @return [Hash, nil] nil if the existing configuration is broken
320
    def export_settings
1✔
321
      Yast::Bootloader.Export
5✔
322
    rescue ::Bootloader::BrokenConfiguration
323
      nil
2✔
324
    end
325

326
    # Add to argument proposal map all errors detected by proposal
327
    # @return modified parameter
328
    def handle_errors(ret)
1✔
329
      current_bl = ::Bootloader::BootloaderFactory.current
14✔
330
      if current_bl.name == "none"
14✔
331
        log.error "No bootloader selected"
1✔
332
        ret["warning_level"] = :warning
1✔
333
        # warning text in the summary richtext
334
        ret["warning"] = _(
1✔
335
          "No boot loader is selected for installation. Your system might not be bootable."
336
        )
337
        return
1✔
338
      end
339

340
      if !Yast::BootStorage.bootloader_installable?
13✔
341
        ret["warning_level"] = :error
1✔
342
        ret["warning"] = _(
1✔
343
          "Because of the partitioning, the bootloader cannot be installed properly"
344
        )
345
        return
1✔
346
      end
347

348
      if !Yast::BootSupportCheck.SystemSupported
12✔
349
        ret["warning_level"] = :error
7✔
350
        ret["warning"] = Yast::BootSupportCheck.StringProblems
7✔
351
        return
7✔
352
      end
353

354
      nil
355
    end
356

357
    CLICK_MAPPING = {
1✔
358
      "boot_mbr"      => :boot_disk_names,
359
      "boot_boot"     => :boot_partition_names,
360
      "boot_extended" => :extended_boot_partitions_names
361
    }.freeze
362
    def single_click_action(option, value)
1✔
363
      bootloader = ::Bootloader::BootloaderFactory.current
5✔
364

365
      log.info "single_click_action: option #{option}, value #{value.inspect}"
5✔
366

367
      case option
5✔
368
      when "boot_mbr", "boot_boot", "boot_extended"
369
        stage1 = bootloader.stage1
4✔
370
        devices = stage1.public_send(CLICK_MAPPING[option])
4✔
371
        log.info "single_click_action: devices #{devices}"
4✔
372
        devices.each do |device|
4✔
373
          value ? stage1.add_udev_device(device) : stage1.remove_device(device)
4✔
374
        end
375
      when "trusted_boot"
376
        bootloader.trusted_boot = value
×
377
      when "update_nvram"
378
        bootloader.update_nvram = value
1✔
379
      when "secure_boot"
380
        bootloader.secure_boot = value
×
381
        if value && Yast::Arch.s390
×
382
          Yast2::Popup.show(
×
383
            _(
384
              "The new secure-boot enabled boot data format works only on z15 " \
385
              "and later and only for zFCP disks.\n\n" \
386
              "The system does not boot if these requirements are not met."
387
            ),
388
            headline: :warning, buttons: :ok
389
          )
390
        end
391
      end
392

393
      Yast::Bootloader.proposed_cfg_changed = true
5✔
394
    end
395

396
    def update_required_packages
1✔
397
      bl = ::Bootloader::BootloaderFactory.current
15✔
398
      bootloader_resolvables = Yast::PackagesProposal.GetResolvables("yast2-bootloader", :package)
15✔
399
      log.info "proposed packages to install #{bl.packages}"
15✔
400
      Yast::PackagesProposal.RemoveResolvables("yast2-bootloader", :package, bootloader_resolvables)
15✔
401
      Yast::PackagesProposal.AddResolvables("yast2-bootloader", :package, bl.packages)
15✔
402
    end
403
  end
404
  # rubocop:enable Metrics/ClassLength
405
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc