From c4c6b1e3b3fcbcf9a04e4d9679943877592a8eb3 Mon Sep 17 00:00:00 2001 From: HuijingHei Date: Wed, 8 Jan 2025 20:48:37 +0800 Subject: [PATCH] Support updating multiple EFIs on mirrored setups(RAID1) The EFI System Partition is not mounted after booted, on systems configured with boot device mirroring, there are independent EFI partitions on each constituent disk, need to mount each disk and updates. But skip updating BIOS in this case. Xref to https://github.com/coreos/bootupd/issues/132 --- src/bios.rs | 127 +++----------- src/blockdev.rs | 206 +++++++++++++++++++++++ src/efi.rs | 175 ++++++++++++------- src/main.rs | 1 + tests/fixtures/example-lsblk-output.json | 7 + tests/kola/raid1/config.bu | 7 + tests/kola/raid1/data/libtest.sh | 1 + tests/kola/raid1/test.sh | 37 ++++ 8 files changed, 403 insertions(+), 158 deletions(-) create mode 100644 src/blockdev.rs create mode 100644 tests/kola/raid1/config.bu create mode 120000 tests/kola/raid1/data/libtest.sh create mode 100755 tests/kola/raid1/test.sh diff --git a/src/bios.rs b/src/bios.rs index f8c644e4..c6d833ed 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -2,67 +2,19 @@ use std::io::prelude::*; use std::path::Path; use std::process::Command; +use crate::blockdev; use crate::component::*; use crate::model::*; use crate::packagesystem; use anyhow::{bail, Result}; -use crate::util; -use serde::{Deserialize, Serialize}; - // grub2-install file path pub(crate) const GRUB_BIN: &str = "usr/sbin/grub2-install"; -#[derive(Serialize, Deserialize, Debug)] -struct BlockDevice { - path: String, - pttype: Option, - parttypename: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -struct Devices { - blockdevices: Vec, -} - #[derive(Default)] pub(crate) struct Bios {} impl Bios { - // get target device for running update - fn get_device(&self) -> Result { - let mut cmd: Command; - #[cfg(target_arch = "x86_64")] - { - // find /boot partition - cmd = Command::new("findmnt"); - cmd.arg("--noheadings") - .arg("--nofsroot") - .arg("--output") - .arg("SOURCE") - .arg("/boot"); - let partition = util::cmd_output(&mut cmd)?; - - // lsblk to find parent device - cmd = Command::new("lsblk"); - cmd.arg("--paths") - .arg("--noheadings") - .arg("--output") - .arg("PKNAME") - .arg(partition.trim()); - } - - #[cfg(target_arch = "powerpc64")] - { - // get PowerPC-PReP-boot partition - cmd = Command::new("realpath"); - cmd.arg("/dev/disk/by-partlabel/PowerPC-PReP-boot"); - } - - let device = util::cmd_output(&mut cmd)?; - Ok(device) - } - // Return `true` if grub2-modules installed fn check_grub_modules(&self) -> Result { let usr_path = Path::new("/usr/lib/grub"); @@ -115,37 +67,18 @@ impl Bios { } // check bios_boot partition on gpt type disk - fn get_bios_boot_partition(&self) -> Result> { - let target = self.get_device()?; - // lsblk to list children with bios_boot - let output = Command::new("lsblk") - .args([ - "--json", - "--output", - "PATH,PTTYPE,PARTTYPENAME", - target.trim(), - ]) - .output()?; - if !output.status.success() { - std::io::stderr().write_all(&output.stderr)?; - bail!("Failed to run lsblk"); + fn get_bios_boot_partition(&self) -> Option> { + let bios_boot_devices = + blockdev::find_colocated_bios_boot("/").expect("get bios_boot devices"); + // skip bios update if has multiple devices + if bios_boot_devices.len() > 1 { + log::warn!("Find multiple devices which are currently not supported"); + return None; } - - let output = String::from_utf8(output.stdout)?; - // Parse the JSON string into the `Devices` struct - let Ok(devices) = serde_json::from_str::(&output) else { - bail!("Could not deserialize JSON output from lsblk"); - }; - - // Find the device with the parttypename "BIOS boot" - for device in devices.blockdevices { - if let Some(parttypename) = &device.parttypename { - if parttypename == "BIOS boot" && device.pttype.as_deref() == Some("gpt") { - return Ok(Some(device.path)); - } - } + if !bios_boot_devices.is_empty() { + return Some(bios_boot_devices); } - Ok(None) + None } } @@ -187,7 +120,7 @@ impl Component for Bios { fn query_adopt(&self) -> 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()? && self.get_bios_boot_partition().is_none() { log::debug!("Skip BIOS adopt"); return Ok(None); } @@ -199,9 +132,13 @@ impl Component for Bios { anyhow::bail!("Failed to find adoptable system") }; - let device = self.get_device()?; - let device = device.trim(); - self.run_grub_install("/", device)?; + let target_root = "/"; + let devices = blockdev::get_backing_devices(&target_root)? + .into_iter() + .next(); + let dev = devices.unwrap(); + self.run_grub_install(target_root, &dev)?; + log::debug!("Install grub2 on {dev}"); Ok(InstalledContent { meta: update.clone(), filetree: None, @@ -215,9 +152,14 @@ impl Component for Bios { fn run_update(&self, sysroot: &openat::Dir, _: &InstalledContent) -> Result { let updatemeta = self.query_update(sysroot)?.expect("update available"); - let device = self.get_device()?; - let device = device.trim(); - self.run_grub_install("/", device)?; + let sysroot = sysroot.recover_path()?; + let dest_root = sysroot.to_str().unwrap_or("/"); + let devices = blockdev::get_backing_devices(&dest_root)? + .into_iter() + .next(); + let dev = devices.unwrap(); + self.run_grub_install(dest_root, &dev)?; + log::debug!("Install grub2 on {dev}"); let adopted_from = None; Ok(InstalledContent { @@ -235,18 +177,3 @@ impl Component for Bios { Ok(None) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_deserialize_lsblk_output() { - let data = include_str!("../tests/fixtures/example-lsblk-output.json"); - let devices: Devices = serde_json::from_str(&data).expect("JSON was not well-formatted"); - assert_eq!(devices.blockdevices.len(), 7); - assert_eq!(devices.blockdevices[0].path, "/dev/sr0"); - assert!(devices.blockdevices[0].pttype.is_none()); - assert!(devices.blockdevices[0].parttypename.is_none()); - } -} diff --git a/src/blockdev.rs b/src/blockdev.rs new file mode 100644 index 00000000..449c56c8 --- /dev/null +++ b/src/blockdev.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; +use std::path::Path; +use std::process::Command; +use std::sync::OnceLock; + +use crate::util; +use anyhow::{bail, Context, Result}; +use fn_error_context::context; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +struct BlockDevices { + blockdevices: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Device { + path: String, + pttype: Option, + parttype: Option, + parttypename: Option, +} + +impl Device { + pub(crate) fn is_esp_part(&self) -> bool { + const ESP_TYPE_GUID: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"; + if let Some(parttype) = &self.parttype { + if parttype.to_lowercase() == ESP_TYPE_GUID { + return true; + } + } + false + } + + pub(crate) fn is_bios_boot_part(&self) -> bool { + const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564454649"; + if let Some(parttype) = &self.parttype { + if parttype.to_lowercase() == BIOS_BOOT_TYPE_GUID + && self.pttype.as_deref() == Some("gpt") + { + return true; + } + } + false + } +} + +/// Parse key-value pairs from lsblk --pairs. +/// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't. +fn split_lsblk_line(line: &str) -> HashMap { + static REGEX: OnceLock = OnceLock::new(); + let regex = REGEX.get_or_init(|| Regex::new(r#"([A-Z-_]+)="([^"]+)""#).unwrap()); + let mut fields: HashMap = HashMap::new(); + for cap in regex.captures_iter(line) { + fields.insert(cap[1].to_string(), cap[2].to_string()); + } + fields +} + +/// This is a bit fuzzy, but... this function will return every block device in the parent +/// hierarchy of `device` capable of containing other partitions. So e.g. parent devices of type +/// "part" doesn't match, but "disk" and "mpath" does. +pub(crate) fn find_parent_devices(device: &str) -> Result> { + let mut cmd = Command::new("lsblk"); + // Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option + cmd.arg("--pairs") + .arg("--paths") + .arg("--inverse") + .arg("--output") + .arg("NAME,TYPE") + .arg(device); + let output = util::cmd_output(&mut cmd)?; + let mut parents = Vec::new(); + // skip first line, which is the device itself + for line in output.lines().skip(1) { + let dev = split_lsblk_line(line); + let name = dev + .get("NAME") + .with_context(|| format!("device in hierarchy of {device} missing NAME"))?; + let kind = dev + .get("TYPE") + .with_context(|| format!("device in hierarchy of {device} missing TYPE"))?; + if kind == "disk" { + parents.push(name.clone()); + } else if kind == "mpath" { + parents.push(name.clone()); + // we don't need to know what disks back the multipath + break; + } + } + if parents.is_empty() { + bail!("no parent devices found for {}", device); + } + Ok(parents) +} + +#[context("get backing devices from mountpoint boot")] +pub fn get_backing_devices>(target_root: P) -> Result> { + let target_root = target_root.as_ref(); + let bootdir = target_root.join("boot"); + if !bootdir.exists() { + bail!("{} does not exist", bootdir.display()); + } + let bootdir = openat::Dir::open(&bootdir)?; + let fsinfo = crate::filesystem::inspect_filesystem(&bootdir, ".")?; + // Find the real underlying backing device for the root. + let backing_devices = find_parent_devices(&fsinfo.source) + .with_context(|| format!("while looking for backing devices of {}", fsinfo.source))?; + log::debug!("Find backing devices: {backing_devices:?}"); + Ok(backing_devices) +} + +#[context("Listing parttype for device {device}")] +fn list_dev(device: &str) -> Result { + let mut cmd = Command::new("lsblk"); + cmd.args([ + "--json", + "--output", + "PATH,PTTYPE,PARTTYPE,PARTTYPENAME", + device, + ]); + let output = util::cmd_output(&mut cmd)?; + // Parse the JSON string into the `BlockDevices` struct + let Ok(devs) = serde_json::from_str::(&output) else { + bail!("Could not deserialize JSON output from lsblk"); + }; + Ok(devs) +} + +/// Find esp partition on the same device +pub fn get_esp_partition(device: &str) -> Result> { + let dev = list_dev(&device)?; + // Find the ESP part on the disk + for part in dev.blockdevices { + if part.is_esp_part() { + return Ok(Some(part.path)); + } + } + log::debug!("Not found any esp partition"); + Ok(None) +} + +/// Find all ESP partitions on the backing devices with mountpoint boot +pub fn find_colocated_esps>(target_root: P) -> Result> { + // first, get the parent device + let backing_devices = + get_backing_devices(&target_root).with_context(|| "while looking for colocated ESPs")?; + + // now, look for all ESPs on those devices + let mut esps = Vec::new(); + for parent_device in backing_devices { + if let Some(esp) = get_esp_partition(&parent_device)? { + esps.push(esp) + } + } + log::debug!("Find esp partitions: {esps:?}"); + Ok(esps) +} + +/// Find bios_boot partition on the same device +pub fn get_bios_boot_partition(device: &str) -> Result> { + let dev = list_dev(&device)?; + // Find the BIOS BOOT part on the disk + for part in dev.blockdevices { + if part.is_bios_boot_part() { + return Ok(Some(part.path)); + } + } + log::debug!("Not found any bios_boot partition"); + Ok(None) +} + +/// Find all bios_boot partitions on the backing devices with mountpoint boot +#[allow(dead_code)] +pub fn find_colocated_bios_boot>(target_root: P) -> Result> { + // first, get the parent device + let backing_devices = get_backing_devices(&target_root) + .with_context(|| "looking for colocated bios_boot parts")?; + + // now, look for all bios_boot parts on those devices + let mut bios_boots = Vec::new(); + for parent_device in backing_devices { + if let Some(bios) = get_bios_boot_partition(&parent_device)? { + bios_boots.push(bios) + } + } + log::debug!("Find bios_boot partitions: {bios_boots:?}"); + Ok(bios_boots) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_lsblk_output() { + let data = include_str!("../tests/fixtures/example-lsblk-output.json"); + let devices: BlockDevices = + serde_json::from_str(&data).expect("JSON was not well-formatted"); + assert_eq!(devices.blockdevices.len(), 7); + assert_eq!(devices.blockdevices[0].path, "/dev/sr0"); + assert!(devices.blockdevices[0].pttype.is_none()); + assert!(devices.blockdevices[0].parttypename.is_none()); + } +} diff --git a/src/efi.rs b/src/efi.rs index 29de6fb6..27b95706 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -19,6 +19,7 @@ use rustix::fd::BorrowedFd; use walkdir::WalkDir; use widestring::U16CString; +use crate::blockdev; use crate::filetree; use crate::model::*; use crate::ostreeutil; @@ -54,31 +55,10 @@ pub(crate) fn is_efi_booted() -> Result { #[derive(Default)] pub(crate) struct Efi { mountpoint: RefCell>, + esps: RefCell>>, } 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) - } - fn get_esp_device(&self) -> Option { let esp_devices = [COREOS_ESP_PART_LABEL, ANACONDA_ESP_PART_LABEL] .into_iter() @@ -93,11 +73,32 @@ impl Efi { return esp_device; } - pub(crate) fn ensure_mounted_esp(&self, root: &Path) -> Result { - let mut mountpoint = self.mountpoint.borrow_mut(); + fn get_all_esp_devices(&self) -> Option> { + let mut esps = self.esps.borrow_mut(); + if let Some(esp_devs) = esps.as_deref() { + log::debug!("Reusing existing esps {esp_devs:?}"); + return Some(esp_devs.to_owned()); + } + + let mut esp_devices = vec![]; + if let Some(esp_device) = self.get_esp_device() { + esp_devices.push(esp_device.to_string_lossy().into_owned()); + } else { + esp_devices = blockdev::find_colocated_esps("/").expect("get esp devices"); + }; + if !esp_devices.is_empty() { + *esps = Some(esp_devices.clone()); + return Some(esp_devices); + } + return None; + } + + fn check_existing_esp>(&self, root: P) -> Result> { + let mountpoint = self.mountpoint.borrow_mut(); if let Some(mountpoint) = mountpoint.as_deref() { - return Ok(mountpoint.to_owned()); + return Ok(Some(mountpoint.to_owned())); } + let root = root.as_ref(); for &mnt in ESP_MOUNTS { let mnt = root.join(mnt); if !mnt.exists() { @@ -109,13 +110,23 @@ impl Efi { continue; } util::ensure_writable_mount(&mnt)?; - log::debug!("Reusing existing {mnt:?}"); - return Ok(mnt); + log::debug!("Reusing existing mount point {mnt:?}"); + return Ok(Some(mnt)); } + Ok(None) + } - let esp_device = self - .get_esp_device() - .ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?; + pub(crate) fn ensure_mounted_esp>( + &self, + root: P, + esp_device: &str, + ) -> Result { + let mut mountpoint = self.mountpoint.borrow_mut(); + if let Some(mountpoint) = mountpoint.as_deref() { + return Ok(mountpoint.to_owned()); + } + + let root = root.as_ref(); for &mnt in ESP_MOUNTS.iter() { let mnt = root.join(mnt); if !mnt.exists() { @@ -134,10 +145,9 @@ impl Efi { } Ok(mountpoint.as_deref().unwrap().to_owned()) } - fn unmount(&self) -> Result<()> { if let Some(mount) = self.mountpoint.borrow_mut().take() { - let status = Command::new("umount").arg(&mount).status()?; + let status = Command::new("umount").arg("-l").arg(&mount).status()?; if !status.success() { anyhow::bail!("Failed to unmount {mount:?}: {status:?}"); } @@ -245,8 +255,7 @@ impl Component for Efi { } fn query_adopt(&self) -> Result> { - let esp = self.open_esp_optional()?; - if esp.is_none() { + if self.get_all_esp_devices().is_none() { log::trace!("No ESP detected"); return Ok(None); }; @@ -269,16 +278,32 @@ impl Component for Efi { anyhow::bail!("Failed to find adoptable system") }; - let esp = self.open_esp()?; - validate_esp(&esp)?; let updated = 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)?; - log::trace!("applying adoption diff: {}", &diff); - filetree::apply_diff(&updated, &esp, &diff, None).context("applying filesystem changes")?; + let esp_devices = self + .get_all_esp_devices() + .expect("get esp devices before adopt"); + let sysroot = sysroot.recover_path()?; + + for esp_dev in esp_devices { + let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? { + dest_path.join("EFI") + } else { + self.ensure_mounted_esp(&sysroot, &esp_dev)?.join("EFI") + }; + + let esp = openat::Dir::open(&dest_path).context("opening EFI dir")?; + validate_esp(&esp)?; + + // For adoption, we should only touch files that we know about. + let diff = updatef.relative_diff_to(&esp)?; + log::trace!("applying adoption diff: {}", &diff); + filetree::apply_diff(&updated, &esp, &diff, None) + .context("applying filesystem changes")?; + self.unmount().context("unmount after adopt")?; + } Ok(InstalledContent { meta: updatemeta.clone(), filetree: Some(updatef), @@ -300,9 +325,18 @@ 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) + let destdir = if let Some(destdir) = self.check_existing_esp(dest_root)? { + destdir + } else { + let esp_device = self + .get_esp_device() + .ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?; + let esp_device = esp_device.to_str().unwrap(); + self.ensure_mounted_esp(dest_root, esp_device)? + }; + + let destd = &openat::Dir::open(&destdir) .with_context(|| format!("opening dest dir {}", destdir.display()))?; validate_esp(destd)?; @@ -344,12 +378,25 @@ impl Component for Efi { .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")?; - validate_esp(&destdir)?; - log::trace!("applying diff: {}", &diff); - filetree::apply_diff(&updated, &destdir, &diff, None) - .context("applying filesystem changes")?; + let esp_devices = self + .get_all_esp_devices() + .context("get esp devices when running update")?; + let sysroot = sysroot.recover_path()?; + + for esp in esp_devices { + let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? { + dest_path.join("EFI") + } else { + self.ensure_mounted_esp(&sysroot, &esp)?.join("EFI") + }; + + let destdir = openat::Dir::open(&dest_path).context("opening EFI dir")?; + validate_esp(&destdir)?; + log::trace!("applying diff: {}", &diff); + filetree::apply_diff(&updated, &destdir, &diff, None) + .context("applying filesystem changes")?; + self.unmount().context("unmount after update")?; + } let adopted_from = None; Ok(InstalledContent { meta: updatemeta, @@ -397,24 +444,36 @@ impl Component for Efi { } fn validate(&self, current: &InstalledContent) -> Result { - if !is_efi_booted()? && self.get_esp_device().is_none() { + let esp_devices = self.get_all_esp_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 diff = currentf.relative_diff_to(&efidir)?; let mut errs = Vec::new(); - for f in diff.changes.iter() { - errs.push(format!("Changed: {}", f)); - } - for f in diff.removals.iter() { - errs.push(format!("Removed: {}", f)); + let esps = esp_devices.ok_or_else(|| anyhow::anyhow!("No esp device found!"))?; + let dest_root = Path::new("/"); + for esp_dev in esps.iter() { + let dest_path = if let Some(dest_path) = self.check_existing_esp(dest_root)? { + dest_path.join("EFI") + } else { + self.ensure_mounted_esp(dest_root, &esp_dev)?.join("EFI") + }; + + let efidir = openat::Dir::open(dest_path.as_path())?; + let diff = currentf.relative_diff_to(&efidir)?; + + for f in diff.changes.iter() { + errs.push(format!("Changed: {}", f)); + } + for f in diff.removals.iter() { + errs.push(format!("Removed: {}", f)); + } + assert_eq!(diff.additions.len(), 0); + self.unmount().context("unmount after validate")?; } - assert_eq!(diff.additions.len(), 0); if !errs.is_empty() { Ok(ValidationResult::Errors(errs)) } else { diff --git a/src/main.rs b/src/main.rs index 7c7cb40c..ac85381f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ Refs: mod backend; #[cfg(any(target_arch = "x86_64", target_arch = "powerpc64"))] mod bios; +mod blockdev; mod bootupd; mod cli; mod component; diff --git a/tests/fixtures/example-lsblk-output.json b/tests/fixtures/example-lsblk-output.json index f0aac3e0..b506a937 100644 --- a/tests/fixtures/example-lsblk-output.json +++ b/tests/fixtures/example-lsblk-output.json @@ -3,30 +3,37 @@ { "path": "/dev/sr0", "pttype": null, + "parttype": null, "parttypename": null },{ "path": "/dev/zram0", "pttype": null, + "parttype": null, "parttypename": null },{ "path": "/dev/vda", "pttype": "gpt", + "parttype": null, "parttypename": null },{ "path": "/dev/vda1", "pttype": "gpt", + "parttype": null, "parttypename": "EFI System" },{ "path": "/dev/vda2", "pttype": "gpt", + "parttype": null, "parttypename": "Linux extended boot" },{ "path": "/dev/vda3", "pttype": "gpt", + "parttype": null, "parttypename": "Linux filesystem" },{ "path": "/dev/mapper/luks-df2d5f95-5725-44dd-83e1-81bc4cdc49b8", "pttype": null, + "parttype": null, "parttypename": null } ] diff --git a/tests/kola/raid1/config.bu b/tests/kola/raid1/config.bu new file mode 100644 index 00000000..b43d4a4a --- /dev/null +++ b/tests/kola/raid1/config.bu @@ -0,0 +1,7 @@ +variant: fcos +version: 1.5.0 +boot_device: + mirror: + devices: + - /dev/vda + - /dev/vdb \ No newline at end of file diff --git a/tests/kola/raid1/data/libtest.sh b/tests/kola/raid1/data/libtest.sh new file mode 120000 index 00000000..59532579 --- /dev/null +++ b/tests/kola/raid1/data/libtest.sh @@ -0,0 +1 @@ +../../data/libtest.sh \ No newline at end of file diff --git a/tests/kola/raid1/test.sh b/tests/kola/raid1/test.sh new file mode 100755 index 00000000..e882e59e --- /dev/null +++ b/tests/kola/raid1/test.sh @@ -0,0 +1,37 @@ +#!/bin/bash +## kola: +## # additionalDisks is only supported on qemu. +## platforms: qemu +## # Root reprovisioning requires at least 4GiB of memory. +## minMemory: 4096 +## # Linear RAID is setup on these disks. +## additionalDisks: ["10G"] +## # This test includes a lot of disk I/O and needs a higher +## # timeout value than the default. +## timeoutMin: 15 +## description: Verify updating multiple EFIs with RAID 1 works. + +set -xeuo pipefail + +# shellcheck disable=SC1091 +. "$KOLA_EXT_DATA/libtest.sh" + +srcdev=$(findmnt -nvr /sysroot -o SOURCE) +[[ ${srcdev} == "/dev/md126" ]] + +blktype=$(lsblk -o TYPE "${srcdev}" --noheadings) +[[ ${blktype} == raid1 ]] + +fstype=$(findmnt -nvr /sysroot -o FSTYPE) +[[ ${fstype} == xfs ]] +ok "source is XFS on RAID1 device" + + +mount -o remount,rw /boot + +rm -f -v /boot/bootupd-state.json + +bootupctl adopt-and-update | grep "Adopted and updated: EFI" + +bootupctl status | grep "Component EFI" +ok "bootupctl adopt-and-update supports multiple EFIs on RAID1"