[PATCH proxmox-backup v2 13/25] tools: replace disks module with proxmox-disks

Lukas Wagner
2026-03-19T09:46:54Z

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>
Reviewed-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
Tested-by: Arthur Bied-Charreton <a.bied-charreton@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 246e7f81f..07a8ea94f 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 = "3"
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 cca340553..26a87afed 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -1878,7 +1878,7 @@ pub fn get_rrd_stats(
_param: Value,
) -> Result<Value, Error> {
let datastore = DataStore::lookup_datastore(&store, Operation::Read)?;
- let disk_manager = crate::tools::disks::DiskManage::new();
+ let disk_manager = proxmox_disks::DiskManage::new();

let mut rrd_fields = vec![
"total",
@@ -2344,7 +2344,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
@@ -2381,13 +2381,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}'");
}
@@ -2618,7 +2618,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 7979b7632..af56eeb19 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