blob: 7bc5ecb28da337bc7bd8d067b3eaffcbda4b118d [file] [log] [blame] [edit]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::path::Path;
use std::process;
use anyhow::Error;
use serde::Deserialize;
use crate::util::get_command_output;
/// Struct for deserializing the JSON output of `lsblk`.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct LsBlkDevice {
/// Device name.
///
/// This is a full path because lsblk is run with "--paths".
pub name: String,
/// Internal kernel device name.
#[serde(rename = "kname")]
pub kernel_name: String,
/// Whether the device is removable.
///
/// Note that this uses the "hotplug" property rather than the "rm"
/// property from lsblk. The hotplug property is broader, for
/// example it includes USB HDDs.
#[serde(rename = "hotplug")]
pub is_removable: bool,
/// Size in bytes.
#[serde(rename = "size")]
pub size_in_bytes: u64,
/// Device type.
#[serde(rename = "type")]
pub device_type: String,
}
impl LsBlkDevice {
/// Get the partition number, e.g. for `/dev/sda12` this returns 12.
pub fn partition_number(&self) -> Option<u64> {
if self.device_type != "part" {
return None;
}
// Find the index of the last non-numeric character.
let index = self.name.rfind(|c: char| !c.is_ascii_digit())?;
// Parse the rest of the string past that index into a number.
let num_part = &self.name[index + 1..];
num_part.parse().ok()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
struct LsBlkDeviceWithChildren {
#[serde(flatten)]
details: LsBlkDevice,
/// Child devices.
#[serde(default)]
children: Vec<LsBlkDeviceWithChildren>,
}
#[derive(Debug, Deserialize, PartialEq)]
struct LsBlkOutput {
#[serde(rename = "blockdevices")]
block_devices: Vec<LsBlkDeviceWithChildren>,
}
impl LsBlkOutput {
fn parse(input: &[u8]) -> Result<LsBlkOutput, serde_json::Error> {
serde_json::from_slice(input)
}
fn flattened(self) -> Vec<LsBlkDevice> {
let mut output = Vec::new();
let mut stack = self.block_devices;
while let Some(device) = stack.pop() {
output.push(device.details);
stack.extend(device.children);
}
output
}
}
/// Capture information about block devices from lsblk.
///
/// lsblk is a convenient tool that already exists on CrOS base builds
/// and in most other linux distributions. Using the "--json" flag
/// makes the output easily parsible.
///
/// target: Block device to show information about. It will limit
/// lsblk to only return information about partitions on the target
/// device. If target is None lsblk will return information about most
/// block devices, excluding the zram device and slow devices such as
/// floppy drives.
///
/// Returns the raw output of lsblk.
fn get_lsblk_output(target_drive: Option<&Path>) -> Result<Vec<u8>, Error> {
let mut command = process::Command::new("lsblk");
command.args([
// Print size in bytes
"--bytes",
// Select the fields to output
"--output",
"KNAME,NAME,HOTPLUG,SIZE,TYPE",
// Format output as JSON
"--json",
// Print full device paths
"--paths",
// Exclude some devices by major number. See
// https://www.kernel.org/doc/Documentation/admin-guide/devices.txt
// for a list of major numbers.
//
// - Exclude floppy drives (2), as they are slow.
// - Exclude scsi cdrom drives (11), as they are slow.
// - Exclude zram (253), not a valid install target.
"--exclude",
"2,11,253",
]);
if let Some(target_drive) = target_drive {
command.arg(target_drive);
}
Ok(get_command_output(command)?)
}
/// Capture information about block devices from lsblk.
///
/// target: Block device to show information about. It will limit
/// lsblk to only return information about partitions on the target
/// device. If target is None lsblk will return information about most
/// block devices, excluding the zram device and slow devices such as
/// floppy drives.
///
/// Returns a flattened vector of devices.
pub fn get_lsblk_devices(target_drive: Option<&Path>) -> Result<Vec<LsBlkDevice>, Error> {
let output = get_lsblk_output(target_drive)?;
let parsed = LsBlkOutput::parse(&output)?;
Ok(parsed.flattened())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partition_number() {
let mkdev = |path: &str| LsBlkDevice {
kernel_name: path.into(),
name: path.into(),
is_removable: false,
size_in_bytes: 0,
device_type: "part".into(),
};
// Valid partition devices.
assert_eq!(mkdev("/dev/sda1").partition_number(), Some(1));
assert_eq!(mkdev("/dev/sda12").partition_number(), Some(12));
assert_eq!(mkdev("/dev/nvme0n1p3").partition_number(), Some(3));
// Doesn't end in a number.
assert_eq!(mkdev("/dev/dev").partition_number(), None);
// Not a partition-type device.
let mut dev = mkdev("/dev/sda1");
dev.device_type = "disk".into();
assert_eq!(dev.partition_number(), None);
}
fn mkdev(
kname: &str,
name: &str,
is_removable: bool,
size_in_bytes: u64,
dtype: &str,
) -> LsBlkDevice {
LsBlkDevice {
kernel_name: kname.into(),
name: name.into(),
is_removable,
size_in_bytes,
device_type: dtype.into(),
}
}
#[test]
fn test_lsblk_deserialization() {
// This test input was generated by running this command in a VM:
//
// lsblk --bytes --output KNAME,NAME,HOTPLUG,SIZE,TYPE \
// --json --paths --exclude 2,11,253
let input = include_bytes!("test_lsblk_output.json");
#[rustfmt::skip]
let expected = vec![
mkdev("/dev/sda", "/dev/sda", false, 6939566592, "disk"),
mkdev("/dev/sda12", "/dev/sda12", false, 67108864, "part"),
mkdev("/dev/sda11", "/dev/sda11", false, 512, "part"),
mkdev("/dev/sda10", "/dev/sda10", false, 512, "part"),
mkdev("/dev/sda9", "/dev/sda9", false, 512, "part"),
mkdev("/dev/sda8", "/dev/sda8", false, 16777216, "part"),
mkdev("/dev/sda7", "/dev/sda7", false, 512, "part"),
mkdev("/dev/sda6", "/dev/sda6", false, 512, "part"),
mkdev("/dev/sda5", "/dev/sda5", false, 2097152, "part"),
mkdev("/dev/sda4", "/dev/sda4", false, 16777216, "part"),
mkdev("/dev/sda3", "/dev/sda3", false, 2516582400, "part"),
mkdev("/dev/sda2", "/dev/sda2", false, 16777216, "part"),
mkdev("/dev/sda1", "/dev/sda1", false, 4301324800, "part"),
mkdev("/dev/loop4", "/dev/loop4", false, 6475776, "loop"),
mkdev("/dev/loop3", "/dev/loop3", false, 9670656, "loop"),
mkdev("/dev/loop2", "/dev/loop2", false, 102133760, "loop"),
mkdev("/dev/loop1", "/dev/loop1", false, 1243103232, "loop"),
mkdev("/dev/dm-1", "/dev/mapper/encstateful", false, 1243103232, "dm"),
mkdev("/dev/loop0", "/dev/loop0", false, 4096, "loop"),
];
let output = LsBlkOutput::parse(input).unwrap();
assert_eq!(output.flattened(), expected);
}
}