diff --git a/src/bios.rs b/src/bios.rs index 62e3e1a6..a927be0a 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -97,6 +97,7 @@ impl Bios { } // check bios_boot partition on gpt type disk + #[allow(dead_code)] fn get_bios_boot_partition(&self) -> Option { match blockdev::get_single_device("/") { Ok(device) => { @@ -147,24 +148,37 @@ impl Component for Bios { Ok(meta) } - fn query_adopt(&self) -> Result> { + fn query_adopt(&self, devices: &Option>) -> Result> { #[cfg(target_arch = "x86_64")] - if crate::efi::is_efi_booted()? && self.get_bios_boot_partition().is_none() { + if crate::efi::is_efi_booted()? && devices.is_none() { log::debug!("Skip BIOS adopt"); return Ok(None); } crate::component::query_adopt_state() } - fn adopt_update(&self, _: &openat::Dir, update: &ContentMetadata) -> Result { - let Some(meta) = self.query_adopt()? else { + fn adopt_update( + &self, + rootcxt: &RootContext, + update: &ContentMetadata, + ) -> Result { + let bios_devices = blockdev::find_colocated_bios_boot(&rootcxt.devices)?; + let Some(meta) = self.query_adopt(&bios_devices)? else { anyhow::bail!("Failed to find adoptable system") }; - let target_root = "/"; - let device = blockdev::get_single_device(&target_root)?; - self.run_grub_install(target_root, &device)?; - log::debug!("Install grub modules on {device}"); + let mut parent_devices = rootcxt.devices.iter(); + let Some(parent) = parent_devices.next() else { + anyhow::bail!("Failed to find parent device"); + }; + + if let Some(next) = parent_devices.next() { + anyhow::bail!( + "Found multiple parent devices {parent} and {next}; not currently supported" + ); + } + self.run_grub_install(rootcxt.path.as_str(), &parent)?; + log::debug!("Installed grub modules on {parent}"); Ok(InstalledContent { meta: update.clone(), filetree: None, @@ -176,12 +190,12 @@ impl Component for Bios { get_component_update(sysroot, self) } - fn run_update(&self, sysroot: &RootContext, _: &InstalledContent) -> Result { + fn run_update(&self, rootcxt: &RootContext, _: &InstalledContent) -> Result { let updatemeta = self - .query_update(&sysroot.sysroot)? + .query_update(&rootcxt.sysroot)? .expect("update available"); - let mut parent_devices = sysroot.devices.iter(); + let mut parent_devices = rootcxt.devices.iter(); let Some(parent) = parent_devices.next() else { anyhow::bail!("Failed to find parent device"); }; @@ -192,7 +206,7 @@ impl Component for Bios { ); } - self.run_grub_install(sysroot.path.as_str(), &parent)?; + self.run_grub_install(rootcxt.path.as_str(), &parent)?; log::debug!("Install grub modules on {parent}"); let adopted_from = None; diff --git a/src/blockdev.rs b/src/blockdev.rs index d2322f4b..7154eee7 100644 --- a/src/blockdev.rs +++ b/src/blockdev.rs @@ -23,6 +23,7 @@ pub fn get_devices>(target_root: P) -> Result> { } // Get single device for the target root +#[allow(dead_code)] pub fn get_single_device>(target_root: P) -> Result { let mut devices = get_devices(&target_root)?.into_iter(); let Some(parent) = devices.next() else { @@ -37,7 +38,6 @@ pub fn get_single_device>(target_root: P) -> Result { /// Find esp partition on the same device /// using sfdisk to get partitiontable -#[allow(dead_code)] pub fn get_esp_partition(device: &str) -> Result> { const ESP_TYPE_GUID: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"; let device_info: PartitionTable = bootc_blockdev::partitions_of(Utf8Path::new(device))?; @@ -51,21 +51,20 @@ pub fn get_esp_partition(device: &str) -> Result> { Ok(None) } -/// Find all ESP partitions on the devices with mountpoint boot -#[allow(dead_code)] -pub fn find_colocated_esps>(target_root: P) -> Result> { - // first, get the parent device - let devices = get_devices(&target_root).with_context(|| "while looking for colocated ESPs")?; - - // now, look for all ESPs on those devices +/// Find all ESP partitions on the devices +pub fn find_colocated_esps(devices: &Vec) -> Result>> { + // look for all ESPs on those devices let mut esps = Vec::new(); for device in devices { if let Some(esp) = get_esp_partition(&device)? { esps.push(esp) } } - log::debug!("Find esp partitions: {esps:?}"); - Ok(esps) + if esps.is_empty() { + return Ok(None); + } + log::debug!("Found esp partitions: {esps:?}"); + Ok(Some(esps)) } /// Find bios_boot partition on the same device @@ -82,20 +81,18 @@ pub fn get_bios_boot_partition(device: &str) -> Result> { Ok(None) } -/// Find all bios_boot partitions on the devices with mountpoint boot -#[allow(dead_code)] -pub fn find_colocated_bios_boot>(target_root: P) -> Result> { - // first, get the parent device - let devices = - get_devices(&target_root).with_context(|| "looking for colocated bios_boot parts")?; - - // now, look for all bios_boot parts on those devices +/// Find all bios_boot partitions on the devices +pub fn find_colocated_bios_boot(devices: &Vec) -> Result>> { + // look for all bios_boot parts on those devices let mut bios_boots = Vec::new(); for device in devices { if let Some(bios) = get_bios_boot_partition(&device)? { bios_boots.push(bios) } } - log::debug!("Find bios_boot partitions: {bios_boots:?}"); - Ok(bios_boots) + if bios_boots.is_empty() { + return Ok(None); + } + log::debug!("Found bios_boot partitions: {bios_boots:?}"); + Ok(Some(bios_boots)) } diff --git a/src/bootupd.rs b/src/bootupd.rs index b6d6eb0f..1f14bb17 100644 --- a/src/bootupd.rs +++ b/src/bootupd.rs @@ -276,7 +276,7 @@ pub(crate) fn adopt_and_update(name: &str, rootcxt: &RootContext) -> Result Result { // Process the remaining components not installed log::trace!("Remaining known components: {}", known_components.len()); - for (name, component) in known_components { - if let Some(adopt_ver) = component.query_adopt()? { + for (name, _) in known_components { + // To determine if not-installed components can be adopted: + // + // `query_adopt_state()` checks for existing installation state, + // such as a `version` in `/sysroot/.coreos-aleph-version.json`, + // or the presence of `/ostree/deploy`. + // + // `component.query_adopt()` performs additional checks, + // including hardware/device requirements. + // For example, it will skip BIOS adoption if the system is booted via EFI + // and lacks a BIOS_BOOT partition. + // + // Once a component is determined to be adoptable, it is added to the + // adoptable list, and adoption proceeds automatically. + // + // Therefore, calling `query_adopt_state()` alone is sufficient. + if let Some(adopt_ver) = crate::component::query_adopt_state()? { ret.adoptable.insert(name.to_string(), adopt_ver); } else { log::trace!("Not adoptable: {}", name); diff --git a/src/component.rs b/src/component.rs index 06560549..5e8a3629 100644 --- a/src/component.rs +++ b/src/component.rs @@ -29,12 +29,12 @@ pub(crate) trait Component { /// In an operating system whose initially booted disk image is not /// using bootupd, detect whether it looks like the component exists /// and "synthesize" content metadata from it. - fn query_adopt(&self) -> Result>; + fn query_adopt(&self, devices: &Option>) -> Result>; /// Given an adoptable system and an update, perform the update. fn adopt_update( &self, - sysroot: &openat::Dir, + rootcxt: &RootContext, update: &ContentMetadata, ) -> Result; @@ -66,7 +66,7 @@ pub(crate) trait Component { /// Used on the client to run an update. fn run_update( &self, - sysroot: &RootContext, + rootcxt: &RootContext, current: &InstalledContent, ) -> Result; diff --git a/src/efi.rs b/src/efi.rs index 599a8e63..4935c9f9 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -20,10 +20,10 @@ use walkdir::WalkDir; use widestring::U16CString; use crate::bootupd::RootContext; -use crate::filetree; use crate::model::*; use crate::ostreeutil; use crate::util::{self, CommandRunExt}; +use crate::{blockdev, filetree}; use crate::{component::*, packagesystem}; /// Well-known paths to the ESP that may have been mounted external to us. @@ -40,10 +40,6 @@ pub(crate) const SHIM: &str = "shimx64.efi"; #[cfg(target_arch = "riscv64")] pub(crate) const SHIM: &str = "shimriscv64.efi"; -/// The ESP partition label on Fedora CoreOS derivatives -pub(crate) const COREOS_ESP_PART_LABEL: &str = "EFI-SYSTEM"; -pub(crate) const ANACONDA_ESP_PART_LABEL: &str = "EFI\\x20System\\x20Partition"; - /// Systemd boot loader info EFI variable names const LOADER_INFO_VAR_STR: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"; const STUB_INFO_VAR_STR: &str = "StubInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"; @@ -61,65 +57,38 @@ pub(crate) struct Efi { } impl Efi { - fn esp_path(&self) -> Result { - self.ensure_mounted_esp(Path::new("/")) - .map(|v| v.join("EFI")) - } - - fn open_esp_optional(&self) -> Result> { - if !is_efi_booted()? && self.get_esp_device().is_none() { - log::debug!("Skip EFI"); - return Ok(None); - } - let sysroot = openat::Dir::open("/")?; - let esp = sysroot.sub_dir_optional(&self.esp_path()?)?; - Ok(esp) - } - - fn open_esp(&self) -> Result { - self.ensure_mounted_esp(Path::new("/"))?; - let sysroot = openat::Dir::open("/")?; - let esp = sysroot.sub_dir(&self.esp_path()?)?; - Ok(esp) - } + // Get mounted point for esp + pub(crate) fn get_mounted_esp(&self, root: &Path) -> Result> { + // First check all potential mount points without holding the borrow + let mut found_mount = None; + for &mnt in ESP_MOUNTS.iter() { + let path = root.join(mnt); + if !path.exists() { + continue; + } - fn get_esp_device(&self) -> 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); + let st = rustix::fs::statfs(&path)?; + if st.f_type == libc::MSDOS_SUPER_MAGIC { + util::ensure_writable_mount(&path)?; + found_mount = Some(path); break; } } - return esp_device; - } - pub(crate) fn ensure_mounted_esp(&self, root: &Path) -> Result { - let mut mountpoint = self.mountpoint.borrow_mut(); - if let Some(mountpoint) = mountpoint.as_deref() { - return Ok(mountpoint.to_owned()); - } - for &mnt in ESP_MOUNTS { - let mnt = root.join(mnt); - if !mnt.exists() { - continue; - } - let st = - rustix::fs::statfs(&mnt).with_context(|| format!("statfs failed for {mnt:?}"))?; - if st.f_type != libc::MSDOS_SUPER_MAGIC { - continue; - } - util::ensure_writable_mount(&mnt)?; - log::debug!("Reusing existing {mnt:?}"); - return Ok(mnt); + // Only borrow mutably if we found a mount point + if let Some(mnt) = found_mount { + log::debug!("Reusing existing mount point {mnt:?}"); + *self.mountpoint.borrow_mut() = Some(mnt.clone()); + Ok(Some(mnt)) + } else { + Ok(None) } + } + + // Mount the passed esp_device, return mount point + pub(crate) fn mount_esp_device(&self, root: &Path, esp_device: &Path) -> Result { + let mut mountpoint = None; - let esp_device = self - .get_esp_device() - .ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?; for &mnt in ESP_MOUNTS.iter() { let mnt = root.join(mnt); if !mnt.exists() { @@ -131,10 +100,25 @@ impl Efi { .run() .with_context(|| format!("Failed to mount {:?}", esp_device))?; log::debug!("Mounted at {mnt:?}"); - *mountpoint = Some(mnt); + mountpoint = Some(mnt); break; } - Ok(mountpoint.as_deref().unwrap().to_owned()) + let mnt = mountpoint.ok_or_else(|| anyhow::anyhow!("No mount point found"))?; + *self.mountpoint.borrow_mut() = Some(mnt.clone()); + Ok(mnt) + } + + // Firstly check if esp is already mounted, then mount the passed esp device + pub(crate) fn ensure_mounted_esp(&self, root: &Path, esp_device: &Path) -> Result { + if let Some(mountpoint) = self.mountpoint.borrow().as_deref() { + return Ok(mountpoint.to_owned()); + } + let destdir = if let Some(destdir) = self.get_mounted_esp(Path::new(root))? { + destdir + } else { + self.mount_esp_device(root, esp_device)? + }; + Ok(destdir) } fn unmount(&self) -> Result<()> { @@ -247,9 +231,8 @@ impl Component for Efi { "EFI" } - fn query_adopt(&self) -> Result> { - let esp = self.open_esp_optional()?; - if esp.is_none() { + fn query_adopt(&self, devices: &Option>) -> Result> { + if devices.is_none() { log::trace!("No ESP detected"); return Ok(None); }; @@ -265,23 +248,40 @@ impl Component for Efi { /// Given an adoptable system and an update, perform the update. fn adopt_update( &self, - sysroot: &openat::Dir, + rootcxt: &RootContext, updatemeta: &ContentMetadata, ) -> Result { - let Some(meta) = self.query_adopt()? else { + let esp_devices = blockdev::find_colocated_esps(&rootcxt.devices)?; + let Some(meta) = self.query_adopt(&esp_devices)? else { anyhow::bail!("Failed to find adoptable system") }; - let esp = self.open_esp()?; - validate_esp(&esp)?; - let updated = sysroot + let esp_devices = esp_devices.unwrap_or_default(); + let mut devices = esp_devices.iter(); + let Some(esp) = devices.next() else { + anyhow::bail!("Failed to find esp device"); + }; + + if let Some(next_esp) = devices.next() { + anyhow::bail!( + "Found multiple esp devices {esp} and {next_esp}; not currently supported" + ); + } + let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?; + + let destdir = &openat::Dir::open(&destpath.join("EFI")) + .with_context(|| format!("opening EFI dir {}", destpath.display()))?; + validate_esp(&destdir)?; + let updated = rootcxt + .sysroot .sub_dir(&component_updatedirname(self)) .context("opening update dir")?; let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; // For adoption, we should only touch files that we know about. - let diff = updatef.relative_diff_to(&esp)?; + let diff = updatef.relative_diff_to(&destdir)?; log::trace!("applying adoption diff: {}", &diff); - filetree::apply_diff(&updated, &esp, &diff, None).context("applying filesystem changes")?; + filetree::apply_diff(&updated, &destdir, &diff, None) + .context("applying filesystem changes")?; Ok(InstalledContent { meta: updatemeta.clone(), filetree: Some(updatef), @@ -289,7 +289,6 @@ impl Component for Efi { }) } - // TODO: Remove dest_root; it was never actually used fn install( &self, src_root: &openat::Dir, @@ -303,10 +302,16 @@ impl Component for Efi { log::debug!("Found metadata {}", meta.version); let srcdir_name = component_updatedirname(self); let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?; - let destdir = &self.ensure_mounted_esp(Path::new(dest_root))?; - let destd = &openat::Dir::open(destdir) - .with_context(|| format!("opening dest dir {}", destdir.display()))?; + // Using `blockdev` to find the partition instead of partlabel because + // we know the target install toplevel device already. + let esp_device = blockdev::get_esp_partition(device)? + .ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?; + + let destpath = &self.ensure_mounted_esp(Path::new(dest_root), Path::new(&esp_device))?; + + let destd = &openat::Dir::open(destpath) + .with_context(|| format!("opening dest dir {}", destpath.display()))?; validate_esp(destd)?; // TODO - add some sort of API that allows directly setting the working @@ -314,7 +319,7 @@ impl Component for Efi { std::process::Command::new("cp") .args(["-rp", "--reflink=auto"]) .arg(&srcdir_name) - .arg(destdir) + .arg(destpath) .current_dir(format!("/proc/self/fd/{}", src_root.as_raw_fd())) .run()?; if update_firmware { @@ -331,22 +336,38 @@ impl Component for Efi { fn run_update( &self, - sysroot: &RootContext, + rootcxt: &RootContext, current: &InstalledContent, ) -> Result { let currentf = current .filetree .as_ref() .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?; - let sysroot = &sysroot.sysroot; - let updatemeta = self.query_update(sysroot)?.expect("update available"); - let updated = sysroot + let sysroot_dir = &rootcxt.sysroot; + let updatemeta = self.query_update(sysroot_dir)?.expect("update available"); + let updated = sysroot_dir .sub_dir(&component_updatedirname(self)) .context("opening update dir")?; let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; let diff = currentf.diff(&updatef)?; - self.ensure_mounted_esp(Path::new("/"))?; - let destdir = self.open_esp().context("opening EFI dir")?; + + let Some(esp_devices) = blockdev::find_colocated_esps(&rootcxt.devices)? else { + anyhow::bail!("Failed to find all esp devices"); + }; + let mut devices = esp_devices.iter(); + let Some(esp) = devices.next() else { + anyhow::bail!("Failed to find esp device"); + }; + + if let Some(next_esp) = devices.next() { + anyhow::bail!( + "Found multiple esp devices {esp} and {next_esp}; not currently supported" + ); + } + let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?; + + let destdir = &openat::Dir::open(&destpath.join("EFI")) + .with_context(|| format!("opening EFI dir {}", destpath.display()))?; validate_esp(&destdir)?; log::trace!("applying diff: {}", &diff); filetree::apply_diff(&updated, &destdir, &diff, None) @@ -399,15 +420,32 @@ impl Component for Efi { } fn validate(&self, current: &InstalledContent) -> Result { - if !is_efi_booted()? && self.get_esp_device().is_none() { + let devices = crate::blockdev::get_devices("/").context("get parent devices")?; + let esp_devices = blockdev::find_colocated_esps(&devices)?; + if !is_efi_booted()? && esp_devices.is_none() { return Ok(ValidationResult::Skip); } let currentf = current .filetree .as_ref() .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?; - self.ensure_mounted_esp(Path::new("/"))?; - let efidir = self.open_esp()?; + + let esp_devices = esp_devices.unwrap_or_default(); + let mut devices = esp_devices.iter(); + let Some(esp) = devices.next() else { + anyhow::bail!("Failed to find esp device"); + }; + + if let Some(next_esp) = devices.next() { + anyhow::bail!( + "Found multiple esp devices {esp} and {next_esp}; not currently supported" + ); + } + let destpath = &self.ensure_mounted_esp(Path::new("/"), Path::new(&esp))?; + + let efidir = &openat::Dir::open(&destpath.join("EFI")) + .with_context(|| format!("opening EFI dir {}", destpath.display()))?; + let diff = currentf.relative_diff_to(&efidir)?; let mut errs = Vec::new(); for f in diff.changes.iter() {