From 4e2183b90ca7279fc8222c990c15e694fa0ecb9f Mon Sep 17 00:00:00 2001 From: Stefan Schubert Date: Mon, 13 Jan 2025 11:36:13 +0100 Subject: [PATCH] Supporting Grub2-BLS (#708) * supporting Grub2-BLS --- README.md | 1 + doc/bootloader_backend.svg | 1490 ++++++--------------- package/yast2-bootloader.changes | 6 + package/yast2-bootloader.spec | 2 +- src/lib/bootloader/autoyast_converter.rb | 26 +- src/lib/bootloader/bls.rb | 105 ++ src/lib/bootloader/bls_sections.rb | 67 + src/lib/bootloader/bootloader_base.rb | 12 + src/lib/bootloader/bootloader_factory.rb | 21 +- src/lib/bootloader/config_dialog.rb | 2 +- src/lib/bootloader/generic_widgets.rb | 84 +- src/lib/bootloader/grub2.rb | 4 +- src/lib/bootloader/grub2_widgets.rb | 103 +- src/lib/bootloader/grub2base.rb | 26 +- src/lib/bootloader/grub2bls.rb | 174 +++ src/lib/bootloader/grub2efi.rb | 29 +- src/lib/bootloader/os_prober.rb | 8 +- src/lib/bootloader/pmbr.rb | 63 + src/lib/bootloader/sysconfig.rb | 2 +- src/lib/bootloader/systemdboot.rb | 81 +- src/lib/bootloader/systemdboot_widgets.rb | 13 + src/lib/bootloader/systeminfo.rb | 60 +- src/lib/cfa/systemd_boot.rb | 100 -- src/modules/BootSupportCheck.rb | 4 +- test/bls_sections_test.rb | 65 + test/bls_test.rb | 59 + test/boot_support_test.rb | 7 + test/bootloader_factory_test.rb | 8 +- test/generic_widgets_test.rb | 25 + test/grub2_bls_test.rb | 171 +++ test/grub2_efi_test.rb | 4 +- test/grub2_test.rb | 4 +- test/grub2_widgets_test.rb | 26 - test/os_prober_test.rb | 15 +- test/systemdboot_test.rb | 55 +- 35 files changed, 1505 insertions(+), 1417 deletions(-) create mode 100644 src/lib/bootloader/bls.rb create mode 100644 src/lib/bootloader/bls_sections.rb create mode 100644 src/lib/bootloader/grub2bls.rb create mode 100644 src/lib/bootloader/pmbr.rb delete mode 100644 src/lib/cfa/systemd_boot.rb create mode 100755 test/bls_sections_test.rb create mode 100644 test/bls_test.rb create mode 100644 test/grub2_bls_test.rb diff --git a/README.md b/README.md index 8ca3e2166..79081195b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ that holds and also can propose the bootloader implementation. So now let's expl - [GRUB2](https://www.rubydoc.info/github/yast/yast-bootloader/master/Bootloader/Grub2) for legacy booting or emulated grub2 boot like s390x. - [GRUB2-EFI](https://www.rubydoc.info/github/yast/yast-bootloader/master/Bootloader/Grub2EFI) for EFI variant of GRUB2 bootloader +- [GRUB2-BLS](https://www.rubydoc.info/github/yast/yast-bootloader/master/Bootloader/Grub2Bls) bootloader based on Boot Loader Specification(BLS) (for EFI only) - [systemd-boot](https://www.rubydoc.info/github/yast/yast-bootloader/master/Bootloader/SystemdBoot) systemd bootloader (for EFI only) - [None](https://www.rubydoc.info/github/yast/yast-bootloader/master/Bootloader/NoneBootloader) when YaST does not manage booting - [GRUB2 base](https://www.rubydoc.info/github/yast/yast-bootloader/master/Bootloader/Grub2Base) is the shared functionality for both GRUB2 implementations diff --git a/doc/bootloader_backend.svg b/doc/bootloader_backend.svg index aa013103e..5f14779c4 100644 --- a/doc/bootloader_backend.svg +++ b/doc/bootloader_backend.svg @@ -1,1044 +1,446 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - systemd-boot - - - - - + + Created with Fabric.js 5.2.4 + + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + systemd-boot + + + + + + + + + + GRUB2-BLS + + + + + + + + + + + + + + + + + + + + sdbootutil + + + + + + \ No newline at end of file diff --git a/package/yast2-bootloader.changes b/package/yast2-bootloader.changes index b29371870..0e772795d 100644 --- a/package/yast2-bootloader.changes +++ b/package/yast2-bootloader.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jan 9 11:26:59 UTC 2025 - Stefan Schubert + +- Added grub2-bls support (jsc#PED-10703). +- 5.0.14 + ------------------------------------------------------------------- Fri Dec 20 10:26:41 UTC 2024 - Josef Reidinger diff --git a/package/yast2-bootloader.spec b/package/yast2-bootloader.spec index 7602e9dfe..ffebaadb7 100644 --- a/package/yast2-bootloader.spec +++ b/package/yast2-bootloader.spec @@ -17,7 +17,7 @@ Name: yast2-bootloader -Version: 5.0.13 +Version: 5.0.14 Release: 0 Summary: YaST2 - Bootloader Configuration License: GPL-2.0-or-later diff --git a/src/lib/bootloader/autoyast_converter.rb b/src/lib/bootloader/autoyast_converter.rb index bbb698ccc..3dcae73ce 100644 --- a/src/lib/bootloader/autoyast_converter.rb +++ b/src/lib/bootloader/autoyast_converter.rb @@ -34,16 +34,18 @@ def import(data) return bootloader if bootloader.name == "none" case bootloader.name - when "grub2", "grub2-efi" - import_grub2(data, bootloader) - import_grub2efi(data, bootloader) - import_stage1(data, bootloader) + when "grub2", "grub2-efi", "grub2-bls" + if ["grub2", "grub2-efi"].include?(bootloader.name) + import_grub2(data, bootloader) + import_grub2efi(data, bootloader) + import_stage1(data, bootloader) + import_device_map(data, bootloader) + import_password(data, bootloader) + # always nil pmbr as autoyast does not support it yet, + # so use nil to always use proposed value (bsc#1081967) + bootloader.pmbr_action = nil + end import_default(data, bootloader.grub_default) - import_device_map(data, bootloader) - import_password(data, bootloader) - # always nil pmbr as autoyast does not support it yet, - # so use nil to always use proposed value (bsc#1081967) - bootloader.pmbr_action = nil cpu_mitigations = data.global.cpu_mitigations if cpu_mitigations bootloader.cpu_mitigations = CpuMitigations.from_string(cpu_mitigations) @@ -72,18 +74,18 @@ def export(config) res["global"] = {} case config.name - when "grub2", "grub2-efi" + when "grub2", "grub2-efi", "grub2-bls" global = res["global"] export_grub2(global, config) if config.name == "grub2" export_grub2efi(global, config) if config.name == "grub2-efi" + export_password(global, config.password) if ["grub2", "grub2-efi"].include?(config.name) export_default(global, config.grub_default) - export_password(global, config.password) res["global"]["cpu_mitigations"] = config.cpu_mitigations.value.to_s when "systemd-boot" res["global"]["timeout"] = config.menu_timeout res["global"]["secure_boot"] = config.secure_boot else - raise UnsupportedBootloader, bootloader.name + raise UnsupportedBootloader, config.name end # Do not export device map as device name are very unpredictable and is used only as # work-around when automatic ones do not work for what-ever reasons ( it can really safe diff --git a/src/lib/bootloader/bls.rb b/src/lib/bootloader/bls.rb new file mode 100644 index 000000000..e47d54045 --- /dev/null +++ b/src/lib/bootloader/bls.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require "fileutils" +require "yast" +require "bootloader/sysconfig" +require "bootloader/cpu_mitigations" +require "cfa/grub2/default" + +Yast.import "Report" + +module Bootloader + # Represents bls compatile system calls which can be used + # e.g. by grub2-bls and systemd-boot + class Bls + include Yast::Logger + extend Yast::I18n + + SDBOOTUTIL = "/usr/bin/sdbootutil" + + def initialize + textdomain "bootloader" + end + + def self.create_menu_entries + Yast::Execute.on_target!(SDBOOTUTIL, "--verbose", "add-all-kernels") + rescue Cheetah::ExecutionFailed => e + Yast::Report.Error( + format(_( + "Cannot create boot menu entry:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + end + + def self.install_bootloader + Yast::Execute.on_target!(SDBOOTUTIL, "--verbose", + "install") + rescue Cheetah::ExecutionFailed => e + Yast::Report.Error( + format(_( + "Cannot install bootloader:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + end + + def self.write_menu_timeout(timeout) + Yast::Execute.on_target!(SDBOOTUTIL, "set-timeout", timeout) + rescue Cheetah::ExecutionFailed => e + Yast::Report.Error( + format(_( + "Cannot write boot menu timeout:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + end + + def self.menu_timeout + begin + output = Yast::Execute.on_target!(SDBOOTUTIL, "get-timeout", stdout: :capture).to_i + rescue Cheetah::ExecutionFailed => e + Yast::Report.Error( + format(_( + "Cannot read boot menu timeout:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + output = -1 + end + output + end + + def self.write_default_menu(default) + Yast::Execute.on_target!(SDBOOTUTIL, "set-default", default) + rescue Cheetah::ExecutionFailed => e + Yast::Report.Error( + format(_( + "Cannot write default boot menu entry:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + end + + def self.default_menu + begin + output = Yast::Execute.on_target!(SDBOOTUTIL, "get-default", stdout: :capture) + rescue Cheetah::ExecutionFailed => e + Yast::Report.Error( + format(_( + "Cannot read default menu:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + output = "" + end + output + end + end +end diff --git a/src/lib/bootloader/bls_sections.rb b/src/lib/bootloader/bls_sections.rb new file mode 100644 index 000000000..808f6935c --- /dev/null +++ b/src/lib/bootloader/bls_sections.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "json" +require "yast" +require "yast2/execute" +require "bootloader/bls" + +Yast.import "Misc" + +module Bootloader + # Represents available sections and handling of default BLS boot entry + class BlsSections + include Yast::Logger + + # @return [Array] list of all available boot titles + # or an empty array + attr_reader :all + + # @return [String] title of default boot section. + attr_reader :default + + def initialize + @all = [] + @default = "" + end + + # Sets default section internally. + # @param [String] value of new boot title to boot + # @note to write it to system use #write later + def default=(value) + log.info "set new default to '#{value.inspect}'" + + # empty value mean no default specified + if !all.empty? && !all.include?(value) && !value.empty? + log.warn "Invalid value #{value} trying to set as default. Fallback to default" + value = "" + end + + @default = value + end + + # writes default to system making it persistent + def write + return if @default.empty? + + Bls.write_default_menu(@default) + end + + def read + @data = read_entries + @all = @data.map { |e| e["title"] } + @default = Bls.default_menu + end + + private + + # @return [Array] return array of entries or [] + def read_entries + output = Yast::Execute.on_target( + "/usr/bin/bootctl", "--json=short", "list", stdout: :capture + ) + return [] if output.nil? + + JSON.parse(output) + end + end +end diff --git a/src/lib/bootloader/bootloader_base.rb b/src/lib/bootloader/bootloader_base.rb index 7268dd55c..b01230467 100644 --- a/src/lib/bootloader/bootloader_base.rb +++ b/src/lib/bootloader/bootloader_base.rb @@ -11,7 +11,11 @@ module Bootloader # Represents base for all kinds of bootloaders class BootloaderBase + include Yast::I18n + def initialize + textdomain "bootloader" + @read = false @proposed = false @initial_sysconfig = Sysconfig.from_system @@ -113,5 +117,13 @@ def include_kexec_tools_package? def restore_initial_sysconfig @initial_sysconfig.write end + + def status_string(status) + if status + _("enabled") + else + _("disabled") + end + end end end diff --git a/src/lib/bootloader/bootloader_factory.rb b/src/lib/bootloader/bootloader_factory.rb index f63b324a3..2bb108d2c 100644 --- a/src/lib/bootloader/bootloader_factory.rb +++ b/src/lib/bootloader/bootloader_factory.rb @@ -5,6 +5,7 @@ require "bootloader/none_bootloader" require "bootloader/grub2" require "bootloader/grub2efi" +require "bootloader/grub2bls" require "bootloader/systemdboot" require "bootloader/exceptions" @@ -24,6 +25,7 @@ class BootloaderFactory # Keyword used in autoyast for default bootloader used for given system. DEFAULT_KEYWORD = "default" SYSTEMDBOOT = "systemd-boot" + GRUB2BLS = "grub2-bls" class << self include Yast::Logger @@ -57,6 +59,7 @@ def supported_names if Yast::Mode.config # default means bootloader use what it think is the best result = BootloaderFactory::SUPPORTED_BOOTLOADERS.clone + result << GRUB2BLS if use_grub2_bls? result << SYSTEMDBOOT if use_systemd_boot? result << DEFAULT_KEYWORD return result @@ -72,6 +75,7 @@ def supported_names # grub2 everywhere except aarch64 or riscv64 ret << "grub2" unless Systeminfo.efi_mandatory? ret << "grub2-efi" if Systeminfo.efi_supported? + ret << GRUB2BLS if use_grub2_bls? ret << SYSTEMDBOOT if use_systemd_boot? ret << "none" # avoid double entry for selected one @@ -89,6 +93,8 @@ def bootloader_by_name(name) @cached_bootloaders["grub2-efi"] ||= Grub2EFI.new when "systemd-boot" @cached_bootloaders["systemd-boot"] ||= SystemdBoot.new + when "grub2-bls" + @cached_bootloaders["grub2-bls"] ||= Grub2Bls.new when "none" @cached_bootloaders["none"] ||= NoneBootloader.new when String @@ -108,15 +114,28 @@ def use_systemd_boot? (Yast::Arch.x86_64 || Yast::Arch.aarch64) # only these architectures are supported. end + def use_grub2_bls? + (Yast::Arch.x86_64 || Yast::Arch.aarch64) # only these architectures are supported. + end + def grub2_efi_installable? Systeminfo.efi_mandatory? || ((Yast::Arch.x86_64 || Yast::Arch.i386) && Systeminfo.efi?) end + def bls_installable? + ((Yast::Arch.x86_64 || Yast::Arch.i386) && Systeminfo.efi?) + end + def proposed_name prefered_bootloader = Yast::ProductFeatures.GetStringFeature("globals", "prefered_bootloader") - if supported_names.include?(prefered_bootloader) && prefered_bootloader != "grub2-efi" + if supported_names.include?(prefered_bootloader) && + !["grub2-efi", "systemd-boot", "grub2-bls"].include?(prefered_bootloader) + return prefered_bootloader + end + + if ["systemd-boot", "grub2-bls"].include?(prefered_bootloader) && bls_installable? return prefered_bootloader end diff --git a/src/lib/bootloader/config_dialog.rb b/src/lib/bootloader/config_dialog.rb index e2d5a697d..52ccfdc29 100644 --- a/src/lib/bootloader/config_dialog.rb +++ b/src/lib/bootloader/config_dialog.rb @@ -93,7 +93,7 @@ def contents boot_code_tab = ::Bootloader::SystemdBootWidget::BootCodeTab.new kernel_tab = ::Bootloader::SystemdBootWidget::KernelTab.new bootloader_tab = ::Bootloader::SystemdBootWidget::BootloaderTab.new - else + else # grub2, grub2-efi, grub2-bls boot_code_tab = ::Bootloader::Grub2Widget::BootCodeTab.new kernel_tab = ::Bootloader::Grub2Widget::KernelTab.new bootloader_tab = ::Bootloader::Grub2Widget::BootloaderTab.new diff --git a/src/lib/bootloader/generic_widgets.rb b/src/lib/bootloader/generic_widgets.rb index f3c938124..203b81c1f 100644 --- a/src/lib/bootloader/generic_widgets.rb +++ b/src/lib/bootloader/generic_widgets.rb @@ -43,8 +43,10 @@ def localized_names(name) names = { "grub2" => _("GRUB2"), "grub2-efi" => _("GRUB2 for EFI"), - # Translators: option in combo box when bootloader is not managed by yast2 + # Translators: Using Boot Loader Specification (BLS) snippets. + "grub2-bls" => _("GRUB2 with BLS"), "systemd-boot" => _("Systemd Boot"), + # Translators: option in combo box when bootloader is not managed by yast2 "none" => _("Not Managed"), "default" => _("Default") } @@ -52,8 +54,6 @@ def localized_names(name) names[name] or raise "Unknown supported bootloader '#{name}'" end - # rubocop:disable Metrics/MethodLength - # It will be reduced again if systemd-boot is not anymore in beta phase. def handle old_bl = BootloaderFactory.current.name new_bl = value @@ -73,25 +73,19 @@ def handle return :redraw if !Yast::Popup.ContinueCancel(popup_msg) end - if new_bl == "systemd-boot" - # popup - Continue/Cancel - popup_msg = _( - "\n" \ - "Systemd-boot support is currently work in progress and\n" \ - "may not work as expected. Use at your own risk.\n" \ - "\n" \ - "Currently we do not provide official maintenance or support.\n" \ - "Proceed?\n" - ) - - return :redraw if !Yast::Popup.ContinueCancel(popup_msg) + if !Yast::Stage.initial && ["systemd-boot", "grub2-bls"].include?(old_bl) + Yast::Popup.Warning(format(_( + "Switching from %s to another bootloader\n" \ + "is currently not supported.\n" + ), old_bl)) + return :redraw end - if !Yast::Stage.initial && (old_bl == "systemd-boot") - Yast::Popup.Warning(_( - "Switching from systemd-boot to another bootloader\n" \ + if !Yast::Stage.initial && ["systemd-boot", "grub2-bls"].include?(new_bl) + Yast::Popup.Warning(format(_( + "Switching to bootloader %s \n" \ "is currently not supported.\n" - )) + ), new_bl)) return :redraw end @@ -101,7 +95,6 @@ def handle :redraw end - # rubocop:enable Metrics/MethodLength def help _( "

Boot Loader\n" \ @@ -171,6 +164,57 @@ def store end end + # Represents Protective MBR action + class PMBRWidget < CWM::ComboBox + def initialize + textdomain "bootloader" + + super + end + + def label + _("&Protective MBR flag") + end + + def help + _( + "

Protective MBR flag is expert only settings, that is needed " \ + "only on exotic hardware. For details see Protective MBR in GPT disks. " \ + "Do not touch if you are not sure.

" + ) + end + + def init + current_bl = ::Bootloader::BootloaderFactory.current + if current_bl.respond_to?(:pmbr_action) + self.value = current_bl.pmbr_action + else + log.error("Bootloader #{current_bl} does not support PMBR.") + disable + end + end + + def items + [ + # TRANSLATORS: set flag on disk + [:add, _("set")], + # TRANSLATORS: remove flag from disk + [:remove, _("remove")], + # TRANSLATORS: do not change flag on disk + [:nothing, _("do not change")] + ] + end + + def store + current_bl = ::Bootloader::BootloaderFactory.current + if current_bl.respond_to?(:pmbr_action) + current_bl.pmbr_action = value + else + log.error("Bootloader #{current_bl} does not support PMBR.") + end + end + end + # represents kernel command line class KernelAppendWidget < CWM::InputField def initialize diff --git a/src/lib/bootloader/grub2.rb b/src/lib/bootloader/grub2.rb index 587c931c6..b4c6685f3 100644 --- a/src/lib/bootloader/grub2.rb +++ b/src/lib/bootloader/grub2.rb @@ -8,6 +8,7 @@ require "bootloader/stage1" require "bootloader/grub_install" require "bootloader/systeminfo" +require "bootloader/pmbr" Yast.import "Arch" Yast.import "BootStorage" @@ -59,9 +60,8 @@ def write(etc_only: false) device_map.write if (Yast::Arch.x86_64 || Yast::Arch.i386) && !etc_only - # TODO: own class handling PBMR # set it only for gpt disk bsc#1008092 - pmbr_setup(*::Yast::BootStorage.gpt_disks(stage1.devices)) + Pmbr.write_none_efi(pmbr_action, stage1) # powernv must not call grub2-install (bnc#970582) if !Yast::Arch.board_powernv diff --git a/src/lib/bootloader/grub2_widgets.rb b/src/lib/bootloader/grub2_widgets.rb index 056126623..0afba8ef7 100644 --- a/src/lib/bootloader/grub2_widgets.rb +++ b/src/lib/bootloader/grub2_widgets.rb @@ -9,9 +9,9 @@ require "bootloader/systeminfo" require "bootloader/os_prober" require "bootloader/device_path" +require "bootloader/pmbr" require "cfa/matcher" -Yast.import "BootStorage" Yast.import "Initrd" Yast.import "Label" Yast.import "Report" @@ -78,7 +78,9 @@ def init end def store - if @hidden_menu_widget.checked? + if @hidden_menu_widget.is_a?(CWM::Empty) + grub_default.timeout = value.to_s + elsif @hidden_menu_widget.checked? grub_default.hidden_timeout = value.to_s grub_default.timeout = "0" else @@ -208,48 +210,6 @@ def store end end - # Represents Protective MBR action - class PMBRWidget < CWM::ComboBox - include Grub2Helper - - def initialize - textdomain "bootloader" - - super - end - - def label - _("&Protective MBR flag") - end - - def help - _( - "

Protective MBR flag is expert only settings, that is needed " \ - "only on exotic hardware. For details see Protective MBR in GPT disks. " \ - "Do not touch if you are not sure.

" - ) - end - - def init - self.value = grub2.pmbr_action - end - - def items - [ - # TRANSLATORS: set flag on disk - [:add, _("set")], - # TRANSLATORS: remove flag from disk - [:remove, _("remove")], - # TRANSLATORS: do not change flag on disk - [:nothing, _("do not change")] - ] - end - - def store - grub2.pmbr_action = value - end - end - # Represents switcher for secure boot on EFI class SecureBootWidget < CWM::CheckBox include Grub2Helper @@ -352,7 +312,8 @@ def store end def validate - return true if Yast::Mode.config || !value || grub2.name == "grub2-efi" + return true if Yast::Mode.config || !value || ["grub2-efi", + "grub2-bls"].include?(grub2.name) tpm_files = Dir.glob("/sys/**/pcrs") if !tpm_files.empty? && !File.read(tpm_files[0], 1).nil? @@ -970,6 +931,8 @@ def handle # represents Tab with kernel related configuration class KernelTab < CWM::Tab + include Grub2Helper + def label textdomain "bootloader" @@ -977,7 +940,6 @@ def label end def contents - console_widget = Yast::Arch.s390 ? CWM::Empty.new("console") : ConsoleWidget.new VBox( VSpacing(1), MarginBox(1, 0.5, KernelAppendWidget.new), @@ -986,6 +948,16 @@ def contents VStretch() ) end + + private + + def console_widget + if Systeminfo.console_supported?(grub2.name) + ConsoleWidget.new + else + CWM::Empty.new("console") + end + end end # Represent tab with options related to stage1 location and bootloader type @@ -1051,11 +1023,11 @@ def horizontal_margin end def loader_location_widget? - (Yast::Arch.x86_64 || Yast::Arch.i386 || Yast::Arch.ppc) && grub2.name == "grub2" + Systeminfo.loader_location_available?(grub2.name) end def generic_mbr_widget? - (Yast::Arch.x86_64 || Yast::Arch.i386) && grub2.name != "grub2-efi" + Systeminfo.generic_mbr_available?(grub2.name) end def secure_boot_widget? @@ -1071,18 +1043,19 @@ def update_nvram_widget? end def pmbr_widget? - (Yast::Arch.x86_64 || Yast::Arch.i386) && - Yast::BootStorage.gpt_boot_disk? + Pmbr.available? end def device_map_button? - (Yast::Arch.x86_64 || Yast::Arch.i386) && grub2.name != "grub2-efi" + Systeminfo.device_map?(grub2.name) end end # Represents bootloader specific options like its timeout, # default section or password protection class BootloaderTab < CWM::Tab + include Grub2Helper + def label textdomain "bootloader" @@ -1090,31 +1063,47 @@ def label end def contents - hiden_menu_widget = HiddenMenuWidget.new VBox( VSpacing(2), HBox( HSpacing(1), - TimeoutWidget.new(hiden_menu_widget), + TimeoutWidget.new(hidden_menu_widget), HSpacing(1), VBox( os_prober_widget, VSpacing(1), - Left(hiden_menu_widget) + Left(hidden_menu_widget) ), HSpacing(1) ), VSpacing(1), - MarginBox(1, 1, DefaultSectionWidget.new), - MarginBox(1, 1, GrubPasswordWidget.new), + MarginBox(1, 1, MinWidth(1, DefaultSectionWidget.new)), + MarginBox(1, 1, grub_password_widget), VStretch() ) end private + def grub_password_widget + if Systeminfo.password_supported?(grub2.name) + GrubPasswordWidget.new + else + CWM::Empty.new("password_widget") + end + end + + def hidden_menu_widget + if Systeminfo.hiding_menu_supported?(grub2.name) + HiddenMenuWidget.new + else + CWM::Empty.new("hidden_menu") + end + end + def os_prober_widget - if OsProber.available? # Checks !Arch.s390 and if package is available + # Checks !Arch.s390, not grub2-bls and if package is available + if OsProber.available?(grub2.name) Left(OSProberWidget.new) else CWM::Empty.new("os_prober") diff --git a/src/lib/bootloader/grub2base.rb b/src/lib/bootloader/grub2base.rb index 788b95120..a98aa0a5d 100644 --- a/src/lib/bootloader/grub2base.rb +++ b/src/lib/bootloader/grub2base.rb @@ -79,22 +79,6 @@ def initialize # general functions - # set pmbr flags on boot disks - # TODO: move it to own place - def pmbr_setup(*devices) - return if @pmbr_action == :nothing - - action_parted = case @pmbr_action - when :add then "on" - when :remove then "off" - else raise "invalid action #{action}" - end - - devices.each do |dev| - Yast::Execute.locally("/usr/sbin/parted", "-s", dev, "disk_set", "pmbr_boot", action_parted) - end - end - def cpu_mitigations CpuMitigations.from_kernel_params(grub_default.kernel_params) end @@ -189,7 +173,7 @@ def packages # # @return [Boolean] true if the os-prober package should be included; false otherwise. def include_os_prober_package? - OsProber.available? + OsProber.arch_supported? && OsProber.package_available? end def enable_serial_console(console_arg_string) @@ -435,14 +419,6 @@ def update_nvram_summary "#{_("Update NVRAM:")} #{status_string(update_nvram)} #{link}" end - - def status_string(status) - if status - _("enabled") - else - _("disabled") - end - end end # rubocop:enable Metrics/ClassLength end diff --git a/src/lib/bootloader/grub2bls.rb b/src/lib/bootloader/grub2bls.rb new file mode 100644 index 000000000..2b0e9ab78 --- /dev/null +++ b/src/lib/bootloader/grub2bls.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require "yast" +require "bootloader/bootloader_base" +require "bootloader/bls" +require "bootloader/bls_sections" + +Yast.import "Arch" +Yast.import "Report" +Yast.import "Stage" + +module Bootloader + # Represents grub2 bls bootloader with efi target + class Grub2Bls < Grub2Base + include Yast::Logger + include Yast::I18n + + attr_reader :sections + + CMDLINE = "/etc/kernel/cmdline" + + def initialize + super + textdomain "bootloader" + + @sections = ::Bootloader::BlsSections.new + @is_read = false + @is_proposed = false + end + + # Display bootloader summary + # @return a list of summary lines + def summary(*) + [ + Yast::Builtins.sformat( + _("Boot Loader Type: %1"), + "GRUB2 BLS" + ) + ] + end + + # @return bootloader name + def name + "grub2-bls" + end + + # reads configuration from target disk + def read + @sections.read + grub_default.timeout = Bls.menu_timeout + log.info "Boot timeout: #{grub_default.timeout}" + lines = "" + filename = File.join(Yast::Installation.destdir, CMDLINE) + if File.exist?(filename) + File.open(filename).each do |line| + lines = + line + end + end + grub_default.kernel_params.replace(lines) + log.info "kernel params: #{grub_default.kernel_params}" + log.info "bls sections: #{@sections.all}" + log.info "bls default: #{@sections.default}" + @is_read = true # flag that settings has been read + end + + # @return true if configuration is already read + def read? + @is_read + end + + # Proposes new configuration + def propose + log.info("Propose settings...") + if grub_default.kernel_params.empty? + kernel_line = Yast::BootArch.DefaultKernelParams(Yast::BootStorage.propose_resume) + grub_default.kernel_params.replace(kernel_line) + end + grub_default.timeout = Yast::ProductFeatures.GetIntegerFeature("globals", "boot_timeout").to_i + @is_proposed = true + # for UEFI always remove PMBR flag on disk (bnc#872054) + self.pmbr_action = :remove + end + + # @return true if configuration is already proposed + def proposed? + @is_proposed + end + + # writes configuration to target disk + def write(*) + Bls.install_bootloader if Yast::Stage.initial # while new installation only (currently) + Bls.create_menu_entries + Bls.install_bootloader + @sections.write + Bls.write_menu_timeout(grub_default.timeout) + + # writing kernel parameter to /etc/kernel/cmdline + File.open(File.join(Yast::Installation.destdir, CMDLINE), "w+") do |fw| + fw.puts(grub_default.kernel_params.serialize) + end + + Pmbr.write_efi(pmbr_action) + end + + # merges other bootloader configuration into this one. + # It have to be same bootloader type. + # rubocop:disable Metrics/AbcSize + def merge(other) + raise "Invalid merge argument #{other.name} for #{name}" if name != other.name + + log.info "merging: timeout: #{grub_default.timeout}=>#{other.grub_default.timeout}" + log.info " mitigations: #{cpu_mitigations.to_human_string}=>" \ + "#{other.cpu_mitigations.to_human_string}" + log.info " pmbr_action: #{pmbr_action}=>#{other.pmbr_action}" + log.info " grub_default.kernel_params: #{grub_default.kernel_params.serialize}=>" \ + "#{other.grub_default.kernel_params.serialize}" + log.info " grub_default.kernel_params: #{grub_default.kernel_params.serialize}=>" \ + "#{other.grub_default.kernel_params.serialize}" + + merge_sections(other) + merge_grub_default(other) + merge_pmbr_action(other) + + log.info "merging result: timeout: #{grub_default.timeout}" + log.info " mitigations: #{cpu_mitigations.to_human_string}" + log.info " kernel_params: #{grub_default.kernel_params.serialize}" + log.info " pmbr_action: #{pmbr_action}" + end + # rubocop:enable Metrics/AbcSize + + # @return [Array] packages required to configure given bootloader + def packages + res = super + res << ("grub2-" + grub2bls_architecture + "-efi-bls") + res << "sdbootutil" + res + end + + private + + def grub2bls_architecture + arch = Yast::Arch.architecture + table = { "x86_64" => "x86_64", + "amd64" => "x86_64", + "sparc" => "sparc64", + "mipsel" => "mipsel", + "mips64el" => "mipsel", + "mips" => "mips", + "mips64" => "mips", + "loongarch64" => "loongarch64" } + ret = table[arch] + if ret.empty? + ret = if arch.start_with?("arm") + "arm" + elsif arch.start_with?("aarch64") + "arm64" + elsif arch.start_with?("riscv32") + "riscv32" + elsif arch.start_with?("riscv64") + "riscv64" + else + arch # fallback, but useful ? + end + end + ret + end + + def merge_sections(other) + return if !other.sections.default || other.sections.default.empty? + + @sections.default = other.sections.default + end + end +end diff --git a/src/lib/bootloader/grub2efi.rb b/src/lib/bootloader/grub2efi.rb index 91234934b..aa3b52808 100644 --- a/src/lib/bootloader/grub2efi.rb +++ b/src/lib/bootloader/grub2efi.rb @@ -4,6 +4,7 @@ require "bootloader/grub2base" require "bootloader/grub_install" require "bootloader/sysconfig" +require "bootloader/pmbr" require "y2storage" Yast.import "Arch" @@ -27,7 +28,7 @@ def write(etc_only: false) # super have to called as first as grub install require some config written in ancestor super - pmbr_write if pmbr_action + Pmbr.write_efi(pmbr_action) unless etc_only @grub_install.execute(secure_boot: secure_boot, trusted_boot: trusted_boot, @@ -99,31 +100,5 @@ def write_sysconfig(prewrite: false) update_nvram: update_nvram) prewrite ? sysconfig.pre_write : sysconfig.write end - - private - - # Filesystems in the staging (planned) devicegraph - # - # @return [Y2Storage::FilesystemsList] - def filesystems - staging = Y2Storage::StorageManager.instance.staging - staging.filesystems - end - - # write pmbr flags - def pmbr_write - fs = filesystems - efi_partition = fs.find { |f| f.mount_path == "/boot/efi" } - efi_partition ||= fs.find { |f| f.mount_path == "/boot" } - efi_partition ||= fs.find { |f| f.mount_path == "/" } - - raise "could not find boot partiton" unless efi_partition - - disks = Yast::BootStorage.stage1_disks_for(efi_partition) - # set only gpt disks - disks.select! { |disk| disk.gpt? } - - pmbr_setup(*disks.map(&:name)) - end end end diff --git a/src/lib/bootloader/os_prober.rb b/src/lib/bootloader/os_prober.rb index b79875615..51903e266 100644 --- a/src/lib/bootloader/os_prober.rb +++ b/src/lib/bootloader/os_prober.rb @@ -13,10 +13,10 @@ def package_name "os-prober" end - # Check if os-prober is supported on this architecture and if the package - # is available - def available? - arch_supported? && package_available? + # Check if os-prober is supported on this architecture + # no grub2-bls bootloader and if the package is available + def available?(bootloader) + arch_supported? && package_available? && bootloader != "grub2-bls" end # Check if the os-prober package is available for installation diff --git a/src/lib/bootloader/pmbr.rb b/src/lib/bootloader/pmbr.rb new file mode 100644 index 000000000..a7902249d --- /dev/null +++ b/src/lib/bootloader/pmbr.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "yast" +require "y2storage" + +Yast.import "Arch" +Yast.import "BootStorage" + +module Bootloader + # Helper methods for PMBR + class Pmbr + class << self + def available? + (Yast::Arch.x86_64 || Yast::Arch.i386) && + Yast::BootStorage.gpt_boot_disk? + end + + def write_none_efi(action, stage1) + pmbr_setup(*::Yast::BootStorage.gpt_disks(stage1.devices), action) + end + + def write_efi(action) + fs = filesystems + efi_partition = fs.find { |f| f.mount_path == "/boot/efi" } + efi_partition ||= fs.find { |f| f.mount_path == "/boot" } + efi_partition ||= fs.find { |f| f.mount_path == "/" } + + raise "could not find boot partiton" unless efi_partition + + disks = Yast::BootStorage.stage1_disks_for(efi_partition) + # set only gpt disks + disks.select! { |disk| disk.gpt? } + pmbr_setup(*disks.map(&:name), action) + end + + private + + # Filesystems in the staging (planned) devicegraph + # + # @return [Y2Storage::FilesystemsList] + def filesystems + staging = Y2Storage::StorageManager.instance.staging + staging.filesystems + end + + # set pmbr flags on boot disks + def pmbr_setup(*devices, action) + return if action == :nothing + + action_parted = case action + when :add then "on" + when :remove then "off" + else raise "invalid action #{action}" + end + + devices.each do |dev| + Yast::Execute.locally("/usr/sbin/parted", "-s", dev, "disk_set", "pmbr_boot", + action_parted) + end + end + end + end +end diff --git a/src/lib/bootloader/sysconfig.rb b/src/lib/bootloader/sysconfig.rb index 772bc7341..9b4da9d77 100644 --- a/src/lib/bootloader/sysconfig.rb +++ b/src/lib/bootloader/sysconfig.rb @@ -61,7 +61,7 @@ def pre_write bootloader: "\n" \ "## Path:\tSystem/Bootloader\n" \ "## Description:\tBootloader configuration\n" \ - "## Type:\tlist(grub,grub2,grub2-efi,systemd-boot,none)\n" \ + "## Type:\tlist(grub,grub2,grub2-efi,grub2-bls,systemd-boot,none)\n" \ "## Default:\tgrub2\n" \ "#\n" \ "# Type of bootloader in use.\n" \ diff --git a/src/lib/bootloader/systemdboot.rb b/src/lib/bootloader/systemdboot.rb index b525afa5c..140dc879d 100644 --- a/src/lib/bootloader/systemdboot.rb +++ b/src/lib/bootloader/systemdboot.rb @@ -4,7 +4,7 @@ require "yast" require "bootloader/sysconfig" require "bootloader/cpu_mitigations" -require "cfa/systemd_boot" +require "bootloader/bls" require "cfa/grub2/default" Yast.import "Report" @@ -29,6 +29,10 @@ class SystemdBoot < BootloaderBase # @return [Boolean] current secure boot setting attr_accessor :secure_boot + # @!attribute pmbr_action + # @return [:remove, :add, :nothing] + attr_accessor :pmbr_action + def initialize super @@ -37,6 +41,7 @@ def initialize # like grub2 in order to be compatible with all calls. @kernel_container = ::CFA::Grub2::Default.new @explicit_cpu_mitigations = false + @pmbr_action = :nothing end def kernel_params @@ -49,11 +54,13 @@ def merge(other) log.info " secure_boot: #{secure_boot}=>#{other.secure_boot}" log.info " mitigations: #{cpu_mitigations.to_human_string}=>" \ "#{other.cpu_mitigations.to_human_string}" + log.info " pmbr_action: #{pmbr_action}=>#{other.pmbr_action}" log.info " kernel_params: #{kernel_params.serialize}=>" \ "#{other.kernel_params.serialize}" super self.menu_timeout = other.menu_timeout unless other.menu_timeout.nil? self.secure_boot = other.secure_boot unless other.secure_boot.nil? + self.pmbr_action = other.pmbr_action if other.pmbr_action kernel_serialize = kernel_params.serialize # handle specially noresume as it should lead to remove all other resume @@ -75,6 +82,7 @@ def merge(other) log.info " secure_boot: #{secure_boot}" log.info " mitigations: #{cpu_mitigations.to_human_string}" log.info " kernel_params: #{kernel_params.serialize}" + log.info " pmbr_action: #{pmbr_action}" end # rubocop:enable Metrics/AbcSize @@ -95,7 +103,7 @@ def cpu_mitigations=(value) def read super - read_menu_timeout + self.menu_timeout = Bls.menu_timeout self.secure_boot = Systeminfo.secure_boot_active? lines = "" @@ -112,9 +120,11 @@ def read def write(etc_only: false) super log.info("Writing settings...") - install_bootloader if Yast::Stage.initial # while new installation only (currently) - create_menu_entries - write_menu_timeout + Bls.install_bootloader if Yast::Stage.initial # while new installation only (currently) + write_kernel_parameter + Bls.create_menu_entries + Bls.write_menu_timeout(menu_timeout) + Pmbr.write_efi(pmbr_action) true end @@ -128,14 +138,8 @@ def propose end self.menu_timeout = Yast::ProductFeatures.GetIntegerFeature("globals", "boot_timeout").to_i self.secure_boot = Systeminfo.secure_boot_supported? - end - - def status_string(status) - if status - _("enabled") - else - _("disabled") - end + # for UEFI always remove PMBR flag on disk (bnc#872054) + self.pmbr_action = :remove end # Secure boot setting shown in summary screen. @@ -197,9 +201,7 @@ def write_sysconfig(prewrite: false) private - SDBOOTUTIL = "/usr/bin/sdbootutil" - - def create_menu_entries + def write_kernel_parameter # writing kernel parameter to /etc/kernel/cmdline File.open(File.join(Yast::Installation.destdir, CMDLINE), "w+") do |fw| if Yast::Stage.initial # while new installation only @@ -208,53 +210,6 @@ def create_menu_entries fw.puts(kernel_params.serialize) end end - - begin - Yast::Execute.on_target!(SDBOOTUTIL, "--verbose", "add-all-kernels") - rescue Cheetah::ExecutionFailed => e - Yast::Report.Error( - format(_( - "Cannot create systemd-boot menu entry:\n" \ - "Command `%{command}`.\n" \ - "Error output: %{stderr}" - ), command: e.commands.inspect, stderr: e.stderr) - ) - end - end - - def read_menu_timeout - config = CFA::SystemdBoot.load - return unless config.menu_timeout - - self.menu_timeout = if config.menu_timeout == "menu-force" - -1 - else - config.menu_timeout.to_i - end - end - - def write_menu_timeout - config = CFA::SystemdBoot.load - config.menu_timeout = if menu_timeout == -1 - "menu-force" - else - menu_timeout.to_s - end - config.save - end - - def install_bootloader - Yast::Execute.on_target!(SDBOOTUTIL, "--verbose", - "install") - rescue Cheetah::ExecutionFailed => e - Yast::Report.Error( - format(_( - "Cannot install systemd bootloader:\n" \ - "Command `%{command}`.\n" \ - "Error output: %{stderr}" - ), command: e.commands.inspect, stderr: e.stderr) - ) - nil end end end diff --git a/src/lib/bootloader/systemdboot_widgets.rb b/src/lib/bootloader/systemdboot_widgets.rb index e921ccfd0..6d891ba75 100644 --- a/src/lib/bootloader/systemdboot_widgets.rb +++ b/src/lib/bootloader/systemdboot_widgets.rb @@ -3,6 +3,7 @@ require "yast" require "bootloader/generic_widgets" require "bootloader/systeminfo" +require "bootloader/pmbr" Yast.import "UI" Yast.import "Arch" @@ -142,6 +143,8 @@ def contents ), VSpacing(1), *widgets, + VSpacing(1), + pmbr_widget, VStretch() ) end @@ -160,6 +163,12 @@ def widgets end end + def pmbr_widget + return Empty() unless pmbr_widget? + + MarginBox(1, 0, Left(PMBRWidget.new)) + end + def horizontal_margin @horizontal_margin ||= Yast::UI.TextMode ? 1 : 1.5 end @@ -167,6 +176,10 @@ def horizontal_margin def secure_boot_widget? Systeminfo.secure_boot_available?(systemdboot.name) end + + def pmbr_widget? + Pmbr.available? + end end # Represents bootloader specific options like its timeout, diff --git a/src/lib/bootloader/systeminfo.rb b/src/lib/bootloader/systeminfo.rb index 28fc8ecd7..7c779d21b 100644 --- a/src/lib/bootloader/systeminfo.rb +++ b/src/lib/bootloader/systeminfo.rb @@ -7,6 +7,7 @@ require "yast2/execute" Yast.import "Arch" +Yast.import "BootStorage" module Bootloader # Provide system and architecture dependent information @@ -45,10 +46,38 @@ def secure_boot_available?(bootloader_name) return false if efi_arch == "i386" # no shim neither secure boot support for 32 bit arm nor riscv64 (bsc#1229070) return false if Yast::Arch.arm || Yast::Arch.riscv64 + # not for grub2-bls + return false if bootloader_name == "grub2-bls" efi_used?(bootloader_name) || s390_secure_boot_available? || ppc_secure_boot_available? end + # Check if mbr configurable with a bootloader. + # + # @param bootloader_name [String] bootloader name + # @return [Boolean] true if available with this bootloader + def generic_mbr_available?(bootloader_name) + (Yast::Arch.x86_64 || Yast::Arch.i386) && !["grub2-efi", + "grub2-bls"].include?(bootloader_name) + end + + # Check if loader location is configurable with a bootloader. + # + # @param bootloader_name [String] bootloader name + # @return [Boolean] true if available with this bootloader + def loader_location_available?(bootloader_name) + (Yast::Arch.x86_64 || Yast::Arch.i386 || Yast::Arch.ppc) && bootloader_name == "grub2" + end + + # Check if setting device map is available. + # + # @param bootloader_name [String] bootloader name + # @return [Boolean] true if available with this bootloader + def device_map?(bootloader_name) + (Yast::Arch.x86_64 || Yast::Arch.i386) && !["grub2-efi", + "grub2-bls"].include?(bootloader_name) + end + # Check current trusted boot state. # # ATM this just returns the config file setting. @@ -62,6 +91,9 @@ def trusted_boot_active? # Check if the system is expected to have nvram - ie. update_nvram_active? makes a difference def nvram_available?(bootloader_name = nil) + # not for grub2-bls + return false if bootloader_name == "grub2-bls" + (bootloader_name ? efi_used?(bootloader_name) : efi_supported?) || Yast::Arch.ppc end @@ -76,6 +108,8 @@ def update_nvram_active? def trusted_boot_available?(bootloader_name) # TPM availability is must have return false unless File.exist?("/dev/tpm0") + # not for grub2-bls + return false if bootloader_name == "grub2-bls" # for details about grub2 efi trusted boot support see FATE#315831 ( @@ -89,7 +123,7 @@ def trusted_boot_available?(bootloader_name) # param bootloader_name [String] bootloader name # @return [Boolean] true if UEFI will be used for booting with this bootloader def efi_used?(bootloader_name) - ["grub2-efi", "systemd-boot"].include?(bootloader_name) + ["grub2-efi", "systemd-boot", "grub2-bls"].include?(bootloader_name) end # Check if UEFI is available on this system. @@ -108,6 +142,30 @@ def efi_mandatory? Yast::Arch.aarch64 || Yast::Arch.arm || Yast::Arch.riscv64 end + # Check if console settings are supported + # + # param bootloader_name [String] bootloader name + # @return [Boolean] true if supported + def console_supported?(bootloader_name) + !Yast::Arch.s390 && bootloader_name != "grub2-bls" + end + + # Check if hiding menu are supported + # + # param bootloader_name [String] bootloader name + # @return [Boolean] true if supported + def hiding_menu_supported?(bootloader_name) + bootloader_name != "grub2-bls" + end + + # Check if setting password is supported + # + # param bootloader_name [String] bootloader name + # @return [Boolean] true if supported + def password_supported?(bootloader_name) + bootloader_name != "grub2-bls" + end + # Check if shim-install should be used instead of grub2-install. # # param bootloader_name [String] bootloader name diff --git a/src/lib/cfa/systemd_boot.rb b/src/lib/cfa/systemd_boot.rb deleted file mode 100644 index 2105346cc..000000000 --- a/src/lib/cfa/systemd_boot.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023] 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 "cfa/base_model" -require "yast2/target_file" -require "yast2/execute" - -module CFA - # CFA based class to handle systemd-boot configuration file - # - # @example Reading a value - # file = CFA::SystemdBoot.new - # file.load - # file.menu_timeout #=> 10 - # - # @example Writing a value - # file = CFA::SystemdBoot.new - # file.menu_timeout = 5 - # file.save - # - # @example Loading shortcut - # file = CFA::SystemdBoot.load - # file.menu_timeout #=> 10 - class SystemdBoot < BaseModel - extend Yast::Logger - include Yast::Logger - - attributes( - menu_timeout: "timeout", - console_mode: "console_mode", - default: "default" - ) - - # Instantiates and loads a file when possible - # - # This method is basically a shortcut to instantiate and load the content in just one call. - # - # @param file_handler [#read,#write] something able to read/write a string (like File) - # @param file_path [String] File path - # @return [SystemdBoot] File with the already loaded content - def self.load(file_handler: Yast::TargetFile, file_path: PATH) - file = new(file_path: file_path, file_handler: file_handler) - file.tap(&:load) - rescue Errno::ENOENT - log.info("#{file_path} couldn't be loaded. Probably the file does not exist yet.") - - file - end - - # Constructor - # - # @param file_handler [#read,#write] something able to read/write a string (like File) - # @param file_path [String] File path - # - # @see CFA::BaseModel#initialize - def initialize(file_handler: Yast::TargetFile, file_path: PATH) - super(AugeasParser.new(LENS), file_path, file_handler: file_handler) - end - - def save - directory = File.dirname(@file_path) - if !Yast::FileUtils.IsDirectory(directory) - Yast::Execute.on_target("/usr/bin/mkdir", "--parents", - directory) - end - super - rescue Errno::EACCES - log.info("Permission denied when writting to #{@file_path}") - false - end - - # Default path to the systemd-boot config file - PATH = "/boot/efi/loader/loader.conf" - private_constant :PATH - - # The lens to be used by Augeas parser - # - LENS = "spacevars.lns" - private_constant :LENS - end -end diff --git a/src/modules/BootSupportCheck.rb b/src/modules/BootSupportCheck.rb index 748c97d18..5faa8c9bd 100644 --- a/src/modules/BootSupportCheck.rb +++ b/src/modules/BootSupportCheck.rb @@ -83,11 +83,11 @@ def correct_loader_type(type) # grub2 is sooo cool... return true if type == "grub2" && !::Bootloader::Systeminfo.efi_mandatory? - if (Arch.i386 || Arch.x86_64) && ["grub2-efi", "systemd-boot"].include?(type) && efi? + if (Arch.i386 || Arch.x86_64) && ["grub2-efi", "grub2-bls", "systemd-boot"].include?(type) && efi? return true end - if ["grub2-efi", "systemd-boot"].include?(type) && ::Bootloader::Systeminfo.efi_mandatory? + if ["grub2-efi", "grub2-bls", "systemd-boot"].include?(type) && ::Bootloader::Systeminfo.efi_mandatory? return true end diff --git a/test/bls_sections_test.rb b/test/bls_sections_test.rb new file mode 100755 index 000000000..8c3c4c4fc --- /dev/null +++ b/test/bls_sections_test.rb @@ -0,0 +1,65 @@ +#! /usr/bin/env rspec --format doc +# frozen_string_literal: true + +require_relative "./test_helper" + +require "bootloader/bls_sections" +require "bootloader/bls" +require "cfa/memory_file" + +describe Bootloader::BlsSections do + + before do + allow(Yast::Misc).to receive(:CustomSysconfigRead) + .with("ID_LIKE", "openSUSE", "/etc/os-release") + .and_return("openSUSE") + allow(Yast::Execute).to receive(:on_target) + .with("/usr/bin/bootctl", "--json=short", "list", stdout: :capture) + .and_return("[{\"title\" : \"openSUSE Tumbleweed\", \"isDefault\" : true }," \ + "{\"title\" : \"Snapper: *openSUSE Tumbleweed 20241107\", \"isDefault\" : false}]") + allow(Bootloader::Bls).to receive(:default_menu) + .and_return("openSUSE Tumbleweed") + + subject.read + end + + describe "#read" do + it "returns list of all available sections" do + expect(subject.all).to eq(["openSUSE Tumbleweed", "Snapper: *openSUSE Tumbleweed 20241107"]) + end + + it "reads default menu entry" do + expect(subject.default).to eq("openSUSE Tumbleweed") + end + end + + describe "#default=" do + it "sets new value for default" do + subject.default = "Snapper: *openSUSE Tumbleweed 20241107" + expect(subject.default).to eq "Snapper: *openSUSE Tumbleweed 20241107" + end + + it "sets default to empty if section do not exists" do + subject.default = "non-exist" + expect(subject.default).to eq "" + end + end + + describe "#write" do + it "writes default value if set" do + subject.default = "Snapper: *openSUSE Tumbleweed 20241107" + expect(Bootloader::Bls).to receive(:write_default_menu) + .with(subject.default) + + subject.write + end + + it "does not write default value if not set" do + subject.default = "" + expect(Bootloader::Bls).to_not receive(:write_default_menu) + + subject.write + end + + end +end diff --git a/test/bls_test.rb b/test/bls_test.rb new file mode 100644 index 000000000..9ed0a8343 --- /dev/null +++ b/test/bls_test.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +describe Bootloader::Bls do + subject = described_class + + describe "#create_menu_entries" do + it "calls sdbootutil add-all-kernels" do + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + subject.create_menu_entries + end + end + + describe "#install_bootloader" do + it "calls sdbootutil install" do + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "install") + subject.install_bootloader + end + end + + describe "#write_menu_timeout" do + it "calls sdbootutil set-timeout" do + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "set-timeout", + 10) + subject.write_menu_timeout(10) + end + end + + describe "#menu_timeout" do + it "calls sdbootutil get-timeout" do + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "get-timeout", stdout: :capture) + .and_return(10) + expect(subject.menu_timeout).to eq 10 + end + end + + describe "#write_default_menu" do + it "calls sdbootutil set-default" do + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "set-default", "openSUSE") + subject.write_default_menu("openSUSE") + end + end + + describe "#default_menu" do + it "calls sdbootutil get-default" do + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "get-default", stdout: :capture) + .and_return("openSUSE") + expect(subject.default_menu).to eq "openSUSE" + end + end + +end diff --git a/test/boot_support_test.rb b/test/boot_support_test.rb index 3949bdcc2..6b58f1f40 100644 --- a/test/boot_support_test.rb +++ b/test/boot_support_test.rb @@ -42,6 +42,13 @@ expect(subject.SystemSupported).to eq false end + it "returns false if grub2-bls is used and UEFI is not supported" do + Bootloader::BootloaderFactory.current_name = "grub2-bls" + allow(subject).to receive(:efi?).and_return(false) + + expect(subject.SystemSupported).to eq false + end + it "returns false if systemd-boot is used and UEFI is not supported" do Bootloader::BootloaderFactory.current_name = "systemd-boot" allow(subject).to receive(:efi?).and_return(false) diff --git a/test/bootloader_factory_test.rb b/test/bootloader_factory_test.rb index 7eede3bca..e36883e70 100644 --- a/test/bootloader_factory_test.rb +++ b/test/bootloader_factory_test.rb @@ -38,7 +38,7 @@ allow(Yast::ProductFeatures).to receive(:GetBooleanFeature).with("globals", "enable_systemd_boot").and_return(true) end it "returns systemd-boot in the list" do - expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2", "grub2-efi", "systemd-boot", "none"] + expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2", "grub2-efi", "grub2-bls", "systemd-boot", "none"] end end context "product does not support systemd-boot" do @@ -46,7 +46,7 @@ allow(Yast::ProductFeatures).to receive(:GetBooleanFeature).with("globals", "enable_systemd_boot").and_return(false) end it "does not include systemd-boot in the list" do - expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2", "grub2-efi", "none"] + expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2", "grub2-efi", "grub2-bls", "none"] end end end @@ -61,7 +61,7 @@ allow(Yast::ProductFeatures).to receive(:GetBooleanFeature).with("globals", "enable_systemd_boot").and_return(true) end it "does not include grub2 in the list" do - expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2-efi", "systemd-boot", "none"] + expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2-efi", "grub2-bls", "systemd-boot", "none"] end end context "product does not support systemd-boot" do @@ -69,7 +69,7 @@ allow(Yast::ProductFeatures).to receive(:GetBooleanFeature).with("globals", "enable_systemd_boot").and_return(false) end it "does not include systemd-boot and grub2 in the list" do - expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2-efi", "none"] + expect(Bootloader::BootloaderFactory.supported_names).to eq ["grub2-efi", "grub2-bls", "none"] end end end diff --git a/test/generic_widgets_test.rb b/test/generic_widgets_test.rb index b74c28a94..cd497728e 100755 --- a/test/generic_widgets_test.rb +++ b/test/generic_widgets_test.rb @@ -15,3 +15,28 @@ include_examples "CWM::ComboBox" end + +describe Bootloader::PMBRWidget do + before do + Bootloader::BootloaderFactory.clear_cache + Bootloader::BootloaderFactory.current_name = "grub2-bls" + end + + it "is initialized to pmbr action" do + Bootloader::BootloaderFactory.current.pmbr_action = :add + expect(subject).to receive(:value=).with(:add) + + subject.init + end + + it "stores pmbr action" do + expect(subject).to receive(:value).and_return(:remove) + subject.store + + expect(Bootloader::BootloaderFactory.current.pmbr_action).to eq :remove + end + + it "offer set, remove and no action options" do + expect(subject.items.size).to eq 3 + end +end diff --git a/test/grub2_bls_test.rb b/test/grub2_bls_test.rb new file mode 100644 index 000000000..d6ec4acd1 --- /dev/null +++ b/test/grub2_bls_test.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "bootloader/bls" +require "bootloader/grub2bls" + +describe Bootloader::Grub2Bls do + subject do + sub = described_class.new + sub + end + + let(:destdir) { File.expand_path("data/", __dir__) } + let(:cmdline_content) { "splash=silent quiet security=apparmor mitigations=off" } + + before do + allow(Yast::Arch).to receive(:architecture).and_return("x86_64") + allow(Bootloader::Bls).to receive(:default_menu) + .and_return(subject.sections.default) + end + + describe "#read" do + before do + allow(Bootloader::Bls).to receive(:menu_timeout) + .and_return(10) + allow(Yast::Installation).to receive(:destdir).and_return(destdir) + end + + it "reads menu timeout" do + subject.read + + expect(subject.grub_default.timeout).to eq 10 + end + + it "reads entries from /etc/kernel/cmdline" do + subject.read + + expect(subject.cpu_mitigations.to_human_string).to eq "Off" + expect(subject.grub_default.kernel_params.serialize).to eq cmdline_content + end + end + + describe "#write" do + before do + allow(Yast::Stage).to receive(:initial).and_return(false) + allow(Yast::Installation).to receive(:destdir).and_return(destdir) + subject.grub_default.kernel_params.replace(cmdline_content) + subject.grub_default.timeout = 10 + end + + it "setups protective mbr to real disks containing /boot/efi" do + subject.pmbr_action = :add + allow(Bootloader::Bls).to receive(:default_menu) + .and_return(subject.sections.default) + allow(Bootloader::Bls).to receive(:write_default_menu) + .with(subject.sections.default) + allow(Bootloader::Bls).to receive(:menu_timeout) + .and_return(subject.grub_default.timeout) + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.grub_default.timeout) + allow(Bootloader::Bls).to receive(:create_menu_entries) + allow(Bootloader::Bls).to receive(:install_bootloader) + allow(Yast::BootStorage).to receive(:gpt_boot_disk?).and_return(true) + + expect(Yast::Execute).to receive(:locally) + .with("/usr/sbin/parted", "-s", "/dev/sda", "disk_set", "pmbr_boot", "on") + + subject.write + end + + it "installs the bootloader" do + allow(Bootloader::Bls).to receive(:write_default_menu) + .with(subject.sections.default) + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.grub_default.timeout) + + # install bootloader + expect(Bootloader::Bls).to receive(:install_bootloader) + + # create menu entries + expect(Bootloader::Bls).to receive(:create_menu_entries) + + subject.write + end + + it "writes kernel cmdline" do + allow(Bootloader::Bls).to receive(:default_menu) + .and_return(subject.sections.default) + allow(Bootloader::Bls).to receive(:write_default_menu) + .with(subject.sections.default) + allow(Bootloader::Bls).to receive(:menu_timeout) + .and_return(subject.grub_default.timeout) + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.grub_default.timeout) + allow(Bootloader::Bls).to receive(:create_menu_entries) + allow(Bootloader::Bls).to receive(:install_bootloader) + + subject.write + # Checking written kernel parameters + subject.read + expect(subject.cpu_mitigations.to_human_string).to eq "Off" + expect(subject.grub_default.kernel_params.serialize).to include cmdline_content + end + + it "saves menu timeout" do + allow(Bootloader::Bls).to receive(:create_menu_entries) + allow(Bootloader::Bls).to receive(:install_bootloader) + allow(Bootloader::Bls).to receive(:write_default_menu) + .with(subject.sections.default) + # Saving menu timeout + expect(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.grub_default.timeout) + subject.write + end + end + + describe "#packages" do + it "adds grub2--efi-bls and sdbootutil packages" do + allow(Yast::Arch).to receive(:architecture).and_return("x86_64") + allow(Yast::Package).to receive(:Available).with("os-prober").and_return(true) + expect(subject.packages).to include("grub2-" + Yast::Arch.architecture + "-efi-bls") + expect(subject.packages).to include("sdbootutil") + end + end + + describe "#summary" do + it "returns line with boot loader type specified" do + expect(subject.summary).to include("Boot Loader Type: GRUB2 BLS") + end + + end + + describe "#merge" do + it "overwrite mitigations and menu timeout if specified in merged one" do + other_cmdline = "splash=silent quiet mitigations=auto" + other = described_class.new + other.grub_default.timeout = 12 + other.grub_default.kernel_params.replace(other_cmdline) + + subject.grub_default.timeout = 10 + subject.grub_default.kernel_params.replace(cmdline_content) + + subject.merge(other) + + expect(subject.grub_default.timeout).to eq 12 + expect(subject.cpu_mitigations.to_human_string).to eq "Auto" + expect(subject.grub_default.kernel_params.serialize).to include "security=apparmor splash=silent quiet mitigations=auto" + end + end + + describe "#propose" do + before do + allow(Yast::BootStorage).to receive(:available_swap_partitions).and_return({}) + end + + it "proposes timeout to product/role default" do + allow(Yast::ProductFeatures).to receive(:GetIntegerFeature) + .with("globals", "boot_timeout").and_return(2) + subject.propose + + expect(subject.grub_default.timeout).to eq 2 + end + + it "proposes kernel cmdline" do + expect(Yast::BootArch).to receive(:DefaultKernelParams).and_return(cmdline_content) + + subject.propose + expect(subject.grub_default.kernel_params.serialize).to eq cmdline_content + end + end +end diff --git a/test/grub2_efi_test.rb b/test/grub2_efi_test.rb index fedfae7bb..87d1ee127 100644 --- a/test/grub2_efi_test.rb +++ b/test/grub2_efi_test.rb @@ -38,8 +38,8 @@ it "setups protective mbr to real disks containing /boot/efi" do subject.pmbr_action = :add allow(Yast::BootStorage).to receive(:gpt_boot_disk?).and_return(true) - - expect(subject).to receive(:pmbr_setup).with("/dev/sda") + expect(Yast::Execute).to receive(:locally) + .with("/usr/sbin/parted", "-s", "/dev/sda", "disk_set", "pmbr_boot", "on") subject.write end diff --git a/test/grub2_test.rb b/test/grub2_test.rb index 903f2cae1..8069dce7c 100644 --- a/test/grub2_test.rb +++ b/test/grub2_test.rb @@ -70,7 +70,9 @@ allow(Yast::BootStorage).to receive(:gpt_boot_disk?).and_return(true) devicegraph_stub("msdos_and_gpt.yaml") - expect(subject).to receive(:pmbr_setup).with("/dev/sdb") + expect(Yast::Execute).to receive(:locally) + .with("/usr/sbin/parted", "-s", "/dev/sdb", "disk_set", "pmbr_boot", "on") + subject.pmbr_action = :add subject.write end diff --git a/test/grub2_widgets_test.rb b/test/grub2_widgets_test.rb index 7f40371ac..e639d6e65 100644 --- a/test/grub2_widgets_test.rb +++ b/test/grub2_widgets_test.rb @@ -238,32 +238,6 @@ def stub_widget_value(id, value) end end -describe Bootloader::Grub2Widget::PMBRWidget do - before do - assign_bootloader - end - - it_behaves_like "labeled widget" - - it "is initialized to pmbr action" do - bootloader.pmbr_action = :add - expect(subject).to receive(:value=).with(:add) - - subject.init - end - - it "stores pmbr action" do - expect(subject).to receive(:value).and_return(:remove) - subject.store - - expect(bootloader.pmbr_action).to eq :remove - end - - it "offer set, remove and no action options" do - expect(subject.items.size).to eq 3 - end -end - describe Bootloader::Grub2Widget::SecureBootWidget do before do assign_bootloader("grub2-efi") diff --git a/test/os_prober_test.rb b/test/os_prober_test.rb index f9f57b949..5da7a9026 100644 --- a/test/os_prober_test.rb +++ b/test/os_prober_test.rb @@ -45,7 +45,7 @@ end it "os-prober is available" do - expect(subject.available?).to eq true + expect(subject.available?("grub2")).to eq true end end @@ -55,9 +55,20 @@ end it "os-prober is not available" do - expect(subject.available?).to eq false + expect(subject.available?("grub2")).to eq false end end + + context "if grub2-bls bootloader" do + before do + allow(Yast::Package).to receive(:Available).and_return(true) + end + + it "os-prober is not available for that bootloader" do + expect(subject.available?("grub2-bls")).to eq false + end + end + end end end diff --git a/test/systemdboot_test.rb b/test/systemdboot_test.rb index fc755c056..001422d98 100644 --- a/test/systemdboot_test.rb +++ b/test/systemdboot_test.rb @@ -2,6 +2,7 @@ require_relative "test_helper" +require "bootloader/bls" require "bootloader/systemdboot" describe Bootloader::SystemdBoot do @@ -17,6 +18,10 @@ allow(Yast::BootStorage).to receive(:available_swap_partitions).and_return([]) allow(Yast::Arch).to receive(:architecture).and_return("x86_64") allow(Yast::Package).to receive(:Available).and_return(true) + allow(Bootloader::Bls).to receive(:menu_timeout) + .and_return(subject.menu_timeout) + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.menu_timeout) end describe "#read" do @@ -49,23 +54,33 @@ end it "installs the bootloader" do - allow(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") - allow_any_instance_of(CFA::SystemdBoot).to receive(:save) - + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.menu_timeout) + allow(Bootloader::Bls).to receive(:create_menu_entries) # install bootloader - expect(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "install") + expect(Bootloader::Bls).to receive(:install_bootloader) + + subject.write + end + + it "setups protective mbr to real disks containing /boot/efi" do + subject.pmbr_action = :add + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.menu_timeout) + allow(Bootloader::Bls).to receive(:create_menu_entries) + allow(Bootloader::Bls).to receive(:install_bootloader) + allow(Yast::BootStorage).to receive(:gpt_boot_disk?).and_return(true) + expect(Yast::Execute).to receive(:locally) + .with("/usr/sbin/parted", "-s", "/dev/sda", "disk_set", "pmbr_boot", "on") subject.write end it "writes kernel cmdline" do - allow(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "install") - allow(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") - allow_any_instance_of(CFA::SystemdBoot).to receive(:save) + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.menu_timeout) + allow(Bootloader::Bls).to receive(:create_menu_entries) + allow(Bootloader::Bls).to receive(:install_bootloader) subject.write # Checking written kernel parameters @@ -75,25 +90,23 @@ end it "creates menu entries" do - allow(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "install") - allow_any_instance_of(CFA::SystemdBoot).to receive(:save) + allow(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.menu_timeout) + allow(Bootloader::Bls).to receive(:install_bootloader) # create menu entries - expect(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + expect(Bootloader::Bls).to receive(:create_menu_entries) subject.write end it "saves menu timeout" do - allow(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "install") - allow(Yast::Execute).to receive(:on_target!) - .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + allow(Bootloader::Bls).to receive(:create_menu_entries) + allow(Bootloader::Bls).to receive(:install_bootloader) # Saving menu timeout - expect_any_instance_of(CFA::SystemdBoot).to receive(:save) + expect(Bootloader::Bls).to receive(:write_menu_timeout) + .with(subject.menu_timeout) subject.write end