-
Notifications
You must be signed in to change notification settings - Fork 170
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 |
|---|---|---|
|
|
@@ -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" | ||
| ); | ||
|
Comment on lines
+107
to
+109
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. The more I look at this the more it feels like the right fix here is for us to push multi-device installation down into bootupd. It's really bootupd that understands e.g. "update payload only has BIOS so that's OK and that's all we should install". It will need to be bootupd that handles non-EFI platforms too (like android boot). |
||
| 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 |
|---|---|---|
|
|
@@ -1141,7 +1141,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>, | ||
|
Comment on lines
+1145
to
+1147
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. Related to above: We can't define this as being filtered to the ESP for e.g. s390x and other non-EFI platforms. I think this just needs to be all backing partitioned blockdevs, and then we filter it later. |
||
| /// 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, | ||
|
|
@@ -1602,7 +1605,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 => { | ||
|
|
@@ -1733,15 +1738,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()), | ||
| ), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -2291,27 +2302,77 @@ 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) => { | ||
| match table.find_partition_of_esp() { | ||
| Ok(Some(_)) => { | ||
| tracing::info!("Found ESP on device {dev}"); | ||
| esp_devices.push(table); | ||
| } | ||
| Ok(None) => (), | ||
| Err(e) => { | ||
| // Some partition table types may not be supported for ESP detection. | ||
| // Log and continue checking other devices. | ||
| 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.
We can do this without an
unwrap()more like thisor so