Skip to content

feat: add upgrade/switch/rollback e2e tests for all 4 boot variants#3

Merged
henrywang merged 17 commits into
mainfrom
more-tests
May 18, 2026
Merged

feat: add upgrade/switch/rollback e2e tests for all 4 boot variants#3
henrywang merged 17 commits into
mainfrom
more-tests

Conversation

@henrywang

Copy link
Copy Markdown
Owner

Adds a three-boot e2e test sequence that exercises the full cbootc lifecycle: switch to a v2 image pulled from a local TAP-backed registry, verify the new deployment boots, roll back, and verify the original deployment is restored. All 4 boot variants are covered (GRUB, GRUB+SecureBoot, UKI, UKI+SecureBoot) for both Fedora and Ubuntu.

fix: rollback.rs uses numeric GRUB index on Ubuntu/Debian

Ubuntu's GRUB does not ship blscfg.mod and does not reliably match set default=<128-char-SHA512> against menuentry --id values in the generated grub.cfg. On non-grub2 systems, cbootc rollback now writes the numeric position (newest-first, matching write_grub_menuentry_cfg's sort order) as next_entry instead of the raw digest. Fedora/RHEL (grub2's blscfg.mod, native BLS ID matching) are unchanged.

Tested locally: Ubuntu GRUB 9/9, Ubuntu GRUB+SB 12/12.

henrywang added 5 commits May 16, 2026 08:33
Adds a three-boot e2e test sequence that exercises the full cbootc
lifecycle: switch to a v2 image pulled from a local TAP-backed registry,
verify the new deployment boots, roll back, and verify the original
deployment is restored.  All 4 boot variants are covered (GRUB,
GRUB+SecureBoot, UKI, UKI+SecureBoot) for both Fedora and Ubuntu.

fix: rollback.rs uses numeric GRUB index on Ubuntu/Debian

Ubuntu's GRUB does not ship blscfg.mod and does not reliably match
`set default=<128-char-SHA512>` against `menuentry --id` values in the
generated grub.cfg.  On non-grub2 systems, cbootc rollback now writes
the numeric position (newest-first, matching write_grub_menuentry_cfg's
sort order) as next_entry instead of the raw digest.  Fedora/RHEL
(grub2's blscfg.mod, native BLS ID matching) are unchanged.

Tested locally: Ubuntu GRUB 9/9, Ubuntu GRUB+SB 12/12.
On Fedora, the autologin shell fires while udev is still renaming the
virtio-net interface, so ip-link shows only lo at that point.  The
previous code ran immediately and got an empty $iface, causing
ip-link-set to fail and the configure_guest_network step to FAIL.
Result: 2 passed / 7 failed (coincidental pass of rolled_back_digest_active).

Fix: poll up to 15 s for a non-loopback interface to appear, then tell
NetworkManager to unmanage it (preventing it from overriding the static
IP) and flush any stale addresses before setting ours.
bootctl set-default was added in upgrade.rs but Ubuntu UKI tests now show
"No such file or directory (os error 2)" on both switch and rollback.
Add _diag() calls before switch_to_v2 and rollback_succeeds (UKI only)
to capture: bootctl path/version, mount table, EFI/Linux contents,
efivars count, and bootctl status output for the next CI run.
…rors

The previous approach copied disk.raw to /tmp with cp --sparse=always,
then booted the copy without snapshot=on.  When QEMU wrote to previously-
sparse regions (ext4 journal commits during boot), the writes failed with
BLK_STS_IOERR because /tmp (tmpfs) had insufficient space.

Fix: pass snapshot=on,format=raw on the QEMU drive instead.  QEMU creates
its own thin qcow2 overlay (delta-only, starts empty, ~100-200 MB for a
switch+rollback run) and persists it across guest reboots within the same
QEMU process.  The original disk.raw is never modified, and there is no
full-disk copy to blow out /tmp.
bootctl set-default / set-next require either the bootctl binary in PATH
or a writable efivarfs (backing OVMF_VARS flash device).  Both may be
absent: the Ubuntu container image does not guarantee bootctl is on PATH,
and test VMs often run with a read-only combined OVMF_CODE.fd that has no
separate writable VARS.

Fix: write `default <entry-id>` directly into /boot/efi/loader/loader.conf.
Systemd-boot reads this file at every boot and honours it as the default
entry when no EFI variable preference is set.  The file is on the FAT ESP
which is always writable when cbootc runs.

upgrade.rs: replace `bootctl set-default` with set_loader_conf_default()
rollback.rs: replace `bootctl set-next` / set_next_entry_bootctl() with the
             same helper (persistent rollback — stays on old entry until the
             next explicit upgrade, which is the correct behaviour here)
tests/e2e.py: replace test_bootctl_next_entry_set (EFI var check) with
              test_loader_conf_default_set (loader.conf check); drop _diag
henrywang added 12 commits May 17, 2026 04:38
…king

On Fedora, nftables is the backend for firewalld; plain iptables rules
are silently ignored, so the host firewall was blocking guest→host TCP
even after 'iptables -I INPUT ACCEPT'.  Switching to
'firewall-cmd --zone=trusted' puts the TAP in the trusted zone and
actually opens the traffic.

configure_guest_network now re-reads the interface name on every
iteration instead of caching it once at the start.  udev can rename
the virtio-net device (eth0→enp0s3) between the name lookup and
ip addr add, returning ENODEV; the retry loop recovers from both the
rename race and the case where the device is not yet visible.
…SC sequences

Ubuntu's bash emits OSC 3008 shell-integration sequences before command
output (\x1b]3008;...;type=command;cwd=/root\x1b\).  The String
Terminator (\x1b\) is not a newline, so re.MULTILINE ^ never matches
at the position where 'default ...' begins.  Switching from '^default'
to '\bdefault' matches regardless of what character precedes it.
After cbootc switch/upgrade creates a new BLS entry, GRUB's blscfg.mod
selects the default based on the grubenv 'default' variable, not by
entry creation order.  Without updating grubenv, GRUB continues to boot
the previously-set default entry regardless of the new BLS conf.

Add set_grub_default() and call it from run() on the Fedora/RHEL path
(has_grub2 = true).  The Ubuntu path is unaffected: it writes a new
grub.cfg via write_grub_menuentry_cfg() where index 0 is already the
newest entry.
firewall-cmd is only present on Fedora/RHEL (firewalld).  On Ubuntu
(GitHub Actions runner) subprocess.run raises FileNotFoundError before
it can even return a non-zero exit code, crashing setup_tap().

Catch FileNotFoundError and fall back to iptables, which works on
Ubuntu and other systems without firewalld.
TAP networking required host-side firewall manipulation (firewall-cmd /
iptables) that was unreliable across distros: firewalld-managed nftables
silently ignores plain iptables rules, and firewall-cmd was crashing CI
runners that lack firewalld.

QEMU user-mode networking (SLIRP) avoids all of this: the guest reaches
the host at 10.0.2.2 and QEMU forwards those connections to host
loopback 127.0.0.1, where the registry container (--network=host,
REGISTRY_HTTP_ADDR=0.0.0.0:5000) is listening.  No TAP, no firewall
rules, no root-level network setup needed.

- Remove setup_tap / teardown_tap and all TAP constants
- boot_with_network: -netdev tap → -netdev user
- configure_guest_network: static IP assignment → wait for DHCP route
- wait_for_network: ping → TCP connect to registry port (more reliable;
  host firewall may pass ICMP but block TCP)
- REGISTRY_HOST: 192.168.200.1 → 10.0.2.2 (SLIRP gateway)
The Ubuntu test image may not have a DHCP client configured for the
virtio-net interface, so waiting for a default route via DHCP would
time out.  Assign 10.0.2.100/24 statically (same approach as the old
TAP code but in the SLIRP subnet).  10.0.2.2 is reachable via the
automatic 10.0.2.0/24 link-local route — no explicit default route
needed.
Raw QEMU serial console output was piped directly to stdout via
pexpect's logfile_read, burying PASS/FAIL lines in escape sequences
and kernel boot noise.

- Redirect console output to e2e-console.log instead of stdout;
  announce the path with a tail -f hint so progress is still
  followable during long boots
- Strip ANSI/control characters from run_cmd output so assertion
  failure messages are readable without post-processing
- On test failure or timeout, print the last 40–50 cleaned console
  lines under the FAIL line with a pointer to the full log
gawk provides 'awk', which the e2e configure_guest_network helper relies
on but was absent from the minimal base images.  Install it in the example
Containerfiles (the right layer for test tooling) and remove the unrelated
vim/htop that was there before.

Also add [diag] output to run_upgrade_sequence for UKI runs: captures
loader.conf, EFI/Linux listing, and bootctl list/status around the first
reboot so the upgraded_digest_active failure can be diagnosed.
Writing loader.conf alone is insufficient: something running during boot
sets the LoaderEntryDefault EFI variable to the currently-running entry,
which takes priority over loader.conf.  This caused rollback to be silently
ignored — Boot 3 would boot the post-upgrade entry again instead of the
original.

Fix: call bootctl set-default after writing loader.conf, mirroring what
the GRUB path does with grub2-editenv.  NotFound errors are ignored for
container/install contexts without a writable efivarfs.
@henrywang henrywang merged commit ab943ce into main May 18, 2026
3 checks passed
@henrywang henrywang deleted the more-tests branch May 18, 2026 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant