use std::fmt;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Capabilities {
pub schema_version: u32,
pub fsys_version: String,
pub kernel_version: String,
pub os_target: String,
pub probed_at_unix_secs: u64,
pub io_uring: bool,
pub io_uring_features: Vec<IoUringFeature>,
pub nvme_passthrough: bool,
pub direct_io: bool,
pub plp_detected: bool,
pub spdk_eligible: bool,
pub spdk_skip_reasons: Vec<SpdkSkipReason>,
pub spdk_eligible_devices: Vec<PciAddress>,
pub hardware: HardwareSummary,
}
impl Capabilities {
pub const SCHEMA_VERSION: u32 = crate::capability::CAPABILITY_CACHE_SCHEMA_VERSION;
#[must_use]
#[inline]
pub fn supports_spdk(&self) -> bool {
self.spdk_eligible
}
#[must_use]
#[inline]
pub fn first_spdk_skip_reason(&self) -> Option<&SpdkSkipReason> {
self.spdk_skip_reasons.first()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct HardwareSummary {
pub drive_type: String,
pub optimal_block_size: u32,
pub queue_depth: u32,
pub sector_size_logical: u32,
pub sector_size_physical: u32,
pub io_uring: bool,
pub nvme_passthrough: bool,
pub direct_io: bool,
pub plp_detected: bool,
}
impl HardwareSummary {
pub(crate) fn from_live_probe() -> Self {
let hw = crate::hardware::info();
Self {
drive_type: hw.drive.kind.as_str().to_string(),
optimal_block_size: hw.drive.optimal_block,
queue_depth: hw.drive.queue_depth,
sector_size_logical: hw.drive.logical_sector,
sector_size_physical: hw.drive.physical_sector,
io_uring: hw.io_primitives.io_uring,
nvme_passthrough: hw.io_primitives.nvme_passthrough,
direct_io: hw.io_primitives.direct_io,
plp_detected: matches!(hw.drive.plp, crate::hardware::PlpStatus::Yes),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PciAddress {
pub domain: u16,
pub bus: u8,
pub device: u8,
pub function: u8,
}
impl PciAddress {
#[must_use]
#[inline]
pub const fn new(domain: u16, bus: u8, device: u8, function: u8) -> Self {
Self {
domain,
bus,
device,
function,
}
}
#[must_use]
pub fn to_canonical(&self) -> String {
format!(
"{:04x}:{:02x}:{:02x}.{:x}",
self.domain, self.bus, self.device, self.function
)
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
let (domain, rest) = match s.split_once(':') {
None => return None,
Some((first, rest)) if rest.contains(':') => {
let domain = u16::from_str_radix(first, 16).ok()?;
(domain, rest)
}
Some(_) => (0u16, s),
};
let (bus_str, devfunc) = rest.split_once(':')?;
let bus = u8::from_str_radix(bus_str, 16).ok()?;
let (dev_str, func_str) = devfunc.split_once('.')?;
let device = u8::from_str_radix(dev_str, 16).ok()?;
let function = u8::from_str_radix(func_str, 16).ok()?;
Some(Self {
domain,
bus,
device,
function,
})
}
}
impl fmt::Display for PciAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_canonical())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IoUringFeature {
FastPoll,
RegisterBuffers,
RegisterFiles,
UringCmd,
SubmitAll,
CoopTaskrun,
SingleIssuer,
DeferTaskrun,
SqPoll,
}
impl IoUringFeature {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
IoUringFeature::FastPoll => "fast_poll",
IoUringFeature::RegisterBuffers => "register_buffers",
IoUringFeature::RegisterFiles => "register_files",
IoUringFeature::UringCmd => "uring_cmd",
IoUringFeature::SubmitAll => "submit_all",
IoUringFeature::CoopTaskrun => "coop_taskrun",
IoUringFeature::SingleIssuer => "single_issuer",
IoUringFeature::DeferTaskrun => "defer_taskrun",
IoUringFeature::SqPoll => "sqpoll",
}
}
#[must_use]
pub fn from_str_canonical(s: &str) -> Option<Self> {
Some(match s {
"fast_poll" => IoUringFeature::FastPoll,
"register_buffers" => IoUringFeature::RegisterBuffers,
"register_files" => IoUringFeature::RegisterFiles,
"uring_cmd" => IoUringFeature::UringCmd,
"submit_all" => IoUringFeature::SubmitAll,
"coop_taskrun" => IoUringFeature::CoopTaskrun,
"single_issuer" => IoUringFeature::SingleIssuer,
"defer_taskrun" => IoUringFeature::DeferTaskrun,
"sqpoll" => IoUringFeature::SqPoll,
_ => return None,
})
}
}
impl fmt::Display for IoUringFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SpdkEligibility {
pub eligible: bool,
pub reasons_failed: Vec<SpdkSkipReason>,
pub eligible_devices: Vec<PciAddress>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SpdkSkipReason {
NotLinux,
HugepagesNotConfigured {
current_mb: u64,
recommended_mb: u64,
},
InsufficientPrivileges,
NoNvmeDevices,
AllDevicesInUse {
devices: Vec<PciAddress>,
},
IommuNotConfigured,
InsufficientCores {
available: usize,
recommended: usize,
},
SpdkLibraryNotFound,
}
impl fmt::Display for SpdkSkipReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SpdkSkipReason::NotLinux => {
f.write_str("SPDK requires Linux; the current platform is unsupported")
}
SpdkSkipReason::HugepagesNotConfigured {
current_mb,
recommended_mb,
} => write!(
f,
"hugepages not configured ({current_mb} MB allocated, {recommended_mb} MB recommended)"
),
SpdkSkipReason::InsufficientPrivileges => {
f.write_str("insufficient privileges (uid 0 or CAP_SYS_ADMIN required)")
}
SpdkSkipReason::NoNvmeDevices => f.write_str("no NVMe devices detected on the PCI bus"),
SpdkSkipReason::AllDevicesInUse { devices } => write!(
f,
"all {} NVMe device(s) are currently bound to the kernel 'nvme' driver",
devices.len()
),
SpdkSkipReason::IommuNotConfigured => {
f.write_str("IOMMU not configured (/sys/kernel/iommu_groups missing or empty)")
}
SpdkSkipReason::InsufficientCores {
available,
recommended,
} => write!(
f,
"insufficient cores ({available} available, {recommended} recommended)"
),
SpdkSkipReason::SpdkLibraryNotFound => {
f.write_str("SPDK runtime library not found on the dynamic loader search path")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pci_address_canonical_format() {
let pci = PciAddress::new(0, 0x1f, 0x03, 2);
assert_eq!(pci.to_canonical(), "0000:1f:03.2");
assert_eq!(pci.to_string(), "0000:1f:03.2");
}
#[test]
fn test_pci_address_parse_four_segment() {
let pci = PciAddress::parse("0000:1f:03.2").expect("parse");
assert_eq!(pci, PciAddress::new(0, 0x1f, 0x03, 2));
}
#[test]
fn test_pci_address_parse_three_segment_implies_domain_zero() {
let pci = PciAddress::parse("1f:03.2").expect("parse");
assert_eq!(pci, PciAddress::new(0, 0x1f, 0x03, 2));
}
#[test]
fn test_pci_address_parse_rejects_garbage() {
assert!(PciAddress::parse("").is_none());
assert!(PciAddress::parse("nonsense").is_none());
assert!(PciAddress::parse("xx:yy:zz.w").is_none());
assert!(PciAddress::parse("0000:1f:03").is_none()); }
#[test]
fn test_pci_address_round_trip_full_form() {
let original = PciAddress::new(0xABCD, 0xEF, 0x12, 5);
let parsed = PciAddress::parse(&original.to_canonical()).expect("parse");
assert_eq!(parsed, original);
}
#[test]
fn test_io_uring_feature_as_str_round_trip() {
for f in [
IoUringFeature::FastPoll,
IoUringFeature::RegisterBuffers,
IoUringFeature::RegisterFiles,
IoUringFeature::UringCmd,
IoUringFeature::SubmitAll,
IoUringFeature::CoopTaskrun,
IoUringFeature::SingleIssuer,
IoUringFeature::DeferTaskrun,
IoUringFeature::SqPoll,
] {
let s = f.as_str();
assert_eq!(IoUringFeature::from_str_canonical(s), Some(f));
}
}
#[test]
fn test_io_uring_feature_from_str_rejects_unknown() {
assert!(IoUringFeature::from_str_canonical("not_a_feature").is_none());
}
#[test]
fn test_spdk_skip_reason_display_is_human_readable() {
assert!(SpdkSkipReason::NotLinux
.to_string()
.to_ascii_lowercase()
.contains("linux"));
assert!(SpdkSkipReason::InsufficientPrivileges
.to_string()
.to_ascii_lowercase()
.contains("privileges"));
assert!(SpdkSkipReason::NoNvmeDevices
.to_string()
.to_ascii_lowercase()
.contains("nvme"));
assert!(SpdkSkipReason::IommuNotConfigured
.to_string()
.to_ascii_lowercase()
.contains("iommu"));
assert!(SpdkSkipReason::SpdkLibraryNotFound
.to_string()
.to_ascii_lowercase()
.contains("spdk"));
}
#[test]
fn test_spdk_skip_reason_hugepages_includes_numbers() {
let r = SpdkSkipReason::HugepagesNotConfigured {
current_mb: 0,
recommended_mb: 1024,
};
let s = r.to_string();
assert!(s.contains('0'));
assert!(s.contains("1024"));
}
#[test]
fn test_spdk_skip_reason_all_devices_in_use_includes_count() {
let r = SpdkSkipReason::AllDevicesInUse {
devices: vec![PciAddress::new(0, 0, 0, 0)],
};
let s = r.to_string();
assert!(s.contains('1'));
}
#[test]
fn test_spdk_skip_reason_insufficient_cores_includes_numbers() {
let r = SpdkSkipReason::InsufficientCores {
available: 2,
recommended: 4,
};
let s = r.to_string();
assert!(s.contains('2'));
assert!(s.contains('4'));
}
}