├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── lib.rs └── nvme.rs └── tests └── proc_scsi /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Install libudev 19 | run: | 20 | sudo apt-get install -y libudev-dev 21 | - uses: actions/checkout@v2 22 | - name: Build 23 | run: cargo build --verbose 24 | - name: Run tests 25 | run: cargo test --verbose 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.swp 3 | Cargo.lock 4 | vscode.code-workspace 5 | .idea/ 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | Pull requests are very welcome. 3 | 4 | # Submitting Changes 5 | - Push your changes to a topic branch in your fork of the repository. 6 | - Submit a pull request 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "block-utils" 3 | version = "0.11.1" 4 | authors = ["Chris Holcombe "] 5 | description = "Utilities to work with block devices. Formatting, getting device info, identifying type of device, etc." 6 | edition = '2018' 7 | 8 | # These URLs point to more information about the repository. 9 | documentation = "https://docs.rs/block-utils" 10 | homepage = "https://github.com/cholcombe973/block-utils" 11 | repository = "https://github.com/cholcombe973/block-utils" 12 | readme = "README.md" 13 | license = "MIT" 14 | 15 | [dev-dependencies] 16 | nix = "0.23" 17 | tempfile = "3" 18 | 19 | [dependencies] 20 | fstab = "0.4" 21 | log = "0.4" 22 | regex = "1.7" 23 | shellscript = "0.3" 24 | serde = { "version" = "1.0", features = ["derive"] } 25 | serde_json = "1.0" 26 | strum = { version = "0.24", features = ["derive"] } 27 | thiserror = "1.0" 28 | uuid = "1.3" 29 | 30 | [target.'cfg(target_os = "linux")'.dependencies] 31 | udev = "0.5" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Chris Holcombe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # block-utils 2 | ![Rust](https://github.com/cholcombe973/block-utils/workflows/Rust/badge.svg) 3 | [![Docs](https://docs.rs/block-utils/badge.svg)](https://docs.rs/block-utils)[![Crates.io](https://img.shields.io/crates/v/block-utils.svg)](https://crates.io/crates/block-utils) 4 | 5 | Rust utilities for working with block devices. Basic support has been added 6 | to detect if block devices are ssd or rotational. Also has support to 7 | determine if a device is in fact a block device. 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod nvme; 2 | 3 | use fstab::{FsEntry, FsTab}; 4 | use log::{debug, warn}; 5 | use uuid::Uuid; 6 | 7 | use std::collections::HashMap; 8 | use std::ffi::OsStr; 9 | use std::fmt; 10 | use std::fs::{self, read_dir, File}; 11 | use std::io::{BufRead, BufReader, Write}; 12 | use std::os::unix::fs::MetadataExt; 13 | use std::path::{Path, PathBuf}; 14 | use std::process::{Child, Command, Output}; 15 | use std::str::FromStr; 16 | use strum::{Display, EnumString, IntoStaticStr}; 17 | use thiserror::Error; 18 | 19 | pub type BlockResult = Result; 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use nix::unistd::{close, ftruncate}; 24 | use tempfile::TempDir; 25 | 26 | use std::fs::File; 27 | use std::os::unix::io::IntoRawFd; 28 | 29 | #[test] 30 | fn test_create_xfs() { 31 | let tmp_dir = TempDir::new().unwrap(); 32 | let file_path = tmp_dir.path().join("xfs_device"); 33 | let f = File::create(&file_path).expect("Failed to create file"); 34 | let fd = f.into_raw_fd(); 35 | // Create a sparse file of 100MB in size to test xfs creation 36 | ftruncate(fd, 104_857_600).unwrap(); 37 | let xfs_options = super::Filesystem::Xfs { 38 | stripe_size: None, 39 | stripe_width: None, 40 | block_size: None, 41 | inode_size: Some(512), 42 | force: false, 43 | agcount: Some(32), 44 | }; 45 | let result = super::format_block_device(&file_path, &xfs_options); 46 | println!("Result: {:?}", result); 47 | close(fd).expect("Failed to close file descriptor"); 48 | } 49 | 50 | #[test] 51 | fn test_create_ext4() { 52 | let tmp_dir = TempDir::new().unwrap(); 53 | let file_path = tmp_dir.path().join("ext4_device"); 54 | let f = File::create(&file_path).expect("Failed to create file"); 55 | let fd = f.into_raw_fd(); 56 | // Create a sparse file of 100MB in size to test ext creation 57 | ftruncate(fd, 104_857_600).unwrap(); 58 | let xfs_options = super::Filesystem::Ext4 { 59 | inode_size: 512, 60 | stride: Some(2), 61 | stripe_width: None, 62 | reserved_blocks_percentage: 10, 63 | }; 64 | let result = super::format_block_device(&file_path, &xfs_options); 65 | println!("Result: {:?}", result); 66 | close(fd).expect("Failed to close file descriptor"); 67 | } 68 | } 69 | 70 | const MTAB_PATH: &str = "/etc/mtab"; 71 | 72 | #[derive(Debug, Error)] 73 | pub enum BlockUtilsError { 74 | #[error("BlockUtilsError : {0}")] 75 | Error(String), 76 | 77 | #[error(transparent)] 78 | IoError(#[from] std::io::Error), 79 | 80 | #[error(transparent)] 81 | ParseBoolError(#[from] std::str::ParseBoolError), 82 | 83 | #[error(transparent)] 84 | ParseIntError(#[from] std::num::ParseIntError), 85 | 86 | #[error(transparent)] 87 | SerdeError(#[from] serde_json::Error), 88 | 89 | #[error(transparent)] 90 | StrumParseError(#[from] strum::ParseError), 91 | } 92 | 93 | impl BlockUtilsError { 94 | /// Create a new BlockUtilsError with a String message 95 | fn new(err: String) -> BlockUtilsError { 96 | BlockUtilsError::Error(err) 97 | } 98 | } 99 | 100 | // Formats a block device at Path p with XFS 101 | /// This is used for formatting btrfs filesystems and setting the metadata profile 102 | #[derive(Clone, Debug, Display)] 103 | #[strum(serialize_all = "snake_case")] 104 | pub enum MetadataProfile { 105 | Raid0, 106 | Raid1, 107 | Raid5, 108 | Raid6, 109 | Raid10, 110 | Single, 111 | Dup, 112 | } 113 | 114 | /// What raid card if any the system is using to serve disks 115 | #[derive(Clone, Debug, EnumString)] 116 | pub enum Vendor { 117 | #[strum(serialize = "ATA")] 118 | None, 119 | #[strum(serialize = "CISCO")] 120 | Cisco, 121 | #[strum(serialize = "HP", serialize = "hp", serialize = "HPE")] 122 | Hp, 123 | #[strum(serialize = "LSI")] 124 | Lsi, 125 | #[strum(serialize = "QEMU")] 126 | Qemu, 127 | #[strum(serialize = "VBOX")] 128 | Vbox, // Virtual Box 129 | #[strum(serialize = "NECVMWar")] 130 | NECVMWar, // VMWare 131 | #[strum(serialize = "VMware")] 132 | VMware, //VMware 133 | } 134 | 135 | // This will be used to make intelligent decisions about setting up the device 136 | /// Device information that is gathered with udev 137 | #[derive(Clone, Debug)] 138 | pub struct Device { 139 | pub id: Option, 140 | pub name: String, 141 | pub media_type: MediaType, 142 | pub device_type: DeviceType, 143 | pub capacity: u64, 144 | pub fs_type: FilesystemType, 145 | pub serial_number: Option, 146 | pub logical_block_size: Option, 147 | pub physical_block_size: Option, 148 | } 149 | 150 | impl Device { 151 | #[cfg(target_os = "linux")] 152 | fn from_udev_device(device: udev::Device) -> BlockResult { 153 | let sys_name = device.sysname(); 154 | let id: Option = get_uuid(&device); 155 | let serial = get_serial(&device); 156 | let media_type = get_media_type(&device); 157 | let device_type = get_device_type(&device)?; 158 | let capacity = match get_size(&device) { 159 | Some(size) => size, 160 | None => 0, 161 | }; 162 | let logical_block_size = get_udev_int_val(&device, "queue/logical_block_size"); 163 | let physical_block_size = get_udev_int_val(&device, "queue/physical_block_size"); 164 | let fs_type = get_fs_type(&device)?; 165 | 166 | Ok(Device { 167 | id, 168 | name: sys_name.to_string_lossy().to_string(), 169 | media_type, 170 | device_type, 171 | capacity, 172 | fs_type, 173 | serial_number: serial, 174 | logical_block_size, 175 | physical_block_size, 176 | }) 177 | } 178 | 179 | fn from_fs_entry(fs_entry: FsEntry) -> BlockResult { 180 | Ok(Device { 181 | id: None, 182 | name: Path::new(&fs_entry.fs_spec) 183 | .file_name() 184 | .unwrap_or_else(|| OsStr::new("")) 185 | .to_string_lossy() 186 | .into_owned(), 187 | media_type: MediaType::Unknown, 188 | device_type: DeviceType::Unknown, 189 | capacity: 0, 190 | fs_type: FilesystemType::from_str(&fs_entry.vfs_type)?, 191 | serial_number: None, 192 | logical_block_size: None, 193 | physical_block_size: None, 194 | }) 195 | } 196 | } 197 | 198 | #[derive(Debug)] 199 | pub struct AsyncInit { 200 | /// The child process needed for this device initializati 201 | /// This will be an async spawned Child handle 202 | pub format_child: Child, 203 | /// After formatting is complete run these commands to se 204 | /// ZFS needs this. These should prob be run in sync mod 205 | pub post_setup_commands: Vec<(String, Vec)>, 206 | /// The device we're initializing 207 | pub device: PathBuf, 208 | } 209 | 210 | #[derive(Debug, Display, EnumString)] 211 | #[strum(serialize_all = "snake_case")] 212 | pub enum Scheduler { 213 | /// Try to balance latency and throughput 214 | Cfq, 215 | /// Latency is most important 216 | Deadline, 217 | /// Throughput is most important 218 | Noop, 219 | } 220 | 221 | /// What type of media has been detected. 222 | #[derive(Clone, Debug, Eq, PartialEq)] 223 | pub enum MediaType { 224 | /// AKA SSD 225 | SolidState, 226 | /// Regular rotational device 227 | Rotational, 228 | /// Special loopback device 229 | Loopback, 230 | // Logical volume device 231 | LVM, 232 | // Software raid device 233 | MdRaid, 234 | // NVM Express 235 | NVME, 236 | // Ramdisk 237 | Ram, 238 | Virtual, 239 | Unknown, 240 | } 241 | 242 | /// What type of device has been detected. 243 | #[derive(Clone, Debug, Eq, PartialEq, Display, IntoStaticStr)] 244 | #[strum(serialize_all = "snake_case")] 245 | pub enum DeviceType { 246 | Disk, 247 | Partition, 248 | Unknown, 249 | } 250 | 251 | impl FromStr for DeviceType { 252 | type Err = BlockUtilsError; 253 | 254 | fn from_str(s: &str) -> Result { 255 | let s = s.to_lowercase(); 256 | match s.as_ref() { 257 | "disk" => Ok(DeviceType::Disk), 258 | "partition" => Ok(DeviceType::Partition), 259 | _ => Ok(DeviceType::Unknown), 260 | } 261 | } 262 | } 263 | 264 | /// What type of filesystem 265 | #[derive(Clone, Debug, Eq, PartialEq, EnumString)] 266 | #[strum(serialize_all = "snake_case")] 267 | pub enum FilesystemType { 268 | Btrfs, 269 | Ext2, 270 | Ext3, 271 | Ext4, 272 | #[strum(serialize = "lvm2_member")] 273 | Lvm, 274 | Xfs, 275 | Zfs, 276 | Ntfs, 277 | /// All FAT-based filesystems, i.e. VFat, Fat16, Fat32, Fat64, ExFat. 278 | Vfat, 279 | /// Unknown filesystem with label (name). 280 | #[strum(default)] 281 | Unrecognised(String), 282 | /// Unknown filesystem without label (name) or absent filesystem. 283 | #[strum(serialize = "")] 284 | Unknown, 285 | } 286 | 287 | impl FilesystemType { 288 | pub fn to_str(&self) -> &str { 289 | match *self { 290 | FilesystemType::Btrfs => "btrfs", 291 | FilesystemType::Ext2 => "ext2", 292 | FilesystemType::Ext3 => "ext3", 293 | FilesystemType::Ext4 => "ext4", 294 | FilesystemType::Lvm => "lvm", 295 | FilesystemType::Xfs => "xfs", 296 | FilesystemType::Zfs => "zfs", 297 | FilesystemType::Vfat => "vfat", 298 | FilesystemType::Ntfs => "ntfs", 299 | FilesystemType::Unrecognised(ref name) => name.as_str(), 300 | FilesystemType::Unknown => "unknown", 301 | } 302 | } 303 | } 304 | 305 | impl fmt::Display for FilesystemType { 306 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 307 | let string = match *self { 308 | FilesystemType::Btrfs => "btrfs".to_string(), 309 | FilesystemType::Ext2 => "ext2".to_string(), 310 | FilesystemType::Ext3 => "ext3".to_string(), 311 | FilesystemType::Ext4 => "ext4".to_string(), 312 | FilesystemType::Lvm => "lvm".to_string(), 313 | FilesystemType::Xfs => "xfs".to_string(), 314 | FilesystemType::Zfs => "zfs".to_string(), 315 | FilesystemType::Vfat => "vfat".to_string(), 316 | FilesystemType::Ntfs => "ntfs".to_string(), 317 | FilesystemType::Unrecognised(ref name) => name.clone(), 318 | FilesystemType::Unknown => "unknown".to_string(), 319 | }; 320 | write!(f, "{}", string) 321 | } 322 | } 323 | 324 | /// This allows you to tweak some settings when you're formatting the filesystem 325 | #[derive(Debug)] 326 | pub enum Filesystem { 327 | Btrfs { 328 | leaf_size: u64, 329 | metadata_profile: MetadataProfile, 330 | node_size: u64, 331 | }, 332 | Ext4 { 333 | inode_size: u64, 334 | reserved_blocks_percentage: u8, 335 | stride: Option, 336 | stripe_width: Option, 337 | }, 338 | Xfs { 339 | /// This is optional. Boost knobs are on by default: 340 | /// http://xfs.org/index.php/XFS_FAQ#Q:_I_want_to_tune_my_XFS_filesystems_ 341 | /// for_.3Csomething.3E 342 | block_size: Option, // Note this MUST be a power of 2 343 | force: bool, 344 | inode_size: Option, 345 | stripe_size: Option, // RAID controllers stripe 346 | stripe_width: Option, // IE # of data disks 347 | agcount: Option, // number of allocation groups 348 | }, 349 | Zfs { 350 | /// The default blocksize for volumes is 8 Kbytes. An 351 | /// power of 2 from 512 bytes to 128 Kbytes is valid. 352 | block_size: Option, 353 | /// Enable compression on the volume. Default is fals 354 | compression: Option, 355 | }, 356 | } 357 | 358 | impl Filesystem { 359 | pub fn new(name: &str) -> Filesystem { 360 | match name.trim() { 361 | // Defaults. Can be changed as needed by the caller 362 | "zfs" => Filesystem::Zfs { 363 | block_size: None, 364 | compression: None, 365 | }, 366 | "xfs" => Filesystem::Xfs { 367 | stripe_size: None, 368 | stripe_width: None, 369 | block_size: None, 370 | inode_size: Some(512), 371 | force: false, 372 | agcount: Some(32), 373 | }, 374 | "btrfs" => Filesystem::Btrfs { 375 | metadata_profile: MetadataProfile::Single, 376 | leaf_size: 32768, 377 | node_size: 32768, 378 | }, 379 | "ext4" => Filesystem::Ext4 { 380 | inode_size: 512, 381 | reserved_blocks_percentage: 0, 382 | stride: None, 383 | stripe_width: None, 384 | }, 385 | _ => Filesystem::Xfs { 386 | stripe_size: None, 387 | stripe_width: None, 388 | block_size: None, 389 | inode_size: None, 390 | force: false, 391 | agcount: None, 392 | }, 393 | } 394 | } 395 | } 396 | 397 | fn run_command>(command: &str, arg_list: &[S]) -> BlockResult { 398 | Ok(Command::new(command).args(arg_list).output()?) 399 | } 400 | 401 | /// Utility function to mount a device at a mount point 402 | /// NOTE: This assumes the device is formatted at this point. The mount 403 | /// will fail if the device isn't formatted. 404 | pub fn mount_device(device: &Device, mount_point: impl AsRef) -> BlockResult { 405 | let mut arg_list: Vec = Vec::new(); 406 | match device.id { 407 | Some(id) => { 408 | arg_list.push("-U".to_string()); 409 | arg_list.push(id.hyphenated().to_string()); 410 | } 411 | None => { 412 | arg_list.push(format!("/dev/{}", device.name)); 413 | } 414 | }; 415 | arg_list.push(mount_point.as_ref().to_string_lossy().into_owned()); 416 | debug!("mount: {:?}", arg_list); 417 | 418 | process_output(&run_command("mount", &arg_list)?) 419 | } 420 | 421 | //Utility function to unmount a device at a mount point 422 | pub fn unmount_device(mount_point: impl AsRef) -> BlockResult { 423 | let mut arg_list: Vec = Vec::new(); 424 | arg_list.push(mount_point.as_ref().to_string_lossy().into_owned()); 425 | 426 | process_output(&run_command("umount", &arg_list)?) 427 | } 428 | 429 | /// Parse mtab and return the device which is mounted at a given directory 430 | pub fn get_mount_device(mount_dir: impl AsRef) -> BlockResult> { 431 | let dir = mount_dir.as_ref().to_string_lossy().into_owned(); 432 | let f = File::open(MTAB_PATH)?; 433 | let reader = BufReader::new(f); 434 | 435 | for line in reader.lines() { 436 | let line = line?; 437 | let parts: Vec<&str> = line.split_whitespace().collect(); 438 | if parts.contains(&dir.as_str()) { 439 | if !parts.is_empty() { 440 | return Ok(Some(PathBuf::from(parts[0]))); 441 | } 442 | } 443 | } 444 | Ok(None) 445 | } 446 | 447 | /// Parse mtab and return iterator over all mounted block devices not including LVM 448 | /// 449 | /// Lazy version of get_mounted_devices 450 | pub fn get_mounted_devices_iter() -> BlockResult>> { 451 | Ok(FsTab::new(Path::new(MTAB_PATH)) 452 | .get_entries()? 453 | .into_iter() 454 | .filter(|d| d.fs_spec.contains("/dev/")) 455 | .filter(|d| !d.fs_spec.contains("mapper")) 456 | .map(Device::from_fs_entry)) 457 | } 458 | /// Parse mtab and return all mounted block devices not including LVM 459 | /// 460 | /// Non-lazy version of get_mounted_devices_iter 461 | pub fn get_mounted_devices() -> BlockResult> { 462 | get_mounted_devices_iter()?.collect() 463 | } 464 | 465 | /// Parse mtab and return the mountpoint the device is mounted at. 466 | /// This is the opposite of get_mount_device 467 | pub fn get_mountpoint(device: impl AsRef) -> BlockResult> { 468 | let s = device.as_ref().to_string_lossy().into_owned(); 469 | let f = File::open(MTAB_PATH)?; 470 | let reader = BufReader::new(f); 471 | 472 | for line in reader.lines() { 473 | let l = line?; 474 | let parts: Vec<&str> = l.split_whitespace().collect(); 475 | let mut index = -1; 476 | for (i, p) in parts.iter().enumerate() { 477 | if p == &s { 478 | index = i as i64; 479 | } 480 | } 481 | if index >= 0 { 482 | return Ok(Some(PathBuf::from(parts[1]))); 483 | } 484 | } 485 | Ok(None) 486 | } 487 | 488 | fn process_output(output: &Output) -> BlockResult { 489 | if output.status.success() { 490 | Ok(0) 491 | } else { 492 | let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); 493 | Err(BlockUtilsError::new(stderr)) 494 | } 495 | } 496 | 497 | pub fn erase_block_device(device: impl AsRef) -> BlockResult<()> { 498 | let output = Command::new("sgdisk") 499 | .args(&["--zap", &device.as_ref().to_string_lossy()]) 500 | .output()?; 501 | if output.status.success() { 502 | Ok(()) 503 | } else { 504 | Err(BlockUtilsError::new(format!( 505 | "Disk {:?} failed to erase: {}", 506 | device.as_ref(), 507 | String::from_utf8_lossy(&output.stderr) 508 | ))) 509 | } 510 | } 511 | 512 | /// Synchronous utility to format a block device with a given filesystem. 513 | /// Note: ZFS creation can be slow because there's potentially several commands that need to 514 | /// be run. async_format_block_device will be faster if you have many block devices to format 515 | pub fn format_block_device(device: impl AsRef, filesystem: &Filesystem) -> BlockResult { 516 | //TODO REFACTOR 517 | match *filesystem { 518 | Filesystem::Btrfs { 519 | ref metadata_profile, 520 | ref leaf_size, 521 | ref node_size, 522 | } => { 523 | let mut arg_list: Vec = Vec::new(); 524 | 525 | arg_list.push("-m".to_string()); 526 | arg_list.push(metadata_profile.clone().to_string()); 527 | 528 | arg_list.push("-l".to_string()); 529 | arg_list.push(leaf_size.to_string()); 530 | 531 | arg_list.push("-n".to_string()); 532 | arg_list.push(node_size.to_string()); 533 | 534 | arg_list.push(device.as_ref().to_string_lossy().to_string()); 535 | // Check if mkfs.btrfs is installed 536 | if !Path::new("/sbin/mkfs.btrfs").exists() { 537 | return Err(BlockUtilsError::new( 538 | "Please install btrfs-tools".to_string(), 539 | )); 540 | } 541 | process_output(&run_command("mkfs.btrfs", &arg_list)?) 542 | } 543 | Filesystem::Xfs { 544 | ref inode_size, 545 | ref force, 546 | ref block_size, 547 | ref stripe_size, 548 | ref stripe_width, 549 | ref agcount, 550 | } => { 551 | let mut arg_list: Vec = Vec::new(); 552 | 553 | if let Some(b) = block_size { 554 | /* 555 | From XFS man page: 556 | The default value is 4096 bytes (4 KiB), the minimum is 557 | 512, and the maximum is 65536 (64 KiB). XFS on Linux currently 558 | only supports pagesize or smaller blocks. 559 | */ 560 | let b: u64 = if *b < 512 { 561 | warn!("xfs block size must be 512 bytes minimum. Correcting"); 562 | 512 563 | } else if *b > 65536 { 564 | warn!("xfs block size must be 65536 bytes maximum. Correcting"); 565 | 65536 566 | } else { 567 | *b 568 | }; 569 | arg_list.push("-b".to_string()); 570 | arg_list.push(format!("size={}", b)); 571 | } 572 | 573 | if (*inode_size).is_some() { 574 | arg_list.push("-i".to_string()); 575 | arg_list.push(format!("size={}", inode_size.unwrap())); 576 | } 577 | 578 | if *force { 579 | arg_list.push("-f".to_string()); 580 | } 581 | 582 | if (*stripe_size).is_some() && (*stripe_width).is_some() { 583 | arg_list.push("-d".to_string()); 584 | arg_list.push(format!("su={}", stripe_size.unwrap())); 585 | arg_list.push(format!("sw={}", stripe_width.unwrap())); 586 | if (*agcount).is_some() { 587 | arg_list.push(format!("agcount={}", agcount.unwrap())); 588 | } 589 | } 590 | arg_list.push(device.as_ref().to_string_lossy().to_string()); 591 | 592 | // Check if mkfs.xfs is installed 593 | if !Path::new("/sbin/mkfs.xfs").exists() { 594 | return Err(BlockUtilsError::new("Please install xfsprogs".into())); 595 | } 596 | process_output(&run_command("/sbin/mkfs.xfs", &arg_list)?) 597 | } 598 | Filesystem::Ext4 { 599 | ref inode_size, 600 | ref reserved_blocks_percentage, 601 | ref stride, 602 | ref stripe_width, 603 | } => { 604 | let mut arg_list: Vec = Vec::new(); 605 | 606 | if stride.is_some() || stripe_width.is_some() { 607 | arg_list.push("-E".to_string()); 608 | if let Some(stride) = stride { 609 | arg_list.push(format!("stride={}", stride)); 610 | } 611 | if let Some(stripe_width) = stripe_width { 612 | arg_list.push(format!(",stripe_width={}", stripe_width)); 613 | } 614 | } 615 | 616 | arg_list.push("-I".to_string()); 617 | arg_list.push(inode_size.to_string()); 618 | 619 | arg_list.push("-m".to_string()); 620 | arg_list.push(reserved_blocks_percentage.to_string()); 621 | 622 | arg_list.push(device.as_ref().to_string_lossy().to_string()); 623 | 624 | process_output(&run_command("mkfs.ext4", &arg_list)?) 625 | } 626 | Filesystem::Zfs { 627 | ref block_size, 628 | ref compression, 629 | } => { 630 | // Check if zfs is installed 631 | if !Path::new("/sbin/zfs").exists() { 632 | return Err(BlockUtilsError::new("Please install zfsutils-linux".into())); 633 | } 634 | let base_name = device.as_ref().file_name(); 635 | match base_name { 636 | Some(name) => { 637 | //Mount at /mnt/{dev_name} 638 | let arg_list: Vec = vec![ 639 | "create".to_string(), 640 | "-f".to_string(), 641 | "-m".to_string(), 642 | format!("/mnt/{}", name.to_string_lossy().into_owned()), 643 | name.to_string_lossy().into_owned(), 644 | device.as_ref().to_string_lossy().into_owned(), 645 | ]; 646 | // Create the zpool 647 | let _ = process_output(&run_command("/sbin/zpool", &arg_list)?)?; 648 | if block_size.is_some() { 649 | // If zpool creation is successful then we set these 650 | let _ = process_output(&run_command( 651 | "/sbin/zfs", 652 | &[ 653 | "set".to_string(), 654 | format!("recordsize={}", block_size.unwrap()), 655 | name.to_string_lossy().into_owned(), 656 | ], 657 | )?)?; 658 | } 659 | if compression.is_some() { 660 | let _ = process_output(&run_command( 661 | "/sbin/zfs", 662 | &[ 663 | "set".to_string(), 664 | "compression=on".to_string(), 665 | name.to_string_lossy().into_owned(), 666 | ], 667 | )?)?; 668 | } 669 | let _ = process_output(&run_command( 670 | "/sbin/zfs", 671 | &[ 672 | "set".to_string(), 673 | "acltype=posixacl".to_string(), 674 | name.to_string_lossy().into_owned(), 675 | ], 676 | )?)?; 677 | let _ = process_output(&run_command( 678 | "/sbin/zfs", 679 | &[ 680 | "set".to_string(), 681 | "atime=off".to_string(), 682 | name.to_string_lossy().into_owned(), 683 | ], 684 | )?)?; 685 | Ok(0) 686 | } 687 | None => Err(BlockUtilsError::new(format!( 688 | "Unable to determine filename for device: {:?}", 689 | device.as_ref() 690 | ))), 691 | } 692 | } 693 | } 694 | } 695 | 696 | pub fn async_format_block_device( 697 | device: impl AsRef, 698 | filesystem: &Filesystem, 699 | ) -> BlockResult { 700 | match *filesystem { 701 | Filesystem::Btrfs { 702 | ref metadata_profile, 703 | ref leaf_size, 704 | ref node_size, 705 | } => { 706 | let arg_list: Vec = vec![ 707 | "-m".to_string(), 708 | metadata_profile.clone().to_string(), 709 | "-l".to_string(), 710 | leaf_size.to_string(), 711 | "-n".to_string(), 712 | node_size.to_string(), 713 | device.as_ref().to_string_lossy().to_string(), 714 | ]; 715 | // Check if mkfs.btrfs is installed 716 | if !Path::new("/sbin/mkfs.btrfs").exists() { 717 | return Err(BlockUtilsError::new("Please install btrfs-tools".into())); 718 | } 719 | Ok(AsyncInit { 720 | format_child: Command::new("mkfs.btrfs").args(&arg_list).spawn()?, 721 | post_setup_commands: vec![], 722 | device: device.as_ref().to_owned(), 723 | }) 724 | } 725 | Filesystem::Xfs { 726 | ref block_size, 727 | ref inode_size, 728 | ref stripe_size, 729 | ref stripe_width, 730 | ref force, 731 | ref agcount, 732 | } => { 733 | let mut arg_list: Vec = Vec::new(); 734 | 735 | if (*inode_size).is_some() { 736 | arg_list.push("-i".to_string()); 737 | arg_list.push(format!("size={}", inode_size.unwrap())); 738 | } 739 | 740 | if *force { 741 | arg_list.push("-f".to_string()); 742 | } 743 | 744 | if let Some(b) = block_size { 745 | arg_list.push("-b".to_string()); 746 | arg_list.push(b.to_string()); 747 | } 748 | 749 | if (*stripe_size).is_some() && (*stripe_width).is_some() { 750 | arg_list.push("-d".to_string()); 751 | arg_list.push(format!("su={}", stripe_size.unwrap())); 752 | arg_list.push(format!("sw={}", stripe_width.unwrap())); 753 | if (*agcount).is_some() { 754 | arg_list.push(format!("agcount={}", agcount.unwrap())); 755 | } 756 | } 757 | 758 | arg_list.push(device.as_ref().to_string_lossy().to_string()); 759 | 760 | // Check if mkfs.xfs is installed 761 | if !Path::new("/sbin/mkfs.xfs").exists() { 762 | return Err(BlockUtilsError::new("Please install xfsprogs".into())); 763 | } 764 | let format_handle = Command::new("/sbin/mkfs.xfs").args(&arg_list).spawn()?; 765 | Ok(AsyncInit { 766 | format_child: format_handle, 767 | post_setup_commands: vec![], 768 | device: device.as_ref().to_owned(), 769 | }) 770 | } 771 | Filesystem::Zfs { 772 | ref block_size, 773 | ref compression, 774 | } => { 775 | // Check if zfs is installed 776 | if !Path::new("/sbin/zfs").exists() { 777 | return Err(BlockUtilsError::new("Please install zfsutils-linux".into())); 778 | } 779 | let base_name = device.as_ref().file_name(); 780 | match base_name { 781 | Some(name) => { 782 | //Mount at /mnt/{dev_name} 783 | let mut post_setup_commands: Vec<(String, Vec)> = Vec::new(); 784 | let arg_list: Vec = vec![ 785 | "create".to_string(), 786 | "-f".to_string(), 787 | "-m".to_string(), 788 | format!("/mnt/{}", name.to_string_lossy().into_owned()), 789 | name.to_string_lossy().into_owned(), 790 | device.as_ref().to_string_lossy().into_owned(), 791 | ]; 792 | let zpool_create = Command::new("/sbin/zpool").args(&arg_list).spawn()?; 793 | 794 | if block_size.is_some() { 795 | // If zpool creation is successful then we set these 796 | post_setup_commands.push(( 797 | "/sbin/zfs".to_string(), 798 | vec![ 799 | "set".to_string(), 800 | format!("recordsize={}", block_size.unwrap()), 801 | name.to_string_lossy().into_owned(), 802 | ], 803 | )); 804 | } 805 | if compression.is_some() { 806 | post_setup_commands.push(( 807 | "/sbin/zfs".to_string(), 808 | vec![ 809 | "set".to_string(), 810 | "compression=on".to_string(), 811 | name.to_string_lossy().into_owned(), 812 | ], 813 | )); 814 | } 815 | post_setup_commands.push(( 816 | "/sbin/zfs".to_string(), 817 | vec![ 818 | "set".to_string(), 819 | "acltype=posixacl".to_string(), 820 | name.to_string_lossy().into_owned(), 821 | ], 822 | )); 823 | post_setup_commands.push(( 824 | "/sbin/zfs".to_string(), 825 | vec![ 826 | "set".to_string(), 827 | "atime=off".to_string(), 828 | name.to_string_lossy().into_owned(), 829 | ], 830 | )); 831 | Ok(AsyncInit { 832 | format_child: zpool_create, 833 | post_setup_commands, 834 | device: device.as_ref().to_owned(), 835 | }) 836 | } 837 | None => Err(BlockUtilsError::new(format!( 838 | "Unable to determine filename for device: {:?}", 839 | device.as_ref() 840 | ))), 841 | } 842 | } 843 | Filesystem::Ext4 { 844 | ref inode_size, 845 | ref reserved_blocks_percentage, 846 | ref stride, 847 | ref stripe_width, 848 | } => { 849 | let mut arg_list: Vec = 850 | vec!["-m".to_string(), reserved_blocks_percentage.to_string()]; 851 | 852 | arg_list.push("-I".to_string()); 853 | arg_list.push(inode_size.to_string()); 854 | 855 | if (*stride).is_some() { 856 | arg_list.push("-E".to_string()); 857 | arg_list.push(format!("stride={}", stride.unwrap())); 858 | } 859 | if (*stripe_width).is_some() { 860 | arg_list.push("-E".to_string()); 861 | arg_list.push(format!("stripe_width={}", stripe_width.unwrap())); 862 | } 863 | arg_list.push(device.as_ref().to_string_lossy().into_owned()); 864 | 865 | Ok(AsyncInit { 866 | format_child: Command::new("mkfs.ext4").args(&arg_list).spawn()?, 867 | post_setup_commands: vec![], 868 | device: device.as_ref().to_owned(), 869 | }) 870 | } 871 | } 872 | } 873 | 874 | #[cfg(target_os = "linux")] 875 | #[test] 876 | fn test_get_device_info() { 877 | print!("{:?}", get_device_info(&PathBuf::from("/dev/sda5"))); 878 | print!("{:?}", get_device_info(&PathBuf::from("/dev/loop0"))); 879 | } 880 | 881 | #[cfg(target_os = "linux")] 882 | fn get_udev_int_val(device: &udev::Device, attr_name: &str) -> Option { 883 | match device.attribute_value(attr_name) { 884 | Some(val_str) => { 885 | let val = val_str.to_str().unwrap_or("0").parse::().unwrap_or(0); 886 | Some(val) 887 | } 888 | None => None, 889 | } 890 | } 891 | 892 | #[cfg(target_os = "linux")] 893 | fn get_size(device: &udev::Device) -> Option { 894 | // 512 is the block size 895 | get_udev_int_val(device, "size").map(|s| s * 512) 896 | } 897 | 898 | #[cfg(target_os = "linux")] 899 | fn get_uuid(device: &udev::Device) -> Option { 900 | match device.property_value("ID_FS_UUID") { 901 | Some(value) => Uuid::parse_str(&value.to_string_lossy()).ok(), 902 | None => None, 903 | } 904 | } 905 | 906 | #[cfg(target_os = "linux")] 907 | fn get_serial(device: &udev::Device) -> Option { 908 | match device.property_value("ID_SERIAL") { 909 | Some(value) => Some(value.to_string_lossy().into_owned()), 910 | None => None, 911 | } 912 | } 913 | 914 | #[cfg(target_os = "linux")] 915 | fn get_fs_type(device: &udev::Device) -> BlockResult { 916 | match device.property_value("ID_FS_TYPE") { 917 | Some(s) => { 918 | let value = s.to_string_lossy(); 919 | Ok(FilesystemType::from_str(&value)?) 920 | } 921 | None => Ok(FilesystemType::Unknown), 922 | } 923 | } 924 | 925 | #[cfg(target_os = "linux")] 926 | fn get_media_type(device: &udev::Device) -> MediaType { 927 | use regex::Regex; 928 | let device_sysname = device.sysname().to_string_lossy(); 929 | 930 | // Test for loopback 931 | if let Ok(loop_regex) = Regex::new(r"loop\d+") { 932 | if loop_regex.is_match(&device_sysname) { 933 | return MediaType::Loopback; 934 | } 935 | } 936 | 937 | // Test for ramdisk 938 | if let Ok(ramdisk_regex) = Regex::new(r"ram\d+") { 939 | if ramdisk_regex.is_match(&device_sysname) { 940 | return MediaType::Ram; 941 | } 942 | } 943 | 944 | // Test for software raid 945 | if let Ok(ramdisk_regex) = Regex::new(r"md\d+") { 946 | if ramdisk_regex.is_match(&device_sysname) { 947 | return MediaType::MdRaid; 948 | } 949 | } 950 | 951 | // Test for nvme 952 | if device_sysname.contains("nvme") { 953 | return MediaType::NVME; 954 | } 955 | 956 | // Test for LVM 957 | if device.property_value("DM_NAME").is_some() { 958 | return MediaType::LVM; 959 | } 960 | 961 | // That should take care of the tricky ones. Lets try to identify if it's 962 | // SSD or rotational now 963 | if let Some(rotation) = device.property_value("ID_ATA_ROTATION_RATE_RPM") { 964 | return if rotation == "0" { 965 | MediaType::SolidState 966 | } else { 967 | MediaType::Rotational 968 | }; 969 | } 970 | 971 | // No rotation rate. Lets see if it's a virtual qemu disk 972 | if let Some(vendor) = device.property_value("ID_VENDOR") { 973 | let value = vendor.to_string_lossy(); 974 | return match value.as_ref() { 975 | "QEMU" => MediaType::Virtual, 976 | _ => MediaType::Unknown, 977 | }; 978 | } 979 | 980 | // I give up 981 | MediaType::Unknown 982 | } 983 | 984 | #[cfg(target_os = "linux")] 985 | fn get_device_type(device: &udev::Device) -> BlockResult { 986 | match device.devtype() { 987 | Some(s) => { 988 | let value = s.to_string_lossy(); 989 | DeviceType::from_str(&value) 990 | } 991 | None => Ok(DeviceType::Unknown), 992 | } 993 | } 994 | 995 | /// Checks and returns if a particular directory is a mountpoint 996 | pub fn is_mounted(directory: impl AsRef) -> BlockResult { 997 | let parent = directory.as_ref().parent(); 998 | 999 | let dir_metadata = fs::metadata(&directory)?; 1000 | let file_type = dir_metadata.file_type(); 1001 | 1002 | if file_type.is_symlink() { 1003 | // A symlink can never be a mount point 1004 | return Ok(false); 1005 | } 1006 | 1007 | Ok(if let Some(parent) = parent { 1008 | let parent_metadata = fs::metadata(parent)?; 1009 | // path/.. on a different device as path 1010 | parent_metadata.dev() != dir_metadata.dev() 1011 | } else { 1012 | // If the directory doesn't have a parent it's the root filesystem 1013 | false 1014 | }) 1015 | } 1016 | 1017 | /// Scan a system and return iterator over all block devices that udev knows about 1018 | /// This function will only return the udev devices identified as `requested_dev_type` 1019 | /// (disk or partition) 1020 | /// If it can't discover this it will error on the side of caution and 1021 | /// return the device 1022 | /// 1023 | #[cfg(target_os = "linux")] 1024 | fn get_specific_block_device_iter( 1025 | requested_dev_type: DeviceType, 1026 | ) -> BlockResult> { 1027 | Ok(udev::Enumerator::new()? 1028 | .scan_devices()? 1029 | .filter_map(move |device| { 1030 | if device.subsystem() == Some(OsStr::new("block")) { 1031 | let is_partition = device.devtype().map_or(false, |d| d == "partition"); 1032 | let dev_type = if is_partition { 1033 | DeviceType::Partition 1034 | } else { 1035 | DeviceType::Disk 1036 | }; 1037 | 1038 | if dev_type == requested_dev_type { 1039 | Some(PathBuf::from("/dev").join(device.sysname())) 1040 | } else { 1041 | None 1042 | } 1043 | } else { 1044 | None 1045 | } 1046 | })) 1047 | } 1048 | 1049 | /// Scan a system and return iterator over all block devices that udev knows about 1050 | /// This function will only retun the udev devices identified as partition. 1051 | /// If it can't discover this it will error on the side of caution and 1052 | /// return the device 1053 | /// 1054 | /// Lazy version of `get_block_partitions` 1055 | #[cfg(target_os = "linux")] 1056 | pub fn get_block_partitions_iter() -> BlockResult> { 1057 | get_specific_block_device_iter(DeviceType::Partition) 1058 | } 1059 | 1060 | /// Scan a system and return all block devices that udev knows about 1061 | /// This function will only retun the udev devices identified as partition. 1062 | /// If it can't discover this it will error on the side of caution and 1063 | /// return the device 1064 | /// 1065 | /// Non-lazy version of `get_block_partitions` 1066 | #[cfg(target_os = "linux")] 1067 | pub fn get_block_partitions() -> BlockResult> { 1068 | get_block_partitions_iter().map(|i| i.collect()) 1069 | } 1070 | 1071 | /// Scan a system and return iterator over all block devices that udev knows about 1072 | /// This function will skip udev devices identified as partition. If 1073 | /// it can't discover this it will error on the side of caution and 1074 | /// return the device 1075 | /// 1076 | /// Lazy version of `get_block_devices()` 1077 | #[cfg(target_os = "linux")] 1078 | pub fn get_block_devices_iter() -> BlockResult> { 1079 | get_specific_block_device_iter(DeviceType::Disk) 1080 | } 1081 | 1082 | /// Scan a system and return all block devices that udev knows about 1083 | /// This function will skip udev devices identified as partition. If 1084 | /// it can't discover this it will error on the side of caution and 1085 | /// return the device 1086 | /// 1087 | /// Non-lazy version of `get_block_devices_iter()` 1088 | #[cfg(target_os = "linux")] 1089 | pub fn get_block_devices() -> BlockResult> { 1090 | get_block_devices_iter().map(|i| i.collect()) 1091 | } 1092 | 1093 | /// Checks to see if the subsystem this device is using is block 1094 | #[cfg(target_os = "linux")] 1095 | pub fn is_block_device(device_path: impl AsRef) -> BlockResult { 1096 | let mut enumerator = udev::Enumerator::new()?; 1097 | let devices = enumerator.scan_devices()?; 1098 | 1099 | let sysname = device_path.as_ref().file_name().ok_or_else(|| { 1100 | BlockUtilsError::new(format!( 1101 | "Unable to get file_name on device {:?}", 1102 | device_path.as_ref() 1103 | )) 1104 | })?; 1105 | 1106 | for device in devices { 1107 | if sysname == device.sysname() && device.subsystem() == Some(OsStr::new("block")) { 1108 | return Ok(true); 1109 | } 1110 | } 1111 | 1112 | Err(BlockUtilsError::new(format!( 1113 | "Unable to find device with name {:?}", 1114 | device_path.as_ref() 1115 | ))) 1116 | } 1117 | 1118 | /// Get sys path (like `/sys/class/block/loop0`) by dev path (like `/dev/loop0`). 1119 | /// Dev path should refer to block device. 1120 | /// Returns error if sys path doesn't exist. 1121 | fn dev_path_to_sys_path(dev_path: impl AsRef) -> BlockResult { 1122 | let sys_path = dev_path 1123 | .as_ref() 1124 | .file_name() 1125 | .map(|name| PathBuf::from("/sys/class/block").join(name)) 1126 | .ok_or_else(|| { 1127 | BlockUtilsError::new(format!( 1128 | "Unable to get file_name on device {:?}", 1129 | dev_path.as_ref() 1130 | )) 1131 | })?; 1132 | if sys_path.exists() { 1133 | Ok(sys_path) 1134 | } else { 1135 | Err(BlockUtilsError::new(format!( 1136 | "Sys path {} doesn't exist. Maybe {} is not a block device", 1137 | sys_path.display(), 1138 | dev_path.as_ref().display() 1139 | ))) 1140 | } 1141 | } 1142 | 1143 | /// Get property value by key `tag` for device with devpath `device_path` (like "/dev/sda") if present 1144 | #[cfg(target_os = "linux")] 1145 | pub fn get_block_dev_property( 1146 | device_path: impl AsRef, 1147 | tag: &str, 1148 | ) -> BlockResult> { 1149 | let syspath = dev_path_to_sys_path(device_path)?; 1150 | 1151 | Ok(udev::Device::from_syspath(&syspath)? 1152 | .property_value(tag) 1153 | .map(|value| value.to_string_lossy().to_string())) 1154 | } 1155 | 1156 | /// Get properties for device with devpath `device_path` (like "/dev/sda") if present 1157 | #[cfg(target_os = "linux")] 1158 | pub fn get_block_dev_properties( 1159 | device_path: impl AsRef, 1160 | ) -> BlockResult> { 1161 | let syspath = dev_path_to_sys_path(device_path)?; 1162 | 1163 | let udev_device = udev::Device::from_syspath(&syspath)?; 1164 | Ok(udev_device 1165 | .clone() 1166 | .properties() 1167 | .map(|property| { 1168 | let key = property.name().to_string_lossy().to_string(); 1169 | let value = property.value().to_string_lossy().to_string(); 1170 | (key, value) 1171 | }) 1172 | .collect()) // We can't return iterator because `udev_device` doesn't live long enough 1173 | } 1174 | 1175 | /// A raid array enclosure 1176 | #[derive(Clone, Debug, Default)] 1177 | pub struct Enclosure { 1178 | pub active: Option, 1179 | pub fault: Option, 1180 | pub power_status: Option, 1181 | pub slot: u8, 1182 | pub status: Option, 1183 | pub enclosure_type: Option, 1184 | } 1185 | 1186 | #[derive(Clone, Debug, PartialEq, Display, EnumString)] 1187 | #[strum(serialize_all = "snake_case")] 1188 | pub enum DeviceState { 1189 | Blocked, 1190 | #[strum(serialize = "failfast")] 1191 | FailFast, 1192 | Lost, 1193 | Running, 1194 | RunningRta, 1195 | } 1196 | 1197 | #[derive(Clone, Debug)] 1198 | pub struct ScsiInfo { 1199 | pub block_device: Option, 1200 | pub enclosure: Option, 1201 | pub host: String, 1202 | pub channel: u8, 1203 | pub id: u8, 1204 | pub lun: u8, 1205 | pub vendor: Vendor, 1206 | pub vendor_str: Option, 1207 | pub model: Option, 1208 | pub rev: Option, 1209 | pub state: Option, 1210 | pub scsi_type: ScsiDeviceType, 1211 | pub scsi_revision: u32, 1212 | } 1213 | 1214 | // Taken from https://github.com/hreinecke/lsscsi/blob/master/src/lsscsi.c 1215 | #[derive(Clone, Copy, Debug, PartialEq, EnumString)] 1216 | pub enum ScsiDeviceType { 1217 | #[strum(serialize = "0", serialize = "Direct-Access")] 1218 | DirectAccess, 1219 | #[strum(serialize = "1")] 1220 | SequentialAccess, 1221 | #[strum(serialize = "2")] 1222 | Printer, 1223 | #[strum(serialize = "3")] 1224 | Processor, 1225 | #[strum(serialize = "4")] 1226 | WriteOnce, 1227 | #[strum(serialize = "5")] 1228 | CdRom, 1229 | #[strum(serialize = "6")] 1230 | Scanner, 1231 | #[strum(serialize = "7")] 1232 | Opticalmemory, 1233 | #[strum(serialize = "8")] 1234 | MediumChanger, 1235 | #[strum(serialize = "9")] 1236 | Communications, 1237 | #[strum(serialize = "10")] 1238 | Unknowna, 1239 | #[strum(serialize = "11")] 1240 | Unknownb, 1241 | #[strum(serialize = "12", serialize = "RAID")] 1242 | StorageArray, 1243 | #[strum(serialize = "13", serialize = "Enclosure")] 1244 | Enclosure, 1245 | #[strum(serialize = "14")] 1246 | SimplifiedDirectAccess, 1247 | #[strum(serialize = "15")] 1248 | OpticalCardReadWriter, 1249 | #[strum(serialize = "16")] 1250 | BridgeController, 1251 | #[strum(serialize = "17")] 1252 | ObjectBasedStorage, 1253 | #[strum(serialize = "18")] 1254 | AutomationDriveInterface, 1255 | #[strum(serialize = "19")] 1256 | SecurityManager, 1257 | #[strum(serialize = "20")] 1258 | ZonedBlock, 1259 | #[strum(serialize = "21")] 1260 | Reserved15, 1261 | #[strum(serialize = "22")] 1262 | Reserved16, 1263 | #[strum(serialize = "23")] 1264 | Reserved17, 1265 | #[strum(serialize = "24")] 1266 | Reserved18, 1267 | #[strum(serialize = "25")] 1268 | Reserved19, 1269 | #[strum(serialize = "26")] 1270 | Reserved1a, 1271 | #[strum(serialize = "27")] 1272 | Reserved1b, 1273 | #[strum(serialize = "28")] 1274 | Reserved1c, 1275 | #[strum(serialize = "29")] 1276 | Reserved1e, 1277 | #[strum(serialize = "30")] 1278 | WellKnownLu, 1279 | #[strum(serialize = "31")] 1280 | NoDevice, 1281 | } 1282 | 1283 | impl Default for ScsiInfo { 1284 | fn default() -> ScsiInfo { 1285 | ScsiInfo { 1286 | block_device: None, 1287 | enclosure: None, 1288 | host: String::new(), 1289 | channel: 0, 1290 | id: 0, 1291 | lun: 0, 1292 | vendor: Vendor::None, 1293 | vendor_str: Option::None, 1294 | model: None, 1295 | rev: None, 1296 | state: None, 1297 | scsi_type: ScsiDeviceType::NoDevice, 1298 | scsi_revision: 0, 1299 | } 1300 | } 1301 | } 1302 | 1303 | impl PartialEq for ScsiInfo { 1304 | fn eq(&self, other: &ScsiInfo) -> bool { 1305 | self.host == other.host 1306 | && self.channel == other.channel 1307 | && self.id == other.id 1308 | && self.lun == other.lun 1309 | } 1310 | } 1311 | 1312 | fn scsi_host_info(input: &str) -> Result, BlockUtilsError> { 1313 | let mut scsi_devices = Vec::new(); 1314 | // Simple brute force parser 1315 | let mut scsi_info = ScsiInfo::default(); 1316 | for line in input.lines() { 1317 | if line.starts_with("Attached devices") { 1318 | continue; 1319 | } 1320 | if line.starts_with("Host") { 1321 | scsi_devices.push(scsi_info); 1322 | scsi_info = ScsiInfo::default(); 1323 | let parts = line.split_whitespace().collect::>(); 1324 | if parts.len() < 8 { 1325 | // Invalid line 1326 | continue; 1327 | } 1328 | // Any part that contains ':' is a key/value pair 1329 | scsi_info.host = parts[1].to_string(); 1330 | scsi_info.channel = parts[3].parse::()?; 1331 | scsi_info.id = parts[5].parse::()?; 1332 | scsi_info.lun = parts[7].parse::()?; 1333 | } 1334 | if line.contains("Vendor") { 1335 | let parts = line.split_whitespace().collect::>(); 1336 | scsi_info.vendor = parts[1].parse::()?; 1337 | // Take until : is found 1338 | let model = parts[3..] 1339 | .iter() 1340 | .take_while(|s| !s.contains(":")) 1341 | .map(|s| *s) 1342 | .collect::>(); 1343 | if !model.is_empty() { 1344 | scsi_info.model = Some(model.join(" ").to_string()); 1345 | } 1346 | // Find where Rev: is and take the next part 1347 | let rev_position = parts.iter().position(|s| s.contains("Rev:")); 1348 | if let Some(rev_position) = rev_position { 1349 | scsi_info.rev = Some(parts[rev_position + 1].to_string()); 1350 | } 1351 | } 1352 | if line.contains("Type") { 1353 | let parts = line.split_whitespace().collect::>(); 1354 | scsi_info.scsi_type = parts[1].parse::()?; 1355 | scsi_info.scsi_revision = parts[5].parse::()?; 1356 | } 1357 | } 1358 | 1359 | Ok(scsi_devices) 1360 | } 1361 | 1362 | #[test] 1363 | fn test_scsi_parser() { 1364 | let s = fs::read_to_string("tests/proc_scsi").unwrap(); 1365 | println!("scsi_host_info {:#?}", scsi_host_info(&s)); 1366 | } 1367 | 1368 | #[test] 1369 | fn test_sort_raid_info() { 1370 | let mut scsi_0 = ScsiInfo::default(); 1371 | scsi_0.host = "scsi6".to_string(); 1372 | scsi_0.channel = 0; 1373 | scsi_0.id = 0; 1374 | scsi_0.lun = 0; 1375 | let mut scsi_1 = ScsiInfo::default(); 1376 | scsi_1.host = "scsi2".to_string(); 1377 | scsi_1.channel = 0; 1378 | scsi_1.id = 0; 1379 | scsi_1.lun = 0; 1380 | let mut scsi_2 = ScsiInfo::default(); 1381 | scsi_2.host = "scsi2".to_string(); 1382 | scsi_2.channel = 1; 1383 | scsi_2.id = 0; 1384 | scsi_2.lun = 0; 1385 | let mut scsi_3 = ScsiInfo::default(); 1386 | scsi_3.host = "scsi2".to_string(); 1387 | scsi_3.channel = 1; 1388 | scsi_3.id = 0; 1389 | scsi_3.lun = 1; 1390 | 1391 | let scsi_info = vec![scsi_0, scsi_1, scsi_2, scsi_3]; 1392 | sort_scsi_info(&scsi_info); 1393 | } 1394 | 1395 | /// Examine the ScsiInfo devices and associate a host ScsiInfo device if it 1396 | /// exists 1397 | /// 1398 | /// Lazy version of `sort_scsi_info` 1399 | pub fn sort_scsi_info_iter<'a>( 1400 | info: &'a [ScsiInfo], 1401 | ) -> impl Iterator)> + 'a { 1402 | info.iter().map(move |dev| { 1403 | // Find the position of the host this device belongs to possibly 1404 | let host = info 1405 | .iter() 1406 | .position(|d| d.host == dev.host && d.channel == 0 && d.id == 0 && d.lun == 0); 1407 | match host { 1408 | Some(pos) => { 1409 | let host_dev = info[pos].clone(); 1410 | // If the host is itself then don't add it 1411 | if host_dev == *dev { 1412 | (dev.clone(), None) 1413 | } else { 1414 | (dev.clone(), Some(info[pos].clone())) 1415 | } 1416 | } 1417 | None => (dev.clone(), None), 1418 | } 1419 | }) 1420 | } 1421 | 1422 | /// Examine the ScsiInfo devices and associate a host ScsiInfo device if it 1423 | /// exists 1424 | /// 1425 | /// Non-lazy version of `sort_scsi_info_iter` 1426 | pub fn sort_scsi_info(info: &[ScsiInfo]) -> Vec<(ScsiInfo, Option)> { 1427 | sort_scsi_info_iter(info).collect() 1428 | } 1429 | 1430 | fn get_enclosure_data(p: impl AsRef) -> BlockResult { 1431 | let mut e = Enclosure::default(); 1432 | for entry in read_dir(p)? { 1433 | let entry = entry?; 1434 | if entry.file_name() == OsStr::new("active") { 1435 | e.active = Some(fs::read_to_string(&entry.path())?.trim().to_string()); 1436 | } else if entry.file_name() == OsStr::new("fault") { 1437 | e.fault = Some(fs::read_to_string(&entry.path())?.trim().to_string()); 1438 | } else if entry.file_name() == OsStr::new("power_status") { 1439 | e.power_status = Some(fs::read_to_string(&entry.path())?.trim().to_string()); 1440 | } else if entry.file_name() == OsStr::new("slot") { 1441 | e.slot = u8::from_str(fs::read_to_string(&entry.path())?.trim())?; 1442 | } else if entry.file_name() == OsStr::new("status") { 1443 | e.status = Some(fs::read_to_string(&entry.path())?.trim().to_string()); 1444 | } else if entry.file_name() == OsStr::new("type") { 1445 | e.enclosure_type = Some(fs::read_to_string(&entry.path())?.trim().to_string()); 1446 | } 1447 | } 1448 | 1449 | Ok(e) 1450 | } 1451 | 1452 | /// Gathers all available scsi information 1453 | pub fn get_scsi_info() -> BlockResult> { 1454 | // Taken from the strace output of lsscsi 1455 | let scsi_path = Path::new("/sys/bus/scsi/devices"); 1456 | if scsi_path.exists() { 1457 | let mut scsi_devices: Vec = Vec::new(); 1458 | for entry in read_dir(&scsi_path)? { 1459 | let entry = entry?; 1460 | let path = entry.path(); 1461 | let name = path.file_name(); 1462 | if let Some(name) = name { 1463 | let n = name.to_string_lossy(); 1464 | let f = match n.chars().next() { 1465 | Some(c) => c, 1466 | None => { 1467 | warn!("{} doesn't have any characters. Skipping", n); 1468 | continue; 1469 | } 1470 | }; 1471 | // Only get the devices that start with a digit 1472 | if f.is_digit(10) { 1473 | let mut s = ScsiInfo::default(); 1474 | let parts: Vec<&str> = n.split(':').collect(); 1475 | if parts.len() != 4 { 1476 | warn!("Invalid device name: {}. Should be 0:0:0:0 format", n); 1477 | continue; 1478 | } 1479 | s.host = parts[0].to_string(); 1480 | s.channel = u8::from_str(parts[1])?; 1481 | s.id = u8::from_str(parts[2])?; 1482 | s.lun = u8::from_str(parts[3])?; 1483 | for scsi_entries in read_dir(&path)? { 1484 | let scsi_entry = scsi_entries?; 1485 | if scsi_entry.file_name() == OsStr::new("block") { 1486 | let block_path = path.join("block"); 1487 | if block_path.exists() { 1488 | let mut device_name = read_dir(&block_path)?.take(1); 1489 | if let Some(name) = device_name.next() { 1490 | s.block_device = 1491 | Some(Path::new("/dev/").join(name?.file_name())); 1492 | } 1493 | } 1494 | } else if scsi_entry 1495 | .file_name() 1496 | .to_string_lossy() 1497 | .starts_with("enclosure_device") 1498 | { 1499 | let enclosure_path = path.join(scsi_entry.file_name()); 1500 | let e = get_enclosure_data(&enclosure_path)?; 1501 | s.enclosure = Some(e); 1502 | } else if scsi_entry.file_name() == OsStr::new("model") { 1503 | s.model = 1504 | Some(fs::read_to_string(&scsi_entry.path())?.trim().to_string()); 1505 | } else if scsi_entry.file_name() == OsStr::new("rev") { 1506 | s.rev = 1507 | Some(fs::read_to_string(&scsi_entry.path())?.trim().to_string()); 1508 | } else if scsi_entry.file_name() == OsStr::new("state") { 1509 | s.state = Some(DeviceState::from_str( 1510 | fs::read_to_string(&scsi_entry.path())?.trim(), 1511 | )?); 1512 | } else if scsi_entry.file_name() == OsStr::new("type") { 1513 | s.scsi_type = ScsiDeviceType::from_str( 1514 | fs::read_to_string(&scsi_entry.path())?.trim(), 1515 | )?; 1516 | } else if scsi_entry.file_name() == OsStr::new("vendor") { 1517 | let vendor_str = fs::read_to_string(&scsi_entry.path())?; 1518 | s.vendor_str = Some(vendor_str.trim().to_string()); 1519 | s.vendor = Vendor::from_str(vendor_str.trim()).unwrap_or(Vendor::None); 1520 | } 1521 | } 1522 | scsi_devices.push(s); 1523 | } 1524 | } 1525 | } 1526 | Ok(scsi_devices) 1527 | } else { 1528 | // Fallback behavior still works but gathers much less information 1529 | let buff = fs::read_to_string("/proc/scsi/scsi")?; 1530 | 1531 | Ok(scsi_host_info(&buff)?) 1532 | } 1533 | } 1534 | 1535 | /// check if the path is a disk device path 1536 | #[cfg(target_os = "linux")] 1537 | pub fn is_disk(dev_path: impl AsRef) -> BlockResult { 1538 | let mut enumerator = udev::Enumerator::new()?; 1539 | let host_devices = enumerator.scan_devices()?; 1540 | for device in host_devices { 1541 | if let Some(dev_type) = device.devtype() { 1542 | let name = Path::new("/dev").join(device.sysname()); 1543 | if dev_type == "disk" && name == dev_path.as_ref() { 1544 | return Ok(true); 1545 | } 1546 | } 1547 | } 1548 | Ok(false) 1549 | } 1550 | 1551 | #[cfg(target_os = "linux")] 1552 | fn get_parent_name(device: &udev::Device) -> Option { 1553 | if let Some(parent_dev) = device.parent() { 1554 | if let Some(dev_type) = parent_dev.devtype() { 1555 | if dev_type == "disk" || dev_type == "partition" { 1556 | let name = Path::new("/dev").join(parent_dev.sysname()); 1557 | Some(name) 1558 | } else { 1559 | None 1560 | } 1561 | } else { 1562 | None 1563 | } 1564 | } else { 1565 | None 1566 | } 1567 | } 1568 | 1569 | /// get the parent device path from a device path (If not a partition or disk, return None) 1570 | #[cfg(target_os = "linux")] 1571 | pub fn get_parent_devpath_from_path(dev_path: impl AsRef) -> BlockResult> { 1572 | let mut enumerator = udev::Enumerator::new()?; 1573 | let host_devices = enumerator.scan_devices()?; 1574 | for device in host_devices { 1575 | if let Some(dev_type) = device.devtype() { 1576 | if dev_type == "disk" || dev_type == "partition" { 1577 | let name = Path::new("/dev").join(device.sysname()); 1578 | let dev_links = OsStr::new("DEVLINKS"); 1579 | if dev_path.as_ref() == name { 1580 | if let Some(name) = get_parent_name(&device) { 1581 | return Ok(Some(name)); 1582 | } 1583 | } 1584 | if let Some(links) = device.property_value(dev_links) { 1585 | let path = dev_path.as_ref().to_string_lossy().to_string(); 1586 | if links.to_string_lossy().contains(&path) { 1587 | if let Some(name) = get_parent_name(&device) { 1588 | return Ok(Some(name)); 1589 | } 1590 | } 1591 | } 1592 | } 1593 | } 1594 | } 1595 | Ok(None) 1596 | } 1597 | 1598 | /// Get the children devices paths from a device path 1599 | #[cfg(target_os = "linux")] 1600 | pub fn get_children_devpaths_from_path(dev_path: impl AsRef) -> BlockResult> { 1601 | get_children_devpaths_from_path_iter(dev_path).map(|iter| iter.collect()) 1602 | } 1603 | 1604 | /// Get the children devices paths from a device path 1605 | /// Note: It has square algorithmic complexity 1606 | #[cfg(target_os = "linux")] 1607 | pub fn get_children_devpaths_from_path_iter( 1608 | dev_path: impl AsRef, 1609 | ) -> BlockResult> { 1610 | Ok(get_block_partitions_iter()?.filter(move |partition| { 1611 | if let Ok(Some(parent_device)) = get_parent_devpath_from_path(partition) { 1612 | dev_path.as_ref() == &parent_device 1613 | } else { 1614 | false 1615 | } 1616 | })) 1617 | } 1618 | 1619 | /// returns the device info and possibly partition entry for the device with the path or symlink given 1620 | #[cfg(target_os = "linux")] 1621 | pub fn get_device_from_path( 1622 | dev_path: impl AsRef, 1623 | ) -> BlockResult<(Option, Option)> { 1624 | let mut enumerator = udev::Enumerator::new()?; 1625 | let host_devices = enumerator.scan_devices()?; 1626 | for device in host_devices { 1627 | if let Some(dev_type) = device.devtype() { 1628 | if dev_type == "disk" || dev_type == "partition" { 1629 | let name = Path::new("/dev").join(device.sysname()); 1630 | let dev_links = OsStr::new("DEVLINKS"); 1631 | if dev_path.as_ref() == name { 1632 | let part_num = match device.property_value("ID_PART_ENTRY_NUMBER") { 1633 | Some(value) => value.to_string_lossy().parse::().ok(), 1634 | None => None, 1635 | }; 1636 | let dev = Device::from_udev_device(device)?; 1637 | return Ok((part_num, Some(dev))); 1638 | } 1639 | if let Some(links) = device.property_value(dev_links) { 1640 | let path = dev_path.as_ref().to_string_lossy().to_string(); 1641 | if links.to_string_lossy().contains(&path) { 1642 | let part_num = match device.property_value("ID_PART_ENTRY_NUMBER") { 1643 | Some(value) => value.to_string_lossy().parse::().ok(), 1644 | None => None, 1645 | }; 1646 | let dev = Device::from_udev_device(device)?; 1647 | return Ok((part_num, Some(dev))); 1648 | } 1649 | } 1650 | } 1651 | } 1652 | } 1653 | Ok((None, None)) 1654 | } 1655 | 1656 | /// Returns iterator over device info on every device it can find in the devices slice 1657 | /// The device info may not be in the same order as the slice so be aware. 1658 | /// This function is more efficient because it only call udev list once 1659 | /// 1660 | /// Lazy version of get_all_device_info 1661 | #[cfg(target_os = "linux")] 1662 | pub fn get_all_device_info_iter( 1663 | devices: T, 1664 | ) -> BlockResult>> 1665 | where 1666 | P: AsRef, 1667 | T: AsRef<[P]>, 1668 | { 1669 | let device_names = devices 1670 | .as_ref() 1671 | .iter() 1672 | .filter_map(|d| d.as_ref().file_name().map(OsStr::to_owned)) 1673 | .collect::>(); 1674 | 1675 | Ok(udev::Enumerator::new()?.scan_devices()?.filter_map( 1676 | move |device| -> Option> { 1677 | if device_names.contains(&device.sysname().to_owned()) 1678 | && device.subsystem() == Some(OsStr::new("block")) 1679 | { 1680 | // Ok we're a block device 1681 | Some(Device::from_udev_device(device)) 1682 | } else { 1683 | None 1684 | } 1685 | }, 1686 | )) 1687 | } 1688 | 1689 | /// Returns device info on every device it can find in the devices slice 1690 | /// The device info may not be in the same order as the slice so be aware. 1691 | /// This function is more efficient because it only call udev list once 1692 | /// 1693 | /// Non-lazy version of `get_all_device_info_iter` 1694 | #[cfg(target_os = "linux")] 1695 | pub fn get_all_device_info(devices: T) -> BlockResult> 1696 | where 1697 | P: AsRef, 1698 | T: AsRef<[P]>, 1699 | { 1700 | get_all_device_info_iter(devices).map(|i| i.collect::>>())? 1701 | } 1702 | 1703 | /// Returns device information that is gathered with udev. 1704 | #[cfg(target_os = "linux")] 1705 | pub fn get_device_info(device_path: impl AsRef) -> BlockResult { 1706 | let error_message = format!( 1707 | "Unable to get file_name on device {:?}", 1708 | device_path.as_ref() 1709 | ); 1710 | let sysname = device_path 1711 | .as_ref() 1712 | .file_name() 1713 | .ok_or_else(|| BlockUtilsError::new(error_message.clone()))?; 1714 | 1715 | udev::Enumerator::new()? 1716 | .scan_devices()? 1717 | .find(|udev_device| { 1718 | sysname == udev_device.sysname() && udev_device.subsystem() == Some(OsStr::new("block")) 1719 | }) 1720 | .ok_or_else(|| BlockUtilsError::new(error_message)) 1721 | .and_then(Device::from_udev_device) 1722 | } 1723 | 1724 | pub fn set_elevator(device_path: impl AsRef, elevator: &Scheduler) -> BlockResult { 1725 | let device_name = match device_path.as_ref().file_name() { 1726 | Some(name) => name.to_string_lossy().into_owned(), 1727 | None => "".to_string(), 1728 | }; 1729 | let mut f = File::open("/etc/rc.local")?; 1730 | let elevator_cmd = format!( 1731 | "echo {scheduler} > /sys/block/{device}/queue/scheduler", 1732 | scheduler = elevator, 1733 | device = device_name 1734 | ); 1735 | 1736 | let mut script = shellscript::parse(&mut f)?; 1737 | let existing_cmd = script 1738 | .commands 1739 | .iter() 1740 | .position(|cmd| cmd.contains(&device_name)); 1741 | if let Some(pos) = existing_cmd { 1742 | script.commands.remove(pos); 1743 | } 1744 | script.commands.push(elevator_cmd); 1745 | let mut f = File::create("/etc/rc.local")?; 1746 | let bytes_written = script.write(&mut f)?; 1747 | Ok(bytes_written) 1748 | } 1749 | 1750 | pub fn weekly_defrag( 1751 | mount: impl AsRef, 1752 | fs_type: &FilesystemType, 1753 | interval: &str, 1754 | ) -> BlockResult { 1755 | let crontab = Path::new("/var/spool/cron/crontabs/root"); 1756 | let defrag_command = match *fs_type { 1757 | FilesystemType::Ext4 => "e4defrag", 1758 | FilesystemType::Btrfs => "btrfs filesystem defragment -r", 1759 | FilesystemType::Xfs => "xfs_fsr", 1760 | _ => "", 1761 | }; 1762 | let job = format!( 1763 | "{interval} {cmd} {path}", 1764 | interval = interval, 1765 | cmd = defrag_command, 1766 | path = mount.as_ref().display() 1767 | ); 1768 | 1769 | //TODO Change over to using the cronparse library. Has much better parsing however 1770 | //there's currently no way to add new entries yet 1771 | let mut existing_crontab = { 1772 | if crontab.exists() { 1773 | let buff = fs::read_to_string("/var/spool/cron/crontabs/root")?; 1774 | buff.split('\n') 1775 | .map(|s| s.to_string()) 1776 | .collect::>() 1777 | } else { 1778 | Vec::new() 1779 | } 1780 | }; 1781 | let mount_str = mount.as_ref().to_string_lossy().into_owned(); 1782 | let existing_job_position = existing_crontab 1783 | .iter() 1784 | .position(|line| line.contains(&mount_str)); 1785 | // If we found an existing job we remove the old and insert the new job 1786 | if let Some(pos) = existing_job_position { 1787 | existing_crontab.remove(pos); 1788 | } 1789 | existing_crontab.push(job); 1790 | 1791 | //Write back out 1792 | let mut f = File::create("/var/spool/cron/crontabs/root")?; 1793 | let written_bytes = f.write(&existing_crontab.join("\n").as_bytes())?; 1794 | Ok(written_bytes) 1795 | } 1796 | -------------------------------------------------------------------------------- /src/nvme.rs: -------------------------------------------------------------------------------- 1 | use crate::{BlockResult, BlockUtilsError}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::Path; 4 | use std::process::Command; 5 | 6 | #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] 7 | #[serde(rename_all = "PascalCase")] 8 | pub struct NvmeDevice { 9 | pub name_space: u64, 10 | pub device_path: String, 11 | pub index: Option, 12 | pub model_number: String, 13 | pub product_name: Option, 14 | pub firmware: Option, 15 | pub serial_number: String, 16 | pub used_bytes: u64, 17 | #[serde(rename = "MaximumLBA")] 18 | pub maximum_lba: u64, 19 | pub physical_size: u64, 20 | pub sector_size: u32, 21 | } 22 | 23 | #[derive(Deserialize)] 24 | #[serde(rename_all = "PascalCase")] 25 | struct NvmeDeviceContainer { 26 | devices: Vec, 27 | } 28 | 29 | /// Retrieve the error logs from the nvme device 30 | pub fn get_error_log(dev: &Path) -> BlockResult { 31 | let out = Command::new("nvme") 32 | .args(&["error-log", &dev.to_string_lossy(), "-o", "json"]) 33 | .output()?; 34 | if !out.status.success() { 35 | return Err(BlockUtilsError::new( 36 | String::from_utf8_lossy(&out.stderr).into_owned(), 37 | )); 38 | } 39 | let stdout = String::from_utf8_lossy(&out.stdout); 40 | let deserialized: serde_json::Value = serde_json::from_str(&stdout)?; 41 | Ok(deserialized) 42 | } 43 | 44 | /// Retrieve the firmware logs from the nvme device 45 | pub fn get_firmware_log(dev: &Path) -> BlockResult { 46 | let out = Command::new("nvme") 47 | .args(&["fw-log", &dev.to_string_lossy(), "-o", "json"]) 48 | .output()?; 49 | if !out.status.success() { 50 | return Err(BlockUtilsError::new( 51 | String::from_utf8_lossy(&out.stderr).into_owned(), 52 | )); 53 | } 54 | let stdout = String::from_utf8_lossy(&out.stdout); 55 | let deserialized: serde_json::Value = serde_json::from_str(&stdout)?; 56 | Ok(deserialized) 57 | } 58 | 59 | /// Retrieve the smart logs from the nvme device 60 | pub fn get_smart_log(dev: &Path) -> BlockResult { 61 | let out = Command::new("nvme") 62 | .args(&["smart-log", &dev.to_string_lossy(), "-o", "json"]) 63 | .output()?; 64 | if !out.status.success() { 65 | return Err(BlockUtilsError::new( 66 | String::from_utf8_lossy(&out.stderr).into_owned(), 67 | )); 68 | } 69 | let stdout = String::from_utf8_lossy(&out.stdout); 70 | let deserialized: serde_json::Value = serde_json::from_str(&stdout)?; 71 | Ok(deserialized) 72 | } 73 | 74 | /// Retrieve a log page from the nvme device 75 | pub fn get_log(dev: &Path, id: u8, len: u16) -> BlockResult> { 76 | let out = Command::new("nvme") 77 | .args(&[ 78 | "get-log", 79 | &dev.to_string_lossy(), 80 | "-i", 81 | id.to_string().as_str(), 82 | "-l", 83 | len.to_string().as_str(), 84 | "-b", 85 | ]) 86 | .output()?; 87 | if !out.status.success() { 88 | return Err(BlockUtilsError::new( 89 | String::from_utf8_lossy(&out.stderr).into_owned(), 90 | )); 91 | } 92 | let stdout: Vec = out.stdout; 93 | Ok(stdout) 94 | } 95 | 96 | // Format an nvme block device 97 | pub fn format(dev: &Path) -> BlockResult<()> { 98 | let out = Command::new("nvme") 99 | .args(&["format", &dev.to_string_lossy()]) 100 | .output()?; 101 | if !out.status.success() { 102 | return Err(BlockUtilsError::new( 103 | String::from_utf8_lossy(&out.stderr).into_owned(), 104 | )); 105 | } 106 | Ok(()) 107 | } 108 | 109 | pub fn list_nvme_namespaces(dev: &Path) -> BlockResult> { 110 | let out = Command::new("nvme") 111 | .args(&["list-ns", &dev.to_string_lossy(), "-o", "json"]) 112 | .output()?; 113 | if !out.status.success() { 114 | return Err(BlockUtilsError::new( 115 | String::from_utf8_lossy(&out.stderr).into_owned(), 116 | )); 117 | } 118 | let stdout = String::from_utf8_lossy(&out.stdout); 119 | let deserialized: Vec = serde_json::from_str(&stdout)?; 120 | Ok(deserialized) 121 | } 122 | 123 | /// List the nvme controllers on the host 124 | pub fn list_nvme_controllers() -> BlockResult> { 125 | let out = Command::new("nvme-list").args(&["-o", "json"]).output()?; 126 | if !out.status.success() { 127 | return Err(BlockUtilsError::new( 128 | String::from_utf8_lossy(&out.stderr).into_owned(), 129 | )); 130 | } 131 | let stdout = String::from_utf8_lossy(&out.stdout); 132 | let deserialized: Vec = serde_json::from_str(&stdout)?; 133 | Ok(deserialized) 134 | } 135 | 136 | /// List the nvme devices on the host 137 | pub fn list_nvme_devices() -> BlockResult> { 138 | let out = Command::new("nvme") 139 | .args(&["list", "-o", "json"]) 140 | .output()?; 141 | if !out.status.success() { 142 | return Err(BlockUtilsError::new( 143 | String::from_utf8_lossy(&out.stderr).into_owned(), 144 | )); 145 | } 146 | let stdout = String::from_utf8_lossy(&out.stdout); 147 | let deserialized: NvmeDeviceContainer = serde_json::from_str(&stdout)?; 148 | Ok(deserialized.devices) 149 | } 150 | -------------------------------------------------------------------------------- /tests/proc_scsi: -------------------------------------------------------------------------------- 1 | Attached devices: 2 | Host: scsi6 Channel: 00 Id: 00 Lun: 00 3 | Vendor: HP Model: P440 Rev: 4.02 4 | Type: RAID ANSI SCSI revision: 05 5 | Host: scsi6 Channel: 00 Id: 01 Lun: 00 6 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 7 | Type: Direct-Access ANSI SCSI revision: 06 8 | Host: scsi6 Channel: 00 Id: 02 Lun: 00 9 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 10 | Type: Direct-Access ANSI SCSI revision: 06 11 | Host: scsi6 Channel: 00 Id: 03 Lun: 00 12 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 13 | Type: Direct-Access ANSI SCSI revision: 06 14 | Host: scsi6 Channel: 00 Id: 04 Lun: 00 15 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 16 | Type: Direct-Access ANSI SCSI revision: 06 17 | Host: scsi6 Channel: 00 Id: 05 Lun: 00 18 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 19 | Type: Direct-Access ANSI SCSI revision: 06 20 | Host: scsi6 Channel: 00 Id: 06 Lun: 00 21 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 22 | Type: Direct-Access ANSI SCSI revision: 06 23 | Host: scsi6 Channel: 00 Id: 07 Lun: 00 24 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 25 | Type: Direct-Access ANSI SCSI revision: 06 26 | Host: scsi6 Channel: 00 Id: 08 Lun: 00 27 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 28 | Type: Direct-Access ANSI SCSI revision: 06 29 | Host: scsi6 Channel: 00 Id: 09 Lun: 00 30 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 31 | Type: Direct-Access ANSI SCSI revision: 06 32 | Host: scsi6 Channel: 00 Id: 10 Lun: 00 33 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 34 | Type: Direct-Access ANSI SCSI revision: 06 35 | Host: scsi6 Channel: 00 Id: 11 Lun: 00 36 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 37 | Type: Direct-Access ANSI SCSI revision: 06 38 | Host: scsi6 Channel: 00 Id: 12 Lun: 00 39 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 40 | Type: Direct-Access ANSI SCSI revision: 06 41 | Host: scsi6 Channel: 00 Id: 13 Lun: 00 42 | Vendor: HPE Model: Apollo 4200 LFF Rev: 1.25 43 | Type: Enclosure ANSI SCSI revision: 06 44 | Host: scsi7 Channel: 00 Id: 00 Lun: 00 45 | Vendor: HP Model: P840ar Rev: 4.02 46 | Type: RAID ANSI SCSI revision: 05 47 | Host: scsi7 Channel: 00 Id: 01 Lun: 00 48 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 49 | Type: Direct-Access ANSI SCSI revision: 06 50 | Host: scsi7 Channel: 00 Id: 02 Lun: 00 51 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 52 | Type: Direct-Access ANSI SCSI revision: 06 53 | Host: scsi7 Channel: 00 Id: 03 Lun: 00 54 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 55 | Type: Direct-Access ANSI SCSI revision: 06 56 | Host: scsi7 Channel: 00 Id: 04 Lun: 00 57 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 58 | Type: Direct-Access ANSI SCSI revision: 06 59 | Host: scsi7 Channel: 00 Id: 05 Lun: 00 60 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 61 | Type: Direct-Access ANSI SCSI revision: 06 62 | Host: scsi7 Channel: 00 Id: 06 Lun: 00 63 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 64 | Type: Direct-Access ANSI SCSI revision: 06 65 | Host: scsi7 Channel: 00 Id: 07 Lun: 00 66 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 67 | Type: Direct-Access ANSI SCSI revision: 06 68 | Host: scsi7 Channel: 00 Id: 08 Lun: 00 69 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 70 | Type: Direct-Access ANSI SCSI revision: 06 71 | Host: scsi7 Channel: 00 Id: 09 Lun: 00 72 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 73 | Type: Direct-Access ANSI SCSI revision: 06 74 | Host: scsi7 Channel: 00 Id: 10 Lun: 00 75 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 76 | Type: Direct-Access ANSI SCSI revision: 06 77 | Host: scsi7 Channel: 00 Id: 11 Lun: 00 78 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 79 | Type: Direct-Access ANSI SCSI revision: 06 80 | Host: scsi7 Channel: 00 Id: 12 Lun: 00 81 | Vendor: HP Model: MB4000JFEPB Rev: HPD1 82 | Type: Direct-Access ANSI SCSI revision: 06 83 | Host: scsi7 Channel: 00 Id: 13 Lun: 00 84 | Vendor: HPE Model: Apollo 4200 LFF Rev: 1.25 85 | Type: Enclosure ANSI SCSI revision: 06 86 | Host: scsi4 Channel: 00 Id: 00 Lun: 00 87 | Vendor: ATA Model: VK0120GFDKE Rev: HPG0 88 | Type: Direct-Access ANSI SCSI revision: 05 89 | --------------------------------------------------------------------------------