diff --git a/Cargo.lock b/Cargo.lock index b5ef21441..78a3dbd74 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", @@ -802,16 +820,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 +836,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 +1185,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 +1607,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" @@ -1703,6 +1692,8 @@ dependencies = [ "clap_mangen", "comfy-table", "composefs", + "composefs-boot", + "composefs-oci", "containers-image-proxy", "flate2", "fn-error-context", @@ -1897,7 +1888,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 +3024,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 +3044,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..388fdeec0 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::{ + 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, +}; 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 _; @@ -218,6 +232,19 @@ 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 InstallComposefsOpts { + #[clap(long, value_enum, default_value_t)] + pub(crate) boot: BootType, +} + #[cfg(feature = "install-to-disk")] #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallToDiskOpts { @@ -241,6 +268,12 @@ pub(crate) struct InstallToDiskOpts { #[clap(long)] #[serde(default)] pub(crate) via_loopback: bool, + + #[clap(long)] + pub(crate) composefs_native: bool, + + #[clap(flatten)] + pub(crate) composefs_opts: InstallComposefsOpts, } #[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -363,6 +396,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 @@ -380,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 { @@ -523,6 +560,17 @@ impl FromStr for MountSpec { } } +impl InstallToDiskOpts { + pub(crate) fn validate(&self) { + if !self.composefs_native { + // 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( @@ -1182,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()) @@ -1326,6 +1375,7 @@ async fn prepare_install( container_root: rootfs, tempdir, host_is_container, + composefs_options: composefs_opts, }); Ok(state) @@ -1425,6 +1475,164 @@ 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 { + name: image_name, + transport, + } = &state.source.imageref; + + // transport's display is already of type ":" + composefs_oci_pull(&Arc::new(repo), &format!("{transport}{image_name}",), None).await +} + +#[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"), + }; + + let cmdline_refs = [ + "console=ttyS0,115200", + &format!("root=UUID={rootfs_uuid}"), + "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")?; + + composefs_write_boot_simple( + &repo, + entry, + &id, + boot_dir.as_std_path(), + None, + Some(&format!("{}", id.to_hex())), + &[], + )?; + + // 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" {{ + insmod fat + insmod chain + search --no-floppy --set=root --fs-uuid {rootfs_uuid} + 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())); + + 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, @@ -1457,34 +1665,47 @@ async fn install_to_filesystem_impl( let bound_images = BoundImages::from_state(state).await?; - // Initialize the ostree sysroot (repo, stateroot, etc.) + if state.composefs_options.is_some() { + // 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"")?; + } + + // 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 { @@ -1505,6 +1726,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 @@ -1526,7 +1749,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) = { @@ -1734,7 +1967,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; 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;