-
Notifications
You must be signed in to change notification settings - Fork 168
install: Enable installing to multi device parents #1911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -520,8 +520,13 @@ pub(crate) fn setup_composefs_bls_boot( | |
|
|
||
| cmdline_options.extend(&Cmdline::from(&composefs_cmdline)); | ||
|
|
||
| // Locate ESP partition device | ||
| let esp_part = esp_in(&root_setup.device_info)?; | ||
| // Locate ESP partition device (use first device) | ||
| // TODO: Handle multiple devices (RAID, LVM, etc) | ||
| let device_info = root_setup | ||
| .device_info | ||
| .first() | ||
| .ok_or_else(|| anyhow!("Cannot locate ESP: no backing device found"))?; | ||
| let esp_part = esp_in(device_info)?; | ||
|
|
||
| ( | ||
| root_setup.physical_root_path.clone(), | ||
|
|
@@ -1063,7 +1068,12 @@ pub(crate) fn setup_composefs_uki_boot( | |
| BootSetupType::Setup((root_setup, state, postfetch, ..)) => { | ||
| state.require_no_kargs_for_uki()?; | ||
|
|
||
| let esp_part = esp_in(&root_setup.device_info)?; | ||
| //TODO: Handle multiple devices (RAID, LVM, etc) | ||
| let device_info = root_setup | ||
| .device_info | ||
| .first() | ||
| .ok_or_else(|| anyhow!("Cannot locate ESP: no backing device found"))?; | ||
| let esp_part = esp_in(device_info)?; | ||
|
|
||
| ( | ||
| root_setup.physical_root_path.clone(), | ||
|
|
@@ -1233,7 +1243,8 @@ pub(crate) async fn setup_composefs_boot( | |
|
|
||
| if cfg!(target_arch = "s390x") { | ||
| // TODO: Integrate s390x support into install_via_bootupd | ||
| crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?; | ||
| // zipl only supports single device | ||
| crate::bootloader::install_via_zipl(root_setup.device_info.first(), boot_uuid)?; | ||
| } else if postfetch.detected_bootloader == Bootloader::Grub { | ||
| crate::bootloader::install_via_bootupd( | ||
| &root_setup.device_info, | ||
|
|
@@ -1242,8 +1253,9 @@ pub(crate) async fn setup_composefs_boot( | |
| None, | ||
| )?; | ||
| } else { | ||
| // systemd-boot only supports single device | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. only a single ESP |
||
| crate::bootloader::install_systemd_boot( | ||
| &root_setup.device_info, | ||
| root_setup.device_info.first(), | ||
| &root_setup.physical_root_path, | ||
| &state.config_opts, | ||
| None, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,7 +82,7 @@ pub(crate) fn supports_bootupd(root: &Dir) -> Result<bool> { | |
|
|
||
| #[context("Installing bootloader")] | ||
| pub(crate) fn install_via_bootupd( | ||
| device: &PartitionTable, | ||
| devices: &[PartitionTable], | ||
| rootfs: &Utf8Path, | ||
| configopts: &crate::install::InstallConfigOpts, | ||
| deployment_path: Option<&str>, | ||
|
|
@@ -97,26 +97,61 @@ pub(crate) fn install_via_bootupd( | |
| } else { | ||
| vec![] | ||
| }; | ||
| let devpath = device.path(); | ||
| println!("Installing bootloader via bootupd"); | ||
| Command::new("bootupctl") | ||
| .args(["backend", "install", "--write-uuid"]) | ||
| .args(verbose) | ||
| .args(bootupd_opts.iter().copied().flatten()) | ||
| .args(src_root_arg) | ||
| .args(["--device", devpath.as_str(), rootfs.as_str()]) | ||
| .log_debug() | ||
| .run_inherited_with_cmd_context() | ||
|
|
||
| // No backing devices with ESP found. Run bootupd without --device and let it | ||
| // try to auto-detect. This works for: | ||
| // - BIOS boot (uses MBR, not ESP) | ||
| // - Systems where bootupd can find ESP via mounted /boot/efi | ||
| // UEFI boot will fail if bootupd cannot locate the ESP. | ||
| if devices.is_empty() { | ||
| tracing::warn!( | ||
| "No backing device with ESP found; UEFI boot may fail if ESP cannot be auto-detected" | ||
| ); | ||
| println!("Installing bootloader via bootupd (no target device specified)"); | ||
| return Command::new("bootupctl") | ||
| .args(["backend", "install", "--write-uuid"]) | ||
| .args(verbose) | ||
| .args(bootupd_opts.iter().copied().flatten()) | ||
| .args(&src_root_arg) | ||
| .arg(rootfs.as_str()) | ||
| .log_debug() | ||
| .run_inherited_with_cmd_context(); | ||
| } | ||
|
|
||
| // Install bootloader to each device | ||
| for dev in devices { | ||
| let devpath = dev.path(); | ||
| println!("Installing bootloader via bootupd to {devpath}"); | ||
| Command::new("bootupctl") | ||
| .args(["backend", "install", "--write-uuid"]) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this will lead to a kind of last-one wins behavior for cc @HuijingHei We probably want to document the right way to do multi-device installs there. (and have man pages in general) Alternatively it might be nicer to explicitly support this in bootupd by just passing each device?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Agree, but we need this like RAID.
That will be cleaner, and we could do this only if we make bootupd not fail if the passed device does not have the esp device. |
||
| .args(verbose) | ||
| .args(bootupd_opts.iter().copied().flatten()) | ||
| .args(&src_root_arg) | ||
| .args(["--device", devpath.as_str()]) | ||
| .arg(rootfs.as_str()) | ||
| .log_debug() | ||
| .run_inherited_with_cmd_context()?; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[context("Installing bootloader")] | ||
| pub(crate) fn install_systemd_boot( | ||
| device: &PartitionTable, | ||
| device: Option<&PartitionTable>, | ||
| _rootfs: &Utf8Path, | ||
| _configopts: &crate::install::InstallConfigOpts, | ||
| _deployment_path: Option<&str>, | ||
| autoenroll: Option<SecurebootKeys>, | ||
| ) -> Result<()> { | ||
| // systemd-boot requires the backing device to locate the ESP partition | ||
| let device = device.ok_or_else(|| { | ||
| anyhow!( | ||
| "Cannot install systemd-boot: no single backing device found \ | ||
| (root may span multiple devices such as LVM across multiple disks)" | ||
| ) | ||
| })?; | ||
|
|
||
| let esp_part = device | ||
| .find_partition_of_type(discoverable_partition_specification::ESP) | ||
| .ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?; | ||
|
|
@@ -161,7 +196,15 @@ pub(crate) fn install_systemd_boot( | |
| } | ||
|
|
||
| #[context("Installing bootloader using zipl")] | ||
| pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Result<()> { | ||
| pub(crate) fn install_via_zipl(device: Option<&PartitionTable>, boot_uuid: &str) -> Result<()> { | ||
| // zipl requires the backing device information to install the bootloader | ||
| let device = device.ok_or_else(|| { | ||
| anyhow!( | ||
| "Cannot install zipl bootloader: no single backing device found \ | ||
| (root may span multiple devices such as LVM across multiple disks)" | ||
| ) | ||
| })?; | ||
|
|
||
| // Identify the target boot partition from UUID | ||
| let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?; | ||
| let boot_dir = Utf8Path::new(&fs.target); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1127,7 +1127,10 @@ pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> { | |
| pub(crate) struct RootSetup { | ||
| #[cfg(feature = "install-to-disk")] | ||
| luks_device: Option<String>, | ||
| pub(crate) device_info: bootc_blockdev::PartitionTable, | ||
| /// Information about the backing block device partition tables. | ||
| /// Contains all devices that have an ESP partition when the root filesystem | ||
| /// spans multiple backing devices (e.g., LVM across multiple disks). | ||
| pub(crate) device_info: Vec<bootc_blockdev::PartitionTable>, | ||
| /// Absolute path to the location where we've mounted the physical | ||
| /// root filesystem for the system we're installing. | ||
| pub(crate) physical_root_path: Utf8PathBuf, | ||
|
|
@@ -1588,7 +1591,9 @@ async fn install_with_sysroot( | |
|
|
||
| if cfg!(target_arch = "s390x") { | ||
| // TODO: Integrate s390x support into install_via_bootupd | ||
| crate::bootloader::install_via_zipl(&rootfs.device_info, boot_uuid)?; | ||
| // zipl only supports single device | ||
| let device = rootfs.device_info.first(); | ||
| crate::bootloader::install_via_zipl(device, boot_uuid)?; | ||
| } else { | ||
| match postfetch.detected_bootloader { | ||
| Bootloader::Grub => { | ||
|
|
@@ -1719,15 +1724,21 @@ async fn install_to_filesystem_impl( | |
| // Drop exclusive ownership since we're done with mutation | ||
| let rootfs = &*rootfs; | ||
|
|
||
| match &rootfs.device_info.label { | ||
| bootc_blockdev::PartitionType::Dos => crate::utils::medium_visibility_warning( | ||
| "Installing to `dos` format partitions is not recommended", | ||
| ), | ||
| bootc_blockdev::PartitionType::Gpt => { | ||
| // The only thing we should be using in general | ||
| } | ||
| bootc_blockdev::PartitionType::Unknown(o) => { | ||
| crate::utils::medium_visibility_warning(&format!("Unknown partition label {o}")) | ||
| // Check partition type of all backing devices | ||
| for device_info in &rootfs.device_info { | ||
| match &device_info.label { | ||
| bootc_blockdev::PartitionType::Dos => { | ||
| crate::utils::medium_visibility_warning(&format!( | ||
| "Installing to `dos` format partitions is not recommended: {}", | ||
| device_info.path() | ||
| )) | ||
| } | ||
| bootc_blockdev::PartitionType::Gpt => { | ||
| // The only thing we should be using in general | ||
| } | ||
| bootc_blockdev::PartitionType::Unknown(o) => crate::utils::medium_visibility_warning( | ||
| &format!("Unknown partition label {o}: {}", device_info.path()), | ||
| ), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -2277,27 +2288,69 @@ pub(crate) async fn install_to_filesystem( | |
| }; | ||
| tracing::debug!("boot UUID: {boot_uuid:?}"); | ||
|
|
||
| // Find the real underlying backing device for the root. This is currently just required | ||
| // for GRUB (BIOS) and in the future zipl (I think). | ||
| let backing_device = { | ||
| // Walk up the block device hierarchy to find physical backing device(s). | ||
| // Examples: | ||
| // /dev/sda3 -> /dev/sda (single disk) | ||
| // /dev/mapper/vg-lv -> /dev/sda2, /dev/sdb2 (LVM across two disks) | ||
| let backing_devices: Vec<String> = { | ||
| let mut dev = inspect.source; | ||
| loop { | ||
| tracing::debug!("Finding parents for {dev}"); | ||
| let mut parents = bootc_blockdev::find_parent_devices(&dev)?.into_iter(); | ||
| let Some(parent) = parents.next() else { | ||
| break; | ||
| }; | ||
| if let Some(next) = parents.next() { | ||
| anyhow::bail!( | ||
| "Found multiple parent devices {parent} and {next}; not currently supported" | ||
| let parents = bootc_blockdev::find_parent_devices(&dev)?; | ||
| if parents.is_empty() { | ||
| // Reached a physical disk | ||
| break vec![dev]; | ||
| } | ||
| if parents.len() > 1 { | ||
| // Multi-device (e.g., LVM across disks) - return all | ||
| tracing::debug!( | ||
| "Found multiple parent devices: {:?}; will search for ESP", | ||
| parents | ||
| ); | ||
| break parents; | ||
| } | ||
| // Single parent (e.g. LVM LV -> VG -> PV) - keep walking up | ||
| dev = parents.into_iter().next().unwrap(); | ||
| } | ||
| }; | ||
| tracing::debug!("Backing devices: {backing_devices:?}"); | ||
|
|
||
| // Determine the device and partition info to use for bootloader installation. | ||
| // If there are multiple backing devices, we search for all that contain an ESP. | ||
| let device_info: Vec<bootc_blockdev::PartitionTable> = if backing_devices.len() == 1 { | ||
| // Single backing device - use it directly | ||
| let dev = &backing_devices[0]; | ||
| vec![bootc_blockdev::partitions_of(Utf8Path::new(dev))?] | ||
| } else { | ||
| // Multiple backing devices - find all with ESP | ||
| let mut esp_devices = Vec::new(); | ||
| for dev in &backing_devices { | ||
| match bootc_blockdev::partitions_of(Utf8Path::new(dev)) { | ||
| Ok(table) => { | ||
| if table.find_partition_of_esp()?.is_some() { | ||
| tracing::info!("Found ESP on device {dev}"); | ||
| esp_devices.push(table); | ||
| } | ||
|
Comment on lines
+2330
to
+2333
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of Consider handling the match table.find_partition_of_esp() {
Ok(Some(_)) => {
tracing::info!("Found ESP on device {dev}");
esp_devices.push(table);
}
Ok(None) => (),
Err(e) => {
tracing::debug!("Could not check for ESP on {dev}: {e}");
}
} |
||
| } | ||
| Err(e) => { | ||
| // Some backing devices may not have partition tables (e.g., raw LVM PVs | ||
| // or whole-disk filesystems). These can't have an ESP, so skip them. | ||
| tracing::debug!("Failed to read partition table from {dev}: {e}"); | ||
| } | ||
| } | ||
| dev = parent; | ||
| } | ||
| dev | ||
| if esp_devices.is_empty() { | ||
| // No ESP found on any backing device. This is not fatal because: | ||
| // - BIOS boot uses MBR, not ESP | ||
| // - bootupd may auto-detect ESP via mounted /boot/efi | ||
| // However, UEFI boot without a detectable ESP will fail. | ||
| tracing::warn!( | ||
| "No ESP found on any backing device ({:?}); UEFI boot may fail", | ||
| backing_devices | ||
| ); | ||
| } | ||
| esp_devices | ||
| }; | ||
| tracing::debug!("Backing device: {backing_device}"); | ||
| let device_info = bootc_blockdev::partitions_of(Utf8Path::new(&backing_device))?; | ||
|
|
||
| let rootarg = format!("root={}", root_info.mount_spec); | ||
| let mut boot = if let Some(spec) = fsopts.boot_mount_spec { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK non-redundant multi-device composefs setups (with systemd-boot e.g.) should work where there's just one ESP.
So I think it should work here to walk the blockdevs until we find an ESP, but we would need to error out if there are multiple.