Skip to content

Commit eaebb85

Browse files
committed
bootloader: Add bootc loader-entries set-options-for-source support
On bootc systems with transient /etc, the state file /etc/tuned/bootcmdline is lost after each reboot. This causes TuneD to lose track of which kernel arguments it previously set, leading to kargs stacking up on every reboot cycle. bootc loader-entries set-options-for-source solves this by recording kargs ownership directly in the BLS config on /boot, which persists across reboots regardless of /etc transience. With --source tuned, bootc automatically removes all previous kargs from the "tuned" source and replaces them with the new set, eliminating the need for TuneD to maintain its own state for diffing. This commit adds bootc source tracking to the bootloader plugin: 1. Detect set-options-for-source availability by checking "bootc loader-entries set-options-for-source --help" exit code during plugin init. 2. When available, use "bootc loader-entries set-options-for-source --source tuned --options ..." for applying kargs and the same command without --options for clearing all TuneD-owned kargs on profile removal. 3. The bootc path is a top-level dispatch branch, checked before rpm-ostree. Fallback chain: bootc -> rpm-ostree legacy --delete/--append -> GRUB2. All existing code paths are unchanged. 4. Continue writing to /etc/tuned/bootcmdline as best-effort for diagnostics and backward compatibility, but do not depend on it for correctness when bootc source tracking is available. 5. Add 15 unit tests for the bootloader plugin covering: bootc detection (3), apply (3), removal (2), init flag derivation (2), and top-level dispatch (5). Requires: bootc with loader-entries set-options-for-source (bootc PR #2114) and ostree >= 2026.1 See: bootc-dev/bootc#899 See: bootc-dev/bootc#2114 Assisted-by: OpenCode (Claude Opus 4.6)
1 parent 5624a61 commit eaebb85

2 files changed

Lines changed: 327 additions & 2 deletions

File tree

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import unittest
2+
3+
try:
4+
from unittest.mock import Mock, patch, call
5+
except ImportError:
6+
from mock import Mock, patch, call
7+
8+
import tuned.consts as consts
9+
10+
11+
class BootloaderPluginBootcTestCase(unittest.TestCase):
12+
"""Tests for the bootc loader-entries set-options-for-source integration
13+
in the bootloader plugin.
14+
15+
These tests exercise the bootc detection, source-based kargs apply,
16+
source-based kargs removal, and the dispatch logic that falls back to
17+
the rpm-ostree or GRUB2 code paths when bootc is not available.
18+
19+
The BootloaderPlugin constructor requires /etc/grub.d/00_tuned to exist,
20+
so we test the methods directly on a minimal mock rather than
21+
instantiating the full plugin.
22+
"""
23+
24+
def _make_plugin_mock(self, bootc_has_source=True):
25+
"""Create a mock that has the real methods under test patched onto it.
26+
27+
This avoids instantiating BootloaderPlugin (which requires GRUB2
28+
template files on disk) while still testing the actual method logic.
29+
30+
Args:
31+
bootc_has_source: Whether bootc set-options-for-source is available
32+
"""
33+
from tuned.plugins.plugin_bootloader import BootloaderPlugin
34+
35+
mock_plugin = Mock(spec=BootloaderPlugin)
36+
mock_plugin._bootc_has_source = bootc_has_source
37+
mock_plugin._cmdline_val = ""
38+
mock_plugin._cmd = Mock()
39+
mock_plugin._cmd.add_modify_option_in_file = Mock(return_value=True)
40+
41+
# Bind real methods to the mock so we test actual logic
42+
mock_plugin._bootc_has_set_options_for_source = \
43+
lambda: BootloaderPlugin._bootc_has_set_options_for_source(mock_plugin)
44+
mock_plugin._bootc_source_update = \
45+
lambda: BootloaderPlugin._bootc_source_update(mock_plugin)
46+
mock_plugin._remove_bootc_source_tuning = \
47+
lambda: BootloaderPlugin._remove_bootc_source_tuning(mock_plugin)
48+
mock_plugin._patch_bootcmdline = \
49+
lambda d: BootloaderPlugin._patch_bootcmdline(mock_plugin, d)
50+
51+
return mock_plugin
52+
53+
# ------------------------------------------------------------------
54+
# _bootc_has_set_options_for_source() detection
55+
# ------------------------------------------------------------------
56+
57+
def test_bootc_has_source_present(self):
58+
"""bootc set-options-for-source is detected when --help succeeds."""
59+
plugin = self._make_plugin_mock()
60+
plugin._cmd.execute = Mock(return_value=(0, "Usage: bootc loader-entries...", ""))
61+
self.assertTrue(plugin._bootc_has_set_options_for_source())
62+
plugin._cmd.execute.assert_called_once_with(
63+
['bootc', 'loader-entries', 'set-options-for-source', '--help'],
64+
return_err=True)
65+
66+
def test_bootc_has_source_absent(self):
67+
"""bootc set-options-for-source is not detected when --help fails."""
68+
plugin = self._make_plugin_mock()
69+
plugin._cmd.execute = Mock(return_value=(1, "", "error: unrecognized subcommand"))
70+
self.assertFalse(plugin._bootc_has_set_options_for_source())
71+
72+
def test_bootc_has_source_not_installed(self):
73+
"""bootc set-options-for-source is not detected when bootc is not installed."""
74+
plugin = self._make_plugin_mock()
75+
plugin._cmd.execute = Mock(return_value=(127, "", "command not found"))
76+
self.assertFalse(plugin._bootc_has_set_options_for_source())
77+
78+
# ------------------------------------------------------------------
79+
# _bootc_source_update()
80+
# ------------------------------------------------------------------
81+
82+
def test_bootc_source_update_basic(self):
83+
"""bootc set-options-for-source is called with correct --options."""
84+
plugin = self._make_plugin_mock()
85+
plugin._cmdline_val = "nohz=full isolcpus=1-3"
86+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
87+
88+
plugin._bootc_source_update()
89+
90+
plugin._cmd.execute.assert_called_once_with(
91+
["bootc", "loader-entries", "set-options-for-source",
92+
"--source", "tuned", "--options", "nohz=full isolcpus=1-3"],
93+
return_err=True)
94+
# Verify bootcmdline state file is updated
95+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
96+
consts.BOOT_CMDLINE_FILE,
97+
{consts.BOOT_CMDLINE_TUNED_VAR: "nohz=full isolcpus=1-3"})
98+
99+
def test_bootc_source_update_empty_cmdline(self):
100+
"""When cmdline is empty, --options is not passed (clears kargs)."""
101+
plugin = self._make_plugin_mock()
102+
plugin._cmdline_val = ""
103+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
104+
105+
plugin._bootc_source_update()
106+
107+
plugin._cmd.execute.assert_called_once_with(
108+
["bootc", "loader-entries", "set-options-for-source",
109+
"--source", "tuned"],
110+
return_err=True)
111+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
112+
consts.BOOT_CMDLINE_FILE,
113+
{consts.BOOT_CMDLINE_TUNED_VAR: ""})
114+
115+
def test_bootc_source_update_command_fails(self):
116+
"""When bootc fails, bootcmdline is not updated."""
117+
plugin = self._make_plugin_mock()
118+
plugin._cmdline_val = "nohz=full"
119+
plugin._cmd.execute = Mock(return_value=(1, "", "error: ostree too old"))
120+
121+
plugin._bootc_source_update()
122+
123+
plugin._cmd.execute.assert_called_once()
124+
plugin._cmd.add_modify_option_in_file.assert_not_called()
125+
126+
# ------------------------------------------------------------------
127+
# _remove_bootc_source_tuning()
128+
# ------------------------------------------------------------------
129+
130+
def test_remove_bootc_source_tuning(self):
131+
"""Removal calls set-options-for-source with no --options (clears all)."""
132+
plugin = self._make_plugin_mock()
133+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
134+
135+
plugin._remove_bootc_source_tuning()
136+
137+
plugin._cmd.execute.assert_called_once_with(
138+
["bootc", "loader-entries", "set-options-for-source",
139+
"--source", "tuned"],
140+
return_err=True)
141+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
142+
consts.BOOT_CMDLINE_FILE,
143+
{consts.BOOT_CMDLINE_TUNED_VAR: ""})
144+
145+
def test_remove_bootc_source_tuning_command_fails(self):
146+
"""When bootc removal fails, bootcmdline is still cleared."""
147+
plugin = self._make_plugin_mock()
148+
plugin._cmd.execute = Mock(return_value=(1, "", "error"))
149+
150+
plugin._remove_bootc_source_tuning()
151+
152+
plugin._cmd.add_modify_option_in_file.assert_called_once_with(
153+
consts.BOOT_CMDLINE_FILE,
154+
{consts.BOOT_CMDLINE_TUNED_VAR: ""})
155+
156+
# ------------------------------------------------------------------
157+
# Init flag derivation logic
158+
# ------------------------------------------------------------------
159+
160+
def test_init_bootc_available(self):
161+
"""When bootc is available, _bootc_has_source is True."""
162+
plugin = Mock()
163+
plugin._cmd = Mock()
164+
plugin._bootc_has_set_options_for_source = Mock(return_value=True)
165+
plugin._rpm_ostree_status = Mock(return_value="idle")
166+
167+
plugin._rpm_ostree = plugin._rpm_ostree_status() is not None
168+
plugin._bootc_has_source = plugin._bootc_has_set_options_for_source()
169+
170+
self.assertTrue(plugin._bootc_has_source)
171+
172+
def test_init_no_bootc_no_rpm_ostree(self):
173+
"""When neither bootc nor rpm-ostree is available, both flags are False."""
174+
plugin = Mock()
175+
plugin._cmd = Mock()
176+
plugin._bootc_has_set_options_for_source = Mock(return_value=False)
177+
plugin._rpm_ostree_status = Mock(return_value=None)
178+
179+
plugin._rpm_ostree = plugin._rpm_ostree_status() is not None
180+
plugin._bootc_has_source = plugin._bootc_has_set_options_for_source()
181+
182+
self.assertFalse(plugin._bootc_has_source)
183+
self.assertFalse(plugin._rpm_ostree)
184+
185+
# ------------------------------------------------------------------
186+
# Top-level dispatch
187+
# ------------------------------------------------------------------
188+
189+
def test_dispatch_bootc_independent_of_rpm_ostree(self):
190+
"""bootc path works even when _rpm_ostree is False (bootc-only system)."""
191+
plugin = self._make_plugin_mock(bootc_has_source=True)
192+
plugin._rpm_ostree = False
193+
plugin._cmdline_val = "nohz=full"
194+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
195+
196+
# Simulate _instance_post_static dispatch
197+
if plugin._bootc_has_source:
198+
plugin._bootc_source_update()
199+
elif plugin._rpm_ostree:
200+
pass # would call _rpm_ostree_update
201+
202+
plugin._cmd.execute.assert_called_once_with(
203+
["bootc", "loader-entries", "set-options-for-source",
204+
"--source", "tuned", "--options", "nohz=full"],
205+
return_err=True)
206+
207+
def test_dispatch_falls_through_to_rpm_ostree(self):
208+
"""When bootc is not available, dispatch falls through to rpm-ostree."""
209+
plugin = self._make_plugin_mock(bootc_has_source=False)
210+
plugin._rpm_ostree = True
211+
212+
# Simulate _instance_post_static dispatch
213+
bootc_called = False
214+
rpm_ostree_called = False
215+
if plugin._bootc_has_source:
216+
bootc_called = True
217+
elif plugin._rpm_ostree:
218+
rpm_ostree_called = True
219+
220+
self.assertFalse(bootc_called)
221+
self.assertTrue(rpm_ostree_called)
222+
223+
def test_instance_post_static_dispatches_to_bootc(self):
224+
"""_instance_post_static calls bootc when _bootc_has_source is True."""
225+
from tuned.plugins.plugin_bootloader import BootloaderPlugin
226+
227+
plugin = self._make_plugin_mock(bootc_has_source=True)
228+
plugin._rpm_ostree = False
229+
plugin._cmdline_val = "nohz=full"
230+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
231+
plugin.update_grub2_cfg = True
232+
plugin._skip_grub_config_val = False
233+
234+
instance = Mock()
235+
BootloaderPlugin._instance_post_static(plugin, instance, enabling=True)
236+
237+
plugin._cmd.execute.assert_called_once_with(
238+
["bootc", "loader-entries", "set-options-for-source",
239+
"--source", "tuned", "--options", "nohz=full"],
240+
return_err=True)
241+
242+
def test_instance_unapply_dispatches_to_bootc(self):
243+
"""_instance_unapply_static calls bootc removal when _bootc_has_source."""
244+
from tuned.plugins.plugin_bootloader import BootloaderPlugin
245+
import tuned.consts as c
246+
247+
plugin = self._make_plugin_mock(bootc_has_source=True)
248+
plugin._rpm_ostree = False
249+
plugin._cmd.execute = Mock(return_value=(0, "", ""))
250+
plugin._skip_grub_config_val = False
251+
252+
instance = Mock()
253+
BootloaderPlugin._instance_unapply_static(plugin, instance, rollback=c.ROLLBACK_FULL)
254+
255+
plugin._cmd.execute.assert_called_once_with(
256+
["bootc", "loader-entries", "set-options-for-source",
257+
"--source", "tuned"],
258+
return_err=True)
259+
260+
def test_instance_unapply_falls_through_to_rpm_ostree(self):
261+
"""_instance_unapply_static falls through to rpm-ostree when no bootc."""
262+
from tuned.plugins.plugin_bootloader import BootloaderPlugin
263+
import tuned.consts as c
264+
265+
plugin = self._make_plugin_mock(bootc_has_source=False)
266+
plugin._rpm_ostree = True
267+
plugin._skip_grub_config_val = False
268+
plugin._remove_rpm_ostree_tuning = Mock()
269+
270+
instance = Mock()
271+
BootloaderPlugin._instance_unapply_static(plugin, instance, rollback=c.ROLLBACK_FULL)
272+
273+
plugin._remove_rpm_ostree_tuning.assert_called_once()
274+
275+
276+
if __name__ == '__main__':
277+
unittest.main()

tuned/plugins/plugin_bootloader.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def _instance_init(self, instance):
194194
self._bls = self._bls_enabled()
195195

196196
self._rpm_ostree = self._rpm_ostree_status() is not None
197+
self._bootc_has_source = self._bootc_has_set_options_for_source()
197198

198199
def _instance_cleanup(self, instance):
199200
pass
@@ -224,6 +225,48 @@ def _rpm_ostree_status(self):
224225
return None
225226
return splited[1]
226227

228+
def _bootc_has_set_options_for_source(self):
229+
"""Check if bootc loader-entries set-options-for-source is available."""
230+
(rc, out, err) = self._cmd.execute(
231+
['bootc', 'loader-entries', 'set-options-for-source', '--help'],
232+
return_err=True)
233+
return rc == 0
234+
235+
def _bootc_source_update(self):
236+
"""Apply kernel parameter tuning using bootc source tracking.
237+
238+
With bootc loader-entries set-options-for-source, bootc automatically
239+
removes all previous kargs from the 'tuned' source and replaces them
240+
with the new set. This eliminates the need to track state in
241+
/etc/tuned/bootcmdline, which is critical for bootc systems with
242+
transient /etc.
243+
"""
244+
cmd = ["bootc", "loader-entries", "set-options-for-source",
245+
"--source", "tuned"]
246+
if self._cmdline_val:
247+
cmd.extend(["--options", self._cmdline_val])
248+
(rc, _, err) = self._cmd.execute(cmd, return_err=True)
249+
if rc != 0:
250+
log.error("Error applying bootc kargs with set-options-for-source: %s" % err)
251+
# Do not update state file — the kargs were not applied
252+
return
253+
# Best-effort state file write for diagnostics and backward compat
254+
self._patch_bootcmdline({consts.BOOT_CMDLINE_TUNED_VAR: self._cmdline_val})
255+
256+
def _remove_bootc_source_tuning(self):
257+
"""Remove kernel parameter tuning using bootc source tracking.
258+
259+
Calling set-options-for-source with --source tuned and no --options
260+
clears all kargs owned by the 'tuned' source.
261+
"""
262+
(rc, _, err) = self._cmd.execute(
263+
["bootc", "loader-entries", "set-options-for-source",
264+
"--source", "tuned"], return_err=True)
265+
if rc != 0:
266+
log.error("Error clearing bootc kargs source: %s" % err)
267+
# Clear the state file even on failure to prevent stale state
268+
self._patch_bootcmdline({consts.BOOT_CMDLINE_TUNED_VAR: ""})
269+
227270
def _wait_till_rpm_ostree_idle(self):
228271
"""Check that rpm-ostree is idle, allowing some waiting time."""
229272
sleep_cycles = 10
@@ -341,7 +384,10 @@ def _remove_rpm_ostree_tuning(self):
341384

342385
def _instance_unapply_static(self, instance, rollback = consts.ROLLBACK_SOFT):
343386
if rollback == consts.ROLLBACK_FULL and not self._skip_grub_config_val:
344-
if self._rpm_ostree:
387+
if self._bootc_has_source:
388+
log.info("removing bootc tuning previously added by Tuned")
389+
self._remove_bootc_source_tuning()
390+
elif self._rpm_ostree:
345391
log.info("removing rpm-ostree tuning previously added by Tuned")
346392
self._remove_rpm_ostree_tuning()
347393
else:
@@ -678,7 +724,9 @@ def _instance_post_static(self, instance, enabling):
678724
# ensure that the desired cmdline is always written to BOOT_CMDLINE_FILE (/etc/tuned/bootcmdline)
679725
self._patch_bootcmdline({consts.BOOT_CMDLINE_TUNED_VAR : self._cmdline_val, consts.BOOT_CMDLINE_INITRD_ADD_VAR : self._initrd_val})
680726
elif enabling and self.update_grub2_cfg:
681-
if self._rpm_ostree:
727+
if self._bootc_has_source:
728+
self._bootc_source_update()
729+
elif self._rpm_ostree:
682730
self._rpm_ostree_update()
683731
else:
684732
self._grub2_update()

0 commit comments

Comments
 (0)