From 10effc9849d7151c4297ff197a53911ea904ab0e Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 5 May 2020 10:14:41 +0200 Subject: [PATCH] add tools/disks.rs (work in progress...) Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 3 + src/tools.rs | 1 + src/tools/disks.rs | 441 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 445 insertions(+) create mode 100644 src/tools/disks.rs diff --git a/Cargo.toml b/Cargo.toml index a155c8728..84c92b86a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/lib.rs" [dependencies] base64 = "0.10" +bitflags = "1.2.1" bytes = "0.5" chrono = "0.4" # Date and time library for Rust crc32fast = "1" @@ -30,6 +31,7 @@ libc = "0.2" log = "0.4" native-tls = "0.2" nix = "0.16" +once_cell = "1.3.1" openssl = "0.10" pam = "0.7" pam-sys = "0.5" @@ -48,6 +50,7 @@ tokio = { version = "0.2.9", features = [ "blocking", "fs", "io-util", "macros", tokio-openssl = "0.4.0" tokio-util = { version = "0.2.0", features = [ "codec" ] } tower-service = "0.3.0" +udev = "0.3" url = "2.1" #valgrind_request = { git = "https://github.com/edef1c/libvalgrind_request", version = "1.1.0", optional = true } walkdir = "2" diff --git a/src/tools.rs b/src/tools.rs index 873bc336d..17f867bf4 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -24,6 +24,7 @@ pub mod acl; pub mod async_io; pub mod borrow; pub mod daemon; +pub mod disks; pub mod fs; pub mod format; pub mod lru_cache; diff --git a/src/tools/disks.rs b/src/tools/disks.rs new file mode 100644 index 000000000..7000f69c8 --- /dev/null +++ b/src/tools/disks.rs @@ -0,0 +1,441 @@ +//! Disk query/management utilities for. + +use std::collections::HashSet; +use std::ffi::{OsStr, OsString}; +use std::io; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use bitflags::bitflags; +use anyhow::{format_err, Error}; +use libc::dev_t; +use once_cell::sync::OnceCell; + +use proxmox::sys::error::io_err_other; +use proxmox::sys::linux::procfs::MountInfo; +use proxmox::{io_bail, io_format_err}; + +bitflags! { + /// Ways a device is being used. + pub struct DiskUse: u32 { + /// Currently mounted. + const MOUNTED = 0x0000_0001; + + /// Currently used as member of a device-mapper device. + const DEVICE_MAPPER = 0x0000_0002; + + /// Contains partitions. + const PARTITIONS = 0x0001_0000; + + /// The disk has a partition type which belongs to an LVM PV. + const LVM = 0x0002_0000; + + /// The disk has a partition type which belongs to a zpool. + const ZFS = 0x0004_0000; + + /// The disk is used by ceph. + const CEPH = 0x0008_0000; + } +} + +/// Disk management context. +/// +/// This provides access to disk information with some caching for faster querying of multiple +/// devices. +pub struct DiskManage { + mount_info: OnceCell, + mounted_devices: OnceCell>, +} + +impl DiskManage { + /// Create a new disk management context. + pub fn new() -> Arc { + Arc::new(Self { + mount_info: OnceCell::new(), + mounted_devices: OnceCell::new(), + }) + } + + /// Get the current mount info. This simply caches the result of `MountInfo::read` from the + /// `proxmox::sys` module. + pub fn mount_info(&self) -> Result<&MountInfo, Error> { + self.mount_info.get_or_try_init(MountInfo::read) + } + + /// Get a `Disk` from a device node (eg. `/dev/sda`). + pub fn disk_by_node>(self: Arc, devnode: P) -> io::Result { + use std::os::unix::fs::MetadataExt; + + let devnode = devnode.as_ref(); + + let meta = std::fs::metadata(devnode)?; + if (meta.mode() & libc::S_IFBLK) == libc::S_IFBLK { + self.disk_by_dev_num(meta.rdev()) + } else { + io_bail!("not a block device: {:?}", devnode); + } + } + + /// Get a `Disk` for a specific device number. + pub fn disk_by_dev_num(self: Arc, devnum: dev_t) -> io::Result { + self.disk_by_sys_path(format!( + "/sys/dev/block/{}:{}", + unsafe { libc::major(devnum) }, + unsafe { libc::minor(devnum) }, + )) + } + + /// Get a `Disk` for a path in `/sys`. + pub fn disk_by_sys_path>(self: Arc, path: P) -> io::Result { + let device = udev::Device::from_syspath(path.as_ref())?; + Ok(Disk { + manager: self, + device, + info: Default::default(), + }) + } + + /// Gather information about mounted disks: + fn mounted_devices(&self) -> Result<&HashSet, Error> { + use std::os::unix::fs::MetadataExt; + + self.mounted_devices + .get_or_try_init(|| -> Result<_, Error> { + let mut mounted = HashSet::new(); + + for (_id, mp) in self.mount_info()? { + let source = match mp.mount_source.as_ref().map(OsString::as_os_str) { + Some(s) => s, + None => continue, + }; + + let path = Path::new(source); + if !path.is_absolute() { + continue; + } + + let meta = match std::fs::metadata(path) { + Ok(meta) => meta, + Err(ref err) if err.kind() == io::ErrorKind::NotFound => continue, + Err(other) => return Err(Error::from(other)), + }; + + if (meta.mode() & libc::S_IFBLK) != libc::S_IFBLK { + // not a block device + continue; + } + + mounted.insert(meta.rdev()); + } + + Ok(mounted) + }) + } + + /// Check whether a specific device node is mounted. + /// + /// Note that this tries to `stat` the sources of all mount points without caching the result + /// of doing so, so this is always somewhat expensive. + pub fn is_devnum_mounted(&self, dev: dev_t) -> Result { + self.mounted_devices().map(|mounted| mounted.contains(&dev)) + } +} + +/// Queries (and caches) various information about a specific disk. +/// +/// This belongs to a `Disks` and provides information for a single disk. +pub struct Disk { + manager: Arc, + device: udev::Device, + info: DiskInfo, +} + +/// Helper struct (so we can initialize this with Default) +/// +/// We probably want this to be serializable to the same hash type we use in perl currently. +#[derive(Default)] +struct DiskInfo { + size: OnceCell, + vendor: OnceCell>, + model: OnceCell>, + rotational: OnceCell>, + // for perl: #[serde(rename = "devpath")] + ata_rotation_rate_rpm: OnceCell>, + // for perl: #[serde(rename = "devpath")] + device_path: OnceCell>, + wwn: OnceCell>, + serial: OnceCell>, + // for perl: #[serde(skip_serializing)] + partition_table_type: OnceCell>, + gpt: OnceCell, + // ??? + bus: OnceCell>, + // ??? + fs_type: OnceCell>, + // ??? + has_holders: OnceCell, + // ??? + is_mounted: OnceCell, +} + +impl Disk { + /// Try to get the device number for this disk. + /// + /// (In udev this can fail...) + pub fn devnum(&self) -> Result { + // not sure when this can fail... + self.device + .devnum() + .ok_or_else(|| format_err!("failed to get device number")) + } + + /// Get the sys-name of this device. (The final component in the `/sys` path). + pub fn sysname(&self) -> &OsStr { + self.device.sysname() + } + + /// Get the this disk's `/sys` path. + pub fn syspath(&self) -> &Path { + self.device.syspath() + } + + /// Get the device node in `/dev`, if any. + pub fn device_path(&self) -> Option<&Path> { + //self.device.devnode() + self.info + .device_path + .get_or_init(|| self.device.devnode().map(Path::to_owned)) + .as_ref() + .map(PathBuf::as_path) + } + + /// Get the parent device. + pub fn parent(&self) -> Option { + self.device.parent().map(|parent| Self { + manager: self.manager.clone(), + device: parent, + info: Default::default(), + }) + } + + /// Read from a file in this device's sys path. + /// + /// Note: path must be a relative path! + fn read_sys(&self, path: &Path) -> io::Result>> { + assert!(path.is_relative()); + + std::fs::read(self.syspath().join(path)) + .map(Some) + .or_else(|err| { + if err.kind() == io::ErrorKind::NotFound { + Ok(None) + } else { + Err(err) + } + }) + } + + /// Convenience wrapper for reading a `/sys` file which contains just a simple `OsString`. + fn read_sys_os_str>(&self, path: P) -> io::Result> { + Ok(self.read_sys(path.as_ref())?.map(OsString::from_vec)) + } + + /// Convenience wrapper for reading a `/sys` file which contains just a simple utf-8 string. + fn read_sys_str>(&self, path: P) -> io::Result> { + Ok(match self.read_sys(path.as_ref())? { + Some(data) => Some(String::from_utf8(data).map_err(io_err_other)?), + None => None, + }) + } + + /// Convenience wrapper for unsigned integer `/sys` values up to 64 bit. + fn read_sys_u64>(&self, path: P) -> io::Result> { + Ok(match self.read_sys_str(path)? { + Some(data) => Some(data.trim().parse().map_err(io_err_other)?), + None => None, + }) + } + + /// Get the disk's size in bytes. + pub fn size(&self) -> io::Result { + Ok(*self.info.size.get_or_try_init(|| { + self.read_sys_u64("size")?.ok_or_else(|| { + io_format_err!( + "failed to get disk size from {:?}", + self.syspath().join("size"), + ) + }) + })?) + } + + /// Get the device vendor (`/sys/.../device/vendor`) entry if available. + pub fn vendor(&self) -> io::Result> { + Ok(self + .info + .vendor + .get_or_try_init(|| self.read_sys_os_str("device/vendor"))? + .as_ref() + .map(OsString::as_os_str)) + } + + /// Get the device model (`/sys/.../device/model`) entry if available. + pub fn model(&self) -> Option<&OsStr> { + self.info + .model + .get_or_init(|| self.device.property_value("ID_MODEL").map(OsStr::to_owned)) + .as_ref() + .map(OsString::as_os_str) + } + + /// Check whether this is a rotational disk. + /// + /// Returns `None` if there's no `queue/rotational` file, in which case no information is + /// known. `Some(false)` if `queue/rotational` is zero, `Some(true)` if it has a non-zero + /// value. + pub fn rotational(&self) -> io::Result> { + Ok(*self + .info + .rotational + .get_or_try_init(|| -> io::Result> { + Ok(self.read_sys_u64("queue/rotational")?.map(|n| n != 0)) + })?) + } + + /// Get the WWN if available. + pub fn wwn(&self) -> Option<&OsStr> { + self.info + .wwn + .get_or_init(|| self.device.property_value("ID_WWN").map(|v| v.to_owned())) + .as_ref() + .map(OsString::as_os_str) + } + + /// Get the device serial if available. + pub fn serial(&self) -> Option<&OsStr> { + self.info + .serial + .get_or_init(|| { + self.device + .property_value("ID_SERIAL_SHORT") + .map(|v| v.to_owned()) + }) + .as_ref() + .map(OsString::as_os_str) + } + + /// Get the ATA rotation rate value from udev. This is not necessarily the same as sysfs' + /// `rotational` value. + pub fn ata_rotation_rate_rpm(&self) -> Option { + *self.info.ata_rotation_rate_rpm.get_or_init(|| { + std::str::from_utf8( + self.device + .property_value("ID_ATA_ROTATION_RATE_RPM")? + .as_bytes(), + ) + .ok()? + .parse() + .ok() + }) + } + + /// Get the partition table type, if any. + pub fn partition_table_type(&self) -> Option<&OsStr> { + self.info + .partition_table_type + .get_or_init(|| { + self.device + .property_value("ID_PART_TABLE_TYPE") + .map(|v| v.to_owned()) + }) + .as_ref() + .map(OsString::as_os_str) + } + + /// Check if this contains a GPT partition table. + pub fn has_gpt(&self) -> bool { + *self.info.gpt.get_or_init(|| { + self.partition_table_type() + .map(|s| s == "gpt") + .unwrap_or(false) + }) + } + + /// Get the bus type used for this disk. + pub fn bus(&self) -> Option<&OsStr> { + self.info + .bus + .get_or_init(|| self.device.property_value("ID_BUS").map(|v| v.to_owned())) + .as_ref() + .map(OsString::as_os_str) + } + + /// Attempt to guess the disk type. + pub fn guess_disk_type(&self) -> io::Result { + Ok(match self.rotational()? { + Some(true) => DiskType::Hdd, + _ => match self.ata_rotation_rate_rpm() { + Some(_) => DiskType::Hdd, + None => match self.bus() { + Some(bus) if bus == "usb" => DiskType::Usb, + _ => DiskType::Unknown, + }, + }, + }) + } + + /// Get the file system type found on the disk, if any. + /// + /// Note that `None` may also just mean "unknown". + pub fn fs_type(&self) -> Option<&OsStr> { + self.info + .fs_type + .get_or_init(|| { + self.device + .property_value("ID_FS_TYPE") + .map(|v| v.to_owned()) + }) + .as_ref() + .map(OsString::as_os_str) + } + + /// Check if there are any "holders" in `/sys`. This usually means the device is in use by + /// another kernel driver like the device mapper. + pub fn has_holders(&self) -> io::Result { + Ok(*self + .info + .has_holders + .get_or_try_init(|| -> io::Result { + for entry in std::fs::read_dir(self.syspath())? { + match entry?.file_name().as_bytes() { + b"." | b".." => (), + _ => return Ok(true), + } + } + Ok(false) + })?) + } + + /// Check if this disk is mounted. + pub fn is_mounted(&self) -> Result { + Ok(*self + .info + .is_mounted + .get_or_try_init(|| self.manager.is_devnum_mounted(self.devnum()?))?) + } +} + +/// This is just a rough estimate for a "type" of disk. +pub enum DiskType { + /// We know nothing. + Unknown, + + /// May also be a USB-HDD. + Hdd, + + /// May also be a USB-SSD. + Ssd, + + /// Some kind of USB disk, but we don't know more than that. + Usb, +}