Skip to content

Commit

Permalink
Support updating multiple EFIs on mirrored setups(RAID1)
Browse files Browse the repository at this point in the history
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 coreos#132
  • Loading branch information
HuijingHei committed Jan 12, 2025
1 parent 15a964a commit da772b8
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 157 deletions.
123 changes: 23 additions & 100 deletions src/bios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
parttypename: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Devices {
blockdevices: Vec<BlockDevice>,
}

#[derive(Default)]
pub(crate) struct Bios {}

impl Bios {
// get target device for running update
fn get_device(&self) -> Result<String> {
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<bool> {
let usr_path = Path::new("/usr/lib/grub");
Expand Down Expand Up @@ -115,37 +67,18 @@ impl Bios {
}

// check bios_boot partition on gpt type disk
fn get_bios_boot_partition(&self) -> Result<Option<String>> {
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<Vec<String>> {
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::<Devices>(&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
}
}

Expand Down Expand Up @@ -187,7 +120,7 @@ impl Component for Bios {

fn query_adopt(&self) -> Result<Option<Adoptable>> {
#[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);
}
Expand All @@ -199,9 +132,11 @@ 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,
Expand All @@ -215,9 +150,12 @@ impl Component for Bios {

fn run_update(&self, sysroot: &openat::Dir, _: &InstalledContent) -> Result<InstalledContent> {
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 {
Expand All @@ -235,18 +173,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());
}
}
206 changes: 206 additions & 0 deletions src/blockdev.rs
Original file line number Diff line number Diff line change
@@ -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<Device>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Device {
path: String,
pttype: Option<String>,
parttype: Option<String>,
parttypename: Option<String>,
}

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<String, String> {
static REGEX: OnceLock<Regex> = OnceLock::new();
let regex = REGEX.get_or_init(|| Regex::new(r#"([A-Z-_]+)="([^"]+)""#).unwrap());
let mut fields: HashMap<String, String> = 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<Vec<String>> {
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<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
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<BlockDevices> {
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::<BlockDevices>(&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<Option<String>> {
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<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
// 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<Option<String>> {
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<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
// 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());
}
}
Loading

0 comments on commit da772b8

Please sign in to comment.