1pub mod nvme;
2
3use fstab::{FsEntry, FsTab};
4use log::{debug, warn};
5use uuid::Uuid;
6
7use std::collections::HashMap;
8use std::ffi::OsStr;
9use std::fmt;
10use std::fs::{self, read_dir, File};
11use std::io::{BufRead, BufReader, Write};
12use std::os::unix::fs::MetadataExt;
13use std::path::{Path, PathBuf};
14use std::process::{Child, Command, Output};
15use std::str::FromStr;
16use strum::{Display, EnumString, IntoStaticStr};
17use thiserror::Error;
18
19pub type BlockResult<T> = Result<T, BlockUtilsError>;
20
21#[cfg(test)]
22mod 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 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 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
70const MTAB_PATH: &str = "/etc/mtab";
71
72#[derive(Debug, Error)]
73pub 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
93impl BlockUtilsError {
94 fn new(err: String) -> BlockUtilsError {
96 BlockUtilsError::Error(err)
97 }
98}
99
100#[derive(Clone, Debug, Display)]
103#[strum(serialize_all = "snake_case")]
104pub enum MetadataProfile {
105 Raid0,
106 Raid1,
107 Raid5,
108 Raid6,
109 Raid10,
110 Single,
111 Dup,
112}
113
114#[derive(Clone, Debug, EnumString)]
116pub 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, #[strum(serialize = "NECVMWar")]
130 NECVMWar, #[strum(serialize = "VMware")]
132 VMware, }
134
135#[derive(Clone, Debug)]
138pub struct Device {
139 pub id: Option<Uuid>,
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<String>,
146 pub logical_block_size: Option<u64>,
147 pub physical_block_size: Option<u64>,
148}
149
150impl Device {
151 #[cfg(target_os = "linux")]
152 fn from_udev_device(device: udev::Device) -> BlockResult<Self> {
153 let sys_name = device.sysname();
154 let id: Option<Uuid> = 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<Self> {
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)]
199pub struct AsyncInit {
200 pub format_child: Child,
203 pub post_setup_commands: Vec<(String, Vec<String>)>,
206 pub device: PathBuf,
208}
209
210#[derive(Debug, Display, EnumString)]
211#[strum(serialize_all = "snake_case")]
212pub enum Scheduler {
213 Cfq,
215 Deadline,
217 Noop,
219}
220
221#[derive(Clone, Debug, Eq, PartialEq)]
223pub enum MediaType {
224 SolidState,
226 Rotational,
228 Loopback,
230 LVM,
232 MdRaid,
234 NVME,
236 Ram,
238 Virtual,
239 Unknown,
240}
241
242#[derive(Clone, Debug, Eq, PartialEq, Display, IntoStaticStr)]
244#[strum(serialize_all = "snake_case")]
245pub enum DeviceType {
246 Disk,
247 Partition,
248 Unknown,
249}
250
251impl FromStr for DeviceType {
252 type Err = BlockUtilsError;
253
254 fn from_str(s: &str) -> Result<Self, Self::Err> {
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#[derive(Clone, Debug, Eq, PartialEq, EnumString)]
266#[strum(serialize_all = "snake_case")]
267pub enum FilesystemType {
268 Btrfs,
269 Ext2,
270 Ext3,
271 Ext4,
272 #[strum(serialize = "lvm2_member")]
273 Lvm,
274 Xfs,
275 Zfs,
276 Ntfs,
277 Vfat,
279 #[strum(default)]
281 Unrecognised(String),
282 #[strum(serialize = "")]
284 Unknown,
285}
286
287impl 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
305impl 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#[derive(Debug)]
326pub 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<u64>,
336 stripe_width: Option<u64>,
337 },
338 Xfs {
339 block_size: Option<u64>, force: bool,
344 inode_size: Option<u64>,
345 stripe_size: Option<u64>, stripe_width: Option<u64>, agcount: Option<u64>, },
349 Zfs {
350 block_size: Option<u64>,
353 compression: Option<bool>,
355 },
356}
357
358impl Filesystem {
359 pub fn new(name: &str) -> Filesystem {
360 match name.trim() {
361 "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
397fn run_command<S: AsRef<OsStr>>(command: &str, arg_list: &[S]) -> BlockResult<Output> {
398 Ok(Command::new(command).args(arg_list).output()?)
399}
400
401pub fn mount_device(device: &Device, mount_point: impl AsRef<Path>) -> BlockResult<i32> {
405 let mut arg_list: Vec<String> = 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
421pub fn unmount_device(mount_point: impl AsRef<Path>) -> BlockResult<i32> {
423 let mut arg_list: Vec<String> = 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
429pub fn get_mount_device(mount_dir: impl AsRef<Path>) -> BlockResult<Option<PathBuf>> {
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
447pub fn get_mounted_devices_iter() -> BlockResult<impl Iterator<Item = BlockResult<Device>>> {
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}
458pub fn get_mounted_devices() -> BlockResult<Vec<Device>> {
462 get_mounted_devices_iter()?.collect()
463}
464
465pub fn get_mountpoint(device: impl AsRef<Path>) -> BlockResult<Option<PathBuf>> {
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
488fn process_output(output: &Output) -> BlockResult<i32> {
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
497pub fn erase_block_device(device: impl AsRef<Path>) -> 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
512pub fn format_block_device(device: impl AsRef<Path>, filesystem: &Filesystem) -> BlockResult<i32> {
516 match *filesystem {
518 Filesystem::Btrfs {
519 ref metadata_profile,
520 ref leaf_size,
521 ref node_size,
522 } => {
523 let mut arg_list: Vec<String> = 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 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<String> = Vec::new();
552
553 if let Some(b) = block_size {
554 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 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<String> = 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 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 let arg_list: Vec<String> = 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 let _ = process_output(&run_command("/sbin/zpool", &arg_list)?)?;
648 if block_size.is_some() {
649 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
696pub fn async_format_block_device(
697 device: impl AsRef<Path>,
698 filesystem: &Filesystem,
699) -> BlockResult<AsyncInit> {
700 match *filesystem {
701 Filesystem::Btrfs {
702 ref metadata_profile,
703 ref leaf_size,
704 ref node_size,
705 } => {
706 let arg_list: Vec<String> = 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 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<String> = 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 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 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 let mut post_setup_commands: Vec<(String, Vec<String>)> = Vec::new();
784 let arg_list: Vec<String> = 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 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<String> =
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]
876fn 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")]
882fn get_udev_int_val(device: &udev::Device, attr_name: &str) -> Option<u64> {
883 match device.attribute_value(attr_name) {
884 Some(val_str) => {
885 let val = val_str.to_str().unwrap_or("0").parse::<u64>().unwrap_or(0);
886 Some(val)
887 }
888 None => None,
889 }
890}
891
892#[cfg(target_os = "linux")]
893fn get_size(device: &udev::Device) -> Option<u64> {
894 get_udev_int_val(device, "size").map(|s| s * 512)
896}
897
898#[cfg(target_os = "linux")]
899fn get_uuid(device: &udev::Device) -> Option<Uuid> {
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")]
907fn get_serial(device: &udev::Device) -> Option<String> {
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")]
915fn get_fs_type(device: &udev::Device) -> BlockResult<FilesystemType> {
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")]
926fn get_media_type(device: &udev::Device) -> MediaType {
927 use regex::Regex;
928 let device_sysname = device.sysname().to_string_lossy();
929
930 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 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 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 if device_sysname.contains("nvme") {
953 return MediaType::NVME;
954 }
955
956 if device.property_value("DM_NAME").is_some() {
958 return MediaType::LVM;
959 }
960
961 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 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 MediaType::Unknown
982}
983
984#[cfg(target_os = "linux")]
985fn get_device_type(device: &udev::Device) -> BlockResult<DeviceType> {
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
995pub fn is_mounted(directory: impl AsRef<Path>) -> BlockResult<bool> {
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 return Ok(false);
1005 }
1006
1007 Ok(if let Some(parent) = parent {
1008 let parent_metadata = fs::metadata(parent)?;
1009 parent_metadata.dev() != dir_metadata.dev()
1011 } else {
1012 false
1014 })
1015}
1016
1017#[cfg(target_os = "linux")]
1024fn get_specific_block_device_iter(
1025 requested_dev_type: DeviceType,
1026) -> BlockResult<impl Iterator<Item = PathBuf>> {
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#[cfg(target_os = "linux")]
1056pub fn get_block_partitions_iter() -> BlockResult<impl Iterator<Item = PathBuf>> {
1057 get_specific_block_device_iter(DeviceType::Partition)
1058}
1059
1060#[cfg(target_os = "linux")]
1067pub fn get_block_partitions() -> BlockResult<Vec<PathBuf>> {
1068 get_block_partitions_iter().map(|i| i.collect())
1069}
1070
1071#[cfg(target_os = "linux")]
1078pub fn get_block_devices_iter() -> BlockResult<impl Iterator<Item = PathBuf>> {
1079 get_specific_block_device_iter(DeviceType::Disk)
1080}
1081
1082#[cfg(target_os = "linux")]
1089pub fn get_block_devices() -> BlockResult<Vec<PathBuf>> {
1090 get_block_devices_iter().map(|i| i.collect())
1091}
1092
1093#[cfg(target_os = "linux")]
1095pub fn is_block_device(device_path: impl AsRef<Path>) -> BlockResult<bool> {
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
1118fn dev_path_to_sys_path(dev_path: impl AsRef<Path>) -> BlockResult<PathBuf> {
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#[cfg(target_os = "linux")]
1145pub fn get_block_dev_property(
1146 device_path: impl AsRef<Path>,
1147 tag: &str,
1148) -> BlockResult<Option<String>> {
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#[cfg(target_os = "linux")]
1158pub fn get_block_dev_properties(
1159 device_path: impl AsRef<Path>,
1160) -> BlockResult<HashMap<String, String>> {
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()) }
1174
1175#[derive(Clone, Debug, Default)]
1177pub struct Enclosure {
1178 pub active: Option<String>,
1179 pub fault: Option<String>,
1180 pub power_status: Option<String>,
1181 pub slot: u8,
1182 pub status: Option<String>,
1183 pub enclosure_type: Option<String>,
1184}
1185
1186#[derive(Clone, Debug, PartialEq, Display, EnumString)]
1187#[strum(serialize_all = "snake_case")]
1188pub enum DeviceState {
1189 Blocked,
1190 #[strum(serialize = "failfast")]
1191 FailFast,
1192 Lost,
1193 Running,
1194 RunningRta,
1195}
1196
1197#[derive(Clone, Debug)]
1198pub struct ScsiInfo {
1199 pub block_device: Option<PathBuf>,
1200 pub enclosure: Option<Enclosure>,
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<String>,
1207 pub model: Option<String>,
1208 pub rev: Option<String>,
1209 pub state: Option<DeviceState>,
1210 pub scsi_type: ScsiDeviceType,
1211 pub scsi_revision: u32,
1212}
1213
1214#[derive(Clone, Copy, Debug, PartialEq, EnumString)]
1216pub 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
1283impl 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
1303impl 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
1312fn scsi_host_info(input: &str) -> Result<Vec<ScsiInfo>, BlockUtilsError> {
1313 let mut scsi_devices = Vec::new();
1314 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::<Vec<&str>>();
1324 if parts.len() < 8 {
1325 continue;
1327 }
1328 scsi_info.host = parts[1].to_string();
1330 scsi_info.channel = parts[3].parse::<u8>()?;
1331 scsi_info.id = parts[5].parse::<u8>()?;
1332 scsi_info.lun = parts[7].parse::<u8>()?;
1333 }
1334 if line.contains("Vendor") {
1335 let parts = line.split_whitespace().collect::<Vec<&str>>();
1336 scsi_info.vendor = parts[1].parse::<Vendor>()?;
1337 let model = parts[3..]
1339 .iter()
1340 .take_while(|s| !s.contains(":"))
1341 .map(|s| *s)
1342 .collect::<Vec<&str>>();
1343 if !model.is_empty() {
1344 scsi_info.model = Some(model.join(" ").to_string());
1345 }
1346 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::<Vec<&str>>();
1354 scsi_info.scsi_type = parts[1].parse::<ScsiDeviceType>()?;
1355 scsi_info.scsi_revision = parts[5].parse::<u32>()?;
1356 }
1357 }
1358
1359 Ok(scsi_devices)
1360}
1361
1362#[test]
1363fn 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]
1369fn 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
1395pub fn sort_scsi_info_iter<'a>(
1400 info: &'a [ScsiInfo],
1401) -> impl Iterator<Item = (ScsiInfo, Option<ScsiInfo>)> + 'a {
1402 info.iter().map(move |dev| {
1403 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 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
1422pub fn sort_scsi_info(info: &[ScsiInfo]) -> Vec<(ScsiInfo, Option<ScsiInfo>)> {
1427 sort_scsi_info_iter(info).collect()
1428}
1429
1430fn get_enclosure_data(p: impl AsRef<Path>) -> BlockResult<Enclosure> {
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
1452pub fn get_scsi_info() -> BlockResult<Vec<ScsiInfo>> {
1454 let scsi_path = Path::new("/sys/bus/scsi/devices");
1456 if scsi_path.exists() {
1457 let mut scsi_devices: Vec<ScsiInfo> = 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 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 let buff = fs::read_to_string("/proc/scsi/scsi")?;
1530
1531 Ok(scsi_host_info(&buff)?)
1532 }
1533}
1534
1535#[cfg(target_os = "linux")]
1537pub fn is_disk(dev_path: impl AsRef<Path>) -> BlockResult<bool> {
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")]
1552fn get_parent_name(device: &udev::Device) -> Option<PathBuf> {
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#[cfg(target_os = "linux")]
1571pub fn get_parent_devpath_from_path(dev_path: impl AsRef<Path>) -> BlockResult<Option<PathBuf>> {
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#[cfg(target_os = "linux")]
1600pub fn get_children_devpaths_from_path(dev_path: impl AsRef<Path>) -> BlockResult<Vec<PathBuf>> {
1601 get_children_devpaths_from_path_iter(dev_path).map(|iter| iter.collect())
1602}
1603
1604#[cfg(target_os = "linux")]
1607pub fn get_children_devpaths_from_path_iter(
1608 dev_path: impl AsRef<Path>,
1609) -> BlockResult<impl Iterator<Item = PathBuf>> {
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#[cfg(target_os = "linux")]
1621pub fn get_device_from_path(
1622 dev_path: impl AsRef<Path>,
1623) -> BlockResult<(Option<u64>, Option<Device>)> {
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::<u64>().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::<u64>().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#[cfg(target_os = "linux")]
1662pub fn get_all_device_info_iter<P, T>(
1663 devices: T,
1664) -> BlockResult<impl Iterator<Item = BlockResult<Device>>>
1665where
1666 P: AsRef<Path>,
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::<Vec<_>>();
1674
1675 Ok(udev::Enumerator::new()?.scan_devices()?.filter_map(
1676 move |device| -> Option<BlockResult<Device>> {
1677 if device_names.contains(&device.sysname().to_owned())
1678 && device.subsystem() == Some(OsStr::new("block"))
1679 {
1680 Some(Device::from_udev_device(device))
1682 } else {
1683 None
1684 }
1685 },
1686 ))
1687}
1688
1689#[cfg(target_os = "linux")]
1695pub fn get_all_device_info<P, T>(devices: T) -> BlockResult<Vec<Device>>
1696where
1697 P: AsRef<Path>,
1698 T: AsRef<[P]>,
1699{
1700 get_all_device_info_iter(devices).map(|i| i.collect::<BlockResult<Vec<Device>>>())?
1701}
1702
1703#[cfg(target_os = "linux")]
1705pub fn get_device_info(device_path: impl AsRef<Path>) -> BlockResult<Device> {
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
1724pub fn set_elevator(device_path: impl AsRef<Path>, elevator: &Scheduler) -> BlockResult<usize> {
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
1750pub fn weekly_defrag(
1751 mount: impl AsRef<Path>,
1752 fs_type: &FilesystemType,
1753 interval: &str,
1754) -> BlockResult<usize> {
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 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::<Vec<String>>()
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 let Some(pos) = existing_job_position {
1787 existing_crontab.remove(pos);
1788 }
1789 existing_crontab.push(job);
1790
1791 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}