diff --git a/src/bin/rdcore/rootmap.rs b/src/bin/rdcore/rootmap.rs index e93bef7f4..7cf75671e 100644 --- a/src/bin/rdcore/rootmap.rs +++ b/src/bin/rdcore/rootmap.rs @@ -65,8 +65,8 @@ pub fn rootmap(config: &RootmapConfig) -> Result<()> { }) .context("appending rootmap kargs")?; eprintln!("Injected kernel arguments into BLS: {}", kargs.join(" ")); - // Note here we're not calling `zipl` on s390x; it will be called anyway on firstboot by - // `coreos-ignition-firstboot-complete.service`, so might as well batch them. + // Note here we're not calling `zipl` on s390x; it will be called anyway on firstboot by + // `coreos-ignition-firstboot-complete.service`, so might as well batch them. } else { // without /boot options, we just print the kargs; note we output to stdout here println!("{}", kargs.join(" ")); @@ -83,13 +83,13 @@ pub fn get_boot_mount_from_cmdline_args( if let Some(path) = boot_mount { Ok(Some(Mount::from_existing(path)?)) } else if let Some(devpath) = boot_device { - let devinfo = lsblk_single(Path::new(devpath))?; + let devinfo = Device::lsblk(Path::new(devpath), false)?; let fs = devinfo - .get("FSTYPE") - .with_context(|| format!("failed to query filesystem for {}", devpath))?; + .fstype + .ok_or_else(|| anyhow::anyhow!("failed to query filesystem for {}", devpath))?; Ok(Some(Mount::try_mount( devpath, - fs, + &fs, mount::MsFlags::empty(), )?)) } else { @@ -98,19 +98,19 @@ pub fn get_boot_mount_from_cmdline_args( } fn device_to_kargs(root: &Mount, device: PathBuf) -> Result>> { - let blkinfo = lsblk_single(&device)?; - let blktype = blkinfo - .get("TYPE") - .with_context(|| format!("missing TYPE for {}", device.display()))?; + let blkinfo = Device::lsblk(&device, false)?; + let blktypeinfo = blkinfo + .blktype + .ok_or_else(|| anyhow::anyhow!("missing type for {}", device.display()))?; // a `match {}` construct would be nice here, but for RAID it's a prefix match - if blktype.starts_with("raid") { + if blktypeinfo.starts_with("raid") { Ok(Some(get_raid_kargs(&device)?)) - } else if blktype == "crypt" { + } else if blktypeinfo == "crypt" { Ok(Some(get_luks_kargs(root, &device)?)) - } else if blktype == "part" || blktype == "disk" || blktype == "mpath" { + } else if blktypeinfo == "part" || blktypeinfo == "disk" || blktypeinfo == "mpath" { Ok(None) } else { - bail!("unknown block device type {}", blktype) + bail!("unknown block device type {}", blktypeinfo) } } diff --git a/src/blockdev.rs b/src/blockdev.rs index 652ef1b33..374485d81 100644 --- a/src/blockdev.rs +++ b/src/blockdev.rs @@ -17,7 +17,7 @@ use gptman::{GPTPartitionEntry, GPT}; use nix::sys::stat::{major, minor}; use nix::{errno::Errno, mount, sched}; use regex::Regex; -use std::collections::HashMap; +use serde::Deserialize; use std::convert::TryInto; use std::env; use std::fs::{ @@ -42,6 +42,47 @@ use crate::util::*; use crate::{runcmd, runcmd_output}; +#[derive(Debug, Deserialize)] +struct DevicesOutput { + blockdevices: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Device { + pub name: String, + pub label: Option, + pub fstype: Option, + #[serde(rename = "type")] + pub blktype: Option, + pub mountpoint: Option, + pub uuid: Option, + pub children: Option>, +} + +impl Device { + pub fn lsblk(dev: &Path, with_children: bool) -> Result { + let mut cmd = Command::new("lsblk"); + cmd.args(&[ + "-J", + "--paths", + "-o", + "NAME,LABEL,FSTYPE,TYPE,MOUNTPOINT,UUID", + ]) + .arg(dev); + if !with_children { + cmd.arg("--nodeps"); + } + let output = cmd_output(&mut cmd)?; + let devs: DevicesOutput = serde_json::from_str(&output)?; + let devinfo = devs + .blockdevices + .into_iter() + .next() + .ok_or_else(|| anyhow!("failed to get device information"))?; + Ok(devinfo) + } +} + #[derive(Debug)] pub struct Disk { pub path: String, @@ -105,37 +146,72 @@ impl Disk { } } + fn validate_blktype(&self, blktype: &Option, with_holders: bool) -> bool { + if let Some(blktype) = &blktype { + match blktype.as_str() { + // If whole-disk device, skip. + "disk" => false, + // If partition, allow. + "part" => true, + // If with_holders is true, allow anything else. + _ if with_holders => true, + // Default case + _ => false, + } + } else { + false + } + } + + fn compute_partition(&self, devinfo: &Device, with_holders: bool) -> Result> { + let mut result: Vec = Vec::new(); + let isblktypeavailable: bool = self.validate_blktype(&devinfo.blktype, with_holders); + if isblktypeavailable { + let (mountpoint, swap) = match &devinfo.mountpoint { + Some(mp) if mp == "[SWAP]" => (None, true), + Some(mp) => (Some(mp.to_string()), false), + None => (None, false), + }; + result.push(Partition { + path: devinfo.name.to_owned(), + label: devinfo.label.clone(), + fstype: devinfo.fstype.clone(), + parent: self.path.to_owned(), + mountpoint, + swap, + }); + } + Ok(result) + } + fn get_partitions(&self, with_holders: bool) -> Result> { // walk each device in the output let mut result: Vec = Vec::new(); - for devinfo in lsblk(Path::new(&self.path), true)? { - if let Some(name) = devinfo.get("NAME") { - match devinfo.get("TYPE") { - // If unknown type, skip. - None => continue, - // If whole-disk device, skip. - Some(t) if t == &"disk".to_string() => continue, - // If partition, allow. - Some(t) if t == &"part".to_string() => (), - // If with_holders is true, allow anything else. - Some(_) if with_holders => (), - // Ignore LVM or RAID devices which are using one of the - // partitions but aren't a partition themselves. - _ => continue, - }; - let (mountpoint, swap) = match devinfo.get("MOUNTPOINT") { - Some(mp) if mp == "[SWAP]" => (None, true), - Some(mp) => (Some(mp.to_string()), false), - None => (None, false), - }; - result.push(Partition { - path: name.to_owned(), - label: devinfo.get("LABEL").map(<_>::to_string), - fstype: devinfo.get("FSTYPE").map(<_>::to_string), - parent: self.path.to_owned(), - mountpoint, - swap, - }); + let deviceinfo = Device::lsblk(Path::new(&self.path), true)?; + let mut partition = self.compute_partition(&deviceinfo, with_holders)?; + if !partition.is_empty() { + result.append(&mut partition); + } + let mut childpartitioninfo = self.get_partitions_recurse(&deviceinfo, with_holders)?; + if !childpartitioninfo.is_empty() { + result.append(&mut childpartitioninfo); + } + Ok(result) + } + + fn get_partitions_recurse( + &self, + deviceinfo: &Device, + with_holders: bool, + ) -> Result> { + let mut result: Vec = Vec::new(); + if let Some(children) = deviceinfo.children.as_ref() { + if !children.is_empty() { + for child in children { + let mut childpartitions = self.compute_partition(&child, with_holders)?; + result.append(&mut childpartitions); + self.get_partitions_recurse(&child, with_holders)?; + } } } Ok(result) @@ -506,11 +582,12 @@ impl Mount { } pub fn get_filesystem_uuid(&self) -> Result { - let devinfo = lsblk_single(Path::new(&self.device))?; - devinfo - .get("UUID") - .map(String::from) - .with_context(|| format!("filesystem {} has no UUID", self.device)) + let devinfo = Device::lsblk(Path::new(&self.device), false)?; + let uuid = devinfo + .uuid + .as_ref() + .ok_or_else(|| anyhow!("missing uuid in {}", devinfo.name))?; + Ok(uuid.to_string()) } } @@ -813,46 +890,6 @@ fn read_sysfs_dev_block_value(maj: u64, min: u64, field: &str) -> Result Ok(read_to_string(&path)?.trim_end().into()) } -pub fn lsblk_single(dev: &Path) -> Result> { - let mut devinfos = lsblk(Path::new(dev), false)?; - if devinfos.is_empty() { - // this should never happen because `lsblk` itself would've failed - bail!("no lsblk results for {}", dev.display()); - } - Ok(devinfos.remove(0)) -} - -pub fn lsblk(dev: &Path, with_deps: bool) -> 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("--output") - .arg("NAME,LABEL,FSTYPE,TYPE,MOUNTPOINT,UUID") - .arg(dev); - if !with_deps { - cmd.arg("--nodeps"); - } - let output = cmd_output(&mut cmd)?; - let mut result: Vec> = Vec::new(); - for line in output.lines() { - // parse key-value pairs - result.push(split_lsblk_line(line)); - } - Ok(result) -} - -/// 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 { - let re = Regex::new(r#"([A-Z-]+)="([^"]+)""#).unwrap(); - let mut fields: HashMap = HashMap::new(); - for cap in re.captures_iter(line) { - fields.insert(cap[1].to_string(), cap[2].to_string()); - } - fields -} - pub fn get_blkdev_deps(device: &Path) -> Result> { let deps = { let mut p = PathBuf::from("/sys/block"); @@ -1041,46 +1078,10 @@ mod ioctl { #[cfg(test)] mod tests { use super::*; - use maplit::hashmap; use std::io::copy; use tempfile::tempfile; use xz2::read::XzDecoder; - #[test] - fn lsblk_split() { - assert_eq!( - split_lsblk_line(r#"NAME="sda" LABEL="" FSTYPE="""#), - hashmap! { - String::from("NAME") => String::from("sda"), - } - ); - assert_eq!( - split_lsblk_line(r#"NAME="sda1" LABEL="" FSTYPE="vfat""#), - hashmap! { - String::from("NAME") => String::from("sda1"), - String::from("FSTYPE") => String::from("vfat") - } - ); - assert_eq!( - split_lsblk_line(r#"NAME="sda2" LABEL="boot" FSTYPE="ext4""#), - hashmap! { - String::from("NAME") => String::from("sda2"), - String::from("LABEL") => String::from("boot"), - String::from("FSTYPE") => String::from("ext4"), - } - ); - assert_eq!( - split_lsblk_line(r#"NAME="sda3" LABEL="foo=\x22bar\x22 baz" FSTYPE="ext4""#), - hashmap! { - String::from("NAME") => String::from("sda3"), - // for now, we don't care about resolving lsblk's hex escapes, - // so we just pass them through - String::from("LABEL") => String::from(r#"foo=\x22bar\x22 baz"#), - String::from("FSTYPE") => String::from("ext4"), - } - ); - } - #[test] fn disk_sector_size_reader() { struct Test {