public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup 14/26] tools: replace disks module with proxmox-disks
Date: Thu, 12 Mar 2026 14:52:15 +0100	[thread overview]
Message-ID: <20260312135229.420729-15-l.wagner@proxmox.com> (raw)
In-Reply-To: <20260312135229.420729-1-l.wagner@proxmox.com>

This commit replaces the disks module with the proxmox-disks crate. It
is extracted to enable disk metric collection in PDM.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml                             |    3 +
 src/api2/admin/datastore.rs            |   10 +-
 src/api2/config/datastore.rs           |    2 +-
 src/api2/node/disks/directory.rs       |   10 +-
 src/api2/node/disks/mod.rs             |   20 +-
 src/api2/node/disks/zfs.rs             |   14 +-
 src/bin/proxmox_backup_manager/disk.rs |    9 +-
 src/server/metric_collection/mod.rs    |    3 +-
 src/tools/disks/lvm.rs                 |   60 -
 src/tools/disks/mod.rs                 | 1394 ------------------------
 src/tools/disks/smart.rs               |  227 ----
 src/tools/disks/zfs.rs                 |  205 ----
 src/tools/mod.rs                       |    1 -
 13 files changed, 33 insertions(+), 1925 deletions(-)
 delete mode 100644 src/tools/disks/lvm.rs
 delete mode 100644 src/tools/disks/mod.rs
 delete mode 100644 src/tools/disks/smart.rs
 delete mode 100644 src/tools/disks/zfs.rs

diff --git a/Cargo.toml b/Cargo.toml
index 57f6aa88e..03a98de64 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -62,6 +62,7 @@ proxmox-borrow = "1"
 proxmox-compression = "1.0.1"
 proxmox-config-digest = "1"
 proxmox-daemon = "1"
+proxmox-disks = "0.1"
 proxmox-fuse = "2"
 proxmox-docgen = "1"
 proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "websocket" ] } # see below
@@ -224,6 +225,7 @@ proxmox-compression.workspace = true
 proxmox-config-digest.workspace = true
 proxmox-daemon.workspace = true
 proxmox-docgen.workspace = true
+proxmox-disks = { workspace =  true, features = ["api-types"] }
 proxmox-http = { workspace = true, features = [ "body", "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
 proxmox-human-byte.workspace = true
 proxmox-io.workspace = true
@@ -290,6 +292,7 @@ proxmox-rrd-api-types.workspace = true
 #proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
 #proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
 #proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
+#proxmox-disks = { path = "../proxmox/proxmox-disks" }
 #proxmox-http = { path = "../proxmox/proxmox-http" }
 #proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
 #proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index 88ad5d53b..d75e10e37 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -1874,7 +1874,7 @@ pub fn get_rrd_stats(
     _param: Value,
 ) -> Result<Value, Error> {
     let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
-    let disk_manager = crate::tools::disks::DiskManage::new();
+    let disk_manager = proxmox_disks::DiskManage::new();
 
     let mut rrd_fields = vec![
         "total",
@@ -2340,7 +2340,7 @@ fn setup_mounted_device(datastore: &DataStoreConfig, tmp_mount_path: &str) -> Re
         datastore.name, datastore.path, mount_point
     );
 
-    crate::tools::disks::bind_mount(Path::new(&full_store_path), Path::new(&mount_point))
+    proxmox_disks::bind_mount(Path::new(&full_store_path), Path::new(&mount_point))
 }
 
 /// Here we
@@ -2377,13 +2377,13 @@ pub fn do_mount_device(datastore: DataStoreConfig) -> Result<bool, Error> {
         )?;
 
         info!("temporarily mounting '{uuid}' to '{}'", tmp_mount_path);
-        crate::tools::disks::mount_by_uuid(uuid, Path::new(&tmp_mount_path))
+        proxmox_disks::mount_by_uuid(uuid, Path::new(&tmp_mount_path))
             .map_err(|e| format_err!("mounting to tmp path failed: {e}"))?;
 
         let setup_result = setup_mounted_device(&datastore, &tmp_mount_path);
 
         let mut unmounted = true;
-        if let Err(e) = crate::tools::disks::unmount_by_mountpoint(Path::new(&tmp_mount_path)) {
+        if let Err(e) = proxmox_disks::unmount_by_mountpoint(Path::new(&tmp_mount_path)) {
             unmounted = false;
             warn!("unmounting from tmp path '{tmp_mount_path} failed: {e}'");
         }
@@ -2614,7 +2614,7 @@ fn do_unmount_device(
 
     let mount_point = datastore.absolute_path();
     run_maintenance_locked(&datastore.name, MaintenanceType::Unmount, worker, || {
-        crate::tools::disks::unmount_by_mountpoint(Path::new(&mount_point))
+        proxmox_disks::unmount_by_mountpoint(Path::new(&mount_point))
     })
 }
 
diff --git a/src/api2/config/datastore.rs b/src/api2/config/datastore.rs
index f845fe2d0..034daf14b 100644
--- a/src/api2/config/datastore.rs
+++ b/src/api2/config/datastore.rs
@@ -8,6 +8,7 @@ use http_body_util::BodyExt;
 use serde_json::Value;
 use tracing::{info, warn};
 
+use proxmox_disks::unmount_by_mountpoint;
 use proxmox_router::{http_bail, Permission, Router, RpcEnvironment, RpcEnvironmentType};
 use proxmox_schema::{api, param_bail, ApiType};
 use proxmox_section_config::SectionConfigData;
@@ -37,7 +38,6 @@ use proxmox_rest_server::WorkerTask;
 use proxmox_s3_client::{S3ObjectKey, S3_HTTP_REQUEST_TIMEOUT};
 
 use crate::server::jobstate;
-use crate::tools::disks::unmount_by_mountpoint;
 
 #[derive(Default, serde::Deserialize, serde::Serialize)]
 #[serde(rename_all = "kebab-case")]
diff --git a/src/api2/node/disks/directory.rs b/src/api2/node/disks/directory.rs
index c37d65b8d..cd8c6b4e5 100644
--- a/src/api2/node/disks/directory.rs
+++ b/src/api2/node/disks/directory.rs
@@ -1,11 +1,15 @@
+use std::os::linux::fs::MetadataExt;
 use std::sync::LazyLock;
 
 use ::serde::{Deserialize, Serialize};
 use anyhow::{bail, Error};
 use serde_json::json;
-use std::os::linux::fs::MetadataExt;
 use tracing::info;
 
+use proxmox_disks::{
+    create_file_system, create_single_linux_partition, get_fs_uuid, DiskManage, DiskUsageQuery,
+    DiskUsageType, FileSystemType,
+};
 use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType};
 use proxmox_schema::api;
 use proxmox_section_config::SectionConfigData;
@@ -15,10 +19,6 @@ use pbs_api_types::{
     PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, UPID_SCHEMA,
 };
 
-use crate::tools::disks::{
-    create_file_system, create_single_linux_partition, get_fs_uuid, DiskManage, DiskUsageQuery,
-    DiskUsageType, FileSystemType,
-};
 use crate::tools::systemd::{self, types::*};
 
 use proxmox_rest_server::WorkerTask;
diff --git a/src/api2/node/disks/mod.rs b/src/api2/node/disks/mod.rs
index abcb8ee40..1c21bb91f 100644
--- a/src/api2/node/disks/mod.rs
+++ b/src/api2/node/disks/mod.rs
@@ -1,23 +1,21 @@
 use anyhow::{bail, Error};
 use serde_json::{json, Value};
-
-use proxmox_router::{
-    list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
-};
-use proxmox_schema::api;
-use proxmox_sortable_macro::sortable;
 use tracing::info;
 
 use pbs_api_types::{
     BLOCKDEVICE_DISK_AND_PARTITION_NAME_SCHEMA, BLOCKDEVICE_NAME_SCHEMA, NODE_SCHEMA,
     PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, UPID_SCHEMA,
 };
-
-use crate::tools::disks::{
-    get_smart_data, inititialize_gpt_disk, wipe_blockdev, DiskManage, DiskUsageInfo,
-    DiskUsageQuery, DiskUsageType, SmartData,
+use proxmox_disks::{
+    get_smart_data, initialize_gpt_disk, wipe_blockdev, DiskManage, DiskUsageInfo, DiskUsageQuery,
+    DiskUsageType, SmartData,
 };
 use proxmox_rest_server::WorkerTask;
+use proxmox_router::{
+    list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
+};
+use proxmox_schema::api;
+use proxmox_sortable_macro::sortable;
 
 pub mod directory;
 pub mod zfs;
@@ -174,7 +172,7 @@ pub fn initialize_disk(
             let disk_manager = DiskManage::new();
             let disk_info = disk_manager.disk_by_name(&disk)?;
 
-            inititialize_gpt_disk(&disk_info, uuid.as_deref())?;
+            initialize_gpt_disk(&disk_info, uuid.as_deref())?;
 
             Ok(())
         },
diff --git a/src/api2/node/disks/zfs.rs b/src/api2/node/disks/zfs.rs
index 3e5a7decf..21f4a3073 100644
--- a/src/api2/node/disks/zfs.rs
+++ b/src/api2/node/disks/zfs.rs
@@ -2,20 +2,18 @@ use anyhow::{bail, Error};
 use serde_json::{json, Value};
 use tracing::{error, info};
 
-use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType};
-use proxmox_schema::api;
-
 use pbs_api_types::{
     DataStoreConfig, ZfsCompressionType, ZfsRaidLevel, ZpoolListItem, DATASTORE_SCHEMA,
     DISK_ARRAY_SCHEMA, DISK_LIST_SCHEMA, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, UPID_SCHEMA,
     ZFS_ASHIFT_SCHEMA, ZPOOL_NAME_SCHEMA,
 };
-
-use crate::tools::disks::{
-    parse_zpool_status_config_tree, vdev_list_to_tree, zpool_list, zpool_status, DiskUsageType,
+use proxmox_disks::{
+    parse_zpool_status_config_tree, vdev_list_to_tree, zpool_list, zpool_status, DiskUsageQuery,
+    DiskUsageType,
 };
-
 use proxmox_rest_server::WorkerTask;
+use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType};
+use proxmox_schema::api;
 
 #[api(
     protected: true,
@@ -174,7 +172,7 @@ pub fn create_zpool(
         .map(|v| v.as_str().unwrap().to_string())
         .collect();
 
-    let disk_map = crate::tools::disks::DiskUsageQuery::new().query()?;
+    let disk_map = DiskUsageQuery::new().query()?;
     for disk in devices.iter() {
         match disk_map.get(disk) {
             Some(info) => {
diff --git a/src/bin/proxmox_backup_manager/disk.rs b/src/bin/proxmox_backup_manager/disk.rs
index cd7a0b7aa..f10c6e696 100644
--- a/src/bin/proxmox_backup_manager/disk.rs
+++ b/src/bin/proxmox_backup_manager/disk.rs
@@ -1,17 +1,14 @@
 use anyhow::{bail, Error};
 use serde_json::Value;
-
-use proxmox_router::{cli::*, ApiHandler, RpcEnvironment};
-use proxmox_schema::api;
 use std::io::IsTerminal;
 
 use pbs_api_types::{
     ZfsCompressionType, ZfsRaidLevel, BLOCKDEVICE_DISK_AND_PARTITION_NAME_SCHEMA,
     BLOCKDEVICE_NAME_SCHEMA, DATASTORE_SCHEMA, DISK_LIST_SCHEMA, ZFS_ASHIFT_SCHEMA,
 };
-use proxmox_backup::tools::disks::{
-    complete_disk_name, complete_partition_name, FileSystemType, SmartAttribute,
-};
+use proxmox_disks::{complete_disk_name, complete_partition_name, FileSystemType, SmartAttribute};
+use proxmox_router::{cli::*, ApiHandler, RpcEnvironment};
+use proxmox_schema::api;
 
 use proxmox_backup::api2;
 
diff --git a/src/server/metric_collection/mod.rs b/src/server/metric_collection/mod.rs
index 9b62cbb42..3fa6e9fbf 100644
--- a/src/server/metric_collection/mod.rs
+++ b/src/server/metric_collection/mod.rs
@@ -10,14 +10,13 @@ use anyhow::Error;
 use tokio::join;
 
 use pbs_api_types::{DataStoreConfig, Operation};
+use proxmox_disks::{zfs_dataset_stats, BlockDevStat, DiskManage};
 use proxmox_network_api::{get_network_interfaces, IpLink};
 use proxmox_sys::{
     fs::FileSystemInformation,
     linux::procfs::{Loadavg, ProcFsMemInfo, ProcFsNetDev, ProcFsStat},
 };
 
-use crate::tools::disks::{zfs_dataset_stats, BlockDevStat, DiskManage};
-
 mod metric_server;
 pub(crate) mod pull_metrics;
 pub(crate) mod rrd;
diff --git a/src/tools/disks/lvm.rs b/src/tools/disks/lvm.rs
deleted file mode 100644
index 1456a21c3..000000000
--- a/src/tools/disks/lvm.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use std::collections::HashSet;
-use std::os::unix::fs::MetadataExt;
-use std::sync::LazyLock;
-
-use anyhow::Error;
-use serde_json::Value;
-
-use super::LsblkInfo;
-
-static LVM_UUIDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
-    let mut set = HashSet::new();
-    set.insert("e6d6d379-f507-44c2-a23c-238f2a3df928");
-    set
-});
-
-/// Get set of devices used by LVM (pvs).
-///
-/// The set is indexed by using the unix raw device number (dev_t is u64)
-pub fn get_lvm_devices(lsblk_info: &[LsblkInfo]) -> Result<HashSet<u64>, Error> {
-    const PVS_BIN_PATH: &str = "pvs";
-
-    let mut command = std::process::Command::new(PVS_BIN_PATH);
-    command.args([
-        "--reportformat",
-        "json",
-        "--noheadings",
-        "--readonly",
-        "-o",
-        "pv_name",
-    ]);
-
-    let output = proxmox_sys::command::run_command(command, None)?;
-
-    let mut device_set: HashSet<u64> = HashSet::new();
-
-    for info in lsblk_info.iter() {
-        if let Some(partition_type) = &info.partition_type {
-            if LVM_UUIDS.contains(partition_type.as_str()) {
-                let meta = std::fs::metadata(&info.path)?;
-                device_set.insert(meta.rdev());
-            }
-        }
-    }
-
-    let output: Value = output.parse()?;
-
-    match output["report"][0]["pv"].as_array() {
-        Some(list) => {
-            for info in list {
-                if let Some(pv_name) = info["pv_name"].as_str() {
-                    let meta = std::fs::metadata(pv_name)?;
-                    device_set.insert(meta.rdev());
-                }
-            }
-        }
-        None => return Ok(device_set),
-    }
-
-    Ok(device_set)
-}
diff --git a/src/tools/disks/mod.rs b/src/tools/disks/mod.rs
deleted file mode 100644
index 4197d0b0f..000000000
--- a/src/tools/disks/mod.rs
+++ /dev/null
@@ -1,1394 +0,0 @@
-//! Disk query/management utilities for.
-
-use std::collections::{HashMap, HashSet};
-use std::ffi::{OsStr, OsString};
-use std::io;
-use std::os::unix::ffi::{OsStrExt, OsStringExt};
-use std::os::unix::fs::{FileExt, MetadataExt, OpenOptionsExt};
-use std::path::{Path, PathBuf};
-use std::sync::{Arc, LazyLock};
-
-use anyhow::{bail, format_err, Context as _, Error};
-use libc::dev_t;
-use once_cell::sync::OnceCell;
-
-use ::serde::{Deserialize, Serialize};
-
-use proxmox_lang::{io_bail, io_format_err};
-use proxmox_log::info;
-use proxmox_schema::api;
-use proxmox_sys::linux::procfs::{mountinfo::Device, MountInfo};
-
-use pbs_api_types::{BLOCKDEVICE_DISK_AND_PARTITION_NAME_REGEX, BLOCKDEVICE_NAME_REGEX};
-
-use proxmox_parallel_handler::ParallelHandler;
-
-mod zfs;
-pub use zfs::*;
-mod zpool_status;
-pub use zpool_status::*;
-mod zpool_list;
-pub use zpool_list::*;
-mod lvm;
-pub use lvm::*;
-mod smart;
-pub use smart::*;
-
-static ISCSI_PATH_REGEX: LazyLock<regex::Regex> =
-    LazyLock::new(|| regex::Regex::new(r"host[^/]*/session[^/]*").unwrap());
-
-/// Disk management context.
-///
-/// This provides access to disk information with some caching for faster querying of multiple
-/// devices.
-pub struct DiskManage {
-    mount_info: OnceCell<MountInfo>,
-    mounted_devices: OnceCell<HashSet<dev_t>>,
-}
-
-/// Information for a device as returned by lsblk.
-#[derive(Deserialize)]
-pub struct LsblkInfo {
-    /// Path to the device.
-    path: String,
-    /// Partition type GUID.
-    #[serde(rename = "parttype")]
-    partition_type: Option<String>,
-    /// File system label.
-    #[serde(rename = "fstype")]
-    file_system_type: Option<String>,
-    /// File system UUID.
-    uuid: Option<String>,
-}
-
-impl DiskManage {
-    /// Create a new disk management context.
-    pub fn new() -> Arc<Self> {
-        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<P: AsRef<Path>>(self: Arc<Self>, devnode: P) -> io::Result<Disk> {
-        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<Self>, devnum: dev_t) -> io::Result<Disk> {
-        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<P: AsRef<Path>>(self: Arc<Self>, path: P) -> io::Result<Disk> {
-        let device = udev::Device::from_syspath(path.as_ref())?;
-        Ok(Disk {
-            manager: self,
-            device,
-            info: Default::default(),
-        })
-    }
-
-    /// Get a `Disk` for a name in `/sys/block/<name>`.
-    pub fn disk_by_name(self: Arc<Self>, name: &str) -> io::Result<Disk> {
-        let syspath = format!("/sys/block/{name}");
-        self.disk_by_sys_path(syspath)
-    }
-
-    /// Get a `Disk` for a name in `/sys/class/block/<name>`.
-    pub fn partition_by_name(self: Arc<Self>, name: &str) -> io::Result<Disk> {
-        let syspath = format!("/sys/class/block/{name}");
-        self.disk_by_sys_path(syspath)
-    }
-
-    /// Gather information about mounted disks:
-    fn mounted_devices(&self) -> Result<&HashSet<dev_t>, Error> {
-        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_deref() {
-                        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)
-            })
-    }
-
-    /// Information about file system type and used device for a path
-    ///
-    /// Returns tuple (fs_type, device, mount_source)
-    pub fn find_mounted_device(
-        &self,
-        path: &std::path::Path,
-    ) -> Result<Option<(String, Device, Option<OsString>)>, Error> {
-        let stat = nix::sys::stat::stat(path)?;
-        let device = Device::from_dev_t(stat.st_dev);
-
-        let root_path = std::path::Path::new("/");
-
-        for (_id, entry) in self.mount_info()? {
-            if entry.root == root_path && entry.device == device {
-                return Ok(Some((
-                    entry.fs_type.clone(),
-                    entry.device,
-                    entry.mount_source.clone(),
-                )));
-            }
-        }
-
-        Ok(None)
-    }
-
-    /// 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<bool, Error> {
-        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<DiskManage>,
-    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<u64>,
-    vendor: OnceCell<Option<OsString>>,
-    model: OnceCell<Option<OsString>>,
-    rotational: OnceCell<Option<bool>>,
-    // for perl: #[serde(rename = "devpath")]
-    ata_rotation_rate_rpm: OnceCell<Option<u64>>,
-    // for perl: #[serde(rename = "devpath")]
-    device_path: OnceCell<Option<PathBuf>>,
-    wwn: OnceCell<Option<OsString>>,
-    serial: OnceCell<Option<OsString>>,
-    // for perl: #[serde(skip_serializing)]
-    partition_table_type: OnceCell<Option<OsString>>,
-    // for perl: #[serde(skip_serializing)]
-    partition_entry_scheme: OnceCell<Option<OsString>>,
-    // for perl: #[serde(skip_serializing)]
-    partition_entry_uuid: OnceCell<Option<OsString>>,
-    // for perl: #[serde(skip_serializing)]
-    partition_entry_type: OnceCell<Option<OsString>>,
-    gpt: OnceCell<bool>,
-    // ???
-    bus: OnceCell<Option<OsString>>,
-    // ???
-    fs_type: OnceCell<Option<OsString>>,
-    // ???
-    has_holders: OnceCell<bool>,
-    // ???
-    is_mounted: OnceCell<bool>,
-}
-
-impl Disk {
-    /// Try to get the device number for this disk.
-    ///
-    /// (In udev this can fail...)
-    pub fn devnum(&self) -> Result<dev_t, Error> {
-        // 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> {
-        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!
-    pub fn read_sys(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
-        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`.
-    pub fn read_sys_os_str<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<OsString>> {
-        Ok(self.read_sys(path.as_ref())?.map(|mut v| {
-            if Some(&b'\n') == v.last() {
-                v.pop();
-            }
-            OsString::from_vec(v)
-        }))
-    }
-
-    /// Convenience wrapper for reading a `/sys` file which contains just a simple utf-8 string.
-    pub fn read_sys_str<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<String>> {
-        Ok(match self.read_sys(path.as_ref())? {
-            Some(data) => Some(String::from_utf8(data).map_err(io::Error::other)?),
-            None => None,
-        })
-    }
-
-    /// Convenience wrapper for unsigned integer `/sys` values up to 64 bit.
-    pub fn read_sys_u64<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<u64>> {
-        Ok(match self.read_sys_str(path)? {
-            Some(data) => Some(data.trim().parse().map_err(io::Error::other)?),
-            None => None,
-        })
-    }
-
-    /// Get the disk's size in bytes.
-    pub fn size(&self) -> io::Result<u64> {
-        Ok(*self.info.size.get_or_try_init(|| {
-            self.read_sys_u64("size")?.map(|s| s * 512).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<Option<&OsStr>> {
-        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<Option<bool>> {
-        Ok(*self
-            .info
-            .rotational
-            .get_or_try_init(|| -> io::Result<Option<bool>> {
-                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<u64> {
-        *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 partitioning scheme of which this device is a partition.
-    pub fn partition_entry_scheme(&self) -> Option<&OsStr> {
-        self.info
-            .partition_entry_scheme
-            .get_or_init(|| {
-                self.device
-                    .property_value("ID_PART_ENTRY_SCHEME")
-                    .map(|v| v.to_owned())
-            })
-            .as_ref()
-            .map(OsString::as_os_str)
-    }
-
-    /// Check if this is a partition.
-    pub fn is_partition(&self) -> bool {
-        self.partition_entry_scheme().is_some()
-    }
-
-    /// Get the type of partition entry (ie. type UUID from the entry in the GPT partition table).
-    pub fn partition_entry_type(&self) -> Option<&OsStr> {
-        self.info
-            .partition_entry_type
-            .get_or_init(|| {
-                self.device
-                    .property_value("ID_PART_ENTRY_TYPE")
-                    .map(|v| v.to_owned())
-            })
-            .as_ref()
-            .map(OsString::as_os_str)
-    }
-
-    /// Get the partition entry UUID (ie. the UUID from the entry in the GPT partition table).
-    pub fn partition_entry_uuid(&self) -> Option<&OsStr> {
-        self.info
-            .partition_entry_uuid
-            .get_or_init(|| {
-                self.device
-                    .property_value("ID_PART_ENTRY_UUID")
-                    .map(|v| v.to_owned())
-            })
-            .as_ref()
-            .map(OsString::as_os_str)
-    }
-
-    /// 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<DiskType> {
-        Ok(match self.rotational()? {
-            Some(false) => DiskType::Ssd,
-            Some(true) => DiskType::Hdd,
-            None => 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<bool> {
-        Ok(*self
-            .info
-            .has_holders
-            .get_or_try_init(|| -> io::Result<bool> {
-                let mut subdir = self.syspath().to_owned();
-                subdir.push("holders");
-                for entry in std::fs::read_dir(subdir)? {
-                    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<bool, Error> {
-        Ok(*self
-            .info
-            .is_mounted
-            .get_or_try_init(|| self.manager.is_devnum_mounted(self.devnum()?))?)
-    }
-
-    /// Read block device stats
-    ///
-    /// see <https://www.kernel.org/doc/Documentation/block/stat.txt>
-    pub fn read_stat(&self) -> std::io::Result<Option<BlockDevStat>> {
-        if let Some(stat) = self.read_sys(Path::new("stat"))? {
-            let stat = unsafe { std::str::from_utf8_unchecked(&stat) };
-            let stat: Vec<u64> = stat
-                .split_ascii_whitespace()
-                .map(|s| s.parse().unwrap_or_default())
-                .collect();
-
-            if stat.len() < 15 {
-                return Ok(None);
-            }
-
-            return Ok(Some(BlockDevStat {
-                read_ios: stat[0],
-                read_sectors: stat[2],
-                write_ios: stat[4] + stat[11],     // write + discard
-                write_sectors: stat[6] + stat[13], // write + discard
-                io_ticks: stat[10],
-            }));
-        }
-        Ok(None)
-    }
-
-    /// List device partitions
-    pub fn partitions(&self) -> Result<HashMap<u64, Disk>, Error> {
-        let sys_path = self.syspath();
-        let device = self.sysname().to_string_lossy().to_string();
-
-        let mut map = HashMap::new();
-
-        for item in proxmox_sys::fs::read_subdir(libc::AT_FDCWD, sys_path)? {
-            let item = item?;
-            let name = match item.file_name().to_str() {
-                Ok(name) => name,
-                Err(_) => continue, // skip non utf8 entries
-            };
-
-            if !name.starts_with(&device) {
-                continue;
-            }
-
-            let mut part_path = sys_path.to_owned();
-            part_path.push(name);
-
-            let disk_part = self.manager.clone().disk_by_sys_path(&part_path)?;
-
-            if let Some(partition) = disk_part.read_sys_u64("partition")? {
-                map.insert(partition, disk_part);
-            }
-        }
-
-        Ok(map)
-    }
-}
-
-#[api()]
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-/// 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,
-}
-
-#[derive(Debug)]
-/// Represents the contents of the `/sys/block/<dev>/stat` file.
-pub struct BlockDevStat {
-    pub read_ios: u64,
-    pub read_sectors: u64,
-    pub write_ios: u64,
-    pub write_sectors: u64,
-    pub io_ticks: u64, // milliseconds
-}
-
-/// Use lsblk to read partition type uuids and file system types.
-pub fn get_lsblk_info() -> Result<Vec<LsblkInfo>, Error> {
-    let mut command = std::process::Command::new("lsblk");
-    command.args(["--json", "-o", "path,parttype,fstype,uuid"]);
-
-    let output = proxmox_sys::command::run_command(command, None)?;
-
-    let mut output: serde_json::Value = output.parse()?;
-
-    Ok(serde_json::from_value(output["blockdevices"].take())?)
-}
-
-/// Get set of devices with a file system label.
-///
-/// The set is indexed by using the unix raw device number (dev_t is u64)
-fn get_file_system_devices(lsblk_info: &[LsblkInfo]) -> Result<HashSet<u64>, Error> {
-    let mut device_set: HashSet<u64> = HashSet::new();
-
-    for info in lsblk_info.iter() {
-        if info.file_system_type.is_some() {
-            let meta = std::fs::metadata(&info.path)?;
-            device_set.insert(meta.rdev());
-        }
-    }
-
-    Ok(device_set)
-}
-
-#[api()]
-#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
-#[serde(rename_all = "lowercase")]
-/// What a block device partition is used for.
-pub enum PartitionUsageType {
-    /// Partition is not used (as far we can tell)
-    Unused,
-    /// Partition is used by LVM
-    LVM,
-    /// Partition is used by ZFS
-    ZFS,
-    /// Partition is ZFS reserved
-    ZfsReserved,
-    /// Partition is an EFI partition
-    EFI,
-    /// Partition is a BIOS partition
-    BIOS,
-    /// Partition contains a file system label
-    FileSystem,
-}
-
-#[api()]
-#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
-#[serde(rename_all = "lowercase")]
-/// What a block device (disk) is used for.
-pub enum DiskUsageType {
-    /// Disk is not used (as far we can tell)
-    Unused,
-    /// Disk is mounted
-    Mounted,
-    /// Disk is used by LVM
-    LVM,
-    /// Disk is used by ZFS
-    ZFS,
-    /// Disk is used by device-mapper
-    DeviceMapper,
-    /// Disk has partitions
-    Partitions,
-    /// Disk contains a file system label
-    FileSystem,
-}
-
-#[api()]
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-/// Basic information about a partition
-pub struct PartitionInfo {
-    /// The partition name
-    pub name: String,
-    /// What the partition is used for
-    pub used: PartitionUsageType,
-    /// Is the partition mounted
-    pub mounted: bool,
-    /// The filesystem of the partition
-    pub filesystem: Option<String>,
-    /// The partition devpath
-    pub devpath: Option<String>,
-    /// Size in bytes
-    pub size: Option<u64>,
-    /// GPT partition
-    pub gpt: bool,
-    /// UUID
-    pub uuid: Option<String>,
-}
-
-#[api(
-    properties: {
-        used: {
-            type: DiskUsageType,
-        },
-        "disk-type": {
-            type: DiskType,
-        },
-        status: {
-            type: SmartStatus,
-        },
-        partitions: {
-            optional: true,
-            items: {
-                type: PartitionInfo
-            }
-        }
-    }
-)]
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-/// Information about how a Disk is used
-pub struct DiskUsageInfo {
-    /// Disk name (`/sys/block/<name>`)
-    pub name: String,
-    pub used: DiskUsageType,
-    pub disk_type: DiskType,
-    pub status: SmartStatus,
-    /// Disk wearout
-    pub wearout: Option<f64>,
-    /// Vendor
-    pub vendor: Option<String>,
-    /// Model
-    pub model: Option<String>,
-    /// WWN
-    pub wwn: Option<String>,
-    /// Disk size
-    pub size: u64,
-    /// Serisal number
-    pub serial: Option<String>,
-    /// Partitions on the device
-    pub partitions: Option<Vec<PartitionInfo>>,
-    /// Linux device path (/dev/xxx)
-    pub devpath: Option<String>,
-    /// Set if disk contains a GPT partition table
-    pub gpt: bool,
-    /// RPM
-    pub rpm: Option<u64>,
-}
-
-fn scan_partitions(
-    disk_manager: Arc<DiskManage>,
-    lvm_devices: &HashSet<u64>,
-    zfs_devices: &HashSet<u64>,
-    device: &str,
-) -> Result<DiskUsageType, Error> {
-    let mut sys_path = std::path::PathBuf::from("/sys/block");
-    sys_path.push(device);
-
-    let mut used = DiskUsageType::Unused;
-
-    let mut found_lvm = false;
-    let mut found_zfs = false;
-    let mut found_mountpoints = false;
-    let mut found_dm = false;
-    let mut found_partitions = false;
-
-    for item in proxmox_sys::fs::read_subdir(libc::AT_FDCWD, &sys_path)? {
-        let item = item?;
-        let name = match item.file_name().to_str() {
-            Ok(name) => name,
-            Err(_) => continue, // skip non utf8 entries
-        };
-        if !name.starts_with(device) {
-            continue;
-        }
-
-        found_partitions = true;
-
-        let mut part_path = sys_path.clone();
-        part_path.push(name);
-
-        let data = disk_manager.clone().disk_by_sys_path(&part_path)?;
-
-        let devnum = data.devnum()?;
-
-        if lvm_devices.contains(&devnum) {
-            found_lvm = true;
-        }
-
-        if data.is_mounted()? {
-            found_mountpoints = true;
-        }
-
-        if data.has_holders()? {
-            found_dm = true;
-        }
-
-        if zfs_devices.contains(&devnum) {
-            found_zfs = true;
-        }
-    }
-
-    if found_mountpoints {
-        used = DiskUsageType::Mounted;
-    } else if found_lvm {
-        used = DiskUsageType::LVM;
-    } else if found_zfs {
-        used = DiskUsageType::ZFS;
-    } else if found_dm {
-        used = DiskUsageType::DeviceMapper;
-    } else if found_partitions {
-        used = DiskUsageType::Partitions;
-    }
-
-    Ok(used)
-}
-
-pub struct DiskUsageQuery {
-    smart: bool,
-    partitions: bool,
-}
-
-impl Default for DiskUsageQuery {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl DiskUsageQuery {
-    pub const fn new() -> Self {
-        Self {
-            smart: true,
-            partitions: false,
-        }
-    }
-
-    pub fn smart(&mut self, smart: bool) -> &mut Self {
-        self.smart = smart;
-        self
-    }
-
-    pub fn partitions(&mut self, partitions: bool) -> &mut Self {
-        self.partitions = partitions;
-        self
-    }
-
-    pub fn query(&self) -> Result<HashMap<String, DiskUsageInfo>, Error> {
-        get_disks(None, !self.smart, self.partitions)
-    }
-
-    pub fn find(&self, disk: &str) -> Result<DiskUsageInfo, Error> {
-        let mut map = get_disks(Some(vec![disk.to_string()]), !self.smart, self.partitions)?;
-        if let Some(info) = map.remove(disk) {
-            Ok(info)
-        } else {
-            bail!("failed to get disk usage info - internal error"); // should not happen
-        }
-    }
-
-    pub fn find_all(&self, disks: Vec<String>) -> Result<HashMap<String, DiskUsageInfo>, Error> {
-        get_disks(Some(disks), !self.smart, self.partitions)
-    }
-}
-
-fn get_partitions_info(
-    partitions: HashMap<u64, Disk>,
-    lvm_devices: &HashSet<u64>,
-    zfs_devices: &HashSet<u64>,
-    file_system_devices: &HashSet<u64>,
-    lsblk_infos: &[LsblkInfo],
-) -> Vec<PartitionInfo> {
-    partitions
-        .values()
-        .map(|disk| {
-            let devpath = disk
-                .device_path()
-                .map(|p| p.to_owned())
-                .map(|p| p.to_string_lossy().to_string());
-
-            let mut used = PartitionUsageType::Unused;
-
-            if let Ok(devnum) = disk.devnum() {
-                if lvm_devices.contains(&devnum) {
-                    used = PartitionUsageType::LVM;
-                } else if zfs_devices.contains(&devnum) {
-                    used = PartitionUsageType::ZFS;
-                } else if file_system_devices.contains(&devnum) {
-                    used = PartitionUsageType::FileSystem;
-                }
-            }
-
-            let mounted = disk.is_mounted().unwrap_or(false);
-            let mut filesystem = None;
-            let mut uuid = None;
-            if let Some(devpath) = devpath.as_ref() {
-                for info in lsblk_infos.iter().filter(|i| i.path.eq(devpath)) {
-                    uuid = info
-                        .uuid
-                        .clone()
-                        .filter(|uuid| pbs_api_types::UUID_REGEX.is_match(uuid));
-                    used = match info.partition_type.as_deref() {
-                        Some("21686148-6449-6e6f-744e-656564454649") => PartitionUsageType::BIOS,
-                        Some("c12a7328-f81f-11d2-ba4b-00a0c93ec93b") => PartitionUsageType::EFI,
-                        Some("6a945a3b-1dd2-11b2-99a6-080020736631") => {
-                            PartitionUsageType::ZfsReserved
-                        }
-                        _ => used,
-                    };
-                    if used == PartitionUsageType::FileSystem {
-                        filesystem.clone_from(&info.file_system_type);
-                    }
-                }
-            }
-
-            PartitionInfo {
-                name: disk.sysname().to_str().unwrap_or("?").to_string(),
-                devpath,
-                used,
-                mounted,
-                filesystem,
-                size: disk.size().ok(),
-                gpt: disk.has_gpt(),
-                uuid,
-            }
-        })
-        .collect()
-}
-
-/// Get disk usage information for multiple disks
-fn get_disks(
-    // filter - list of device names (without leading /dev)
-    disks: Option<Vec<String>>,
-    // do no include data from smartctl
-    no_smart: bool,
-    // include partitions
-    include_partitions: bool,
-) -> Result<HashMap<String, DiskUsageInfo>, Error> {
-    let disk_manager = DiskManage::new();
-
-    let lsblk_info = get_lsblk_info()?;
-
-    let zfs_devices =
-        zfs_devices(&lsblk_info, None).or_else(|err| -> Result<HashSet<u64>, Error> {
-            eprintln!("error getting zfs devices: {err}");
-            Ok(HashSet::new())
-        })?;
-
-    let lvm_devices = get_lvm_devices(&lsblk_info)?;
-
-    let file_system_devices = get_file_system_devices(&lsblk_info)?;
-
-    // fixme: ceph journals/volumes
-
-    let mut result = HashMap::new();
-    let mut device_paths = Vec::new();
-
-    for item in proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX)?
-    {
-        let item = item?;
-
-        let name = item.file_name().to_str().unwrap().to_string();
-
-        if let Some(ref disks) = disks {
-            if !disks.contains(&name) {
-                continue;
-            }
-        }
-
-        let sys_path = format!("/sys/block/{name}");
-
-        if let Ok(target) = std::fs::read_link(&sys_path) {
-            if let Some(target) = target.to_str() {
-                if ISCSI_PATH_REGEX.is_match(target) {
-                    continue;
-                } // skip iSCSI devices
-            }
-        }
-
-        let disk = disk_manager.clone().disk_by_sys_path(&sys_path)?;
-
-        let devnum = disk.devnum()?;
-
-        let size = match disk.size() {
-            Ok(size) => size,
-            Err(_) => continue, // skip devices with unreadable size
-        };
-
-        let disk_type = match disk.guess_disk_type() {
-            Ok(disk_type) => disk_type,
-            Err(_) => continue, // skip devices with undetectable type
-        };
-
-        let mut usage = DiskUsageType::Unused;
-
-        if lvm_devices.contains(&devnum) {
-            usage = DiskUsageType::LVM;
-        }
-
-        match disk.is_mounted() {
-            Ok(true) => usage = DiskUsageType::Mounted,
-            Ok(false) => {}
-            Err(_) => continue, // skip devices with undetectable mount status
-        }
-
-        if zfs_devices.contains(&devnum) {
-            usage = DiskUsageType::ZFS;
-        }
-
-        let vendor = disk
-            .vendor()
-            .unwrap_or(None)
-            .map(|s| s.to_string_lossy().trim().to_string());
-
-        let model = disk.model().map(|s| s.to_string_lossy().into_owned());
-
-        let serial = disk.serial().map(|s| s.to_string_lossy().into_owned());
-
-        let devpath = disk
-            .device_path()
-            .map(|p| p.to_owned())
-            .map(|p| p.to_string_lossy().to_string());
-
-        device_paths.push((name.clone(), devpath.clone()));
-
-        let wwn = disk.wwn().map(|s| s.to_string_lossy().into_owned());
-
-        let partitions: Option<Vec<PartitionInfo>> = if include_partitions {
-            disk.partitions().map_or(None, |parts| {
-                Some(get_partitions_info(
-                    parts,
-                    &lvm_devices,
-                    &zfs_devices,
-                    &file_system_devices,
-                    &lsblk_info,
-                ))
-            })
-        } else {
-            None
-        };
-
-        if usage != DiskUsageType::Mounted {
-            match scan_partitions(disk_manager.clone(), &lvm_devices, &zfs_devices, &name) {
-                Ok(part_usage) => {
-                    if part_usage != DiskUsageType::Unused {
-                        usage = part_usage;
-                    }
-                }
-                Err(_) => continue, // skip devices if scan_partitions fail
-            };
-        }
-
-        if usage == DiskUsageType::Unused && file_system_devices.contains(&devnum) {
-            usage = DiskUsageType::FileSystem;
-        }
-
-        if usage == DiskUsageType::Unused && disk.has_holders()? {
-            usage = DiskUsageType::DeviceMapper;
-        }
-
-        let info = DiskUsageInfo {
-            name: name.clone(),
-            vendor,
-            model,
-            partitions,
-            serial,
-            devpath,
-            size,
-            wwn,
-            disk_type,
-            status: SmartStatus::Unknown,
-            wearout: None,
-            used: usage,
-            gpt: disk.has_gpt(),
-            rpm: disk.ata_rotation_rate_rpm(),
-        };
-
-        result.insert(name, info);
-    }
-
-    if !no_smart {
-        let (tx, rx) = crossbeam_channel::bounded(result.len());
-
-        let parallel_handler =
-            ParallelHandler::new("smartctl data", 4, move |device: (String, String)| {
-                match get_smart_data(Path::new(&device.1), false) {
-                    Ok(smart_data) => tx.send((device.0, smart_data))?,
-                    // do not fail the whole disk output just because smartctl couldn't query one
-                    Err(err) => log::error!("failed to gather smart data for {} – {err}", device.1),
-                }
-                Ok(())
-            });
-
-        for (name, path) in device_paths.into_iter() {
-            if let Some(p) = path {
-                parallel_handler.send((name, p))?;
-            }
-        }
-
-        parallel_handler.complete()?;
-        while let Ok(msg) = rx.recv() {
-            if let Some(value) = result.get_mut(&msg.0) {
-                value.wearout = msg.1.wearout;
-                value.status = msg.1.status;
-            }
-        }
-    }
-    Ok(result)
-}
-
-/// Try to reload the partition table
-pub fn reread_partition_table(disk: &Disk) -> Result<(), Error> {
-    let disk_path = match disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", disk.syspath()),
-    };
-
-    let mut command = std::process::Command::new("blockdev");
-    command.arg("--rereadpt");
-    command.arg(disk_path);
-
-    proxmox_sys::command::run_command(command, None)?;
-
-    Ok(())
-}
-
-/// Initialize disk by writing a GPT partition table
-pub fn inititialize_gpt_disk(disk: &Disk, uuid: Option<&str>) -> Result<(), Error> {
-    let disk_path = match disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", disk.syspath()),
-    };
-
-    let uuid = uuid.unwrap_or("R"); // R .. random disk GUID
-
-    let mut command = std::process::Command::new("sgdisk");
-    command.arg(disk_path);
-    command.args(["-U", uuid]);
-
-    proxmox_sys::command::run_command(command, None)?;
-
-    Ok(())
-}
-
-/// Wipes all labels, the first 200 MiB, and the last 4096 bytes of a disk/partition.
-/// If called with a partition, also sets the partition type to 0x83 'Linux filesystem'.
-pub fn wipe_blockdev(disk: &Disk) -> Result<(), Error> {
-    let disk_path = match disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", disk.syspath()),
-    };
-
-    let is_partition = disk.is_partition();
-
-    let mut to_wipe: Vec<PathBuf> = Vec::new();
-
-    let partitions_map = disk.partitions()?;
-    for part_disk in partitions_map.values() {
-        let part_path = match part_disk.device_path() {
-            Some(path) => path,
-            None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
-        };
-        to_wipe.push(part_path.to_path_buf());
-    }
-
-    to_wipe.push(disk_path.to_path_buf());
-
-    info!("Wiping block device {}", disk_path.display());
-
-    let mut wipefs_command = std::process::Command::new("wipefs");
-    wipefs_command.arg("--all").args(&to_wipe);
-
-    let wipefs_output = proxmox_sys::command::run_command(wipefs_command, None)?;
-    info!("wipefs output: {wipefs_output}");
-
-    zero_disk_start_and_end(disk)?;
-
-    if is_partition {
-        // set the partition type to 0x83 'Linux filesystem'
-        change_parttype(disk, "8300")?;
-    }
-
-    Ok(())
-}
-
-pub fn zero_disk_start_and_end(disk: &Disk) -> Result<(), Error> {
-    let disk_path = match disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", disk.syspath()),
-    };
-
-    let disk_size = disk.size()?;
-    let file = std::fs::OpenOptions::new()
-        .write(true)
-        .custom_flags(libc::O_CLOEXEC | libc::O_DSYNC)
-        .open(disk_path)
-        .with_context(|| "failed to open device {disk_path:?} for writing")?;
-    let write_size = disk_size.min(200 * 1024 * 1024);
-    let zeroes = proxmox_io::boxed::zeroed(write_size as usize);
-    file.write_all_at(&zeroes, 0)
-        .with_context(|| "failed to wipe start of device {disk_path:?}")?;
-    if disk_size > write_size {
-        file.write_all_at(&zeroes[0..4096], disk_size - 4096)
-            .with_context(|| "failed to wipe end of device {disk_path:?}")?;
-    }
-    Ok(())
-}
-
-pub fn change_parttype(part_disk: &Disk, part_type: &str) -> Result<(), Error> {
-    let part_path = match part_disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
-    };
-    if let Ok(stat) = nix::sys::stat::stat(part_path) {
-        let mut sgdisk_command = std::process::Command::new("sgdisk");
-        let major = unsafe { libc::major(stat.st_rdev) };
-        let minor = unsafe { libc::minor(stat.st_rdev) };
-        let partnum_path = &format!("/sys/dev/block/{major}:{minor}/partition");
-        let partnum: u32 = std::fs::read_to_string(partnum_path)?.trim_end().parse()?;
-        sgdisk_command.arg(format!("-t{partnum}:{part_type}"));
-        let part_disk_parent = match part_disk.parent() {
-            Some(disk) => disk,
-            None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
-        };
-        let part_disk_parent_path = match part_disk_parent.device_path() {
-            Some(path) => path,
-            None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
-        };
-        sgdisk_command.arg(part_disk_parent_path);
-        let sgdisk_output = proxmox_sys::command::run_command(sgdisk_command, None)?;
-        info!("sgdisk output: {sgdisk_output}");
-    }
-    Ok(())
-}
-
-/// Create a single linux partition using the whole available space
-pub fn create_single_linux_partition(disk: &Disk) -> Result<Disk, Error> {
-    let disk_path = match disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", disk.syspath()),
-    };
-
-    let mut command = std::process::Command::new("sgdisk");
-    command.args(["-n1", "-t1:8300"]);
-    command.arg(disk_path);
-
-    proxmox_sys::command::run_command(command, None)?;
-
-    let mut partitions = disk.partitions()?;
-
-    match partitions.remove(&1) {
-        Some(partition) => Ok(partition),
-        None => bail!("unable to lookup device partition"),
-    }
-}
-
-#[api()]
-#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
-#[serde(rename_all = "lowercase")]
-/// A file system type supported by our tooling.
-pub enum FileSystemType {
-    /// Linux Ext4
-    Ext4,
-    /// XFS
-    Xfs,
-}
-
-impl std::fmt::Display for FileSystemType {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let text = match self {
-            FileSystemType::Ext4 => "ext4",
-            FileSystemType::Xfs => "xfs",
-        };
-        write!(f, "{text}")
-    }
-}
-
-impl std::str::FromStr for FileSystemType {
-    type Err = serde_json::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        use serde::de::IntoDeserializer;
-        Self::deserialize(s.into_deserializer())
-    }
-}
-
-/// Create a file system on a disk or disk partition
-pub fn create_file_system(disk: &Disk, fs_type: FileSystemType) -> Result<(), Error> {
-    let disk_path = match disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", disk.syspath()),
-    };
-
-    let fs_type = fs_type.to_string();
-
-    let mut command = std::process::Command::new("mkfs");
-    command.args(["-t", &fs_type]);
-    command.arg(disk_path);
-
-    proxmox_sys::command::run_command(command, None)?;
-
-    Ok(())
-}
-/// Block device name completion helper
-pub fn complete_disk_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
-    let dir =
-        match proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX) {
-            Ok(dir) => dir,
-            Err(_) => return vec![],
-        };
-
-    dir.flatten()
-        .map(|item| item.file_name().to_str().unwrap().to_string())
-        .collect()
-}
-
-/// Block device partition name completion helper
-pub fn complete_partition_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
-    let dir = match proxmox_sys::fs::scan_subdir(
-        libc::AT_FDCWD,
-        "/sys/class/block",
-        &BLOCKDEVICE_DISK_AND_PARTITION_NAME_REGEX,
-    ) {
-        Ok(dir) => dir,
-        Err(_) => return vec![],
-    };
-
-    dir.flatten()
-        .map(|item| item.file_name().to_str().unwrap().to_string())
-        .collect()
-}
-
-/// Read the FS UUID (parse blkid output)
-///
-/// Note: Calling blkid is more reliable than using the udev ID_FS_UUID property.
-pub fn get_fs_uuid(disk: &Disk) -> Result<String, Error> {
-    let disk_path = match disk.device_path() {
-        Some(path) => path,
-        None => bail!("disk {:?} has no node in /dev", disk.syspath()),
-    };
-
-    let mut command = std::process::Command::new("blkid");
-    command.args(["-o", "export"]);
-    command.arg(disk_path);
-
-    let output = proxmox_sys::command::run_command(command, None)?;
-
-    for line in output.lines() {
-        if let Some(uuid) = line.strip_prefix("UUID=") {
-            return Ok(uuid.to_string());
-        }
-    }
-
-    bail!("get_fs_uuid failed - missing UUID");
-}
-
-/// Mount a disk by its UUID and the mount point.
-pub fn mount_by_uuid(uuid: &str, mount_point: &Path) -> Result<(), Error> {
-    let mut command = std::process::Command::new("mount");
-    command.arg(format!("UUID={uuid}"));
-    command.arg(mount_point);
-
-    proxmox_sys::command::run_command(command, None)?;
-    Ok(())
-}
-
-/// Create bind mount.
-pub fn bind_mount(path: &Path, target: &Path) -> Result<(), Error> {
-    let mut command = std::process::Command::new("mount");
-    command.arg("--bind");
-    command.arg(path);
-    command.arg(target);
-
-    proxmox_sys::command::run_command(command, None)?;
-    Ok(())
-}
-
-/// Unmount a disk by its mount point.
-pub fn unmount_by_mountpoint(path: &Path) -> Result<(), Error> {
-    let mut command = std::process::Command::new("umount");
-    command.arg(path);
-
-    proxmox_sys::command::run_command(command, None)?;
-    Ok(())
-}
diff --git a/src/tools/disks/smart.rs b/src/tools/disks/smart.rs
deleted file mode 100644
index 1d41cee24..000000000
--- a/src/tools/disks/smart.rs
+++ /dev/null
@@ -1,227 +0,0 @@
-use std::sync::LazyLock;
-use std::{
-    collections::{HashMap, HashSet},
-    path::Path,
-};
-
-use ::serde::{Deserialize, Serialize};
-use anyhow::Error;
-
-use proxmox_schema::api;
-
-#[api()]
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-/// SMART status
-pub enum SmartStatus {
-    /// Smart tests passed - everything is OK
-    Passed,
-    /// Smart tests failed - disk has problems
-    Failed,
-    /// Unknown status
-    Unknown,
-}
-
-#[api()]
-#[derive(Debug, Serialize, Deserialize)]
-/// SMART Attribute
-pub struct SmartAttribute {
-    /// Attribute name
-    name: String,
-    // FIXME: remove value with next major release (PBS 3.0)
-    /// duplicate of raw - kept for API stability
-    value: String,
-    /// Attribute raw value
-    raw: String,
-    // the rest of the values is available for ATA type
-    /// ATA Attribute ID
-    #[serde(skip_serializing_if = "Option::is_none")]
-    id: Option<u64>,
-    /// ATA Flags
-    #[serde(skip_serializing_if = "Option::is_none")]
-    flags: Option<String>,
-    /// ATA normalized value (0..100)
-    #[serde(skip_serializing_if = "Option::is_none")]
-    normalized: Option<f64>,
-    /// ATA worst
-    #[serde(skip_serializing_if = "Option::is_none")]
-    worst: Option<f64>,
-    /// ATA threshold
-    #[serde(skip_serializing_if = "Option::is_none")]
-    threshold: Option<f64>,
-}
-
-#[api(
-    properties: {
-        status: {
-            type: SmartStatus,
-        },
-        wearout: {
-            description: "Wearout level.",
-            type: f64,
-            optional: true,
-        },
-        attributes: {
-            description: "SMART attributes.",
-            type: Array,
-            items: {
-                type: SmartAttribute,
-            },
-        },
-    },
-)]
-#[derive(Debug, Serialize, Deserialize)]
-/// Data from smartctl
-pub struct SmartData {
-    pub status: SmartStatus,
-    pub wearout: Option<f64>,
-    pub attributes: Vec<SmartAttribute>,
-}
-
-/// Read smartctl data for a disk (/dev/XXX).
-pub fn get_smart_data(disk_path: &Path, health_only: bool) -> Result<SmartData, Error> {
-    const SMARTCTL_BIN_PATH: &str = "smartctl";
-
-    let mut command = std::process::Command::new(SMARTCTL_BIN_PATH);
-    command.arg("-H");
-    if !health_only {
-        command.args(["-A", "-j"]);
-    }
-
-    command.arg(disk_path);
-
-    let output = proxmox_sys::command::run_command(
-        command,
-        Some(
-            |exitcode| (exitcode & 0b0011) == 0, // only bits 0-1 are fatal errors
-        ),
-    )?;
-
-    let output: serde_json::Value = output.parse()?;
-
-    let mut wearout = None;
-
-    let mut attributes = Vec::new();
-    let mut wearout_candidates = HashMap::new();
-
-    // ATA devices
-    if let Some(list) = output["ata_smart_attributes"]["table"].as_array() {
-        for item in list {
-            let id = match item["id"].as_u64() {
-                Some(id) => id,
-                None => continue, // skip attributes without id
-            };
-
-            let name = match item["name"].as_str() {
-                Some(name) => name.to_string(),
-                None => continue, // skip attributes without name
-            };
-
-            let raw_value = match item["raw"]["string"].as_str() {
-                Some(value) => value.to_string(),
-                None => continue, // skip attributes without raw value
-            };
-
-            let flags = match item["flags"]["string"].as_str() {
-                Some(flags) => flags.to_string(),
-                None => continue, // skip attributes without flags
-            };
-
-            let normalized = match item["value"].as_f64() {
-                Some(v) => v,
-                None => continue, // skip attributes without normalize value
-            };
-
-            let worst = match item["worst"].as_f64() {
-                Some(v) => v,
-                None => continue, // skip attributes without worst entry
-            };
-
-            let threshold = match item["thresh"].as_f64() {
-                Some(v) => v,
-                None => continue, // skip attributes without threshold entry
-            };
-
-            if WEAROUT_FIELD_NAMES.contains(&name as &str) {
-                wearout_candidates.insert(name.clone(), normalized);
-            }
-
-            attributes.push(SmartAttribute {
-                name,
-                value: raw_value.clone(),
-                raw: raw_value,
-                id: Some(id),
-                flags: Some(flags),
-                normalized: Some(normalized),
-                worst: Some(worst),
-                threshold: Some(threshold),
-            });
-        }
-    }
-
-    if !wearout_candidates.is_empty() {
-        for field in WEAROUT_FIELD_ORDER {
-            if let Some(value) = wearout_candidates.get(field as &str) {
-                wearout = Some(*value);
-                break;
-            }
-        }
-    }
-
-    // NVME devices
-    if let Some(list) = output["nvme_smart_health_information_log"].as_object() {
-        for (name, value) in list {
-            if name == "percentage_used" {
-                // extract wearout from nvme text, allow for decimal values
-                if let Some(v) = value.as_f64() {
-                    if v <= 100.0 {
-                        wearout = Some(100.0 - v);
-                    }
-                }
-            }
-            if let Some(value) = value.as_f64() {
-                attributes.push(SmartAttribute {
-                    name: name.to_string(),
-                    value: value.to_string(),
-                    raw: value.to_string(),
-                    id: None,
-                    flags: None,
-                    normalized: None,
-                    worst: None,
-                    threshold: None,
-                });
-            }
-        }
-    }
-
-    let status = match output["smart_status"]["passed"].as_bool() {
-        None => SmartStatus::Unknown,
-        Some(true) => SmartStatus::Passed,
-        Some(false) => SmartStatus::Failed,
-    };
-
-    Ok(SmartData {
-        status,
-        wearout,
-        attributes,
-    })
-}
-
-static WEAROUT_FIELD_ORDER: &[&str] = &[
-    "Media_Wearout_Indicator",
-    "SSD_Life_Left",
-    "Wear_Leveling_Count",
-    "Perc_Write/Erase_Ct_BC",
-    "Perc_Rated_Life_Remain",
-    "Remaining_Lifetime_Perc",
-    "Percent_Lifetime_Remain",
-    "Lifetime_Left",
-    "PCT_Life_Remaining",
-    "Lifetime_Remaining",
-    "Percent_Life_Remaining",
-    "Percent_Lifetime_Used",
-    "Perc_Rated_Life_Used",
-];
-
-static WEAROUT_FIELD_NAMES: LazyLock<HashSet<&'static str>> =
-    LazyLock::new(|| WEAROUT_FIELD_ORDER.iter().cloned().collect());
diff --git a/src/tools/disks/zfs.rs b/src/tools/disks/zfs.rs
deleted file mode 100644
index 0babb8870..000000000
--- a/src/tools/disks/zfs.rs
+++ /dev/null
@@ -1,205 +0,0 @@
-use std::collections::HashSet;
-use std::os::unix::fs::MetadataExt;
-use std::path::PathBuf;
-use std::sync::{LazyLock, Mutex};
-
-use anyhow::{bail, Error};
-
-use proxmox_schema::const_regex;
-
-use super::*;
-
-static ZFS_UUIDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
-    let mut set = HashSet::new();
-    set.insert("6a898cc3-1dd2-11b2-99a6-080020736631"); // apple
-    set.insert("516e7cba-6ecf-11d6-8ff8-00022d09712b"); // bsd
-    set
-});
-
-fn get_pool_from_dataset(dataset: &str) -> &str {
-    if let Some(idx) = dataset.find('/') {
-        dataset[0..idx].as_ref()
-    } else {
-        dataset
-    }
-}
-
-/// Returns kernel IO-stats for zfs pools
-pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> {
-    let mut path = PathBuf::from("/proc/spl/kstat/zfs");
-    path.push(pool);
-    path.push("io");
-
-    let text = match proxmox_sys::fs::file_read_optional_string(&path)? {
-        Some(text) => text,
-        None => {
-            return Ok(None);
-        }
-    };
-
-    let lines: Vec<&str> = text.lines().collect();
-
-    if lines.len() < 3 {
-        bail!("unable to parse {:?} - got less than 3 lines", path);
-    }
-
-    // https://github.com/openzfs/zfs/blob/master/lib/libspl/include/sys/kstat.h#L578
-    // nread    nwritten reads    writes   wtime    wlentime wupdate  rtime    rlentime rupdate  wcnt     rcnt
-    // Note: w -> wait (wtime -> wait time)
-    // Note: r -> run  (rtime -> run time)
-    // All times are nanoseconds
-    let stat: Vec<u64> = lines[2]
-        .split_ascii_whitespace()
-        .map(|s| s.parse().unwrap_or_default())
-        .collect();
-
-    let ticks = (stat[4] + stat[7]) / 1_000_000; // convert to milisec
-
-    let stat = BlockDevStat {
-        read_sectors: stat[0] >> 9,
-        write_sectors: stat[1] >> 9,
-        read_ios: stat[2],
-        write_ios: stat[3],
-        io_ticks: ticks,
-    };
-
-    Ok(Some(stat))
-}
-
-/// Get set of devices used by zfs (or a specific zfs pool)
-///
-/// The set is indexed by using the unix raw device number (dev_t is u64)
-pub fn zfs_devices(lsblk_info: &[LsblkInfo], pool: Option<String>) -> Result<HashSet<u64>, Error> {
-    let list = zpool_list(pool.as_ref(), true)?;
-
-    let mut device_set = HashSet::new();
-    for entry in list {
-        for device in entry.devices {
-            let meta = std::fs::metadata(device)?;
-            device_set.insert(meta.rdev());
-        }
-    }
-    if pool.is_none() {
-        for info in lsblk_info.iter() {
-            if let Some(partition_type) = &info.partition_type {
-                if ZFS_UUIDS.contains(partition_type.as_str()) {
-                    let meta = std::fs::metadata(&info.path)?;
-                    device_set.insert(meta.rdev());
-                }
-            }
-        }
-    }
-
-    Ok(device_set)
-}
-
-const ZFS_KSTAT_BASE_PATH: &str = "/proc/spl/kstat/zfs";
-const_regex! {
-    OBJSET_REGEX = r"^objset-0x[a-fA-F0-9]+$";
-}
-
-static ZFS_DATASET_OBJSET_MAP: LazyLock<Mutex<HashMap<String, (String, String)>>> =
-    LazyLock::new(|| Mutex::new(HashMap::new()));
-
-// parses /proc/spl/kstat/zfs/POOL/objset-ID files
-// they have the following format:
-//
-// 0 0 0x00 0 0000 00000000000 000000000000000000
-// name                            type data
-// dataset_name                    7    pool/dataset
-// writes                          4    0
-// nwritten                        4    0
-// reads                           4    0
-// nread                           4    0
-// nunlinks                        4    0
-// nunlinked                       4    0
-//
-// we are only interested in the dataset_name, writes, nwrites, reads and nread
-fn parse_objset_stat(pool: &str, objset_id: &str) -> Result<(String, BlockDevStat), Error> {
-    let path = PathBuf::from(format!("{ZFS_KSTAT_BASE_PATH}/{pool}/{objset_id}"));
-
-    let text = match proxmox_sys::fs::file_read_optional_string(path)? {
-        Some(text) => text,
-        None => bail!("could not parse '{}' stat file", objset_id),
-    };
-
-    let mut dataset_name = String::new();
-    let mut stat = BlockDevStat {
-        read_sectors: 0,
-        write_sectors: 0,
-        read_ios: 0,
-        write_ios: 0,
-        io_ticks: 0,
-    };
-
-    for (i, line) in text.lines().enumerate() {
-        if i < 2 {
-            continue;
-        }
-
-        let mut parts = line.split_ascii_whitespace();
-        let name = parts.next();
-        parts.next(); // discard type
-        let value = parts.next().ok_or_else(|| format_err!("no value found"))?;
-        match name {
-            Some("dataset_name") => dataset_name = value.to_string(),
-            Some("writes") => stat.write_ios = value.parse().unwrap_or_default(),
-            Some("nwritten") => stat.write_sectors = value.parse::<u64>().unwrap_or_default() / 512,
-            Some("reads") => stat.read_ios = value.parse().unwrap_or_default(),
-            Some("nread") => stat.read_sectors = value.parse::<u64>().unwrap_or_default() / 512,
-            _ => {}
-        }
-    }
-
-    Ok((dataset_name, stat))
-}
-
-fn get_mapping(dataset: &str) -> Option<(String, String)> {
-    ZFS_DATASET_OBJSET_MAP
-        .lock()
-        .unwrap()
-        .get(dataset)
-        .map(|c| c.to_owned())
-}
-
-/// Updates the dataset <-> objset_map
-pub(crate) fn update_zfs_objset_map(pool: &str) -> Result<(), Error> {
-    let mut map = ZFS_DATASET_OBJSET_MAP.lock().unwrap();
-    map.clear();
-    let path = PathBuf::from(format!("{ZFS_KSTAT_BASE_PATH}/{pool}"));
-
-    proxmox_sys::fs::scandir(
-        libc::AT_FDCWD,
-        &path,
-        &OBJSET_REGEX,
-        |_l2_fd, filename, _type| {
-            let (name, _) = parse_objset_stat(pool, filename)?;
-            map.insert(name, (pool.to_string(), filename.to_string()));
-            Ok(())
-        },
-    )?;
-
-    Ok(())
-}
-
-/// Gets io stats for the dataset from /proc/spl/kstat/zfs/POOL/objset-ID
-pub fn zfs_dataset_stats(dataset: &str) -> Result<BlockDevStat, Error> {
-    let mut mapping = get_mapping(dataset);
-    if mapping.is_none() {
-        let pool = get_pool_from_dataset(dataset);
-        update_zfs_objset_map(pool)?;
-        mapping = get_mapping(dataset);
-    }
-    let (pool, objset_id) =
-        mapping.ok_or_else(|| format_err!("could not find objset id for dataset"))?;
-
-    match parse_objset_stat(&pool, &objset_id) {
-        Ok((_, stat)) => Ok(stat),
-        Err(err) => {
-            // on error remove dataset from map, it probably vanished or the
-            // mapping was incorrect
-            ZFS_DATASET_OBJSET_MAP.lock().unwrap().remove(dataset);
-            Err(err)
-        }
-    }
-}
diff --git a/src/tools/mod.rs b/src/tools/mod.rs
index 7f5acc0e3..4a30c1f71 100644
--- a/src/tools/mod.rs
+++ b/src/tools/mod.rs
@@ -14,7 +14,6 @@ use pbs_datastore::backup_info::{BackupDir, BackupInfo};
 use pbs_datastore::manifest::BackupManifest;
 
 pub mod config;
-pub mod disks;
 pub mod fs;
 pub mod statistics;
 pub mod systemd;
-- 
2.47.3





  parent reply	other threads:[~2026-03-12 13:53 UTC|newest]

Thread overview: 31+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-03-12 13:52 [PATCH datacenter-manager/proxmox{,-backup,-yew-comp} 00/26] metric collection for the PDM host Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 01/26] sys: procfs: don't read from sysfs during unit tests Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 02/26] parallel-handler: import code from Proxmox Backup Server Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 03/26] parallel-handler: introduce custom error type Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 04/26] parallel-handler: add documentation Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 05/26] parallel-handler: add simple unit-test suite Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 06/26] disks: import from Proxmox Backup Server Lukas Wagner
2026-03-16 13:13   ` Arthur Bied-Charreton
2026-03-12 13:52 ` [PATCH proxmox 07/26] disks: fix typo in `initialize_gpt_disk` Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 08/26] disks: add parts of gather_disk_stats from PBS Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 09/26] disks: gate api macro behind 'api-types' feature Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 10/26] disks: clippy: collapse if-let chains where possible Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 11/26] procfs: add helpers for querying pressure stall information Lukas Wagner
2026-03-16 13:25   ` Arthur Bied-Charreton
2026-03-12 13:52 ` [PATCH proxmox 12/26] time: use u64 parse helper from nom Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-backup 13/26] tools: move ParallelHandler to new proxmox-parallel-handler crate Lukas Wagner
2026-03-12 13:52 ` Lukas Wagner [this message]
2026-03-16 13:27   ` [PATCH proxmox-backup 14/26] tools: replace disks module with proxmox-disks Arthur Bied-Charreton
2026-03-12 13:52 ` [PATCH proxmox-backup 15/26] metric collection: use blockdev_stat_for_path from proxmox_disks Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-yew-comp 16/26] node status panel: add `children` property Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-yew-comp 17/26] RRDGrid: fix size observer by attaching node reference to rendered container Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-yew-comp 18/26] RRDGrid: add padding and increase gap between elements Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 19/26] metric collection: clarify naming for remote metric collection Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 20/26] metric collection: fix minor typo in error message Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 21/26] metric collection: collect PDM host metrics in a new collection task Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 22/26] api: fix /nodes/localhost/rrddata endpoint Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 23/26] pdm: node rrd data: rename 'total-time' to 'metric-collection-total-time' Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 24/26] pdm-api-types: add PDM host metric fields Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 25/26] ui: node status: add RRD graphs for PDM host metrics Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 26/26] ui: lxc/qemu/node: use RRD value render helpers Lukas Wagner
2026-03-16 13:42 ` [PATCH datacenter-manager/proxmox{,-backup,-yew-comp} 00/26] metric collection for the PDM host Arthur Bied-Charreton

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260312135229.420729-15-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal