Skip to content

Commit c4e71a3

Browse files
committed
tests: Add TMT integration test for loader-entries set-options-for-source
Add a multi-reboot integration test covering the full lifecycle of source-tracked kernel arguments: - Input validation (invalid/empty source names, special characters) - Adding source-tracked kargs and verifying /proc/cmdline after reboot - x-options-source-* BLS keys surviving the staging roundtrip - Source replacement semantics (old kargs removed, new ones added) - Multiple sources coexisting independently - Source removal (--source without --options clears owned kargs) - --options "" (empty string) clears kargs distinctly from no --options - Existing system kargs (root=, ostree=, rw, console=) preserved through all operations - Staged deployment interaction: bootc switch stages a deployment, then set-options-for-source replaces it using the staged commit, preserving the image switch alongside the source kargs - Idempotent operation (no deployment staged when kargs unchanged) The test uses 5 reboots across 6 phases. Also includes auto-generated man pages for the new loader-entries subcommand. Assisted-by: OpenCode (Claude claude-opus-4-6)
1 parent 7d942d3 commit c4e71a3

6 files changed

Lines changed: 332 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# NAME
2+
3+
bootc-loader-entries-set-options-for-source - Set or update the kernel arguments owned by a specific source
4+
5+
# SYNOPSIS
6+
7+
bootc loader-entries set-options-for-source
8+
9+
# DESCRIPTION
10+
11+
Set or update the kernel arguments owned by a specific source
12+
13+
# OPTIONS
14+
15+
<!-- BEGIN GENERATED OPTIONS -->
16+
**--source**=*SOURCE*
17+
18+
The name of the source that owns these kernel arguments
19+
20+
**--options**=*OPTIONS*
21+
22+
The kernel arguments to set for this source
23+
24+
<!-- END GENERATED OPTIONS -->
25+
26+
# EXAMPLES
27+
28+
TODO: Add practical examples showing how to use this command.
29+
30+
# SEE ALSO
31+
32+
**bootc**(8)
33+
34+
# VERSION
35+
36+
<!-- VERSION PLACEHOLDER -->
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# NAME
2+
3+
bootc-loader-entries - Operations on Boot Loader Specification (BLS) entries
4+
5+
# SYNOPSIS
6+
7+
bootc loader-entries
8+
9+
# DESCRIPTION
10+
11+
Operations on Boot Loader Specification (BLS) entries
12+
13+
<!-- BEGIN GENERATED OPTIONS -->
14+
<!-- END GENERATED OPTIONS -->
15+
16+
# EXAMPLES
17+
18+
TODO: Add practical examples showing how to use this command.
19+
20+
# SEE ALSO
21+
22+
**bootc**(8)
23+
24+
# VERSION
25+
26+
<!-- VERSION PLACEHOLDER -->

docs/src/man/bootc.8.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pulled and `bootc upgrade`.
3333
| **bootc usr-overlay** | Add a transient overlayfs on `/usr` |
3434
| **bootc install** | Install the running container to a target |
3535
| **bootc container** | Operations which can be executed as part of a container build |
36+
| **bootc loader-entries** | Operations on Boot Loader Specification (BLS) entries |
3637
| **bootc composefs-finalize-staged** | |
3738

3839
<!-- END GENERATED SUBCOMMANDS -->

tmt/plans/integration.fmf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,11 @@ execute:
237237
how: fmf
238238
test:
239239
- /tmt/tests/tests/test-39-upgrade-tag
240+
241+
/plan-40-loader-entries-source:
242+
summary: Test bootc loader-entries set-options-for-source
243+
discover:
244+
how: fmf
245+
test:
246+
- /tmt/tests/tests/test-40-loader-entries-source
240247
# END GENERATED PLANS
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# number: 40
2+
# tmt:
3+
# summary: Test bootc loader-entries set-options-for-source
4+
# duration: 30m
5+
#
6+
# This test verifies the source-tracked kernel argument management via
7+
# bootc loader-entries set-options-for-source. It covers:
8+
# 1. Input validation (invalid/empty source names)
9+
# 2. Adding source-tracked kargs and verifying they appear in /proc/cmdline
10+
# 3. Kargs and x-options-source-* BLS keys surviving the staging roundtrip
11+
# 4. Source replacement semantics (old kargs removed, new ones added)
12+
# 5. Multiple sources coexisting independently
13+
# 6. Source removal (--source without --options clears all owned kargs)
14+
# 7. Idempotent operation (no changes when kargs already match)
15+
# 8. Existing system kargs (root=, ostree=, etc.) preserved through changes
16+
# 9. --options "" (empty string) clears kargs without removing the source
17+
# 10. Staged deployment interaction (bootc switch + set-options-for-source
18+
# preserves the pending image switch)
19+
#
20+
# Requires ostree with bootconfig-extra support (>= 2026.1).
21+
# See: https://github.com/ostreedev/ostree/pull/3570
22+
# See: https://github.com/bootc-dev/bootc/issues/899
23+
use std assert
24+
use tap.nu
25+
use bootc_testlib.nu
26+
27+
def parse_cmdline [] {
28+
open /proc/cmdline | str trim | split row " "
29+
}
30+
31+
# Read x-options-source-* keys from the booted BLS entry
32+
def read_bls_source_keys [] {
33+
let entries = glob /boot/loader/entries/ostree-*.conf
34+
if ($entries | length) == 0 {
35+
error make { msg: "No BLS entries found" }
36+
}
37+
let entry = open ($entries | first)
38+
$entry | lines | where { |line| $line starts-with "x-options-source-" }
39+
}
40+
41+
# Save the current system kargs (root=, ostree=, rw, etc.) for later comparison
42+
def save_system_kargs [] {
43+
let cmdline = parse_cmdline
44+
# Filter to well-known system kargs that must never be lost
45+
let system_kargs = $cmdline | where { |k|
46+
($k starts-with "root=") or
47+
($k starts-with "ostree=") or
48+
($k == "rw") or
49+
($k starts-with "console=")
50+
}
51+
$system_kargs | to json | save -f /var/bootc-test-system-kargs.json
52+
}
53+
54+
def load_system_kargs [] {
55+
open /var/bootc-test-system-kargs.json | from json
56+
}
57+
58+
def first_boot [] {
59+
tap begin "loader-entries set-options-for-source"
60+
61+
# Save system kargs for later verification
62+
save_system_kargs
63+
64+
# -- Input validation --
65+
66+
# Invalid source name (spaces)
67+
let r = do -i { bootc loader-entries set-options-for-source --source "bad name" --options "foo=bar" } | complete
68+
assert ($r.exit_code != 0) "spaces in source name should fail"
69+
70+
# Invalid source name (special chars)
71+
let r = do -i { bootc loader-entries set-options-for-source --source "foo@bar" --options "foo=bar" } | complete
72+
assert ($r.exit_code != 0) "special chars in source name should fail"
73+
74+
# Empty source name
75+
let r = do -i { bootc loader-entries set-options-for-source --source "" --options "foo=bar" } | complete
76+
assert ($r.exit_code != 0) "empty source name should fail"
77+
78+
# Valid name with underscores/dashes
79+
let r = do -i { bootc loader-entries set-options-for-source --source "my_custom-src" --options "testvalid=1" } | complete
80+
assert ($r.exit_code == 0) "valid source name should succeed"
81+
82+
# Clear it immediately (no --options = remove source)
83+
let r = do -i { bootc loader-entries set-options-for-source --source "my_custom-src" } | complete
84+
assert ($r.exit_code == 0) "clearing source should succeed"
85+
86+
# -- Add source kargs --
87+
bootc loader-entries set-options-for-source --source tuned --options "nohz=full isolcpus=1-3"
88+
89+
# Verify deployment is staged
90+
let st = bootc status --json | from json
91+
assert ($st.status.staged != null) "deployment should be staged"
92+
93+
print "ok: validation and initial staging"
94+
tmt-reboot
95+
}
96+
97+
def second_boot [] {
98+
# Verify kargs survived the staging roundtrip
99+
let cmdline = parse_cmdline
100+
assert ("nohz=full" in $cmdline) "nohz=full should be in cmdline after reboot"
101+
assert ("isolcpus=1-3" in $cmdline) "isolcpus=1-3 should be in cmdline after reboot"
102+
103+
# Verify system kargs were preserved
104+
let system_kargs = load_system_kargs
105+
for karg in $system_kargs {
106+
assert ($karg in $cmdline) $"system karg '($karg)' must be preserved"
107+
}
108+
print "ok: system kargs preserved"
109+
110+
# Verify x-options-source-tuned key in BLS entry
111+
let source_keys = read_bls_source_keys
112+
let tuned_key = $source_keys | where { |line| $line starts-with "x-options-source-tuned" }
113+
assert (($tuned_key | length) > 0) "x-options-source-tuned should be in BLS entry"
114+
let tuned_line = $tuned_key | first
115+
assert ($tuned_line | str contains "nohz=full") "tuned source key should contain nohz=full"
116+
assert ($tuned_line | str contains "isolcpus=1-3") "tuned source key should contain isolcpus=1-3"
117+
print "ok: kargs and source key survived reboot"
118+
119+
# -- Source replacement: new kargs replace old ones --
120+
bootc loader-entries set-options-for-source --source tuned --options "nohz=on rcu_nocbs=2-7"
121+
122+
tmt-reboot
123+
}
124+
125+
def third_boot [] {
126+
# Verify replacement worked
127+
let cmdline = parse_cmdline
128+
assert ("nohz=full" not-in $cmdline) "old nohz=full should be gone"
129+
assert ("isolcpus=1-3" not-in $cmdline) "old isolcpus=1-3 should be gone"
130+
assert ("nohz=on" in $cmdline) "new nohz=on should be present"
131+
assert ("rcu_nocbs=2-7" in $cmdline) "new rcu_nocbs=2-7 should be present"
132+
133+
# Verify system kargs still preserved after replacement
134+
let system_kargs = load_system_kargs
135+
for karg in $system_kargs {
136+
assert ($karg in $cmdline) $"system karg '($karg)' must survive replacement"
137+
}
138+
print "ok: source replacement persisted, system kargs preserved"
139+
140+
# -- Multiple sources coexist --
141+
bootc loader-entries set-options-for-source --source dracut --options "rd.driver.pre=vfio-pci"
142+
143+
tmt-reboot
144+
}
145+
146+
def fourth_boot [] {
147+
# Verify both sources persisted
148+
let cmdline = parse_cmdline
149+
assert ("nohz=on" in $cmdline) "tuned nohz=on should still be present"
150+
assert ("rcu_nocbs=2-7" in $cmdline) "tuned rcu_nocbs=2-7 should still be present"
151+
assert ("rd.driver.pre=vfio-pci" in $cmdline) "dracut karg should be present"
152+
153+
# Verify both source keys in BLS
154+
let source_keys = read_bls_source_keys
155+
let tuned_keys = $source_keys | where { |line| $line starts-with "x-options-source-tuned" }
156+
let dracut_keys = $source_keys | where { |line| $line starts-with "x-options-source-dracut" }
157+
assert (($tuned_keys | length) > 0) "tuned source key should exist"
158+
assert (($dracut_keys | length) > 0) "dracut source key should exist"
159+
print "ok: multiple sources coexist"
160+
161+
# -- Clear source with empty --options "" (different from no --options) --
162+
# --options "" should remove the kargs but the key can remain with empty value
163+
bootc loader-entries set-options-for-source --source dracut --options ""
164+
# dracut kargs should be removed from pending deployment
165+
let st = bootc status --json | from json
166+
assert ($st.status.staged != null) "empty options should still stage a deployment"
167+
print "ok: --options '' clears kargs"
168+
169+
# Now also test no --options (remove the source entirely)
170+
# First re-add dracut so we can test removal
171+
bootc loader-entries set-options-for-source --source dracut --options "rd.driver.pre=vfio-pci"
172+
# Then remove it with no --options
173+
bootc loader-entries set-options-for-source --source dracut
174+
175+
tmt-reboot
176+
}
177+
178+
def fifth_boot [] {
179+
# Verify dracut cleared, tuned preserved
180+
let cmdline = parse_cmdline
181+
assert ("rd.driver.pre=vfio-pci" not-in $cmdline) "dracut karg should be gone"
182+
assert ("nohz=on" in $cmdline) "tuned nohz=on should still be present"
183+
assert ("rcu_nocbs=2-7" in $cmdline) "tuned rcu_nocbs=2-7 should still be present"
184+
print "ok: source clear persisted"
185+
186+
# -- Idempotent: same kargs again should be a no-op --
187+
let r = bootc loader-entries set-options-for-source --source tuned --options "nohz=on rcu_nocbs=2-7" | complete
188+
# Should not stage a new deployment (idempotent)
189+
let st = bootc status --json | from json
190+
assert ($st.status.staged == null) "idempotent call should not stage a deployment"
191+
print "ok: idempotent operation"
192+
193+
# -- Staged deployment interaction --
194+
# Build a derived image and switch to it (this stages a deployment).
195+
# Then call set-options-for-source on top. The staged deployment should
196+
# be replaced with one that has the new image AND the source kargs.
197+
bootc image copy-to-storage
198+
199+
let td = mktemp -d
200+
$"FROM localhost/bootc
201+
RUN echo source-test-marker > /usr/share/source-test-marker.txt
202+
" | save $"($td)/Dockerfile"
203+
podman build -t localhost/bootc-source-test $"($td)"
204+
205+
bootc switch --transport containers-storage localhost/bootc-source-test
206+
let st = bootc status --json | from json
207+
assert ($st.status.staged != null) "switch should stage a deployment"
208+
209+
# Now add source kargs on top of the staged switch
210+
bootc loader-entries set-options-for-source --source tuned --options "nohz=on rcu_nocbs=2-7 skew_tick=1"
211+
212+
# Verify a deployment is still staged (it was replaced, not removed)
213+
let st = bootc status --json | from json
214+
assert ($st.status.staged != null) "deployment should still be staged after set-options-for-source"
215+
216+
tmt-reboot
217+
}
218+
219+
def sixth_boot [] {
220+
# Verify the image switch landed (the derived image's marker file exists)
221+
assert ("/usr/share/source-test-marker.txt" | path exists) "derived image marker should exist"
222+
print "ok: image switch preserved"
223+
224+
# Verify the source kargs also landed
225+
let cmdline = parse_cmdline
226+
assert ("nohz=on" in $cmdline) "tuned nohz=on should be present"
227+
assert ("rcu_nocbs=2-7" in $cmdline) "tuned rcu_nocbs=2-7 should be present"
228+
assert ("skew_tick=1" in $cmdline) "tuned skew_tick=1 should be present"
229+
230+
# Verify source key in BLS
231+
let source_keys = read_bls_source_keys
232+
let tuned_key = $source_keys | where { |line| $line starts-with "x-options-source-tuned" }
233+
assert (($tuned_key | length) > 0) "tuned source key should exist after staged interaction"
234+
print "ok: staged deployment interaction preserved both image and source kargs"
235+
236+
# Verify system kargs still intact
237+
let system_kargs = load_system_kargs
238+
let cmdline = parse_cmdline
239+
for karg in $system_kargs {
240+
assert ($karg in $cmdline) $"system karg '($karg)' must survive staged interaction"
241+
}
242+
print "ok: system kargs preserved through all phases"
243+
244+
tap ok
245+
}
246+
247+
def main [] {
248+
match $env.TMT_REBOOT_COUNT? {
249+
null | "0" => first_boot,
250+
"1" => second_boot,
251+
"2" => third_boot,
252+
"3" => fourth_boot,
253+
"4" => fifth_boot,
254+
"5" => sixth_boot,
255+
$o => { error make { msg: $"Unexpected TMT_REBOOT_COUNT ($o)" } },
256+
}
257+
}

tmt/tests/tests.fmf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,8 @@
136136
summary: Test bootc upgrade --tag functionality with containers-storage
137137
duration: 30m
138138
test: nu booted/test-upgrade-tag.nu
139+
140+
/test-40-loader-entries-source:
141+
summary: Test bootc loader-entries set-options-for-source
142+
duration: 30m
143+
test: nu booted/test-loader-entries-source.nu

0 commit comments

Comments
 (0)