summaryrefslogtreecommitdiffstats
path: root/src/data_collection/disks.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/data_collection/disks.rs')
-rw-r--r--src/data_collection/disks.rs193
1 files changed, 193 insertions, 0 deletions
diff --git a/src/data_collection/disks.rs b/src/data_collection/disks.rs
new file mode 100644
index 00000000..c4a66eff
--- /dev/null
+++ b/src/data_collection/disks.rs
@@ -0,0 +1,193 @@
+//! Data collection about disks (e.g. I/O, usage, space).
+
+use cfg_if::cfg_if;
+use hashbrown::HashMap;
+
+use crate::app::filter::Filter;
+
+cfg_if! {
+ if #[cfg(target_os = "freebsd")] {
+ mod freebsd;
+ #[cfg(feature = "zfs")]
+ mod io_counters;
+ #[cfg(feature = "zfs")]
+ mod zfs_io_counters;
+ #[cfg(feature = "zfs")]
+ pub use io_counters::IoCounters;
+ pub(crate) use self::freebsd::*;
+ } else if #[cfg(target_os = "windows")] {
+ mod windows;
+ pub(crate) use self::windows::*;
+ } else if #[cfg(target_os = "linux")] {
+ mod unix;
+ #[cfg(feature = "zfs")]
+ mod zfs_io_counters;
+ pub(crate) use self::unix::*;
+ } else if #[cfg(target_os = "macos")] {
+ mod unix;
+ pub(crate) use self::unix::*;
+ } else {
+ mod other;
+ pub(crate) use self::other::*;
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct DiskHarvest {
+ pub name: String,
+ pub mount_point: String,
+
+ /// Windows also contains an additional volume name field.
+ #[cfg(target_os = "windows")]
+ pub volume_name: Option<String>,
+
+ // TODO: Maybe unify all these?
+ pub free_space: Option<u64>,
+ pub used_space: Option<u64>,
+ pub total_space: Option<u64>,
+}
+
+#[derive(Clone, Debug)]
+pub struct IoData {
+ pub read_bytes: u64,
+ pub write_bytes: u64,
+}
+
+pub type IoHarvest = HashMap<String, Option<IoData>>;
+
+cfg_if! {
+ if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] {
+ mod io_counters;
+ pub use io_counters::IoCounters;
+
+ /// Returns the I/O usage of certain mount points.
+ pub fn get_io_usage() -> anyhow::Result<IoHarvest> {
+ let mut io_hash: HashMap<String, Option<IoData>> = HashMap::new();
+
+ // TODO: Maybe rewrite this to not do a result of vec of result...
+ for io in io_stats()?.into_iter() {
+ let mount_point = io.device_name().to_string_lossy();
+
+ io_hash.insert(
+ mount_point.to_string(),
+ Some(IoData {
+ read_bytes: io.read_bytes(),
+ write_bytes: io.write_bytes(),
+ }),
+ );
+ }
+
+ Ok(io_hash)
+ }
+ } else if #[cfg(not(target_os = "freebsd"))] {
+ pub fn get_io_usage() -> anyhow::Result<IoHarvest> {
+ anyhow::bail!("Unsupported OS");
+ }
+ }
+}
+
+/// Whether to keep the current disk entry given the filters, disk name, and disk mount.
+/// Precedence ordering in the case where name and mount filters disagree, "allow"
+/// takes precedence over "deny".
+///
+/// For implementation, we do this as follows:
+///
+/// 1. Is the entry allowed through any filter? That is, does it match an entry in a
+/// filter where `is_list_ignored` is `false`? If so, we always keep this entry.
+/// 2. Is the entry denied through any filter? That is, does it match an entry in a
+/// filter where `is_list_ignored` is `true`? If so, we always deny this entry.
+/// 3. Anything else is allowed.
+pub fn keep_disk_entry(
+ disk_name: &str, mount_point: &str, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
+) -> bool {
+ match (disk_filter, mount_filter) {
+ (Some(d), Some(m)) => match (d.is_list_ignored, m.is_list_ignored) {
+ (true, true) => !(d.has_match(disk_name) || m.has_match(mount_point)),
+ (true, false) => {
+ if m.has_match(mount_point) {
+ true
+ } else {
+ d.keep_entry(disk_name)
+ }
+ }
+ (false, true) => {
+ if d.has_match(disk_name) {
+ true
+ } else {
+ m.keep_entry(mount_point)
+ }
+ }
+ (false, false) => d.has_match(disk_name) || m.has_match(mount_point),
+ },
+ (Some(d), None) => d.keep_entry(disk_name),
+ (None, Some(m)) => m.keep_entry(mount_point),
+ (None, None) => true,
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use regex::Regex;
+
+ use super::keep_disk_entry;
+ use crate::app::filter::Filter;
+
+ fn run_filter(disk_filter: &Option<Filter>, mount_filter: &Option<Filter>) -> Vec<usize> {
+ let targets = [
+ ("/dev/nvme0n1p1", "/boot"),
+ ("/dev/nvme0n1p2", "/"),
+ ("/dev/nvme0n1p3", "/home"),
+ ("/dev/sda1", "/mnt/test"),
+ ("/dev/sda2", "/mnt/boot"),
+ ];
+
+ targets
+ .into_iter()
+ .enumerate()
+ .filter_map(|(itx, (name, mount))| {
+ if keep_disk_entry(name, mount, disk_filter, mount_filter) {
+ Some(itx)
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
+
+ #[test]
+ fn test_keeping_disk_entry() {
+ let disk_ignore = Some(Filter {
+ is_list_ignored: true,
+ list: vec![Regex::new("nvme").unwrap()],
+ });
+
+ let disk_keep = Some(Filter {
+ is_list_ignored: false,
+ list: vec![Regex::new("nvme").unwrap()],
+ });
+
+ let mount_ignore = Some(Filter {
+ is_list_ignored: true,
+ list: vec![Regex::new("boot").unwrap()],
+ });
+
+ let mount_keep = Some(Filter {
+ is_list_ignored: false,
+ list: vec![Regex::new("boot").unwrap()],
+ });
+
+ assert_eq!(run_filter(&None, &None), vec![0, 1, 2, 3, 4]);
+
+ assert_eq!(run_filter(&disk_ignore, &None), vec![3, 4]);
+ assert_eq!(run_filter(&disk_keep, &None), vec![0, 1, 2]);
+
+ assert_eq!(run_filter(&None, &mount_ignore), vec![1, 2, 3]);
+ assert_eq!(run_filter(&None, &mount_keep), vec![0, 4]);
+
+ assert_eq!(run_filter(&disk_ignore, &mount_ignore), vec![3]);
+ assert_eq!(run_filter(&disk_keep, &mount_ignore), vec![0, 1, 2, 3]);
+
+ assert_eq!(run_filter(&disk_ignore, &mount_keep), vec![0, 3, 4]);
+ assert_eq!(run_filter(&disk_keep, &mount_keep), vec![0, 1, 2, 4]);
+ }
+}