diff --git a/Cargo.lock b/Cargo.lock index b5ef21441..ca6227eed 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,50 @@ dependencies = [ [[package]] name = "composefs" -version = "0.2.0" -source = "git+https://github.com/containers/composefs-rs?rev=821eeae93e48f1ee381c49b8cd4d22fda92d27a2#821eeae93e48f1ee381c49b8cd4d22fda92d27a2" +version = "0.3.0" dependencies = [ "anyhow", - "async-compression", - "clap", - "containers-image-proxy", - "env_logger 0.11.6", "hex", - "indicatif", "log", - "oci-spec", - "regex-automata 0.4.9", + "once_cell", "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" +dependencies = [ + "anyhow", + "composefs", + "regex-automata 0.4.9", + "thiserror 2.0.12", + "zerocopy 0.8.25", +] + +[[package]] +name = "composefs-oci" +version = "0.3.0" +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" @@ -558,9 +576,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", @@ -608,6 +626,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossterm" version = "0.28.1" @@ -802,16 +826,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 +842,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 +1191,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" @@ -1618,9 +1613,13 @@ 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" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "openssh-keys" @@ -1703,6 +1702,8 @@ dependencies = [ "clap_mangen", "comfy-table", "composefs", + "composefs-boot", + "composefs-oci", "containers-image-proxy", "flate2", "fn-error-context", @@ -1897,7 +1898,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 +3034,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 +3054,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/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(()) } diff --git a/lib/src/install.rs b/lib/src/install.rs index 395293c19..daf166a17 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; @@ -38,12 +39,25 @@ use chrono::prelude::*; use clap::ValueEnum; use fn_error_context::context; use ostree::gio; +use ostree_ext::composefs::{ + fsverity::{FsVerityHashValue, Sha256HashValue}, + repository::Repository as ComposefsRepository, + util::Sha256Digest, +}; +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; 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 _; @@ -241,6 +255,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)] @@ -363,6 +380,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 @@ -1425,10 +1443,119 @@ 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, +) -> 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 = open_composefs_repo(rootfs_dir)?; + + 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 +} + +#[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, + )?; + + // 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" {{ + 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())); + + 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, cleanup: Cleanup, + composefs: bool, ) -> Result<()> { if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) { rootfs.kargs.push("selinux=0".to_string()); @@ -1457,34 +1584,47 @@ 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() + ); - 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"")?; - } + setup_composefs_boot(rootfs, state, &hex::encode(id))?; + } 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?; - // We must drop the sysroot here in order to close any open file - // descriptors. - }; + 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"")?; + } - // Run this on every install as the penultimate step - install_finalize(&rootfs.physical_root_path).await?; + // 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?; + } // Finalize mounted filesystems if !rootfs.skip_finalize { @@ -1547,7 +1687,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(); @@ -1933,7 +2073,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); 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}")); diff --git a/ostree-ext/Cargo.toml b/ostree-ext/Cargo.toml index 6ba83425a..258b2aab3 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,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 = "821eeae93e48f1ee381c49b8cd4d22fda92d27a2" } + +composefs = { git = "https://github.com/containers/composefs-rs", rev = "2a71e03791a50f7c254b98b20b70b9ab3c0ca997", package = "composefs" } +composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "2a71e03791a50f7c254b98b20b70b9ab3c0ca997", package = "composefs-boot" } +composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "2a71e03791a50f7c254b98b20b70b9ab3c0ca997", 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;