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 index 877502a3c..b934bb8a9 100644 --- a/src/lib/bootloader/bls_sections.rb +++ b/src/lib/bootloader/bls_sections.rb @@ -74,10 +74,12 @@ def write_default ret = Yast::Execute.on_target("/usr/bin/sdbootutil", "set-default", @default, allowed_exitstatus: [0, 1]) - if ret != 0 # fallback directly over grub2-editenv - Yast::Execute.on_target("/usr/bin/grub2-editenv", grubenv_path, - "set", "default=" + @default) - end + + 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 [] diff --git a/src/lib/bootloader/bootloader_base.rb b/src/lib/bootloader/bootloader_base.rb index 3f03ea4d2..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 diff --git a/src/lib/bootloader/generic_widgets.rb b/src/lib/bootloader/generic_widgets.rb index a6770bc2f..5709dba8a 100644 --- a/src/lib/bootloader/generic_widgets.rb +++ b/src/lib/bootloader/generic_widgets.rb @@ -53,7 +53,6 @@ def localized_names(name) names[name] or raise "Unknown supported bootloader '#{name}'" end - # It will be reduced again if systemd-boot is not anymore in beta phase. def handle old_bl = BootloaderFactory.current.name new_bl = value diff --git a/src/lib/bootloader/grub2bls.rb b/src/lib/bootloader/grub2bls.rb index 7e653c46f..37dcb4bae 100644 --- a/src/lib/bootloader/grub2bls.rb +++ b/src/lib/bootloader/grub2bls.rb @@ -20,9 +20,9 @@ class Grub2Bls < Grub2Base CMDLINE = "/etc/kernel/cmdline" def initialize + super textdomain "bootloader" - @grub_default = ::CFA::Grub2::Default.new @sections = ::Bootloader::BlsSections.new @is_read = false @is_proposed = false @@ -84,9 +84,7 @@ def proposed? end # writes configuration to target disk - # @param etc_only [Boolean] true on transactional systems - # because /boot is read-only there - def write(etc_only: false) + def write(*) install_bootloader if Yast::Stage.initial # while new installation only (currently) create_menu_entries install_bootloader @@ -100,6 +98,7 @@ def write(etc_only: false) # 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 @@ -116,6 +115,7 @@ def merge(other) 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 @@ -147,14 +147,16 @@ def read_menu_timeout end def write_menu_timeout - ret = Yast::Execute.on_target("/usr/bin/sdbootutil", + ret = Yast::Execute.on_target(SDBOOTUTIL, "set-timeout", grub_default.timeout, allowed_exitstatus: [0, 1]) - if ret != 0 # fallback directly over grub2-editenv - Yast::Execute.on_target("/usr/bin/grub2-editenv", grubenv_path, - "set", "timeout=#{grub_default.timeout}") - end + + 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) @@ -168,7 +170,7 @@ def create_menu_entries rescue Cheetah::ExecutionFailed => e Yast::Report.Error( format(_( - "Cannot create systemd-boot menu entry:\n" \ + "Cannot create grub2-bls menu entry:\n" \ "Command `%{command}`.\n" \ "Error output: %{stderr}" ), command: e.commands.inspect, stderr: e.stderr) @@ -181,7 +183,7 @@ def install_bootloader rescue Cheetah::ExecutionFailed => e Yast::Report.Error( format(_( - "Cannot install systemd bootloader:\n" \ + "Cannot install grub2-bls bootloader:\n" \ "Command `%{command}`.\n" \ "Error output: %{stderr}" ), command: e.commands.inspect, stderr: e.stderr) 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/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