diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 177d4ab00..f604fe660 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -1,7 +1,7 @@ +use std::ffi::OsStr; use std::fs::create_dir_all; use std::io::Write; use std::path::Path; -use std::{ffi::OsStr, path::PathBuf}; use anyhow::{anyhow, Context, Result}; use bootc_blockdev::find_parent_devices; @@ -35,7 +35,8 @@ use serde::{Deserialize, Serialize}; use crate::bootc_composefs::repo::open_composefs_repo; use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state}; use crate::bootc_composefs::status::get_sorted_uki_boot_entries; -use crate::parsers::bls_config::BLSConfig; +use crate::composefs_consts::{TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED}; +use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; use crate::spec::ImageReference; use crate::task::Task; @@ -55,6 +56,16 @@ pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg"; /// The EFI Linux directory const EFI_LINUX: &str = "EFI/Linux"; +/// Timeout for systemd-boot bootloader menu +const SYSTEMD_TIMEOUT: &str = "timeout 5"; +const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf"; + +/// We want to be able to control the ordering of UKIs so we put them in a directory that's not the +/// directory specified by the BLS spec. We do this because we want systemd-boot to only look at +/// our config files and not show the actual UKIs in the bootloader menu +/// This is relative to the ESP +const SYSTEMD_UKI_DIR: &str = "EFI/Linux/bootc"; + pub(crate) enum BootSetupType<'a> { /// For initial setup, i.e. install to-disk Setup((&'a RootSetup, &'a State, &'a FileSystem)), @@ -448,25 +459,35 @@ pub(crate) fn setup_composefs_bls_boot( .with_title(title) .with_sort_key(default_sort_key.into()) .with_version(version) - .with_linux(format!( - "/{}/{id_hex}/vmlinuz", - entry_paths.abs_entries_path - )) - .with_initrd(vec![format!( - "/{}/{id_hex}/initrd", - entry_paths.abs_entries_path - )]) - .with_options(cmdline_refs); + .with_cfg(BLSConfigType::NonEFI { + linux: format!("/{}/{id_hex}/vmlinuz", entry_paths.abs_entries_path).into(), + initrd: vec![ + format!("/{}/{id_hex}/initrd", entry_paths.abs_entries_path).into() + ], + options: Some(cmdline_refs), + }); match find_vmlinuz_initrd_duplicates(&boot_digest)? { Some(symlink_to) => { - bls_config.linux = - format!("/{}/{symlink_to}/vmlinuz", entry_paths.abs_entries_path); - - bls_config.initrd = vec![format!( - "/{}/{symlink_to}/initrd", - entry_paths.abs_entries_path - )]; + match bls_config.cfg_type { + BLSConfigType::NonEFI { + ref mut linux, + ref mut initrd, + .. + } => { + *linux = + format!("/{}/{symlink_to}/vmlinuz", entry_paths.abs_entries_path) + .into(); + + *initrd = vec![format!( + "/{}/{symlink_to}/initrd", + entry_paths.abs_entries_path + ) + .into()]; + } + + _ => unreachable!(), + }; } None => { @@ -486,7 +507,9 @@ pub(crate) fn setup_composefs_bls_boot( let loader_path = entry_paths.config_path.join("loader"); let (config_path, booted_bls) = if is_upgrade { - let mut booted_bls = get_booted_bls()?; + let boot_dir = Dir::open_ambient_dir(&entry_paths.config_path, ambient_authority())?; + + let mut booted_bls = get_booted_bls(&boot_dir)?; booted_bls.sort_key = Some("0".into()); // entries are sorted by their filename in reverse order // This will be atomically renamed to 'loader/entries' on shutdown/reboot @@ -530,11 +553,12 @@ pub(crate) fn setup_composefs_bls_boot( fn write_pe_to_esp( repo: &ComposefsRepository, file: &RegularFile, - file_path: &PathBuf, + file_path: &Utf8Path, pe_type: PEType, uki_id: &String, is_insecure_from_opts: bool, mounted_efi: impl AsRef, + bootloader: &Bootloader, ) -> Result> { let efi_bin = read_file(file, &repo).context("Reading .efi binary")?; @@ -571,12 +595,16 @@ fn write_pe_to_esp( } // Write the UKI to ESP - let efi_linux_path = mounted_efi.as_ref().join(EFI_LINUX); + let efi_linux_path = mounted_efi.as_ref().join(match bootloader { + Bootloader::Grub => EFI_LINUX, + Bootloader::Systemd => SYSTEMD_UKI_DIR, + }); + create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?; let final_pe_path = match file_path.parent() { Some(parent) => { - let renamed_path = match parent.as_str()?.ends_with(EFI_ADDON_DIR_EXT) { + let renamed_path = match parent.as_str().ends_with(EFI_ADDON_DIR_EXT) { true => { let dir_name = format!("{}{}", uki_id, EFI_ADDON_DIR_EXT); @@ -602,8 +630,12 @@ fn write_pe_to_esp( .with_context(|| format!("Opening {final_pe_path:?}"))?; let pe_name = match pe_type { - PEType::Uki => format!("{}{}", uki_id, EFI_EXT), - PEType::UkiAddon => format!("{}{}", uki_id, EFI_ADDON_FILE_EXT), + PEType::Uki => &format!("{}{}", uki_id, EFI_EXT), + PEType::UkiAddon => file_path + .components() + .last() + .ok_or_else(|| anyhow::anyhow!("Failed to get UKI Addon file name"))? + .as_str(), }; pe_dir @@ -624,7 +656,7 @@ fn write_pe_to_esp( fn write_grub_uki_menuentry( root_path: Utf8PathBuf, setup_type: &BootSetupType, - boot_label: &String, + boot_label: String, id: &Sha256HashValue, esp_device: &String, ) -> Result<()> { @@ -708,6 +740,76 @@ fn write_grub_uki_menuentry( Ok(()) } +#[context("Writing systemd UKI config")] +fn write_systemd_uki_config( + esp_dir: &Dir, + setup_type: &BootSetupType, + boot_label: String, + id: &Sha256HashValue, +) -> Result<()> { + let default_sort_key = "0"; + + let mut bls_conf = BLSConfig::default(); + bls_conf + .with_title(boot_label) + .with_cfg(BLSConfigType::EFI { + efi: format!("/{SYSTEMD_UKI_DIR}/{}{}", id.to_hex(), EFI_EXT).into(), + }) + .with_sort_key(default_sort_key.into()) + // TODO (Johan-Liebert1): Get version from UKI like we get boot label + .with_version(default_sort_key.into()); + + let (entries_dir, booted_bls) = match setup_type { + BootSetupType::Setup(..) => { + esp_dir + .create_dir_all(TYPE1_ENT_PATH) + .with_context(|| format!("Creating {TYPE1_ENT_PATH}"))?; + + (esp_dir.open_dir(TYPE1_ENT_PATH)?, None) + } + + BootSetupType::Upgrade(_) => { + esp_dir + .create_dir_all(TYPE1_ENT_PATH_STAGED) + .with_context(|| format!("Creating {TYPE1_ENT_PATH_STAGED}"))?; + + let mut booted_bls = get_booted_bls(&esp_dir)?; + booted_bls.sort_key = Some("1".into()); + + (esp_dir.open_dir(TYPE1_ENT_PATH_STAGED)?, Some(booted_bls)) + } + }; + + entries_dir + .atomic_write( + type1_entry_conf_file_name(default_sort_key), + bls_conf.to_string().as_bytes(), + ) + .context("Writing conf file")?; + + if let Some(booted_bls) = booted_bls { + entries_dir.atomic_write( + // SAFETY: We set sort_key above + type1_entry_conf_file_name(booted_bls.sort_key.as_ref().unwrap()), + booted_bls.to_string().as_bytes(), + )?; + } + + // Write the timeout for bootloader menu if not exists + if !esp_dir.exists(SYSTEMD_LOADER_CONF_PATH) { + esp_dir + .atomic_write(SYSTEMD_LOADER_CONF_PATH, SYSTEMD_TIMEOUT) + .with_context(|| format!("Writing to {SYSTEMD_LOADER_CONF_PATH}"))?; + } + + let esp_dir = esp_dir + .reopen_as_ownedfd() + .context("Reopening as owned fd")?; + rustix::fs::fsync(esp_dir).context("fsync")?; + + Ok(()) +} + #[context("Setting up UKI boot")] pub(crate) fn setup_composefs_uki_boot( setup_type: BootSetupType, @@ -716,7 +818,7 @@ pub(crate) fn setup_composefs_uki_boot( id: &Sha256HashValue, entries: Vec>, ) -> Result<()> { - let (root_path, esp_device, bootloader, is_insecure_from_opts) = match setup_type { + let (root_path, esp_device, bootloader, is_insecure_from_opts, uki_addons) = match setup_type { BootSetupType::Setup((root_setup, state, ..)) => { if let Some(v) = &state.config_opts.karg { if v.len() > 0 { @@ -724,6 +826,10 @@ pub(crate) fn setup_composefs_uki_boot( } } + let Some(cfs_opts) = &state.composefs_options else { + anyhow::bail!("ComposeFS options not found"); + }; + let esp_part = root_setup .device_info .partitions @@ -731,23 +837,12 @@ pub(crate) fn setup_composefs_uki_boot( .find(|p| p.parttype.as_str() == ESP_GUID) .ok_or_else(|| anyhow!("ESP partition not found"))?; - let bootloader = state - .composefs_options - .as_ref() - .map(|opts| opts.bootloader.clone()) - .unwrap_or(Bootloader::default()); - - let is_insecure = state - .composefs_options - .as_ref() - .map(|x| x.insecure) - .unwrap_or(false); - ( root_setup.physical_root_path.clone(), esp_part.node.clone(), - bootloader, - is_insecure, + cfs_opts.bootloader.clone(), + cfs_opts.insecure, + cfs_opts.uki_addon.as_ref(), ) } @@ -761,6 +856,7 @@ pub(crate) fn setup_composefs_uki_boot( get_esp_partition(&sysroot_parent)?.0, bootloader, false, + None, ) } }; @@ -777,14 +873,42 @@ pub(crate) fn setup_composefs_uki_boot( } ComposefsBootEntry::Type2(entry) => { + // If --uki-addon is not passed, we don't install any addon + if matches!(entry.pe_type, PEType::UkiAddon) { + let Some(addons) = uki_addons else { + continue; + }; + + let addon_name = entry + .file_path + .components() + .last() + .ok_or_else(|| anyhow::anyhow!("Could not get UKI addon name"))?; + + let addon_name = addon_name.as_str()?; + + let addon_name = + addon_name.strip_suffix(EFI_ADDON_FILE_EXT).ok_or_else(|| { + anyhow::anyhow!("UKI addon doesn't end with {EFI_ADDON_DIR_EXT}") + })?; + + if !addons.iter().any(|passed_addon| passed_addon == addon_name) { + continue; + } + } + + let utf8_file_path = Utf8Path::from_path(&entry.file_path) + .ok_or_else(|| anyhow::anyhow!("Path is not valid UTf8"))?; + let ret = write_pe_to_esp( &repo, &entry.file, - &entry.file_path, + utf8_file_path, entry.pe_type, &id.to_hex(), is_insecure_from_opts, esp_mount.dir.path(), + &bootloader, )?; if let Some(label) = ret { @@ -796,12 +920,11 @@ pub(crate) fn setup_composefs_uki_boot( match bootloader { Bootloader::Grub => { - write_grub_uki_menuentry(root_path, &setup_type, &boot_label, id, &esp_device)? + write_grub_uki_menuentry(root_path, &setup_type, boot_label, id, &esp_device)? } Bootloader::Systemd => { - // No-op for now, but later we want to have .conf files so we can control the order of - // entries. + write_systemd_uki_config(&esp_mount.fd, &setup_type, boot_label, id)? } }; diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index d8aba078b..22971009d 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -104,7 +104,12 @@ pub(crate) async fn composefs_native_finalize() -> Result<()> { let entries_dir = esp_mount.fd.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; } - BootType::Uki => rename_staged_uki_entries(&esp_mount.fd)?, + BootType::Uki => { + rename_staged_uki_entries(&esp_mount.fd)?; + + let entries_dir = esp_mount.fd.open_dir("loader")?; + rename_exchange_bls_entries(&entries_dir)?; + } }, }; diff --git a/crates/lib/src/bootc_composefs/repo.rs b/crates/lib/src/bootc_composefs/repo.rs index 1538b72bd..51323c716 100644 --- a/crates/lib/src/bootc_composefs/repo.rs +++ b/crates/lib/src/bootc_composefs/repo.rs @@ -54,6 +54,23 @@ pub(crate) async fn initialize_composefs_repository( .await } +/// skopeo (in composefs-rs) doesn't understand "registry:" +/// This function will convert it to "docker://" and return the image ref +/// +/// Ex +/// docker://quay.io/some-image +/// containers-storage:some-image +pub(crate) fn get_imgref(transport: &str, image: &str) -> String { + let img = image.strip_prefix(":").unwrap_or(&image); + let transport = transport.strip_suffix(":").unwrap_or(&transport); + + if transport == "registry" { + format!("docker://{img}") + } else { + format!("{transport}:{img}") + } +} + /// 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")] @@ -70,10 +87,13 @@ pub(crate) async fn pull_composefs_repo( let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?; - let (id, verity) = - composefs_oci_pull(&Arc::new(repo), &format!("{transport}:{image}"), None, None) - .await - .context("Pulling composefs repo")?; + let final_imgref = get_imgref(transport, image); + + tracing::debug!("Image to pull {final_imgref}"); + + let (id, verity) = composefs_oci_pull(&Arc::new(repo), &final_imgref, None, None) + .await + .context("Pulling composefs repo")?; tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex()); @@ -86,3 +106,39 @@ pub(crate) async fn pull_composefs_repo( Ok((repo, entries, id, fs)) } + +#[cfg(test)] +mod tests { + use super::*; + + const IMAGE_NAME: &str = "quay.io/example/image:latest"; + + #[test] + fn test_get_imgref_registry_transport() { + assert_eq!( + get_imgref("registry:", IMAGE_NAME), + format!("docker://{IMAGE_NAME}") + ); + } + + #[test] + fn test_get_imgref_containers_storage() { + assert_eq!( + get_imgref("containers-storage", IMAGE_NAME), + format!("containers-storage:{IMAGE_NAME}") + ); + + assert_eq!( + get_imgref("containers-storage:", IMAGE_NAME), + format!("containers-storage:{IMAGE_NAME}") + ); + } + + #[test] + fn test_get_imgref_edge_cases() { + assert_eq!( + get_imgref("registry", IMAGE_NAME), + format!("docker://{IMAGE_NAME}") + ); + } +} diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index 0ea23b7cd..b34a8b71e 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -8,7 +8,7 @@ use fn_error_context::context; use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags}; use crate::bootc_composefs::boot::BootType; -use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls_boot_entries}; +use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_type1_boot_entries}; use crate::{ bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries}, composefs_consts::{ @@ -53,8 +53,9 @@ pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> { .context("renameat")?; tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}"); - rustix::fs::unlinkat(&entries_dir, STAGED_BOOT_LOADER_ENTRIES, AtFlags::REMOVEDIR) - .context("unlinkat")?; + entries_dir + .remove_dir_all(STAGED_BOOT_LOADER_ENTRIES) + .context("Removing staged dir")?; tracing::debug!("Syncing to disk"); let entries_dir = entries_dir @@ -110,7 +111,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> { // After this: // all_configs[0] -> booted depl // all_configs[1] -> rollback depl - let mut all_configs = get_sorted_bls_boot_entries(&boot_dir, false)?; + let mut all_configs = get_sorted_type1_boot_entries(&boot_dir, false)?; // Update the indicies so that they're swapped for (idx, cfg) in all_configs.iter_mut().enumerate() { diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 7ff10c048..b163d3717 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -9,7 +9,7 @@ use bootc_utils::CommandRunExt; use camino::Utf8PathBuf; use cap_std_ext::cap_std::ambient_authority; use cap_std_ext::cap_std::fs::Dir; -use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; +use cap_std_ext::dirext::CapStdExtDirExt; use composefs::fsverity::{FsVerityHashValue, Sha256HashValue}; use fn_error_context::context; @@ -20,40 +20,54 @@ use rustix::{ }; use crate::bootc_composefs::boot::BootType; +use crate::bootc_composefs::repo::get_imgref; +use crate::bootc_composefs::status::get_sorted_type1_boot_entries; +use crate::parsers::bls_config::BLSConfigType; use crate::{ composefs_consts::{ COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH, STATE_DIR_RELATIVE, }, - parsers::bls_config::{parse_bls_config, BLSConfig}, + parsers::bls_config::BLSConfig, spec::ImageReference, utils::path_relative_to, }; -pub(crate) fn get_booted_bls() -> Result { +pub(crate) fn get_booted_bls(boot_dir: &Dir) -> Result { let cmdline = Cmdline::from_proc()?; let booted = cmdline .find(COMPOSEFS_CMDLINE) .ok_or_else(|| anyhow::anyhow!("Failed to find composefs parameter in kernel cmdline"))?; - for entry in std::fs::read_dir("/sysroot/boot/loader/entries")? { - let entry = entry?; + let sorted_entries = get_sorted_type1_boot_entries(boot_dir, true)?; - if !entry.file_name().as_str()?.ends_with(".conf") { - continue; - } + for entry in sorted_entries { + match &entry.cfg_type { + BLSConfigType::EFI { efi } => { + let composefs_param_value = booted.value().ok_or_else(|| { + anyhow::anyhow!("Failed to get composefs kernel cmdline value") + })?; - let bls = parse_bls_config(&std::fs::read_to_string(&entry.path())?)?; + if efi.as_str().contains(composefs_param_value) { + return Ok(entry); + } + } - let Some(opts) = &bls.options else { - anyhow::bail!("options not found in bls config") - }; - let opts = Cmdline::from(opts); + BLSConfigType::NonEFI { options, .. } => { + let Some(opts) = options else { + anyhow::bail!("options not found in bls config") + }; + + let opts = Cmdline::from(opts); + + if opts.iter().any(|v| v == booted) { + return Ok(entry); + } + } - if opts.iter().any(|v| v == booted) { - return Ok(bls); - } + BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config type"), + }; } Err(anyhow::anyhow!("Booted BLS not found")) @@ -121,9 +135,12 @@ pub(crate) fn write_composefs_state( .. } = &imgref; + let imgref = get_imgref(&transport, &image_name); + let mut config = tini::Ini::new().section("origin").item( ORIGIN_CONTAINER, - format!("ostree-unverified-image:{transport}{image_name}"), + // TODO (Johan-Liebert1): The image won't always be unverified + format!("ostree-unverified-image:{imgref}"), ); config = config @@ -136,8 +153,8 @@ pub(crate) fn write_composefs_state( .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest); } - let state_dir = cap_std::fs::Dir::open_ambient_dir(&state_path, cap_std::ambient_authority()) - .context("Opening state dir")?; + let state_dir = + Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?; state_dir .atomic_write( @@ -150,11 +167,9 @@ pub(crate) fn write_composefs_state( std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR) .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; - let staged_depl_dir = cap_std::fs::Dir::open_ambient_dir( - COMPOSEFS_TRANSIENT_STATE_DIR, - cap_std::ambient_authority(), - ) - .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; + let staged_depl_dir = + Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority()) + .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?; staged_depl_dir .atomic_write( diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index fc82524a0..cec3667d7 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -7,9 +7,9 @@ use fn_error_context::context; use crate::{ bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, BootType}, - composefs_consts::{BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, USER_CFG}, + composefs_consts::{COMPOSEFS_CMDLINE, TYPE1_ENT_PATH, USER_CFG}, parsers::{ - bls_config::{parse_bls_config, BLSConfig}, + bls_config::{parse_bls_config, BLSConfig, BLSConfigType}, grub_menuconfig::{parse_grub_menuentry_file, MenuEntry}, }, spec::{BootEntry, BootOrder, Host, HostSpec, ImageReference, ImageStatus}, @@ -91,14 +91,14 @@ pub(crate) fn get_sorted_uki_boot_entries<'a>( parse_grub_menuentry_file(str) } -#[context("Getting sorted BLS entries")] -pub(crate) fn get_sorted_bls_boot_entries( +#[context("Getting sorted Type1 boot entries")] +pub(crate) fn get_sorted_type1_boot_entries( boot_dir: &Dir, ascending: bool, ) -> Result> { let mut all_configs = vec![]; - for entry in boot_dir.read_dir(format!("loader/{BOOT_LOADER_ENTRIES}"))? { + for entry in boot_dir.read_dir(TYPE1_ENT_PATH)? { let entry = entry?; let file_name = entry.file_name(); @@ -358,29 +358,63 @@ pub(crate) async fn composefs_deployment_status() -> Result { } }; - match boot_type { - BootType::Bls => { - host.status.rollback_queued = !get_sorted_bls_boot_entries(&boot_dir, false)? - .first() - .ok_or(anyhow::anyhow!("First boot entry not found"))? - .options - .as_ref() - .ok_or(anyhow::anyhow!("options key not found in bls config"))? - .contains(composefs_digest.as_ref()); - } + let is_rollback_queued = match booted.bootloader { + Bootloader::Grub => match boot_type { + BootType::Bls => { + let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = bls_config + .first() + .ok_or(anyhow::anyhow!("First boot entry not found"))?; + + match &bls_config.cfg_type { + BLSConfigType::NonEFI { options, .. } => !options + .as_ref() + .ok_or(anyhow::anyhow!("options key not found in bls config"))? + .contains(composefs_digest.as_ref()), + + BLSConfigType::EFI { .. } => { + anyhow::bail!("Found 'efi' field in Type1 boot entry") + } + BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"), + } + } - BootType::Uki => { - let mut s = String::new(); + BootType::Uki => { + let mut s = String::new(); + + !get_sorted_uki_boot_entries(&boot_dir, &mut s)? + .first() + .ok_or(anyhow::anyhow!("First boot entry not found"))? + .body + .chainloader + .contains(composefs_digest.as_ref()) + } + }, - host.status.rollback_queued = !get_sorted_uki_boot_entries(&boot_dir, &mut s)? + // We will have BLS stuff and the UKI stuff in the same DIR + Bootloader::Systemd => { + let bls_config = get_sorted_type1_boot_entries(&boot_dir, false)?; + let bls_config = bls_config .first() - .ok_or(anyhow::anyhow!("First boot entry not found"))? - .body - .chainloader - .contains(composefs_digest.as_ref()) + .ok_or(anyhow::anyhow!("First boot entry not found"))?; + + match &bls_config.cfg_type { + // For UKI boot + BLSConfigType::EFI { efi } => efi.as_str().contains(composefs_digest.as_ref()), + + // For boot entry Type1 + BLSConfigType::NonEFI { options, .. } => !options + .as_ref() + .ok_or(anyhow::anyhow!("options key not found in bls config"))? + .contains(composefs_digest.as_ref()), + + BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"), + } } }; + host.status.rollback_queued = is_rollback_queued; + if host.status.rollback_queued { host.spec.boot_order = BootOrder::Rollback }; @@ -392,7 +426,7 @@ pub(crate) async fn composefs_deployment_status() -> Result { mod tests { use cap_std_ext::{cap_std, dirext::CapStdExtDirExt}; - use crate::parsers::grub_menuconfig::MenuentryBody; + use crate::parsers::{bls_config::BLSConfigType, grub_menuconfig::MenuentryBody}; use super::*; @@ -437,26 +471,30 @@ mod tests { tempdir.atomic_write("loader/entries/entry1.conf", entry1)?; tempdir.atomic_write("loader/entries/entry2.conf", entry2)?; - let result = get_sorted_bls_boot_entries(&tempdir, true).unwrap(); + let result = get_sorted_type1_boot_entries(&tempdir, true).unwrap(); let mut config1 = BLSConfig::default(); config1.title = Some("Fedora 42.20250623.3.1 (CoreOS)".into()); config1.sort_key = Some("1".into()); - config1.linux = "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(); - config1.initrd = vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into()]; - config1.options = Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into()); + config1.cfg_type = BLSConfigType::NonEFI { + linux: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(), + initrd: vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into()], + options: Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into()), + }; let mut config2 = BLSConfig::default(); config2.title = Some("Fedora 41.20250214.2.0 (CoreOS)".into()); config2.sort_key = Some("2".into()); - config2.linux = "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(); - config2.initrd = vec!["/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into()]; - config2.options = Some("root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into()); + config2.cfg_type = BLSConfigType::NonEFI { + linux: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(), + initrd: vec!["/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into()], + options: Some("root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into()) + }; assert_eq!(result[0].sort_key.as_ref().unwrap(), "1"); assert_eq!(result[1].sort_key.as_ref().unwrap(), "2"); - let result = get_sorted_bls_boot_entries(&tempdir, false).unwrap(); + let result = get_sorted_type1_boot_entries(&tempdir, false).unwrap(); assert_eq!(result[0].sort_key.as_ref().unwrap(), "2"); assert_eq!(result[1].sort_key.as_ref().unwrap(), "1"); diff --git a/crates/lib/src/composefs_consts.rs b/crates/lib/src/composefs_consts.rs index 3c18287a1..828504a86 100644 --- a/crates/lib/src/composefs_consts.rs +++ b/crates/lib/src/composefs_consts.rs @@ -31,3 +31,8 @@ pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged"; pub(crate) const USER_CFG: &str = "user.cfg"; /// Filename for staged grub user config pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; + +/// Path to the config files directory for Type1 boot entries +/// This is relative to the boot/efi directory +pub(crate) const TYPE1_ENT_PATH: &str = "loader/entries"; +pub(crate) const TYPE1_ENT_PATH_STAGED: &str = "loader/entries.staged"; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index f40d51fed..defa9d19c 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -256,6 +256,12 @@ pub(crate) struct InstallComposefsOpts { #[clap(long, default_value_t)] #[serde(default)] pub(crate) bootloader: Bootloader, + + /// Name of the UKI addons to install without the ".efi.addon" suffix. + /// This option can be provided multiple times if multiple addons are to be installed. + #[clap(long)] + #[serde(default)] + pub(crate) uki_addon: Option>, } #[cfg(feature = "install-to-disk")] diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index 29b8f3e7b..1d24c9ebe 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -5,10 +5,30 @@ #![allow(dead_code)] use anyhow::{anyhow, Result}; +use camino::Utf8PathBuf; +use core::fmt; use std::collections::HashMap; use std::fmt::Display; use uapi_version::Version; +#[derive(Debug, PartialEq, PartialOrd, Eq, Default)] +pub enum BLSConfigType { + EFI { + /// The path to the EFI binary, usually a UKI + efi: Utf8PathBuf, + }, + NonEFI { + /// The path to the linux kernel to boot. + linux: Utf8PathBuf, + /// The paths to the initrd images. + initrd: Vec, + /// Kernel command line options. + options: Option, + }, + #[default] + Unknown, +} + /// Represents a single Boot Loader Specification config file. /// /// The boot loader should present the available boot menu entries to the user in a sorted list. @@ -24,12 +44,9 @@ pub(crate) struct BLSConfig { /// /// This is hidden and must be accessed via [`Self::version()`]; version: String, - /// The path to the linux kernel to boot. - pub(crate) linux: String, - /// The paths to the initrd images. - pub(crate) initrd: Vec, - /// Kernel command line options. - pub(crate) options: Option, + + pub(crate) cfg_type: BLSConfigType, + /// The machine ID of the OS. pub(crate) machine_id: Option, /// The sort key for the boot menu. @@ -79,13 +96,30 @@ impl Display for BLSConfig { } writeln!(f, "version {}", self.version)?; - writeln!(f, "linux {}", self.linux)?; - for initrd in self.initrd.iter() { - writeln!(f, "initrd {}", initrd)?; - } - if let Some(options) = self.options.as_deref() { - writeln!(f, "options {}", options)?; + + match &self.cfg_type { + BLSConfigType::EFI { efi } => { + writeln!(f, "efi {}", efi)?; + } + + BLSConfigType::NonEFI { + linux, + initrd, + options, + } => { + writeln!(f, "linux {}", linux)?; + for initrd in initrd.iter() { + writeln!(f, "initrd {}", initrd)?; + } + + if let Some(options) = options.as_deref() { + writeln!(f, "options {}", options)?; + } + } + + BLSConfigType::Unknown => return Err(fmt::Error), } + if let Some(machine_id) = self.machine_id.as_deref() { writeln!(f, "machine-id {}", machine_id)?; } @@ -114,16 +148,8 @@ impl BLSConfig { self.version = new_val; self } - pub(crate) fn with_linux(&mut self, new_val: String) -> &mut Self { - self.linux = new_val; - self - } - pub(crate) fn with_initrd(&mut self, new_val: Vec) -> &mut Self { - self.initrd = new_val; - self - } - pub(crate) fn with_options(&mut self, new_val: String) -> &mut Self { - self.options = Some(new_val); + pub(crate) fn with_cfg(&mut self, config: BLSConfigType) -> &mut Self { + self.cfg_type = config; self } #[allow(dead_code)] @@ -146,6 +172,7 @@ pub(crate) fn parse_bls_config(input: &str) -> Result { let mut title = None; let mut version = None; let mut linux = None; + let mut efi = None; let mut initrd = Vec::new(); let mut options = None; let mut machine_id = None; @@ -163,11 +190,12 @@ pub(crate) fn parse_bls_config(input: &str) -> Result { match key { "title" => title = Some(value), "version" => version = Some(value), - "linux" => linux = Some(value), - "initrd" => initrd.push(value), + "linux" => linux = Some(Utf8PathBuf::from(value)), + "initrd" => initrd.push(Utf8PathBuf::from(value)), "options" => options = Some(value), "machine-id" => machine_id = Some(value), "sort-key" => sort_key = Some(value), + "efi" => efi = Some(Utf8PathBuf::from(value)), _ => { extra.insert(key.to_string(), value); } @@ -175,15 +203,27 @@ pub(crate) fn parse_bls_config(input: &str) -> Result { } } - let linux = linux.ok_or_else(|| anyhow!("Missing 'linux' value"))?; let version = version.ok_or_else(|| anyhow!("Missing 'version' value"))?; + let cfg_type = match (linux, efi) { + (None, Some(efi)) => BLSConfigType::EFI { efi }, + + (Some(linux), None) => BLSConfigType::NonEFI { + linux, + initrd, + options, + }, + + // The spec makes no mention of whether both can be present or not + // Fow now, for us, we won't have both at the same time + (Some(_), Some(_)) => anyhow::bail!("'linux' and 'efi' values present"), + (None, None) => anyhow::bail!("Missing 'linux' or 'efi' value"), + }; + Ok(BLSConfig { title, version, - linux, - initrd, - options, + cfg_type, machine_id, sort_key, extra, @@ -208,14 +248,23 @@ mod tests { let config = parse_bls_config(input)?; + let BLSConfigType::NonEFI { + linux, + initrd, + options, + } = config.cfg_type + else { + panic!("Expected non EFI variant"); + }; + assert_eq!( config.title, Some("Fedora 42.20250623.3.1 (CoreOS)".to_string()) ); assert_eq!(config.version, "2"); - assert_eq!(config.linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"); - assert_eq!(config.initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]); - assert_eq!(config.options, Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".to_string())); + assert_eq!(linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"); + assert_eq!(initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]); + assert_eq!(options, Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".to_string())); assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string())); assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string())); @@ -235,8 +284,12 @@ mod tests { let config = parse_bls_config(input)?; + let BLSConfigType::NonEFI { initrd, .. } = config.cfg_type else { + panic!("Expected non EFI variant"); + }; + assert_eq!( - config.initrd, + initrd, vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"] );