block_utils/
lib.rs

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        // 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
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    /// 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")]
104pub 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)]
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, // 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)]
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    /// 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<String>)>,
206    /// The device we're initializing
207    pub device: PathBuf,
208}
209
210#[derive(Debug, Display, EnumString)]
211#[strum(serialize_all = "snake_case")]
212pub 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)]
223pub 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")]
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/// What type of filesystem
265#[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    /// 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
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/// This allows you to tweak some settings when you're formatting the filesystem
325#[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        /// 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<u64>, // Note this MUST be a power of 2
343        force: bool,
344        inode_size: Option<u64>,
345        stripe_size: Option<u64>,  // RAID controllers stripe
346        stripe_width: Option<u64>, // IE # of data disks
347        agcount: Option<u64>,      // 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<u64>,
353        /// Enable compression on the volume. Default is fals
354        compression: Option<bool>,
355    },
356}
357
358impl 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
397fn run_command<S: AsRef<OsStr>>(command: &str, arg_list: &[S]) -> BlockResult<Output> {
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.
404pub 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
421//Utility function to unmount a device at a mount point
422pub 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
429/// Parse mtab and return the device which is mounted at a given directory
430pub 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
447/// Parse mtab and return iterator over all mounted block devices not including LVM
448///
449/// Lazy version of get_mounted_devices
450pub 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}
458/// Parse mtab and return all mounted block devices not including LVM
459///
460/// Non-lazy version of get_mounted_devices_iter
461pub fn get_mounted_devices() -> BlockResult<Vec<Device>> {
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
467pub 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
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
515pub fn format_block_device(device: impl AsRef<Path>, filesystem: &Filesystem) -> BlockResult<i32> {
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<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            // 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<String> = 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<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            // 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<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                    // 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
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            // 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<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            // 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<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                        // 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<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    // 512 is the block size
895    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    // 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")]
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
995/// Checks and returns if a particular directory is a mountpoint
996pub 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        // 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")]
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/// 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")]
1056pub fn get_block_partitions_iter() -> BlockResult<impl Iterator<Item = PathBuf>> {
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")]
1067pub fn get_block_partitions() -> BlockResult<Vec<PathBuf>> {
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")]
1078pub fn get_block_devices_iter() -> BlockResult<impl Iterator<Item = PathBuf>> {
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")]
1089pub fn get_block_devices() -> BlockResult<Vec<PathBuf>> {
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")]
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
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.
1121fn 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/// Get property value by key `tag` for device with devpath `device_path` (like "/dev/sda") if present
1144#[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/// Get properties for device with devpath `device_path` (like "/dev/sda") if present
1157#[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()) // 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)]
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// Taken from https://github.com/hreinecke/lsscsi/blob/master/src/lsscsi.c
1215#[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    // 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::<Vec<&str>>();
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::<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            // Take until : is found
1338            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            // 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::<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
1395/// Examine the ScsiInfo devices and associate a host ScsiInfo device if it
1396/// exists
1397///
1398/// Lazy version of `sort_scsi_info`
1399pub 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        // 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`
1426pub 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
1452/// Gathers all available scsi information
1453pub fn get_scsi_info() -> BlockResult<Vec<ScsiInfo>> {
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<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                // 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")]
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/// get the parent device path from a device path (If not a partition or disk, return None)
1570#[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/// Get the children devices paths from a device path
1599#[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/// Get the children devices paths from a device path
1605/// Note: It has square algorithmic complexity
1606#[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/// returns the device info and possibly partition entry for the device with the path or symlink given
1620#[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/// 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")]
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                // 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")]
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/// Returns device information that is gathered with udev.
1704#[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    //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::<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 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}