Viewing: mount.rs
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 DDN. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
use crate::{Error, ObdUuid};
use cstrbuf::CStrBuf;
use lustreapi_sys::{
LL_STATFS_LMV, LL_STATFS_LOV, LOV_ALL_STRIPES, llapi_obd_fstatfs, llapi_search_mounts,
obd_statfs, obd_uuid,
};
use nix::errno::Errno;
use std::{
ffi::CString,
fmt,
fs::File,
os::fd::AsRawFd,
path::{Path, PathBuf},
};
/// Lustre filesystem mount discovery and statistics collection.
///
/// This module provides functionality for discovering and collecting detailed statistics
/// from Lustre filesystem mounts, including individual component information for
/// MDTs and OSTs.
///
/// The primary entry point is [`MountStats`], which provides methods to discover
/// all Lustre mounts on the system and collect statistics about
/// their components and overall filesystem status.
///
/// # Discovery Process
///
/// The discovery process works in two phases:
/// 1. **Mount Discovery**: Uses `llapi_search_mounts` to find all Lustre mount points
/// 2. **Component Collection**: For each mount, queries individual MDTs and OSTs
///
/// # Component Statistics
///
/// For each component, the following information is collected:
/// - Block statistics (total, free, available space)
/// - Inode statistics (total, free inodes)
/// - Component UUID and identification
/// - Component status (active/inactive)
///
/// # Usage Patterns
///
/// The typical usage involves calling [`discover_mounts`](#method.discover_mounts) to get
/// all Lustre mounts, or [`collect_lustre_mounts`](#method.collect_lustre_mounts) for
/// a specific filesystem.
///
/// # Error Handling
///
/// Component queries may fail for various reasons (inactive components, temporary
/// failures, etc.). The implementation handles these scenarios:
/// - Inactive components are included with error status
/// - Temporary failures trigger retries
/// - Unexpected errors are logged but don't stop collection
///
/// # Examples
///
/// ```no_run
/// # use rustreapi::MountStats;
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
/// // Discover all Lustre mounts on the system
/// let mounts = MountStats::discover_mounts()?;
///
/// for mount in &mounts {
/// println!("Mount: {}", mount.info.mount_point.display());
/// let stats = &mount.stats;
/// println!(" Available space: {} bytes", stats.bavail * stats.bsize);
/// if let Some(inodes) = &stats.inodes {
/// println!(" Available inodes: {}", inodes.favail);
/// }
/// }
///
/// // Collect mounts for a specific filesystem
/// let specific_mounts = MountStats::collect_lustre_mounts("/mnt/lustre", "testfs")?;
/// println!("Found {} components for testfs", specific_mounts.len());
///
/// Ok(())
/// # }
/// ```
///
/// # Component Types
///
/// The returned [`Mount`] entries include different types:
/// - Individual MDT entries (one per metadata target)
/// - Individual OST entries (one per object storage target)
/// - Filesystem summary entry (aggregated statistics)
///
/// Each component type can be distinguished by examining the `fs_label` field
/// and `dev.major` field in the mount information.
#[derive(Debug)]
pub enum LustreQueryResult {
Success(obd_statfs, obd_uuid),
/// `ENODATA` - component exists but is inactive
Inactive(obd_statfs, obd_uuid),
/// `EAGAIN` - Temporary failure, should retry with next index
RetryNext,
/// `ENODEV` - No more components available
NoMoreComponents,
}
#[derive(Debug, Clone, Copy)]
enum ComponentStatus {
Active,
Inactive,
}
#[repr(u32)]
#[derive(Debug, Clone, Copy)]
pub enum Statfs {
NONE = 0,
LMV = LL_STATFS_LMV,
LOV = LL_STATFS_LOV,
}
impl fmt::Display for Statfs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Statfs::NONE => "NONE",
Statfs::LMV => "MDT",
Statfs::LOV => "OST",
};
write!(f, "{s}")
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct MountStats;
#[derive(Debug, PartialEq, Eq)]
pub struct DeviceId {
pub major: u32,
pub minor: u32,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Inodes {
pub files: u64,
pub ffree: u64,
pub favail: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Mount {
pub info: MountInfo,
pub fs_label: Option<String>,
pub stats: Stats,
pub uuid: Option<String>,
pub part_uuid: Option<String>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct MountInfo {
pub id: u32,
pub parent: u32,
pub dev: DeviceId,
pub root: PathBuf,
pub mount_point: PathBuf,
pub fs: String,
pub fs_type: String,
pub bound: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Stats {
pub bsize: u64,
pub blocks: u64,
pub bfree: u64,
pub bavail: u64,
pub inodes: Option<Inodes>,
}
impl MountStats {
pub fn discover_mounts() -> Result<Vec<Mount>, Error> {
let mut lustre_mounts = Vec::new();
let mut index = 0;
let path = PathBuf::new();
while let Some((mntdir_str, fsname_str)) = MountStats::search_mounts(&path, index)? {
// Found a mount
if !mntdir_str.is_empty() {
let mut fs_mounts = MountStats::collect_lustre_mounts(&mntdir_str, &fsname_str)?;
lustre_mounts.append(&mut fs_mounts);
}
index += 1;
}
Ok(lustre_mounts)
}
pub fn collect_lustre_mounts(mntdir: &str, fsname: &str) -> Result<Vec<Mount>, Error> {
let mut mounts = Vec::new();
let file = File::open(mntdir)?;
// Create individual MDT entries
Self::collect_component_mounts(&file, Statfs::LMV, mntdir, fsname, &mut mounts)?;
// Create individual OST entries
Self::collect_component_mounts(&file, Statfs::LOV, mntdir, fsname, &mut mounts)?;
// Create aggregated client mount entry (filesystem summary)
let client_mount = Self::create_client_mount(mntdir, fsname)?;
mounts.push(client_mount);
Ok(mounts)
}
pub fn search_mounts(path: &Path, index: i32) -> Result<Option<(String, String)>, Error> {
let path_str = path.to_str().ok_or_else(|| Error::InvalidPath {
path: path.to_path_buf().into_boxed_path(),
})?;
let c_path = CString::new(path_str)?;
let mut mntdir = CStrBuf::new(4096);
let mut fsname = CStrBuf::new(256);
let result = unsafe {
llapi_search_mounts(
c_path.as_ptr(),
index,
mntdir.as_mut_ptr(),
fsname.as_mut_ptr(),
)
};
match result {
0 => {
// Found a mount - convert buffers to strings
let mntdir_str = mntdir.to_string();
let fsname_str = fsname.to_string();
Ok(Some((mntdir_str?, fsname_str?)))
}
-19 => Ok(None), // ENODEV - no more mounts
other => Err(Error::MountSearchError {
index,
errno: Errno::from_raw(-other),
}),
}
}
pub fn query_lustre_component<F: AsRawFd>(
fd: &F,
stat_type: Statfs,
index: u32,
) -> Result<LustreQueryResult, Error> {
let mut stat_buf = obd_statfs::default();
let mut uuid_buf = obd_uuid::default();
let rc = unsafe {
llapi_obd_fstatfs(
fd.as_raw_fd(),
stat_type as u32,
index,
&mut stat_buf,
&mut uuid_buf,
)
};
match rc {
0 => Ok(LustreQueryResult::Success(stat_buf, uuid_buf)),
-61 => Ok(LustreQueryResult::Inactive(stat_buf, uuid_buf)),
-11 => Ok(LustreQueryResult::RetryNext),
-19 => Ok(LustreQueryResult::NoMoreComponents),
other => Err(Error::LustreStatfs {
stat: stat_type,
index,
source: Errno::from_raw(-other),
}),
}
}
fn collect_component_mounts<F: AsRawFd>(
fd: &F,
stat_type: Statfs,
mntdir: &str,
fsname: &str,
mounts: &mut Vec<Mount>,
) -> Result<(), Error> {
let mut index = 0;
while index < LOV_ALL_STRIPES {
match Self::query_lustre_component(fd, stat_type, index)? {
LustreQueryResult::NoMoreComponents => {
break;
}
LustreQueryResult::RetryNext => {
index += 1;
continue;
}
LustreQueryResult::Success(stat_buf, uuid_buf) => {
let obd_uuid = ObdUuid::new(uuid_buf);
let mount = Self::create_component_mount(
mntdir,
fsname,
&stat_buf,
&obd_uuid,
stat_type,
index,
ComponentStatus::Active,
)?;
mounts.push(mount);
}
LustreQueryResult::Inactive(stat_buf, uuid_buf) => {
let obd_uuid = ObdUuid::new(uuid_buf);
let mount = Self::create_component_mount(
mntdir,
fsname,
&stat_buf,
&obd_uuid,
stat_type,
index,
ComponentStatus::Inactive,
)?;
mounts.push(mount);
}
}
index += 1;
}
Ok(())
}
fn create_component_mount(
mntdir: &str,
fsname: &str,
stat_buf: &obd_statfs,
uuid: &ObdUuid,
stat_type: Statfs,
index: u32,
status: ComponentStatus,
) -> crate::Result<Mount> {
let uuid_str = uuid.as_string();
let component_name = if uuid_str.is_empty() {
format!("{stat_type}:{index:04x}")
} else {
uuid_str.clone()
};
// Create a unique mount point path for this component
let component_mount_point = PathBuf::from(format!("{mntdir}[{stat_type}:{index}]"));
let mount_info = MountInfo {
id: 0,
parent: 0,
dev: DeviceId {
major: match stat_type {
Statfs::LMV => 1, // MDT
Statfs::LOV => 2, // OST
Statfs::NONE => 0,
},
minor: index,
},
fs: component_name,
fs_type: "lustre".to_string(),
mount_point: component_mount_point,
bound: false,
root: Default::default(),
};
let stats = match status {
ComponentStatus::Active => Stats {
bsize: u64::from(stat_buf.os_bsize),
blocks: stat_buf.os_blocks,
bfree: stat_buf.os_bfree,
bavail: stat_buf.os_bavail,
inodes: if matches!(stat_type, Statfs::LMV)
|| (matches!(stat_type, Statfs::LOV) && stat_buf.os_files > 0)
{
Some(Inodes {
files: stat_buf.os_files,
ffree: stat_buf.os_ffree,
favail: stat_buf.os_ffree,
})
} else {
None
},
},
ComponentStatus::Inactive => {
return Err(Error::ComponentInactive {
component_type: { stat_type },
index,
});
}
};
let mount = Mount {
info: mount_info,
stats,
fs_label: Some(format!("{fsname}-{stat_type}")),
uuid: if uuid_str.is_empty() {
None
} else {
Some(uuid_str)
},
part_uuid: None,
};
Ok(mount)
}
fn create_client_mount(mntdir: &str, fsname: &str) -> Result<Mount, Error> {
let file = File::open(mntdir)?;
let mut total_blocks = 0u64;
let mut total_bfree = 0u64;
let mut total_bavail = 0u64;
let mut total_files = 0u64;
let mut total_ffree = 0u64;
let mut bsize = 4096u32;
let mut ost_count = 0;
// Get OST stats for space
let ost_uuids = Self::component_stats(&file, Statfs::LOV, |stat_buf| {
total_blocks += stat_buf.os_blocks;
total_bfree += stat_buf.os_bfree;
total_bavail += stat_buf.os_bavail;
if bsize == 4096 {
bsize = stat_buf.os_bsize;
}
ost_count += 1;
})?;
// Get MDT stats for inode information
let mdt_uuids = Self::component_stats(&file, Statfs::LMV, |stat_buf| {
total_files += stat_buf.os_files;
total_ffree += stat_buf.os_ffree;
})?;
// Early return with specific error if no valid components found
if total_blocks == 0 || ost_count == 0 {
return Err(Error::InsufficientComponents {
filesystem: fsname.to_string(),
ost_count,
total_blocks,
});
}
// Create a combined UUID string from the first OST or MDT UUID if available
let filesystem_uuid = ost_uuids
.first()
.or_else(|| mdt_uuids.first())
.map(|uuid| uuid.as_string())
.filter(|s| !s.is_empty());
let mount_info = MountInfo {
id: 0,
parent: 0,
dev: DeviceId { major: 0, minor: 0 },
fs: "filesystem_summary".to_string(),
fs_type: "lustre".to_string(),
mount_point: PathBuf::from(mntdir),
bound: false,
root: Default::default(),
};
let stats = Stats {
bsize: u64::from(bsize),
blocks: total_blocks,
bfree: total_bfree,
bavail: total_bavail,
inodes: if total_files > 0 {
Some(Inodes {
files: total_files,
ffree: total_ffree,
favail: total_ffree,
})
} else {
None
},
};
let mount = Mount {
info: mount_info,
stats,
fs_label: Some(format!("Lustre-{fsname}")),
uuid: filesystem_uuid,
part_uuid: None,
};
Ok(mount)
}
fn component_stats<F, H>(
fd: &F,
stat_type: Statfs,
mut stats_handler: H,
) -> Result<Vec<ObdUuid>, Error>
where
F: AsRawFd,
H: FnMut(&obd_statfs),
{
let mut uuids = Vec::new();
let mut index = 0;
while index < LOV_ALL_STRIPES {
match Self::query_lustre_component(fd, stat_type, index)? {
LustreQueryResult::NoMoreComponents => {
break;
}
LustreQueryResult::RetryNext => {
index += 1;
continue;
}
LustreQueryResult::Success(stat_buf, uuid_buf) => {
stats_handler(&stat_buf);
let obd_uuid = ObdUuid::new(uuid_buf);
uuids.push(obd_uuid);
}
LustreQueryResult::Inactive(_, uuid_buf) => {
// Inactive component - still collect UUID but don't aggregate stats
let obd_uuid = ObdUuid::new(uuid_buf);
uuids.push(obd_uuid);
}
}
index += 1;
}
Ok(uuids)
}
}