pub mod nvme;
use fstab::{FsEntry, FsTab};
use log::{debug, warn};
use uuid::Uuid;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt;
use std::fs::{self, read_dir, File};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output};
use std::str::FromStr;
use strum::{Display, EnumString, IntoStaticStr};
use thiserror::Error;
pub type BlockResult<T> = Result<T, BlockUtilsError>;
#[cfg(test)]
mod tests {
use nix::unistd::{close, ftruncate};
use tempfile::TempDir;
use std::fs::File;
use std::os::unix::io::IntoRawFd;
#[test]
fn test_create_xfs() {
let tmp_dir = TempDir::new().unwrap();
let file_path = tmp_dir.path().join("xfs_device");
let f = File::create(&file_path).expect("Failed to create file");
let fd = f.into_raw_fd();
ftruncate(fd, 104_857_600).unwrap();
let xfs_options = super::Filesystem::Xfs {
stripe_size: None,
stripe_width: None,
block_size: None,
inode_size: Some(512),
force: false,
agcount: Some(32),
};
let result = super::format_block_device(&file_path, &xfs_options);
println!("Result: {:?}", result);
close(fd).expect("Failed to close file descriptor");
}
#[test]
fn test_create_ext4() {
let tmp_dir = TempDir::new().unwrap();
let file_path = tmp_dir.path().join("ext4_device");
let f = File::create(&file_path).expect("Failed to create file");
let fd = f.into_raw_fd();
ftruncate(fd, 104_857_600).unwrap();
let xfs_options = super::Filesystem::Ext4 {
inode_size: 512,
stride: Some(2),
stripe_width: None,
reserved_blocks_percentage: 10,
};
let result = super::format_block_device(&file_path, &xfs_options);
println!("Result: {:?}", result);
close(fd).expect("Failed to close file descriptor");
}
}
const MTAB_PATH: &str = "/etc/mtab";
#[derive(Debug, Error)]
pub enum BlockUtilsError {
#[error("BlockUtilsError : {0}")]
Error(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
ParseBoolError(#[from] std::str::ParseBoolError),
#[error(transparent)]
ParseIntError(#[from] std::num::ParseIntError),
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
#[error(transparent)]
StrumParseError(#[from] strum::ParseError),
}
impl BlockUtilsError {
fn new(err: String) -> BlockUtilsError {
BlockUtilsError::Error(err)
}
}
#[derive(Clone, Debug, Display)]
#[strum(serialize_all = "snake_case")]
pub enum MetadataProfile {
Raid0,
Raid1,
Raid5,
Raid6,
Raid10,
Single,
Dup,
}
#[derive(Clone, Debug, EnumString)]
pub enum Vendor {
#[strum(serialize = "ATA")]
None,
#[strum(serialize = "CISCO")]
Cisco,
#[strum(serialize = "HP", serialize = "hp", serialize = "HPE")]
Hp,
#[strum(serialize = "LSI")]
Lsi,
#[strum(serialize = "QEMU")]
Qemu,
#[strum(serialize = "VBOX")]
Vbox, #[strum(serialize = "NECVMWar")]
NECVMWar, #[strum(serialize = "VMware")]
VMware, }
#[derive(Clone, Debug)]
pub struct Device {
pub id: Option<Uuid>,
pub name: String,
pub media_type: MediaType,
pub device_type: DeviceType,
pub capacity: u64,
pub fs_type: FilesystemType,
pub serial_number: Option<String>,
pub logical_block_size: Option<u64>,
pub physical_block_size: Option<u64>,
}
impl Device {
#[cfg(target_os = "linux")]
fn from_udev_device(device: udev::Device) -> BlockResult<Self> {
let sys_name = device.sysname();
let id: Option<Uuid> = get_uuid(&device);
let serial = get_serial(&device);
let media_type = get_media_type(&device);
let device_type = get_device_type(&device)?;
let capacity = match get_size(&device) {
Some(size) => size,
None => 0,
};
let logical_block_size = get_udev_int_val(&device, "queue/logical_block_size");
let physical_block_size = get_udev_int_val(&device, "queue/physical_block_size");
let fs_type = get_fs_type(&device)?;
Ok(Device {
id,
name: sys_name.to_string_lossy().to_string(),
media_type,
device_type,
capacity,
fs_type,
serial_number: serial,
logical_block_size,
physical_block_size,
})
}
fn from_fs_entry(fs_entry: FsEntry) -> BlockResult<Self> {
Ok(Device {
id: None,
name: Path::new(&fs_entry.fs_spec)
.file_name()
.unwrap_or_else(|| OsStr::new(""))
.to_string_lossy()
.into_owned(),
media_type: MediaType::Unknown,
device_type: DeviceType::Unknown,
capacity: 0,
fs_type: FilesystemType::from_str(&fs_entry.vfs_type)?,
serial_number: None,
logical_block_size: None,
physical_block_size: None,
})
}
}
#[derive(Debug)]
pub struct AsyncInit {
pub format_child: Child,
pub post_setup_commands: Vec<(String, Vec<String>)>,
pub device: PathBuf,
}
#[derive(Debug, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum Scheduler {
Cfq,
Deadline,
Noop,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MediaType {
SolidState,
Rotational,
Loopback,
LVM,
MdRaid,
NVME,
Ram,
Virtual,
Unknown,
}
#[derive(Clone, Debug, Eq, PartialEq, Display, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum DeviceType {
Disk,
Partition,
Unknown,
}
impl FromStr for DeviceType {
type Err = BlockUtilsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
match s.as_ref() {
"disk" => Ok(DeviceType::Disk),
"partition" => Ok(DeviceType::Partition),
_ => Ok(DeviceType::Unknown),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum FilesystemType {
Btrfs,
Ext2,
Ext3,
Ext4,
#[strum(serialize = "lvm2_member")]
Lvm,
Xfs,
Zfs,
Ntfs,
Vfat,
#[strum(default)]
Unrecognised(String),
#[strum(serialize = "")]
Unknown,
}
impl FilesystemType {
pub fn to_str(&self) -> &str {
match *self {
FilesystemType::Btrfs => "btrfs",
FilesystemType::Ext2 => "ext2",
FilesystemType::Ext3 => "ext3",
FilesystemType::Ext4 => "ext4",
FilesystemType::Lvm => "lvm",
FilesystemType::Xfs => "xfs",
FilesystemType::Zfs => "zfs",
FilesystemType::Vfat => "vfat",
FilesystemType::Ntfs => "ntfs",
FilesystemType::Unrecognised(ref name) => name.as_str(),
FilesystemType::Unknown => "unknown",
}
}
}
impl fmt::Display for FilesystemType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let string = match *self {
FilesystemType::Btrfs => "btrfs".to_string(),
FilesystemType::Ext2 => "ext2".to_string(),
FilesystemType::Ext3 => "ext3".to_string(),
FilesystemType::Ext4 => "ext4".to_string(),
FilesystemType::Lvm => "lvm".to_string(),
FilesystemType::Xfs => "xfs".to_string(),
FilesystemType::Zfs => "zfs".to_string(),
FilesystemType::Vfat => "vfat".to_string(),
FilesystemType::Ntfs => "ntfs".to_string(),
FilesystemType::Unrecognised(ref name) => name.clone(),
FilesystemType::Unknown => "unknown".to_string(),
};
write!(f, "{}", string)
}
}
#[derive(Debug)]
pub enum Filesystem {
Btrfs {
leaf_size: u64,
metadata_profile: MetadataProfile,
node_size: u64,
},
Ext4 {
inode_size: u64,
reserved_blocks_percentage: u8,
stride: Option<u64>,
stripe_width: Option<u64>,
},
Xfs {
block_size: Option<u64>, force: bool,
inode_size: Option<u64>,
stripe_size: Option<u64>, stripe_width: Option<u64>, agcount: Option<u64>, },
Zfs {
block_size: Option<u64>,
compression: Option<bool>,
},
}
impl Filesystem {
pub fn new(name: &str) -> Filesystem {
match name.trim() {
"zfs" => Filesystem::Zfs {
block_size: None,
compression: None,
},
"xfs" => Filesystem::Xfs {
stripe_size: None,
stripe_width: None,
block_size: None,
inode_size: Some(512),
force: false,
agcount: Some(32),
},
"btrfs" => Filesystem::Btrfs {
metadata_profile: MetadataProfile::Single,
leaf_size: 32768,
node_size: 32768,
},
"ext4" => Filesystem::Ext4 {
inode_size: 512,
reserved_blocks_percentage: 0,
stride: None,
stripe_width: None,
},
_ => Filesystem::Xfs {
stripe_size: None,
stripe_width: None,
block_size: None,
inode_size: None,
force: false,
agcount: None,
},
}
}
}
fn run_command<S: AsRef<OsStr>>(command: &str, arg_list: &[S]) -> BlockResult<Output> {
Ok(Command::new(command).args(arg_list).output()?)
}
pub fn mount_device(device: &Device, mount_point: impl AsRef<Path>) -> BlockResult<i32> {
let mut arg_list: Vec<String> = Vec::new();
match device.id {
Some(id) => {
arg_list.push("-U".to_string());
arg_list.push(id.hyphenated().to_string());
}
None => {
arg_list.push(format!("/dev/{}", device.name));
}
};
arg_list.push(mount_point.as_ref().to_string_lossy().into_owned());
debug!("mount: {:?}", arg_list);
process_output(&run_command("mount", &arg_list)?)
}
pub fn unmount_device(mount_point: impl AsRef<Path>) -> BlockResult<i32> {
let mut arg_list: Vec<String> = Vec::new();
arg_list.push(mount_point.as_ref().to_string_lossy().into_owned());
process_output(&run_command("umount", &arg_list)?)
}
pub fn get_mount_device(mount_dir: impl AsRef<Path>) -> BlockResult<Option<PathBuf>> {
let dir = mount_dir.as_ref().to_string_lossy().into_owned();
let f = File::open(MTAB_PATH)?;
let reader = BufReader::new(f);
for line in reader.lines() {
let line = line?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.contains(&dir.as_str()) {
if !parts.is_empty() {
return Ok(Some(PathBuf::from(parts[0])));
}
}
}
Ok(None)
}
pub fn get_mounted_devices_iter() -> BlockResult<impl Iterator<Item = BlockResult<Device>>> {
Ok(FsTab::new(Path::new(MTAB_PATH))
.get_entries()?
.into_iter()
.filter(|d| d.fs_spec.contains("/dev/"))
.filter(|d| !d.fs_spec.contains("mapper"))
.map(Device::from_fs_entry))
}
pub fn get_mounted_devices() -> BlockResult<Vec<Device>> {
get_mounted_devices_iter()?.collect()
}
pub fn get_mountpoint(device: impl AsRef<Path>) -> BlockResult<Option<PathBuf>> {
let s = device.as_ref().to_string_lossy().into_owned();
let f = File::open(MTAB_PATH)?;
let reader = BufReader::new(f);
for line in reader.lines() {
let l = line?;
let parts: Vec<&str> = l.split_whitespace().collect();
let mut index = -1;
for (i, p) in parts.iter().enumerate() {
if p == &s {
index = i as i64;
}
}
if index >= 0 {
return Ok(Some(PathBuf::from(parts[1])));
}
}
Ok(None)
}
fn process_output(output: &Output) -> BlockResult<i32> {
if output.status.success() {
Ok(0)
} else {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Err(BlockUtilsError::new(stderr))
}
}
pub fn erase_block_device(device: impl AsRef<Path>) -> BlockResult<()> {
let output = Command::new("sgdisk")
.args(&["--zap", &device.as_ref().to_string_lossy()])
.output()?;
if output.status.success() {
Ok(())
} else {
Err(BlockUtilsError::new(format!(
"Disk {:?} failed to erase: {}",
device.as_ref(),
String::from_utf8_lossy(&output.stderr)
)))
}
}
pub fn format_block_device(device: impl AsRef<Path>, filesystem: &Filesystem) -> BlockResult<i32> {
match *filesystem {
Filesystem::Btrfs {
ref metadata_profile,
ref leaf_size,
ref node_size,
} => {
let mut arg_list: Vec<String> = Vec::new();
arg_list.push("-m".to_string());
arg_list.push(metadata_profile.clone().to_string());
arg_list.push("-l".to_string());
arg_list.push(leaf_size.to_string());
arg_list.push("-n".to_string());
arg_list.push(node_size.to_string());
arg_list.push(device.as_ref().to_string_lossy().to_string());
if !Path::new("/sbin/mkfs.btrfs").exists() {
return Err(BlockUtilsError::new(
"Please install btrfs-tools".to_string(),
));
}
process_output(&run_command("mkfs.btrfs", &arg_list)?)
}
Filesystem::Xfs {
ref inode_size,
ref force,
ref block_size,
ref stripe_size,
ref stripe_width,
ref agcount,
} => {
let mut arg_list: Vec<String> = Vec::new();
if let Some(b) = block_size {
let b: u64 = if *b < 512 {
warn!("xfs block size must be 512 bytes minimum. Correcting");
512
} else if *b > 65536 {
warn!("xfs block size must be 65536 bytes maximum. Correcting");
65536
} else {
*b
};
arg_list.push("-b".to_string());
arg_list.push(format!("size={}", b));
}
if (*inode_size).is_some() {
arg_list.push("-i".to_string());
arg_list.push(format!("size={}", inode_size.unwrap()));
}
if *force {
arg_list.push("-f".to_string());
}
if (*stripe_size).is_some() && (*stripe_width).is_some() {
arg_list.push("-d".to_string());
arg_list.push(format!("su={}", stripe_size.unwrap()));
arg_list.push(format!("sw={}", stripe_width.unwrap()));
if (*agcount).is_some() {
arg_list.push(format!("agcount={}", agcount.unwrap()));
}
}
arg_list.push(device.as_ref().to_string_lossy().to_string());
if !Path::new("/sbin/mkfs.xfs").exists() {
return Err(BlockUtilsError::new("Please install xfsprogs".into()));
}
process_output(&run_command("/sbin/mkfs.xfs", &arg_list)?)
}
Filesystem::Ext4 {
ref inode_size,
ref reserved_blocks_percentage,
ref stride,
ref stripe_width,
} => {
let mut arg_list: Vec<String> = Vec::new();
if stride.is_some() || stripe_width.is_some() {
arg_list.push("-E".to_string());
if let Some(stride) = stride {
arg_list.push(format!("stride={}", stride));
}
if let Some(stripe_width) = stripe_width {
arg_list.push(format!(",stripe_width={}", stripe_width));
}
}
arg_list.push("-I".to_string());
arg_list.push(inode_size.to_string());
arg_list.push("-m".to_string());
arg_list.push(reserved_blocks_percentage.to_string());
arg_list.push(device.as_ref().to_string_lossy().to_string());
process_output(&run_command("mkfs.ext4", &arg_list)?)
}
Filesystem::Zfs {
ref block_size,
ref compression,
} => {
if !Path::new("/sbin/zfs").exists() {
return Err(BlockUtilsError::new("Please install zfsutils-linux".into()));
}
let base_name = device.as_ref().file_name();
match base_name {
Some(name) => {
let arg_list: Vec<String> = vec![
"create".to_string(),
"-f".to_string(),
"-m".to_string(),
format!("/mnt/{}", name.to_string_lossy().into_owned()),
name.to_string_lossy().into_owned(),
device.as_ref().to_string_lossy().into_owned(),
];
let _ = process_output(&run_command("/sbin/zpool", &arg_list)?)?;
if block_size.is_some() {
let _ = process_output(&run_command(
"/sbin/zfs",
&[
"set".to_string(),
format!("recordsize={}", block_size.unwrap()),
name.to_string_lossy().into_owned(),
],
)?)?;
}
if compression.is_some() {
let _ = process_output(&run_command(
"/sbin/zfs",
&[
"set".to_string(),
"compression=on".to_string(),
name.to_string_lossy().into_owned(),
],
)?)?;
}
let _ = process_output(&run_command(
"/sbin/zfs",
&[
"set".to_string(),
"acltype=posixacl".to_string(),
name.to_string_lossy().into_owned(),
],
)?)?;
let _ = process_output(&run_command(
"/sbin/zfs",
&[
"set".to_string(),
"atime=off".to_string(),
name.to_string_lossy().into_owned(),
],
)?)?;
Ok(0)
}
None => Err(BlockUtilsError::new(format!(
"Unable to determine filename for device: {:?}",
device.as_ref()
))),
}
}
}
}
pub fn async_format_block_device(
device: impl AsRef<Path>,
filesystem: &Filesystem,
) -> BlockResult<AsyncInit> {
match *filesystem {
Filesystem::Btrfs {
ref metadata_profile,
ref leaf_size,
ref node_size,
} => {
let arg_list: Vec<String> = vec![
"-m".to_string(),
metadata_profile.clone().to_string(),
"-l".to_string(),
leaf_size.to_string(),
"-n".to_string(),
node_size.to_string(),
device.as_ref().to_string_lossy().to_string(),
];
if !Path::new("/sbin/mkfs.btrfs").exists() {
return Err(BlockUtilsError::new("Please install btrfs-tools".into()));
}
Ok(AsyncInit {
format_child: Command::new("mkfs.btrfs").args(&arg_list).spawn()?,
post_setup_commands: vec![],
device: device.as_ref().to_owned(),
})
}
Filesystem::Xfs {
ref block_size,
ref inode_size,
ref stripe_size,
ref stripe_width,
ref force,
ref agcount,
} => {
let mut arg_list: Vec<String> = Vec::new();
if (*inode_size).is_some() {
arg_list.push("-i".to_string());
arg_list.push(format!("size={}", inode_size.unwrap()));
}
if *force {
arg_list.push("-f".to_string());
}
if let Some(b) = block_size {
arg_list.push("-b".to_string());
arg_list.push(b.to_string());
}
if (*stripe_size).is_some() && (*stripe_width).is_some() {
arg_list.push("-d".to_string());
arg_list.push(format!("su={}", stripe_size.unwrap()));
arg_list.push(format!("sw={}", stripe_width.unwrap()));
if (*agcount).is_some() {
arg_list.push(format!("agcount={}", agcount.unwrap()));
}
}
arg_list.push(device.as_ref().to_string_lossy().to_string());
if !Path::new("/sbin/mkfs.xfs").exists() {
return Err(BlockUtilsError::new("Please install xfsprogs".into()));
}
let format_handle = Command::new("/sbin/mkfs.xfs").args(&arg_list).spawn()?;
Ok(AsyncInit {
format_child: format_handle,
post_setup_commands: vec![],
device: device.as_ref().to_owned(),
})
}
Filesystem::Zfs {
ref block_size,
ref compression,
} => {
if !Path::new("/sbin/zfs").exists() {
return Err(BlockUtilsError::new("Please install zfsutils-linux".into()));
}
let base_name = device.as_ref().file_name();
match base_name {
Some(name) => {
let mut post_setup_commands: Vec<(String, Vec<String>)> = Vec::new();
let arg_list: Vec<String> = vec![
"create".to_string(),
"-f".to_string(),
"-m".to_string(),
format!("/mnt/{}", name.to_string_lossy().into_owned()),
name.to_string_lossy().into_owned(),
device.as_ref().to_string_lossy().into_owned(),
];
let zpool_create = Command::new("/sbin/zpool").args(&arg_list).spawn()?;
if block_size.is_some() {
post_setup_commands.push((
"/sbin/zfs".to_string(),
vec![
"set".to_string(),
format!("recordsize={}", block_size.unwrap()),
name.to_string_lossy().into_owned(),
],
));
}
if compression.is_some() {
post_setup_commands.push((
"/sbin/zfs".to_string(),
vec![
"set".to_string(),
"compression=on".to_string(),
name.to_string_lossy().into_owned(),
],
));
}
post_setup_commands.push((
"/sbin/zfs".to_string(),
vec![
"set".to_string(),
"acltype=posixacl".to_string(),
name.to_string_lossy().into_owned(),
],
));
post_setup_commands.push((
"/sbin/zfs".to_string(),
vec![
"set".to_string(),
"atime=off".to_string(),
name.to_string_lossy().into_owned(),
],
));
Ok(AsyncInit {
format_child: zpool_create,
post_setup_commands,
device: device.as_ref().to_owned(),
})
}
None => Err(BlockUtilsError::new(format!(
"Unable to determine filename for device: {:?}",
device.as_ref()
))),
}
}
Filesystem::Ext4 {
ref inode_size,
ref reserved_blocks_percentage,
ref stride,
ref stripe_width,
} => {
let mut arg_list: Vec<String> =
vec!["-m".to_string(), reserved_blocks_percentage.to_string()];
arg_list.push("-I".to_string());
arg_list.push(inode_size.to_string());
if (*stride).is_some() {
arg_list.push("-E".to_string());
arg_list.push(format!("stride={}", stride.unwrap()));
}
if (*stripe_width).is_some() {
arg_list.push("-E".to_string());
arg_list.push(format!("stripe_width={}", stripe_width.unwrap()));
}
arg_list.push(device.as_ref().to_string_lossy().into_owned());
Ok(AsyncInit {
format_child: Command::new("mkfs.ext4").args(&arg_list).spawn()?,
post_setup_commands: vec![],
device: device.as_ref().to_owned(),
})
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_get_device_info() {
print!("{:?}", get_device_info(&PathBuf::from("/dev/sda5")));
print!("{:?}", get_device_info(&PathBuf::from("/dev/loop0")));
}
#[cfg(target_os = "linux")]
fn get_udev_int_val(device: &udev::Device, attr_name: &str) -> Option<u64> {
match device.attribute_value(attr_name) {
Some(val_str) => {
let val = val_str.to_str().unwrap_or("0").parse::<u64>().unwrap_or(0);
Some(val)
}
None => None,
}
}
#[cfg(target_os = "linux")]
fn get_size(device: &udev::Device) -> Option<u64> {
get_udev_int_val(device, "size").map(|s| s * 512)
}
#[cfg(target_os = "linux")]
fn get_uuid(device: &udev::Device) -> Option<Uuid> {
match device.property_value("ID_FS_UUID") {
Some(value) => Uuid::parse_str(&value.to_string_lossy()).ok(),
None => None,
}
}
#[cfg(target_os = "linux")]
fn get_serial(device: &udev::Device) -> Option<String> {
match device.property_value("ID_SERIAL") {
Some(value) => Some(value.to_string_lossy().into_owned()),
None => None,
}
}
#[cfg(target_os = "linux")]
fn get_fs_type(device: &udev::Device) -> BlockResult<FilesystemType> {
match device.property_value("ID_FS_TYPE") {
Some(s) => {
let value = s.to_string_lossy();
Ok(FilesystemType::from_str(&value)?)
}
None => Ok(FilesystemType::Unknown),
}
}
#[cfg(target_os = "linux")]
fn get_media_type(device: &udev::Device) -> MediaType {
use regex::Regex;
let device_sysname = device.sysname().to_string_lossy();
if let Ok(loop_regex) = Regex::new(r"loop\d+") {
if loop_regex.is_match(&device_sysname) {
return MediaType::Loopback;
}
}
if let Ok(ramdisk_regex) = Regex::new(r"ram\d+") {
if ramdisk_regex.is_match(&device_sysname) {
return MediaType::Ram;
}
}
if let Ok(ramdisk_regex) = Regex::new(r"md\d+") {
if ramdisk_regex.is_match(&device_sysname) {
return MediaType::MdRaid;
}
}
if device_sysname.contains("nvme") {
return MediaType::NVME;
}
if device.property_value("DM_NAME").is_some() {
return MediaType::LVM;
}
if let Some(rotation) = device.property_value("ID_ATA_ROTATION_RATE_RPM") {
return if rotation == "0" {
MediaType::SolidState
} else {
MediaType::Rotational
};
}
if let Some(vendor) = device.property_value("ID_VENDOR") {
let value = vendor.to_string_lossy();
return match value.as_ref() {
"QEMU" => MediaType::Virtual,
_ => MediaType::Unknown,
};
}
MediaType::Unknown
}
#[cfg(target_os = "linux")]
fn get_device_type(device: &udev::Device) -> BlockResult<DeviceType> {
match device.devtype() {
Some(s) => {
let value = s.to_string_lossy();
DeviceType::from_str(&value)
}
None => Ok(DeviceType::Unknown),
}
}
pub fn is_mounted(directory: impl AsRef<Path>) -> BlockResult<bool> {
let parent = directory.as_ref().parent();
let dir_metadata = fs::metadata(&directory)?;
let file_type = dir_metadata.file_type();
if file_type.is_symlink() {
return Ok(false);
}
Ok(if let Some(parent) = parent {
let parent_metadata = fs::metadata(parent)?;
parent_metadata.dev() != dir_metadata.dev()
} else {
false
})
}
#[cfg(target_os = "linux")]
fn get_specific_block_device_iter(
requested_dev_type: DeviceType,
) -> BlockResult<impl Iterator<Item = PathBuf>> {
Ok(udev::Enumerator::new()?
.scan_devices()?
.filter_map(move |device| {
if device.subsystem() == Some(OsStr::new("block")) {
let is_partition = device.devtype().map_or(false, |d| d == "partition");
let dev_type = if is_partition {
DeviceType::Partition
} else {
DeviceType::Disk
};
if dev_type == requested_dev_type {
Some(PathBuf::from("/dev").join(device.sysname()))
} else {
None
}
} else {
None
}
}))
}
#[cfg(target_os = "linux")]
pub fn get_block_partitions_iter() -> BlockResult<impl Iterator<Item = PathBuf>> {
get_specific_block_device_iter(DeviceType::Partition)
}
#[cfg(target_os = "linux")]
pub fn get_block_partitions() -> BlockResult<Vec<PathBuf>> {
get_block_partitions_iter().map(|i| i.collect())
}
#[cfg(target_os = "linux")]
pub fn get_block_devices_iter() -> BlockResult<impl Iterator<Item = PathBuf>> {
get_specific_block_device_iter(DeviceType::Disk)
}
#[cfg(target_os = "linux")]
pub fn get_block_devices() -> BlockResult<Vec<PathBuf>> {
get_block_devices_iter().map(|i| i.collect())
}
#[cfg(target_os = "linux")]
pub fn is_block_device(device_path: impl AsRef<Path>) -> BlockResult<bool> {
let mut enumerator = udev::Enumerator::new()?;
let devices = enumerator.scan_devices()?;
let sysname = device_path.as_ref().file_name().ok_or_else(|| {
BlockUtilsError::new(format!(
"Unable to get file_name on device {:?}",
device_path.as_ref()
))
})?;
for device in devices {
if sysname == device.sysname() && device.subsystem() == Some(OsStr::new("block")) {
return Ok(true);
}
}
Err(BlockUtilsError::new(format!(
"Unable to find device with name {:?}",
device_path.as_ref()
)))
}
fn dev_path_to_sys_path(dev_path: impl AsRef<Path>) -> BlockResult<PathBuf> {
let sys_path = dev_path
.as_ref()
.file_name()
.map(|name| PathBuf::from("/sys/class/block").join(name))
.ok_or_else(|| {
BlockUtilsError::new(format!(
"Unable to get file_name on device {:?}",
dev_path.as_ref()
))
})?;
if sys_path.exists() {
Ok(sys_path)
} else {
Err(BlockUtilsError::new(format!(
"Sys path {} doesn't exist. Maybe {} is not a block device",
sys_path.display(),
dev_path.as_ref().display()
)))
}
}
#[cfg(target_os = "linux")]
pub fn get_block_dev_property(
device_path: impl AsRef<Path>,
tag: &str,
) -> BlockResult<Option<String>> {
let syspath = dev_path_to_sys_path(device_path)?;
Ok(udev::Device::from_syspath(&syspath)?
.property_value(tag)
.map(|value| value.to_string_lossy().to_string()))
}
#[cfg(target_os = "linux")]
pub fn get_block_dev_properties(
device_path: impl AsRef<Path>,
) -> BlockResult<HashMap<String, String>> {
let syspath = dev_path_to_sys_path(device_path)?;
let udev_device = udev::Device::from_syspath(&syspath)?;
Ok(udev_device
.clone()
.properties()
.map(|property| {
let key = property.name().to_string_lossy().to_string();
let value = property.value().to_string_lossy().to_string();
(key, value)
})
.collect()) }
#[derive(Clone, Debug, Default)]
pub struct Enclosure {
pub active: Option<String>,
pub fault: Option<String>,
pub power_status: Option<String>,
pub slot: u8,
pub status: Option<String>,
pub enclosure_type: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum DeviceState {
Blocked,
#[strum(serialize = "failfast")]
FailFast,
Lost,
Running,
RunningRta,
}
#[derive(Clone, Debug)]
pub struct ScsiInfo {
pub block_device: Option<PathBuf>,
pub enclosure: Option<Enclosure>,
pub host: String,
pub channel: u8,
pub id: u8,
pub lun: u8,
pub vendor: Vendor,
pub vendor_str: Option<String>,
pub model: Option<String>,
pub rev: Option<String>,
pub state: Option<DeviceState>,
pub scsi_type: ScsiDeviceType,
pub scsi_revision: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, EnumString)]
pub enum ScsiDeviceType {
#[strum(serialize = "0", serialize = "Direct-Access")]
DirectAccess,
#[strum(serialize = "1")]
SequentialAccess,
#[strum(serialize = "2")]
Printer,
#[strum(serialize = "3")]
Processor,
#[strum(serialize = "4")]
WriteOnce,
#[strum(serialize = "5")]
CdRom,
#[strum(serialize = "6")]
Scanner,
#[strum(serialize = "7")]
Opticalmemory,
#[strum(serialize = "8")]
MediumChanger,
#[strum(serialize = "9")]
Communications,
#[strum(serialize = "10")]
Unknowna,
#[strum(serialize = "11")]
Unknownb,
#[strum(serialize = "12", serialize = "RAID")]
StorageArray,
#[strum(serialize = "13", serialize = "Enclosure")]
Enclosure,
#[strum(serialize = "14")]
SimplifiedDirectAccess,
#[strum(serialize = "15")]
OpticalCardReadWriter,
#[strum(serialize = "16")]
BridgeController,
#[strum(serialize = "17")]
ObjectBasedStorage,
#[strum(serialize = "18")]
AutomationDriveInterface,
#[strum(serialize = "19")]
SecurityManager,
#[strum(serialize = "20")]
ZonedBlock,
#[strum(serialize = "21")]
Reserved15,
#[strum(serialize = "22")]
Reserved16,
#[strum(serialize = "23")]
Reserved17,
#[strum(serialize = "24")]
Reserved18,
#[strum(serialize = "25")]
Reserved19,
#[strum(serialize = "26")]
Reserved1a,
#[strum(serialize = "27")]
Reserved1b,
#[strum(serialize = "28")]
Reserved1c,
#[strum(serialize = "29")]
Reserved1e,
#[strum(serialize = "30")]
WellKnownLu,
#[strum(serialize = "31")]
NoDevice,
}
impl Default for ScsiInfo {
fn default() -> ScsiInfo {
ScsiInfo {
block_device: None,
enclosure: None,
host: String::new(),
channel: 0,
id: 0,
lun: 0,
vendor: Vendor::None,
vendor_str: Option::None,
model: None,
rev: None,
state: None,
scsi_type: ScsiDeviceType::NoDevice,
scsi_revision: 0,
}
}
}
impl PartialEq for ScsiInfo {
fn eq(&self, other: &ScsiInfo) -> bool {
self.host == other.host
&& self.channel == other.channel
&& self.id == other.id
&& self.lun == other.lun
}
}
fn scsi_host_info(input: &str) -> Result<Vec<ScsiInfo>, BlockUtilsError> {
let mut scsi_devices = Vec::new();
let mut scsi_info = ScsiInfo::default();
for line in input.lines() {
if line.starts_with("Attached devices") {
continue;
}
if line.starts_with("Host") {
scsi_devices.push(scsi_info);
scsi_info = ScsiInfo::default();
let parts = line.split_whitespace().collect::<Vec<&str>>();
if parts.len() < 8 {
continue;
}
scsi_info.host = parts[1].to_string();
scsi_info.channel = parts[3].parse::<u8>()?;
scsi_info.id = parts[5].parse::<u8>()?;
scsi_info.lun = parts[7].parse::<u8>()?;
}
if line.contains("Vendor") {
let parts = line.split_whitespace().collect::<Vec<&str>>();
scsi_info.vendor = parts[1].parse::<Vendor>()?;
let model = parts[3..]
.iter()
.take_while(|s| !s.contains(":"))
.map(|s| *s)
.collect::<Vec<&str>>();
if !model.is_empty() {
scsi_info.model = Some(model.join(" ").to_string());
}
let rev_position = parts.iter().position(|s| s.contains("Rev:"));
if let Some(rev_position) = rev_position {
scsi_info.rev = Some(parts[rev_position + 1].to_string());
}
}
if line.contains("Type") {
let parts = line.split_whitespace().collect::<Vec<&str>>();
scsi_info.scsi_type = parts[1].parse::<ScsiDeviceType>()?;
scsi_info.scsi_revision = parts[5].parse::<u32>()?;
}
}
Ok(scsi_devices)
}
#[test]
fn test_scsi_parser() {
let s = fs::read_to_string("tests/proc_scsi").unwrap();
println!("scsi_host_info {:#?}", scsi_host_info(&s));
}
#[test]
fn test_sort_raid_info() {
let mut scsi_0 = ScsiInfo::default();
scsi_0.host = "scsi6".to_string();
scsi_0.channel = 0;
scsi_0.id = 0;
scsi_0.lun = 0;
let mut scsi_1 = ScsiInfo::default();
scsi_1.host = "scsi2".to_string();
scsi_1.channel = 0;
scsi_1.id = 0;
scsi_1.lun = 0;
let mut scsi_2 = ScsiInfo::default();
scsi_2.host = "scsi2".to_string();
scsi_2.channel = 1;
scsi_2.id = 0;
scsi_2.lun = 0;
let mut scsi_3 = ScsiInfo::default();
scsi_3.host = "scsi2".to_string();
scsi_3.channel = 1;
scsi_3.id = 0;
scsi_3.lun = 1;
let scsi_info = vec![scsi_0, scsi_1, scsi_2, scsi_3];
sort_scsi_info(&scsi_info);
}
pub fn sort_scsi_info_iter<'a>(
info: &'a [ScsiInfo],
) -> impl Iterator<Item = (ScsiInfo, Option<ScsiInfo>)> + 'a {
info.iter().map(move |dev| {
let host = info
.iter()
.position(|d| d.host == dev.host && d.channel == 0 && d.id == 0 && d.lun == 0);
match host {
Some(pos) => {
let host_dev = info[pos].clone();
if host_dev == *dev {
(dev.clone(), None)
} else {
(dev.clone(), Some(info[pos].clone()))
}
}
None => (dev.clone(), None),
}
})
}
pub fn sort_scsi_info(info: &[ScsiInfo]) -> Vec<(ScsiInfo, Option<ScsiInfo>)> {
sort_scsi_info_iter(info).collect()
}
fn get_enclosure_data(p: impl AsRef<Path>) -> BlockResult<Enclosure> {
let mut e = Enclosure::default();
for entry in read_dir(p)? {
let entry = entry?;
if entry.file_name() == OsStr::new("active") {
e.active = Some(fs::read_to_string(&entry.path())?.trim().to_string());
} else if entry.file_name() == OsStr::new("fault") {
e.fault = Some(fs::read_to_string(&entry.path())?.trim().to_string());
} else if entry.file_name() == OsStr::new("power_status") {
e.power_status = Some(fs::read_to_string(&entry.path())?.trim().to_string());
} else if entry.file_name() == OsStr::new("slot") {
e.slot = u8::from_str(fs::read_to_string(&entry.path())?.trim())?;
} else if entry.file_name() == OsStr::new("status") {
e.status = Some(fs::read_to_string(&entry.path())?.trim().to_string());
} else if entry.file_name() == OsStr::new("type") {
e.enclosure_type = Some(fs::read_to_string(&entry.path())?.trim().to_string());
}
}
Ok(e)
}
pub fn get_scsi_info() -> BlockResult<Vec<ScsiInfo>> {
let scsi_path = Path::new("/sys/bus/scsi/devices");
if scsi_path.exists() {
let mut scsi_devices: Vec<ScsiInfo> = Vec::new();
for entry in read_dir(&scsi_path)? {
let entry = entry?;
let path = entry.path();
let name = path.file_name();
if let Some(name) = name {
let n = name.to_string_lossy();
let f = match n.chars().next() {
Some(c) => c,
None => {
warn!("{} doesn't have any characters. Skipping", n);
continue;
}
};
if f.is_digit(10) {
let mut s = ScsiInfo::default();
let parts: Vec<&str> = n.split(':').collect();
if parts.len() != 4 {
warn!("Invalid device name: {}. Should be 0:0:0:0 format", n);
continue;
}
s.host = parts[0].to_string();
s.channel = u8::from_str(parts[1])?;
s.id = u8::from_str(parts[2])?;
s.lun = u8::from_str(parts[3])?;
for scsi_entries in read_dir(&path)? {
let scsi_entry = scsi_entries?;
if scsi_entry.file_name() == OsStr::new("block") {
let block_path = path.join("block");
if block_path.exists() {
let mut device_name = read_dir(&block_path)?.take(1);
if let Some(name) = device_name.next() {
s.block_device =
Some(Path::new("/dev/").join(name?.file_name()));
}
}
} else if scsi_entry
.file_name()
.to_string_lossy()
.starts_with("enclosure_device")
{
let enclosure_path = path.join(scsi_entry.file_name());
let e = get_enclosure_data(&enclosure_path)?;
s.enclosure = Some(e);
} else if scsi_entry.file_name() == OsStr::new("model") {
s.model =
Some(fs::read_to_string(&scsi_entry.path())?.trim().to_string());
} else if scsi_entry.file_name() == OsStr::new("rev") {
s.rev =
Some(fs::read_to_string(&scsi_entry.path())?.trim().to_string());
} else if scsi_entry.file_name() == OsStr::new("state") {
s.state = Some(DeviceState::from_str(
fs::read_to_string(&scsi_entry.path())?.trim(),
)?);
} else if scsi_entry.file_name() == OsStr::new("type") {
s.scsi_type = ScsiDeviceType::from_str(
fs::read_to_string(&scsi_entry.path())?.trim(),
)?;
} else if scsi_entry.file_name() == OsStr::new("vendor") {
let vendor_str = fs::read_to_string(&scsi_entry.path())?;
s.vendor_str = Some(vendor_str.trim().to_string());
s.vendor = Vendor::from_str(vendor_str.trim()).unwrap_or(Vendor::None);
}
}
scsi_devices.push(s);
}
}
}
Ok(scsi_devices)
} else {
let buff = fs::read_to_string("/proc/scsi/scsi")?;
Ok(scsi_host_info(&buff)?)
}
}
#[cfg(target_os = "linux")]
pub fn is_disk(dev_path: impl AsRef<Path>) -> BlockResult<bool> {
let mut enumerator = udev::Enumerator::new()?;
let host_devices = enumerator.scan_devices()?;
for device in host_devices {
if let Some(dev_type) = device.devtype() {
let name = Path::new("/dev").join(device.sysname());
if dev_type == "disk" && name == dev_path.as_ref() {
return Ok(true);
}
}
}
Ok(false)
}
#[cfg(target_os = "linux")]
fn get_parent_name(device: &udev::Device) -> Option<PathBuf> {
if let Some(parent_dev) = device.parent() {
if let Some(dev_type) = parent_dev.devtype() {
if dev_type == "disk" || dev_type == "partition" {
let name = Path::new("/dev").join(parent_dev.sysname());
Some(name)
} else {
None
}
} else {
None
}
} else {
None
}
}
#[cfg(target_os = "linux")]
pub fn get_parent_devpath_from_path(dev_path: impl AsRef<Path>) -> BlockResult<Option<PathBuf>> {
let mut enumerator = udev::Enumerator::new()?;
let host_devices = enumerator.scan_devices()?;
for device in host_devices {
if let Some(dev_type) = device.devtype() {
if dev_type == "disk" || dev_type == "partition" {
let name = Path::new("/dev").join(device.sysname());
let dev_links = OsStr::new("DEVLINKS");
if dev_path.as_ref() == name {
if let Some(name) = get_parent_name(&device) {
return Ok(Some(name));
}
}
if let Some(links) = device.property_value(dev_links) {
let path = dev_path.as_ref().to_string_lossy().to_string();
if links.to_string_lossy().contains(&path) {
if let Some(name) = get_parent_name(&device) {
return Ok(Some(name));
}
}
}
}
}
}
Ok(None)
}
#[cfg(target_os = "linux")]
pub fn get_children_devpaths_from_path(dev_path: impl AsRef<Path>) -> BlockResult<Vec<PathBuf>> {
get_children_devpaths_from_path_iter(dev_path).map(|iter| iter.collect())
}
#[cfg(target_os = "linux")]
pub fn get_children_devpaths_from_path_iter(
dev_path: impl AsRef<Path>,
) -> BlockResult<impl Iterator<Item = PathBuf>> {
Ok(get_block_partitions_iter()?.filter(move |partition| {
if let Ok(Some(parent_device)) = get_parent_devpath_from_path(partition) {
dev_path.as_ref() == &parent_device
} else {
false
}
}))
}
#[cfg(target_os = "linux")]
pub fn get_device_from_path(
dev_path: impl AsRef<Path>,
) -> BlockResult<(Option<u64>, Option<Device>)> {
let mut enumerator = udev::Enumerator::new()?;
let host_devices = enumerator.scan_devices()?;
for device in host_devices {
if let Some(dev_type) = device.devtype() {
if dev_type == "disk" || dev_type == "partition" {
let name = Path::new("/dev").join(device.sysname());
let dev_links = OsStr::new("DEVLINKS");
if dev_path.as_ref() == name {
let part_num = match device.property_value("ID_PART_ENTRY_NUMBER") {
Some(value) => value.to_string_lossy().parse::<u64>().ok(),
None => None,
};
let dev = Device::from_udev_device(device)?;
return Ok((part_num, Some(dev)));
}
if let Some(links) = device.property_value(dev_links) {
let path = dev_path.as_ref().to_string_lossy().to_string();
if links.to_string_lossy().contains(&path) {
let part_num = match device.property_value("ID_PART_ENTRY_NUMBER") {
Some(value) => value.to_string_lossy().parse::<u64>().ok(),
None => None,
};
let dev = Device::from_udev_device(device)?;
return Ok((part_num, Some(dev)));
}
}
}
}
}
Ok((None, None))
}
#[cfg(target_os = "linux")]
pub fn get_all_device_info_iter<P, T>(
devices: T,
) -> BlockResult<impl Iterator<Item = BlockResult<Device>>>
where
P: AsRef<Path>,
T: AsRef<[P]>,
{
let device_names = devices
.as_ref()
.iter()
.filter_map(|d| d.as_ref().file_name().map(OsStr::to_owned))
.collect::<Vec<_>>();
Ok(udev::Enumerator::new()?.scan_devices()?.filter_map(
move |device| -> Option<BlockResult<Device>> {
if device_names.contains(&device.sysname().to_owned())
&& device.subsystem() == Some(OsStr::new("block"))
{
Some(Device::from_udev_device(device))
} else {
None
}
},
))
}
#[cfg(target_os = "linux")]
pub fn get_all_device_info<P, T>(devices: T) -> BlockResult<Vec<Device>>
where
P: AsRef<Path>,
T: AsRef<[P]>,
{
get_all_device_info_iter(devices).map(|i| i.collect::<BlockResult<Vec<Device>>>())?
}
#[cfg(target_os = "linux")]
pub fn get_device_info(device_path: impl AsRef<Path>) -> BlockResult<Device> {
let error_message = format!(
"Unable to get file_name on device {:?}",
device_path.as_ref()
);
let sysname = device_path
.as_ref()
.file_name()
.ok_or_else(|| BlockUtilsError::new(error_message.clone()))?;
udev::Enumerator::new()?
.scan_devices()?
.find(|udev_device| {
sysname == udev_device.sysname() && udev_device.subsystem() == Some(OsStr::new("block"))
})
.ok_or_else(|| BlockUtilsError::new(error_message))
.and_then(Device::from_udev_device)
}
pub fn set_elevator(device_path: impl AsRef<Path>, elevator: &Scheduler) -> BlockResult<usize> {
let device_name = match device_path.as_ref().file_name() {
Some(name) => name.to_string_lossy().into_owned(),
None => "".to_string(),
};
let mut f = File::open("/etc/rc.local")?;
let elevator_cmd = format!(
"echo {scheduler} > /sys/block/{device}/queue/scheduler",
scheduler = elevator,
device = device_name
);
let mut script = shellscript::parse(&mut f)?;
let existing_cmd = script
.commands
.iter()
.position(|cmd| cmd.contains(&device_name));
if let Some(pos) = existing_cmd {
script.commands.remove(pos);
}
script.commands.push(elevator_cmd);
let mut f = File::create("/etc/rc.local")?;
let bytes_written = script.write(&mut f)?;
Ok(bytes_written)
}
pub fn weekly_defrag(
mount: impl AsRef<Path>,
fs_type: &FilesystemType,
interval: &str,
) -> BlockResult<usize> {
let crontab = Path::new("/var/spool/cron/crontabs/root");
let defrag_command = match *fs_type {
FilesystemType::Ext4 => "e4defrag",
FilesystemType::Btrfs => "btrfs filesystem defragment -r",
FilesystemType::Xfs => "xfs_fsr",
_ => "",
};
let job = format!(
"{interval} {cmd} {path}",
interval = interval,
cmd = defrag_command,
path = mount.as_ref().display()
);
let mut existing_crontab = {
if crontab.exists() {
let buff = fs::read_to_string("/var/spool/cron/crontabs/root")?;
buff.split('\n')
.map(|s| s.to_string())
.collect::<Vec<String>>()
} else {
Vec::new()
}
};
let mount_str = mount.as_ref().to_string_lossy().into_owned();
let existing_job_position = existing_crontab
.iter()
.position(|line| line.contains(&mount_str));
if let Some(pos) = existing_job_position {
existing_crontab.remove(pos);
}
existing_crontab.push(job);
let mut f = File::create("/var/spool/cron/crontabs/root")?;
let written_bytes = f.write(&existing_crontab.join("\n").as_bytes())?;
Ok(written_bytes)
}