Skip to content

Commit

Permalink
test
Browse files Browse the repository at this point in the history
  • Loading branch information
HuijingHei committed Jan 9, 2025
1 parent 15a964a commit 2867bd1
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 156 deletions.
122 changes: 21 additions & 101 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,13 @@ 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");
}

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));
}
}
fn get_bios_boot_partition(&self) -> Option<Vec<String>> {
let bios_boot_devices = blockdev::find_colocated_bios_boot("/")
.expect("get bios_boot devices");
if !bios_boot_devices.is_empty() {
return Some(bios_boot_devices);
}
Ok(None)
return None;
}
}

Expand Down Expand Up @@ -187,7 +115,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 +127,12 @@ 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)?;
for dev in devices.iter() {
self.run_grub_install(target_root, dev)?;
log::debug!("Install grub2 on {dev}");
}
Ok(InstalledContent {
meta: update.clone(),
filetree: None,
Expand All @@ -215,9 +146,13 @@ 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)?;
for dev in devices.iter() {
self.run_grub_install(dest_root, dev)?;
log::debug!("Install grub2 on {dev}");
}

let adopted_from = None;
Ok(InstalledContent {
Expand All @@ -235,18 +170,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());
}
}
202 changes: 202 additions & 0 deletions src/blockdev.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
use std::sync::OnceLock;

use anyhow::{bail, Context, Result};
use fn_error_context::context;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::util;

#[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
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(|| "while looking for colocated ESPs")?;

// now, look for all ESPs 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 2867bd1

Please sign in to comment.