blob: 30abd622dd4843af8643bb2990fa40015410d9ed [file] [log] [blame]
// Copyright 2023 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::process;
use anyhow::bail;
use anyhow::Result;
use log::info;
use serde::Deserialize;
use std::process::Command;
/// 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,
/// Device type.
#[serde(rename = "type")]
pub device_type: String,
}
#[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> {
Ok(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() -> Result<Vec<u8>> {
let mut command = process::Command::new("/bin/lsblk");
command.args([
// Select the fields to output
"--output",
"NAME,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",
]);
get_command_output(command)
}
/// Capture information about block devices from lsblk.
///
/// Returns a flattened vector of devices.
pub fn get_lsblk_devices() -> Result<Vec<LsBlkDevice>> {
let output = get_lsblk_output()?;
let parsed = LsBlkOutput::parse(&output)?;
Ok(parsed.flattened())
}
/// Run a command and get its stdout as raw bytes. An error is
/// returned if the process fails to launch, or if it exits non-zero.
fn get_command_output(mut command: Command) -> Result<Vec<u8>> {
info!("running command: {:?}", command);
let output = match command.output() {
Ok(output) => output,
Err(err) => {
bail!("Failed to execute command: {err}");
}
};
if !output.status.success() {
bail!("Failed to execute command");
}
Ok(output.stdout)
}
#[cfg(test)]
mod tests {
use super::*;
fn mkdev(name: &str, dtype: &str) -> LsBlkDevice {
LsBlkDevice {
name: name.into(),
device_type: dtype.into(),
}
}
#[test]
fn test_lsblk_deserialization() {
// This test input was generated by running this command in a VM:
//
// lsblk --bytes --output NAME,TYPE \
// --json --paths --exclude 2,11,253
let input = include_bytes!("test_lsblk_output.json");
#[rustfmt::skip]
let expected = vec![
mkdev("/dev/sda", "disk"),
mkdev("/dev/sda12", "part"),
mkdev("/dev/sda11", "part"),
mkdev("/dev/sda10", "part"),
mkdev("/dev/sda9", "part"),
mkdev("/dev/sda8", "part"),
mkdev("/dev/sda7", "part"),
mkdev("/dev/sda6", "part"),
mkdev("/dev/sda5", "part"),
mkdev("/dev/sda4", "part"),
mkdev("/dev/sda3", "part"),
mkdev("/dev/sda2", "part"),
mkdev("/dev/sda1", "part"),
mkdev("/dev/loop4", "loop"),
mkdev("/dev/loop3", "loop"),
mkdev("/dev/loop2", "loop"),
mkdev("/dev/loop1", "loop"),
mkdev("/dev/mapper/encstateful", "dm"),
mkdev("/dev/loop0", "loop"),
];
let output = LsBlkOutput::parse(input).unwrap();
assert_eq!(output.flattened(), expected);
}
}