use super::types::{IoUringFeature, PciAddress, SpdkEligibility, SpdkSkipReason};
pub(crate) const SPDK_HUGEPAGE_MIN_MB: u64 = 256;
pub(crate) const SPDK_HUGEPAGE_RECOMMENDED_MB: u64 = 1024;
pub(crate) const SPDK_MIN_CORES: usize = 4;
#[must_use]
pub fn spdk_eligibility() -> SpdkEligibility {
spdk_eligibility_with_root(SysfsRoot::live())
}
pub(crate) fn spdk_eligibility_with_root(root: SysfsRoot<'_>) -> SpdkEligibility {
if !cfg!(target_os = "linux") {
return SpdkEligibility {
eligible: false,
reasons_failed: vec![SpdkSkipReason::NotLinux],
eligible_devices: Vec::new(),
};
}
let mut reasons: Vec<SpdkSkipReason> = Vec::new();
let hugepages_mb = root.hugepages_total_mb();
if hugepages_mb < SPDK_HUGEPAGE_MIN_MB {
reasons.push(SpdkSkipReason::HugepagesNotConfigured {
current_mb: hugepages_mb,
recommended_mb: SPDK_HUGEPAGE_RECOMMENDED_MB,
});
}
if !root.has_sys_admin_capability() {
reasons.push(SpdkSkipReason::InsufficientPrivileges);
}
let devices = root.enumerate_nvme_devices();
if devices.is_empty() {
reasons.push(SpdkSkipReason::NoNvmeDevices);
}
let mut eligible_devices: Vec<PciAddress> = Vec::new();
let mut kernel_bound: Vec<PciAddress> = Vec::new();
for dev in &devices {
match root.nvme_driver_binding(dev) {
DriverBinding::Unbound | DriverBinding::Vfio | DriverBinding::Uio => {
eligible_devices.push(dev.clone());
}
DriverBinding::KernelNvme => kernel_bound.push(dev.clone()),
}
}
if !devices.is_empty() && eligible_devices.is_empty() {
reasons.push(SpdkSkipReason::AllDevicesInUse {
devices: kernel_bound,
});
}
if !root.iommu_groups_present() {
reasons.push(SpdkSkipReason::IommuNotConfigured);
}
let cores = root.available_cores();
if cores < SPDK_MIN_CORES {
reasons.push(SpdkSkipReason::InsufficientCores {
available: cores,
recommended: SPDK_MIN_CORES,
});
}
SpdkEligibility {
eligible: reasons.is_empty() && !eligible_devices.is_empty(),
reasons_failed: reasons,
eligible_devices,
}
}
#[must_use]
pub fn io_uring_features() -> Vec<IoUringFeature> {
#[cfg(target_os = "linux")]
{
let raw = crate::platform::iouring_features::features();
let mut v = Vec::new();
if raw.coop_taskrun {
v.push(IoUringFeature::CoopTaskrun);
}
if raw.single_issuer {
v.push(IoUringFeature::SingleIssuer);
}
if raw.defer_taskrun {
v.push(IoUringFeature::DeferTaskrun);
}
v
}
#[cfg(not(target_os = "linux"))]
{
Vec::new()
}
}
#[must_use]
pub fn kernel_version_string() -> String {
#[cfg(target_os = "linux")]
{
match std::fs::read_to_string("/proc/sys/kernel/osrelease") {
Ok(s) => s.trim().to_string(),
Err(_) => "unknown".to_string(),
}
}
#[cfg(not(target_os = "linux"))]
{
std::env::consts::OS.to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DriverBinding {
Unbound,
KernelNvme,
Vfio,
Uio,
}
pub(crate) struct SysfsRoot<'a> {
pub(crate) root: &'a str,
pub(crate) hugepages_override_mb: Option<u64>,
pub(crate) sys_admin_override: Option<bool>,
pub(crate) cores_override: Option<usize>,
}
impl<'a> SysfsRoot<'a> {
pub(crate) fn live() -> SysfsRoot<'static> {
SysfsRoot {
root: "",
hugepages_override_mb: None,
sys_admin_override: None,
cores_override: None,
}
}
fn hugepages_total_mb(&self) -> u64 {
if let Some(v) = self.hugepages_override_mb {
return v;
}
let path = format!("{}/proc/meminfo", self.root);
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return 0,
};
let mut total: Option<u64> = None;
let mut size_kb: Option<u64> = None;
for line in contents.lines() {
if let Some(v) = parse_meminfo_field(line, "HugePages_Total:") {
total = Some(v);
} else if let Some(v) = parse_meminfo_field(line, "Hugepagesize:") {
size_kb = Some(v);
}
}
match (total, size_kb) {
(Some(t), Some(kb)) => t.saturating_mul(kb) / 1024,
_ => 0,
}
}
fn has_sys_admin_capability(&self) -> bool {
if let Some(v) = self.sys_admin_override {
return v;
}
#[cfg(target_os = "linux")]
{
let uid = unsafe { libc::getuid() };
if uid == 0 {
return true;
}
let path = format!("{}/proc/self/status", self.root);
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return false,
};
for line in contents.lines() {
if let Some(rest) = line.strip_prefix("CapEff:") {
if let Ok(mask) = u64::from_str_radix(rest.trim(), 16) {
const CAP_SYS_ADMIN: u64 = 1 << 21;
return mask & CAP_SYS_ADMIN != 0;
}
}
}
false
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
fn enumerate_nvme_devices(&self) -> Vec<PciAddress> {
let path = format!("{}/sys/bus/pci/devices", self.root);
let entries = match std::fs::read_dir(&path) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let Some(pci) = PciAddress::parse(&name) else {
continue;
};
let class_path = format!("{}/sys/bus/pci/devices/{}/class", self.root, name);
let class = match std::fs::read_to_string(&class_path) {
Ok(c) => c.trim().to_string(),
Err(_) => continue,
};
if class.starts_with("0x010802") {
out.push(pci);
}
}
out.sort_by_key(|a| a.to_canonical());
out
}
fn nvme_driver_binding(&self, pci: &PciAddress) -> DriverBinding {
let driver_path = format!(
"{}/sys/bus/pci/devices/{}/driver",
self.root,
pci.to_canonical()
);
let target = match std::fs::read_link(&driver_path) {
Ok(t) => t,
Err(_) => return DriverBinding::Unbound,
};
let name = target
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
match name.as_str() {
"nvme" => DriverBinding::KernelNvme,
"vfio-pci" => DriverBinding::Vfio,
"uio_pci_generic" => DriverBinding::Uio,
_ => DriverBinding::Unbound,
}
}
fn iommu_groups_present(&self) -> bool {
let path = format!("{}/sys/kernel/iommu_groups", self.root);
match std::fs::read_dir(&path) {
Ok(mut entries) => entries.next().is_some(),
Err(_) => false,
}
}
fn available_cores(&self) -> usize {
if let Some(v) = self.cores_override {
return v;
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
}
fn parse_meminfo_field(line: &str, prefix: &str) -> Option<u64> {
let rest = line.strip_prefix(prefix)?.trim();
let num_part: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if num_part.is_empty() {
return None;
}
num_part.parse::<u64>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_root(p: &str) -> SysfsRoot<'_> {
SysfsRoot {
root: p,
hugepages_override_mb: None,
sys_admin_override: None,
cores_override: None,
}
}
#[test]
fn test_parse_meminfo_field_handles_units() {
assert_eq!(
parse_meminfo_field("Hugepagesize: 2048 kB", "Hugepagesize:"),
Some(2048)
);
assert_eq!(
parse_meminfo_field("HugePages_Total: 512", "HugePages_Total:"),
Some(512)
);
assert_eq!(
parse_meminfo_field("Other: 100", "HugePages_Total:"),
None
);
assert_eq!(
parse_meminfo_field("HugePages_Total:", "HugePages_Total:"),
None
);
}
#[test]
fn test_kernel_version_string_is_non_empty() {
let v = kernel_version_string();
assert!(!v.is_empty());
}
#[test]
fn test_io_uring_features_does_not_panic() {
let _features = io_uring_features();
}
#[test]
#[cfg(not(target_os = "linux"))]
fn test_spdk_eligibility_off_linux_returns_not_linux() {
let e = spdk_eligibility();
assert!(!e.eligible);
assert!(e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::NotLinux)));
assert!(e.eligible_devices.is_empty());
}
#[test]
#[cfg(target_os = "linux")]
fn test_spdk_eligibility_synthetic_empty_root_reports_no_nvme() {
let tmp = std::env::temp_dir().join(format!(
"fsys-cap-empty-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&tmp).expect("mktmpdir");
std::fs::create_dir_all(tmp.join("sys/bus/pci/devices")).expect("mkdevs");
std::fs::create_dir_all(tmp.join("proc")).expect("mkproc");
std::fs::write(
tmp.join("proc/meminfo"),
"HugePages_Total: 0\nHugepagesize: 2048 kB\n",
)
.expect("meminfo");
let root_str = tmp.to_string_lossy().to_string();
let root = SysfsRoot {
root: &root_str,
hugepages_override_mb: None,
sys_admin_override: Some(true),
cores_override: Some(8),
};
let e = spdk_eligibility_with_root(root);
assert!(!e.eligible);
assert!(e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::NoNvmeDevices)));
assert!(e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::HugepagesNotConfigured { .. })));
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn test_spdk_eligibility_hugepages_threshold_boundary() {
if !cfg!(target_os = "linux") {
return;
}
let root = SysfsRoot {
root: "/tmp/this-must-not-exist-fsys",
hugepages_override_mb: Some(SPDK_HUGEPAGE_MIN_MB),
sys_admin_override: Some(true),
cores_override: Some(8),
};
let e = spdk_eligibility_with_root(root);
assert!(!e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::HugepagesNotConfigured { .. })));
let root_below = SysfsRoot {
root: "/tmp/this-must-not-exist-fsys",
hugepages_override_mb: Some(SPDK_HUGEPAGE_MIN_MB - 1),
sys_admin_override: Some(true),
cores_override: Some(8),
};
let e = spdk_eligibility_with_root(root_below);
assert!(e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::HugepagesNotConfigured { .. })));
}
#[test]
fn test_spdk_eligibility_core_threshold_boundary() {
if !cfg!(target_os = "linux") {
return;
}
let mut root = synthetic_root("/tmp/this-must-not-exist-fsys");
root.hugepages_override_mb = Some(SPDK_HUGEPAGE_RECOMMENDED_MB);
root.sys_admin_override = Some(true);
root.cores_override = Some(SPDK_MIN_CORES);
let e = spdk_eligibility_with_root(root);
assert!(!e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::InsufficientCores { .. })));
let mut root_below = synthetic_root("/tmp/this-must-not-exist-fsys");
root_below.hugepages_override_mb = Some(SPDK_HUGEPAGE_RECOMMENDED_MB);
root_below.sys_admin_override = Some(true);
root_below.cores_override = Some(SPDK_MIN_CORES - 1);
let e = spdk_eligibility_with_root(root_below);
assert!(e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::InsufficientCores { .. })));
}
#[test]
fn test_spdk_eligibility_insufficient_privileges_recorded() {
if !cfg!(target_os = "linux") {
return;
}
let mut root = synthetic_root("/tmp/this-must-not-exist-fsys");
root.hugepages_override_mb = Some(SPDK_HUGEPAGE_RECOMMENDED_MB);
root.sys_admin_override = Some(false);
root.cores_override = Some(SPDK_MIN_CORES);
let e = spdk_eligibility_with_root(root);
assert!(e
.reasons_failed
.iter()
.any(|r| matches!(r, SpdkSkipReason::InsufficientPrivileges)));
}
}