From 6d33cc44e5ef7615374b168671496fa9359795da Mon Sep 17 00:00:00 2001 From: Stefan Schubert Date: Wed, 30 Oct 2024 15:55:57 +0100 Subject: [PATCH] grub2-bls grub2-bls grub2-bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls grub2bls rubocop rubocop grub2bls grub2bls grub2bls sync text about secure boot on s390 to be correct (bsc#1219989) make rubocop happy rubocop rubocop fixed test cases fixed test cases grub2bls grub2bls grub2bls grub2bls grub2bls rubocop rubocop rubocop rubocop rubocop docu added yast-bootloader added yast-bootloader --- .rubocop.yml | 2 +- README.md | 1 + doc/bootloader_backend.svg | 1490 +++++++--------------- src/lib/bootloader/autoyast_converter.rb | 24 +- src/lib/bootloader/bls_sections.rb | 95 ++ src/lib/bootloader/bootloader_base.rb | 5 +- src/lib/bootloader/bootloader_factory.rb | 3 +- src/lib/bootloader/generic_widgets.rb | 17 - src/lib/bootloader/grub2_widgets.rb | 60 +- src/lib/bootloader/grub2base.rb | 1 - src/lib/bootloader/grub2bls.rb | 175 ++- src/lib/bootloader/proposal_client.rb | 11 +- src/lib/bootloader/sysconfig.rb | 2 +- src/lib/bootloader/systemdboot.rb | 6 +- test/bls_sections_test.rb | 76 ++ test/boot_support_test.rb | 7 + test/bootloader_factory_test.rb | 8 +- test/grub2_bls_test.rb | 166 +++ 18 files changed, 997 insertions(+), 1152 deletions(-) create mode 100644 src/lib/bootloader/bls_sections.rb create mode 100755 test/bls_sections_test.rb create mode 100644 test/grub2_bls_test.rb diff --git a/.rubocop.yml b/.rubocop.yml index d01c10756..d3c2ebb20 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -80,7 +80,7 @@ Metrics/BlockLength: # Configuration parameters: CountComments. Metrics/MethodLength: - Max: 30 # TODO this should be lower for new code + Max: 31 # TODO this should be lower for new code Include: - 'src/lib/**/*.rb' # be more strict for new code in lib 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..619e319c0 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/src/lib/bootloader/autoyast_converter.rb b/src/lib/bootloader/autoyast_converter.rb index fa2291ddc..3dcae73ce 100644 --- a/src/lib/bootloader/autoyast_converter.rb +++ b/src/lib/bootloader/autoyast_converter.rb @@ -35,15 +35,17 @@ def import(data) case bootloader.name when "grub2", "grub2-efi", "grub2-bls" - import_grub2(data, bootloader) - import_grub2efi(data, bootloader) - import_stage1(data, bootloader) + 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) @@ -75,9 +77,9 @@ def export(config) when "grub2", "grub2-efi", "grub2-bls" global = res["global"] export_grub2(global, config) if config.name == "grub2" - export_grub2efi(global, config) if ["grub2-efi", "grub2-bls"].include?(config.name) + 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 @@ -106,7 +108,7 @@ def import_grub2(data, bootloader) end def import_grub2efi(data, bootloader) - return unless ["grub2-efi", "grub2-bls"].include?(bootloader.name) + return unless bootloader.name == "grub2-efi" GRUB2EFI_BOOLEAN_MAPPING.each do |key, method| val = data.global.public_send(key) diff --git a/src/lib/bootloader/bls_sections.rb b/src/lib/bootloader/bls_sections.rb new file mode 100644 index 000000000..b934bb8a9 --- /dev/null +++ b/src/lib/bootloader/bls_sections.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "json" +require "yast" +require "yast2/execute" + +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? + + write_default + end + + def read + @data = read_entries + @all = @data.map { |e| e["title"] } + @default = read_default + end + + private + + OS_RELEASE_PATH = "/etc/os-release" + + def grubenv_path + str = Yast::Misc.CustomSysconfigRead("ID_LIKE", "openSUSE", + OS_RELEASE_PATH) + os = str.split.first + File.join("/boot/efi/EFI/", os, "/grubenv") + end + + # @return [String] return default boot as string or "" if not set + # or something goes wrong + def read_default + Yast::Misc.CustomSysconfigRead("default", "", + grubenv_path) + end + + # write default entry + def write_default + ret = Yast::Execute.on_target("/usr/bin/sdbootutil", + "set-default", @default, + allowed_exitstatus: [0, 1]) + + return unless ret != 0 + + # fallback directly over grub2-editenv + Yast::Execute.on_target("/usr/bin/grub2-editenv", grubenv_path, + "set", "default=" + @default) + end + + # @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 d91210ce2..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 @@ -121,6 +125,5 @@ def status_string(status) _("disabled") end end - end end diff --git a/src/lib/bootloader/bootloader_factory.rb b/src/lib/bootloader/bootloader_factory.rb index 850e1e031..9e39df361 100644 --- a/src/lib/bootloader/bootloader_factory.rb +++ b/src/lib/bootloader/bootloader_factory.rb @@ -115,8 +115,7 @@ def use_systemd_boot? end def use_grub2_bls? -# Yast::ProductFeatures.GetBooleanFeature("globals", "enable_grub2_bls") && - (Yast::Arch.x86_64 || Yast::Arch.aarch64) # only these architectures are supported. + (Yast::Arch.x86_64 || Yast::Arch.aarch64) # only these architectures are supported. end def grub2_efi_installable? diff --git a/src/lib/bootloader/generic_widgets.rb b/src/lib/bootloader/generic_widgets.rb index 2a9b95b6b..5709dba8a 100644 --- a/src/lib/bootloader/generic_widgets.rb +++ b/src/lib/bootloader/generic_widgets.rb @@ -53,8 +53,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 @@ -74,20 +72,6 @@ def handle return :redraw if !Yast::Popup.ContinueCancel(popup_msg) end - if ["systemd-boot", "grub2-bls"].include?(new_bl) - # popup - Continue/Cancel - popup_msg = _( - "\n" \ - "%s 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" - ) % new_bl - - return :redraw if !Yast::Popup.ContinueCancel(popup_msg) - end - if !Yast::Stage.initial && (old_bl == "systemd-boot") Yast::Popup.Warning(_( "Switching from systemd-boot to another bootloader\n" \ @@ -102,7 +86,6 @@ def handle :redraw end - # rubocop:enable Metrics/MethodLength def help _( "

Boot Loader\n" \ diff --git a/src/lib/bootloader/grub2_widgets.rb b/src/lib/bootloader/grub2_widgets.rb index 95cce7412..d7926e673 100644 --- a/src/lib/bootloader/grub2_widgets.rb +++ b/src/lib/bootloader/grub2_widgets.rb @@ -78,16 +78,14 @@ def init end def store - if !@hidden_menu_widget.is_a?(CWM::Empty) - if @hidden_menu_widget.checked? - grub_default.hidden_timeout = value.to_s - grub_default.timeout = "0" - else - grub_default.hidden_timeout = "0" - grub_default.timeout = value.to_s - end + 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 - grub_default.timeout = value.to_s + grub_default.hidden_timeout = "0" + grub_default.timeout = value.to_s end end end @@ -355,7 +353,8 @@ def store end def validate - return true if Yast::Mode.config || !value || ["grub2-efi", "grub2-bls"].include?(grub2.name) + 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? @@ -973,6 +972,8 @@ def handle # represents Tab with kernel related configuration class KernelTab < CWM::Tab + include Grub2Helper + def label textdomain "bootloader" @@ -980,10 +981,10 @@ def label end def contents - if (Yast::Arch.s390 || grub2.name = "grub2-bls") - console_widget = CWM::Empty.new("console") + console_widget = if Yast::Arch.s390 || grub2.name == "grub2-bls" + CWM::Empty.new("console") else - console_widget = ConsoleWidget.new + ConsoleWidget.new end VBox( VSpacing(1), @@ -1062,29 +1063,29 @@ def loader_location_widget? end def generic_mbr_widget? - (Yast::Arch.x86_64 || Yast::Arch.i386) && !(["grub2-efi", "grub2-bls"].include?(grub2.name)) + (Yast::Arch.x86_64 || Yast::Arch.i386) && !["grub2-efi", "grub2-bls"].include?(grub2.name) end def secure_boot_widget? - Systeminfo.secure_boot_available?(grub2.name) && !grub2.name == "grub2-bls" + Systeminfo.secure_boot_available?(grub2.name) && grub2.name != "grub2-bls" end def trusted_boot_widget? - Systeminfo.trusted_boot_available?(grub2.name) && !grub2.name == "grub2-bls" + Systeminfo.trusted_boot_available?(grub2.name) && grub2.name != "grub2-bls" end - def update_nvram_widget? && !grub2.name == "grub2-bls" - Systeminfo.nvram_available?(grub2.name) + def update_nvram_widget? + Systeminfo.nvram_available?(grub2.name) && grub2.name != "grub2-bls" end def pmbr_widget? (Yast::Arch.x86_64 || Yast::Arch.i386) && Yast::BootStorage.gpt_boot_disk? && - !grub2.name == "grub2-bls" + grub2.name != "grub2-bls" end def device_map_button? - (Yast::Arch.x86_64 || Yast::Arch.i386) && !(["grub2-efi", "grub2-bls"].include?(grub2.name)) + (Yast::Arch.x86_64 || Yast::Arch.i386) && !["grub2-efi", "grub2-bls"].include?(grub2.name) end end @@ -1092,7 +1093,7 @@ def device_map_button? # default section or password protection class BootloaderTab < CWM::Tab include Grub2Helper - + def label textdomain "bootloader" @@ -1100,10 +1101,10 @@ def label end def contents - if grub2.name != "grub2-bls" - hidden_menu_widget = HiddenMenuWidget.new + hidden_menu_widget = if grub2.name == "grub2-bls" + CWM::Empty.new("hidden_menu") else - hidden_menu_widget = CWM::Empty.new("hidden_menu") + HiddenMenuWidget.new end VBox( VSpacing(2), @@ -1119,24 +1120,25 @@ def contents HSpacing(1) ), VSpacing(1), - MarginBox(1, 1, DefaultSectionWidget.new), + MarginBox(1, 1, MinWidth(1, DefaultSectionWidget.new)), MarginBox(1, 1, grub_password_widget), VStretch() ) end private + def grub_password_widget - if grub2.name != "grub2-bls" - GrubPasswordWidget.new - else + if grub2.name == "grub2-bls" CWM::Empty.new("password_widget") + else + GrubPasswordWidget.new end end def os_prober_widget if OsProber.available? && # Checks !Arch.s390 and if package is available - grub2.name != "grub2-bls" + grub2.name != "grub2-bls" Left(OSProberWidget.new) else CWM::Empty.new("os_prober") diff --git a/src/lib/bootloader/grub2base.rb b/src/lib/bootloader/grub2base.rb index e9dabe809..83caa8419 100644 --- a/src/lib/bootloader/grub2base.rb +++ b/src/lib/bootloader/grub2base.rb @@ -435,7 +435,6 @@ def update_nvram_summary "#{_("Update NVRAM:")} #{status_string(update_nvram)} #{link}" end - end # rubocop:enable Metrics/ClassLength end diff --git a/src/lib/bootloader/grub2bls.rb b/src/lib/bootloader/grub2bls.rb index 1c1249320..37dcb4bae 100644 --- a/src/lib/bootloader/grub2bls.rb +++ b/src/lib/bootloader/grub2bls.rb @@ -1,92 +1,193 @@ # frozen_string_literal: true require "yast" -require "bootloader/grub2efi" +require "bootloader/bootloader_base" +require "bootloader/bls_sections" Yast.import "Arch" Yast.import "Report" Yast.import "Stage" -Yast.import "BootStorage" +Yast.import "Misc" module Bootloader # Represents grub2 bls bootloader with efi target - class Grub2Bls < Grub2EFI + 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(*) - result = [ + [ Yast::Builtins.sformat( _("Boot Loader Type: %1"), "GRUB2 BLS" ) ] - result << secure_boot_summary if Systeminfo.secure_boot_available?(name) - result << trusted_boot_summary if Systeminfo.trusted_boot_available?(name) - result << update_nvram_summary if Systeminfo.nvram_available?(name) - - result end + # @return bootloader name def name "grub2-bls" end - def write(etc_only: false) - super + # reads configuration from target disk + def read + read_menu_timeout + @sections.read + 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 + end + + # @return true if configuration is already proposed + def proposed? + @is_proposed + end + + # writes configuration to target disk + def write(*) install_bootloader if Yast::Stage.initial # while new installation only (currently) create_menu_entries install_bootloader + @sections.write + write_menu_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 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 " grub_default.kernel_params: #{grub_default.kernel_params.serialize}=>" \ + "#{other.grub_default.kernel_params.serialize}" + + merge_sections(other) + merge_grub_default(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}" + end + # rubocop:enable Metrics/AbcSize + + # @return [Array] packages required to configure given bootloader def packages res = super - res << "grub2-" + Yast::Arch.architecture + "-efi-bls" - res << "sdbootutil" + res << ("grub2-" + Yast::Arch.architecture + "-efi-bls") + res << "sdbootutil" + res << "grub2" res end private SDBOOTUTIL = "/usr/bin/sdbootutil" - CMDLINE = "/etc/kernel/cmdline" + OS_RELEASE_PATH = "/etc/os-release" - def create_menu_entries + def grubenv_path + str = Yast::Misc.CustomSysconfigRead("ID_LIKE", "openSUSE", + OS_RELEASE_PATH) + os = str.split.first + File.join("/boot/efi/EFI/", os, "/grubenv") + 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 + # @return [String] return default boot as string or "" if not set + # or something goes wrong + def read_menu_timeout + grub_default.timeout = Yast::Misc.CustomSysconfigRead("timeout", "", + grubenv_path) + log.info "Boot timeout: #{grub_default.timeout}" end - + + def write_menu_timeout + ret = Yast::Execute.on_target(SDBOOTUTIL, + "set-timeout", + grub_default.timeout, + allowed_exitstatus: [0, 1]) + + return unless ret != 0 + + # fallback directly over grub2-editenv + Yast::Execute.on_target("/usr/bin/grub2-editenv", grubenv_path, + "set", "timeout=#{grub_default.timeout}") + end + + def merge_sections(other) + return if !other.sections.default || other.sections.default.empty? + + @sections.default = other.sections.default + end + + def create_menu_entries + Yast::Execute.on_target!(SDBOOTUTIL, "--verbose", "add-all-kernels") + rescue Cheetah::ExecutionFailed => e + Yast::Report.Error( + format(_( + "Cannot create grub2-bls menu entry:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + 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 - + format(_( + "Cannot install grub2-bls bootloader:\n" \ + "Command `%{command}`.\n" \ + "Error output: %{stderr}" + ), command: e.commands.inspect, stderr: e.stderr) + ) + end end end diff --git a/src/lib/bootloader/proposal_client.rb b/src/lib/bootloader/proposal_client.rb index 8b44f1320..dec04a852 100644 --- a/src/lib/bootloader/proposal_client.rb +++ b/src/lib/bootloader/proposal_client.rb @@ -381,10 +381,15 @@ def single_click_action(option, value) bootloader.secure_boot = value if value && Yast::Arch.s390 Yast2::Popup.show( + # text is identical like one in grub2_widgets. Keep in sync! + # TRANSLATORS: IPL stands for Initial Program Load, IBM speak for system boot _( - "The new secure-boot enabled boot data format works only on z15 " \ - "and later and only for zFCP disks.\n\n" \ - "The system does not boot if these requirements are not met." + "Secure boot IPL has the following minimum system requirements,\n" \ + "depending on the boot device to be IPLed:\n" \ + "NVMe disk: IBM LinuxONE III or newer.\n" \ + "FC-attached SCSI disk: IBM LinuxONE III, IBM z15 or newer.\n" \ + "ECKD DASD with CDL layout: IBM z16, LinuxONE 4 or newer.\n" \ + "If these requirements are not met, the system can be IPLed in non-secure mode only." ), headline: :warning, buttons: :ok ) 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 d0292b62d..9235c671e 100644 --- a/src/lib/bootloader/systemdboot.rb +++ b/src/lib/bootloader/systemdboot.rb @@ -191,7 +191,7 @@ def write_sysconfig(prewrite: false) 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 @@ -200,6 +200,10 @@ def create_menu_entries fw.puts(kernel_params.serialize) end end + end + + def create_menu_entries + write_kernel_parameter begin Yast::Execute.on_target!(SDBOOTUTIL, "--verbose", "add-all-kernels") diff --git a/test/bls_sections_test.rb b/test/bls_sections_test.rb new file mode 100755 index 000000000..08a7530b6 --- /dev/null +++ b/test/bls_sections_test.rb @@ -0,0 +1,76 @@ +#! /usr/bin/env rspec --format doc +# frozen_string_literal: true + +require_relative "./test_helper" + +require "bootloader/bls_sections" +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") + end + + describe "#read" do + before do + 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(Yast::Misc).to receive(:CustomSysconfigRead) + .with("default", "", "/boot/efi/EFI/openSUSE/grubenv") + .and_return("openSUSE Tumbleweed") + subject.read + end + + 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 + before do + 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(Yast::Misc).to receive(:CustomSysconfigRead) + .with("default", "", "/boot/efi/EFI/openSUSE/grubenv") + .and_return("openSUSE Tumbleweed") + subject.read + end + 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(Yast::Execute).to receive(:on_target) + .with("/usr/bin/sdbootutil", "set-default", subject.default, { :allowed_exitstatus=>[0, 1] }) + subject.write + end + + it "does not write default value if not set" do + subject.default = "" + expect(Yast::Execute).to_not receive(:on_target) + .with("/usr/bin/sdbootutil", "set-default", subject.default, { :allowed_exitstatus=>[0, 1] }) + subject.write + 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/grub2_bls_test.rb b/test/grub2_bls_test.rb new file mode 100644 index 000000000..a8bf6f4fe --- /dev/null +++ b/test/grub2_bls_test.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +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") + end + + describe "#read" do + before do + allow(Yast::Misc).to receive(:CustomSysconfigRead) + .with("ID_LIKE", "openSUSE", "/etc/os-release") + .and_return("openSUSE") + allow(Yast::Misc).to receive(:CustomSysconfigRead) + .with("timeout", "", "/boot/efi/EFI/openSUSE/grubenv") + .and_return("10") + allow(Yast::Misc).to receive(:CustomSysconfigRead) + .with("default", "", "/boot/efi/EFI/openSUSE/grubenv") + .and_return("") + 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 "installs the bootloader" do + allow(Yast::Execute).to receive(:on_target) + .with("/usr/bin/sdbootutil", "set-timeout", + subject.grub_default.timeout, + allowed_exitstatus: [0, 1]) + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "set-default", subject.sections.default) + + # install bootloader + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "install") + + # create menu entries + expect(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "--verbose", "add-all-kernels") + + subject.write + end + + it "writes kernel cmdline" do + allow(Yast::Execute).to receive(:on_target) + .with("/usr/bin/sdbootutil", "set-timeout", + subject.grub_default.timeout, + allowed_exitstatus: [0, 1]) + allow(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "set-default", subject.sections.default) + 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") + + 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(Yast::Execute).to receive(:on_target!) + .with("/usr/bin/sdbootutil", "set-default", subject.sections.default) + 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") + + # Saving menu timeout + expect(Yast::Execute).to receive(:on_target) + .with("/usr/bin/sdbootutil", "set-timeout", + subject.grub_default.timeout, + allowed_exitstatus: [0, 1]) + subject.write + end + end + + describe "#packages" do + it "adds grub2* 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") + expect(subject.packages).to include("grub2") + 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