|
| 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 | +} |
0 commit comments