diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 106919f04..921d1a5c4 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -4,6 +4,7 @@ use anyhow::{Context, Result, anyhow}; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; +use ocidir::cap_std::ambient_authority; use rustix::fs::{AtFlags, RenameFlags, fsync, renameat_with}; use crate::bootc_composefs::boot::{ @@ -11,8 +12,11 @@ use crate::bootc_composefs::boot::{ secondary_sort_key, type1_entry_conf_file_name, }; use crate::bootc_composefs::status::{get_composefs_status, get_sorted_type1_boot_entries}; -use crate::composefs_consts::TYPE1_ENT_PATH_STAGED; -use crate::spec::Bootloader; +use crate::composefs_consts::{ + COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, TYPE1_ENT_PATH_STAGED, +}; +use crate::deploy::ROLLBACK_JOURNAL_ID; +use crate::spec::{Bootloader, Host}; use crate::store::{BootedComposefs, Storage}; use crate::{ bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_grub_uki_boot_entries}, @@ -124,7 +128,7 @@ fn rollback_grub_uki_entries(boot_dir: &Dir) -> Result<()> { /// a. Here we assume that rollback is queued as there's no way to differentiate between this /// case and Case 1-b. This is what ostree does as well #[context("Rolling back {bootloader} entries")] -fn rollback_composefs_entries(boot_dir: &Dir, bootloader: Bootloader) -> Result<()> { +fn rollback_composefs_entries(host: &Host, boot_dir: &Dir, bootloader: Bootloader) -> Result<()> { // Get all boot entries sorted in descending order by sort-key let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?; @@ -143,6 +147,32 @@ fn rollback_composefs_entries(boot_dir: &Dir, bootloader: Bootloader) -> Result< // OR if rollback was queued, it would become secondary all_configs[1].sort_key = Some(secondary_sort_key(os_id)); + // Ostree will drop any staged deployment on rollback + // We follow the same approach for now + // + // Cleanup any previous staged entries + boot_dir + .remove_all_optional(TYPE1_ENT_PATH_STAGED) + .context("Removing staged entries")?; + + if let Some(staged) = &host.status.staged { + tracing::info!( + message_id = ROLLBACK_JOURNAL_ID, + "Removing currently staged composefs deployment {}", + // SAFETY: This is a staged composefs entry, so composefs property + // will always exist + staged.composefs.as_ref().unwrap().verity + ); + + let transient_dir = + Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority()) + .context("Opening transient dir")?; + + transient_dir + .remove_file(COMPOSEFS_STAGED_DEPLOYMENT_FNAME) + .context("Removing staged deployment file")?; + } + // Write these boot_dir .create_dir_all(TYPE1_ENT_PATH_STAGED) @@ -213,11 +243,9 @@ pub(crate) async fn composefs_rollback( let rollback_status = host .status .rollback + .as_ref() .ok_or_else(|| anyhow!("No rollback available"))?; - // TODO: Handle staged deployment - // Ostree will drop any staged deployment on rollback but will keep it if it is the first item - // in the new deployment list let Some(rollback_entry) = &rollback_status.composefs else { anyhow::bail!("Rollback deployment not a composefs deployment") }; @@ -227,7 +255,7 @@ pub(crate) async fn composefs_rollback( match &rollback_entry.bootloader { Bootloader::Grub => match rollback_entry.boot_type { BootType::Bls => { - rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; + rollback_composefs_entries(&host, boot_dir, rollback_entry.bootloader.clone())?; } BootType::Uki => { rollback_grub_uki_entries(boot_dir)?; @@ -236,7 +264,7 @@ pub(crate) async fn composefs_rollback( Bootloader::Systemd => { // We use BLS entries for systemd UKI as well - rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; + rollback_composefs_entries(&host, boot_dir, rollback_entry.bootloader.clone())?; } Bootloader::None => unreachable!("Checked at install time"), diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index bd8b7a19c..e686049fa 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, io::Read, sync::OnceLock}; +use std::{io::Read, sync::OnceLock}; use anyhow::{Context, Result}; use bootc_kernel_cmdline::utf8::Cmdline; @@ -243,18 +243,26 @@ fn get_sorted_grub_uki_boot_entries_helper<'a>( parse_grub_menuentry_file(str) } +/// Get sorted boot entries +/// The sort here is done in terms of what will be shown on the boot menu +/// For systemd-boot, the entries are sorted by `sort-key` +/// For grub, the entries are sorted by the filename in descending order pub(crate) fn get_sorted_type1_boot_entries( boot_dir: &Dir, ascending: bool, ) -> Result> { - get_sorted_type1_boot_entries_helper(boot_dir, ascending, false) + let bootloader = get_bootloader()?; + get_sorted_type1_boot_entries_helper(boot_dir, ascending, false, bootloader) } +/// Same as [`get_sorted_type1_boot_entries`], but returns staged entries +/// See [`get_sorted_type1_boot_entries`] for more details pub(crate) fn get_sorted_staged_type1_boot_entries( boot_dir: &Dir, ascending: bool, ) -> Result> { - get_sorted_type1_boot_entries_helper(boot_dir, ascending, true) + let bootloader = get_bootloader()?; + get_sorted_type1_boot_entries_helper(boot_dir, ascending, true, bootloader) } #[context("Getting sorted Type1 boot entries")] @@ -262,15 +270,20 @@ fn get_sorted_type1_boot_entries_helper( boot_dir: &Dir, ascending: bool, get_staged_entries: bool, + bootloader: crate::spec::Bootloader, ) -> Result> { - let mut all_configs = vec![]; + #[derive(Debug)] + struct ConfigWithFilename { + config: BLSConfig, + filename: String, + } let dir = match get_staged_entries { true => { let dir = boot_dir.open_dir_optional(TYPE1_ENT_PATH_STAGED)?; let Some(dir) = dir else { - return Ok(all_configs); + return Ok(vec![]); }; dir.read_dir(".")? @@ -279,6 +292,8 @@ fn get_sorted_type1_boot_entries_helper( false => boot_dir.read_dir(TYPE1_ENT_PATH)?, }; + let mut configs_with_filenames = vec![]; + for entry in dir { let entry = entry?; @@ -302,12 +317,31 @@ fn get_sorted_type1_boot_entries_helper( let config = parse_bls_config(&contents).context("Parsing bls config")?; - all_configs.push(config); + configs_with_filenames.push(ConfigWithFilename { + config, + filename: file_name.to_string(), + }); } - all_configs.sort_by(|a, b| if ascending { a.cmp(b) } else { b.cmp(a) }); + // Sort based on bootloader type + configs_with_filenames.sort_by(|a, b| { + let ord = match bootloader { + // For systemd-boot sort by sort-key + Bootloader::Systemd => a.config.cmp(&b.config), + // For grub, sort by filename in descending order + Bootloader::Grub => b.filename.cmp(&a.filename), + Bootloader::None => { + unreachable!("Bootloader checked during installation should not have been none") + } + }; + + if ascending { ord } else { ord.reverse() } + }); - Ok(all_configs) + Ok(configs_with_filenames + .into_iter() + .map(|c| c.config) + .collect()) } pub(crate) fn list_type1_entries(boot_dir: &Dir) -> Result> { @@ -797,7 +831,6 @@ async fn composefs_deployment_status_from( Err(e) => Err(e), }?; - // NOTE: This cannot work if we support both BLS and UKI at the same time let mut boot_type: Option = None; // Boot entries from deployments that are neither booted nor staged deployments @@ -809,8 +842,19 @@ async fn composefs_deployment_status_from( .. } in bootloader_entry_verity { - let ini = read_origin(&storage.physical_root, &verity_digest)? - .ok_or_else(|| anyhow::anyhow!("No origin file for deployment {verity_digest}"))?; + let ini = read_origin(&storage.physical_root, &verity_digest)?; + + let Some(ini) = ini else { + const STATUS_JOURNAL_ID: &str = "d264f924dadb4c31bff0412107d391fb"; + + tracing::warn!( + message_id = STATUS_JOURNAL_ID, + bootc.operation = "status", + "No origin file for deployment {verity_digest}" + ); + + continue; + }; let mut boot_entry = boot_entry_from_composefs_deployment( storage, @@ -932,6 +976,8 @@ async fn composefs_deployment_status_from( // Determine rollback deployment by matching extra deployment boot entries against entires read from /boot // This collects verity digest across bls and grub enties, we should just have one of them, but still works + // + // We want this ordered, so we have a vector here let bootloader_configured_verity = sorted_bls_config .iter() .flatten() @@ -942,9 +988,9 @@ async fn composefs_deployment_status_from( .flatten() .map(|menu| menu.get_verity()), ) - .collect::>>()?; + .collect::>>()?; - let rollback_candidates: Vec<_> = extra_deployment_boot_entries + let mut rollback_candidates: Vec<_> = extra_deployment_boot_entries .into_iter() .filter(|entry| { let verity = &entry @@ -956,10 +1002,22 @@ async fn composefs_deployment_status_from( }) .collect(); - if rollback_candidates.len() > 1 { - anyhow::bail!("Multiple extra entries in /boot, could not determine rollback entry"); - } else if let Some(rollback_entry) = rollback_candidates.into_iter().next() { - host.status.rollback = Some(rollback_entry); + // We get sorted bootloader entries, so here we re-sort the rollback candidates + // wrt their positions in the sorted bootloader entries as that's what determines + // what's shown on the bootloader menu. The very next boot entry, that's not the + // default should be the rollback + rollback_candidates.sort_by_key(|ent| { + bootloader_configured_verity + .iter() + // SAFETY: ent.composefs will definitely exist + .position(|v| ent.composefs.as_ref().unwrap().verity == *v) + }); + + if !rollback_candidates.is_empty() { + let mut iter = rollback_candidates.into_iter(); + + host.status.rollback = iter.next(); + host.status.other_deployments = iter.collect(); } host.status.rollback_queued = is_rollback_queued; @@ -979,7 +1037,11 @@ async fn composefs_deployment_status_from( mod tests { use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; - use crate::parsers::{bls_config::BLSConfigType, grub_menuconfig::MenuentryBody}; + use crate::bootc_composefs::boot::{ + FILENAME_PRIORITY_PRIMARY, FILENAME_PRIORITY_SECONDARY, primary_sort_key, + secondary_sort_key, type1_entry_conf_file_name, + }; + use crate::parsers::grub_menuconfig::MenuentryBody; use super::*; @@ -1024,30 +1086,16 @@ mod tests { tempdir.atomic_write("loader/entries/entry1.conf", entry1)?; tempdir.atomic_write("loader/entries/entry2.conf", entry2)?; - let result = get_sorted_type1_boot_entries(&tempdir, true).unwrap(); - - let mut config1 = BLSConfig::default(); - config1.title = Some("Fedora 42.20250623.3.1 (CoreOS)".into()); - config1.sort_key = Some("1".into()); - config1.cfg_type = BLSConfigType::NonEFI { - linux: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(), - initrd: vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into()], - options: Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into()), - }; - - let mut config2 = BLSConfig::default(); - config2.title = Some("Fedora 41.20250214.2.0 (CoreOS)".into()); - config2.sort_key = Some("2".into()); - config2.cfg_type = BLSConfigType::NonEFI { - linux: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(), - initrd: vec!["/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into()], - options: Some("root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into()) - }; + let result = + get_sorted_type1_boot_entries_helper(&tempdir, true, false, Bootloader::Systemd) + .unwrap(); assert_eq!(result[0].sort_key.as_ref().unwrap(), "1"); assert_eq!(result[1].sort_key.as_ref().unwrap(), "2"); - let result = get_sorted_type1_boot_entries(&tempdir, false).unwrap(); + let result = + get_sorted_type1_boot_entries_helper(&tempdir, false, false, Bootloader::Systemd) + .unwrap(); assert_eq!(result[0].sort_key.as_ref().unwrap(), "2"); assert_eq!(result[1].sort_key.as_ref().unwrap(), "1"); @@ -1217,4 +1265,178 @@ mod tests { Ok(()) } + + #[test] + fn test_get_sorted_type1_boot_entries_helper_systemd() -> Result<()> { + let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + + // Create entries with different sort-keys for systemd-boot testing + let entry1 = format!( + r#" + title Fedora Linux (1.0.0) + version 1.0.0 + sort-key {} + linux /boot/vmlinuz + initrd /boot/initramfs.img + "#, + secondary_sort_key("fedora") + ); + + let entry2 = format!( + r#" + title Fedora Linux (2.0.0) + version 2.0.0 + sort-key {} + linux /boot/vmlinuz + initrd /boot/initramfs.img + "#, + primary_sort_key("fedora") + ); + + let entry3 = format!( + r#" + title Fedora Linux (1.5.0) + version 1.5.0 + sort-key {} + linux /boot/vmlinuz + initrd /boot/initramfs.img + "#, + primary_sort_key("fedora") + ); + + tempdir.create_dir_all("loader/entries")?; + + // Use realistic filenames as used in production + let filename1 = type1_entry_conf_file_name("fedora", "1.0.0", FILENAME_PRIORITY_SECONDARY); + let filename2 = type1_entry_conf_file_name("fedora", "2.0.0", FILENAME_PRIORITY_PRIMARY); + let filename3 = type1_entry_conf_file_name("fedora", "1.5.0", FILENAME_PRIORITY_PRIMARY); + + tempdir.atomic_write(format!("loader/entries/{}", filename1), entry1)?; + tempdir.atomic_write(format!("loader/entries/{}", filename2), entry2)?; + tempdir.atomic_write(format!("loader/entries/{}", filename3), entry3)?; + + // Test systemd-boot sorting (by sort-key, then by version in descending order) + let result = get_sorted_type1_boot_entries_helper( + &tempdir, + true, + false, + crate::spec::Bootloader::Systemd, + )?; + + assert_eq!(result.len(), 3); + // With ascending=true, primary sort-key (bootc-fedora-0) should come before secondary (bootc-fedora-1) + // Within primary sort-key, version 2.0.0 should come before 1.5.0 (descending version order) + assert_eq!( + result[0].sort_key.as_ref().unwrap(), + &primary_sort_key("fedora") + ); + assert_eq!(result[0].version(), "2.0.0".into()); // Entry 2 (version 2.0.0) + assert_eq!( + result[1].sort_key.as_ref().unwrap(), + &primary_sort_key("fedora") + ); + assert_eq!(result[1].version(), "1.5.0".into()); // Entry 3 (version 1.5.0) + assert_eq!( + result[2].sort_key.as_ref().unwrap(), + &secondary_sort_key("fedora") + ); + assert_eq!(result[2].version(), "1.0.0".into()); // Entry 1 (version 1.0.0) + + // Test descending order + let result = get_sorted_type1_boot_entries_helper( + &tempdir, + false, + false, + crate::spec::Bootloader::Systemd, + )?; + + assert_eq!(result.len(), 3); + // With ascending=false, secondary sort-key should come before primary + assert_eq!( + result[0].sort_key.as_ref().unwrap(), + &secondary_sort_key("fedora") + ); + assert_eq!(result[0].version(), "1.0.0".into()); // Entry 1 + assert_eq!( + result[1].sort_key.as_ref().unwrap(), + &primary_sort_key("fedora") + ); + assert_eq!(result[1].version(), "1.5.0".into()); // Entry 3 + assert_eq!( + result[2].sort_key.as_ref().unwrap(), + &primary_sort_key("fedora") + ); + assert_eq!(result[2].version(), "2.0.0".into()); // Entry 2 + + Ok(()) + } + + #[test] + fn test_get_sorted_type1_boot_entries_helper_grub() -> Result<()> { + let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; + + // Create entries with sort-keys but GRUB ignores them and sorts by filename + let entry1 = format!( + r#" + title Fedora Linux (41.20251125.0) + version 41.20251125.0 + sort-key {} + linux /boot/vmlinuz + initrd /boot/initramfs.img + "#, + secondary_sort_key("fedora") + ); + + let entry2 = format!( + r#" + title Fedora Linux (42.20251125.0) + version 42.20251125.0 + sort-key {} + linux /boot/vmlinuz + initrd /boot/initramfs.img + "#, + primary_sort_key("fedora") + ); + + tempdir.create_dir_all("loader/entries")?; + + // Use realistic filenames - GRUB will sort by these, not sort-key + let filename1 = + type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY); + let filename2 = + type1_entry_conf_file_name("fedora", "42.20251125.0", FILENAME_PRIORITY_PRIMARY); + + tempdir.atomic_write(format!("loader/entries/{}", filename1), entry1)?; + tempdir.atomic_write(format!("loader/entries/{}", filename2), entry2)?; + + let result = get_sorted_type1_boot_entries_helper( + &tempdir, + true, + false, + crate::spec::Bootloader::Grub, + )?; + + assert_eq!(result.len(), 2); + // With ascending=true for GRUB, we reverse the default descending filename order + // Filenames: bootc_fedora-41.20251125.0-0.conf, bootc_fedora-42.20251125.0-1.conf + // Ascending filename order should be: 42-1, 41-0 + assert_eq!(result[0].version(), "42.20251125.0".into()); + assert_eq!(result[1].version(), "41.20251125.0".into()); + + // Test descending order (GRUB's default filename sorting) + let result = get_sorted_type1_boot_entries_helper( + &tempdir, + false, + false, + crate::spec::Bootloader::Grub, + )?; + + assert_eq!(result.len(), 2); + // With ascending=false for GRUB, filenames should be sorted in descending order + // Descending filename order should be: 42-1, 41-0 + assert_eq!(result[0].version(), "41.20251125.0".into()); + assert_eq!(result[1].version(), "42.20251125.0".into()); + + Ok(()) + } } diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index e82314629..041e96a44 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -1118,9 +1118,10 @@ fn write_reboot_required(image: &str) -> Result<()> { Ok(()) } +pub(crate) const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468"; + /// Implementation of rollback functionality pub(crate) async fn rollback(sysroot: &Storage) -> Result<()> { - const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468"; let ostree = sysroot.get_ostree()?; let (booted_ostree, deployments, host) = crate::status::get_status_require_booted(ostree)?; diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index b3366648b..8e87d2a3e 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -276,4 +276,11 @@ execute: how: fmf test: - /tmt/tests/tests/test-44-shadow-fixup + +/plan-45-composefs-corruped-state-resilience: + summary: Test composefs backend resilience to state corruption + discover: + how: fmf + test: + - /tmt/tests/tests/test-45-composefs-corruped-state-resilience # END GENERATED PLANS diff --git a/tmt/tests/booted/test-composefs-corruped-state-resilience.nu b/tmt/tests/booted/test-composefs-corruped-state-resilience.nu new file mode 100644 index 000000000..bafa0f0f3 --- /dev/null +++ b/tmt/tests/booted/test-composefs-corruped-state-resilience.nu @@ -0,0 +1,116 @@ +# number: 45 +# tmt: +# summary: Test composefs backend resilience to state corruption +# duration: 30m + +use std assert +use tap.nu + +if not (tap is_composefs) { + exit 0 +} + +let st = bootc status --json | from json +let booted = $st.status.booted.image +let is_uki = ($st.status.booted.composefs.bootType | str downcase) == "uki" +let is_grub = $st.status.booted.composefs.bootloader == "grub" + +# NOTE: Not testing for grub menuentries as that's niche case and we will +# remove it once we have https://github.com/bootc-dev/bootc/issues/2212 +if ($is_uki and $is_grub) { + exit 0 +} + +def first_boot [] { + bootc image copy-to-storage + + let bootloader = $st.status.booted.composefs.bootloader + let entries_dir = if ($bootloader | str downcase) == "systemd" { + mkdir /var/tmp/efi + mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi + "/var/tmp/efi/loader/entries" + } else { + "/sysroot/boot/loader/entries" + } + + let booted_verity = $st.status.booted.composefs.verity + + # Add some random entry in /boot/loader/entries to simulate + # https://github.com/bootc-dev/bootc/issues/2208 + systemd-run -p MountFlags=slave -qdPG -- /bin/sh -c $" + mount -orw,remount /sysroot + cd ($entries_dir) + cp * new-entry.conf + + sed -i 's;($booted_verity);bad-verity;' new-entry.conf + " + + # This should work but log a warning in journal + bootc status + + assert ( + journalctl F_MESSAGE_ID=d264f924dadb4c31bff0412107d391fb + | str contains $"No origin file for deployment bad-verity" + ) + + # Create a simple derived image to switch to + tap make_uki_containerfile $" + FROM localhost/bootc + RUN echo 'first-deployment' > /usr/share/deployment-marker + " | podman build -t localhost/bootc-test-1 . -f - + + bootc switch --transport containers-storage localhost/bootc-test-1 + + # Remove /run/composefs to simulate switch/update that failed midway + # and make sure switch still works + rm -rf /run/composefs + + assert ((bootc status --json | from json | get status.staged) == null) + + bootc switch --transport containers-storage localhost/bootc-test-1 + + tmt-reboot +} + +# Test the same thing but with an existing rollback deployment +def second_boot [] { + assert equal $booted.image.image "localhost/bootc-test-1" + + # Create another derived image + tap make_uki_containerfile $" + FROM localhost/bootc + RUN echo 'second-deployment' > /usr/share/deployment-marker + " | podman build -t localhost/bootc-test-2 . -f - + + bootc switch --transport containers-storage localhost/bootc-test-2 + + # Remove the origin file for staged deployment + # Make sure bootc status and switch still work + let staged_verity = (bootc status --json | from json).status.staged.composefs.verity + + systemd-run -p MountFlags=slave -qdPG -- /bin/sh -c $" + mount -orw,remount /sysroot + rm -rvf /sysroot/state/deploy/($staged_verity)/($staged_verity).origin + " + + # This should work but log a warning in journal + bootc status + + assert ( + journalctl F_MESSAGE_ID=d264f924dadb4c31bff0412107d391fb + | str contains $"No origin file for deployment ($staged_verity)" + ) + + # Switch again and it should work + bootc switch --transport containers-storage localhost/bootc-test-2 + + tap ok +} + +def main [] { + match $env.TMT_REBOOT_COUNT? { + null | "0" => first_boot, + "1" => second_boot, + $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, + } +} diff --git a/tmt/tests/booted/test-rollback.nu b/tmt/tests/booted/test-rollback.nu index 0afe377ac..fae0ab0e6 100644 --- a/tmt/tests/booted/test-rollback.nu +++ b/tmt/tests/booted/test-rollback.nu @@ -103,6 +103,31 @@ def third_boot_verify [] { def fourth_boot_verify [] { back_to_first_depl Fourth + + # Stage a new deployment, then rollback -> staged deployment should be removed + let dockerfile = $" + FROM localhost/bootc as base + RUN echo 'Second Stage' > /usr/share/second-stage + " + + (tap make_uki_containerfile $dockerfile) | podman build -t localhost/second-stage . -f - + + bootc switch --transport containers-storage localhost/second-stage + + assert ( + (bootc status --json | from json).status + | get staged + | is-not-empty + ) + + bootc rollback + + assert ( + (bootc status --json | from json).status + | get staged + | is-empty + ) + tap ok } diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index 9c6b41713..a8b5b91f8 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -173,3 +173,8 @@ check: summary: Test bootc-sysusers-shadow-sync removes orphaned gshadow entries before sysusers duration: 30m test: nu booted/test-44-shadow-fixup.nu + +/test-45-composefs-corruped-state-resilience: + summary: Test composefs backend resilience to state corruption + duration: 30m + test: nu booted/test-composefs-corruped-state-resilience.nu