From 6cfb4beda4fd0fe9abc0c7c4cef7842046aceb80 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 7 May 2025 10:56:43 +0530 Subject: [PATCH 01/21] cli/install: Add `composefs` option to `InstallToDiskOpts` Done to facilitate the installation of a composefs repository to disk Signed-off-by: Pragyan Poudyal --- Cargo.lock | 11 ++++++----- lib/src/install.rs | 3 +++ ostree-ext/Cargo.toml | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5ef21441..eec630ac8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,7 +518,7 @@ dependencies = [ [[package]] name = "composefs" version = "0.2.0" -source = "git+https://github.com/containers/composefs-rs?rev=821eeae93e48f1ee381c49b8cd4d22fda92d27a2#821eeae93e48f1ee381c49b8cd4d22fda92d27a2" +source = "git+https://github.com/containers/composefs-rs?rev=b1bd9c1d253c9b15bbf8e04068ad96edd974ccd0#b1bd9c1d253c9b15bbf8e04068ad96edd974ccd0" dependencies = [ "anyhow", "async-compression", @@ -529,6 +529,7 @@ dependencies = [ "indicatif", "log", "oci-spec", + "once_cell", "regex-automata 0.4.9", "rustix 1.0.3", "serde", @@ -558,9 +559,9 @@ dependencies = [ [[package]] name = "containers-image-proxy" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b4ec45d60513c498a40c69d89447d8bf91bbd17f71a32aa285b39e4dc03294" +checksum = "e366fb6e732b808c920cfdc758949a2f4d80445d413b040640aeb3d744cabcdb" dependencies = [ "cap-std-ext", "fn-error-context", @@ -1618,9 +1619,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssh-keys" diff --git a/lib/src/install.rs b/lib/src/install.rs index 395293c19..20b3d00ac 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -241,6 +241,9 @@ pub(crate) struct InstallToDiskOpts { #[clap(long)] #[serde(default)] pub(crate) via_loopback: bool, + + #[clap(long)] + pub(crate) composefs: bool, } #[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/ostree-ext/Cargo.toml b/ostree-ext/Cargo.toml index 6ba83425a..221f804f0 100644 --- a/ostree-ext/Cargo.toml +++ b/ostree-ext/Cargo.toml @@ -12,7 +12,7 @@ version = "0.15.3" # Note that we re-export the oci-spec types # that are exported by this crate, so when bumping # semver here you must also bump our semver. -containers-image-proxy = "0.7.0" +containers-image-proxy = "0.7.1" # We re-export this library too. ostree = { features = ["v2025_1"], version = "0.20.0" } @@ -20,7 +20,7 @@ ostree = { features = ["v2025_1"], version = "0.20.0" } anyhow = { workspace = true } bootc-utils = { path = "../utils" } camino = { workspace = true, features = ["serde1"] } -composefs = { git = "https://github.com/containers/composefs-rs", rev = "821eeae93e48f1ee381c49b8cd4d22fda92d27a2" } +composefs = { git = "https://github.com/containers/composefs-rs", rev = "b1bd9c1d253c9b15bbf8e04068ad96edd974ccd0" } chrono = { workspace = true } olpc-cjson = "0.1.1" clap = { workspace = true, features = ["derive","cargo"] } From d03feb283b54cf1c9605e7d14978c4958518738c Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 7 May 2025 11:31:59 +0530 Subject: [PATCH 02/21] cli: Handle compatibility with latest composefs-rs Signed-off-by: Pragyan Poudyal --- lib/src/cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 44919d312..a2e177ef0 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -17,7 +17,7 @@ use fn_error_context::context; use indoc::indoc; use ostree::gio; use ostree_container::store::PrepareResult; -use ostree_ext::composefs::fsverity; +use ostree_ext::composefs::fsverity::{self, FsVerityHashValue}; use ostree_ext::container as ostree_container; use ostree_ext::container_utils::ostree_booted; use ostree_ext::keyfileext::KeyFileExt; @@ -1199,7 +1199,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> { let fd = std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?; let digest: fsverity::Sha256HashValue = fsverity::measure_verity(&fd)?; - let digest = hex::encode(digest); + let digest = digest.to_hex(); println!("{digest}"); Ok(()) } From 6be556015f6c2928366d43b482e2331b941877f4 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 7 May 2025 11:33:01 +0530 Subject: [PATCH 03/21] install: Pull composefs repository Signed-off-by: Pragyan Poudyal --- Cargo.lock | 1 - lib/src/install.rs | 96 +++++++++++++++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eec630ac8..c5daf0c75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,7 +518,6 @@ dependencies = [ [[package]] name = "composefs" version = "0.2.0" -source = "git+https://github.com/containers/composefs-rs?rev=b1bd9c1d253c9b15bbf8e04068ad96edd974ccd0#b1bd9c1d253c9b15bbf8e04068ad96edd974ccd0" dependencies = [ "anyhow", "async-compression", diff --git a/lib/src/install.rs b/lib/src/install.rs index 20b3d00ac..9a19cb080 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -38,12 +38,20 @@ use chrono::prelude::*; use clap::ValueEnum; use fn_error_context::context; use ostree::gio; +use ostree_ext::composefs::{ + fsverity::{FsVerityHashValue, Sha256HashValue}, + oci::pull as composefs_oci_pull, + repository::Repository as ComposefsRepository, + util::Sha256Digest, +}; use ostree_ext::oci_spec; use ostree_ext::ostree; use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate}; use ostree_ext::prelude::Cast; use ostree_ext::sysroot::SysrootLock; -use ostree_ext::{container as ostree_container, ostree_prepareroot}; +use ostree_ext::{ + container as ostree_container, container::ImageReference as OstreeExtImgRef, ostree_prepareroot, +}; #[cfg(feature = "install-to-disk")] use rustix::fs::FileTypeExt; use rustix::fs::MetadataExt as _; @@ -366,6 +374,7 @@ pub(crate) struct SourceInfo { } // Shared read-only global state +#[derive(Debug)] pub(crate) struct State { pub(crate) source: SourceInfo, /// Force SELinux off in target system @@ -1428,10 +1437,32 @@ impl BoundImages { } } +async fn initialize_composefs_repository( + state: &State, + root_setup: &RootSetup, +) -> Result<(Sha256Digest, impl FsVerityHashValue)> { + let rootfs_dir = &root_setup.physical_root; + + rootfs_dir + .create_dir_all("composefs") + .context("Creating dir 'composefs'")?; + + tracing::warn!("STATE: {state:#?}"); + + let repo: ComposefsRepository = + ComposefsRepository::open_path(rootfs_dir, "composefs").expect("failed to open_path"); + + let OstreeExtImgRef { transport, name } = &state.target_imgref.imgref; + + // transport's display is already of type ":" + composefs_oci_pull(&Arc::new(repo), &format!("{transport}{name}",), None).await +} + async fn install_to_filesystem_impl( state: &State, rootfs: &mut RootSetup, cleanup: Cleanup, + composefs: bool, ) -> Result<()> { if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) { rootfs.kargs.push("selinux=0".to_string()); @@ -1460,34 +1491,45 @@ async fn install_to_filesystem_impl( let bound_images = BoundImages::from_state(state).await?; - // Initialize the ostree sysroot (repo, stateroot, etc.) + if composefs { + // Load a fd for the mounted target physical root + let (id, verity) = initialize_composefs_repository(state, rootfs).await?; - { - let (sysroot, has_ostree, imgstore) = initialize_ostree_root(state, rootfs).await?; - - install_with_sysroot( - state, - rootfs, - &sysroot, - &boot_uuid, - bound_images, - has_ostree, - &imgstore, - ) - .await?; + tracing::warn!( + "id = {id}, verity = {verity}", + id = hex::encode(id), + verity = verity.to_hex() + ); + } else { + // Initialize the ostree sysroot (repo, stateroot, etc.) + + { + let (sysroot, has_ostree, imgstore) = initialize_ostree_root(state, rootfs).await?; + + install_with_sysroot( + state, + rootfs, + &sysroot, + &boot_uuid, + bound_images, + has_ostree, + &imgstore, + ) + .await?; - if matches!(cleanup, Cleanup::TriggerOnNextBoot) { - let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?; - tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}"); - sysroot_dir.atomic_write(format!("etc/{}", DESTRUCTIVE_CLEANUP), b"")?; - } + if matches!(cleanup, Cleanup::TriggerOnNextBoot) { + let sysroot_dir = crate::utils::sysroot_dir(&sysroot)?; + tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}"); + sysroot_dir.atomic_write(format!("etc/{}", DESTRUCTIVE_CLEANUP), b"")?; + } - // We must drop the sysroot here in order to close any open file - // descriptors. - }; + // We must drop the sysroot here in order to close any open file + // descriptors. + }; - // Run this on every install as the penultimate step - install_finalize(&rootfs.physical_root_path).await?; + // Run this on every install as the penultimate step + install_finalize(&rootfs.physical_root_path).await?; + } // Finalize mounted filesystems if !rootfs.skip_finalize { @@ -1550,7 +1592,7 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { (rootfs, loopback_dev) }; - install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?; + install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip, opts.composefs).await?; // Drop all data about the root except the bits we need to ensure any file descriptors etc. are closed. let (root_path, luksdev) = rootfs.into_storage(); @@ -1936,7 +1978,7 @@ pub(crate) async fn install_to_filesystem( skip_finalize, }; - install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?; + install_to_filesystem_impl(&state, &mut rootfs, cleanup, false).await?; // Drop all data about the root except the path to ensure any file descriptors etc. are closed. drop(rootfs); From b3d19529c396ea8086a5fe05c97fedf15891e902 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 13 May 2025 12:10:03 +0530 Subject: [PATCH 04/21] install/composefs: Write boot entries Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index 9a19cb080..4c789ac39 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -14,6 +14,7 @@ mod osbuild; pub(crate) mod osconfig; use std::collections::HashMap; +use std::fs::create_dir_all; use std::io::Write; use std::os::fd::{AsFd, AsRawFd}; use std::os::unix::process::CommandExt; @@ -40,9 +41,11 @@ use fn_error_context::context; use ostree::gio; use ostree_ext::composefs::{ fsverity::{FsVerityHashValue, Sha256HashValue}, + oci::image::create_filesystem as create_composefs_filesystem, oci::pull as composefs_oci_pull, repository::Repository as ComposefsRepository, util::Sha256Digest, + write_boot::write_boot_simple as composefs_write_boot_simple, }; use ostree_ext::oci_spec; use ostree_ext::ostree; @@ -1437,6 +1440,11 @@ impl BoundImages { } } +fn open_composefs_repo(rootfs_dir: &Dir) -> Result> { + ComposefsRepository::open_path(rootfs_dir, "composefs") + .context("Failed to open composefs repository") +} + async fn initialize_composefs_repository( state: &State, root_setup: &RootSetup, @@ -1449,8 +1457,7 @@ async fn initialize_composefs_repository( tracing::warn!("STATE: {state:#?}"); - let repo: ComposefsRepository = - ComposefsRepository::open_path(rootfs_dir, "composefs").expect("failed to open_path"); + let repo = open_composefs_repo(rootfs_dir)?; let OstreeExtImgRef { transport, name } = &state.target_imgref.imgref; @@ -1458,6 +1465,72 @@ async fn initialize_composefs_repository( composefs_oci_pull(&Arc::new(repo), &format!("{transport}{name}",), None).await } +#[context("Setting up composefs boot")] +fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) -> Result<()> { + let boot_uuid = root_setup + .get_boot_uuid()? + .or(root_setup.rootfs_uuid.as_deref()) + .ok_or_else(|| anyhow!("No uuid for boot/root"))?; + + if cfg!(target_arch = "s390x") { + // TODO: Integrate s390x support into install_via_bootupd + crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?; + } else { + crate::bootloader::install_via_bootupd( + &root_setup.device_info, + &root_setup.physical_root_path, + &state.config_opts, + )?; + } + + let repo = open_composefs_repo(&root_setup.physical_root)?; + + let mut fs = create_composefs_filesystem(&repo, image_id, None)?; + + let entries = fs.transform_for_boot(&repo)?; + let id = fs.commit_image(&repo, None)?; + + println!("{entries:#?}"); + + let Some(entry) = entries.into_iter().next() else { + anyhow::bail!("No boot entries!"); + }; + + let rootfs_uuid = match &root_setup.rootfs_uuid { + Some(u) => u, + None => anyhow::bail!("Expected rootfs to have a UUID by now"), + }; + + let cmdline_refs = [ + "console=ttyS0,115200", + &format!("root=UUID={rootfs_uuid}"), + "rw", + ]; + + let boot_dir = root_setup.physical_root_path.join("boot"); + create_dir_all(&boot_dir).context("Failed to create boot dir")?; + + composefs_write_boot_simple( + &repo, + entry, + &id, + boot_dir.as_std_path(), + Some(&format!("{}", id.to_hex())), + Some("/boot"), + &cmdline_refs, + )?; + + let state_path = root_setup + .physical_root_path + .join(format!("state/{}", id.to_hex())); + + create_dir_all(state_path.join("var"))?; + create_dir_all(state_path.join("etc/upper"))?; + create_dir_all(state_path.join("etc/work"))?; + + Ok(()) +} + async fn install_to_filesystem_impl( state: &State, rootfs: &mut RootSetup, @@ -1500,6 +1573,8 @@ async fn install_to_filesystem_impl( id = hex::encode(id), verity = verity.to_hex() ); + + setup_composefs_boot(rootfs, state, &hex::encode(id))?; } else { // Initialize the ostree sysroot (repo, stateroot, etc.) From 83227f554733003621115755584dea6720baec9b Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 16 May 2025 10:27:49 +0530 Subject: [PATCH 05/21] Grub wip Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/install.rs b/lib/src/install.rs index 4c789ac39..0f3acbaa9 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1520,6 +1520,21 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - &cmdline_refs, )?; + // Add the user grug cfg + let grub_user_config = format!( +r#" +menuentry "Some Fedora Idk" {{ + insmod fat + insmod chain + search --no-floppy --set=root --fs-uuid {rootfs_uuid} + chainloader /boot/EFI/Linux/uki.efi +}} +"# + ); + + std::fs::write(boot_dir.join("grub2/user.cfg"), grub_user_config) + .context("Failed to write grub2/user.cfg")?; + let state_path = root_setup .physical_root_path .join(format!("state/{}", id.to_hex())); From 97b902cb638468b73d845dc1a8fd0e13a61e2eed Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 20 May 2025 10:18:59 +0530 Subject: [PATCH 06/21] Use discoverable partition specification for rootfs UUID Signed-off-by: Pragyan Poudyal --- lib/src/install/baseline.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/install/baseline.rs b/lib/src/install/baseline.rs index a09e78265..95041cdce 100644 --- a/lib/src/install/baseline.rs +++ b/lib/src/install/baseline.rs @@ -104,10 +104,15 @@ fn mkfs<'a>( label: &str, wipe: bool, opts: impl IntoIterator, + dps_uuid: Option, ) -> Result { let devinfo = bootc_blockdev::list_dev(dev.into())?; let size = ostree_ext::glib::format_size(devinfo.size); - let u = uuid::Uuid::new_v4(); + let u = if let Some(u) = dps_uuid { + u + } else { + uuid::Uuid::new_v4() + }; let mut t = Task::new( &format!("Creating {label} filesystem ({fs}) on device {dev} (size={size})"), format!("mkfs.{fs}"), @@ -383,6 +388,7 @@ pub(crate) fn install_create_rootfs( "boot", opts.wipe, [], + None, ) .context("Initializing /boot")?, ) @@ -403,6 +409,8 @@ pub(crate) fn install_create_rootfs( "root", opts.wipe, mkfs_options.iter().copied(), + // TODO: Add cli option for this + Some(uuid::uuid!("6523f8ae-3eb1-4e2a-a05a-18b695ae656f")), )?; let rootarg = format!("root=UUID={root_uuid}"); let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}")); From 151963e1627829483dd67d598747aa9fea16464f Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 20 May 2025 10:42:09 +0530 Subject: [PATCH 07/21] Bump composefs-rs Signed-off-by: Pragyan Poudyal --- Cargo.lock | 96 ++++++++++++++++++++----------------------- lib/src/install.rs | 15 ++++--- ostree-ext/Cargo.toml | 6 ++- ostree-ext/src/lib.rs | 2 + 4 files changed, 62 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5daf0c75..ef3a39eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,15 +98,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-compression" -version = "0.4.21" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "flate2", "futures-core", @@ -517,32 +517,53 @@ dependencies = [ [[package]] name = "composefs" -version = "0.2.0" +version = "0.3.0" +source = "git+https://github.com/containers/composefs-rs?rev=d8e285f14c3c9fa969496fd98e12ffba3079842d#d8e285f14c3c9fa969496fd98e12ffba3079842d" dependencies = [ "anyhow", - "async-compression", - "clap", - "containers-image-proxy", - "env_logger 0.11.6", "hex", - "indicatif", "log", - "oci-spec", "once_cell", - "regex-automata 0.4.9", "rustix 1.0.3", - "serde", "sha2", - "tar", "tempfile", "thiserror 2.0.12", "tokio", - "toml", "xxhash-rust", - "zerocopy 0.8.23", + "zerocopy 0.8.25", "zstd", ] +[[package]] +name = "composefs-boot" +version = "0.3.0" +source = "git+https://github.com/containers/composefs-rs?rev=d8e285f14c3c9fa969496fd98e12ffba3079842d#d8e285f14c3c9fa969496fd98e12ffba3079842d" +dependencies = [ + "anyhow", + "composefs", + "regex-automata 0.4.9", + "thiserror 2.0.12", + "zerocopy 0.8.25", +] + +[[package]] +name = "composefs-oci" +version = "0.3.0" +source = "git+https://github.com/containers/composefs-rs?rev=d8e285f14c3c9fa969496fd98e12ffba3079842d#d8e285f14c3c9fa969496fd98e12ffba3079842d" +dependencies = [ + "anyhow", + "async-compression", + "composefs", + "containers-image-proxy", + "hex", + "indicatif", + "oci-spec", + "rustix 1.0.3", + "sha2", + "tar", + "tokio", +] + [[package]] name = "console" version = "0.15.8" @@ -802,16 +823,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - [[package]] name = "env_home" version = "0.1.0" @@ -828,19 +839,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -1190,12 +1188,6 @@ dependencies = [ "digest", ] -[[package]] -name = "humantime" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" - [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1703,6 +1695,8 @@ dependencies = [ "clap_mangen", "comfy-table", "composefs", + "composefs-boot", + "composefs-oci", "containers-image-proxy", "flate2", "fn-error-context", @@ -1897,7 +1891,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger 0.8.4", + "env_logger", "log", "rand", ] @@ -3033,11 +3027,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "zerocopy-derive 0.8.23", + "zerocopy-derive 0.8.25", ] [[package]] @@ -3053,9 +3047,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", diff --git a/lib/src/install.rs b/lib/src/install.rs index 0f3acbaa9..daf166a17 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -41,11 +41,14 @@ use fn_error_context::context; use ostree::gio; use ostree_ext::composefs::{ fsverity::{FsVerityHashValue, Sha256HashValue}, - oci::image::create_filesystem as create_composefs_filesystem, - oci::pull as composefs_oci_pull, repository::Repository as ComposefsRepository, util::Sha256Digest, - write_boot::write_boot_simple as composefs_write_boot_simple, +}; +use ostree_ext::composefs_boot::{ + write_boot::write_boot_simple as composefs_write_boot_simple, BootOps, +}; +use ostree_ext::composefs_oci::{ + image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull, }; use ostree_ext::oci_spec; use ostree_ext::ostree; @@ -1521,9 +1524,11 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - )?; // Add the user grug cfg + // TODO: We don't need this for BLS. Have a flag for BLS vs UKI, or maybe we can figure it out + // via the boot entries above let grub_user_config = format!( -r#" -menuentry "Some Fedora Idk" {{ + r#" +menuentry "Some Fedora" {{ insmod fat insmod chain search --no-floppy --set=root --fs-uuid {rootfs_uuid} diff --git a/ostree-ext/Cargo.toml b/ostree-ext/Cargo.toml index 221f804f0..fc8925135 100644 --- a/ostree-ext/Cargo.toml +++ b/ostree-ext/Cargo.toml @@ -20,7 +20,11 @@ ostree = { features = ["v2025_1"], version = "0.20.0" } anyhow = { workspace = true } bootc-utils = { path = "../utils" } camino = { workspace = true, features = ["serde1"] } -composefs = { git = "https://github.com/containers/composefs-rs", rev = "b1bd9c1d253c9b15bbf8e04068ad96edd974ccd0" } + +composefs = { git = "https://github.com/containers/composefs-rs", rev = "d8e285f14c3c9fa969496fd98e12ffba3079842d", package = "composefs" } +composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "d8e285f14c3c9fa969496fd98e12ffba3079842d", package = "composefs-boot" } +composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "d8e285f14c3c9fa969496fd98e12ffba3079842d", package = "composefs-oci" } + chrono = { workspace = true } olpc-cjson = "0.1.1" clap = { workspace = true, features = ["derive","cargo"] } diff --git a/ostree-ext/src/lib.rs b/ostree-ext/src/lib.rs index 53f8267cc..06f231690 100644 --- a/ostree-ext/src/lib.rs +++ b/ostree-ext/src/lib.rs @@ -17,6 +17,8 @@ // "Dependencies are re-exported". Users will need e.g. `gio::File`, so this avoids // them needing to update matching versions. pub use composefs; +pub use composefs_boot; +pub use composefs_oci; pub use containers_image_proxy; pub use containers_image_proxy::oci_spec; pub use ostree; From 200d669d1ed10432b795e8962d0774a0e17a91f1 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 11 Jun 2025 10:15:32 +0530 Subject: [PATCH 08/21] install/composefs: Update composefs install options Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index daf166a17..fcdec683b 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -232,6 +232,18 @@ pub(crate) struct InstallConfigOpts { pub(crate) stateroot: Option, } +#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub(crate) enum BootType { + #[default] + Bls, + Uki, +} + +#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] +pub(crate) struct InstallComposefsOptions { + pub(crate) boot: BootType, +} + #[cfg(feature = "install-to-disk")] #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallToDiskOpts { @@ -257,7 +269,10 @@ pub(crate) struct InstallToDiskOpts { pub(crate) via_loopback: bool, #[clap(long)] - pub(crate) composefs: bool, + pub(crate) composefs_experimental: bool, + + #[clap(flatten)] + pub(crate) composefs_opts: InstallComposefsOptions, } #[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -541,6 +556,17 @@ impl FromStr for MountSpec { } } +impl InstallToDiskOpts { + pub(crate) fn validate(&self) { + if !self.composefs_experimental { + // Reject using --boot without --composefs + if self.composefs_opts.boot != BootType::default() { + panic!("--boot must not be provided without --composefs"); + } + } + } +} + impl InstallAleph { #[context("Creating aleph data")] pub(crate) fn new( @@ -1645,6 +1671,8 @@ fn installation_complete() { #[context("Installing to disk")] #[cfg(feature = "install-to-disk")] pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { + opts.validate(); + let mut block_opts = opts.block_opts; let target_blockdev_meta = block_opts .device @@ -1687,7 +1715,7 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { (rootfs, loopback_dev) }; - install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip, opts.composefs).await?; + install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip, opts.composefs_experimental).await?; // Drop all data about the root except the bits we need to ensure any file descriptors etc. are closed. let (root_path, luksdev) = rootfs.into_storage(); From dbcce3f8a9c288bbb020ce15081732b26f2c5943 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 11 Jun 2025 12:18:42 +0530 Subject: [PATCH 09/21] install/composefs: Introduce flag for bls/uki boot Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 155 +++++++++++++++++++++++++++++++-------------- 1 file changed, 108 insertions(+), 47 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index fcdec683b..4e2e7ed3d 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -45,7 +45,7 @@ use ostree_ext::composefs::{ util::Sha256Digest, }; use ostree_ext::composefs_boot::{ - write_boot::write_boot_simple as composefs_write_boot_simple, BootOps, + bootloader::BootEntry, write_boot::write_boot_simple as composefs_write_boot_simple, BootOps, }; use ostree_ext::composefs_oci::{ image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull, @@ -240,7 +240,8 @@ pub(crate) enum BootType { } #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] -pub(crate) struct InstallComposefsOptions { +pub(crate) struct InstallComposefsOpts { + #[clap(long, value_enum, default_value_t)] pub(crate) boot: BootType, } @@ -269,10 +270,10 @@ pub(crate) struct InstallToDiskOpts { pub(crate) via_loopback: bool, #[clap(long)] - pub(crate) composefs_experimental: bool, + pub(crate) composefs_native: bool, #[clap(flatten)] - pub(crate) composefs_opts: InstallComposefsOptions, + pub(crate) composefs_opts: InstallComposefsOpts, } #[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -413,6 +414,9 @@ pub(crate) struct State { /// The root filesystem of the running container pub(crate) container_root: Dir, pub(crate) tempdir: TempDir, + + // If Some, then --composefs_native is passed + pub(crate) composefs_options: Option, } impl State { @@ -558,7 +562,7 @@ impl FromStr for MountSpec { impl InstallToDiskOpts { pub(crate) fn validate(&self) { - if !self.composefs_experimental { + if !self.composefs_native { // Reject using --boot without --composefs if self.composefs_opts.boot != BootType::default() { panic!("--boot must not be provided without --composefs"); @@ -1226,6 +1230,7 @@ async fn prepare_install( config_opts: InstallConfigOpts, source_opts: InstallSourceOpts, target_opts: InstallTargetOpts, + composefs_opts: Option, ) -> Result> { tracing::trace!("Preparing install"); let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority()) @@ -1370,6 +1375,7 @@ async fn prepare_install( container_root: rootfs, tempdir, host_is_container, + composefs_options: composefs_opts, }); Ok(state) @@ -1494,37 +1500,14 @@ async fn initialize_composefs_repository( composefs_oci_pull(&Arc::new(repo), &format!("{transport}{name}",), None).await } -#[context("Setting up composefs boot")] -fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) -> Result<()> { - let boot_uuid = root_setup - .get_boot_uuid()? - .or(root_setup.rootfs_uuid.as_deref()) - .ok_or_else(|| anyhow!("No uuid for boot/root"))?; - - if cfg!(target_arch = "s390x") { - // TODO: Integrate s390x support into install_via_bootupd - crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?; - } else { - crate::bootloader::install_via_bootupd( - &root_setup.device_info, - &root_setup.physical_root_path, - &state.config_opts, - )?; - } - - let repo = open_composefs_repo(&root_setup.physical_root)?; - - let mut fs = create_composefs_filesystem(&repo, image_id, None)?; - - let entries = fs.transform_for_boot(&repo)?; - let id = fs.commit_image(&repo, None)?; - - println!("{entries:#?}"); - - let Some(entry) = entries.into_iter().next() else { - anyhow::bail!("No boot entries!"); - }; - +#[context("Setting up BLS boot")] +fn setup_composefs_bls_boot( + root_setup: &RootSetup, + // TODO: Make this generic + repo: ComposefsRepository, + id: &Sha256HashValue, + entry: BootEntry, +) -> Result<()> { let rootfs_uuid = match &root_setup.rootfs_uuid { Some(u) => u, None => anyhow::bail!("Expected rootfs to have a UUID by now"), @@ -1536,6 +1519,32 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - "rw", ]; + composefs_write_boot_simple( + &repo, + entry, + &id, + root_setup.physical_root_path.as_std_path(), // /run/mounts/bootc/boot + Some("boot"), + Some(&format!("{}", id.to_hex())), + &cmdline_refs, + )?; + + Ok(()) +} + +#[context("Setting up UKI boot")] +fn setup_composefs_uki_boot( + root_setup: &RootSetup, + // TODO: Make this generic + repo: ComposefsRepository, + id: &Sha256HashValue, + entry: BootEntry, +) -> Result<()> { + let rootfs_uuid = match &root_setup.rootfs_uuid { + Some(u) => u, + None => anyhow::bail!("Expected rootfs to have a UUID by now"), + }; + let boot_dir = root_setup.physical_root_path.join("boot"); create_dir_all(&boot_dir).context("Failed to create boot dir")?; @@ -1544,9 +1553,9 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - entry, &id, boot_dir.as_std_path(), + None, Some(&format!("{}", id.to_hex())), - Some("/boot"), - &cmdline_refs, + &[], )?; // Add the user grug cfg @@ -1554,18 +1563,61 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - // via the boot entries above let grub_user_config = format!( r#" -menuentry "Some Fedora" {{ +menuentry "Fedora Bootc UKI" {{ insmod fat insmod chain search --no-floppy --set=root --fs-uuid {rootfs_uuid} - chainloader /boot/EFI/Linux/uki.efi + chainloader /boot/EFI/Linux/{uki_id}.efi }} -"# +"#, uki_id=id.to_hex() ); std::fs::write(boot_dir.join("grub2/user.cfg"), grub_user_config) .context("Failed to write grub2/user.cfg")?; + Ok(()) +} + +#[context("Setting up composefs boot")] +fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) -> Result<()> { + let boot_uuid = root_setup + .get_boot_uuid()? + .or(root_setup.rootfs_uuid.as_deref()) + .ok_or_else(|| anyhow!("No uuid for boot/root"))?; + + if cfg!(target_arch = "s390x") { + // TODO: Integrate s390x support into install_via_bootupd + crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?; + } else { + crate::bootloader::install_via_bootupd( + &root_setup.device_info, + &root_setup.physical_root_path, + &state.config_opts, + )?; + } + + let repo = open_composefs_repo(&root_setup.physical_root)?; + + let mut fs = create_composefs_filesystem(&repo, image_id, None)?; + + let entries = fs.transform_for_boot(&repo)?; + let id = fs.commit_image(&repo, None)?; + + println!("{entries:#?}"); + + let Some(entry) = entries.into_iter().next() else { + anyhow::bail!("No boot entries!"); + }; + + let Some(composefs_opts) = &state.composefs_options else { + anyhow::bail!("Could not find options for composefs") + }; + + match composefs_opts.boot { + BootType::Bls => setup_composefs_bls_boot(root_setup, repo, &id, entry)?, + BootType::Uki => setup_composefs_uki_boot(root_setup, repo, &id, entry)?, + }; + let state_path = root_setup .physical_root_path .join(format!("state/{}", id.to_hex())); @@ -1581,7 +1633,6 @@ async fn install_to_filesystem_impl( state: &State, rootfs: &mut RootSetup, cleanup: Cleanup, - composefs: bool, ) -> Result<()> { if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) { rootfs.kargs.push("selinux=0".to_string()); @@ -1610,7 +1661,7 @@ async fn install_to_filesystem_impl( let bound_images = BoundImages::from_state(state).await?; - if composefs { + if state.composefs_options.is_some() { // Load a fd for the mounted target physical root let (id, verity) = initialize_composefs_repository(state, rootfs).await?; @@ -1694,7 +1745,17 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { } else if !target_blockdev_meta.file_type().is_block_device() { anyhow::bail!("Not a block device: {}", block_opts.device); } - let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?; + let state = prepare_install( + opts.config_opts, + opts.source_opts, + opts.target_opts, + if opts.composefs_native { + Some(opts.composefs_opts) + } else { + None + }, + ) + .await?; // This is all blocking stuff let (mut rootfs, loopback) = { @@ -1715,7 +1776,7 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { (rootfs, loopback_dev) }; - install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip, opts.composefs_experimental).await?; + install_to_filesystem_impl(&state, &mut rootfs, Cleanup::Skip).await?; // Drop all data about the root except the bits we need to ensure any file descriptors etc. are closed. let (root_path, luksdev) = rootfs.into_storage(); @@ -1902,7 +1963,7 @@ pub(crate) async fn install_to_filesystem( // IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT. // IMPORTANT: In practice, we should only be gathering information before this point, // IMPORTANT: and not performing any mutations at all. - let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?; + let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts, None).await?; // And the last bit of state here is the fsopts, which we also destructure now. let mut fsopts = opts.filesystem_opts; @@ -2101,7 +2162,7 @@ pub(crate) async fn install_to_filesystem( skip_finalize, }; - install_to_filesystem_impl(&state, &mut rootfs, cleanup, false).await?; + install_to_filesystem_impl(&state, &mut rootfs, cleanup).await?; // Drop all data about the root except the path to ensure any file descriptors etc. are closed. drop(rootfs); From 79f39573af8bab204930cad78654073a87e5dd38 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 11 Jun 2025 13:24:33 +0530 Subject: [PATCH 10/21] install/composefs: Get image and transport `source_imgref` Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index 4e2e7ed3d..388fdeec0 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1494,10 +1494,13 @@ async fn initialize_composefs_repository( let repo = open_composefs_repo(rootfs_dir)?; - let OstreeExtImgRef { transport, name } = &state.target_imgref.imgref; + let OstreeExtImgRef { + name: image_name, + transport, + } = &state.source.imageref; // transport's display is already of type ":" - composefs_oci_pull(&Arc::new(repo), &format!("{transport}{name}",), None).await + composefs_oci_pull(&Arc::new(repo), &format!("{transport}{image_name}",), None).await } #[context("Setting up BLS boot")] @@ -1569,7 +1572,8 @@ menuentry "Fedora Bootc UKI" {{ search --no-floppy --set=root --fs-uuid {rootfs_uuid} chainloader /boot/EFI/Linux/{uki_id}.efi }} -"#, uki_id=id.to_hex() +"#, + uki_id = id.to_hex() ); std::fs::write(boot_dir.join("grub2/user.cfg"), grub_user_config) From 42632cead557a6806b4d11b3f37b402071dd4a7f Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 17 Jun 2025 11:25:19 +0530 Subject: [PATCH 11/21] install/composefs: Return result from opts.validate Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index 388fdeec0..cb25d97f7 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -561,13 +561,15 @@ impl FromStr for MountSpec { } impl InstallToDiskOpts { - pub(crate) fn validate(&self) { + pub(crate) fn validate(&self) -> Result<()> { if !self.composefs_native { // Reject using --boot without --composefs if self.composefs_opts.boot != BootType::default() { - panic!("--boot must not be provided without --composefs"); + anyhow::bail!("--boot must not be provided without --composefs"); } } + + Ok(()) } } @@ -1726,7 +1728,7 @@ fn installation_complete() { #[context("Installing to disk")] #[cfg(feature = "install-to-disk")] pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> { - opts.validate(); + opts.validate()?; let mut block_opts = opts.block_opts; let target_blockdev_meta = block_opts From eac8e4dadb3a94635f3df6cd908934fea618fc8a Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 17 Jun 2025 17:12:16 +0530 Subject: [PATCH 12/21] install/composefs: Handle kargs for BLS Also exit early if kargs are passed for `--boot=uki` Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index cb25d97f7..45849c467 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -200,7 +200,7 @@ pub(crate) struct InstallConfigOpts { /// /// Example: --karg=nosmt --karg=console=ttyS0,114800n8 #[clap(long)] - karg: Option>, + pub(crate) karg: Option>, /// The path to an `authorized_keys` that will be injected into the `root` account. /// @@ -569,6 +569,11 @@ impl InstallToDiskOpts { } } + // Can't add kargs to UKI + if self.composefs_opts.boot == BootType::Uki && self.config_opts.karg.is_some() { + anyhow::bail!("Cannot pass kargs to UKI"); + } + Ok(()) } } @@ -1518,17 +1523,17 @@ fn setup_composefs_bls_boot( None => anyhow::bail!("Expected rootfs to have a UUID by now"), }; - let cmdline_refs = [ - "console=ttyS0,115200", - &format!("root=UUID={rootfs_uuid}"), - "rw", - ]; + let root_uuid_karg = format!("root=UUID={rootfs_uuid}"); + + let mut cmdline_refs = vec!["console=ttyS0,115200", &root_uuid_karg, "rw"]; + + cmdline_refs.extend(root_setup.kargs.iter().map(String::as_str)); composefs_write_boot_simple( &repo, entry, &id, - root_setup.physical_root_path.as_std_path(), // /run/mounts/bootc/boot + root_setup.physical_root_path.as_std_path(), Some("boot"), Some(&format!("{}", id.to_hex())), &cmdline_refs, @@ -1564,8 +1569,6 @@ fn setup_composefs_uki_boot( )?; // Add the user grug cfg - // TODO: We don't need this for BLS. Have a flag for BLS vs UKI, or maybe we can figure it out - // via the boot entries above let grub_user_config = format!( r#" menuentry "Fedora Bootc UKI" {{ From edcf7210c64eb5b4af04c3107a99277c9257557c Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 17 Jun 2025 22:26:47 +0530 Subject: [PATCH 13/21] install/composefs: Put UKI in the ESP Instead of putting the UKI in `/boot` in the root partition, this commit moves it to the ESP. Update grub user.cfg and pass the disk UUID to `--fs-uuid` Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 74 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index 45849c467..c5befc890 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -18,7 +18,7 @@ use std::fs::create_dir_all; use std::io::Write; use std::os::fd::{AsFd, AsRawFd}; use std::os::unix::process::CommandExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; use std::sync::Arc; @@ -107,6 +107,10 @@ const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[ /// Kernel argument used to specify we want the rootfs mounted read-write by default const RW_KARG: &str = "rw"; +/// The ESP partition label on Fedora CoreOS derivatives +const COREOS_ESP_PART_LABEL: &str = "EFI-SYSTEM"; +const ANACONDA_ESP_PART_LABEL: &str = "EFI\\x20System\\x20Partition"; + #[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallTargetOpts { // TODO: A size specifier which allocates free space for the root in *addition* to the base container image size @@ -1542,6 +1546,44 @@ fn setup_composefs_bls_boot( Ok(()) } +fn get_esp_device() -> Option { + let esp_devices = [COREOS_ESP_PART_LABEL, ANACONDA_ESP_PART_LABEL] + .into_iter() + .map(|p| Path::new("/dev/disk/by-partlabel/").join(p)); + let mut esp_device = None; + for path in esp_devices { + if path.exists() { + esp_device = Some(path); + break; + } + } + return esp_device; +} + +/// esp_device - /dev/disk/by-partlabel/ +fn get_esp_uuid(esp_device: &PathBuf) -> Result { + // not using blkid here as the output might change from under us + let resolved = std::fs::canonicalize(esp_device) + .with_context(|| format!("Failed to resolve link {esp_device:?}"))?; + + let mut uuid = String::new(); + + for dir_entry in std::fs::read_dir("/dev/disk/by-uuid")? { + let file = dir_entry?; + + let uuid_resolve = std::fs::canonicalize(file.path()) + .with_context(|| format!("Failed to resolve link {file:?}"))?; + + if resolved == uuid_resolve { + // SAFETY: UUID has to be [A-Fa-f0-9\-] + uuid = file.file_name().to_string_lossy().into_owned(); + break; + } + } + + Ok(uuid) +} + #[context("Setting up UKI boot")] fn setup_composefs_uki_boot( root_setup: &RootSetup, @@ -1550,35 +1592,47 @@ fn setup_composefs_uki_boot( id: &Sha256HashValue, entry: BootEntry, ) -> Result<()> { - let rootfs_uuid = match &root_setup.rootfs_uuid { - Some(u) => u, - None => anyhow::bail!("Expected rootfs to have a UUID by now"), + // Write the UKI to /EFI/Linux + let Some(esp_device) = get_esp_device() else { + anyhow::bail!("ESP device not found"); }; - let boot_dir = root_setup.physical_root_path.join("boot"); - create_dir_all(&boot_dir).context("Failed to create boot dir")?; + let mounted_esp: PathBuf = root_setup.physical_root_path.join("../esp").into(); + create_dir_all(&mounted_esp).context("Failed to create dir {mounted_esp:?}")?; + + Task::new("Mounting ESP", "mount") + .args([&esp_device, &mounted_esp.clone()]) + .run()?; composefs_write_boot_simple( &repo, entry, &id, - boot_dir.as_std_path(), + &mounted_esp, None, Some(&format!("{}", id.to_hex())), &[], )?; + Task::new("Unmounting ESP", "umount") + .arg(mounted_esp) + .run()?; + + let boot_dir = root_setup.physical_root_path.join("boot"); + create_dir_all(&boot_dir).context("Failed to create boot dir")?; + // Add the user grug cfg let grub_user_config = format!( r#" menuentry "Fedora Bootc UKI" {{ insmod fat insmod chain - search --no-floppy --set=root --fs-uuid {rootfs_uuid} - chainloader /boot/EFI/Linux/{uki_id}.efi + search --no-floppy --set=root --fs-uuid {esp_uuid} + chainloader /EFI/Linux/{uki_id}.efi }} "#, - uki_id = id.to_hex() + uki_id = id.to_hex(), + esp_uuid = get_esp_uuid(&esp_device)? ); std::fs::write(boot_dir.join("grub2/user.cfg"), grub_user_config) From af8fa4e6cfc21a3824149f324e2f544f26438d5b Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 20 Jun 2025 09:39:28 +0530 Subject: [PATCH 14/21] cli/composefs: Implement `status` cmd for compoesfs booted system This is currently WIP, as it only shows the currently booted image in use and not any staged or rollback deployments. Signed-off-by: Pragyan Poudyal --- lib/src/spec.rs | 12 ++++++ lib/src/status.rs | 71 +++++++++++++++++++++++++++++-- ostree-ext/src/container_utils.rs | 7 +++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/lib/src/spec.rs b/lib/src/spec.rs index 90d2e3975..84fe07793 100644 --- a/lib/src/spec.rs +++ b/lib/src/spec.rs @@ -162,6 +162,15 @@ pub struct BootEntryOstree { pub deploy_serial: u32, } + +/// A bootable entry +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct BootEntryComposefs { + /// The erofs verity + pub verity: String, +} + /// A bootable entry #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] @@ -179,6 +188,8 @@ pub struct BootEntry { pub store: Option, /// If this boot entry is ostree based, the corresponding state pub ostree: Option, + /// If this boot entry is composefs based, the corresponding state + pub composefs: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -514,6 +525,7 @@ mod tests { pinned: false, store: None, ostree: None, + composefs: None, } } diff --git a/lib/src/status.rs b/lib/src/status.rs index aa75b047a..e5d325648 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -9,6 +9,7 @@ use fn_error_context::context; use ostree::glib; use ostree_container::OstreeImageReference; use ostree_ext::container as ostree_container; +use ostree_ext::container_utils::composefs_booted; use ostree_ext::container_utils::ostree_booted; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::oci_spec; @@ -164,6 +165,7 @@ fn boot_entry_from_deployment( // SAFETY: The deployserial is really unsigned deploy_serial: deployment.deployserial().try_into().unwrap(), }), + composefs: None, }; Ok(r) } @@ -293,13 +295,53 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { 0 | 1 => {} o => anyhow::bail!("Unsupported format version: {o}"), }; - let mut host = if !ostree_booted()? { - Default::default() - } else { + let mut host = if ostree_booted()? { let sysroot = super::cli::get_storage().await?; let booted_deployment = sysroot.booted_deployment(); let (_deployments, host) = get_status(&sysroot, booted_deployment.as_ref())?; host + } else if composefs_booted()? { + let dir_contents = std::fs::read_dir("/sysroot/composefs/images")?; + + let host_spec = HostSpec { + image: Some(ImageReference { + image: "".into(), + transport: "".into(), + signature: None, + }), + boot_order: BootOrder::Default, + }; + + let mut host = Host::new(host_spec); + + let cmdline = crate::kernel::parse_cmdline()?; + let booted = cmdline.iter().find_map(|x| x.strip_prefix("composefs=")); + + let Some(booted) = booted else { + anyhow::bail!("Failed to find composefs parameter in kernel cmdline"); + }; + + host.status = HostStatus { + staged: None, + booted: Some(BootEntry { + image: None, + cached_update: None, + incompatible: false, + pinned: false, + store: None, + ostree: None, + composefs: Some(crate::spec::BootEntryComposefs { + verity: booted.into(), + }), + }), + rollback: None, + rollback_queued: false, + ty: None, + }; + + host + } else { + Default::default() }; // We could support querying the staged or rollback deployments @@ -434,6 +476,27 @@ fn human_render_slot_ostree( Ok(()) } +/// Output a rendering of a non-container composefs boot entry. +fn human_render_slot_composefs( + mut out: impl Write, + slot: Slot, + entry: &crate::spec::BootEntry, + erofs_verity: &str, +) -> Result<()> { + // TODO consider rendering more ostree stuff here like rpm-ostree status does + let prefix = match slot { + Slot::Staged => " Staged composefs".into(), + Slot::Booted => format!("{} Booted composefs", crate::glyph::Glyph::BlackCircle), + Slot::Rollback => " Rollback composefs".into(), + }; + let prefix_len = prefix.len(); + writeln!(out, "{prefix}")?; + write_row_name(&mut out, "Commit", prefix_len)?; + writeln!(out, "{erofs_verity}")?; + tracing::debug!("pinned={}", entry.pinned); + Ok(()) +} + fn human_readable_output_booted(mut out: impl Write, host: &Host) -> Result<()> { let mut first = true; for (slot_name, status) in [ @@ -451,6 +514,8 @@ fn human_readable_output_booted(mut out: impl Write, host: &Host) -> Result<()> human_render_slot(&mut out, slot_name, host_status, image)?; } else if let Some(ostree) = host_status.ostree.as_ref() { human_render_slot_ostree(&mut out, slot_name, host_status, &ostree.checksum)?; + } else if let Some(composefs) = &host_status.composefs { + human_render_slot_composefs(&mut out, slot_name, host_status, &composefs.verity)?; } else { writeln!(out, "Current {slot_name} state is unknown")?; } diff --git a/ostree-ext/src/container_utils.rs b/ostree-ext/src/container_utils.rs index 7737e986c..631a0c524 100644 --- a/ostree-ext/src/container_utils.rs +++ b/ostree-ext/src/container_utils.rs @@ -77,6 +77,13 @@ pub fn ostree_booted() -> io::Result { Path::new(&format!("/{OSTREE_BOOTED}")).try_exists() } + +/// Returns true if the system appears to have been booted with composefs. +pub fn composefs_booted() -> io::Result { + let cmdline = std::fs::read_to_string("/proc/cmdline")?; + Ok(cmdline.contains("composefs=")) +} + /// Returns true if the target root appears to have been booted via ostree. pub fn is_ostree_booted_in(rootfs: &Dir) -> io::Result { rootfs.try_exists(OSTREE_BOOTED) From 770a57b63b983231e8c6dcab0bfa2b9df6b84771 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 24 Jun 2025 11:55:56 +0530 Subject: [PATCH 15/21] install/composefs: Create origin file Create a .origin file in /sysroot/state to store image info Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/src/install.rs b/lib/src/install.rs index c5befc890..8d9b726cf 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1689,6 +1689,22 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - create_dir_all(state_path.join("etc/upper"))?; create_dir_all(state_path.join("etc/work"))?; + let OstreeExtImgRef { + name: image_name, + transport, + } = &state.source.imageref; + + let config = tini::Ini::new().section("origin").item( + ORIGIN_CONTAINER, + format!("ostree-unverified-image:{transport}{image_name}"), + ); + + let mut origin_file = std::fs::File::create(state_path.join(format!("{}.origin", id.to_hex()))) + .context("Failed to open .origin file")?; + origin_file + .write(config.to_string().as_bytes()) + .context("Falied to write to .origin file")?; + Ok(()) } From 8e68e45fb7c10a3a59373353dd14c8b7a5629f7f Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 26 Jun 2025 12:16:28 +0530 Subject: [PATCH 16/21] install/composefs: Move state to `/state/deploy/` Also create `/var` inside `/state/deploy/` as a symlink to `../../fedora/os/var` which is to be the case for every deployment Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index 8d9b726cf..abc62f0b1 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1683,12 +1683,20 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - let state_path = root_setup .physical_root_path - .join(format!("state/{}", id.to_hex())); + .join(format!("state/deploy/{}", id.to_hex())); - create_dir_all(state_path.join("var"))?; create_dir_all(state_path.join("etc/upper"))?; create_dir_all(state_path.join("etc/work"))?; + let actual_var_path = root_setup + .physical_root_path + .join(format!("state/os/fedora/var")); + + create_dir_all(&actual_var_path)?; + + symlink(Path::new("../../os/fedora/var"), state_path.join("var")) + .context("Failed to create symlink for /var")?; + let OstreeExtImgRef { name: image_name, transport, From 8d9b9232ec742df805dafdd0a1022d5386d9c812 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 26 Jun 2025 15:17:48 +0530 Subject: [PATCH 17/21] cli: Implement `bootc status` for composefs native booted system Signed-off-by: Pragyan Poudyal --- lib/src/status.rs | 180 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 141 insertions(+), 39 deletions(-) diff --git a/lib/src/status.rs b/lib/src/status.rs index e5d325648..33eecf70c 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -3,19 +3,25 @@ use std::collections::VecDeque; use std::io::IsTerminal; use std::io::Read; use std::io::Write; +use std::str::FromStr; use anyhow::{Context, Result}; +use cap_std_ext::cap_std; use fn_error_context::context; use ostree::glib; use ostree_container::OstreeImageReference; use ostree_ext::container as ostree_container; +use ostree_ext::container::deploy::ORIGIN_CONTAINER; use ostree_ext::container_utils::composefs_booted; use ostree_ext::container_utils::ostree_booted; +use ostree_ext::containers_image_proxy; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::oci_spec; use ostree_ext::ostree; +use tokio::io::AsyncReadExt; use crate::cli::OutputFormat; +use crate::spec::ImageStatus; use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType}; use crate::spec::{ImageReference, ImageSignature}; use crate::store::{CachedImageStatus, ContainerImageStore, Storage}; @@ -287,6 +293,140 @@ pub(crate) fn get_status( Ok((deployments, host)) } +#[context("Getting composefs deployment metadata")] +async fn boot_entry_from_composefs_deployment( + origin: tini::Ini, + booted: &String, +) -> Result { + let image = match origin.get::("origin", ORIGIN_CONTAINER) { + Some(img_name_from_config) => { + let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?; + let imgref = ostree_img_ref.imgref.to_string(); + + let img_ref = ImageReference::from(ostree_img_ref); + + let config = containers_image_proxy::ImageProxyConfig::default(); + let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?; + + // The image might've been removed, so don't error if we can't get the image manifest + let (image_digest, version, architecture, created_at) = + match proxy.open_image(&imgref).await.context("Opening image") { + Ok(img) => { + let (_, manifest) = proxy.fetch_manifest(&img).await?; + let (mut reader, driver) = + proxy.get_descriptor(&img, manifest.config()).await?; + + let mut buf = Vec::with_capacity(manifest.config().size() as usize); + buf.resize(manifest.config().size() as usize, 0); + reader.read_exact(&mut buf).await?; + driver.await?; + + let config: oci_spec::image::ImageConfiguration = + serde_json::from_slice(&buf)?; + + let digest = manifest.config().digest().to_string(); + let arch = config.architecture().to_string(); + let created = config.created().clone(); + let version = manifest + .annotations() + .as_ref() + .and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned()); + + (digest, version, arch, created) + } + + Err(e) => { + tracing::debug!("Failed to open image {img_ref}, because {e:?}"); + ("".into(), None, "".into(), None) + } + }; + + let timestamp = if let Some(created_at) = created_at { + try_deserialize_timestamp(&created_at) + } else { + None + }; + + let image_status = ImageStatus { + image: img_ref, + version, + timestamp, + image_digest, + architecture, + }; + + Some(image_status) + } + + // Wasn't booted using a container image. Do nothing + None => None, + }; + + let e = BootEntry { + image, + cached_update: None, + incompatible: false, + pinned: false, + store: None, + ostree: None, + composefs: Some(crate::spec::BootEntryComposefs { + verity: booted.into(), + }), + }; + + return Ok(e); +} + +#[context("Getting composefs deployment status")] +pub(crate) async fn composefs_deployment_status() -> Result { + let cmdline = crate::kernel::parse_cmdline()?; + let booted = cmdline.iter().find_map(|x| x.strip_prefix("composefs=")); + + let Some(booted) = booted else { + anyhow::bail!("Failed to find composefs parameter in kernel cmdline"); + }; + + let sysroot = cap_std::fs::Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority()) + .context("Opening sysroot")?; + let deployments = sysroot + .read_dir("state/deploy") + .context("Reading sysroot state/deploy")?; + + let host_spec = HostSpec { + image: None, + boot_order: BootOrder::Default, + }; + + let mut host = Host::new(host_spec); + + for depl in deployments { + let depl = depl?; + + let depl_file_name = depl.file_name(); + let depl_file_name = depl_file_name.to_string_lossy(); + + // read the origin file + let config = depl + .open_dir() + .with_context(|| format!("Failed to open {depl_file_name}"))? + .read_to_string(format!("{depl_file_name}.origin")) + .with_context(|| format!("Reading file {depl_file_name}.origin"))?; + + let ini = tini::Ini::from_string(&config) + .with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?; + + let boot_entry = boot_entry_from_composefs_deployment(ini, &booted.into()).await?; + + if depl.file_name() == booted { + host.spec.image = boot_entry.image.as_ref().map(|x| x.image.clone()); + host.status.booted = Some(boot_entry); + continue; + } + } + + Ok(host) +} + /// Implementation of the `bootc status` CLI command. #[context("Status")] pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { @@ -301,45 +441,7 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> { let (_deployments, host) = get_status(&sysroot, booted_deployment.as_ref())?; host } else if composefs_booted()? { - let dir_contents = std::fs::read_dir("/sysroot/composefs/images")?; - - let host_spec = HostSpec { - image: Some(ImageReference { - image: "".into(), - transport: "".into(), - signature: None, - }), - boot_order: BootOrder::Default, - }; - - let mut host = Host::new(host_spec); - - let cmdline = crate::kernel::parse_cmdline()?; - let booted = cmdline.iter().find_map(|x| x.strip_prefix("composefs=")); - - let Some(booted) = booted else { - anyhow::bail!("Failed to find composefs parameter in kernel cmdline"); - }; - - host.status = HostStatus { - staged: None, - booted: Some(BootEntry { - image: None, - cached_update: None, - incompatible: false, - pinned: false, - store: None, - ostree: None, - composefs: Some(crate::spec::BootEntryComposefs { - verity: booted.into(), - }), - }), - rollback: None, - rollback_queued: false, - ty: None, - }; - - host + composefs_deployment_status().await? } else { Default::default() }; From 4383a35c76a06f986855ca87c41a0f2584834c35 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 30 Jun 2025 17:43:26 +0530 Subject: [PATCH 18/21] install/composefs: Implement `bootc upgrade` * Implement `bootc upgrade` command for a composefs-native booted system * Refactor UKI and BLS boot entry writing functions to work for upgrade * Prevent using global `/dev/disk/by-partuuid` to get UUID for ESP * Refactor out code for populating `/sysroot/state` Signed-off-by: Pragyan Poudyal --- lib/src/cli.rs | 89 ++++++++++- lib/src/install.rs | 294 +++++++++++++++++++++++++----------- lib/src/install/baseline.rs | 3 +- 3 files changed, 291 insertions(+), 95 deletions(-) diff --git a/lib/src/cli.rs b/lib/src/cli.rs index a2e177ef0..cafa616fe 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -6,6 +6,7 @@ use std::ffi::{CString, OsStr, OsString}; use std::io::Seek; use std::os::unix::process::CommandExt; use std::process::Command; +use std::sync::Arc; use anyhow::{ensure, Context, Result}; use camino::Utf8PathBuf; @@ -19,19 +20,29 @@ use ostree::gio; use ostree_container::store::PrepareResult; use ostree_ext::composefs::fsverity::{self, FsVerityHashValue}; use ostree_ext::container as ostree_container; -use ostree_ext::container_utils::ostree_booted; +use ostree_ext::container_utils::{composefs_booted, ostree_booted}; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; use schemars::schema_for; use serde::{Deserialize, Serialize}; use crate::deploy::RequiredHostSpec; +use crate::install::{ + open_composefs_repo, setup_composefs_bls_boot, setup_composefs_uki_boot, write_composefs_state, + BootType, BootSetupType, +}; use crate::lints; use crate::progress_jsonl::{ProgressWriter, RawProgressFd}; use crate::spec::Host; use crate::spec::ImageReference; +use crate::status::composefs_deployment_status; use crate::utils::sigpolicy_from_opt; +use ostree_ext::composefs_boot::BootOps; +use ostree_ext::composefs_oci::{ + image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull, +}; + /// Shared progress options #[derive(Debug, Parser, PartialEq, Eq)] pub(crate) struct ProgressOptions { @@ -747,6 +758,74 @@ fn prepare_for_write() -> Result<()> { Ok(()) } +#[context("Upgrading composefs")] +async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { + // TODO: IMPORTANT Have all the checks here that `bootc upgrade` has for an ostree booted system + + let host = composefs_deployment_status() + .await + .context("Getting composefs deployment status")?; + + // TODO: IMPORTANT We need to check if any deployment is staged and get the image from that + let imgref = host + .spec + .image + .as_ref() + .ok_or_else(|| anyhow::anyhow!("No image source specified"))?; + + let booted_image = host + .status + .booted + .ok_or(anyhow::anyhow!("Could not find booted image"))? + .image + .ok_or(anyhow::anyhow!("Could not find booted image"))?; + + tracing::debug!("booted_image: {booted_image:#?}"); + tracing::debug!("imgref: {imgref:#?}"); + + let digest = booted_image + .digest() + .context("Getting digest for booted image")?; + + let rootfs_dir = cap_std::fs::Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())?; + + let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?; + + let (id, verity) = composefs_oci_pull( + &Arc::new(repo), + &format!("{}:{}", imgref.transport, imgref.image), + None, + ) + .await + .context("Pulling composefs repo")?; + + tracing::debug!( + "id = {id}, verity = {verity}", + id = hex::encode(id), + verity = verity.to_hex() + ); + + let repo = open_composefs_repo(&rootfs_dir)?; + let mut fs = create_composefs_filesystem(&repo, digest.digest(), None) + .context("Failed to create composefs filesystem")?; + + let entries = fs.transform_for_boot(&repo)?; + let id = fs.commit_image(&repo, None)?; + + let Some(entry) = entries.into_iter().next() else { + anyhow::bail!("No boot entries!"); + }; + + match BootType::from(&entry) { + BootType::Bls => setup_composefs_bls_boot(BootSetupType::Upgrade, repo, &id, entry), + BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry), + }?; + + write_composefs_state(&Utf8PathBuf::from("/sysroot"), id, imgref)?; + + Ok(()) +} + /// Implementation of the `bootc upgrade` CLI command. #[context("Upgrading")] async fn upgrade(opts: UpgradeOpts) -> Result<()> { @@ -1084,7 +1163,13 @@ impl Opt { async fn run_from_opt(opt: Opt) -> Result<()> { let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?; match opt { - Opt::Upgrade(opts) => upgrade(opts).await, + Opt::Upgrade(opts) => { + if composefs_booted()? { + upgrade_composefs(opts).await + } else { + upgrade(opts).await + } + } Opt::Switch(opts) => switch(opts).await, Opt::Rollback(opts) => rollback(opts).await, Opt::Edit(opts) => edit(opts).await, diff --git a/lib/src/install.rs b/lib/src/install.rs index abc62f0b1..27826b1e9 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -15,8 +15,9 @@ pub(crate) mod osconfig; use std::collections::HashMap; use std::fs::create_dir_all; -use std::io::Write; +use std::io::{Seek, SeekFrom, Write}; use std::os::fd::{AsFd, AsRawFd}; +use std::os::unix::fs::symlink; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::Command; @@ -25,6 +26,8 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, ensure, Context, Result}; +use baseline::{DPS_UUID, ESP_GUID}; +use bootc_blockdev::{find_parent_devices, PartitionTable}; use bootc_utils::CommandRunExt; use camino::Utf8Path; use camino::Utf8PathBuf; @@ -50,6 +53,7 @@ use ostree_ext::composefs_boot::{ use ostree_ext::composefs_oci::{ image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull, }; +use ostree_ext::container::deploy::ORIGIN_CONTAINER; use ostree_ext::oci_spec; use ostree_ext::ostree; use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate}; @@ -74,7 +78,7 @@ use crate::spec::ImageReference; use crate::store::Storage; use crate::task::Task; use crate::utils::sigpolicy_from_opt; -use bootc_mount::Filesystem; +use bootc_mount::{inspect_filesystem, Filesystem}; /// The toplevel boot directory const BOOT: &str = "boot"; @@ -107,10 +111,6 @@ const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[ /// Kernel argument used to specify we want the rootfs mounted read-write by default const RW_KARG: &str = "rw"; -/// The ESP partition label on Fedora CoreOS derivatives -const COREOS_ESP_PART_LABEL: &str = "EFI-SYSTEM"; -const ANACONDA_ESP_PART_LABEL: &str = "EFI\\x20System\\x20Partition"; - #[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallTargetOpts { // TODO: A size specifier which allocates free space for the root in *addition* to the base container image size @@ -243,6 +243,17 @@ pub(crate) enum BootType { Uki, } +impl From<&BootEntry> for BootType { + fn from(entry: &BootEntry) -> Self { + match entry { + BootEntry::Type1(..) => Self::Bls, + BootEntry::Type2(..) => Self::Uki, + BootEntry::UsrLibModulesUki(..) => Self::Uki, + BootEntry::UsrLibModulesVmLinuz(..) => Self::Bls, + } + } +} + #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallComposefsOpts { #[clap(long, value_enum, default_value_t)] @@ -1486,7 +1497,9 @@ impl BoundImages { } } -fn open_composefs_repo(rootfs_dir: &Dir) -> Result> { +pub(crate) fn open_composefs_repo( + rootfs_dir: &Dir, +) -> Result> { ComposefsRepository::open_path(rootfs_dir, "composefs") .context("Failed to open composefs repository") } @@ -1501,8 +1514,6 @@ async fn initialize_composefs_repository( .create_dir_all("composefs") .context("Creating dir 'composefs'")?; - tracing::warn!("STATE: {state:#?}"); - let repo = open_composefs_repo(rootfs_dir)?; let OstreeExtImgRef { @@ -1511,132 +1522,211 @@ async fn initialize_composefs_repository( } = &state.source.imageref; // transport's display is already of type ":" - composefs_oci_pull(&Arc::new(repo), &format!("{transport}{image_name}",), None).await + composefs_oci_pull(&Arc::new(repo), &format!("{transport}{image_name}"), None).await +} + +pub(crate) enum BootSetupType<'a> { + /// For initial setup, i.e. install to-disk + Setup(&'a RootSetup), + /// For `bootc upgrade` + Upgrade, } #[context("Setting up BLS boot")] -fn setup_composefs_bls_boot( - root_setup: &RootSetup, +pub(crate) fn setup_composefs_bls_boot( + setup_type: BootSetupType, // TODO: Make this generic repo: ComposefsRepository, id: &Sha256HashValue, entry: BootEntry, ) -> Result<()> { - let rootfs_uuid = match &root_setup.rootfs_uuid { - Some(u) => u, - None => anyhow::bail!("Expected rootfs to have a UUID by now"), - }; - - let root_uuid_karg = format!("root=UUID={rootfs_uuid}"); + let (root_path, cmdline_refs) = match setup_type { + BootSetupType::Setup(root_setup) => { + // root_setup.kargs has [root=UUID=, "rw"] + (root_setup.physical_root_path.clone(), &root_setup.kargs) + } - let mut cmdline_refs = vec!["console=ttyS0,115200", &root_uuid_karg, "rw"]; + BootSetupType::Upgrade => ( + Utf8PathBuf::from("/sysroot"), + &vec![format!("root=UUID={DPS_UUID}"), RW_KARG.to_string()], + ), + }; - cmdline_refs.extend(root_setup.kargs.iter().map(String::as_str)); + let str_slice = cmdline_refs + .iter() + .map(|x| x.as_str()) + .collect::>(); composefs_write_boot_simple( &repo, entry, &id, - root_setup.physical_root_path.as_std_path(), + false, + root_path.as_std_path(), Some("boot"), - Some(&format!("{}", id.to_hex())), - &cmdline_refs, - )?; - - Ok(()) + Some(&id.to_hex()), + &str_slice, + ) } -fn get_esp_device() -> Option { - let esp_devices = [COREOS_ESP_PART_LABEL, ANACONDA_ESP_PART_LABEL] +pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { + let device_info: PartitionTable = bootc_blockdev::partitions_of(Utf8Path::new(device))?; + let esp = device_info + .partitions .into_iter() - .map(|p| Path::new("/dev/disk/by-partlabel/").join(p)); - let mut esp_device = None; - for path in esp_devices { - if path.exists() { - esp_device = Some(path); - break; - } - } - return esp_device; -} - -/// esp_device - /dev/disk/by-partlabel/ -fn get_esp_uuid(esp_device: &PathBuf) -> Result { - // not using blkid here as the output might change from under us - let resolved = std::fs::canonicalize(esp_device) - .with_context(|| format!("Failed to resolve link {esp_device:?}"))?; + .find(|p| p.parttype.as_str() == ESP_GUID) + .ok_or(anyhow::anyhow!("ESP not found for device: {device}"))?; - let mut uuid = String::new(); - - for dir_entry in std::fs::read_dir("/dev/disk/by-uuid")? { - let file = dir_entry?; - - let uuid_resolve = std::fs::canonicalize(file.path()) - .with_context(|| format!("Failed to resolve link {file:?}"))?; - - if resolved == uuid_resolve { - // SAFETY: UUID has to be [A-Fa-f0-9\-] - uuid = file.file_name().to_string_lossy().into_owned(); - break; - } - } - - Ok(uuid) + Ok((esp.node, esp.uuid)) } #[context("Setting up UKI boot")] -fn setup_composefs_uki_boot( - root_setup: &RootSetup, +pub(crate) fn setup_composefs_uki_boot( + setup_type: BootSetupType, // TODO: Make this generic repo: ComposefsRepository, id: &Sha256HashValue, entry: BootEntry, ) -> Result<()> { - // Write the UKI to /EFI/Linux - let Some(esp_device) = get_esp_device() else { - anyhow::bail!("ESP device not found"); + let (root_path, esp_device) = match setup_type { + BootSetupType::Setup(root_setup) => { + let esp_part = root_setup + .device_info + .partitions + .iter() + .find(|p| p.parttype.as_str() == ESP_GUID) + .ok_or_else(|| anyhow!("ESP partition not found"))?; + + (root_setup.physical_root_path.clone(), esp_part.node.clone()) + } + + BootSetupType::Upgrade => { + let sysroot = Utf8PathBuf::from("/sysroot"); + + let fsinfo = inspect_filesystem(&sysroot)?; + let parent_devices = find_parent_devices(&fsinfo.source)?; + + let Some(parent) = parent_devices.into_iter().next() else { + anyhow::bail!("Could not find parent device for mountpoint /sysroot"); + }; + + (sysroot, get_esp_partition(&parent)?.0) + } }; - let mounted_esp: PathBuf = root_setup.physical_root_path.join("../esp").into(); + let mounted_esp: PathBuf = root_path.join("esp").into(); + let esp_mount_point_existed = mounted_esp.exists(); + create_dir_all(&mounted_esp).context("Failed to create dir {mounted_esp:?}")?; Task::new("Mounting ESP", "mount") - .args([&esp_device, &mounted_esp.clone()]) + .args([&PathBuf::from(&esp_device), &mounted_esp.clone()]) .run()?; composefs_write_boot_simple( &repo, entry, &id, + false, &mounted_esp, None, - Some(&format!("{}", id.to_hex())), + Some(&id.to_hex()), &[], )?; Task::new("Unmounting ESP", "umount") - .arg(mounted_esp) + .arg(&mounted_esp) .run()?; - let boot_dir = root_setup.physical_root_path.join("boot"); + if !esp_mount_point_existed { + // This shouldn't be a fatal error + if let Err(e) = std::fs::remove_dir(&mounted_esp) { + tracing::error!("Failed to remove mount point '{mounted_esp:?}': {e}"); + } + } + + let boot_dir = root_path.join("boot"); create_dir_all(&boot_dir).context("Failed to create boot dir")?; + let is_upgrade = matches!(setup_type, BootSetupType::Upgrade); + // Add the user grug cfg let grub_user_config = format!( r#" -menuentry "Fedora Bootc UKI" {{ +menuentry "Fedora Bootc UKI: ({uki_id})" {{ insmod fat insmod chain - search --no-floppy --set=root --fs-uuid {esp_uuid} + search --no-floppy --set=root --fs-uuid "${{EFI_PART_UUID}}" chainloader /EFI/Linux/{uki_id}.efi }} "#, uki_id = id.to_hex(), - esp_uuid = get_esp_uuid(&esp_device)? ); - std::fs::write(boot_dir.join("grub2/user.cfg"), grub_user_config) - .context("Failed to write grub2/user.cfg")?; + let user_cfg_name = "grub2/user.cfg"; + let user_cfg_path = boot_dir.join(user_cfg_name); + + // TODO: Figure out a better way to sort these menuentries. This is a bit scuffed + // + // Read the current user.cfg, split at the first menuentry, then stick the new menuentry in + // between the efiuuid.cfg sourcing code and the previous menuentry + if is_upgrade { + let contents = + std::fs::read_to_string(&user_cfg_path).context(format!("Reading {user_cfg_name}"))?; + + let mut usr_cfg = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(user_cfg_path) + .with_context(|| format!("Opening {user_cfg_name}"))?; + + let Some((before, after)) = contents.split_once("menuentry") else { + anyhow::bail!("Did not find menuentry in {user_cfg_name}") + }; + + usr_cfg + .seek(SeekFrom::Start(0)) + .with_context(|| format!("Seek {user_cfg_name}"))?; + + usr_cfg + .write_all(format!("{before} {grub_user_config}\nmenuentry {after}").as_bytes()) + .with_context(|| format!("Writing to {user_cfg_name}"))?; + + return Ok(()); + } + + // Open grub2/efiuuid.cfg and write the EFI partition UUID in there + // This will be sourced by grub2/user.cfg to be used for `--fs-uuid` + let mut efi_uuid_file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(boot_dir.join("grub2/efiuuid.cfg")) + .context("Opening grub2/efiuuid.cfg")?; + + let esp_uuid = Task::new("blkid for ESP UUID", "blkid") + .args(["-s", "UUID", "-o", "value", &esp_device]) + .read()?; + + efi_uuid_file + .write_all(format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes()) + .context("Writing to grub2/efiuuid.cfg")?; + + // Write to grub2/user.cfg + let mut usr_cfg = std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(user_cfg_path) + .with_context(|| format!("Opening {user_cfg_name}"))?; + + let efi_uuid_source = r#" +if [ -f ${config_directory}/efiuuid.cfg ]; then + source ${config_directory}/efiuuid.cfg +fi +"#; + + usr_cfg + .write_all(format!("{efi_uuid_source}\n{grub_user_config}").as_bytes()) + .with_context(|| format!("Writing to {user_cfg_name}"))?; Ok(()) } @@ -1666,8 +1756,6 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - let entries = fs.transform_for_boot(&repo)?; let id = fs.commit_image(&repo, None)?; - println!("{entries:#?}"); - let Some(entry) = entries.into_iter().next() else { anyhow::bail!("No boot entries!"); }; @@ -1677,38 +1765,60 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - }; match composefs_opts.boot { - BootType::Bls => setup_composefs_bls_boot(root_setup, repo, &id, entry)?, - BootType::Uki => setup_composefs_uki_boot(root_setup, repo, &id, entry)?, + BootType::Bls => { + setup_composefs_bls_boot(BootSetupType::Setup(&root_setup), repo, &id, entry)? + } + BootType::Uki => { + setup_composefs_uki_boot(BootSetupType::Setup(&root_setup), repo, &id, entry)? + } }; - let state_path = root_setup - .physical_root_path - .join(format!("state/deploy/{}", id.to_hex())); + write_composefs_state( + &root_setup.physical_root_path, + id, + &ImageReference { + image: state.source.imageref.name.clone(), + transport: state.source.imageref.transport.to_string(), + signature: None, + }, + )?; + + Ok(()) +} + +/// Creates and populates /sysroot/state/deploy/image_id +#[context("Writing composefs state")] +pub(crate) fn write_composefs_state( + root_path: &Utf8PathBuf, + deployment_id: Sha256HashValue, + imgref: &ImageReference, +) -> Result<()> { + let state_path = root_path.join(format!("state/deploy/{}", deployment_id.to_hex())); create_dir_all(state_path.join("etc/upper"))?; create_dir_all(state_path.join("etc/work"))?; - let actual_var_path = root_setup - .physical_root_path - .join(format!("state/os/fedora/var")); - + let actual_var_path = root_path.join(format!("state/os/fedora/var")); create_dir_all(&actual_var_path)?; symlink(Path::new("../../os/fedora/var"), state_path.join("var")) .context("Failed to create symlink for /var")?; - let OstreeExtImgRef { - name: image_name, + let ImageReference { + image: image_name, transport, - } = &state.source.imageref; + .. + } = &imgref; let config = tini::Ini::new().section("origin").item( ORIGIN_CONTAINER, - format!("ostree-unverified-image:{transport}{image_name}"), + format!("ostree-unverified-image:{transport}:{image_name}"), ); - let mut origin_file = std::fs::File::create(state_path.join(format!("{}.origin", id.to_hex()))) - .context("Failed to open .origin file")?; + let mut origin_file = + std::fs::File::create(state_path.join(format!("{}.origin", deployment_id.to_hex()))) + .context("Failed to open .origin file")?; + origin_file .write(config.to_string().as_bytes()) .context("Falied to write to .origin file")?; diff --git a/lib/src/install/baseline.rs b/lib/src/install/baseline.rs index 95041cdce..505347f4c 100644 --- a/lib/src/install/baseline.rs +++ b/lib/src/install/baseline.rs @@ -42,6 +42,7 @@ pub(crate) const PREPBOOT_GUID: &str = "9E1A2D38-C612-4316-AA26-8B49521E5A8B"; pub(crate) const PREPBOOT_LABEL: &str = "PowerPC-PReP-boot"; #[cfg(feature = "install-to-disk")] pub(crate) const ESP_GUID: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"; +pub(crate) const DPS_UUID: &str = "6523F8AE-3EB1-4E2A-A05A-18B695AE656F"; #[derive(clap::ValueEnum, Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -410,7 +411,7 @@ pub(crate) fn install_create_rootfs( opts.wipe, mkfs_options.iter().copied(), // TODO: Add cli option for this - Some(uuid::uuid!("6523f8ae-3eb1-4e2a-a05a-18b695ae656f")), + Some(uuid::uuid!(DPS_UUID)), )?; let rootarg = format!("root=UUID={root_uuid}"); let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}")); From 3c7a2e611f7c36f32b93ca050f4165810ce7ef99 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 1 Jul 2025 16:45:27 +0530 Subject: [PATCH 19/21] cli/composefs: Show staged/rollback deployments Store the staged deployment at `/run/composefs/staged-deployment` similar to how it's done in ostree Signed-off-by: Pragyan Poudyal --- lib/src/cli.rs | 2 +- lib/src/install.rs | 24 ++++++++++++++++++++++-- lib/src/status.rs | 25 +++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/src/cli.rs b/lib/src/cli.rs index cafa616fe..691f6d264 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -821,7 +821,7 @@ async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry), }?; - write_composefs_state(&Utf8PathBuf::from("/sysroot"), id, imgref)?; + write_composefs_state(&Utf8PathBuf::from("/sysroot"), id, imgref, true)?; Ok(()) } diff --git a/lib/src/install.rs b/lib/src/install.rs index 27826b1e9..80664ffff 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -15,7 +15,7 @@ pub(crate) mod osconfig; use std::collections::HashMap; use std::fs::create_dir_all; -use std::io::{Seek, SeekFrom, Write}; +use std::io::Write; use std::os::fd::{AsFd, AsRawFd}; use std::os::unix::fs::symlink; use std::os::unix::process::CommandExt; @@ -1781,19 +1781,26 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) - transport: state.source.imageref.transport.to_string(), signature: None, }, + false, )?; Ok(()) } +pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; +pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_PATH: &str = "/run/composefs/staged-deployment"; +/// Relative to /sysroot +pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; + /// Creates and populates /sysroot/state/deploy/image_id #[context("Writing composefs state")] pub(crate) fn write_composefs_state( root_path: &Utf8PathBuf, deployment_id: Sha256HashValue, imgref: &ImageReference, + staged: bool, ) -> Result<()> { - let state_path = root_path.join(format!("state/deploy/{}", deployment_id.to_hex())); + let state_path = root_path.join(format!("{STATE_DIR_RELATIVE}/{}", deployment_id.to_hex())); create_dir_all(state_path.join("etc/upper"))?; create_dir_all(state_path.join("etc/work"))?; @@ -1823,6 +1830,19 @@ pub(crate) fn write_composefs_state( .write(config.to_string().as_bytes()) .context("Falied to write to .origin file")?; + if staged { + std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR) + .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; + + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(COMPOSEFS_STAGED_DEPLOYMENT_PATH) + .context("Opening staged-deployment file")?; + + file.write_all(deployment_id.to_hex().as_bytes())?; + } + Ok(()) } diff --git a/lib/src/status.rs b/lib/src/status.rs index 33eecf70c..455eaf084 100644 --- a/lib/src/status.rs +++ b/lib/src/status.rs @@ -21,6 +21,7 @@ use ostree_ext::ostree; use tokio::io::AsyncReadExt; use crate::cli::OutputFormat; +use crate::install::{COMPOSEFS_STAGED_DEPLOYMENT_PATH, STATE_DIR_RELATIVE}; use crate::spec::ImageStatus; use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType}; use crate::spec::{ImageReference, ImageSignature}; @@ -389,8 +390,8 @@ pub(crate) async fn composefs_deployment_status() -> Result { let sysroot = cap_std::fs::Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority()) .context("Opening sysroot")?; let deployments = sysroot - .read_dir("state/deploy") - .context("Reading sysroot state/deploy")?; + .read_dir(STATE_DIR_RELATIVE) + .with_context(|| format!("Reading sysroot {STATE_DIR_RELATIVE}"))?; let host_spec = HostSpec { image: None, @@ -399,6 +400,17 @@ pub(crate) async fn composefs_deployment_status() -> Result { let mut host = Host::new(host_spec); + let staged_deployment_id = match std::fs::File::open(COMPOSEFS_STAGED_DEPLOYMENT_PATH) { + Ok(mut f) => { + let mut s = String::new(); + f.read_to_string(&mut s)?; + + Ok(Some(s)) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(e), + }?; + for depl in deployments { let depl = depl?; @@ -422,6 +434,15 @@ pub(crate) async fn composefs_deployment_status() -> Result { host.status.booted = Some(boot_entry); continue; } + + if let Some(staged_deployment_id) = &staged_deployment_id { + if depl_file_name == staged_deployment_id.trim() { + host.status.staged = Some(boot_entry); + continue; + } + } + + host.status.rollback = Some(boot_entry); } Ok(host) From ab8cdc9cc5dc7904448ecc6116aa6a0051f839ec Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 2 Jul 2025 11:33:21 +0530 Subject: [PATCH 20/21] composefs/install: Write entries as staged On upgrade/switch write UKI and BLS entries with the suffix .staged This should be atomically renamed by the equivalent of `ostree-finalize.service` for composefs. In case of grub menuentries for UKI case, rewrite the entire file by reading /sysroot/state/deploy/ Signed-off-by: Pragyan Poudyal --- lib/src/install.rs | 103 +++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/lib/src/install.rs b/lib/src/install.rs index 80664ffff..b6d9c2da5 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1540,15 +1540,18 @@ pub(crate) fn setup_composefs_bls_boot( id: &Sha256HashValue, entry: BootEntry, ) -> Result<()> { - let (root_path, cmdline_refs) = match setup_type { - BootSetupType::Setup(root_setup) => { + let (root_path, cmdline_refs, entry_id) = match setup_type { + BootSetupType::Setup(root_setup) => ( + root_setup.physical_root_path.clone(), // root_setup.kargs has [root=UUID=, "rw"] - (root_setup.physical_root_path.clone(), &root_setup.kargs) - } + &root_setup.kargs, + format!("{}", id.to_hex()), + ), BootSetupType::Upgrade => ( Utf8PathBuf::from("/sysroot"), &vec![format!("root=UUID={DPS_UUID}"), RW_KARG.to_string()], + format!("{}.staged", id.to_hex()), ), }; @@ -1564,7 +1567,7 @@ pub(crate) fn setup_composefs_bls_boot( false, root_path.as_std_path(), Some("boot"), - Some(&id.to_hex()), + Some(&entry_id), &str_slice, ) } @@ -1580,6 +1583,24 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { Ok((esp.node, esp.uuid)) } +fn get_user_config(uki_id: &str) -> String { + let s = format!( + r#" +menuentry "Fedora Bootc UKI: ({uki_id})" {{ + insmod fat + insmod chain + search --no-floppy --set=root --fs-uuid "${{EFI_PART_UUID}}" + chainloader /EFI/Linux/{uki_id}.efi +}} +"# + ); + + return s; +} + +/// Contains the EFP's filesystem UUID. Used by grub +const EFI_UUID_FILE: &str = "efiuuid.cfg"; + #[context("Setting up UKI boot")] pub(crate) fn setup_composefs_uki_boot( setup_type: BootSetupType, @@ -1650,58 +1671,55 @@ pub(crate) fn setup_composefs_uki_boot( let is_upgrade = matches!(setup_type, BootSetupType::Upgrade); - // Add the user grug cfg - let grub_user_config = format!( + let efi_uuid_source = format!( r#" -menuentry "Fedora Bootc UKI: ({uki_id})" {{ - insmod fat - insmod chain - search --no-floppy --set=root --fs-uuid "${{EFI_PART_UUID}}" - chainloader /EFI/Linux/{uki_id}.efi -}} -"#, - uki_id = id.to_hex(), +if [ -f ${{config_directory}}/{EFI_UUID_FILE} ]; then + source ${{config_directory}}/{EFI_UUID_FILE} +fi +"# ); - let user_cfg_name = "grub2/user.cfg"; + let user_cfg_name = if is_upgrade { + "grub2/user.cfg.staged" + } else { + "grub2/user.cfg" + }; let user_cfg_path = boot_dir.join(user_cfg_name); - // TODO: Figure out a better way to sort these menuentries. This is a bit scuffed - // - // Read the current user.cfg, split at the first menuentry, then stick the new menuentry in - // between the efiuuid.cfg sourcing code and the previous menuentry + // Iterate over all available deployments, and generate a menuentry for each if is_upgrade { - let contents = - std::fs::read_to_string(&user_cfg_path).context(format!("Reading {user_cfg_name}"))?; - let mut usr_cfg = std::fs::OpenOptions::new() .write(true) - .truncate(true) + .create(true) .open(user_cfg_path) .with_context(|| format!("Opening {user_cfg_name}"))?; - let Some((before, after)) = contents.split_once("menuentry") else { - anyhow::bail!("Did not find menuentry in {user_cfg_name}") - }; + usr_cfg.write_all(efi_uuid_source.as_bytes())?; + usr_cfg.write_all(get_user_config(&id.to_hex()).as_bytes())?; + + // root_path here will be /sysroot + for entry in std::fs::read_dir(root_path.join(STATE_DIR_RELATIVE))? { + let entry = entry?; - usr_cfg - .seek(SeekFrom::Start(0)) - .with_context(|| format!("Seek {user_cfg_name}"))?; + let depl_file_name = entry.file_name(); + // SAFETY: Deployment file name shouldn't containg non UTF-8 chars + let depl_file_name = depl_file_name.to_string_lossy(); - usr_cfg - .write_all(format!("{before} {grub_user_config}\nmenuentry {after}").as_bytes()) - .with_context(|| format!("Writing to {user_cfg_name}"))?; + usr_cfg.write_all(get_user_config(&depl_file_name).as_bytes())?; + } return Ok(()); } - // Open grub2/efiuuid.cfg and write the EFI partition UUID in there + let efi_uuid_file_path = format!("grub2/{EFI_UUID_FILE}"); + + // Open grub2/efiuuid.cfg and write the EFI partition fs-UUID in there // This will be sourced by grub2/user.cfg to be used for `--fs-uuid` let mut efi_uuid_file = std::fs::OpenOptions::new() .write(true) .create(true) - .open(boot_dir.join("grub2/efiuuid.cfg")) - .context("Opening grub2/efiuuid.cfg")?; + .open(boot_dir.join(&efi_uuid_file_path)) + .with_context(|| format!("Opening {efi_uuid_file_path}"))?; let esp_uuid = Task::new("blkid for ESP UUID", "blkid") .args(["-s", "UUID", "-o", "value", &esp_device]) @@ -1709,7 +1727,7 @@ menuentry "Fedora Bootc UKI: ({uki_id})" {{ efi_uuid_file .write_all(format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes()) - .context("Writing to grub2/efiuuid.cfg")?; + .with_context(|| format!("Writing to {efi_uuid_file_path}"))?; // Write to grub2/user.cfg let mut usr_cfg = std::fs::OpenOptions::new() @@ -1718,15 +1736,8 @@ menuentry "Fedora Bootc UKI: ({uki_id})" {{ .open(user_cfg_path) .with_context(|| format!("Opening {user_cfg_name}"))?; - let efi_uuid_source = r#" -if [ -f ${config_directory}/efiuuid.cfg ]; then - source ${config_directory}/efiuuid.cfg -fi -"#; - - usr_cfg - .write_all(format!("{efi_uuid_source}\n{grub_user_config}").as_bytes()) - .with_context(|| format!("Writing to {user_cfg_name}"))?; + usr_cfg.write_all(efi_uuid_source.as_bytes())?; + usr_cfg.write_all(get_user_config(&id.to_hex()).as_bytes())?; Ok(()) } From 609837920e8d4b2ab63e02fc74ced3fdef53fd98 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 2 Jul 2025 12:09:49 +0530 Subject: [PATCH 21/21] cli/composefs: Implement `bootc switch` This does not yet "apply" the switch, it only stages a deployment Also refactor pulling of a composefs repository to a separate function Signed-off-by: Pragyan Poudyal --- lib/src/cli.rs | 119 +++++++++++++++++++++++++++------------------ lib/src/install.rs | 35 +++++++++++++ 2 files changed, 107 insertions(+), 47 deletions(-) diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 691f6d264..0e2b6abdc 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -6,7 +6,6 @@ use std::ffi::{CString, OsStr, OsString}; use std::io::Seek; use std::os::unix::process::CommandExt; use std::process::Command; -use std::sync::Arc; use anyhow::{ensure, Context, Result}; use camino::Utf8PathBuf; @@ -28,8 +27,8 @@ use serde::{Deserialize, Serialize}; use crate::deploy::RequiredHostSpec; use crate::install::{ - open_composefs_repo, setup_composefs_bls_boot, setup_composefs_uki_boot, write_composefs_state, - BootType, BootSetupType, + pull_composefs_repo, setup_composefs_bls_boot, setup_composefs_uki_boot, write_composefs_state, + BootSetupType, BootType, }; use crate::lints; use crate::progress_jsonl::{ProgressWriter, RawProgressFd}; @@ -38,11 +37,6 @@ use crate::spec::ImageReference; use crate::status::composefs_deployment_status; use crate::utils::sigpolicy_from_opt; -use ostree_ext::composefs_boot::BootOps; -use ostree_ext::composefs_oci::{ - image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull, -}; - /// Shared progress options #[derive(Debug, Parser, PartialEq, Eq)] pub(crate) struct ProgressOptions { @@ -773,44 +767,21 @@ async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { .as_ref() .ok_or_else(|| anyhow::anyhow!("No image source specified"))?; - let booted_image = host - .status - .booted - .ok_or(anyhow::anyhow!("Could not find booted image"))? - .image - .ok_or(anyhow::anyhow!("Could not find booted image"))?; - - tracing::debug!("booted_image: {booted_image:#?}"); - tracing::debug!("imgref: {imgref:#?}"); - - let digest = booted_image - .digest() - .context("Getting digest for booted image")?; + // let booted_image = host + // .status + // .booted + // .ok_or(anyhow::anyhow!("Could not find booted image"))? + // .image + // .ok_or(anyhow::anyhow!("Could not find booted image"))?; - let rootfs_dir = cap_std::fs::Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())?; + // tracing::debug!("booted_image: {booted_image:#?}"); + // tracing::debug!("imgref: {imgref:#?}"); - let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?; + // let digest = booted_image + // .digest() + // .context("Getting digest for booted image")?; - let (id, verity) = composefs_oci_pull( - &Arc::new(repo), - &format!("{}:{}", imgref.transport, imgref.image), - None, - ) - .await - .context("Pulling composefs repo")?; - - tracing::debug!( - "id = {id}, verity = {verity}", - id = hex::encode(id), - verity = verity.to_hex() - ); - - let repo = open_composefs_repo(&rootfs_dir)?; - let mut fs = create_composefs_filesystem(&repo, digest.digest(), None) - .context("Failed to create composefs filesystem")?; - - let entries = fs.transform_for_boot(&repo)?; - let id = fs.commit_image(&repo, None)?; + let (repo, entries, id) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; let Some(entry) = entries.into_iter().next() else { anyhow::bail!("No boot entries!"); @@ -939,9 +910,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { Ok(()) } -/// Implementation of the `bootc switch` CLI command. -#[context("Switching")] -async fn switch(opts: SwitchOpts) -> Result<()> { +fn imgref_for_switch(opts: &SwitchOpts) -> Result { let transport = ostree_container::Transport::try_from(opts.transport.as_str())?; let imgref = ostree_container::ImageReference { transport, @@ -950,6 +919,56 @@ async fn switch(opts: SwitchOpts) -> Result<()> { let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy); let target = ostree_container::OstreeImageReference { sigverify, imgref }; let target = ImageReference::from(target); + + return Ok(target); +} + +#[context("Composefs Switching")] +async fn switch_composefs(opts: SwitchOpts) -> Result<()> { + let target = imgref_for_switch(&opts)?; + // TODO: Handle in-place + + let host = composefs_deployment_status() + .await + .context("Getting composefs deployment status")?; + + let new_spec = { + let mut new_spec = host.spec.clone(); + new_spec.image = Some(target.clone()); + new_spec + }; + + if new_spec == host.spec { + println!("Image specification is unchanged."); + return Ok(()); + } + + let Some(target_imgref) = new_spec.image else { + anyhow::bail!("Target image is undefined") + }; + + let (repo, entries, id) = + pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?; + + let Some(entry) = entries.into_iter().next() else { + anyhow::bail!("No boot entries!"); + }; + + match BootType::from(&entry) { + BootType::Bls => setup_composefs_bls_boot(BootSetupType::Upgrade, repo, &id, entry), + BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry), + }?; + + write_composefs_state(&Utf8PathBuf::from("/sysroot"), id, &target_imgref, true)?; + + Ok(()) +} + +/// Implementation of the `bootc switch` CLI command. +#[context("Switching")] +async fn switch(opts: SwitchOpts) -> Result<()> { + let target = imgref_for_switch(&opts)?; + let prog: ProgressWriter = opts.progress.try_into()?; // If we're doing an in-place mutation, we shortcut most of the rest of the work here @@ -1170,7 +1189,13 @@ async fn run_from_opt(opt: Opt) -> Result<()> { upgrade(opts).await } } - Opt::Switch(opts) => switch(opts).await, + Opt::Switch(opts) => { + if composefs_booted()? { + switch_composefs(opts).await + } else { + switch(opts).await + } + } Opt::Rollback(opts) => rollback(opts).await, Opt::Edit(opts) => edit(opts).await, Opt::UsrOverlay => usroverlay().await, diff --git a/lib/src/install.rs b/lib/src/install.rs index b6d9c2da5..00aa86bf4 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -1742,6 +1742,41 @@ fi Ok(()) } +/// Pulls the `image` from `transport` into a composefs repository at /sysroot +/// Checks for boot entries in the image and returns them +#[context("Pulling composefs repository")] +pub(crate) async fn pull_composefs_repo( + transport: &String, + image: &String, +) -> Result<( + ComposefsRepository, + Vec>, + Sha256HashValue, +)> { + let rootfs_dir = cap_std::fs::Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())?; + + let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?; + + let (id, verity) = composefs_oci_pull(&Arc::new(repo), &format!("{transport}:{image}"), None) + .await + .context("Pulling composefs repo")?; + + tracing::debug!( + "id = {id}, verity = {verity}", + id = hex::encode(id), + verity = verity.to_hex() + ); + + let repo = open_composefs_repo(&rootfs_dir)?; + let mut fs = create_composefs_filesystem(&repo, &hex::encode(id), None) + .context("Failed to create composefs filesystem")?; + + let entries = fs.transform_for_boot(&repo)?; + let id = fs.commit_image(&repo, None)?; + + Ok((repo, entries, id)) +} + #[context("Setting up composefs boot")] fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) -> Result<()> { let boot_uuid = root_setup