Merge pull request from dezgeg/blockdev

blockdev: Add tool
This commit is contained in:
Sylvestre Ledru 2025-02-05 18:19:41 +01:00 committed by GitHub
commit 9f5f567d90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 548 additions and 0 deletions

12
Cargo.lock generated

@ -1043,6 +1043,7 @@ dependencies = [
"rlimit",
"tempfile",
"textwrap",
"uu_blockdev",
"uu_ctrlaltdel",
"uu_dmesg",
"uu_fsfreeze",
@ -1056,6 +1057,17 @@ dependencies = [
"xattr",
]
[[package]]
name = "uu_blockdev"
version = "0.0.1"
dependencies = [
"clap",
"linux-raw-sys 0.7.0",
"regex",
"sysinfo",
"uucore",
]
[[package]]
name = "uu_ctrlaltdel"
version = "0.0.1"

@ -26,6 +26,7 @@ default = ["feat_common_core"]
uudoc = []
feat_common_core = [
"blockdev",
"ctrlaltdel",
"dmesg",
"fsfreeze",
@ -67,6 +68,7 @@ phf = { workspace = true }
textwrap = { workspace = true }
uucore = { workspace = true }
blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" }
ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" }
dmesg = { optional = true, version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg" }
fsfreeze = { optional = true, version = "0.0.1", package = "uu_fsfreeze", path = "src/uu/fsfreeze" }

@ -0,0 +1,19 @@
[package]
name = "uu_blockdev"
version = "0.0.1"
edition = "2021"
description = "blockdev ~ Get or set various block device attributes."
[lib]
path = "src/blockdev.rs"
[[bin]]
name = "blockdev"
path = "src/main.rs"
[dependencies]
clap = { workspace = true }
linux-raw-sys = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
uucore = { workspace = true }

@ -0,0 +1,8 @@
# blockdev
```
blockdev <COMMAND...> <DEVICE...>
blockdev --report <DEVICE...>
```
Get or set various block device attributes.

@ -0,0 +1,432 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use clap::{crate_version, value_parser, Arg, ArgAction, Command};
use linux_raw_sys::ioctl::*;
#[cfg(target_os = "linux")]
use std::collections::BTreeMap;
#[cfg(target_os = "linux")]
use uucore::error::USimpleError;
use uucore::{error::UResult, format_usage, help_about, help_usage};
const ABOUT: &str = help_about!("blockdev.md");
const USAGE: &str = help_usage!("blockdev.md");
#[derive(Copy, Clone, Debug)]
enum IoctlArgType {
Short,
Int,
Long,
U64Sectors,
U64,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
enum IoctlCommand {
GetAttribute(IoctlArgType),
SetAttribute,
Operation(u32),
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
enum BlockdevCommand {
SetVerbosity(bool),
Ioctl(&'static str, u32, IoctlCommand),
}
const BLOCKDEV_ACTIONS: &[(&str, BlockdevCommand)] = &[
("verbose", BlockdevCommand::SetVerbosity(true)),
("quiet", BlockdevCommand::SetVerbosity(false)),
(
"flushbufs",
BlockdevCommand::Ioctl("flush buffers", BLKFLSBUF, IoctlCommand::Operation(0)),
),
(
"getalignoff",
BlockdevCommand::Ioctl(
"get alignment offset in bytes",
BLKALIGNOFF,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getbsz",
BlockdevCommand::Ioctl(
"get blocksize",
BLKBSZGET,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getdiscardzeroes",
BlockdevCommand::Ioctl(
"get discard zeroes support status",
BLKDISCARDZEROES,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getfra",
BlockdevCommand::Ioctl(
"get filesystem readahead",
BLKFRAGET,
IoctlCommand::GetAttribute(IoctlArgType::Long),
),
),
(
"getiomin",
BlockdevCommand::Ioctl(
"get minimum I/O size",
BLKIOMIN,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getioopt",
BlockdevCommand::Ioctl(
"get optimal I/O size",
BLKIOOPT,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getmaxsect",
BlockdevCommand::Ioctl(
"get max sectors per request",
BLKSECTGET,
IoctlCommand::GetAttribute(IoctlArgType::Short),
),
),
(
"getpbsz",
BlockdevCommand::Ioctl(
"get physical block (sector) size",
BLKPBSZGET,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getra",
BlockdevCommand::Ioctl(
"get readahead",
BLKRAGET,
IoctlCommand::GetAttribute(IoctlArgType::Long),
),
),
(
"getro",
BlockdevCommand::Ioctl(
"get read-only",
BLKROGET,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getsize64",
BlockdevCommand::Ioctl(
"get size in bytes",
BLKGETSIZE64,
IoctlCommand::GetAttribute(IoctlArgType::U64),
),
),
(
"getsize",
BlockdevCommand::Ioctl(
"get 32-bit sector count (deprecated, use --getsz)",
BLKGETSIZE,
IoctlCommand::GetAttribute(IoctlArgType::Long),
),
),
(
"getss",
BlockdevCommand::Ioctl(
"get logical block (sector) size",
BLKSSZGET,
IoctlCommand::GetAttribute(IoctlArgType::Int),
),
),
(
"getsz",
BlockdevCommand::Ioctl(
"get size in 512-byte sectors",
BLKGETSIZE64,
IoctlCommand::GetAttribute(IoctlArgType::U64Sectors),
),
),
(
"rereadpt",
BlockdevCommand::Ioctl(
"reread partition table",
BLKRRPART,
IoctlCommand::Operation(0),
),
),
(
"setbsz",
BlockdevCommand::Ioctl("set blocksize", BLKBSZSET, IoctlCommand::SetAttribute),
),
(
"setfra",
BlockdevCommand::Ioctl(
"set filesystem readahead",
BLKFRASET,
IoctlCommand::SetAttribute,
),
),
(
"setra",
BlockdevCommand::Ioctl("set readahead", BLKRASET, IoctlCommand::SetAttribute),
),
(
"setro",
BlockdevCommand::Ioctl("set read-only", BLKROSET, IoctlCommand::Operation(1)),
),
(
"setrw",
BlockdevCommand::Ioctl("set read-write", BLKROSET, IoctlCommand::Operation(0)),
),
];
#[cfg(target_os = "linux")]
mod linux {
use crate::*;
use std::{fs::File, io, os::fd::AsRawFd};
use std::{io::Read, os::unix::fs::MetadataExt, path::Path};
use uucore::{error::UIoError, libc};
unsafe fn uu_ioctl<T>(device_file: &File, ioctl_code: u32, input: T) -> UResult<()> {
if libc::ioctl(device_file.as_raw_fd(), ioctl_code.into(), input) < 0 {
Err(Box::new(UIoError::from(io::Error::last_os_error())))
} else {
Ok(())
}
}
unsafe fn get_ioctl_attribute(
device_file: &File,
ioctl_code: u32,
ioctl_type: IoctlArgType,
) -> UResult<u64> {
unsafe fn ioctl_get<T: Default + Into<u64>>(
device: &File,
ioctl_code: u32,
) -> UResult<u64> {
let mut retval: T = Default::default();
uu_ioctl(device, ioctl_code, &mut retval as *mut T as usize).map(|_| retval.into())
}
match ioctl_type {
IoctlArgType::Int => ioctl_get::<libc::c_uint>(device_file, ioctl_code),
IoctlArgType::Long => ioctl_get::<libc::c_ulong>(device_file, ioctl_code),
IoctlArgType::Short => ioctl_get::<libc::c_ushort>(device_file, ioctl_code),
IoctlArgType::U64 => ioctl_get::<u64>(device_file, ioctl_code),
IoctlArgType::U64Sectors => Ok(ioctl_get::<u64>(device_file, ioctl_code)? / 512),
}
}
fn get_partition_offset(device_file: &File) -> UResult<usize> {
let rdev = device_file.metadata()?.rdev();
let major = unsafe { libc::major(rdev) };
let minor = unsafe { libc::minor(rdev) };
if Path::new(&format!("/sys/dev/block/{}:{}/partition", major, minor)).exists() {
let mut start_fd = File::open(format!("/sys/dev/block/{}:{}/start", major, minor))?;
let mut str = String::new();
start_fd.read_to_string(&mut str)?;
return str
.trim()
.parse()
.map_err(|_| USimpleError::new(1, "Unable to parse partition start offset"));
}
Ok(0)
}
pub fn do_report(device_path: &str) -> UResult<()> {
let device_file = File::open(device_path)?;
let partition_offset = get_partition_offset(&device_file)?;
let report_ioctls = &["getro", "getra", "getss", "getbsz", "getsize64"];
let ioctl_values = report_ioctls
.iter()
.map(|flag| {
let Some((
_,
BlockdevCommand::Ioctl(_, ioctl_code, IoctlCommand::GetAttribute(ioctl_type)),
)) = BLOCKDEV_ACTIONS.iter().find(|(n, _)| flag == n)
else {
unreachable!()
};
unsafe { get_ioctl_attribute(&device_file, *ioctl_code, *ioctl_type) }
})
.collect::<Result<Vec<u64>, _>>()?;
println!(
"{} {:5} {:5} {:5} {:15} {:15} {}",
if ioctl_values[0] == 1 { "ro" } else { "rw" },
ioctl_values[1],
ioctl_values[2],
ioctl_values[3],
partition_offset,
ioctl_values[4],
device_path
);
Ok(())
}
pub fn do_ioctl_command(
device: &File,
name: &str,
ioctl_code: u32,
ioctl_action: &IoctlCommand,
verbose: bool,
arg: usize,
) -> UResult<()> {
match ioctl_action {
IoctlCommand::GetAttribute(ioctl_type) => {
let ret = unsafe { get_ioctl_attribute(device, ioctl_code, *ioctl_type)? };
if verbose {
println!("{}: {}", name, ret);
} else {
println!("{}", ret);
}
}
IoctlCommand::SetAttribute => {
unsafe { uu_ioctl(device, ioctl_code, arg)? };
if verbose {
println!("{} succeeded.", name);
}
}
IoctlCommand::Operation(param) => {
unsafe { uu_ioctl(device, ioctl_code, param)? };
if verbose {
println!("{} succeeded.", name);
}
}
};
Ok(())
}
}
#[cfg(target_os = "linux")]
use linux::*;
#[cfg(target_os = "linux")]
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
use std::fs::File;
let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
let devices = matches
.get_many::<String>("devices")
.expect("Required command-line argument");
if matches.get_flag("report") {
println!("RO RA SSZ BSZ StartSec Size Device");
for device_path in devices {
uucore::show_if_err!(do_report(device_path));
}
Ok(())
} else {
// Recover arguments from clap in the same order they were passed
// Based on https://docs.rs/clap/latest/clap/_cookbook/find/index.html
let mut operations = BTreeMap::new();
for (id, op) in BLOCKDEV_ACTIONS {
if matches.value_source(id) != Some(clap::parser::ValueSource::CommandLine) {
continue;
}
let indices = matches.indices_of(id).unwrap();
let values = matches.get_many::<usize>(id).unwrap();
for (index, value) in indices.zip(values) {
operations.insert(index, (op.clone(), *value));
}
}
for device_path in devices {
let mut verbose = false;
let device_file = File::open(device_path)?;
for (operation, value) in operations.values() {
match operation {
BlockdevCommand::SetVerbosity(true) => verbose = true,
BlockdevCommand::SetVerbosity(false) => verbose = false,
BlockdevCommand::Ioctl(description, ioctl_code, ioctl_action) => {
if let Err(e) = do_ioctl_command(
&device_file,
description,
*ioctl_code,
ioctl_action,
verbose,
*value,
) {
if verbose {
println!("{} failed.", description);
}
return Err(e);
}
}
}
}
}
Ok(())
}
}
pub fn uu_app() -> Command {
let mut cmd = Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.arg(
Arg::new("report")
.long("report")
.help("print report for specified devices")
.action(ArgAction::SetTrue),
)
.arg(Arg::new("devices").required(true).action(ArgAction::Append));
for (flag, action) in BLOCKDEV_ACTIONS {
let mut arg = Arg::new(flag)
.long(flag)
.conflicts_with("report")
.action(ArgAction::Append)
.value_parser(value_parser!(usize));
match action {
BlockdevCommand::SetVerbosity(true) => {
arg = arg.short('v').help("verbose mode");
}
BlockdevCommand::SetVerbosity(false) => {
arg = arg.short('q').help("quiet mode");
}
BlockdevCommand::Ioctl(name, _, _) => {
arg = arg.help(name);
}
}
match action {
BlockdevCommand::Ioctl(_, _, IoctlCommand::SetAttribute) => {
arg = arg.num_args(1);
}
_ => {
arg = arg
.num_args(0)
.default_value("0")
.default_missing_value("0");
}
}
cmd = cmd.arg(arg);
}
cmd
}
#[cfg(not(target_os = "linux"))]
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
Err(uucore::error::USimpleError::new(
1,
"`blockdev` is available only on Linux.",
))
}

@ -0,0 +1 @@
uucore::bin!(uu_blockdev);

@ -0,0 +1,70 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::common::util::TestScenario;
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
}
#[test]
fn test_report_mutually_exclusive_with_others() {
new_ucmd!()
.arg("--report")
.arg("--getalignoff")
.arg("/foo")
.fails()
.code_is(1)
.stderr_contains("the argument '--report' cannot be used with '--getalignoff'");
}
#[cfg(target_os = "linux")]
mod linux {
use crate::common::util::TestScenario;
use regex::Regex;
#[test]
fn test_fails_on_first_error() {
new_ucmd!()
.arg("-v")
.arg("--getalignoff")
.arg("--getbsz")
.arg("/dev/null")
.fails()
.code_is(1)
.stdout_is("get alignment offset in bytes failed.\n")
.stderr_contains("Inappropriate ioctl for device");
}
#[test]
fn test_report_continues_on_errors() {
new_ucmd!()
.arg("--report")
.arg("/dev/null")
.arg("/non/existing")
.fails()
.code_is(1)
.stderr_matches(
&Regex::new("(?ms)Inappropriate ioctl for device.*No such file or directory")
.unwrap(),
);
}
}
#[cfg(not(target_os = "linux"))]
mod non_linux {
use crate::common::util::TestScenario;
#[test]
fn test_fails_on_unsupported_platforms() {
new_ucmd!()
.arg("--report")
.arg("/dev/null")
.fails()
.code_is(1)
.stderr_is("blockdev: `blockdev` is available only on Linux.\n");
}
}

@ -17,6 +17,10 @@ mod test_lsmem;
#[path = "by-util/test_mountpoint.rs"]
mod test_mountpoint;
#[cfg(feature = "blockdev")]
#[path = "by-util/test_blockdev.rs"]
mod test_blockdev;
#[cfg(feature = "ctrlaltdel")]
#[path = "by-util/test_ctrlaltdel.rs"]
mod test_ctrlaltdel;