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 @@
-
-
+
\ 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