diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 0cf0aa755..1a5d25728 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -923,20 +923,6 @@ 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 (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?; let Some(entry) = entries.into_iter().next() else { @@ -949,14 +935,16 @@ async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> { match boot_type { BootType::Bls => { boot_digest = Some(setup_composefs_bls_boot( - BootSetupType::Upgrade(&fs), + BootSetupType::Upgrade((&fs, &host)), repo, &id, entry, )?) } - BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade(&fs), repo, &id, entry)?, + BootType::Uki => { + setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entry)? + } }; write_composefs_state( @@ -1134,13 +1122,15 @@ async fn switch_composefs(opts: SwitchOpts) -> Result<()> { match boot_type { BootType::Bls => { boot_digest = Some(setup_composefs_bls_boot( - BootSetupType::Upgrade(&fs), + BootSetupType::Upgrade((&fs, &host)), repo, &id, entry, )?) } - BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade(&fs), repo, &id, entry)?, + BootType::Uki => { + setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entry)? + } }; write_composefs_state( diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 18aa44795..709c46bb7 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -94,7 +94,7 @@ use crate::lsm; use crate::parsers::bls_config::{parse_bls_config, BLSConfig}; use crate::parsers::grub_menuconfig::MenuEntry; use crate::progress_jsonl::ProgressWriter; -use crate::spec::ImageReference; +use crate::spec::{Bootloader, Host, ImageReference}; use crate::store::Storage; use crate::task::Task; use crate::utils::{path_relative_to, sigpolicy_from_opt}; @@ -119,7 +119,7 @@ const OSTREE_COMPOSEFS_SUPER: &str = ".ostree.cfs"; /// The mount path for selinux const SELINUXFS: &str = "/sys/fs/selinux"; /// The mount path for uefi -const EFIVARFS: &str = "/sys/firmware/efi/efivars"; +pub(crate) const EFIVARFS: &str = "/sys/firmware/efi/efivars"; pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64")); pub(crate) const ESP_GUID: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"; pub(crate) const DPS_UUID: &str = "6523f8ae-3eb1-4e2a-a05a-18b695ae656f"; @@ -311,6 +311,10 @@ pub(crate) struct InstallComposefsOpts { #[clap(long, default_value_t)] #[serde(default)] pub(crate) insecure: bool, + + #[clap(long, default_value_t)] + #[serde(default)] + pub(crate) bootloader: Bootloader, } #[cfg(feature = "install-to-disk")] @@ -1581,7 +1585,7 @@ pub(crate) enum BootSetupType<'a> { /// For initial setup, i.e. install to-disk Setup((&'a RootSetup, &'a State, &'a FileSystem)), /// For `bootc upgrade` - Upgrade(&'a FileSystem), + Upgrade((&'a FileSystem, &'a Host)), } /// Compute SHA256Sum of VMlinuz + Initrd @@ -1707,6 +1711,18 @@ fn write_bls_boot_entries_to_disk( Ok(()) } +struct BLSEntryPath<'a> { + /// Where to write vmlinuz/initrd + entries_path: Utf8PathBuf, + /// The absolute path, with reference to the partition's root, where the vmlinuz/initrd are written to + /// We need this as when installing, the mounted path will not + abs_entries_path: &'a str, + /// Where to write the .conf files + config_path: Utf8PathBuf, + /// If we mounted EFI, the target path + mount_path: Option, +} + /// Sets up and writes BLS entries and binaries (VMLinuz + Initrd) to disk /// /// # Returns @@ -1721,7 +1737,7 @@ pub(crate) fn setup_composefs_bls_boot( ) -> Result { let id_hex = id.to_hex(); - let (esp_device, cmdline_refs, fs) = match setup_type { + let (root_path, esp_device, cmdline_refs, fs, bootloader) = match setup_type { BootSetupType::Setup((root_setup, state, fs)) => { // root_setup.kargs has [root=UUID=, "rw"] let mut cmdline_options = String::from(root_setup.kargs.join(" ")); @@ -1743,10 +1759,20 @@ pub(crate) fn setup_composefs_bls_boot( .find(|p| p.parttype.as_str() == ESP_GUID) .ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?; - (esp_part.node.clone(), cmdline_options, fs) + ( + root_setup.physical_root_path.clone(), + esp_part.node.clone(), + cmdline_options, + fs, + state + .composefs_options + .as_ref() + .map(|opts| opts.bootloader.clone()) + .unwrap_or(Bootloader::default()), + ) } - BootSetupType::Upgrade(fs) => { + BootSetupType::Upgrade((fs, host)) => { let sysroot = Utf8PathBuf::from("/sysroot"); let fsinfo = inspect_filesystem(&sysroot)?; @@ -1756,7 +1782,10 @@ pub(crate) fn setup_composefs_bls_boot( anyhow::bail!("Could not find parent device for mountpoint /sysroot"); }; + let bootloader = host.require_composefs_booted()?.bootloader.clone(); + ( + Utf8PathBuf::from("/sysroot"), get_esp_partition(&parent)?.0, vec![ format!("root=UUID={DPS_UUID}"), @@ -1765,24 +1794,51 @@ pub(crate) fn setup_composefs_bls_boot( ] .join(" "), fs, + bootloader, ) } }; - let temp_efi_dir = tempfile::tempdir() - .map_err(|e| anyhow::anyhow!("Failed to create temporary directory for EFI mount: {e}"))?; - let mounted_efi = temp_efi_dir.path().to_path_buf(); + let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..)); - Command::new("mount") - .args([&PathBuf::from(&esp_device), &mounted_efi]) - .log_debug() - .run_inherited_with_cmd_context() - .context("Mounting EFI")?; + let (entry_paths, _tmpdir_guard) = match bootloader { + Bootloader::Grub => ( + BLSEntryPath { + entries_path: root_path.join("boot"), + config_path: root_path.join("boot"), + abs_entries_path: "boot", + mount_path: None, + }, + None, + ), - let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..)); + Bootloader::Systemd => { + let temp_efi_dir = tempfile::tempdir().map_err(|e| { + anyhow::anyhow!("Failed to create temporary directory for EFI mount: {e}") + })?; + + let mounted_efi = Utf8PathBuf::from_path_buf(temp_efi_dir.path().to_path_buf()) + .map_err(|_| anyhow::anyhow!("EFI dir is not valid UTF-8"))?; - let efi_dir = Utf8PathBuf::from_path_buf(mounted_efi.join(EFI_LINUX)) - .map_err(|_| anyhow::anyhow!("EFI dir is not valid UTF-8"))?; + Command::new("mount") + .args([&PathBuf::from(&esp_device), mounted_efi.as_std_path()]) + .log_debug() + .run_inherited_with_cmd_context() + .context("Mounting EFI")?; + + let efi_linux_dir = mounted_efi.join(EFI_LINUX); + + ( + BLSEntryPath { + entries_path: efi_linux_dir, + config_path: mounted_efi.clone(), + abs_entries_path: EFI_LINUX, + mount_path: Some(mounted_efi), + }, + Some(temp_efi_dir), + ) + } + }; let (bls_config, boot_digest) = match &entry { ComposefsBootEntry::Type1(..) => unimplemented!(), @@ -1831,44 +1887,66 @@ pub(crate) fn setup_composefs_bls_boot( .with_title(id_hex.clone()) .with_sort_key(default_sort_key.into()) .with_version(version.unwrap_or(default_sort_key.into())) - .with_linux(format!("/{EFI_LINUX}/{id_hex}/vmlinuz")) - .with_initrd(vec![format!("/{EFI_LINUX}/{id_hex}/initrd")]) + .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); if let Some(symlink_to) = find_vmlinuz_initrd_duplicates(&boot_digest)? { - bls_config.linux = format!("/{EFI_LINUX}/{symlink_to}/vmlinuz"); - bls_config.initrd = vec![format!("/{EFI_LINUX}/{symlink_to}/initrd")]; + 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 + )]; } else { - write_bls_boot_entries_to_disk(&efi_dir, id, usr_lib_modules_vmlinuz, &repo)?; + write_bls_boot_entries_to_disk( + &entry_paths.entries_path, + id, + usr_lib_modules_vmlinuz, + &repo, + )?; } (bls_config, boot_digest) } }; - let (entries_path, booted_bls) = if is_upgrade { + let (config_path, booted_bls) = if is_upgrade { let mut booted_bls = get_booted_bls()?; 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 ( - mounted_efi.join(format!("loader/{STAGED_BOOT_LOADER_ENTRIES}")), + entry_paths + .config_path + .join("loader") + .join(STAGED_BOOT_LOADER_ENTRIES), Some(booted_bls), ) } else { ( - mounted_efi.join(format!("loader/{BOOT_LOADER_ENTRIES}")), + entry_paths + .config_path + .join("loader") + .join(BOOT_LOADER_ENTRIES), None, ) }; - create_dir_all(&entries_path).with_context(|| format!("Creating {:?}", entries_path))?; + create_dir_all(&config_path).with_context(|| format!("Creating {:?}", config_path))?; // Scope to allow for proper unmounting { let loader_entries_dir = - cap_std::fs::Dir::open_ambient_dir(&entries_path, cap_std::ambient_authority()) - .with_context(|| format!("Opening {entries_path:?}"))?; + cap_std::fs::Dir::open_ambient_dir(&config_path, cap_std::ambient_authority()) + .with_context(|| format!("Opening {config_path:?}"))?; loader_entries_dir.atomic_write( // SAFETY: We set sort_key above @@ -1893,14 +1971,17 @@ pub(crate) fn setup_composefs_bls_boot( let owned_loader_entries_fd = loader_entries_dir .reopen_as_ownedfd() .context("Reopening as owned fd")?; + rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?; } - Command::new("umount") - .arg(&mounted_efi) - .log_debug() - .run_inherited_with_cmd_context() - .context("Unmounting EFI")?; + if let Some(mounted_efi) = entry_paths.mount_path { + Command::new("umount") + .arg(mounted_efi) + .log_debug() + .run_inherited_with_cmd_context() + .context("Unmounting EFI")?; + } Ok(boot_digest) } diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index da398c2cb..76b605582 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -1,6 +1,7 @@ //! The definition for host system state. use std::fmt::Display; +use std::str::FromStr; use anyhow::Result; use ostree_ext::container::Transport; @@ -161,14 +162,49 @@ pub struct BootEntryOstree { pub deploy_serial: u32, } +/// Bootloader type to determine whether system was booted via Grub or Systemd +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub enum Bootloader { + /// Booted via Grub + #[default] + Grub, + /// Booted via Systemd + Systemd, +} + +impl Display for Bootloader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = match self { + Bootloader::Grub => "grub", + Bootloader::Systemd => "systemd", + }; + + write!(f, "{}", string) + } +} + +impl FromStr for Bootloader { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + match value { + "grub" => Ok(Self::Grub), + "systemd" => Ok(Self::Systemd), + unrecognized => Err(anyhow::anyhow!("Unrecognized bootloader: '{unrecognized}'")), + } + } +} + /// A bootable entry #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct BootEntryComposefs { /// The erofs verity pub verity: String, - /// Whether this deployment is to be booted via BLS or UKI + /// Whether this deployment is to be booted via Type1 (vmlinuz + initrd) or Type2 (UKI) entry pub boot_type: BootType, + /// Whether we boot using systemd or grub + pub bootloader: Bootloader, } /// A bootable entry @@ -263,6 +299,19 @@ impl Host { } } } + + pub(crate) fn require_composefs_booted(&self) -> anyhow::Result<&BootEntryComposefs> { + let cfs = self + .status + .booted + .as_ref() + .ok_or(anyhow::anyhow!("Could not find booted deployment"))? + .composefs + .as_ref() + .ok_or(anyhow::anyhow!("Could not find booted image"))?; + + Ok(cfs) + } } impl Default for Host { diff --git a/crates/lib/src/status.rs b/crates/lib/src/status.rs index 3eb69527a..0ea8ba9e3 100644 --- a/crates/lib/src/status.rs +++ b/crates/lib/src/status.rs @@ -11,6 +11,8 @@ use bootc_kernel_cmdline::Cmdline; use bootc_utils::try_deserialize_timestamp; use canon_json::CanonJsonSerialize; use cap_std_ext::cap_std; +use cap_std_ext::cap_std::ambient_authority; +use cap_std_ext::cap_std::fs::Dir; use fn_error_context::context; use ostree::glib; use ostree_container::OstreeImageReference; @@ -36,6 +38,8 @@ use crate::composefs_consts::{ use crate::deploy::get_sorted_bls_boot_entries; use crate::deploy::get_sorted_uki_boot_entries; use crate::install::BootType; +use crate::install::EFIVARFS; +use crate::spec::Bootloader; use crate::spec::ImageStatus; use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType}; use crate::spec::{ImageReference, ImageSignature}; @@ -419,6 +423,32 @@ async fn get_container_manifest_and_config( Ok((manifest, config)) } +#[context("Getting bootloader")] +fn get_bootloader() -> Result { + let efivarfs = match Dir::open_ambient_dir(EFIVARFS, ambient_authority()) { + Ok(dir) => dir, + // Most likely using BIOS + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Bootloader::Grub), + Err(e) => Err(e).context(format!("Opening {EFIVARFS}"))?, + }; + + const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"; + + match efivarfs.read_to_string(EFI_LOADER_INFO) { + Ok(loader) => { + if loader.to_lowercase().contains("systemd-boot") { + return Ok(Bootloader::Systemd); + } + + return Ok(Bootloader::Grub); + } + + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Bootloader::Grub), + + Err(e) => Err(e).context(format!("Opening {EFI_LOADER_INFO}"))?, + } +} + #[context("Getting composefs deployment metadata")] async fn boot_entry_from_composefs_deployment( origin: tini::Ini, @@ -480,7 +510,11 @@ async fn boot_entry_from_composefs_deployment( pinned: false, store: None, ostree: None, - composefs: Some(crate::spec::BootEntryComposefs { verity, boot_type }), + composefs: Some(crate::spec::BootEntryComposefs { + verity, + boot_type, + bootloader: get_bootloader()?, + }), soft_reboot_capable: false, };